@fenglimg/fabric-cli 2.0.0-rc.15 → 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.
- package/dist/chunk-4HC5ZK7H.js +598 -0
- package/dist/{chunk-AXIFEVAS.js → chunk-FNO7CQDG.js} +1 -1
- package/dist/chunk-KZ2YITOS.js +225 -0
- package/dist/{chunk-SKSYUHKK.js → chunk-MF3OTILQ.js} +0 -4
- package/dist/{chunk-OBQU6NHO.js → chunk-ZSESMG6L.js} +0 -6
- package/dist/{config-7YD365I3.js → config-AYP5F72E.js} +2 -2
- package/dist/{doctor-6XHLQJXB.js → doctor-L6TIXXIX.js} +129 -3
- package/dist/index.js +11 -8
- package/dist/{install-JLDCHAXV.js → install-DNZXGFHJ.js} +23 -25
- package/dist/{plan-context-hint-73U4FGKO.js → plan-context-hint-CFDGXHCA.js} +4 -4
- package/dist/{serve-L3X5UHG2.js → serve-6PPQX7AW.js} +1 -1
- package/dist/{uninstall-DD6FIFCI.js → uninstall-L2HEEOU3.js} +147 -55
- package/package.json +3 -3
- package/templates/hooks/fabric-hint.cjs +350 -21
- package/templates/hooks/knowledge-hint-broad.cjs +39 -14
- package/templates/hooks/knowledge-hint-narrow.cjs +31 -7
- package/templates/hooks/lib/banner-i18n.cjs +252 -0
- package/dist/chunk-AIB54QRT.js +0 -82
- package/dist/chunk-UTF4YBDN.js +0 -366
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
deepMerge
|
|
4
|
+
} from "./chunk-MF3OTILQ.js";
|
|
5
|
+
|
|
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);
|
|
16
|
+
}
|
|
17
|
+
function projectRulesPath(targetRoot) {
|
|
18
|
+
return join(targetRoot, PROJECT_RULES_RELPATH);
|
|
19
|
+
}
|
|
20
|
+
function readProjectRulesIfPresent(targetRoot) {
|
|
21
|
+
const path = projectRulesPath(targetRoot);
|
|
22
|
+
if (!existsSync(path)) return null;
|
|
23
|
+
return readFileSync(path, "utf8");
|
|
24
|
+
}
|
|
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" };
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await mkdir(dirname(target), { recursive: true });
|
|
38
|
+
await atomicWriteText(target, BOOTSTRAP_CANONICAL);
|
|
39
|
+
return { step, path: target, status: "written" };
|
|
40
|
+
}
|
|
41
|
+
|
|
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";
|
|
54
|
+
var SKILL_TEMPLATE_REL = "skills/fabric-archive/SKILL.md";
|
|
55
|
+
var SKILL_REVIEW_TEMPLATE_REL = "skills/fabric-review/SKILL.md";
|
|
56
|
+
var SKILL_IMPORT_TEMPLATE_REL = "skills/fabric-import/SKILL.md";
|
|
57
|
+
var HOOK_SCRIPT_TEMPLATE_REL = "hooks/fabric-hint.cjs";
|
|
58
|
+
var HOOK_BROAD_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-broad.cjs";
|
|
59
|
+
var HOOK_NARROW_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-narrow.cjs";
|
|
60
|
+
var HOOK_LIB_TEMPLATE_DIR_REL = "hooks/lib";
|
|
61
|
+
var CLAUDE_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/claude-code.json";
|
|
62
|
+
var CODEX_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/codex-hooks.json";
|
|
63
|
+
var CURSOR_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/cursor-hooks.json";
|
|
64
|
+
var SKILL_DESTINATIONS = {
|
|
65
|
+
fabricArchive: [
|
|
66
|
+
".claude/skills/fabric-archive/SKILL.md",
|
|
67
|
+
".codex/skills/fabric-archive/SKILL.md"
|
|
68
|
+
],
|
|
69
|
+
fabricReview: [
|
|
70
|
+
".claude/skills/fabric-review/SKILL.md",
|
|
71
|
+
".codex/skills/fabric-review/SKILL.md"
|
|
72
|
+
],
|
|
73
|
+
fabricImport: [
|
|
74
|
+
".claude/skills/fabric-import/SKILL.md",
|
|
75
|
+
".codex/skills/fabric-import/SKILL.md"
|
|
76
|
+
]
|
|
77
|
+
};
|
|
78
|
+
var HOOK_SCRIPT_DESTINATIONS = {
|
|
79
|
+
fabricHint: [
|
|
80
|
+
".claude/hooks/fabric-hint.cjs",
|
|
81
|
+
".codex/hooks/fabric-hint.cjs",
|
|
82
|
+
".cursor/hooks/fabric-hint.cjs"
|
|
83
|
+
],
|
|
84
|
+
knowledgeHintBroad: [
|
|
85
|
+
".claude/hooks/knowledge-hint-broad.cjs",
|
|
86
|
+
".codex/hooks/knowledge-hint-broad.cjs",
|
|
87
|
+
".cursor/hooks/knowledge-hint-broad.cjs"
|
|
88
|
+
],
|
|
89
|
+
knowledgeHintNarrow: [
|
|
90
|
+
".claude/hooks/knowledge-hint-narrow.cjs",
|
|
91
|
+
".codex/hooks/knowledge-hint-narrow.cjs",
|
|
92
|
+
".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
var HOOK_LIB_DESTINATIONS = [
|
|
96
|
+
".claude/hooks/lib",
|
|
97
|
+
".codex/hooks/lib",
|
|
98
|
+
".cursor/hooks/lib"
|
|
99
|
+
];
|
|
100
|
+
var HOOK_CONFIG_TARGETS = {
|
|
101
|
+
claudeCode: ".claude/settings.json",
|
|
102
|
+
codex: ".codex/hooks.json",
|
|
103
|
+
cursor: ".cursor/hooks.json"
|
|
104
|
+
};
|
|
105
|
+
var HOOK_CONFIG_ARRAY_PATHS = {
|
|
106
|
+
claudeCode: ["hooks.Stop", "hooks.SessionStart", "hooks.PreToolUse"],
|
|
107
|
+
codex: ["events.Stop", "events.SessionStart", "events.PreToolUse"],
|
|
108
|
+
cursor: ["hooks.stop", "hooks.sessionStart", "hooks.preToolUse"]
|
|
109
|
+
};
|
|
110
|
+
var FABRIC_HOOK_COMMAND_PATHS = {
|
|
111
|
+
claudeCode: {
|
|
112
|
+
fabricHint: ".claude/hooks/fabric-hint.cjs",
|
|
113
|
+
knowledgeHintBroad: ".claude/hooks/knowledge-hint-broad.cjs",
|
|
114
|
+
knowledgeHintNarrow: ".claude/hooks/knowledge-hint-narrow.cjs"
|
|
115
|
+
},
|
|
116
|
+
codex: {
|
|
117
|
+
fabricHint: ".codex/hooks/fabric-hint.cjs",
|
|
118
|
+
knowledgeHintBroad: ".codex/hooks/knowledge-hint-broad.cjs",
|
|
119
|
+
knowledgeHintNarrow: ".codex/hooks/knowledge-hint-narrow.cjs"
|
|
120
|
+
},
|
|
121
|
+
cursor: {
|
|
122
|
+
fabricHint: ".cursor/hooks/fabric-hint.cjs",
|
|
123
|
+
knowledgeHintBroad: ".cursor/hooks/knowledge-hint-broad.cjs",
|
|
124
|
+
knowledgeHintNarrow: ".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
function readFabricLanguagePreference(projectRoot) {
|
|
128
|
+
const configPath = join2(projectRoot, ".fabric", "fabric-config.json");
|
|
129
|
+
if (!existsSync2(configPath)) {
|
|
130
|
+
return "match-existing";
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
136
|
+
return "match-existing";
|
|
137
|
+
}
|
|
138
|
+
const value = parsed["fabric_language"];
|
|
139
|
+
return typeof value === "string" && value.length > 0 ? value : "match-existing";
|
|
140
|
+
} catch {
|
|
141
|
+
return "match-existing";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function installFabricArchiveSkill(projectRoot, _options = {}) {
|
|
145
|
+
const source = await readTemplate(SKILL_TEMPLATE_REL);
|
|
146
|
+
const targets = SKILL_DESTINATIONS.fabricArchive.map((rel) => join2(projectRoot, rel));
|
|
147
|
+
const results = [];
|
|
148
|
+
for (const target of targets) {
|
|
149
|
+
results.push(await copyTextIdempotent("skill", source, target));
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
async function installFabricReviewSkill(projectRoot, _options = {}) {
|
|
154
|
+
const source = await readTemplate(SKILL_REVIEW_TEMPLATE_REL);
|
|
155
|
+
const targets = SKILL_DESTINATIONS.fabricReview.map((rel) => join2(projectRoot, rel));
|
|
156
|
+
const results = [];
|
|
157
|
+
for (const target of targets) {
|
|
158
|
+
results.push(await copyTextIdempotent("skill-review", source, target));
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
async function installFabricImportSkill(projectRoot, _options = {}) {
|
|
163
|
+
const source = await readTemplate(SKILL_IMPORT_TEMPLATE_REL);
|
|
164
|
+
const targets = SKILL_DESTINATIONS.fabricImport.map((rel) => join2(projectRoot, rel));
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const target of targets) {
|
|
167
|
+
results.push(await copyTextIdempotent("skill-import", source, target));
|
|
168
|
+
}
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
async function installArchiveHintHook(projectRoot, _options = {}) {
|
|
172
|
+
const source = await readTemplate(HOOK_SCRIPT_TEMPLATE_REL);
|
|
173
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.fabricHint.map((rel) => join2(projectRoot, rel));
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const target of targets) {
|
|
176
|
+
const result = await copyTextIdempotent("hook-script", source, target);
|
|
177
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
178
|
+
try {
|
|
179
|
+
chmodSync(target, 493);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
results.push(result);
|
|
184
|
+
}
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
187
|
+
async function installKnowledgeHintBroadHook(projectRoot, _options = {}) {
|
|
188
|
+
const source = await readTemplate(HOOK_BROAD_SCRIPT_TEMPLATE_REL);
|
|
189
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.knowledgeHintBroad.map((rel) => join2(projectRoot, rel));
|
|
190
|
+
const results = [];
|
|
191
|
+
for (const target of targets) {
|
|
192
|
+
const result = await copyTextIdempotent("hook-broad-script", source, target);
|
|
193
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
194
|
+
try {
|
|
195
|
+
chmodSync(target, 493);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
results.push(result);
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
async function installKnowledgeHintNarrowHook(projectRoot, _options = {}) {
|
|
204
|
+
const source = await readTemplate(HOOK_NARROW_SCRIPT_TEMPLATE_REL);
|
|
205
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.knowledgeHintNarrow.map((rel) => join2(projectRoot, rel));
|
|
206
|
+
const results = [];
|
|
207
|
+
for (const target of targets) {
|
|
208
|
+
const result = await copyTextIdempotent("hook-narrow-script", source, target);
|
|
209
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
210
|
+
try {
|
|
211
|
+
chmodSync(target, 493);
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
results.push(result);
|
|
216
|
+
}
|
|
217
|
+
return results;
|
|
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
|
+
}
|
|
266
|
+
async function mergeClaudeCodeHookConfig(projectRoot, _options = {}) {
|
|
267
|
+
const fragment = await readJsonTemplate(CLAUDE_HOOK_CONFIG_TEMPLATE_REL);
|
|
268
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.claudeCode);
|
|
269
|
+
return mergeJsonIdempotent(
|
|
270
|
+
"claude-hook-config",
|
|
271
|
+
targetPath,
|
|
272
|
+
fragment,
|
|
273
|
+
[...HOOK_CONFIG_ARRAY_PATHS.claudeCode]
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
async function mergeCodexHookConfig(projectRoot, _options = {}) {
|
|
277
|
+
const fragment = await readJsonTemplate(CODEX_HOOK_CONFIG_TEMPLATE_REL);
|
|
278
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.codex);
|
|
279
|
+
return mergeJsonIdempotent(
|
|
280
|
+
"codex-hook-config",
|
|
281
|
+
targetPath,
|
|
282
|
+
fragment,
|
|
283
|
+
[...HOOK_CONFIG_ARRAY_PATHS.codex]
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
async function mergeCursorHookConfig(projectRoot, _options = {}) {
|
|
287
|
+
const fragment = await readJsonTemplate(CURSOR_HOOK_CONFIG_TEMPLATE_REL);
|
|
288
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.cursor);
|
|
289
|
+
return mergeJsonIdempotent(
|
|
290
|
+
"cursor-hook-config",
|
|
291
|
+
targetPath,
|
|
292
|
+
fragment,
|
|
293
|
+
[...HOOK_CONFIG_ARRAY_PATHS.cursor]
|
|
294
|
+
);
|
|
295
|
+
}
|
|
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
|
+
};
|
|
339
|
+
}
|
|
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)) {
|
|
403
|
+
try {
|
|
404
|
+
existing = await readFile(target, "utf8");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
step,
|
|
408
|
+
path: target,
|
|
409
|
+
status: "error",
|
|
410
|
+
message: error instanceof Error ? error.message : String(error)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
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}
|
|
429
|
+
`;
|
|
430
|
+
} else {
|
|
431
|
+
const trailingNewline = stripped.endsWith("\n") ? "" : "\n";
|
|
432
|
+
next = `${stripped}${trailingNewline}
|
|
433
|
+
${managedBlock}
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
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
|
+
}
|
|
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)) {
|
|
475
|
+
try {
|
|
476
|
+
existing = await readFile(target, "utf8");
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return {
|
|
479
|
+
step,
|
|
480
|
+
path: target,
|
|
481
|
+
status: "error",
|
|
482
|
+
message: error instanceof Error ? error.message : String(error)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
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
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function copyTextIdempotent(step, source, target) {
|
|
503
|
+
if (existsSync2(target)) {
|
|
504
|
+
try {
|
|
505
|
+
const existing = readFileSync2(target, "utf8");
|
|
506
|
+
if (existing === source) {
|
|
507
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
508
|
+
}
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
513
|
+
await atomicWriteText2(target, source);
|
|
514
|
+
return { step, path: target, status: "written" };
|
|
515
|
+
}
|
|
516
|
+
async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
|
|
517
|
+
const existing = await readJsonObjectOrEmpty(target);
|
|
518
|
+
const merged = deepMerge(existing, fragment, { arrayAppendPaths });
|
|
519
|
+
if (jsonEqual(existing, merged)) {
|
|
520
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
521
|
+
}
|
|
522
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
523
|
+
await atomicWriteJson(target, merged, { indent: 2 });
|
|
524
|
+
return { step, path: target, status: "written" };
|
|
525
|
+
}
|
|
526
|
+
async function readJsonObjectOrEmpty(path) {
|
|
527
|
+
try {
|
|
528
|
+
const raw = await readFile(path, "utf8");
|
|
529
|
+
if (raw.trim().length === 0) {
|
|
530
|
+
return {};
|
|
531
|
+
}
|
|
532
|
+
const parsed = JSON.parse(raw);
|
|
533
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
return parsed;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function jsonEqual(a, b) {
|
|
545
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
546
|
+
}
|
|
547
|
+
async function readTemplate(relativePath) {
|
|
548
|
+
const path = findTemplatePath(relativePath);
|
|
549
|
+
return readFile(path, "utf8");
|
|
550
|
+
}
|
|
551
|
+
async function readJsonTemplate(relativePath) {
|
|
552
|
+
const raw = await readTemplate(relativePath);
|
|
553
|
+
const parsed = JSON.parse(raw);
|
|
554
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
555
|
+
throw new Error(`Template at ${relativePath} is not a JSON object`);
|
|
556
|
+
}
|
|
557
|
+
return parsed;
|
|
558
|
+
}
|
|
559
|
+
function findTemplatePath(relativePath) {
|
|
560
|
+
const startDir = dirname2(fileURLToPath(import.meta.url));
|
|
561
|
+
let current = resolve(startDir);
|
|
562
|
+
while (true) {
|
|
563
|
+
const candidate = join2(current, "templates", relativePath);
|
|
564
|
+
if (existsSync2(candidate)) {
|
|
565
|
+
return candidate;
|
|
566
|
+
}
|
|
567
|
+
const parent = dirname2(current);
|
|
568
|
+
if (parent === current || parse(current).root === current) {
|
|
569
|
+
throw new Error(`Template not found: templates/${relativePath} (searched up from ${startDir})`);
|
|
570
|
+
}
|
|
571
|
+
current = parent;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export {
|
|
576
|
+
fabricAgentsSnapshotPath,
|
|
577
|
+
writeFabricAgentsSnapshot,
|
|
578
|
+
SKILL_DESTINATIONS,
|
|
579
|
+
HOOK_SCRIPT_DESTINATIONS,
|
|
580
|
+
HOOK_LIB_DESTINATIONS,
|
|
581
|
+
HOOK_CONFIG_TARGETS,
|
|
582
|
+
HOOK_CONFIG_ARRAY_PATHS,
|
|
583
|
+
FABRIC_HOOK_COMMAND_PATHS,
|
|
584
|
+
readFabricLanguagePreference,
|
|
585
|
+
installFabricArchiveSkill,
|
|
586
|
+
installFabricReviewSkill,
|
|
587
|
+
installFabricImportSkill,
|
|
588
|
+
installArchiveHintHook,
|
|
589
|
+
installKnowledgeHintBroadHook,
|
|
590
|
+
installKnowledgeHintNarrowHook,
|
|
591
|
+
installHookLibs,
|
|
592
|
+
mergeClaudeCodeHookConfig,
|
|
593
|
+
mergeCodexHookConfig,
|
|
594
|
+
mergeCursorHookConfig,
|
|
595
|
+
writeClaudeBootstrapThinShell,
|
|
596
|
+
writeCodexBootstrapManagedBlock,
|
|
597
|
+
writeCursorBootstrapManagedBlock
|
|
598
|
+
};
|