@fenglimg/fabric-cli 2.0.0 → 2.0.1
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/LICENSE +21 -0
- package/README.md +6 -5
- package/dist/chunk-BATF4PEJ.js +361 -0
- package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
- package/dist/chunk-D25XJ4BC.js +880 -0
- package/dist/chunk-MF3OTILQ.js +544 -0
- package/dist/chunk-PWLW3B57.js +18 -0
- package/dist/config-XJIPZNUP.js +13 -0
- package/dist/doctor-EJDSEJSS.js +810 -0
- package/dist/index.js +15 -8
- package/dist/{init-BIRSIOXO.js → install-EKWMFLUU.js} +622 -711
- package/dist/metrics-ACEQFPDU.js +122 -0
- package/dist/onboard-coverage-MFCAEBDO.js +220 -0
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
- package/dist/uninstall-MH7ZIB6M.js +1064 -0
- package/package.json +30 -5
- package/templates/hooks/cite-policy-evict.cjs +231 -0
- package/templates/hooks/configs/README.md +29 -6
- package/templates/hooks/configs/claude-code.json +14 -3
- package/templates/hooks/configs/codex-hooks.json +6 -3
- package/templates/hooks/configs/cursor-hooks.json +8 -10
- package/templates/hooks/fabric-hint.cjs +833 -105
- package/templates/hooks/knowledge-hint-broad.cjs +509 -135
- package/templates/hooks/knowledge-hint-narrow.cjs +791 -26
- package/templates/hooks/lib/banner-i18n.cjs +309 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +173 -0
- package/templates/hooks/lib/cite-line-parser.cjs +158 -0
- package/templates/hooks/lib/client-adapter.cjs +106 -0
- package/templates/hooks/lib/config-cache.cjs +107 -0
- package/templates/hooks/lib/state-store.cjs +84 -0
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +93 -419
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
- package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
- package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
- package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
- package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
- package/templates/skills/fabric-import/SKILL.md +75 -516
- package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
- package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
- package/templates/skills/fabric-import/ref/output-contract.md +61 -0
- package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
- package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
- package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
- package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
- package/templates/skills/fabric-review/SKILL.md +86 -284
- package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
- package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
- package/templates/skills/fabric-review/ref/output-contract.md +58 -0
- package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
- package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
- package/templates/skills/lib/shared-policy.md +69 -0
- package/dist/chunk-6ICJICVU.js +0 -10
- package/dist/chunk-74SZWYPH.js +0 -658
- package/dist/chunk-EYIDD2YS.js +0 -1000
- package/dist/doctor-T7JWODKG.js +0 -282
- package/dist/hooks-Y74Y5LQS.js +0 -12
- package/dist/scan-LMK3UCWL.js +0 -22
- package/dist/serve-H554BHLG.js +0 -124
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -1,444 +1,253 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
hooksCommand,
|
|
3
|
+
installMcpClients
|
|
4
|
+
} from "./chunk-BATF4PEJ.js";
|
|
5
|
+
import {
|
|
6
|
+
cleanupDeprecatedSkills,
|
|
8
7
|
installArchiveHintHook,
|
|
8
|
+
installCitePolicyEvictHook,
|
|
9
9
|
installFabricArchiveSkill,
|
|
10
10
|
installFabricImportSkill,
|
|
11
11
|
installFabricReviewSkill,
|
|
12
|
-
|
|
12
|
+
installHookLibs,
|
|
13
13
|
installKnowledgeHintBroadHook,
|
|
14
14
|
installKnowledgeHintNarrowHook,
|
|
15
|
+
installSharedSkillLib,
|
|
15
16
|
mergeClaudeCodeHookConfig,
|
|
16
17
|
mergeCodexHookConfig,
|
|
17
18
|
mergeCursorHookConfig,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
readFabricLanguagePreference,
|
|
20
|
+
writeClaudeBootstrapThinShell,
|
|
21
|
+
writeCodexBootstrapManagedBlock,
|
|
22
|
+
writeCursorBootstrapManagedBlock,
|
|
23
|
+
writeFabricAgentsSnapshot
|
|
24
|
+
} from "./chunk-D25XJ4BC.js";
|
|
21
25
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "./chunk-EYIDD2YS.js";
|
|
26
|
+
detectClientSupports
|
|
27
|
+
} from "./chunk-MF3OTILQ.js";
|
|
25
28
|
import {
|
|
26
29
|
displayWidth,
|
|
27
30
|
padEnd,
|
|
28
31
|
paint
|
|
29
32
|
} from "./chunk-WWNXR34K.js";
|
|
30
|
-
import {
|
|
31
|
-
t
|
|
32
|
-
} from "./chunk-6ICJICVU.js";
|
|
33
33
|
import {
|
|
34
34
|
createDebugLogger,
|
|
35
35
|
resolveDevMode
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-COI5VDFU.js";
|
|
37
|
+
import {
|
|
38
|
+
t
|
|
39
|
+
} from "./chunk-PWLW3B57.js";
|
|
37
40
|
|
|
38
|
-
// src/commands/
|
|
41
|
+
// src/commands/install.ts
|
|
39
42
|
import { randomUUID } from "crypto";
|
|
40
|
-
import { homedir
|
|
43
|
+
import { homedir } from "os";
|
|
41
44
|
import * as childProcess from "child_process";
|
|
42
|
-
import { appendFileSync, existsSync as
|
|
43
|
-
import { dirname
|
|
45
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, rmSync, statSync as statSync4, writeFileSync } from "fs";
|
|
46
|
+
import { dirname, isAbsolute as isAbsolute3, join as join4, resolve as resolve3 } from "path";
|
|
44
47
|
import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
|
|
45
48
|
import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
|
|
46
|
-
import { atomicWriteJson
|
|
47
|
-
import { defineCommand as defineCommand2 } from "citty";
|
|
48
|
-
import { checkLockOrThrow } from "@fenglimg/fabric-server";
|
|
49
|
-
|
|
50
|
-
// src/commands/config.ts
|
|
51
|
-
import { existsSync as existsSync4 } from "fs";
|
|
52
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
53
|
-
import { resolve as resolve3 } from "path";
|
|
54
|
-
import { fileURLToPath } from "url";
|
|
49
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
55
50
|
import { defineCommand } from "citty";
|
|
56
51
|
|
|
57
|
-
// src/
|
|
58
|
-
import { existsSync
|
|
59
|
-
import { join
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(() => installSharedSkillLib(normalizedTarget)));
|
|
63
|
+
results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
|
|
64
|
+
results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
|
|
65
|
+
results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
|
|
66
|
+
results.push(...await runStep(() => installCitePolicyEvictHook(normalizedTarget)));
|
|
67
|
+
results.push(...await runStep(() => installHookLibs(normalizedTarget)));
|
|
68
|
+
results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
|
|
69
|
+
results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
|
|
70
|
+
results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
|
|
71
|
+
results.push(await runSingleStep("bootstrap-snapshot", () => writeFabricAgentsSnapshot(normalizedTarget)));
|
|
72
|
+
results.push(await runSingleStep("bootstrap-claude", () => writeClaudeBootstrapThinShell(normalizedTarget)));
|
|
73
|
+
results.push(await runSingleStep("bootstrap-codex", () => writeCodexBootstrapManagedBlock(normalizedTarget)));
|
|
74
|
+
results.push(await runSingleStep("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(normalizedTarget)));
|
|
75
|
+
results.push(...validateHookPaths(normalizedTarget));
|
|
76
|
+
return summarizeResults(results);
|
|
77
|
+
}
|
|
78
|
+
function validateHookPaths(projectRoot) {
|
|
79
|
+
const scripts = [
|
|
80
|
+
{ stepSuffix: "", hookFile: "fabric-hint.cjs" },
|
|
81
|
+
{ stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
|
|
82
|
+
{ stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
|
|
83
|
+
];
|
|
84
|
+
const clients = [
|
|
85
|
+
{
|
|
86
|
+
client: "claude",
|
|
87
|
+
configRel: join(".claude", "settings.json"),
|
|
88
|
+
hookDir: join(".claude", "hooks")
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
client: "codex",
|
|
92
|
+
configRel: join(".codex", "hooks.json"),
|
|
93
|
+
hookDir: join(".codex", "hooks")
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
client: "cursor",
|
|
97
|
+
configRel: join(".cursor", "hooks.json"),
|
|
98
|
+
hookDir: join(".cursor", "hooks")
|
|
90
99
|
}
|
|
91
|
-
await writeJsonClientConfig(configPath, {
|
|
92
|
-
command: process.execPath,
|
|
93
|
-
args: [serverPath]
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// src/config/toml.ts
|
|
99
|
-
import { existsSync as existsSync2 } from "fs";
|
|
100
|
-
import { mkdir, readFile } from "fs/promises";
|
|
101
|
-
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
102
|
-
import { homedir as homedir2 } from "os";
|
|
103
|
-
import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
104
|
-
function expandHome(filePath) {
|
|
105
|
-
if (filePath === "~") {
|
|
106
|
-
return homedir2();
|
|
107
|
-
}
|
|
108
|
-
if (filePath.startsWith("~/")) {
|
|
109
|
-
return join2(homedir2(), filePath.slice(2));
|
|
110
|
-
}
|
|
111
|
-
return filePath;
|
|
112
|
-
}
|
|
113
|
-
function escapeTomlString(value) {
|
|
114
|
-
return JSON.stringify(value);
|
|
115
|
-
}
|
|
116
|
-
function serializeTomlStringArray(values) {
|
|
117
|
-
return `[${values.map((value) => escapeTomlString(value)).join(", ")}]`;
|
|
118
|
-
}
|
|
119
|
-
function serializeTomlInlineTable(values) {
|
|
120
|
-
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
|
|
121
|
-
return `{ ${entries.join(", ")} }`;
|
|
122
|
-
}
|
|
123
|
-
function serializeCodexServerBlock(serverName, serverEntry) {
|
|
124
|
-
const lines = [
|
|
125
|
-
`[mcp_servers.${serverName}]`,
|
|
126
|
-
`command = ${escapeTomlString(serverEntry.command)}`,
|
|
127
|
-
`args = ${serializeTomlStringArray(serverEntry.args)}`
|
|
128
100
|
];
|
|
129
|
-
|
|
130
|
-
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const { client, configRel, hookDir } of clients) {
|
|
103
|
+
const configPath = resolve(projectRoot, configRel);
|
|
104
|
+
if (!existsSync(configPath)) {
|
|
105
|
+
results.push({
|
|
106
|
+
step: `hook-validate-${client}`,
|
|
107
|
+
path: configPath,
|
|
108
|
+
status: "skipped",
|
|
109
|
+
message: "missing-config"
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const { stepSuffix, hookFile } of scripts) {
|
|
114
|
+
const expectedHookPath = resolve(projectRoot, hookDir, hookFile);
|
|
115
|
+
const expectedHookRel = join(hookDir, hookFile);
|
|
116
|
+
const step = `hook-validate-${client}${stepSuffix}`;
|
|
117
|
+
if (!existsSync(expectedHookPath)) {
|
|
118
|
+
results.push({
|
|
119
|
+
step,
|
|
120
|
+
path: expectedHookPath,
|
|
121
|
+
status: "error",
|
|
122
|
+
message: `hook script missing: ${expectedHookRel}`
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
|
|
127
|
+
}
|
|
131
128
|
}
|
|
132
|
-
return
|
|
133
|
-
`;
|
|
129
|
+
return results;
|
|
134
130
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const withoutExisting = withoutLegacy.replace(currentPattern, "");
|
|
148
|
-
const trimmed = trimTrailingBlankLines(withoutExisting);
|
|
149
|
-
if (trimmed.length === 0) {
|
|
150
|
-
return block;
|
|
131
|
+
async function runStep(fn) {
|
|
132
|
+
try {
|
|
133
|
+
return await fn();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
step: "hook-install",
|
|
138
|
+
path: "",
|
|
139
|
+
status: "error",
|
|
140
|
+
message: error instanceof Error ? error.message : String(error)
|
|
141
|
+
}
|
|
142
|
+
];
|
|
151
143
|
}
|
|
152
|
-
return `${trimmed}
|
|
153
|
-
|
|
154
|
-
${block}`;
|
|
155
144
|
}
|
|
156
|
-
async function
|
|
145
|
+
async function runSingleStep(step, fn) {
|
|
157
146
|
try {
|
|
158
|
-
return await
|
|
147
|
+
return await fn();
|
|
159
148
|
} catch (error) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
return {
|
|
150
|
+
step,
|
|
151
|
+
path: "",
|
|
152
|
+
status: "error",
|
|
153
|
+
message: error instanceof Error ? error.message : String(error)
|
|
154
|
+
};
|
|
164
155
|
}
|
|
165
156
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
157
|
+
function summarizeResults(results) {
|
|
158
|
+
const installed = [];
|
|
159
|
+
const skipped = [];
|
|
160
|
+
const errors = [];
|
|
161
|
+
for (const r of results) {
|
|
162
|
+
switch (r.status) {
|
|
163
|
+
case "written":
|
|
164
|
+
installed.push(r.path);
|
|
165
|
+
break;
|
|
166
|
+
case "skipped":
|
|
167
|
+
skipped.push(r.path);
|
|
168
|
+
break;
|
|
169
|
+
case "error":
|
|
170
|
+
errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
|
|
171
|
+
break;
|
|
176
172
|
}
|
|
177
|
-
const codexDir = join2(homedir2(), ".codex");
|
|
178
|
-
return existsSync2(codexDir) ? resolve2(join2(codexDir, "config.toml")) : null;
|
|
179
173
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await atomicWriteText(configPath, nextConfig);
|
|
174
|
+
return { installed, skipped, errors };
|
|
175
|
+
}
|
|
176
|
+
function normalizeTarget(targetInput) {
|
|
177
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
178
|
+
}
|
|
179
|
+
function assertExistingDirectory(target) {
|
|
180
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
181
|
+
throw new Error(t("cli.shared.target-invalid", { target }));
|
|
189
182
|
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// src/config/resolver.ts
|
|
193
|
-
import { clientPathsSchema, fabricConfigSchema } from "@fenglimg/fabric-shared";
|
|
194
|
-
function hasExplicitPath(clientPaths, key) {
|
|
195
|
-
return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
|
|
196
|
-
}
|
|
197
|
-
function addIfDetected(writers, detected, createWriter, configuredPath) {
|
|
198
|
-
if (configuredPath !== void 0 || detected) {
|
|
199
|
-
writers.push(createWriter(configuredPath));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
|
|
203
|
-
const clientPaths = fabricConfig.clientPaths;
|
|
204
|
-
const writers = [];
|
|
205
|
-
const claudeMcpScope = opts.claudeMcpScope ?? "project";
|
|
206
|
-
addIfDetected(
|
|
207
|
-
writers,
|
|
208
|
-
existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude")),
|
|
209
|
-
(configuredPath) => new ClaudeCodeCLIWriter(configuredPath, claudeMcpScope),
|
|
210
|
-
hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
|
|
211
|
-
);
|
|
212
|
-
addIfDetected(
|
|
213
|
-
writers,
|
|
214
|
-
existsSync3(getClaudeDesktopConfigPath()),
|
|
215
|
-
(configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
|
|
216
|
-
hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
|
|
217
|
-
);
|
|
218
|
-
addIfDetected(
|
|
219
|
-
writers,
|
|
220
|
-
existsSync3(join3(workspaceRoot, ".cursor")),
|
|
221
|
-
(configuredPath) => new CursorWriter(configuredPath),
|
|
222
|
-
hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
|
|
223
|
-
);
|
|
224
|
-
addIfDetected(
|
|
225
|
-
writers,
|
|
226
|
-
existsSync3(join3(homedir3(), ".codex")),
|
|
227
|
-
(configuredPath) => new CodexTOMLConfigWriter(configuredPath),
|
|
228
|
-
hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
|
|
229
|
-
);
|
|
230
|
-
return writers;
|
|
231
|
-
}
|
|
232
|
-
function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
233
|
-
const clientPaths = fabricConfig.clientPaths;
|
|
234
|
-
const claudeDetected = existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude"));
|
|
235
|
-
const claudeDesktopDetected = existsSync3(getClaudeDesktopConfigPath());
|
|
236
|
-
const cursorDetected = existsSync3(join3(workspaceRoot, ".cursor"));
|
|
237
|
-
const codexDetected = existsSync3(join3(homedir3(), ".codex"));
|
|
238
|
-
return [
|
|
239
|
-
{
|
|
240
|
-
clientKind: "ClaudeCodeCLI",
|
|
241
|
-
label: "Claude Code CLI",
|
|
242
|
-
detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
|
|
243
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
244
|
-
configPath: "project .claude/settings.json",
|
|
245
|
-
capabilities: {
|
|
246
|
-
bootstrap: true,
|
|
247
|
-
mcp: true,
|
|
248
|
-
hook: true,
|
|
249
|
-
skill: true
|
|
250
|
-
},
|
|
251
|
-
installedCapabilities: {
|
|
252
|
-
hook: true,
|
|
253
|
-
skill: true
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
clientKind: "ClaudeCodeDesktop",
|
|
258
|
-
label: "Claude Code Desktop",
|
|
259
|
-
detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
|
|
260
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
261
|
-
configPath: "desktop Claude config",
|
|
262
|
-
capabilities: {
|
|
263
|
-
bootstrap: true,
|
|
264
|
-
mcp: true,
|
|
265
|
-
hook: false,
|
|
266
|
-
skill: false
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
clientKind: "Cursor",
|
|
271
|
-
label: "Cursor",
|
|
272
|
-
detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
|
|
273
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
274
|
-
configPath: ".cursor/mcp.json",
|
|
275
|
-
capabilities: {
|
|
276
|
-
bootstrap: true,
|
|
277
|
-
mcp: true,
|
|
278
|
-
hook: false,
|
|
279
|
-
skill: false
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
clientKind: "CodexCLI",
|
|
284
|
-
label: "Codex CLI",
|
|
285
|
-
detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
|
|
286
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
287
|
-
configPath: "~/.codex/config.toml",
|
|
288
|
-
capabilities: {
|
|
289
|
-
bootstrap: true,
|
|
290
|
-
mcp: true,
|
|
291
|
-
hook: true,
|
|
292
|
-
skill: true
|
|
293
|
-
},
|
|
294
|
-
installedCapabilities: {
|
|
295
|
-
hook: existsSync3(join3(workspaceRoot, ".codex", "hooks.json")),
|
|
296
|
-
// v2/rc.2: v1 client-side init skill removed; skill-installation probes
|
|
297
|
-
// will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
|
|
298
|
-
// fabric-review, fabric-import). Until then there is nothing to probe.
|
|
299
|
-
skill: false
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
];
|
|
303
183
|
}
|
|
304
184
|
|
|
305
|
-
// src/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
"codex-cli": "CodexCLI",
|
|
317
|
-
codex: "CodexCLI"
|
|
318
|
-
};
|
|
319
|
-
function parseClientFilter(value) {
|
|
320
|
-
if (value === void 0 || value.trim().length === 0) {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
const clients = /* @__PURE__ */ new Set();
|
|
324
|
-
for (const rawClient of value.split(",")) {
|
|
325
|
-
const alias = rawClient.trim().toLowerCase();
|
|
326
|
-
const clientKind = CLIENT_ALIASES[alias];
|
|
327
|
-
if (clientKind === void 0) {
|
|
328
|
-
throw new Error(t("cli.config.errors.unknown-client", { client: rawClient }));
|
|
185
|
+
// src/lib/detect-language.ts
|
|
186
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
|
|
187
|
+
import { join as join2 } from "path";
|
|
188
|
+
function detectExistingLanguage(target) {
|
|
189
|
+
const ZH_CN_RATIO_THRESHOLD = 0.3;
|
|
190
|
+
const samples = [];
|
|
191
|
+
const readmePath = join2(target, "README.md");
|
|
192
|
+
if (existsSync2(readmePath)) {
|
|
193
|
+
try {
|
|
194
|
+
samples.push(readFileSync(readmePath, "utf8"));
|
|
195
|
+
} catch {
|
|
329
196
|
}
|
|
330
|
-
clients.add(clientKind);
|
|
331
197
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return parsed;
|
|
344
|
-
}
|
|
345
|
-
function resolveServerPath(override) {
|
|
346
|
-
if (override) return override;
|
|
347
|
-
if (process.env.FAB_SERVER_PATH) return resolve3(process.env.FAB_SERVER_PATH);
|
|
348
|
-
return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
|
|
349
|
-
}
|
|
350
|
-
function writeStderr(message) {
|
|
351
|
-
process.stderr.write(`${message}
|
|
352
|
-
`);
|
|
353
|
-
}
|
|
354
|
-
var configCmd = defineCommand({
|
|
355
|
-
meta: {
|
|
356
|
-
name: "config",
|
|
357
|
-
description: t("cli.config.description")
|
|
358
|
-
},
|
|
359
|
-
subCommands: {
|
|
360
|
-
hooks: hooksCommand,
|
|
361
|
-
install: defineCommand({
|
|
362
|
-
meta: {
|
|
363
|
-
name: "install",
|
|
364
|
-
description: t("cli.config.install.description")
|
|
365
|
-
},
|
|
366
|
-
args: {
|
|
367
|
-
clients: {
|
|
368
|
-
type: "string",
|
|
369
|
-
description: t("cli.config.install.args.clients.description")
|
|
370
|
-
},
|
|
371
|
-
"dry-run": {
|
|
372
|
-
type: "boolean",
|
|
373
|
-
description: t("cli.config.install.args.dry-run.description"),
|
|
374
|
-
default: false
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
async run({ args }) {
|
|
378
|
-
const selectedClients = parseClientFilter(args.clients);
|
|
379
|
-
const result = await installMcpClients(process.cwd(), {
|
|
380
|
-
clients: selectedClients === null ? void 0 : Array.from(selectedClients),
|
|
381
|
-
dryRun: args["dry-run"]
|
|
382
|
-
});
|
|
383
|
-
if (result.details.length === 0) {
|
|
384
|
-
writeStderr(t("cli.config.install.no-configs"));
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
for (const detail of result.details) {
|
|
388
|
-
if (detail.action === "skipped") {
|
|
389
|
-
writeStderr(t("cli.config.install.no-config-path", { client: detail.client }));
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if (detail.action === "dry-run" && detail.path !== null) {
|
|
393
|
-
writeStderr(t("cli.config.install.dry-run", { client: detail.client, path: detail.path }));
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (detail.path !== null) {
|
|
397
|
-
writeStderr(t("cli.config.install.wrote", { client: detail.client, path: detail.path }));
|
|
198
|
+
const docsDir = join2(target, "docs");
|
|
199
|
+
if (existsSync2(docsDir)) {
|
|
200
|
+
try {
|
|
201
|
+
const stat = statSync2(docsDir);
|
|
202
|
+
if (stat.isDirectory()) {
|
|
203
|
+
for (const entry of readdirSync(docsDir, { withFileTypes: true })) {
|
|
204
|
+
if (!entry.isFile()) continue;
|
|
205
|
+
if (!/\.(md|mdx|txt)$/iu.test(entry.name)) continue;
|
|
206
|
+
try {
|
|
207
|
+
samples.push(readFileSync(join2(docsDir, entry.name), "utf8"));
|
|
208
|
+
} catch {
|
|
398
209
|
}
|
|
399
210
|
}
|
|
400
211
|
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
async function installMcpClients(target, options = {}) {
|
|
405
|
-
const workspaceRoot = resolve3(target);
|
|
406
|
-
const fabricConfig = await loadFabricConfig(workspaceRoot);
|
|
407
|
-
const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
|
|
408
|
-
const serverPath = resolveServerPath(options.localServerPath);
|
|
409
|
-
const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
|
|
410
|
-
(writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
|
|
411
|
-
);
|
|
412
|
-
const installed = [];
|
|
413
|
-
const skipped = [];
|
|
414
|
-
const details = [];
|
|
415
|
-
for (const writer of writers) {
|
|
416
|
-
const configPath = await writer.detect(workspaceRoot);
|
|
417
|
-
if (configPath === null) {
|
|
418
|
-
skipped.push(writer.clientKind);
|
|
419
|
-
details.push({ client: writer.clientKind, path: null, action: "skipped" });
|
|
420
|
-
continue;
|
|
212
|
+
} catch {
|
|
421
213
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
214
|
+
}
|
|
215
|
+
if (samples.length === 0) {
|
|
216
|
+
return "en";
|
|
217
|
+
}
|
|
218
|
+
let cjkCount = 0;
|
|
219
|
+
let asciiLetterCount = 0;
|
|
220
|
+
for (const sample of samples) {
|
|
221
|
+
for (const ch of sample) {
|
|
222
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
223
|
+
if (code >= 19968 && code <= 40959) {
|
|
224
|
+
cjkCount += 1;
|
|
225
|
+
} else if (code >= 65 && code <= 90 || code >= 97 && code <= 122) {
|
|
226
|
+
asciiLetterCount += 1;
|
|
227
|
+
}
|
|
426
228
|
}
|
|
427
|
-
await writer.write(serverPath, workspaceRoot);
|
|
428
|
-
installed.push(writer.clientKind);
|
|
429
|
-
details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
|
|
430
229
|
}
|
|
431
|
-
|
|
230
|
+
const denominator = cjkCount + asciiLetterCount;
|
|
231
|
+
if (denominator === 0) {
|
|
232
|
+
return "en";
|
|
233
|
+
}
|
|
234
|
+
const ratio = cjkCount / denominator;
|
|
235
|
+
return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN-hybrid" : "en";
|
|
432
236
|
}
|
|
433
237
|
|
|
434
238
|
// src/scanner/forensic.ts
|
|
435
239
|
import { execFileSync } from "child_process";
|
|
436
|
-
import { existsSync as
|
|
240
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
|
|
437
241
|
import { createRequire } from "module";
|
|
438
|
-
import { basename, extname, isAbsolute, join as
|
|
242
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join3, posix, relative, resolve as resolve2, sep } from "path";
|
|
439
243
|
import {
|
|
440
244
|
forensicReportSchema
|
|
441
245
|
} from "@fenglimg/fabric-shared";
|
|
246
|
+
|
|
247
|
+
// src/scanner/detector.ts
|
|
248
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
249
|
+
|
|
250
|
+
// src/scanner/forensic.ts
|
|
442
251
|
var require2 = createRequire(import.meta.url);
|
|
443
252
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
444
253
|
".fabric",
|
|
@@ -521,7 +330,7 @@ var parserInitPromise = null;
|
|
|
521
330
|
var languagePromiseByKind = {};
|
|
522
331
|
var parserBundlePromiseByKind = {};
|
|
523
332
|
async function buildForensicReport(targetInput) {
|
|
524
|
-
const target =
|
|
333
|
+
const target = normalizeTarget2(targetInput);
|
|
525
334
|
const framework = detectFramework(target);
|
|
526
335
|
const topology = buildTopology(target);
|
|
527
336
|
const entryPoints = collectEntryPoints(target, topology.files);
|
|
@@ -533,7 +342,7 @@ async function buildForensicReport(targetInput) {
|
|
|
533
342
|
const report = {
|
|
534
343
|
version: "1.0",
|
|
535
344
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
536
|
-
generated_by: `
|
|
345
|
+
generated_by: `fabric-cli@${getCliVersion()}`,
|
|
537
346
|
target,
|
|
538
347
|
project_name: readProjectName(target),
|
|
539
348
|
framework,
|
|
@@ -557,11 +366,11 @@ async function buildForensicReport(targetInput) {
|
|
|
557
366
|
}
|
|
558
367
|
return validation.data;
|
|
559
368
|
}
|
|
560
|
-
function
|
|
561
|
-
return
|
|
369
|
+
function normalizeTarget2(targetInput) {
|
|
370
|
+
return isAbsolute2(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
|
|
562
371
|
}
|
|
563
372
|
function buildTopology(root) {
|
|
564
|
-
|
|
373
|
+
assertExistingDirectory2(root);
|
|
565
374
|
const byExt = {};
|
|
566
375
|
const keyDirs = /* @__PURE__ */ new Set();
|
|
567
376
|
const files = [];
|
|
@@ -573,8 +382,8 @@ function buildTopology(root) {
|
|
|
573
382
|
if (current === void 0) {
|
|
574
383
|
continue;
|
|
575
384
|
}
|
|
576
|
-
for (const entry of
|
|
577
|
-
const absolutePath =
|
|
385
|
+
for (const entry of readdirSync2(current, { withFileTypes: true })) {
|
|
386
|
+
const absolutePath = join3(current, entry.name);
|
|
578
387
|
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
579
388
|
if (relativePath.length === 0) {
|
|
580
389
|
continue;
|
|
@@ -594,7 +403,7 @@ function buildTopology(root) {
|
|
|
594
403
|
if (!entry.isFile()) {
|
|
595
404
|
continue;
|
|
596
405
|
}
|
|
597
|
-
const stats =
|
|
406
|
+
const stats = statSync3(absolutePath);
|
|
598
407
|
const extension = extname(entry.name) || "[none]";
|
|
599
408
|
byExt[extension] = (byExt[extension] ?? 0) + 1;
|
|
600
409
|
totalFiles += 1;
|
|
@@ -612,8 +421,8 @@ function buildTopology(root) {
|
|
|
612
421
|
files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
613
422
|
};
|
|
614
423
|
}
|
|
615
|
-
function
|
|
616
|
-
if (!
|
|
424
|
+
function assertExistingDirectory2(target) {
|
|
425
|
+
if (!existsSync3(target) || !statSync3(target).isDirectory()) {
|
|
617
426
|
throw new Error(`Target must be an existing directory: ${target}`);
|
|
618
427
|
}
|
|
619
428
|
}
|
|
@@ -662,7 +471,7 @@ function getEntryPointReason(relativePath) {
|
|
|
662
471
|
async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
|
|
663
472
|
const samples = [];
|
|
664
473
|
for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
|
|
665
|
-
const absolutePath =
|
|
474
|
+
const absolutePath = join3(target, ...entryPoint.path.split("/"));
|
|
666
475
|
const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
|
|
667
476
|
const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
|
|
668
477
|
frameworkKind,
|
|
@@ -682,7 +491,7 @@ async function buildCodeSamples(target, entryPoints, frameworkKind, topology, pa
|
|
|
682
491
|
}
|
|
683
492
|
function readFirstLines(path, lineLimit) {
|
|
684
493
|
try {
|
|
685
|
-
const lines =
|
|
494
|
+
const lines = readFileSync2(path, "utf8").split(/\r?\n/);
|
|
686
495
|
if (lines.at(-1) === "") {
|
|
687
496
|
lines.pop();
|
|
688
497
|
}
|
|
@@ -699,12 +508,12 @@ function readFirstLines(path, lineLimit) {
|
|
|
699
508
|
}
|
|
700
509
|
}
|
|
701
510
|
function readPackageDependencies(target) {
|
|
702
|
-
const packageJsonPath =
|
|
703
|
-
if (!
|
|
511
|
+
const packageJsonPath = join3(target, "package.json");
|
|
512
|
+
if (!existsSync3(packageJsonPath)) {
|
|
704
513
|
return /* @__PURE__ */ new Map();
|
|
705
514
|
}
|
|
706
515
|
try {
|
|
707
|
-
const packageJson = JSON.parse(
|
|
516
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
708
517
|
return new Map([
|
|
709
518
|
...Object.entries(packageJson.dependencies ?? {}),
|
|
710
519
|
...Object.entries(packageJson.devDependencies ?? {}),
|
|
@@ -1040,16 +849,16 @@ function scoreFrameworkConfidence(input) {
|
|
|
1040
849
|
return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
|
|
1041
850
|
}
|
|
1042
851
|
function readReadmeInfo(target) {
|
|
1043
|
-
const readmePath =
|
|
1044
|
-
const hasContributing =
|
|
1045
|
-
if (!
|
|
852
|
+
const readmePath = join3(target, "README.md");
|
|
853
|
+
const hasContributing = existsSync3(join3(target, "CONTRIBUTING.md"));
|
|
854
|
+
if (!existsSync3(readmePath)) {
|
|
1046
855
|
return {
|
|
1047
856
|
quality: "missing",
|
|
1048
857
|
line_count: 0,
|
|
1049
858
|
has_contributing: hasContributing
|
|
1050
859
|
};
|
|
1051
860
|
}
|
|
1052
|
-
const readme =
|
|
861
|
+
const readme = readFileSync2(readmePath, "utf8");
|
|
1053
862
|
const wordCount = readme.trim().split(/\s+/).filter(Boolean).length;
|
|
1054
863
|
return {
|
|
1055
864
|
quality: wordCount >= 200 ? "ok" : "stub",
|
|
@@ -1527,10 +1336,10 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
|
|
|
1527
1336
|
return recommendations;
|
|
1528
1337
|
}
|
|
1529
1338
|
function readProjectName(target) {
|
|
1530
|
-
const packageJsonPath =
|
|
1531
|
-
if (
|
|
1339
|
+
const packageJsonPath = join3(target, "package.json");
|
|
1340
|
+
if (existsSync3(packageJsonPath)) {
|
|
1532
1341
|
try {
|
|
1533
|
-
const packageJson = JSON.parse(
|
|
1342
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
1534
1343
|
if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
|
|
1535
1344
|
return packageJson.name;
|
|
1536
1345
|
}
|
|
@@ -1541,7 +1350,7 @@ function readProjectName(target) {
|
|
|
1541
1350
|
return basename(target);
|
|
1542
1351
|
}
|
|
1543
1352
|
function getCliVersion() {
|
|
1544
|
-
return true ? "2.0.
|
|
1353
|
+
return true ? "2.0.1" : "unknown";
|
|
1545
1354
|
}
|
|
1546
1355
|
function sortRecord(record) {
|
|
1547
1356
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
@@ -1550,109 +1359,139 @@ function toPosixPath(path) {
|
|
|
1550
1359
|
return path.split(sep).join("/");
|
|
1551
1360
|
}
|
|
1552
1361
|
|
|
1553
|
-
// src/commands/
|
|
1554
|
-
var
|
|
1555
|
-
|
|
1556
|
-
This project uses [Fabric](https://github.com/fenglimg/fabric) for cross-client AI knowledge management.
|
|
1557
|
-
|
|
1558
|
-
Knowledge entries live in \`.fabric/knowledge/\` (team) and \`~/.fabric/knowledge/\` (personal).
|
|
1559
|
-
Run \`fabric doctor\` to verify state.
|
|
1560
|
-
|
|
1561
|
-
See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
|
|
1562
|
-
`;
|
|
1563
|
-
var LOCAL_FABRIC_SERVER_PATH = join5("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
|
|
1362
|
+
// src/commands/install.ts
|
|
1363
|
+
var LOCAL_FABRIC_SERVER_PATH = join4("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
|
|
1564
1364
|
var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
|
|
1565
1365
|
var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
|
|
1566
|
-
var
|
|
1366
|
+
var installCommand = defineCommand({
|
|
1567
1367
|
meta: {
|
|
1568
|
-
name: "
|
|
1569
|
-
description: t("cli.
|
|
1368
|
+
name: "install",
|
|
1369
|
+
description: t("cli.install.description")
|
|
1570
1370
|
},
|
|
1571
1371
|
args: {
|
|
1572
|
-
target: {
|
|
1573
|
-
type: "string",
|
|
1574
|
-
description: t("cli.init.args.target.description")
|
|
1575
|
-
},
|
|
1576
1372
|
debug: {
|
|
1577
1373
|
type: "boolean",
|
|
1578
|
-
description: t("cli.
|
|
1374
|
+
description: t("cli.install.args.debug.description"),
|
|
1579
1375
|
default: false
|
|
1580
1376
|
},
|
|
1581
|
-
|
|
1377
|
+
"dry-run": {
|
|
1582
1378
|
type: "boolean",
|
|
1583
|
-
description: t("cli.
|
|
1379
|
+
description: t("cli.install.args.dry-run.description"),
|
|
1584
1380
|
default: false
|
|
1585
1381
|
},
|
|
1382
|
+
target: {
|
|
1383
|
+
type: "string",
|
|
1384
|
+
description: t("cli.install.args.target.description")
|
|
1385
|
+
},
|
|
1586
1386
|
yes: {
|
|
1587
1387
|
type: "boolean",
|
|
1588
|
-
description: t("cli.
|
|
1388
|
+
description: t("cli.install.args.yes.description"),
|
|
1589
1389
|
default: false
|
|
1590
1390
|
},
|
|
1591
|
-
|
|
1391
|
+
"force-skills-only": {
|
|
1592
1392
|
type: "boolean",
|
|
1593
|
-
description: t("cli.
|
|
1393
|
+
description: t("cli.install.args.force-skills-only.description"),
|
|
1594
1394
|
default: false
|
|
1595
1395
|
},
|
|
1596
|
-
|
|
1396
|
+
"force-hooks-only": {
|
|
1597
1397
|
type: "boolean",
|
|
1598
|
-
description: t("cli.
|
|
1398
|
+
description: t("cli.install.args.force-hooks-only.description"),
|
|
1599
1399
|
default: false
|
|
1600
|
-
},
|
|
1601
|
-
bootstrap: {
|
|
1602
|
-
type: "boolean",
|
|
1603
|
-
default: true,
|
|
1604
|
-
negativeDescription: t("cli.init.args.no-bootstrap.description")
|
|
1605
|
-
},
|
|
1606
|
-
mcp: {
|
|
1607
|
-
type: "boolean",
|
|
1608
|
-
default: true,
|
|
1609
|
-
negativeDescription: t("cli.init.args.no-mcp.description")
|
|
1610
|
-
},
|
|
1611
|
-
hooks: {
|
|
1612
|
-
type: "boolean",
|
|
1613
|
-
default: true,
|
|
1614
|
-
negativeDescription: t("cli.init.args.no-hooks.description")
|
|
1615
|
-
},
|
|
1616
|
-
interactive: {
|
|
1617
|
-
type: "boolean",
|
|
1618
|
-
description: t("cli.init.args.interactive.description"),
|
|
1619
|
-
default: true
|
|
1620
|
-
},
|
|
1621
|
-
"mcp-install": {
|
|
1622
|
-
type: "string",
|
|
1623
|
-
default: "global",
|
|
1624
|
-
description: t("cli.init.mcp.install.prompt")
|
|
1625
|
-
},
|
|
1626
|
-
scope: {
|
|
1627
|
-
type: "string",
|
|
1628
|
-
description: t("cli.init.mcp.scope.description")
|
|
1629
1400
|
}
|
|
1630
1401
|
},
|
|
1631
1402
|
async run({ args }) {
|
|
1632
1403
|
await runInitCommand(args);
|
|
1633
1404
|
}
|
|
1634
1405
|
});
|
|
1635
|
-
var
|
|
1406
|
+
var install_default = installCommand;
|
|
1407
|
+
async function runSkillsOnlyRefresh(targetInput) {
|
|
1408
|
+
const target = normalizeTarget3(targetInput);
|
|
1409
|
+
const metaPath = join4(target, ".fabric", "agents.meta.json");
|
|
1410
|
+
if (!existsSync4(metaPath)) {
|
|
1411
|
+
const message = t("cli.install.force-skills-only.uninitialised.message");
|
|
1412
|
+
const hint = t("cli.install.force-skills-only.uninitialised.hint");
|
|
1413
|
+
process.stderr.write(`${message}
|
|
1414
|
+
${hint}
|
|
1415
|
+
`);
|
|
1416
|
+
process.exitCode = 1;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
console.log(formatInitStageHeader(t("cli.install.force-skills-only.banner")));
|
|
1420
|
+
const results = [];
|
|
1421
|
+
results.push(...await cleanupDeprecatedSkills(target));
|
|
1422
|
+
results.push(...await installFabricArchiveSkill(target));
|
|
1423
|
+
results.push(...await installFabricReviewSkill(target));
|
|
1424
|
+
results.push(...await installFabricImportSkill(target));
|
|
1425
|
+
let written = 0;
|
|
1426
|
+
let skipped = 0;
|
|
1427
|
+
let errors = 0;
|
|
1428
|
+
for (const r of results) {
|
|
1429
|
+
if (r.status === "written") written += 1;
|
|
1430
|
+
else if (r.status === "skipped") skipped += 1;
|
|
1431
|
+
else if (r.status === "error") errors += 1;
|
|
1432
|
+
}
|
|
1433
|
+
console.log(
|
|
1434
|
+
t("cli.install.force-skills-only.summary", {
|
|
1435
|
+
written: String(written),
|
|
1436
|
+
skipped: String(skipped),
|
|
1437
|
+
errors: String(errors)
|
|
1438
|
+
})
|
|
1439
|
+
);
|
|
1440
|
+
if (errors > 0) {
|
|
1441
|
+
for (const r of results) {
|
|
1442
|
+
if (r.status === "error") {
|
|
1443
|
+
process.stderr.write(` ${r.step} ${r.path}: ${r.message ?? "error"}
|
|
1444
|
+
`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
process.exitCode = 1;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async function runHooksOnlyRefresh(targetInput) {
|
|
1451
|
+
const target = normalizeTarget3(targetInput);
|
|
1452
|
+
const metaPath = join4(target, ".fabric", "agents.meta.json");
|
|
1453
|
+
if (!existsSync4(metaPath)) {
|
|
1454
|
+
const message = t("cli.install.force-hooks-only.uninitialised.message");
|
|
1455
|
+
const hint = t("cli.install.force-hooks-only.uninitialised.hint");
|
|
1456
|
+
process.stderr.write(`${message}
|
|
1457
|
+
${hint}
|
|
1458
|
+
`);
|
|
1459
|
+
process.exitCode = 1;
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
console.log(formatInitStageHeader(t("cli.install.force-hooks-only.banner")));
|
|
1463
|
+
const result = await installHooks(target);
|
|
1464
|
+
console.log(
|
|
1465
|
+
t("cli.install.force-hooks-only.summary", {
|
|
1466
|
+
written: String(result.installed.length),
|
|
1467
|
+
skipped: String(result.skipped.length),
|
|
1468
|
+
errors: String(result.errors.length)
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
if (result.errors.length > 0) {
|
|
1472
|
+
for (const err of result.errors) {
|
|
1473
|
+
process.stderr.write(` ${err}
|
|
1474
|
+
`);
|
|
1475
|
+
}
|
|
1476
|
+
process.exitCode = 1;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1636
1479
|
async function runInitCommand(args) {
|
|
1637
1480
|
const logger = createDebugLogger(args.debug);
|
|
1638
1481
|
const resolution = resolveDevMode(args.target, process.cwd());
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1482
|
+
if (args["force-skills-only"] === true) {
|
|
1483
|
+
await runSkillsOnlyRefresh(resolution.target);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (args["force-hooks-only"] === true) {
|
|
1487
|
+
await runHooksOnlyRefresh(resolution.target);
|
|
1488
|
+
return;
|
|
1642
1489
|
}
|
|
1490
|
+
const intent = resolveInitCliIntent(args, resolution.target);
|
|
1643
1491
|
logger(`init target source: ${resolution.source}`);
|
|
1644
1492
|
for (const step of resolution.chain) {
|
|
1645
1493
|
logger(step);
|
|
1646
1494
|
}
|
|
1647
|
-
if (intent.options.planOnly) {
|
|
1648
|
-
writeStderr2(t("cli.init.compat.plan"));
|
|
1649
|
-
}
|
|
1650
|
-
if (args.interactive === false) {
|
|
1651
|
-
writeStderr2(t("cli.init.compat.interactive"));
|
|
1652
|
-
}
|
|
1653
|
-
if (args.bootstrap === false || args.mcp === false || args.hooks === false) {
|
|
1654
|
-
writeStderr2(t("cli.init.compat.legacy-stage-flags"));
|
|
1655
|
-
}
|
|
1656
1495
|
const supports = detectClientSupports(intent.target);
|
|
1657
1496
|
const basePlan = await buildInitExecutionPlan({
|
|
1658
1497
|
target: intent.target,
|
|
@@ -1662,81 +1501,107 @@ async function runInitCommand(args) {
|
|
|
1662
1501
|
interactive: intent.interactiveSummary && !intent.wizardEnabled,
|
|
1663
1502
|
supports
|
|
1664
1503
|
});
|
|
1665
|
-
const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan,
|
|
1504
|
+
const plan = intent.wizardEnabled ? await resolveInitExecutionPlanWithWizard(basePlan, createDefaultInitWizardAdapter()) : basePlan;
|
|
1666
1505
|
if (plan === null) {
|
|
1667
1506
|
process.exitCode = 130;
|
|
1668
1507
|
return;
|
|
1669
1508
|
}
|
|
1670
1509
|
const result = await executeInitExecutionPlan(plan);
|
|
1671
|
-
try {
|
|
1672
|
-
await maybeWriteImportSentinel({
|
|
1673
|
-
target: intent.target,
|
|
1674
|
-
planOnly: intent.options.planOnly === true,
|
|
1675
|
-
wizardEnabled: intent.wizardEnabled,
|
|
1676
|
-
terminalInteractive: intent.interactiveSummary
|
|
1677
|
-
});
|
|
1678
|
-
} catch {
|
|
1679
|
-
}
|
|
1680
1510
|
if (!intent.options.planOnly) {
|
|
1511
|
+
console.log("");
|
|
1512
|
+
console.log(t("cli.install.next-steps"));
|
|
1513
|
+
console.log("");
|
|
1681
1514
|
console.log(paint.muted("More: docs/surfaces.md explains when to use CLI vs Skill vs MCP."));
|
|
1682
1515
|
}
|
|
1683
1516
|
return result;
|
|
1684
1517
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
if (
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1518
|
+
function writeDefaultFabricConfig(fabricDir, targetRoot) {
|
|
1519
|
+
const target = join4(fabricDir, "fabric-config.json");
|
|
1520
|
+
if (existsSync4(target)) return;
|
|
1521
|
+
const detectedLanguage = detectExistingLanguage(targetRoot);
|
|
1522
|
+
const FABRIC_CONFIG_DEFAULTS = {
|
|
1523
|
+
// Scan/import language policy. Fixated at init time by probing
|
|
1524
|
+
// README.md + docs/*.md (CJK ratio > 0.3 → "zh-CN", else "en"). Users
|
|
1525
|
+
// can edit `.fabric/fabric-config.json` to override. See
|
|
1526
|
+
// packages/shared/src/schemas/fabric-config.ts for the enum.
|
|
1527
|
+
fabric_language: detectedLanguage,
|
|
1528
|
+
// fabric-hint Stop hook Signal A (archive): time-branch threshold, hours
|
|
1529
|
+
// since last knowledge_proposed event.
|
|
1530
|
+
archive_hint_hours: 24,
|
|
1531
|
+
// fabric-hint Stop hook cooldown after ANY signal fires, in hours.
|
|
1532
|
+
archive_hint_cooldown_hours: 12,
|
|
1533
|
+
// fabric-hint Stop hook Signal B (review): pending-count cutoff.
|
|
1534
|
+
review_hint_pending_count: 10,
|
|
1535
|
+
// fabric-hint Stop hook Signal B (review): pending-age cutoff in days.
|
|
1536
|
+
review_hint_pending_age_days: 7,
|
|
1537
|
+
// fabric-hint Stop hook Signal D (maintenance): days since last doctor.
|
|
1538
|
+
maintenance_hint_days: 14,
|
|
1539
|
+
// fabric-hint Stop hook Signal D (maintenance): cooldown between
|
|
1540
|
+
// reminders, in days.
|
|
1541
|
+
maintenance_hint_cooldown_days: 7,
|
|
1542
|
+
// fabric-hint Stop hook Signal A (archive): edit-count branch threshold;
|
|
1543
|
+
// PreToolUse fires recorded in .fabric/.cache/edit-counter since the
|
|
1544
|
+
// last knowledge_proposed event.
|
|
1545
|
+
archive_edit_threshold: 20,
|
|
1546
|
+
// fabric-hint Stop hook Signal C (import) + doctor lint #22: canonical
|
|
1547
|
+
// knowledge node count below this value flags an underseeded workspace.
|
|
1548
|
+
underseed_node_threshold: 10,
|
|
1549
|
+
// rc.9+ (skill-contract-fix B1): fabric-import first-run git-history
|
|
1550
|
+
// window in months. Default 60 captures the bulk of a mature repo's
|
|
1551
|
+
// signal in one pass; lower to 12-24 for fresh / small repos.
|
|
1552
|
+
import_window_first_run_months: 60,
|
|
1553
|
+
// rc.9+ (skill-contract-fix B1): fabric-import rerun window in months.
|
|
1554
|
+
// Default 2; raise to 6 if the workspace pauses imports for long stretches.
|
|
1555
|
+
import_window_rerun_months: 2,
|
|
1556
|
+
// rc.9+ (skill-contract-fix B1): hard cap on pending entries produced
|
|
1557
|
+
// per fabric-import invocation. Default 10 matches one-sitting triage.
|
|
1558
|
+
import_max_pending_per_run: 10,
|
|
1559
|
+
// rc.9+ (skill-contract-fix B1): hard cap on commits scanned per
|
|
1560
|
+
// fabric-import invocation. Default 500 covers ~2 months of typical churn.
|
|
1561
|
+
import_max_commits_scan: 500,
|
|
1562
|
+
// rc.9+ (skill-contract-fix B1): canonical-node count above which
|
|
1563
|
+
// fabric-import suggests review over importing more. Default 50.
|
|
1564
|
+
import_skip_canonical_threshold: 50,
|
|
1565
|
+
// rc.9+ (skill-contract-fix B1): max candidates per fabric-archive batch.
|
|
1566
|
+
// Default 8 keeps each batch reviewable in one sitting.
|
|
1567
|
+
archive_max_candidates_per_batch: 8,
|
|
1568
|
+
// rc.9+ (skill-contract-fix B1): max recently-touched paths in
|
|
1569
|
+
// fabric-archive's relevance digest. Default 20.
|
|
1570
|
+
archive_max_recent_paths: 20,
|
|
1571
|
+
// rc.9+ (skill-contract-fix B1): max prior fabric-archive sessions
|
|
1572
|
+
// summarised in the digest the skill loads on start. Default 10.
|
|
1573
|
+
archive_digest_max_sessions: 10,
|
|
1574
|
+
// rc.9+ (skill-contract-fix B1): max review results per topic cluster
|
|
1575
|
+
// in fabric-review. Default 8.
|
|
1576
|
+
review_topic_result_cap: 8,
|
|
1577
|
+
// rc.9+ (skill-contract-fix B1): age (days) above which a pending entry
|
|
1578
|
+
// is considered stale by fabric-review. Default 14.
|
|
1579
|
+
review_stale_pending_days: 14
|
|
1580
|
+
};
|
|
1581
|
+
mkdirSync(fabricDir, { recursive: true });
|
|
1582
|
+
writeFileSync(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
|
|
1583
|
+
log.info(
|
|
1584
|
+
`Detected and fixated fabric_language = ${detectedLanguage}; edit ${target} to override.`
|
|
1585
|
+
);
|
|
1705
1586
|
}
|
|
1706
1587
|
function resolveInitCliIntent(args, targetInput) {
|
|
1707
|
-
const target =
|
|
1708
|
-
const mcpInstallMode =
|
|
1709
|
-
const claudeMcpScope =
|
|
1588
|
+
const target = normalizeTarget3(targetInput);
|
|
1589
|
+
const mcpInstallMode = "global";
|
|
1590
|
+
const claudeMcpScope = "project";
|
|
1710
1591
|
const terminalInteractive = isInteractiveInit();
|
|
1711
|
-
const planOnly = args
|
|
1712
|
-
const reapply = args.reapply === true;
|
|
1592
|
+
const planOnly = args["dry-run"] === true;
|
|
1713
1593
|
const options = {
|
|
1714
|
-
|
|
1715
|
-
skipBootstrap: args.bootstrap === false ? true : args.skipBootstrap,
|
|
1716
|
-
skipMcp: args.mcp === false ? true : args.skipMcp,
|
|
1717
|
-
skipHooks: args.hooks === false ? true : args.skipHooks,
|
|
1718
|
-
planOnly,
|
|
1719
|
-
reapply
|
|
1594
|
+
planOnly
|
|
1720
1595
|
};
|
|
1721
1596
|
return {
|
|
1722
1597
|
target,
|
|
1723
1598
|
options,
|
|
1724
1599
|
mcpInstallMode,
|
|
1725
1600
|
claudeMcpScope,
|
|
1726
|
-
interactiveSummary:
|
|
1601
|
+
interactiveSummary: terminalInteractive,
|
|
1727
1602
|
wizardEnabled: shouldUseInitWizard(args, terminalInteractive) && !planOnly
|
|
1728
1603
|
};
|
|
1729
1604
|
}
|
|
1730
|
-
function resolveClaudeMcpScope(raw) {
|
|
1731
|
-
if (raw === void 0 || raw === "project") {
|
|
1732
|
-
return "project";
|
|
1733
|
-
}
|
|
1734
|
-
if (raw === "user") {
|
|
1735
|
-
return "user";
|
|
1736
|
-
}
|
|
1737
|
-
writeStderr2(t("cli.init.mcp.scope.invalid", { value: raw }));
|
|
1738
|
-
return "project";
|
|
1739
|
-
}
|
|
1740
1605
|
async function buildInitExecutionPlan(input) {
|
|
1741
1606
|
const options = input.options ?? {};
|
|
1742
1607
|
const scaffold = await buildInitFabricPlan(input.target, options);
|
|
@@ -1773,17 +1638,17 @@ async function buildInitExecutionPlan(input) {
|
|
|
1773
1638
|
};
|
|
1774
1639
|
}
|
|
1775
1640
|
async function executeInitExecutionPlan(plan) {
|
|
1776
|
-
if (plan.options.force) {
|
|
1777
|
-
writeStderr2(t("cli.init.force.warning", { path: plan.target }));
|
|
1778
|
-
}
|
|
1779
|
-
if (plan.options.reapply && !plan.options.planOnly && !plan.interactive) {
|
|
1780
|
-
writeStderr2(formatInitModeBanner(plan.options));
|
|
1781
|
-
}
|
|
1782
1641
|
if (plan.interactive) {
|
|
1783
1642
|
printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
|
|
1784
1643
|
}
|
|
1644
|
+
const scaffoldStates = [
|
|
1645
|
+
{ path: plan.scaffold.metaPath, state: plan.scaffold.metaState },
|
|
1646
|
+
{ path: plan.scaffold.eventsPath, state: plan.scaffold.eventsState },
|
|
1647
|
+
{ path: plan.scaffold.forensicPath, state: plan.scaffold.forensicState }
|
|
1648
|
+
];
|
|
1785
1649
|
if (plan.options.planOnly) {
|
|
1786
1650
|
printInitPlanPreview(plan);
|
|
1651
|
+
printInitDiffStateTable(scaffoldStates);
|
|
1787
1652
|
return {
|
|
1788
1653
|
plan,
|
|
1789
1654
|
created: buildPlanOnlyScaffoldResult(plan.scaffold),
|
|
@@ -1791,6 +1656,17 @@ async function executeInitExecutionPlan(plan) {
|
|
|
1791
1656
|
finalSupports: plan.supports
|
|
1792
1657
|
};
|
|
1793
1658
|
}
|
|
1659
|
+
if (existsSync4(plan.scaffold.fabricDir) && !statSync4(plan.scaffold.fabricDir).isDirectory()) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
t("cli.install.diff.drift-abort", { path: plan.scaffold.fabricDir })
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
const drifted = scaffoldStates.find(
|
|
1665
|
+
(entry) => entry.state === "drifted" || entry.state === "user-modified"
|
|
1666
|
+
);
|
|
1667
|
+
if (drifted !== void 0) {
|
|
1668
|
+
throw new Error(t("cli.install.diff.drift-abort", { path: drifted.path }));
|
|
1669
|
+
}
|
|
1794
1670
|
let created = null;
|
|
1795
1671
|
const stageResults = [];
|
|
1796
1672
|
let finalSupports = plan.supports;
|
|
@@ -1815,6 +1691,11 @@ async function executeInitExecutionPlan(plan) {
|
|
|
1815
1691
|
exhaustiveInitExecutionStep(step);
|
|
1816
1692
|
}
|
|
1817
1693
|
}
|
|
1694
|
+
if (scaffoldStates.every((entry) => entry.state === "present-canonical")) {
|
|
1695
|
+
console.log(
|
|
1696
|
+
t("cli.install.diff.canonical", { count: String(scaffoldStates.length) })
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1818
1699
|
return {
|
|
1819
1700
|
plan,
|
|
1820
1701
|
created: created ?? unreachableInitScaffold(),
|
|
@@ -1824,23 +1705,26 @@ async function executeInitExecutionPlan(plan) {
|
|
|
1824
1705
|
}
|
|
1825
1706
|
var KNOWLEDGE_SUBDIRS = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
|
|
1826
1707
|
function resolvePersonalFabricRoot() {
|
|
1827
|
-
return process.env.FABRIC_HOME ??
|
|
1708
|
+
return process.env.FABRIC_HOME ?? homedir();
|
|
1828
1709
|
}
|
|
1829
1710
|
async function buildInitFabricPlan(target, options) {
|
|
1830
|
-
|
|
1831
|
-
const fabricDir =
|
|
1832
|
-
const agentsMdPath =
|
|
1833
|
-
const agentsMdAction =
|
|
1834
|
-
const knowledgeDir =
|
|
1835
|
-
const personalKnowledgeDir =
|
|
1836
|
-
const forensicPath =
|
|
1837
|
-
const eventsPath =
|
|
1838
|
-
const metaPath =
|
|
1711
|
+
assertExistingDirectory3(target);
|
|
1712
|
+
const fabricDir = join4(target, ".fabric");
|
|
1713
|
+
const agentsMdPath = join4(target, "AGENTS.md");
|
|
1714
|
+
const agentsMdAction = existsSync4(agentsMdPath) ? "preserved" : "created";
|
|
1715
|
+
const knowledgeDir = join4(fabricDir, "knowledge");
|
|
1716
|
+
const personalKnowledgeDir = join4(resolvePersonalFabricRoot(), ".fabric", "knowledge");
|
|
1717
|
+
const forensicPath = join4(fabricDir, "forensic.json");
|
|
1718
|
+
const eventsPath = join4(fabricDir, "events.jsonl");
|
|
1719
|
+
const metaPath = join4(fabricDir, "agents.meta.json");
|
|
1839
1720
|
const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
|
|
1840
|
-
const knowledgeDirAction =
|
|
1841
|
-
const
|
|
1842
|
-
const
|
|
1843
|
-
const
|
|
1721
|
+
const knowledgeDirAction = existsSync4(knowledgeDir) ? "overwritten" : "created";
|
|
1722
|
+
const metaClassification = classifyFreshPath(metaPath, "structural");
|
|
1723
|
+
const eventsClassification = classifyFreshPath(eventsPath, "presence");
|
|
1724
|
+
const forensicClassification = classifyFreshPath(forensicPath, "always-rewrite");
|
|
1725
|
+
const metaAction = diffStateToWriteAction(metaClassification.state);
|
|
1726
|
+
const eventsAction = diffStateToWriteAction(eventsClassification.state);
|
|
1727
|
+
const forensicAction = diffStateToWriteAction(forensicClassification.state);
|
|
1844
1728
|
const forensicReport = await buildForensicReport(target);
|
|
1845
1729
|
const meta = createInitialMeta();
|
|
1846
1730
|
return {
|
|
@@ -1860,60 +1744,63 @@ async function buildInitFabricPlan(target, options) {
|
|
|
1860
1744
|
eventsAction,
|
|
1861
1745
|
forensicPath,
|
|
1862
1746
|
forensicAction,
|
|
1863
|
-
forensicReport
|
|
1747
|
+
forensicReport,
|
|
1748
|
+
metaState: metaClassification.state,
|
|
1749
|
+
eventsState: eventsClassification.state,
|
|
1750
|
+
forensicState: forensicClassification.state
|
|
1864
1751
|
};
|
|
1865
1752
|
}
|
|
1866
1753
|
async function executeInitFabricPlan(plan) {
|
|
1867
|
-
const isReapply = plan.options?.reapply === true;
|
|
1868
1754
|
if (plan.replaceFabricDir) {
|
|
1869
1755
|
rmSync(plan.fabricDir, { force: true });
|
|
1870
1756
|
}
|
|
1871
1757
|
mkdirSync(plan.fabricDir, { recursive: true });
|
|
1872
|
-
|
|
1873
|
-
await atomicWriteText2(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
|
|
1874
|
-
}
|
|
1758
|
+
writeDefaultFabricConfig(plan.fabricDir, plan.target);
|
|
1875
1759
|
mkdirSync(plan.knowledgeDir, { recursive: true });
|
|
1876
1760
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
1877
|
-
const teamSubDir =
|
|
1761
|
+
const teamSubDir = join4(plan.knowledgeDir, sub);
|
|
1878
1762
|
mkdirSync(teamSubDir, { recursive: true });
|
|
1879
|
-
const teamGitkeep =
|
|
1880
|
-
if (!
|
|
1763
|
+
const teamGitkeep = join4(teamSubDir, ".gitkeep");
|
|
1764
|
+
if (!existsSync4(teamGitkeep)) {
|
|
1881
1765
|
writeFileSync(teamGitkeep, "", "utf8");
|
|
1882
1766
|
}
|
|
1883
1767
|
}
|
|
1884
1768
|
try {
|
|
1885
1769
|
mkdirSync(plan.personalKnowledgeDir, { recursive: true });
|
|
1886
1770
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
1887
|
-
mkdirSync(
|
|
1771
|
+
mkdirSync(join4(plan.personalKnowledgeDir, sub), { recursive: true });
|
|
1888
1772
|
}
|
|
1889
1773
|
} catch {
|
|
1890
1774
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
writeFileSync(plan.eventsPath, "", "utf8");
|
|
1897
|
-
}
|
|
1898
|
-
} else {
|
|
1775
|
+
if (plan.metaState === "missing") {
|
|
1776
|
+
preparePlannedPath(plan.metaPath, plan.metaAction);
|
|
1777
|
+
await atomicWriteJson(plan.metaPath, plan.meta);
|
|
1778
|
+
}
|
|
1779
|
+
if (plan.eventsState === "missing") {
|
|
1899
1780
|
preparePlannedPath(plan.eventsPath, plan.eventsAction);
|
|
1781
|
+
mkdirSync(dirname(plan.eventsPath), { recursive: true });
|
|
1900
1782
|
writeFileSync(plan.eventsPath, "", "utf8");
|
|
1901
1783
|
}
|
|
1902
1784
|
preparePlannedPath(plan.forensicPath, plan.forensicAction);
|
|
1903
1785
|
await atomicWriteJson(plan.forensicPath, plan.forensicReport);
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1786
|
+
if (existsSync4(plan.eventsPath)) {
|
|
1787
|
+
const applied = [];
|
|
1788
|
+
const canonical = [];
|
|
1789
|
+
const drifted = [];
|
|
1790
|
+
for (const entry of [
|
|
1791
|
+
{ path: plan.metaPath, state: plan.metaState },
|
|
1792
|
+
{ path: plan.eventsPath, state: plan.eventsState },
|
|
1793
|
+
{ path: plan.forensicPath, state: plan.forensicState }
|
|
1794
|
+
]) {
|
|
1795
|
+
if (entry.state === "missing") {
|
|
1796
|
+
applied.push(entry.path);
|
|
1797
|
+
} else if (entry.state === "present-canonical") {
|
|
1798
|
+
canonical.push(entry.path);
|
|
1799
|
+
} else {
|
|
1800
|
+
drifted.push(entry.path);
|
|
1801
|
+
}
|
|
1911
1802
|
}
|
|
1912
|
-
|
|
1913
|
-
if (isReapply) {
|
|
1914
|
-
appendReapplyLedgerEvent(plan.eventsPath, {
|
|
1915
|
-
preserved_ledger: true
|
|
1916
|
-
});
|
|
1803
|
+
appendInstallDiffLedgerEvent(plan.eventsPath, { applied, canonical, drifted });
|
|
1917
1804
|
}
|
|
1918
1805
|
return {
|
|
1919
1806
|
agentsMdPath: plan.agentsMdPath,
|
|
@@ -1933,16 +1820,16 @@ async function initFabric(target, options) {
|
|
|
1933
1820
|
return await executeInitFabricPlan(await buildInitFabricPlan(target, options));
|
|
1934
1821
|
}
|
|
1935
1822
|
function shouldUseInitWizard(args, terminalInteractive = isInteractiveInit()) {
|
|
1936
|
-
return terminalInteractive && args.
|
|
1823
|
+
return terminalInteractive && args.yes !== true;
|
|
1937
1824
|
}
|
|
1938
|
-
async function resolveInitExecutionPlanWithWizard(basePlan,
|
|
1825
|
+
async function resolveInitExecutionPlanWithWizard(basePlan, wizardAdapter) {
|
|
1939
1826
|
const selection = await wizardAdapter.run({
|
|
1940
1827
|
target: basePlan.target,
|
|
1941
1828
|
options: basePlan.options,
|
|
1942
1829
|
supports: basePlan.supports,
|
|
1943
1830
|
mcpInstallMode: basePlan.mcpInstallMode,
|
|
1944
1831
|
claudeMcpScope: basePlan.claudeMcpScope,
|
|
1945
|
-
lockedStages:
|
|
1832
|
+
lockedStages: []
|
|
1946
1833
|
});
|
|
1947
1834
|
if (selection === null) {
|
|
1948
1835
|
return null;
|
|
@@ -1980,27 +1867,36 @@ function printInitScaffoldResult(created) {
|
|
|
1980
1867
|
function printInitPostSetup(plan, stageResults, finalSupports) {
|
|
1981
1868
|
if (shouldPrintHooksNextStep(plan.options, stageResults)) {
|
|
1982
1869
|
console.log(
|
|
1983
|
-
t("cli.
|
|
1870
|
+
t("cli.install.next-step", {
|
|
1984
1871
|
label: nextLabel(),
|
|
1985
|
-
message: paint.muted(t("cli.
|
|
1872
|
+
message: paint.muted(t("cli.install.next-step.message"))
|
|
1986
1873
|
})
|
|
1987
1874
|
);
|
|
1988
1875
|
}
|
|
1989
1876
|
console.log(
|
|
1990
|
-
t("cli.
|
|
1877
|
+
t("cli.install.reason-message", {
|
|
1991
1878
|
label: reasonLabel(),
|
|
1992
1879
|
message: paint.muted(formatInitReasonMessage(finalSupports))
|
|
1993
1880
|
})
|
|
1994
1881
|
);
|
|
1995
1882
|
printInitStageSummary(stageResults);
|
|
1996
1883
|
printInitCapabilitySummary(finalSupports, stageResults, plan.options);
|
|
1884
|
+
const fabricLanguage = readFabricLanguagePreference(plan.target);
|
|
1885
|
+
console.log(
|
|
1886
|
+
paint.muted(t("cli.install.language_preference_hint", { value: fabricLanguage }))
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
function printInitDiffStateTable(entries) {
|
|
1890
|
+
for (const entry of entries) {
|
|
1891
|
+
console.log(` ${formatDiffFileState(entry.state)} ${entry.path}`);
|
|
1892
|
+
}
|
|
1997
1893
|
}
|
|
1998
1894
|
function printInitPlanPreview(plan) {
|
|
1999
|
-
console.log(t("cli.
|
|
1895
|
+
console.log(t("cli.install.plan.preview-title"));
|
|
2000
1896
|
printInitPlanSummary(plan.target, plan.options, plan.mcpInstallMode, plan.supports);
|
|
2001
1897
|
console.log(
|
|
2002
|
-
t("cli.
|
|
2003
|
-
mode:
|
|
1898
|
+
t("cli.install.plan.preview-result", {
|
|
1899
|
+
mode: t("cli.install.mode.default"),
|
|
2004
1900
|
bootstrap: yesNoLabel(!plan.options.skipBootstrap),
|
|
2005
1901
|
mcp: yesNoLabel(!plan.options.skipMcp),
|
|
2006
1902
|
hooks: yesNoLabel(!plan.options.skipHooks)
|
|
@@ -2030,27 +1926,32 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
2030
1926
|
if (stage.skipped) {
|
|
2031
1927
|
return { name: stageName, disposition: "skipped" };
|
|
2032
1928
|
}
|
|
2033
|
-
console.log(formatInitStageHeader(t(`cli.
|
|
1929
|
+
console.log(formatInitStageHeader(t(`cli.install.stages.${stageName}`)));
|
|
2034
1930
|
try {
|
|
2035
1931
|
switch (stage.name) {
|
|
2036
1932
|
case "bootstrap": {
|
|
2037
1933
|
const installResults = [];
|
|
1934
|
+
installResults.push(...await runBestEffort("skill-deprecated-cleanup", () => cleanupDeprecatedSkills(plan.target)));
|
|
2038
1935
|
installResults.push(...await runBestEffort("skill-install", () => installFabricArchiveSkill(plan.target)));
|
|
2039
1936
|
installResults.push(...await runBestEffort("skill-review-install", () => installFabricReviewSkill(plan.target)));
|
|
2040
1937
|
installResults.push(...await runBestEffort("skill-import-install", () => installFabricImportSkill(plan.target)));
|
|
2041
1938
|
installResults.push(...await runBestEffort("hook-script", () => installArchiveHintHook(plan.target)));
|
|
2042
1939
|
installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
|
|
2043
1940
|
installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
|
|
1941
|
+
installResults.push(...await runBestEffort("hook-lib", () => installHookLibs(plan.target)));
|
|
2044
1942
|
installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
|
|
2045
1943
|
installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
|
|
2046
1944
|
installResults.push(await runBestEffortSingle("cursor-hook-config", () => mergeCursorHookConfig(plan.target)));
|
|
2047
|
-
installResults.push(
|
|
1945
|
+
installResults.push(await runBestEffortSingle("bootstrap-snapshot", () => writeFabricAgentsSnapshot(plan.target)));
|
|
1946
|
+
installResults.push(await runBestEffortSingle("bootstrap-claude", () => writeClaudeBootstrapThinShell(plan.target)));
|
|
1947
|
+
installResults.push(await runBestEffortSingle("bootstrap-codex", () => writeCodexBootstrapManagedBlock(plan.target)));
|
|
1948
|
+
installResults.push(await runBestEffortSingle("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(plan.target)));
|
|
2048
1949
|
const installedCount = installResults.filter((r) => r.status === "written").length;
|
|
2049
1950
|
const skippedCount = installResults.filter((r) => r.status === "skipped").length;
|
|
2050
1951
|
const errorCount = installResults.filter((r) => r.status === "error").length;
|
|
2051
1952
|
for (const result of installResults) {
|
|
2052
1953
|
if (result.status === "error") {
|
|
2053
|
-
|
|
1954
|
+
writeStderr(`bootstrap ${result.step} ${result.path}: ${result.message ?? "unknown error"}`);
|
|
2054
1955
|
}
|
|
2055
1956
|
}
|
|
2056
1957
|
const note2 = errorCount > 0 ? `errors=${errorCount}` : void 0;
|
|
@@ -2060,15 +1961,14 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
2060
1961
|
case "mcp": {
|
|
2061
1962
|
if (stage.installMode === "local") {
|
|
2062
1963
|
const manager = stage.packageManager ?? detectPackageManager(plan.target);
|
|
2063
|
-
|
|
2064
|
-
|
|
1964
|
+
writeStderr(t("cli.install.mcp.install.local"));
|
|
1965
|
+
writeStderr(t("cli.install.mcp.local.installing", { manager }));
|
|
2065
1966
|
installLocalFabricServer(plan.target, manager);
|
|
2066
|
-
|
|
1967
|
+
writeStderr(t("cli.install.mcp.local.installed"));
|
|
2067
1968
|
} else {
|
|
2068
|
-
|
|
1969
|
+
writeStderr(t("cli.install.mcp.install.global"));
|
|
2069
1970
|
}
|
|
2070
1971
|
const result = await installMcpClients(plan.target, {
|
|
2071
|
-
force: plan.options.force,
|
|
2072
1972
|
localServerPath: stage.localServerPath,
|
|
2073
1973
|
claudeMcpScope: stage.claudeMcpScope
|
|
2074
1974
|
});
|
|
@@ -2080,7 +1980,7 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
2080
1980
|
return { name: "mcp", disposition: "ran" };
|
|
2081
1981
|
}
|
|
2082
1982
|
case "hooks": {
|
|
2083
|
-
const result = await installHooks(plan.target
|
|
1983
|
+
const result = await installHooks(plan.target);
|
|
2084
1984
|
console.log(formatInitStageResult("hooks", "completed", result.installed.length, result.skipped.length));
|
|
2085
1985
|
return { name: "hooks", disposition: "ran" };
|
|
2086
1986
|
}
|
|
@@ -2088,93 +1988,129 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
2088
1988
|
return exhaustiveInitStagePlan(stage);
|
|
2089
1989
|
}
|
|
2090
1990
|
} catch (error) {
|
|
2091
|
-
|
|
1991
|
+
writeStderr(formatInitStageFailure(stageName, error));
|
|
2092
1992
|
return { name: stageName, disposition: "failed" };
|
|
2093
1993
|
}
|
|
2094
1994
|
}
|
|
2095
|
-
function shouldReplaceWritableDirectory(path,
|
|
2096
|
-
if (!
|
|
1995
|
+
function shouldReplaceWritableDirectory(path, _options) {
|
|
1996
|
+
if (!existsSync4(path)) {
|
|
2097
1997
|
return false;
|
|
2098
1998
|
}
|
|
2099
|
-
if (
|
|
1999
|
+
if (statSync4(path).isDirectory()) {
|
|
2100
2000
|
return false;
|
|
2101
2001
|
}
|
|
2102
|
-
|
|
2103
|
-
throw new Error(t("cli.init.errors.abort-existing", { path }));
|
|
2104
|
-
}
|
|
2105
|
-
return true;
|
|
2002
|
+
return false;
|
|
2106
2003
|
}
|
|
2107
|
-
function
|
|
2108
|
-
if (!
|
|
2109
|
-
return "
|
|
2004
|
+
function classifyFreshPath(path, strategy) {
|
|
2005
|
+
if (!existsSync4(path)) {
|
|
2006
|
+
return { path, state: "missing" };
|
|
2007
|
+
}
|
|
2008
|
+
let stat;
|
|
2009
|
+
try {
|
|
2010
|
+
stat = statSync4(path);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
return {
|
|
2013
|
+
path,
|
|
2014
|
+
state: "user-modified",
|
|
2015
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
if (!stat.isFile()) {
|
|
2019
|
+
return { path, state: "user-modified", reason: "expected a file" };
|
|
2110
2020
|
}
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2021
|
+
if (strategy === "presence" || strategy === "always-rewrite") {
|
|
2022
|
+
return { path, state: "present-canonical" };
|
|
2023
|
+
}
|
|
2024
|
+
try {
|
|
2025
|
+
const raw = readFileSync3(path, "utf8");
|
|
2026
|
+
const parsed = JSON.parse(raw);
|
|
2027
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2028
|
+
return { path, state: "user-modified", reason: "not a JSON object" };
|
|
2029
|
+
}
|
|
2030
|
+
const record = parsed;
|
|
2031
|
+
const hasRevision = typeof record["revision"] === "string";
|
|
2032
|
+
const hasNodes = record["nodes"] !== void 0 && record["nodes"] !== null && typeof record["nodes"] === "object" && !Array.isArray(record["nodes"]);
|
|
2033
|
+
const hasCounters = record["counters"] !== void 0 && record["counters"] !== null && typeof record["counters"] === "object" && !Array.isArray(record["counters"]);
|
|
2034
|
+
if (!hasRevision || !hasNodes || !hasCounters) {
|
|
2035
|
+
return { path, state: "drifted", reason: "missing required AgentsMeta fields" };
|
|
2036
|
+
}
|
|
2037
|
+
return { path, state: "present-canonical" };
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
return {
|
|
2040
|
+
path,
|
|
2041
|
+
state: "user-modified",
|
|
2042
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2043
|
+
};
|
|
2113
2044
|
}
|
|
2114
|
-
|
|
2045
|
+
}
|
|
2046
|
+
function diffStateToWriteAction(_state) {
|
|
2047
|
+
return "created";
|
|
2048
|
+
}
|
|
2049
|
+
function formatDiffFileState(state) {
|
|
2050
|
+
return t(`cli.install.diff.state.${state}`);
|
|
2115
2051
|
}
|
|
2116
2052
|
function preparePlannedPath(path, action) {
|
|
2117
|
-
mkdirSync(
|
|
2118
|
-
if (action === "overwritten" &&
|
|
2053
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2054
|
+
if (action === "overwritten" && existsSync4(path)) {
|
|
2119
2055
|
rmSync(path, { recursive: true, force: true });
|
|
2120
2056
|
}
|
|
2121
2057
|
}
|
|
2122
2058
|
function createDefaultInitWizardAdapter() {
|
|
2123
2059
|
return {
|
|
2124
2060
|
async run(context) {
|
|
2125
|
-
intro(t("cli.
|
|
2061
|
+
intro(t("cli.install.wizard.intro"));
|
|
2126
2062
|
note(
|
|
2127
|
-
t("cli.
|
|
2063
|
+
t("cli.install.wizard.overview.body", {
|
|
2128
2064
|
target: context.target,
|
|
2129
2065
|
mode: formatInitModeBadge(context.options)
|
|
2130
2066
|
}),
|
|
2131
|
-
t("cli.
|
|
2067
|
+
t("cli.install.wizard.overview.title")
|
|
2132
2068
|
);
|
|
2133
2069
|
printInitPlanSummary(context.target, context.options, context.mcpInstallMode, context.supports);
|
|
2134
|
-
log.step(t("cli.
|
|
2070
|
+
log.step(t("cli.install.wizard.step.target"));
|
|
2135
2071
|
const continueWithTarget = await confirm({
|
|
2136
|
-
message: t("cli.
|
|
2072
|
+
message: t("cli.install.wizard.target.confirm", { target: context.target }),
|
|
2137
2073
|
initialValue: true
|
|
2138
2074
|
});
|
|
2139
2075
|
if (isCancel(continueWithTarget) || !continueWithTarget) {
|
|
2140
2076
|
emitInitWizardCancellation();
|
|
2141
2077
|
return null;
|
|
2142
2078
|
}
|
|
2143
|
-
log.step(t("cli.
|
|
2079
|
+
log.step(t("cli.install.wizard.step.plan"));
|
|
2144
2080
|
let groupedSelection;
|
|
2145
2081
|
try {
|
|
2146
2082
|
groupedSelection = await group(
|
|
2147
2083
|
{
|
|
2148
2084
|
bootstrap: async () => context.lockedStages.includes("bootstrap") ? false : confirmInGroup({
|
|
2149
|
-
message: t("cli.
|
|
2085
|
+
message: t("cli.install.wizard.stage.bootstrap", {
|
|
2150
2086
|
defaultValue: formatPromptDefault(!context.options.skipBootstrap)
|
|
2151
2087
|
}),
|
|
2152
2088
|
initialValue: !context.options.skipBootstrap
|
|
2153
2089
|
}),
|
|
2154
2090
|
mcp: async () => context.lockedStages.includes("mcp") ? false : confirmInGroup({
|
|
2155
|
-
message: t("cli.
|
|
2091
|
+
message: t("cli.install.wizard.stage.mcp", {
|
|
2156
2092
|
defaultValue: formatPromptDefault(!context.options.skipMcp)
|
|
2157
2093
|
}),
|
|
2158
2094
|
initialValue: !context.options.skipMcp
|
|
2159
2095
|
}),
|
|
2160
2096
|
mcpInstallMode: async ({ results }) => results.mcp ? selectMcpInstallModeInGroup({
|
|
2161
|
-
message: t("cli.
|
|
2097
|
+
message: t("cli.install.wizard.mcp-install", { defaultValue: context.mcpInstallMode }),
|
|
2162
2098
|
initialValue: context.mcpInstallMode,
|
|
2163
2099
|
options: [
|
|
2164
|
-
{ value: "global", label: "global", hint: t("cli.
|
|
2165
|
-
{ value: "local", label: "local", hint: t("cli.
|
|
2100
|
+
{ value: "global", label: "global", hint: t("cli.install.mcp.install.global") },
|
|
2101
|
+
{ value: "local", label: "local", hint: t("cli.install.mcp.install.local") }
|
|
2166
2102
|
]
|
|
2167
2103
|
}) : context.mcpInstallMode,
|
|
2168
2104
|
claudeMcpScope: async ({ results }) => results.mcp ? selectClaudeMcpScopeInGroup({
|
|
2169
|
-
message: t("cli.
|
|
2105
|
+
message: t("cli.install.wizard.mcp-scope", { defaultValue: context.claudeMcpScope }),
|
|
2170
2106
|
initialValue: context.claudeMcpScope,
|
|
2171
2107
|
options: [
|
|
2172
|
-
{ value: "project", label: "project", hint: t("cli.
|
|
2173
|
-
{ value: "user", label: "user", hint: t("cli.
|
|
2108
|
+
{ value: "project", label: "project", hint: t("cli.install.mcp.scope.project") },
|
|
2109
|
+
{ value: "user", label: "user", hint: t("cli.install.mcp.scope.user") }
|
|
2174
2110
|
]
|
|
2175
2111
|
}) : context.claudeMcpScope,
|
|
2176
2112
|
hooks: async () => context.lockedStages.includes("hooks") ? false : confirmInGroup({
|
|
2177
|
-
message: t("cli.
|
|
2113
|
+
message: t("cli.install.wizard.stage.hooks", {
|
|
2178
2114
|
defaultValue: formatPromptDefault(!context.options.skipHooks)
|
|
2179
2115
|
}),
|
|
2180
2116
|
initialValue: !context.options.skipHooks
|
|
@@ -2203,23 +2139,23 @@ function createDefaultInitWizardAdapter() {
|
|
|
2203
2139
|
skipMcp: !groupedSelection.mcp,
|
|
2204
2140
|
skipHooks: !groupedSelection.hooks
|
|
2205
2141
|
};
|
|
2206
|
-
log.step(t("cli.
|
|
2142
|
+
log.step(t("cli.install.wizard.step.review"));
|
|
2207
2143
|
printInitPlanSummary(context.target, previewOptions, groupedSelection.mcpInstallMode, context.supports);
|
|
2208
2144
|
const confirmed = await confirm({
|
|
2209
|
-
message: t("cli.
|
|
2145
|
+
message: t("cli.install.wizard.execute.confirm"),
|
|
2210
2146
|
initialValue: true
|
|
2211
2147
|
});
|
|
2212
2148
|
if (isCancel(confirmed) || !confirmed) {
|
|
2213
2149
|
emitInitWizardCancellation();
|
|
2214
2150
|
return null;
|
|
2215
2151
|
}
|
|
2216
|
-
outro(t("cli.
|
|
2152
|
+
outro(t("cli.install.wizard.outro"));
|
|
2217
2153
|
return groupedSelection;
|
|
2218
2154
|
}
|
|
2219
2155
|
};
|
|
2220
2156
|
}
|
|
2221
2157
|
function emitInitWizardCancellation() {
|
|
2222
|
-
cancel(t("cli.
|
|
2158
|
+
cancel(t("cli.install.wizard.cancelled"));
|
|
2223
2159
|
}
|
|
2224
2160
|
async function confirmInGroup(options) {
|
|
2225
2161
|
const result = await confirm(options);
|
|
@@ -2250,74 +2186,42 @@ async function selectClaudeMcpScopeInGroup(options) {
|
|
|
2250
2186
|
}
|
|
2251
2187
|
return result;
|
|
2252
2188
|
}
|
|
2253
|
-
function collectLockedWizardStages(args) {
|
|
2254
|
-
const lockedStages = [];
|
|
2255
|
-
if (args.bootstrap === false) {
|
|
2256
|
-
lockedStages.push("bootstrap");
|
|
2257
|
-
}
|
|
2258
|
-
if (args.mcp === false) {
|
|
2259
|
-
lockedStages.push("mcp");
|
|
2260
|
-
}
|
|
2261
|
-
if (args.hooks === false) {
|
|
2262
|
-
lockedStages.push("hooks");
|
|
2263
|
-
}
|
|
2264
|
-
return lockedStages;
|
|
2265
|
-
}
|
|
2266
2189
|
function formatPromptDefault(value) {
|
|
2267
2190
|
return value ? "Y/n" : "y/N";
|
|
2268
2191
|
}
|
|
2269
2192
|
function formatInitModeBanner(options) {
|
|
2270
|
-
if (options.planOnly && options.reapply) {
|
|
2271
|
-
return t("cli.init.plan.mode-banner.plan-reapply");
|
|
2272
|
-
}
|
|
2273
2193
|
if (options.planOnly) {
|
|
2274
|
-
return t("cli.
|
|
2275
|
-
}
|
|
2276
|
-
if (options.reapply) {
|
|
2277
|
-
return t("cli.init.plan.mode-banner.reapply");
|
|
2194
|
+
return t("cli.install.plan.mode-banner.plan");
|
|
2278
2195
|
}
|
|
2279
|
-
return t("cli.
|
|
2196
|
+
return t("cli.install.plan.mode-banner.default");
|
|
2280
2197
|
}
|
|
2281
2198
|
function formatInitModeBadge(options) {
|
|
2282
|
-
if (options.planOnly && options.reapply) {
|
|
2283
|
-
return t("cli.init.mode.badge.plan-reapply");
|
|
2284
|
-
}
|
|
2285
2199
|
if (options.planOnly) {
|
|
2286
|
-
return t("cli.
|
|
2287
|
-
}
|
|
2288
|
-
if (options.reapply) {
|
|
2289
|
-
return t("cli.init.mode.badge.reapply");
|
|
2200
|
+
return t("cli.install.mode.badge.plan");
|
|
2290
2201
|
}
|
|
2291
|
-
return t("cli.
|
|
2202
|
+
return t("cli.install.mode.badge.default");
|
|
2292
2203
|
}
|
|
2293
|
-
function
|
|
2294
|
-
return
|
|
2204
|
+
function normalizeTarget3(targetInput) {
|
|
2205
|
+
return isAbsolute3(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
2295
2206
|
}
|
|
2296
|
-
function
|
|
2297
|
-
if (!
|
|
2207
|
+
function assertExistingDirectory3(target) {
|
|
2208
|
+
if (!existsSync4(target) || !statSync4(target).isDirectory()) {
|
|
2298
2209
|
throw new Error(`Target must be an existing directory: ${target}`);
|
|
2299
2210
|
}
|
|
2300
2211
|
}
|
|
2301
2212
|
function detectPackageManager(cwd) {
|
|
2302
|
-
const workspaceRoot =
|
|
2303
|
-
if (
|
|
2213
|
+
const workspaceRoot = resolve3(cwd);
|
|
2214
|
+
if (existsSync4(join4(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
2304
2215
|
return "pnpm";
|
|
2305
2216
|
}
|
|
2306
|
-
if (
|
|
2217
|
+
if (existsSync4(join4(workspaceRoot, "yarn.lock"))) {
|
|
2307
2218
|
return "yarn";
|
|
2308
2219
|
}
|
|
2309
|
-
if (
|
|
2220
|
+
if (existsSync4(join4(workspaceRoot, "package-lock.json"))) {
|
|
2310
2221
|
return "npm";
|
|
2311
2222
|
}
|
|
2312
2223
|
return "npm";
|
|
2313
2224
|
}
|
|
2314
|
-
function resolveMcpInstallMode(rawMode) {
|
|
2315
|
-
if (rawMode === void 0 || rawMode === "global" || rawMode === "local") {
|
|
2316
|
-
return rawMode ?? "global";
|
|
2317
|
-
}
|
|
2318
|
-
writeStderr2(t("cli.init.mcp.install.invalid", { value: rawMode }));
|
|
2319
|
-
return "global";
|
|
2320
|
-
}
|
|
2321
2225
|
function installLocalFabricServer(target, manager) {
|
|
2322
2226
|
const installArgs = manager === "npm" ? ["install", "-D", FABRIC_SERVER_PACKAGE] : ["add", "-D", FABRIC_SERVER_PACKAGE];
|
|
2323
2227
|
childProcess.execFileSync(manager, installArgs, {
|
|
@@ -2333,14 +2237,16 @@ function createInitialMeta() {
|
|
|
2333
2237
|
counters: defaultAgentsMetaCounters()
|
|
2334
2238
|
};
|
|
2335
2239
|
}
|
|
2336
|
-
function
|
|
2240
|
+
function appendInstallDiffLedgerEvent(eventsPath, payload) {
|
|
2337
2241
|
const event = {
|
|
2338
2242
|
kind: "fabric-event",
|
|
2339
2243
|
id: `event:${randomUUID()}`,
|
|
2340
2244
|
ts: Date.now(),
|
|
2341
2245
|
schema_version: 1,
|
|
2342
|
-
event_type: "
|
|
2343
|
-
|
|
2246
|
+
event_type: "install_diff_applied",
|
|
2247
|
+
applied: payload.applied,
|
|
2248
|
+
canonical: payload.canonical,
|
|
2249
|
+
drifted: payload.drifted
|
|
2344
2250
|
};
|
|
2345
2251
|
const line = `${JSON.stringify(event)}
|
|
2346
2252
|
`;
|
|
@@ -2391,7 +2297,7 @@ function printInitStageSummary(stageResults) {
|
|
|
2391
2297
|
console.log(formatInitStageSummaryLine("failed", collectInitStageNames(stageResults, "failed")));
|
|
2392
2298
|
}
|
|
2393
2299
|
function formatInitStageSummaryLine(disposition, stages) {
|
|
2394
|
-
const label = disposition === "ran" ? paint.success(t("cli.
|
|
2300
|
+
const label = disposition === "ran" ? paint.success(t("cli.install.stages.summary.ran")) : disposition === "skipped" ? paint.muted(t("cli.install.stages.summary.skipped")) : paint.error(t("cli.install.stages.summary.failed"));
|
|
2395
2301
|
return `${label}: ${stages.length > 0 ? stages.join(", ") : t("cli.shared.none")}`;
|
|
2396
2302
|
}
|
|
2397
2303
|
function collectInitStageNames(stageResults, disposition) {
|
|
@@ -2404,11 +2310,11 @@ function isInteractiveInit() {
|
|
|
2404
2310
|
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
2405
2311
|
}
|
|
2406
2312
|
function printInitPlanSummary(target, options, mcpInstallMode, supports) {
|
|
2407
|
-
console.log(t("cli.
|
|
2313
|
+
console.log(t("cli.install.plan.title"));
|
|
2408
2314
|
console.log(formatInitModeBanner(options));
|
|
2409
|
-
console.log(t("cli.
|
|
2315
|
+
console.log(t("cli.install.plan.target", { target }));
|
|
2410
2316
|
console.log(
|
|
2411
|
-
t("cli.
|
|
2317
|
+
t("cli.install.plan.actions", {
|
|
2412
2318
|
bootstrap: yesNoLabel(!options.skipBootstrap),
|
|
2413
2319
|
mcp: yesNoLabel(!options.skipMcp),
|
|
2414
2320
|
hooks: yesNoLabel(!options.skipHooks),
|
|
@@ -2417,31 +2323,32 @@ function printInitPlanSummary(target, options, mcpInstallMode, supports) {
|
|
|
2417
2323
|
);
|
|
2418
2324
|
const detected = supports.filter((support) => support.detected);
|
|
2419
2325
|
console.log(
|
|
2420
|
-
t("cli.
|
|
2326
|
+
t("cli.install.plan.detected", {
|
|
2421
2327
|
clients: detected.length > 0 ? detected.map((support) => support.label).join(", ") : t("cli.shared.none")
|
|
2422
2328
|
})
|
|
2423
2329
|
);
|
|
2424
|
-
console.log(t("cli.
|
|
2330
|
+
console.log(t("cli.install.plan.writes"));
|
|
2425
2331
|
console.log(` - ${target}/.fabric/knowledge/{decisions,pitfalls,guidelines,models,processes,pending}/`);
|
|
2426
2332
|
console.log(` - ${target}/.fabric/agents.meta.json`);
|
|
2427
2333
|
console.log(` - ${target}/.fabric/events.jsonl`);
|
|
2428
2334
|
console.log(` - ${target}/.fabric/forensic.json`);
|
|
2335
|
+
console.log(` - ${target}/.fabric/fabric-config.json`);
|
|
2429
2336
|
}
|
|
2430
2337
|
function printInitCapabilitySummary(supports, stageResults, options) {
|
|
2431
2338
|
const detected = supports.filter((support) => support.detected);
|
|
2432
2339
|
if (detected.length === 0) {
|
|
2433
|
-
console.log(t("cli.
|
|
2340
|
+
console.log(t("cli.install.capabilities.none"));
|
|
2434
2341
|
return;
|
|
2435
2342
|
}
|
|
2436
|
-
console.log(t("cli.
|
|
2343
|
+
console.log(t("cli.install.capabilities.title"));
|
|
2437
2344
|
const rows = detected.map((support) => toCapabilityRow(support, stageResults, options));
|
|
2438
2345
|
const headers = {
|
|
2439
|
-
client: t("cli.
|
|
2440
|
-
bootstrap: t("cli.
|
|
2441
|
-
mcp: t("cli.
|
|
2442
|
-
hook: t("cli.
|
|
2443
|
-
skill: t("cli.
|
|
2444
|
-
followUp: t("cli.
|
|
2346
|
+
client: t("cli.install.capabilities.header.client"),
|
|
2347
|
+
bootstrap: t("cli.install.capabilities.header.bootstrap"),
|
|
2348
|
+
mcp: t("cli.install.capabilities.header.mcp"),
|
|
2349
|
+
hook: t("cli.install.capabilities.header.hook"),
|
|
2350
|
+
skill: t("cli.install.capabilities.header.skill"),
|
|
2351
|
+
followUp: t("cli.install.capabilities.header.follow-up")
|
|
2445
2352
|
};
|
|
2446
2353
|
const widths = {
|
|
2447
2354
|
client: Math.max(displayWidth(headers.client), ...rows.map((row) => displayWidth(row.client))),
|
|
@@ -2456,11 +2363,13 @@ function printInitCapabilitySummary(supports, stageResults, options) {
|
|
|
2456
2363
|
for (const row of rows) {
|
|
2457
2364
|
console.log(formatCapabilityTableRow(row, widths));
|
|
2458
2365
|
}
|
|
2366
|
+
console.log("");
|
|
2367
|
+
console.log(t("cli.install.restart-banner"));
|
|
2459
2368
|
}
|
|
2460
2369
|
function toCapabilityRow(support, stageResults, options) {
|
|
2461
2370
|
const stage = (name) => stageResults.find((entry) => entry.name === name)?.disposition ?? null;
|
|
2462
|
-
const bootstrap = support.capabilities.bootstrap ? capabilityStatus(options.skipBootstrap ? "skipped" : stage("bootstrap")) : t("cli.
|
|
2463
|
-
const mcp = support.capabilities.mcp ? capabilityStatus(options.skipMcp ? "skipped" : stage("mcp")) : t("cli.
|
|
2371
|
+
const bootstrap = support.capabilities.bootstrap ? capabilityStatus(options.skipBootstrap ? "skipped" : stage("bootstrap")) : t("cli.install.capabilities.status.na");
|
|
2372
|
+
const mcp = support.capabilities.mcp ? capabilityStatus(options.skipMcp ? "skipped" : stage("mcp")) : t("cli.install.capabilities.status.na");
|
|
2464
2373
|
const hook = capabilityInstallStatus(support, "hook");
|
|
2465
2374
|
const skill = capabilityInstallStatus(support, "skill");
|
|
2466
2375
|
return {
|
|
@@ -2469,14 +2378,14 @@ function toCapabilityRow(support, stageResults, options) {
|
|
|
2469
2378
|
mcp,
|
|
2470
2379
|
hook,
|
|
2471
2380
|
skill,
|
|
2472
|
-
followUp: hasInstalledCapability(support, "skill") ? t("cli.
|
|
2381
|
+
followUp: hasInstalledCapability(support, "skill") ? t("cli.install.capabilities.follow-up.ready") : support.capabilities.skill ? t("cli.install.capabilities.follow-up.install") : t("cli.install.capabilities.follow-up.manual")
|
|
2473
2382
|
};
|
|
2474
2383
|
}
|
|
2475
2384
|
function capabilityInstallStatus(support, capability) {
|
|
2476
2385
|
if (!support.capabilities[capability]) {
|
|
2477
|
-
return t("cli.
|
|
2386
|
+
return t("cli.install.capabilities.status.na");
|
|
2478
2387
|
}
|
|
2479
|
-
return hasInstalledCapability(support, capability) ? t("cli.
|
|
2388
|
+
return hasInstalledCapability(support, capability) ? t("cli.install.capabilities.status.installed") : t("cli.install.capabilities.status.supported");
|
|
2480
2389
|
}
|
|
2481
2390
|
function hasInstalledCapability(support, capability) {
|
|
2482
2391
|
return support.installedCapabilities?.[capability] === true;
|
|
@@ -2484,15 +2393,15 @@ function hasInstalledCapability(support, capability) {
|
|
|
2484
2393
|
function capabilityStatus(disposition) {
|
|
2485
2394
|
switch (disposition) {
|
|
2486
2395
|
case "ran":
|
|
2487
|
-
return t("cli.
|
|
2396
|
+
return t("cli.install.capabilities.status.ready");
|
|
2488
2397
|
case "skipped":
|
|
2489
|
-
return t("cli.
|
|
2398
|
+
return t("cli.install.capabilities.status.skipped");
|
|
2490
2399
|
case "failed":
|
|
2491
|
-
return t("cli.
|
|
2400
|
+
return t("cli.install.capabilities.status.failed");
|
|
2492
2401
|
case null:
|
|
2493
|
-
return t("cli.
|
|
2402
|
+
return t("cli.install.capabilities.status.na");
|
|
2494
2403
|
default:
|
|
2495
|
-
return t("cli.
|
|
2404
|
+
return t("cli.install.capabilities.status.ready");
|
|
2496
2405
|
}
|
|
2497
2406
|
}
|
|
2498
2407
|
function formatCapabilityTableRow(row, widths) {
|
|
@@ -2518,21 +2427,21 @@ function formatCapabilityDivider(widths) {
|
|
|
2518
2427
|
function formatInitReasonMessage(supports) {
|
|
2519
2428
|
const detected = supports.filter((support) => support.detected);
|
|
2520
2429
|
if (detected.some((support) => support.capabilities.skill)) {
|
|
2521
|
-
return t("cli.
|
|
2430
|
+
return t("cli.install.reason-message.installable-body");
|
|
2522
2431
|
}
|
|
2523
|
-
return t("cli.
|
|
2432
|
+
return t("cli.install.reason-message.manual-body");
|
|
2524
2433
|
}
|
|
2525
2434
|
function yesNoLabel(value) {
|
|
2526
2435
|
return value ? t("cli.shared.yes") : t("cli.shared.no");
|
|
2527
2436
|
}
|
|
2528
2437
|
function formatInitPathAction(path, action) {
|
|
2529
|
-
return t("cli.
|
|
2438
|
+
return t("cli.install.created-path", { label: labelForInitWriteAction(action), path });
|
|
2530
2439
|
}
|
|
2531
2440
|
function formatAgentsMdAction(path, action) {
|
|
2532
2441
|
if (action === "preserved") {
|
|
2533
|
-
return t("cli.
|
|
2442
|
+
return t("cli.install.skipped-existing-path", { label: skippedLabel(), path });
|
|
2534
2443
|
}
|
|
2535
|
-
return t("cli.
|
|
2444
|
+
return t("cli.install.created-path", { label: createdLabel(), path });
|
|
2536
2445
|
}
|
|
2537
2446
|
function labelForInitWriteAction(action) {
|
|
2538
2447
|
return action === "overwritten" ? overwrittenLabel() : createdLabel();
|
|
@@ -2550,18 +2459,18 @@ function reasonLabel() {
|
|
|
2550
2459
|
return paint.human(t("cli.shared.reason"));
|
|
2551
2460
|
}
|
|
2552
2461
|
function overwrittenLabel() {
|
|
2553
|
-
return paint.warn(t("cli.
|
|
2462
|
+
return paint.warn(t("cli.install.label.overwritten"));
|
|
2554
2463
|
}
|
|
2555
2464
|
function completedStageLabel() {
|
|
2556
|
-
return paint.success(t("cli.
|
|
2465
|
+
return paint.success(t("cli.install.stages.completed"));
|
|
2557
2466
|
}
|
|
2558
2467
|
function skippedStageLabel() {
|
|
2559
|
-
return paint.muted(t("cli.
|
|
2468
|
+
return paint.muted(t("cli.install.stages.skipped"));
|
|
2560
2469
|
}
|
|
2561
2470
|
function failedStageLabel() {
|
|
2562
|
-
return paint.error(t("cli.
|
|
2471
|
+
return paint.error(t("cli.install.stages.failed"));
|
|
2563
2472
|
}
|
|
2564
|
-
function
|
|
2473
|
+
function writeStderr(message) {
|
|
2565
2474
|
process.stderr.write(`${message}
|
|
2566
2475
|
`);
|
|
2567
2476
|
}
|
|
@@ -2569,13 +2478,15 @@ export {
|
|
|
2569
2478
|
buildInitExecutionPlan,
|
|
2570
2479
|
buildInitFabricPlan,
|
|
2571
2480
|
createDefaultInitWizardAdapter,
|
|
2572
|
-
|
|
2481
|
+
install_default as default,
|
|
2573
2482
|
detectPackageManager,
|
|
2574
2483
|
executeInitExecutionPlan,
|
|
2575
2484
|
executeInitFabricPlan,
|
|
2576
|
-
initCommand,
|
|
2577
2485
|
initFabric,
|
|
2486
|
+
installCommand,
|
|
2578
2487
|
resolveInitExecutionPlanWithWizard,
|
|
2488
|
+
runHooksOnlyRefresh,
|
|
2579
2489
|
runInitCommand,
|
|
2490
|
+
runSkillsOnlyRefresh,
|
|
2580
2491
|
shouldUseInitWizard
|
|
2581
2492
|
};
|