carrick 0.3.64 → 0.3.66
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 +8 -0
- package/dist/init/connect.js +10 -8
- package/dist/init/connect.js.map +1 -1
- package/dist/init/hosted.d.ts +58 -0
- package/dist/init/hosted.js +174 -0
- package/dist/init/hosted.js.map +1 -0
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -18
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/output.d.ts +44 -0
- package/dist/init/output.js +134 -0
- package/dist/init/output.js.map +1 -0
- package/dist/init/run.d.ts +44 -30
- package/dist/init/run.js +158 -208
- package/dist/init/run.js.map +1 -1
- package/package.json +8 -6
- package/sidecar/dist/src/capture/machinery.d.ts +10 -0
- package/sidecar/dist/src/capture/machinery.js +19 -9
- package/sidecar/dist/src/type-inferrer.d.ts +66 -4
- package/sidecar/dist/src/type-inferrer.js +249 -18
package/dist/init/run.js
CHANGED
|
@@ -1,63 +1,42 @@
|
|
|
1
1
|
// Authenticated setup; Rust owns the workspace and service proposal.
|
|
2
|
+
//
|
|
3
|
+
// What this prints is one line per thing that happened (carrick#1026). The
|
|
4
|
+
// reasoning behind each step — how the hooks deliver, which editor extension
|
|
5
|
+
// to install, why the scan is left to CI, what a hand-written carrick.json
|
|
6
|
+
// looks like — is the quickstart's, at `output.ts`'s `DOCS`, and this command
|
|
7
|
+
// links to it rather than repeating it. The record of the run stays here,
|
|
8
|
+
// because nothing else can state it: this login, this project, these repos,
|
|
9
|
+
// this many packages, this index.
|
|
2
10
|
import fs from "node:fs";
|
|
3
11
|
import path from "node:path";
|
|
4
|
-
import readline from "node:readline/promises";
|
|
5
12
|
import { spawnSync } from "node:child_process";
|
|
6
13
|
import { readCredential } from "../auth/credentials.js";
|
|
7
14
|
import { signIn } from "../auth/run.js";
|
|
8
15
|
import { resolveRepos } from "../auth/read.js";
|
|
9
|
-
import { deriveWorkspace, writeProposal, PROPOSAL_FILE, repoIdentity } from "./repos.js";
|
|
16
|
+
import { deriveWorkspace, writeProposal, PROPOSAL_FILE, repoIdentity, } from "./repos.js";
|
|
10
17
|
import { connectRepos, reposAreInProject, projectAssignments } from "./connect.js";
|
|
18
|
+
import { downloadHostedIndex, hostedReport, nativeRunner } from "./hosted.js";
|
|
11
19
|
import { ensureProject, projectStep, SLUG } from "./projects.js";
|
|
12
|
-
import { connectMcpClients,
|
|
20
|
+
import { connectMcpClients, MCP_LINE } from "./mcp.js";
|
|
13
21
|
import { hookCommand, mergeCarrickHooks } from "./settings.js";
|
|
22
|
+
import { createOutput, DOCS } from "./output.js";
|
|
14
23
|
import { renderTemplate } from "../templates.js";
|
|
15
|
-
import { packageRoot } from "../native.js";
|
|
16
|
-
const EXTENSION_ID = "carrick-tools.carrick";
|
|
17
24
|
/**
|
|
18
|
-
* The
|
|
25
|
+
* The one sentence the run ends on.
|
|
19
26
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* proposal this command derived, READS it back against the repo, and then runs
|
|
26
|
-
* the one scan. There is no rehearsal in front of it — `carrick index` is the
|
|
27
|
-
* only scan in the flow (carrick#1008, cloud#832), and a facts-only pass has
|
|
28
|
-
* no user here, so nothing in this copy names one. Keep the two copies in
|
|
29
|
-
* step; the tool's instructions, not this text, decide what gets written.
|
|
27
|
+
* The scaffold tool carries the instructions — which files to create, how to
|
|
28
|
+
* seed `carrick.json` from the proposal, whether to run the scan at all
|
|
29
|
+
* (carrick-cloud#832) — so the terminal names the tool and stops. The copy
|
|
30
|
+
* that used to stand here was a second statement of the same instructions, and
|
|
31
|
+
* two copies of a sequence drift.
|
|
30
32
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* cloud#832).
|
|
33
|
+
* The argument is named because the tool's branch turns on it and turns soft
|
|
34
|
+
* without it: `repo` is what resolves the hosted rows, and with no `repo` the
|
|
35
|
+
* response keeps its default ending and tells the agent to run the scan — on a
|
|
36
|
+
* repo CI already indexes, which is the row the whole workspace reads
|
|
37
|
+
* (carrick-cloud `src/tools/scaffold.ts`, cloud#805 item 1).
|
|
37
38
|
*/
|
|
38
|
-
export
|
|
39
|
-
return ("Run the carrick scaffold tool, passing this repo's owner/repo from " +
|
|
40
|
-
"`git remote get-url origin` as `repo`, and follow the instructions it " +
|
|
41
|
-
"returns: create each file at its path, write carrick.json by seeding it " +
|
|
42
|
-
"from .carrick/proposal.json and reading this repo's source for the env " +
|
|
43
|
-
"vars and domains its services call (a nested services config for a " +
|
|
44
|
-
"monorepo, with library packages folded into the services that import " +
|
|
45
|
-
"them), and add the Carrick section to AGENTS.md if this repo already " +
|
|
46
|
-
"has one. " +
|
|
47
|
-
(hosted
|
|
48
|
-
? "Then read carrick.json back against the repo. Do not run `carrick " +
|
|
49
|
-
"index`: it is the scan, this repo already has a hosted index, and a " +
|
|
50
|
-
"laptop scan from a branch replaces the CI row for everyone. "
|
|
51
|
-
: // carrick#960/#1003: an agent's shell caps a command well below the
|
|
52
|
-
// fifteen minutes a first scan takes, so the scan is detached and the
|
|
53
|
-
// agent asks `carrick status` how it went. The three words it polls
|
|
54
|
-
// for are carrick#1007 item 4's. Mirrors the scaffold tool's own
|
|
55
|
-
// wording (carrick-cloud#821, rewritten in carrick-cloud#832).
|
|
56
|
-
"Then read carrick.json back against the repo and run `carrick index " +
|
|
57
|
-
"--detach` once for the first scan, checking `carrick status` about " +
|
|
58
|
-
"once a minute until it says the scan finished, stopped or failed. ") +
|
|
59
|
-
"Answer the closing checklist before you open the PR.");
|
|
60
|
-
}
|
|
39
|
+
export const SCAFFOLD_SENTENCE = "Run the carrick scaffold tool for this repo, passing its owner/repo as `repo`, and follow what it returns.";
|
|
61
40
|
const OWNER_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
62
41
|
export function parseArgs(argv, cwd = process.cwd()) {
|
|
63
42
|
const options = { workspace: cwd, project: null, repo: null, assumeYes: false };
|
|
@@ -117,40 +96,18 @@ function help() {
|
|
|
117
96
|
"or a folder of repos: the project, the repo connection, the agent hooks,",
|
|
118
97
|
"the MCP connection, and the service proposal your agent turns into",
|
|
119
98
|
"carrick.json. It writes nothing into the repository but the ignored",
|
|
120
|
-
".carrick directory and the hook settings, and it runs no scan
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"GitHub App grant connects them.",
|
|
99
|
+
".carrick directory and the hook settings, and it runs no paid scan: where",
|
|
100
|
+
"Carrick already holds an index for these repos, it reads that index into",
|
|
101
|
+
".carrick so this machine can answer from it.",
|
|
124
102
|
"",
|
|
125
103
|
" -w, --workspace DIR The folder holding the repos (default: this one)",
|
|
126
104
|
" --project SLUG Require these repos in this Carrick project",
|
|
127
105
|
" --repo OWNER/REPO Name the GitHub repo whose origin remote names none",
|
|
128
106
|
" -y, --yes Take the repo list as proposed",
|
|
107
|
+
"",
|
|
108
|
+
`The editor extension, the hooks, CI and a carrick.json written by hand: ${DOCS}`,
|
|
129
109
|
].join("\n");
|
|
130
110
|
}
|
|
131
|
-
function say(line = "") {
|
|
132
|
-
process.stdout.write(`${line}\n`);
|
|
133
|
-
}
|
|
134
|
-
async function confirm(question) {
|
|
135
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
136
|
-
try {
|
|
137
|
-
const answer = await rl.question(`${question} [Y/n] `);
|
|
138
|
-
return answer.trim() === "" || /^y(es)?$/i.test(answer.trim());
|
|
139
|
-
}
|
|
140
|
-
finally {
|
|
141
|
-
rl.close();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/** A typed answer, for the questions whose answer is not yes or no. */
|
|
145
|
-
async function ask(question) {
|
|
146
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
147
|
-
try {
|
|
148
|
-
return (await rl.question(`${question}\n> `)).trim();
|
|
149
|
-
}
|
|
150
|
-
finally {
|
|
151
|
-
rl.close();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
111
|
function writeIfChanged(target, body) {
|
|
155
112
|
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null;
|
|
156
113
|
if (existing === body)
|
|
@@ -170,7 +127,7 @@ function onPath(command) {
|
|
|
170
127
|
* The repos a project holds that this folder does not, one line per project.
|
|
171
128
|
*
|
|
172
129
|
* Carrick answers across every repo in a project, so a machine holding half of
|
|
173
|
-
* one gets half the answers, and the connection
|
|
130
|
+
* one gets half the answers, and the connection line above names only what is
|
|
174
131
|
* here. `project_repos` is the workspace read's answer to that: the whole
|
|
175
132
|
* membership of every project the requested repos are in (carrick#993 row 18).
|
|
176
133
|
* Capped and counted like the identity lines, because a project can hold two
|
|
@@ -195,35 +152,59 @@ export function absentRepos(identity, names, project) {
|
|
|
195
152
|
}
|
|
196
153
|
return lines;
|
|
197
154
|
}
|
|
155
|
+
/** The repos this run connected, as one line however many there are. */
|
|
156
|
+
export function connectedLine(names) {
|
|
157
|
+
if (names.length === 1)
|
|
158
|
+
return `Repo ${names[0]} connected`;
|
|
159
|
+
const shown = names.length > 3 ? `${names.slice(0, 3).join(", ")} and ${names.length - 3} more` : names.join(", ");
|
|
160
|
+
return `${names.length} repos connected: ${shown}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* What the derive found, in the words the workspace stated it in.
|
|
164
|
+
*
|
|
165
|
+
* The manifest kind is the scanner's own `reason` for each repo — "npm
|
|
166
|
+
* workspaces", "pnpm workspaces", "Deno manifests", "carrick.json", "single
|
|
167
|
+
* repository", or a `+` join of them (`src/service_derivation.rs`) — and it is
|
|
168
|
+
* named only where every repo here agrees on one, because a mixed workspace
|
|
169
|
+
* has no one word for what was found.
|
|
170
|
+
*/
|
|
171
|
+
export function packagesFound(plan) {
|
|
172
|
+
const count = plan.repos.reduce((total, repo) => total + repo.services.length, 0);
|
|
173
|
+
const reasons = plan.repos.map((repo) => repo.reason);
|
|
174
|
+
const every = (word) => reasons.length > 0 && reasons.every((reason) => reason.includes(word));
|
|
175
|
+
const kind = every("Deno") ? "Deno " : every("pnpm") ? "pnpm " : every("npm") ? "npm " : "";
|
|
176
|
+
return `${count} ${kind}package${count === 1 ? "" : "s"}`;
|
|
177
|
+
}
|
|
178
|
+
/** The line that states it, and names where the proposal landed. */
|
|
179
|
+
export function packagesLine(plan) {
|
|
180
|
+
return `${packagesFound(plan)} found, proposal in ${PROPOSAL_FILE}`;
|
|
181
|
+
}
|
|
198
182
|
/**
|
|
199
|
-
* The
|
|
183
|
+
* The one line for the hooks and the MCP connection.
|
|
200
184
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
185
|
+
* A client is named only where this run changed something for it: an "already
|
|
186
|
+
* connected" line is a line about nothing, and a first run prints four of them
|
|
187
|
+
* (carrick#1026). The hooks are always this repo's, so they are always the
|
|
188
|
+
* subject.
|
|
189
|
+
*/
|
|
190
|
+
export function configuredLine(mcp) {
|
|
191
|
+
const claude = mcp.some((outcome) => outcome.client === "Claude Code" && outcome.state === "written");
|
|
192
|
+
return claude
|
|
193
|
+
? "Claude Code hooks and MCP configured (restart the client)"
|
|
194
|
+
: "Claude Code hooks configured";
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* One line per other client this run wrote a file for, naming that file.
|
|
208
198
|
*
|
|
209
|
-
*
|
|
199
|
+
* The path is printed because this command guessed it: a client detected by its
|
|
200
|
+
* own data directory gets an entry written into its own config file, and a
|
|
201
|
+
* wrong guess has to be one visible line and one entry to delete (`mcp.ts`).
|
|
202
|
+
* A client left exactly as it was gets no line at all (carrick#1026).
|
|
210
203
|
*/
|
|
211
|
-
export function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
["cursor", "Cursor"],
|
|
216
|
-
["windsurf", "Windsurf"],
|
|
217
|
-
]) {
|
|
218
|
-
if (!onPath(command))
|
|
219
|
-
continue;
|
|
220
|
-
lines.push(` ${editor}, for diagnostics in the Problems panel:`);
|
|
221
|
-
lines.push(` ${command} --install-extension ${EXTENSION_ID}`);
|
|
222
|
-
}
|
|
223
|
-
if (lines.length === 0) {
|
|
224
|
-
lines.push(" Any editor with an LSP client starts the server itself as `carrick lsp --stdio`.");
|
|
225
|
-
}
|
|
226
|
-
return lines;
|
|
204
|
+
export function mcpClientLines(mcp) {
|
|
205
|
+
return mcp
|
|
206
|
+
.filter((outcome) => outcome.state === "written" && outcome.client !== "Claude Code")
|
|
207
|
+
.map((outcome) => `MCP added for ${outcome.client}: ${outcome.detail}`);
|
|
227
208
|
}
|
|
228
209
|
export async function init(argv) {
|
|
229
210
|
const parsed = parseArgs(argv);
|
|
@@ -237,12 +218,11 @@ export async function init(argv) {
|
|
|
237
218
|
return 1;
|
|
238
219
|
}
|
|
239
220
|
const interactive = process.stdin.isTTY === true;
|
|
221
|
+
const out = createOutput();
|
|
240
222
|
// Authentication and all derivation validation precede local writes.
|
|
241
223
|
let derived;
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// be told to run the paid scan again (carrick#993).
|
|
245
|
-
let requested = [];
|
|
224
|
+
// The repos the workspace read found an index for: where CI has already built
|
|
225
|
+
// one, this run reads it rather than ordering a scan (carrick#993 row 2).
|
|
246
226
|
const hostedIndex = [];
|
|
247
227
|
try {
|
|
248
228
|
// A machine that has never signed in signs in here rather than being told
|
|
@@ -254,8 +234,7 @@ export async function init(argv) {
|
|
|
254
234
|
if (!interactive) {
|
|
255
235
|
throw new Error("carrick init requires a Carrick login. Run carrick login, or set CARRICK_TOKEN.");
|
|
256
236
|
}
|
|
257
|
-
|
|
258
|
-
credential = await signIn(say);
|
|
237
|
+
credential = await signIn(out.say);
|
|
259
238
|
}
|
|
260
239
|
derived = deriveWorkspace(workspace);
|
|
261
240
|
const derivedIdentities = derived.plan.repos.map((repo) => repoIdentity(repo.path));
|
|
@@ -269,11 +248,11 @@ export async function init(argv) {
|
|
|
269
248
|
throw new Error(`--repo names one repository, but ${unnamed.length} repos here have no GitHub identity: ${unnamed.map((repo) => repo.path).join(", ")}. Run carrick init --repo in each of them, or fix their origin remotes.`);
|
|
270
249
|
}
|
|
271
250
|
if (unnamed.length === 0) {
|
|
272
|
-
|
|
251
|
+
out.warn(`--repo ${parsed.repo} was not needed: every repo here names its own GitHub repository.`);
|
|
273
252
|
}
|
|
274
253
|
else {
|
|
275
254
|
taken = unnamed[0].path;
|
|
276
|
-
|
|
255
|
+
out.done(`${parsed.repo} taken as the GitHub repository for ${taken}`);
|
|
277
256
|
}
|
|
278
257
|
}
|
|
279
258
|
const repoIdentities = derivedIdentities.map((repo) => repo.path === taken ? { ...repo, name: parsed.repo, problem: null } : repo);
|
|
@@ -288,14 +267,14 @@ export async function init(argv) {
|
|
|
288
267
|
// A folder of clones can hold dozens, so the list is capped and the rest
|
|
289
268
|
// is counted rather than dropped.
|
|
290
269
|
for (const repo of missingIdentities.slice(0, 10)) {
|
|
291
|
-
|
|
270
|
+
out.warn(`${repo.path} contributes no GitHub identity: ${repo.problem}.`);
|
|
292
271
|
}
|
|
293
272
|
if (missingIdentities.length > 10) {
|
|
294
|
-
|
|
273
|
+
out.warn(`${missingIdentities.length - 10} more repos here name no GitHub identity either.`);
|
|
295
274
|
}
|
|
296
275
|
if (missingIdentities.length > 0) {
|
|
297
276
|
const one = missingIdentities.length === 1;
|
|
298
|
-
|
|
277
|
+
out.warn(`Carrick has nothing to connect ${one ? "it" : "them"} to. Run carrick init --repo owner/repo${one ? "" : " in each of them"}, or give the alias a HostName github.com line in your ssh config.`);
|
|
299
278
|
}
|
|
300
279
|
if (parsed.project && missingIdentities.length > 0) {
|
|
301
280
|
throw new Error(`--project ${parsed.project} cannot verify ${missingIdentities.map((repo) => repo.path).join(", ")} because ${missingIdentities.length === 1 ? "it has" : "they have"} no GitHub origin. No project assignment was verified.`);
|
|
@@ -303,18 +282,12 @@ export async function init(argv) {
|
|
|
303
282
|
if (parsed.project && names.length === 0) {
|
|
304
283
|
throw new Error(`--project ${parsed.project} found no GitHub repos to verify.`);
|
|
305
284
|
}
|
|
306
|
-
if (
|
|
307
|
-
say(`Repos requested for project "${parsed.project}":`);
|
|
308
|
-
for (const name of names)
|
|
309
|
-
say(` ${name}`);
|
|
310
|
-
}
|
|
311
|
-
else if (names.length === 0) {
|
|
285
|
+
if (names.length === 0) {
|
|
312
286
|
// The sentence that stops the rest of this run from reading as a
|
|
313
287
|
// complete one: no project is chosen, no connection is checked, and a
|
|
314
288
|
// later upload has no repo identity to resolve a project from.
|
|
315
|
-
|
|
289
|
+
out.warn("No repo here names a GitHub repository, so this run chooses no project and checks no connection.");
|
|
316
290
|
}
|
|
317
|
-
requested = names;
|
|
318
291
|
const initial = await resolveRepos(credential.token, names);
|
|
319
292
|
// The project half of the browser round trip, where this API can do it
|
|
320
293
|
// from here: the project is created here, and `connectRepos` puts the
|
|
@@ -322,7 +295,7 @@ export async function init(argv) {
|
|
|
322
295
|
// the GitHub App grant and nothing else. Without --project the step reads
|
|
323
296
|
// the assignment the repos already have and offers the list, rather than
|
|
324
297
|
// doing nothing at all (carrick#987).
|
|
325
|
-
const prompts = { say, ask, confirm, interactive, assumeYes: parsed.assumeYes };
|
|
298
|
+
const prompts = { say: out.say, ask: out.ask, confirm: out.confirm, interactive, assumeYes: parsed.assumeYes };
|
|
326
299
|
let project = parsed.project;
|
|
327
300
|
let projectExists = false;
|
|
328
301
|
if (project === null) {
|
|
@@ -337,66 +310,72 @@ export async function init(argv) {
|
|
|
337
310
|
interactive,
|
|
338
311
|
project: project ?? undefined,
|
|
339
312
|
projectExists,
|
|
340
|
-
say,
|
|
313
|
+
say: out.say,
|
|
341
314
|
});
|
|
315
|
+
// Who this machine is, and where its answers come from. The workspace slug
|
|
316
|
+
// is the login: nothing on the credential or in the workspace read names a
|
|
317
|
+
// user (carrick#1026).
|
|
318
|
+
out.done(project === null
|
|
319
|
+
? `Signed in as ${identity.workspace.slug}`
|
|
320
|
+
: `Signed in as ${identity.workspace.slug} · project ${project}`);
|
|
342
321
|
if (project !== null && !reposAreInProject(identity, names, project)) {
|
|
343
322
|
// An unverified project does not end the run, whether it was named on
|
|
344
323
|
// the command line or picked here. Stopping cost someone their hooks and
|
|
345
324
|
// their proposal for a browser step they could only take afterwards, and
|
|
346
325
|
// the documented command then needed two runs (carrick#993 row 8).
|
|
347
|
-
|
|
326
|
+
out.warn(`Finish the browser steps above to put these repos in "${project}", then run carrick init --project ${project} again to verify.`);
|
|
348
327
|
}
|
|
349
|
-
say(`Carrick workspace: ${identity.workspace.slug}`);
|
|
350
328
|
if (identity.allowance_sentence)
|
|
351
|
-
say(identity.allowance_sentence);
|
|
329
|
+
out.say(identity.allowance_sentence);
|
|
330
|
+
const connected = [];
|
|
352
331
|
for (const repo of identity.repos) {
|
|
353
332
|
if (!repo.connected)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
hostedIndex.push(repo.full_name);
|
|
333
|
+
out.warn(`${repo.full_name} is not connected to this Carrick workspace.`);
|
|
334
|
+
else {
|
|
335
|
+
connected.push(repo.full_name);
|
|
336
|
+
if (repo.services.length > 0)
|
|
337
|
+
hostedIndex.push(repo.full_name);
|
|
338
|
+
}
|
|
361
339
|
}
|
|
340
|
+
if (connected.length > 0)
|
|
341
|
+
out.done(connectedLine(connected));
|
|
362
342
|
// The rest of the project, which this machine does not hold. Carrick
|
|
363
343
|
// answers across every repo in a project, so a folder holding half of one
|
|
364
344
|
// is a partial index and nothing here would otherwise say so
|
|
365
345
|
// (carrick#993 row 18).
|
|
366
346
|
for (const line of absentRepos(identity, names, project))
|
|
367
|
-
|
|
347
|
+
out.warn(line);
|
|
368
348
|
}
|
|
369
349
|
catch (error) {
|
|
370
350
|
process.stderr.write(`carrick init: ${error.message}\n`);
|
|
371
351
|
return 1;
|
|
372
352
|
}
|
|
373
353
|
const plan = derived.plan;
|
|
374
|
-
say(`Repos to index, in ${plan.workspace} (${plan.repos_detected_by}):`);
|
|
375
354
|
if (plan.parent_proposal) {
|
|
376
355
|
const parent = plan.parent_proposal;
|
|
377
|
-
// Reads like the scanner's own sentence for the same proposal, capped the
|
|
378
|
-
// same way: a folder of scratch checkouts holds dozens, and the folder
|
|
379
|
-
// that holds them is already named here.
|
|
380
356
|
const names = parent.repos.map((repo) => path.basename(repo));
|
|
381
357
|
const shown = names.length > 3 ? `${names.slice(0, 3).join(", ")} and ${names.length - 3} more` : names.join(", ");
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
for (const repo of plan.repos) {
|
|
385
|
-
say(` ${repo.path} (${repo.reason})`);
|
|
386
|
-
for (const service of repo.services) {
|
|
387
|
-
say(` ${service.serviceName ?? "<repository>"}: directory ${service.directory ?? "."}, tsconfig ${service.tsconfig ?? "scanner default"}`);
|
|
388
|
-
}
|
|
389
|
-
for (const warning of repo.warnings)
|
|
390
|
-
say(` ${warning}`);
|
|
358
|
+
out.warn(`The parent folder ${parent.directory} holds ${parent.repos.length} ${parent.repos.length === 1 ? "repo" : "repos"}: ${shown}. Run carrick init .. to initialise that workspace.`);
|
|
391
359
|
}
|
|
360
|
+
// The scanner's own warnings about what it proposed. Capped: the proposal
|
|
361
|
+
// file carries every one of them, and it is named on the line above.
|
|
362
|
+
const warnings = plan.repos.flatMap((repo) => repo.warnings);
|
|
363
|
+
for (const warning of warnings.slice(0, 3))
|
|
364
|
+
out.warn(warning);
|
|
365
|
+
if (warnings.length > 3)
|
|
366
|
+
out.warn(`${warnings.length - 3} more notes on the proposal are in ${PROPOSAL_FILE}.`);
|
|
392
367
|
for (const missing of plan.missing)
|
|
393
|
-
|
|
368
|
+
out.warn(`Missing workspace override: ${missing}`);
|
|
369
|
+
const proposed = packagesLine(plan);
|
|
370
|
+
const subject = packagesFound(plan);
|
|
394
371
|
if (!parsed.assumeYes) {
|
|
395
372
|
if (!process.stdin.isTTY) {
|
|
396
373
|
process.stderr.write("carrick init: use --yes to accept this proposal without a terminal.\n");
|
|
397
374
|
return 1;
|
|
398
375
|
}
|
|
399
|
-
|
|
376
|
+
// The count is in the question, because the listing that used to stand
|
|
377
|
+
// above it is gone: this is where a reader decides (carrick#1026).
|
|
378
|
+
if (!await out.confirm(`Write the proposal for ${subject} and configure hooks?`))
|
|
400
379
|
return 0;
|
|
401
380
|
}
|
|
402
381
|
try {
|
|
@@ -404,17 +383,22 @@ export async function init(argv) {
|
|
|
404
383
|
// without a model is written into the repository, because the first scan
|
|
405
384
|
// is the paid one and it has to run against a config someone has read
|
|
406
385
|
// (carrick-cloud#799).
|
|
407
|
-
|
|
386
|
+
writeProposal(plan.workspace, derived);
|
|
387
|
+
out.done(proposed);
|
|
408
388
|
}
|
|
409
389
|
catch (error) {
|
|
410
390
|
process.stderr.write(`carrick init: ${error.message}\n`);
|
|
411
391
|
return 1;
|
|
412
392
|
}
|
|
413
|
-
//
|
|
414
|
-
//
|
|
393
|
+
// The agent hooks, merged by command: this file may already hold a user's
|
|
394
|
+
// own hooks, or the hook pack the hosted index installs. And the MCP
|
|
395
|
+
// connection, for work that crosses repos this machine does not hold,
|
|
396
|
+
// configured for every client this machine has rather than printed for one
|
|
397
|
+
// of them (carrick#955).
|
|
415
398
|
const command = hookCommand({ onPath });
|
|
416
399
|
const settingsName = path.join(".claude", command.bare ? "settings.json" : "settings.local.json");
|
|
417
400
|
const settingsFile = path.join(workspace, settingsName);
|
|
401
|
+
let hooksWritten = true;
|
|
418
402
|
try {
|
|
419
403
|
const settings = fs.existsSync(settingsFile) ? fs.readFileSync(settingsFile, "utf8") : null;
|
|
420
404
|
const otherFile = path.join(workspace, ".claude", command.bare ? "settings.local.json" : "settings.json");
|
|
@@ -424,77 +408,43 @@ export async function init(argv) {
|
|
|
424
408
|
// Validate both documents before migrating our entries between them.
|
|
425
409
|
if (cleaned?.changed)
|
|
426
410
|
writeIfChanged(otherFile, cleaned.body);
|
|
427
|
-
|
|
428
|
-
say(`${wroteHooks === "written" ? "wrote" : "unchanged"} ${settingsName}`);
|
|
429
|
-
if (!command.bare) {
|
|
430
|
-
say(` \`carrick\` is not on PATH here, so those hooks name this install: ${command.command}.`);
|
|
431
|
-
say(" An npx run leaves nothing on PATH afterwards, and a hook that cannot find carrick");
|
|
432
|
-
say(" says nothing rather than failing your edit. `npm install -g carrick` and run init");
|
|
433
|
-
say(" again to write the short command instead.");
|
|
434
|
-
}
|
|
411
|
+
writeIfChanged(settingsFile, hooks.body);
|
|
435
412
|
}
|
|
436
413
|
catch (error) {
|
|
437
|
-
|
|
414
|
+
hooksWritten = false;
|
|
415
|
+
out.refuse(`Could not configure Carrick hooks: ${error.message}. Fix ${settingsName} and run carrick init again.`);
|
|
438
416
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
say();
|
|
445
|
-
for (const line of mcpLines(connectMcpClients()))
|
|
446
|
-
say(line);
|
|
447
|
-
say();
|
|
448
|
-
for (const line of editorLines(onPath))
|
|
449
|
-
say(line);
|
|
450
|
-
say();
|
|
451
|
-
say(`Start Claude Code in this folder — the hooks above are ${settingsName} here, and`);
|
|
452
|
-
say("the index covers every repo in it. The hooks need no plugin; the language server does:");
|
|
453
|
-
const plugin = path.join(packageRoot(), "plugin");
|
|
454
|
-
say(` claude --plugin-dir ${fs.existsSync(plugin) ? plugin : "<carrick checkout>/plugin"}`);
|
|
417
|
+
const mcp = connectMcpClients();
|
|
418
|
+
if (hooksWritten)
|
|
419
|
+
out.done(configuredLine(mcp));
|
|
420
|
+
for (const line of mcpClientLines(mcp))
|
|
421
|
+
out.done(line);
|
|
455
422
|
if (!command.bare) {
|
|
456
|
-
|
|
457
|
-
|
|
423
|
+
out.warn(`\`carrick\` is not on PATH, so the hooks in ${settingsName} name this install. Run \`npm install -g carrick\` and carrick init again for the short command.`);
|
|
424
|
+
}
|
|
425
|
+
for (const outcome of mcp.filter((entry) => entry.state === "failed")) {
|
|
426
|
+
out.warn(`MCP not configured for ${outcome.client}: ${outcome.detail}`);
|
|
458
427
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// facts-only one has no user (carrick#1008, cloud#832).
|
|
467
|
-
//
|
|
468
|
-
// Unless CI has already built one. The hosted index is the whole workspace's
|
|
469
|
-
// row, and `carrick index` from a laptop on a branch replaces it for
|
|
470
|
-
// everyone who queries it, so where the workspace read found services this
|
|
471
|
-
// says so and orders no scan at all (carrick#993 row 2).
|
|
428
|
+
if (mcp.length === 0)
|
|
429
|
+
out.warn(`No agent client found on this machine. In Claude Code: ${MCP_LINE}`);
|
|
430
|
+
// No paid scan ran here, and that is the point (carrick-cloud#799): the one
|
|
431
|
+
// scan runs against a config someone has read. Where CI has already built an
|
|
432
|
+
// index, this run reads it onto the machine instead — the hosted index is
|
|
433
|
+
// the whole workspace's row, and `carrick index` from a laptop on a branch
|
|
434
|
+
// replaces it for everyone who queries it (carrick#993 row 2, carrick#1020).
|
|
472
435
|
if (hostedIndex.length > 0) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
? `${hostedIndex.slice(0, 3).join(", ")} and ${hostedIndex.length - 3} more`
|
|
477
|
-
: hostedIndex.join(", ");
|
|
478
|
-
say(` ${subject} already ${hostedIndex.length === 1 ? "has" : "have"} a hosted index. Do not run \`carrick index\`:`);
|
|
479
|
-
say(" from a branch it replaces the CI row for everyone, and CI keeps it current.");
|
|
480
|
-
say(` ${PROPOSAL_FILE} holds the services this run derived, to compare`);
|
|
481
|
-
say(" with the carrick.json already committed.");
|
|
436
|
+
const outcome = await out.step("Reading the hosted index into .carrick/", () => downloadHostedIndex(plan.workspace, nativeRunner(out.quiet)));
|
|
437
|
+
const report = hostedReport(outcome);
|
|
438
|
+
out[report.kind](report.text);
|
|
482
439
|
}
|
|
483
440
|
else {
|
|
484
|
-
|
|
485
|
-
say(" an agent turns it into carrick.json, adds the CI check (which needs no secret),");
|
|
486
|
-
say(" reads the config back against the repo, and runs `carrick index --detach` once:");
|
|
487
|
-
say(" the single scan that builds the index and asks Carrick to classify the rest.");
|
|
488
|
-
say(" By hand instead: `carrick templates workflow > .github/workflows/carrick.yml`, a");
|
|
489
|
-
say(" carrick.json per https://docs.carrick.tools/carrick-json, then that command.");
|
|
441
|
+
out.done("No index yet: your agent runs the one scan");
|
|
490
442
|
}
|
|
491
|
-
|
|
492
|
-
say(
|
|
493
|
-
say();
|
|
494
|
-
say(` ${agentScaffoldPrompt(hostedIndex.length > 0)}`);
|
|
443
|
+
out.note("Next: paste this to your agent", [SCAFFOLD_SENTENCE]);
|
|
444
|
+
out.say(`Docs: ${out.accent(DOCS)}`);
|
|
495
445
|
return 0;
|
|
496
446
|
}
|
|
497
|
-
/** `carrick templates <name>`, so
|
|
447
|
+
/** `carrick templates <name>`, so a workflow is one command. */
|
|
498
448
|
export function templates(argv) {
|
|
499
449
|
const [name, ...rest] = argv;
|
|
500
450
|
const variables = {};
|