@fenglimg/fabric-cli 2.0.0 → 2.1.0-rc.2

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -5
  3. package/dist/chunk-BATF4PEJ.js +361 -0
  4. package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
  5. package/dist/chunk-F46ORPOA.js +903 -0
  6. package/dist/chunk-HFQVXY6P.js +86 -0
  7. package/dist/chunk-L4Q55UC4.js +52 -0
  8. package/dist/chunk-LFIKMVY7.js +27 -0
  9. package/dist/chunk-MF3OTILQ.js +544 -0
  10. package/dist/chunk-PWLW3B57.js +18 -0
  11. package/dist/chunk-RYAFBNES.js +33 -0
  12. package/dist/chunk-T5RPGCCM.js +40 -0
  13. package/dist/chunk-WU6GAPKH.js +36 -0
  14. package/dist/config-XJIPZNUP.js +13 -0
  15. package/dist/doctor-QVNPHLJK.js +920 -0
  16. package/dist/index.js +23 -8
  17. package/dist/{init-BIRSIOXO.js → install-2HDO5FTQ.js} +807 -705
  18. package/dist/metrics-ACEQFPDU.js +122 -0
  19. package/dist/onboard-coverage-MFCAEBDO.js +220 -0
  20. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
  21. package/dist/scope-explain-2F2R5URO.js +33 -0
  22. package/dist/status-GLQWLWH6.js +23 -0
  23. package/dist/store-XTSE5TY6.js +105 -0
  24. package/dist/sync-BJCWDPNC.js +245 -0
  25. package/dist/uninstall-TAXSUSKH.js +1073 -0
  26. package/dist/whoami-B6AEMSEV.js +31 -0
  27. package/package.json +30 -5
  28. package/templates/hooks/cite-policy-evict.cjs +231 -0
  29. package/templates/hooks/configs/README.md +29 -6
  30. package/templates/hooks/configs/claude-code.json +14 -3
  31. package/templates/hooks/configs/codex-hooks.json +6 -3
  32. package/templates/hooks/configs/cursor-hooks.json +8 -10
  33. package/templates/hooks/fabric-hint.cjs +873 -105
  34. package/templates/hooks/knowledge-hint-broad.cjs +549 -135
  35. package/templates/hooks/knowledge-hint-narrow.cjs +830 -26
  36. package/templates/hooks/lib/banner-i18n.cjs +309 -0
  37. package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
  38. package/templates/hooks/lib/cite-contract-reminder.cjs +179 -0
  39. package/templates/hooks/lib/cite-line-parser.cjs +180 -0
  40. package/templates/hooks/lib/client-adapter.cjs +106 -0
  41. package/templates/hooks/lib/config-cache.cjs +107 -0
  42. package/templates/hooks/lib/state-store.cjs +84 -0
  43. package/templates/hooks/lib/summary-fallback.cjs +210 -0
  44. package/templates/skills/fabric-archive/SKILL.md +97 -419
  45. package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
  46. package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
  47. package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
  48. package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
  49. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
  50. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
  51. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
  52. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
  53. package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
  54. package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
  55. package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
  56. package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
  57. package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
  58. package/templates/skills/fabric-import/SKILL.md +77 -514
  59. package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
  60. package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
  61. package/templates/skills/fabric-import/ref/output-contract.md +61 -0
  62. package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
  63. package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
  64. package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
  65. package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
  66. package/templates/skills/fabric-review/SKILL.md +90 -284
  67. package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
  68. package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
  69. package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
  70. package/templates/skills/fabric-review/ref/output-contract.md +58 -0
  71. package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
  72. package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
  73. package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
  74. package/templates/skills/fabric-sync/SKILL.md +46 -0
  75. package/templates/skills/lib/shared-policy.md +69 -0
  76. package/dist/chunk-6ICJICVU.js +0 -10
  77. package/dist/chunk-74SZWYPH.js +0 -658
  78. package/dist/chunk-EYIDD2YS.js +0 -1000
  79. package/dist/doctor-T7JWODKG.js +0 -282
  80. package/dist/hooks-Y74Y5LQS.js +0 -12
  81. package/dist/scan-LMK3UCWL.js +0 -22
  82. package/dist/serve-H554BHLG.js +0 -124
  83. package/templates/agents-md/AGENTS.md.template +0 -59
  84. package/templates/bootstrap/CLAUDE.md +0 -8
  85. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  86. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
@@ -1,444 +1,431 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ClaudeCodeCLIWriter,
4
- CursorWriter,
5
- addArchiveSkillPointer,
6
- createServerEntry,
7
- hooksCommand,
3
+ cleanupDeprecatedSkills,
8
4
  installArchiveHintHook,
5
+ installCitePolicyEvictHook,
9
6
  installFabricArchiveSkill,
10
7
  installFabricImportSkill,
11
8
  installFabricReviewSkill,
12
- installHooks,
9
+ installFabricSyncSkill,
10
+ installHookLibs,
13
11
  installKnowledgeHintBroadHook,
14
12
  installKnowledgeHintNarrowHook,
13
+ installSharedSkillLib,
15
14
  mergeClaudeCodeHookConfig,
16
15
  mergeCodexHookConfig,
17
16
  mergeCursorHookConfig,
18
- normalizeConfigPath,
19
- writeJsonClientConfig
20
- } from "./chunk-74SZWYPH.js";
21
- import {
22
- detectFramework,
23
- runInitScan
24
- } from "./chunk-EYIDD2YS.js";
17
+ readFabricLanguagePreference,
18
+ writeClaudeBootstrapThinShell,
19
+ writeCodexBootstrapManagedBlock,
20
+ writeCursorBootstrapManagedBlock,
21
+ writeFabricAgentsSnapshot
22
+ } from "./chunk-F46ORPOA.js";
25
23
  import {
26
24
  displayWidth,
27
25
  padEnd,
28
26
  paint
29
27
  } from "./chunk-WWNXR34K.js";
30
- import {
31
- t
32
- } from "./chunk-6ICJICVU.js";
33
28
  import {
34
29
  createDebugLogger,
35
30
  resolveDevMode
36
- } from "./chunk-OBQU6NHO.js";
31
+ } from "./chunk-COI5VDFU.js";
32
+ import {
33
+ installMcpClients
34
+ } from "./chunk-BATF4PEJ.js";
35
+ import {
36
+ detectClientSupports
37
+ } from "./chunk-MF3OTILQ.js";
38
+ import {
39
+ t
40
+ } from "./chunk-PWLW3B57.js";
41
+ import {
42
+ globalConfigPath,
43
+ loadGlobalConfig,
44
+ resolveGlobalRoot,
45
+ saveGlobalConfig
46
+ } from "./chunk-RYAFBNES.js";
37
47
 
38
- // src/commands/init.ts
39
- import { randomUUID } from "crypto";
40
- import { homedir as homedir4 } from "os";
48
+ // src/commands/install.ts
49
+ import { randomUUID as randomUUID2 } from "crypto";
50
+ import { homedir } from "os";
41
51
  import * as childProcess from "child_process";
42
- import { appendFileSync, existsSync as existsSync6, mkdirSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
43
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join5, resolve as resolve5 } from "path";
52
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync4, writeFileSync } from "fs";
53
+ import { dirname, isAbsolute as isAbsolute3, join as join6, resolve as resolve3 } from "path";
44
54
  import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
45
55
  import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
