@fenglimg/fabric-cli 2.0.0-rc.13 → 2.0.0-rc.15
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-AIB54QRT.js +82 -0
- package/dist/{chunk-FDRLV5PL.js → chunk-AXIFEVAS.js} +5 -213
- package/dist/{chunk-WWNXR34K.js → chunk-G2CIOLD4.js} +16 -1
- package/dist/{chunk-OHWQNSLH.js → chunk-SKSYUHKK.js} +267 -40
- package/dist/{chunk-X7QPY5KH.js → chunk-UTF4YBDN.js} +34 -271
- package/dist/config-7YD365I3.js +13 -0
- package/dist/{doctor-RILCO5OG.js → doctor-6XHLQJXB.js} +67 -50
- package/dist/index.js +7 -8
- package/dist/{install-SLS5W27W.js → install-JLDCHAXV.js} +328 -341
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-73U4FGKO.js} +6 -1
- package/dist/{serve-NGLXHDYC.js → serve-L3X5UHG2.js} +15 -10
- package/dist/{uninstall-JHUSFENL.js → uninstall-DD6FIFCI.js} +56 -163
- package/package.json +3 -3
- package/templates/hooks/configs/README.md +9 -5
- package/templates/hooks/configs/cursor-hooks.json +7 -10
- package/dist/chunk-Q72D24BG.js +0 -186
- package/dist/hooks-HIWYI3VG.js +0 -13
- package/dist/scan-VHKZPT2W.js +0 -24
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./chunk-Q72D24BG.js";
|
|
3
|
+
installMcpClients
|
|
4
|
+
} from "./chunk-AIB54QRT.js";
|
|
6
5
|
import {
|
|
7
6
|
detectExistingLanguage,
|
|
8
|
-
detectFramework,
|
|
9
7
|
runInitScan
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import {
|
|
12
|
-
detectClientSupports,
|
|
13
|
-
resolveClients
|
|
14
|
-
} from "./chunk-OHWQNSLH.js";
|
|
8
|
+
} from "./chunk-AXIFEVAS.js";
|
|
15
9
|
import {
|
|
16
10
|
addFabricKnowledgeBaseSection,
|
|
17
11
|
installArchiveHintHook,
|
|
@@ -24,174 +18,178 @@ import {
|
|
|
24
18
|
mergeCodexHookConfig,
|
|
25
19
|
mergeCursorHookConfig,
|
|
26
20
|
readFabricLanguagePreference
|
|
27
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-UTF4YBDN.js";
|
|
22
|
+
import {
|
|
23
|
+
detectClientSupports
|
|
24
|
+
} from "./chunk-SKSYUHKK.js";
|
|
28
25
|
import {
|
|
29
26
|
displayWidth,
|
|
27
|
+
hasActionHint,
|
|
30
28
|
padEnd,
|
|
31
|
-
paint
|
|
32
|
-
|
|
29
|
+
paint,
|
|
30
|
+
renderFabricError
|
|
31
|
+
} from "./chunk-G2CIOLD4.js";
|
|
32
|
+
import {
|
|
33
|
+
t
|
|
34
|
+
} from "./chunk-6ICJICVU.js";
|
|
33
35
|
import {
|
|
34
36
|
createDebugLogger,
|
|
35
37
|
resolveDevMode
|
|
36
38
|
} from "./chunk-OBQU6NHO.js";
|
|
37
|
-
import {
|
|
38
|
-
t
|
|
39
|
-
} from "./chunk-6ICJICVU.js";
|
|
40
39
|
|
|
41
40
|
// src/commands/install.ts
|
|
42
41
|
import { randomUUID } from "crypto";
|
|
43
42
|
import { homedir } from "os";
|
|
44
43
|
import * as childProcess from "child_process";
|
|
45
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync, rmSync, statSync as
|
|
46
|
-
import { dirname, isAbsolute as
|
|
44
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync3, writeFileSync } from "fs";
|
|
45
|
+
import { dirname, isAbsolute as isAbsolute3, join as join3, resolve as resolve3 } from "path";
|
|
47
46
|
import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
|
|
48
47
|
import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
|
|
49
48
|
import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
50
|
-
import { defineCommand
|
|
49
|
+
import { defineCommand } from "citty";
|
|
51
50
|
import { checkLockOrThrow } from "@fenglimg/fabric-server";
|
|
52
51
|
|
|
53
|
-
// src/
|
|
54
|
-
import { existsSync } from "fs";
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
52
|
+
// src/install/hooks-orchestrator.ts
|
|
53
|
+
import { existsSync, statSync } from "fs";
|
|
54
|
+
import { isAbsolute, join, resolve } from "path";
|
|
55
|
+
async function installHooks(target, _options = {}) {
|
|
56
|
+
const normalizedTarget = normalizeTarget(target);
|
|
57
|
+
assertExistingDirectory(normalizedTarget);
|
|
58
|
+
const results = [];
|
|
59
|
+
results.push(...await runStep(() => installFabricArchiveSkill(normalizedTarget)));
|
|
60
|
+
results.push(...await runStep(() => installFabricReviewSkill(normalizedTarget)));
|
|
61
|
+
results.push(...await runStep(() => installFabricImportSkill(normalizedTarget)));
|
|
62
|
+
results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
|
|
63
|
+
results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
|
|
64
|
+
results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
|
|
65
|
+
results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
|
|
66
|
+
results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
|
|
67
|
+
results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
|
|
68
|
+
const fabricLanguage = readFabricLanguagePreference(normalizedTarget);
|
|
69
|
+
results.push(...await runStep(() => addFabricKnowledgeBaseSection(normalizedTarget, fabricLanguage)));
|
|
70
|
+
results.push(...validateHookPaths(normalizedTarget));
|
|
71
|
+
return summarizeResults(results);
|
|
72
|
+
}
|
|
73
|
+
function validateHookPaths(projectRoot) {
|
|
74
|
+
const scripts = [
|
|
75
|
+
{ stepSuffix: "", hookFile: "fabric-hint.cjs" },
|
|
76
|
+
{ stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
|
|
77
|
+
{ stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
|
|
78
|
+
];
|
|
79
|
+
const clients = [
|
|
80
|
+
{
|
|
81
|
+
client: "claude",
|
|
82
|
+
configRel: join(".claude", "settings.json"),
|
|
83
|
+
hookDir: join(".claude", "hooks")
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
client: "codex",
|
|
87
|
+
configRel: join(".codex", "hooks.json"),
|
|
88
|
+
hookDir: join(".codex", "hooks")
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
client: "cursor",
|
|
92
|
+
configRel: join(".cursor", "hooks.json"),
|
|
93
|
+
hookDir: join(".cursor", "hooks")
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const { client, configRel, hookDir } of clients) {
|
|
98
|
+
const configPath = resolve(projectRoot, configRel);
|
|
99
|
+
if (!existsSync(configPath)) {
|
|
100
|
+
results.push({
|
|
101
|
+
step: `hook-validate-${client}`,
|
|
102
|
+
path: configPath,
|
|
103
|
+
status: "skipped",
|
|
104
|
+
message: "missing-config"
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const { stepSuffix, hookFile } of scripts) {
|
|
109
|
+
const expectedHookPath = resolve(projectRoot, hookDir, hookFile);
|
|
110
|
+
const expectedHookRel = join(hookDir, hookFile);
|
|
111
|
+
const step = `hook-validate-${client}${stepSuffix}`;
|
|
112
|
+
if (!existsSync(expectedHookPath)) {
|
|
113
|
+
results.push({
|
|
114
|
+
step,
|
|
115
|
+
path: expectedHookPath,
|
|
116
|
+
status: "error",
|
|
117
|
+
message: `hook script missing: ${expectedHookRel}`
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
|
|
82
122
|
}
|
|
83
|
-
clients.add(clientKind);
|
|
84
123
|
}
|
|
85
|
-
return
|
|
124
|
+
return results;
|
|
86
125
|
}
|
|
87
|
-
async function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
async function runStep(fn) {
|
|
127
|
+
try {
|
|
128
|
+
return await fn();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
step: "hook-install",
|
|
133
|
+
path: "",
|
|
134
|
+
status: "error",
|
|
135
|
+
message: error instanceof Error ? error.message : String(error)
|
|
136
|
+
}
|
|
137
|
+
];
|
|
95
138
|
}
|
|
96
|
-
return parsed;
|
|
97
|
-
}
|
|
98
|
-
function resolveServerPath(override) {
|
|
99
|
-
if (override) return override;
|
|
100
|
-
if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
|
|
101
|
-
return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
|
|
102
|
-
}
|
|
103
|
-
function writeStderr(message) {
|
|
104
|
-
process.stderr.write(`${message}
|
|
105
|
-
`);
|
|
106
139
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
description: t("cli.config.install.description")
|
|
118
|
-
},
|
|
119
|
-
args: {
|
|
120
|
-
clients: {
|
|
121
|
-
type: "string",
|
|
122
|
-
description: t("cli.config.install.args.clients.description")
|
|
123
|
-
},
|
|
124
|
-
"dry-run": {
|
|
125
|
-
type: "boolean",
|
|
126
|
-
description: t("cli.config.install.args.dry-run.description"),
|
|
127
|
-
default: false
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
async run({ args }) {
|
|
131
|
-
const selectedClients = parseClientFilter(args.clients);
|
|
132
|
-
const result = await installMcpClients(process.cwd(), {
|
|
133
|
-
clients: selectedClients === null ? void 0 : Array.from(selectedClients),
|
|
134
|
-
dryRun: args["dry-run"]
|
|
135
|
-
});
|
|
136
|
-
if (result.details.length === 0) {
|
|
137
|
-
writeStderr(t("cli.config.install.no-configs"));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
for (const detail of result.details) {
|
|
141
|
-
if (detail.action === "skipped") {
|
|
142
|
-
writeStderr(t("cli.config.install.no-config-path", { client: detail.client }));
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (detail.action === "dry-run" && detail.path !== null) {
|
|
146
|
-
writeStderr(t("cli.config.install.dry-run", { client: detail.client, path: detail.path }));
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (detail.path !== null) {
|
|
150
|
-
writeStderr(t("cli.config.install.wrote", { client: detail.client, path: detail.path }));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
})
|
|
140
|
+
async function runSingleStep(step, fn) {
|
|
141
|
+
try {
|
|
142
|
+
return await fn();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
step,
|
|
146
|
+
path: "",
|
|
147
|
+
status: "error",
|
|
148
|
+
message: error instanceof Error ? error.message : String(error)
|
|
149
|
+
};
|
|
155
150
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const workspaceRoot = resolve(target);
|
|
159
|
-
const fabricConfig = await loadFabricConfig(workspaceRoot);
|
|
160
|
-
const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
|
|
161
|
-
const serverPath = resolveServerPath(options.localServerPath);
|
|
162
|
-
const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
|
|
163
|
-
(writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
|
|
164
|
-
);
|
|
151
|
+
}
|
|
152
|
+
function summarizeResults(results) {
|
|
165
153
|
const installed = [];
|
|
166
154
|
const skipped = [];
|
|
167
|
-
const
|
|
168
|
-
for (const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
155
|
+
const errors = [];
|
|
156
|
+
for (const r of results) {
|
|
157
|
+
switch (r.status) {
|
|
158
|
+
case "written":
|
|
159
|
+
installed.push(r.path);
|
|
160
|
+
break;
|
|
161
|
+
case "skipped":
|
|
162
|
+
skipped.push(r.path);
|
|
163
|
+
break;
|
|
164
|
+
case "error":
|
|
165
|
+
errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
|
|
166
|
+
break;
|
|
179
167
|
}
|
|
180
|
-
await writer.write(serverPath, workspaceRoot);
|
|
181
|
-
installed.push(writer.clientKind);
|
|
182
|
-
details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
|
|
183
168
|
}
|
|
184
|
-
return { installed, skipped,
|
|
169
|
+
return { installed, skipped, errors };
|
|
170
|
+
}
|
|
171
|
+
function normalizeTarget(targetInput) {
|
|
172
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
173
|
+
}
|
|
174
|
+
function assertExistingDirectory(target) {
|
|
175
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
176
|
+
throw new Error(t("cli.shared.target-invalid", { target }));
|
|
177
|
+
}
|
|
185
178
|
}
|
|
186
179
|
|
|
187
180
|
// src/scanner/forensic.ts
|
|
188
181
|
import { execFileSync } from "child_process";
|
|
189
|
-
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
|
|
182
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
|
|
190
183
|
import { createRequire } from "module";
|
|
191
|
-
import { basename, extname, isAbsolute, join, posix, relative, resolve as resolve2, sep } from "path";
|
|
184
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join2, posix, relative, resolve as resolve2, sep } from "path";
|
|
192
185
|
import {
|
|
193
186
|
forensicReportSchema
|
|
194
187
|
} from "@fenglimg/fabric-shared";
|
|
188
|
+
|
|
189
|
+
// src/scanner/detector.ts
|
|
190
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
191
|
+
|
|
192
|
+
// src/scanner/forensic.ts
|
|
195
193
|
var require2 = createRequire(import.meta.url);
|
|
196
194
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
197
195
|
".fabric",
|
|
@@ -274,7 +272,7 @@ var parserInitPromise = null;
|
|
|
274
272
|
var languagePromiseByKind = {};
|
|
275
273
|
var parserBundlePromiseByKind = {};
|
|
276
274
|
async function buildForensicReport(targetInput) {
|
|
277
|
-
const target =
|
|
275
|
+
const target = normalizeTarget2(targetInput);
|
|
278
276
|
const framework = detectFramework(target);
|
|
279
277
|
const topology = buildTopology(target);
|
|
280
278
|
const entryPoints = collectEntryPoints(target, topology.files);
|
|
@@ -310,11 +308,11 @@ async function buildForensicReport(targetInput) {
|
|
|
310
308
|
}
|
|
311
309
|
return validation.data;
|
|
312
310
|
}
|
|
313
|
-
function
|
|
314
|
-
return
|
|
311
|
+
function normalizeTarget2(targetInput) {
|
|
312
|
+
return isAbsolute2(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
|
|
315
313
|
}
|
|
316
314
|
function buildTopology(root) {
|
|
317
|
-
|
|
315
|
+
assertExistingDirectory2(root);
|
|
318
316
|
const byExt = {};
|
|
319
317
|
const keyDirs = /* @__PURE__ */ new Set();
|
|
320
318
|
const files = [];
|
|
@@ -327,7 +325,7 @@ function buildTopology(root) {
|
|
|
327
325
|
continue;
|
|
328
326
|
}
|
|
329
327
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
330
|
-
const absolutePath =
|
|
328
|
+
const absolutePath = join2(current, entry.name);
|
|
331
329
|
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
332
330
|
if (relativePath.length === 0) {
|
|
333
331
|
continue;
|
|
@@ -347,7 +345,7 @@ function buildTopology(root) {
|
|
|
347
345
|
if (!entry.isFile()) {
|
|
348
346
|
continue;
|
|
349
347
|
}
|
|
350
|
-
const stats =
|
|
348
|
+
const stats = statSync2(absolutePath);
|
|
351
349
|
const extension = extname(entry.name) || "[none]";
|
|
352
350
|
byExt[extension] = (byExt[extension] ?? 0) + 1;
|
|
353
351
|
totalFiles += 1;
|
|
@@ -365,8 +363,8 @@ function buildTopology(root) {
|
|
|
365
363
|
files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
366
364
|
};
|
|
367
365
|
}
|
|
368
|
-
function
|
|
369
|
-
if (!existsSync2(target) || !
|
|
366
|
+
function assertExistingDirectory2(target) {
|
|
367
|
+
if (!existsSync2(target) || !statSync2(target).isDirectory()) {
|
|
370
368
|
throw new Error(`Target must be an existing directory: ${target}`);
|
|
371
369
|
}
|
|
372
370
|
}
|
|
@@ -415,7 +413,7 @@ function getEntryPointReason(relativePath) {
|
|
|
415
413
|
async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
|
|
416
414
|
const samples = [];
|
|
417
415
|
for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
|
|
418
|
-
const absolutePath =
|
|
416
|
+
const absolutePath = join2(target, ...entryPoint.path.split("/"));
|
|
419
417
|
const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
|
|
420
418
|
const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
|
|
421
419
|
frameworkKind,
|
|
@@ -452,7 +450,7 @@ function readFirstLines(path, lineLimit) {
|
|
|
452
450
|
}
|
|
453
451
|
}
|
|
454
452
|
function readPackageDependencies(target) {
|
|
455
|
-
const packageJsonPath =
|
|
453
|
+
const packageJsonPath = join2(target, "package.json");
|
|
456
454
|
if (!existsSync2(packageJsonPath)) {
|
|
457
455
|
return /* @__PURE__ */ new Map();
|
|
458
456
|
}
|
|
@@ -793,8 +791,8 @@ function scoreFrameworkConfidence(input) {
|
|
|
793
791
|
return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
|
|
794
792
|
}
|
|
795
793
|
function readReadmeInfo(target) {
|
|
796
|
-
const readmePath =
|
|
797
|
-
const hasContributing = existsSync2(
|
|
794
|
+
const readmePath = join2(target, "README.md");
|
|
795
|
+
const hasContributing = existsSync2(join2(target, "CONTRIBUTING.md"));
|
|
798
796
|
if (!existsSync2(readmePath)) {
|
|
799
797
|
return {
|
|
800
798
|
quality: "missing",
|
|
@@ -1280,7 +1278,7 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
|
|
|
1280
1278
|
return recommendations;
|
|
1281
1279
|
}
|
|
1282
1280
|
function readProjectName(target) {
|
|
1283
|
-
const packageJsonPath =
|
|
1281
|
+
const packageJsonPath = join2(target, "package.json");
|
|
1284
1282
|
if (existsSync2(packageJsonPath)) {
|
|
1285
1283
|
try {
|
|
1286
1284
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
@@ -1294,7 +1292,7 @@ function readProjectName(target) {
|
|
|
1294
1292
|
return basename(target);
|
|
1295
1293
|
}
|
|
1296
1294
|
function getCliVersion() {
|
|
1297
|
-
return true ? "2.0.0-rc.
|
|
1295
|
+
return true ? "2.0.0-rc.15" : "unknown";
|
|
1298
1296
|
}
|
|
1299
1297
|
function sortRecord(record) {
|
|
1300
1298
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
@@ -1313,72 +1311,33 @@ Run \`fabric doctor\` to verify state.
|
|
|
1313
1311
|
|
|
1314
1312
|
See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
|
|
1315
1313
|
`;
|
|
1316
|
-
var LOCAL_FABRIC_SERVER_PATH =
|
|
1314
|
+
var LOCAL_FABRIC_SERVER_PATH = join3("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
|
|
1317
1315
|
var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
|
|
1318
1316
|
var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
|
|
1319
|
-
var installCommand =
|
|
1317
|
+
var installCommand = defineCommand({
|
|
1320
1318
|
meta: {
|
|
1321
1319
|
name: "install",
|
|
1322
1320
|
description: t("cli.install.description")
|
|
1323
1321
|
},
|
|
1324
1322
|
args: {
|
|
1325
|
-
target: {
|
|
1326
|
-
type: "string",
|
|
1327
|
-
description: t("cli.install.args.target.description")
|
|
1328
|
-
},
|
|
1329
1323
|
debug: {
|
|
1330
1324
|
type: "boolean",
|
|
1331
1325
|
description: t("cli.install.args.debug.description"),
|
|
1332
1326
|
default: false
|
|
1333
1327
|
},
|
|
1334
|
-
|
|
1328
|
+
"dry-run": {
|
|
1335
1329
|
type: "boolean",
|
|
1336
|
-
description: t("cli.install.args.
|
|
1330
|
+
description: t("cli.install.args.dry-run.description"),
|
|
1337
1331
|
default: false
|
|
1338
1332
|
},
|
|
1333
|
+
target: {
|
|
1334
|
+
type: "string",
|
|
1335
|
+
description: t("cli.install.args.target.description")
|
|
1336
|
+
},
|
|
1339
1337
|
yes: {
|
|
1340
1338
|
type: "boolean",
|
|
1341
1339
|
description: t("cli.install.args.yes.description"),
|
|
1342
1340
|
default: false
|
|
1343
|
-
},
|
|
1344
|
-
plan: {
|
|
1345
|
-
type: "boolean",
|
|
1346
|
-
description: t("cli.install.args.plan.description"),
|
|
1347
|
-
default: false
|
|
1348
|
-
},
|
|
1349
|
-
reapply: {
|
|
1350
|
-
type: "boolean",
|
|
1351
|
-
description: t("cli.install.args.reapply.description"),
|
|
1352
|
-
default: false
|
|
1353
|
-
},
|
|
1354
|
-
bootstrap: {
|
|
1355
|
-
type: "boolean",
|
|
1356
|
-
default: true,
|
|
1357
|
-
negativeDescription: t("cli.install.args.no-bootstrap.description")
|
|
1358
|
-
},
|
|
1359
|
-
mcp: {
|
|
1360
|
-
type: "boolean",
|
|
1361
|
-
default: true,
|
|
1362
|
-
negativeDescription: t("cli.install.args.no-mcp.description")
|
|
1363
|
-
},
|
|
1364
|
-
hooks: {
|
|
1365
|
-
type: "boolean",
|
|
1366
|
-
default: true,
|
|
1367
|
-
negativeDescription: t("cli.install.args.no-hooks.description")
|
|
1368
|
-
},
|
|
1369
|
-
interactive: {
|
|
1370
|
-
type: "boolean",
|
|
1371
|
-
description: t("cli.install.args.interactive.description"),
|
|
1372
|
-
default: true
|
|
1373
|
-
},
|
|
1374
|
-
"mcp-install": {
|
|
1375
|
-
type: "string",
|
|
1376
|
-
default: "global",
|
|
1377
|
-
description: t("cli.install.mcp.install.prompt")
|
|
1378
|
-
},
|
|
1379
|
-
scope: {
|
|
1380
|
-
type: "string",
|
|
1381
|
-
description: t("cli.install.mcp.scope.description")
|
|
1382
1341
|
}
|
|
1383
1342
|
},
|
|
1384
1343
|
async run({ args }) {
|
|
@@ -1390,22 +1349,22 @@ async function runInitCommand(args) {
|
|
|
1390
1349
|
const logger = createDebugLogger(args.debug);
|
|
1391
1350
|
const resolution = resolveDevMode(args.target, process.cwd());
|
|
1392
1351
|
const intent = resolveInitCliIntent(args, resolution.target);
|
|
1393
|
-
|
|
1394
|
-
|
|
1352
|
+
const fabricInitialized = existsSync3(join3(intent.target, ".fabric", "events.jsonl"));
|
|
1353
|
+
if (fabricInitialized) {
|
|
1354
|
+
try {
|
|
1355
|
+
checkLockOrThrow(intent.target);
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
if (hasActionHint(err)) {
|
|
1358
|
+
renderFabricError(err);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
throw err;
|
|
1362
|
+
}
|
|
1395
1363
|
}
|
|
1396
1364
|
logger(`init target source: ${resolution.source}`);
|
|
1397
1365
|
for (const step of resolution.chain) {
|
|
1398
1366
|
logger(step);
|
|
1399
1367
|
}
|
|
1400
|
-
if (intent.options.planOnly) {
|
|
1401
|
-
writeStderr2(t("cli.install.compat.plan"));
|
|
1402
|
-
}
|
|
1403
|
-
if (args.interactive === false) {
|
|
1404
|
-
writeStderr2(t("cli.install.compat.interactive"));
|
|
1405
|
-
}
|
|
1406
|
-
if (args.bootstrap === false || args.mcp === false || args.hooks === false) {
|
|
1407
|
-
writeStderr2(t("cli.install.compat.legacy-stage-flags"));
|
|
1408
|
-
}
|
|
1409
1368
|
const supports = detectClientSupports(intent.target);
|
|
1410
1369
|
const basePlan = await buildInitExecutionPlan({
|
|
1411
1370
|
target: intent.target,
|
|
@@ -1415,7 +1374,7 @@ async function runInitCommand(args) {
|
|
|
1415
1374
|
interactive: intent.interactiveSummary && !intent.wizardEnabled,
|
|
1416
1375
|
supports
|
|
1417
1376
|
});
|
|
1418
|
-
const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan,
|
|
1377
|
+
const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan, createDefaultInitWizardAdapter()) : basePlan;
|
|
1419
1378
|
if (plan === null) {
|
|
1420
1379
|
process.exitCode = 130;
|
|
1421
1380
|
return;
|
|
@@ -1427,7 +1386,7 @@ async function runInitCommand(args) {
|
|
|
1427
1386
|
return result;
|
|
1428
1387
|
}
|
|
1429
1388
|
function writeDefaultFabricConfig(fabricDir, targetRoot) {
|
|
1430
|
-
const target =
|
|
1389
|
+
const target = join3(fabricDir, "fabric-config.json");
|
|
1431
1390
|
if (existsSync3(target)) return;
|
|
1432
1391
|
const detectedLanguage = detectExistingLanguage(targetRoot);
|
|
1433
1392
|
const FABRIC_CONFIG_DEFAULTS = {
|
|
@@ -1496,39 +1455,23 @@ function writeDefaultFabricConfig(fabricDir, targetRoot) {
|
|
|
1496
1455
|
);
|
|
1497
1456
|
}
|
|
1498
1457
|
function resolveInitCliIntent(args, targetInput) {
|
|
1499
|
-
const target =
|
|
1500
|
-
const mcpInstallMode =
|
|
1501
|
-
const claudeMcpScope =
|
|
1458
|
+
const target = normalizeTarget3(targetInput);
|
|
1459
|
+
const mcpInstallMode = "global";
|
|
1460
|
+
const claudeMcpScope = "project";
|
|
1502
1461
|
const terminalInteractive = isInteractiveInit();
|
|
1503
|
-
const planOnly = args
|
|
1504
|
-
const reapply = args.reapply === true;
|
|
1462
|
+
const planOnly = args["dry-run"] === true;
|
|
1505
1463
|
const options = {
|
|
1506
|
-
|
|
1507
|
-
skipBootstrap: args.bootstrap === false ? true : args.skipBootstrap,
|
|
1508
|
-
skipMcp: args.mcp === false ? true : args.skipMcp,
|
|
1509
|
-
skipHooks: args.hooks === false ? true : args.skipHooks,
|
|
1510
|
-
planOnly,
|
|
1511
|
-
reapply
|
|
1464
|
+
planOnly
|
|
1512
1465
|
};
|
|
1513
1466
|
return {
|
|
1514
1467
|
target,
|
|
1515
1468
|
options,
|
|
1516
1469
|
mcpInstallMode,
|
|
1517
1470
|
claudeMcpScope,
|
|
1518
|
-
interactiveSummary:
|
|
1471
|
+
interactiveSummary: terminalInteractive,
|
|
1519
1472
|
wizardEnabled: shouldUseInitWizard(args, terminalInteractive) && !planOnly
|
|
1520
1473
|
};
|
|
1521
1474
|
}
|
|
1522
|
-
function resolveClaudeMcpScope(raw) {
|
|
1523
|
-
if (raw === void 0 || raw === "project") {
|
|
1524
|
-
return "project";
|
|
1525
|
-
}
|
|
1526
|
-
if (raw === "user") {
|
|
1527
|
-
return "user";
|
|
1528
|
-
}
|
|
1529
|
-
writeStderr2(t("cli.install.mcp.scope.invalid", { value: raw }));
|
|
1530
|
-
return "project";
|
|
1531
|
-
}
|
|
1532
1475
|
async function buildInitExecutionPlan(input) {
|
|
1533
1476
|
const options = input.options ?? {};
|
|
1534
1477
|
const scaffold = await buildInitFabricPlan(input.target, options);
|
|
@@ -1565,17 +1508,17 @@ async function buildInitExecutionPlan(input) {
|
|
|
1565
1508
|
};
|
|
1566
1509
|
}
|
|
1567
1510
|
async function executeInitExecutionPlan(plan) {
|
|
1568
|
-
if (plan.options.force) {
|
|
1569
|
-
writeStderr2(t("cli.install.force.warning", { path: plan.target }));
|
|
1570
|
-
}
|
|
1571
|
-
if (plan.options.reapply && !plan.options.planOnly && !plan.interactive) {
|
|
1572
|
-
writeStderr2(formatInitModeBanner(plan.options));
|
|
1573
|
-
}
|
|
1574
1511
|
if (plan.interactive) {
|
|
1575
1512
|
printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
|
|
1576
1513
|
}
|
|
1514
|
+
const scaffoldStates = [
|
|
1515
|
+
{ path: plan.scaffold.metaPath, state: plan.scaffold.metaState },
|
|
1516
|
+
{ path: plan.scaffold.eventsPath, state: plan.scaffold.eventsState },
|
|
1517
|
+
{ path: plan.scaffold.forensicPath, state: plan.scaffold.forensicState }
|
|
1518
|
+
];
|
|
1577
1519
|
if (plan.options.planOnly) {
|
|
1578
1520
|
printInitPlanPreview(plan);
|
|
1521
|
+
printInitDiffStateTable(scaffoldStates);
|
|
1579
1522
|
return {
|
|
1580
1523
|
plan,
|
|
1581
1524
|
created: buildPlanOnlyScaffoldResult(plan.scaffold),
|
|
@@ -1583,6 +1526,17 @@ async function executeInitExecutionPlan(plan) {
|
|
|
1583
1526
|
finalSupports: plan.supports
|
|
1584
1527
|
};
|
|
1585
1528
|
}
|
|
1529
|
+
if (existsSync3(plan.scaffold.fabricDir) && !statSync3(plan.scaffold.fabricDir).isDirectory()) {
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
t("cli.install.diff.drift-abort", { path: plan.scaffold.fabricDir })
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
const drifted = scaffoldStates.find(
|
|
1535
|
+
(entry) => entry.state === "drifted" || entry.state === "user-modified"
|
|
1536
|
+
);
|
|
1537
|
+
if (drifted !== void 0) {
|
|
1538
|
+
throw new Error(t("cli.install.diff.drift-abort", { path: drifted.path }));
|
|
1539
|
+
}
|
|
1586
1540
|
let created = null;
|
|
1587
1541
|
const stageResults = [];
|
|
1588
1542
|
let finalSupports = plan.supports;
|
|
@@ -1607,6 +1561,11 @@ async function executeInitExecutionPlan(plan) {
|
|
|
1607
1561
|
exhaustiveInitExecutionStep(step);
|
|
1608
1562
|
}
|
|
1609
1563
|
}
|
|
1564
|
+
if (scaffoldStates.every((entry) => entry.state === "present-canonical")) {
|
|
1565
|
+
console.log(
|
|
1566
|
+
t("cli.install.diff.canonical", { count: String(scaffoldStates.length) })
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1610
1569
|
return {
|
|
1611
1570
|
plan,
|
|
1612
1571
|
created: created ?? unreachableInitScaffold(),
|
|
@@ -1619,20 +1578,23 @@ function resolvePersonalFabricRoot() {
|
|
|
1619
1578
|
return process.env.FABRIC_HOME ?? homedir();
|
|
1620
1579
|
}
|
|
1621
1580
|
async function buildInitFabricPlan(target, options) {
|
|
1622
|
-
|
|
1623
|
-
const fabricDir =
|
|
1624
|
-
const agentsMdPath =
|
|
1581
|
+
assertExistingDirectory3(target);
|
|
1582
|
+
const fabricDir = join3(target, ".fabric");
|
|
1583
|
+
const agentsMdPath = join3(target, "AGENTS.md");
|
|
1625
1584
|
const agentsMdAction = existsSync3(agentsMdPath) ? "preserved" : "created";
|
|
1626
|
-
const knowledgeDir =
|
|
1627
|
-
const personalKnowledgeDir =
|
|
1628
|
-
const forensicPath =
|
|
1629
|
-
const eventsPath =
|
|
1630
|
-
const metaPath =
|
|
1585
|
+
const knowledgeDir = join3(fabricDir, "knowledge");
|
|
1586
|
+
const personalKnowledgeDir = join3(resolvePersonalFabricRoot(), ".fabric", "knowledge");
|
|
1587
|
+
const forensicPath = join3(fabricDir, "forensic.json");
|
|
1588
|
+
const eventsPath = join3(fabricDir, "events.jsonl");
|
|
1589
|
+
const metaPath = join3(fabricDir, "agents.meta.json");
|
|
1631
1590
|
const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
|
|
1632
1591
|
const knowledgeDirAction = existsSync3(knowledgeDir) ? "overwritten" : "created";
|
|
1633
|
-
const
|
|
1634
|
-
const
|
|
1635
|
-
const
|
|
1592
|
+
const metaClassification = classifyFreshPath(metaPath, "structural");
|
|
1593
|
+
const eventsClassification = classifyFreshPath(eventsPath, "presence");
|
|
1594
|
+
const forensicClassification = classifyFreshPath(forensicPath, "always-rewrite");
|
|
1595
|
+
const metaAction = diffStateToWriteAction(metaClassification.state);
|
|
1596
|
+
const eventsAction = diffStateToWriteAction(eventsClassification.state);
|
|
1597
|
+
const forensicAction = diffStateToWriteAction(forensicClassification.state);
|
|
1636
1598
|
const forensicReport = await buildForensicReport(target);
|
|
1637
1599
|
const meta = createInitialMeta();
|
|
1638
1600
|
return {
|
|
@@ -1652,11 +1614,13 @@ async function buildInitFabricPlan(target, options) {
|
|
|
1652
1614
|
eventsAction,
|
|
1653
1615
|
forensicPath,
|
|
1654
1616
|
forensicAction,
|
|
1655
|
-
forensicReport
|
|
1617
|
+
forensicReport,
|
|
1618
|
+
metaState: metaClassification.state,
|
|
1619
|
+
eventsState: eventsClassification.state,
|
|
1620
|
+
forensicState: forensicClassification.state
|
|
1656
1621
|
};
|
|
1657
1622
|
}
|
|
1658
1623
|
async function executeInitFabricPlan(plan) {
|
|
1659
|
-
const isReapply = plan.options?.reapply === true;
|
|
1660
1624
|
if (plan.replaceFabricDir) {
|
|
1661
1625
|
rmSync(plan.fabricDir, { force: true });
|
|
1662
1626
|
}
|
|
@@ -1667,9 +1631,9 @@ async function executeInitFabricPlan(plan) {
|
|
|
1667
1631
|
}
|
|
1668
1632
|
mkdirSync(plan.knowledgeDir, { recursive: true });
|
|
1669
1633
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
1670
|
-
const teamSubDir =
|
|
1634
|
+
const teamSubDir = join3(plan.knowledgeDir, sub);
|
|
1671
1635
|
mkdirSync(teamSubDir, { recursive: true });
|
|
1672
|
-
const teamGitkeep =
|
|
1636
|
+
const teamGitkeep = join3(teamSubDir, ".gitkeep");
|
|
1673
1637
|
if (!existsSync3(teamGitkeep)) {
|
|
1674
1638
|
writeFileSync(teamGitkeep, "", "utf8");
|
|
1675
1639
|
}
|
|
@@ -1677,36 +1641,49 @@ async function executeInitFabricPlan(plan) {
|
|
|
1677
1641
|
try {
|
|
1678
1642
|
mkdirSync(plan.personalKnowledgeDir, { recursive: true });
|
|
1679
1643
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
1680
|
-
mkdirSync(
|
|
1644
|
+
mkdirSync(join3(plan.personalKnowledgeDir, sub), { recursive: true });
|
|
1681
1645
|
}
|
|
1682
1646
|
} catch {
|
|
1683
1647
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
writeFileSync(plan.eventsPath, "", "utf8");
|
|
1690
|
-
}
|
|
1691
|
-
} else {
|
|
1648
|
+
if (plan.metaState === "missing") {
|
|
1649
|
+
preparePlannedPath(plan.metaPath, plan.metaAction);
|
|
1650
|
+
await atomicWriteJson(plan.metaPath, plan.meta);
|
|
1651
|
+
}
|
|
1652
|
+
if (plan.eventsState === "missing") {
|
|
1692
1653
|
preparePlannedPath(plan.eventsPath, plan.eventsAction);
|
|
1654
|
+
mkdirSync(dirname(plan.eventsPath), { recursive: true });
|
|
1693
1655
|
writeFileSync(plan.eventsPath, "", "utf8");
|
|
1694
1656
|
}
|
|
1695
1657
|
preparePlannedPath(plan.forensicPath, plan.forensicAction);
|
|
1696
1658
|
await atomicWriteJson(plan.forensicPath, plan.forensicReport);
|
|
1697
|
-
|
|
1659
|
+
const wasCanonicalReRun = plan.metaState === "present-canonical" && plan.eventsState === "present-canonical";
|
|
1660
|
+
if (!wasCanonicalReRun) {
|
|
1698
1661
|
try {
|
|
1699
1662
|
await runInitScan(plan.target, { source: "init" });
|
|
1700
1663
|
} catch (error) {
|
|
1701
|
-
|
|
1664
|
+
writeStderr(
|
|
1702
1665
|
`[warn] init-scan failed: ${error instanceof Error ? error.message : String(error)} \u2014 re-run \`fab scan\` to populate baseline knowledge entries.`
|
|
1703
1666
|
);
|
|
1704
1667
|
}
|
|
1705
1668
|
}
|
|
1706
|
-
if (
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1669
|
+
if (existsSync3(plan.eventsPath)) {
|
|
1670
|
+
const applied = [];
|
|
1671
|
+
const canonical = [];
|
|
1672
|
+
const drifted = [];
|
|
1673
|
+
for (const entry of [
|
|
1674
|
+
{ path: plan.metaPath, state: plan.metaState },
|
|
1675
|
+
{ path: plan.eventsPath, state: plan.eventsState },
|
|
1676
|
+
{ path: plan.forensicPath, state: plan.forensicState }
|
|
1677
|
+
]) {
|
|
1678
|
+
if (entry.state === "missing") {
|
|
1679
|
+
applied.push(entry.path);
|
|
1680
|
+
} else if (entry.state === "present-canonical") {
|
|
1681
|
+
canonical.push(entry.path);
|
|
1682
|
+
} else {
|
|
1683
|
+
drifted.push(entry.path);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
appendInstallDiffLedgerEvent(plan.eventsPath, { applied, canonical, drifted });
|
|
1710
1687
|
}
|
|
1711
1688
|
return {
|
|
1712
1689
|
agentsMdPath: plan.agentsMdPath,
|
|
@@ -1726,16 +1703,16 @@ async function initFabric(target, options) {
|
|
|
1726
1703
|
return await executeInitFabricPlan(await buildInitFabricPlan(target, options));
|
|
1727
1704
|
}
|
|
1728
1705
|
function shouldUseInitWizard(args, terminalInteractive = isInteractiveInit()) {
|
|
1729
|
-
return terminalInteractive && args.
|
|
1706
|
+
return terminalInteractive && args.yes !== true;
|
|
1730
1707
|
}
|
|
1731
|
-
async function resolveInitExecutionPlanWithWizard(basePlan,
|
|
1708
|
+
async function resolveInitExecutionPlanWithWizard(basePlan, wizardAdapter) {
|
|
1732
1709
|
const selection = await wizardAdapter.run({
|
|
1733
1710
|
target: basePlan.target,
|
|
1734
1711
|
options: basePlan.options,
|
|
1735
1712
|
supports: basePlan.supports,
|
|
1736
1713
|
mcpInstallMode: basePlan.mcpInstallMode,
|
|
1737
1714
|
claudeMcpScope: basePlan.claudeMcpScope,
|
|
1738
|
-
lockedStages:
|
|
1715
|
+
lockedStages: []
|
|
1739
1716
|
});
|
|
1740
1717
|
if (selection === null) {
|
|
1741
1718
|
return null;
|
|
@@ -1792,12 +1769,17 @@ function printInitPostSetup(plan, stageResults, finalSupports) {
|
|
|
1792
1769
|
paint.muted(t("cli.install.language_preference_hint", { value: fabricLanguage }))
|
|
1793
1770
|
);
|
|
1794
1771
|
}
|
|
1772
|
+
function printInitDiffStateTable(entries) {
|
|
1773
|
+
for (const entry of entries) {
|
|
1774
|
+
console.log(` ${formatDiffFileState(entry.state)} ${entry.path}`);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1795
1777
|
function printInitPlanPreview(plan) {
|
|
1796
1778
|
console.log(t("cli.install.plan.preview-title"));
|
|
1797
1779
|
printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
|
|
1798
1780
|
console.log(
|
|
1799
1781
|
t("cli.install.plan.preview-result", {
|
|
1800
|
-
mode:
|
|
1782
|
+
mode: t("cli.install.mode.default"),
|
|
1801
1783
|
bootstrap: yesNoLabel(!plan.options.skipBootstrap),
|
|
1802
1784
|
mcp: yesNoLabel(!plan.options.skipMcp),
|
|
1803
1785
|
hooks: yesNoLabel(!plan.options.skipHooks)
|
|
@@ -1848,7 +1830,7 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
1848
1830
|
const errorCount = installResults.filter((r) => r.status === "error").length;
|
|
1849
1831
|
for (const result of installResults) {
|
|
1850
1832
|
if (result.status === "error") {
|
|
1851
|
-
|
|
1833
|
+
writeStderr(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
|
|
1852
1834
|
}
|
|
1853
1835
|
}
|
|
1854
1836
|
const note2 = errorCount > 0 ? `errors=${errorCount}` : void 0;
|
|
@@ -1858,15 +1840,14 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
1858
1840
|
case "mcp": {
|
|
1859
1841
|
if (stage.installMode === "local") {
|
|
1860
1842
|
const manager = stage.packageManager ?? detectPackageManager(plan.target);
|
|
1861
|
-
|
|
1862
|
-
|
|
1843
|
+
writeStderr(t("cli.install.mcp.install.local"));
|
|
1844
|
+
writeStderr(t("cli.install.mcp.local.installing", { manager }));
|
|
1863
1845
|
installLocalFabricServer(plan.target, manager);
|
|
1864
|
-
|
|
1846
|
+
writeStderr(t("cli.install.mcp.local.installed"));
|
|
1865
1847
|
} else {
|
|
1866
|
-
|
|
1848
|
+
writeStderr(t("cli.install.mcp.install.global"));
|
|
1867
1849
|
}
|
|
1868
1850
|
const result = await installMcpClients(plan.target, {
|
|
1869
|
-
force: plan.options.force,
|
|
1870
1851
|
localServerPath: stage.localServerPath,
|
|
1871
1852
|
claudeMcpScope: stage.claudeMcpScope
|
|
1872
1853
|
});
|
|
@@ -1878,7 +1859,7 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
1878
1859
|
return { name: "mcp", disposition: "ran" };
|
|
1879
1860
|
}
|
|
1880
1861
|
case "hooks": {
|
|
1881
|
-
const result = await installHooks(plan.target
|
|
1862
|
+
const result = await installHooks(plan.target);
|
|
1882
1863
|
console.log(formatInitStageResult("hooks", "completed", result.installed.length, result.skipped.length));
|
|
1883
1864
|
return { name: "hooks", disposition: "ran" };
|
|
1884
1865
|
}
|
|
@@ -1886,30 +1867,66 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
1886
1867
|
return exhaustiveInitStagePlan(stage);
|
|
1887
1868
|
}
|
|
1888
1869
|
} catch (error) {
|
|
1889
|
-
|
|
1870
|
+
writeStderr(formatInitStageFailure(stageName, error));
|
|
1890
1871
|
return { name: stageName, disposition: "failed" };
|
|
1891
1872
|
}
|
|
1892
1873
|
}
|
|
1893
|
-
function shouldReplaceWritableDirectory(path,
|
|
1874
|
+
function shouldReplaceWritableDirectory(path, _options) {
|
|
1894
1875
|
if (!existsSync3(path)) {
|
|
1895
1876
|
return false;
|
|
1896
1877
|
}
|
|
1897
|
-
if (
|
|
1878
|
+
if (statSync3(path).isDirectory()) {
|
|
1898
1879
|
return false;
|
|
1899
1880
|
}
|
|
1900
|
-
|
|
1901
|
-
throw new Error(t("cli.install.errors.abort-existing", { path }));
|
|
1902
|
-
}
|
|
1903
|
-
return true;
|
|
1881
|
+
return false;
|
|
1904
1882
|
}
|
|
1905
|
-
function
|
|
1883
|
+
function classifyFreshPath(path, strategy) {
|
|
1906
1884
|
if (!existsSync3(path)) {
|
|
1907
|
-
return "
|
|
1885
|
+
return { path, state: "missing" };
|
|
1886
|
+
}
|
|
1887
|
+
let stat;
|
|
1888
|
+
try {
|
|
1889
|
+
stat = statSync3(path);
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
return {
|
|
1892
|
+
path,
|
|
1893
|
+
state: "user-modified",
|
|
1894
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
if (!stat.isFile()) {
|
|
1898
|
+
return { path, state: "user-modified", reason: "expected a file" };
|
|
1899
|
+
}
|
|
1900
|
+
if (strategy === "presence" || strategy === "always-rewrite") {
|
|
1901
|
+
return { path, state: "present-canonical" };
|
|
1908
1902
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1903
|
+
try {
|
|
1904
|
+
const raw = readFileSync2(path, "utf8");
|
|
1905
|
+
const parsed = JSON.parse(raw);
|
|
1906
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1907
|
+
return { path, state: "user-modified", reason: "not a JSON object" };
|
|
1908
|
+
}
|
|
1909
|
+
const record = parsed;
|
|
1910
|
+
const hasRevision = typeof record["revision"] === "string";
|
|
1911
|
+
const hasNodes = record["nodes"] !== void 0 && record["nodes"] !== null && typeof record["nodes"] === "object" && !Array.isArray(record["nodes"]);
|
|
1912
|
+
const hasCounters = record["counters"] !== void 0 && record["counters"] !== null && typeof record["counters"] === "object" && !Array.isArray(record["counters"]);
|
|
1913
|
+
if (!hasRevision || !hasNodes || !hasCounters) {
|
|
1914
|
+
return { path, state: "drifted", reason: "missing required AgentsMeta fields" };
|
|
1915
|
+
}
|
|
1916
|
+
return { path, state: "present-canonical" };
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
return {
|
|
1919
|
+
path,
|
|
1920
|
+
state: "user-modified",
|
|
1921
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1922
|
+
};
|
|
1911
1923
|
}
|
|
1912
|
-
|
|
1924
|
+
}
|
|
1925
|
+
function diffStateToWriteAction(_state) {
|
|
1926
|
+
return "created";
|
|
1927
|
+
}
|
|
1928
|
+
function formatDiffFileState(state) {
|
|
1929
|
+
return t(`cli.install.diff.state.${state}`);
|
|
1913
1930
|
}
|
|
1914
1931
|
function preparePlannedPath(path, action) {
|
|
1915
1932
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -2048,74 +2065,42 @@ async function selectClaudeMcpScopeInGroup(options) {
|
|
|
2048
2065
|
}
|
|
2049
2066
|
return result;
|
|
2050
2067
|
}
|
|
2051
|
-
function collectLockedWizardStages(args) {
|
|
2052
|
-
const lockedStages = [];
|
|
2053
|
-
if (args.bootstrap === false) {
|
|
2054
|
-
lockedStages.push("bootstrap");
|
|
2055
|
-
}
|
|
2056
|
-
if (args.mcp === false) {
|
|
2057
|
-
lockedStages.push("mcp");
|
|
2058
|
-
}
|
|
2059
|
-
if (args.hooks === false) {
|
|
2060
|
-
lockedStages.push("hooks");
|
|
2061
|
-
}
|
|
2062
|
-
return lockedStages;
|
|
2063
|
-
}
|
|
2064
2068
|
function formatPromptDefault(value) {
|
|
2065
2069
|
return value ? "Y/n" : "y/N";
|
|
2066
2070
|
}
|
|
2067
2071
|
function formatInitModeBanner(options) {
|
|
2068
|
-
if (options.planOnly && options.reapply) {
|
|
2069
|
-
return t("cli.install.plan.mode-banner.plan-reapply");
|
|
2070
|
-
}
|
|
2071
2072
|
if (options.planOnly) {
|
|
2072
2073
|
return t("cli.install.plan.mode-banner.plan");
|
|
2073
2074
|
}
|
|
2074
|
-
if (options.reapply) {
|
|
2075
|
-
return t("cli.install.plan.mode-banner.reapply");
|
|
2076
|
-
}
|
|
2077
2075
|
return t("cli.install.plan.mode-banner.default");
|
|
2078
2076
|
}
|
|
2079
2077
|
function formatInitModeBadge(options) {
|
|
2080
|
-
if (options.planOnly && options.reapply) {
|
|
2081
|
-
return t("cli.install.mode.badge.plan-reapply");
|
|
2082
|
-
}
|
|
2083
2078
|
if (options.planOnly) {
|
|
2084
2079
|
return t("cli.install.mode.badge.plan");
|
|
2085
2080
|
}
|
|
2086
|
-
if (options.reapply) {
|
|
2087
|
-
return t("cli.install.mode.badge.reapply");
|
|
2088
|
-
}
|
|
2089
2081
|
return t("cli.install.mode.badge.default");
|
|
2090
2082
|
}
|
|
2091
|
-
function
|
|
2092
|
-
return
|
|
2083
|
+
function normalizeTarget3(targetInput) {
|
|
2084
|
+
return isAbsolute3(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
2093
2085
|
}
|
|
2094
|
-
function
|
|
2095
|
-
if (!existsSync3(target) || !
|
|
2086
|
+
function assertExistingDirectory3(target) {
|
|
2087
|
+
if (!existsSync3(target) || !statSync3(target).isDirectory()) {
|
|
2096
2088
|
throw new Error(`Target must be an existing directory: ${target}`);
|
|
2097
2089
|
}
|
|
2098
2090
|
}
|
|
2099
2091
|
function detectPackageManager(cwd) {
|
|
2100
2092
|
const workspaceRoot = resolve3(cwd);
|
|
2101
|
-
if (existsSync3(
|
|
2093
|
+
if (existsSync3(join3(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
2102
2094
|
return "pnpm";
|
|
2103
2095
|
}
|
|
2104
|
-
if (existsSync3(
|
|
2096
|
+
if (existsSync3(join3(workspaceRoot, "yarn.lock"))) {
|
|
2105
2097
|
return "yarn";
|
|
2106
2098
|
}
|
|
2107
|
-
if (existsSync3(
|
|
2099
|
+
if (existsSync3(join3(workspaceRoot, "package-lock.json"))) {
|
|
2108
2100
|
return "npm";
|
|
2109
2101
|
}
|
|
2110
2102
|
return "npm";
|
|
2111
2103
|
}
|
|
2112
|
-
function resolveMcpInstallMode(rawMode) {
|
|
2113
|
-
if (rawMode === void 0 || rawMode === "global" || rawMode === "local") {
|
|
2114
|
-
return rawMode ?? "global";
|
|
2115
|
-
}
|
|
2116
|
-
writeStderr2(t("cli.install.mcp.install.invalid", { value: rawMode }));
|
|
2117
|
-
return "global";
|
|
2118
|
-
}
|
|
2119
2104
|
function installLocalFabricServer(target, manager) {
|
|
2120
2105
|
const installArgs = manager === "npm" ? ["install", "-D", FABRIC_SERVER_PACKAGE] : ["add", "-D", FABRIC_SERVER_PACKAGE];
|
|
2121
2106
|
childProcess.execFileSync(manager, installArgs, {
|
|
@@ -2131,14 +2116,16 @@ function createInitialMeta() {
|
|
|
2131
2116
|
counters: defaultAgentsMetaCounters()
|
|
2132
2117
|
};
|
|
2133
2118
|
}
|
|
2134
|
-
function
|
|
2119
|
+
function appendInstallDiffLedgerEvent(eventsPath, payload) {
|
|
2135
2120
|
const event = {
|
|
2136
2121
|
kind: "fabric-event",
|
|
2137
2122
|
id: `event:${randomUUID()}`,
|
|
2138
2123
|
ts: Date.now(),
|
|
2139
2124
|
schema_version: 1,
|
|
2140
|
-
event_type: "
|
|
2141
|
-
|
|
2125
|
+
event_type: "install_diff_applied",
|
|
2126
|
+
applied: payload.applied,
|
|
2127
|
+
canonical: payload.canonical,
|
|
2128
|
+
drifted: payload.drifted
|
|
2142
2129
|
};
|
|
2143
2130
|
const line = `${JSON.stringify(event)}
|
|
2144
2131
|
`;
|
|
@@ -2349,7 +2336,7 @@ function reasonLabel() {
|
|
|
2349
2336
|
return paint.human(t("cli.shared.reason"));
|
|
2350
2337
|
}
|
|
2351
2338
|
function overwrittenLabel() {
|
|
2352
|
-
return paint.warn(t("cli.install.
|
|
2339
|
+
return paint.warn(t("cli.install.label.overwritten"));
|
|
2353
2340
|
}
|
|
2354
2341
|
function completedStageLabel() {
|
|
2355
2342
|
return paint.success(t("cli.install.stages.completed"));
|
|
@@ -2360,7 +2347,7 @@ function skippedStageLabel() {
|
|
|
2360
2347
|
function failedStageLabel() {
|
|
2361
2348
|
return paint.error(t("cli.install.stages.failed"));
|
|
2362
2349
|
}
|
|
2363
|
-
function
|
|
2350
|
+
function writeStderr(message) {
|
|
2364
2351
|
process.stderr.write(`${message}
|
|
2365
2352
|
`);
|
|
2366
2353
|
}
|