@cargo-ai/cli 1.0.72 → 1.0.73
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/build/commands/auth/email.d.ts +24 -0
- package/build/commands/auth/email.d.ts.map +1 -1
- package/build/commands/auth/email.js +27 -14
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +7 -1
- package/build/commands/auth/workspace.d.ts.map +1 -1
- package/build/commands/auth/workspace.js +99 -26
- package/build/commands/connection/connector.d.ts.map +1 -1
- package/build/commands/connection/connector.js +144 -21
- package/build/commands/start/brief.d.ts +107 -0
- package/build/commands/start/brief.d.ts.map +1 -0
- package/build/commands/start/brief.js +222 -0
- package/build/commands/start/index.d.ts +44 -0
- package/build/commands/start/index.d.ts.map +1 -0
- package/build/commands/start/index.js +431 -0
- package/build/commands/start/state.d.ts +50 -0
- package/build/commands/start/state.d.ts.map +1 -0
- package/build/commands/start/state.js +101 -0
- package/build/config.d.ts +1 -0
- package/build/config.d.ts.map +1 -1
- package/build/config.js +1 -0
- package/build/index.js +50 -6
- package/build/paletteLayout.d.ts +16 -0
- package/build/paletteLayout.d.ts.map +1 -0
- package/build/paletteLayout.js +131 -0
- package/build/setupIssues.d.ts +4 -16
- package/build/setupIssues.d.ts.map +1 -1
- package/build/setupIssues.js +3 -52
- package/package.json +3 -3
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// `cargo-ai start` — and what a bare `cargo-ai` opens when nobody is signed in
|
|
2
|
+
// yet.
|
|
3
|
+
//
|
|
4
|
+
// Open a coding agent, run the guided demo, scaffold a CDK project, explore in
|
|
5
|
+
// the palette, or nothing. Each is a value of `--continue`, so the whole flow
|
|
6
|
+
// runs headless. `onboard` survives as an alias for anything already written
|
|
7
|
+
// down.
|
|
8
|
+
//
|
|
9
|
+
// Three steps and no more: sign in, land in a workspace, choose where to carry
|
|
10
|
+
// on. Everything past that belongs to whatever you chose — `cdk init` asks its
|
|
11
|
+
// own questions about a project, a coding agent asks its own about the work.
|
|
12
|
+
// The flow writes nothing to disk except its own state.
|
|
13
|
+
//
|
|
14
|
+
// Every question is also a flag, so the whole thing runs headless for an agent
|
|
15
|
+
// or a docs snippet, and every step leaves a valid machine behind: cancel after
|
|
16
|
+
// signing in and you are signed in.
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { askText, availableCodingAgents, cdkProjectRoot, COMPANY_CONTEXT_PATH, hasCompanyContext, installedCookbooks, isInteractive, launchBriefed, requireAnswer, selectOne, skillsInstalledFor, } from "@cargo-ai/cdk/cli";
|
|
19
|
+
import { AGENT_SKILLS_NEXT_STEP } from "../../agentSkills.js";
|
|
20
|
+
import { isSignedIn } from "../../setupIssues.js";
|
|
21
|
+
import { parseBooleanOption } from "../../utils/booleanOption.js";
|
|
22
|
+
import { signInWithEmail } from "../auth/email.js";
|
|
23
|
+
import { runOAuthDeviceFlow } from "../auth/oauth.js";
|
|
24
|
+
import { establishSession } from "../auth/session.js";
|
|
25
|
+
import { ExitCodes, failWith, handleApiCall, info, success, } from "../runHandler.js";
|
|
26
|
+
import { buildBrief, buildDemoBrief, CDK_SKILL, DEMO_SKILL, EXPLORE_PATH, formatStartNextSteps, parseDestination, ROUTER_SKILL, } from "./brief.js";
|
|
27
|
+
import { saveStartState } from "./state.js";
|
|
28
|
+
const OAUTH = "oauth";
|
|
29
|
+
const EMAIL = "email";
|
|
30
|
+
/**
|
|
31
|
+
* Where the agent is about to be opened. Three cases, all decided locally —
|
|
32
|
+
* this runs before every handoff, including from the palette, so it must not
|
|
33
|
+
* touch the network.
|
|
34
|
+
*
|
|
35
|
+
* Exported for tests; `go` is the only production caller.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveProjectContext(cwd) {
|
|
38
|
+
const root = cdkProjectRoot(cwd);
|
|
39
|
+
if (root === undefined) {
|
|
40
|
+
return { kind: "none" };
|
|
41
|
+
}
|
|
42
|
+
const companyContextPath = hasCompanyContext(root) === true ? COMPANY_CONTEXT_PATH : undefined;
|
|
43
|
+
const cookbooks = installedCookbooks(root);
|
|
44
|
+
if (cookbooks.length > 0) {
|
|
45
|
+
return { kind: "cookbook", root, companyContextPath, cookbooks };
|
|
46
|
+
}
|
|
47
|
+
return { kind: "cdk", root, companyContextPath };
|
|
48
|
+
}
|
|
49
|
+
export function registerStartCommand(program, getApi, deps) {
|
|
50
|
+
program
|
|
51
|
+
.command("start")
|
|
52
|
+
.alias("onboard")
|
|
53
|
+
.description("Open a coding agent, run the guided demo, or set up a project — signing you in first if needed")
|
|
54
|
+
.option("--email <email>", "Sign in with a code emailed to this address")
|
|
55
|
+
.option("--code <code>", "The emailed code, to finish a sign-in this command already started (`-` reads it from stdin)")
|
|
56
|
+
.option("--oauth [value]", "Sign in via browser", parseBooleanOption)
|
|
57
|
+
.option("--workspace-name <name>", "Workspace to use, created if it does not exist")
|
|
58
|
+
.option("--workspace-uuid <uuid>", "Workspace to use")
|
|
59
|
+
.option("--continue <where>", 'Where to continue: a coding agent ("claude", "cursor", "codex", "gemini"), "code" for a CDK project, "cli" for the palette, "demo" for the guided tour, or "none"')
|
|
60
|
+
.option("--directory <path>", "Directory to scaffold into, for `--continue code`. Required when there is nobody to ask; supplying it also skips the project wizard, the way `cdk init <directory>` does")
|
|
61
|
+
.option("--icp <description>", "Who you sell to, used to brief the guided demo")
|
|
62
|
+
.option("--base-url <url>", "Cargo API base URL")
|
|
63
|
+
.addHelpText("after", `
|
|
64
|
+
This is what a bare \`cargo-ai\` opens for someone who has never signed in, and
|
|
65
|
+
what to run any time after that to open an agent, take the tour, or start a
|
|
66
|
+
project. Signed in already? The sign-in and workspace steps are skipped.
|
|
67
|
+
|
|
68
|
+
Every question has a flag, so the whole flow runs unattended:
|
|
69
|
+
$ cargo-ai start --email you@company.com --workspace-name "Acme GTM" --continue claude
|
|
70
|
+
|
|
71
|
+
Emailed sign-in takes two runs where there is no terminal to type the code
|
|
72
|
+
into. The first sends the code and prints the second, which carries the same
|
|
73
|
+
flags so the destination is not lost:
|
|
74
|
+
$ cargo-ai start --email you@company.com --continue claude --code 123456
|
|
75
|
+
|
|
76
|
+
Already have an API token? \`cargo-ai login --token <token>\` is the whole setup.`)
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
await runStart(getApi, deps, opts);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export async function runStart(getApi, deps, opts) {
|
|
82
|
+
// Checked before anything happens: a mistyped `--continue` is a usage
|
|
83
|
+
// error, and finding it out after a browser sign-in is a poor way to learn.
|
|
84
|
+
const supplied = opts.continue === undefined ? undefined : requireDestination(opts.continue);
|
|
85
|
+
// Same reasoning, one step further. Scaffolding headless needs a directory,
|
|
86
|
+
// and `cdk init` cannot ask for one — so without this the run signs in,
|
|
87
|
+
// saves its state, and *then* dies on a usage error from a child
|
|
88
|
+
// process, having done everything except the thing it was asked to do.
|
|
89
|
+
if (supplied !== undefined &&
|
|
90
|
+
supplied.kind === "code" &&
|
|
91
|
+
opts.directory === undefined &&
|
|
92
|
+
isInteractive() === false) {
|
|
93
|
+
requireAnswer({
|
|
94
|
+
needs: "scaffolding a project needs a directory to scaffold into",
|
|
95
|
+
retryCommand: "cargo-ai start --continue code --directory my-workspace",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
info("");
|
|
99
|
+
info("Cargo — GTM infrastructure your agents can run on.");
|
|
100
|
+
info("");
|
|
101
|
+
const workspace = isSignedIn() === true ? await currentWorkspace(getApi) : await signIn(opts);
|
|
102
|
+
success(`Workspace: ${workspace.name}`);
|
|
103
|
+
const facts = {
|
|
104
|
+
workspace,
|
|
105
|
+
// Decides whether the briefs and the next-steps block tell the reader to
|
|
106
|
+
// go and install the skills. Shared with `ensureSkillsInstalled` so the
|
|
107
|
+
// two cannot disagree about a repo that carries its own copy.
|
|
108
|
+
skillsInstalled: skillsInstalledFor(process.cwd()),
|
|
109
|
+
};
|
|
110
|
+
const destination = supplied !== undefined ? supplied : await askDestination();
|
|
111
|
+
saveStartState({
|
|
112
|
+
version: 1,
|
|
113
|
+
completedAt: new Date().toISOString(),
|
|
114
|
+
workspaceUuid: workspace.uuid,
|
|
115
|
+
continued: destination.kind === "agent" ? destination.agent.bin : destination.kind,
|
|
116
|
+
});
|
|
117
|
+
await go(destination, facts, deps, opts);
|
|
118
|
+
}
|
|
119
|
+
/** Runs the chosen destination. */
|
|
120
|
+
async function go(destination, facts, deps, opts) {
|
|
121
|
+
// A handoff may install the skills on its way past. When one then fails to
|
|
122
|
+
// launch, the block at the bottom of this function is what the user is left
|
|
123
|
+
// with, and reading the facts it came in with told them to install skills
|
|
124
|
+
// that had just been installed.
|
|
125
|
+
let known = facts;
|
|
126
|
+
if (destination.kind === "agent") {
|
|
127
|
+
// Offered before the agent starts, not mentioned in its brief afterwards:
|
|
128
|
+
// an agent that has to install its own instructions spends its first turn
|
|
129
|
+
// on setup, and often just guesses at the CLI instead.
|
|
130
|
+
const cwd = process.cwd();
|
|
131
|
+
// Resolved once and used twice: the context decides which skill the launch
|
|
132
|
+
// loads, and what the brief tells the agent to do once it is open.
|
|
133
|
+
const project = resolveProjectContext(cwd);
|
|
134
|
+
const skill = agentSkill(project);
|
|
135
|
+
const launch = await launchBriefed({
|
|
136
|
+
agent: destination.agent,
|
|
137
|
+
cwd,
|
|
138
|
+
skill,
|
|
139
|
+
brief: (skillsInstalled) => buildBrief({ ...facts, skillsInstalled }, skill, project),
|
|
140
|
+
});
|
|
141
|
+
known = { ...facts, skillsInstalled: launch.skillsInstalled };
|
|
142
|
+
if (launch.launched === true) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (destination.kind === "demo") {
|
|
147
|
+
const demo = await runDemo(facts, opts);
|
|
148
|
+
known = demo.facts;
|
|
149
|
+
if (demo.launched === true) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (destination.kind === "code") {
|
|
154
|
+
scaffoldProject(deps, opts);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (destination.kind === "cli") {
|
|
158
|
+
await deps.openPalette(EXPLORE_PATH);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
info("");
|
|
162
|
+
info(formatStartNextSteps(known));
|
|
163
|
+
if (known.skillsInstalled === false) {
|
|
164
|
+
info("");
|
|
165
|
+
info(AGENT_SKILLS_NEXT_STEP);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The skill to load for a general agent handoff: `cargo-cdk` when inside a
|
|
170
|
+
* CDK project (the user is building infra as code), `cargo` (the router)
|
|
171
|
+
* everywhere else.
|
|
172
|
+
*/
|
|
173
|
+
function agentSkill(project) {
|
|
174
|
+
return project.kind === "none" ? ROUTER_SKILL : CDK_SKILL;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Hand the whole project question to `cdk init`, which owns it: where the
|
|
178
|
+
* project goes, what it starts from, and the company questions worth asking
|
|
179
|
+
* once there is a repo to write the answers into.
|
|
180
|
+
*
|
|
181
|
+
* Returns only when a project was created. Anything else exits, because
|
|
182
|
+
* `start --continue code` is a scripted path, and reporting success over a
|
|
183
|
+
* project that does not exist is the one outcome nothing downstream can
|
|
184
|
+
* recover from.
|
|
185
|
+
*/
|
|
186
|
+
function scaffoldProject(deps, opts) {
|
|
187
|
+
// `init` asks for a directory only when it can; headless it needs one on the
|
|
188
|
+
// command line, which `--directory` is here to forward.
|
|
189
|
+
const scaffold = spawnSync(process.execPath, [
|
|
190
|
+
deps.binPath,
|
|
191
|
+
"cdk",
|
|
192
|
+
"init",
|
|
193
|
+
...(opts.directory === undefined ? [] : [opts.directory]),
|
|
194
|
+
], { stdio: "inherit" });
|
|
195
|
+
if (scaffold.error !== undefined) {
|
|
196
|
+
failWith(`Could not run \`cdk init\`: ${scaffold.error.message}`);
|
|
197
|
+
}
|
|
198
|
+
// The child has already said what went wrong on this terminal, so this
|
|
199
|
+
// carries its code rather than adding a second message. A null status means
|
|
200
|
+
// a signal killed it, which must not read as a scaffolded project either.
|
|
201
|
+
if (scaffold.status !== 0) {
|
|
202
|
+
process.exit(scaffold.status === null ? ExitCodes.GenericError : scaffold.status);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The guided demo: find companies matching an ICP, then the people at them.
|
|
207
|
+
* It runs inside a coding agent rather than as a scripted CLI sequence —
|
|
208
|
+
* sourcing is a conversation about who you sell to, and the agent can price
|
|
209
|
+
* each step and stop when the answer is good enough.
|
|
210
|
+
*
|
|
211
|
+
* Returns whether one started, and the facts as they now stand: this may
|
|
212
|
+
* have installed the skills, which the caller's fallback has to know about.
|
|
213
|
+
*/
|
|
214
|
+
async function runDemo(facts, opts) {
|
|
215
|
+
const agents = availableCodingAgents();
|
|
216
|
+
const icp = opts.icp !== undefined && opts.icp.length > 0
|
|
217
|
+
? opts.icp
|
|
218
|
+
: isInteractive() === false
|
|
219
|
+
? undefined
|
|
220
|
+
: blankToUndefined(await askText({
|
|
221
|
+
message: "Who are you trying to reach? (Enter to skip)",
|
|
222
|
+
placeholder: "heads of growth at Series B SaaS in Europe",
|
|
223
|
+
defaultValue: "",
|
|
224
|
+
// Enter already skips it; Escape must not instead throw away a
|
|
225
|
+
// sign-in and the destination that came with it.
|
|
226
|
+
skippable: true,
|
|
227
|
+
}));
|
|
228
|
+
const agent = agents[0];
|
|
229
|
+
if (agent === undefined) {
|
|
230
|
+
info("");
|
|
231
|
+
info("The demo runs inside a coding agent, and none is installed on this machine.");
|
|
232
|
+
info("Install one (Claude Code, Cursor, Codex, Gemini CLI) and run:");
|
|
233
|
+
info(" cargo-ai start --continue demo");
|
|
234
|
+
return { launched: false, facts };
|
|
235
|
+
}
|
|
236
|
+
const launch = await launchBriefed({
|
|
237
|
+
agent,
|
|
238
|
+
cwd: process.cwd(),
|
|
239
|
+
skill: DEMO_SKILL,
|
|
240
|
+
brief: (skillsInstalled) => buildDemoBrief({ ...facts, skillsInstalled }, icp),
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
launched: launch.launched,
|
|
244
|
+
facts: { ...facts, skillsInstalled: launch.skillsInstalled },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async function currentWorkspace(getApi) {
|
|
248
|
+
const api = getApi();
|
|
249
|
+
const { workspace } = await handleApiCall(() => api.workspaceManagement.workspace.getCurrent());
|
|
250
|
+
return { uuid: workspace.uuid, name: workspace.name };
|
|
251
|
+
}
|
|
252
|
+
async function signIn(opts) {
|
|
253
|
+
const channel = await chooseChannel(opts);
|
|
254
|
+
return channel === EMAIL
|
|
255
|
+
? await signInByEmail(opts)
|
|
256
|
+
: await signInByBrowser(opts);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Sign in with an emailed code, which takes two runs where there is no
|
|
260
|
+
* terminal to type the code into: the first sends it and prints the command
|
|
261
|
+
* that finishes the job — see `resumeCommand` for why that is `start` again
|
|
262
|
+
* rather than `login`.
|
|
263
|
+
*/
|
|
264
|
+
async function signInByEmail(opts) {
|
|
265
|
+
const email = opts.email !== undefined && opts.email.length > 0
|
|
266
|
+
? opts.email
|
|
267
|
+
: await askText({
|
|
268
|
+
message: "Your work email",
|
|
269
|
+
placeholder: "you@company.com",
|
|
270
|
+
defaultValue: "",
|
|
271
|
+
validate: (raw) => raw !== undefined && raw.includes("@") === true
|
|
272
|
+
? undefined
|
|
273
|
+
: "That does not look like an email address.",
|
|
274
|
+
});
|
|
275
|
+
const result = await signInWithEmail({
|
|
276
|
+
email,
|
|
277
|
+
code: opts.code,
|
|
278
|
+
workspaceName: opts.workspaceName,
|
|
279
|
+
workspaceUuid: opts.workspaceUuid,
|
|
280
|
+
baseUrl: opts.baseUrl,
|
|
281
|
+
resumeCommand: resumeCommand(email, opts),
|
|
282
|
+
});
|
|
283
|
+
if (result.outcome === "codeSent") {
|
|
284
|
+
// The code is already sent, so this is a pause rather than a failure.
|
|
285
|
+
info(`Re-run with the code to finish: ${result.next}`);
|
|
286
|
+
process.exit(ExitCodes.Success);
|
|
287
|
+
}
|
|
288
|
+
success(`Signed in as ${result.email}.`);
|
|
289
|
+
return result.workspace;
|
|
290
|
+
}
|
|
291
|
+
async function signInByBrowser(opts) {
|
|
292
|
+
const workspace = await establishSession({
|
|
293
|
+
tokens: await runOAuthDeviceFlow(),
|
|
294
|
+
baseUrlOverride: opts.baseUrl,
|
|
295
|
+
workspaceUuid: opts.workspaceUuid,
|
|
296
|
+
workspaceName: opts.workspaceName,
|
|
297
|
+
});
|
|
298
|
+
success("Signed in.");
|
|
299
|
+
return workspace;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The command that finishes what this run started, for a machine with no
|
|
303
|
+
* terminal to type the emailed code into.
|
|
304
|
+
*
|
|
305
|
+
* It has to be `start` rather than `login`: `login --code` signs the user in
|
|
306
|
+
* and stops, so the destination this run was headed for is lost and re-running
|
|
307
|
+
* `start --email` issues a second code instead of spending the first. Every
|
|
308
|
+
* flag that decided where this was going is carried over.
|
|
309
|
+
*
|
|
310
|
+
* Exported for tests; `signIn` is the only production caller.
|
|
311
|
+
*/
|
|
312
|
+
export function resumeCommand(email, opts) {
|
|
313
|
+
const carried = [
|
|
314
|
+
["--email", email],
|
|
315
|
+
["--continue", opts.continue],
|
|
316
|
+
["--directory", opts.directory],
|
|
317
|
+
["--workspace-name", opts.workspaceName],
|
|
318
|
+
["--workspace-uuid", opts.workspaceUuid],
|
|
319
|
+
["--icp", opts.icp],
|
|
320
|
+
["--base-url", opts.baseUrl],
|
|
321
|
+
];
|
|
322
|
+
const flags = carried
|
|
323
|
+
.filter((entry) => entry[1] !== undefined)
|
|
324
|
+
.map(([flag, value]) => `${flag} ${shellArgument(value)}`);
|
|
325
|
+
// `<code>` is a placeholder for the reader to replace, not a value, so it is
|
|
326
|
+
// not quoted — quoting would read as something to type literally.
|
|
327
|
+
return `cargo-ai start ${flags.join(" ")} --code <code>`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* `value` as one shell argument.
|
|
331
|
+
*
|
|
332
|
+
* This line is printed to be run — copied by a person or relayed by an agent —
|
|
333
|
+
* and every value in it came off a command line the caller controls. Wrapping
|
|
334
|
+
* in double quotes, as this used to, means a value carrying a quote ends the
|
|
335
|
+
* argument early and the rest is read as more command. Single quotes have
|
|
336
|
+
* exactly one escape to handle and no interpolation inside them.
|
|
337
|
+
*
|
|
338
|
+
* Left bare when there is nothing to quote, so the common case still reads as
|
|
339
|
+
* something you would type.
|
|
340
|
+
*/
|
|
341
|
+
function shellArgument(value) {
|
|
342
|
+
// Not `-foo`: bare, the next `cargo-ai` would read it as a flag rather than
|
|
343
|
+
// as this flag's value, and the line is printed to be run.
|
|
344
|
+
if (/^[\w.@:/=-]+$/.test(value) === true && value.startsWith("-") === false) {
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
return `'${value.split("'").join(`'\\''`)}'`;
|
|
348
|
+
}
|
|
349
|
+
async function chooseChannel(opts) {
|
|
350
|
+
if (opts.email !== undefined && opts.email.length > 0) {
|
|
351
|
+
return EMAIL;
|
|
352
|
+
}
|
|
353
|
+
if (opts.oauth === true) {
|
|
354
|
+
return OAUTH;
|
|
355
|
+
}
|
|
356
|
+
if (isInteractive() === false) {
|
|
357
|
+
requireAnswer({
|
|
358
|
+
needs: "`cargo-ai start` needs to know how to sign you in",
|
|
359
|
+
retryCommand: "cargo-ai start --email you@company.com",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return await selectOne({
|
|
363
|
+
message: "How do you want to sign in?",
|
|
364
|
+
recommended: OAUTH,
|
|
365
|
+
choices: [
|
|
366
|
+
{
|
|
367
|
+
value: OAUTH,
|
|
368
|
+
label: "Browser sign-in",
|
|
369
|
+
hint: "opens a browser; nothing to copy",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
value: EMAIL,
|
|
373
|
+
label: "Emailed code",
|
|
374
|
+
hint: "no browser; signs you up on first use",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** `--continue <where>`, or a usage error naming what it accepts. */
|
|
380
|
+
function requireDestination(value) {
|
|
381
|
+
const parsed = parseDestination(value, availableCodingAgents());
|
|
382
|
+
if (parsed === undefined) {
|
|
383
|
+
failWith(`Cannot continue in "${value}". Expected a coding agent installed on this machine, "code", "cli", "demo" or "none".`, { code: ExitCodes.InvalidUsage });
|
|
384
|
+
}
|
|
385
|
+
return parsed;
|
|
386
|
+
}
|
|
387
|
+
async function askDestination() {
|
|
388
|
+
const agents = availableCodingAgents();
|
|
389
|
+
if (isInteractive() === false) {
|
|
390
|
+
return { kind: "none" };
|
|
391
|
+
}
|
|
392
|
+
const chosen = await selectOne({
|
|
393
|
+
message: "What do you want to do?",
|
|
394
|
+
recommended: agents[0] === undefined ? "demo" : agents[0].bin,
|
|
395
|
+
choices: [
|
|
396
|
+
...agents.map((agent) => ({
|
|
397
|
+
value: agent.bin,
|
|
398
|
+
label: `Open ${agent.label}`,
|
|
399
|
+
hint: "briefed on this workspace",
|
|
400
|
+
})),
|
|
401
|
+
{
|
|
402
|
+
value: "cli",
|
|
403
|
+
label: "Explore in the CLI",
|
|
404
|
+
hint: "the action catalog, in the palette",
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
value: "code",
|
|
408
|
+
label: "Set it up as code",
|
|
409
|
+
hint: "scaffold a CDK project with cargo-ai cdk init",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
value: "demo",
|
|
413
|
+
label: "Show me a guided demo",
|
|
414
|
+
hint: "find companies, then the people, matching your ICP",
|
|
415
|
+
},
|
|
416
|
+
{ value: "none", label: "Nothing for now", hint: "print the next steps" },
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
const parsed = parseDestination(chosen, agents);
|
|
420
|
+
if (parsed === undefined) {
|
|
421
|
+
return { kind: "none" };
|
|
422
|
+
}
|
|
423
|
+
return parsed;
|
|
424
|
+
}
|
|
425
|
+
/** Clack returns the empty fallback for a skipped question; absent is undefined. */
|
|
426
|
+
function blankToUndefined(answer) {
|
|
427
|
+
if (answer.trim() === "") {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
return answer.trim();
|
|
431
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type StartState = {
|
|
2
|
+
version: 1;
|
|
3
|
+
completedAt: string;
|
|
4
|
+
workspaceUuid: string;
|
|
5
|
+
/** Where the person chose to continue: an agent bin, or "code"/"cli"/"demo"/"none". */
|
|
6
|
+
continued?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function getStartStatePath(): string;
|
|
9
|
+
export declare function loadStartState(): StartState | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* `value` as state, or undefined when it is not.
|
|
12
|
+
*
|
|
13
|
+
* Checked rather than cast, because the file is on a disk people edit and the
|
|
14
|
+
* cast promised a shape nothing verified. Unknown `version` reads as absent so
|
|
15
|
+
* a future format degrades to "never ran" — the entrance opens once more,
|
|
16
|
+
* which is the harmless direction.
|
|
17
|
+
*
|
|
18
|
+
* Exported for tests; `loadStartState` is the only production caller.
|
|
19
|
+
*/
|
|
20
|
+
export declare function asStartState(value: unknown): StartState | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Best-effort: a machine that cannot write its config directory — a sandbox, a
|
|
23
|
+
* read-only home — has still just been through the flow, and failing the
|
|
24
|
+
* command at the end of a successful one would be the worst possible moment to
|
|
25
|
+
* complain.
|
|
26
|
+
*/
|
|
27
|
+
export declare function saveStartState(state: StartState): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Forget that this machine has been through the flow, so the next bare
|
|
30
|
+
* `cargo-ai` runs it again. Signing out is the one unambiguous "start me over"
|
|
31
|
+
* this CLI has — someone who does it and then types `cargo-ai` is asking for
|
|
32
|
+
* the entrance, not for a palette of commands that all exit 3.
|
|
33
|
+
*/
|
|
34
|
+
export declare function clearStartState(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a bare `cargo-ai` should run `start` rather than open the palette.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately not "are you signed in" alone: the entrance greets a person
|
|
39
|
+
* once. After that, and for anything that is not a person at a terminal, bare
|
|
40
|
+
* `cargo-ai` keeps doing exactly what it did before.
|
|
41
|
+
*
|
|
42
|
+
* Exported for tests; `index.ts` is the only production caller.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shouldRunStart(input: {
|
|
45
|
+
signedIn: boolean;
|
|
46
|
+
interactive: boolean;
|
|
47
|
+
/** Whether the flow has already run on this machine. */
|
|
48
|
+
ranBefore: boolean;
|
|
49
|
+
}): boolean;
|
|
50
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/commands/start/state.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,cAAc,IAAI,UAAU,GAAG,SAAS,CAYvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CA0BnE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAUzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,wDAAwD;IACxD,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAMV"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// What `start` remembers between runs.
|
|
2
|
+
//
|
|
3
|
+
// The file exists to answer one question — "has this machine been through the
|
|
4
|
+
// flow?" — because the alternative signal, "is there a credential?", is wrong
|
|
5
|
+
// in both directions: someone who signs out has still seen the entrance, and
|
|
6
|
+
// someone who has only ever run `cargo-ai login --token` in CI has not.
|
|
7
|
+
//
|
|
8
|
+
// It sits beside the credentials rather than in the repo: this is a fact about
|
|
9
|
+
// a person and their machine, not about a project.
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { getConfigDir } from "@cargo-ai/cdk/cli";
|
|
13
|
+
export function getStartStatePath() {
|
|
14
|
+
return join(getConfigDir(), "start.json");
|
|
15
|
+
}
|
|
16
|
+
export function loadStartState() {
|
|
17
|
+
const path = getStartStatePath();
|
|
18
|
+
if (existsSync(path) === false) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return asStartState(JSON.parse(readFileSync(path, "utf-8")));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `value` as state, or undefined when it is not.
|
|
30
|
+
*
|
|
31
|
+
* Checked rather than cast, because the file is on a disk people edit and the
|
|
32
|
+
* cast promised a shape nothing verified. Unknown `version` reads as absent so
|
|
33
|
+
* a future format degrades to "never ran" — the entrance opens once more,
|
|
34
|
+
* which is the harmless direction.
|
|
35
|
+
*
|
|
36
|
+
* Exported for tests; `loadStartState` is the only production caller.
|
|
37
|
+
*/
|
|
38
|
+
export function asStartState(value) {
|
|
39
|
+
if (value === null || typeof value !== "object") {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const { version, completedAt, workspaceUuid, continued } = value;
|
|
43
|
+
if (version !== 1 ||
|
|
44
|
+
typeof completedAt !== "string" ||
|
|
45
|
+
typeof workspaceUuid !== "string") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: 1,
|
|
50
|
+
completedAt,
|
|
51
|
+
workspaceUuid,
|
|
52
|
+
continued: typeof continued === "string" ? continued : undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Best-effort: a machine that cannot write its config directory — a sandbox, a
|
|
57
|
+
* read-only home — has still just been through the flow, and failing the
|
|
58
|
+
* command at the end of a successful one would be the worst possible moment to
|
|
59
|
+
* complain.
|
|
60
|
+
*/
|
|
61
|
+
export function saveStartState(state) {
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(getConfigDir(), { recursive: true });
|
|
64
|
+
writeFileSync(getStartStatePath(), `${JSON.stringify(state, null, 2)}\n`, {
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Forget that this machine has been through the flow, so the next bare
|
|
75
|
+
* `cargo-ai` runs it again. Signing out is the one unambiguous "start me over"
|
|
76
|
+
* this CLI has — someone who does it and then types `cargo-ai` is asking for
|
|
77
|
+
* the entrance, not for a palette of commands that all exit 3.
|
|
78
|
+
*/
|
|
79
|
+
export function clearStartState() {
|
|
80
|
+
try {
|
|
81
|
+
rmSync(getStartStatePath(), { force: true });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether a bare `cargo-ai` should run `start` rather than open the palette.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately not "are you signed in" alone: the entrance greets a person
|
|
92
|
+
* once. After that, and for anything that is not a person at a terminal, bare
|
|
93
|
+
* `cargo-ai` keeps doing exactly what it did before.
|
|
94
|
+
*
|
|
95
|
+
* Exported for tests; `index.ts` is the only production caller.
|
|
96
|
+
*/
|
|
97
|
+
export function shouldRunStart(input) {
|
|
98
|
+
return (input.interactive === true &&
|
|
99
|
+
input.signedIn === false &&
|
|
100
|
+
input.ranBefore === false);
|
|
101
|
+
}
|
package/build/config.d.ts
CHANGED
package/build/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO7D,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAEvE,MAAM,MAAM,MAAM,GAAG;IACnB;;;OAGG;IACH,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,wBAAgB,SAAS,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO7D,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAEvE,MAAM,MAAM,MAAM,GAAG;IACnB;;;OAGG;IACH,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,wBAAgB,SAAS,IAAI,MAAM,CA2BlC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAAC,IAAI,EAAE;IACtD,qBAAqB,EAAE,OAAO,CAAC;IAC/B,WAAW,EAAE,YAAY,CAAC;IAC1B,wBAAwB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C,GAAG,MAAM,GAAG,SAAS,CA0BrB"}
|