carrick 0.3.89 → 0.3.91
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/README.md +5 -3
- package/dist/auth/oauth.d.ts +8 -0
- package/dist/auth/oauth.js +65 -11
- package/dist/auth/oauth.js.map +1 -1
- package/dist/init/connect.d.ts +42 -3
- package/dist/init/connect.js +172 -113
- package/dist/init/connect.js.map +1 -1
- package/dist/init/doctor.js +2 -2
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/mcp.d.ts +11 -0
- package/dist/init/mcp.js +14 -8
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/output.d.ts +6 -0
- package/dist/init/output.js +37 -2
- package/dist/init/output.js.map +1 -1
- package/dist/init/projects.d.ts +56 -5
- package/dist/init/projects.js +124 -17
- package/dist/init/projects.js.map +1 -1
- package/dist/init/remove.js +2 -2
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repos.d.ts +33 -0
- package/dist/init/repos.js +8 -1
- package/dist/init/repos.js.map +1 -1
- package/dist/init/run.d.ts +64 -36
- package/dist/init/run.js +268 -166
- package/dist/init/run.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/api.d.ts +13 -1
- package/sidecar/dist/src/capture/check-classify.js +12 -7
- package/sidecar/dist/src/capture/check-probe.d.ts +10 -0
- package/sidecar/dist/src/capture/check-probe.js +21 -3
- package/sidecar/dist/src/capture/check.js +1 -0
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +1 -0
- package/sidecar/dist/src/index.js +37 -9
- package/sidecar/dist/src/retype.d.ts +67 -0
- package/sidecar/dist/src/retype.js +813 -0
- package/sidecar/dist/src/type-inferrer.d.ts +85 -1
- package/sidecar/dist/src/type-inferrer.js +384 -1
- package/sidecar/dist/src/types.d.ts +58 -2
- package/sidecar/dist/src/validators.d.ts +153 -20
- package/sidecar/dist/src/validators.js +17 -0
package/dist/init/run.js
CHANGED
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
// What this prints is one line per thing that happened (carrick#1026). The
|
|
4
4
|
// reasoning behind each step — how the hooks deliver, which editor extension
|
|
5
5
|
// to install, why the scan is left to CI, what a hand-written carrick.json
|
|
6
|
-
// looks like — is the
|
|
7
|
-
// links to
|
|
6
|
+
// looks like — is in the docs, at the `DOCS*` links in `output.ts`, and this
|
|
7
|
+
// command links to them rather than repeating them. The record of the run stays here,
|
|
8
8
|
// because nothing else can state it: this login, this project, these repos,
|
|
9
9
|
// this many packages, this index.
|
|
10
10
|
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { readCredential } from "../auth/credentials.js";
|
|
14
|
+
import { openBrowser } from "../auth/oauth.js";
|
|
13
15
|
import { signIn } from "../auth/run.js";
|
|
14
16
|
import { resolveRepos } from "../auth/read.js";
|
|
15
17
|
import { deriveWorkspace, selectedProposal, selectRepos, writeProposal, PROPOSAL_FILE, repoIdentity, } from "./repos.js";
|
|
16
|
-
import { connectRepos, reposAreInProject,
|
|
18
|
+
import { connectLine, connectRepos, projectAssignments, reposAreInProject, unconnectedRepos, workspaceUrls, } from "./connect.js";
|
|
17
19
|
import { downloadHostedIndex, hostedReport, nativeRunner, STEP_LABEL } from "./hosted.js";
|
|
18
20
|
import { createProject, listProjects, planProject, projectLabel, projectStep, SLUG, } from "./projects.js";
|
|
19
|
-
import { connectMcpClients, mcpLine, offeredFileClients } from "./mcp.js";
|
|
21
|
+
import { claudeCodeFound, connectMcpClients, mcpLine, offeredFileClients } from "./mcp.js";
|
|
20
22
|
import { hookCommand, mergeCarrickHooks, removeCarrickHooks } from "./settings.js";
|
|
21
23
|
import { CODEX_HOOKS_FILE, writeCodexHooks } from "./codex.js";
|
|
22
24
|
import { ignoredSkillRoots, taskSkillLines, writeTaskSkills } from "./task-skills.js";
|
|
@@ -25,7 +27,7 @@ import { findCarrick, offerGlobalInstall } from "../global-install.js";
|
|
|
25
27
|
import { currentVersion } from "../update.js";
|
|
26
28
|
import { writeIfChanged } from "./files.js";
|
|
27
29
|
import { excludedRepos, writeSelection, WORKSPACE_FILE } from "./workspace-file.js";
|
|
28
|
-
import { createOutput,
|
|
30
|
+
import { createOutput, DOCS_EDITOR, DOCS_INDEX, DOCS_INIT_FILES, PromptCancelled } from "./output.js";
|
|
29
31
|
import { renderTemplate, TEMPLATE_PATHS } from "../templates.js";
|
|
30
32
|
/**
|
|
31
33
|
* The repos this run covers that the scaffold tool still has work in.
|
|
@@ -180,7 +182,9 @@ function help() {
|
|
|
180
182
|
" so the agent hooks can run it by name after this run",
|
|
181
183
|
" ends. A terminal is asked instead; --yes is not this",
|
|
182
184
|
"",
|
|
183
|
-
`
|
|
185
|
+
`What init installs, the hooks included: ${DOCS_INIT_FILES}`,
|
|
186
|
+
`CI and a carrick.json written by hand: ${DOCS_INDEX}`,
|
|
187
|
+
`The editor extension: ${DOCS_EDITOR}`,
|
|
184
188
|
].join("\n");
|
|
185
189
|
}
|
|
186
190
|
/**
|
|
@@ -226,61 +230,130 @@ export function absentRepos(identity, names, project, label = (slug) => slug) {
|
|
|
226
230
|
}
|
|
227
231
|
return lines;
|
|
228
232
|
}
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
/** A list as a sentence names it, capped: "a, b and 3 more". */
|
|
234
|
+
function listed(items, cap) {
|
|
235
|
+
if (items.length <= 1)
|
|
236
|
+
return items.join("");
|
|
237
|
+
if (items.length > cap)
|
|
238
|
+
return `${items.slice(0, cap).join(", ")} and ${items.length - cap} more`;
|
|
239
|
+
return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
235
240
|
}
|
|
236
241
|
/**
|
|
237
|
-
*
|
|
242
|
+
* The one line that says what is set up (carrick#1489).
|
|
238
243
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* has no one word for what was found.
|
|
244
|
+
* It used to be nine blocks: the login, the connection, the packages and the
|
|
245
|
+
* proposal path, each hook file, the skills, the MCP clients, the index. The
|
|
246
|
+
* files were named in the question that asked to write them, so what is left
|
|
247
|
+
* to say is the state.
|
|
244
248
|
*/
|
|
245
|
-
export function
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
export function setupLine(state) {
|
|
250
|
+
const subject = state.repos.length === 1 ? state.repos[0] : `${state.repos.length} repos`;
|
|
251
|
+
const where = state.project === null ? state.workspace : `${state.workspace} · project ${state.project}`;
|
|
252
|
+
const parts = [
|
|
253
|
+
...(state.hooks.length > 0 ? [`${state.hooks.join(" and ")} hooks`] : []),
|
|
254
|
+
...(state.skills ? ["task skills"] : []),
|
|
255
|
+
...(state.mcp.length > 0 ? [`MCP in ${state.mcp.join(", ")}`] : []),
|
|
256
|
+
];
|
|
257
|
+
const head = state.repos.length === 0 ? `Set up in ${where}` : `Set up ${subject} in ${where}`;
|
|
258
|
+
return parts.length === 0 ? head : `${head}: ${parts.join(", ")}`;
|
|
251
259
|
}
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
260
|
+
/**
|
|
261
|
+
* The installs a first scan would be refused over, as commands to run.
|
|
262
|
+
*
|
|
263
|
+
* The scanner states them (`not_installed` on each derived repo, from the same
|
|
264
|
+
* preflight rule the scan refuses on), so this only says where each runs, as
|
|
265
|
+
* a path from the workspace the agent is in (carrick#1489).
|
|
266
|
+
*/
|
|
267
|
+
export function installCommands(plan) {
|
|
268
|
+
const commands = [];
|
|
269
|
+
for (const repo of plan.repos) {
|
|
270
|
+
for (const row of repo.not_installed) {
|
|
271
|
+
const where = path.join(path.relative(plan.workspace, repo.path), row.directory);
|
|
272
|
+
const command = where === "." ? `\`${row.command}\`` : `\`${row.command}\` in ${where}`;
|
|
273
|
+
if (!commands.includes(command))
|
|
274
|
+
commands.push(command);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return commands;
|
|
255
278
|
}
|
|
256
279
|
/**
|
|
257
|
-
* The
|
|
280
|
+
* The sentence that installs them, when there is anything to install: an
|
|
281
|
+
* instruction to the agent, or a line to the reader when no instruction is
|
|
282
|
+
* printed.
|
|
283
|
+
*/
|
|
284
|
+
export function installSentence(commands, to) {
|
|
285
|
+
if (commands.length === 0)
|
|
286
|
+
return null;
|
|
287
|
+
return to === "agent"
|
|
288
|
+
? `First install dependencies: run ${listed(commands, 5)}.`
|
|
289
|
+
: `Dependencies are not installed. Run ${listed(commands, 5)} before the next scan.`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* One block per project: the repos in it, each once, with what is about to
|
|
293
|
+
* happen to it (carrick#1489).
|
|
258
294
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
295
|
+
* This replaced a listing that named every repo twice and restated the project
|
|
296
|
+
* the reader had just confirmed. The block header is the project, and a repo
|
|
297
|
+
* row says only what differs from the header: a package count above one, or
|
|
298
|
+
* the project it moves out of.
|
|
263
299
|
*/
|
|
264
|
-
export function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
:
|
|
300
|
+
export function summaryBlocks(input) {
|
|
301
|
+
const groups = new Map();
|
|
302
|
+
const header = (repo) => {
|
|
303
|
+
if (input.project !== null) {
|
|
304
|
+
return input.created !== null ? `New project ${input.created}` : `Project ${input.label(input.project)}`;
|
|
305
|
+
}
|
|
306
|
+
const current = repo.name === null ? null : (input.assigned.get(repo.name) ?? null);
|
|
307
|
+
return current === null ? "No project" : `Project ${input.label(current)}`;
|
|
308
|
+
};
|
|
309
|
+
for (const repo of input.selected) {
|
|
310
|
+
const key = header(repo);
|
|
311
|
+
groups.set(key, [...(groups.get(key) ?? []), repo]);
|
|
312
|
+
}
|
|
313
|
+
const moving = new Set(input.moving.map((name) => name.toLowerCase()));
|
|
314
|
+
const lines = [];
|
|
315
|
+
for (const [title, repos] of groups) {
|
|
316
|
+
lines.push(` ${title}`);
|
|
317
|
+
const width = Math.max(...repos.map((repo) => path.basename(repo.path).length));
|
|
318
|
+
for (const repo of repos.slice(0, 10)) {
|
|
319
|
+
const found = input.plan.repos.find((entry) => entry.path === repo.path);
|
|
320
|
+
const packages = found?.services.length ?? 0;
|
|
321
|
+
const from = repo.name === null ? null : (input.assigned.get(repo.name) ?? null);
|
|
322
|
+
const notes = [
|
|
323
|
+
repo.name ?? "no GitHub repo",
|
|
324
|
+
...(packages > 1 ? [`${packages} packages`] : []),
|
|
325
|
+
...(repo.name !== null && moving.has(repo.name.toLowerCase()) && from !== null
|
|
326
|
+
? [`moves from ${input.label(from)}`]
|
|
327
|
+
: []),
|
|
328
|
+
];
|
|
329
|
+
lines.push(` ${path.basename(repo.path).padEnd(width)} ${notes.join(" ")}`);
|
|
330
|
+
}
|
|
331
|
+
if (repos.length > 10)
|
|
332
|
+
lines.push(` and ${repos.length - 10} more`);
|
|
333
|
+
}
|
|
334
|
+
return lines;
|
|
271
335
|
}
|
|
272
336
|
/**
|
|
273
|
-
*
|
|
337
|
+
* The files the write step creates, by path, for the question that asks to
|
|
338
|
+
* write them (carrick#1489).
|
|
274
339
|
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
340
|
+
* Directories inside the workspace, because each holds several files and the
|
|
341
|
+
* list has to fit one line; every file outside it by its own path, because
|
|
342
|
+
* those belong to somebody's editor. Claude Code's server list is written by
|
|
343
|
+
* its own command, so it is named rather than given a path.
|
|
279
344
|
*/
|
|
280
|
-
export function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
345
|
+
export function writesLine(options) {
|
|
346
|
+
const home = options.home ?? os.homedir();
|
|
347
|
+
const outside = options.editorFiles.map((file) => file.startsWith(`${home}${path.sep}`) ? `~${file.slice(home.length)}` : file);
|
|
348
|
+
return `Writes: ${[
|
|
349
|
+
".carrick/",
|
|
350
|
+
".claude/",
|
|
351
|
+
".agents/",
|
|
352
|
+
".codex/",
|
|
353
|
+
...(options.workspaceFile ? [WORKSPACE_FILE] : []),
|
|
354
|
+
...outside,
|
|
355
|
+
...(options.claudeCode ? ["Claude Code's MCP servers"] : []),
|
|
356
|
+
].join(", ")}`;
|
|
284
357
|
}
|
|
285
358
|
/**
|
|
286
359
|
* An end to the run with its own exit code, decided before anything is written.
|
|
@@ -403,7 +476,7 @@ export async function chooseRepos(plan, candidates, options, out) {
|
|
|
403
476
|
throw new Error(`${plan.workspace} holds ${candidates.length} repos and there is no terminal to choose in. Name the ones this install covers with --repo owner/repo (repeatable), or add --yes to cover all ${candidates.length}.`);
|
|
404
477
|
}
|
|
405
478
|
const { keep, reasons } = preselectedRepos(candidates, options.identity ?? null, options.label);
|
|
406
|
-
const kept = new Set(await out.choose("Which repos
|
|
479
|
+
const kept = new Set(await out.choose("Which repos should Carrick index?", "a repo", repoChoices(plan, candidates, reasons), { initial: keep, required: true }));
|
|
407
480
|
return candidates.filter((repo) => kept.has(repo.path));
|
|
408
481
|
}
|
|
409
482
|
/**
|
|
@@ -470,31 +543,6 @@ export async function chooseEditors(offered, options, out) {
|
|
|
470
543
|
hint: client.installed ? client.file : `${client.file}, not detected on this machine`,
|
|
471
544
|
})), { initial: offered.filter((client) => client.installed).map((client) => client.name), required: false });
|
|
472
545
|
}
|
|
473
|
-
/** What the proposal question adds when an editor file is part of the answer. */
|
|
474
|
-
export function editorClause(offered, chosen) {
|
|
475
|
-
const files = offered.filter((client) => chosen.includes(client.name)).map((client) => client.file);
|
|
476
|
-
if (files.length === 0)
|
|
477
|
-
return "";
|
|
478
|
-
return `, and add Carrick to ${files.join(" and ")}`;
|
|
479
|
-
}
|
|
480
|
-
/** The repos this install covers, one line each, for the proposal. */
|
|
481
|
-
export function coverageLines(plan, selected) {
|
|
482
|
-
return selected.slice(0, 10).map((repo) => {
|
|
483
|
-
const found = plan.repos.find((entry) => entry.path === repo.path);
|
|
484
|
-
const packages = found?.services.length ?? 0;
|
|
485
|
-
return ` ${repo.name ?? path.basename(repo.path)} ${packages} package${packages === 1 ? "" : "s"}`;
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* What the server would be asked to change, said before it is asked.
|
|
490
|
-
*
|
|
491
|
-
* A move is named with the project it comes OUT of, display name and slug
|
|
492
|
-
* both, because that is the project whose agents stop seeing this repo and
|
|
493
|
-
* the one a reader has to find again to undo it (carrick#1338).
|
|
494
|
-
*/
|
|
495
|
-
export function moveLines(moving, from, target, label) {
|
|
496
|
-
return moving.map((name) => ` ${name} will move from ${label(from(name))} to ${label(target)}`);
|
|
497
|
-
}
|
|
498
546
|
export async function init(argv) {
|
|
499
547
|
return initWith(argv, createOutput(), process.stdin.isTTY === true);
|
|
500
548
|
}
|
|
@@ -534,6 +582,10 @@ export async function initWith(argv, out, interactive) {
|
|
|
534
582
|
let decision;
|
|
535
583
|
// The repos this run has permission to take out of another project.
|
|
536
584
|
const movable = new Set();
|
|
585
|
+
// The ones among them that sit in a project somebody chose, at the read the
|
|
586
|
+
// proposal was built from. Their move is said; a repo the App grant has
|
|
587
|
+
// just put in the default project is not (carrick#1489).
|
|
588
|
+
const announced = new Set();
|
|
537
589
|
// The directory names this run leaves out, for the workspace file below.
|
|
538
590
|
let deselected = [];
|
|
539
591
|
// The editors outside this workspace the answer covers, settled before it.
|
|
@@ -666,7 +718,16 @@ export async function initWith(argv, out, interactive) {
|
|
|
666
718
|
// would do, and the proposal below is where that is accepted. Without
|
|
667
719
|
// --project the step reads the assignment the repos already have and
|
|
668
720
|
// offers the list, rather than doing nothing at all (carrick#987).
|
|
669
|
-
const
|
|
721
|
+
const dashboard = workspaceUrls(initial.workspace.slug).projects;
|
|
722
|
+
const prompts = {
|
|
723
|
+
say: out.say,
|
|
724
|
+
ask: out.ask,
|
|
725
|
+
confirm: out.confirm,
|
|
726
|
+
pick: out.pick,
|
|
727
|
+
dashboard: { url: dashboard, open: (url) => void openBrowser(url).catch(() => false) },
|
|
728
|
+
interactive,
|
|
729
|
+
assumeYes: parsed.assumeYes,
|
|
730
|
+
};
|
|
670
731
|
decision =
|
|
671
732
|
parsed.project === null
|
|
672
733
|
? await projectStep(projectAssignments(initial, names), projects, prompts)
|
|
@@ -705,22 +766,6 @@ export async function initWith(argv, out, interactive) {
|
|
|
705
766
|
out.warn(`${warnings.length - 3} more notes on the proposal are in ${PROPOSAL_FILE}.`);
|
|
706
767
|
for (const missing of plan.missing)
|
|
707
768
|
out.warn(`Missing workspace override: ${missing}`);
|
|
708
|
-
if (selected.length > 1) {
|
|
709
|
-
out.say("This install covers:");
|
|
710
|
-
for (const line of coverageLines(plan, selected))
|
|
711
|
-
out.say(line);
|
|
712
|
-
if (selected.length > 10)
|
|
713
|
-
out.say(` and ${selected.length - 10} more`);
|
|
714
|
-
}
|
|
715
|
-
if (project !== null) {
|
|
716
|
-
for (const line of moveLines(moving, (name) => assigned.get(name) ?? "", project, label))
|
|
717
|
-
out.say(line);
|
|
718
|
-
if (joining.length > 0) {
|
|
719
|
-
out.say(` ${joining.join(", ")} will be placed in project ${label(project)} once the browser connects ${joining.length === 1 ? "it" : "them"}`);
|
|
720
|
-
}
|
|
721
|
-
if (decision.create)
|
|
722
|
-
out.say(` Project "${project}" will be created in this workspace`);
|
|
723
|
-
}
|
|
724
769
|
// Which editors get an entry, asked before the yes that covers it. The
|
|
725
770
|
// files are outside the workspace and belong to editors this reader may not
|
|
726
771
|
// use, and they used to be written on the strength of a yes to the proposal
|
|
@@ -728,17 +773,33 @@ export async function initWith(argv, out, interactive) {
|
|
|
728
773
|
// and one this machine has no command for is offered unticked.
|
|
729
774
|
const offered = offeredFileClients();
|
|
730
775
|
editors = await chooseEditors(offered, { editors: parsed.editors, assumeYes: parsed.assumeYes, interactive }, out);
|
|
731
|
-
|
|
776
|
+
// The summary the yes is given to: one block per project, each repo once,
|
|
777
|
+
// the one browser step on its own line with its link, and the files by
|
|
778
|
+
// path (carrick#1489).
|
|
779
|
+
const created = decision.create && project !== null
|
|
780
|
+
? decision.name !== undefined && decision.name !== project
|
|
781
|
+
? `${decision.name} (${project})`
|
|
782
|
+
: project
|
|
783
|
+
: null;
|
|
784
|
+
for (const line of summaryBlocks({ plan, selected, project, created, label, assigned, moving }))
|
|
785
|
+
out.say(line);
|
|
786
|
+
const unconnected = unconnectedRepos(initial, names);
|
|
787
|
+
if (unconnected.length > 0) {
|
|
788
|
+
out.warn(connectLine(unconnected, names.length, workspaceUrls(initial.workspace.slug).connect));
|
|
789
|
+
}
|
|
790
|
+
out.say(writesLine({
|
|
791
|
+
workspaceFile: deselected.length > 0,
|
|
792
|
+
editorFiles: offered.filter((client) => editors.includes(client.name)).map((client) => client.file),
|
|
793
|
+
claudeCode: claudeCodeFound(),
|
|
794
|
+
}));
|
|
732
795
|
if (!parsed.assumeYes) {
|
|
733
796
|
if (!interactive) {
|
|
734
797
|
throw new Stop(1, `use --yes to accept this proposal without a terminal${moving.length > 0 && !parsed.allowMove ? ", and --allow-move to accept the move above" : ""}.`);
|
|
735
798
|
}
|
|
736
|
-
// The
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
const alsoEditors = editorClause(offered, editors);
|
|
741
|
-
if (!(await out.confirm(`Write the proposal for ${subject} and configure hooks${alsoEditors}?`))) {
|
|
799
|
+
// The files are named on the line above it, editor files included,
|
|
800
|
+
// because a question that does not name a file is not consent to write
|
|
801
|
+
// it (carrick#1365, carrick#1489).
|
|
802
|
+
if (!(await out.confirm("Write these files?"))) {
|
|
742
803
|
out.refuse(NOTHING_WRITTEN);
|
|
743
804
|
throw new Stop(0);
|
|
744
805
|
}
|
|
@@ -759,6 +820,8 @@ export async function initWith(argv, out, interactive) {
|
|
|
759
820
|
}
|
|
760
821
|
for (const name of [...moving, ...joining])
|
|
761
822
|
movable.add(name.toLowerCase());
|
|
823
|
+
for (const name of moving)
|
|
824
|
+
announced.add(name.toLowerCase());
|
|
762
825
|
}
|
|
763
826
|
catch (error) {
|
|
764
827
|
// A question the reader ended stops the run where it stands. Nothing above
|
|
@@ -780,59 +843,27 @@ export async function initWith(argv, out, interactive) {
|
|
|
780
843
|
const plan = derived.plan;
|
|
781
844
|
const project = decision.slug;
|
|
782
845
|
const label = (slug) => projectLabel(slug, projects);
|
|
846
|
+
// What the reader still has to do, said once at the end rather than between
|
|
847
|
+
// the steps (carrick#1489).
|
|
848
|
+
const todo = [];
|
|
849
|
+
let workspaceSlug = initial.workspace.slug;
|
|
850
|
+
let projectExists = decision.exists;
|
|
783
851
|
try {
|
|
784
|
-
let projectExists = decision.exists;
|
|
785
852
|
if (decision.create && project !== null) {
|
|
786
|
-
const outcome = await createProject(credential.token, project);
|
|
853
|
+
const outcome = await createProject(credential.token, project, decision.name ?? project);
|
|
787
854
|
if (outcome.kind === "created") {
|
|
788
|
-
|
|
855
|
+
// Known by its name from here on, the same as a listed one.
|
|
856
|
+
projects = [...(projects ?? []), outcome.project];
|
|
857
|
+
// A workspace's first project takes the repos the GitHub App install
|
|
858
|
+
// staged (carrick-cloud#1359); any other starts empty.
|
|
859
|
+
const holds = outcome.project.repo_count;
|
|
860
|
+
out.done(`Created project ${label(project)}${holds > 0 ? ` with ${holds} repo${holds === 1 ? "" : "s"}` : ""}`);
|
|
789
861
|
projectExists = true;
|
|
790
862
|
}
|
|
791
863
|
else if (outcome.kind === "refused") {
|
|
792
|
-
out.
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const identity = await connectRepos(credential.token, names, initial, {
|
|
796
|
-
interactive,
|
|
797
|
-
project: project ?? undefined,
|
|
798
|
-
projectExists,
|
|
799
|
-
movable,
|
|
800
|
-
label,
|
|
801
|
-
say: out.say,
|
|
802
|
-
});
|
|
803
|
-
// Who this machine is, and where its answers come from. The workspace slug
|
|
804
|
-
// is the login: nothing on the credential or in the workspace read names a
|
|
805
|
-
// user (carrick#1026).
|
|
806
|
-
out.done(project === null
|
|
807
|
-
? `Signed in as ${identity.workspace.slug}`
|
|
808
|
-
: `Signed in as ${identity.workspace.slug} · project ${label(project)}`);
|
|
809
|
-
if (project !== null && !reposAreInProject(identity, names, project)) {
|
|
810
|
-
// An unverified project does not end the run, whether it was named on
|
|
811
|
-
// the command line or picked here. Stopping cost someone their hooks and
|
|
812
|
-
// their proposal for a browser step they could only take afterwards, and
|
|
813
|
-
// the documented command then needed two runs (carrick#993 row 8).
|
|
814
|
-
out.warn(`Finish the browser steps above to put these repos in ${label(project)}, then run carrick init --project ${project} again to verify.`);
|
|
815
|
-
}
|
|
816
|
-
if (identity.allowance_sentence)
|
|
817
|
-
out.say(identity.allowance_sentence);
|
|
818
|
-
const connected = [];
|
|
819
|
-
for (const repo of identity.repos) {
|
|
820
|
-
if (!repo.connected)
|
|
821
|
-
out.warn(`${repo.full_name} is not connected to this Carrick workspace.`);
|
|
822
|
-
else {
|
|
823
|
-
connected.push(repo.full_name);
|
|
824
|
-
if (repo.services.length > 0)
|
|
825
|
-
hostedIndex.push(repo.full_name);
|
|
864
|
+
out.warn(`Carrick did not create ${label(project)}: ${outcome.message}`);
|
|
826
865
|
}
|
|
827
866
|
}
|
|
828
|
-
if (connected.length > 0)
|
|
829
|
-
out.done(connectedLine(connected));
|
|
830
|
-
// The rest of the project, which this machine does not hold. Carrick
|
|
831
|
-
// answers across every repo in a project, so a folder holding half of one
|
|
832
|
-
// is a partial index and nothing here would otherwise say so
|
|
833
|
-
// (carrick#993 row 18).
|
|
834
|
-
for (const line of absentRepos(identity, names, project, label))
|
|
835
|
-
out.warn(line);
|
|
836
867
|
}
|
|
837
868
|
catch (error) {
|
|
838
869
|
process.stderr.write(`carrick init: ${error.message}\n`);
|
|
@@ -843,16 +874,14 @@ export async function initWith(argv, out, interactive) {
|
|
|
843
874
|
// without a model is written into the repository, because the first scan
|
|
844
875
|
// is the paid one and it has to run against a config someone has read
|
|
845
876
|
// (carrick-cloud#799).
|
|
877
|
+
// Both files were named in the question that asked to write them, so
|
|
878
|
+
// neither gets a line of its own (carrick#1489).
|
|
846
879
|
writeProposal(plan.workspace, derived);
|
|
847
|
-
out.done(packagesLine(plan));
|
|
848
880
|
// And the selection itself, in the file every later command reads. Without
|
|
849
881
|
// it the choice lasted one run: `carrick refresh` and `carrick index` walk
|
|
850
882
|
// the folder, the editor hook answers for a file inside a repo nobody
|
|
851
883
|
// covers, and the next `init` asks again (carrick#1344).
|
|
852
|
-
|
|
853
|
-
if (selection !== null && selection.added.length > 0) {
|
|
854
|
-
out.done(`${selection.added.join(", ")} excluded in ${WORKSPACE_FILE}, so the scans, the hooks and the next run leave ${selection.added.length === 1 ? "it" : "them"} alone`);
|
|
855
|
-
}
|
|
884
|
+
writeSelection(plan.workspace, deselected);
|
|
856
885
|
}
|
|
857
886
|
catch (error) {
|
|
858
887
|
process.stderr.write(`carrick init: ${error.message}\n`);
|
|
@@ -908,10 +937,12 @@ export async function initWith(argv, out, interactive) {
|
|
|
908
937
|
// from. Written for both hosts for the same reason the task skills are
|
|
909
938
|
// (carrick#1335): which agent opens this workspace is not a thing init can
|
|
910
939
|
// know, and a hook file for a host nobody runs costs nothing.
|
|
940
|
+
const hosts = hooksWritten ? ["Claude Code"] : [];
|
|
911
941
|
try {
|
|
912
942
|
if (writeCodexHooks(workspace, command.command) === "written") {
|
|
913
|
-
|
|
943
|
+
todo.push("Codex asks you to trust the Carrick hooks the next time it starts; until you do, they do not run.");
|
|
914
944
|
}
|
|
945
|
+
hosts.push("Codex");
|
|
915
946
|
}
|
|
916
947
|
catch (error) {
|
|
917
948
|
out.refuse(`Could not configure the Codex hooks: ${error.message}. Fix ${CODEX_HOOKS_FILE} and run carrick init again.`);
|
|
@@ -920,12 +951,11 @@ export async function initWith(argv, out, interactive) {
|
|
|
920
951
|
// somebody hand-edited into invalid JSON is no reason to withhold the bodies
|
|
921
952
|
// an agent reads. A file this package did not write, or one somebody has
|
|
922
953
|
// since changed, is left alone and named.
|
|
954
|
+
let skills = false;
|
|
923
955
|
try {
|
|
924
956
|
const { done, warn } = taskSkillLines(writeTaskSkills(workspace, { slug: projectSlug }));
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
for (const line of warn)
|
|
928
|
-
out.warn(line);
|
|
957
|
+
skills = done.length > 0;
|
|
958
|
+
todo.push(...warn);
|
|
929
959
|
// Which build wrote them, so a hook running an older `carrick` than this
|
|
930
960
|
// one says so instead of answering from files it does not match
|
|
931
961
|
// (carrick#1372).
|
|
@@ -933,25 +963,88 @@ export async function initWith(argv, out, interactive) {
|
|
|
933
963
|
const ignored = ignoredSkillRoots(workspace);
|
|
934
964
|
if (ignored.length > 0) {
|
|
935
965
|
const one = ignored.length === 1;
|
|
936
|
-
|
|
966
|
+
todo.push(`${ignored.join(" and ")} ${one ? "is" : "are"} git-ignored here, so these skills stay on this machine. Track ${one ? "it" : "them"} to give the rest of the team the same answers.`);
|
|
937
967
|
}
|
|
938
968
|
}
|
|
939
969
|
catch (error) {
|
|
940
970
|
out.refuse(`Could not write the task skills: ${error.message}`);
|
|
941
971
|
}
|
|
942
972
|
const mcp = connectMcpClients(editors);
|
|
943
|
-
if (hooksWritten)
|
|
944
|
-
out.done(configuredLine(mcp));
|
|
945
|
-
for (const line of mcpClientLines(mcp))
|
|
946
|
-
out.done(line);
|
|
947
973
|
// Nothing here about `carrick` not being on PATH: the offer above said it, in
|
|
948
974
|
// front of the decision it changes, and with the command for this machine's
|
|
949
975
|
// package manager rather than a guess at npm (carrick#1372).
|
|
976
|
+
if (mcp.some((outcome) => outcome.client === "Claude Code" && outcome.state === "written")) {
|
|
977
|
+
todo.push("Restart Claude Code to load the Carrick MCP server.");
|
|
978
|
+
}
|
|
950
979
|
for (const outcome of mcp.filter((entry) => entry.state === "failed")) {
|
|
951
|
-
|
|
980
|
+
todo.push(`MCP not configured for ${outcome.client}: ${outcome.detail}`);
|
|
981
|
+
}
|
|
982
|
+
// Said as what is missing, not as "no agent client": the hooks above were
|
|
983
|
+
// written for two of them (carrick#1489).
|
|
984
|
+
if (!mcp.some((outcome) => outcome.client === "Claude Code")) {
|
|
985
|
+
todo.push(`No claude command on this machine, so Claude Code has no Carrick MCP server. Once it is installed: ${mcpLine()}`);
|
|
952
986
|
}
|
|
953
|
-
|
|
954
|
-
|
|
987
|
+
// The connection, last of the steps that talk to Carrick, so a read that
|
|
988
|
+
// fails costs nothing already decided: the files above are written
|
|
989
|
+
// (carrick#1489 review).
|
|
990
|
+
//
|
|
991
|
+
// Read again, now. The read the questions were answered from can be minutes
|
|
992
|
+
// old — the App is often installed in another tab while init waits at a
|
|
993
|
+
// prompt — and a first project may have just taken the repos. The
|
|
994
|
+
// connection lines used to come from the old read, and sent a reader to the
|
|
995
|
+
// grant page for repos that were already connected (carrick#1489).
|
|
996
|
+
try {
|
|
997
|
+
const current = await resolveRepos(credential.token, names);
|
|
998
|
+
const identity = await connectRepos(credential.token, names, current, {
|
|
999
|
+
interactive,
|
|
1000
|
+
project: project ?? undefined,
|
|
1001
|
+
projectExists,
|
|
1002
|
+
movable,
|
|
1003
|
+
announce: announced,
|
|
1004
|
+
label,
|
|
1005
|
+
say: out.say,
|
|
1006
|
+
track: (text, work, report) => out.step(text, work, report),
|
|
1007
|
+
});
|
|
1008
|
+
workspaceSlug = identity.workspace.slug;
|
|
1009
|
+
if (project !== null && !reposAreInProject(identity, names, project)) {
|
|
1010
|
+
// An unverified project does not end the run, whether it was named on
|
|
1011
|
+
// the command line or picked here. Stopping cost someone their hooks and
|
|
1012
|
+
// their proposal for a browser step they could only take afterwards, and
|
|
1013
|
+
// the documented command then needed two runs (carrick#993 row 8).
|
|
1014
|
+
todo.push(`Finish the browser steps above to put these repos in ${label(project)}, then run carrick init --project ${project} again to verify.`);
|
|
1015
|
+
}
|
|
1016
|
+
// After a wait that was stopped, the grant is still the thing to do, and
|
|
1017
|
+
// the line that said so is above the wait.
|
|
1018
|
+
const unconnected = unconnectedRepos(identity, names);
|
|
1019
|
+
if (interactive && unconnected.length > 0) {
|
|
1020
|
+
todo.push(connectLine(unconnected, names.length, workspaceUrls(identity.workspace.slug).connect));
|
|
1021
|
+
}
|
|
1022
|
+
if (identity.allowance_sentence)
|
|
1023
|
+
out.say(identity.allowance_sentence);
|
|
1024
|
+
for (const repo of identity.repos) {
|
|
1025
|
+
if (repo.connected && repo.services.length > 0)
|
|
1026
|
+
hostedIndex.push(repo.full_name);
|
|
1027
|
+
}
|
|
1028
|
+
// The rest of the project, which this machine does not hold. Carrick
|
|
1029
|
+
// answers across every repo in a project, so a folder holding half of one
|
|
1030
|
+
// is a partial index and nothing here would otherwise say so
|
|
1031
|
+
// (carrick#993 row 18).
|
|
1032
|
+
todo.push(...absentRepos(identity, names, project, label));
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
// The server's own sentence says what failed; this one says what is left.
|
|
1036
|
+
todo.push(`${error.message} The files here are written; run carrick init again to verify the connection and the project.`);
|
|
1037
|
+
}
|
|
1038
|
+
// What is set up, in one line; then the index; then what is left to do;
|
|
1039
|
+
// then the instruction for the agent (carrick#1489).
|
|
1040
|
+
out.done(setupLine({
|
|
1041
|
+
repos: selectedRepos.map((repo) => repo.name ?? path.basename(repo.path)),
|
|
1042
|
+
workspace: workspaceSlug,
|
|
1043
|
+
project: project === null ? null : label(project),
|
|
1044
|
+
hooks: hosts,
|
|
1045
|
+
skills,
|
|
1046
|
+
mcp: mcp.filter((outcome) => outcome.state !== "failed").map((outcome) => outcome.client),
|
|
1047
|
+
}));
|
|
955
1048
|
// No paid scan ran here, and that is the point (carrick-cloud#799): the one
|
|
956
1049
|
// scan runs against a config someone has read. Where CI has already built an
|
|
957
1050
|
// index, this run reads it onto the machine instead — the hosted index is
|
|
@@ -969,21 +1062,30 @@ export async function initWith(argv, out, interactive) {
|
|
|
969
1062
|
const startedAt = Date.now();
|
|
970
1063
|
await out.step(STEP_LABEL, (progress) => downloadHostedIndex(plan.workspace, nativeRunner(out.quiet, progress), plan.repos.map((repo) => repo.path), progress), (outcome) => hostedReport(outcome, (Date.now() - startedAt) / 1000));
|
|
971
1064
|
}
|
|
972
|
-
|
|
973
|
-
out.
|
|
974
|
-
}
|
|
1065
|
+
for (const line of todo)
|
|
1066
|
+
out.warn(line);
|
|
975
1067
|
// The scaffold tool writes the files a repo needs; a repo that has them needs
|
|
976
1068
|
// nothing pasted anywhere. The line used to close every run, including one
|
|
977
1069
|
// where every covered repo already had its config and its workflow, and
|
|
978
1070
|
// "this repo" named nothing in a folder of several (carrick#1365).
|
|
1071
|
+
//
|
|
1072
|
+
// An install the first scan would be refused over goes in front of it: the
|
|
1073
|
+
// agent otherwise found out from the refusal (carrick#1489).
|
|
1074
|
+
const installs = installCommands(plan);
|
|
979
1075
|
const unscaffolded = reposToScaffold(selectedRepos);
|
|
980
1076
|
if (unscaffolded.length > 0) {
|
|
981
|
-
|
|
1077
|
+
const install = installSentence(installs, "agent");
|
|
1078
|
+
out.note("Next: paste this to your agent", [
|
|
1079
|
+
...(install === null ? [] : [install]),
|
|
1080
|
+
scaffoldSentence(unscaffolded),
|
|
1081
|
+
]);
|
|
982
1082
|
}
|
|
983
1083
|
else {
|
|
1084
|
+
const install = installSentence(installs, "reader");
|
|
1085
|
+
if (install !== null)
|
|
1086
|
+
out.warn(install);
|
|
984
1087
|
out.done(READY_SENTENCE);
|
|
985
1088
|
}
|
|
986
|
-
out.say(`Docs: ${out.accent(DOCS)}`);
|
|
987
1089
|
return 0;
|
|
988
1090
|
}
|
|
989
1091
|
/** `carrick templates <name>`, so a workflow is one command. */
|