setup-agent 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents.d.ts +33 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +221 -0
- package/dist/agents.js.map +1 -0
- package/dist/cli.d.ts +17 -12
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -61
- package/dist/cli.js.map +1 -1
- package/package.json +12 -2
- package/src/agents.test.ts +137 -0
- package/src/agents.ts +243 -0
- package/src/cli.test.ts +22 -0
- package/src/cli.ts +67 -84
package/src/agents.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A coding agent a person can hand a setup to. `launch` is set when the
|
|
7
|
+
* agent has a terminal command that opens an interactive session with a
|
|
8
|
+
* first prompt; the others are handed the prompt to paste.
|
|
9
|
+
*/
|
|
10
|
+
export type CodingAgent = {
|
|
11
|
+
kind: string;
|
|
12
|
+
name: string;
|
|
13
|
+
launch?: {
|
|
14
|
+
/** Command names to try in order; the first one on PATH runs. */
|
|
15
|
+
commands: readonly string[];
|
|
16
|
+
args: (prompt: string) => string[];
|
|
17
|
+
install: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The coding agents people use most, in the order the picker lists them. */
|
|
22
|
+
export const CODING_AGENTS: readonly CodingAgent[] = [
|
|
23
|
+
{
|
|
24
|
+
kind: "claude",
|
|
25
|
+
name: "Claude Code",
|
|
26
|
+
launch: {
|
|
27
|
+
commands: ["claude"],
|
|
28
|
+
args: (prompt) => [prompt],
|
|
29
|
+
install: "npm install -g @anthropic-ai/claude-code",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: "codex",
|
|
34
|
+
name: "Codex",
|
|
35
|
+
launch: {
|
|
36
|
+
commands: ["codex"],
|
|
37
|
+
args: (prompt) => [prompt],
|
|
38
|
+
install: "npm install -g @openai/codex",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
kind: "cursor",
|
|
43
|
+
name: "Cursor",
|
|
44
|
+
launch: {
|
|
45
|
+
commands: ["cursor-agent", "agent"],
|
|
46
|
+
args: (prompt) => [prompt],
|
|
47
|
+
install: "curl https://cursor.com/install -fsS | bash",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
kind: "gemini",
|
|
52
|
+
name: "Gemini CLI",
|
|
53
|
+
launch: {
|
|
54
|
+
commands: ["gemini"],
|
|
55
|
+
args: (prompt) => ["-i", prompt],
|
|
56
|
+
install: "npm install -g @google/gemini-cli",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: "copilot",
|
|
61
|
+
name: "Copilot CLI",
|
|
62
|
+
launch: {
|
|
63
|
+
commands: ["copilot"],
|
|
64
|
+
args: (prompt) => ["-i", prompt],
|
|
65
|
+
install: "npm install -g @github/copilot",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{ kind: "amp", name: "Amp" },
|
|
69
|
+
{
|
|
70
|
+
kind: "opencode",
|
|
71
|
+
name: "OpenCode",
|
|
72
|
+
launch: {
|
|
73
|
+
commands: ["opencode"],
|
|
74
|
+
args: (prompt) => ["--prompt", prompt],
|
|
75
|
+
install: "npm install -g opencode-ai",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{ kind: "cline", name: "Cline" },
|
|
79
|
+
{ kind: "aider", name: "Aider" },
|
|
80
|
+
{ kind: "goose", name: "Goose" },
|
|
81
|
+
{ kind: "warp", name: "Warp" },
|
|
82
|
+
{ kind: "windsurf", name: "Windsurf" },
|
|
83
|
+
{
|
|
84
|
+
kind: "kiro",
|
|
85
|
+
name: "Kiro",
|
|
86
|
+
launch: {
|
|
87
|
+
commands: ["kiro-cli"],
|
|
88
|
+
args: (prompt) => ["chat", prompt],
|
|
89
|
+
install: "curl -fsSL https://cli.kiro.dev/install | bash",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{ kind: "junie", name: "Junie" },
|
|
93
|
+
{
|
|
94
|
+
kind: "qwen",
|
|
95
|
+
name: "Qwen Code",
|
|
96
|
+
launch: {
|
|
97
|
+
commands: ["qwen"],
|
|
98
|
+
args: (prompt) => ["-i", prompt],
|
|
99
|
+
install: "npm install -g @qwen-code/qwen-code",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{ kind: "roo", name: "Roo Code" },
|
|
103
|
+
{ kind: "zed", name: "Zed" },
|
|
104
|
+
{
|
|
105
|
+
kind: "augment",
|
|
106
|
+
name: "Augment",
|
|
107
|
+
launch: {
|
|
108
|
+
commands: ["auggie"],
|
|
109
|
+
args: (prompt) => [prompt],
|
|
110
|
+
install: "npm install -g @augmentcode/auggie",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
kind: "droid",
|
|
115
|
+
name: "Factory Droid",
|
|
116
|
+
launch: {
|
|
117
|
+
commands: ["droid"],
|
|
118
|
+
args: (prompt) => [prompt],
|
|
119
|
+
install: "curl -fsSL https://app.factory.ai/cli | sh",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{ kind: "crush", name: "Crush" },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/** The variables each agent exports to the shells it runs commands in. */
|
|
126
|
+
const AGENT_ENV: readonly [kind: string, variable: string][] = [
|
|
127
|
+
["claude", "CLAUDECODE"],
|
|
128
|
+
["claude", "CLAUDE_CODE"],
|
|
129
|
+
["codex", "CODEX_SANDBOX"],
|
|
130
|
+
["codex", "CODEX_SANDBOX_NETWORK_DISABLED"],
|
|
131
|
+
["codex", "CODEX_CI"],
|
|
132
|
+
["codex", "CODEX_THREAD_ID"],
|
|
133
|
+
["cursor", "CURSOR_AGENT"],
|
|
134
|
+
["gemini", "GEMINI_CLI"],
|
|
135
|
+
["qwen", "QWEN_CODE"],
|
|
136
|
+
["copilot", "COPILOT_AGENT"],
|
|
137
|
+
["copilot", "COPILOT_CLI"],
|
|
138
|
+
["opencode", "OPENCODE_CLIENT"],
|
|
139
|
+
["augment", "AUGMENT_AGENT"],
|
|
140
|
+
["junie", "JUNIE_DATA"],
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
/** `AI_AGENT` values name the agent first, such as `claude-code_2-1-3_agent` or `github-copilot`. */
|
|
144
|
+
const AI_AGENT_PREFIXES: readonly [prefix: string, kind: string][] = [
|
|
145
|
+
["claude", "claude"],
|
|
146
|
+
["codex", "codex"],
|
|
147
|
+
["cursor", "cursor"],
|
|
148
|
+
["gemini", "gemini"],
|
|
149
|
+
["github-copilot", "copilot"],
|
|
150
|
+
["github_copilot", "copilot"],
|
|
151
|
+
["copilot", "copilot"],
|
|
152
|
+
["opencode", "opencode"],
|
|
153
|
+
["augment", "augment"],
|
|
154
|
+
["auggie", "augment"],
|
|
155
|
+
["qwen", "qwen"],
|
|
156
|
+
["amp", "amp"],
|
|
157
|
+
["goose", "goose"],
|
|
158
|
+
["junie", "junie"],
|
|
159
|
+
["droid", "droid"],
|
|
160
|
+
["factory", "droid"],
|
|
161
|
+
["kiro", "kiro"],
|
|
162
|
+
["cline", "cline"],
|
|
163
|
+
["crush", "crush"],
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/** The coding agent this process runs under, from the variables each one exports to its shell. */
|
|
167
|
+
export const detectAgentKind = (
|
|
168
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
169
|
+
): string | undefined => {
|
|
170
|
+
const named = env.AI_AGENT?.trim().toLowerCase();
|
|
171
|
+
if (named) {
|
|
172
|
+
// The name, or the name followed by a separator and a version or role.
|
|
173
|
+
const match = AI_AGENT_PREFIXES.find(
|
|
174
|
+
([prefix]) =>
|
|
175
|
+
named.startsWith(prefix) &&
|
|
176
|
+
(named.length === prefix.length ||
|
|
177
|
+
/[-_ ./0-9]/.test(named[prefix.length]!)),
|
|
178
|
+
);
|
|
179
|
+
return match?.[1] ?? "other";
|
|
180
|
+
}
|
|
181
|
+
return AGENT_ENV.find(([, variable]) => env[variable])?.[0];
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** True when a person typed the command into a terminal rather than an agent running it from its shell. */
|
|
185
|
+
export const isDirectInvocation = (
|
|
186
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
187
|
+
stdin: { isTTY?: boolean | undefined } = process.stdin,
|
|
188
|
+
stdout: { isTTY?: boolean | undefined } = process.stdout,
|
|
189
|
+
) =>
|
|
190
|
+
detectAgentKind(env) === undefined &&
|
|
191
|
+
stdin.isTTY === true &&
|
|
192
|
+
stdout.isTTY === true;
|
|
193
|
+
|
|
194
|
+
/** The first of `commands` found on PATH, or `undefined`. */
|
|
195
|
+
export const findCommand = (
|
|
196
|
+
commands: readonly string[],
|
|
197
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
198
|
+
): string | undefined => {
|
|
199
|
+
// Empty entries (the working directory) are skipped on purpose: a project
|
|
200
|
+
// must not be able to plant its own `claude` next to the user.
|
|
201
|
+
const directories = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
202
|
+
const extensions =
|
|
203
|
+
process.platform === "win32"
|
|
204
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
|
|
205
|
+
: [""];
|
|
206
|
+
return commands.find((command) =>
|
|
207
|
+
directories.some((directory) =>
|
|
208
|
+
extensions.some((extension) => {
|
|
209
|
+
try {
|
|
210
|
+
accessSync(join(directory, command + extension), constants.X_OK);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Starts `agent` in this terminal with `prompt` as its first message and
|
|
222
|
+
* resolves with its exit code; `undefined` when its command is not installed.
|
|
223
|
+
*/
|
|
224
|
+
export const runCodingAgent = (
|
|
225
|
+
agent: CodingAgent,
|
|
226
|
+
prompt: string,
|
|
227
|
+
): Promise<number | undefined> => {
|
|
228
|
+
const launch = agent.launch;
|
|
229
|
+
if (!launch) return Promise.resolve(undefined);
|
|
230
|
+
const command = findCommand(launch.commands);
|
|
231
|
+
if (command === undefined) return Promise.resolve(undefined);
|
|
232
|
+
const child = spawn(command, launch.args(prompt), { stdio: "inherit" });
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
235
|
+
if (error.code === "ENOENT") resolve(undefined);
|
|
236
|
+
else {
|
|
237
|
+
process.stderr.write(`${error.message}\n`);
|
|
238
|
+
resolve(1);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
child.on("exit", (status) => resolve(status ?? 1));
|
|
242
|
+
});
|
|
243
|
+
};
|
package/src/cli.test.ts
CHANGED
|
@@ -602,6 +602,28 @@ describe("begin planning", () => {
|
|
|
602
602
|
});
|
|
603
603
|
|
|
604
604
|
describe("agent instructions", () => {
|
|
605
|
+
it("goes straight to planning for a setup the agent started", () => {
|
|
606
|
+
const browserUrl =
|
|
607
|
+
"https://www.assistant-ui.com/components/setup#link=abc12345&setup=def67890";
|
|
608
|
+
const started = agentInstructions("https://setup.test/abc12345", {
|
|
609
|
+
startedByAgent: { browserUrl },
|
|
610
|
+
});
|
|
611
|
+
expect(started).not.toContain("click Begin plan");
|
|
612
|
+
expect(started).not.toContain("wait start\nKeep the stream running");
|
|
613
|
+
expect(started).toContain("there is no Begin plan click to wait for");
|
|
614
|
+
expect(started).toContain(browserUrl);
|
|
615
|
+
expect(started).toContain("a setup you opened in the user's browser");
|
|
616
|
+
const opened = agentInstructions("https://setup.test/abc12345");
|
|
617
|
+
expect(opened).toContain("click Begin plan");
|
|
618
|
+
expect(opened).toContain("a setup the user opened in their browser");
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("asks what to set up when the setup has no products", () => {
|
|
622
|
+
expect(agentInstructions("https://setup.test/s")).toContain(
|
|
623
|
+
"If the setup lists no products, first ask the user what to set up",
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
|
|
605
627
|
it("prints a setup URL as one quoted shell word", () => {
|
|
606
628
|
const instructions = agentInstructions("https://checkout.test/a'b;c");
|
|
607
629
|
expect(instructions).toContain(
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CODING_AGENTS,
|
|
3
|
+
detectAgentKind,
|
|
4
|
+
isDirectInvocation,
|
|
5
|
+
runCodingAgent,
|
|
6
|
+
} from "./agents";
|
|
7
|
+
import type { CodingAgent } from "./agents";
|
|
1
8
|
import { connectCheckout } from "./client";
|
|
2
9
|
import type { CheckoutClient } from "./client";
|
|
3
10
|
import { INPUT_PRESETS, isPresetId, presetInput } from "./presets";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
12
|
import { constants } from "node:fs";
|
|
7
13
|
import {
|
|
@@ -113,9 +119,41 @@ const shellWord = (value: string) =>
|
|
|
113
119
|
? value
|
|
114
120
|
: `'${value.replaceAll("'", `'\\''`)}'`;
|
|
115
121
|
|
|
116
|
-
export
|
|
122
|
+
export type AgentInstructionsOptions = {
|
|
123
|
+
/**
|
|
124
|
+
* The agent started this setup with `assistant-ui setup`, which already
|
|
125
|
+
* began planning and opened the setup page in the user's browser; there is
|
|
126
|
+
* no Begin plan click to wait for.
|
|
127
|
+
*/
|
|
128
|
+
startedByAgent?: {
|
|
129
|
+
browserUrl: string;
|
|
130
|
+
/** False when the browser was not opened for the user; the agent then hands them the link. */
|
|
131
|
+
opened?: boolean;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const beginPlanHandoff = (url: string) =>
|
|
136
|
+
`Before planning, check the current setup status. If it is "waiting", tell the user in your own conversation: "I'm connected. Go back to your browser and click Begin plan. You can watch my progress, answer questions, and steer me there." Then run:
|
|
137
|
+
npx setup-agent ${url} wait start
|
|
138
|
+
Keep the stream running while you wait. Do not inspect the project, fetch guides, ask setup questions, or propose a plan until this command returns {"ok":true}. If it returns {"ok":false,"status":"cancelled"}, the user ended this setup before planning: do not plan, tell the user you are connected and waiting for their next setup, and run "wait start" again. Only the user in the browser should start planning; never invoke the browser's start command yourself. "planning.started" also arrives on the stream. If the status is already "planning" or "installing", resume that phase without asking the user to begin again. If it is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.`;
|
|
139
|
+
|
|
140
|
+
const agentStartedHandoff = ({
|
|
141
|
+
browserUrl,
|
|
142
|
+
opened = true,
|
|
143
|
+
}: NonNullable<AgentInstructionsOptions["startedByAgent"]>) =>
|
|
144
|
+
`You started this setup, and it is already planning: there is no Begin plan click to wait for. The browser asks the user to connect to you first; it shows your working directory once your stream is running. Check "status" before planning: if it is "planning" or "installing", go on with that phase. Tell the user in your own conversation, once: ${
|
|
145
|
+
opened
|
|
146
|
+
? `"I opened the assistant-ui setup in your browser. I'll ask my questions there, and you can watch my progress and steer me from that page. If it didn't open, go to ${browserUrl} ."`
|
|
147
|
+
: `"Open ${browserUrl} to follow the assistant-ui setup. I'll ask my questions there, and you can watch my progress and steer me from that page."`
|
|
148
|
+
} If the status is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.`;
|
|
149
|
+
|
|
150
|
+
export const agentInstructions = (
|
|
151
|
+
setupUrl: string,
|
|
152
|
+
options: AgentInstructionsOptions = {},
|
|
153
|
+
) => {
|
|
117
154
|
const url = shellWord(setupUrl);
|
|
118
|
-
|
|
155
|
+
const started = options.startedByAgent;
|
|
156
|
+
return `You are the coding agent for a setup ${started ? "you opened in the user's browser" : "the user opened in their browser"}. The browser shows this setup live; every command below updates it. The user watches the browser, not this terminal: once setup begins, send questions through "ask" so the user can answer in their browser.
|
|
119
157
|
|
|
120
158
|
First, start a persistent Monitor on
|
|
121
159
|
npx setup-agent ${url} stream
|
|
@@ -125,9 +163,7 @@ The user can message you at any time, including while a question is waiting. Rea
|
|
|
125
163
|
|
|
126
164
|
"log" is a message to the user, and the user reads every one: send it only to reply to their message, to tell them something no question or step conveys (a blocker, a deviation from the plan, a decision you made for them), or once as a wrap-up when the install is finished. Findings go in the plan, progress in step notes, questions in "ask"; do not narrate.
|
|
127
165
|
|
|
128
|
-
|
|
129
|
-
npx setup-agent ${url} wait start
|
|
130
|
-
Keep the stream running while you wait. Do not inspect the project, fetch guides, ask setup questions, or propose a plan until this command returns {"ok":true}. Only the user in the browser should start planning; never invoke the browser's start command yourself. "planning.started" also arrives on the stream. If the status is already "planning" or "installing", resume that phase without asking the user to begin again. If it is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.
|
|
166
|
+
${started ? agentStartedHandoff(started) : beginPlanHandoff(url)}
|
|
131
167
|
|
|
132
168
|
The remaining phases are plan, then install.
|
|
133
169
|
|
|
@@ -136,7 +172,7 @@ The remaining phases are plan, then install.
|
|
|
136
172
|
Read the project: its app framework and package manager, the agent framework and model provider it already uses, where components live, any existing chat route. Fetch each product's "guide" URL from the setup below; it is the install reference. Ask for what the project does not decide, using the standard questions so the browser can render them well:
|
|
137
173
|
npx setup-agent ${url} ask --preset framework
|
|
138
174
|
npx setup-agent ${url} ask --preset llm-provider
|
|
139
|
-
The setup's products are starting names and guide references. Explore each product's setup instructions, then discover and plan any other relevant products or dependencies the app needs. Read /catalog.md on the guide URL’s origin to find related products and fetch their setup guides. The starting list does not restrict the implementation or require adding another product to the list. Include relevant additions in the plan for approval.
|
|
175
|
+
The setup's products are starting names and guide references. If the setup lists no products, first ask the user what to set up with a choice question whose options are slugs from https://www.assistant-ui.com/catalog.md, then fetch https://www.assistant-ui.com/install.md?items=<slugs> as the guide. Explore each product's setup instructions, then discover and plan any other relevant products or dependencies the app needs. Read /catalog.md on the guide URL’s origin to find related products and fetch their setup guides. The starting list does not restrict the implementation or require adding another product to the list. Include relevant additions in the plan for approval.
|
|
140
176
|
When the setup puts an assistant in the app, ask how users should access it after discovering the actual app and before proposing the plan. Skip this question when no product's guide installs an assistant (a standalone library or service setup, for example). Use an entry-point question with only 1–3 sensible alternatives. Describe the actual app's pages, workflow and component placement, not generic placeholder copy. Recommend one clear fit when there is one; two options may use the same form factor in different places. Each option needs a stable id, label, contextual description and entryPoint {"formFactor":"modal"|"sidebar"|"full-page","placement":"where it appears","trigger":"how users open it or navigate to it","recommended":true?}. Write the JSON array for this app and send it with:
|
|
141
177
|
npx setup-agent ${url} ask "Where should users access the assistant?" --entry-points '<JSON array>' --wait
|
|
142
178
|
Mark a clear fit with "recommended":true rather than --default, so the user makes the pick. When you ask it, do not submit the plan before the user picks an entry point. Respect the selected placement and trigger in the plan and implementation. Do not ask for a new-versus-existing app setting; discover the app from its files and ask only for an install path when needed.
|
|
@@ -185,6 +221,15 @@ const parseArgs = (argv: readonly string[]): Parsed => {
|
|
|
185
221
|
return { positional, flags };
|
|
186
222
|
};
|
|
187
223
|
|
|
224
|
+
export {
|
|
225
|
+
CODING_AGENTS,
|
|
226
|
+
detectAgentKind,
|
|
227
|
+
findCommand,
|
|
228
|
+
isDirectInvocation,
|
|
229
|
+
runCodingAgent,
|
|
230
|
+
} from "./agents";
|
|
231
|
+
export type { CodingAgent } from "./agents";
|
|
232
|
+
|
|
188
233
|
const flagText = (flags: Parsed["flags"], name: string) => {
|
|
189
234
|
const value = flags.get(name);
|
|
190
235
|
return typeof value === "string" ? value : undefined;
|
|
@@ -199,66 +244,13 @@ const print = (value: unknown) => {
|
|
|
199
244
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
200
245
|
};
|
|
201
246
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
["
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/** The coding agent this process runs under, from the variables each one exports to its shell. */
|
|
211
|
-
export const detectAgentKind = (env: NodeJS.ProcessEnv = process.env) =>
|
|
212
|
-
AGENT_ENV.find(([, variable]) => env[variable])?.[0];
|
|
213
|
-
|
|
214
|
-
type Launcher = {
|
|
215
|
-
kind: string;
|
|
216
|
-
name: string;
|
|
217
|
-
command: string;
|
|
218
|
-
args: (prompt: string) => string[];
|
|
219
|
-
install: string;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const LAUNCHERS: Launcher[] = [
|
|
223
|
-
{
|
|
224
|
-
kind: "claude",
|
|
225
|
-
name: "Claude Code",
|
|
226
|
-
command: "claude",
|
|
227
|
-
args: (prompt) => [prompt],
|
|
228
|
-
install: "npm install -g @anthropic-ai/claude-code",
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
kind: "codex",
|
|
232
|
-
name: "Codex",
|
|
233
|
-
command: "codex",
|
|
234
|
-
args: (prompt) => [prompt],
|
|
235
|
-
install: "npm install -g @openai/codex",
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
kind: "cursor",
|
|
239
|
-
name: "Cursor",
|
|
240
|
-
command: "agent",
|
|
241
|
-
args: (prompt) => [prompt],
|
|
242
|
-
install: "curl https://cursor.com/install -fsS | bash",
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
kind: "gemini",
|
|
246
|
-
name: "Gemini CLI",
|
|
247
|
-
command: "gemini",
|
|
248
|
-
args: (prompt) => ["-i", prompt],
|
|
249
|
-
install: "npm install -g @google/gemini-cli",
|
|
250
|
-
},
|
|
251
|
-
];
|
|
252
|
-
|
|
253
|
-
/** True when a person typed the command into a terminal rather than an agent running it from its shell. */
|
|
254
|
-
export const isDirectInvocation = (
|
|
255
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
256
|
-
stdin: { isTTY?: boolean | undefined } = process.stdin,
|
|
257
|
-
stdout: { isTTY?: boolean | undefined } = process.stdout,
|
|
258
|
-
) =>
|
|
259
|
-
detectAgentKind(env) === undefined &&
|
|
260
|
-
stdin.isTTY === true &&
|
|
261
|
-
stdout.isTTY === true;
|
|
247
|
+
/** The agents `setup-agent <url>` can start itself, for a person who ran it in a terminal. */
|
|
248
|
+
const LAUNCHERS = CODING_AGENTS.filter(
|
|
249
|
+
(
|
|
250
|
+
agent,
|
|
251
|
+
): agent is CodingAgent & { launch: NonNullable<CodingAgent["launch"]> } =>
|
|
252
|
+
agent.launch !== undefined,
|
|
253
|
+
);
|
|
262
254
|
|
|
263
255
|
const chooseLauncher = async () => {
|
|
264
256
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -293,23 +285,14 @@ const launchAgent = async (url: string) => {
|
|
|
293
285
|
const launcher = await chooseLauncher();
|
|
294
286
|
const prompt = `Run \`npx setup-agent ${shellWord(url)}\` and follow the instructions it prints.`;
|
|
295
287
|
process.stdout.write(`\nStarting ${launcher.name}...\n\n`);
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
} else {
|
|
306
|
-
process.stderr.write(`${error.message}\n`);
|
|
307
|
-
}
|
|
308
|
-
resolve(1);
|
|
309
|
-
});
|
|
310
|
-
child.on("exit", (status) => resolve(status ?? 1));
|
|
311
|
-
});
|
|
312
|
-
process.exit(code);
|
|
288
|
+
const code = await runCodingAgent(launcher, prompt);
|
|
289
|
+
if (code === undefined) {
|
|
290
|
+
process.stderr.write(
|
|
291
|
+
`${launcher.name} is not installed. Install it with:\n ${launcher.launch.install}\nthen run this command again.\n`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
// Exit once stdout and stderr have drained.
|
|
295
|
+
process.exitCode = code ?? 1;
|
|
313
296
|
};
|
|
314
297
|
|
|
315
298
|
const requireState = (client: CheckoutClient): Checkout.State =>
|