46
- import { atomicWriteJson, atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
47
- import { defineCommand as defineCommand2 } from "citty";
48
- import { checkLockOrThrow } from "@fenglimg/fabric-server";
49
-
50
- // src/commands/config.ts
51
- import { existsSync as existsSync4 } from "fs";
52
- import { readFile as readFile2 } from "fs/promises";
53
- import { resolve as resolve3 } from "path";
54
- import { fileURLToPath } from "url";
56
+ import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
55
57
  import { defineCommand } from "citty";
56
58
 
57
- // src/config/resolver.ts
58
- import { existsSync as existsSync3 } from "fs";
59
- import { join as join3 } from "path";
60
- import { homedir as homedir3 } from "os";
61
-
62
- // src/config/claude-code.ts
63
- import { existsSync } from "fs";
64
- import { join, resolve } from "path";
65
- import { homedir, platform } from "os";
66
- function getClaudeDesktopConfigPath() {
67
- const os = platform();
68
- if (os === "darwin") {
69
- return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
70
- }
71
- if (os === "win32") {
72
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
73
- }
74
- return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
75
- }
76
- var ClaudeCodeDesktopWriter = class {
77
- clientKind = "ClaudeCodeDesktop";
78
- configuredPath;
79
- constructor(configuredPath) {
80
- this.configuredPath = configuredPath;
81
- }
82
- async detect(_workspaceRoot, overridePath) {
83
- const configPath = normalizeConfigPath(overridePath ?? this.configuredPath ?? getClaudeDesktopConfigPath());
84
- return existsSync(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
85
- }
86
- async write(serverPath, workspaceRoot, overridePath) {
87
- const configPath = await this.detect(workspaceRoot, overridePath);
88
- if (configPath === null) {
89
- return;
59
+ // src/install/hooks-orchestrator.ts
60
+ import { existsSync, statSync } from "fs";
61
+ import { isAbsolute, join, resolve } from "path";
62
+ async function installHooks(target, _options = {}) {
63
+ const normalizedTarget = normalizeTarget(target);
64
+ assertExistingDirectory(normalizedTarget);
65
+ const results = [];
66
+ results.push(...await runStep(() => installFabricArchiveSkill(normalizedTarget)));
67
+ results.push(...await runStep(() => installFabricReviewSkill(normalizedTarget)));
68
+ results.push(...await runStep(() => installFabricImportSkill(normalizedTarget)));
69
+ results.push(...await runStep(() => installFabricSyncSkill(normalizedTarget)));
70
+ results.push(...await runStep(() => installSharedSkillLib(normalizedTarget)));
71
+ results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
72
+ results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
73
+ results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
74
+ results.push(...await runStep(() => installCitePolicyEvictHook(normalizedTarget)));
75
+ results.push(...await runStep(() => installHookLibs(normalizedTarget)));
76
+ results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
77
+ results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
78
+ results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
79
+ results.push(await runSingleStep("bootstrap-snapshot", () => writeFabricAgentsSnapshot(normalizedTarget)));
80
+ results.push(await runSingleStep("bootstrap-claude", () => writeClaudeBootstrapThinShell(normalizedTarget)));
81
+ results.push(await runSingleStep("bootstrap-codex", () => writeCodexBootstrapManagedBlock(normalizedTarget)));
82
+ results.push(await runSingleStep("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(normalizedTarget)));
83
+ results.push(...validateHookPaths(normalizedTarget));
84
+ return summarizeResults(results);
85
+ }
86
+ function validateHookPaths(projectRoot) {
87
+ const scripts = [
88
+ { stepSuffix: "", hookFile: "fabric-hint.cjs" },
89
+ { stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
90
+ { stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
91
+ ];
92
+ const clients = [
93
+ {
94
+ client: "claude",
95
+ configRel: join(".claude", "settings.json"),
96
+ hookDir: join(".claude", "hooks")
97
+ },
98
+ {
99
+ client: "codex",
100
+ configRel: join(".codex", "hooks.json"),
101
+ hookDir: join(".codex", "hooks")
102
+ },
103
+ {
104
+ client: "cursor",
105
+ configRel: join(".cursor", "hooks.json"),
106
+ hookDir: join(".cursor", "hooks")
90
107
  }
91
- await writeJsonClientConfig(configPath, {
92
- command: process.execPath,
93
- args: [serverPath]
94
- });
95
- }
96
- };
97
-
98
- // src/config/toml.ts
99
- import { existsSync as existsSync2 } from "fs";
100
- import { mkdir, readFile } from "fs/promises";
101
- import { dirname, join as join2, resolve as resolve2 } from "path";
102
- import { homedir as homedir2 } from "os";
103
- import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
104
- function expandHome(filePath) {
105
- if (filePath === "~") {
106
- return homedir2();
107
- }
108
- if (filePath.startsWith("~/")) {
109
- return join2(homedir2(), filePath.slice(2));
110
- }
111
- return filePath;
112
- }
113
- function escapeTomlString(value) {
114
- return JSON.stringify(value);
115
- }
116
- function serializeTomlStringArray(values) {
117
- return `[${values.map((value) => escapeTomlString(value)).join(", ")}]`;
118
- }
119
- function serializeTomlInlineTable(values) {
120
- const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
121
- return `{ ${entries.join(", ")} }`;
122
- }
123
- function serializeCodexServerBlock(serverName, serverEntry) {
124
- const lines = [
125
- `[mcp_servers.${serverName}]`,
126
- `command = ${escapeTomlString(serverEntry.command)}`,
127
- `args = ${serializeTomlStringArray(serverEntry.args)}`
128
108
  ];
129
- if (serverEntry.env !== void 0 && Object.keys(serverEntry.env).length > 0) {
130
- lines.push(`env = ${serializeTomlInlineTable(serverEntry.env)}`);
109
+ const results = [];
110
+ for (const { client, configRel, hookDir } of clients) {
111
+ const configPath = resolve(projectRoot, configRel);
112
+ if (!existsSync(configPath)) {
113
+ results.push({
114
+ step: `hook-validate-${client}`,
115
+ path: configPath,
116
+ status: "skipped",
117
+ message: "missing-config"
118
+ });
119
+ continue;
120
+ }
121
+ for (const { stepSuffix, hookFile } of scripts) {
122
+ const expectedHookPath = resolve(projectRoot, hookDir, hookFile);
123
+ const expectedHookRel = join(hookDir, hookFile);
124
+ const step = `hook-validate-${client}${stepSuffix}`;
125
+ if (!existsSync(expectedHookPath)) {
126
+ results.push({
127
+ step,
128
+ path: expectedHookPath,
129
+ status: "error",
130
+ message: `hook script missing: ${expectedHookRel}`
131
+ });
132
+ continue;
133
+ }
134
+ results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
135
+ }
131
136
  }
132
- return `${lines.join("\n")}
133
- `;
134
- }
135
- function trimTrailingBlankLines(value) {
136
- return value.replace(/\s+$/u, "");
137
+ return results;
137
138
  }
138
- function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
139
- const block = serializeCodexServerBlock(serverName, serverEntry);
140
- const normalized = rawConfig.replace(/\r\n/g, "\n");
141
- const legacyPattern = new RegExp(String.raw`\n?\[mcp\.servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`, "g");
142
- const currentPattern = new RegExp(
143
- String.raw`\n?\[mcp_servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
144
- "g"
145
- );
146
- const withoutLegacy = normalized.replace(legacyPattern, "");
147
- const withoutExisting = withoutLegacy.replace(currentPattern, "");
148
- const trimmed = trimTrailingBlankLines(withoutExisting);
149
- if (trimmed.length === 0) {
150
- return block;
139
+ async function runStep(fn) {
140
+ try {
141
+ return await fn();
142
+ } catch (error) {
143
+ return [
144
+ {
145
+ step: "hook-install",
146
+ path: "",
147
+ status: "error",
148
+ message: error instanceof Error ? error.message : String(error)
149
+ }
150
+ ];
151
151
  }
152
- return `${trimmed}
153
-
154
- ${block}`;
155
152
  }
156
- async function readTomlConfigText(configPath) {
153
+ async function runSingleStep(step, fn) {
157
154
  try {
158
- return await readFile(configPath, "utf8");
155
+ return await fn();
159
156
  } catch (error) {
160
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
161
- return "";
157
+ return {
158
+ step,
159
+ path: "",
160
+ status: "error",
161
+ message: error instanceof Error ? error.message : String(error)
162
+ };
163
+ }
164
+ }
165
+ function summarizeResults(results) {
166
+ const installed = [];
167
+ const skipped = [];
168
+ const errors = [];
169
+ for (const r of results) {
170
+ switch (r.status) {
171
+ case "written":
172
+ installed.push(r.path);
173
+ break;
174
+ case "skipped":
175
+ skipped.push(r.path);
176
+ break;
177
+ case "error":
178
+ errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
179
+ break;
162
180
  }
163
- throw error;
164
181
  }
182
+ return { installed, skipped, errors };
165
183
  }
166
- var CodexTOMLConfigWriter = class {
167
- clientKind = "CodexCLI";
168
- configuredPath;
169
- constructor(configuredPath) {
170
- this.configuredPath = configuredPath;
184
+ function normalizeTarget(targetInput) {
185
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
186
+ }
187
+ function assertExistingDirectory(target) {
188
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
189
+ throw new Error(t("cli.shared.target-invalid", { target }));
171
190
  }
172
- async detect(_workspaceRoot, overridePath) {
173
- const explicitPath = overridePath ?? this.configuredPath;
174
- if (explicitPath !== void 0) {
175
- return resolve2(expandHome(explicitPath));
176
- }
177
- const codexDir = join2(homedir2(), ".codex");
178
- return existsSync2(codexDir) ? resolve2(join2(codexDir, "config.toml")) : null;
191
+ }
192
+
193
+ // src/install/run-global-install.ts
194
+ import { execFileSync as execFileSync2 } from "child_process";
195
+ import { randomUUID } from "crypto";
196
+ import { mkdirSync, mkdtempSync, renameSync } from "fs";
197
+ import { tmpdir } from "os";
198
+ import { join as join3 } from "path";
199
+ import { STORES_ROOT_DIR as STORES_ROOT_DIR2, addMountedStore, readStoreIdentity } from "@fenglimg/fabric-shared";
200
+
201
+ // src/store/uid.ts
202
+ import { execFileSync } from "child_process";
203
+ import { createHash } from "crypto";
204
+ function deriveUid() {
205
+ let email = "";
206
+ try {
207
+ email = execFileSync("git", ["config", "user.email"], {
208
+ encoding: "utf8",
209
+ stdio: ["ignore", "pipe", "ignore"]
210
+ }).trim();
211
+ } catch {
212
+ email = "";
179
213
  }
180
- async write(serverPath, workspaceRoot, overridePath) {
181
- const configPath = await this.detect(workspaceRoot, overridePath);
182
- if (configPath === null) {
183
- return;
184
- }
185
- const rawConfig = await readTomlConfigText(configPath);
186
- const nextConfig = upsertCodexServerBlock(rawConfig, "fabric", createServerEntry(serverPath));
187
- await mkdir(dirname(configPath), { recursive: true });
188
- await atomicWriteText(configPath, nextConfig);
214
+ if (email === "") {
215
+ return "u-anon";
189
216
  }
190
- };
217
+ const hash = createHash("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
218
+ return `u-${hash}`;
219
+ }
191
220
 
192
- // src/config/resolver.ts
193
- import { clientPathsSchema, fabricConfigSchema } from "@fenglimg/fabric-shared";
194
- function hasExplicitPath(clientPaths, key) {
195
- return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
196
- }
197
- function addIfDetected(writers, detected, createWriter, configuredPath) {
198
- if (configuredPath !== void 0 || detected) {
199
- writers.push(createWriter(configuredPath));
200
- }
201
- }
202
- function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
203
- const clientPaths = fabricConfig.clientPaths;
204
- const writers = [];
205
- const claudeMcpScope = opts.claudeMcpScope ?? "project";
206
- addIfDetected(
207
- writers,
208
- existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude")),
209
- (configuredPath) => new ClaudeCodeCLIWriter(configuredPath, claudeMcpScope),
210
- hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
211
- );
212
- addIfDetected(
213
- writers,
214
- existsSync3(getClaudeDesktopConfigPath()),
215
- (configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
216
- hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
217
- );
218
- addIfDetected(
219
- writers,
220
- existsSync3(join3(workspaceRoot, ".cursor")),
221
- (configuredPath) => new CursorWriter(configuredPath),
222
- hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
223
- );
224
- addIfDetected(
225
- writers,
226
- existsSync3(join3(homedir3(), ".codex")),
227
- (configuredPath) => new CodexTOMLConfigWriter(configuredPath),
228
- hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
229
- );
230
- return writers;
231
- }
232
- function detectClientSupports(workspaceRoot, fabricConfig = {}) {
233
- const clientPaths = fabricConfig.clientPaths;
234
- const claudeDetected = existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude"));
235
- const claudeDesktopDetected = existsSync3(getClaudeDesktopConfigPath());
236
- const cursorDetected = existsSync3(join3(workspaceRoot, ".cursor"));
237
- const codexDetected = existsSync3(join3(homedir3(), ".codex"));
238
- return [
239
- {
240
- clientKind: "ClaudeCodeCLI",
241
- label: "Claude Code CLI",
242
- detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
243
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
244
- configPath: "project .claude/settings.json",
245
- capabilities: {
246
- bootstrap: true,
247
- mcp: true,
248
- hook: true,
249
- skill: true
250
- },
251
- installedCapabilities: {
252
- hook: true,
253
- skill: true
221
+ // src/install/install-global.ts
222
+ import { rmSync } from "fs";
223
+ import { join as join2 } from "path";
224
+ import {
225
+ STORES_ROOT_DIR,
226
+ globalConfigSchema,
227
+ initStore
228
+ } from "@fenglimg/fabric-shared";
229
+
230
+ // src/install/transaction.ts
231
+ function errorMessage(error) {
232
+ return error instanceof Error ? error.message : String(error);
233
+ }
234
+ async function runInstallTransaction(steps) {
235
+ const receipt = { ok: true, steps: [] };
236
+ const applied = [];
237
+ for (let i = 0; i < steps.length; i++) {
238
+ const step = steps[i];
239
+ try {
240
+ await step.apply();
241
+ applied.push(step);
242
+ receipt.steps.push({ name: step.name, status: "applied" });
243
+ } catch (error) {
244
+ receipt.ok = false;
245
+ receipt.failedStep = step.name;
246
+ receipt.error = errorMessage(error);
247
+ receipt.steps.push({ name: step.name, status: "failed", error: errorMessage(error) });
248
+ for (let j = i + 1; j < steps.length; j++) {
249
+ receipt.steps.push({ name: steps[j].name, status: "skipped" });
254
250
  }
255
- },
256
- {
257
- clientKind: "ClaudeCodeDesktop",
258
- label: "Claude Code Desktop",
259
- detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
260
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
261
- configPath: "desktop Claude config",
262
- capabilities: {
263
- bootstrap: true,
264
- mcp: true,
265
- hook: false,
266
- skill: false
251
+ for (const done of [...applied].reverse()) {
252
+ const entry = receipt.steps.find((s) => s.name === done.name);
253
+ try {
254
+ await done.rollback();
255
+ if (entry !== void 0) {
256
+ entry.status = "rolled_back";
257
+ }
258
+ } catch (rollbackError) {
259
+ if (entry !== void 0) {
260
+ entry.status = "rollback_failed";
261
+ entry.error = errorMessage(rollbackError);
262
+ }
263
+ }
267
264
  }
268
- },
265
+ return receipt;
266
+ }
267
+ }
268
+ return receipt;
269
+ }
270
+
271
+ // src/install/install-global.ts
272
+ async function installGlobalCore(options) {
273
+ const existing = loadGlobalConfig(options.globalRoot);
274
+ if (existing !== null) {
275
+ return {
276
+ receipt: { ok: true, steps: [{ name: "already-installed", status: "applied" }] },
277
+ config: existing,
278
+ alreadyInstalled: true
279
+ };
280
+ }
281
+ const alias = options.personalAlias ?? "personal";
282
+ const personalDir = join2(options.globalRoot, STORES_ROOT_DIR, options.personalStoreUuid);
283
+ let config = null;
284
+ const receipt = await runInstallTransaction([
269
285
  {
270
- clientKind: "Cursor",
271
- label: "Cursor",
272
- detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
273
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
274
- configPath: ".cursor/mcp.json",
275
- capabilities: {
276
- bootstrap: true,
277
- mcp: true,
278
- hook: false,
279
- skill: false
286
+ name: "init-personal-store",
287
+ apply: () => {
288
+ initStore(
289
+ personalDir,
290
+ {
291
+ store_uuid: options.personalStoreUuid,
292
+ created_at: options.now,
293
+ canonical_alias: alias
294
+ },
295
+ { git: options.git }
296
+ );
297
+ },
298
+ rollback: () => {
299
+ rmSync(personalDir, { recursive: true, force: true });
280
300
  }
281
301
  },
282
302
  {
283
- clientKind: "CodexCLI",
284
- label: "Codex CLI",
285
- detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
286
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
287
- configPath: "~/.codex/config.toml",
288
- capabilities: {
289
- bootstrap: true,
290
- mcp: true,
291
- hook: true,
292
- skill: true
303
+ name: "write-global-config",
304
+ apply: () => {
305
+ const next = globalConfigSchema.parse({
306
+ uid: options.uid,
307
+ stores: [{ store_uuid: options.personalStoreUuid, alias, personal: true }]
308
+ });
309
+ saveGlobalConfig(next, options.globalRoot);
310
+ config = next;
293
311
  },
294
- installedCapabilities: {
295
- hook: existsSync3(join3(workspaceRoot, ".codex", "hooks.json")),
296
- // v2/rc.2: v1 client-side init skill removed; skill-installation probes
297
- // will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
298
- // fabric-review, fabric-import). Until then there is nothing to probe.
299
- skill: false
312
+ rollback: () => {
313
+ rmSync(globalConfigPath(options.globalRoot), { force: true });
300
314
  }
301
315
  }
302
- ];
316
+ ]);
317
+ return { receipt, config, alreadyInstalled: false };
303
318
  }
304
319
 
305
- // src/commands/config.ts
306
- var CLIENT_ALIASES = {
307
- claude: "ClaudeCodeCLI",
308
- claudecodecli: "ClaudeCodeCLI",
309
- "claude-code-cli": "ClaudeCodeCLI",
310
- claudecli: "ClaudeCodeCLI",
311
- claudecodedesktop: "ClaudeCodeDesktop",
312
- "claude-code-desktop": "ClaudeCodeDesktop",
313
- claudedesktop: "ClaudeCodeDesktop",
314
- cursor: "Cursor",
315
- codexcli: "CodexCLI",
316
- "codex-cli": "CodexCLI",
317
- codex: "CodexCLI"
318
- };
319
- function parseClientFilter(value) {
320
- if (value === void 0 || value.trim().length === 0) {
321
- return null;
322
- }
323
- const clients = /* @__PURE__ */ new Set();
324
- for (const rawClient of value.split(",")) {
325
- const alias = rawClient.trim().toLowerCase();
326
- const clientKind = CLIENT_ALIASES[alias];
327
- if (clientKind === void 0) {
328
- throw new Error(t("cli.config.errors.unknown-client", { client: rawClient }));
329
- }
330
- clients.add(clientKind);
331
- }
332
- return clients;
320
+ // src/install/run-global-install.ts
321
+ function gitClone(url, dest) {
322
+ execFileSync2("git", ["clone", url, dest], { stdio: ["ignore", "ignore", "pipe"] });
323
+ }
324
+ function mountStoreFromRemote(url, globalRoot) {
325
+ const storesRoot = join3(globalRoot, STORES_ROOT_DIR2);
326
+ mkdirSync(storesRoot, { recursive: true });
327
+ const tmp = mkdtempSync(join3(tmpdir(), "fabric-clone-"));
328
+ const cloneDest = join3(tmp, "store");
329
+ gitClone(url, cloneDest);
330
+ const identity = readStoreIdentity(cloneDest);
331
+ if (identity === null) {
332
+ throw new Error(`cloned store at ${url} has no valid store.json (not a Fabric store)`);
333
+ }
334
+ const finalDir = join3(storesRoot, identity.store_uuid);
335
+ renameSync(cloneDest, finalDir);
336
+ const config = loadGlobalConfig(globalRoot);
337
+ if (config === null) {
338
+ throw new Error("global config missing after install");
339
+ }
340
+ const alias = identity.canonical_alias ?? "team";
341
+ saveGlobalConfig(
342
+ addMountedStore(config, { store_uuid: identity.store_uuid, alias, remote: url }),
343
+ globalRoot
344
+ );
345
+ console.log(`mounted store '${alias}' (${identity.store_uuid}) from ${url}`);
333
346
  }
334
- async function loadFabricConfig(workspaceRoot) {
335
- const configPath = resolve3(workspaceRoot, "fabric.config.json");
336
- if (!existsSync4(configPath)) {
337
- return {};
347
+ async function runGlobalInstall(options = {}, globalRoot = resolveGlobalRoot()) {
348
+ const uid = options.uid ?? deriveUid();
349
+ const personalStoreUuid = options.personalStoreUuid ?? randomUUID();
350
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
351
+ const result = await installGlobalCore({ globalRoot, uid, personalStoreUuid, now });
352
+ if (!result.receipt.ok) {
353
+ throw new Error(`global install failed at step '${result.receipt.failedStep}': ${result.receipt.error}`);
338
354
  }
339
- const parsed = JSON.parse(await readFile2(configPath, "utf8"));
340
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
341
- throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
355
+ console.log(
356
+ result.alreadyInstalled ? "global Fabric already installed" : `installed global Fabric (uid ${uid})`
357
+ );
358
+ if (options.url !== void 0) {
359
+ mountStoreFromRemote(options.url, globalRoot);
342
360
  }
343
- return parsed;
344
- }
345
- function resolveServerPath(override) {
346
- if (override) return override;
347
- if (process.env.FAB_SERVER_PATH) return resolve3(process.env.FAB_SERVER_PATH);
348
- return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
349
361
  }
350
- function writeStderr(message) {
351
- process.stderr.write(`${message}
352
- `);
353
- }
354
- var configCmd = defineCommand({
355
- meta: {
356
- name: "config",
357
- description: t("cli.config.description")
358
- },
359
- subCommands: {
360
- hooks: hooksCommand,
361
- install: defineCommand({
362
- meta: {
363
- name: "install",
364
- description: t("cli.config.install.description")
365
- },
366
- args: {
367
- clients: {
368
- type: "string",
369
- description: t("cli.config.install.args.clients.description")
370
- },
371
- "dry-run": {
372
- type: "boolean",
373
- description: t("cli.config.install.args.dry-run.description"),
374
- default: false
375
- }
376
- },
377
- async run({ args }) {
378
- const selectedClients = parseClientFilter(args.clients);
379
- const result = await installMcpClients(process.cwd(), {
380
- clients: selectedClients === null ? void 0 : Array.from(selectedClients),
381
- dryRun: args["dry-run"]
382
- });
383
- if (result.details.length === 0) {
384
- writeStderr(t("cli.config.install.no-configs"));
385
- return;
386
- }
387
- for (const detail of result.details) {
388
- if (detail.action === "skipped") {
389
- writeStderr(t("cli.config.install.no-config-path", { client: detail.client }));
390
- continue;
391
- }
392
- if (detail.action === "dry-run" && detail.path !== null) {
393
- writeStderr(t("cli.config.install.dry-run", { client: detail.client, path: detail.path }));
394
- continue;
395
- }
396
- if (detail.path !== null) {
397
- writeStderr(t("cli.config.install.wrote", { client: detail.client, path: detail.path }));
362
+
363
+ // src/lib/detect-language.ts
364
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
365
+ import { join as join4 } from "path";
366
+ function detectExistingLanguage(target) {
367
+ const ZH_CN_RATIO_THRESHOLD = 0.3;
368
+ const samples = [];
369
+ const readmePath = join4(target, "README.md");
370
+ if (existsSync2(readmePath)) {
371
+ try {
372
+ samples.push(readFileSync(readmePath, "utf8"));
373
+ } catch {
374
+ }
375
+ }
376
+ const docsDir = join4(target, "docs");
377
+ if (existsSync2(docsDir)) {
378
+ try {
379
+ const stat = statSync2(docsDir);
380
+ if (stat.isDirectory()) {
381
+ for (const entry of readdirSync(docsDir, { withFileTypes: true })) {
382
+ if (!entry.isFile()) continue;
383
+ if (!/\.(md|mdx|txt)$/iu.test(entry.name)) continue;
384
+ try {
385
+ samples.push(readFileSync(join4(docsDir, entry.name), "utf8"));
386
+ } catch {
398
387
  }
399
388
  }
400
389
  }
401
- })
402
- }
403
- });
404
- async function installMcpClients(target, options = {}) {
405
- const workspaceRoot = resolve3(target);
406
- const fabricConfig = await loadFabricConfig(workspaceRoot);
407
- const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
408
- const serverPath = resolveServerPath(options.localServerPath);
409
- const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
410
- (writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
411
- );
412
- const installed = [];
413
- const skipped = [];
414
- const details = [];
415
- for (const writer of writers) {
416
- const configPath = await writer.detect(workspaceRoot);
417
- if (configPath === null) {
418
- skipped.push(writer.clientKind);
419
- details.push({ client: writer.clientKind, path: null, action: "skipped" });
420
- continue;
390
+ } catch {
421
391
  }
422
- if (options.dryRun) {
423
- skipped.push(writer.clientKind);
424
- details.push({ client: writer.clientKind, path: configPath, action: "dry-run" });
425
- continue;
392
+ }
393
+ if (samples.length === 0) {
394
+ return "en";
395
+ }
396
+ let cjkCount = 0;
397
+ let asciiLetterCount = 0;
398
+ for (const sample of samples) {
399
+ for (const ch of sample) {
400
+ const code = ch.codePointAt(0) ?? 0;
401
+ if (code >= 19968 && code <= 40959) {
402
+ cjkCount += 1;
403
+ } else if (code >= 65 && code <= 90 || code >= 97 && code <= 122) {
404
+ asciiLetterCount += 1;
405
+ }
426
406
  }
427
- await writer.write(serverPath, workspaceRoot);
428
- installed.push(writer.clientKind);
429
- details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
430
407
  }
431
- return { installed, skipped, details };
408
+ const denominator = cjkCount + asciiLetterCount;
409
+ if (denominator === 0) {
410
+ return "en";
411
+ }
412
+ const ratio = cjkCount / denominator;
413
+ return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN-hybrid" : "en";
432
414
  }
433
415
 
434
416
  // src/scanner/forensic.ts
435
- import { execFileSync } from "child_process";
436
- import { existsSync as existsSync5, readdirSync, readFileSync, statSync } from "fs";
417
+ import { execFileSync as execFileSync3 } from "child_process";
418
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
437
419
  import { createRequire } from "module";
438
- import { basename, extname, isAbsolute, join as join4, posix, relative, resolve as resolve4, sep } from "path";
420
+ import { basename, extname, isAbsolute as isAbsolute2, join as join5, posix, relative, resolve as resolve2, sep } from "path";
439
421
  import {
440
422
  forensicReportSchema
441
423
  } from "@fenglimg/fabric-shared";
424
+
425
+ // src/scanner/detector.ts
426
+ import { detectFramework } from "@fenglimg/fabric-shared/node";
427
+
428
+ // src/scanner/forensic.ts
442
429
  var require2 = createRequire(import.meta.url);
443
430
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
444
431
  ".fabric",
@@ -521,7 +508,7 @@ var parserInitPromise = null;
521
508
  var languagePromiseByKind = {};
522
509
  var parserBundlePromiseByKind = {};
523
510
  async function buildForensicReport(targetInput) {
524
- const target = normalizeTarget(targetInput);
511
+ const target = normalizeTarget2(targetInput);
525
512
  const framework = detectFramework(target);
526
513
  const topology = buildTopology(target);
527
514
  const entryPoints = collectEntryPoints(target, topology.files);
@@ -533,7 +520,7 @@ async function buildForensicReport(targetInput) {
533
520
  const report = {
534
521
  version: "1.0",
535
522
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
536
- generated_by: `fab-cli@${getCliVersion()}`,
523
+ generated_by: `fabric-cli@${getCliVersion()}`,
537
524
  target,
538
525
  project_name: readProjectName(target),
539
526
  framework,
@@ -557,11 +544,11 @@ async function buildForensicReport(targetInput) {
557
544
  }
558
545
  return validation.data;
559
546
  }
560
- function normalizeTarget(targetInput) {
561
- return isAbsolute(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
547
+ function normalizeTarget2(targetInput) {
548
+ return isAbsolute2(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
562
549
  }
563
550
  function buildTopology(root) {
564
- assertExistingDirectory(root);
551
+ assertExistingDirectory2(root);
565
552
  const byExt = {};
566
553
  const keyDirs = /* @__PURE__ */ new Set();
567
554
  const files = [];
@@ -573,8 +560,8 @@ function buildTopology(root) {
573
560
  if (current === void 0) {
574
561
  continue;
575
562
  }
576
- for (const entry of readdirSync(current, { withFileTypes: true })) {
577
- const absolutePath = join4(current, entry.name);
563
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
564
+ const absolutePath = join5(current, entry.name);
578
565
  const relativePath = toPosixPath(relative(root, absolutePath));
579
566
  if (relativePath.length === 0) {
580
567
  continue;
@@ -594,7 +581,7 @@ function buildTopology(root) {
594
581
  if (!entry.isFile()) {
595
582
  continue;
596
583
  }
597
- const stats = statSync(absolutePath);
584
+ const stats = statSync3(absolutePath);
598
585
  const extension = extname(entry.name) || "[none]";
599
586
  byExt[extension] = (byExt[extension] ?? 0) + 1;
600
587
  totalFiles += 1;
@@ -612,8 +599,8 @@ function buildTopology(root) {
612
599
  files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
613
600
  };
614
601
  }
615
- function assertExistingDirectory(target) {
616
- if (!existsSync5(target) || !statSync(target).isDirectory()) {
602
+ function assertExistingDirectory2(target) {
603
+ if (!existsSync3(target) || !statSync3(target).isDirectory()) {
617
604
  throw new Error(`Target must be an existing directory: ${target}`);
618
605
  }
619
606
  }
@@ -662,7 +649,7 @@ function getEntryPointReason(relativePath) {
662
649
  async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
663
650
  const samples = [];
664
651
  for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
665
- const absolutePath = join4(target, ...entryPoint.path.split("/"));
652
+ const absolutePath = join5(target, ...entryPoint.path.split("/"));
666
653
  const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
667
654
  const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
668
655
  frameworkKind,
@@ -682,7 +669,7 @@ async function buildCodeSamples(target, entryPoints, frameworkKind, topology, pa
682
669
  }
683
670
  function readFirstLines(path, lineLimit) {
684
671
  try {
685
- const lines = readFileSync(path, "utf8").split(/\r?\n/);
672
+ const lines = readFileSync2(path, "utf8").split(/\r?\n/);
686
673
  if (lines.at(-1) === "") {
687
674
  lines.pop();
688
675
  }
@@ -699,12 +686,12 @@ function readFirstLines(path, lineLimit) {
699
686
  }
700
687
  }
701
688
  function readPackageDependencies(target) {
702
- const packageJsonPath = join4(target, "package.json");
703
- if (!existsSync5(packageJsonPath)) {
689
+ const packageJsonPath = join5(target, "package.json");
690
+ if (!existsSync3(packageJsonPath)) {
704
691
  return /* @__PURE__ */ new Map();
705
692
  }
706
693
  try {
707
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
694
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
708
695
  return new Map([
709
696
  ...Object.entries(packageJson.dependencies ?? {}),
710
697
  ...Object.entries(packageJson.devDependencies ?? {}),
@@ -717,7 +704,7 @@ function readPackageDependencies(target) {
717
704
  }
718
705
  function readGitChurnWeight(target, relativePath) {
719
706
  try {
720
- const output = execFileSync("git", ["log", "--follow", "--oneline", "-20", "--", relativePath], {
707
+ const output = execFileSync3("git", ["log", "--follow", "--oneline", "-20", "--", relativePath], {
721
708
  cwd: target,
722
709
  encoding: "utf8",
723
710
  stdio: ["ignore", "pipe", "ignore"],
@@ -1040,16 +1027,16 @@ function scoreFrameworkConfidence(input) {
1040
1027
  return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
1041
1028
  }
1042
1029
  function readReadmeInfo(target) {
1043
- const readmePath = join4(target, "README.md");
1044
- const hasContributing = existsSync5(join4(target, "CONTRIBUTING.md"));
1045
- if (!existsSync5(readmePath)) {
1030
+ const readmePath = join5(target, "README.md");
1031
+ const hasContributing = existsSync3(join5(target, "CONTRIBUTING.md"));
1032
+ if (!existsSync3(readmePath)) {
1046
1033
  return {
1047
1034
  quality: "missing",
1048
1035
  line_count: 0,
1049
1036
  has_contributing: hasContributing
1050
1037
  };
1051
1038
  }
1052
- const readme = readFileSync(readmePath, "utf8");
1039
+ const readme = readFileSync2(readmePath, "utf8");
1053
1040
  const wordCount = readme.trim().split(/\s+/).filter(Boolean).length;
1054
1041
  return {
1055
1042
  quality: wordCount >= 200 ? "ok" : "stub",
@@ -1527,10 +1514,10 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
1527
1514
  return recommendations;
1528
1515
  }
1529
1516
  function readProjectName(target) {
1530
- const packageJsonPath = join4(target, "package.json");
1531
- if (existsSync5(packageJsonPath)) {
1517
+ const packageJsonPath = join5(target, "package.json");
1518
+ if (existsSync3(packageJsonPath)) {
1532
1519
  try {
1533
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1520
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
1534
1521
  if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
1535
1522
  return packageJson.name;
1536
1523
  }
@@ -1541,7 +1528,7 @@ function readProjectName(target) {
1541
1528
  return basename(target);
1542
1529
  }
1543
1530
  function getCliVersion() {
1544
- return true ? "2.0.0" : "unknown";
1531
+ return true ? "2.1.0-rc.2" : "unknown";
1545
1532
  }
1546
1533
  function sortRecord(record) {
1547
1534
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -1550,109 +1537,152 @@ function toPosixPath(path) {
1550
1537
  return path.split(sep).join("/");
1551
1538
  }
1552
1539
 
1553
- // src/commands/init.ts
1554
- var AGENTS_MD_DEFAULT_CONTENT = `# Project Knowledge
1555
-
1556
- This project uses [Fabric](https://github.com/fenglimg/fabric) for cross-client AI knowledge management.
1557
-
1558
- Knowledge entries live in \`.fabric/knowledge/\` (team) and \`~/.fabric/knowledge/\` (personal).
1559
- Run \`fabric doctor\` to verify state.
1560
-
1561
- See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
1562
- `;
1563
- var LOCAL_FABRIC_SERVER_PATH = join5("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1540
+ // src/commands/install.ts
1541
+ var LOCAL_FABRIC_SERVER_PATH = join6("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1564
1542
  var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
1565
1543
  var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
1566
- var initCommand = defineCommand2({
1544
+ var installCommand = defineCommand({
1567
1545
  meta: {
1568
- name: "init",
1569
- description: t("cli.init.description")
1546
+ name: "install",
1547
+ description: t("cli.install.description")
1570
1548
  },
1571
1549
  args: {
1572
- target: {
1573
- type: "string",
1574
- description: t("cli.init.args.target.description")
1575
- },
1576
1550
  debug: {
1577
1551
  type: "boolean",
1578
- description: t("cli.init.args.debug.description"),
1552
+ description: t("cli.install.args.debug.description"),
1579
1553
  default: false
1580
1554
  },
1581
- force: {
1555
+ "dry-run": {
1582
1556
  type: "boolean",
1583
- description: t("cli.init.args.force.description"),
1557
+ description: t("cli.install.args.dry-run.description"),
1584
1558
  default: false
1585
1559
  },
1560
+ target: {
1561
+ type: "string",
1562
+ description: t("cli.install.args.target.description")
1563
+ },
1586
1564
  yes: {
1587
1565
  type: "boolean",
1588
- description: t("cli.init.args.yes.description"),
1566
+ description: t("cli.install.args.yes.description"),
1589
1567
  default: false
1590
1568
  },
1591
- plan: {
1569
+ "force-skills-only": {
1592
1570
  type: "boolean",
1593
- description: t("cli.init.args.plan.description"),
1571
+ description: t("cli.install.args.force-skills-only.description"),
1594
1572
  default: false
1595
1573
  },
1596
- reapply: {
1574
+ "force-hooks-only": {
1597
1575
  type: "boolean",
1598
- description: t("cli.init.args.reapply.description"),
1576
+ description: t("cli.install.args.force-hooks-only.description"),
1599
1577
  default: false
1600
1578
  },
1601
- bootstrap: {
1602
- type: "boolean",
1603
- default: true,
1604
- negativeDescription: t("cli.init.args.no-bootstrap.description")
1605
- },
1606
- mcp: {
1607
- type: "boolean",
1608
- default: true,
1609
- negativeDescription: t("cli.init.args.no-mcp.description")
1610
- },
1611
- hooks: {
1612
- type: "boolean",
1613
- default: true,
1614
- negativeDescription: t("cli.init.args.no-hooks.description")
1615
- },
1616
- interactive: {
1579
+ global: {
1617
1580
  type: "boolean",
1618
- description: t("cli.init.args.interactive.description"),
1619
- default: true
1620
- },
1621
- "mcp-install": {
1622
- type: "string",
1623
- default: "global",
1624
- description: t("cli.init.mcp.install.prompt")
1581
+ description: "Set up global Fabric (~/.fabric: uid + personal store + config)",
1582
+ default: false
1625
1583
  },
1626
- scope: {
1584
+ url: {
1627
1585
  type: "string",
1628
- description: t("cli.init.mcp.scope.description")
1586
+ description: "With --global: clone + mount this shared store remote"
1629
1587
  }
1630
1588
  },
1631
1589
  async run({ args }) {
1632
1590
  await runInitCommand(args);
1633
1591
  }
1634
1592
  });
1635
- var init_default = initCommand;
1593
+ var install_default = installCommand;
1594
+ async function runSkillsOnlyRefresh(targetInput) {
1595
+ const target = normalizeTarget3(targetInput);
1596
+ const metaPath = join6(target, ".fabric", "agents.meta.json");
1597
+ if (!existsSync4(metaPath)) {
1598
+ const message = t("cli.install.force-skills-only.uninitialised.message");
1599
+ const hint = t("cli.install.force-skills-only.uninitialised.hint");
1600
+ process.stderr.write(`${message}
1601
+ ${hint}
1602
+ `);
1603
+ process.exitCode = 1;
1604
+ return;
1605
+ }
1606
+ console.log(formatInitStageHeader(t("cli.install.force-skills-only.banner")));
1607
+ const results = [];
1608
+ results.push(...await cleanupDeprecatedSkills(target));
1609
+ results.push(...await installFabricArchiveSkill(target));
1610
+ results.push(...await installFabricReviewSkill(target));
1611
+ results.push(...await installFabricImportSkill(target));
1612
+ let written = 0;
1613
+ let skipped = 0;
1614
+ let errors = 0;
1615
+ for (const r of results) {
1616
+ if (r.status === "written") written += 1;
1617
+ else if (r.status === "skipped") skipped += 1;
1618
+ else if (r.status === "error") errors += 1;
1619
+ }
1620
+ console.log(
1621
+ t("cli.install.force-skills-only.summary", {
1622
+ written: String(written),
1623
+ skipped: String(skipped),
1624
+ errors: String(errors)
1625
+ })
1626
+ );
1627
+ if (errors > 0) {
1628
+ for (const r of results) {
1629
+ if (r.status === "error") {
1630
+ process.stderr.write(` ${r.step} ${r.path}: ${r.message ?? "error"}
1631
+ `);
1632
+ }
1633
+ }
1634
+ process.exitCode = 1;
1635
+ }
1636
+ }
1637
+ async function runHooksOnlyRefresh(targetInput) {
1638
+ const target = normalizeTarget3(targetInput);
1639
+ const metaPath = join6(target, ".fabric", "agents.meta.json");
1640
+ if (!existsSync4(metaPath)) {
1641
+ const message = t("cli.install.force-hooks-only.uninitialised.message");
1642
+ const hint = t("cli.install.force-hooks-only.uninitialised.hint");
1643
+ process.stderr.write(`${message}
1644
+ ${hint}
1645
+ `);
1646
+ process.exitCode = 1;
1647
+ return;
1648
+ }
1649
+ console.log(formatInitStageHeader(t("cli.install.force-hooks-only.banner")));
1650
+ const result = await installHooks(target);
1651
+ console.log(
1652
+ t("cli.install.force-hooks-only.summary", {
1653
+ written: String(result.installed.length),
1654
+ skipped: String(result.skipped.length),
1655
+ errors: String(result.errors.length)
1656
+ })
1657
+ );
1658
+ if (result.errors.length > 0) {
1659
+ for (const err of result.errors) {
1660
+ process.stderr.write(` ${err}
1661
+ `);
1662
+ }
1663
+ process.exitCode = 1;
1664
+ }
1665
+ }
1636
1666
  async function runInitCommand(args) {
1637
1667
  const logger = createDebugLogger(args.debug);
1668
+ if (args.global === true) {
1669
+ await runGlobalInstall({ url: args.url });
1670
+ return;
1671
+ }
1638
1672
  const resolution = resolveDevMode(args.target, process.cwd());
1639
- const intent = resolveInitCliIntent(args, resolution.target);
1640
- if (args.reapply === true) {
1641
- checkLockOrThrow(intent.target, { force: args.force });
1673
+ if (args["force-skills-only"] === true) {
1674
+ await runSkillsOnlyRefresh(resolution.target);
1675
+ return;
1676
+ }
1677
+ if (args["force-hooks-only"] === true) {
1678
+ await runHooksOnlyRefresh(resolution.target);
1679
+ return;
1642
1680
  }
1681
+ const intent = resolveInitCliIntent(args, resolution.target);
1643
1682
  logger(`init target source: ${resolution.source}`);
1644
1683
  for (const step of resolution.chain) {
1645
1684
  logger(step);
1646
1685
  }
1647
- if (intent.options.planOnly) {
1648
- writeStderr2(t("cli.init.compat.plan"));
1649
- }
1650
- if (args.interactive === false) {
1651
- writeStderr2(t("cli.init.compat.interactive"));
1652
- }
1653
- if (args.bootstrap === false || args.mcp === false || args.hooks === false) {
1654
- writeStderr2(t("cli.init.compat.legacy-stage-flags"));
1655
- }
1656
1686
  const supports = detectClientSupports(intent.target);
1657
1687
  const basePlan = await buildInitExecutionPlan({
1658
1688
  target: intent.target,
@@ -1662,81 +1692,107 @@ async function runInitCommand(args) {
1662
1692
  interactive: intent.interactiveSummary && !intent.wizardEnabled,
1663
1693
  supports
1664
1694
  });
1665
- const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan, args, createDefaultInitWizardAdapter()) : basePlan;
1695
+ const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan, createDefaultInitWizardAdapter()) : basePlan;
1666
1696
  if (plan === null) {
1667
1697
  process.exitCode = 130;
1668
1698
  return;
1669
1699
  }
1670
1700
  const result = await executeInitExecutionPlan(plan);
1671
- try {
1672
- await maybeWriteImportSentinel({
1673
- target: intent.target,
1674
- planOnly: intent.options.planOnly === true,
1675
- wizardEnabled: intent.wizardEnabled,
1676
- terminalInteractive: intent.interactiveSummary
1677
- });
1678
- } catch {
1679
- }
1680
1701
  if (!intent.options.planOnly) {
1702
+ console.log("");
1703
+ console.log(t("cli.install.next-steps"));
1704
+ console.log("");
1681
1705
  console.log(paint.muted("More: docs/surfaces.md explains when to use CLI vs Skill vs MCP."));
1682
1706
  }
1683
1707
  return result;
1684
1708
  }
1685
- async function maybeWriteImportSentinel(opts) {
1686
- if (opts.planOnly) return;
1687
- if (process.env.FABRIC_NONINTERACTIVE === "1") return;
1688
- if (!opts.wizardEnabled || !opts.terminalInteractive) return;
1689
- if (!Boolean(process.stdin.isTTY)) return;
1690
- const answer = await confirm({
1691
- message: "\u4E0B\u6B21\u5F00 AI \u65F6\u8BA9\u6211\u4ECE git log \u62BD\u66F4\u591A\u77E5\u8BC6\u5417?",
1692
- initialValue: true
1693
- });
1694
- if (isCancel(answer) || answer !== true) return;
1695
- const fabricDir = join5(opts.target, ".fabric");
1696
- const sentinelPath = join5(fabricDir, ".import-requested");
1697
- try {
1698
- if (!existsSync6(fabricDir)) {
1699
- mkdirSync(fabricDir, { recursive: true });
1700
- }
1701
- writeFileSync(sentinelPath, "", "utf8");
1702
- log.success("\u4E0B\u6B21\u5F00 AI \u4F1A\u770B\u5230\u63D0\u793A");
1703
- } catch {
1704
- }
1709
+ function writeDefaultFabricConfig(fabricDir, targetRoot) {
1710
+ const target = join6(fabricDir, "fabric-config.json");
1711
+ if (existsSync4(target)) return;
1712
+ const detectedLanguage = detectExistingLanguage(targetRoot);
1713
+ const FABRIC_CONFIG_DEFAULTS = {
1714
+ // Scan/import language policy. Fixated at init time by probing
1715
+ // README.md + docs/*.md (CJK ratio > 0.3 → "zh-CN", else "en"). Users
1716
+ // can edit `.fabric/fabric-config.json` to override. See
1717
+ // packages/shared/src/schemas/fabric-config.ts for the enum.
1718
+ fabric_language: detectedLanguage,
1719
+ // fabric-hint Stop hook Signal A (archive): time-branch threshold, hours
1720
+ // since last knowledge_proposed event.
1721
+ archive_hint_hours: 24,
1722
+ // fabric-hint Stop hook cooldown after ANY signal fires, in hours.
1723
+ archive_hint_cooldown_hours: 12,
1724
+ // fabric-hint Stop hook Signal B (review): pending-count cutoff.
1725
+ review_hint_pending_count: 10,
1726
+ // fabric-hint Stop hook Signal B (review): pending-age cutoff in days.
1727
+ review_hint_pending_age_days: 7,
1728
+ // fabric-hint Stop hook Signal D (maintenance): days since last doctor.
1729
+ maintenance_hint_days: 14,
1730
+ // fabric-hint Stop hook Signal D (maintenance): cooldown between
1731
+ // reminders, in days.
1732
+ maintenance_hint_cooldown_days: 7,
1733
+ // fabric-hint Stop hook Signal A (archive): edit-count branch threshold;
1734
+ // PreToolUse fires recorded in .fabric/.cache/edit-counter since the
1735
+ // last knowledge_proposed event.
1736
+ archive_edit_threshold: 20,
1737
+ // fabric-hint Stop hook Signal C (import) + doctor lint #22: canonical
1738
+ // knowledge node count below this value flags an underseeded workspace.
1739
+ underseed_node_threshold: 10,
1740
+ // rc.9+ (skill-contract-fix B1): fabric-import first-run git-history
1741
+ // window in months. Default 60 captures the bulk of a mature repo's
1742
+ // signal in one pass; lower to 12-24 for fresh / small repos.
1743
+ import_window_first_run_months: 60,
1744
+ // rc.9+ (skill-contract-fix B1): fabric-import rerun window in months.
1745
+ // Default 2; raise to 6 if the workspace pauses imports for long stretches.
1746
+ import_window_rerun_months: 2,
1747
+ // rc.9+ (skill-contract-fix B1): hard cap on pending entries produced
1748
+ // per fabric-import invocation. Default 10 matches one-sitting triage.
1749
+ import_max_pending_per_run: 10,
1750
+ // rc.9+ (skill-contract-fix B1): hard cap on commits scanned per
1751
+ // fabric-import invocation. Default 500 covers ~2 months of typical churn.
1752
+ import_max_commits_scan: 500,
1753
+ // rc.9+ (skill-contract-fix B1): canonical-node count above which
1754
+ // fabric-import suggests review over importing more. Default 50.
1755
+ import_skip_canonical_threshold: 50,
1756
+ // rc.9+ (skill-contract-fix B1): max candidates per fabric-archive batch.
1757
+ // Default 8 keeps each batch reviewable in one sitting.
1758
+ archive_max_candidates_per_batch: 8,
1759
+ // rc.9+ (skill-contract-fix B1): max recently-touched paths in
1760
+ // fabric-archive's relevance digest. Default 20.
1761
+ archive_max_recent_paths: 20,
1762
+ // rc.9+ (skill-contract-fix B1): max prior fabric-archive sessions
1763
+ // summarised in the digest the skill loads on start. Default 10.
1764
+ archive_digest_max_sessions: 10,
1765
+ // rc.9+ (skill-contract-fix B1): max review results per topic cluster
1766
+ // in fabric-review. Default 8.
1767
+ review_topic_result_cap: 8,
1768
+ // rc.9+ (skill-contract-fix B1): age (days) above which a pending entry
1769
+ // is considered stale by fabric-review. Default 14.
1770
+ review_stale_pending_days: 14
1771
+ };
1772
+ mkdirSync2(fabricDir, { recursive: true });
1773
+ writeFileSync(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
1774
+ log.info(
1775
+ `Detected and fixated fabric_language = ${detectedLanguage}; edit ${target} to override.`
1776
+ );
1705
1777
  }
1706
1778
  function resolveInitCliIntent(args, targetInput) {
1707
- const target = normalizeTarget2(targetInput);
1708
- const mcpInstallMode = resolveMcpInstallMode(args["mcp-install"]);
1709
- const claudeMcpScope = resolveClaudeMcpScope(args.scope);
1779
+ const target = normalizeTarget3(targetInput);
1780
+ const mcpInstallMode = "global";
1781
+ const claudeMcpScope = "project";
1710
1782
  const terminalInteractive = isInteractiveInit();
1711
- const planOnly = args.plan === true;
1712
- const reapply = args.reapply === true;
1783
+ const planOnly = args["dry-run"] === true;
1713
1784
  const options = {
1714
- force: reapply ? true : args.force,
1715
- skipBootstrap: args.bootstrap === false ? true : args.skipBootstrap,
1716
- skipMcp: args.mcp === false ? true : args.skipMcp,
1717
- skipHooks: args.hooks === false ? true : args.skipHooks,
1718
- planOnly,
1719
- reapply
1785
+ planOnly
1720
1786
  };
1721
1787
  return {
1722
1788
  target,
1723
1789
  options,
1724
1790
  mcpInstallMode,
1725
1791
  claudeMcpScope,
1726
- interactiveSummary: args.interactive !== false && terminalInteractive,
1792
+ interactiveSummary: terminalInteractive,
1727
1793
  wizardEnabled: shouldUseInitWizard(args, terminalInteractive) && !planOnly
1728
1794
  };
1729
1795
  }
1730
- function resolveClaudeMcpScope(raw) {
1731
- if (raw === void 0 || raw === "project") {
1732
- return "project";
1733
- }
1734
- if (raw === "user") {
1735
- return "user";
1736
- }
1737
- writeStderr2(t("cli.init.mcp.scope.invalid", { value: raw }));
1738
- return "project";
1739
- }
1740
1796
  async function buildInitExecutionPlan(input) {
1741
1797
  const options = input.options ?? {};
1742
1798
  const scaffold = await buildInitFabricPlan(input.target, options);
@@ -1773,17 +1829,17 @@ async function buildInitExecutionPlan(input) {
1773
1829
  };
1774
1830
  }
1775
1831
  async function executeInitExecutionPlan(plan) {
1776
- if (plan.options.force) {
1777
- writeStderr2(t("cli.init.force.warning", { path: plan.target }));
1778
- }
1779
- if (plan.options.reapply && !plan.options.planOnly && !plan.interactive) {
1780
- writeStderr2(formatInitModeBanner(plan.options));
1781
- }
1782
1832
  if (plan.interactive) {
1783
1833
  printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
1784
1834
  }
1835
+ const scaffoldStates = [
1836
+ { path: plan.scaffold.metaPath, state: plan.scaffold.metaState },
1837
+ { path: plan.scaffold.eventsPath, state: plan.scaffold.eventsState },
1838
+ { path: plan.scaffold.forensicPath, state: plan.scaffold.forensicState }
1839
+ ];
1785
1840
  if (plan.options.planOnly) {
1786
1841
  printInitPlanPreview(plan);
1842
+ printInitDiffStateTable(scaffoldStates);
1787
1843
  return {
1788
1844
  plan,
1789
1845
  created: buildPlanOnlyScaffoldResult(plan.scaffold),
@@ -1791,6 +1847,17 @@ async function executeInitExecutionPlan(plan) {
1791
1847
  finalSupports: plan.supports
1792
1848
  };
1793
1849
  }
1850
+ if (existsSync4(plan.scaffold.fabricDir) && !statSync4(plan.scaffold.fabricDir).isDirectory()) {
1851
+ throw new Error(
1852
+ t("cli.install.diff.drift-abort", { path: plan.scaffold.fabricDir })
1853
+ );
1854
+ }
1855
+ const drifted = scaffoldStates.find(
1856
+ (entry) => entry.state === "drifted" || entry.state === "user-modified"
1857
+ );
1858
+ if (drifted !== void 0) {
1859
+ throw new Error(t("cli.install.diff.drift-abort", { path: drifted.path }));
1860
+ }
1794
1861
  let created = null;
1795
1862
  const stageResults = [];
1796
1863
  let finalSupports = plan.supports;
@@ -1815,6 +1882,11 @@ async function executeInitExecutionPlan(plan) {
1815
1882
  exhaustiveInitExecutionStep(step);
1816
1883
  }
1817
1884
  }
1885
+ if (scaffoldStates.every((entry) => entry.state === "present-canonical")) {
1886
+ console.log(
1887
+ t("cli.install.diff.canonical", { count: String(scaffoldStates.length) })
1888
+ );
1889
+ }
1818
1890
  return {
1819
1891
  plan,
1820
1892
  created: created ?? unreachableInitScaffold(),
@@ -1824,23 +1896,26 @@ async function executeInitExecutionPlan(plan) {
1824
1896
  }
1825
1897
  var KNOWLEDGE_SUBDIRS = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1826
1898
  function resolvePersonalFabricRoot() {
1827
- return process.env.FABRIC_HOME ?? homedir4();
1899
+ return process.env.FABRIC_HOME ?? homedir();
1828
1900
  }
1829
1901
  async function buildInitFabricPlan(target, options) {
1830
- assertExistingDirectory2(target);
1831
- const fabricDir = join5(target, ".fabric");
1832
- const agentsMdPath = join5(target, "AGENTS.md");
1833
- const agentsMdAction = existsSync6(agentsMdPath) ? "preserved" : "created";
1834
- const knowledgeDir = join5(fabricDir, "knowledge");
1835
- const personalKnowledgeDir = join5(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1836
- const forensicPath = join5(fabricDir, "forensic.json");
1837
- const eventsPath = join5(fabricDir, "events.jsonl");
1838
- const metaPath = join5(fabricDir, "agents.meta.json");
1902
+ assertExistingDirectory3(target);
1903
+ const fabricDir = join6(target, ".fabric");
1904
+ const agentsMdPath = join6(target, "AGENTS.md");
1905
+ const agentsMdAction = existsSync4(agentsMdPath) ? "preserved" : "created";
1906
+ const knowledgeDir = join6(fabricDir, "knowledge");
1907
+ const personalKnowledgeDir = join6(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1908
+ const forensicPath = join6(fabricDir, "forensic.json");
1909
+ const eventsPath = join6(fabricDir, "events.jsonl");
1910
+ const metaPath = join6(fabricDir, "agents.meta.json");
1839
1911
  const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
1840
- const knowledgeDirAction = existsSync6(knowledgeDir) ? "overwritten" : "created";
1841
- const metaAction = planFreshPath(metaPath, options);
1842
- const eventsAction = planFreshPath(eventsPath, options);
1843
- const forensicAction = planFreshPath(forensicPath, options);
1912
+ const knowledgeDirAction = existsSync4(knowledgeDir) ? "overwritten" : "created";
1913
+ const metaClassification = classifyFreshPath(metaPath, "structural");
1914
+ const eventsClassification = classifyFreshPath(eventsPath, "presence");
1915
+ const forensicClassification = classifyFreshPath(forensicPath, "always-rewrite");
1916
+ const metaAction = diffStateToWriteAction(metaClassification.state);
1917
+ const eventsAction = diffStateToWriteAction(eventsClassification.state);
1918
+ const forensicAction = diffStateToWriteAction(forensicClassification.state);
1844
1919
  const forensicReport = await buildForensicReport(target);
1845
1920
  const meta = createInitialMeta();
1846
1921
  return {
@@ -1860,60 +1935,63 @@ async function buildInitFabricPlan(target, options) {
1860
1935
  eventsAction,
1861
1936
  forensicPath,
1862
1937
  forensicAction,
1863
- forensicReport
1938
+ forensicReport,
1939
+ metaState: metaClassification.state,
1940
+ eventsState: eventsClassification.state,
1941
+ forensicState: forensicClassification.state
1864
1942
  };
1865
1943
  }
1866
1944
  async function executeInitFabricPlan(plan) {
1867
- const isReapply = plan.options?.reapply === true;
1868
1945
  if (plan.replaceFabricDir) {
1869
- rmSync(plan.fabricDir, { force: true });
1870
- }
1871
- mkdirSync(plan.fabricDir, { recursive: true });
1872
- if (plan.agentsMdAction === "created" && !existsSync6(plan.agentsMdPath)) {
1873
- await atomicWriteText2(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
1946
+ rmSync2(plan.fabricDir, { force: true });
1874
1947
  }
1875
- mkdirSync(plan.knowledgeDir, { recursive: true });
1948
+ mkdirSync2(plan.fabricDir, { recursive: true });
1949
+ writeDefaultFabricConfig(plan.fabricDir, plan.target);
1950
+ mkdirSync2(plan.knowledgeDir, { recursive: true });
1876
1951
  for (const sub of KNOWLEDGE_SUBDIRS) {
1877
- const teamSubDir = join5(plan.knowledgeDir, sub);
1878
- mkdirSync(teamSubDir, { recursive: true });
1879
- const teamGitkeep = join5(teamSubDir, ".gitkeep");
1880
- if (!existsSync6(teamGitkeep)) {
1952
+ const teamSubDir = join6(plan.knowledgeDir, sub);
1953
+ mkdirSync2(teamSubDir, { recursive: true });
1954
+ const teamGitkeep = join6(teamSubDir, ".gitkeep");
1955
+ if (!existsSync4(teamGitkeep)) {
1881
1956
  writeFileSync(teamGitkeep, "", "utf8");
1882
1957
  }
1883
1958
  }
1884
1959
  try {
1885
- mkdirSync(plan.personalKnowledgeDir, { recursive: true });
1960
+ mkdirSync2(plan.personalKnowledgeDir, { recursive: true });
1886
1961
  for (const sub of KNOWLEDGE_SUBDIRS) {
1887
- mkdirSync(join5(plan.personalKnowledgeDir, sub), { recursive: true });
1962
+ mkdirSync2(join6(plan.personalKnowledgeDir, sub), { recursive: true });
1888
1963
  }
1889
1964
  } catch {
1890
1965
  }
1891
- preparePlannedPath(plan.metaPath, plan.metaAction);
1892
- await atomicWriteJson(plan.metaPath, plan.meta);
1893
- if (isReapply) {
1894
- if (!existsSync6(plan.eventsPath)) {
1895
- mkdirSync(dirname2(plan.eventsPath), { recursive: true });
1896
- writeFileSync(plan.eventsPath, "", "utf8");
1897
- }
1898
- } else {
1966
+ if (plan.metaState === "missing") {
1967
+ preparePlannedPath(plan.metaPath, plan.metaAction);
1968
+ await atomicWriteJson(plan.metaPath, plan.meta);
1969
+ }
1970
+ if (plan.eventsState === "missing") {
1899
1971
  preparePlannedPath(plan.eventsPath, plan.eventsAction);
1972
+ mkdirSync2(dirname(plan.eventsPath), { recursive: true });
1900
1973
  writeFileSync(plan.eventsPath, "", "utf8");
1901
1974
  }
1902
1975
  preparePlannedPath(plan.forensicPath, plan.forensicAction);
1903
1976
  await atomicWriteJson(plan.forensicPath, plan.forensicReport);
1904
- if (!plan.options?.reapply) {
1905
- try {
1906
- await runInitScan(plan.target, { source: "init" });
1907
- } catch (error) {
1908
- writeStderr2(
1909
- `[warn] init-scan failed: ${error instanceof Error ? error.message : String(error)} \u2014 re-run \`fab scan\` to populate baseline knowledge entries.`
1910
- );
1977
+ if (existsSync4(plan.eventsPath)) {
1978
+ const applied = [];
1979
+ const canonical = [];
1980
+ const drifted = [];
1981
+ for (const entry of [
1982
+ { path: plan.metaPath, state: plan.metaState },
1983
+ { path: plan.eventsPath, state: plan.eventsState },
1984
+ { path: plan.forensicPath, state: plan.forensicState }
1985
+ ]) {
1986
+ if (entry.state === "missing") {
1987
+ applied.push(entry.path);
1988
+ } else if (entry.state === "present-canonical") {
1989
+ canonical.push(entry.path);
1990
+ } else {
1991
+ drifted.push(entry.path);
1992
+ }
1911
1993
  }
1912
- }
1913
- if (isReapply) {
1914
- appendReapplyLedgerEvent(plan.eventsPath, {
1915
- preserved_ledger: true
1916
- });
1994
+ appendInstallDiffLedgerEvent(plan.eventsPath, { applied, canonical, drifted });
1917
1995
  }
1918
1996
  return {
1919
1997
  agentsMdPath: plan.agentsMdPath,
@@ -1933,16 +2011,16 @@ async function initFabric(target, options) {
1933
2011
  return await executeInitFabricPlan(await buildInitFabricPlan(target, options));
1934
2012
  }
1935
2013
  function shouldUseInitWizard(args, terminalInteractive = isInteractiveInit()) {
1936
- return terminalInteractive && args.interactive !== false && args.yes !== true;
2014
+ return terminalInteractive && args.yes !== true;
1937
2015
  }
1938
- async function resolveInitExecutionPlanWithWizard(basePlan, args, wizardAdapter) {
2016
+ async function resolveInitExecutionPlanWithWizard(basePlan, wizardAdapter) {
1939
2017
  const selection = await wizardAdapter.run({
1940
2018
  target: basePlan.target,
1941
2019
  options: basePlan.options,
1942
2020
  supports: basePlan.supports,
1943
2021
  mcpInstallMode: basePlan.mcpInstallMode,
1944
2022
  claudeMcpScope: basePlan.claudeMcpScope,
1945
- lockedStages: collectLockedWizardStages(args)
2023
+ lockedStages: []
1946
2024
  });
1947
2025
  if (selection === null) {
1948
2026
  return null;
@@ -1980,27 +2058,36 @@ function printInitScaffoldResult(created) {
1980
2058
  function printInitPostSetup(plan, stageResults, finalSupports) {
1981
2059
  if (shouldPrintHooksNextStep(plan.options, stageResults)) {
1982
2060
  console.log(
1983
- t("cli.init.next-step", {
2061
+ t("cli.install.next-step", {
1984
2062
  label: nextLabel(),
1985
- message: paint.muted(t("cli.init.next-step.message"))
2063
+ message: paint.muted(t("cli.install.next-step.message"))
1986
2064
  })
1987
2065
  );
1988
2066
  }
1989
2067
  console.log(
1990
- t("cli.init.reason-message", {
2068
+ t("cli.install.reason-message", {
1991
2069
  label: reasonLabel(),
1992
2070
  message: paint.muted(formatInitReasonMessage(finalSupports))
1993
2071
  })
1994
2072
  );
1995
2073
  printInitStageSummary(stageResults);
1996
2074
  printInitCapabilitySummary(finalSupports, stageResults, plan.options);
2075
+ const fabricLanguage = readFabricLanguagePreference(plan.target);
2076
+ console.log(
2077
+ paint.muted(t("cli.install.language_preference_hint", { value: fabricLanguage }))
2078
+ );
2079
+ }
2080
+ function printInitDiffStateTable(entries) {
2081
+ for (const entry of entries) {
2082
+ console.log(` ${formatDiffFileState(entry.state)} ${entry.path}`);
2083
+ }
1997
2084
  }
1998
2085
  function printInitPlanPreview(plan) {
1999
- console.log(t("cli.init.plan.preview-title"));
2086
+ console.log(t("cli.install.plan.preview-title"));
2000
2087
  printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
2001
2088
  console.log(
2002
- t("cli.init.plan.preview-result", {
2003
- mode: plan.options.reapply ? t("cli.init.mode.reapply") : t("cli.init.mode.default"),
2089
+ t("cli.install.plan.preview-result", {
2090
+ mode: t("cli.install.mode.default"),
2004
2091
  bootstrap: yesNoLabel(!plan.options.skipBootstrap),
2005
2092
  mcp: yesNoLabel(!plan.options.skipMcp),
2006
2093
  hooks: yesNoLabel(!plan.options.skipHooks)
@@ -2030,27 +2117,32 @@ async function executeInitStagePlan(plan, stageName) {
2030
2117
  if (stage.skipped) {
2031
2118
  return { name: stageName, disposition: "skipped" };
2032
2119
  }
2033
- console.log(formatInitStageHeader(t(`cli.init.stages.${stageName}`)));
2120
+ console.log(formatInitStageHeader(t(`cli.install.stages.${stageName}`)));
2034
2121
  try {
2035
2122
  switch (stage.name) {
2036
2123
  case "bootstrap": {
2037
2124
  const installResults = [];
2125
+ installResults.push(...await runBestEffort("skill-deprecated-cleanup", () => cleanupDeprecatedSkills(plan.target)));
2038
2126
  installResults.push(...await runBestEffort("skill-install", () => installFabricArchiveSkill(plan.target)));
2039
2127
  installResults.push(...await runBestEffort("skill-review-install", () => installFabricReviewSkill(plan.target)));
2040
2128
  installResults.push(...await runBestEffort("skill-import-install", () => installFabricImportSkill(plan.target)));
2041
2129
  installResults.push(...await runBestEffort("hook-script", () => installArchiveHintHook(plan.target)));
2042
2130
  installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
2043
2131
  installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
2132
+ installResults.push(...await runBestEffort("hook-lib", () => installHookLibs(plan.target)));
2044
2133
  installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
2045
2134
  installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
2046
2135
  installResults.push(await runBestEffortSingle("cursor-hook-config", () => mergeCursorHookConfig(plan.target)));
2047
- installResults.push(...await runBestEffort("pointer", () => addArchiveSkillPointer(plan.target)));
2136
+ installResults.push(await runBestEffortSingle("bootstrap-snapshot", () => writeFabricAgentsSnapshot(plan.target)));
2137
+ installResults.push(await runBestEffortSingle("bootstrap-claude", () => writeClaudeBootstrapThinShell(plan.target)));
2138
+ installResults.push(await runBestEffortSingle("bootstrap-codex", () => writeCodexBootstrapManagedBlock(plan.target)));
2139
+ installResults.push(await runBestEffortSingle("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(plan.target)));
2048
2140
  const installedCount = installResults.filter((r) => r.status === "written").length;
2049
2141
  const skippedCount = installResults.filter((r) => r.status === "skipped").length;
2050
2142
  const errorCount = installResults.filter((r) => r.status === "error").length;
2051
2143
  for (const result of installResults) {
2052
2144
  if (result.status === "error") {
2053
- writeStderr2(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
2145
+ writeStderr(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
2054
2146
  }
2055
2147
  }
2056
2148
  const note2 = errorCount > 0 ? `errors=${errorCount}` : void 0;
@@ -2060,15 +2152,14 @@ async function executeInitStagePlan(plan, stageName) {
2060
2152
  case "mcp": {
2061
2153
  if (stage.installMode === "local") {
2062
2154
  const manager = stage.packageManager ?? detectPackageManager(plan.target);
2063
- writeStderr2(t("cli.init.mcp.install.local"));
2064
- writeStderr2(t("cli.init.mcp.local.installing", { manager }));
2155
+ writeStderr(t("cli.install.mcp.install.local"));
2156
+ writeStderr(t("cli.install.mcp.local.installing", { manager }));
2065
2157
  installLocalFabricServer(plan.target, manager);
2066
- writeStderr2(t("cli.init.mcp.local.installed"));
2158
+ writeStderr(t("cli.install.mcp.local.installed"));
2067
2159
  } else {
2068
- writeStderr2(t("cli.init.mcp.install.global"));
2160
+ writeStderr(t("cli.install.mcp.install.global"));
2069
2161
  }
2070
2162
  const result = await installMcpClients(plan.target, {
2071
- force: plan.options.force,
2072
2163
  localServerPath: stage.localServerPath,
2073
2164
  claudeMcpScope: stage.claudeMcpScope
2074
2165
  });
@@ -2080,7 +2171,7 @@ async function executeInitStagePlan(plan, stageName) {
2080
2171
  return { name: "mcp", disposition: "ran" };
2081
2172
  }
2082
2173
  case "hooks": {
2083
- const result = await installHooks(plan.target, { force: plan.options.force });
2174
+ const result = await installHooks(plan.target);
2084
2175
  console.log(formatInitStageResult("hooks", "completed", result.installed.length, result.skipped.length));
2085
2176
  return { name: "hooks", disposition: "ran" };
2086
2177
  }
@@ -2088,93 +2179,129 @@ async function executeInitStagePlan(plan, stageName) {
2088
2179
  return exhaustiveInitStagePlan(stage);
2089
2180
  }
2090
2181
  } catch (error) {
2091
- writeStderr2(formatInitStageFailure(stageName, error));
2182
+ writeStderr(formatInitStageFailure(stageName, error));
2092
2183
  return { name: stageName, disposition: "failed" };
2093
2184
  }
2094
2185
  }
2095
- function shouldReplaceWritableDirectory(path, options) {
2096
- if (!existsSync6(path)) {
2186
+ function shouldReplaceWritableDirectory(path, _options) {
2187
+ if (!existsSync4(path)) {
2097
2188
  return false;
2098
2189
  }
2099
- if (statSync2(path).isDirectory()) {
2190
+ if (statSync4(path).isDirectory()) {
2100
2191
  return false;
2101
2192
  }
2102
- if (!options?.force) {
2103
- throw new Error(t("cli.init.errors.abort-existing", { path }));
2104
- }
2105
- return true;
2193
+ return false;
2106
2194
  }
2107
- function planFreshPath(path, options) {
2108
- if (!existsSync6(path)) {
2109
- return "created";
2195
+ function classifyFreshPath(path, strategy) {
2196
+ if (!existsSync4(path)) {
2197
+ return { path, state: "missing" };
2110
2198
  }
2111
- if (!options?.force) {
2112
- throw new Error(t("cli.init.errors.abort-existing", { path }));
2199
+ let stat;
2200
+ try {
2201
+ stat = statSync4(path);
2202
+ } catch (error) {
2203
+ return {
2204
+ path,
2205
+ state: "user-modified",
2206
+ reason: error instanceof Error ? error.message : String(error)
2207
+ };
2208
+ }
2209
+ if (!stat.isFile()) {
2210
+ return { path, state: "user-modified", reason: "expected a file" };
2211
+ }
2212
+ if (strategy === "presence" || strategy === "always-rewrite") {
2213
+ return { path, state: "present-canonical" };
2214
+ }
2215
+ try {
2216
+ const raw = readFileSync3(path, "utf8");
2217
+ const parsed = JSON.parse(raw);
2218
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2219
+ return { path, state: "user-modified", reason: "not a JSON object" };
2220
+ }
2221
+ const record = parsed;
2222
+ const hasRevision = typeof record["revision"] === "string";
2223
+ const hasNodes = record["nodes"] !== void 0 && record["nodes"] !== null && typeof record["nodes"] === "object" && !Array.isArray(record["nodes"]);
2224
+ const hasCounters = record["counters"] !== void 0 && record["counters"] !== null && typeof record["counters"] === "object" && !Array.isArray(record["counters"]);
2225
+ if (!hasRevision || !hasNodes || !hasCounters) {
2226
+ return { path, state: "drifted", reason: "missing required AgentsMeta fields" };
2227
+ }
2228
+ return { path, state: "present-canonical" };
2229
+ } catch (error) {
2230
+ return {
2231
+ path,
2232
+ state: "user-modified",
2233
+ reason: error instanceof Error ? error.message : String(error)
2234
+ };
2113
2235
  }
2114
- return "overwritten";
2236
+ }
2237
+ function diffStateToWriteAction(_state) {
2238
+ return "created";
2239
+ }
2240
+ function formatDiffFileState(state) {
2241
+ return t(`cli.install.diff.state.${state}`);
2115
2242
  }
2116
2243
  function preparePlannedPath(path, action) {
2117
- mkdirSync(dirname2(path), { recursive: true });
2118
- if (action === "overwritten" && existsSync6(path)) {
2119
- rmSync(path, { recursive: true, force: true });
2244
+ mkdirSync2(dirname(path), { recursive: true });
2245
+ if (action === "overwritten" && existsSync4(path)) {
2246
+ rmSync2(path, { recursive: true, force: true });
2120
2247
  }
2121
2248
  }
2122
2249
  function createDefaultInitWizardAdapter() {
2123
2250
  return {
2124
2251
  async run(context) {
2125
- intro(t("cli.init.wizard.intro"));
2252
+ intro(t("cli.install.wizard.intro"));
2126
2253
  note(
2127
- t("cli.init.wizard.overview.body", {
2254
+ t("cli.install.wizard.overview.body", {
2128
2255
  target: context.target,
2129
2256
  mode: formatInitModeBadge(context.options)
2130
2257
  }),
2131
- t("cli.init.wizard.overview.title")
2258
+ t("cli.install.wizard.overview.title")
2132
2259
  );
2133
2260
  printInitPlanSummary(context.target, context.options, context.mcpInstallMode, context.supports);
2134
- log.step(t("cli.init.wizard.step.target"));
2261
+ log.step(t("cli.install.wizard.step.target"));
2135
2262
  const continueWithTarget = await confirm({
2136
- message: t("cli.init.wizard.target.confirm", { target: context.target }),
2263
+ message: t("cli.install.wizard.target.confirm", { target: context.target }),
2137
2264
  initialValue: true
2138
2265
  });
2139
2266
  if (isCancel(continueWithTarget) || !continueWithTarget) {
2140
2267
  emitInitWizardCancellation();
2141
2268
  return null;
2142
2269
  }
2143
- log.step(t("cli.init.wizard.step.plan"));
2270
+ log.step(t("cli.install.wizard.step.plan"));
2144
2271
  let groupedSelection;
2145
2272
  try {
2146
2273
  groupedSelection = await group(
2147
2274
  {
2148
2275
  bootstrap: async () => context.lockedStages.includes("bootstrap") ? false : confirmInGroup({
2149
- message: t("cli.init.wizard.stage.bootstrap", {
2276
+ message: t("cli.install.wizard.stage.bootstrap", {
2150
2277
  defaultValue: formatPromptDefault(!context.options.skipBootstrap)
2151
2278
  }),
2152
2279
  initialValue: !context.options.skipBootstrap
2153
2280
  }),
2154
2281
  mcp: async () => context.lockedStages.includes("mcp") ? false : confirmInGroup({
2155
- message: t("cli.init.wizard.stage.mcp", {
2282
+ message: t("cli.install.wizard.stage.mcp", {
2156
2283
  defaultValue: formatPromptDefault(!context.options.skipMcp)
2157
2284
  }),
2158
2285
  initialValue: !context.options.skipMcp
2159
2286
  }),
2160
2287
  mcpInstallMode: async ({ results }) => results.mcp ? selectMcpInstallModeInGroup({
2161
- message: t("cli.init.wizard.mcp-install", { defaultValue: context.mcpInstallMode }),
2288
+ message: t("cli.install.wizard.mcp-install", { defaultValue: context.mcpInstallMode }),
2162
2289
  initialValue: context.mcpInstallMode,
2163
2290
  options: [
2164
- { value: "global", label: "global", hint: t("cli.init.mcp.install.global") },
2165
- { value: "local", label: "local", hint: t("cli.init.mcp.install.local") }
2291
+ { value: "global", label: "global", hint: t("cli.install.mcp.install.global") },
2292
+ { value: "local", label: "local", hint: t("cli.install.mcp.install.local") }
2166
2293
  ]
2167
2294
  }) : context.mcpInstallMode,
2168
2295
  claudeMcpScope: async ({ results }) => results.mcp ? selectClaudeMcpScopeInGroup({
2169
- message: t("cli.init.wizard.mcp-scope", { defaultValue: context.claudeMcpScope }),
2296
+ message: t("cli.install.wizard.mcp-scope", { defaultValue: context.claudeMcpScope }),
2170
2297
  initialValue: context.claudeMcpScope,
2171
2298
  options: [
2172
- { value: "project", label: "project", hint: t("cli.init.mcp.scope.project") },
2173
- { value: "user", label: "user", hint: t("cli.init.mcp.scope.user") }
2299
+ { value: "project", label: "project", hint: t("cli.install.mcp.scope.project") },
2300
+ { value: "user", label: "user", hint: t("cli.install.mcp.scope.user") }
2174
2301
  ]
2175
2302
  }) : context.claudeMcpScope,
2176
2303
  hooks: async () => context.lockedStages.includes("hooks") ? false : confirmInGroup({
2177
- message: t("cli.init.wizard.stage.hooks", {
2304
+ message: t("cli.install.wizard.stage.hooks", {
2178
2305
  defaultValue: formatPromptDefault(!context.options.skipHooks)
2179
2306
  }),
2180
2307
  initialValue: !context.options.skipHooks
@@ -2203,23 +2330,23 @@ function createDefaultInitWizardAdapter() {
2203
2330
  skipMcp: !groupedSelection.mcp,
2204
2331
  skipHooks: !groupedSelection.hooks
2205
2332
  };
2206
- log.step(t("cli.init.wizard.step.review"));
2333
+ log.step(t("cli.install.wizard.step.review"));
2207
2334
  printInitPlanSummary(context.target, previewOptions, groupedSelection.mcpInstallMode, context.supports);
2208
2335
  const confirmed = await confirm({
2209
- message: t("cli.init.wizard.execute.confirm"),
2336
+ message: t("cli.install.wizard.execute.confirm"),
2210
2337
  initialValue: true
2211
2338
  });
2212
2339
  if (isCancel(confirmed) || !confirmed) {
2213
2340
  emitInitWizardCancellation();
2214
2341
  return null;
2215
2342
  }
2216
- outro(t("cli.init.wizard.outro"));
2343
+ outro(t("cli.install.wizard.outro"));
2217
2344
  return groupedSelection;
2218
2345
  }
2219
2346
  };
2220
2347
  }
2221
2348
  function emitInitWizardCancellation() {
2222
- cancel(t("cli.init.wizard.cancelled"));
2349
+ cancel(t("cli.install.wizard.cancelled"));
2223
2350
  }
2224
2351
  async function confirmInGroup(options) {
2225
2352
  const result = await confirm(options);
@@ -2250,74 +2377,42 @@ async function selectClaudeMcpScopeInGroup(options) {
2250
2377
  }
2251
2378
  return result;
2252
2379
  }
2253
- function collectLockedWizardStages(args) {
2254
- const lockedStages = [];
2255
- if (args.bootstrap === false) {
2256
- lockedStages.push("bootstrap");
2257
- }
2258
- if (args.mcp === false) {
2259
- lockedStages.push("mcp");
2260
- }
2261
- if (args.hooks === false) {
2262
- lockedStages.push("hooks");
2263
- }
2264
- return lockedStages;
2265
- }
2266
2380
  function formatPromptDefault(value) {
2267
2381
  return value ? "Y/n" : "y/N";
2268
2382
  }
2269
2383
  function formatInitModeBanner(options) {
2270
- if (options.planOnly && options.reapply) {
2271
- return t("cli.init.plan.mode-banner.plan-reapply");
2272
- }
2273
2384
  if (options.planOnly) {
2274
- return t("cli.init.plan.mode-banner.plan");
2385
+ return t("cli.install.plan.mode-banner.plan");
2275
2386
  }
2276
- if (options.reapply) {
2277
- return t("cli.init.plan.mode-banner.reapply");
2278
- }
2279
- return t("cli.init.plan.mode-banner.default");
2387
+ return t("cli.install.plan.mode-banner.default");
2280
2388
  }
2281
2389
  function formatInitModeBadge(options) {
2282
- if (options.planOnly && options.reapply) {
2283
- return t("cli.init.mode.badge.plan-reapply");
2284
- }
2285
2390
  if (options.planOnly) {
2286
- return t("cli.init.mode.badge.plan");
2391
+ return t("cli.install.mode.badge.plan");
2287
2392
  }
2288
- if (options.reapply) {
2289
- return t("cli.init.mode.badge.reapply");
2290
- }
2291
- return t("cli.init.mode.badge.default");
2393
+ return t("cli.install.mode.badge.default");
2292
2394
  }
2293
- function normalizeTarget2(targetInput) {
2294
- return isAbsolute2(targetInput) ? targetInput : resolve5(process.cwd(), targetInput);
2395
+ function normalizeTarget3(targetInput) {
2396
+ return isAbsolute3(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2295
2397
  }
2296
- function assertExistingDirectory2(target) {
2297
- if (!existsSync6(target) || !statSync2(target).isDirectory()) {
2398
+ function assertExistingDirectory3(target) {
2399
+ if (!existsSync4(target) || !statSync4(target).isDirectory()) {
2298
2400
  throw new Error(`Target must be an existing directory: ${target}`);
2299
2401
  }
2300
2402
  }
2301
2403
  function detectPackageManager(cwd) {
2302
- const workspaceRoot = resolve5(cwd);
2303
- if (existsSync6(join5(workspaceRoot, "pnpm-lock.yaml"))) {
2404
+ const workspaceRoot = resolve3(cwd);
2405
+ if (existsSync4(join6(workspaceRoot, "pnpm-lock.yaml"))) {
2304
2406
  return "pnpm";
2305
2407
  }
2306
- if (existsSync6(join5(workspaceRoot, "yarn.lock"))) {
2408
+ if (existsSync4(join6(workspaceRoot, "yarn.lock"))) {
2307
2409
  return "yarn";
2308
2410
  }
2309
- if (existsSync6(join5(workspaceRoot, "package-lock.json"))) {
2411
+ if (existsSync4(join6(workspaceRoot, "package-lock.json"))) {
2310
2412
  return "npm";
2311
2413
  }
2312
2414
  return "npm";
2313
2415
  }
2314
- function resolveMcpInstallMode(rawMode) {
2315
- if (rawMode === void 0 || rawMode === "global" || rawMode === "local") {
2316
- return rawMode ?? "global";
2317
- }
2318
- writeStderr2(t("cli.init.mcp.install.invalid", { value: rawMode }));
2319
- return "global";
2320
- }
2321
2416
  function installLocalFabricServer(target, manager) {
2322
2417
  const installArgs = manager === "npm" ? ["install", "-D", FABRIC_SERVER_PACKAGE] : ["add", "-D", FABRIC_SERVER_PACKAGE];
2323
2418
  childProcess.execFileSync(manager, installArgs, {
@@ -2333,14 +2428,16 @@ function createInitialMeta() {
2333
2428
  counters: defaultAgentsMetaCounters()
2334
2429
  };
2335
2430
  }
2336
- function appendReapplyLedgerEvent(eventsPath, payload) {
2431
+ function appendInstallDiffLedgerEvent(eventsPath, payload) {
2337
2432
  const event = {
2338
2433
  kind: "fabric-event",
2339
- id: `event:${randomUUID()}`,
2434
+ id: `event:${randomUUID2()}`,
2340
2435
  ts: Date.now(),
2341
2436
  schema_version: 1,
2342
- event_type: "reapply_completed",
2343
- preserved_ledger: payload.preserved_ledger
2437
+ event_type: "install_diff_applied",
2438
+ applied: payload.applied,
2439
+ canonical: payload.canonical,
2440
+ drifted: payload.drifted
2344
2441
  };
2345
2442
  const line = `${JSON.stringify(event)}
2346
2443
  `;
@@ -2391,7 +2488,7 @@ function printInitStageSummary(stageResults) {
2391
2488
  console.log(formatInitStageSummaryLine("failed", collectInitStageNames(stageResults, "failed")));
2392
2489
  }
2393
2490
  function formatInitStageSummaryLine(disposition, stages) {
2394
- const label = disposition === "ran" ? paint.success(t("cli.init.stages.summary.ran")) : disposition === "skipped" ? paint.muted(t("cli.init.stages.summary.skipped")) : paint.error(t("cli.init.stages.summary.failed"));
2491
+ const label = disposition === "ran" ? paint.success(t("cli.install.stages.summary.ran")) : disposition === "skipped" ? paint.muted(t("cli.install.stages.summary.skipped")) : paint.error(t("cli.install.stages.summary.failed"));
2395
2492
  return `${label}: ${stages.length > 0 ? stages.join(", ") : t("cli.shared.none")}`;
2396
2493
  }
2397
2494
  function collectInitStageNames(stageResults, disposition) {
@@ -2404,11 +2501,11 @@ function isInteractiveInit() {
2404
2501
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
2405
2502
  }
2406
2503
  function printInitPlanSummary(target, options, mcpInstallMode, supports) {
2407
- console.log(t("cli.init.plan.title"));
2504
+ console.log(t("cli.install.plan.title"));
2408
2505
  console.log(formatInitModeBanner(options));
2409
- console.log(t("cli.init.plan.target", { target }));
2506
+ console.log(t("cli.install.plan.target", { target }));
2410
2507
  console.log(
2411
- t("cli.init.plan.actions", {
2508
+ t("cli.install.plan.actions", {
2412
2509
  bootstrap: yesNoLabel(!options.skipBootstrap),
2413
2510
  mcp: yesNoLabel(!options.skipMcp),
2414
2511
  hooks: yesNoLabel(!options.skipHooks),
@@ -2417,31 +2514,32 @@ function printInitPlanSummary(target, options, mcpInstallMode, supports) {
2417
2514
  );
2418
2515
  const detected = supports.filter((support) => support.detected);
2419
2516
  console.log(
2420
- t("cli.init.plan.detected", {
2517
+ t("cli.install.plan.detected", {
2421
2518
  clients: detected.length > 0 ? detected.map((support) => support.label).join(", ") : t("cli.shared.none")
2422
2519
  })
2423
2520
  );
2424
- console.log(t("cli.init.plan.writes"));
2521
+ console.log(t("cli.install.plan.writes"));
2425
2522
  console.log(` - ${target}/.fabric/knowledge/{decisions,pitfalls,guidelines,models,processes,pending}/`);
2426
2523
  console.log(` - ${target}/.fabric/agents.meta.json`);
2427
2524
  console.log(` - ${target}/.fabric/events.jsonl`);
2428
2525
  console.log(` - ${target}/.fabric/forensic.json`);
2526
+ console.log(` - ${target}/.fabric/fabric-config.json`);
2429
2527
  }
2430
2528
  function printInitCapabilitySummary(supports, stageResults, options) {
2431
2529
  const detected = supports.filter((support) => support.detected);
2432
2530
  if (detected.length === 0) {
2433
- console.log(t("cli.init.capabilities.none"));
2531
+ console.log(t("cli.install.capabilities.none"));
2434
2532
  return;
2435
2533
  }
2436
- console.log(t("cli.init.capabilities.title"));
2534
+ console.log(t("cli.install.capabilities.title"));
2437
2535
  const rows = detected.map((support) => toCapabilityRow(support, stageResults, options));
2438
2536
  const headers = {
2439
- client: t("cli.init.capabilities.header.client"),
2440
- bootstrap: t("cli.init.capabilities.header.bootstrap"),
2441
- mcp: t("cli.init.capabilities.header.mcp"),
2442
- hook: t("cli.init.capabilities.header.hook"),
2443
- skill: t("cli.init.capabilities.header.skill"),
2444
- followUp: t("cli.init.capabilities.header.follow-up")
2537
+ client: t("cli.install.capabilities.header.client"),
2538
+ bootstrap: t("cli.install.capabilities.header.bootstrap"),
2539
+ mcp: t("cli.install.capabilities.header.mcp"),
2540
+ hook: t("cli.install.capabilities.header.hook"),
2541
+ skill: t("cli.install.capabilities.header.skill"),
2542
+ followUp: t("cli.install.capabilities.header.follow-up")
2445
2543
  };
2446
2544
  const widths = {
2447
2545
  client: Math.max(displayWidth(headers.client), ...rows.map((row) => displayWidth(row.client))),
@@ -2456,11 +2554,13 @@ function printInitCapabilitySummary(supports, stageResults, options) {
2456
2554
  for (const row of rows) {
2457
2555
  console.log(formatCapabilityTableRow(row, widths));
2458
2556
  }
2557
+ console.log("");
2558
+ console.log(t("cli.install.restart-banner"));
2459
2559
  }
2460
2560
  function toCapabilityRow(support, stageResults, options) {
2461
2561
  const stage = (name) => stageResults.find((entry) => entry.name === name)?.disposition ?? null;
2462
- const bootstrap = support.capabilities.bootstrap ? capabilityStatus(options.skipBootstrap ? "skipped" : stage("bootstrap")) : t("cli.init.capabilities.status.na");
2463
- const mcp = support.capabilities.mcp ? capabilityStatus(options.skipMcp ? "skipped" : stage("mcp")) : t("cli.init.capabilities.status.na");
2562
+ const bootstrap = support.capabilities.bootstrap ? capabilityStatus(options.skipBootstrap ? "skipped" : stage("bootstrap")) : t("cli.install.capabilities.status.na");
2563
+ const mcp = support.capabilities.mcp ? capabilityStatus(options.skipMcp ? "skipped" : stage("mcp")) : t("cli.install.capabilities.status.na");
2464
2564
  const hook = capabilityInstallStatus(support, "hook");
2465
2565
  const skill = capabilityInstallStatus(support, "skill");
2466
2566
  return {
@@ -2469,14 +2569,14 @@ function toCapabilityRow(support, stageResults, options) {
2469
2569
  mcp,
2470
2570
  hook,
2471
2571
  skill,
2472
- followUp: hasInstalledCapability(support, "skill") ? t("cli.init.capabilities.follow-up.ready") : support.capabilities.skill ? t("cli.init.capabilities.follow-up.install") : t("cli.init.capabilities.follow-up.manual")
2572
+ followUp: hasInstalledCapability(support, "skill") ? t("cli.install.capabilities.follow-up.ready") : support.capabilities.skill ? t("cli.install.capabilities.follow-up.install") : t("cli.install.capabilities.follow-up.manual")
2473
2573
  };
2474
2574
  }
2475
2575
  function capabilityInstallStatus(support, capability) {
2476
2576
  if (!support.capabilities[capability]) {
2477
- return t("cli.init.capabilities.status.na");
2577
+ return t("cli.install.capabilities.status.na");
2478
2578
  }
2479
- return hasInstalledCapability(support, capability) ? t("cli.init.capabilities.status.installed") : t("cli.init.capabilities.status.supported");
2579
+ return hasInstalledCapability(support, capability) ? t("cli.install.capabilities.status.installed") : t("cli.install.capabilities.status.supported");
2480
2580
  }
2481
2581
  function hasInstalledCapability(support, capability) {
2482
2582
  return support.installedCapabilities?.[capability] === true;
@@ -2484,15 +2584,15 @@ function hasInstalledCapability(support, capability) {
2484
2584
  function capabilityStatus(disposition) {
2485
2585
  switch (disposition) {
2486
2586
  case "ran":
2487
- return t("cli.init.capabilities.status.ready");
2587
+ return t("cli.install.capabilities.status.ready");
2488
2588
  case "skipped":
2489
- return t("cli.init.capabilities.status.skipped");
2589
+ return t("cli.install.capabilities.status.skipped");
2490
2590
  case "failed":
2491
- return t("cli.init.capabilities.status.failed");
2591
+ return t("cli.install.capabilities.status.failed");
2492
2592
  case null:
2493
- return t("cli.init.capabilities.status.na");
2593
+ return t("cli.install.capabilities.status.na");
2494
2594
  default:
2495
- return t("cli.init.capabilities.status.ready");
2595
+ return t("cli.install.capabilities.status.ready");
2496
2596
  }
2497
2597
  }
2498
2598
  function formatCapabilityTableRow(row, widths) {
@@ -2518,21 +2618,21 @@ function formatCapabilityDivider(widths) {
2518
2618
  function formatInitReasonMessage(supports) {
2519
2619
  const detected = supports.filter((support) => support.detected);
2520
2620
  if (detected.some((support) => support.capabilities.skill)) {
2521
- return t("cli.init.reason-message.installable-body");
2621
+ return t("cli.install.reason-message.installable-body");
2522
2622
  }
2523
- return t("cli.init.reason-message.manual-body");
2623
+ return t("cli.install.reason-message.manual-body");
2524
2624
  }
2525
2625
  function yesNoLabel(value) {
2526
2626
  return value ? t("cli.shared.yes") : t("cli.shared.no");
2527
2627
  }
2528
2628
  function formatInitPathAction(path, action) {
2529
- return t("cli.init.created-path", { label: labelForInitWriteAction(action), path });
2629
+ return t("cli.install.created-path", { label: labelForInitWriteAction(action), path });
2530
2630
  }
2531
2631
  function formatAgentsMdAction(path, action) {
2532
2632
  if (action === "preserved") {
2533
- return t("cli.init.skipped-existing-path", { label: skippedLabel(), path });
2633
+ return t("cli.install.skipped-existing-path", { label: skippedLabel(), path });
2534
2634
  }
2535
- return t("cli.init.created-path", { label: createdLabel(), path });
2635
+ return t("cli.install.created-path", { label: createdLabel(), path });
2536
2636
  }
2537
2637
  function labelForInitWriteAction(action) {
2538
2638
  return action === "overwritten" ? overwrittenLabel() : createdLabel();
@@ -2550,18 +2650,18 @@ function reasonLabel() {
2550
2650
  return paint.human(t("cli.shared.reason"));
2551
2651
  }
2552
2652
  function overwrittenLabel() {
2553
- return paint.warn(t("cli.init.force.overwritten"));
2653
+ return paint.warn(t("cli.install.label.overwritten"));
2554
2654
  }
2555
2655
  function completedStageLabel() {
2556
- return paint.success(t("cli.init.stages.completed"));
2656
+ return paint.success(t("cli.install.stages.completed"));
2557
2657
  }
2558
2658
  function skippedStageLabel() {
2559
- return paint.muted(t("cli.init.stages.skipped"));
2659
+ return paint.muted(t("cli.install.stages.skipped"));
2560
2660
  }
2561
2661
  function failedStageLabel() {
2562
- return paint.error(t("cli.init.stages.failed"));
2662
+ return paint.error(t("cli.install.stages.failed"));
2563
2663
  }
2564
- function writeStderr2(message) {
2664
+ function writeStderr(message) {
2565
2665
  process.stderr.write(`${message}
2566
2666
  `);
2567
2667
  }
@@ -2569,13 +2669,15 @@ export {
2569
2669
  buildInitExecutionPlan,
2570
2670
  buildInitFabricPlan,
2571
2671
  createDefaultInitWizardAdapter,
2572
- init_default as default,
2672
+ install_default as default,
2573
2673
  detectPackageManager,
2574
2674
  executeInitExecutionPlan,
2575
2675
  executeInitFabricPlan,
2576
- initCommand,
2577
2676
  initFabric,
2677
+ installCommand,
2578
2678
  resolveInitExecutionPlanWithWizard,
2679
+ runHooksOnlyRefresh,
2579
2680
  runInitCommand,
2681
+ runSkillsOnlyRefresh,
2580
2682
  shouldUseInitWizard
2581
2683
  };