@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0-rc.11
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-UHNP7T7W.js → chunk-5MQ52F42.js} +347 -86
- package/dist/chunk-6ICJICVU.js +10 -0
- package/dist/chunk-AW3G7ZH5.js +576 -0
- package/dist/chunk-HQLEHH4O.js +321 -0
- 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-C56PWHID.js} +225 -491
- package/dist/plan-context-hint-QMUPAXIB.js +98 -0
- package/dist/{scan-HU2EGITF.js → scan-66EKMNAY.js} +6 -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 +640 -0
- package/templates/skills/fabric-import/SKILL.md +850 -0
- package/templates/skills/fabric-review/SKILL.md +717 -0
- package/dist/doctor-DUHWLAYD.js +0 -98
|
@@ -1,447 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
hooksCommand,
|
|
4
|
+
installHooks
|
|
5
|
+
} from "./chunk-WPTA74BY.js";
|
|
6
|
+
import {
|
|
7
|
+
detectExistingLanguage,
|
|
3
8
|
detectFramework,
|
|
4
9
|
runInitScan
|
|
5
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5MQ52F42.js";
|
|
11
|
+
import {
|
|
12
|
+
detectClientSupports,
|
|
13
|
+
resolveClients
|
|
14
|
+
} from "./chunk-HQLEHH4O.js";
|
|
15
|
+
import {
|
|
16
|
+
addArchiveSkillPointer,
|
|
17
|
+
installArchiveHintHook,
|
|
18
|
+
installFabricArchiveSkill,
|
|
19
|
+
installFabricImportSkill,
|
|
20
|
+
installFabricReviewSkill,
|
|
21
|
+
installKnowledgeHintBroadHook,
|
|
22
|
+
installKnowledgeHintNarrowHook,
|
|
23
|
+
mergeClaudeCodeHookConfig,
|
|
24
|
+
mergeCodexHookConfig,
|
|
25
|
+
mergeCursorHookConfig
|
|
26
|
+
} from "./chunk-AW3G7ZH5.js";
|
|
6
27
|
import {
|
|
7
|
-
createDebugLogger,
|
|
8
28
|
displayWidth,
|
|
9
29
|
padEnd,
|
|
10
|
-
paint
|
|
11
|
-
|
|
30
|
+
paint
|
|
31
|
+
} from "./chunk-WWNXR34K.js";
|
|
32
|
+
import {
|
|
33
|
+
createDebugLogger,
|
|
34
|
+
resolveDevMode
|
|
35
|
+
} from "./chunk-OBQU6NHO.js";
|
|
36
|
+
import {
|
|
12
37
|
t
|
|
13
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-6ICJICVU.js";
|
|
14
39
|
|
|
15
40
|
// src/commands/init.ts
|
|
16
41
|
import { randomUUID } from "crypto";
|
|
17
|
-
import { homedir
|
|
42
|
+
import { homedir } from "os";
|
|
18
43
|
import * as childProcess from "child_process";
|
|
19
|
-
import { appendFileSync, existsSync as
|
|
20
|
-
import { dirname
|
|
44
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
|
|
45
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, resolve as resolve3 } from "path";
|
|
21
46
|
import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
|
|
22
47
|
import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
|
|
23
|
-
import { atomicWriteJson
|
|
24
|
-
import { defineCommand as
|
|
48
|
+
import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
49
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
25
50
|
import { checkLockOrThrow } from "@fenglimg/fabric-server";
|
|
26
51
|
|
|
27
52
|
// 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
53
|
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";
|
|
54
|
+
import { readFile } from "fs/promises";
|
|
55
|
+
import { resolve } from "path";
|
|
56
|
+
import { fileURLToPath } from "url";
|
|
403
57
|
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
58
|
var CLIENT_ALIASES = {
|
|
446
59
|
claude: "ClaudeCodeCLI",
|
|
447
60
|
claudecodecli: "ClaudeCodeCLI",
|
|
@@ -471,11 +84,11 @@ function parseClientFilter(value) {
|
|
|
471
84
|
return clients;
|
|
472
85
|
}
|
|
473
86
|
async function loadFabricConfig(workspaceRoot) {
|
|
474
|
-
const configPath =
|
|
475
|
-
if (!
|
|
87
|
+
const configPath = resolve(workspaceRoot, "fabric.config.json");
|
|
88
|
+
if (!existsSync(configPath)) {
|
|
476
89
|
return {};
|
|
477
90
|
}
|
|
478
|
-
const parsed = JSON.parse(await
|
|
91
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
479
92
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
480
93
|
throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
|
|
481
94
|
}
|
|
@@ -483,21 +96,21 @@ async function loadFabricConfig(workspaceRoot) {
|
|
|
483
96
|
}
|
|
484
97
|
function resolveServerPath(override) {
|
|
485
98
|
if (override) return override;
|
|
486
|
-
if (process.env.FAB_SERVER_PATH) return
|
|
99
|
+
if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
|
|
487
100
|
return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
|
|
488
101
|
}
|
|
489
102
|
function writeStderr(message) {
|
|
490
103
|
process.stderr.write(`${message}
|
|
491
104
|
`);
|
|
492
105
|
}
|
|
493
|
-
var configCmd =
|
|
106
|
+
var configCmd = defineCommand({
|
|
494
107
|
meta: {
|
|
495
108
|
name: "config",
|
|
496
109
|
description: t("cli.config.description")
|
|
497
110
|
},
|
|
498
111
|
subCommands: {
|
|
499
112
|
hooks: hooksCommand,
|
|
500
|
-
install:
|
|
113
|
+
install: defineCommand({
|
|
501
114
|
meta: {
|
|
502
115
|
name: "install",
|
|
503
116
|
description: t("cli.config.install.description")
|
|
@@ -541,7 +154,7 @@ var configCmd = defineCommand2({
|
|
|
541
154
|
}
|
|
542
155
|
});
|
|
543
156
|
async function installMcpClients(target, options = {}) {
|
|
544
|
-
const workspaceRoot =
|
|
157
|
+
const workspaceRoot = resolve(target);
|
|
545
158
|
const fabricConfig = await loadFabricConfig(workspaceRoot);
|
|
546
159
|
const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
|
|
547
160
|
const serverPath = resolveServerPath(options.localServerPath);
|
|
@@ -572,9 +185,9 @@ async function installMcpClients(target, options = {}) {
|
|
|
572
185
|
|
|
573
186
|
// src/scanner/forensic.ts
|
|
574
187
|
import { execFileSync } from "child_process";
|
|
575
|
-
import { existsSync as
|
|
188
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
|
|
576
189
|
import { createRequire } from "module";
|
|
577
|
-
import { basename, extname, isAbsolute
|
|
190
|
+
import { basename, extname, isAbsolute, join, posix, relative, resolve as resolve2, sep } from "path";
|
|
578
191
|
import {
|
|
579
192
|
forensicReportSchema
|
|
580
193
|
} from "@fenglimg/fabric-shared";
|
|
@@ -660,7 +273,7 @@ var parserInitPromise = null;
|
|
|
660
273
|
var languagePromiseByKind = {};
|
|
661
274
|
var parserBundlePromiseByKind = {};
|
|
662
275
|
async function buildForensicReport(targetInput) {
|
|
663
|
-
const target =
|
|
276
|
+
const target = normalizeTarget(targetInput);
|
|
664
277
|
const framework = detectFramework(target);
|
|
665
278
|
const topology = buildTopology(target);
|
|
666
279
|
const entryPoints = collectEntryPoints(target, topology.files);
|
|
@@ -696,11 +309,11 @@ async function buildForensicReport(targetInput) {
|
|
|
696
309
|
}
|
|
697
310
|
return validation.data;
|
|
698
311
|
}
|
|
699
|
-
function
|
|
700
|
-
return
|
|
312
|
+
function normalizeTarget(targetInput) {
|
|
313
|
+
return isAbsolute(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
|
|
701
314
|
}
|
|
702
315
|
function buildTopology(root) {
|
|
703
|
-
|
|
316
|
+
assertExistingDirectory(root);
|
|
704
317
|
const byExt = {};
|
|
705
318
|
const keyDirs = /* @__PURE__ */ new Set();
|
|
706
319
|
const files = [];
|
|
@@ -713,7 +326,7 @@ function buildTopology(root) {
|
|
|
713
326
|
continue;
|
|
714
327
|
}
|
|
715
328
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
716
|
-
const absolutePath =
|
|
329
|
+
const absolutePath = join(current, entry.name);
|
|
717
330
|
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
718
331
|
if (relativePath.length === 0) {
|
|
719
332
|
continue;
|
|
@@ -733,7 +346,7 @@ function buildTopology(root) {
|
|
|
733
346
|
if (!entry.isFile()) {
|
|
734
347
|
continue;
|
|
735
348
|
}
|
|
736
|
-
const stats =
|
|
349
|
+
const stats = statSync(absolutePath);
|
|
737
350
|
const extension = extname(entry.name) || "[none]";
|
|
738
351
|
byExt[extension] = (byExt[extension] ?? 0) + 1;
|
|
739
352
|
totalFiles += 1;
|
|
@@ -751,8 +364,8 @@ function buildTopology(root) {
|
|
|
751
364
|
files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
752
365
|
};
|
|
753
366
|
}
|
|
754
|
-
function
|
|
755
|
-
if (!
|
|
367
|
+
function assertExistingDirectory(target) {
|
|
368
|
+
if (!existsSync2(target) || !statSync(target).isDirectory()) {
|
|
756
369
|
throw new Error(`Target must be an existing directory: ${target}`);
|
|
757
370
|
}
|
|
758
371
|
}
|
|
@@ -801,7 +414,7 @@ function getEntryPointReason(relativePath) {
|
|
|
801
414
|
async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
|
|
802
415
|
const samples = [];
|
|
803
416
|
for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
|
|
804
|
-
const absolutePath =
|
|
417
|
+
const absolutePath = join(target, ...entryPoint.path.split("/"));
|
|
805
418
|
const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
|
|
806
419
|
const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
|
|
807
420
|
frameworkKind,
|
|
@@ -838,8 +451,8 @@ function readFirstLines(path, lineLimit) {
|
|
|
838
451
|
}
|
|
839
452
|
}
|
|
840
453
|
function readPackageDependencies(target) {
|
|
841
|
-
const packageJsonPath =
|
|
842
|
-
if (!
|
|
454
|
+
const packageJsonPath = join(target, "package.json");
|
|
455
|
+
if (!existsSync2(packageJsonPath)) {
|
|
843
456
|
return /* @__PURE__ */ new Map();
|
|
844
457
|
}
|
|
845
458
|
try {
|
|
@@ -1179,9 +792,9 @@ function scoreFrameworkConfidence(input) {
|
|
|
1179
792
|
return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
|
|
1180
793
|
}
|
|
1181
794
|
function readReadmeInfo(target) {
|
|
1182
|
-
const readmePath =
|
|
1183
|
-
const hasContributing =
|
|
1184
|
-
if (!
|
|
795
|
+
const readmePath = join(target, "README.md");
|
|
796
|
+
const hasContributing = existsSync2(join(target, "CONTRIBUTING.md"));
|
|
797
|
+
if (!existsSync2(readmePath)) {
|
|
1185
798
|
return {
|
|
1186
799
|
quality: "missing",
|
|
1187
800
|
line_count: 0,
|
|
@@ -1666,8 +1279,8 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
|
|
|
1666
1279
|
return recommendations;
|
|
1667
1280
|
}
|
|
1668
1281
|
function readProjectName(target) {
|
|
1669
|
-
const packageJsonPath =
|
|
1670
|
-
if (
|
|
1282
|
+
const packageJsonPath = join(target, "package.json");
|
|
1283
|
+
if (existsSync2(packageJsonPath)) {
|
|
1671
1284
|
try {
|
|
1672
1285
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1673
1286
|
if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
|
|
@@ -1680,7 +1293,7 @@ function readProjectName(target) {
|
|
|
1680
1293
|
return basename(target);
|
|
1681
1294
|
}
|
|
1682
1295
|
function getCliVersion() {
|
|
1683
|
-
return true ? "2.0.0-rc.
|
|
1296
|
+
return true ? "2.0.0-rc.11" : "unknown";
|
|
1684
1297
|
}
|
|
1685
1298
|
function sortRecord(record) {
|
|
1686
1299
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
@@ -1699,10 +1312,10 @@ Run \`fabric doctor\` to verify state.
|
|
|
1699
1312
|
|
|
1700
1313
|
See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
|
|
1701
1314
|
`;
|
|
1702
|
-
var LOCAL_FABRIC_SERVER_PATH =
|
|
1315
|
+
var LOCAL_FABRIC_SERVER_PATH = join2("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
|
|
1703
1316
|
var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
|
|
1704
1317
|
var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
|
|
1705
|
-
var initCommand =
|
|
1318
|
+
var initCommand = defineCommand2({
|
|
1706
1319
|
meta: {
|
|
1707
1320
|
name: "init",
|
|
1708
1321
|
description: t("cli.init.description")
|
|
@@ -1806,10 +1419,83 @@ async function runInitCommand(args) {
|
|
|
1806
1419
|
process.exitCode = 130;
|
|
1807
1420
|
return;
|
|
1808
1421
|
}
|
|
1809
|
-
|
|
1422
|
+
const result = await executeInitExecutionPlan(plan);
|
|
1423
|
+
if (!intent.options.planOnly) {
|
|
1424
|
+
console.log(paint.muted("More: docs/surfaces.md explains when to use CLI vs Skill vs MCP."));
|
|
1425
|
+
}
|
|
1426
|
+
return result;
|
|
1427
|
+
}
|
|
1428
|
+
function writeDefaultFabricConfig(fabricDir, targetRoot) {
|
|
1429
|
+
const target = join2(fabricDir, "fabric-config.json");
|
|
1430
|
+
if (existsSync3(target)) return;
|
|
1431
|
+
const detectedLanguage = detectExistingLanguage(targetRoot);
|
|
1432
|
+
const FABRIC_CONFIG_DEFAULTS = {
|
|
1433
|
+
// Scan/import language policy. Fixated at init time by probing
|
|
1434
|
+
// README.md + docs/*.md (CJK ratio > 0.3 → "zh-CN", else "en"). Users
|
|
1435
|
+
// can edit `.fabric/fabric-config.json` to override. See
|
|
1436
|
+
// packages/shared/src/schemas/fabric-config.ts for the enum.
|
|
1437
|
+
knowledge_language: detectedLanguage,
|
|
1438
|
+
// fabric-hint Stop hook Signal A (archive): time-branch threshold, hours
|
|
1439
|
+
// since last knowledge_proposed event.
|
|
1440
|
+
archive_hint_hours: 24,
|
|
1441
|
+
// fabric-hint Stop hook cooldown after ANY signal fires, in hours.
|
|
1442
|
+
archive_hint_cooldown_hours: 12,
|
|
1443
|
+
// fabric-hint Stop hook Signal B (review): pending-count cutoff.
|
|
1444
|
+
review_hint_pending_count: 10,
|
|
1445
|
+
// fabric-hint Stop hook Signal B (review): pending-age cutoff in days.
|
|
1446
|
+
review_hint_pending_age_days: 7,
|
|
1447
|
+
// fabric-hint Stop hook Signal D (maintenance): days since last doctor.
|
|
1448
|
+
maintenance_hint_days: 14,
|
|
1449
|
+
// fabric-hint Stop hook Signal D (maintenance): cooldown between
|
|
1450
|
+
// reminders, in days.
|
|
1451
|
+
maintenance_hint_cooldown_days: 7,
|
|
1452
|
+
// fabric-hint Stop hook Signal A (archive): edit-count branch threshold;
|
|
1453
|
+
// PreToolUse fires recorded in .fabric/.cache/edit-counter since the
|
|
1454
|
+
// last knowledge_proposed event.
|
|
1455
|
+
archive_edit_threshold: 20,
|
|
1456
|
+
// fabric-hint Stop hook Signal C (import) + doctor lint #22: canonical
|
|
1457
|
+
// knowledge node count below this value flags an underseeded workspace.
|
|
1458
|
+
underseed_node_threshold: 10,
|
|
1459
|
+
// rc.9+ (skill-contract-fix B1): fabric-import first-run git-history
|
|
1460
|
+
// window in months. Default 60 captures the bulk of a mature repo's
|
|
1461
|
+
// signal in one pass; lower to 12-24 for fresh / small repos.
|
|
1462
|
+
import_window_first_run_months: 60,
|
|
1463
|
+
// rc.9+ (skill-contract-fix B1): fabric-import rerun window in months.
|
|
1464
|
+
// Default 2; raise to 6 if the workspace pauses imports for long stretches.
|
|
1465
|
+
import_window_rerun_months: 2,
|
|
1466
|
+
// rc.9+ (skill-contract-fix B1): hard cap on pending entries produced
|
|
1467
|
+
// per fabric-import invocation. Default 10 matches one-sitting triage.
|
|
1468
|
+
import_max_pending_per_run: 10,
|
|
1469
|
+
// rc.9+ (skill-contract-fix B1): hard cap on commits scanned per
|
|
1470
|
+
// fabric-import invocation. Default 500 covers ~2 months of typical churn.
|
|
1471
|
+
import_max_commits_scan: 500,
|
|
1472
|
+
// rc.9+ (skill-contract-fix B1): canonical-node count above which
|
|
1473
|
+
// fabric-import suggests review over importing more. Default 50.
|
|
1474
|
+
import_skip_canonical_threshold: 50,
|
|
1475
|
+
// rc.9+ (skill-contract-fix B1): max candidates per fabric-archive batch.
|
|
1476
|
+
// Default 8 keeps each batch reviewable in one sitting.
|
|
1477
|
+
archive_max_candidates_per_batch: 8,
|
|
1478
|
+
// rc.9+ (skill-contract-fix B1): max recently-touched paths in
|
|
1479
|
+
// fabric-archive's relevance digest. Default 20.
|
|
1480
|
+
archive_max_recent_paths: 20,
|
|
1481
|
+
// rc.9+ (skill-contract-fix B1): max prior fabric-archive sessions
|
|
1482
|
+
// summarised in the digest the skill loads on start. Default 10.
|
|
1483
|
+
archive_digest_max_sessions: 10,
|
|
1484
|
+
// rc.9+ (skill-contract-fix B1): max review results per topic cluster
|
|
1485
|
+
// in fabric-review. Default 8.
|
|
1486
|
+
review_topic_result_cap: 8,
|
|
1487
|
+
// rc.9+ (skill-contract-fix B1): age (days) above which a pending entry
|
|
1488
|
+
// is considered stale by fabric-review. Default 14.
|
|
1489
|
+
review_stale_pending_days: 14
|
|
1490
|
+
};
|
|
1491
|
+
mkdirSync(fabricDir, { recursive: true });
|
|
1492
|
+
writeFileSync(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
|
|
1493
|
+
log.info(
|
|
1494
|
+
`Detected and fixated knowledge_language = ${detectedLanguage}; edit ${target} to override.`
|
|
1495
|
+
);
|
|
1810
1496
|
}
|
|
1811
1497
|
function resolveInitCliIntent(args, targetInput) {
|
|
1812
|
-
const target =
|
|
1498
|
+
const target = normalizeTarget2(targetInput);
|
|
1813
1499
|
const mcpInstallMode = resolveMcpInstallMode(args["mcp-install"]);
|
|
1814
1500
|
const claudeMcpScope = resolveClaudeMcpScope(args.scope);
|
|
1815
1501
|
const terminalInteractive = isInteractiveInit();
|
|
@@ -1929,20 +1615,20 @@ async function executeInitExecutionPlan(plan) {
|
|
|
1929
1615
|
}
|
|
1930
1616
|
var KNOWLEDGE_SUBDIRS = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
|
|
1931
1617
|
function resolvePersonalFabricRoot() {
|
|
1932
|
-
return process.env.FABRIC_HOME ??
|
|
1618
|
+
return process.env.FABRIC_HOME ?? homedir();
|
|
1933
1619
|
}
|
|
1934
1620
|
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 =
|
|
1621
|
+
assertExistingDirectory2(target);
|
|
1622
|
+
const fabricDir = join2(target, ".fabric");
|
|
1623
|
+
const agentsMdPath = join2(target, "AGENTS.md");
|
|
1624
|
+
const agentsMdAction = existsSync3(agentsMdPath) ? "preserved" : "created";
|
|
1625
|
+
const knowledgeDir = join2(fabricDir, "knowledge");
|
|
1626
|
+
const personalKnowledgeDir = join2(resolvePersonalFabricRoot(), ".fabric", "knowledge");
|
|
1627
|
+
const forensicPath = join2(fabricDir, "forensic.json");
|
|
1628
|
+
const eventsPath = join2(fabricDir, "events.jsonl");
|
|
1629
|
+
const metaPath = join2(fabricDir, "agents.meta.json");
|
|
1944
1630
|
const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
|
|
1945
|
-
const knowledgeDirAction =
|
|
1631
|
+
const knowledgeDirAction = existsSync3(knowledgeDir) ? "overwritten" : "created";
|
|
1946
1632
|
const metaAction = planFreshPath(metaPath, options);
|
|
1947
1633
|
const eventsAction = planFreshPath(eventsPath, options);
|
|
1948
1634
|
const forensicAction = planFreshPath(forensicPath, options);
|
|
@@ -1974,30 +1660,31 @@ async function executeInitFabricPlan(plan) {
|
|
|
1974
1660
|
rmSync(plan.fabricDir, { force: true });
|
|
1975
1661
|
}
|
|
1976
1662
|
mkdirSync(plan.fabricDir, { recursive: true });
|
|
1977
|
-
|
|
1978
|
-
|
|
1663
|
+
writeDefaultFabricConfig(plan.fabricDir, plan.target);
|
|
1664
|
+
if (plan.agentsMdAction === "created" && !existsSync3(plan.agentsMdPath)) {
|
|
1665
|
+
await atomicWriteText(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
|
|
1979
1666
|
}
|
|
1980
1667
|
mkdirSync(plan.knowledgeDir, { recursive: true });
|
|
1981
1668
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
1982
|
-
const teamSubDir =
|
|
1669
|
+
const teamSubDir = join2(plan.knowledgeDir, sub);
|
|
1983
1670
|
mkdirSync(teamSubDir, { recursive: true });
|
|
1984
|
-
const teamGitkeep =
|
|
1985
|
-
if (!
|
|
1671
|
+
const teamGitkeep = join2(teamSubDir, ".gitkeep");
|
|
1672
|
+
if (!existsSync3(teamGitkeep)) {
|
|
1986
1673
|
writeFileSync(teamGitkeep, "", "utf8");
|
|
1987
1674
|
}
|
|
1988
1675
|
}
|
|
1989
1676
|
try {
|
|
1990
1677
|
mkdirSync(plan.personalKnowledgeDir, { recursive: true });
|
|
1991
1678
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
1992
|
-
mkdirSync(
|
|
1679
|
+
mkdirSync(join2(plan.personalKnowledgeDir, sub), { recursive: true });
|
|
1993
1680
|
}
|
|
1994
1681
|
} catch {
|
|
1995
1682
|
}
|
|
1996
1683
|
preparePlannedPath(plan.metaPath, plan.metaAction);
|
|
1997
|
-
await
|
|
1684
|
+
await atomicWriteJson(plan.metaPath, plan.meta);
|
|
1998
1685
|
if (isReapply) {
|
|
1999
|
-
if (!
|
|
2000
|
-
mkdirSync(
|
|
1686
|
+
if (!existsSync3(plan.eventsPath)) {
|
|
1687
|
+
mkdirSync(dirname(plan.eventsPath), { recursive: true });
|
|
2001
1688
|
writeFileSync(plan.eventsPath, "", "utf8");
|
|
2002
1689
|
}
|
|
2003
1690
|
} else {
|
|
@@ -2005,7 +1692,7 @@ async function executeInitFabricPlan(plan) {
|
|
|
2005
1692
|
writeFileSync(plan.eventsPath, "", "utf8");
|
|
2006
1693
|
}
|
|
2007
1694
|
preparePlannedPath(plan.forensicPath, plan.forensicAction);
|
|
2008
|
-
await
|
|
1695
|
+
await atomicWriteJson(plan.forensicPath, plan.forensicReport);
|
|
2009
1696
|
if (!plan.options?.reapply) {
|
|
2010
1697
|
try {
|
|
2011
1698
|
await runInitScan(plan.target, { source: "init" });
|
|
@@ -2139,8 +1826,28 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
2139
1826
|
try {
|
|
2140
1827
|
switch (stage.name) {
|
|
2141
1828
|
case "bootstrap": {
|
|
2142
|
-
|
|
2143
|
-
|
|
1829
|
+
const installResults = [];
|
|
1830
|
+
installResults.push(...await runBestEffort("skill-install", () => installFabricArchiveSkill(plan.target)));
|
|
1831
|
+
installResults.push(...await runBestEffort("skill-review-install", () => installFabricReviewSkill(plan.target)));
|
|
1832
|
+
installResults.push(...await runBestEffort("skill-import-install", () => installFabricImportSkill(plan.target)));
|
|
1833
|
+
installResults.push(...await runBestEffort("hook-script", () => installArchiveHintHook(plan.target)));
|
|
1834
|
+
installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
|
|
1835
|
+
installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
|
|
1836
|
+
installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
|
|
1837
|
+
installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
|
|
1838
|
+
installResults.push(await runBestEffortSingle("cursor-hook-config", () => mergeCursorHookConfig(plan.target)));
|
|
1839
|
+
installResults.push(...await runBestEffort("pointer", () => addArchiveSkillPointer(plan.target)));
|
|
1840
|
+
const installedCount = installResults.filter((r) => r.status === "written").length;
|
|
1841
|
+
const skippedCount = installResults.filter((r) => r.status === "skipped").length;
|
|
1842
|
+
const errorCount = installResults.filter((r) => r.status === "error").length;
|
|
1843
|
+
for (const result of installResults) {
|
|
1844
|
+
if (result.status === "error") {
|
|
1845
|
+
writeStderr2(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
const note2 = errorCount > 0 ? `errors=${errorCount}` : void 0;
|
|
1849
|
+
console.log(formatInitStageResult("bootstrap", "completed", installedCount, skippedCount, note2));
|
|
1850
|
+
return { name: "bootstrap", disposition: "ran" };
|
|
2144
1851
|
}
|
|
2145
1852
|
case "mcp": {
|
|
2146
1853
|
if (stage.installMode === "local") {
|
|
@@ -2178,10 +1885,10 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
2178
1885
|
}
|
|
2179
1886
|
}
|
|
2180
1887
|
function shouldReplaceWritableDirectory(path, options) {
|
|
2181
|
-
if (!
|
|
1888
|
+
if (!existsSync3(path)) {
|
|
2182
1889
|
return false;
|
|
2183
1890
|
}
|
|
2184
|
-
if (
|
|
1891
|
+
if (statSync2(path).isDirectory()) {
|
|
2185
1892
|
return false;
|
|
2186
1893
|
}
|
|
2187
1894
|
if (!options?.force) {
|
|
@@ -2190,7 +1897,7 @@ function shouldReplaceWritableDirectory(path, options) {
|
|
|
2190
1897
|
return true;
|
|
2191
1898
|
}
|
|
2192
1899
|
function planFreshPath(path, options) {
|
|
2193
|
-
if (!
|
|
1900
|
+
if (!existsSync3(path)) {
|
|
2194
1901
|
return "created";
|
|
2195
1902
|
}
|
|
2196
1903
|
if (!options?.force) {
|
|
@@ -2199,8 +1906,8 @@ function planFreshPath(path, options) {
|
|
|
2199
1906
|
return "overwritten";
|
|
2200
1907
|
}
|
|
2201
1908
|
function preparePlannedPath(path, action) {
|
|
2202
|
-
mkdirSync(
|
|
2203
|
-
if (action === "overwritten" &&
|
|
1909
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1910
|
+
if (action === "overwritten" && existsSync3(path)) {
|
|
2204
1911
|
rmSync(path, { recursive: true, force: true });
|
|
2205
1912
|
}
|
|
2206
1913
|
}
|
|
@@ -2375,23 +2082,23 @@ function formatInitModeBadge(options) {
|
|
|
2375
2082
|
}
|
|
2376
2083
|
return t("cli.init.mode.badge.default");
|
|
2377
2084
|
}
|
|
2378
|
-
function
|
|
2379
|
-
return
|
|
2085
|
+
function normalizeTarget2(targetInput) {
|
|
2086
|
+
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
2380
2087
|
}
|
|
2381
|
-
function
|
|
2382
|
-
if (!
|
|
2088
|
+
function assertExistingDirectory2(target) {
|
|
2089
|
+
if (!existsSync3(target) || !statSync2(target).isDirectory()) {
|
|
2383
2090
|
throw new Error(`Target must be an existing directory: ${target}`);
|
|
2384
2091
|
}
|
|
2385
2092
|
}
|
|
2386
2093
|
function detectPackageManager(cwd) {
|
|
2387
|
-
const workspaceRoot =
|
|
2388
|
-
if (
|
|
2094
|
+
const workspaceRoot = resolve3(cwd);
|
|
2095
|
+
if (existsSync3(join2(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
2389
2096
|
return "pnpm";
|
|
2390
2097
|
}
|
|
2391
|
-
if (
|
|
2098
|
+
if (existsSync3(join2(workspaceRoot, "yarn.lock"))) {
|
|
2392
2099
|
return "yarn";
|
|
2393
2100
|
}
|
|
2394
|
-
if (
|
|
2101
|
+
if (existsSync3(join2(workspaceRoot, "package-lock.json"))) {
|
|
2395
2102
|
return "npm";
|
|
2396
2103
|
}
|
|
2397
2104
|
return "npm";
|
|
@@ -2431,6 +2138,32 @@ function appendReapplyLedgerEvent(eventsPath, payload) {
|
|
|
2431
2138
|
`;
|
|
2432
2139
|
appendFileSync(eventsPath, line, "utf8");
|
|
2433
2140
|
}
|
|
2141
|
+
async function runBestEffort(step, fn) {
|
|
2142
|
+
try {
|
|
2143
|
+
return await fn();
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
return [
|
|
2146
|
+
{
|
|
2147
|
+
step,
|
|
2148
|
+
path: "",
|
|
2149
|
+
status: "error",
|
|
2150
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2151
|
+
}
|
|
2152
|
+
];
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
async function runBestEffortSingle(step, fn) {
|
|
2156
|
+
try {
|
|
2157
|
+
return await fn();
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
return {
|
|
2160
|
+
step,
|
|
2161
|
+
path: "",
|
|
2162
|
+
status: "error",
|
|
2163
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2434
2167
|
function formatInitStageHeader(message) {
|
|
2435
2168
|
return `${nextLabel()} ${paint.muted(message)}`;
|
|
2436
2169
|
}
|
|
@@ -2485,6 +2218,7 @@ function printInitPlanSummary(target, options, mcpInstallMode, supports) {
|
|
|
2485
2218
|
console.log(` - ${target}/.fabric/agents.meta.json`);
|
|
2486
2219
|
console.log(` - ${target}/.fabric/events.jsonl`);
|
|
2487
2220
|
console.log(` - ${target}/.fabric/forensic.json`);
|
|
2221
|
+
console.log(` - ${target}/.fabric/fabric-config.json`);
|
|
2488
2222
|
}
|
|
2489
2223
|
function printInitCapabilitySummary(supports, stageResults, options) {
|
|
2490
2224
|
const detected = supports.filter((support) => support.detected);
|