@fenglimg/fabric-cli 2.0.0-rc.13 → 2.0.0-rc.21

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 (29) hide show
  1. package/README.md +4 -2
  2. package/dist/{chunk-X7QPY5KH.js → chunk-4HC5ZK7H.js} +296 -301
  3. package/dist/{chunk-FDRLV5PL.js → chunk-FNO7CQDG.js} +5 -213
  4. package/dist/{chunk-WWNXR34K.js → chunk-G2CIOLD4.js} +16 -1
  5. package/dist/chunk-KZ2YITOS.js +225 -0
  6. package/dist/{chunk-OHWQNSLH.js → chunk-MF3OTILQ.js} +267 -44
  7. package/dist/{chunk-OBQU6NHO.js → chunk-ZSESMG6L.js} +0 -6
  8. package/dist/config-AYP5F72E.js +13 -0
  9. package/dist/doctor-L6TIXXIX.js +425 -0
  10. package/dist/index.js +11 -9
  11. package/dist/{install-SLS5W27W.js → install-DNZXGFHJ.js} +344 -359
  12. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-CFDGXHCA.js} +10 -5
  13. package/dist/{serve-NGLXHDYC.js → serve-6PPQX7AW.js} +16 -11
  14. package/dist/{uninstall-JHUSFENL.js → uninstall-L2HEEOU3.js} +200 -215
  15. package/package.json +3 -3
  16. package/templates/hooks/configs/README.md +9 -5
  17. package/templates/hooks/configs/cursor-hooks.json +7 -10
  18. package/templates/hooks/fabric-hint.cjs +350 -21
  19. package/templates/hooks/knowledge-hint-broad.cjs +39 -14
  20. package/templates/hooks/knowledge-hint-narrow.cjs +31 -7
  21. package/templates/hooks/lib/banner-i18n.cjs +252 -0
  22. package/dist/chunk-Q72D24BG.js +0 -186
  23. package/dist/doctor-RILCO5OG.js +0 -282
  24. package/dist/hooks-HIWYI3VG.js +0 -13
  25. package/dist/scan-VHKZPT2W.js +0 -24
  26. package/templates/agents-md/AGENTS.md.template +0 -59
  27. package/templates/bootstrap/CLAUDE.md +0 -8
  28. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  29. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
@@ -1,197 +1,202 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- hooksCommand,
4
- installHooks
5
- } from "./chunk-Q72D24BG.js";
3
+ installMcpClients
4
+ } from "./chunk-KZ2YITOS.js";
6
5
  import {
7
6
  detectExistingLanguage,
8
- detectFramework,
9
7
  runInitScan
10
- } from "./chunk-FDRLV5PL.js";
8
+ } from "./chunk-FNO7CQDG.js";
11
9
  import {
12
- detectClientSupports,
13
- resolveClients
14
- } from "./chunk-OHWQNSLH.js";
15
- import {
16
- addFabricKnowledgeBaseSection,
17
10
  installArchiveHintHook,
18
11
  installFabricArchiveSkill,
19
12
  installFabricImportSkill,
20
13
  installFabricReviewSkill,
14
+ installHookLibs,
21
15
  installKnowledgeHintBroadHook,
22
16
  installKnowledgeHintNarrowHook,
23
17
  mergeClaudeCodeHookConfig,
24
18
  mergeCodexHookConfig,
25
19
  mergeCursorHookConfig,
26
- readFabricLanguagePreference
27
- } from "./chunk-X7QPY5KH.js";
20
+ readFabricLanguagePreference,
21
+ writeClaudeBootstrapThinShell,
22
+ writeCodexBootstrapManagedBlock,
23
+ writeCursorBootstrapManagedBlock,
24
+ writeFabricAgentsSnapshot
25
+ } from "./chunk-4HC5ZK7H.js";
26
+ import {
27
+ detectClientSupports
28
+ } from "./chunk-MF3OTILQ.js";
28
29
  import {
29
30
  displayWidth,
31
+ hasActionHint,
30
32
  padEnd,
31
- paint
32
- } from "./chunk-WWNXR34K.js";
33
- import {
34
- createDebugLogger,
35
- resolveDevMode
36
- } from "./chunk-OBQU6NHO.js";
33
+ paint,
34
+ renderFabricError
35
+ } from "./chunk-G2CIOLD4.js";
37
36
  import {
38
37
  t
39
38
  } from "./chunk-6ICJICVU.js";
39
+ import {
40
+ createDebugLogger,
41
+ resolveDevMode
42
+ } from "./chunk-ZSESMG6L.js";
40
43
 
41
44
  // src/commands/install.ts
42
45
  import { randomUUID } from "crypto";
43
46
  import { homedir } from "os";
44
47
  import * as childProcess from "child_process";
45
- import { appendFileSync, existsSync as existsSync3, mkdirSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
46
- import { dirname, isAbsolute as isAbsolute2, join as join2, resolve as resolve3 } from "path";
48
+ import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync3, writeFileSync } from "fs";
49
+ import { dirname, isAbsolute as isAbsolute3, join as join3, resolve as resolve3 } from "path";
47
50
  import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
48
51
  import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
49
- import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
50
- import { defineCommand as defineCommand2 } from "citty";
52
+ import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
53
+ import { defineCommand } from "citty";
51
54
  import { checkLockOrThrow } from "@fenglimg/fabric-server";
52
55
 
