@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,251 +1,63 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ deepMerge
4
+ } from "./chunk-MF3OTILQ.js";
2
5
 
3
- // src/install/skills-and-hooks.ts
4
- import { chmodSync, existsSync as existsSync2, readFileSync } from "fs";
5
- import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
6
- import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
7
- import { fileURLToPath } from "url";
8
- import { atomicWriteJson as atomicWriteJson2, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
9
-
10
- // src/config/json.ts
11
- import { existsSync } from "fs";
12
- import { mkdir, readFile } from "fs/promises";
13
- import { dirname, join, resolve } from "path";
14
- import { homedir } from "os";
15
- import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
16
-
17
- // src/config/writer.ts
18
- function createServerEntry(serverPath) {
19
- return {
20
- command: process.execPath,
21
- args: [serverPath]
22
- };
23
- }
24
-
25
- // src/config/json.ts
26
- function deepMerge(target, source, options = {}) {
27
- return deepMergeAtPath(target, source, "", options);
6
+ // src/install/write-bootstrap-snapshot.ts
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { mkdir } from "fs/promises";
9
+ import { dirname, join } from "path";
10
+ import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
11
+ import { BOOTSTRAP_CANONICAL } from "@fenglimg/fabric-shared/templates/bootstrap-canonical";
12
+ var FABRIC_AGENTS_RELPATH = join(".fabric", "AGENTS.md");
13
+ var PROJECT_RULES_RELPATH = join(".fabric", "project-rules.md");
14
+ function fabricAgentsSnapshotPath(targetRoot) {
15
+ return join(targetRoot, FABRIC_AGENTS_RELPATH);
28
16
  }
29
- function deepMergeAtPath(target, source, path, options) {
30
- if (options.arrayAppendPaths && options.arrayAppendPaths.includes(path) && Array.isArray(target) && Array.isArray(source)) {
31
- return appendArrayWithDedupe(target, source);
32
- }
33
- if (target === null || typeof target !== "object" || Array.isArray(target) || source === null || typeof source !== "object" || Array.isArray(source)) {
34
- return source;
35
- }
36
- const out = { ...target };
37
- for (const key of Object.keys(source)) {
38
- const childPath = path === "" ? key : `${path}.${key}`;
39
- out[key] = deepMergeAtPath(
40
- target[key],
41
- source[key],
42
- childPath,
43
- options
44
- );
45
- }
46
- return out;
17
+ function projectRulesPath(targetRoot) {
18
+ return join(targetRoot, PROJECT_RULES_RELPATH);
47
19
  }
48
- function appendArrayWithDedupe(target, source) {
49
- const out = [...target];
50
- for (const candidate of source) {
51
- if (out.some((existing) => isSameHookEntry(existing, candidate))) {
52
- continue;
53
- }
54
- out.push(candidate);
55
- }
56
- return out;
57
- }
58
- function isSameHookEntry(a, b) {
59
- const cmdA = extractHookCommand(a);
60
- const cmdB = extractHookCommand(b);
61
- if (cmdA !== null && cmdB !== null) {
62
- return cmdA === cmdB;
63
- }
64
- return deepEqual(a, b);
20
+ function readProjectRulesIfPresent(targetRoot) {
21
+ const path = projectRulesPath(targetRoot);
22
+ if (!existsSync(path)) return null;
23
+ return readFileSync(path, "utf8");
65
24
  }
66
- function extractHookCommand(item) {
67
- if (item === null || typeof item !== "object") {
68
- return null;
69
- }
70
- const obj = item;
71
- if (typeof obj.command === "string") {
72
- return obj.command;
73
- }
74
- if (Array.isArray(obj.hooks)) {
75
- for (const inner of obj.hooks) {
76
- if (inner !== null && typeof inner === "object") {
77
- const innerObj = inner;
78
- if (typeof innerObj.command === "string") {
79
- return innerObj.command;
80
- }
25
+ async function writeFabricAgentsSnapshot(targetRoot) {
26
+ const step = "bootstrap-snapshot";
27
+ const target = fabricAgentsSnapshotPath(targetRoot);
28
+ if (existsSync(target)) {
29
+ try {
30
+ const existing = readFileSync(target, "utf8");
31
+ if (existing === BOOTSTRAP_CANONICAL) {
32
+ return { step, path: target, status: "skipped", message: "up-to-date" };
81
33
  }
34
+ } catch {
82
35
  }
83
36
  }
84
- return null;
85
- }
86
- function deepEqual(a, b) {
87
- if (a === b) {
88
- return true;
89
- }
90
- if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
91
- return false;
92
- }
93
- if (Array.isArray(a) !== Array.isArray(b)) {
94
- return false;
95
- }
96
- if (Array.isArray(a) && Array.isArray(b)) {
97
- if (a.length !== b.length) {
98
- return false;
99
- }
100
- return a.every((value, index) => deepEqual(value, b[index]));
101
- }
102
- const aObj = a;
103
- const bObj = b;
104
- const aKeys = Object.keys(aObj);
105
- const bKeys = Object.keys(bObj);
106
- if (aKeys.length !== bKeys.length) {
107
- return false;
108
- }
109
- return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
110
- }
111
- function expandHome(filePath) {
112
- if (filePath === "~") {
113
- return homedir();
114
- }
115
- if (filePath.startsWith("~/")) {
116
- return join(homedir(), filePath.slice(2));
117
- }
118
- return filePath;
119
- }
120
- function normalizeConfigPath(filePath) {
121
- return resolve(expandHome(filePath));
122
- }
123
- async function readJsonConfig(configPath) {
124
- try {
125
- const raw = await readFile(configPath, "utf8");
126
- if (raw.trim().length === 0) {
127
- return {};
128
- }
129
- const parsed = JSON.parse(raw);
130
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
131
- throw new Error(`Expected JSON object in ${configPath}`);
132
- }
133
- return parsed;
134
- } catch (error) {
135
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
136
- return {};
137
- }
138
- throw error;
139
- }
140
- }
141
- async function writeJsonClientConfig(configPath, serverEntry) {
142
- const existing = await readJsonConfig(configPath);
143
- const merged = deepMerge(existing, { mcpServers: { fabric: serverEntry } });
144
- await mkdir(dirname(configPath), { recursive: true });
145
- await atomicWriteJson(configPath, merged, { indent: 2 });
146
- }
147
- async function removeJsonClientConfigEntry(configPath, serverName) {
148
- if (!existsSync(configPath)) {
149
- return { status: "skipped", path: configPath, message: "no-config-file" };
150
- }
151
- let existing;
152
- try {
153
- existing = await readJsonConfig(configPath);
154
- } catch (error) {
155
- return {
156
- status: "error",
157
- path: configPath,
158
- message: error instanceof Error ? error.message : String(error)
159
- };
160
- }
161
- const mcpServers = existing.mcpServers;
162
- if (mcpServers === void 0 || mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
163
- return { status: "skipped", path: configPath, message: "no-mcp-servers-object" };
164
- }
165
- const servers = mcpServers;
166
- if (!Object.prototype.hasOwnProperty.call(servers, serverName)) {
167
- return { status: "skipped", path: configPath, message: "not-present" };
168
- }
169
- const nextServers = { ...servers };
170
- delete nextServers[serverName];
171
- const next = { ...existing, mcpServers: nextServers };
172
- try {
173
- await mkdir(dirname(configPath), { recursive: true });
174
- await atomicWriteJson(configPath, next, { indent: 2 });
175
- return { status: "removed", path: configPath };
176
- } catch (error) {
177
- return {
178
- status: "error",
179
- path: configPath,
180
- message: error instanceof Error ? error.message : String(error)
181
- };
182
- }
37
+ await mkdir(dirname(target), { recursive: true });
38
+ await atomicWriteText(target, BOOTSTRAP_CANONICAL);
39
+ return { step, path: target, status: "written" };
183
40
  }
184
- var JsonClientConfigWriter = class {
185
- configuredPath;
186
- constructor(configuredPath) {
187
- this.configuredPath = configuredPath;
188
- }
189
- async detect(workspaceRoot, overridePath) {
190
- const explicitPath = overridePath ?? this.configuredPath;
191
- if (explicitPath !== void 0) {
192
- return normalizeConfigPath(explicitPath);
193
- }
194
- const configPath = this.defaultPath(workspaceRoot);
195
- return configPath === null ? null : normalizeConfigPath(configPath);
196
- }
197
- async write(serverPath, workspaceRoot, overridePath) {
198
- const configPath = await this.detect(workspaceRoot, overridePath);
199
- if (configPath === null) {
200
- return;
201
- }
202
- await writeJsonClientConfig(configPath, createServerEntry(serverPath));
203
- }
204
- async remove(serverName, workspaceRoot, overridePath) {
205
- const configPath = await this.detect(workspaceRoot, overridePath);
206
- if (configPath === null) {
207
- return { status: "skipped", message: "no-config-path" };
208
- }
209
- return removeJsonClientConfigEntry(configPath, serverName);
210
- }
211
- };
212
- var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
213
- clientKind = "ClaudeCodeCLI";
214
- scope;
215
- constructor(configuredPath, scope = "project") {
216
- super(configuredPath);
217
- this.scope = scope;
218
- }
219
- // Writes to project-level .mcp.json (per Claude Code MCP spec) by default,
220
- // or ~/.claude.json for user scope.
221
- // Detection still checks ~/.claude to confirm Claude Code is installed.
222
- defaultPath(workspaceRoot) {
223
- const globalClaudeDir = join(homedir(), ".claude");
224
- const projectClaudeDir = join(workspaceRoot, ".claude");
225
- if (!existsSync(globalClaudeDir) && !existsSync(projectClaudeDir)) {
226
- return null;
227
- }
228
- return this.scope === "user" ? join(homedir(), ".claude.json") : join(workspaceRoot, ".mcp.json");
229
- }
230
- };
231
- var CursorWriter = class extends JsonClientConfigWriter {
232
- clientKind = "Cursor";
233
- constructor(configuredPath) {
234
- super(configuredPath);
235
- }
236
- defaultPath(workspaceRoot) {
237
- const cursorDir = join(workspaceRoot, ".cursor");
238
- return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
239
- }
240
- };
241
41
 
242
42
  // src/install/skills-and-hooks.ts
43
+ import { chmodSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
44
+ import { mkdir as mkdir2, readFile, rm } from "fs/promises";
45
+ import { dirname as dirname2, join as join2, parse, resolve } from "path";
46
+ import { fileURLToPath } from "url";
47
+ import { atomicWriteJson, atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
48
+ import {
49
+ BOOTSTRAP_MARKER_BEGIN,
50
+ BOOTSTRAP_MARKER_END,
51
+ BOOTSTRAP_REGEX,
52
+ LEGACY_KB_REGEX
53
+ } from "@fenglimg/fabric-shared/templates/bootstrap-canonical";
243
54
  var SKILL_TEMPLATE_REL = "skills/fabric-archive/SKILL.md";
244
55
  var SKILL_REVIEW_TEMPLATE_REL = "skills/fabric-review/SKILL.md";
245
56
  var SKILL_IMPORT_TEMPLATE_REL = "skills/fabric-import/SKILL.md";
246
57
  var HOOK_SCRIPT_TEMPLATE_REL = "hooks/fabric-hint.cjs";
247
58
  var HOOK_BROAD_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-broad.cjs";
248
59
  var HOOK_NARROW_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-narrow.cjs";
60
+ var HOOK_LIB_TEMPLATE_DIR_REL = "hooks/lib";
249
61
  var CLAUDE_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/claude-code.json";
250
62
  var CODEX_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/codex-hooks.json";
251
63
  var CURSOR_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/cursor-hooks.json";
@@ -280,6 +92,11 @@ var HOOK_SCRIPT_DESTINATIONS = {
280
92
  ".cursor/hooks/knowledge-hint-narrow.cjs"
281
93
  ]
282
94
  };
95
+ var HOOK_LIB_DESTINATIONS = [
96
+ ".claude/hooks/lib",
97
+ ".codex/hooks/lib",
98
+ ".cursor/hooks/lib"
99
+ ];
283
100
  var HOOK_CONFIG_TARGETS = {
284
101
  claudeCode: ".claude/settings.json",
285
102
  codex: ".codex/hooks.json",
@@ -288,7 +105,7 @@ var HOOK_CONFIG_TARGETS = {
288
105
  var HOOK_CONFIG_ARRAY_PATHS = {
289
106
  claudeCode: ["hooks.Stop", "hooks.SessionStart", "hooks.PreToolUse"],
290
107
  codex: ["events.Stop", "events.SessionStart", "events.PreToolUse"],
291
- cursor: ["events.Stop", "events.SessionStart", "events.PreToolUse"]
108
+ cursor: ["hooks.stop", "hooks.sessionStart", "hooks.preToolUse"]
292
109
  };
293
110
  var FABRIC_HOOK_COMMAND_PATHS = {
294
111
  claudeCode: {
@@ -307,17 +124,13 @@ var FABRIC_HOOK_COMMAND_PATHS = {
307
124
  knowledgeHintNarrow: ".cursor/hooks/knowledge-hint-narrow.cjs"
308
125
  }
309
126
  };
310
- var SECTION_TARGETS = ["CLAUDE.md", "AGENTS.md", join2(".cursor", "rules")];
311
- var FABRIC_SECTION_BEGIN_MARKER = "<!-- fabric:knowledge-base:begin -->";
312
- var FABRIC_SECTION_END_MARKER = "<!-- fabric:knowledge-base:end -->";
313
- var FABRIC_SECTION_REGEX = /(?:\r?\n){0,2}<!-- fabric:knowledge-base:begin -->[\s\S]*?<!-- fabric:knowledge-base:end -->/;
314
127
  function readFabricLanguagePreference(projectRoot) {
315
128
  const configPath = join2(projectRoot, ".fabric", "fabric-config.json");
316
129
  if (!existsSync2(configPath)) {
317
130
  return "match-existing";
318
131
  }
319
132
  try {
320
- const raw = readFileSync(configPath, "utf8");
133
+ const raw = readFileSync2(configPath, "utf8");
321
134
  const parsed = JSON.parse(raw);
322
135
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
323
136
  return "match-existing";
@@ -328,20 +141,6 @@ function readFabricLanguagePreference(projectRoot) {
328
141
  return "match-existing";
329
142
  }
330
143
  }
331
- function buildFabricKnowledgeBaseSection(fabricLanguage) {
332
- return `${FABRIC_SECTION_BEGIN_MARKER}
333
-
334
- ## Fabric Knowledge Base
335
-
336
- This project uses Fabric for persistent project knowledge under \`.fabric/knowledge/\`.
337
-
338
- - **Discovery**: SessionStart lists available entries (broad menu); editing files may surface narrow hints
339
- - **Usage**: call \`fab_get_knowledge_sections\` to fetch full content of any entry by id
340
- - **Write flows**: see fabric-archive (record), fabric-review (validate), fabric-import (backfill) Skills
341
- - **Language**: rendered per \`fabric_language\` in \`.fabric/fabric-config.json\` (current: \`${fabricLanguage}\`)
342
-
343
- ${FABRIC_SECTION_END_MARKER}`;
344
- }
345
144
  async function installFabricArchiveSkill(projectRoot, _options = {}) {
346
145
  const source = await readTemplate(SKILL_TEMPLATE_REL);
347
146
  const targets = SKILL_DESTINATIONS.fabricArchive.map((rel) => join2(projectRoot, rel));
@@ -417,6 +216,53 @@ async function installKnowledgeHintNarrowHook(projectRoot, _options = {}) {
417
216
  }
418
217
  return results;
419
218
  }
219
+ async function installHookLibs(projectRoot, _options = {}) {
220
+ const libTemplateDir = findTemplatePath(HOOK_LIB_TEMPLATE_DIR_REL);
221
+ let libFiles;
222
+ try {
223
+ libFiles = readdirSync(libTemplateDir).filter((name) => name.endsWith(".cjs"));
224
+ } catch (error) {
225
+ return [
226
+ {
227
+ step: "hook-lib",
228
+ path: libTemplateDir,
229
+ status: "error",
230
+ message: error instanceof Error ? error.message : String(error)
231
+ }
232
+ ];
233
+ }
234
+ if (libFiles.length === 0) {
235
+ return [
236
+ {
237
+ step: "hook-lib",
238
+ path: libTemplateDir,
239
+ status: "skipped",
240
+ message: "no-libs-to-ship"
241
+ }
242
+ ];
243
+ }
244
+ const results = [];
245
+ for (const libFile of libFiles) {
246
+ const sourcePath = join2(libTemplateDir, libFile);
247
+ let source;
248
+ try {
249
+ source = readFileSync2(sourcePath, "utf8");
250
+ } catch (error) {
251
+ results.push({
252
+ step: "hook-lib",
253
+ path: sourcePath,
254
+ status: "error",
255
+ message: error instanceof Error ? error.message : String(error)
256
+ });
257
+ continue;
258
+ }
259
+ for (const destDirRel of HOOK_LIB_DESTINATIONS) {
260
+ const target = join2(projectRoot, destDirRel, libFile);
261
+ results.push(await copyTextIdempotent("hook-lib", source, target));
262
+ }
263
+ }
264
+ return results;
265
+ }
420
266
  async function mergeClaudeCodeHookConfig(projectRoot, _options = {}) {
421
267
  const fragment = await readJsonTemplate(CLAUDE_HOOK_CONFIG_TEMPLATE_REL);
422
268
  const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.claudeCode);
@@ -447,65 +293,216 @@ async function mergeCursorHookConfig(projectRoot, _options = {}) {
447
293
  [...HOOK_CONFIG_ARRAY_PATHS.cursor]
448
294
  );
449
295
  }
450
- async function addFabricKnowledgeBaseSection(projectRoot, fabricLanguage, _options = {}) {
451
- const sectionBody = buildFabricKnowledgeBaseSection(fabricLanguage);
452
- const results = [];
453
- for (const rel of SECTION_TARGETS) {
454
- const target = join2(projectRoot, rel);
455
- if (!existsSync2(target)) {
456
- results.push({ step: "section", path: target, status: "skipped", message: "absent" });
457
- continue;
296
+ function buildManagedBlockBody(targetRoot) {
297
+ const snapshotPath = fabricAgentsSnapshotPath(targetRoot);
298
+ const snapshot = readFileSync2(snapshotPath, "utf8");
299
+ const projectRules = readProjectRulesIfPresent(targetRoot);
300
+ if (projectRules === null) {
301
+ return snapshot;
302
+ }
303
+ return `${snapshot}
304
+ ---
305
+ ${projectRules}`;
306
+ }
307
+ function wrapInBootstrapMarkers(body) {
308
+ return `${BOOTSTRAP_MARKER_BEGIN}
309
+ ${body}
310
+ ${BOOTSTRAP_MARKER_END}`;
311
+ }
312
+ function stripLegacyKnowledgeBaseSection(existing) {
313
+ const match = existing.match(LEGACY_KB_REGEX);
314
+ if (match === null) return existing;
315
+ const before = existing.slice(0, match.index ?? 0);
316
+ const after = existing.slice((match.index ?? 0) + match[0].length);
317
+ return `${before}${after.replace(/^\r?\n/, "")}`;
318
+ }
319
+ var CLAUDE_BOOTSTRAP_HEADER = "# Project Knowledge";
320
+ var CLAUDE_AGENTS_IMPORT_LINE = "@.fabric/AGENTS.md";
321
+ var CLAUDE_PROJECT_RULES_IMPORT_LINE = "@.fabric/project-rules.md";
322
+ async function writeClaudeBootstrapThinShell(targetRoot, _options = {}) {
323
+ const step = "bootstrap-claude";
324
+ const target = join2(targetRoot, "CLAUDE.md");
325
+ const projectRulesPresent = existsSync2(projectRulesPath(targetRoot));
326
+ let existing = "";
327
+ let preExisted = false;
328
+ if (existsSync2(target)) {
329
+ preExisted = true;
330
+ try {
331
+ existing = await readFile(target, "utf8");
332
+ } catch (error) {
333
+ return {
334
+ step,
335
+ path: target,
336
+ status: "error",
337
+ message: error instanceof Error ? error.message : String(error)
338
+ };
458
339
  }
459
- let existing;
340
+ }
341
+ let next = stripLegacyKnowledgeBaseSection(existing);
342
+ if (!projectRulesPresent) {
343
+ next = removeImportLine(next, CLAUDE_PROJECT_RULES_IMPORT_LINE);
344
+ }
345
+ if (!preExisted && next.length === 0) {
346
+ next = `${CLAUDE_BOOTSTRAP_HEADER}
347
+ `;
348
+ }
349
+ next = ensureImportLine(next, CLAUDE_AGENTS_IMPORT_LINE);
350
+ if (projectRulesPresent) {
351
+ next = ensureImportLine(next, CLAUDE_PROJECT_RULES_IMPORT_LINE);
352
+ }
353
+ if (next === existing) {
354
+ return { step, path: target, status: "skipped", message: "up-to-date" };
355
+ }
356
+ try {
357
+ await mkdir2(dirname2(target), { recursive: true });
358
+ await atomicWriteText2(target, next);
359
+ return { step, path: target, status: "written" };
360
+ } catch (error) {
361
+ return {
362
+ step,
363
+ path: target,
364
+ status: "error",
365
+ message: error instanceof Error ? error.message : String(error)
366
+ };
367
+ }
368
+ }
369
+ function ensureImportLine(content, line) {
370
+ if (hasExactLine(content, line)) return content;
371
+ if (content.length === 0) return `${line}
372
+ `;
373
+ const endsWithBlank = content.endsWith("\n\n");
374
+ const endsWithNewline = content.endsWith("\n");
375
+ if (endsWithBlank) {
376
+ return `${content}${line}
377
+ `;
378
+ }
379
+ if (endsWithNewline) {
380
+ return `${content}
381
+ ${line}
382
+ `;
383
+ }
384
+ return `${content}
385
+
386
+ ${line}
387
+ `;
388
+ }
389
+ function removeImportLine(content, line) {
390
+ const lines = content.split(/\r?\n/);
391
+ const filtered = lines.filter((l) => l.replace(/\s+$/, "") !== line);
392
+ return filtered.join("\n");
393
+ }
394
+ function hasExactLine(content, line) {
395
+ const lines = content.split(/\r?\n/);
396
+ return lines.some((l) => l.replace(/\s+$/, "") === line);
397
+ }
398
+ async function writeCodexBootstrapManagedBlock(targetRoot, _options = {}) {
399
+ const step = "bootstrap-codex";
400
+ const target = join2(targetRoot, "AGENTS.md");
401
+ let existing = "";
402
+ if (existsSync2(target)) {
460
403
  try {
461
- existing = await readFile2(target, "utf8");
404
+ existing = await readFile(target, "utf8");
462
405
  } catch (error) {
463
- results.push({
464
- step: "section",
406
+ return {
407
+ step,
465
408
  path: target,
466
409
  status: "error",
467
410
  message: error instanceof Error ? error.message : String(error)
468
- });
469
- continue;
411
+ };
470
412
  }
471
- let next;
472
- const match = existing.match(FABRIC_SECTION_REGEX);
473
- if (match !== null) {
474
- const before = existing.slice(0, match.index ?? 0);
475
- const after = existing.slice((match.index ?? 0) + match[0].length);
476
- const stripped = `${before}${after.replace(/^\r?\n/, "")}`;
477
- const trailingNewline = stripped.length === 0 || stripped.endsWith("\n") ? "" : "\n";
478
- next = `${stripped}${trailingNewline}
479
- ${sectionBody}
413
+ }
414
+ const body = buildManagedBlockBody(targetRoot);
415
+ const managedBlock = wrapInBootstrapMarkers(body);
416
+ const stripped = stripLegacyKnowledgeBaseSection(existing);
417
+ let next;
418
+ const match = stripped.match(BOOTSTRAP_REGEX);
419
+ if (match !== null) {
420
+ const before = stripped.slice(0, match.index ?? 0);
421
+ const after = stripped.slice((match.index ?? 0) + match[0].length);
422
+ const cleaned = `${before}${after.replace(/^\r?\n/, "")}`;
423
+ const trailingNewline = cleaned.length === 0 || cleaned.endsWith("\n") ? "" : "\n";
424
+ next = `${cleaned}${trailingNewline}${cleaned.length === 0 ? "" : "\n"}${managedBlock}
425
+ `;
426
+ } else {
427
+ if (stripped.length === 0) {
428
+ next = `${managedBlock}
480
429
  `;
481
430
  } else {
482
- const trailingNewline = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
483
- next = `${existing}${trailingNewline}
484
- ${sectionBody}
431
+ const trailingNewline = stripped.endsWith("\n") ? "" : "\n";
432
+ next = `${stripped}${trailingNewline}
433
+ ${managedBlock}
485
434
  `;
486
435
  }
487
- if (next === existing) {
488
- results.push({ step: "section", path: target, status: "skipped", message: "up-to-date" });
489
- continue;
436
+ }
437
+ if (next === existing) {
438
+ return { step, path: target, status: "skipped", message: "up-to-date" };
439
+ }
440
+ try {
441
+ await mkdir2(dirname2(target), { recursive: true });
442
+ await atomicWriteText2(target, next);
443
+ return { step, path: target, status: "written" };
444
+ } catch (error) {
445
+ return {
446
+ step,
447
+ path: target,
448
+ status: "error",
449
+ message: error instanceof Error ? error.message : String(error)
450
+ };
451
+ }
452
+ }
453
+ var CURSOR_RULE_FRONT_MATTER = "---\nalwaysApply: true\ndescription: Fabric Protocol bootstrap rules\n---\n\n";
454
+ var CURSOR_LEGACY_FLAT_FILE_REL = join2(".cursor", "rules");
455
+ var CURSOR_BOOTSTRAP_MDC_REL = join2(".cursor", "rules", "fabric-bootstrap.mdc");
456
+ async function writeCursorBootstrapManagedBlock(targetRoot, _options = {}) {
457
+ const step = "bootstrap-cursor";
458
+ const target = join2(targetRoot, CURSOR_BOOTSTRAP_MDC_REL);
459
+ const legacyFlatFile = join2(targetRoot, CURSOR_LEGACY_FLAT_FILE_REL);
460
+ try {
461
+ if (existsSync2(legacyFlatFile)) {
462
+ const stat = statSync(legacyFlatFile);
463
+ if (stat.isFile()) {
464
+ await rm(legacyFlatFile, { force: true });
465
+ }
490
466
  }
467
+ } catch {
468
+ }
469
+ const body = buildManagedBlockBody(targetRoot);
470
+ const managedBlock = wrapInBootstrapMarkers(body);
471
+ const expected = `${CURSOR_RULE_FRONT_MATTER}${managedBlock}
472
+ `;
473
+ let existing = "";
474
+ if (existsSync2(target)) {
491
475
  try {
492
- await atomicWriteText(target, next);
493
- results.push({ step: "section", path: target, status: "written" });
476
+ existing = await readFile(target, "utf8");
494
477
  } catch (error) {
495
- results.push({
496
- step: "section",
478
+ return {
479
+ step,
497
480
  path: target,
498
481
  status: "error",
499
482
  message: error instanceof Error ? error.message : String(error)
500
- });
483
+ };
501
484
  }
502
485
  }
503
- return results;
486
+ if (existing === expected) {
487
+ return { step, path: target, status: "skipped", message: "up-to-date" };
488
+ }
489
+ try {
490
+ await mkdir2(dirname2(target), { recursive: true });
491
+ await atomicWriteText2(target, expected);
492
+ return { step, path: target, status: "written" };
493
+ } catch (error) {
494
+ return {
495
+ step,
496
+ path: target,
497
+ status: "error",
498
+ message: error instanceof Error ? error.message : String(error)
499
+ };
500
+ }
504
501
  }
505
502
  async function copyTextIdempotent(step, source, target) {
506
503
  if (existsSync2(target)) {
507
504
  try {
508
- const existing = readFileSync(target, "utf8");
505
+ const existing = readFileSync2(target, "utf8");
509
506
  if (existing === source) {
510
507
  return { step, path: target, status: "skipped", message: "up-to-date" };
511
508
  }
@@ -513,7 +510,7 @@ async function copyTextIdempotent(step, source, target) {
513
510
  }
514
511
  }
515
512
  await mkdir2(dirname2(target), { recursive: true });
516
- await atomicWriteText(target, source);
513
+ await atomicWriteText2(target, source);
517
514
  return { step, path: target, status: "written" };
518
515
  }
519
516
  async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
@@ -523,12 +520,12 @@ async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
523
520
  return { step, path: target, status: "skipped", message: "up-to-date" };
524
521
  }
525
522
  await mkdir2(dirname2(target), { recursive: true });
526
- await atomicWriteJson2(target, merged, { indent: 2 });
523
+ await atomicWriteJson(target, merged, { indent: 2 });
527
524
  return { step, path: target, status: "written" };
528
525
  }
529
526
  async function readJsonObjectOrEmpty(path) {
530
527
  try {
531
- const raw = await readFile2(path, "utf8");
528
+ const raw = await readFile(path, "utf8");
532
529
  if (raw.trim().length === 0) {
533
530
  return {};
534
531
  }
@@ -549,7 +546,7 @@ function jsonEqual(a, b) {
549
546
  }
550
547
  async function readTemplate(relativePath) {
551
548
  const path = findTemplatePath(relativePath);
552
- return readFile2(path, "utf8");
549
+ return readFile(path, "utf8");
553
550
  }
554
551
  async function readJsonTemplate(relativePath) {
555
552
  const raw = await readTemplate(relativePath);
@@ -561,7 +558,7 @@ async function readJsonTemplate(relativePath) {
561
558
  }
562
559
  function findTemplatePath(relativePath) {
563
560
  const startDir = dirname2(fileURLToPath(import.meta.url));
564
- let current = resolve2(startDir);
561
+ let current = resolve(startDir);
565
562
  while (true) {
566
563
  const candidate = join2(current, "templates", relativePath);
567
564
  if (existsSync2(candidate)) {
@@ -576,19 +573,14 @@ function findTemplatePath(relativePath) {
576
573
  }
577
574
 
578
575
  export {
579
- createServerEntry,
580
- normalizeConfigPath,
581
- writeJsonClientConfig,
582
- removeJsonClientConfigEntry,
583
- ClaudeCodeCLIWriter,
584
- CursorWriter,
576
+ fabricAgentsSnapshotPath,
577
+ writeFabricAgentsSnapshot,
585
578
  SKILL_DESTINATIONS,
586
579
  HOOK_SCRIPT_DESTINATIONS,
580
+ HOOK_LIB_DESTINATIONS,
587
581
  HOOK_CONFIG_TARGETS,
588
582
  HOOK_CONFIG_ARRAY_PATHS,
589
583
  FABRIC_HOOK_COMMAND_PATHS,
590
- SECTION_TARGETS,
591
- FABRIC_SECTION_REGEX,
592
584
  readFabricLanguagePreference,
593
585
  installFabricArchiveSkill,
594
586
  installFabricReviewSkill,
@@ -596,8 +588,11 @@ export {
596
588
  installArchiveHintHook,
597
589
  installKnowledgeHintBroadHook,
598
590
  installKnowledgeHintNarrowHook,
591
+ installHookLibs,
599
592
  mergeClaudeCodeHookConfig,
600
593
  mergeCodexHookConfig,
601
594
  mergeCursorHookConfig,
602
- addFabricKnowledgeBaseSection
595
+ writeClaudeBootstrapThinShell,
596
+ writeCodexBootstrapManagedBlock,
597
+ writeCursorBootstrapManagedBlock
603
598
  };