From 0f44efca6b3bc2f7b5ebb6227caf848268c56c5f Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:38:31 +0530 Subject: [PATCH 01/10] feat(cli): let devspace init run without prompts Setup could only be answered interactively, so an agent or script could not onboard itself, and an unconfigured `serve` in a non-interactive shell had no way forward. `init` now takes --use, --providers, --roots, --public-url, and --json; any of them, --yes, or a missing terminal skips the prompts, and unanswered questions take the same defaults the prompts preselect. Prompts and flags share one step that writes config and auth. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 396 +++++++++++++++++++++++++---------------- src/onboarding.test.ts | 66 +++++++ src/onboarding.ts | 105 +++++++++++ 3 files changed, 413 insertions(+), 154 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 752555375..ca5a773a5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -49,8 +49,13 @@ import { presentAgentTargetCatalog, } from "./local-agent-presentation.js"; import { + type InitAnswers, + type InitFlags, type OnboardingDestination, SUBAGENT_SKILL_INSTALL_COMMAND, + isNonInteractiveInit, + parseInitArgs, + resolveInitAnswers, resolveOnboardingUsage, updateOnboardingSubagentsConfig, usesChatGpt, @@ -102,7 +107,7 @@ async function main(argv: string[]): Promise { return; case "init": - await runInit({ force: args.includes("--force") }); + await runInit(parseInitArgs(args)); return; case "doctor": @@ -182,196 +187,277 @@ async function ensureConfigured(): Promise { "Run:", " devspace init", "", + "Agents and scripts can answer setup with flags, for example:", + " devspace init --use chatgpt --public-url https://your-tunnel-host.example.com", + "", "Or provide DEVSPACE_OAUTH_OWNER_TOKEN.", ].join("\n"), ); } - await runInit({ force: false }); + await runInit({ force: false, yes: false, json: false }); } -async function runInit({ force }: { force: boolean }): Promise { +async function runInit(flags: InitFlags): Promise { const files = loadDevspaceFiles(); + const nonInteractive = isNonInteractiveInit(flags, Boolean(input.isTTY && output.isTTY)); - if (!force && files.configExists && files.authExists) { - prompts.log.info(`DevSpace is already configured at ${files.dir}`); - prompts.log.info("Run `devspace init --force` to update it."); + if (!flags.force && files.configExists && files.authExists) { + if (flags.json) { + console.log(JSON.stringify({ status: "already_configured", config_path: files.configPath }, null, 2)); + } else { + console.log(`DevSpace is already configured at ${files.dir}`); + console.log("Run `devspace init --force` to update it."); + } return; } - try { - prompts.intro("DevSpace setup"); + const currentSubagents = resolveSubagentsConfig(files.config.subagents ?? disabledSubagentsConfig()); + const availability = getLocalAgentProviderAvailabilitySnapshot(process.env, currentSubagents); + const configuredProviders = currentSubagents.providers + .filter((provider) => provider.enabled) + .map((provider) => provider.id); + const defaults = { + destinations: [files.config.server.publicBaseUrl ? "chatgpt" : "coding-agents"] as OnboardingDestination[], + providers: configuredProviders.length > 0 + ? configuredProviders + : availability.filter((provider) => provider.available).map((provider) => provider.name), + knownProviders: availability.map((provider) => provider.name), + roots: files.config.workspaces.allowedRoots.length > 0 ? files.config.workspaces.allowedRoots : [process.cwd()], + publicUrl: files.config.server.publicBaseUrl ?? null, + }; - const destinationAnswer = await prompts.multiselect({ - message: "Where do you want to use DevSpace?", - options: [ - { - value: "chatgpt", - label: "ChatGPT", - hint: "Connect ChatGPT to projects on this computer.", - }, - { - value: "coding-agents", - label: "Coding Agents", - hint: "Use DevSpace from Codex, Claude Code, OpenCode, Pi, and similar tools.", - }, - ], - initialValues: files.config.server.publicBaseUrl ? ["chatgpt"] : ["coding-agents"], - required: true, - }); + if (nonInteractive) { + const answers = resolveInitAnswers(flags, defaults); + const publicUrlError = answers.publicUrl ? validateRequiredPublicBaseUrl(answers.publicUrl) : undefined; - if (prompts.isCancel(destinationAnswer)) throw new SetupCancelledError(); - // SAFETY: The surrounding checks or construction already match this type. - const usage = resolveOnboardingUsage(destinationAnswer as OnboardingDestination[]); - const useChatGpt = usesChatGpt(usage); - const useCodingAgents = usesCodingAgents(usage); + if (publicUrlError) throw new Error(`--public-url: ${publicUrlError}`); + const result = applyInitAnswers(files, currentSubagents, answers); + const unavailable = availability + .filter((provider) => result.providers.includes(provider.name) && !provider.available) + .map((provider) => `${provider.name} (${provider.reason ?? "provider preflight failed"})`); - let allowedRoots: string[] | undefined; + printInitResult(result, unavailable, flags.json); - if (useChatGpt) { - const defaultRoots = files.config.workspaces.allowedRoots.join(", ") || process.cwd(); + return; + } - const rootsAnswer = await textPrompt({ - message: `Which project folders can DevSpace access? Press Enter to use ${defaultRoots}`, - placeholder: defaultRoots, - defaultValue: defaultRoots, - validate: (value) => value?.trim() ? undefined : "Enter at least one project root.", - }); + try { + prompts.intro("DevSpace setup"); + const result = applyInitAnswers(files, currentSubagents, await promptInitAnswers(defaults, availability, files.config.server.port)); - allowedRoots = rootsAnswer - .split(",") - .map((root) => resolve(expandHomePath(root.trim()))) - .filter(Boolean); - } + printInteractiveInitResult(result); + } catch (error) { + if (error instanceof SetupCancelledError) { + prompts.cancel("Setup cancelled"); - const port = files.config.server.port; - - let publicBaseUrl: string | null = null; - - if (useChatGpt) { - prompts.note( - [ - `Point your HTTPS tunnel or reverse proxy to http://127.0.0.1:${port}.`, - "Paste its public URL below.", - "", - "Example: https://your-tunnel-host.example.com", - ].join("\n"), - "Connect ChatGPT", - ); - publicBaseUrl = normalizePublicBaseUrl(await textPrompt({ - message: files.config.server.publicBaseUrl - ? `What public URL will ChatGPT connect to? Press Enter to keep ${files.config.server.publicBaseUrl}` - : "What public URL will ChatGPT connect to?", - placeholder: files.config.server.publicBaseUrl ?? "https://your-tunnel-host.example.com", - defaultValue: files.config.server.publicBaseUrl ?? "", - validate: validateRequiredPublicBaseUrl, - })); + return; } - const currentSubagents = resolveSubagentsConfig(files.config.subagents ?? disabledSubagentsConfig()); + throw error; + } +} - const availability = getLocalAgentProviderAvailabilitySnapshot( - process.env, - currentSubagents, - ); +async function promptInitAnswers( + defaults: { destinations: OnboardingDestination[]; providers: string[]; roots: string[]; publicUrl: string | null }, + availability: ReturnType, + port: number, +): Promise { + const destinationAnswer = await prompts.multiselect({ + message: "Where do you want to use DevSpace?", + options: [ + { + value: "chatgpt", + label: "ChatGPT", + hint: "Connect ChatGPT to projects on this computer.", + }, + { + value: "coding-agents", + label: "Coding Agents", + hint: "Use DevSpace from Codex, Claude Code, OpenCode, Pi, and similar tools.", + }, + ], + initialValues: defaults.destinations, + required: true, + }); - const configuredProviders = currentSubagents.providers - .filter((provider) => provider.enabled) - .map((provider) => provider.id); + if (prompts.isCancel(destinationAnswer)) throw new SetupCancelledError(); + // SAFETY: The surrounding checks or construction already match this type. + const usage = resolveOnboardingUsage(destinationAnswer as OnboardingDestination[]); + let roots: string[] | undefined; + let publicUrl: string | undefined; - const initialValues = configuredProviders.length > 0 - ? configuredProviders - : availability - .filter((provider) => provider.available) - .map((provider) => provider.name); + if (usesChatGpt(usage)) { + const defaultRoots = defaults.roots.join(", "); - prompts.log.info( - "DevSpace can delegate work to these agents from ChatGPT or another coding agent.", - ); + roots = (await textPrompt({ + message: `Which project folders can DevSpace access? Press Enter to use ${defaultRoots}`, + placeholder: defaultRoots, + defaultValue: defaultRoots, + validate: (value) => value?.trim() ? undefined : "Enter at least one project root.", + })).split(","); - const providerAnswer = await prompts.multiselect({ - message: "Which agents can DevSpace use as subagents?", - options: availability.map((provider) => ({ - value: provider.name, - label: provider.name, - hint: provider.available - ? provider.note ?? "available" - : `unavailable: ${provider.reason ?? "provider preflight failed"}`, - })), - initialValues, - required: true, + prompts.note( + [ + `Point your HTTPS tunnel or reverse proxy to http://127.0.0.1:${port}.`, + "Paste its public URL below.", + "", + "Example: https://your-tunnel-host.example.com", + ].join("\n"), + "Connect ChatGPT", + ); + publicUrl = await textPrompt({ + message: defaults.publicUrl + ? `What public URL will ChatGPT connect to? Press Enter to keep ${defaults.publicUrl}` + : "What public URL will ChatGPT connect to?", + placeholder: defaults.publicUrl ?? "https://your-tunnel-host.example.com", + defaultValue: defaults.publicUrl ?? "", + validate: validateRequiredPublicBaseUrl, }); + } - if (prompts.isCancel(providerAnswer)) throw new SetupCancelledError(); - // SAFETY: The surrounding checks or construction already match this type. - const selectedProviders = providerAnswer as string[]; + prompts.log.info( + "DevSpace can delegate work to these agents from ChatGPT or another coding agent.", + ); - const subagents = updateOnboardingSubagentsConfig( - currentSubagents, - selectedProviders, - ); + const providerAnswer = await prompts.multiselect({ + message: "Which agents can DevSpace use as subagents?", + options: availability.map((provider) => ({ + value: provider.name, + label: provider.name, + hint: provider.available + ? provider.note ?? "available" + : `unavailable: ${provider.reason ?? "provider preflight failed"}`, + })), + initialValues: defaults.providers, + required: true, + }); - const auth = { - ownerToken: files.auth.ownerToken ?? generateOwnerToken(), - }; - - setDevspaceConfigValues([ - { path: ["server", "port"], value: port }, - ...(useChatGpt - ? [{ path: ["server", "publicBaseUrl"], value: publicBaseUrl }] - : []), - ...(allowedRoots - ? [{ path: ["workspaces", "allowedRoots"], value: allowedRoots }] - : []), - { path: ["subagents"], value: subagents }, - ]); - writeDevspaceAuth(auth); - - const lines = [ - ...(allowedRoots ? [`Project folders: ${allowedRoots.join(", ")}`] : []), - `Subagents: ${selectedProviders.join(", ")}`, - ...(publicBaseUrl ? [`ChatGPT connection URL: ${publicBaseUrl}/mcp`] : []), - ]; - - prompts.note(lines.join("\n"), "DevSpace is ready"); - - if (useChatGpt) { - prompts.note( - [ - `Owner password: ${auth.ownerToken}`, - "Use this when ChatGPT asks you to approve DevSpace access.", - ].join("\n"), - "Owner password", - ); - } + if (prompts.isCancel(providerAnswer)) throw new SetupCancelledError(); - if (useCodingAgents) { - prompts.note( - [ - SUBAGENT_SKILL_INSTALL_COMMAND, - "", - "The Skills CLI will let you choose which Coding Agents receive it.", - ].join("\n"), - "Install the Subagents skill", - ); - } + // SAFETY: The surrounding checks or construction already match this type. + return { usage, providers: providerAnswer as string[], roots, publicUrl }; +} - const nextSteps = [ - useChatGpt ? "Run `devspace serve`, then connect ChatGPT." : undefined, - useCodingAgents ? "Run the skill command above before delegating from your Coding Agents." : undefined, - ].filter(Boolean).join(" "); +interface InitResult { + configPath: string; + usage: InitAnswers["usage"]; + providers: string[]; + allowedRoots?: string[]; + publicBaseUrl?: string; + ownerToken: string; +} - prompts.outro(nextSteps); - } catch (error) { - if (error instanceof SetupCancelledError) { - prompts.cancel("Setup cancelled"); +/** Persist setup answers, from prompts or flags, and return what was written. */ +function applyInitAnswers( + files: ReturnType, + currentSubagents: ReturnType, + answers: InitAnswers, +): InitResult { + const allowedRoots = answers.roots + ?.map((root) => root.trim()) + .filter(Boolean) + .map((root) => resolve(expandHomePath(root))); + const publicBaseUrl = answers.publicUrl ? normalizePublicBaseUrl(answers.publicUrl) : undefined; + const auth = { ownerToken: files.auth.ownerToken ?? generateOwnerToken() }; + + setDevspaceConfigValues([ + { path: ["server", "port"], value: files.config.server.port }, + ...(publicBaseUrl ? [{ path: ["server", "publicBaseUrl"], value: publicBaseUrl }] : []), + ...(allowedRoots ? [{ path: ["workspaces", "allowedRoots"], value: allowedRoots }] : []), + { path: ["subagents"], value: updateOnboardingSubagentsConfig(currentSubagents, answers.providers) }, + ]); + writeDevspaceAuth(auth); + + return { + configPath: files.configPath, + usage: answers.usage, + providers: answers.providers, + allowedRoots, + publicBaseUrl, + ownerToken: auth.ownerToken, + }; +} - return; - } +function initNextSteps(result: InitResult): string[] { + return [ + ...(usesChatGpt(result.usage) ? ["Run `devspace serve`, then connect ChatGPT."] : []), + ...(usesCodingAgents(result.usage) ? [`Install the Subagents skill: ${SUBAGENT_SKILL_INSTALL_COMMAND}`] : []), + ]; +} - throw error; +function printInitResult(result: InitResult, unavailable: string[], json: boolean): void { + const chatGpt = usesChatGpt(result.usage); + + if (json) { + console.log(JSON.stringify({ + status: "configured", + config_path: result.configPath, + usage: result.usage, + providers: result.providers, + unavailable_providers: unavailable, + ...(chatGpt ? { + allowed_roots: result.allowedRoots, + mcp_url: `${result.publicBaseUrl}/mcp`, + owner_password: result.ownerToken, + } : {}), + next_steps: initNextSteps(result), + }, null, 2)); + + return; + } + + console.log([ + `DevSpace is configured at ${result.configPath}`, + `Subagents: ${result.providers.join(", ")}`, + ...unavailable.map((provider) => `Warning: ${provider} is not available yet.`), + ...(chatGpt ? [ + `Project folders: ${result.allowedRoots!.join(", ")}`, + `ChatGPT connection URL: ${result.publicBaseUrl}/mcp`, + `Owner password: ${result.ownerToken}`, + "The user enters the owner password when ChatGPT asks to approve DevSpace access.", + ] : []), + ...initNextSteps(result), + ].join("\n")); +} + +function printInteractiveInitResult(result: InitResult): void { + const lines = [ + ...(result.allowedRoots ? [`Project folders: ${result.allowedRoots.join(", ")}`] : []), + `Subagents: ${result.providers.join(", ")}`, + ...(result.publicBaseUrl ? [`ChatGPT connection URL: ${result.publicBaseUrl}/mcp`] : []), + ]; + + prompts.note(lines.join("\n"), "DevSpace is ready"); + + if (usesChatGpt(result.usage)) { + prompts.note( + [ + `Owner password: ${result.ownerToken}`, + "Use this when ChatGPT asks you to approve DevSpace access.", + ].join("\n"), + "Owner password", + ); } + + if (usesCodingAgents(result.usage)) { + prompts.note( + [ + SUBAGENT_SKILL_INSTALL_COMMAND, + "", + "The Skills CLI will let you choose which Coding Agents receive it.", + ].join("\n"), + "Install the Subagents skill", + ); + } + + const nextSteps = [ + usesChatGpt(result.usage) ? "Run `devspace serve`, then connect ChatGPT." : undefined, + usesCodingAgents(result.usage) ? "Run the skill command above before delegating from your Coding Agents." : undefined, + ].filter(Boolean).join(" "); + + prompts.outro(nextSteps); } async function serve(): Promise { @@ -566,6 +652,8 @@ function printHelp(): void { " devspace Run first-time setup if needed, then start the server", " devspace serve Start the server", " devspace init Create or update ~/.devspace/config.jsonc and auth.json", + " devspace init --use [--providers ] [--roots ] [--public-url ] [--force] [--json]", + " Set up without prompts, for agents and scripts", " devspace doctor Show config, runtime, and native dependency status", " devspace config get Print persisted config", " devspace config set publicBaseUrl ", diff --git a/src/onboarding.test.ts b/src/onboarding.test.ts index 9ca02237d..1503654d9 100644 --- a/src/onboarding.test.ts +++ b/src/onboarding.test.ts @@ -1,6 +1,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + isNonInteractiveInit, + parseInitArgs, + resolveInitAnswers, resolveOnboardingUsage, updateOnboardingSubagentsConfig, } from "./onboarding.js"; @@ -74,3 +77,66 @@ test("onboarding toggles existing providers and preserves their settings", () => }, ); }); + +const DEFAULTS = { + destinations: ["coding-agents" as const], + providers: ["codex"], + knownProviders: ["codex", "claude", "pi"], + roots: ["/home/me/work"], + publicUrl: null, +}; + +test("init flags parse spaced and inline values and reject unknown options", () => { + assert.deepEqual( + parseInitArgs(["--use=both", "--providers", "codex, claude", "--roots", "~/a,~/b", "--public-url", "https://t.example", "--json"]), + { + force: false, + yes: false, + json: true, + use: ["chatgpt", "coding-agents"], + providers: ["codex", "claude"], + roots: ["~/a", "~/b"], + publicUrl: "https://t.example", + }, + ); + assert.throws(() => parseInitArgs(["--use", "slack"]), /Unknown --use value: slack/); + assert.throws(() => parseInitArgs(["--providers"]), /--providers needs a value/); + assert.throws(() => parseInitArgs(["--bogus"]), /Unknown option: --bogus/); +}); + +test("init runs without prompts when given answers or no terminal", () => { + assert.equal(isNonInteractiveInit(parseInitArgs([]), true), false); + assert.equal(isNonInteractiveInit(parseInitArgs([]), false), true); + assert.equal(isNonInteractiveInit(parseInitArgs(["--yes"]), true), true); + assert.equal(isNonInteractiveInit(parseInitArgs(["--providers", "codex"]), true), true); +}); + +test("init answers fall back to the prompt defaults for coding agents", () => { + assert.deepEqual(resolveInitAnswers(parseInitArgs(["--yes"]), DEFAULTS), { + usage: "coding-agents", + providers: ["codex"], + }); + assert.throws( + () => resolveInitAnswers(parseInitArgs(["--providers", "codex,nope"]), DEFAULTS), + /Unknown provider: nope. Known providers: codex, claude, pi/, + ); + assert.throws( + () => resolveInitAnswers(parseInitArgs(["--yes"]), { ...DEFAULTS, providers: [] }), + /No coding agents were detected/, + ); + assert.throws( + () => resolveInitAnswers(parseInitArgs(["--public-url", "https://t.example"]), DEFAULTS), + /apply only with --use chatgpt/, + ); +}); + +test("init answers for an MCP host need a public URL and default the roots", () => { + assert.throws( + () => resolveInitAnswers(parseInitArgs(["--use", "chatgpt"]), DEFAULTS), + /--public-url is required/, + ); + assert.deepEqual( + resolveInitAnswers(parseInitArgs(["--use", "chatgpt", "--public-url", "https://t.example"]), DEFAULTS), + { usage: "chatgpt", providers: ["codex"], roots: ["/home/me/work"], publicUrl: "https://t.example" }, + ); +}); diff --git a/src/onboarding.ts b/src/onboarding.ts index 2039854f0..b280e22ca 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -66,3 +66,108 @@ export function updateOnboardingSubagentsConfig( ], }; } + +/** `devspace init` options; any answer flag makes setup non-interactive. */ +export interface InitFlags { + force: boolean; + yes: boolean; + json: boolean; + use?: OnboardingDestination[]; + providers?: string[]; + roots?: string[]; + publicUrl?: string; +} + +export const INIT_USAGE = + "Usage: devspace init [--force] [--yes] [--json] [--use ] [--providers ] [--roots ] [--public-url ]"; + +const list = (value: string) => value.split(",").map((item) => item.trim()).filter(Boolean); + +export function parseInitArgs(args: readonly string[]): InitFlags { + const flags: InitFlags = { force: false, yes: false, json: false }; + + for (let i = 0; i < args.length; i++) { + const [name, inline] = args[i]!.split(/=(.*)/s, 2) as [string, string | undefined]; + const value = () => { + const next = inline ?? args[++i]; + + if (next === undefined || next.startsWith("--")) throw new Error(`${name} needs a value.\n${INIT_USAGE}`); + + return next; + }; + + switch (name) { + case "--force": flags.force = true; break; + case "--yes": case "-y": flags.yes = true; break; + case "--json": flags.json = true; break; + case "--use": flags.use = parseDestinations(value()); break; + case "--providers": flags.providers = list(value()); break; + case "--roots": flags.roots = list(value()); break; + case "--public-url": flags.publicUrl = value().trim(); break; + default: throw new Error(`Unknown option: ${args[i]}\n${INIT_USAGE}`); + } + } + + return flags; +} + +function parseDestinations(value: string): OnboardingDestination[] { + return list(value).flatMap((item) => { + if (item === "both") return [...ONBOARDING_DESTINATIONS]; + + if ((ONBOARDING_DESTINATIONS as readonly string[]).includes(item)) return [item as OnboardingDestination]; + throw new Error(`Unknown --use value: ${item}. Use chatgpt, coding-agents, or both.`); + }); +} + +/** Setup runs without prompts when asked to, when given answers, or when nobody can answer. */ +export function isNonInteractiveInit(flags: InitFlags, interactiveTerminal: boolean): boolean { + return flags.yes || flags.json || !interactiveTerminal + || [flags.use, flags.providers, flags.roots, flags.publicUrl].some((value) => value !== undefined); +} + +export interface InitAnswers { + usage: OnboardingUsage; + providers: string[]; + /** Raw root paths; set only when DevSpace serves an MCP host. */ + roots?: string[]; + /** Raw public URL; set only when DevSpace serves an MCP host. */ + publicUrl?: string; +} + +export interface InitDefaults { + destinations: OnboardingDestination[]; + providers: string[]; + knownProviders: string[]; + roots: string[]; + publicUrl: string | null; +} + +/** Fill unanswered flags with the same defaults the prompts preselect. */ +export function resolveInitAnswers(flags: InitFlags, defaults: InitDefaults): InitAnswers { + const usage = resolveOnboardingUsage(flags.use ?? defaults.destinations); + const providers = flags.providers ?? defaults.providers; + const unknown = providers.filter((id) => !defaults.knownProviders.includes(id)); + + if (unknown.length > 0) { + throw new Error(`Unknown provider: ${unknown.join(", ")}. Known providers: ${defaults.knownProviders.join(", ")}.`); + } + + if (providers.length === 0) { + throw new Error("No coding agents were detected. Install one, or pass --providers ."); + } + + if (!usesChatGpt(usage)) { + if (flags.roots || flags.publicUrl) throw new Error("--roots and --public-url apply only with --use chatgpt or both."); + + return { usage, providers }; + } + + const publicUrl = flags.publicUrl ?? defaults.publicUrl; + + if (!publicUrl) { + throw new Error("--public-url is required with --use chatgpt: the HTTPS origin of your tunnel, without /mcp."); + } + + return { usage, providers, roots: flags.roots ?? defaults.roots, publicUrl }; +} From 9edec52fd53d3896a7184b34211b8661a0ad542e Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:38:31 +0530 Subject: [PATCH 02/10] docs: document setup without prompts and agent self-onboarding Co-Authored-By: Claude Opus 5.5 --- website/docs/01-quickstart.mdx | 21 +++++++++++++++++++++ website/docs/reference/cli.mdx | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/website/docs/01-quickstart.mdx b/website/docs/01-quickstart.mdx index ef473a4db..0e8d0bac9 100644 --- a/website/docs/01-quickstart.mdx +++ b/website/docs/01-quickstart.mdx @@ -74,6 +74,27 @@ The installer brings its own Node when yours is missing or outside +## Let your agent set itself up + +A coding agent can do all of the above for itself. Paste this into Codex, +Claude Code, or any agent with a shell — every step runs without prompts: + +```text +Set up DevSpace (https://devspace.run) on this computer so you can delegate +coding work to my other coding agents. + +1. Install it, unless `devspace --version` already works: + curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh +2. Configure it: devspace init --use coding-agents --json +3. Install the subagents skill for yourself: + devspace agents skill --install +4. Check it works: devspace agents targets +``` + +The **Copy prompt** button on [devspace.run](https://devspace.run) copies a +version that also covers Windows. For an MCP host, see +[Setup without prompts](/reference/cli#setup-without-prompts). + ## Your first delegation From a project directory, ask your harness to delegate — or try the commands diff --git a/website/docs/reference/cli.mdx b/website/docs/reference/cli.mdx index 5c8122609..ce34fb158 100644 --- a/website/docs/reference/cli.mdx +++ b/website/docs/reference/cli.mdx @@ -9,6 +9,7 @@ description: Every devspace command — setup, the MCP server, subagents, workfl | --- | --- | | `devspace` | Run first-time setup if needed, then start the server. | | `devspace init [--force]` | Create or update `~/.devspace/config.jsonc` and `auth.json`. | +| `devspace init --use [--providers ] [--roots ] [--public-url ] [--force] [--json]` | Set up without prompts. See [Setup without prompts](#setup-without-prompts). | | `devspace serve` | Start the MCP server on `127.0.0.1:7676`. | | `devspace doctor` | Show config, runtime, Git, Bash, and native dependency status. | | `devspace config get` | Print the persisted config. | @@ -17,6 +18,33 @@ description: Every devspace command — setup, the MCP server, subagents, workfl | `devspace show-changes [--json]` | Inspect a Git-backed review checkpoint. | | `devspace -v`, `--version` | Print the installed version. | +### Setup without prompts + +`devspace init` asks its questions interactively. Agents and scripts answer +them with flags instead. Any answer flag, `--yes`, `--json`, or a missing +terminal skips the prompts, and unanswered questions take the defaults the +prompts preselect. + +| Flag | Default | Meaning | +| --- | --- | --- | +| `--use` | `coding-agents` (or `chatgpt` when a public URL is saved) | Where DevSpace is used: `chatgpt`, `coding-agents`, or `both`. | +| `--providers` | Configured providers, else every detected one | Comma-separated subagent providers, such as `codex,claude`. | +| `--roots` | Configured roots, else the current directory | Project folders an MCP host may open. Only with `--use chatgpt` or `both`. | +| `--public-url` | The saved public URL | Your tunnel's HTTPS origin, without `/mcp`. Required for `chatgpt` or `both`. | +| `--json` | — | Print the result as JSON, including the MCP URL and owner password for MCP hosts. | + +```bash +# A coding agent setting itself up +devspace init --use coding-agents --json + +# An MCP host, once the user's tunnel is running +devspace init --use chatgpt --roots ~/work --public-url https://your-tunnel-host.example.com +``` + +The tunnel and approving the host's connection stay with you: DevSpace never +creates a tunnel, and the host asks for the owner password before it gets +access. + ## Subagents Agent commands need no MCP server, OAuth, or tunnel. They run from the project From a82ea3323db4b99810f2d5689134f9abcded2f3e Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:38:31 +0530 Subject: [PATCH 03/10] docs(website): add a copy-prompt button for agent onboarding Beside "Read the docs", a button with stacked agent marks copies a prompt that has a coding agent install, configure, and verify DevSpace itself using only non-interactive commands. Co-Authored-By: Claude Opus 5.5 --- website/landing/content.ts | 14 ++++++++++++++ website/landing/home/Landing.astro | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/website/landing/content.ts b/website/landing/content.ts index 954024766..60b511ae1 100644 --- a/website/landing/content.ts +++ b/website/landing/content.ts @@ -9,6 +9,20 @@ export const INSTALL_METHODS = [ ] as const; export const INSTALL = INSTALL_METHODS[0].command; + +// Pasted into a coding agent, which onboards itself: every step runs without +// prompts (`init` flags, the bundled skill installer), so no human input is needed. +export const AGENT_PROMPT = `Set up DevSpace (https://devspace.run) on this computer so you can delegate coding work to my other coding agents, such as Codex and Claude Code. + +1. Install it, unless \`devspace --version\` already works: + - macOS / Linux: curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh + - Windows: $env:DEVSPACE_NO_INIT=1; irm https://devspace.run/install.ps1 | iex + If \`devspace\` is not on PATH afterwards, use ~/.local/bin/devspace (Windows: %LOCALAPPDATA%\\devspace\\bin\\devspace.cmd). +2. Configure it without prompts: devspace init --use coding-agents --json +3. Install the subagents skill for yourself: devspace agents skill --install +4. Check it works: devspace agents targets + +Then tell me which agents you can delegate to. Docs: https://devspace.run/docs/quickstart`; export const GITHUB = "https://github.com/Waishnav/devspace"; export const NPM = "https://www.npmjs.com/package/@waishnav/devspace"; export const DOCS = "/docs"; diff --git a/website/landing/home/Landing.astro b/website/landing/home/Landing.astro index 52a38936b..a55ecfb48 100644 --- a/website/landing/home/Landing.astro +++ b/website/landing/home/Landing.astro @@ -13,7 +13,7 @@ import InstallCommand from "./InstallCommand.astro"; import { Code } from "astro:components"; import { workbenchLightTheme } from "./theme"; import Logo from "../../logos/Logo.astro"; -import { DOCS, GITHUB, INSTALL, MATRIX, PILLARS, FAQ } from "../content"; +import { AGENT_PROMPT, DOCS, GITHUB, INSTALL, MATRIX, PILLARS, FAQ } from "../content"; const REVIEWER_PROFILE = `--- name: reviewer @@ -58,6 +58,12 @@ effort: high {INSTALL} + Read the docs → @@ -242,6 +248,13 @@ effort: high } .hero__right p { margin: 0 0 30px; max-width: 46ch; font-size: 14.5px; } .hero__cta { display: flex; flex-wrap: wrap; gap: 10px; } + /* Agent marks stacked like avatars, so the prompt button reads as "for your agent". */ + .prompt__logos { display: flex; } + .prompt__logo { + display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; + background: #fff; border: 1px solid var(--line); box-shadow: 0 0 0 2px var(--paper); + } + .prompt__logo + .prompt__logo { margin-left: -7px; } .stage { position: relative; padding: 14px 14px 0; background: #ededea; } .stage__art { height: 846px; } From 2391cc2d6ba296251a93ebd1f8627fdba497d670 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:47:23 +0530 Subject: [PATCH 04/10] style(website): satisfy lint in the landing and docs additions Readable-spacing fixes and a SAFETY note on the cloned docs action row. These files reached main through #448, which merged before its lint check finished. Co-Authored-By: Claude Opus 5.5 --- website/islands/DelegationDiagram.tsx | 2 ++ website/landing/Shell.astro | 4 ++++ website/landing/home/SkillsPopover.astro | 2 ++ website/scripts/og-cards.mjs | 4 ++++ website/ui/DocsAside.astro | 1 + 5 files changed, 13 insertions(+) diff --git a/website/islands/DelegationDiagram.tsx b/website/islands/DelegationDiagram.tsx index cf34ecd37..49418f48e 100644 --- a/website/islands/DelegationDiagram.tsx +++ b/website/islands/DelegationDiagram.tsx @@ -14,11 +14,13 @@ const ROW = 62; // px per host slot const TICKS_PER_STEP = 8; const TICK_MS = 550; const AGENT_IDS = ["7Kq2", "Q9m4", "Xb31", "Lp58", "Tn0d", "Ve66", "Rw29", "Zc74"]; + // Agent rows, matching `.dm__agents li` height and gap in routing.css, so the // fan's curves land on each row's centre. const AGENT_ROW = 38; const AGENT_GAP = 9; const FAN_HEIGHT = PROVIDERS.length * AGENT_ROW + (PROVIDERS.length - 1) * AGENT_GAP; + const fanCurve = (i: number) => { const y = i * (AGENT_ROW + AGENT_GAP) + AGENT_ROW / 2; const mid = FAN_HEIGHT / 2; diff --git a/website/landing/Shell.astro b/website/landing/Shell.astro index 1f6a7d235..476fce78d 100644 --- a/website/landing/Shell.astro +++ b/website/landing/Shell.astro @@ -8,14 +8,18 @@ interface Props { } const SITE = "https://devspace.run"; + const { title = "DevSpace — let your agents delegate to each other", description = "Connect Grok Bot, ChatGPT, and Claude to your machine, and let any agent delegate to Codex, Claude Code, OpenCode, Cursor, and more.", bodyClass, } = Astro.props; + const url = new URL(Astro.url.pathname, SITE).href; + // Blume generates the home route's card; scripts/og-cards.mjs redraws it after the build. const image = `${SITE}/og/index.png`; + // Docs pages get Blume's WebSite and SoftwareApplication graph; the landing page // uses its own shell, so it declares the same identity here. const structuredData = { diff --git a/website/landing/home/SkillsPopover.astro b/website/landing/home/SkillsPopover.astro index f3b9d5381..41864113c 100644 --- a/website/landing/home/SkillsPopover.astro +++ b/website/landing/home/SkillsPopover.astro @@ -7,11 +7,13 @@ import Logo from "../../logos/Logo.astro"; import type { LogoId } from "../../logos"; const SKILLS = "https://skills.sh/waishnav/devspace"; + const AGENTS: ReadonlyArray<{ id: LogoId; name: string }> = [ { id: "grok", name: "Grok Bot" }, { id: "openai", name: "ChatGPT" }, { id: "claude", name: "Claude" }, ]; + const COMMANDS = [ { label: "Subagents", note: undefined, command: "npx skills add Waishnav/devspace --skill subagents --global" }, { label: "Dynamic workflows", note: "experimental", command: "npx skills add Waishnav/devspace --skill workflows --global" }, diff --git a/website/scripts/og-cards.mjs b/website/scripts/og-cards.mjs index 91bc08d48..67191052b 100644 --- a/website/scripts/og-cards.mjs +++ b/website/scripts/og-cards.mjs @@ -25,7 +25,9 @@ const fonts = [ { name: "IBM Plex Mono", weight: 500, data: await font("@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2") }, { name: "Hanken Grotesk", data: await font("@fontsource-variable/hanken-grotesk/files/hanken-grotesk-latin-wght-normal.woff2") }, ]; + const mark = `data:image/png;base64,${(await readFile(join(root, "public/logos/devspace-blue.png"))).toString("base64")}`; + const github = `data:image/svg+xml;base64,${Buffer.from( ``, ).toString("base64")}`; @@ -77,6 +79,7 @@ const card = ({ title, description }) => const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", "#x27": "'" }; const decode = (value) => value.replace(/&(amp|lt|gt|quot|#39|#x27);/g, (_, name) => ENTITIES[name]); + const meta = (html, property) => decode(html.match(new RegExp(` 0) url.searchParams.set("subpath", segments.join("/")); + // SAFETY: a shallow clone of an is an . const row = github.cloneNode() as HTMLAnchorElement; row.href = url.href; From fc4357d70969db6bdc8e02bd8867accd566f9f86 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:47:24 +0530 Subject: [PATCH 05/10] refactor(cli): infer the init setup from its answers instead of --use `--use chatgpt|coding-agents|both` asked callers to name a mode in DevSpace's own terms. Subagents are always set up, and a public URL is the only thing an MCP host adds, so init now infers the setup: `--yes` takes the defaults, and `--public-url` (with optional `--roots`) also serves an MCP host. The flags are documented as ordinary init options rather than a separate mode. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 52 ++++++++++++++++++++++++---------- src/onboarding.test.ts | 25 +++++++--------- src/onboarding.ts | 47 +++++++++++++----------------- website/docs/01-quickstart.mdx | 6 ++-- website/docs/reference/cli.mdx | 32 ++++++++------------- website/landing/content.ts | 7 +++-- 6 files changed, 87 insertions(+), 82 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index ca5a773a5..d8e229b59 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -187,8 +187,8 @@ async function ensureConfigured(): Promise { "Run:", " devspace init", "", - "Agents and scripts can answer setup with flags, for example:", - " devspace init --use chatgpt --public-url https://your-tunnel-host.example.com", + "Setup questions can also be answered with flags, for example:", + " devspace init --public-url https://your-tunnel-host.example.com", "", "Or provide DEVSPACE_OAUTH_OWNER_TOKEN.", ].join("\n"), @@ -215,11 +215,12 @@ async function runInit(flags: InitFlags): Promise { const currentSubagents = resolveSubagentsConfig(files.config.subagents ?? disabledSubagentsConfig()); const availability = getLocalAgentProviderAvailabilitySnapshot(process.env, currentSubagents); + const configuredProviders = currentSubagents.providers .filter((provider) => provider.enabled) .map((provider) => provider.id); + const defaults = { - destinations: [files.config.server.publicBaseUrl ? "chatgpt" : "coding-agents"] as OnboardingDestination[], providers: configuredProviders.length > 0 ? configuredProviders : availability.filter((provider) => provider.available).map((provider) => provider.name), @@ -234,6 +235,7 @@ async function runInit(flags: InitFlags): Promise { if (publicUrlError) throw new Error(`--public-url: ${publicUrlError}`); const result = applyInitAnswers(files, currentSubagents, answers); + const unavailable = availability .filter((provider) => result.providers.includes(provider.name) && !provider.available) .map((provider) => `${provider.name} (${provider.reason ?? "provider preflight failed"})`); @@ -245,7 +247,13 @@ async function runInit(flags: InitFlags): Promise { try { prompts.intro("DevSpace setup"); - const result = applyInitAnswers(files, currentSubagents, await promptInitAnswers(defaults, availability, files.config.server.port)); + const destinations: OnboardingDestination[] = [defaults.publicUrl ? "chatgpt" : "coding-agents"]; + + const result = applyInitAnswers( + files, + currentSubagents, + await promptInitAnswers({ ...defaults, destinations }, availability, files.config.server.port), + ); printInteractiveInitResult(result); } catch (error) { @@ -359,6 +367,7 @@ function applyInitAnswers( ?.map((root) => root.trim()) .filter(Boolean) .map((root) => resolve(expandHomePath(root))); + const publicBaseUrl = answers.publicUrl ? normalizePublicBaseUrl(answers.publicUrl) : undefined; const auth = { ownerToken: files.auth.ownerToken ?? generateOwnerToken() }; @@ -387,23 +396,37 @@ function initNextSteps(result: InitResult): string[] { ]; } +/** `devspace init --json` output; the MCP fields appear only when an MCP host is set up. */ +interface InitReport { + status: "configured"; + config_path: string; + providers: string[]; + unavailable_providers: string[]; + next_steps: string[]; + allowed_roots?: string[]; + mcp_url?: string; + owner_password?: string; +} + function printInitResult(result: InitResult, unavailable: string[], json: boolean): void { const chatGpt = usesChatGpt(result.usage); if (json) { - console.log(JSON.stringify({ + const report: InitReport = { status: "configured", config_path: result.configPath, - usage: result.usage, providers: result.providers, unavailable_providers: unavailable, - ...(chatGpt ? { - allowed_roots: result.allowedRoots, - mcp_url: `${result.publicBaseUrl}/mcp`, - owner_password: result.ownerToken, - } : {}), next_steps: initNextSteps(result), - }, null, 2)); + }; + + if (chatGpt) { + report.allowed_roots = result.allowedRoots; + report.mcp_url = `${result.publicBaseUrl}/mcp`; + report.owner_password = result.ownerToken; + } + + console.log(JSON.stringify(report, null, 2)); return; } @@ -651,9 +674,8 @@ function printHelp(): void { "Usage:", " devspace Run first-time setup if needed, then start the server", " devspace serve Start the server", - " devspace init Create or update ~/.devspace/config.jsonc and auth.json", - " devspace init --use [--providers ] [--roots ] [--public-url ] [--force] [--json]", - " Set up without prompts, for agents and scripts", + " devspace init [--yes] [--providers ] [--public-url ] [--roots ] [--force] [--json]", + " Create or update ~/.devspace/config.jsonc and auth.json", " devspace doctor Show config, runtime, and native dependency status", " devspace config get Print persisted config", " devspace config set publicBaseUrl ", diff --git a/src/onboarding.test.ts b/src/onboarding.test.ts index 1503654d9..f05dbc429 100644 --- a/src/onboarding.test.ts +++ b/src/onboarding.test.ts @@ -79,7 +79,6 @@ test("onboarding toggles existing providers and preserves their settings", () => }); const DEFAULTS = { - destinations: ["coding-agents" as const], providers: ["codex"], knownProviders: ["codex", "claude", "pi"], roots: ["/home/me/work"], @@ -88,20 +87,18 @@ const DEFAULTS = { test("init flags parse spaced and inline values and reject unknown options", () => { assert.deepEqual( - parseInitArgs(["--use=both", "--providers", "codex, claude", "--roots", "~/a,~/b", "--public-url", "https://t.example", "--json"]), + parseInitArgs(["--providers", "codex, claude", "--roots=~/a,~/b", "--public-url", "https://t.example", "--json"]), { force: false, yes: false, json: true, - use: ["chatgpt", "coding-agents"], providers: ["codex", "claude"], roots: ["~/a", "~/b"], publicUrl: "https://t.example", }, ); - assert.throws(() => parseInitArgs(["--use", "slack"]), /Unknown --use value: slack/); assert.throws(() => parseInitArgs(["--providers"]), /--providers needs a value/); - assert.throws(() => parseInitArgs(["--bogus"]), /Unknown option: --bogus/); + assert.throws(() => parseInitArgs(["--use", "chatgpt"]), /Unknown option: --use/); }); test("init runs without prompts when given answers or no terminal", () => { @@ -111,7 +108,7 @@ test("init runs without prompts when given answers or no terminal", () => { assert.equal(isNonInteractiveInit(parseInitArgs(["--providers", "codex"]), true), true); }); -test("init answers fall back to the prompt defaults for coding agents", () => { +test("init sets up subagents from the prompt defaults when no public URL is known", () => { assert.deepEqual(resolveInitAnswers(parseInitArgs(["--yes"]), DEFAULTS), { usage: "coding-agents", providers: ["codex"], @@ -125,18 +122,18 @@ test("init answers fall back to the prompt defaults for coding agents", () => { /No coding agents were detected/, ); assert.throws( - () => resolveInitAnswers(parseInitArgs(["--public-url", "https://t.example"]), DEFAULTS), - /apply only with --use chatgpt/, + () => resolveInitAnswers(parseInitArgs(["--roots", "~/work"]), DEFAULTS), + /--roots needs --public-url/, ); }); -test("init answers for an MCP host need a public URL and default the roots", () => { - assert.throws( - () => resolveInitAnswers(parseInitArgs(["--use", "chatgpt"]), DEFAULTS), - /--public-url is required/, +test("init also serves an MCP host once a public URL is given or saved", () => { + assert.deepEqual( + resolveInitAnswers(parseInitArgs(["--public-url", "https://t.example"]), DEFAULTS), + { usage: "both", providers: ["codex"], roots: ["/home/me/work"], publicUrl: "https://t.example" }, ); assert.deepEqual( - resolveInitAnswers(parseInitArgs(["--use", "chatgpt", "--public-url", "https://t.example"]), DEFAULTS), - { usage: "chatgpt", providers: ["codex"], roots: ["/home/me/work"], publicUrl: "https://t.example" }, + resolveInitAnswers(parseInitArgs(["--yes"]), { ...DEFAULTS, publicUrl: "https://saved.example" }), + { usage: "both", providers: ["codex"], roots: ["/home/me/work"], publicUrl: "https://saved.example" }, ); }); diff --git a/src/onboarding.ts b/src/onboarding.ts index b280e22ca..691ed28c6 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -67,19 +67,21 @@ export function updateOnboardingSubagentsConfig( }; } -/** `devspace init` options; any answer flag makes setup non-interactive. */ +/** + * `devspace init` options. Each flag answers one setup question; `--yes` takes + * the defaults for the rest. A public URL is what additionally serves an MCP host. + */ export interface InitFlags { force: boolean; yes: boolean; json: boolean; - use?: OnboardingDestination[]; providers?: string[]; roots?: string[]; publicUrl?: string; } export const INIT_USAGE = - "Usage: devspace init [--force] [--yes] [--json] [--use ] [--providers ] [--roots ] [--public-url ]"; + "Usage: devspace init [--yes] [--providers ] [--public-url ] [--roots ] [--force] [--json]"; const list = (value: string) => value.split(",").map((item) => item.trim()).filter(Boolean); @@ -87,7 +89,11 @@ export function parseInitArgs(args: readonly string[]): InitFlags { const flags: InitFlags = { force: false, yes: false, json: false }; for (let i = 0; i < args.length; i++) { - const [name, inline] = args[i]!.split(/=(.*)/s, 2) as [string, string | undefined]; + const arg = args[i]!; + const equals = arg.indexOf("="); + const name = equals === -1 ? arg : arg.slice(0, equals); + const inline = equals === -1 ? undefined : arg.slice(equals + 1); + const value = () => { const next = inline ?? args[++i]; @@ -100,30 +106,20 @@ export function parseInitArgs(args: readonly string[]): InitFlags { case "--force": flags.force = true; break; case "--yes": case "-y": flags.yes = true; break; case "--json": flags.json = true; break; - case "--use": flags.use = parseDestinations(value()); break; case "--providers": flags.providers = list(value()); break; case "--roots": flags.roots = list(value()); break; case "--public-url": flags.publicUrl = value().trim(); break; - default: throw new Error(`Unknown option: ${args[i]}\n${INIT_USAGE}`); + default: throw new Error(`Unknown option: ${arg}\n${INIT_USAGE}`); } } return flags; } -function parseDestinations(value: string): OnboardingDestination[] { - return list(value).flatMap((item) => { - if (item === "both") return [...ONBOARDING_DESTINATIONS]; - - if ((ONBOARDING_DESTINATIONS as readonly string[]).includes(item)) return [item as OnboardingDestination]; - throw new Error(`Unknown --use value: ${item}. Use chatgpt, coding-agents, or both.`); - }); -} - /** Setup runs without prompts when asked to, when given answers, or when nobody can answer. */ export function isNonInteractiveInit(flags: InitFlags, interactiveTerminal: boolean): boolean { return flags.yes || flags.json || !interactiveTerminal - || [flags.use, flags.providers, flags.roots, flags.publicUrl].some((value) => value !== undefined); + || [flags.providers, flags.roots, flags.publicUrl].some((value) => value !== undefined); } export interface InitAnswers { @@ -136,16 +132,17 @@ export interface InitAnswers { } export interface InitDefaults { - destinations: OnboardingDestination[]; providers: string[]; knownProviders: string[]; roots: string[]; publicUrl: string | null; } -/** Fill unanswered flags with the same defaults the prompts preselect. */ +/** + * Fill unanswered flags with the defaults the prompts preselect. Subagents are + * always set up; a public URL, given or already saved, also serves an MCP host. + */ export function resolveInitAnswers(flags: InitFlags, defaults: InitDefaults): InitAnswers { - const usage = resolveOnboardingUsage(flags.use ?? defaults.destinations); const providers = flags.providers ?? defaults.providers; const unknown = providers.filter((id) => !defaults.knownProviders.includes(id)); @@ -157,17 +154,13 @@ export function resolveInitAnswers(flags: InitFlags, defaults: InitDefaults): In throw new Error("No coding agents were detected. Install one, or pass --providers ."); } - if (!usesChatGpt(usage)) { - if (flags.roots || flags.publicUrl) throw new Error("--roots and --public-url apply only with --use chatgpt or both."); - - return { usage, providers }; - } - const publicUrl = flags.publicUrl ?? defaults.publicUrl; if (!publicUrl) { - throw new Error("--public-url is required with --use chatgpt: the HTTPS origin of your tunnel, without /mcp."); + if (flags.roots) throw new Error("--roots needs --public-url: project folders apply only when an MCP host connects."); + + return { usage: "coding-agents", providers }; } - return { usage, providers, roots: flags.roots ?? defaults.roots, publicUrl }; + return { usage: "both", providers, roots: flags.roots ?? defaults.roots, publicUrl }; } diff --git a/website/docs/01-quickstart.mdx b/website/docs/01-quickstart.mdx index 0e8d0bac9..c0dd7e3db 100644 --- a/website/docs/01-quickstart.mdx +++ b/website/docs/01-quickstart.mdx @@ -85,15 +85,15 @@ coding work to my other coding agents. 1. Install it, unless `devspace --version` already works: curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh -2. Configure it: devspace init --use coding-agents --json +2. Configure it: devspace init --yes 3. Install the subagents skill for yourself: devspace agents skill --install 4. Check it works: devspace agents targets ``` The **Copy prompt** button on [devspace.run](https://devspace.run) copies a -version that also covers Windows. For an MCP host, see -[Setup without prompts](/reference/cli#setup-without-prompts). +version that also covers Windows. To answer setup questions with flags, see +[init options](/reference/cli#init-options). ## Your first delegation diff --git a/website/docs/reference/cli.mdx b/website/docs/reference/cli.mdx index ce34fb158..d617e188e 100644 --- a/website/docs/reference/cli.mdx +++ b/website/docs/reference/cli.mdx @@ -8,8 +8,7 @@ description: Every devspace command — setup, the MCP server, subagents, workfl | Command | Description | | --- | --- | | `devspace` | Run first-time setup if needed, then start the server. | -| `devspace init [--force]` | Create or update `~/.devspace/config.jsonc` and `auth.json`. | -| `devspace init --use [--providers ] [--roots ] [--public-url ] [--force] [--json]` | Set up without prompts. See [Setup without prompts](#setup-without-prompts). | +| `devspace init [options]` | Create or update `~/.devspace/config.jsonc` and `auth.json`. See [init options](#init-options). | | `devspace serve` | Start the MCP server on `127.0.0.1:7676`. | | `devspace doctor` | Show config, runtime, Git, Bash, and native dependency status. | | `devspace config get` | Print the persisted config. | @@ -18,33 +17,26 @@ description: Every devspace command — setup, the MCP server, subagents, workfl | `devspace show-changes [--json]` | Inspect a Git-backed review checkpoint. | | `devspace -v`, `--version` | Print the installed version. | -### Setup without prompts +### init options -`devspace init` asks its questions interactively. Agents and scripts answer -them with flags instead. Any answer flag, `--yes`, `--json`, or a missing -terminal skips the prompts, and unanswered questions take the defaults the -prompts preselect. +In a terminal, `devspace init` asks its setup questions. Each flag answers one +of them, and `--yes` takes the defaults for the rest. Without a terminal, +`init` uses the defaults too. | Flag | Default | Meaning | | --- | --- | --- | -| `--use` | `coding-agents` (or `chatgpt` when a public URL is saved) | Where DevSpace is used: `chatgpt`, `coding-agents`, or `both`. | | `--providers` | Configured providers, else every detected one | Comma-separated subagent providers, such as `codex,claude`. | -| `--roots` | Configured roots, else the current directory | Project folders an MCP host may open. Only with `--use chatgpt` or `both`. | -| `--public-url` | The saved public URL | Your tunnel's HTTPS origin, without `/mcp`. Required for `chatgpt` or `both`. | -| `--json` | — | Print the result as JSON, including the MCP URL and owner password for MCP hosts. | +| `--public-url` | The saved public URL | Your tunnel's HTTPS origin, without `/mcp`. Setting one also serves an MCP host such as ChatGPT. | +| `--roots` | Configured roots, else the current directory | Project folders an MCP host may open. Needs a public URL. | +| `--yes`, `-y` | — | Use the defaults for every unanswered question. | +| `--force` | — | Update an existing setup. | +| `--json` | — | Print the result as JSON. With a public URL it includes the MCP URL and owner password. | ```bash -# A coding agent setting itself up -devspace init --use coding-agents --json - -# An MCP host, once the user's tunnel is running -devspace init --use chatgpt --roots ~/work --public-url https://your-tunnel-host.example.com +devspace init --yes +devspace init --public-url https://your-tunnel-host.example.com --roots ~/work ``` -The tunnel and approving the host's connection stay with you: DevSpace never -creates a tunnel, and the host asks for the owner password before it gets -access. - ## Subagents Agent commands need no MCP server, OAuth, or tunnel. They run from the project diff --git a/website/landing/content.ts b/website/landing/content.ts index 60b511ae1..d68a03bab 100644 --- a/website/landing/content.ts +++ b/website/landing/content.ts @@ -10,19 +10,20 @@ export const INSTALL_METHODS = [ export const INSTALL = INSTALL_METHODS[0].command; -// Pasted into a coding agent, which onboards itself: every step runs without -// prompts (`init` flags, the bundled skill installer), so no human input is needed. +// Pasted into a coding agent, which sets DevSpace up itself; every step runs +// without prompts, so the agent never waits on the user. export const AGENT_PROMPT = `Set up DevSpace (https://devspace.run) on this computer so you can delegate coding work to my other coding agents, such as Codex and Claude Code. 1. Install it, unless \`devspace --version\` already works: - macOS / Linux: curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh - Windows: $env:DEVSPACE_NO_INIT=1; irm https://devspace.run/install.ps1 | iex If \`devspace\` is not on PATH afterwards, use ~/.local/bin/devspace (Windows: %LOCALAPPDATA%\\devspace\\bin\\devspace.cmd). -2. Configure it without prompts: devspace init --use coding-agents --json +2. Configure it: devspace init --yes 3. Install the subagents skill for yourself: devspace agents skill --install 4. Check it works: devspace agents targets Then tell me which agents you can delegate to. Docs: https://devspace.run/docs/quickstart`; + export const GITHUB = "https://github.com/Waishnav/devspace"; export const NPM = "https://www.npmjs.com/package/@waishnav/devspace"; export const DOCS = "/docs"; From 14ca238b04881b008a4d71b6b0eceb53f2b9ea32 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:54:42 +0530 Subject: [PATCH 06/10] fix(cli): keep init's MCP host setup explicit and its secrets out of output Setup without prompts now requires --roots for an MCP host unless roots are already configured, instead of trusting the directory init ran from. It reports where the owner password is stored rather than printing it, so the password stays out of tool logs. Boolean flags reject values (--force=false), /mcp/ is rejected like /mcp, an MCP host can be set up without any coding agent installed, and the output covers restarting a running server and updating an existing setup. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 28 +++++++++++++++++----------- src/onboarding.test.ts | 15 +++++++++++++++ src/onboarding.ts | 30 ++++++++++++++++++++++-------- website/docs/reference/cli.mdx | 4 ++-- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index d8e229b59..77fbfbf47 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -204,7 +204,11 @@ async function runInit(flags: InitFlags): Promise { if (!flags.force && files.configExists && files.authExists) { if (flags.json) { - console.log(JSON.stringify({ status: "already_configured", config_path: files.configPath }, null, 2)); + console.log(JSON.stringify({ + status: "already_configured", + config_path: files.configPath, + next_steps: ["Run `devspace init --force` with your answers to update it."], + }, null, 2)); } else { console.log(`DevSpace is already configured at ${files.dir}`); console.log("Run `devspace init --force` to update it."); @@ -225,7 +229,7 @@ async function runInit(flags: InitFlags): Promise { ? configuredProviders : availability.filter((provider) => provider.available).map((provider) => provider.name), knownProviders: availability.map((provider) => provider.name), - roots: files.config.workspaces.allowedRoots.length > 0 ? files.config.workspaces.allowedRoots : [process.cwd()], + roots: files.config.workspaces.allowedRoots, publicUrl: files.config.server.publicBaseUrl ?? null, }; @@ -240,7 +244,7 @@ async function runInit(flags: InitFlags): Promise { .filter((provider) => result.providers.includes(provider.name) && !provider.available) .map((provider) => `${provider.name} (${provider.reason ?? "provider preflight failed"})`); - printInitResult(result, unavailable, flags.json); + printInitResult(result, files.authPath, unavailable, flags.json); return; } @@ -248,11 +252,13 @@ async function runInit(flags: InitFlags): Promise { try { prompts.intro("DevSpace setup"); const destinations: OnboardingDestination[] = [defaults.publicUrl ? "chatgpt" : "coding-agents"]; + // A person confirms the prompt's root, so the current directory is a fine suggestion there. + const roots = defaults.roots.length > 0 ? defaults.roots : [process.cwd()]; const result = applyInitAnswers( files, currentSubagents, - await promptInitAnswers({ ...defaults, destinations }, availability, files.config.server.port), + await promptInitAnswers({ ...defaults, roots, destinations }, availability, files.config.server.port), ); printInteractiveInitResult(result); @@ -391,7 +397,7 @@ function applyInitAnswers( function initNextSteps(result: InitResult): string[] { return [ - ...(usesChatGpt(result.usage) ? ["Run `devspace serve`, then connect ChatGPT."] : []), + ...(usesChatGpt(result.usage) ? ["Run `devspace serve` (restart it if it is already running), then connect ChatGPT."] : []), ...(usesCodingAgents(result.usage) ? [`Install the Subagents skill: ${SUBAGENT_SKILL_INSTALL_COMMAND}`] : []), ]; } @@ -405,10 +411,11 @@ interface InitReport { next_steps: string[]; allowed_roots?: string[]; mcp_url?: string; - owner_password?: string; + /** Where the owner password lives; it is never printed here, so it stays out of tool logs. */ + owner_password_file?: string; } -function printInitResult(result: InitResult, unavailable: string[], json: boolean): void { +function printInitResult(result: InitResult, authPath: string, unavailable: string[], json: boolean): void { const chatGpt = usesChatGpt(result.usage); if (json) { @@ -423,7 +430,7 @@ function printInitResult(result: InitResult, unavailable: string[], json: boolea if (chatGpt) { report.allowed_roots = result.allowedRoots; report.mcp_url = `${result.publicBaseUrl}/mcp`; - report.owner_password = result.ownerToken; + report.owner_password_file = authPath; } console.log(JSON.stringify(report, null, 2)); @@ -438,8 +445,7 @@ function printInitResult(result: InitResult, unavailable: string[], json: boolea ...(chatGpt ? [ `Project folders: ${result.allowedRoots!.join(", ")}`, `ChatGPT connection URL: ${result.publicBaseUrl}/mcp`, - `Owner password: ${result.ownerToken}`, - "The user enters the owner password when ChatGPT asks to approve DevSpace access.", + `Owner password: stored in ${authPath}. You enter it when ChatGPT asks to approve DevSpace access.`, ] : []), ...initNextSteps(result), ].join("\n")); @@ -1366,7 +1372,7 @@ function validateRequiredPublicBaseUrl(value: string | undefined): string | unde if (!trimmed) return "Enter the public URL from your tunnel or reverse proxy."; - if (trimmed.endsWith("/mcp")) return "Enter the base URL only, without /mcp."; + if (trimmed.replace(/\/+$/, "").endsWith("/mcp")) return "Enter the base URL only, without /mcp."; return validatePublicBaseUrl(trimmed); } diff --git a/src/onboarding.test.ts b/src/onboarding.test.ts index f05dbc429..e1485bf9c 100644 --- a/src/onboarding.test.ts +++ b/src/onboarding.test.ts @@ -99,6 +99,7 @@ test("init flags parse spaced and inline values and reject unknown options", () ); assert.throws(() => parseInitArgs(["--providers"]), /--providers needs a value/); assert.throws(() => parseInitArgs(["--use", "chatgpt"]), /Unknown option: --use/); + assert.throws(() => parseInitArgs(["--force=false"]), /--force takes no value/); }); test("init runs without prompts when given answers or no terminal", () => { @@ -137,3 +138,17 @@ test("init also serves an MCP host once a public URL is given or saved", () => { { usage: "both", providers: ["codex"], roots: ["/home/me/work"], publicUrl: "https://saved.example" }, ); }); + +test("init never guesses the roots an MCP host may open", () => { + assert.throws( + () => resolveInitAnswers(parseInitArgs(["--public-url", "https://t.example"]), { ...DEFAULTS, roots: [] }), + /--roots is required with --public-url/, + ); +}); + +test("init can serve an MCP host without any coding agent installed", () => { + assert.deepEqual( + resolveInitAnswers(parseInitArgs(["--public-url", "https://t.example", "--roots", "~/work"]), { ...DEFAULTS, providers: [] }), + { usage: "both", providers: [], roots: ["~/work"], publicUrl: "https://t.example" }, + ); +}); diff --git a/src/onboarding.ts b/src/onboarding.ts index 691ed28c6..d11cceba4 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -94,6 +94,12 @@ export function parseInitArgs(args: readonly string[]): InitFlags { const name = equals === -1 ? arg : arg.slice(0, equals); const inline = equals === -1 ? undefined : arg.slice(equals + 1); + const flag = () => { + if (inline !== undefined) throw new Error(`${name} takes no value.\n${INIT_USAGE}`); + + return true; + }; + const value = () => { const next = inline ?? args[++i]; @@ -103,9 +109,9 @@ export function parseInitArgs(args: readonly string[]): InitFlags { }; switch (name) { - case "--force": flags.force = true; break; - case "--yes": case "-y": flags.yes = true; break; - case "--json": flags.json = true; break; + case "--force": flags.force = flag(); break; + case "--yes": case "-y": flags.yes = flag(); break; + case "--json": flags.json = flag(); break; case "--providers": flags.providers = list(value()); break; case "--roots": flags.roots = list(value()); break; case "--public-url": flags.publicUrl = value().trim(); break; @@ -134,6 +140,7 @@ export interface InitAnswers { export interface InitDefaults { providers: string[]; knownProviders: string[]; + /** Configured roots only; an MCP host never gets a guessed root. */ roots: string[]; publicUrl: string | null; } @@ -150,17 +157,24 @@ export function resolveInitAnswers(flags: InitFlags, defaults: InitDefaults): In throw new Error(`Unknown provider: ${unknown.join(", ")}. Known providers: ${defaults.knownProviders.join(", ")}.`); } - if (providers.length === 0) { - throw new Error("No coding agents were detected. Install one, or pass --providers ."); - } - const publicUrl = flags.publicUrl ?? defaults.publicUrl; if (!publicUrl) { if (flags.roots) throw new Error("--roots needs --public-url: project folders apply only when an MCP host connects."); + if (providers.length === 0) { + throw new Error("No coding agents were detected. Install one, or pass --providers ."); + } + return { usage: "coding-agents", providers }; } - return { usage: "both", providers, roots: flags.roots ?? defaults.roots, publicUrl }; + // Roots bound what an MCP host may open, so they are never guessed from wherever init happened to run. + const roots = flags.roots ?? defaults.roots; + + if (roots.length === 0) { + throw new Error("--roots is required with --public-url: the project folders an MCP host may open."); + } + + return { usage: "both", providers, roots, publicUrl }; } diff --git a/website/docs/reference/cli.mdx b/website/docs/reference/cli.mdx index d617e188e..5fc522b79 100644 --- a/website/docs/reference/cli.mdx +++ b/website/docs/reference/cli.mdx @@ -27,10 +27,10 @@ of them, and `--yes` takes the defaults for the rest. Without a terminal, | --- | --- | --- | | `--providers` | Configured providers, else every detected one | Comma-separated subagent providers, such as `codex,claude`. | | `--public-url` | The saved public URL | Your tunnel's HTTPS origin, without `/mcp`. Setting one also serves an MCP host such as ChatGPT. | -| `--roots` | Configured roots, else the current directory | Project folders an MCP host may open. Needs a public URL. | +| `--roots` | Configured roots | Project folders an MCP host may open. Needs a public URL, and is required with one unless roots are already configured. | | `--yes`, `-y` | — | Use the defaults for every unanswered question. | | `--force` | — | Update an existing setup. | -| `--json` | — | Print the result as JSON. With a public URL it includes the MCP URL and owner password. | +| `--json` | — | Print the result as JSON. With a public URL it includes the MCP URL and where the owner password is stored, never the password itself. | ```bash devspace init --yes From 2eebc15a6fd794b39ea8e3b72a18a1c19a18b231 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:54:43 +0530 Subject: [PATCH 07/10] fix(website): harden the agent setup prompt and announce copies The prompt always runs the installer, which updates an older install that lacks the new init options, uses a PowerShell-valid launcher path, and keeps an existing setup. Copy buttons announce the result through a status region for screen readers. Co-Authored-By: Claude Opus 5.5 --- website/docs/01-quickstart.mdx | 2 +- website/landing/Shell.astro | 7 +++++++ website/landing/content.ts | 7 ++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/website/docs/01-quickstart.mdx b/website/docs/01-quickstart.mdx index c0dd7e3db..d0a0fdfc9 100644 --- a/website/docs/01-quickstart.mdx +++ b/website/docs/01-quickstart.mdx @@ -83,7 +83,7 @@ Claude Code, or any agent with a shell — every step runs without prompts: Set up DevSpace (https://devspace.run) on this computer so you can delegate coding work to my other coding agents. -1. Install it, unless `devspace --version` already works: +1. Install or update it: curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh 2. Configure it: devspace init --yes 3. Install the subagents skill for yourself: diff --git a/website/landing/Shell.astro b/website/landing/Shell.astro index 476fce78d..6e550aa5c 100644 --- a/website/landing/Shell.astro +++ b/website/landing/Shell.astro @@ -76,6 +76,8 @@ const structuredData = { + +

@@ -98,6 +100,10 @@ const structuredData = { target.dataset.copied = ""; setTimeout(() => delete target.dataset.copied, 1400); + + const status = document.querySelector("[data-copy-status]"); + + if (status) status.textContent = "Copied to clipboard"; }); @@ -108,4 +114,5 @@ const structuredData = { img, svg { display: block; max-width: 100%; } a { color: inherit; } button { font: inherit; color: inherit; } + .sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } diff --git a/website/landing/content.ts b/website/landing/content.ts index d68a03bab..fa24394ea 100644 --- a/website/landing/content.ts +++ b/website/landing/content.ts @@ -14,11 +14,12 @@ export const INSTALL = INSTALL_METHODS[0].command; // without prompts, so the agent never waits on the user. export const AGENT_PROMPT = `Set up DevSpace (https://devspace.run) on this computer so you can delegate coding work to my other coding agents, such as Codex and Claude Code. -1. Install it, unless \`devspace --version\` already works: +1. Install or update it: - macOS / Linux: curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh - - Windows: $env:DEVSPACE_NO_INIT=1; irm https://devspace.run/install.ps1 | iex - If \`devspace\` is not on PATH afterwards, use ~/.local/bin/devspace (Windows: %LOCALAPPDATA%\\devspace\\bin\\devspace.cmd). + - Windows (PowerShell): $env:DEVSPACE_NO_INIT=1; irm https://devspace.run/install.ps1 | iex + If \`devspace\` is not on PATH afterwards, use ~/.local/bin/devspace (Windows: & "$env:LOCALAPPDATA\\devspace\\bin\\devspace.cmd"). 2. Configure it: devspace init --yes + If it says DevSpace is already configured, keep that setup. 3. Install the subagents skill for yourself: devspace agents skill --install 4. Check it works: devspace agents targets From b646a47311556224ed494bfa558c98a11809e4c9 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:59:47 +0530 Subject: [PATCH 08/10] test(agents-replay): ignore client versions when matching recordings Codex and ACP recordings embed DevSpace's own version in the initialize frame's clientInfo. The v1.1.0 bump made every replay fail on its first frame, so Smoke has failed on main since the release. Versions now compare by type only, like ids and timestamps; protocolVersion is a separate key and stays strict. Co-Authored-By: Claude Opus 5.5 --- test/agents-replay/providers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/agents-replay/providers.ts b/test/agents-replay/providers.ts index 8edd29f4a..9e63fc66d 100644 --- a/test/agents-replay/providers.ts +++ b/test/agents-replay/providers.ts @@ -113,7 +113,9 @@ const jsonRpc: ReplayProtocol = { : "id" in frame ? "response" : "unknown", requestId: (frame: JsonObject) => isJsonString(frame.method) ? frame.id : undefined, responseTo: (frame: JsonObject) => isJsonString(frame.method) ? undefined : frame.id, - volatileKeys: new Set(["id", "sessionId", "threadId", "turnId", "itemId", "timestamp"]), + // `version` is DevSpace's own release in clientInfo (and the provider's in its reply): it changes every + // release and says nothing about the protocol, so a version bump must not invalidate recordings. + volatileKeys: new Set(["id", "sessionId", "threadId", "turnId", "itemId", "timestamp", "version"]), elasticKinds: new Set(), }; From 358e15e0f2bb2c4cd58d0fe800e9dfa5188eda84 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:13:17 +0530 Subject: [PATCH 09/10] fix(cli): tighten init's MCP host checks and examples The /mcp check now runs on the parsed path, so a query or fragment can't smuggle it past validation. The non-interactive hint includes --roots, which a fresh install needs, and the interactive provider prompt is optional when an MCP host is being set up. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 77fbfbf47..0c00dbdb4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -188,7 +188,7 @@ async function ensureConfigured(): Promise { " devspace init", "", "Setup questions can also be answered with flags, for example:", - " devspace init --public-url https://your-tunnel-host.example.com", + " devspace init --public-url https://your-tunnel-host.example.com --roots ~/work", "", "Or provide DEVSPACE_OAUTH_OWNER_TOKEN.", ].join("\n"), @@ -345,7 +345,8 @@ async function promptInitAnswers( : `unavailable: ${provider.reason ?? "provider preflight failed"}`, })), initialValues: defaults.providers, - required: true, + // An MCP host works without subagents, so only a subagents-only setup must pick one. + required: !usesChatGpt(usage), }); if (prompts.isCancel(providerAnswer)) throw new SetupCancelledError(); @@ -1372,9 +1373,14 @@ function validateRequiredPublicBaseUrl(value: string | undefined): string | unde if (!trimmed) return "Enter the public URL from your tunnel or reverse proxy."; - if (trimmed.replace(/\/+$/, "").endsWith("/mcp")) return "Enter the base URL only, without /mcp."; + const invalid = validatePublicBaseUrl(trimmed); + + if (invalid) return invalid; - return validatePublicBaseUrl(trimmed); + // Check the parsed path, since the query and fragment are dropped when the URL is saved. + return new URL(trimmed).pathname.replace(/\/+$/, "").endsWith("/mcp") + ? "Enter the base URL only, without /mcp." + : undefined; } function validatePublicBaseUrl(value: string): string | undefined { From f9d66f42dfcad7ffaeeaf91611528aa4e50f7e9c Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:13:17 +0530 Subject: [PATCH 10/10] fix(website): reannounce repeat copies and give Cursor its own skill step Co-Authored-By: Claude Opus 5.5 --- website/docs/01-quickstart.mdx | 3 ++- website/landing/Shell.astro | 6 +++++- website/landing/content.ts | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/website/docs/01-quickstart.mdx b/website/docs/01-quickstart.mdx index d0a0fdfc9..2da06091d 100644 --- a/website/docs/01-quickstart.mdx +++ b/website/docs/01-quickstart.mdx @@ -87,7 +87,8 @@ coding work to my other coding agents. curl -fsSL https://devspace.run/install | DEVSPACE_NO_INIT=1 sh 2. Configure it: devspace init --yes 3. Install the subagents skill for yourself: - devspace agents skill --install + devspace agents skill --install + (Cursor: devspace agents skill --install cursor --project, from the project root) 4. Check it works: devspace agents targets ``` diff --git a/website/landing/Shell.astro b/website/landing/Shell.astro index 6e550aa5c..cabf15d29 100644 --- a/website/landing/Shell.astro +++ b/website/landing/Shell.astro @@ -103,7 +103,11 @@ const structuredData = { const status = document.querySelector("[data-copy-status]"); - if (status) status.textContent = "Copied to clipboard"; + // Clear first so a repeat copy is a new change that screen readers announce again. + if (status) { + status.textContent = ""; + setTimeout(() => (status.textContent = "Copied to clipboard"), 50); + } }); diff --git a/website/landing/content.ts b/website/landing/content.ts index fa24394ea..2323d982a 100644 --- a/website/landing/content.ts +++ b/website/landing/content.ts @@ -20,7 +20,8 @@ export const AGENT_PROMPT = `Set up DevSpace (https://devspace.run) on this comp If \`devspace\` is not on PATH afterwards, use ~/.local/bin/devspace (Windows: & "$env:LOCALAPPDATA\\devspace\\bin\\devspace.cmd"). 2. Configure it: devspace init --yes If it says DevSpace is already configured, keep that setup. -3. Install the subagents skill for yourself: devspace agents skill --install +3. Install the subagents skill for yourself: devspace agents skill --install + For Cursor, run devspace agents skill --install cursor --project from the project root. 4. Check it works: devspace agents targets Then tell me which agents you can delegate to. Docs: https://devspace.run/docs/quickstart`;