53
- // src/commands/config.ts
54
- import { existsSync } from "fs";
55
- import { readFile } from "fs/promises";
56
- import { resolve } from "path";
57
- import { fileURLToPath } from "url";
58
- import { defineCommand } from "citty";
59
- var CLIENT_ALIASES = {
60
- claude: "ClaudeCodeCLI",
61
- claudecodecli: "ClaudeCodeCLI",
62
- "claude-code-cli": "ClaudeCodeCLI",
63
- claudecli: "ClaudeCodeCLI",
64
- claudecodedesktop: "ClaudeCodeDesktop",
65
- "claude-code-desktop": "ClaudeCodeDesktop",
66
- claudedesktop: "ClaudeCodeDesktop",
67
- cursor: "Cursor",
68
- codexcli: "CodexCLI",
69
- "codex-cli": "CodexCLI",
70
- codex: "CodexCLI"
71
- };
72
- function parseClientFilter(value) {
73
- if (value === void 0 || value.trim().length === 0) {
74
- return null;
75
- }
76
- const clients = /* @__PURE__ */ new Set();
77
- for (const rawClient of value.split(",")) {
78
- const alias = rawClient.trim().toLowerCase();
79
- const clientKind = CLIENT_ALIASES[alias];
80
- if (clientKind === void 0) {
81
- throw new Error(t("cli.config.errors.unknown-client", { client: rawClient }));
56
+ // src/install/hooks-orchestrator.ts
57
+ import { existsSync, statSync } from "fs";
58
+ import { isAbsolute, join, resolve } from "path";
59
+ async function installHooks(target, _options = {}) {
60
+ const normalizedTarget = normalizeTarget(target);
61
+ assertExistingDirectory(normalizedTarget);
62
+ const results = [];
63
+ results.push(...await runStep(() => installFabricArchiveSkill(normalizedTarget)));
64
+ results.push(...await runStep(() => installFabricReviewSkill(normalizedTarget)));
65
+ results.push(...await runStep(() => installFabricImportSkill(normalizedTarget)));
66
+ results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
67
+ results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
68
+ results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
69
+ results.push(...await runStep(() => installHookLibs(normalizedTarget)));
70
+ results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
71
+ results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
72
+ results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
73
+ results.push(await runSingleStep("bootstrap-snapshot", () => writeFabricAgentsSnapshot(normalizedTarget)));
74
+ results.push(await runSingleStep("bootstrap-claude", () => writeClaudeBootstrapThinShell(normalizedTarget)));
75
+ results.push(await runSingleStep("bootstrap-codex", () => writeCodexBootstrapManagedBlock(normalizedTarget)));
76
+ results.push(await runSingleStep("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(normalizedTarget)));
77
+ results.push(...validateHookPaths(normalizedTarget));
78
+ return summarizeResults(results);
79
+ }
80
+ function validateHookPaths(projectRoot) {
81
+ const scripts = [
82
+ { stepSuffix: "", hookFile: "fabric-hint.cjs" },
83
+ { stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
84
+ { stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
85
+ ];
86
+ const clients = [
87
+ {
88
+ client: "claude",
89
+ configRel: join(".claude", "settings.json"),
90
+ hookDir: join(".claude", "hooks")
91
+ },
92
+ {
93
+ client: "codex",
94
+ configRel: join(".codex", "hooks.json"),
95
+ hookDir: join(".codex", "hooks")
96
+ },
97
+ {
98
+ client: "cursor",
99
+ configRel: join(".cursor", "hooks.json"),
100
+ hookDir: join(".cursor", "hooks")
101
+ }
102
+ ];
103
+ const results = [];
104
+ for (const { client, configRel, hookDir } of clients) {
105
+ const configPath = resolve(projectRoot, configRel);
106
+ if (!existsSync(configPath)) {
107
+ results.push({
108
+ step: `hook-validate-${client}`,
109
+ path: configPath,
110
+ status: "skipped",
111
+ message: "missing-config"
112
+ });
113
+ continue;
114
+ }
115
+ for (const { stepSuffix, hookFile } of scripts) {
116
+ const expectedHookPath = resolve(projectRoot, hookDir, hookFile);
117
+ const expectedHookRel = join(hookDir, hookFile);
118
+ const step = `hook-validate-${client}${stepSuffix}`;
119
+ if (!existsSync(expectedHookPath)) {
120
+ results.push({
121
+ step,
122
+ path: expectedHookPath,
123
+ status: "error",
124
+ message: `hook script missing: ${expectedHookRel}`
125
+ });
126
+ continue;
127
+ }
128
+ results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
82
129
  }
83
- clients.add(clientKind);
84
130
  }
85
- return clients;
131
+ return results;
86
132
  }
87
- async function loadFabricConfig(workspaceRoot) {
88
- const configPath = resolve(workspaceRoot, "fabric.config.json");
89
- if (!existsSync(configPath)) {
90
- return {};
91
- }
92
- const parsed = JSON.parse(await readFile(configPath, "utf8"));
93
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
94
- throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
133
+ async function runStep(fn) {
134
+ try {
135
+ return await fn();
136
+ } catch (error) {
137
+ return [
138
+ {
139
+ step: "hook-install",
140
+ path: "",
141
+ status: "error",
142
+ message: error instanceof Error ? error.message : String(error)
143
+ }
144
+ ];
95
145
  }
96
- return parsed;
97
- }
98
- function resolveServerPath(override) {
99
- if (override) return override;
100
- if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
101
- return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
102
146
  }
103
- function writeStderr(message) {
104
- process.stderr.write(`${message}
105
- `);
106
- }
107
- var configCmd = defineCommand({
108
- meta: {
109
- name: "config",
110
- description: t("cli.config.description")
111
- },
112
- subCommands: {
113
- hooks: hooksCommand,
114
- install: defineCommand({
115
- meta: {
116
- name: "install",
117
- description: t("cli.config.install.description")
118
- },
119
- args: {
120
- clients: {
121
- type: "string",
122
- description: t("cli.config.install.args.clients.description")
123
- },
124
- "dry-run": {
125
- type: "boolean",
126
- description: t("cli.config.install.args.dry-run.description"),
127
- default: false
128
- }
129
- },
130
- async run({ args }) {
131
- const selectedClients = parseClientFilter(args.clients);
132
- const result = await installMcpClients(process.cwd(), {
133
- clients: selectedClients === null ? void 0 : Array.from(selectedClients),
134
- dryRun: args["dry-run"]
135
- });
136
- if (result.details.length === 0) {
137
- writeStderr(t("cli.config.install.no-configs"));
138
- return;
139
- }
140
- for (const detail of result.details) {
141
- if (detail.action === "skipped") {
142
- writeStderr(t("cli.config.install.no-config-path", { client: detail.client }));
143
- continue;
144
- }
145
- if (detail.action === "dry-run" && detail.path !== null) {
146
- writeStderr(t("cli.config.install.dry-run", { client: detail.client, path: detail.path }));
147
- continue;
148
- }
149
- if (detail.path !== null) {
150
- writeStderr(t("cli.config.install.wrote", { client: detail.client, path: detail.path }));
151
- }
152
- }
153
- }
154
- })
147
+ async function runSingleStep(step, fn) {
148
+ try {
149
+ return await fn();
150
+ } catch (error) {
151
+ return {
152
+ step,
153
+ path: "",
154
+ status: "error",
155
+ message: error instanceof Error ? error.message : String(error)
156
+ };
155
157
  }
156
- });
157
- async function installMcpClients(target, options = {}) {
158
- const workspaceRoot = resolve(target);
159
- const fabricConfig = await loadFabricConfig(workspaceRoot);
160
- const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
161
- const serverPath = resolveServerPath(options.localServerPath);
162
- const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
163
- (writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
164
- );
158
+ }
159
+ function summarizeResults(results) {
165
160
  const installed = [];
166
161
  const skipped = [];
167
- const details = [];
168
- for (const writer of writers) {
169
- const configPath = await writer.detect(workspaceRoot);
170
- if (configPath === null) {
171
- skipped.push(writer.clientKind);
172
- details.push({ client: writer.clientKind, path: null, action: "skipped" });
173
- continue;
174
- }
175
- if (options.dryRun) {
176
- skipped.push(writer.clientKind);
177
- details.push({ client: writer.clientKind, path: configPath, action: "dry-run" });
178
- continue;
162
+ const errors = [];
163
+ for (const r of results) {
164
+ switch (r.status) {
165
+ case "written":
166
+ installed.push(r.path);
167
+ break;
168
+ case "skipped":
169
+ skipped.push(r.path);
170
+ break;
171
+ case "error":
172
+ errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
173
+ break;
179
174
  }
180
- await writer.write(serverPath, workspaceRoot);
181
- installed.push(writer.clientKind);
182
- details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
183
175
  }
184
- return { installed, skipped, details };
176
+ return { installed, skipped, errors };
177
+ }
178
+ function normalizeTarget(targetInput) {
179
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
180
+ }
181
+ function assertExistingDirectory(target) {
182
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
183
+ throw new Error(t("cli.shared.target-invalid", { target }));
184
+ }
185
185
  }
186
186
 
187
187
  // src/scanner/forensic.ts
188
188
  import { execFileSync } from "child_process";
189
- import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
189
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
190
190
  import { createRequire } from "module";
191
- import { basename, extname, isAbsolute, join, posix, relative, resolve as resolve2, sep } from "path";
191
+ import { basename, extname, isAbsolute as isAbsolute2, join as join2, posix, relative, resolve as resolve2, sep } from "path";
192
192
  import {
193
193
  forensicReportSchema
194
194
  } from "@fenglimg/fabric-shared";
195
+
196
+ // src/scanner/detector.ts
197
+ import { detectFramework } from "@fenglimg/fabric-shared/node";
198
+
199
+ // src/scanner/forensic.ts
195
200
  var require2 = createRequire(import.meta.url);
196
201
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
197
202
  ".fabric",
@@ -274,7 +279,7 @@ var parserInitPromise = null;
274
279
  var languagePromiseByKind = {};
275
280
  var parserBundlePromiseByKind = {};
276
281
  async function buildForensicReport(targetInput) {
277
- const target = normalizeTarget(targetInput);
282
+ const target = normalizeTarget2(targetInput);
278
283
  const framework = detectFramework(target);
279
284
  const topology = buildTopology(target);
280
285
  const entryPoints = collectEntryPoints(target, topology.files);
@@ -310,11 +315,11 @@ async function buildForensicReport(targetInput) {
310
315
  }
311
316
  return validation.data;
312
317
  }
313
- function normalizeTarget(targetInput) {
314
- return isAbsolute(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
318
+ function normalizeTarget2(targetInput) {
319
+ return isAbsolute2(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
315
320
  }
316
321
  function buildTopology(root) {
317
- assertExistingDirectory(root);
322
+ assertExistingDirectory2(root);
318
323
  const byExt = {};
319
324
  const keyDirs = /* @__PURE__ */ new Set();
320
325
  const files = [];
@@ -327,7 +332,7 @@ function buildTopology(root) {
327
332
  continue;
328
333
  }
329
334
  for (const entry of readdirSync(current, { withFileTypes: true })) {
330
- const absolutePath = join(current, entry.name);
335
+ const absolutePath = join2(current, entry.name);
331
336
  const relativePath = toPosixPath(relative(root, absolutePath));
332
337
  if (relativePath.length === 0) {
333
338
  continue;
@@ -347,7 +352,7 @@ function buildTopology(root) {
347
352
  if (!entry.isFile()) {
348
353
  continue;
349
354
  }
350
- const stats = statSync(absolutePath);
355
+ const stats = statSync2(absolutePath);
351
356
  const extension = extname(entry.name) || "[none]";
352
357
  byExt[extension] = (byExt[extension] ?? 0) + 1;
353
358
  totalFiles += 1;
@@ -365,8 +370,8 @@ function buildTopology(root) {
365
370
  files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
366
371
  };
367
372
  }
368
- function assertExistingDirectory(target) {
369
- if (!existsSync2(target) || !statSync(target).isDirectory()) {
373
+ function assertExistingDirectory2(target) {
374
+ if (!existsSync2(target) || !statSync2(target).isDirectory()) {
370
375
  throw new Error(`Target must be an existing directory: ${target}`);
371
376
  }
372
377
  }
@@ -415,7 +420,7 @@ function getEntryPointReason(relativePath) {
415
420
  async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
416
421
  const samples = [];
417
422
  for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
418
- const absolutePath = join(target, ...entryPoint.path.split("/"));
423
+ const absolutePath = join2(target, ...entryPoint.path.split("/"));
419
424
  const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
420
425
  const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
421
426
  frameworkKind,
@@ -452,7 +457,7 @@ function readFirstLines(path, lineLimit) {
452
457
  }
453
458
  }
454
459
  function readPackageDependencies(target) {
455
- const packageJsonPath = join(target, "package.json");
460
+ const packageJsonPath = join2(target, "package.json");
456
461
  if (!existsSync2(packageJsonPath)) {
457
462
  return /* @__PURE__ */ new Map();
458
463
  }
@@ -793,8 +798,8 @@ function scoreFrameworkConfidence(input) {
793
798
  return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
794
799
  }
795
800
  function readReadmeInfo(target) {
796
- const readmePath = join(target, "README.md");
797
- const hasContributing = existsSync2(join(target, "CONTRIBUTING.md"));
801
+ const readmePath = join2(target, "README.md");
802
+ const hasContributing = existsSync2(join2(target, "CONTRIBUTING.md"));
798
803
  if (!existsSync2(readmePath)) {
799
804
  return {
800
805
  quality: "missing",
@@ -1280,7 +1285,7 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
1280
1285
  return recommendations;
1281
1286
  }
1282
1287
  function readProjectName(target) {
1283
- const packageJsonPath = join(target, "package.json");
1288
+ const packageJsonPath = join2(target, "package.json");
1284
1289
  if (existsSync2(packageJsonPath)) {
1285
1290
  try {
1286
1291
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
@@ -1294,7 +1299,7 @@ function readProjectName(target) {
1294
1299
  return basename(target);
1295
1300
  }
1296
1301
  function getCliVersion() {
1297
- return true ? "2.0.0-rc.13" : "unknown";
1302
+ return true ? "2.0.0-rc.21" : "unknown";
1298
1303
  }
1299
1304
  function sortRecord(record) {
1300
1305
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -1304,81 +1309,33 @@ function toPosixPath(path) {
1304
1309
  }
1305
1310
 
1306
1311
  // src/commands/install.ts
1307
- var AGENTS_MD_DEFAULT_CONTENT = `# Project Knowledge
1308
-
1309
- This project uses [Fabric](https://github.com/fenglimg/fabric) for cross-client AI knowledge management.
1310
-
1311
- Knowledge entries live in \`.fabric/knowledge/\` (team) and \`~/.fabric/knowledge/\` (personal).
1312
- Run \`fabric doctor\` to verify state.
1313
-
1314
- See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
1315
- `;
1316
- var LOCAL_FABRIC_SERVER_PATH = join2("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1312
+ var LOCAL_FABRIC_SERVER_PATH = join3("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1317
1313
  var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
1318
1314
  var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
1319
- var installCommand = defineCommand2({
1315
+ var installCommand = defineCommand({
1320
1316
  meta: {
1321
1317
  name: "install",
1322
1318
  description: t("cli.install.description")
1323
1319
  },
1324
1320
  args: {
1325
- target: {
1326
- type: "string",
1327
- description: t("cli.install.args.target.description")
1328
- },
1329
1321
  debug: {
1330
1322
  type: "boolean",
1331
1323
  description: t("cli.install.args.debug.description"),
1332
1324
  default: false
1333
1325
  },
1334
- force: {
1326
+ "dry-run": {
1335
1327
  type: "boolean",
1336
- description: t("cli.install.args.force.description"),
1328
+ description: t("cli.install.args.dry-run.description"),
1337
1329
  default: false
1338
1330
  },
1331
+ target: {
1332
+ type: "string",
1333
+ description: t("cli.install.args.target.description")
1334
+ },
1339
1335
  yes: {
1340
1336
  type: "boolean",
1341
1337
  description: t("cli.install.args.yes.description"),
1342
1338
  default: false
1343
- },
1344
- plan: {
1345
- type: "boolean",
1346
- description: t("cli.install.args.plan.description"),
1347
- default: false
1348
- },
1349
- reapply: {
1350
- type: "boolean",
1351
- description: t("cli.install.args.reapply.description"),
1352
- default: false
1353
- },
1354
- bootstrap: {
1355
- type: "boolean",
1356
- default: true,
1357
- negativeDescription: t("cli.install.args.no-bootstrap.description")
1358
- },
1359
- mcp: {
1360
- type: "boolean",
1361
- default: true,
1362
- negativeDescription: t("cli.install.args.no-mcp.description")
1363
- },
1364
- hooks: {
1365
- type: "boolean",
1366
- default: true,
1367
- negativeDescription: t("cli.install.args.no-hooks.description")
1368
- },
1369
- interactive: {
1370
- type: "boolean",
1371
- description: t("cli.install.args.interactive.description"),
1372
- default: true
1373
- },
1374
- "mcp-install": {
1375
- type: "string",
1376
- default: "global",
1377
- description: t("cli.install.mcp.install.prompt")
1378
- },
1379
- scope: {
1380
- type: "string",
1381
- description: t("cli.install.mcp.scope.description")
1382
1339
  }
1383
1340
  },
1384
1341
  async run({ args }) {
@@ -1390,22 +1347,22 @@ async function runInitCommand(args) {
1390
1347
  const logger = createDebugLogger(args.debug);
1391
1348
  const resolution = resolveDevMode(args.target, process.cwd());
1392
1349
  const intent = resolveInitCliIntent(args, resolution.target);
1393
- if (args.reapply === true) {
1394
- checkLockOrThrow(intent.target, { force: args.force });
1350
+ const fabricInitialized = existsSync3(join3(intent.target, ".fabric", "events.jsonl"));
1351
+ if (fabricInitialized) {
1352
+ try {
1353
+ checkLockOrThrow(intent.target);
1354
+ } catch (err) {
1355
+ if (hasActionHint(err)) {
1356
+ renderFabricError(err);
1357
+ process.exit(1);
1358
+ }
1359
+ throw err;
1360
+ }
1395
1361
  }
1396
1362
  logger(`init target source: ${resolution.source}`);
1397
1363
  for (const step of resolution.chain) {
1398
1364
  logger(step);
1399
1365
  }
1400
- if (intent.options.planOnly) {
1401
- writeStderr2(t("cli.install.compat.plan"));
1402
- }
1403
- if (args.interactive === false) {
1404
- writeStderr2(t("cli.install.compat.interactive"));
1405
- }
1406
- if (args.bootstrap === false || args.mcp === false || args.hooks === false) {
1407
- writeStderr2(t("cli.install.compat.legacy-stage-flags"));
1408
- }
1409
1366
  const supports = detectClientSupports(intent.target);
1410
1367
  const basePlan = await buildInitExecutionPlan({
1411
1368
  target: intent.target,
@@ -1415,7 +1372,7 @@ async function runInitCommand(args) {
1415
1372
  interactive: intent.interactiveSummary && !intent.wizardEnabled,
1416
1373
  supports
1417
1374
  });
1418
- const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan, args, createDefaultInitWizardAdapter()) : basePlan;
1375
+ const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan, createDefaultInitWizardAdapter()) : basePlan;
1419
1376
  if (plan === null) {
1420
1377
  process.exitCode = 130;
1421
1378
  return;
@@ -1427,7 +1384,7 @@ async function runInitCommand(args) {
1427
1384
  return result;
1428
1385
  }
1429
1386
  function writeDefaultFabricConfig(fabricDir, targetRoot) {
1430
- const target = join2(fabricDir, "fabric-config.json");
1387
+ const target = join3(fabricDir, "fabric-config.json");
1431
1388
  if (existsSync3(target)) return;
1432
1389
  const detectedLanguage = detectExistingLanguage(targetRoot);
1433
1390
  const FABRIC_CONFIG_DEFAULTS = {
@@ -1496,39 +1453,23 @@ function writeDefaultFabricConfig(fabricDir, targetRoot) {
1496
1453
  );
1497
1454
  }
1498
1455
  function resolveInitCliIntent(args, targetInput) {
1499
- const target = normalizeTarget2(targetInput);
1500
- const mcpInstallMode = resolveMcpInstallMode(args["mcp-install"]);
1501
- const claudeMcpScope = resolveClaudeMcpScope(args.scope);
1456
+ const target = normalizeTarget3(targetInput);
1457
+ const mcpInstallMode = "global";
1458
+ const claudeMcpScope = "project";
1502
1459
  const terminalInteractive = isInteractiveInit();
1503
- const planOnly = args.plan === true;
1504
- const reapply = args.reapply === true;
1460
+ const planOnly = args["dry-run"] === true;
1505
1461
  const options = {
1506
- force: reapply ? true : args.force,
1507
- skipBootstrap: args.bootstrap === false ? true : args.skipBootstrap,
1508
- skipMcp: args.mcp === false ? true : args.skipMcp,
1509
- skipHooks: args.hooks === false ? true : args.skipHooks,
1510
- planOnly,
1511
- reapply
1462
+ planOnly
1512
1463
  };
1513
1464
  return {
1514
1465
  target,
1515
1466
  options,
1516
1467
  mcpInstallMode,
1517
1468
  claudeMcpScope,
1518
- interactiveSummary: args.interactive !== false && terminalInteractive,
1469
+ interactiveSummary: terminalInteractive,
1519
1470
  wizardEnabled: shouldUseInitWizard(args, terminalInteractive) && !planOnly
1520
1471
  };
1521
1472
  }
1522
- function resolveClaudeMcpScope(raw) {
1523
- if (raw === void 0 || raw === "project") {
1524
- return "project";
1525
- }
1526
- if (raw === "user") {
1527
- return "user";
1528
- }
1529
- writeStderr2(t("cli.install.mcp.scope.invalid", { value: raw }));
1530
- return "project";
1531
- }
1532
1473
  async function buildInitExecutionPlan(input) {
1533
1474
  const options = input.options ?? {};
1534
1475
  const scaffold = await buildInitFabricPlan(input.target, options);
@@ -1565,17 +1506,17 @@ async function buildInitExecutionPlan(input) {
1565
1506
  };
1566
1507
  }
1567
1508
  async function executeInitExecutionPlan(plan) {
1568
- if (plan.options.force) {
1569
- writeStderr2(t("cli.install.force.warning", { path: plan.target }));
1570
- }
1571
- if (plan.options.reapply && !plan.options.planOnly && !plan.interactive) {
1572
- writeStderr2(formatInitModeBanner(plan.options));
1573
- }
1574
1509
  if (plan.interactive) {
1575
1510
  printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
1576
1511
  }
1512
+ const scaffoldStates = [
1513
+ { path: plan.scaffold.metaPath, state: plan.scaffold.metaState },
1514
+ { path: plan.scaffold.eventsPath, state: plan.scaffold.eventsState },
1515
+ { path: plan.scaffold.forensicPath, state: plan.scaffold.forensicState }
1516
+ ];
1577
1517
  if (plan.options.planOnly) {
1578
1518
  printInitPlanPreview(plan);
1519
+ printInitDiffStateTable(scaffoldStates);
1579
1520
  return {
1580
1521
  plan,
1581
1522
  created: buildPlanOnlyScaffoldResult(plan.scaffold),
@@ -1583,6 +1524,17 @@ async function executeInitExecutionPlan(plan) {
1583
1524
  finalSupports: plan.supports
1584
1525
  };
1585
1526
  }
1527
+ if (existsSync3(plan.scaffold.fabricDir) && !statSync3(plan.scaffold.fabricDir).isDirectory()) {
1528
+ throw new Error(
1529
+ t("cli.install.diff.drift-abort", { path: plan.scaffold.fabricDir })
1530
+ );
1531
+ }
1532
+ const drifted = scaffoldStates.find(
1533
+ (entry) => entry.state === "drifted" || entry.state === "user-modified"
1534
+ );
1535
+ if (drifted !== void 0) {
1536
+ throw new Error(t("cli.install.diff.drift-abort", { path: drifted.path }));
1537
+ }
1586
1538
  let created = null;
1587
1539
  const stageResults = [];
1588
1540
  let finalSupports = plan.supports;
@@ -1607,6 +1559,11 @@ async function executeInitExecutionPlan(plan) {
1607
1559
  exhaustiveInitExecutionStep(step);
1608
1560
  }
1609
1561
  }
1562
+ if (scaffoldStates.every((entry) => entry.state === "present-canonical")) {
1563
+ console.log(
1564
+ t("cli.install.diff.canonical", { count: String(scaffoldStates.length) })
1565
+ );
1566
+ }
1610
1567
  return {
1611
1568
  plan,
1612
1569
  created: created ?? unreachableInitScaffold(),
@@ -1619,20 +1576,23 @@ function resolvePersonalFabricRoot() {
1619
1576
  return process.env.FABRIC_HOME ?? homedir();
1620
1577
  }
1621
1578
  async function buildInitFabricPlan(target, options) {
1622
- assertExistingDirectory2(target);
1623
- const fabricDir = join2(target, ".fabric");
1624
- const agentsMdPath = join2(target, "AGENTS.md");
1579
+ assertExistingDirectory3(target);
1580
+ const fabricDir = join3(target, ".fabric");
1581
+ const agentsMdPath = join3(target, "AGENTS.md");
1625
1582
  const agentsMdAction = existsSync3(agentsMdPath) ? "preserved" : "created";
1626
- const knowledgeDir = join2(fabricDir, "knowledge");
1627
- const personalKnowledgeDir = join2(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1628
- const forensicPath = join2(fabricDir, "forensic.json");
1629
- const eventsPath = join2(fabricDir, "events.jsonl");
1630
- const metaPath = join2(fabricDir, "agents.meta.json");
1583
+ const knowledgeDir = join3(fabricDir, "knowledge");
1584
+ const personalKnowledgeDir = join3(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1585
+ const forensicPath = join3(fabricDir, "forensic.json");
1586
+ const eventsPath = join3(fabricDir, "events.jsonl");
1587
+ const metaPath = join3(fabricDir, "agents.meta.json");
1631
1588
  const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
1632
1589
  const knowledgeDirAction = existsSync3(knowledgeDir) ? "overwritten" : "created";
1633
- const metaAction = planFreshPath(metaPath, options);
1634
- const eventsAction = planFreshPath(eventsPath, options);
1635
- const forensicAction = planFreshPath(forensicPath, options);
1590
+ const metaClassification = classifyFreshPath(metaPath, "structural");
1591
+ const eventsClassification = classifyFreshPath(eventsPath, "presence");
1592
+ const forensicClassification = classifyFreshPath(forensicPath, "always-rewrite");
1593
+ const metaAction = diffStateToWriteAction(metaClassification.state);
1594
+ const eventsAction = diffStateToWriteAction(eventsClassification.state);
1595
+ const forensicAction = diffStateToWriteAction(forensicClassification.state);
1636
1596
  const forensicReport = await buildForensicReport(target);
1637
1597
  const meta = createInitialMeta();
1638
1598
  return {
@@ -1652,24 +1612,23 @@ async function buildInitFabricPlan(target, options) {
1652
1612
  eventsAction,
1653
1613
  forensicPath,
1654
1614
  forensicAction,
1655
- forensicReport
1615
+ forensicReport,
1616
+ metaState: metaClassification.state,
1617
+ eventsState: eventsClassification.state,
1618
+ forensicState: forensicClassification.state
1656
1619
  };
1657
1620
  }
1658
1621
  async function executeInitFabricPlan(plan) {
1659
- const isReapply = plan.options?.reapply === true;
1660
1622
  if (plan.replaceFabricDir) {
1661
1623
  rmSync(plan.fabricDir, { force: true });
1662
1624
  }
1663
1625
  mkdirSync(plan.fabricDir, { recursive: true });
1664
1626
  writeDefaultFabricConfig(plan.fabricDir, plan.target);
1665
- if (plan.agentsMdAction === "created" && !existsSync3(plan.agentsMdPath)) {
1666
- await atomicWriteText(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
1667
- }
1668
1627
  mkdirSync(plan.knowledgeDir, { recursive: true });
1669
1628
  for (const sub of KNOWLEDGE_SUBDIRS) {
1670
- const teamSubDir = join2(plan.knowledgeDir, sub);
1629
+ const teamSubDir = join3(plan.knowledgeDir, sub);
1671
1630
  mkdirSync(teamSubDir, { recursive: true });
1672
- const teamGitkeep = join2(teamSubDir, ".gitkeep");
1631
+ const teamGitkeep = join3(teamSubDir, ".gitkeep");
1673
1632
  if (!existsSync3(teamGitkeep)) {
1674
1633
  writeFileSync(teamGitkeep, "", "utf8");
1675
1634
  }
@@ -1677,36 +1636,49 @@ async function executeInitFabricPlan(plan) {
1677
1636
  try {
1678
1637
  mkdirSync(plan.personalKnowledgeDir, { recursive: true });
1679
1638
  for (const sub of KNOWLEDGE_SUBDIRS) {
1680
- mkdirSync(join2(plan.personalKnowledgeDir, sub), { recursive: true });
1639
+ mkdirSync(join3(plan.personalKnowledgeDir, sub), { recursive: true });
1681
1640
  }
1682
1641
  } catch {
1683
1642
  }
1684
- preparePlannedPath(plan.metaPath, plan.metaAction);
1685
- await atomicWriteJson(plan.metaPath, plan.meta);
1686
- if (isReapply) {
1687
- if (!existsSync3(plan.eventsPath)) {
1688
- mkdirSync(dirname(plan.eventsPath), { recursive: true });
1689
- writeFileSync(plan.eventsPath, "", "utf8");
1690
- }
1691
- } else {
1643
+ if (plan.metaState === "missing") {
1644
+ preparePlannedPath(plan.metaPath, plan.metaAction);
1645
+ await atomicWriteJson(plan.metaPath, plan.meta);
1646
+ }
1647
+ if (plan.eventsState === "missing") {
1692
1648
  preparePlannedPath(plan.eventsPath, plan.eventsAction);
1649
+ mkdirSync(dirname(plan.eventsPath), { recursive: true });
1693
1650
  writeFileSync(plan.eventsPath, "", "utf8");
1694
1651
  }
1695
1652
  preparePlannedPath(plan.forensicPath, plan.forensicAction);
1696
1653
  await atomicWriteJson(plan.forensicPath, plan.forensicReport);
1697
- if (!plan.options?.reapply) {
1654
+ const wasCanonicalReRun = plan.metaState === "present-canonical" && plan.eventsState === "present-canonical";
1655
+ if (!wasCanonicalReRun) {
1698
1656
  try {
1699
1657
  await runInitScan(plan.target, { source: "init" });
1700
1658
  } catch (error) {
1701
- writeStderr2(
1659
+ writeStderr(
1702
1660
  `[warn] init-scan failed: ${error instanceof Error ? error.message : String(error)} \u2014 re-run \`fab scan\` to populate baseline knowledge entries.`
1703
1661
  );
1704
1662
  }
1705
1663
  }
1706
- if (isReapply) {
1707
- appendReapplyLedgerEvent(plan.eventsPath, {
1708
- preserved_ledger: true
1709
- });
1664
+ if (existsSync3(plan.eventsPath)) {
1665
+ const applied = [];
1666
+ const canonical = [];
1667
+ const drifted = [];
1668
+ for (const entry of [
1669
+ { path: plan.metaPath, state: plan.metaState },
1670
+ { path: plan.eventsPath, state: plan.eventsState },
1671
+ { path: plan.forensicPath, state: plan.forensicState }
1672
+ ]) {
1673
+ if (entry.state === "missing") {
1674
+ applied.push(entry.path);
1675
+ } else if (entry.state === "present-canonical") {
1676
+ canonical.push(entry.path);
1677
+ } else {
1678
+ drifted.push(entry.path);
1679
+ }
1680
+ }
1681
+ appendInstallDiffLedgerEvent(plan.eventsPath, { applied, canonical, drifted });
1710
1682
  }
1711
1683
  return {
1712
1684
  agentsMdPath: plan.agentsMdPath,
@@ -1726,16 +1698,16 @@ async function initFabric(target, options) {
1726
1698
  return await executeInitFabricPlan(await buildInitFabricPlan(target, options));
1727
1699
  }
1728
1700
  function shouldUseInitWizard(args, terminalInteractive = isInteractiveInit()) {
1729
- return terminalInteractive && args.interactive !== false && args.yes !== true;
1701
+ return terminalInteractive && args.yes !== true;
1730
1702
  }
1731
- async function resolveInitExecutionPlanWithWizard(basePlan, args, wizardAdapter) {
1703
+ async function resolveInitExecutionPlanWithWizard(basePlan, wizardAdapter) {
1732
1704
  const selection = await wizardAdapter.run({
1733
1705
  target: basePlan.target,
1734
1706
  options: basePlan.options,
1735
1707
  supports: basePlan.supports,
1736
1708
  mcpInstallMode: basePlan.mcpInstallMode,
1737
1709
  claudeMcpScope: basePlan.claudeMcpScope,
1738
- lockedStages: collectLockedWizardStages(args)
1710
+ lockedStages: []
1739
1711
  });
1740
1712
  if (selection === null) {
1741
1713
  return null;
@@ -1792,12 +1764,17 @@ function printInitPostSetup(plan, stageResults, finalSupports) {
1792
1764
  paint.muted(t("cli.install.language_preference_hint", { value: fabricLanguage }))
1793
1765
  );
1794
1766
  }
1767
+ function printInitDiffStateTable(entries) {
1768
+ for (const entry of entries) {
1769
+ console.log(` ${formatDiffFileState(entry.state)} ${entry.path}`);
1770
+ }
1771
+ }
1795
1772
  function printInitPlanPreview(plan) {
1796
1773
  console.log(t("cli.install.plan.preview-title"));
1797
1774
  printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
1798
1775
  console.log(
1799
1776
  t("cli.install.plan.preview-result", {
1800
- mode: plan.options.reapply ? t("cli.install.mode.reapply") : t("cli.install.mode.default"),
1777
+ mode: t("cli.install.mode.default"),
1801
1778
  bootstrap: yesNoLabel(!plan.options.skipBootstrap),
1802
1779
  mcp: yesNoLabel(!plan.options.skipMcp),
1803
1780
  hooks: yesNoLabel(!plan.options.skipHooks)
@@ -1838,17 +1815,20 @@ async function executeInitStagePlan(plan, stageName) {
1838
1815
  installResults.push(...await runBestEffort("hook-script", () => installArchiveHintHook(plan.target)));
1839
1816
  installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
1840
1817
  installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
1818
+ installResults.push(...await runBestEffort("hook-lib", () => installHookLibs(plan.target)));
1841
1819
  installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
1842
1820
  installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
1843
1821
  installResults.push(await runBestEffortSingle("cursor-hook-config", () => mergeCursorHookConfig(plan.target)));
1844
- const fabricLanguage = readFabricLanguagePreference(plan.target);
1845
- installResults.push(...await runBestEffort("section", () => addFabricKnowledgeBaseSection(plan.target, fabricLanguage)));
1822
+ installResults.push(await runBestEffortSingle("bootstrap-snapshot", () => writeFabricAgentsSnapshot(plan.target)));
1823
+ installResults.push(await runBestEffortSingle("bootstrap-claude", () => writeClaudeBootstrapThinShell(plan.target)));
1824
+ installResults.push(await runBestEffortSingle("bootstrap-codex", () => writeCodexBootstrapManagedBlock(plan.target)));
1825
+ installResults.push(await runBestEffortSingle("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(plan.target)));
1846
1826
  const installedCount = installResults.filter((r) => r.status === "written").length;
1847
1827
  const skippedCount = installResults.filter((r) => r.status === "skipped").length;
1848
1828
  const errorCount = installResults.filter((r) => r.status === "error").length;
1849
1829
  for (const result of installResults) {
1850
1830
  if (result.status === "error") {
1851
- writeStderr2(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
1831
+ writeStderr(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
1852
1832
  }
1853
1833
  }
1854
1834
  const note2 = errorCount > 0 ? `errors=${errorCount}` : void 0;
@@ -1858,15 +1838,14 @@ async function executeInitStagePlan(plan, stageName) {
1858
1838
  case "mcp": {
1859
1839
  if (stage.installMode === "local") {
1860
1840
  const manager = stage.packageManager ?? detectPackageManager(plan.target);
1861
- writeStderr2(t("cli.install.mcp.install.local"));
1862
- writeStderr2(t("cli.install.mcp.local.installing", { manager }));
1841
+ writeStderr(t("cli.install.mcp.install.local"));
1842
+ writeStderr(t("cli.install.mcp.local.installing", { manager }));
1863
1843
  installLocalFabricServer(plan.target, manager);
1864
- writeStderr2(t("cli.install.mcp.local.installed"));
1844
+ writeStderr(t("cli.install.mcp.local.installed"));
1865
1845
  } else {
1866
- writeStderr2(t("cli.install.mcp.install.global"));
1846
+ writeStderr(t("cli.install.mcp.install.global"));
1867
1847
  }
1868
1848
  const result = await installMcpClients(plan.target, {
1869
- force: plan.options.force,
1870
1849
  localServerPath: stage.localServerPath,
1871
1850
  claudeMcpScope: stage.claudeMcpScope
1872
1851
  });
@@ -1878,7 +1857,7 @@ async function executeInitStagePlan(plan, stageName) {
1878
1857
  return { name: "mcp", disposition: "ran" };
1879
1858
  }
1880
1859
  case "hooks": {
1881
- const result = await installHooks(plan.target, { force: plan.options.force });
1860
+ const result = await installHooks(plan.target);
1882
1861
  console.log(formatInitStageResult("hooks", "completed", result.installed.length, result.skipped.length));
1883
1862
  return { name: "hooks", disposition: "ran" };
1884
1863
  }
@@ -1886,30 +1865,66 @@ async function executeInitStagePlan(plan, stageName) {
1886
1865
  return exhaustiveInitStagePlan(stage);
1887
1866
  }
1888
1867
  } catch (error) {
1889
- writeStderr2(formatInitStageFailure(stageName, error));
1868
+ writeStderr(formatInitStageFailure(stageName, error));
1890
1869
  return { name: stageName, disposition: "failed" };
1891
1870
  }
1892
1871
  }
1893
- function shouldReplaceWritableDirectory(path, options) {
1872
+ function shouldReplaceWritableDirectory(path, _options) {
1894
1873
  if (!existsSync3(path)) {
1895
1874
  return false;
1896
1875
  }
1897
- if (statSync2(path).isDirectory()) {
1876
+ if (statSync3(path).isDirectory()) {
1898
1877
  return false;
1899
1878
  }
1900
- if (!options?.force) {
1901
- throw new Error(t("cli.install.errors.abort-existing", { path }));
1902
- }
1903
- return true;
1879
+ return false;
1904
1880
  }
1905
- function planFreshPath(path, options) {
1881
+ function classifyFreshPath(path, strategy) {
1906
1882
  if (!existsSync3(path)) {
1907
- return "created";
1883
+ return { path, state: "missing" };
1908
1884
  }
1909
- if (!options?.force) {
1910
- throw new Error(t("cli.install.errors.abort-existing", { path }));
1885
+ let stat;
1886
+ try {
1887
+ stat = statSync3(path);
1888
+ } catch (error) {
1889
+ return {
1890
+ path,
1891
+ state: "user-modified",
1892
+ reason: error instanceof Error ? error.message : String(error)
1893
+ };
1894
+ }
1895
+ if (!stat.isFile()) {
1896
+ return { path, state: "user-modified", reason: "expected a file" };
1897
+ }
1898
+ if (strategy === "presence" || strategy === "always-rewrite") {
1899
+ return { path, state: "present-canonical" };
1900
+ }
1901
+ try {
1902
+ const raw = readFileSync2(path, "utf8");
1903
+ const parsed = JSON.parse(raw);
1904
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1905
+ return { path, state: "user-modified", reason: "not a JSON object" };
1906
+ }
1907
+ const record = parsed;
1908
+ const hasRevision = typeof record["revision"] === "string";
1909
+ const hasNodes = record["nodes"] !== void 0 && record["nodes"] !== null && typeof record["nodes"] === "object" && !Array.isArray(record["nodes"]);
1910
+ const hasCounters = record["counters"] !== void 0 && record["counters"] !== null && typeof record["counters"] === "object" && !Array.isArray(record["counters"]);
1911
+ if (!hasRevision || !hasNodes || !hasCounters) {
1912
+ return { path, state: "drifted", reason: "missing required AgentsMeta fields" };
1913
+ }
1914
+ return { path, state: "present-canonical" };
1915
+ } catch (error) {
1916
+ return {
1917
+ path,
1918
+ state: "user-modified",
1919
+ reason: error instanceof Error ? error.message : String(error)
1920
+ };
1911
1921
  }
1912
- return "overwritten";
1922
+ }
1923
+ function diffStateToWriteAction(_state) {
1924
+ return "created";
1925
+ }
1926
+ function formatDiffFileState(state) {
1927
+ return t(`cli.install.diff.state.${state}`);
1913
1928
  }
1914
1929
  function preparePlannedPath(path, action) {
1915
1930
  mkdirSync(dirname(path), { recursive: true });
@@ -2048,74 +2063,42 @@ async function selectClaudeMcpScopeInGroup(options) {
2048
2063
  }
2049
2064
  return result;
2050
2065
  }
2051
- function collectLockedWizardStages(args) {
2052
- const lockedStages = [];
2053
- if (args.bootstrap === false) {
2054
- lockedStages.push("bootstrap");
2055
- }
2056
- if (args.mcp === false) {
2057
- lockedStages.push("mcp");
2058
- }
2059
- if (args.hooks === false) {
2060
- lockedStages.push("hooks");
2061
- }
2062
- return lockedStages;
2063
- }
2064
2066
  function formatPromptDefault(value) {
2065
2067
  return value ? "Y/n" : "y/N";
2066
2068
  }
2067
2069
  function formatInitModeBanner(options) {
2068
- if (options.planOnly && options.reapply) {
2069
- return t("cli.install.plan.mode-banner.plan-reapply");
2070
- }
2071
2070
  if (options.planOnly) {
2072
2071
  return t("cli.install.plan.mode-banner.plan");
2073
2072
  }
2074
- if (options.reapply) {
2075
- return t("cli.install.plan.mode-banner.reapply");
2076
- }
2077
2073
  return t("cli.install.plan.mode-banner.default");
2078
2074
  }
2079
2075
  function formatInitModeBadge(options) {
2080
- if (options.planOnly && options.reapply) {
2081
- return t("cli.install.mode.badge.plan-reapply");
2082
- }
2083
2076
  if (options.planOnly) {
2084
2077
  return t("cli.install.mode.badge.plan");
2085
2078
  }
2086
- if (options.reapply) {
2087
- return t("cli.install.mode.badge.reapply");
2088
- }
2089
2079
  return t("cli.install.mode.badge.default");
2090
2080
  }
2091
- function normalizeTarget2(targetInput) {
2092
- return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2081
+ function normalizeTarget3(targetInput) {
2082
+ return isAbsolute3(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2093
2083
  }
2094
- function assertExistingDirectory2(target) {
2095
- if (!existsSync3(target) || !statSync2(target).isDirectory()) {
2084
+ function assertExistingDirectory3(target) {
2085
+ if (!existsSync3(target) || !statSync3(target).isDirectory()) {
2096
2086
  throw new Error(`Target must be an existing directory: ${target}`);
2097
2087
  }
2098
2088
  }
2099
2089
  function detectPackageManager(cwd) {
2100
2090
  const workspaceRoot = resolve3(cwd);
2101
- if (existsSync3(join2(workspaceRoot, "pnpm-lock.yaml"))) {
2091
+ if (existsSync3(join3(workspaceRoot, "pnpm-lock.yaml"))) {
2102
2092
  return "pnpm";
2103
2093
  }
2104
- if (existsSync3(join2(workspaceRoot, "yarn.lock"))) {
2094
+ if (existsSync3(join3(workspaceRoot, "yarn.lock"))) {
2105
2095
  return "yarn";
2106
2096
  }
2107
- if (existsSync3(join2(workspaceRoot, "package-lock.json"))) {
2097
+ if (existsSync3(join3(workspaceRoot, "package-lock.json"))) {
2108
2098
  return "npm";
2109
2099
  }
2110
2100
  return "npm";
2111
2101
  }
2112
- function resolveMcpInstallMode(rawMode) {
2113
- if (rawMode === void 0 || rawMode === "global" || rawMode === "local") {
2114
- return rawMode ?? "global";
2115
- }
2116
- writeStderr2(t("cli.install.mcp.install.invalid", { value: rawMode }));
2117
- return "global";
2118
- }
2119
2102
  function installLocalFabricServer(target, manager) {
2120
2103
  const installArgs = manager === "npm" ? ["install", "-D", FABRIC_SERVER_PACKAGE] : ["add", "-D", FABRIC_SERVER_PACKAGE];
2121
2104
  childProcess.execFileSync(manager, installArgs, {
@@ -2131,14 +2114,16 @@ function createInitialMeta() {
2131
2114
  counters: defaultAgentsMetaCounters()
2132
2115
  };
2133
2116
  }
2134
- function appendReapplyLedgerEvent(eventsPath, payload) {
2117
+ function appendInstallDiffLedgerEvent(eventsPath, payload) {
2135
2118
  const event = {
2136
2119
  kind: "fabric-event",
2137
2120
  id: `event:${randomUUID()}`,
2138
2121
  ts: Date.now(),
2139
2122
  schema_version: 1,
2140
- event_type: "reapply_completed",
2141
- preserved_ledger: payload.preserved_ledger
2123
+ event_type: "install_diff_applied",
2124
+ applied: payload.applied,
2125
+ canonical: payload.canonical,
2126
+ drifted: payload.drifted
2142
2127
  };
2143
2128
  const line = `${JSON.stringify(event)}
2144
2129
  `;
@@ -2349,7 +2334,7 @@ function reasonLabel() {
2349
2334
  return paint.human(t("cli.shared.reason"));
2350
2335
  }
2351
2336
  function overwrittenLabel() {
2352
- return paint.warn(t("cli.install.force.overwritten"));
2337
+ return paint.warn(t("cli.install.label.overwritten"));
2353
2338
  }
2354
2339
  function completedStageLabel() {
2355
2340
  return paint.success(t("cli.install.stages.completed"));
@@ -2360,7 +2345,7 @@ function skippedStageLabel() {
2360
2345
  function failedStageLabel() {
2361
2346
  return paint.error(t("cli.install.stages.failed"));
2362
2347
  }
2363
- function writeStderr2(message) {
2348
+ function writeStderr(message) {
2364
2349
  process.stderr.write(`${message}
2365
2350
  `);
2366
2351
  }