carrick 0.3.53

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.
Files changed (114) hide show
  1. package/LICENSE.md +99 -0
  2. package/README.md +79 -0
  3. package/bin/carrick.mjs +168 -0
  4. package/dist/channel.d.ts +16 -0
  5. package/dist/channel.js +37 -0
  6. package/dist/channel.js.map +1 -0
  7. package/dist/cli.d.ts +29 -0
  8. package/dist/cli.js +60 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/contract.d.ts +154 -0
  11. package/dist/contract.js +141 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/diagnostics.d.ts +54 -0
  14. package/dist/diagnostics.js +199 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/hook/post-edit.d.ts +13 -0
  17. package/dist/hook/post-edit.js +76 -0
  18. package/dist/hook/post-edit.js.map +1 -0
  19. package/dist/hook/session-start.d.ts +2 -0
  20. package/dist/hook/session-start.js +44 -0
  21. package/dist/hook/session-start.js.map +1 -0
  22. package/dist/init/identity.d.ts +20 -0
  23. package/dist/init/identity.js +60 -0
  24. package/dist/init/identity.js.map +1 -0
  25. package/dist/init/repos.d.ts +19 -0
  26. package/dist/init/repos.js +46 -0
  27. package/dist/init/repos.js.map +1 -0
  28. package/dist/init/run.d.ts +11 -0
  29. package/dist/init/run.js +228 -0
  30. package/dist/init/run.js.map +1 -0
  31. package/dist/init/settings.d.ts +50 -0
  32. package/dist/init/settings.js +138 -0
  33. package/dist/init/settings.js.map +1 -0
  34. package/dist/log.d.ts +2 -0
  35. package/dist/log.js +35 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/native.d.ts +48 -0
  38. package/dist/native.js +127 -0
  39. package/dist/native.js.map +1 -0
  40. package/dist/render.d.ts +65 -0
  41. package/dist/render.js +334 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/root.d.ts +30 -0
  44. package/dist/root.js +82 -0
  45. package/dist/root.js.map +1 -0
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +255 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/templates.d.ts +18 -0
  50. package/dist/templates.js +66 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +72 -0
  53. package/plugin/.claude-plugin/plugin.json +6 -0
  54. package/plugin/.lsp.json +14 -0
  55. package/plugin/hooks/hooks.json +27 -0
  56. package/sidecar/dist/src/bundler.d.ts +141 -0
  57. package/sidecar/dist/src/bundler.js +680 -0
  58. package/sidecar/dist/src/capture/anchors.d.ts +61 -0
  59. package/sidecar/dist/src/capture/anchors.js +1132 -0
  60. package/sidecar/dist/src/capture/api.d.ts +378 -0
  61. package/sidecar/dist/src/capture/api.js +10 -0
  62. package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
  63. package/sidecar/dist/src/capture/augmentations.js +60 -0
  64. package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
  65. package/sidecar/dist/src/capture/check-classify.js +189 -0
  66. package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
  67. package/sidecar/dist/src/capture/check-deep.js +91 -0
  68. package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
  69. package/sidecar/dist/src/capture/check-poison.js +155 -0
  70. package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
  71. package/sidecar/dist/src/capture/check-probe.js +147 -0
  72. package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
  73. package/sidecar/dist/src/capture/check-scrub.js +71 -0
  74. package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
  75. package/sidecar/dist/src/capture/check-workspace.js +218 -0
  76. package/sidecar/dist/src/capture/check.d.ts +39 -0
  77. package/sidecar/dist/src/capture/check.js +450 -0
  78. package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
  79. package/sidecar/dist/src/capture/deep-walk.js +243 -0
  80. package/sidecar/dist/src/capture/index.d.ts +36 -0
  81. package/sidecar/dist/src/capture/index.js +477 -0
  82. package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
  83. package/sidecar/dist/src/capture/lockfile.js +490 -0
  84. package/sidecar/dist/src/capture/machinery.d.ts +59 -0
  85. package/sidecar/dist/src/capture/machinery.js +160 -0
  86. package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
  87. package/sidecar/dist/src/capture/node-builder.js +123 -0
  88. package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
  89. package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
  90. package/sidecar/dist/src/capture/self-check.d.ts +38 -0
  91. package/sidecar/dist/src/capture/self-check.js +317 -0
  92. package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
  93. package/sidecar/dist/src/capture/specifiers.js +56 -0
  94. package/sidecar/dist/src/definition-resolver.d.ts +57 -0
  95. package/sidecar/dist/src/definition-resolver.js +153 -0
  96. package/sidecar/dist/src/index.d.ts +14 -0
  97. package/sidecar/dist/src/index.js +564 -0
  98. package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
  99. package/sidecar/dist/src/monorepo-builder.js +584 -0
  100. package/sidecar/dist/src/project-loader.d.ts +130 -0
  101. package/sidecar/dist/src/project-loader.js +399 -0
  102. package/sidecar/dist/src/type-inferrer.d.ts +940 -0
  103. package/sidecar/dist/src/type-inferrer.js +3540 -0
  104. package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
  105. package/sidecar/dist/src/type-structural-expander.js +283 -0
  106. package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
  107. package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
  108. package/sidecar/dist/src/types.d.ts +669 -0
  109. package/sidecar/dist/src/types.js +5 -0
  110. package/sidecar/dist/src/validators.d.ts +2214 -0
  111. package/sidecar/dist/src/validators.js +336 -0
  112. package/sidecar/package.json +6 -0
  113. package/templates/carrick.json +7 -0
  114. package/templates/carrick.yml +45 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook: one orientation line about the index this workspace has.
