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