@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0-rc.10

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 (32) hide show
  1. package/README.md +6 -6
  2. package/dist/chunk-6ICJICVU.js +10 -0
  3. package/dist/chunk-AW3G7ZH5.js +576 -0
  4. package/dist/chunk-HQLEHH4O.js +321 -0
  5. package/dist/{chunk-UHNP7T7W.js → chunk-MT3R57VG.js} +346 -86
  6. package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
  7. package/dist/chunk-WPTA74BY.js +184 -0
  8. package/dist/chunk-WWNXR34K.js +49 -0
  9. package/dist/doctor-RILCO5OG.js +282 -0
  10. package/dist/hooks-NX32PPEN.js +13 -0
  11. package/dist/index.js +8 -5
  12. package/dist/{init-DRHUYHYA.js → init-SAVH4SKE.js} +188 -491
  13. package/dist/plan-context-hint-QMUPAXIB.js +98 -0
  14. package/dist/{scan-HU2EGITF.js → scan-ELSNCSKS.js} +4 -2
  15. package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
  16. package/dist/uninstall-DBAR2JBS.js +1082 -0
  17. package/package.json +3 -3
  18. package/templates/bootstrap/CLAUDE.md +1 -1
  19. package/templates/bootstrap/codex-AGENTS-header.md +1 -1
  20. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
  21. package/templates/hooks/configs/README.md +73 -0
  22. package/templates/hooks/configs/claude-code.json +37 -0
  23. package/templates/hooks/configs/codex-hooks.json +20 -0
  24. package/templates/hooks/configs/cursor-hooks.json +20 -0
  25. package/templates/hooks/fabric-hint.cjs +1337 -0
  26. package/templates/hooks/knowledge-hint-broad.cjs +612 -0
  27. package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
  28. package/templates/hooks/lib/session-digest-writer.cjs +172 -0
  29. package/templates/skills/fabric-archive/SKILL.md +486 -0
  30. package/templates/skills/fabric-import/SKILL.md +560 -0
  31. package/templates/skills/fabric-review/SKILL.md +382 -0
  32. package/dist/doctor-DUHWLAYD.js +0 -98
@@ -1,447 +1,59 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ hooksCommand,
4
+ installHooks
5
+ } from "./chunk-WPTA74BY.js";
2
6
  import {
3
7
  detectFramework,
4
8
  runInitScan
5
- } from "./chunk-UHNP7T7W.js";
9
+ } from "./chunk-MT3R57VG.js";
10
+ import {
11
+ detectClientSupports,
12
+ resolveClients
13
+ } from "./chunk-HQLEHH4O.js";
14
+ import {
15
+ addArchiveSkillPointer,
16
+ installArchiveHintHook,
17
+ installFabricArchiveSkill,
18
+ installFabricImportSkill,
19
+ installFabricReviewSkill,
20
+ installKnowledgeHintBroadHook,
21
+ installKnowledgeHintNarrowHook,
22
+ mergeClaudeCodeHookConfig,
23
+ mergeCodexHookConfig,
24
+ mergeCursorHookConfig
25
+ } from "./chunk-AW3G7ZH5.js";
6
26
  import {
7
- createDebugLogger,
8
27
  displayWidth,
9
28
  padEnd,
10
- paint,
11
- resolveDevMode,
29
+ paint
30
+ } from "./chunk-WWNXR34K.js";
31
+ import {
32
+ createDebugLogger,
33
+ resolveDevMode
34
+ } from "./chunk-OBQU6NHO.js";
35
+ import {
12
36
  t
13
- } from "./chunk-5LOYBXWD.js";
37
+ } from "./chunk-6ICJICVU.js";
14
38
 
15
39
  // src/commands/init.ts
16
40
  import { randomUUID } from "crypto";
17
- import { homedir as homedir5 } from "os";
41
+ import { homedir } from "os";
18
42
  import * as childProcess from "child_process";
