@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.
- package/README.md +4 -2
- package/dist/{chunk-X7QPY5KH.js → chunk-4HC5ZK7H.js} +296 -301
- package/dist/{chunk-FDRLV5PL.js → chunk-FNO7CQDG.js} +5 -213
- package/dist/{chunk-WWNXR34K.js → chunk-G2CIOLD4.js} +16 -1
- package/dist/chunk-KZ2YITOS.js +225 -0
- package/dist/{chunk-OHWQNSLH.js → chunk-MF3OTILQ.js} +267 -44
- package/dist/{chunk-OBQU6NHO.js → chunk-ZSESMG6L.js} +0 -6
- package/dist/config-AYP5F72E.js +13 -0
- package/dist/doctor-L6TIXXIX.js +425 -0
- package/dist/index.js +11 -9
- package/dist/{install-SLS5W27W.js → install-DNZXGFHJ.js} +344 -359
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-CFDGXHCA.js} +10 -5
- package/dist/{serve-NGLXHDYC.js → serve-6PPQX7AW.js} +16 -11
- package/dist/{uninstall-JHUSFENL.js → uninstall-L2HEEOU3.js} +200 -215
- package/package.json +3 -3
- package/templates/hooks/configs/README.md +9 -5
- package/templates/hooks/configs/cursor-hooks.json +7 -10
- 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-Q72D24BG.js +0 -186
- package/dist/doctor-RILCO5OG.js +0 -282
- package/dist/hooks-HIWYI3VG.js +0 -13
- package/dist/scan-VHKZPT2W.js +0 -24
- 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
|
@@ -1,251 +1,63 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
deepMerge
|
|
4
|
+
} from "./chunk-MF3OTILQ.js";
|
|
2
5
|
|
|
3
|
-
// src/install/
|
|
4
|
-
import {
|
|
5
|
-
import { mkdir
|
|
6
|
-
import { dirname
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
30
|
-
|
|
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
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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: ["
|
|
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 =
|
|
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
|
-
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
|
404
|
+
existing = await readFile(target, "utf8");
|
|
462
405
|
} catch (error) {
|
|
463
|
-
|
|
464
|
-
step
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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 =
|
|
483
|
-
next = `${
|
|
484
|
-
${
|
|
431
|
+
const trailingNewline = stripped.endsWith("\n") ? "" : "\n";
|
|
432
|
+
next = `${stripped}${trailingNewline}
|
|
433
|
+
${managedBlock}
|
|
485
434
|
`;
|
|
486
435
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
493
|
-
results.push({ step: "section", path: target, status: "written" });
|
|
476
|
+
existing = await readFile(target, "utf8");
|
|
494
477
|
} catch (error) {
|
|
495
|
-
|
|
496
|
-
step
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
580
|
-
|
|
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
|
-
|
|
595
|
+
writeClaudeBootstrapThinShell,
|
|
596
|
+
writeCodexBootstrapManagedBlock,
|
|
597
|
+
writeCursorBootstrapManagedBlock
|
|
603
598
|
};
|