@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.
- package/README.md +6 -6
- package/dist/chunk-6ICJICVU.js +10 -0
- package/dist/chunk-AW3G7ZH5.js +576 -0
- package/dist/chunk-HQLEHH4O.js +321 -0
- package/dist/{chunk-UHNP7T7W.js → chunk-MT3R57VG.js} +346 -86
- package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
- package/dist/chunk-WPTA74BY.js +184 -0
- package/dist/chunk-WWNXR34K.js +49 -0
- package/dist/doctor-RILCO5OG.js +282 -0
- package/dist/hooks-NX32PPEN.js +13 -0
- package/dist/index.js +8 -5
- package/dist/{init-DRHUYHYA.js → init-SAVH4SKE.js} +188 -491
- package/dist/plan-context-hint-QMUPAXIB.js +98 -0
- package/dist/{scan-HU2EGITF.js → scan-ELSNCSKS.js} +4 -2
- package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
- package/dist/uninstall-DBAR2JBS.js +1082 -0
- package/package.json +3 -3
- package/templates/bootstrap/CLAUDE.md +1 -1
- package/templates/bootstrap/codex-AGENTS-header.md +1 -1
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
- package/templates/hooks/configs/README.md +73 -0
- package/templates/hooks/configs/claude-code.json +37 -0
- package/templates/hooks/configs/codex-hooks.json +20 -0
- package/templates/hooks/configs/cursor-hooks.json +20 -0
- package/templates/hooks/fabric-hint.cjs +1337 -0
- package/templates/hooks/knowledge-hint-broad.cjs +612 -0
- package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
- package/templates/hooks/lib/session-digest-writer.cjs +172 -0
- package/templates/skills/fabric-archive/SKILL.md +486 -0
- package/templates/skills/fabric-import/SKILL.md +560 -0
- package/templates/skills/fabric-review/SKILL.md +382 -0
- 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-
|
|
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
|
-
|
|
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-
|
|
37
|
+
} from "./chunk-6ICJICVU.js";
|
|
14
38
|
|
|
15
39
|
// src/commands/init.ts
|
|
16
40
|
import { randomUUID } from "crypto";
|
|
17
|
-
import { homedir
|
|
41
|
+
import { homedir } from "os";
|
|
18
42
|
import * as childProcess from "child_process";
|
|
19
|
-
import { appendFileSync, existsSync as
|
|
20
|
-
import { dirname
|
|
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
|
|
24
|
-
import { defineCommand as
|
|
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 {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
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 =
|
|
475
|
-
if (!
|
|
86
|
+
const configPath = resolve(workspaceRoot, "fabric.config.json");
|
|
87
|
+
if (!existsSync(configPath)) {
|
|
476
88
|
return {};
|
|
477
89
|
}
|
|
478
|
-
const parsed = JSON.parse(await
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
187
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
|
|
576
188
|
import { createRequire } from "module";
|
|
577
|
-
import { basename, extname, isAbsolute
|
|
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 =
|
|
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
|
|
700
|
-
return
|
|
311
|
+
function normalizeTarget(targetInput) {
|
|
312
|
+
return isAbsolute(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
|
|
701
313
|
}
|
|
702
314
|
function buildTopology(root) {
|
|
703
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
755
|
-
if (!
|
|
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 =
|
|
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 =
|
|
842
|
-
if (!
|
|
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 =
|
|
1183
|
-
const hasContributing =
|
|
1184
|
-
if (!
|
|
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 =
|
|
1670
|
-
if (
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 ??
|
|
1581
|
+
return process.env.FABRIC_HOME ?? homedir();
|
|
1933
1582
|
}
|
|
1934
1583
|
async function buildInitFabricPlan(target, options) {
|
|
1935
|
-
|
|
1936
|
-
const fabricDir =
|
|
1937
|
-
const agentsMdPath =
|
|
1938
|
-
const agentsMdAction =
|
|
1939
|
-
const knowledgeDir =
|
|
1940
|
-
const personalKnowledgeDir =
|
|
1941
|
-
const forensicPath =
|
|
1942
|
-
const eventsPath =
|
|
1943
|
-
const metaPath =
|
|
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 =
|
|
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
|
-
|
|
1978
|
-
|
|
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 =
|
|
1632
|
+
const teamSubDir = join2(plan.knowledgeDir, sub);
|
|
1983
1633
|
mkdirSync(teamSubDir, { recursive: true });
|
|
1984
|
-
const teamGitkeep =
|
|
1985
|
-
if (!
|
|
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(
|
|
1642
|
+
mkdirSync(join2(plan.personalKnowledgeDir, sub), { recursive: true });
|
|
1993
1643
|
}
|
|
1994
1644
|
} catch {
|
|
1995
1645
|
}
|
|
1996
1646
|
preparePlannedPath(plan.metaPath, plan.metaAction);
|
|
1997
|
-
await
|
|
1647
|
+
await atomicWriteJson(plan.metaPath, plan.meta);
|
|
1998
1648
|
if (isReapply) {
|
|
1999
|
-
if (!
|
|
2000
|
-
mkdirSync(
|
|
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
|
|
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
|
-
|
|
2143
|
-
|
|
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 (!
|
|
1851
|
+
if (!existsSync3(path)) {
|
|
2182
1852
|
return false;
|
|
2183
1853
|
}
|
|
2184
|
-
if (
|
|
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 (!
|
|
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(
|
|
2203
|
-
if (action === "overwritten" &&
|
|
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
|
|
2379
|
-
return
|
|
2048
|
+
function normalizeTarget2(targetInput) {
|
|
2049
|
+
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
2380
2050
|
}
|
|
2381
|
-
function
|
|
2382
|
-
if (!
|
|
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 =
|
|
2388
|
-
if (
|
|
2057
|
+
const workspaceRoot = resolve3(cwd);
|
|
2058
|
+
if (existsSync3(join2(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
2389
2059
|
return "pnpm";
|
|
2390
2060
|
}
|
|
2391
|
-
if (
|
|
2061
|
+
if (existsSync3(join2(workspaceRoot, "yarn.lock"))) {
|
|
2392
2062
|
return "yarn";
|
|
2393
2063
|
}
|
|
2394
|
-
if (
|
|
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);
|