3
+ //
4
+ // `carrick status --json` is the read that answers for a workspace: `check` and
5
+ // `touch` each take exactly one file. It carries, per service, what the index
6
+ // holds, the commit it was taken at, how far that repo has moved since, and the
7
+ // boundary. Claude Code adds a SessionStart hook's stdout to the session on
8
+ // exit 0, so a plain print is the whole mechanism.
9
+ //
10
+ // This is not a verdict channel. `status` states what is indexed and what each
11
+ // service could not classify, and nothing about whether a contract holds, so
12
+ // the line cannot be confused with the hook or LSP delivery of a verdict.
13
+ // `CARRICK_CHANNEL=off` silences it along with everything else.
14
+ import { status } from "../cli.js";
15
+ import { resolveChannel } from "../channel.js";
16
+ import { createLogger } from "../log.js";
17
+ import { renderSessionStart } from "../render.js";
18
+ import { resolveRoot, rootNote } from "../root.js";
19
+ const log = createLogger("carrick-session");
20
+ async function main() {
21
+ if (resolveChannel({ hooksInstalled: true }).channel === "off") {
22
+ log("CARRICK_CHANNEL=off; printing nothing");
23
+ return;
24
+ }
25
+ const choice = resolveRoot({
26
+ clientRoot: process.cwd(),
27
+ projectDir: process.env["CLAUDE_PROJECT_DIR"] ?? null,
28
+ filePath: process.cwd(),
29
+ });
30
+ const note = rootNote(choice);
31
+ if (note)
32
+ log(note);
33
+ const outcome = await status({ cwd: choice.root, workspace: choice.markerFound ? choice.root : null });
34
+ if (!outcome.result) {
35
+ log("no answer", outcome.failure ?? "");
36
+ return;
37
+ }
38
+ // Ends with a newline: this is a line a developer also runs by hand and
39
+ // compares against `carrick status`, and that one ends its output properly.
40
+ process.stdout.write(`${renderSessionStart(outcome.result)}\n`);
41
+ }
42
+ await main();
43
+ process.exit(0);
44
+ //# sourceMappingURL=session-start.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../src/hook/session-start.ts"],"names":[],"mappings":";AACA,8EAA8E;AAC9E,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,mDAAmD;AACnD,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,0EAA0E;AAC1E,gEAAgE;AAEhE,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,GAAG,GAAG,YAAY,CAAC,iBAAiB,CAAC,CAAC;AAE5C,KAAK,UAAU,IAAI;IACjB,IAAI,cAAc,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC/D,GAAG,CAAC,uCAAuC,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC;QACzB,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE;QACzB,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,IAAI;QACrD,QAAQ,EAAE,OAAO,CAAC,GAAG,EAAE;KACxB,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEpB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QACxC,OAAO;IACT,CAAC;IACD,wEAAwE;IACxE,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,IAAI,EAAE,CAAC;AACb,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ export type Identity = {
2
+ /** The GitHub login, when the source could name one. */
3
+ login: string | null;
4
+ source: "gh" | "token";
5
+ };
6
+ export type IdentityLookup = {
7
+ identity: Identity | null;
8
+ /** What to do about it, ready to print. Null when there is an identity. */
9
+ problem: string | null;
10
+ };
11
+ export type IdentityOptions = {
12
+ env?: NodeJS.ProcessEnv;
13
+ /** Injectable for tests: runs `gh auth status` and returns its output. */
14
+ ghStatus?: () => string;
15
+ };
16
+ /** The login out of `gh auth status`, whatever wording it used. */
17
+ export declare function loginFromGhStatus(output: string): string | null;
18
+ export declare function githubIdentity(options?: IdentityOptions): IdentityLookup;
19
+ /** One line for the summary. */
20
+ export declare function describeIdentity(identity: Identity): string;
@@ -0,0 +1,60 @@
1
+ // The GitHub identity `carrick init` requires.
2
+ //
3
+ // There is no anonymous tier (carrick#729, ruled 2026-09-08): the local index
4
+ // is built from the identified free tier's allowance, so setup asks who you are
5
+ // before it writes anything. The check is offline and reuses whatever the
6
+ // machine already has — the GitHub CLI's login, or a token in the environment —
7
+ // rather than opening a browser flow of its own.
8
+ //
9
+ // This states the identity. Spending an allowance against it is the cloud's
10
+ // side (carrick-cloud#601) and is not wired here.
11
+ import { spawnSync } from "node:child_process";
12
+ function defaultGhStatus() {
13
+ // Both streams: `gh auth status` writes to stderr on older versions and to
14
+ // stdout on newer ones, and a run that reads one of them finds nothing on
15
+ // half the machines it runs on.
16
+ const run = spawnSync("gh", ["auth", "status"], { encoding: "utf8" });
17
+ if (run.error)
18
+ throw run.error;
19
+ return `${run.stdout ?? ""}\n${run.stderr ?? ""}`;
20
+ }
21
+ /** The login out of `gh auth status`, whatever wording it used. */
22
+ export function loginFromGhStatus(output) {
23
+ const match = /Logged in to \S+ account (\S+)/.exec(output) ?? /Logged in to \S+ as (\S+)/.exec(output);
24
+ return match?.[1] ?? null;
25
+ }
26
+ export function githubIdentity(options = {}) {
27
+ const env = options.env ?? process.env;
28
+ const ghStatus = options.ghStatus ?? defaultGhStatus;
29
+ try {
30
+ const login = loginFromGhStatus(ghStatus());
31
+ if (login)
32
+ return { identity: { login, source: "gh" }, problem: null };
33
+ }
34
+ catch {
35
+ // No gh, or gh is not logged in. The token below is the other way in.
36
+ }
37
+ const token = env["GITHUB_TOKEN"] || env["GH_TOKEN"];
38
+ if (token)
39
+ return { identity: { login: null, source: "token" }, problem: null };
40
+ return {
41
+ identity: null,
42
+ problem: [
43
+ "carrick init needs your GitHub identity, and this machine has none it can use.",
44
+ "",
45
+ "Either:",
46
+ " gh auth login (the GitHub CLI: https://cli.github.com)",
47
+ " export GITHUB_TOKEN=<token> (any token that identifies you)",
48
+ "",
49
+ "Carrick indexes what you already have access to, and the free tier's",
50
+ "allowance is counted against the account that asks for it.",
51
+ ].join("\n"),
52
+ };
53
+ }
54
+ /** One line for the summary. */
55
+ export function describeIdentity(identity) {
56
+ return identity.login
57
+ ? `GitHub identity: ${identity.login} (from the GitHub CLI)`
58
+ : "GitHub identity: the token in this environment";
59
+ }
60
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/init/identity.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,gFAAgF;AAChF,iDAAiD;AACjD,EAAE;AACF,4EAA4E;AAC5E,kDAAkD;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAoB/C,SAAS,eAAe;IACtB,2EAA2E;IAC3E,0EAA0E;IAC1E,gCAAgC;IAChC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACtE,IAAI,GAAG,CAAC,KAAK;QAAE,MAAM,GAAG,CAAC,KAAK,CAAC;IAC/B,OAAO,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AACpD,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,MAAM,KAAK,GACT,gCAAgC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5F,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,UAA2B,EAAE;IAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5C,IAAI,KAAK;YAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;IACxE,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACrD,IAAI,KAAK;QAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAEhF,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE;YACP,gFAAgF;YAChF,EAAE;YACF,SAAS;YACT,0EAA0E;YAC1E,iEAAiE;YACjE,EAAE;YACF,sEAAsE;YACtE,4DAA4D;SAC7D,CAAC,IAAI,CAAC,IAAI,CAAC;KACb,CAAC;AACJ,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,OAAO,QAAQ,CAAC,KAAK;QACnB,CAAC,CAAC,oBAAoB,QAAQ,CAAC,KAAK,wBAAwB;QAC5D,CAAC,CAAC,gDAAgD,CAAC;AACvD,CAAC"}
@@ -0,0 +1,19 @@
1
+ export type FindOptions = {
2
+ readdir?: (dir: string) => Array<{
3
+ name: string;
4
+ isDirectory: () => boolean;
5
+ }>;
6
+ exists?: (target: string) => boolean;
7
+ };
8
+ export declare function findRepos(workspace: string, options?: FindOptions): string[];
9
+ /**
10
+ * The workspace file to write: the repos already listed, in the order they were
11
+ * listed, plus the ones found since. Re-running init updates rather than
12
+ * duplicates, and never reorders or drops a path a user put there by hand —
13
+ * including one pointing outside this folder.
14
+ */
15
+ export declare function mergeWorkspace(existing: string | null, found: string[]): {
16
+ body: string;
17
+ repos: string[];
18
+ added: string[];
19
+ };
@@ -0,0 +1,46 @@
1
+ // Which directories in this folder are repos to index.
2
+ //
3
+ // A proposal, not a decision: the list goes into `carrick-workspace.json`,
4
+ // which the scanner reads literally and a user can edit. There is no walk
5
+ // deeper than one level and no search order to reason about (E20) — a repo is
6
+ // a directory here with a `package.json` in it.
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ /** Directories that are never a repo, whatever they hold. */
10
+ const NEVER = new Set(["node_modules", "dist", "build", "out", "coverage", "target"]);
11
+ export function findRepos(workspace, options = {}) {
12
+ const readdir = options.readdir ?? ((dir) => fs.readdirSync(dir, { withFileTypes: true }));
13
+ const exists = options.exists ?? ((target) => fs.existsSync(target));
14
+ const found = [];
15
+ for (const entry of readdir(workspace)) {
16
+ if (!entry.isDirectory())
17
+ continue;
18
+ if (entry.name.startsWith(".") || NEVER.has(entry.name))
19
+ continue;
20
+ if (!exists(path.join(workspace, entry.name, "package.json")))
21
+ continue;
22
+ found.push(`./${entry.name}`);
23
+ }
24
+ return found.sort();
25
+ }
26
+ /**
27
+ * The workspace file to write: the repos already listed, in the order they were
28
+ * listed, plus the ones found since. Re-running init updates rather than
29
+ * duplicates, and never reorders or drops a path a user put there by hand —
30
+ * including one pointing outside this folder.
31
+ */
32
+ export function mergeWorkspace(existing, found) {
33
+ let listed = [];
34
+ if (existing) {
35
+ const parsed = JSON.parse(existing);
36
+ if (Array.isArray(parsed.repos)) {
37
+ listed = parsed.repos.filter((entry) => typeof entry === "string");
38
+ }
39
+ }
40
+ const normal = (entry) => entry.replace(/^\.\//, "").replace(/\/$/, "");
41
+ const known = new Set(listed.map(normal));
42
+ const added = found.filter((entry) => !known.has(normal(entry)));
43
+ const repos = [...listed, ...added];
44
+ return { body: `${JSON.stringify({ repos }, null, 2)}\n`, repos, added };
45
+ }
46
+ //# sourceMappingURL=repos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repos.js","sourceRoot":"","sources":["../../src/init/repos.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,8EAA8E;AAC9E,gDAAgD;AAEhD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,6DAA6D;AAC7D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAOtF,MAAM,UAAU,SAAS,CAAC,SAAiB,EAAE,UAAuB,EAAE;IACpE,MAAM,OAAO,GACX,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACrF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAE7E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QACnC,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,SAAS;QAClE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;YAAE,SAAS;QACxE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,QAAuB,EAAE,KAAe;IAKrE,IAAI,MAAM,GAAa,EAAE,CAAC;IAC1B,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAwB,CAAC;QAC3D,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAChF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC;IACpC,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,11 @@
1
+ export type InitOptions = {
2
+ workspace: string;
3
+ /** Answer yes to the repo list rather than asking. */
4
+ assumeYes: boolean;
5
+ /** Write the files, print the lines, and do not build the index. */
6
+ skipIndex: boolean;
7
+ };
8
+ export declare function parseArgs(argv: string[], cwd?: string): InitOptions | string;
9
+ export declare function init(argv: string[]): Promise<number>;
10
+ /** `carrick templates <name>`, so the workflow above is one command. */
11
+ export declare function templates(argv: string[]): number;
@@ -0,0 +1,228 @@
1
+ // `carrick init`: the one command, run in the folder that holds the repos.
2
+ //
3
+ // It does the four things that are otherwise four installs — the repo list,
4
+ // the first index, the agent hooks, and the editor line — and prints the two
5
+ // it cannot do for you. Everything it writes is a file you can read and edit
6
+ // afterwards; nothing here is state only Carrick understands.
7
+ //
8
+ // Re-running it updates rather than duplicates: the repo list keeps the order
9
+ // and the hand-written entries it already had, and the hook entries are merged
10
+ // by command rather than replacing the settings file's `hooks` key.
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import readline from "node:readline/promises";
14
+ import { spawnSync } from "node:child_process";
15
+ import { describeIdentity, githubIdentity } from "./identity.js";
16
+ import { findRepos, mergeWorkspace } from "./repos.js";
17
+ import { hookCommand, mergeCarrickHooks } from "./settings.js";
18
+ import { renderTemplate } from "../templates.js";
19
+ import { resolveNativeBinary, nativeEnv, packageRoot } from "../native.js";
20
+ const WORKSPACE_FILE = "carrick-workspace.json";
21
+ const SETTINGS_FILE = path.join(".claude", "settings.json");
22
+ const MCP_LINE = "claude mcp add --scope user --transport http carrick https://api.carrick.tools/mcp";
23
+ export function parseArgs(argv, cwd = process.cwd()) {
24
+ const options = { workspace: cwd, assumeYes: false, skipIndex: false };
25
+ for (let index = 0; index < argv.length; index += 1) {
26
+ const argument = argv[index];
27
+ switch (argument) {
28
+ case "--yes":
29
+ case "-y":
30
+ options.assumeYes = true;
31
+ break;
32
+ case "--skip-index":
33
+ options.skipIndex = true;
34
+ break;
35
+ case "--workspace":
36
+ case "-w": {
37
+ const value = argv[index + 1];
38
+ if (!value)
39
+ return "--workspace needs a directory";
40
+ options.workspace = path.resolve(cwd, value);
41
+ index += 1;
42
+ break;
43
+ }
44
+ case "--help":
45
+ case "-h":
46
+ return help();
47
+ default:
48
+ if (argument?.startsWith("-"))
49
+ return `unknown option for \`carrick init\`: ${argument}`;
50
+ options.workspace = path.resolve(cwd, argument ?? ".");
51
+ }
52
+ }
53
+ return options;
54
+ }
55
+ function help() {
56
+ return [
57
+ "carrick init [DIRECTORY]",
58
+ "",
59
+ "Set up the folder that holds your repos: the list to index, the first index,",
60
+ "and the wiring that puts its answers where you work.",
61
+ "",
62
+ " -w, --workspace DIR The folder holding the repos (default: this one)",
63
+ " -y, --yes Take the repo list as proposed",
64
+ " --skip-index Write the files and print the lines, index later",
65
+ ].join("\n");
66
+ }
67
+ function say(line = "") {
68
+ process.stdout.write(`${line}\n`);
69
+ }
70
+ async function confirm(question) {
71
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
72
+ try {
73
+ const answer = await rl.question(`${question} [Y/n] `);
74
+ return answer.trim() === "" || /^y(es)?$/i.test(answer.trim());
75
+ }
76
+ finally {
77
+ rl.close();
78
+ }
79
+ }
80
+ function writeIfChanged(target, body) {
81
+ const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null;
82
+ if (existing === body)
83
+ return "unchanged";
84
+ fs.mkdirSync(path.dirname(target), { recursive: true });
85
+ fs.writeFileSync(target, body);
86
+ return "written";
87
+ }
88
+ /** Whether a command answers on this machine, for a line we should not print. */
89
+ function onPath(command) {
90
+ const probe = spawnSync(process.platform === "win32" ? "where" : "which", [command], {
91
+ stdio: "ignore",
92
+ });
93
+ return probe.status === 0;
94
+ }
95
+ export async function init(argv) {
96
+ const parsed = parseArgs(argv);
97
+ if (typeof parsed === "string") {
98
+ process.stdout.write(`${parsed}\n`);
99
+ return parsed.startsWith("carrick init") ? 0 : 2;
100
+ }
101
+ const { workspace } = parsed;
102
+ if (!fs.existsSync(workspace)) {
103
+ process.stderr.write(`carrick init: ${workspace} is not a directory on this machine\n`);
104
+ return 1;
105
+ }
106
+ // 1. Who is asking. There is no anonymous tier: the local index draws on the
107
+ // identified free tier's allowance, so this is the first question and not
108
+ // a thing to discover halfway through a scan.
109
+ const lookup = githubIdentity();
110
+ if (!lookup.identity) {
111
+ process.stderr.write(`${lookup.problem}\n`);
112
+ return 1;
113
+ }
114
+ say(describeIdentity(lookup.identity));
115
+ say();
116
+ // 2. The repos, proposed and confirmed.
117
+ const workspaceFile = path.join(workspace, WORKSPACE_FILE);
118
+ const existing = fs.existsSync(workspaceFile) ? fs.readFileSync(workspaceFile, "utf8") : null;
119
+ const found = findRepos(workspace);
120
+ const merged = mergeWorkspace(existing, found);
121
+ if (merged.repos.length === 0) {
122
+ process.stderr.write(`carrick init: no repos in ${workspace}. Carrick indexes the directories beside each other, so run it in the folder that holds them, or pass one: carrick init ~/code\n`);
123
+ return 1;
124
+ }
125
+ say(`Repos to index, in ${workspace}:`);
126
+ for (const repo of merged.repos) {
127
+ say(` ${repo}${merged.added.includes(repo) ? " (new)" : ""}`);
128
+ }
129
+ say();
130
+ if (!parsed.assumeYes && merged.added.length > 0) {
131
+ const ok = await confirm("Index these?");
132
+ if (!ok) {
133
+ say(`Nothing written. Edit ${WORKSPACE_FILE} yourself and run carrick index.`);
134
+ return 0;
135
+ }
136
+ }
137
+ const wrote = writeIfChanged(workspaceFile, merged.body);
138
+ say(`${wrote === "written" ? "wrote" : "unchanged"} ${WORKSPACE_FILE}`);
139
+ // 3. The agent hooks. Merged by command: this file may already hold a user's
140
+ // own hooks, or the hook pack the hosted index installs.
141
+ const settingsFile = path.join(workspace, SETTINGS_FILE);
142
+ const settings = fs.existsSync(settingsFile) ? fs.readFileSync(settingsFile, "utf8") : null;
143
+ const command = hookCommand({ onPath });
144
+ try {
145
+ const hooks = mergeCarrickHooks(settings, command.command);
146
+ const wroteHooks = writeIfChanged(settingsFile, hooks.body);
147
+ say(`${wroteHooks === "written" ? "wrote" : "unchanged"} ${SETTINGS_FILE}`);
148
+ if (!command.bare) {
149
+ say(` \`carrick\` is not on PATH here, so those hooks name this install: ${command.command}.`);
150
+ say(" An npx run leaves nothing on PATH afterwards, and a hook that cannot find carrick");
151
+ say(" says nothing rather than failing your edit. `npm install -g carrick` and run init");
152
+ say(" again to write the short command instead.");
153
+ }
154
+ }
155
+ catch (error) {
156
+ say(`skipped ${SETTINGS_FILE}: it is not valid JSON (${error.message}). Fix it and run carrick init again; nothing was overwritten.`);
157
+ }
158
+ // 4. The index itself.
159
+ if (parsed.skipIndex) {
160
+ say();
161
+ say("Skipped the index. Build it with: carrick index");
162
+ }
163
+ else {
164
+ say();
165
+ const binary = resolveNativeBinary();
166
+ if (!binary.binary) {
167
+ process.stderr.write(`carrick init: ${binary.problem}\n`);
168
+ return 1;
169
+ }
170
+ const scan = spawnSync(binary.binary, ["index", "--workspace", workspace], {
171
+ stdio: "inherit",
172
+ env: nativeEnv(),
173
+ });
174
+ if (scan.status !== 0) {
175
+ process.stderr.write(`carrick init: the first index did not finish. The files above are written, so fix what it reported and run: carrick index\n`);
176
+ return scan.status ?? 1;
177
+ }
178
+ }
179
+ // 5. The lines it cannot run for you.
180
+ say();
181
+ say("Next, and neither of these is done for you:");
182
+ say();
183
+ say(" The org index, for work that crosses repos you do not have on disk:");
184
+ say(` ${MCP_LINE}`);
185
+ say();
186
+ say(" The CI check, once per repo (it needs no secret):");
187
+ say(" carrick templates workflow > .github/workflows/carrick.yml");
188
+ say();
189
+ if (onPath("code")) {
190
+ say(" VS Code, for diagnostics in the Problems panel:");
191
+ say(" code --install-extension carrick-tools.carrick");
192
+ say();
193
+ }
194
+ say(`Start Claude Code in this folder — the hooks above are ${SETTINGS_FILE} here, and`);
195
+ say("the index covers every repo in it. The hooks need no plugin; the language server does:");
196
+ const plugin = path.join(packageRoot(), "plugin");
197
+ say(` claude --plugin-dir ${fs.existsSync(plugin) ? plugin : "<carrick checkout>/plugin"}`);
198
+ if (!command.bare) {
199
+ say(" That plugin's language server is started as `carrick`, which this machine cannot resolve;");
200
+ say(" install it globally first, or the hooks above are the channel.");
201
+ }
202
+ say();
203
+ say("Then: edit a file with a route or a call in it, and Carrick answers on the edit.");
204
+ return 0;
205
+ }
206
+ /** `carrick templates <name>`, so the workflow above is one command. */
207
+ export function templates(argv) {
208
+ const [name, ...rest] = argv;
209
+ const variables = {};
210
+ for (let index = 0; index < rest.length; index += 2) {
211
+ const key = rest[index];
212
+ const value = rest[index + 1];
213
+ if (!key?.startsWith("--") || value === undefined) {
214
+ process.stderr.write("carrick templates: expected --name value pairs\n");
215
+ return 2;
216
+ }
217
+ variables[key.slice(2).toUpperCase().replace(/-/g, "_")] = value;
218
+ }
219
+ try {
220
+ process.stdout.write(renderTemplate(name, variables));
221
+ return 0;
222
+ }
223
+ catch (error) {
224
+ process.stderr.write(`carrick templates: ${error.message}\n`);
225
+ return 2;
226
+ }
227
+ }
228
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/init/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The entry point in this package, for a settings file that must name a path.
3
+ *
4
+ * A hook command is a line handed to a shell, so this file has to be runnable
5
+ * on its own: it carries a `#!/usr/bin/env node` shebang, it is executable in
6
+ * the repository, and npm sets the mode on a `bin` target at install time
7
+ * (verified 2026-09-09 against a packed tarball installed with
8
+ * `--ignore-scripts`: 644 in the archive, 755 once installed).
9
+ */
10
+ export declare function ownEntryPoint(root?: string): string;
11
+ export type HookCommandChoice = {
12
+ /** What a hook entry runs, quoted if it has to be. */
13
+ command: string;
14
+ /** True when the bare name resolved and the settings file stays portable. */
15
+ bare: boolean;
16
+ };
17
+ /**
18
+ * The command to write into a settings file.
19
+ *
20
+ * `onPath` is injected so a test can state both machines; in `carrick init` it
21
+ * is `which carrick`.
22
+ */
23
+ export declare function hookCommand(options: {
24
+ onPath: (command: string) => boolean;
25
+ root?: string;
26
+ }): HookCommandChoice;
27
+ export type HookEntry = {
28
+ type: "command";
29
+ command: string;
30
+ timeout?: number;
31
+ };
32
+ export type HookGroup = {
33
+ matcher?: string;
34
+ hooks: HookEntry[];
35
+ };
36
+ /** What this package registers, and the only entries it will ever remove. */
37
+ export declare function carrickHooks(command?: string): Record<string, HookGroup[]>;
38
+ export type MergeResult = {
39
+ /** The file body to write. */
40
+ body: string;
41
+ /** True when it differs from what was there. */
42
+ changed: boolean;
43
+ };
44
+ /**
45
+ * Merge this package's hooks into a settings file, or produce one.
46
+ *
47
+ * Throws when the existing file is not JSON: a settings file a user has hand
48
+ * edited into invalid JSON is a thing to report, never a thing to overwrite.
49
+ */
50
+ export declare function mergeCarrickHooks(existing: string | null, command?: string): MergeResult;
@@ -0,0 +1,138 @@
1
+ // The hook entries `carrick init` writes into a project's `.claude/settings.json`.
2
+ //
3
+ // Merged entry by entry, never wholesale. Two things already write to that
4
+ // file: a user's own hooks, and the MCP hook pack the cloud's `scaffold` tool
5
+ // installs for the hosted index. Replacing the `hooks` key would silently
6
+ // uninstall either of them, so this only ever touches entries whose command is
7
+ // this CLI, and leaves the rest of the file — keys, order and formatting of
8
+ // what it does not own — as it found it.
9
+ //
10
+ // The command it writes is `carrick` when `carrick` resolves on PATH, and this
11
+ // install's own entry point when it does not: `npx carrick init` puts the shim
12
+ // on PATH for the length of that one command, so a bare `carrick` in a settings
13
+ // file it wrote would fail on every edit afterwards, silently, because the hook
14
+ // is built never to fail an edit (#837).
15
+ import path from "node:path";
16
+ import { packageRoot } from "../native.js";
17
+ /**
18
+ * The entry point in this package, for a settings file that must name a path.
19
+ *
20
+ * A hook command is a line handed to a shell, so this file has to be runnable
21
+ * on its own: it carries a `#!/usr/bin/env node` shebang, it is executable in
22
+ * the repository, and npm sets the mode on a `bin` target at install time
23
+ * (verified 2026-09-09 against a packed tarball installed with
24
+ * `--ignore-scripts`: 644 in the archive, 755 once installed).
25
+ */
26
+ export function ownEntryPoint(root = packageRoot()) {
27
+ return path.join(root, "bin", "carrick.mjs");
28
+ }
29
+ /**
30
+ * The command to write into a settings file.
31
+ *
32
+ * `onPath` is injected so a test can state both machines; in `carrick init` it
33
+ * is `which carrick`.
34
+ */
35
+ export function hookCommand(options) {
36
+ if (options.onPath("carrick"))
37
+ return { command: "carrick", bare: true };
38
+ const entry = ownEntryPoint(options.root);
39
+ return { command: /\s/.test(entry) ? `"${entry}"` : entry, bare: false };
40
+ }
41
+ /** What this package registers, and the only entries it will ever remove. */
42
+ export function carrickHooks(command = "carrick") {
43
+ return {
44
+ // Attached to the tool result, so the verdicts arrive in the same turn as
45
+ // the edit rather than the turn after it.
46
+ PostToolUse: [
47
+ {
48
+ matcher: "Write|Edit|MultiEdit",
49
+ hooks: [{ type: "command", command: `${command} hook post-edit`, timeout: 15 }],
50
+ },
51
+ ],
52
+ // No matcher: a resumed, cleared or compacted session has lost the map and
53
+ // re-orients for one read of the index.
54
+ SessionStart: [
55
+ {
56
+ hooks: [{ type: "command", command: `${command} hook session-start`, timeout: 30 }],
57
+ },
58
+ ],
59
+ };
60
+ }
61
+ /**
62
+ * Whether a settings entry is one of ours, whatever it names us by.
63
+ *
64
+ * An entry written on a machine with no `carrick` on PATH holds an absolute
65
+ * path, so the test is the file it runs and the `hook` subcommand, not a fixed
66
+ * prefix: otherwise re-running init on such a machine would leave the old entry
67
+ * behind and add a second one.
68
+ */
69
+ const HOOK_CALL = /^"?(.*?)"?\s+hook\s/;
70
+ function isOurs(entry) {
71
+ if (typeof entry !== "object" || entry === null)
72
+ return false;
73
+ const command = entry.command;
74
+ if (typeof command !== "string")
75
+ return false;
76
+ const match = HOOK_CALL.exec(command.trim());
77
+ if (!match?.[1])
78
+ return false;
79
+ const name = path.basename(match[1]);
80
+ return name === "carrick" || name === "carrick.mjs";
81
+ }
82
+ /** Strip our entries from one event's groups, keeping everyone else's. */
83
+ function withoutOurs(groups) {
84
+ if (!Array.isArray(groups))
85
+ return [];
86
+ const kept = [];
87
+ for (const group of groups) {
88
+ if (typeof group !== "object" || group === null) {
89
+ kept.push(group);
90
+ continue;
91
+ }
92
+ const hooks = group.hooks;
93
+ if (!Array.isArray(hooks)) {
94
+ kept.push(group);
95
+ continue;
96
+ }
97
+ const others = hooks.filter((entry) => !isOurs(entry));
98
+ // A group that held only our entry goes with it; one that held a mix keeps
99
+ // the rest of its entries and its matcher.
100
+ if (others.length > 0)
101
+ kept.push({ ...group, hooks: others });
102
+ }
103
+ return kept;
104
+ }
105
+ /**
106
+ * Merge this package's hooks into a settings file, or produce one.
107
+ *
108
+ * Throws when the existing file is not JSON: a settings file a user has hand
109
+ * edited into invalid JSON is a thing to report, never a thing to overwrite.
110
+ */
111
+ export function mergeCarrickHooks(existing, command = "carrick") {
112
+ const base = existing == null || existing.trim() === ""
113
+ ? {}
114
+ : JSON.parse(existing);
115
+ const hooksBefore = (base["hooks"] ?? {});
116
+ const hooks = {};
117
+ for (const [event, groups] of Object.entries(hooksBefore)) {
118
+ hooks[event] = withoutOurs(groups);
119
+ }
120
+ for (const [event, groups] of Object.entries(carrickHooks(command))) {
121
+ hooks[event] = [...(hooks[event] ?? []), ...groups];
122
+ }
123
+ // An event that only ever held our entry, and no longer does, leaves no
124
+ // empty array behind to puzzle over.
125
+ for (const [event, groups] of Object.entries(hooks)) {
126
+ if (Array.isArray(groups) && groups.length === 0)
127
+ delete hooks[event];
128
+ }
129
+ const merged = {};
130
+ for (const [key, value] of Object.entries(base)) {
131
+ merged[key] = key === "hooks" ? hooks : value;
132
+ }
133
+ if (!("hooks" in merged))
134
+ merged["hooks"] = hooks;
135
+ const body = `${JSON.stringify(merged, null, 2)}\n`;
136
+ return { body, changed: body !== existing };
137
+ }
138
+ //# sourceMappingURL=settings.js.map