19
- import { appendFileSync, existsSync as existsSync8, mkdirSync, rmSync, statSync as statSync3, writeFileSync } from "fs";
20
- import { dirname as dirname3, isAbsolute as isAbsolute3, join as join6, resolve as resolve7 } from "path";
43
+ import { appendFileSync, existsSync as existsSync3, mkdirSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
44
+ import { dirname, isAbsolute as isAbsolute2, join as join2, resolve as resolve3 } from "path";
21
45
  import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
22
46
  import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
23
- import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
24
- import { defineCommand as defineCommand3 } from "citty";
47
+ import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
48
+ import { defineCommand as defineCommand2 } from "citty";
25
49
  import { checkLockOrThrow } from "@fenglimg/fabric-server";
26
50
 
27
51
  // src/commands/config.ts
28
- import { existsSync as existsSync6 } from "fs";
29
- import { readFile as readFile3 } from "fs/promises";
30
- import { resolve as resolve5 } from "path";
31
- import { fileURLToPath } from "url";
32
- import { defineCommand as defineCommand2 } from "citty";
33
-
34
- // src/config/resolver.ts
35
- import { existsSync as existsSync4 } from "fs";
36
- import { join as join4 } from "path";
37
- import { homedir as homedir4 } from "os";
38
-
39
- // src/config/claude-code.ts
40
- import { existsSync as existsSync2 } from "fs";
41
- import { join as join2, resolve as resolve2 } from "path";
42
- import { homedir as homedir2, platform } from "os";
43
-
44
- // src/config/json.ts
45
52
  import { existsSync } from "fs";
46
- import { mkdir, readFile } from "fs/promises";
47
- import { dirname, join, resolve } from "path";
48
- import { homedir } from "os";
49
- import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
50
-
51
- // src/config/writer.ts
52
- function createServerEntry(serverPath) {
53
- return {
54
- command: process.execPath,
55
- args: [serverPath]
56
- };
57
- }
58
-
59
- // src/config/json.ts
60
- function deepMerge(target, source) {
61
- if (target === null || typeof target !== "object" || Array.isArray(target) || source === null || typeof source !== "object" || Array.isArray(source)) {
62
- return source;
63
- }
64
- const out = { ...target };
65
- for (const key of Object.keys(source)) {
66
- out[key] = deepMerge(
67
- target[key],
68
- source[key]
69
- );
70
- }
71
- return out;
72
- }
73
- function expandHome(filePath) {
74
- if (filePath === "~") {
75
- return homedir();
76
- }
77
- if (filePath.startsWith("~/")) {
78
- return join(homedir(), filePath.slice(2));
79
- }
80
- return filePath;
81
- }
82
- function normalizeConfigPath(filePath) {
83
- return resolve(expandHome(filePath));
84
- }
85
- async function readJsonConfig(configPath) {
86
- try {
87
- const raw = await readFile(configPath, "utf8");
88
- if (raw.trim().length === 0) {
89
- return {};
90
- }
91
- const parsed = JSON.parse(raw);
92
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
93
- throw new Error(`Expected JSON object in ${configPath}`);
94
- }
95
- return parsed;
96
- } catch (error) {
97
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
98
- return {};
99
- }
100
- throw error;
101
- }
102
- }
103
- async function writeJsonClientConfig(configPath, serverEntry) {
104
- const existing = await readJsonConfig(configPath);
105
- const merged = deepMerge(existing, { mcpServers: { fabric: serverEntry } });
106
- await mkdir(dirname(configPath), { recursive: true });
107
- await atomicWriteJson(configPath, merged, { indent: 2 });
108
- }
109
- var JsonClientConfigWriter = class {
110
- configuredPath;
111
- constructor(configuredPath) {
112
- this.configuredPath = configuredPath;
113
- }
114
- async detect(workspaceRoot, overridePath) {
115
- const explicitPath = overridePath ?? this.configuredPath;
116
- if (explicitPath !== void 0) {
117
- return normalizeConfigPath(explicitPath);
118
- }
119
- const configPath = this.defaultPath(workspaceRoot);
120
- return configPath === null ? null : normalizeConfigPath(configPath);
121
- }
122
- async write(serverPath, workspaceRoot, overridePath) {
123
- const configPath = await this.detect(workspaceRoot, overridePath);
124
- if (configPath === null) {
125
- return;
126
- }
127
- await writeJsonClientConfig(configPath, createServerEntry(serverPath));
128
- }
129
- };
130
- var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
131
- clientKind = "ClaudeCodeCLI";
132
- scope;
133
- constructor(configuredPath, scope = "project") {
134
- super(configuredPath);
135
- this.scope = scope;
136
- }
137
- // Writes to project-level .mcp.json (per Claude Code MCP spec) by default,
138
- // or ~/.claude.json for user scope.
139
- // Detection still checks ~/.claude to confirm Claude Code is installed.
140
- defaultPath(workspaceRoot) {
141
- const globalClaudeDir = join(homedir(), ".claude");
142
- const projectClaudeDir = join(workspaceRoot, ".claude");
143
- if (!existsSync(globalClaudeDir) && !existsSync(projectClaudeDir)) {
144
- return null;
145
- }
146
- return this.scope === "user" ? join(homedir(), ".claude.json") : join(workspaceRoot, ".mcp.json");
147
- }
148
- };
149
- var CursorWriter = class extends JsonClientConfigWriter {
150
- clientKind = "Cursor";
151
- constructor(configuredPath) {
152
- super(configuredPath);
153
- }
154
- defaultPath(workspaceRoot) {
155
- const cursorDir = join(workspaceRoot, ".cursor");
156
- return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
157
- }
158
- };
159
-
160
- // src/config/claude-code.ts
161
- function getClaudeDesktopConfigPath() {
162
- const os = platform();
163
- if (os === "darwin") {
164
- return join2(homedir2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
165
- }
166
- if (os === "win32") {
167
- return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
168
- }
169
- return join2(homedir2(), ".config", "Claude", "claude_desktop_config.json");
170
- }
171
- var ClaudeCodeDesktopWriter = class {
172
- clientKind = "ClaudeCodeDesktop";
173
- configuredPath;
174
- constructor(configuredPath) {
175
- this.configuredPath = configuredPath;
176
- }
177
- async detect(_workspaceRoot, overridePath) {
178
- const configPath = normalizeConfigPath(overridePath ?? this.configuredPath ?? getClaudeDesktopConfigPath());
179
- return existsSync2(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
180
- }
181
- async write(serverPath, workspaceRoot, overridePath) {
182
- const configPath = await this.detect(workspaceRoot, overridePath);
183
- if (configPath === null) {
184
- return;
185
- }
186
- await writeJsonClientConfig(configPath, {
187
- command: process.execPath,
188
- args: [serverPath]
189
- });
190
- }
191
- };
192
-
193
- // src/config/toml.ts
194
- import { existsSync as existsSync3 } from "fs";
195
- import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
196
- import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
197
- import { homedir as homedir3 } from "os";
198
- import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
199
- function expandHome2(filePath) {
200
- if (filePath === "~") {
201
- return homedir3();
202
- }
203
- if (filePath.startsWith("~/")) {
204
- return join3(homedir3(), filePath.slice(2));
205
- }
206
- return filePath;
207
- }
208
- function escapeTomlString(value) {
209
- return JSON.stringify(value);
210
- }
211
- function serializeTomlStringArray(values) {
212
- return `[${values.map((value) => escapeTomlString(value)).join(", ")}]`;
213
- }
214
- function serializeTomlInlineTable(values) {
215
- const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
216
- return `{ ${entries.join(", ")} }`;
217
- }
218
- function serializeCodexServerBlock(serverName, serverEntry) {
219
- const lines = [
220
- `[mcp_servers.${serverName}]`,
221
- `command = ${escapeTomlString(serverEntry.command)}`,
222
- `args = ${serializeTomlStringArray(serverEntry.args)}`
223
- ];
224
- if (serverEntry.env !== void 0 && Object.keys(serverEntry.env).length > 0) {
225
- lines.push(`env = ${serializeTomlInlineTable(serverEntry.env)}`);
226
- }
227
- return `${lines.join("\n")}
228
- `;
229
- }
230
- function trimTrailingBlankLines(value) {
231
- return value.replace(/\s+$/u, "");
232
- }
233
- function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
234
- const block = serializeCodexServerBlock(serverName, serverEntry);
235
- const normalized = rawConfig.replace(/\r\n/g, "\n");
236
- const legacyPattern = new RegExp(String.raw`\n?\[mcp\.servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`, "g");
237
- const currentPattern = new RegExp(
238
- String.raw`\n?\[mcp_servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
239
- "g"
240
- );
241
- const withoutLegacy = normalized.replace(legacyPattern, "");
242
- const withoutExisting = withoutLegacy.replace(currentPattern, "");
243
- const trimmed = trimTrailingBlankLines(withoutExisting);
244
- if (trimmed.length === 0) {
245
- return block;
246
- }
247
- return `${trimmed}
248
-
249
- ${block}`;
250
- }
251
- async function readTomlConfigText(configPath) {
252
- try {
253
- return await readFile2(configPath, "utf8");
254
- } catch (error) {
255
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
256
- return "";
257
- }
258
- throw error;
259
- }
260
- }
261
- var CodexTOMLConfigWriter = class {
262
- clientKind = "CodexCLI";
263
- configuredPath;
264
- constructor(configuredPath) {
265
- this.configuredPath = configuredPath;
266
- }
267
- async detect(_workspaceRoot, overridePath) {
268
- const explicitPath = overridePath ?? this.configuredPath;
269
- if (explicitPath !== void 0) {
270
- return resolve3(expandHome2(explicitPath));
271
- }
272
- const codexDir = join3(homedir3(), ".codex");
273
- return existsSync3(codexDir) ? resolve3(join3(codexDir, "config.toml")) : null;
274
- }
275
- async write(serverPath, workspaceRoot, overridePath) {
276
- const configPath = await this.detect(workspaceRoot, overridePath);
277
- if (configPath === null) {
278
- return;
279
- }
280
- const rawConfig = await readTomlConfigText(configPath);
281
- const nextConfig = upsertCodexServerBlock(rawConfig, "fabric", createServerEntry(serverPath));
282
- await mkdir2(dirname2(configPath), { recursive: true });
283
- await atomicWriteText(configPath, nextConfig);
284
- }
285
- };
286
-
287
- // src/config/resolver.ts
288
- import { clientPathsSchema, fabricConfigSchema } from "@fenglimg/fabric-shared";
289
- function hasExplicitPath(clientPaths, key) {
290
- return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
291
- }
292
- function addIfDetected(writers, detected, createWriter, configuredPath) {
293
- if (configuredPath !== void 0 || detected) {
294
- writers.push(createWriter(configuredPath));
295
- }
296
- }
297
- function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
298
- const clientPaths = fabricConfig.clientPaths;
299
- const writers = [];
300
- const claudeMcpScope = opts.claudeMcpScope ?? "project";
301
- addIfDetected(
302
- writers,
303
- existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude")),
304
- (configuredPath) => new ClaudeCodeCLIWriter(configuredPath, claudeMcpScope),
305
- hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
306
- );
307
- addIfDetected(
308
- writers,
309
- existsSync4(getClaudeDesktopConfigPath()),
310
- (configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
311
- hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
312
- );
313
- addIfDetected(
314
- writers,
315
- existsSync4(join4(workspaceRoot, ".cursor")),
316
- (configuredPath) => new CursorWriter(configuredPath),
317
- hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
318
- );
319
- addIfDetected(
320
- writers,
321
- existsSync4(join4(homedir4(), ".codex")),
322
- (configuredPath) => new CodexTOMLConfigWriter(configuredPath),
323
- hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
324
- );
325
- return writers;
326
- }
327
- function detectClientSupports(workspaceRoot, fabricConfig = {}) {
328
- const clientPaths = fabricConfig.clientPaths;
329
- const claudeDetected = existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude"));
330
- const claudeDesktopDetected = existsSync4(getClaudeDesktopConfigPath());
331
- const cursorDetected = existsSync4(join4(workspaceRoot, ".cursor"));
332
- const codexDetected = existsSync4(join4(homedir4(), ".codex"));
333
- return [
334
- {
335
- clientKind: "ClaudeCodeCLI",
336
- label: "Claude Code CLI",
337
- detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
338
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
339
- configPath: "project .claude/settings.json",
340
- capabilities: {
341
- bootstrap: true,
342
- mcp: true,
343
- hook: true,
344
- skill: true
345
- },
346
- installedCapabilities: {
347
- hook: true,
348
- skill: true
349
- }
350
- },
351
- {
352
- clientKind: "ClaudeCodeDesktop",
353
- label: "Claude Code Desktop",
354
- detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
355
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
356
- configPath: "desktop Claude config",
357
- capabilities: {
358
- bootstrap: true,
359
- mcp: true,
360
- hook: false,
361
- skill: false
362
- }
363
- },
364
- {
365
- clientKind: "Cursor",
366
- label: "Cursor",
367
- detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
368
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
369
- configPath: ".cursor/mcp.json",
370
- capabilities: {
371
- bootstrap: true,
372
- mcp: true,
373
- hook: false,
374
- skill: false
375
- }
376
- },
377
- {
378
- clientKind: "CodexCLI",
379
- label: "Codex CLI",
380
- detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
381
- bootstrapTargetPath: ".fabric/bootstrap/README.md",
382
- configPath: "~/.codex/config.toml",
383
- capabilities: {
384
- bootstrap: true,
385
- mcp: true,
386
- hook: true,
387
- skill: true
388
- },
389
- installedCapabilities: {
390
- hook: existsSync4(join4(workspaceRoot, ".codex", "hooks.json")),
391
- // v2/rc.2: v1 client-side init skill removed; skill-installation probes
392
- // will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
393
- // fabric-review, fabric-import). Until then there is nothing to probe.
394
- skill: false
395
- }
396
- }
397
- ];
398
- }
399
-
400
- // src/commands/hooks.ts
401
- import { existsSync as existsSync5, statSync } from "fs";
402
- import { isAbsolute, resolve as resolve4 } from "path";
53
+ import { readFile } from "fs/promises";
54
+ import { resolve } from "path";
55
+ import { fileURLToPath } from "url";
403
56
  import { defineCommand } from "citty";
404
- var hooksCommand = defineCommand({
405
- meta: {
406
- name: "hooks",
407
- description: t("cli.hooks.description")
408
- },
409
- subCommands: {
410
- install: defineCommand({
411
- meta: {
412
- name: "install",
413
- description: t("cli.hooks.install.description")
414
- },
415
- args: {
416
- target: {
417
- type: "string",
418
- description: t("cli.hooks.install.args.target.description"),
419
- default: process.cwd()
420
- }
421
- },
422
- async run({ args }) {
423
- await installHooks(args.target);
424
- }
425
- })
426
- }
427
- });
428
- async function installHooks(target, _options = {}) {
429
- const normalizedTarget = normalizeTarget(target);
430
- assertExistingDirectory(normalizedTarget);
431
- throw new Error(
432
- `fab hooks install is not available in v2 (husky pre-commit template removed). Use per-client hooks under .claude/ and .codex/ instead. Target: ${normalizedTarget}`
433
- );
434
- }
435
- function normalizeTarget(targetInput) {
436
- return isAbsolute(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
437
- }
438
- function assertExistingDirectory(target) {
439
- if (!existsSync5(target) || !statSync(target).isDirectory()) {
440
- throw new Error(t("cli.shared.target-invalid", { target }));
441
- }
442
- }
443
-
444
- // src/commands/config.ts
445
57
  var CLIENT_ALIASES = {
446
58
  claude: "ClaudeCodeCLI",
447
59
  claudecodecli: "ClaudeCodeCLI",
@@ -471,11 +83,11 @@ function parseClientFilter(value) {
471
83
  return clients;
472
84
  }
473
85
  async function loadFabricConfig(workspaceRoot) {
474
- const configPath = resolve5(workspaceRoot, "fabric.config.json");
475
- if (!existsSync6(configPath)) {
86
+ const configPath = resolve(workspaceRoot, "fabric.config.json");
87
+ if (!existsSync(configPath)) {
476
88
  return {};
477
89
  }
478
- const parsed = JSON.parse(await readFile3(configPath, "utf8"));
90
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
479
91
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
480
92
  throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
481
93
  }
@@ -483,21 +95,21 @@ async function loadFabricConfig(workspaceRoot) {
483
95
  }
484
96
  function resolveServerPath(override) {
485
97
  if (override) return override;
486
- if (process.env.FAB_SERVER_PATH) return resolve5(process.env.FAB_SERVER_PATH);
98
+ if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
487
99
  return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
488
100
  }
489
101
  function writeStderr(message) {
490
102
  process.stderr.write(`${message}
491
103
  `);
492
104
  }
493
- var configCmd = defineCommand2({
105
+ var configCmd = defineCommand({
494
106
  meta: {
495
107
  name: "config",
496
108
  description: t("cli.config.description")
497
109
  },
498
110
  subCommands: {
499
111
  hooks: hooksCommand,
500
- install: defineCommand2({
112
+ install: defineCommand({
501
113
  meta: {
502
114
  name: "install",
503
115
  description: t("cli.config.install.description")
@@ -541,7 +153,7 @@ var configCmd = defineCommand2({
541
153
  }
542
154
  });
543
155
  async function installMcpClients(target, options = {}) {
544
- const workspaceRoot = resolve5(target);
156
+ const workspaceRoot = resolve(target);
545
157
  const fabricConfig = await loadFabricConfig(workspaceRoot);
546
158
  const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
547
159
  const serverPath = resolveServerPath(options.localServerPath);
@@ -572,9 +184,9 @@ async function installMcpClients(target, options = {}) {
572
184
 
573
185
  // src/scanner/forensic.ts
574
186
  import { execFileSync } from "child_process";
575
- import { existsSync as existsSync7, readdirSync, readFileSync, statSync as statSync2 } from "fs";
187
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
576
188
  import { createRequire } from "module";
577
- import { basename, extname, isAbsolute as isAbsolute2, join as join5, posix, relative, resolve as resolve6, sep } from "path";
189
+ import { basename, extname, isAbsolute, join, posix, relative, resolve as resolve2, sep } from "path";
578
190
  import {
579
191
  forensicReportSchema
580
192
  } from "@fenglimg/fabric-shared";
@@ -660,7 +272,7 @@ var parserInitPromise = null;
660
272
  var languagePromiseByKind = {};
661
273
  var parserBundlePromiseByKind = {};
662
274
  async function buildForensicReport(targetInput) {
663
- const target = normalizeTarget2(targetInput);
275
+ const target = normalizeTarget(targetInput);
664
276
  const framework = detectFramework(target);
665
277
  const topology = buildTopology(target);
666
278
  const entryPoints = collectEntryPoints(target, topology.files);
@@ -696,11 +308,11 @@ async function buildForensicReport(targetInput) {
696
308
  }
697
309
  return validation.data;
698
310
  }
699
- function normalizeTarget2(targetInput) {
700
- return isAbsolute2(targetInput) ? targetInput : resolve6(process.cwd(), targetInput);
311
+ function normalizeTarget(targetInput) {
312
+ return isAbsolute(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
701
313
  }
702
314
  function buildTopology(root) {
703
- assertExistingDirectory2(root);
315
+ assertExistingDirectory(root);
704
316
  const byExt = {};
705
317
  const keyDirs = /* @__PURE__ */ new Set();
706
318
  const files = [];
@@ -713,7 +325,7 @@ function buildTopology(root) {
713
325
  continue;
714
326
  }
715
327
  for (const entry of readdirSync(current, { withFileTypes: true })) {
716
- const absolutePath = join5(current, entry.name);
328
+ const absolutePath = join(current, entry.name);
717
329
  const relativePath = toPosixPath(relative(root, absolutePath));
718
330
  if (relativePath.length === 0) {
719
331
  continue;
@@ -733,7 +345,7 @@ function buildTopology(root) {
733
345
  if (!entry.isFile()) {
734
346
  continue;
735
347
  }
736
- const stats = statSync2(absolutePath);
348
+ const stats = statSync(absolutePath);
737
349
  const extension = extname(entry.name) || "[none]";
738
350
  byExt[extension] = (byExt[extension] ?? 0) + 1;
739
351
  totalFiles += 1;
@@ -751,8 +363,8 @@ function buildTopology(root) {
751
363
  files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
752
364
  };
753
365
  }
754
- function assertExistingDirectory2(target) {
755
- if (!existsSync7(target) || !statSync2(target).isDirectory()) {
366
+ function assertExistingDirectory(target) {
367
+ if (!existsSync2(target) || !statSync(target).isDirectory()) {
756
368
  throw new Error(`Target must be an existing directory: ${target}`);
757
369
  }
758
370
  }
@@ -801,7 +413,7 @@ function getEntryPointReason(relativePath) {
801
413
  async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
802
414
  const samples = [];
803
415
  for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
804
- const absolutePath = join5(target, ...entryPoint.path.split("/"));
416
+ const absolutePath = join(target, ...entryPoint.path.split("/"));
805
417
  const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
806
418
  const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
807
419
  frameworkKind,
@@ -838,8 +450,8 @@ function readFirstLines(path, lineLimit) {
838
450
  }
839
451
  }
840
452
  function readPackageDependencies(target) {
841
- const packageJsonPath = join5(target, "package.json");
842
- if (!existsSync7(packageJsonPath)) {
453
+ const packageJsonPath = join(target, "package.json");
454
+ if (!existsSync2(packageJsonPath)) {
843
455
  return /* @__PURE__ */ new Map();
844
456
  }
845
457
  try {
@@ -1179,9 +791,9 @@ function scoreFrameworkConfidence(input) {
1179
791
  return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
1180
792
  }
1181
793
  function readReadmeInfo(target) {
1182
- const readmePath = join5(target, "README.md");
1183
- const hasContributing = existsSync7(join5(target, "CONTRIBUTING.md"));
1184
- if (!existsSync7(readmePath)) {
794
+ const readmePath = join(target, "README.md");
795
+ const hasContributing = existsSync2(join(target, "CONTRIBUTING.md"));
796
+ if (!existsSync2(readmePath)) {
1185
797
  return {
1186
798
  quality: "missing",
1187
799
  line_count: 0,
@@ -1666,8 +1278,8 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
1666
1278
  return recommendations;
1667
1279
  }
1668
1280
  function readProjectName(target) {
1669
- const packageJsonPath = join5(target, "package.json");
1670
- if (existsSync7(packageJsonPath)) {
1281
+ const packageJsonPath = join(target, "package.json");
1282
+ if (existsSync2(packageJsonPath)) {
1671
1283
  try {
1672
1284
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1673
1285
  if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
@@ -1680,7 +1292,7 @@ function readProjectName(target) {
1680
1292
  return basename(target);
1681
1293
  }
1682
1294
  function getCliVersion() {
1683
- return true ? "2.0.0-rc.1" : "unknown";
1295
+ return true ? "2.0.0-rc.10" : "unknown";
1684
1296
  }
1685
1297
  function sortRecord(record) {
1686
1298
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -1699,10 +1311,10 @@ Run \`fabric doctor\` to verify state.
1699
1311
 
1700
1312
  See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
1701
1313
  `;
1702
- var LOCAL_FABRIC_SERVER_PATH = join6("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1314
+ var LOCAL_FABRIC_SERVER_PATH = join2("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1703
1315
  var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
1704
1316
  var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
1705
- var initCommand = defineCommand3({
1317
+ var initCommand = defineCommand2({
1706
1318
  meta: {
1707
1319
  name: "init",
1708
1320
  description: t("cli.init.description")
@@ -1806,10 +1418,47 @@ async function runInitCommand(args) {
1806
1418
  process.exitCode = 130;
1807
1419
  return;
1808
1420
  }
1809
- return executeInitExecutionPlan(plan);
1421
+ const result = await executeInitExecutionPlan(plan);
1422
+ if (!intent.options.planOnly) {
1423
+ console.log(paint.muted("More: docs/surfaces.md explains when to use CLI vs Skill vs MCP."));
1424
+ }
1425
+ return result;
1426
+ }
1427
+ function writeDefaultFabricConfig(fabricDir) {
1428
+ const target = join2(fabricDir, "fabric-config.json");
1429
+ if (existsSync3(target)) return;
1430
+ const FABRIC_CONFIG_DEFAULTS = {
1431
+ // Scan/import language policy. `match-existing` lets init resolve the
1432
+ // effective language from project content; explicit `zh-CN` / `en`
1433
+ // lock the policy. See packages/shared/src/schemas/fabric-config.ts.
1434
+ knowledge_language: "match-existing",
1435
+ // fabric-hint Stop hook Signal A (archive): time-branch threshold, hours
1436
+ // since last knowledge_proposed event.
1437
+ archive_hint_hours: 24,
1438
+ // fabric-hint Stop hook cooldown after ANY signal fires, in hours.
1439
+ archive_hint_cooldown_hours: 12,
1440
+ // fabric-hint Stop hook Signal B (review): pending-count cutoff.
1441
+ review_hint_pending_count: 10,
1442
+ // fabric-hint Stop hook Signal B (review): pending-age cutoff in days.
1443
+ review_hint_pending_age_days: 7,
1444
+ // fabric-hint Stop hook Signal D (maintenance): days since last doctor.
1445
+ maintenance_hint_days: 14,
1446
+ // fabric-hint Stop hook Signal D (maintenance): cooldown between
1447
+ // reminders, in days.
1448
+ maintenance_hint_cooldown_days: 7,
1449
+ // fabric-hint Stop hook Signal A (archive): edit-count branch threshold;
1450
+ // PreToolUse fires recorded in .fabric/.cache/edit-counter since the
1451
+ // last knowledge_proposed event.
1452
+ archive_edit_threshold: 20,
1453
+ // fabric-hint Stop hook Signal C (import) + doctor lint #22: canonical
1454
+ // knowledge node count below this value flags an underseeded workspace.
1455
+ underseed_node_threshold: 10
1456
+ };
1457
+ mkdirSync(fabricDir, { recursive: true });
1458
+ writeFileSync(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
1810
1459
  }
1811
1460
  function resolveInitCliIntent(args, targetInput) {
1812
- const target = normalizeTarget3(targetInput);
1461
+ const target = normalizeTarget2(targetInput);
1813
1462
  const mcpInstallMode = resolveMcpInstallMode(args["mcp-install"]);
1814
1463
  const claudeMcpScope = resolveClaudeMcpScope(args.scope);
1815
1464
  const terminalInteractive = isInteractiveInit();
@@ -1929,20 +1578,20 @@ async function executeInitExecutionPlan(plan) {
1929
1578
  }
1930
1579
  var KNOWLEDGE_SUBDIRS = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1931
1580
  function resolvePersonalFabricRoot() {
1932
- return process.env.FABRIC_HOME ?? homedir5();
1581
+ return process.env.FABRIC_HOME ?? homedir();
1933
1582
  }
1934
1583
  async function buildInitFabricPlan(target, options) {
1935
- assertExistingDirectory3(target);
1936
- const fabricDir = join6(target, ".fabric");
1937
- const agentsMdPath = join6(target, "AGENTS.md");
1938
- const agentsMdAction = existsSync8(agentsMdPath) ? "preserved" : "created";
1939
- const knowledgeDir = join6(fabricDir, "knowledge");
1940
- const personalKnowledgeDir = join6(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1941
- const forensicPath = join6(fabricDir, "forensic.json");
1942
- const eventsPath = join6(fabricDir, "events.jsonl");
1943
- const metaPath = join6(fabricDir, "agents.meta.json");
1584
+ assertExistingDirectory2(target);
1585
+ const fabricDir = join2(target, ".fabric");
1586
+ const agentsMdPath = join2(target, "AGENTS.md");
1587
+ const agentsMdAction = existsSync3(agentsMdPath) ? "preserved" : "created";
1588
+ const knowledgeDir = join2(fabricDir, "knowledge");
1589
+ const personalKnowledgeDir = join2(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1590
+ const forensicPath = join2(fabricDir, "forensic.json");
1591
+ const eventsPath = join2(fabricDir, "events.jsonl");
1592
+ const metaPath = join2(fabricDir, "agents.meta.json");
1944
1593
  const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
1945
- const knowledgeDirAction = existsSync8(knowledgeDir) ? "overwritten" : "created";
1594
+ const knowledgeDirAction = existsSync3(knowledgeDir) ? "overwritten" : "created";
1946
1595
  const metaAction = planFreshPath(metaPath, options);
1947
1596
  const eventsAction = planFreshPath(eventsPath, options);
1948
1597
  const forensicAction = planFreshPath(forensicPath, options);
@@ -1974,30 +1623,31 @@ async function executeInitFabricPlan(plan) {
1974
1623
  rmSync(plan.fabricDir, { force: true });
1975
1624
  }
1976
1625
  mkdirSync(plan.fabricDir, { recursive: true });
1977
- if (plan.agentsMdAction === "created" && !existsSync8(plan.agentsMdPath)) {
1978
- await atomicWriteText2(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
1626
+ writeDefaultFabricConfig(plan.fabricDir);
1627
+ if (plan.agentsMdAction === "created" && !existsSync3(plan.agentsMdPath)) {
1628
+ await atomicWriteText(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
1979
1629
  }
1980
1630
  mkdirSync(plan.knowledgeDir, { recursive: true });
1981
1631
  for (const sub of KNOWLEDGE_SUBDIRS) {
1982
- const teamSubDir = join6(plan.knowledgeDir, sub);
1632
+ const teamSubDir = join2(plan.knowledgeDir, sub);
1983
1633
  mkdirSync(teamSubDir, { recursive: true });
1984
- const teamGitkeep = join6(teamSubDir, ".gitkeep");
1985
- if (!existsSync8(teamGitkeep)) {
1634
+ const teamGitkeep = join2(teamSubDir, ".gitkeep");
1635
+ if (!existsSync3(teamGitkeep)) {
1986
1636
  writeFileSync(teamGitkeep, "", "utf8");
1987
1637
  }
1988
1638
  }
1989
1639
  try {
1990
1640
  mkdirSync(plan.personalKnowledgeDir, { recursive: true });
1991
1641
  for (const sub of KNOWLEDGE_SUBDIRS) {
1992
- mkdirSync(join6(plan.personalKnowledgeDir, sub), { recursive: true });
1642
+ mkdirSync(join2(plan.personalKnowledgeDir, sub), { recursive: true });
1993
1643
  }
1994
1644
  } catch {
1995
1645
  }
1996
1646
  preparePlannedPath(plan.metaPath, plan.metaAction);
1997
- await atomicWriteJson2(plan.metaPath, plan.meta);
1647
+ await atomicWriteJson(plan.metaPath, plan.meta);
1998
1648
  if (isReapply) {
1999
- if (!existsSync8(plan.eventsPath)) {
2000
- mkdirSync(dirname3(plan.eventsPath), { recursive: true });
1649
+ if (!existsSync3(plan.eventsPath)) {
1650
+ mkdirSync(dirname(plan.eventsPath), { recursive: true });
2001
1651
  writeFileSync(plan.eventsPath, "", "utf8");
2002
1652
  }
2003
1653
  } else {
@@ -2005,7 +1655,7 @@ async function executeInitFabricPlan(plan) {
2005
1655
  writeFileSync(plan.eventsPath, "", "utf8");
2006
1656
  }
2007
1657
  preparePlannedPath(plan.forensicPath, plan.forensicAction);
2008
- await atomicWriteJson2(plan.forensicPath, plan.forensicReport);
1658
+ await atomicWriteJson(plan.forensicPath, plan.forensicReport);
2009
1659
  if (!plan.options?.reapply) {
2010
1660
  try {
2011
1661
  await runInitScan(plan.target, { source: "init" });
@@ -2139,8 +1789,28 @@ async function executeInitStagePlan(plan, stageName) {
2139
1789
  try {
2140
1790
  switch (stage.name) {
2141
1791
  case "bootstrap": {
2142
- console.log(formatInitStageResult("bootstrap", "skipped", 0, 0, t("cli.bootstrap.install.no-targets")));
2143
- return { name: "bootstrap", disposition: "skipped" };
1792
+ const installResults = [];
1793
+ installResults.push(...await runBestEffort("skill-install", () => installFabricArchiveSkill(plan.target)));
1794
+ installResults.push(...await runBestEffort("skill-review-install", () => installFabricReviewSkill(plan.target)));
1795
+ installResults.push(...await runBestEffort("skill-import-install", () => installFabricImportSkill(plan.target)));
1796
+ installResults.push(...await runBestEffort("hook-script", () => installArchiveHintHook(plan.target)));
1797
+ installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
1798
+ installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
1799
+ installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
1800
+ installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
1801
+ installResults.push(await runBestEffortSingle("cursor-hook-config", () => mergeCursorHookConfig(plan.target)));
1802
+ installResults.push(...await runBestEffort("pointer", () => addArchiveSkillPointer(plan.target)));
1803
+ const installedCount = installResults.filter((r) => r.status === "written").length;
1804
+ const skippedCount = installResults.filter((r) => r.status === "skipped").length;
1805
+ const errorCount = installResults.filter((r) => r.status === "error").length;
1806
+ for (const result of installResults) {
1807
+ if (result.status === "error") {
1808
+ writeStderr2(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
1809
+ }
1810
+ }
1811
+ const note2 = errorCount > 0 ? `errors=${errorCount}` : void 0;
1812
+ console.log(formatInitStageResult("bootstrap", "completed", installedCount, skippedCount, note2));
1813
+ return { name: "bootstrap", disposition: "ran" };
2144
1814
  }
2145
1815
  case "mcp": {
2146
1816
  if (stage.installMode === "local") {
@@ -2178,10 +1848,10 @@ async function executeInitStagePlan(plan, stageName) {
2178
1848
  }
2179
1849
  }
2180
1850
  function shouldReplaceWritableDirectory(path, options) {
2181
- if (!existsSync8(path)) {
1851
+ if (!existsSync3(path)) {
2182
1852
  return false;
2183
1853
  }
2184
- if (statSync3(path).isDirectory()) {
1854
+ if (statSync2(path).isDirectory()) {
2185
1855
  return false;
2186
1856
  }
2187
1857
  if (!options?.force) {
@@ -2190,7 +1860,7 @@ function shouldReplaceWritableDirectory(path, options) {
2190
1860
  return true;
2191
1861
  }
2192
1862
  function planFreshPath(path, options) {
2193
- if (!existsSync8(path)) {
1863
+ if (!existsSync3(path)) {
2194
1864
  return "created";
2195
1865
  }
2196
1866
  if (!options?.force) {
@@ -2199,8 +1869,8 @@ function planFreshPath(path, options) {
2199
1869
  return "overwritten";
2200
1870
  }
2201
1871
  function preparePlannedPath(path, action) {
2202
- mkdirSync(dirname3(path), { recursive: true });
2203
- if (action === "overwritten" && existsSync8(path)) {
1872
+ mkdirSync(dirname(path), { recursive: true });
1873
+ if (action === "overwritten" && existsSync3(path)) {
2204
1874
  rmSync(path, { recursive: true, force: true });
2205
1875
  }
2206
1876
  }
@@ -2375,23 +2045,23 @@ function formatInitModeBadge(options) {
2375
2045
  }
2376
2046
  return t("cli.init.mode.badge.default");
2377
2047
  }
2378
- function normalizeTarget3(targetInput) {
2379
- return isAbsolute3(targetInput) ? targetInput : resolve7(process.cwd(), targetInput);
2048
+ function normalizeTarget2(targetInput) {
2049
+ return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2380
2050
  }
2381
- function assertExistingDirectory3(target) {
2382
- if (!existsSync8(target) || !statSync3(target).isDirectory()) {
2051
+ function assertExistingDirectory2(target) {
2052
+ if (!existsSync3(target) || !statSync2(target).isDirectory()) {
2383
2053
  throw new Error(`Target must be an existing directory: ${target}`);
2384
2054
  }
2385
2055
  }
2386
2056
  function detectPackageManager(cwd) {
2387
- const workspaceRoot = resolve7(cwd);
2388
- if (existsSync8(join6(workspaceRoot, "pnpm-lock.yaml"))) {
2057
+ const workspaceRoot = resolve3(cwd);
2058
+ if (existsSync3(join2(workspaceRoot, "pnpm-lock.yaml"))) {
2389
2059
  return "pnpm";
2390
2060
  }
2391
- if (existsSync8(join6(workspaceRoot, "yarn.lock"))) {
2061
+ if (existsSync3(join2(workspaceRoot, "yarn.lock"))) {
2392
2062
  return "yarn";
2393
2063
  }
2394
- if (existsSync8(join6(workspaceRoot, "package-lock.json"))) {
2064
+ if (existsSync3(join2(workspaceRoot, "package-lock.json"))) {
2395
2065
  return "npm";
2396
2066
  }
2397
2067
  return "npm";
@@ -2431,6 +2101,32 @@ function appendReapplyLedgerEvent(eventsPath, payload) {
2431
2101
  `;
2432
2102
  appendFileSync(eventsPath, line, "utf8");
2433
2103
  }
2104
+ async function runBestEffort(step, fn) {
2105
+ try {
2106
+ return await fn();
2107
+ } catch (error) {
2108
+ return [
2109
+ {
2110
+ step,
2111
+ path: "",
2112
+ status: "error",
2113
+ message: error instanceof Error ? error.message : String(error)
2114
+ }
2115
+ ];
2116
+ }
2117
+ }
2118
+ async function runBestEffortSingle(step, fn) {
2119
+ try {
2120
+ return await fn();
2121
+ } catch (error) {
2122
+ return {
2123
+ step,
2124
+ path: "",
2125
+ status: "error",
2126
+ message: error instanceof Error ? error.message : String(error)
2127
+ };
2128
+ }
2129
+ }
2434
2130
  function formatInitStageHeader(message) {
2435
2131
  return `${nextLabel()} ${paint.muted(message)}`;
2436
2132
  }
@@ -2485,6 +2181,7 @@ function printInitPlanSummary(target, options, mcpInstallMode, supports) {
2485
2181
  console.log(` - ${target}/.fabric/agents.meta.json`);
2486
2182
  console.log(` - ${target}/.fabric/events.jsonl`);
2487
2183
  console.log(` - ${target}/.fabric/forensic.json`);
2184
+ console.log(` - ${target}/.fabric/fabric-config.json`);
2488
2185
  }
2489
2186
  function printInitCapabilitySummary(supports, stageResults, options) {
2490
2187
  const detected = supports.filter((support) => support.detected);