@fenglimg/fabric-cli 2.1.0-rc.2 → 2.2.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/chunk-27HK6H5Y.js +69 -0
- package/dist/{chunk-BATF4PEJ.js → chunk-2KBCTMID.js} +31 -8
- package/dist/chunk-3D7B2UAZ.js +149 -0
- package/dist/{chunk-MF3OTILQ.js → chunk-3IOLS5EK.js} +48 -42
- package/dist/{plan-context-hint-FC6P3WFE.js → chunk-722JU5BP.js} +52 -12
- package/dist/{chunk-F46ORPOA.js → chunk-7ZDXBOOU.js} +271 -166
- package/dist/{doctor-QVNPHLJK.js → chunk-E7HJUU34.js} +248 -72
- package/dist/chunk-EOT63RDH.js +36 -0
- package/dist/chunk-FNHDQTPC.js +16 -0
- package/dist/chunk-HORSMSZL.js +26 -0
- package/dist/chunk-NLNH64A3.js +43 -0
- package/dist/{chunk-WU6GAPKH.js → chunk-PTGQAZEW.js} +12 -4
- package/dist/chunk-QFIVFZRH.js +13 -0
- package/dist/chunk-QPAW6IYT.js +387 -0
- package/dist/{chunk-COI5VDFU.js → chunk-WA3DYGSY.js} +1 -2
- package/dist/{config-XJIPZNUP.js → config-A3LTECAY.js} +4 -3
- package/dist/context-UJCGYOT6.js +117 -0
- package/dist/doctor-MDTZWKBK.js +24 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +167 -16
- package/dist/info-7FKBTMVO.js +139 -0
- package/dist/install-v2-RINEA24K.js +3279 -0
- package/dist/{metrics-ACEQFPDU.js → metrics-HMFH4YHK.js} +22 -9
- package/dist/{onboard-coverage-MFCAEBDO.js → onboard-coverage-XSG77LL3.js} +48 -27
- package/dist/plan-context-hint-5TNGH3R4.js +12 -0
- package/dist/scope-explain-HLJZ2M33.js +48 -0
- package/dist/status-4R3TM4FJ.js +37 -0
- package/dist/store-HOCORVL3.js +563 -0
- package/dist/sync-DT5UJMMR.js +418 -0
- package/dist/{uninstall-TAXSUSKH.js → uninstall-IFN2KYBK.js} +128 -140
- package/dist/whoami-ITGEFWH4.js +49 -0
- package/package.json +7 -5
- package/templates/hooks/cite-policy-evict.cjs +412 -160
- package/templates/hooks/configs/README.md +14 -27
- package/templates/hooks/configs/claude-code.json +17 -2
- package/templates/hooks/configs/codex-hooks.json +15 -3
- package/templates/hooks/fabric-hint.cjs +573 -180
- package/templates/hooks/knowledge-hint-broad.cjs +648 -190
- package/templates/hooks/knowledge-hint-narrow.cjs +123 -77
- package/templates/hooks/lib/banner-i18n.cjs +31 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +118 -7
- package/templates/hooks/lib/cite-line-parser.cjs +12 -20
- package/templates/hooks/lib/client-adapter.cjs +66 -7
- package/templates/hooks/lib/injection-log.cjs +91 -0
- package/templates/hooks/lib/nudge-policy.cjs +117 -0
- package/templates/hooks/lib/state-store.cjs +90 -11
- package/templates/hooks/post-tooluse-mutation.cjs +386 -0
- package/templates/hooks/session-end-marker.cjs +140 -0
- package/templates/skills/fabric/SKILL.md +100 -0
- package/templates/skills/fabric-archive/SKILL.md +35 -24
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +2 -3
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +18 -0
- package/templates/skills/fabric-archive/ref/phase-3-7-semantic-scope.md +47 -0
- package/templates/skills/fabric-audit/SKILL.md +63 -0
- package/templates/skills/fabric-connect/SKILL.md +48 -0
- package/templates/skills/fabric-import/SKILL.md +7 -7
- package/templates/skills/fabric-import/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-import/ref/state-recovery.md +1 -2
- package/templates/skills/fabric-review/SKILL.md +16 -5
- package/templates/skills/fabric-review/ref/cite-contract.md +56 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-review/ref/output-contract.md +1 -1
- package/templates/skills/fabric-review/ref/per-mode-flows.md +2 -2
- package/templates/skills/fabric-review/ref/worked-examples.md +1 -1
- package/templates/skills/fabric-store/SKILL.md +44 -0
- package/templates/skills/fabric-sync/SKILL.md +1 -1
- package/templates/skills/lib/shared-policy.md +2 -2
- package/dist/chunk-HFQVXY6P.js +0 -86
- package/dist/chunk-L4Q55UC4.js +0 -52
- package/dist/chunk-LFIKMVY7.js +0 -27
- package/dist/chunk-PWLW3B57.js +0 -18
- package/dist/chunk-RYAFBNES.js +0 -33
- package/dist/chunk-T5RPGCCM.js +0 -40
- package/dist/chunk-WWNXR34K.js +0 -49
- package/dist/install-2HDO5FTQ.js +0 -2683
- package/dist/scope-explain-2F2R5URO.js +0 -33
- package/dist/status-GLQWLWH6.js +0 -23
- package/dist/store-XTSE5TY6.js +0 -105
- package/dist/sync-BJCWDPNC.js +0 -245
- package/dist/whoami-B6AEMSEV.js +0 -31
- package/templates/hooks/configs/cursor-hooks.json +0 -18
- package/templates/hooks/lib/cite-contract-reminder.cjs +0 -179
- package/templates/hooks/lib/summary-fallback.cjs +0 -210
|
@@ -0,0 +1,3279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
cleanupDeprecatedSkills,
|
|
4
|
+
installArchiveHintHook,
|
|
5
|
+
installCitePolicyEvictHook,
|
|
6
|
+
installFabricArchiveSkill,
|
|
7
|
+
installFabricAuditSkill,
|
|
8
|
+
installFabricConnectSkill,
|
|
9
|
+
installFabricImportSkill,
|
|
10
|
+
installFabricReviewSkill,
|
|
11
|
+
installFabricRouterSkill,
|
|
12
|
+
installFabricStoreSkill,
|
|
13
|
+
installFabricSyncSkill,
|
|
14
|
+
installHookLibs,
|
|
15
|
+
installKnowledgeHintBroadHook,
|
|
16
|
+
installKnowledgeHintNarrowHook,
|
|
17
|
+
installPostTooluseMutationHook,
|
|
18
|
+
installSessionEndMarkerHook,
|
|
19
|
+
installSharedSkillLib,
|
|
20
|
+
mergeClaudeCodeHookConfig,
|
|
21
|
+
mergeCodexHookConfig,
|
|
22
|
+
writeClaudeBootstrapThinShell,
|
|
23
|
+
writeCodexBootstrapManagedBlock,
|
|
24
|
+
writeFabricAgentsSnapshot
|
|
25
|
+
} from "./chunk-7ZDXBOOU.js";
|
|
26
|
+
import {
|
|
27
|
+
ensureStoreProjectBinding,
|
|
28
|
+
migrateRootConfig,
|
|
29
|
+
normalizeStoreProjectId,
|
|
30
|
+
suggestStoreProjectId
|
|
31
|
+
} from "./chunk-3D7B2UAZ.js";
|
|
32
|
+
import {
|
|
33
|
+
createDebugLogger,
|
|
34
|
+
resolveDevMode
|
|
35
|
+
} from "./chunk-WA3DYGSY.js";
|
|
36
|
+
import {
|
|
37
|
+
installMcpClients
|
|
38
|
+
} from "./chunk-2KBCTMID.js";
|
|
39
|
+
import {
|
|
40
|
+
detectClientSupports
|
|
41
|
+
} from "./chunk-3IOLS5EK.js";
|
|
42
|
+
import {
|
|
43
|
+
paint
|
|
44
|
+
} from "./chunk-NLNH64A3.js";
|
|
45
|
+
import "./chunk-PTGQAZEW.js";
|
|
46
|
+
import "./chunk-EOT63RDH.js";
|
|
47
|
+
import {
|
|
48
|
+
storeCreate,
|
|
49
|
+
storeList,
|
|
50
|
+
storeProjectList,
|
|
51
|
+
syncStoreAliasLinks,
|
|
52
|
+
unboundAvailableStores
|
|
53
|
+
} from "./chunk-QPAW6IYT.js";
|
|
54
|
+
import {
|
|
55
|
+
loadProjectConfig
|
|
56
|
+
} from "./chunk-QFIVFZRH.js";
|
|
57
|
+
import {
|
|
58
|
+
globalConfigPath,
|
|
59
|
+
loadGlobalConfig,
|
|
60
|
+
resolveGlobalRoot,
|
|
61
|
+
saveGlobalConfig
|
|
62
|
+
} from "./chunk-FNHDQTPC.js";
|
|
63
|
+
import {
|
|
64
|
+
getProjectTranslator,
|
|
65
|
+
refreshLocale,
|
|
66
|
+
t
|
|
67
|
+
} from "./chunk-HORSMSZL.js";
|
|
68
|
+
|
|
69
|
+
// src/commands/install-v2.ts
|
|
70
|
+
import { defineCommand } from "citty";
|
|
71
|
+
|
|
72
|
+
// src/install/run-global-install.ts
|
|
73
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
74
|
+
import { randomUUID } from "crypto";
|
|
75
|
+
import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync as rmSync2 } from "fs";
|
|
76
|
+
import { tmpdir } from "os";
|
|
77
|
+
import { join as join2 } from "path";
|
|
78
|
+
import {
|
|
79
|
+
STORES_ROOT_DIR,
|
|
80
|
+
addMountedStore,
|
|
81
|
+
deriveMountLabel,
|
|
82
|
+
disambiguateAlias,
|
|
83
|
+
globalConfigSchema as globalConfigSchema2,
|
|
84
|
+
readStoreIdentity,
|
|
85
|
+
storeRelativePathForMount as storeRelativePathForMount2
|
|
86
|
+
} from "@fenglimg/fabric-shared";
|
|
87
|
+
import { GenericIOError } from "@fenglimg/fabric-shared/errors";
|
|
88
|
+
|
|
89
|
+
// src/store/uid.ts
|
|
90
|
+
import { execFileSync } from "child_process";
|
|
91
|
+
import { createHash } from "crypto";
|
|
92
|
+
function deriveUid(opts = {}) {
|
|
93
|
+
let email = "";
|
|
94
|
+
try {
|
|
95
|
+
email = execFileSync("git", ["config", "user.email"], {
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
98
|
+
}).trim();
|
|
99
|
+
} catch {
|
|
100
|
+
email = "";
|
|
101
|
+
}
|
|
102
|
+
if (email === "") {
|
|
103
|
+
return "u-anon";
|
|
104
|
+
}
|
|
105
|
+
const material = opts.salt !== void 0 && opts.salt.length > 0 ? `${opts.salt}:${email.toLowerCase()}` : email.toLowerCase();
|
|
106
|
+
const hash = createHash("sha256").update(material).digest("hex").slice(0, 12);
|
|
107
|
+
return `u-${hash}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/install/install-global.ts
|
|
111
|
+
import { rmSync } from "fs";
|
|
112
|
+
import { join } from "path";
|
|
113
|
+
import { globalConfigSchema, initStore, storeRelativePathForMount } from "@fenglimg/fabric-shared";
|
|
114
|
+
|
|
115
|
+
// src/install/transaction.ts
|
|
116
|
+
function errorMessage(error) {
|
|
117
|
+
return error instanceof Error ? error.message : String(error);
|
|
118
|
+
}
|
|
119
|
+
async function runInstallTransaction(steps) {
|
|
120
|
+
const receipt = { ok: true, steps: [] };
|
|
121
|
+
const applied = [];
|
|
122
|
+
for (let i = 0; i < steps.length; i++) {
|
|
123
|
+
const step = steps[i];
|
|
124
|
+
try {
|
|
125
|
+
await step.apply();
|
|
126
|
+
applied.push(step);
|
|
127
|
+
receipt.steps.push({ name: step.name, status: "applied" });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
receipt.ok = false;
|
|
130
|
+
receipt.failedStep = step.name;
|
|
131
|
+
receipt.error = errorMessage(error);
|
|
132
|
+
receipt.steps.push({ name: step.name, status: "failed", error: errorMessage(error) });
|
|
133
|
+
for (let j = i + 1; j < steps.length; j++) {
|
|
134
|
+
receipt.steps.push({ name: steps[j].name, status: "skipped" });
|
|
135
|
+
}
|
|
136
|
+
for (const done of [...applied].reverse()) {
|
|
137
|
+
const entry = receipt.steps.find((s) => s.name === done.name);
|
|
138
|
+
try {
|
|
139
|
+
await done.rollback();
|
|
140
|
+
if (entry !== void 0) {
|
|
141
|
+
entry.status = "rolled_back";
|
|
142
|
+
}
|
|
143
|
+
} catch (rollbackError) {
|
|
144
|
+
if (entry !== void 0) {
|
|
145
|
+
entry.status = "rollback_failed";
|
|
146
|
+
entry.error = errorMessage(rollbackError);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return receipt;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return receipt;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/install/install-global.ts
|
|
157
|
+
async function installGlobalCore(options) {
|
|
158
|
+
const existing = loadGlobalConfig(options.globalRoot);
|
|
159
|
+
if (existing !== null) {
|
|
160
|
+
return {
|
|
161
|
+
receipt: { ok: true, steps: [{ name: "already-installed", status: "applied" }] },
|
|
162
|
+
config: existing,
|
|
163
|
+
alreadyInstalled: true
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const alias = options.personalAlias ?? "personal";
|
|
167
|
+
const personalStore = {
|
|
168
|
+
store_uuid: options.personalStoreUuid,
|
|
169
|
+
alias,
|
|
170
|
+
mount_name: alias,
|
|
171
|
+
personal: true
|
|
172
|
+
};
|
|
173
|
+
const personalDir = join(options.globalRoot, storeRelativePathForMount(personalStore));
|
|
174
|
+
let config = null;
|
|
175
|
+
const receipt = await runInstallTransaction([
|
|
176
|
+
{
|
|
177
|
+
name: "init-personal-store",
|
|
178
|
+
apply: async () => {
|
|
179
|
+
await initStore(
|
|
180
|
+
personalDir,
|
|
181
|
+
{
|
|
182
|
+
store_uuid: options.personalStoreUuid,
|
|
183
|
+
created_at: options.now,
|
|
184
|
+
canonical_alias: alias
|
|
185
|
+
},
|
|
186
|
+
{ git: options.git }
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
rollback: () => {
|
|
190
|
+
rmSync(personalDir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "write-global-config",
|
|
195
|
+
apply: () => {
|
|
196
|
+
const next = globalConfigSchema.parse({
|
|
197
|
+
uid: options.uid,
|
|
198
|
+
stores: [personalStore]
|
|
199
|
+
});
|
|
200
|
+
saveGlobalConfig(next, options.globalRoot);
|
|
201
|
+
config = next;
|
|
202
|
+
},
|
|
203
|
+
rollback: () => {
|
|
204
|
+
rmSync(globalConfigPath(options.globalRoot), { force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
]);
|
|
208
|
+
return { receipt, config, alreadyInstalled: false };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/install/run-global-install.ts
|
|
212
|
+
function gitClone(url, dest) {
|
|
213
|
+
console.log(`cloning store from ${url} (this may take a while)\u2026`);
|
|
214
|
+
try {
|
|
215
|
+
execFileSync2("git", ["clone", "--", url, dest], { stdio: ["ignore", "ignore", "inherit"] });
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw new GenericIOError(`git clone of ${url} failed`, {
|
|
218
|
+
actionHint: "check the url is reachable and points to a Fabric store git repo (the git error above shows the cause), then re-run `fabric install --global <url>`",
|
|
219
|
+
details: error
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function mountStoreFromRemote(url, globalRoot) {
|
|
224
|
+
const config = loadGlobalConfig(globalRoot);
|
|
225
|
+
if (config === null) {
|
|
226
|
+
throw new GenericIOError("global config missing \u2014 run `fabric install --global` first", {
|
|
227
|
+
actionHint: "re-run `fabric install --global` to (re)create the global config, then retry mounting the store; if it persists, inspect ~/.fabric for a partial install"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const storesRoot = join2(globalRoot, STORES_ROOT_DIR);
|
|
231
|
+
mkdirSync(storesRoot, { recursive: true });
|
|
232
|
+
const tmp = mkdtempSync(join2(tmpdir(), "fabric-clone-"));
|
|
233
|
+
const cloneDest = join2(tmp, "store");
|
|
234
|
+
gitClone(url, cloneDest);
|
|
235
|
+
const identity = readStoreIdentity(cloneDest);
|
|
236
|
+
if (identity === null) {
|
|
237
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
238
|
+
throw new GenericIOError(`cloned store at ${url} has no valid store.json (not a Fabric store)`, {
|
|
239
|
+
actionHint: "verify the url points to a repository created by `fabric` (it must contain a store.json at its root); if you meant to mount a different store, re-run with the correct url"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const alreadyMounted = config.stores.find((s) => s.store_uuid === identity.store_uuid);
|
|
243
|
+
if (alreadyMounted !== void 0) {
|
|
244
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
245
|
+
console.log(`store '${alreadyMounted.alias}' (${identity.store_uuid}) already mounted; reusing`);
|
|
246
|
+
return { store_uuid: identity.store_uuid, alias: alreadyMounted.alias };
|
|
247
|
+
}
|
|
248
|
+
const desiredAlias = identity.canonical_alias ?? "team";
|
|
249
|
+
const alias = disambiguateAlias(config.stores.map((s) => s.alias), desiredAlias);
|
|
250
|
+
const mount_name = deriveMountLabel({ remote: url, alias: desiredAlias, store_uuid: identity.store_uuid });
|
|
251
|
+
const finalDir = join2(globalRoot, storeRelativePathForMount2({ store_uuid: identity.store_uuid, mount_name }));
|
|
252
|
+
if (existsSync(finalDir)) {
|
|
253
|
+
const onDisk = readStoreIdentity(finalDir);
|
|
254
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
255
|
+
if (onDisk === null || onDisk.store_uuid !== identity.store_uuid) {
|
|
256
|
+
throw new GenericIOError(
|
|
257
|
+
`cannot mount store from ${url}: a different store already occupies ${finalDir}` + (onDisk === null ? " (no valid store.json there)" : ` (uuid ${onDisk.store_uuid})`),
|
|
258
|
+
{
|
|
259
|
+
actionHint: "remove or relocate that directory, then retry \u2014 identity is the intrinsic store_uuid, not the directory name"
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
saveGlobalConfig(
|
|
264
|
+
addMountedStore(config, { store_uuid: identity.store_uuid, alias, mount_name, remote: url }),
|
|
265
|
+
globalRoot
|
|
266
|
+
);
|
|
267
|
+
syncStoreAliasLinks(globalRoot);
|
|
268
|
+
console.log(`adopted existing store '${alias}' (${identity.store_uuid}) at ${finalDir}`);
|
|
269
|
+
return { store_uuid: identity.store_uuid, alias };
|
|
270
|
+
}
|
|
271
|
+
mkdirSync(join2(finalDir, ".."), { recursive: true });
|
|
272
|
+
renameSync(cloneDest, finalDir);
|
|
273
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
274
|
+
saveGlobalConfig(
|
|
275
|
+
addMountedStore(config, { store_uuid: identity.store_uuid, alias, mount_name, remote: url }),
|
|
276
|
+
globalRoot
|
|
277
|
+
);
|
|
278
|
+
syncStoreAliasLinks(globalRoot);
|
|
279
|
+
console.log(`mounted store '${alias}' (${identity.store_uuid}) from ${url}`);
|
|
280
|
+
return { store_uuid: identity.store_uuid, alias };
|
|
281
|
+
}
|
|
282
|
+
function cloneGlobalPersonalFromRemote(url, globalRoot, uid = deriveUid()) {
|
|
283
|
+
if (loadGlobalConfig(globalRoot) !== null) {
|
|
284
|
+
throw new GenericIOError("global config already exists; refusing to clone a personal store over it", {
|
|
285
|
+
actionHint: "this machine already has a personal store; to adopt a remote one, use `fabric store` after install rather than re-running first-touch install"
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const storesRoot = join2(globalRoot, STORES_ROOT_DIR);
|
|
289
|
+
mkdirSync(storesRoot, { recursive: true });
|
|
290
|
+
const tmp = mkdtempSync(join2(tmpdir(), "fabric-personal-clone-"));
|
|
291
|
+
const cloneDest = join2(tmp, "store");
|
|
292
|
+
gitClone(url, cloneDest);
|
|
293
|
+
const identity = readStoreIdentity(cloneDest);
|
|
294
|
+
if (identity === null) {
|
|
295
|
+
throw new GenericIOError(`cloned store at ${url} has no valid store.json (not a Fabric store)`, {
|
|
296
|
+
actionHint: "verify the url points to a Fabric personal store git repo (it must contain a store.json at its root)"
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const personalStore = {
|
|
300
|
+
store_uuid: identity.store_uuid,
|
|
301
|
+
alias: "personal",
|
|
302
|
+
// D4 — label from the remote repo name; `personal: true` routes it to the
|
|
303
|
+
// `personal/` group bucket regardless of the label.
|
|
304
|
+
mount_name: deriveMountLabel({ remote: url, alias: "personal", store_uuid: identity.store_uuid }),
|
|
305
|
+
personal: true,
|
|
306
|
+
remote: url
|
|
307
|
+
};
|
|
308
|
+
const finalDir = join2(globalRoot, storeRelativePathForMount2(personalStore));
|
|
309
|
+
mkdirSync(join2(finalDir, ".."), { recursive: true });
|
|
310
|
+
renameSync(cloneDest, finalDir);
|
|
311
|
+
saveGlobalConfig(globalConfigSchema2.parse({ uid, stores: [personalStore] }), globalRoot);
|
|
312
|
+
syncStoreAliasLinks(globalRoot);
|
|
313
|
+
console.log(`cloned personal store '${identity.store_uuid}' from ${url}`);
|
|
314
|
+
return { store_uuid: identity.store_uuid };
|
|
315
|
+
}
|
|
316
|
+
async function runGlobalInstall(options = {}, globalRoot = resolveGlobalRoot()) {
|
|
317
|
+
const uid = options.uid ?? deriveUid();
|
|
318
|
+
const personalStoreUuid = options.personalStoreUuid ?? randomUUID();
|
|
319
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
320
|
+
const result = await installGlobalCore({ globalRoot, uid, personalStoreUuid, now });
|
|
321
|
+
if (!result.receipt.ok) {
|
|
322
|
+
throw new GenericIOError(
|
|
323
|
+
`global install failed at step '${result.receipt.failedStep}': ${result.receipt.error}`,
|
|
324
|
+
{
|
|
325
|
+
actionHint: "check write permissions and free space under ~/.fabric, then re-run `fabric install --global` (the install is transactional and rolls back partial state)"
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
console.log(
|
|
330
|
+
result.alreadyInstalled ? "global Fabric already installed" : `installed global Fabric (uid ${uid})`
|
|
331
|
+
);
|
|
332
|
+
if (options.url !== void 0) {
|
|
333
|
+
mountStoreFromRemote(options.url, globalRoot);
|
|
334
|
+
}
|
|
335
|
+
syncStoreAliasLinks(globalRoot);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/install/pipeline/pipeline.ts
|
|
339
|
+
function stageLabel(name) {
|
|
340
|
+
return t(`cli.install.pipeline.label.${name}`);
|
|
341
|
+
}
|
|
342
|
+
var STAGE_DESCRIPTION_KEYS = {
|
|
343
|
+
store: "cli.install.pipeline.desc.store"
|
|
344
|
+
};
|
|
345
|
+
var STAGE_ICONS = {
|
|
346
|
+
preflight: "\u{1F50D}",
|
|
347
|
+
env: "\u{1F3D7}\uFE0F",
|
|
348
|
+
store: "\u{1F4E6}",
|
|
349
|
+
hooks: "\u{1FA9D}",
|
|
350
|
+
mcp: "\u{1F50C}",
|
|
351
|
+
validate: "\u2705",
|
|
352
|
+
guidance: "\u{1F4D6}"
|
|
353
|
+
};
|
|
354
|
+
var InstallPipeline = class {
|
|
355
|
+
stages = [];
|
|
356
|
+
/**
|
|
357
|
+
* Add a stage to the pipeline.
|
|
358
|
+
* Stages execute in the order they are added.
|
|
359
|
+
*/
|
|
360
|
+
addStage(stage) {
|
|
361
|
+
this.stages.push(stage);
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Execute all stages in order.
|
|
366
|
+
* On failure, executes rollback actions in reverse order.
|
|
367
|
+
*/
|
|
368
|
+
async execute(initialContext) {
|
|
369
|
+
const context = initialContext;
|
|
370
|
+
const totalStages = this.stages.length;
|
|
371
|
+
const renderer = context.renderer;
|
|
372
|
+
if (renderer) {
|
|
373
|
+
renderer.renderSection(t("cli.install.pipeline.title"));
|
|
374
|
+
renderer.renderInfo(t("cli.install.pipeline.running", { count: String(totalStages) }));
|
|
375
|
+
} else {
|
|
376
|
+
console.log(t("cli.install.pipeline.running", { count: String(totalStages) }));
|
|
377
|
+
}
|
|
378
|
+
for (let i = 0; i < this.stages.length; i++) {
|
|
379
|
+
const stage = this.stages[i];
|
|
380
|
+
const stepNum = i + 1;
|
|
381
|
+
const stageName = stage.name;
|
|
382
|
+
if (renderer) {
|
|
383
|
+
renderer.renderSection(`${STAGE_ICONS[stageName]} ${stageLabel(stageName)}`);
|
|
384
|
+
} else {
|
|
385
|
+
console.log(`[${stepNum}/${totalStages}] ${stageLabel(stageName)}`);
|
|
386
|
+
const descriptionKey = STAGE_DESCRIPTION_KEYS[stageName];
|
|
387
|
+
if (descriptionKey !== void 0) {
|
|
388
|
+
console.log(` ${t(descriptionKey)}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (renderer) {
|
|
392
|
+
renderer.renderStep({
|
|
393
|
+
name: stageLabel(stageName),
|
|
394
|
+
current: stepNum,
|
|
395
|
+
total: totalStages,
|
|
396
|
+
status: "running"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const result = await stage.execute(context);
|
|
401
|
+
context.stageResults.push(result);
|
|
402
|
+
if (renderer) {
|
|
403
|
+
if (result.disposition === "ran") {
|
|
404
|
+
renderer.renderStep({
|
|
405
|
+
name: stageLabel(stageName),
|
|
406
|
+
current: stepNum,
|
|
407
|
+
total: totalStages,
|
|
408
|
+
status: "success",
|
|
409
|
+
detail: result.installed.length > 0 ? `${result.installed.length} installed, ${result.skipped.length} skipped` : void 0
|
|
410
|
+
});
|
|
411
|
+
} else if (result.disposition === "skipped") {
|
|
412
|
+
renderer.renderStep({
|
|
413
|
+
name: stageLabel(stageName),
|
|
414
|
+
current: stepNum,
|
|
415
|
+
total: totalStages,
|
|
416
|
+
status: "skipped",
|
|
417
|
+
detail: result.payload && typeof result.payload === "object" && "reason" in result.payload ? String(result.payload.reason) : void 0
|
|
418
|
+
});
|
|
419
|
+
} else if (result.disposition === "failed") {
|
|
420
|
+
renderer.renderStep({
|
|
421
|
+
name: stageLabel(stageName),
|
|
422
|
+
current: stepNum,
|
|
423
|
+
total: totalStages,
|
|
424
|
+
status: "error",
|
|
425
|
+
detail: result.errors.join(", ")
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (result.disposition === "failed") {
|
|
430
|
+
await this.rollback(context);
|
|
431
|
+
if (renderer) {
|
|
432
|
+
const errorInfo = {
|
|
433
|
+
title: `${stageLabel(stageName)} ${t("cli.install.stages.failed")}`,
|
|
434
|
+
message: result.errors.join(", "),
|
|
435
|
+
hint: "Check the error details above. Run with --debug for more information."
|
|
436
|
+
};
|
|
437
|
+
renderer.renderError(errorInfo);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
success: false,
|
|
441
|
+
context,
|
|
442
|
+
error: new Error(`Stage ${stageName} failed: ${result.errors.join(", ")}`)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
await this.rollback(context);
|
|
447
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
448
|
+
if (renderer) {
|
|
449
|
+
renderer.renderError(err);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
success: false,
|
|
453
|
+
context,
|
|
454
|
+
error: err
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (renderer) {
|
|
459
|
+
const summary = this.buildSummary(context);
|
|
460
|
+
renderer.renderSummaryCard(summary);
|
|
461
|
+
renderer.renderComplete();
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
success: true,
|
|
465
|
+
context
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Build summary info from accumulated stage results.
|
|
470
|
+
*/
|
|
471
|
+
buildSummary(context) {
|
|
472
|
+
const results = context.stageResults;
|
|
473
|
+
const successCount = results.filter((r) => r.disposition === "ran").length;
|
|
474
|
+
const skippedCount = results.filter((r) => r.disposition === "skipped").length;
|
|
475
|
+
const errorCount = results.filter((r) => r.disposition === "failed").length;
|
|
476
|
+
const details = results.map((r) => ({
|
|
477
|
+
label: stageLabel(r.name),
|
|
478
|
+
value: r.disposition === "ran" ? `${r.installed.length} installed` : r.disposition === "skipped" ? "skipped" : `${r.errors.length} error(s)`,
|
|
479
|
+
status: r.disposition === "ran" ? "success" : r.disposition === "skipped" ? "skipped" : "error"
|
|
480
|
+
}));
|
|
481
|
+
return {
|
|
482
|
+
title: t("cli.install.pipeline.complete"),
|
|
483
|
+
successCount,
|
|
484
|
+
skippedCount,
|
|
485
|
+
errorCount,
|
|
486
|
+
details
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Execute rollback actions in reverse order.
|
|
491
|
+
*/
|
|
492
|
+
async rollback(context) {
|
|
493
|
+
const rollbackStack = [...context.rollbackStack].reverse();
|
|
494
|
+
for (const { action } of rollbackStack) {
|
|
495
|
+
try {
|
|
496
|
+
await action();
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const stage of [...this.stages].reverse()) {
|
|
501
|
+
if (stage.rollback) {
|
|
502
|
+
try {
|
|
503
|
+
await stage.rollback(context);
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
function stageRan(name, installed = [], skipped = [], payload) {
|
|
511
|
+
return {
|
|
512
|
+
name,
|
|
513
|
+
disposition: "ran",
|
|
514
|
+
installed,
|
|
515
|
+
skipped,
|
|
516
|
+
errors: [],
|
|
517
|
+
payload
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function stageSkipped(name, reason) {
|
|
521
|
+
return {
|
|
522
|
+
name,
|
|
523
|
+
disposition: "skipped",
|
|
524
|
+
installed: [],
|
|
525
|
+
skipped: [],
|
|
526
|
+
errors: [],
|
|
527
|
+
payload: reason ? { reason } : void 0
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function stageFailed(name, errors) {
|
|
531
|
+
return {
|
|
532
|
+
name,
|
|
533
|
+
disposition: "failed",
|
|
534
|
+
installed: [],
|
|
535
|
+
skipped: [],
|
|
536
|
+
errors
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function stageFailedFromError(name, error) {
|
|
540
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
541
|
+
return stageFailed(name, [message]);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/install/pipeline/preflight.stage.ts
|
|
545
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
546
|
+
import { existsSync as existsSync2, rmSync as rmSync3, statSync, writeFileSync } from "fs";
|
|
547
|
+
import { dirname, isAbsolute, join as join3, resolve } from "path";
|
|
548
|
+
var PreflightStage = class {
|
|
549
|
+
name = "preflight";
|
|
550
|
+
async execute(context) {
|
|
551
|
+
const target = this.normalizeTarget(context.args.target ?? process.cwd());
|
|
552
|
+
try {
|
|
553
|
+
this.assertExistingDirectory(target);
|
|
554
|
+
context.target = target;
|
|
555
|
+
context.state.globalRoot = this.resolveGlobalRoot();
|
|
556
|
+
if (context.options.planOnly === true) {
|
|
557
|
+
this.assertGlobalRootPlannable(context.state.globalRoot);
|
|
558
|
+
} else {
|
|
559
|
+
this.assertGlobalRootWritable(context.state.globalRoot);
|
|
560
|
+
this.assertWritable(target);
|
|
561
|
+
}
|
|
562
|
+
if (context.args.url) {
|
|
563
|
+
this.assertGitAvailable();
|
|
564
|
+
}
|
|
565
|
+
return stageRan("preflight", [], [target]);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
return stageFailedFromError("preflight", error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
normalizeTarget(targetInput) {
|
|
571
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
572
|
+
}
|
|
573
|
+
assertExistingDirectory(target) {
|
|
574
|
+
if (!existsSync2(target) || !statSync(target).isDirectory()) {
|
|
575
|
+
throw new Error(t("cli.shared.target-invalid", { target }));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
resolveGlobalRoot() {
|
|
579
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
580
|
+
if (!home) {
|
|
581
|
+
throw new Error("Cannot determine home directory for global root");
|
|
582
|
+
}
|
|
583
|
+
return resolve(home, ".fabric");
|
|
584
|
+
}
|
|
585
|
+
assertGlobalRootWritable(globalRoot) {
|
|
586
|
+
if (existsSync2(globalRoot)) {
|
|
587
|
+
if (!statSync(globalRoot).isDirectory()) {
|
|
588
|
+
throw new Error(`Global Fabric root is not a directory: ${globalRoot}`);
|
|
589
|
+
}
|
|
590
|
+
this.assertWritable(globalRoot, "Global Fabric root");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const parent = dirname(globalRoot);
|
|
594
|
+
if (!existsSync2(parent) || !statSync(parent).isDirectory()) {
|
|
595
|
+
throw new Error(`Global Fabric root parent is not a directory: ${parent}`);
|
|
596
|
+
}
|
|
597
|
+
this.assertWritable(parent, "Global Fabric root parent");
|
|
598
|
+
}
|
|
599
|
+
assertGlobalRootPlannable(globalRoot) {
|
|
600
|
+
if (existsSync2(globalRoot)) {
|
|
601
|
+
if (!statSync(globalRoot).isDirectory()) {
|
|
602
|
+
throw new Error(`Global Fabric root is not a directory: ${globalRoot}`);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const parent = dirname(globalRoot);
|
|
607
|
+
if (!existsSync2(parent) || !statSync(parent).isDirectory()) {
|
|
608
|
+
throw new Error(`Global Fabric root parent is not a directory: ${parent}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
assertWritable(path, label = "Target") {
|
|
612
|
+
const probePath = join3(path, `.fabric-preflight-${process.pid}-${Date.now()}.tmp`);
|
|
613
|
+
try {
|
|
614
|
+
writeFileSync(probePath, "", { flag: "wx" });
|
|
615
|
+
} catch (error) {
|
|
616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
617
|
+
throw new Error(`${label} is not writable: ${path} (${message})`);
|
|
618
|
+
} finally {
|
|
619
|
+
rmSync3(probePath, { force: true });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
assertGitAvailable() {
|
|
623
|
+
try {
|
|
624
|
+
execFileSync3("git", ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
625
|
+
} catch (error) {
|
|
626
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
627
|
+
throw new Error(`git is required for --url installs but was not available: ${message}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/install/pipeline/env.stage.ts
|
|
633
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
634
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
635
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
636
|
+
import { resolveGlobalLocale } from "@fenglimg/fabric-shared";
|
|
637
|
+
|
|
638
|
+
// src/scanner/forensic.ts
|
|
639
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
640
|
+
import { existsSync as existsSync3, readdirSync, readFileSync, statSync as statSync2 } from "fs";
|
|
641
|
+
import { createRequire } from "module";
|
|
642
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join4, posix, relative, resolve as resolve2, sep } from "path";
|
|
643
|
+
import {
|
|
644
|
+
buildScanRecommendations,
|
|
645
|
+
forensicReportSchema
|
|
646
|
+
} from "@fenglimg/fabric-shared";
|
|
647
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
648
|
+
var require2 = createRequire(import.meta.url);
|
|
649
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
650
|
+
".fabric",
|
|
651
|
+
".git",
|
|
652
|
+
".next",
|
|
653
|
+
".turbo",
|
|
654
|
+
"Library",
|
|
655
|
+
"Temp",
|
|
656
|
+
"build",
|
|
657
|
+
"coverage",
|
|
658
|
+
"dist",
|
|
659
|
+
"node_modules"
|
|
660
|
+
]);
|
|
661
|
+
var KEY_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
662
|
+
"app",
|
|
663
|
+
"components",
|
|
664
|
+
"pages",
|
|
665
|
+
"prefabs",
|
|
666
|
+
"scenes",
|
|
667
|
+
"scripts",
|
|
668
|
+
"src"
|
|
669
|
+
]);
|
|
670
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
671
|
+
var DOMAIN_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".json", ".md"]);
|
|
672
|
+
var EXPECTED_CONFIG_FILES_BY_FRAMEWORK = {
|
|
673
|
+
"cocos-creator": ["package.json", "project.config.json", "tsconfig.json"],
|
|
674
|
+
react: ["package.json", "tsconfig.json"],
|
|
675
|
+
next: ["package.json", "tsconfig.json"],
|
|
676
|
+
vite: ["package.json", "tsconfig.json"]
|
|
677
|
+
};
|
|
678
|
+
var FRAMEWORK_IMPORT_PROFILES = {
|
|
679
|
+
"cocos-creator": {
|
|
680
|
+
pattern: "cocos-component-class",
|
|
681
|
+
family: "component",
|
|
682
|
+
statement: "Sampled entry files use Cocos Creator component classes.",
|
|
683
|
+
proposedRule: "Treat assets/scripts/*.ts and adjacent .meta files as framework-owned structure unless the user says otherwise.",
|
|
684
|
+
alternatives: ["Generic TypeScript utility module"],
|
|
685
|
+
rationale: "Cocos framework imports and component markers co-occur in sampled entry files.",
|
|
686
|
+
packages: ["cc"]
|
|
687
|
+
},
|
|
688
|
+
react: {
|
|
689
|
+
pattern: "react-root",
|
|
690
|
+
family: "entry",
|
|
691
|
+
statement: "Sampled entry files import React framework packages.",
|
|
692
|
+
proposedRule: "Keep root rendering and component composition aligned with React entry conventions.",
|
|
693
|
+
alternatives: ["Server-rendered route module"],
|
|
694
|
+
rationale: "AST import declarations reference React packages rather than comments or strings.",
|
|
695
|
+
packages: ["react", "react-dom", "react/jsx-runtime", "react-dom/client"]
|
|
696
|
+
},
|
|
697
|
+
vite: {
|
|
698
|
+
pattern: "vite-main-entry",
|
|
699
|
+
family: "entry",
|
|
700
|
+
statement: "Sampled entry files use the conventional Vite main entrypoint.",
|
|
701
|
+
proposedRule: "Keep primary bootstrapping logic inside src/main.*.",
|
|
702
|
+
alternatives: ["Alternative bundler entrypoint"],
|
|
703
|
+
rationale: "Entry path and framework imports align with a Vite bootstrap surface.",
|
|
704
|
+
packages: ["@vitejs/plugin-react", "@vitejs/plugin-vue", "vite", "react", "vue"]
|
|
705
|
+
},
|
|
706
|
+
next: {
|
|
707
|
+
pattern: "next-route-component",
|
|
708
|
+
family: "entry",
|
|
709
|
+
statement: "Sampled entry files align with Next.js route modules.",
|
|
710
|
+
proposedRule: "Preserve route-segment boundaries when editing app/ or pages/ files.",
|
|
711
|
+
alternatives: ["Generic source module"],
|
|
712
|
+
rationale: "Route placement and Next/React imports anchor these files to the request surface.",
|
|
713
|
+
packages: ["next", "next/link", "next/navigation", "react"]
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
var SAMPLE_LIMIT = 5;
|
|
717
|
+
var SAMPLE_LINE_LIMIT = 30;
|
|
718
|
+
var ENTRY_FAMILY_LIMIT = 1;
|
|
719
|
+
var FAMILY_LIMIT = 3;
|
|
720
|
+
var CANDIDATE_FILE_LIMIT = 12;
|
|
721
|
+
var DEFAULT_SAMPLING_BUDGET = {
|
|
722
|
+
max_files: 15,
|
|
723
|
+
max_lines_per_file: 100
|
|
724
|
+
};
|
|
725
|
+
var treeSitterModulePromise = null;
|
|
726
|
+
var parserInitPromise = null;
|
|
727
|
+
var languagePromiseByKind = {};
|
|
728
|
+
var parserBundlePromiseByKind = {};
|
|
729
|
+
async function buildForensicReport(targetInput) {
|
|
730
|
+
const target = normalizeTarget(targetInput);
|
|
731
|
+
const framework = detectFramework(target);
|
|
732
|
+
const topology = buildTopology(target);
|
|
733
|
+
const entryPoints = collectEntryPoints(target, topology.files);
|
|
734
|
+
const packageDependencies = readPackageDependencies(target);
|
|
735
|
+
const codeSamples = await buildCodeSamples(target, entryPoints, framework.kind, topology, packageDependencies);
|
|
736
|
+
const assertions = buildAssertions(framework.kind, topology, codeSamples);
|
|
737
|
+
const candidateFiles = buildCandidateFiles(topology, codeSamples, entryPoints);
|
|
738
|
+
const readme = readReadmeInfo(target);
|
|
739
|
+
const report = {
|
|
740
|
+
version: "1.0",
|
|
741
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
742
|
+
generated_by: `fabric-cli@${getCliVersion()}`,
|
|
743
|
+
target,
|
|
744
|
+
project_name: readProjectName(target),
|
|
745
|
+
framework,
|
|
746
|
+
topology: {
|
|
747
|
+
total_files: topology.total_files,
|
|
748
|
+
by_ext: topology.by_ext,
|
|
749
|
+
key_dirs: topology.key_dirs,
|
|
750
|
+
max_depth: topology.max_depth
|
|
751
|
+
},
|
|
752
|
+
entry_points: entryPoints,
|
|
753
|
+
code_samples: codeSamples.map(({ pattern_analysis: _patternAnalysis, evidence: _evidence, ...sample }) => sample),
|
|
754
|
+
assertions,
|
|
755
|
+
candidate_files: candidateFiles,
|
|
756
|
+
sampling_budget: DEFAULT_SAMPLING_BUDGET,
|
|
757
|
+
readme,
|
|
758
|
+
recommendations_for_skill: buildSkillRecommendations(framework.kind, topology, readme, target)
|
|
759
|
+
};
|
|
760
|
+
const validation = forensicReportSchema.safeParse(report);
|
|
761
|
+
if (!validation.success) {
|
|
762
|
+
throw new Error(`ForensicReport schema validation failed: ${validation.error.message}`);
|
|
763
|
+
}
|
|
764
|
+
return validation.data;
|
|
765
|
+
}
|
|
766
|
+
function normalizeTarget(targetInput) {
|
|
767
|
+
return isAbsolute2(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
|
|
768
|
+
}
|
|
769
|
+
function buildTopology(root) {
|
|
770
|
+
assertExistingDirectory(root);
|
|
771
|
+
const byExt = {};
|
|
772
|
+
const keyDirs = /* @__PURE__ */ new Set();
|
|
773
|
+
const files = [];
|
|
774
|
+
let totalFiles = 0;
|
|
775
|
+
let maxDepth = 0;
|
|
776
|
+
const stack = [root];
|
|
777
|
+
while (stack.length > 0) {
|
|
778
|
+
const current = stack.pop();
|
|
779
|
+
if (current === void 0) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
783
|
+
const absolutePath = join4(current, entry.name);
|
|
784
|
+
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
785
|
+
if (relativePath.length === 0) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const depth = relativePath.split("/").length;
|
|
789
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
790
|
+
if (entry.isDirectory()) {
|
|
791
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (isKeyDirectory(relativePath)) {
|
|
795
|
+
keyDirs.add(relativePath);
|
|
796
|
+
}
|
|
797
|
+
stack.push(absolutePath);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (!entry.isFile()) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const stats = statSync2(absolutePath);
|
|
804
|
+
const extension = extname(entry.name) || "[none]";
|
|
805
|
+
byExt[extension] = (byExt[extension] ?? 0) + 1;
|
|
806
|
+
totalFiles += 1;
|
|
807
|
+
files.push({
|
|
808
|
+
relativePath,
|
|
809
|
+
sizeBytes: stats.size
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
total_files: totalFiles,
|
|
815
|
+
by_ext: sortRecord(byExt),
|
|
816
|
+
key_dirs: [...keyDirs].sort(),
|
|
817
|
+
max_depth: maxDepth,
|
|
818
|
+
files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function assertExistingDirectory(target) {
|
|
822
|
+
if (!existsSync3(target) || !statSync2(target).isDirectory()) {
|
|
823
|
+
throw new Error(`Target must be an existing directory: ${target}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function isKeyDirectory(relativePath) {
|
|
827
|
+
const name = basename(relativePath);
|
|
828
|
+
return KEY_DIRECTORY_NAMES.has(name);
|
|
829
|
+
}
|
|
830
|
+
function collectEntryPoints(target, files) {
|
|
831
|
+
const entryPoints = [];
|
|
832
|
+
for (const file of files) {
|
|
833
|
+
const reason = getEntryPointReason(file.relativePath);
|
|
834
|
+
if (reason === null) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
entryPoints.push({
|
|
838
|
+
path: file.relativePath,
|
|
839
|
+
reason,
|
|
840
|
+
size_bytes: file.sizeBytes
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const churnByPath = new Map(
|
|
844
|
+
entryPoints.map((entryPoint) => [entryPoint.path, readGitChurnWeight(target, entryPoint.path)])
|
|
845
|
+
);
|
|
846
|
+
return entryPoints.sort(
|
|
847
|
+
(left, right) => compareCandidateScore(churnByPath.get(right.path) ?? 0, churnByPath.get(left.path) ?? 0)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
function getEntryPointReason(relativePath) {
|
|
851
|
+
if (!SCRIPT_EXTENSIONS.has(extname(relativePath))) {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
const directory = posix.dirname(relativePath);
|
|
855
|
+
const fileName = basename(relativePath);
|
|
856
|
+
const fileBase = basename(relativePath, extname(relativePath));
|
|
857
|
+
if (directory === "assets/scripts" || directory === "scripts") {
|
|
858
|
+
return "top-level script";
|
|
859
|
+
}
|
|
860
|
+
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
861
|
+
return "application entry";
|
|
862
|
+
}
|
|
863
|
+
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
864
|
+
return "next app route";
|
|
865
|
+
}
|
|
866
|
+
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
867
|
+
return "next page route";
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
|
|
872
|
+
const samples = [];
|
|
873
|
+
for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
|
|
874
|
+
const absolutePath = join4(target, ...entryPoint.path.split("/"));
|
|
875
|
+
const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
|
|
876
|
+
const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
|
|
877
|
+
frameworkKind,
|
|
878
|
+
topology,
|
|
879
|
+
packageDependencies
|
|
880
|
+
});
|
|
881
|
+
samples.push({
|
|
882
|
+
path: entryPoint.path,
|
|
883
|
+
lines: `1-${sample.lineCount}`,
|
|
884
|
+
snippet: sample.snippet,
|
|
885
|
+
pattern_hint: patternAnalysis.pattern,
|
|
886
|
+
pattern_analysis: patternAnalysis,
|
|
887
|
+
evidence: buildEvidenceAnchors(entryPoint.path, sample.snippet, patternAnalysis.evidence_lines)
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return samples;
|
|
891
|
+
}
|
|
892
|
+
function readFirstLines(path, lineLimit) {
|
|
893
|
+
try {
|
|
894
|
+
const lines = readFileSync(path, "utf8").split(/\r?\n/);
|
|
895
|
+
if (lines.at(-1) === "") {
|
|
896
|
+
lines.pop();
|
|
897
|
+
}
|
|
898
|
+
const sampledLines = lines.slice(0, lineLimit);
|
|
899
|
+
return {
|
|
900
|
+
snippet: sampledLines.join("\n"),
|
|
901
|
+
lineCount: sampledLines.length
|
|
902
|
+
};
|
|
903
|
+
} catch {
|
|
904
|
+
return {
|
|
905
|
+
snippet: "",
|
|
906
|
+
lineCount: 0
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function readPackageDependencies(target) {
|
|
911
|
+
const packageJsonPath = join4(target, "package.json");
|
|
912
|
+
if (!existsSync3(packageJsonPath)) {
|
|
913
|
+
return /* @__PURE__ */ new Map();
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
917
|
+
return new Map([
|
|
918
|
+
...Object.entries(packageJson.dependencies ?? {}),
|
|
919
|
+
...Object.entries(packageJson.devDependencies ?? {}),
|
|
920
|
+
...Object.entries(packageJson.peerDependencies ?? {}),
|
|
921
|
+
...Object.entries(packageJson.optionalDependencies ?? {})
|
|
922
|
+
]);
|
|
923
|
+
} catch {
|
|
924
|
+
return /* @__PURE__ */ new Map();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function readGitChurnWeight(target, relativePath) {
|
|
928
|
+
try {
|
|
929
|
+
const output = execFileSync4("git", ["log", "--follow", "--oneline", "-20", "--", relativePath], {
|
|
930
|
+
cwd: target,
|
|
931
|
+
encoding: "utf8",
|
|
932
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
933
|
+
timeout: 1e3
|
|
934
|
+
});
|
|
935
|
+
return output.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
|
|
936
|
+
} catch {
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function inferPatternHint(relativePath, snippet, options = {}) {
|
|
941
|
+
const input = {
|
|
942
|
+
relativePath,
|
|
943
|
+
snippet,
|
|
944
|
+
frameworkKind: options.frameworkKind ?? "unknown",
|
|
945
|
+
topology: options.topology ?? createEmptyTopology(),
|
|
946
|
+
packageDependencies: options.packageDependencies ?? /* @__PURE__ */ new Map()
|
|
947
|
+
};
|
|
948
|
+
const importAnalysis = await analyzeImports(input.relativePath, input.snippet);
|
|
949
|
+
if (importAnalysis.astLevel) {
|
|
950
|
+
const astResult = buildAstPatternHint(input, importAnalysis.imports);
|
|
951
|
+
if (astResult !== null) {
|
|
952
|
+
return astResult;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return inferTextPatternHint(input.relativePath, input.snippet);
|
|
956
|
+
}
|
|
957
|
+
function createEmptyTopology() {
|
|
958
|
+
return {
|
|
959
|
+
total_files: 0,
|
|
960
|
+
by_ext: {},
|
|
961
|
+
key_dirs: [],
|
|
962
|
+
max_depth: 0,
|
|
963
|
+
files: []
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function buildAstPatternHint(input, imports) {
|
|
967
|
+
const profile = resolveFrameworkImportProfile(input.frameworkKind, input.relativePath, imports);
|
|
968
|
+
if (profile === null) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
const matchingImports = imports.filter((source) => matchesAnyFrameworkPackage(source, profile.packages));
|
|
972
|
+
const configFiles = getExpectedConfigFiles(input.frameworkKind).filter((file) => hasFile(input.topology.files, file));
|
|
973
|
+
const packageMatches = profile.packages.filter((packageName) => input.packageDependencies.has(packageName));
|
|
974
|
+
const coOccurring = compactPatternNames([
|
|
975
|
+
...matchingImports.map((source) => `import:${source}`),
|
|
976
|
+
...configFiles.map(normalizeConfigPattern),
|
|
977
|
+
...packageMatches.map((packageName) => `package:${packageName}`),
|
|
978
|
+
input.relativePath.startsWith("app/") ? "app-router" : null,
|
|
979
|
+
input.relativePath.startsWith("pages/") ? "pages-router" : null,
|
|
980
|
+
input.relativePath === "src/main.ts" || input.relativePath === "src/main.js" ? "main-entry" : null,
|
|
981
|
+
input.snippet.includes("@ccclass(") ? "ccclass-decorator" : null,
|
|
982
|
+
input.snippet.includes("extends Component") ? "component-base" : null
|
|
983
|
+
]);
|
|
984
|
+
return {
|
|
985
|
+
pattern: profile.pattern,
|
|
986
|
+
type: "pattern",
|
|
987
|
+
confidence: scoreFrameworkConfidence({
|
|
988
|
+
importCount: matchingImports.length,
|
|
989
|
+
configCount: configFiles.length,
|
|
990
|
+
packageCount: packageMatches.length,
|
|
991
|
+
astLevel: true
|
|
992
|
+
}),
|
|
993
|
+
evidence_lines: matchingImports.length > 0 ? matchingImports : imports.slice(0, 3),
|
|
994
|
+
co_occurring: coOccurring,
|
|
995
|
+
family: profile.family,
|
|
996
|
+
ast_level: true,
|
|
997
|
+
statement: profile.statement,
|
|
998
|
+
proposed_rule: profile.proposedRule,
|
|
999
|
+
alternatives: profile.alternatives,
|
|
1000
|
+
rationale: profile.rationale
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function inferTextPatternHint(relativePath, snippet) {
|
|
1004
|
+
const cocosCoOccurring = compactPatternNames([
|
|
1005
|
+
snippet.includes('from "cc"') || snippet.includes("from 'cc'") ? "cc-import" : null,
|
|
1006
|
+
snippet.includes("@ccclass(") || snippet.includes("ccclass(") ? "ccclass-decorator" : null,
|
|
1007
|
+
snippet.includes("extends Component") ? "component-base" : null,
|
|
1008
|
+
snippet.includes("const { ccclass } = _decorator") ? "decorator-destructure" : null
|
|
1009
|
+
]);
|
|
1010
|
+
if (cocosCoOccurring.length > 0) {
|
|
1011
|
+
return {
|
|
1012
|
+
pattern: "cocos-component-class",
|
|
1013
|
+
type: "pattern",
|
|
1014
|
+
confidence: scoreFrameworkConfidence({
|
|
1015
|
+
importCount: 0,
|
|
1016
|
+
configCount: 0,
|
|
1017
|
+
packageCount: 0,
|
|
1018
|
+
astLevel: false,
|
|
1019
|
+
keywordCount: cocosCoOccurring.length
|
|
1020
|
+
}),
|
|
1021
|
+
evidence_lines: compactPatternNames([
|
|
1022
|
+
snippet.includes("_decorator") ? "_decorator" : null,
|
|
1023
|
+
snippet.includes("@ccclass(") ? "@ccclass(" : null,
|
|
1024
|
+
snippet.includes("extends Component") ? "extends Component" : null
|
|
1025
|
+
]),
|
|
1026
|
+
co_occurring: cocosCoOccurring,
|
|
1027
|
+
family: "component",
|
|
1028
|
+
ast_level: false,
|
|
1029
|
+
statement: "Sampled entry files use Cocos Creator component classes.",
|
|
1030
|
+
proposed_rule: "Treat assets/scripts/*.ts and adjacent .meta files as framework-owned structure unless the user says otherwise.",
|
|
1031
|
+
alternatives: ["Generic TypeScript utility module"],
|
|
1032
|
+
rationale: "Cocos-specific decorators and Component inheritance co-occur in sampled entry files."
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
const reactCoOccurring = compactPatternNames([
|
|
1036
|
+
snippet.includes("createRoot(") ? "create-root" : null,
|
|
1037
|
+
snippet.includes("ReactDOM.render(") ? "react-dom-render" : null,
|
|
1038
|
+
snippet.includes('from "react-dom"') || snippet.includes("from 'react-dom'") ? "react-dom-import" : null
|
|
1039
|
+
]);
|
|
1040
|
+
if (reactCoOccurring.length > 0) {
|
|
1041
|
+
return {
|
|
1042
|
+
pattern: "react-root",
|
|
1043
|
+
type: "pattern",
|
|
1044
|
+
confidence: scoreFrameworkConfidence({
|
|
1045
|
+
importCount: 0,
|
|
1046
|
+
configCount: 0,
|
|
1047
|
+
packageCount: 0,
|
|
1048
|
+
astLevel: false,
|
|
1049
|
+
keywordCount: reactCoOccurring.length
|
|
1050
|
+
}),
|
|
1051
|
+
evidence_lines: compactPatternNames([
|
|
1052
|
+
snippet.includes("createRoot(") ? "createRoot(" : null,
|
|
1053
|
+
snippet.includes("ReactDOM.render(") ? "ReactDOM.render(" : null
|
|
1054
|
+
]),
|
|
1055
|
+
co_occurring: reactCoOccurring,
|
|
1056
|
+
family: "entry",
|
|
1057
|
+
ast_level: false,
|
|
1058
|
+
statement: "Sampled entry files bootstrap a React DOM root.",
|
|
1059
|
+
proposed_rule: "Keep root rendering logic in the main application entry file.",
|
|
1060
|
+
alternatives: ["Server-rendered route module"],
|
|
1061
|
+
rationale: "React DOM root markers identify a frontend entrypoint."
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
if (relativePath.startsWith("app/") || relativePath.startsWith("pages/")) {
|
|
1065
|
+
const coOccurring = compactPatternNames([
|
|
1066
|
+
relativePath.startsWith("app/") ? "app-router" : null,
|
|
1067
|
+
relativePath.startsWith("pages/") ? "pages-router" : null,
|
|
1068
|
+
snippet.includes("export default") ? "default-export-route" : null
|
|
1069
|
+
]);
|
|
1070
|
+
return {
|
|
1071
|
+
pattern: "next-route-component",
|
|
1072
|
+
type: "pattern",
|
|
1073
|
+
confidence: scoreFrameworkConfidence({
|
|
1074
|
+
importCount: 0,
|
|
1075
|
+
configCount: 0,
|
|
1076
|
+
packageCount: 0,
|
|
1077
|
+
astLevel: false,
|
|
1078
|
+
keywordCount: coOccurring.length
|
|
1079
|
+
}),
|
|
1080
|
+
evidence_lines: compactPatternNames([
|
|
1081
|
+
relativePath.startsWith("app/") ? "app/" : null,
|
|
1082
|
+
relativePath.startsWith("pages/") ? "pages/" : null
|
|
1083
|
+
]),
|
|
1084
|
+
co_occurring: coOccurring,
|
|
1085
|
+
family: "entry",
|
|
1086
|
+
ast_level: false,
|
|
1087
|
+
statement: "Sampled entry files align with Next.js route modules.",
|
|
1088
|
+
proposed_rule: "Preserve route-segment boundaries when editing app/ or pages/ files.",
|
|
1089
|
+
alternatives: ["Generic source module"],
|
|
1090
|
+
rationale: "Route directory placement anchors these files to the Next.js request surface."
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (relativePath === "src/main.ts" || relativePath === "src/main.js") {
|
|
1094
|
+
const coOccurring = compactPatternNames([
|
|
1095
|
+
"main-entry",
|
|
1096
|
+
snippet.includes("import.meta") ? "import-meta" : null,
|
|
1097
|
+
snippet.includes("createRoot(") ? "react-root" : null
|
|
1098
|
+
]);
|
|
1099
|
+
return {
|
|
1100
|
+
pattern: "vite-main-entry",
|
|
1101
|
+
type: "pattern",
|
|
1102
|
+
confidence: scoreFrameworkConfidence({
|
|
1103
|
+
importCount: 0,
|
|
1104
|
+
configCount: 0,
|
|
1105
|
+
packageCount: 0,
|
|
1106
|
+
astLevel: false,
|
|
1107
|
+
keywordCount: coOccurring.length
|
|
1108
|
+
}),
|
|
1109
|
+
evidence_lines: ["src/main"],
|
|
1110
|
+
co_occurring: coOccurring,
|
|
1111
|
+
family: "entry",
|
|
1112
|
+
ast_level: false,
|
|
1113
|
+
statement: "Sampled entry files use the conventional Vite main entrypoint.",
|
|
1114
|
+
proposed_rule: "Keep primary bootstrapping logic inside src/main.*.",
|
|
1115
|
+
alternatives: ["Alternative bundler entrypoint"],
|
|
1116
|
+
rationale: "src/main.* is the expected Vite bootstrap path."
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
pattern: "source-entry",
|
|
1121
|
+
type: "pattern",
|
|
1122
|
+
confidence: "LOW",
|
|
1123
|
+
evidence_lines: [basename(relativePath)],
|
|
1124
|
+
co_occurring: [],
|
|
1125
|
+
family: "domain",
|
|
1126
|
+
ast_level: false,
|
|
1127
|
+
statement: "Sampled entry file appears to be a generic source entry.",
|
|
1128
|
+
alternatives: ["Framework-specific entrypoint"],
|
|
1129
|
+
rationale: "No strong framework markers were detected in the sampled snippet."
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
async function analyzeImports(relativePath, snippet) {
|
|
1133
|
+
if (snippet.trim().length === 0) {
|
|
1134
|
+
return { imports: [], astLevel: false };
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
const imports = await extractImports(snippet, getLanguageKindForPath(relativePath));
|
|
1138
|
+
return { imports, astLevel: true };
|
|
1139
|
+
} catch {
|
|
1140
|
+
return { imports: [], astLevel: false };
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function extractImports(source, languageKind) {
|
|
1144
|
+
const { parser } = await loadTreeSitter(languageKind);
|
|
1145
|
+
let tree = null;
|
|
1146
|
+
try {
|
|
1147
|
+
tree = parser.parse(source);
|
|
1148
|
+
if (tree === null || tree.rootNode.hasError) {
|
|
1149
|
+
throw new Error("tree-sitter parse failed");
|
|
1150
|
+
}
|
|
1151
|
+
const imports = [];
|
|
1152
|
+
collectImportSources(tree.rootNode, imports);
|
|
1153
|
+
return compactPatternNames(imports);
|
|
1154
|
+
} finally {
|
|
1155
|
+
tree?.delete();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async function loadTreeSitter(languageKind) {
|
|
1159
|
+
parserBundlePromiseByKind[languageKind] ??= createTreeSitterParserBundle(languageKind);
|
|
1160
|
+
return parserBundlePromiseByKind[languageKind];
|
|
1161
|
+
}
|
|
1162
|
+
async function createTreeSitterParserBundle(languageKind) {
|
|
1163
|
+
const treeSitter = await loadTreeSitterModule();
|
|
1164
|
+
await initTreeSitterParser(treeSitter);
|
|
1165
|
+
const language = await loadTreeSitterLanguage(treeSitter, languageKind);
|
|
1166
|
+
const parser = new treeSitter.Parser();
|
|
1167
|
+
parser.setLanguage(language);
|
|
1168
|
+
return { parser, language };
|
|
1169
|
+
}
|
|
1170
|
+
function loadTreeSitterModule() {
|
|
1171
|
+
treeSitterModulePromise ??= import("web-tree-sitter");
|
|
1172
|
+
return treeSitterModulePromise;
|
|
1173
|
+
}
|
|
1174
|
+
function initTreeSitterParser(treeSitter) {
|
|
1175
|
+
parserInitPromise ??= treeSitter.Parser.init({
|
|
1176
|
+
locateFile: (scriptName) => scriptName.endsWith(".wasm") ? require2.resolve("web-tree-sitter/web-tree-sitter.wasm") : scriptName
|
|
1177
|
+
});
|
|
1178
|
+
return parserInitPromise;
|
|
1179
|
+
}
|
|
1180
|
+
function loadTreeSitterLanguage(treeSitter, languageKind) {
|
|
1181
|
+
languagePromiseByKind[languageKind] ??= treeSitter.Language.load(resolveTreeSitterGrammarPath(languageKind));
|
|
1182
|
+
return languagePromiseByKind[languageKind];
|
|
1183
|
+
}
|
|
1184
|
+
function resolveTreeSitterGrammarPath(languageKind) {
|
|
1185
|
+
switch (languageKind) {
|
|
1186
|
+
case "typescript":
|
|
1187
|
+
return require2.resolve("tree-sitter-typescript/tree-sitter-typescript.wasm");
|
|
1188
|
+
case "tsx":
|
|
1189
|
+
return require2.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
|
|
1190
|
+
case "javascript":
|
|
1191
|
+
return require2.resolve("tree-sitter-javascript/tree-sitter-javascript.wasm");
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function getLanguageKindForPath(relativePath) {
|
|
1195
|
+
const extension = extname(relativePath);
|
|
1196
|
+
if (extension === ".tsx") {
|
|
1197
|
+
return "tsx";
|
|
1198
|
+
}
|
|
1199
|
+
if (extension === ".ts") {
|
|
1200
|
+
return "typescript";
|
|
1201
|
+
}
|
|
1202
|
+
return "javascript";
|
|
1203
|
+
}
|
|
1204
|
+
function collectImportSources(node, imports) {
|
|
1205
|
+
if (node.type === "import_statement" || node.type === "import_declaration") {
|
|
1206
|
+
const sourceNode = node.childForFieldName("source");
|
|
1207
|
+
if (sourceNode !== null) {
|
|
1208
|
+
const source = stripStringLiteral(sourceNode.text);
|
|
1209
|
+
if (source.length > 0) {
|
|
1210
|
+
imports.push(source);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
1215
|
+
const child = node.namedChild(index);
|
|
1216
|
+
if (child !== null) {
|
|
1217
|
+
collectImportSources(child, imports);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function stripStringLiteral(value) {
|
|
1222
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
1223
|
+
}
|
|
1224
|
+
function resolveFrameworkImportProfile(frameworkKind, relativePath, imports) {
|
|
1225
|
+
const primaryProfile = FRAMEWORK_IMPORT_PROFILES[frameworkKind];
|
|
1226
|
+
if (primaryProfile !== void 0 && imports.some((source) => matchesAnyFrameworkPackage(source, primaryProfile.packages))) {
|
|
1227
|
+
return primaryProfile;
|
|
1228
|
+
}
|
|
1229
|
+
if ((relativePath.startsWith("app/") || relativePath.startsWith("pages/")) && FRAMEWORK_IMPORT_PROFILES.next !== void 0) {
|
|
1230
|
+
return FRAMEWORK_IMPORT_PROFILES.next;
|
|
1231
|
+
}
|
|
1232
|
+
return Object.values(FRAMEWORK_IMPORT_PROFILES).find(
|
|
1233
|
+
(profile) => imports.some((source) => matchesAnyFrameworkPackage(source, profile.packages))
|
|
1234
|
+
) ?? null;
|
|
1235
|
+
}
|
|
1236
|
+
function matchesAnyFrameworkPackage(source, packageNames) {
|
|
1237
|
+
return packageNames.some((packageName) => source === packageName || source.startsWith(`${packageName}/`));
|
|
1238
|
+
}
|
|
1239
|
+
function scoreFrameworkConfidence(input) {
|
|
1240
|
+
if (!input.astLevel) {
|
|
1241
|
+
return (input.keywordCount ?? 0) > 0 ? "MEDIUM" : "LOW";
|
|
1242
|
+
}
|
|
1243
|
+
if (input.importCount > 3) {
|
|
1244
|
+
return "HIGH";
|
|
1245
|
+
}
|
|
1246
|
+
if (input.importCount >= 1 && input.importCount <= 3) {
|
|
1247
|
+
return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "MEDIUM";
|
|
1248
|
+
}
|
|
1249
|
+
return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
|
|
1250
|
+
}
|
|
1251
|
+
function readReadmeInfo(target) {
|
|
1252
|
+
const readmePath = join4(target, "README.md");
|
|
1253
|
+
const hasContributing = existsSync3(join4(target, "CONTRIBUTING.md"));
|
|
1254
|
+
if (!existsSync3(readmePath)) {
|
|
1255
|
+
return {
|
|
1256
|
+
quality: "missing",
|
|
1257
|
+
line_count: 0,
|
|
1258
|
+
has_contributing: hasContributing
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
1262
|
+
const wordCount = readme.trim().split(/\s+/).filter(Boolean).length;
|
|
1263
|
+
return {
|
|
1264
|
+
quality: wordCount >= 200 ? "ok" : "stub",
|
|
1265
|
+
line_count: readme.length === 0 ? 0 : readme.split(/\r?\n/).length,
|
|
1266
|
+
has_contributing: hasContributing
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function buildAssertions(frameworkKind, topology, codeSamples) {
|
|
1270
|
+
const assertions = [
|
|
1271
|
+
buildFrameworkAssertion(frameworkKind, topology, codeSamples),
|
|
1272
|
+
buildDominantPatternAssertion(codeSamples),
|
|
1273
|
+
buildEntryDirectoryAssertion(frameworkKind, codeSamples),
|
|
1274
|
+
buildMetaSidecarAssertion(frameworkKind, topology),
|
|
1275
|
+
buildConfigAssertion(frameworkKind, topology),
|
|
1276
|
+
buildDomainAssertion(codeSamples)
|
|
1277
|
+
];
|
|
1278
|
+
return assertions.filter((assertion) => assertion !== null);
|
|
1279
|
+
}
|
|
1280
|
+
function buildCandidateFiles(topology, codeSamples, entryPoints) {
|
|
1281
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1282
|
+
const codeSamplesByPath = new Map(codeSamples.map((sample) => [sample.path, sample]));
|
|
1283
|
+
const configFiles = topology.files.filter((file) => isConfigFile(file.relativePath));
|
|
1284
|
+
const testFiles = topology.files.filter((file) => isTestFile(file.relativePath));
|
|
1285
|
+
const domainFiles = topology.files.filter((file) => isDomainFile(file.relativePath));
|
|
1286
|
+
const componentSamples = codeSamples.filter((sample) => sample.pattern_analysis.family === "component").sort((left, right) => compareCandidateScore(buildComponentCandidateScore(right), buildComponentCandidateScore(left)));
|
|
1287
|
+
addCandidateFamily(
|
|
1288
|
+
selected,
|
|
1289
|
+
entryPoints.map((entryPoint) => ({
|
|
1290
|
+
path: entryPoint.path,
|
|
1291
|
+
family: "entry",
|
|
1292
|
+
rationale: `Representative ${entryPoint.reason} used as an application entry surface.`,
|
|
1293
|
+
score: buildEntryCandidateScore(entryPoint)
|
|
1294
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
1295
|
+
ENTRY_FAMILY_LIMIT
|
|
1296
|
+
);
|
|
1297
|
+
addCandidateFamily(
|
|
1298
|
+
selected,
|
|
1299
|
+
componentSamples.map((sample) => ({
|
|
1300
|
+
path: sample.path,
|
|
1301
|
+
family: "component",
|
|
1302
|
+
rationale: sample.pattern_analysis.rationale,
|
|
1303
|
+
score: buildComponentCandidateScore(sample)
|
|
1304
|
+
})),
|
|
1305
|
+
FAMILY_LIMIT
|
|
1306
|
+
);
|
|
1307
|
+
addCandidateFamily(
|
|
1308
|
+
selected,
|
|
1309
|
+
configFiles.map((file) => ({
|
|
1310
|
+
path: file.relativePath,
|
|
1311
|
+
family: "config",
|
|
1312
|
+
rationale: "Bootstrap or compiler configuration file used to infer framework and project boundaries.",
|
|
1313
|
+
score: buildConfigCandidateScore(file.relativePath)
|
|
1314
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
1315
|
+
FAMILY_LIMIT
|
|
1316
|
+
);
|
|
1317
|
+
addCandidateFamily(
|
|
1318
|
+
selected,
|
|
1319
|
+
testFiles.map((file) => ({
|
|
1320
|
+
path: file.relativePath,
|
|
1321
|
+
family: "test",
|
|
1322
|
+
rationale: "Existing test coverage surface that captures behavior expectations.",
|
|
1323
|
+
score: file.relativePath.includes("__tests__") ? 2 : 1
|
|
1324
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
1325
|
+
FAMILY_LIMIT
|
|
1326
|
+
);
|
|
1327
|
+
addCandidateFamily(
|
|
1328
|
+
selected,
|
|
1329
|
+
domainFiles.filter((file) => !codeSamplesByPath.has(file.relativePath)).map((file) => ({
|
|
1330
|
+
path: file.relativePath,
|
|
1331
|
+
family: "domain",
|
|
1332
|
+
rationale: "Representative domain file outside entry/config/test hotspots.",
|
|
1333
|
+
score: buildDomainCandidateScore(file.relativePath)
|
|
1334
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
1335
|
+
FAMILY_LIMIT
|
|
1336
|
+
);
|
|
1337
|
+
return [...selected.values()].slice(0, CANDIDATE_FILE_LIMIT);
|
|
1338
|
+
}
|
|
1339
|
+
function buildFrameworkAssertion(frameworkKind, topology, codeSamples) {
|
|
1340
|
+
if (frameworkKind === "unknown") {
|
|
1341
|
+
return createAssertion({
|
|
1342
|
+
type: "framework",
|
|
1343
|
+
statement: "Framework could not be determined from the sampled topology.",
|
|
1344
|
+
evidence: codeSamples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
1345
|
+
matched: 0,
|
|
1346
|
+
total: codeSamples.length,
|
|
1347
|
+
coOccurring: [],
|
|
1348
|
+
alternatives: ["Ask the user to confirm the primary framework"]
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
const matchedSamples = codeSamples.filter((sample) => matchesFrameworkPattern(frameworkKind, sample.pattern_analysis.pattern));
|
|
1352
|
+
const coOccurring = compactPatternNames([
|
|
1353
|
+
...matchedSamples.flatMap((sample) => sample.pattern_analysis.co_occurring),
|
|
1354
|
+
hasFile(topology.files, "project.config.json") ? "project-config-json" : null,
|
|
1355
|
+
(topology.by_ext[".meta"] ?? 0) > 0 ? "meta-sidecars" : null,
|
|
1356
|
+
hasFile(topology.files, "package.json") ? "package-json" : null
|
|
1357
|
+
]);
|
|
1358
|
+
const evidence = [
|
|
1359
|
+
...matchedSamples.flatMap((sample) => sample.evidence),
|
|
1360
|
+
...buildTopologyEvidence(topology, getExpectedConfigFiles(frameworkKind))
|
|
1361
|
+
].slice(0, 3);
|
|
1362
|
+
return createAssertion({
|
|
1363
|
+
type: "framework",
|
|
1364
|
+
statement: buildFrameworkStatement(frameworkKind),
|
|
1365
|
+
evidence,
|
|
1366
|
+
matched: matchedSamples.length,
|
|
1367
|
+
total: codeSamples.length,
|
|
1368
|
+
coOccurring,
|
|
1369
|
+
astLevel: matchedSamples.some((sample) => sample.pattern_analysis.ast_level),
|
|
1370
|
+
proposedRule: buildFrameworkRule(frameworkKind),
|
|
1371
|
+
alternatives: frameworkKind === "cocos-creator" ? ["Generic TypeScript utility modules"] : ["Alternative framework entry layout"]
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
function buildDominantPatternAssertion(codeSamples) {
|
|
1375
|
+
if (codeSamples.length === 0) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1379
|
+
for (const sample of codeSamples) {
|
|
1380
|
+
const existing = counts.get(sample.pattern_analysis.pattern) ?? [];
|
|
1381
|
+
existing.push(sample);
|
|
1382
|
+
counts.set(sample.pattern_analysis.pattern, existing);
|
|
1383
|
+
}
|
|
1384
|
+
const dominant = [...counts.entries()].sort((left, right) => right[1].length - left[1].length)[0];
|
|
1385
|
+
if (dominant === void 0) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
const [, samples] = dominant;
|
|
1389
|
+
const first = samples[0];
|
|
1390
|
+
return createAssertion({
|
|
1391
|
+
type: first.pattern_analysis.type,
|
|
1392
|
+
statement: first.pattern_analysis.statement,
|
|
1393
|
+
evidence: samples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
1394
|
+
matched: samples.length,
|
|
1395
|
+
total: codeSamples.length,
|
|
1396
|
+
coOccurring: compactPatternNames(samples.flatMap((sample) => sample.pattern_analysis.co_occurring)),
|
|
1397
|
+
astLevel: samples.some((sample) => sample.pattern_analysis.ast_level),
|
|
1398
|
+
proposedRule: first.pattern_analysis.proposed_rule,
|
|
1399
|
+
alternatives: first.pattern_analysis.alternatives
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
function buildEntryDirectoryAssertion(frameworkKind, codeSamples) {
|
|
1403
|
+
if (codeSamples.length === 0) {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
const directoryGroups = /* @__PURE__ */ new Map();
|
|
1407
|
+
for (const sample of codeSamples) {
|
|
1408
|
+
const directory2 = posix.dirname(sample.path);
|
|
1409
|
+
const existing = directoryGroups.get(directory2) ?? [];
|
|
1410
|
+
existing.push(sample);
|
|
1411
|
+
directoryGroups.set(directory2, existing);
|
|
1412
|
+
}
|
|
1413
|
+
const primaryDirectory = [...directoryGroups.entries()].sort((left, right) => right[1].length - left[1].length)[0];
|
|
1414
|
+
if (primaryDirectory === void 0) {
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
const [directory, samples] = primaryDirectory;
|
|
1418
|
+
return createAssertion({
|
|
1419
|
+
type: "pattern",
|
|
1420
|
+
statement: `Entry samples are concentrated in ${directory}, indicating a stable primary source boundary.`,
|
|
1421
|
+
evidence: samples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
1422
|
+
matched: samples.length,
|
|
1423
|
+
total: codeSamples.length,
|
|
1424
|
+
coOccurring: compactPatternNames([
|
|
1425
|
+
directory === "." ? "root-entry" : directory,
|
|
1426
|
+
frameworkKind !== "unknown" ? frameworkKind : null,
|
|
1427
|
+
...samples.flatMap((sample) => sample.pattern_analysis.co_occurring.slice(0, 1))
|
|
1428
|
+
]),
|
|
1429
|
+
proposedRule: directory === "." ? "Keep primary entry files at the repository root only if the framework expects it." : `Treat ${directory} as the main execution boundary during initialization.`
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
function buildMetaSidecarAssertion(frameworkKind, topology) {
|
|
1433
|
+
const relevantScripts = topology.files.filter((file) => SCRIPT_EXTENSIONS.has(extname(file.relativePath)));
|
|
1434
|
+
if (relevantScripts.length === 0) {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
const matchedScripts = relevantScripts.filter((file) => hasFile(topology.files, `${file.relativePath}.meta`));
|
|
1438
|
+
if (matchedScripts.length === 0 && frameworkKind !== "cocos-creator") {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
return createAssertion({
|
|
1442
|
+
type: "invariant",
|
|
1443
|
+
statement: matchedScripts.length > 0 ? "Script files have adjacent .meta sidecars, which should be treated as coupled assets." : "No .meta sidecars were detected for sampled scripts.",
|
|
1444
|
+
evidence: matchedScripts.length > 0 ? matchedScripts.slice(0, 3).map((file) => makeSyntheticEvidence(`${file.relativePath}.meta`, `${file.relativePath}.meta sidecar present`)) : buildTopologyEvidence(topology, relevantScripts.slice(0, 1).map((file) => file.relativePath)),
|
|
1445
|
+
matched: matchedScripts.length,
|
|
1446
|
+
total: relevantScripts.length,
|
|
1447
|
+
coOccurring: compactPatternNames([
|
|
1448
|
+
matchedScripts.length > 0 ? "meta-sidecar" : null,
|
|
1449
|
+
frameworkKind === "cocos-creator" ? "cocos-creator" : null,
|
|
1450
|
+
relevantScripts.some((file) => file.relativePath.startsWith("assets/scripts/")) ? "assets-scripts" : null
|
|
1451
|
+
]),
|
|
1452
|
+
proposedRule: matchedScripts.length > 0 ? "Do not edit or delete .meta sidecars without explicit user confirmation." : void 0
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
function buildConfigAssertion(frameworkKind, topology) {
|
|
1456
|
+
const expectedFiles = getExpectedConfigFiles(frameworkKind);
|
|
1457
|
+
if (expectedFiles.length === 0) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
const matchedFiles = expectedFiles.filter((file) => hasFile(topology.files, file));
|
|
1461
|
+
return createAssertion({
|
|
1462
|
+
type: "invariant",
|
|
1463
|
+
statement: `Project configuration is anchored by ${expectedFiles.join(", ")}.`,
|
|
1464
|
+
evidence: buildTopologyEvidence(topology, matchedFiles),
|
|
1465
|
+
matched: matchedFiles.length,
|
|
1466
|
+
total: expectedFiles.length,
|
|
1467
|
+
coOccurring: compactPatternNames(matchedFiles.map(normalizeConfigPattern)),
|
|
1468
|
+
proposedRule: "Read bootstrap and compiler config before generating new rules or project structure."
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
function buildDomainAssertion(codeSamples) {
|
|
1472
|
+
if (codeSamples.length === 0) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
const namedSamples = codeSamples.filter((sample) => {
|
|
1476
|
+
const fileBase = basename(sample.path, extname(sample.path));
|
|
1477
|
+
return sample.snippet.includes(`class ${fileBase}`) || sample.snippet.includes(`class ${sanitizeIdentifier(fileBase)}`);
|
|
1478
|
+
});
|
|
1479
|
+
if (namedSamples.length === 0) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
const namedModules = compactPatternNames(namedSamples.map((sample) => basename(sample.path, extname(sample.path))));
|
|
1483
|
+
return createAssertion({
|
|
1484
|
+
type: "domain",
|
|
1485
|
+
statement: `Sampled modules are named as concrete domain concepts (${namedModules.join(", ")}).`,
|
|
1486
|
+
evidence: namedSamples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
1487
|
+
matched: namedSamples.length,
|
|
1488
|
+
total: codeSamples.length,
|
|
1489
|
+
coOccurring: compactPatternNames([
|
|
1490
|
+
namedSamples.every((sample) => /^[A-Z]/.test(basename(sample.path))) ? "pascal-case-modules" : null,
|
|
1491
|
+
namedModules.length >= 2 ? "domain-named-components" : null,
|
|
1492
|
+
namedSamples.some((sample) => sample.snippet.includes("start():")) ? "lifecycle-hook" : null
|
|
1493
|
+
]),
|
|
1494
|
+
proposedRule: "Preserve domain-specific module names when authoring knowledge entries that reference these modules."
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
function createAssertion(input) {
|
|
1498
|
+
const coverage = {
|
|
1499
|
+
ratio: input.total === 0 ? 0 : roundCoverageRatio(input.matched / input.total),
|
|
1500
|
+
total: input.total,
|
|
1501
|
+
matched: input.matched,
|
|
1502
|
+
co_occurring_patterns: compactPatternNames(input.coOccurring)
|
|
1503
|
+
};
|
|
1504
|
+
return {
|
|
1505
|
+
type: input.type,
|
|
1506
|
+
statement: input.statement,
|
|
1507
|
+
confidence: determineConfidence(coverage.ratio, coverage.co_occurring_patterns, input.astLevel ?? false),
|
|
1508
|
+
evidence: dedupeEvidence(input.evidence),
|
|
1509
|
+
coverage,
|
|
1510
|
+
proposed_rule: input.proposedRule,
|
|
1511
|
+
alternatives: input.alternatives
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
function buildEvidenceAnchors(relativePath, snippet, evidenceLines) {
|
|
1515
|
+
const lines = snippet.split("\n");
|
|
1516
|
+
const anchors = [];
|
|
1517
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1518
|
+
for (const pattern of evidenceLines) {
|
|
1519
|
+
const lineIndex = lines.findIndex((line) => line.includes(pattern));
|
|
1520
|
+
if (lineIndex === -1) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const key = `${relativePath}:${lineIndex + 1}`;
|
|
1524
|
+
if (seen.has(key)) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
seen.add(key);
|
|
1528
|
+
anchors.push({
|
|
1529
|
+
file: relativePath,
|
|
1530
|
+
line: String(lineIndex + 1),
|
|
1531
|
+
snippet: lines[lineIndex]?.trim() ?? ""
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
if (anchors.length > 0) {
|
|
1535
|
+
return anchors;
|
|
1536
|
+
}
|
|
1537
|
+
const fallbackIndex = lines.findIndex((line) => line.trim().length > 0);
|
|
1538
|
+
return [
|
|
1539
|
+
{
|
|
1540
|
+
file: relativePath,
|
|
1541
|
+
line: String(fallbackIndex === -1 ? 1 : fallbackIndex + 1),
|
|
1542
|
+
snippet: fallbackIndex === -1 ? "" : lines[fallbackIndex]?.trim() ?? ""
|
|
1543
|
+
}
|
|
1544
|
+
];
|
|
1545
|
+
}
|
|
1546
|
+
function addCandidateFamily(selected, candidates, familyLimit) {
|
|
1547
|
+
let added = 0;
|
|
1548
|
+
for (const candidate of candidates) {
|
|
1549
|
+
if (selected.size >= CANDIDATE_FILE_LIMIT || added >= familyLimit || selected.has(candidate.path)) {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
selected.set(candidate.path, {
|
|
1553
|
+
path: candidate.path,
|
|
1554
|
+
family: candidate.family,
|
|
1555
|
+
rationale: candidate.rationale
|
|
1556
|
+
});
|
|
1557
|
+
added += 1;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function buildTopologyEvidence(topology, preferredPaths) {
|
|
1561
|
+
return preferredPaths.filter((path) => hasFile(topology.files, path)).slice(0, 3).map((path) => makeSyntheticEvidence(path, `${path} present in project topology`));
|
|
1562
|
+
}
|
|
1563
|
+
function makeSyntheticEvidence(file, snippet) {
|
|
1564
|
+
return {
|
|
1565
|
+
file,
|
|
1566
|
+
line: "1",
|
|
1567
|
+
snippet
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
function dedupeEvidence(evidence) {
|
|
1571
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1572
|
+
const deduped = [];
|
|
1573
|
+
for (const entry of evidence) {
|
|
1574
|
+
const key = `${entry.file}:${entry.line}`;
|
|
1575
|
+
if (seen.has(key)) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
seen.add(key);
|
|
1579
|
+
deduped.push(entry);
|
|
1580
|
+
}
|
|
1581
|
+
return deduped.slice(0, 3);
|
|
1582
|
+
}
|
|
1583
|
+
function matchesFrameworkPattern(frameworkKind, pattern) {
|
|
1584
|
+
if (frameworkKind === "cocos-creator") {
|
|
1585
|
+
return pattern === "cocos-component-class";
|
|
1586
|
+
}
|
|
1587
|
+
if (frameworkKind === "next") {
|
|
1588
|
+
return pattern === "next-route-component";
|
|
1589
|
+
}
|
|
1590
|
+
if (frameworkKind === "vite") {
|
|
1591
|
+
return pattern === "vite-main-entry" || pattern === "react-root";
|
|
1592
|
+
}
|
|
1593
|
+
return pattern !== "source-entry";
|
|
1594
|
+
}
|
|
1595
|
+
function buildFrameworkStatement(frameworkKind) {
|
|
1596
|
+
if (frameworkKind === "cocos-creator") {
|
|
1597
|
+
return "Project strongly matches a Cocos Creator TypeScript component layout.";
|
|
1598
|
+
}
|
|
1599
|
+
if (frameworkKind === "next") {
|
|
1600
|
+
return "Project topology and entry samples align with a Next.js route-driven application.";
|
|
1601
|
+
}
|
|
1602
|
+
if (frameworkKind === "vite") {
|
|
1603
|
+
return "Project topology aligns with a Vite-style application bootstrap.";
|
|
1604
|
+
}
|
|
1605
|
+
return `Project surfaces align with ${frameworkKind}.`;
|
|
1606
|
+
}
|
|
1607
|
+
function buildFrameworkRule(frameworkKind) {
|
|
1608
|
+
if (frameworkKind === "cocos-creator") {
|
|
1609
|
+
return "Preserve Cocos component decorators, lifecycle methods, and paired .meta files during initialization.";
|
|
1610
|
+
}
|
|
1611
|
+
if (frameworkKind === "next") {
|
|
1612
|
+
return "Respect app/pages route boundaries when generating instructions or edits.";
|
|
1613
|
+
}
|
|
1614
|
+
if (frameworkKind === "vite") {
|
|
1615
|
+
return "Keep bootstrap logic centered on src/main.* and surrounding config files.";
|
|
1616
|
+
}
|
|
1617
|
+
return void 0;
|
|
1618
|
+
}
|
|
1619
|
+
function determineConfidence(ratio, coOccurringPatterns, astLevel, hasConflict = false) {
|
|
1620
|
+
if (hasConflict) {
|
|
1621
|
+
return "LOW";
|
|
1622
|
+
}
|
|
1623
|
+
if (astLevel) {
|
|
1624
|
+
return "HIGH";
|
|
1625
|
+
}
|
|
1626
|
+
if (ratio < 0.5) {
|
|
1627
|
+
return "LOW";
|
|
1628
|
+
}
|
|
1629
|
+
if (ratio >= 0.8 && coOccurringPatterns.length >= 2) {
|
|
1630
|
+
return "HIGH";
|
|
1631
|
+
}
|
|
1632
|
+
return "MEDIUM";
|
|
1633
|
+
}
|
|
1634
|
+
function compactPatternNames(patterns) {
|
|
1635
|
+
return [...new Set(patterns.filter((pattern) => pattern !== null && pattern !== void 0 && pattern.length > 0))];
|
|
1636
|
+
}
|
|
1637
|
+
function roundCoverageRatio(value) {
|
|
1638
|
+
return Math.round(value * 1e3) / 1e3;
|
|
1639
|
+
}
|
|
1640
|
+
function getExpectedConfigFiles(frameworkKind) {
|
|
1641
|
+
return EXPECTED_CONFIG_FILES_BY_FRAMEWORK[frameworkKind] ?? ["package.json"];
|
|
1642
|
+
}
|
|
1643
|
+
function hasFile(files, relativePath) {
|
|
1644
|
+
return files.some((file) => file.relativePath === relativePath);
|
|
1645
|
+
}
|
|
1646
|
+
function normalizeConfigPattern(relativePath) {
|
|
1647
|
+
return relativePath.replace(/\./g, "-");
|
|
1648
|
+
}
|
|
1649
|
+
function sanitizeIdentifier(value) {
|
|
1650
|
+
return value.replace(/[^A-Za-z0-9_$]/g, "");
|
|
1651
|
+
}
|
|
1652
|
+
function compareCandidateScore(left, right) {
|
|
1653
|
+
return left - right;
|
|
1654
|
+
}
|
|
1655
|
+
function buildEntryCandidateScore(entryPoint) {
|
|
1656
|
+
let score = 0;
|
|
1657
|
+
if (entryPoint.reason === "application entry") {
|
|
1658
|
+
score += 3;
|
|
1659
|
+
}
|
|
1660
|
+
if (entryPoint.reason.includes("route")) {
|
|
1661
|
+
score += 2;
|
|
1662
|
+
}
|
|
1663
|
+
if ((entryPoint.size_bytes ?? 0) > 0) {
|
|
1664
|
+
score += 1;
|
|
1665
|
+
}
|
|
1666
|
+
return score;
|
|
1667
|
+
}
|
|
1668
|
+
function buildComponentCandidateScore(sample) {
|
|
1669
|
+
let score = sample.pattern_analysis.co_occurring.length;
|
|
1670
|
+
if (sample.pattern_analysis.ast_level) {
|
|
1671
|
+
score += 3;
|
|
1672
|
+
}
|
|
1673
|
+
if (sample.pattern_analysis.confidence === "HIGH") {
|
|
1674
|
+
score += 2;
|
|
1675
|
+
}
|
|
1676
|
+
return score;
|
|
1677
|
+
}
|
|
1678
|
+
function buildConfigCandidateScore(relativePath) {
|
|
1679
|
+
if (relativePath === "project.config.json") {
|
|
1680
|
+
return 4;
|
|
1681
|
+
}
|
|
1682
|
+
if (relativePath === "package.json") {
|
|
1683
|
+
return 3;
|
|
1684
|
+
}
|
|
1685
|
+
if (relativePath === "tsconfig.json") {
|
|
1686
|
+
return 2;
|
|
1687
|
+
}
|
|
1688
|
+
return 1;
|
|
1689
|
+
}
|
|
1690
|
+
function buildDomainCandidateScore(relativePath) {
|
|
1691
|
+
let score = 0;
|
|
1692
|
+
if (relativePath.startsWith("src/") || relativePath.startsWith("assets/")) {
|
|
1693
|
+
score += 2;
|
|
1694
|
+
}
|
|
1695
|
+
if (SCRIPT_EXTENSIONS.has(extname(relativePath))) {
|
|
1696
|
+
score += 1;
|
|
1697
|
+
}
|
|
1698
|
+
if (relativePath.includes("/domain/") || relativePath.includes("/models/")) {
|
|
1699
|
+
score += 1;
|
|
1700
|
+
}
|
|
1701
|
+
return score;
|
|
1702
|
+
}
|
|
1703
|
+
function isConfigFile(relativePath) {
|
|
1704
|
+
return /(^|\/)(package\.json|project\.config\.json|tsconfig\.json|vite\.config\.[^.]+|next\.config\.[^.]+)$/.test(relativePath);
|
|
1705
|
+
}
|
|
1706
|
+
function isTestFile(relativePath) {
|
|
1707
|
+
return /(^|\/)(__tests__|tests)(\/|$)/.test(relativePath) || /\.(test|spec)\.[^.]+$/.test(relativePath);
|
|
1708
|
+
}
|
|
1709
|
+
function isDomainFile(relativePath) {
|
|
1710
|
+
const extension = extname(relativePath);
|
|
1711
|
+
if (!DOMAIN_FILE_EXTENSIONS.has(extension)) {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
return !isConfigFile(relativePath) && !isTestFile(relativePath);
|
|
1715
|
+
}
|
|
1716
|
+
function buildSkillRecommendations(frameworkKind, topology, readme, projectRoot) {
|
|
1717
|
+
return buildScanRecommendations(
|
|
1718
|
+
{
|
|
1719
|
+
frameworkKind,
|
|
1720
|
+
hasMeta: (topology.by_ext[".meta"] ?? 0) > 0,
|
|
1721
|
+
readmeOk: readme.quality === "ok"
|
|
1722
|
+
},
|
|
1723
|
+
getProjectTranslator(projectRoot)
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
function readProjectName(target) {
|
|
1727
|
+
const packageJsonPath = join4(target, "package.json");
|
|
1728
|
+
if (existsSync3(packageJsonPath)) {
|
|
1729
|
+
try {
|
|
1730
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1731
|
+
if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
|
|
1732
|
+
return packageJson.name;
|
|
1733
|
+
}
|
|
1734
|
+
} catch {
|
|
1735
|
+
return basename(target);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return basename(target);
|
|
1739
|
+
}
|
|
1740
|
+
function getCliVersion() {
|
|
1741
|
+
return true ? "2.2.0-rc.10" : "unknown";
|
|
1742
|
+
}
|
|
1743
|
+
function sortRecord(record) {
|
|
1744
|
+
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
1745
|
+
}
|
|
1746
|
+
function toPosixPath(path) {
|
|
1747
|
+
return path.split(sep).join("/");
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/install/pipeline/env.stage.ts
|
|
1751
|
+
var EnvStage = class {
|
|
1752
|
+
name = "env";
|
|
1753
|
+
async execute(context) {
|
|
1754
|
+
const target = context.target;
|
|
1755
|
+
try {
|
|
1756
|
+
const clientSupports = detectClientSupports(target);
|
|
1757
|
+
context.state.clientSupports = clientSupports;
|
|
1758
|
+
const scaffold = await this.buildScaffoldPlan(target, context.options);
|
|
1759
|
+
context.state.scaffold = scaffold;
|
|
1760
|
+
if (context.options.planOnly === true) {
|
|
1761
|
+
const fabricLanguage2 = this.readFabricLanguagePreference(target);
|
|
1762
|
+
context.state.fabricLanguage = fabricLanguage2;
|
|
1763
|
+
return stageSkipped("env", "dry-run: scaffold planned without writing files");
|
|
1764
|
+
}
|
|
1765
|
+
const created = await this.executeScaffold(scaffold, target);
|
|
1766
|
+
const fabricLanguage = this.readFabricLanguagePreference(target);
|
|
1767
|
+
context.state.fabricLanguage = fabricLanguage;
|
|
1768
|
+
const installed = [
|
|
1769
|
+
scaffold.fabricDir,
|
|
1770
|
+
scaffold.eventsPath,
|
|
1771
|
+
scaffold.forensicPath
|
|
1772
|
+
].filter((p) => existsSync4(p));
|
|
1773
|
+
return stageRan("env", installed, [], created);
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
return stageFailedFromError("env", error);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
async buildScaffoldPlan(target, _options) {
|
|
1779
|
+
const fabricDir = join5(target, ".fabric");
|
|
1780
|
+
const agentsMdPath = join5(target, "AGENTS.md");
|
|
1781
|
+
const eventsPath = join5(fabricDir, "events.jsonl");
|
|
1782
|
+
const forensicPath = join5(fabricDir, "forensic.json");
|
|
1783
|
+
const eventsState = this.classifyPath(eventsPath, "presence");
|
|
1784
|
+
const forensicState = this.classifyPath(forensicPath, "always-rewrite");
|
|
1785
|
+
const agentsMdAction = existsSync4(agentsMdPath) ? "preserved" : "created";
|
|
1786
|
+
const showScanProgress = process.stderr.isTTY === true;
|
|
1787
|
+
if (showScanProgress) {
|
|
1788
|
+
process.stderr.write(`${t("cli.install.scanning")}
|
|
1789
|
+
`);
|
|
1790
|
+
}
|
|
1791
|
+
const forensicReport = await buildForensicReport(target);
|
|
1792
|
+
if (showScanProgress) {
|
|
1793
|
+
process.stderr.write(`${t("cli.install.scan-complete")}
|
|
1794
|
+
`);
|
|
1795
|
+
}
|
|
1796
|
+
return {
|
|
1797
|
+
fabricDir,
|
|
1798
|
+
agentsMdPath,
|
|
1799
|
+
agentsMdAction,
|
|
1800
|
+
eventsPath,
|
|
1801
|
+
eventsAction: this.diffStateToWriteAction(eventsState),
|
|
1802
|
+
eventsState,
|
|
1803
|
+
forensicPath,
|
|
1804
|
+
forensicAction: this.diffStateToWriteAction(forensicState),
|
|
1805
|
+
forensicState,
|
|
1806
|
+
forensicReport
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
async executeScaffold(scaffold, target) {
|
|
1810
|
+
mkdirSync2(scaffold.fabricDir, { recursive: true });
|
|
1811
|
+
this.writeDefaultFabricConfig(scaffold.fabricDir, target);
|
|
1812
|
+
migrateRootConfig(target);
|
|
1813
|
+
this.writeDefaultGitignore(scaffold.fabricDir);
|
|
1814
|
+
if (scaffold.eventsState === "missing") {
|
|
1815
|
+
mkdirSync2(dirname2(scaffold.eventsPath), { recursive: true });
|
|
1816
|
+
writeFileSync2(scaffold.eventsPath, "", "utf8");
|
|
1817
|
+
}
|
|
1818
|
+
await atomicWriteJson(scaffold.forensicPath, scaffold.forensicReport);
|
|
1819
|
+
return scaffold;
|
|
1820
|
+
}
|
|
1821
|
+
classifyPath(path, _strategy) {
|
|
1822
|
+
if (!existsSync4(path)) {
|
|
1823
|
+
return "missing";
|
|
1824
|
+
}
|
|
1825
|
+
let stat;
|
|
1826
|
+
try {
|
|
1827
|
+
stat = statSync3(path);
|
|
1828
|
+
} catch {
|
|
1829
|
+
return "user-modified";
|
|
1830
|
+
}
|
|
1831
|
+
if (!stat.isFile()) {
|
|
1832
|
+
return "user-modified";
|
|
1833
|
+
}
|
|
1834
|
+
return "present-canonical";
|
|
1835
|
+
}
|
|
1836
|
+
diffStateToWriteAction(_state) {
|
|
1837
|
+
return "created";
|
|
1838
|
+
}
|
|
1839
|
+
writeDefaultFabricConfig(fabricDir, _targetRoot) {
|
|
1840
|
+
const target = join5(fabricDir, "fabric-config.json");
|
|
1841
|
+
if (existsSync4(target)) return;
|
|
1842
|
+
const FABRIC_CONFIG_DEFAULTS = {
|
|
1843
|
+
archive_hint_hours: 24,
|
|
1844
|
+
archive_hint_cooldown_hours: 12,
|
|
1845
|
+
review_hint_pending_count: 10,
|
|
1846
|
+
review_hint_pending_age_days: 7,
|
|
1847
|
+
maintenance_hint_days: 14,
|
|
1848
|
+
maintenance_hint_cooldown_days: 7,
|
|
1849
|
+
archive_edit_threshold: 20,
|
|
1850
|
+
underseed_node_threshold: 10,
|
|
1851
|
+
import_window_first_run_months: 60,
|
|
1852
|
+
import_window_rerun_months: 2,
|
|
1853
|
+
import_max_pending_per_run: 10,
|
|
1854
|
+
import_max_commits_scan: 500,
|
|
1855
|
+
import_skip_canonical_threshold: 50,
|
|
1856
|
+
archive_max_candidates_per_batch: 8,
|
|
1857
|
+
archive_max_recent_paths: 20,
|
|
1858
|
+
archive_digest_max_sessions: 10,
|
|
1859
|
+
review_topic_result_cap: 8,
|
|
1860
|
+
review_stale_pending_days: 14
|
|
1861
|
+
};
|
|
1862
|
+
mkdirSync2(fabricDir, { recursive: true });
|
|
1863
|
+
writeFileSync2(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
|
|
1864
|
+
}
|
|
1865
|
+
writeDefaultGitignore(fabricDir) {
|
|
1866
|
+
const target = join5(fabricDir, ".gitignore");
|
|
1867
|
+
if (existsSync4(target)) return;
|
|
1868
|
+
const FABRIC_GITIGNORE_CONTENT = [
|
|
1869
|
+
"# Fabric per-dev activity ledgers & caches \u2014 auto-generated, not shared.",
|
|
1870
|
+
"# Managed by `fabric install`; edit freely (re-install never overwrites this).",
|
|
1871
|
+
"events.jsonl",
|
|
1872
|
+
"metrics.jsonl",
|
|
1873
|
+
"cite-rollup.jsonl",
|
|
1874
|
+
"injections.jsonl",
|
|
1875
|
+
".cache/",
|
|
1876
|
+
"*.lock",
|
|
1877
|
+
"*.corrupted.*",
|
|
1878
|
+
""
|
|
1879
|
+
].join("\n");
|
|
1880
|
+
mkdirSync2(fabricDir, { recursive: true });
|
|
1881
|
+
writeFileSync2(target, FABRIC_GITIGNORE_CONTENT, "utf8");
|
|
1882
|
+
}
|
|
1883
|
+
readFabricLanguagePreference(_projectRoot) {
|
|
1884
|
+
return resolveGlobalLocale();
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// src/install/pipeline/store.stage.ts
|
|
1889
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1890
|
+
import { initStore as initStore2, resolveGlobalLocale as resolveGlobalLocale2, storeRelativePathForMount as storeRelativePathForMount3 } from "@fenglimg/fabric-shared";
|
|
1891
|
+
import { isCancel, select, text } from "@clack/prompts";
|
|
1892
|
+
import { join as join6 } from "path";
|
|
1893
|
+
var StoreStage = class {
|
|
1894
|
+
name = "store";
|
|
1895
|
+
async execute(context) {
|
|
1896
|
+
try {
|
|
1897
|
+
const globalRoot = resolveGlobalRoot();
|
|
1898
|
+
context.state.globalRoot = globalRoot;
|
|
1899
|
+
if (context.options.planOnly === true) {
|
|
1900
|
+
return stageSkipped("store", "dry-run: store setup planned without global/project writes");
|
|
1901
|
+
}
|
|
1902
|
+
const globalConfig = loadGlobalConfig(globalRoot);
|
|
1903
|
+
const pickedLanguage = context.wizardEnabled && globalConfig?.language === void 0 ? await this.promptLanguage(globalRoot) : void 0;
|
|
1904
|
+
if (globalConfig === null) {
|
|
1905
|
+
const cloned = context.wizardEnabled ? await this.promptPersonalStoreOnboarding(globalRoot) : false;
|
|
1906
|
+
if (!cloned) {
|
|
1907
|
+
await runGlobalInstall({}, globalRoot);
|
|
1908
|
+
}
|
|
1909
|
+
context.state.globalConfigCreated = true;
|
|
1910
|
+
} else {
|
|
1911
|
+
await this.ensurePersonalStore(globalConfig, globalRoot);
|
|
1912
|
+
}
|
|
1913
|
+
this.persistLanguageSelection(globalRoot, pickedLanguage);
|
|
1914
|
+
if (context.args.url) {
|
|
1915
|
+
await this.bindRemoteStoreToProject(
|
|
1916
|
+
context.target,
|
|
1917
|
+
context.args.url,
|
|
1918
|
+
globalRoot,
|
|
1919
|
+
context.interactive
|
|
1920
|
+
);
|
|
1921
|
+
return stageRan("store", [context.args.url], []);
|
|
1922
|
+
}
|
|
1923
|
+
const installed = [];
|
|
1924
|
+
const unboundStores = unboundAvailableStores(context.target);
|
|
1925
|
+
if (!context.wizardEnabled) {
|
|
1926
|
+
if (unboundStores.length > 0) {
|
|
1927
|
+
this.warnUnboundStores(unboundStores);
|
|
1928
|
+
}
|
|
1929
|
+
return stageRan("store", installed, []);
|
|
1930
|
+
}
|
|
1931
|
+
const projectConfig = loadProjectConfig(context.target);
|
|
1932
|
+
const hasWriteStore = typeof projectConfig?.active_write_store === "string" && projectConfig.active_write_store.length > 0;
|
|
1933
|
+
if (hasWriteStore && unboundStores.length === 0) {
|
|
1934
|
+
return stageRan("store", installed, []);
|
|
1935
|
+
}
|
|
1936
|
+
const outcome = await this.promptStoreSetup(context, unboundStores, globalRoot);
|
|
1937
|
+
if (outcome !== null) {
|
|
1938
|
+
installed.push(outcome);
|
|
1939
|
+
}
|
|
1940
|
+
return stageRan("store", installed, []);
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
return stageFailedFromError("store", error);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* grill-6fixes (D1b): the install-time language selector, surfaced as the
|
|
1947
|
+
* first interactive prompt of the install (see execute()). Returns the picked
|
|
1948
|
+
* tone, or undefined on cancel (resolvers then keep falling back to env
|
|
1949
|
+
* detection until the user picks via `fabric config`). The default
|
|
1950
|
+
* pre-highlight follows the env-detected locale (Chinese shell → zh-CN).
|
|
1951
|
+
* Persistence is deferred to persistLanguageSelection so the pick can be
|
|
1952
|
+
* captured before the global config exists on a first-ever install.
|
|
1953
|
+
*/
|
|
1954
|
+
async promptLanguage(globalRoot) {
|
|
1955
|
+
const picked = await select({
|
|
1956
|
+
message: t("cli.install.language.prompt"),
|
|
1957
|
+
options: [
|
|
1958
|
+
{ value: "zh-CN", label: t("cli.install.language.option.zh-CN") },
|
|
1959
|
+
{ value: "en", label: t("cli.install.language.option.en") }
|
|
1960
|
+
],
|
|
1961
|
+
initialValue: resolveGlobalLocale2(globalRoot)
|
|
1962
|
+
});
|
|
1963
|
+
if (isCancel(picked)) return void 0;
|
|
1964
|
+
return picked;
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Persist the language pick onto the (now-guaranteed) global config and
|
|
1968
|
+
* refresh the process locale so the rest of THIS install run renders in the
|
|
1969
|
+
* chosen tone. No-op when nothing was picked (non-wizard / already set /
|
|
1970
|
+
* cancelled). refreshLocale() runs whenever a pick exists — even if the value
|
|
1971
|
+
* already matched — so the module-level translator, bound to the env locale
|
|
1972
|
+
* before the config carried `language`, picks up the persisted value.
|
|
1973
|
+
*/
|
|
1974
|
+
persistLanguageSelection(globalRoot, picked) {
|
|
1975
|
+
if (picked === void 0) return;
|
|
1976
|
+
const config = loadGlobalConfig(globalRoot);
|
|
1977
|
+
if (config !== null && config.language !== picked) {
|
|
1978
|
+
saveGlobalConfig({ ...config, language: picked }, globalRoot);
|
|
1979
|
+
}
|
|
1980
|
+
refreshLocale();
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Shared bind tail (DRY): resolve which project this repo binds to inside
|
|
1984
|
+
* `alias` (git-suggested, silent in the common case), then run the ONE
|
|
1985
|
+
* function that mints `project_id`, registers the project in the store, sets
|
|
1986
|
+
* `active_project`, switches the write target, and writes the project
|
|
1987
|
+
* write-route — `ensureStoreProjectBinding`. All three onboarding paths
|
|
1988
|
+
* (join / create / bind-mounted) route through here so the project scope axis
|
|
1989
|
+
* is wired identically; previously only the bind-mounted path did, leaving a
|
|
1990
|
+
* fresh `install --url` / create flow with no `project_id` / `active_project`.
|
|
1991
|
+
*
|
|
1992
|
+
* Returns the resolved project id, or null when the user cancels the
|
|
1993
|
+
* disambiguation prompt (interactive ambiguity only).
|
|
1994
|
+
*/
|
|
1995
|
+
async bindStoreToProject(projectRoot, alias, globalRoot, options) {
|
|
1996
|
+
const project = await this.resolveProjectIdWithGuard(
|
|
1997
|
+
projectRoot,
|
|
1998
|
+
alias,
|
|
1999
|
+
globalRoot,
|
|
2000
|
+
options.interactive
|
|
2001
|
+
);
|
|
2002
|
+
if (project === null) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
await ensureStoreProjectBinding(projectRoot, alias, {
|
|
2006
|
+
globalRoot,
|
|
2007
|
+
requestedProjectId: project,
|
|
2008
|
+
...options.suggestedRemote === void 0 ? {} : { suggestedRemote: options.suggestedRemote }
|
|
2009
|
+
});
|
|
2010
|
+
return project;
|
|
2011
|
+
}
|
|
2012
|
+
async bindRemoteStoreToProject(projectRoot, url, globalRoot, interactive) {
|
|
2013
|
+
const already = storeList(globalRoot).find((store) => store.remote === url);
|
|
2014
|
+
const mounted = already ?? mountStoreFromRemote(url, globalRoot);
|
|
2015
|
+
const bound = await this.bindStoreToProject(projectRoot, mounted.alias, globalRoot, {
|
|
2016
|
+
suggestedRemote: url,
|
|
2017
|
+
interactive
|
|
2018
|
+
});
|
|
2019
|
+
if (bound === null) {
|
|
2020
|
+
this.warnUnboundStores([{ alias: mounted.alias }]);
|
|
2021
|
+
return mounted.alias;
|
|
2022
|
+
}
|
|
2023
|
+
console.log("");
|
|
2024
|
+
console.log(paint.success(t("cli.install.store.bound-success", { alias: mounted.alias })));
|
|
2025
|
+
return mounted.alias;
|
|
2026
|
+
}
|
|
2027
|
+
async bindCreatedStoreToProject(projectRoot, alias, options) {
|
|
2028
|
+
await storeCreate(alias, (/* @__PURE__ */ new Date()).toISOString(), {
|
|
2029
|
+
...options.remote === void 0 ? {} : { remote: options.remote },
|
|
2030
|
+
globalRoot: options.globalRoot
|
|
2031
|
+
});
|
|
2032
|
+
await this.bindStoreToProject(projectRoot, alias, options.globalRoot, {
|
|
2033
|
+
...options.remote === void 0 ? {} : { suggestedRemote: options.remote },
|
|
2034
|
+
interactive: options.interactive
|
|
2035
|
+
});
|
|
2036
|
+
console.log("");
|
|
2037
|
+
console.log(paint.success(t("cli.install.store.created-success", { alias })));
|
|
2038
|
+
return alias;
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Merged store-setup prompt (Q1/Q2 of the store-onboarding grill): ONE select
|
|
2042
|
+
* whose top options are every mounted-but-unbound store (direct bind, zero
|
|
2043
|
+
* clone), followed by join-from-remote / create-local / skip. Replaces the old
|
|
2044
|
+
* `promptBindMountedStore` + `promptStoreOnboarding` pair — a mounted store can
|
|
2045
|
+
* no longer be invisible in one prompt and then re-cloned in the next.
|
|
2046
|
+
*
|
|
2047
|
+
* Already-bound non-personal stores are surfaced as an info line above the
|
|
2048
|
+
* prompt (pure visibility); the personal store is implicit and never listed.
|
|
2049
|
+
* Returns an install marker (`bound:<alias>` / `created:<alias>`) or null on
|
|
2050
|
+
* skip / cancel.
|
|
2051
|
+
*/
|
|
2052
|
+
async promptStoreSetup(context, unboundStores, globalRoot) {
|
|
2053
|
+
const boundAliases = this.boundStoreAliases(context.target, globalRoot);
|
|
2054
|
+
if (boundAliases.length > 0) {
|
|
2055
|
+
console.log(
|
|
2056
|
+
paint.muted(
|
|
2057
|
+
t("cli.install.store.setup.already-bound", {
|
|
2058
|
+
aliases: boundAliases.map((a) => `'${a}'`).join(", ")
|
|
2059
|
+
})
|
|
2060
|
+
)
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
const JOIN = "__join__";
|
|
2064
|
+
const CREATE = "__create__";
|
|
2065
|
+
const SKIP = "skip";
|
|
2066
|
+
const choice = await select({
|
|
2067
|
+
message: t("cli.install.store.setup.prompt"),
|
|
2068
|
+
initialValue: unboundStores.length > 0 ? `bind:${unboundStores[0].alias}` : SKIP,
|
|
2069
|
+
options: [
|
|
2070
|
+
...unboundStores.map((store) => ({
|
|
2071
|
+
value: `bind:${store.alias}`,
|
|
2072
|
+
label: t("cli.install.store.setup.bind-label", { alias: store.alias }),
|
|
2073
|
+
hint: store.remote ?? t("cli.install.store.local-store")
|
|
2074
|
+
})),
|
|
2075
|
+
{
|
|
2076
|
+
value: JOIN,
|
|
2077
|
+
label: t("cli.install.store.onboard.join-label"),
|
|
2078
|
+
hint: t("cli.install.store.onboard.join-hint")
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
value: CREATE,
|
|
2082
|
+
label: t("cli.install.store.onboard.create-label"),
|
|
2083
|
+
hint: t("cli.install.store.onboard.create-hint")
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
value: SKIP,
|
|
2087
|
+
label: t("cli.install.store.skip-label"),
|
|
2088
|
+
hint: t("cli.install.store.onboard.skip-hint")
|
|
2089
|
+
}
|
|
2090
|
+
]
|
|
2091
|
+
});
|
|
2092
|
+
if (isCancel(choice) || choice === SKIP || typeof choice !== "string") {
|
|
2093
|
+
if (unboundStores.length > 0) {
|
|
2094
|
+
this.warnUnboundStores(unboundStores);
|
|
2095
|
+
}
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
if (choice.startsWith("bind:")) {
|
|
2099
|
+
const alias2 = choice.slice("bind:".length);
|
|
2100
|
+
const bound = await this.bindStoreToProject(context.target, alias2, globalRoot, {
|
|
2101
|
+
interactive: true
|
|
2102
|
+
});
|
|
2103
|
+
if (bound === null) {
|
|
2104
|
+
this.warnUnboundStores(unboundStores);
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
console.log("");
|
|
2108
|
+
console.log(paint.success(t("cli.install.store.bound-success", { alias: alias2 })));
|
|
2109
|
+
return `bound:${alias2}`;
|
|
2110
|
+
}
|
|
2111
|
+
if (choice === JOIN) {
|
|
2112
|
+
const url = await text({
|
|
2113
|
+
message: t("cli.install.store.onboard.join-url"),
|
|
2114
|
+
placeholder: "git@github.com:org/knowledge.git"
|
|
2115
|
+
});
|
|
2116
|
+
if (isCancel(url) || typeof url !== "string" || url.length === 0) {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
return `bound:${await this.bindRemoteStoreToProject(context.target, url, globalRoot, true)}`;
|
|
2120
|
+
}
|
|
2121
|
+
const alias = await text({ message: t("cli.install.store.onboard.alias"), initialValue: "team" });
|
|
2122
|
+
if (isCancel(alias) || typeof alias !== "string" || alias.length === 0) {
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
const remote = await text({
|
|
2126
|
+
message: t("cli.install.store.onboard.remote"),
|
|
2127
|
+
placeholder: "git@github.com:org/knowledge.git"
|
|
2128
|
+
});
|
|
2129
|
+
const remoteStr = !isCancel(remote) && typeof remote === "string" && remote.length > 0 ? remote : void 0;
|
|
2130
|
+
return `created:${await this.bindCreatedStoreToProject(
|
|
2131
|
+
context.target,
|
|
2132
|
+
alias,
|
|
2133
|
+
remoteStr === void 0 ? { globalRoot, interactive: true } : { remote: remoteStr, globalRoot, interactive: true }
|
|
2134
|
+
)}`;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* The project's already-bound non-personal store aliases (Q2 visibility line).
|
|
2138
|
+
* Reads `required_stores` from the project config and keeps only those still
|
|
2139
|
+
* mounted as non-personal stores in the global registry.
|
|
2140
|
+
*/
|
|
2141
|
+
boundStoreAliases(projectRoot, globalRoot) {
|
|
2142
|
+
const declared = loadProjectConfig(projectRoot)?.required_stores ?? [];
|
|
2143
|
+
if (declared.length === 0) {
|
|
2144
|
+
return [];
|
|
2145
|
+
}
|
|
2146
|
+
const mounted = new Map(
|
|
2147
|
+
storeList(globalRoot).filter((s) => s.personal !== true).flatMap((s) => [
|
|
2148
|
+
[s.alias, s.alias],
|
|
2149
|
+
[s.store_uuid, s.alias]
|
|
2150
|
+
])
|
|
2151
|
+
);
|
|
2152
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
2153
|
+
for (const entry of declared) {
|
|
2154
|
+
const alias = mounted.get(entry.id);
|
|
2155
|
+
if (alias !== void 0) {
|
|
2156
|
+
aliases.add(alias);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return [...aliases];
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* grill-6fixes (D6): pick which project this repo binds to inside `alias`.
|
|
2163
|
+
* Default is the git-repo-derived id, applied SILENTLY in the common case
|
|
2164
|
+
* (the store has no projects yet, or the git id already matches an existing
|
|
2165
|
+
* one). The user is asked ONLY on genuine ambiguity — the store already
|
|
2166
|
+
* enumerates projects AND the git id matches none of them — the one case
|
|
2167
|
+
* where silently auto-creating would fork a parallel project away from the
|
|
2168
|
+
* team's existing one. Returns the resolved project id, or null on cancel.
|
|
2169
|
+
*
|
|
2170
|
+
* `interactive` gates the disambiguation prompt: non-interactive flows (e.g.
|
|
2171
|
+
* `install --url` in CI) never block — they fall back to the deterministic
|
|
2172
|
+
* git-suggested id instead of stalling on a clack prompt with no TTY.
|
|
2173
|
+
*/
|
|
2174
|
+
async resolveProjectIdWithGuard(projectRoot, alias, globalRoot, interactive) {
|
|
2175
|
+
const suggested = suggestStoreProjectId(projectRoot);
|
|
2176
|
+
const existing = await storeProjectList(alias, globalRoot);
|
|
2177
|
+
if (existing.length === 0 || existing.some((project) => project.id === suggested)) {
|
|
2178
|
+
return suggested;
|
|
2179
|
+
}
|
|
2180
|
+
if (!interactive) {
|
|
2181
|
+
return suggested;
|
|
2182
|
+
}
|
|
2183
|
+
const NEW_PROJECT = "__new_project__";
|
|
2184
|
+
const picked = await select({
|
|
2185
|
+
message: t("cli.install.store.project-pick.prompt", { store: alias }),
|
|
2186
|
+
initialValue: NEW_PROJECT,
|
|
2187
|
+
options: [
|
|
2188
|
+
...existing.map((project) => ({
|
|
2189
|
+
value: project.id,
|
|
2190
|
+
label: t("cli.install.store.project-pick.join", {
|
|
2191
|
+
name: project.name ?? project.id,
|
|
2192
|
+
id: project.id
|
|
2193
|
+
})
|
|
2194
|
+
})),
|
|
2195
|
+
{ value: NEW_PROJECT, label: t("cli.install.store.project-pick.new", { id: suggested }) }
|
|
2196
|
+
]
|
|
2197
|
+
});
|
|
2198
|
+
if (isCancel(picked)) return null;
|
|
2199
|
+
if (picked !== NEW_PROJECT) return picked;
|
|
2200
|
+
const entered = await text({
|
|
2201
|
+
message: t("cli.install.store.project-pick.new-name"),
|
|
2202
|
+
initialValue: suggested
|
|
2203
|
+
});
|
|
2204
|
+
if (isCancel(entered) || typeof entered !== "string" || entered.length === 0) {
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
return normalizeStoreProjectId(entered);
|
|
2208
|
+
}
|
|
2209
|
+
warnUnboundStores(unboundStores) {
|
|
2210
|
+
console.log("");
|
|
2211
|
+
console.log(
|
|
2212
|
+
t("cli.install.store.unbound-note", {
|
|
2213
|
+
aliases: unboundStores.map((s) => `'${s.alias}'`).join(", ")
|
|
2214
|
+
})
|
|
2215
|
+
);
|
|
2216
|
+
console.log(t("cli.install.store.unbound-hint", { first: unboundStores[0].alias }));
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* C4: first-touch personal-store onboarding. Offers "create local (default)"
|
|
2220
|
+
* vs "clone existing from a remote". Returns true only when it cloned a
|
|
2221
|
+
* personal store AND wrote the global config (so the caller skips the fresh
|
|
2222
|
+
* mint). Default / cancel / clone-failure all return false → caller mints
|
|
2223
|
+
* fresh via runGlobalInstall. Never adds a keystroke to the non-interactive
|
|
2224
|
+
* path (only invoked when wizardEnabled).
|
|
2225
|
+
*/
|
|
2226
|
+
async promptPersonalStoreOnboarding(globalRoot) {
|
|
2227
|
+
const choice = await select({
|
|
2228
|
+
message: t("cli.install.store.personal.prompt"),
|
|
2229
|
+
initialValue: "new",
|
|
2230
|
+
options: [
|
|
2231
|
+
{
|
|
2232
|
+
value: "new",
|
|
2233
|
+
label: t("cli.install.store.personal.new-label"),
|
|
2234
|
+
hint: t("cli.install.store.personal.new-hint")
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
value: "clone",
|
|
2238
|
+
label: t("cli.install.store.personal.clone-label"),
|
|
2239
|
+
hint: t("cli.install.store.personal.clone-hint")
|
|
2240
|
+
}
|
|
2241
|
+
]
|
|
2242
|
+
});
|
|
2243
|
+
if (isCancel(choice) || choice !== "clone") {
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
const url = await text({
|
|
2247
|
+
message: t("cli.install.store.personal.clone-url"),
|
|
2248
|
+
placeholder: "git@github.com:you/fabric-personal.git"
|
|
2249
|
+
});
|
|
2250
|
+
if (isCancel(url) || typeof url !== "string" || url.length === 0) {
|
|
2251
|
+
return false;
|
|
2252
|
+
}
|
|
2253
|
+
try {
|
|
2254
|
+
const { store_uuid } = cloneGlobalPersonalFromRemote(url, globalRoot);
|
|
2255
|
+
console.log("");
|
|
2256
|
+
console.log(paint.success(t("cli.install.store.personal.cloned-success", { uuid: store_uuid })));
|
|
2257
|
+
return true;
|
|
2258
|
+
} catch (error) {
|
|
2259
|
+
console.log(
|
|
2260
|
+
paint.warn(
|
|
2261
|
+
t("cli.install.store.personal.clone-failed", {
|
|
2262
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2263
|
+
})
|
|
2264
|
+
)
|
|
2265
|
+
);
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async ensurePersonalStore(config, globalRoot) {
|
|
2270
|
+
const personalAlias = config.stores.find((store) => store.alias === "personal");
|
|
2271
|
+
if (personalAlias === void 0) {
|
|
2272
|
+
const uuid = randomUUID2();
|
|
2273
|
+
const mounted = { store_uuid: uuid, alias: "personal", mount_name: "personal", personal: true };
|
|
2274
|
+
await initStore2(
|
|
2275
|
+
join6(globalRoot, storeRelativePathForMount3(mounted)),
|
|
2276
|
+
{ store_uuid: uuid, created_at: (/* @__PURE__ */ new Date()).toISOString(), canonical_alias: "personal" }
|
|
2277
|
+
);
|
|
2278
|
+
saveGlobalConfig({ ...config, stores: [mounted, ...config.stores] }, globalRoot);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const nextStores = config.stores.map((store) => ({
|
|
2282
|
+
...store,
|
|
2283
|
+
...store.alias === "personal" ? { personal: true } : { personal: false }
|
|
2284
|
+
}));
|
|
2285
|
+
if (JSON.stringify(nextStores) !== JSON.stringify(config.stores)) {
|
|
2286
|
+
saveGlobalConfig({ ...config, stores: nextStores }, globalRoot);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
// src/install/hooks-orchestrator.ts
|
|
2292
|
+
import { existsSync as existsSync5, statSync as statSync4 } from "fs";
|
|
2293
|
+
import { isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
|
|
2294
|
+
function validateHookPaths(projectRoot) {
|
|
2295
|
+
const scripts = [
|
|
2296
|
+
{ stepSuffix: "", hookFile: "fabric-hint.cjs" },
|
|
2297
|
+
{ stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
|
|
2298
|
+
{ stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" },
|
|
2299
|
+
// lifecycle-refactor W2-T2/T3: SessionEnd + PostToolUse marker hooks.
|
|
2300
|
+
{ stepSuffix: "-session-end", hookFile: "session-end-marker.cjs" },
|
|
2301
|
+
{ stepSuffix: "-post-tooluse", hookFile: "post-tooluse-mutation.cjs" }
|
|
2302
|
+
];
|
|
2303
|
+
const clients = [
|
|
2304
|
+
{
|
|
2305
|
+
client: "claude",
|
|
2306
|
+
configRel: join7(".claude", "settings.json"),
|
|
2307
|
+
hookDir: join7(".claude", "hooks")
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
client: "codex",
|
|
2311
|
+
configRel: join7(".codex", "hooks.json"),
|
|
2312
|
+
hookDir: join7(".codex", "hooks")
|
|
2313
|
+
}
|
|
2314
|
+
];
|
|
2315
|
+
const results = [];
|
|
2316
|
+
for (const { client, configRel, hookDir } of clients) {
|
|
2317
|
+
const configPath = resolve3(projectRoot, configRel);
|
|
2318
|
+
if (!existsSync5(configPath)) {
|
|
2319
|
+
results.push({
|
|
2320
|
+
step: `hook-validate-${client}`,
|
|
2321
|
+
path: configPath,
|
|
2322
|
+
status: "skipped",
|
|
2323
|
+
message: "missing-config"
|
|
2324
|
+
});
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
for (const { stepSuffix, hookFile } of scripts) {
|
|
2328
|
+
const expectedHookPath = resolve3(projectRoot, hookDir, hookFile);
|
|
2329
|
+
const expectedHookRel = join7(hookDir, hookFile);
|
|
2330
|
+
const step = `hook-validate-${client}${stepSuffix}`;
|
|
2331
|
+
if (!existsSync5(expectedHookPath)) {
|
|
2332
|
+
results.push({
|
|
2333
|
+
step,
|
|
2334
|
+
path: expectedHookPath,
|
|
2335
|
+
status: "error",
|
|
2336
|
+
message: `hook script missing: ${expectedHookRel}`
|
|
2337
|
+
});
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return results;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/install/pipeline/hooks.stage.ts
|
|
2347
|
+
var HooksStage = class {
|
|
2348
|
+
name = "hooks";
|
|
2349
|
+
async execute(context) {
|
|
2350
|
+
if (context.options.skipHooks) {
|
|
2351
|
+
return stageSkipped("hooks", "skipped via --skipHooks");
|
|
2352
|
+
}
|
|
2353
|
+
if (context.options.planOnly === true) {
|
|
2354
|
+
return stageSkipped("hooks", "dry-run: hook and skill install planned without writing files");
|
|
2355
|
+
}
|
|
2356
|
+
try {
|
|
2357
|
+
const target = context.target;
|
|
2358
|
+
const installResults = [];
|
|
2359
|
+
installResults.push(...await this.runBestEffort("skill-deprecated-cleanup", () => cleanupDeprecatedSkills(target)));
|
|
2360
|
+
installResults.push(...await this.runBestEffort("skill-router-install", () => installFabricRouterSkill(target)));
|
|
2361
|
+
installResults.push(...await this.runBestEffort("skill-install", () => installFabricArchiveSkill(target)));
|
|
2362
|
+
installResults.push(...await this.runBestEffort("skill-review-install", () => installFabricReviewSkill(target)));
|
|
2363
|
+
installResults.push(...await this.runBestEffort("skill-import-install", () => installFabricImportSkill(target)));
|
|
2364
|
+
installResults.push(...await this.runBestEffort("skill-sync-install", () => installFabricSyncSkill(target)));
|
|
2365
|
+
installResults.push(...await this.runBestEffort("skill-store-install", () => installFabricStoreSkill(target)));
|
|
2366
|
+
installResults.push(...await this.runBestEffort("skill-audit-install", () => installFabricAuditSkill(target)));
|
|
2367
|
+
installResults.push(...await this.runBestEffort("skill-connect-install", () => installFabricConnectSkill(target)));
|
|
2368
|
+
installResults.push(...await this.runBestEffort("skill-shared-lib", () => installSharedSkillLib(target)));
|
|
2369
|
+
installResults.push(...await this.runBestEffort("hook-script", () => installArchiveHintHook(target)));
|
|
2370
|
+
installResults.push(...await this.runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(target)));
|
|
2371
|
+
installResults.push(...await this.runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(target)));
|
|
2372
|
+
installResults.push(...await this.runBestEffort("hook-cite-policy-evict-script", () => installCitePolicyEvictHook(target)));
|
|
2373
|
+
installResults.push(...await this.runBestEffort("hook-session-end-script", () => installSessionEndMarkerHook(target)));
|
|
2374
|
+
installResults.push(...await this.runBestEffort("hook-post-tooluse-script", () => installPostTooluseMutationHook(target)));
|
|
2375
|
+
installResults.push(...await this.runBestEffort("hook-lib", () => installHookLibs(target)));
|
|
2376
|
+
installResults.push(await this.runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(target)));
|
|
2377
|
+
installResults.push(await this.runSingleStep("codex-hook-config", () => mergeCodexHookConfig(target)));
|
|
2378
|
+
installResults.push(await this.runSingleStep("bootstrap-snapshot", () => writeFabricAgentsSnapshot(target)));
|
|
2379
|
+
installResults.push(await this.runSingleStep("bootstrap-claude", () => writeClaudeBootstrapThinShell(target)));
|
|
2380
|
+
installResults.push(await this.runSingleStep("bootstrap-codex", () => writeCodexBootstrapManagedBlock(target)));
|
|
2381
|
+
installResults.push(...validateHookPaths(target));
|
|
2382
|
+
for (const result of installResults) {
|
|
2383
|
+
if (result.status === "error") {
|
|
2384
|
+
process.stderr.write(`hooks ${result.step} ${result.path}: ${result.message ?? "unknown error"}
|
|
2385
|
+
`);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
const installed = installResults.filter((r) => r.status === "written").map((r) => r.path);
|
|
2389
|
+
const skipped = installResults.filter((r) => r.status === "skipped").map((r) => r.path);
|
|
2390
|
+
const errors = installResults.filter((r) => r.status === "error").map((r) => `${r.step}: ${r.message}`);
|
|
2391
|
+
console.log(this.formatStageHeader(t("cli.install.stages.hooks")));
|
|
2392
|
+
if (errors.length > 0) {
|
|
2393
|
+
console.log(this.formatStageResult("hooks", "failed", installed.length, skipped.length));
|
|
2394
|
+
return {
|
|
2395
|
+
...stageFailed("hooks", errors),
|
|
2396
|
+
installed,
|
|
2397
|
+
skipped,
|
|
2398
|
+
payload: { installResults }
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
console.log(this.formatStageResult("hooks", "completed", installed.length, skipped.length));
|
|
2402
|
+
return stageRan("hooks", installed, skipped);
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
return stageFailedFromError("hooks", error);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
async runBestEffort(step, fn) {
|
|
2408
|
+
try {
|
|
2409
|
+
return await fn();
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
return [
|
|
2412
|
+
{
|
|
2413
|
+
step,
|
|
2414
|
+
path: "",
|
|
2415
|
+
status: "error",
|
|
2416
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2417
|
+
}
|
|
2418
|
+
];
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
async runSingleStep(step, fn) {
|
|
2422
|
+
try {
|
|
2423
|
+
return await fn();
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
return {
|
|
2426
|
+
step,
|
|
2427
|
+
path: "",
|
|
2428
|
+
status: "error",
|
|
2429
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
formatStageHeader(message) {
|
|
2434
|
+
const nextLabel = () => paint.ai(t("cli.shared.next"));
|
|
2435
|
+
return `${nextLabel()} ${paint.muted(message)}`;
|
|
2436
|
+
}
|
|
2437
|
+
formatStageResult(stage, status, installedCount, skippedCount) {
|
|
2438
|
+
const completedStageLabel = () => status === "failed" ? paint.error("failed") : paint.success(t("cli.install.stages.completed"));
|
|
2439
|
+
const counts = `installed=${installedCount} skipped=${skippedCount}`;
|
|
2440
|
+
return `${completedStageLabel()} ${stage}: ${counts}`;
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
// src/install/pipeline/mcp.stage.ts
|
|
2445
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
2446
|
+
import { join as join9 } from "path";
|
|
2447
|
+
|
|
2448
|
+
// src/lib/package-manager.ts
|
|
2449
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2450
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
2451
|
+
function detectPackageManager(cwd) {
|
|
2452
|
+
const workspaceRoot = resolve4(cwd);
|
|
2453
|
+
if (existsSync6(join8(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
2454
|
+
return "pnpm";
|
|
2455
|
+
}
|
|
2456
|
+
if (existsSync6(join8(workspaceRoot, "yarn.lock"))) {
|
|
2457
|
+
return "yarn";
|
|
2458
|
+
}
|
|
2459
|
+
if (existsSync6(join8(workspaceRoot, "package-lock.json"))) {
|
|
2460
|
+
return "npm";
|
|
2461
|
+
}
|
|
2462
|
+
return "npm";
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/install/pipeline/mcp.stage.ts
|
|
2466
|
+
var LOCAL_FABRIC_SERVER_PATH = join9("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
|
|
2467
|
+
var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
|
|
2468
|
+
var McpStage = class {
|
|
2469
|
+
name = "mcp";
|
|
2470
|
+
async execute(context) {
|
|
2471
|
+
if (context.options.skipMcp) {
|
|
2472
|
+
return stageSkipped("mcp", "skipped via --skipMcp");
|
|
2473
|
+
}
|
|
2474
|
+
if (context.options.planOnly === true) {
|
|
2475
|
+
return stageSkipped("mcp", "dry-run: MCP config install planned without writing files");
|
|
2476
|
+
}
|
|
2477
|
+
try {
|
|
2478
|
+
const target = context.target;
|
|
2479
|
+
const mode = context.mcpInstallMode;
|
|
2480
|
+
console.log(this.formatStageHeader(t("cli.install.stages.mcp")));
|
|
2481
|
+
if (mode === "local") {
|
|
2482
|
+
const manager = detectPackageManager(target);
|
|
2483
|
+
process.stderr.write(`${t("cli.install.mcp.install.local")}
|
|
2484
|
+
`);
|
|
2485
|
+
process.stderr.write(`${t("cli.install.mcp.local.installing", { manager })}
|
|
2486
|
+
`);
|
|
2487
|
+
this.installLocalFabricServer(target, manager);
|
|
2488
|
+
process.stderr.write(`${t("cli.install.mcp.local.installed")}
|
|
2489
|
+
`);
|
|
2490
|
+
} else {
|
|
2491
|
+
process.stderr.write(`${t("cli.install.mcp.install.global")}
|
|
2492
|
+
`);
|
|
2493
|
+
}
|
|
2494
|
+
const result = await installMcpClients(target, {
|
|
2495
|
+
localServerPath: mode === "local" ? LOCAL_FABRIC_SERVER_PATH : void 0,
|
|
2496
|
+
claudeMcpScope: context.claudeMcpScope
|
|
2497
|
+
});
|
|
2498
|
+
if (result.details.length === 0) {
|
|
2499
|
+
console.log(this.formatStageResult("mcp", "skipped", 0, 0, t("cli.config.install.no-configs")));
|
|
2500
|
+
return stageSkipped("mcp", "no MCP configs to install");
|
|
2501
|
+
}
|
|
2502
|
+
console.log(this.formatStageResult("mcp", "completed", result.installed.length, result.skipped.length));
|
|
2503
|
+
return stageRan("mcp", result.installed, result.skipped);
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
return stageFailedFromError("mcp", error);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
installLocalFabricServer(target, manager) {
|
|
2509
|
+
const installArgs = manager === "npm" ? ["install", "-D", FABRIC_SERVER_PACKAGE] : ["add", "-D", FABRIC_SERVER_PACKAGE];
|
|
2510
|
+
execFileSync5(manager, installArgs, {
|
|
2511
|
+
cwd: target,
|
|
2512
|
+
stdio: "inherit",
|
|
2513
|
+
shell: process.platform === "win32"
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
formatStageHeader(message) {
|
|
2517
|
+
const nextLabel = () => paint.ai(t("cli.shared.next"));
|
|
2518
|
+
return `${nextLabel()} ${paint.muted(message)}`;
|
|
2519
|
+
}
|
|
2520
|
+
formatStageResult(stage, status, installedCount, skippedCount, note) {
|
|
2521
|
+
const completedStageLabel = () => paint.success(t("cli.install.stages.completed"));
|
|
2522
|
+
const skippedStageLabel = () => paint.muted(t("cli.install.stages.skipped"));
|
|
2523
|
+
const label = status === "completed" ? completedStageLabel() : skippedStageLabel();
|
|
2524
|
+
const counts = `installed=${installedCount} skipped=${skippedCount}`;
|
|
2525
|
+
const suffix = note ? ` ${paint.muted(`(${note})`)}` : "";
|
|
2526
|
+
return `${label} ${stage}: ${counts}${suffix}`;
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
// src/install/pipeline/validate.stage.ts
|
|
2531
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2532
|
+
import { join as join10 } from "path";
|
|
2533
|
+
var ValidateStage = class {
|
|
2534
|
+
name = "validate";
|
|
2535
|
+
async execute(context) {
|
|
2536
|
+
if (context.options.planOnly === true) {
|
|
2537
|
+
return stageSkipped("validate", "dry-run: validation skipped because no files were written");
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
const target = context.target;
|
|
2541
|
+
const errors = [];
|
|
2542
|
+
const installed = [];
|
|
2543
|
+
const skipped = [];
|
|
2544
|
+
const hookValidationResults = validateHookPaths(target);
|
|
2545
|
+
for (const result of hookValidationResults) {
|
|
2546
|
+
if (result.status === "error") {
|
|
2547
|
+
errors.push(`${result.step}: ${result.message}`);
|
|
2548
|
+
} else {
|
|
2549
|
+
skipped.push(result.path);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
const fabricDir = join10(target, ".fabric");
|
|
2553
|
+
if (!existsSync7(fabricDir)) {
|
|
2554
|
+
errors.push(".fabric directory missing");
|
|
2555
|
+
} else {
|
|
2556
|
+
installed.push(fabricDir);
|
|
2557
|
+
}
|
|
2558
|
+
const configPath = join10(fabricDir, "fabric-config.json");
|
|
2559
|
+
if (!existsSync7(configPath)) {
|
|
2560
|
+
errors.push("fabric-config.json missing");
|
|
2561
|
+
} else {
|
|
2562
|
+
installed.push(configPath);
|
|
2563
|
+
}
|
|
2564
|
+
const eventsPath = join10(fabricDir, "events.jsonl");
|
|
2565
|
+
if (!existsSync7(eventsPath)) {
|
|
2566
|
+
errors.push("events.jsonl missing");
|
|
2567
|
+
} else {
|
|
2568
|
+
installed.push(eventsPath);
|
|
2569
|
+
}
|
|
2570
|
+
if (errors.length === 0) {
|
|
2571
|
+
console.log(paint.success("Validation passed"));
|
|
2572
|
+
} else {
|
|
2573
|
+
console.log(paint.error(`Validation failed: ${errors.length} error(s)`));
|
|
2574
|
+
for (const error of errors) {
|
|
2575
|
+
console.log(paint.error(` - ${error}`));
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
if (errors.length > 0) {
|
|
2579
|
+
return stageFailedFromError("validate", new Error(errors.join("; ")));
|
|
2580
|
+
}
|
|
2581
|
+
return stageRan("validate", installed, skipped);
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
return stageFailedFromError("validate", error);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
|
|
2588
|
+
// src/install/pipeline/guidance.stage.ts
|
|
2589
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2590
|
+
import { confirm, isCancel as isCancel2 } from "@clack/prompts";
|
|
2591
|
+
|
|
2592
|
+
// src/install/semantic-search.ts
|
|
2593
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2594
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
2595
|
+
var DEFAULT_EMBED_MODEL_PIN = "fast-bge-small-zh-v1.5";
|
|
2596
|
+
function enableSemanticSearch(projectRoot, opts = {}) {
|
|
2597
|
+
const model = typeof opts.model === "string" && opts.model.length > 0 ? opts.model : DEFAULT_EMBED_MODEL_PIN;
|
|
2598
|
+
const configPath = join11(projectRoot, ".fabric", "fabric-config.json");
|
|
2599
|
+
let existing = {};
|
|
2600
|
+
if (existsSync8(configPath)) {
|
|
2601
|
+
try {
|
|
2602
|
+
const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
2603
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2604
|
+
existing = parsed;
|
|
2605
|
+
}
|
|
2606
|
+
} catch {
|
|
2607
|
+
existing = {};
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
const alreadyEnabled = existing.embed_enabled === true && existing.embed_model === model;
|
|
2611
|
+
if (alreadyEnabled) {
|
|
2612
|
+
return { configPath, model, alreadyEnabled: true, changed: false };
|
|
2613
|
+
}
|
|
2614
|
+
const merged = { ...existing, embed_enabled: true, embed_model: model };
|
|
2615
|
+
mkdirSync3(dirname3(configPath), { recursive: true });
|
|
2616
|
+
writeFileSync3(configPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2617
|
+
return { configPath, model, alreadyEnabled: false, changed: true };
|
|
2618
|
+
}
|
|
2619
|
+
function renderSemanticSearchInstructions(model) {
|
|
2620
|
+
return [
|
|
2621
|
+
t("cli.install.semantic.enabled", { model }),
|
|
2622
|
+
...t("cli.install.semantic.manual-steps").split("\n")
|
|
2623
|
+
];
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// src/install/pipeline/guidance.stage.ts
|
|
2627
|
+
var GuidanceStage = class {
|
|
2628
|
+
name = "guidance";
|
|
2629
|
+
async execute(context) {
|
|
2630
|
+
try {
|
|
2631
|
+
const translate = context.translate ?? t;
|
|
2632
|
+
if (context.options.planOnly) {
|
|
2633
|
+
return stageRan("guidance", [], []);
|
|
2634
|
+
}
|
|
2635
|
+
if (context.args["enable-embed"]) {
|
|
2636
|
+
this.enableSemanticSearchAndReport(context.target, context.args["embed-model"]);
|
|
2637
|
+
} else if (context.wizardEnabled) {
|
|
2638
|
+
await this.promptSemanticSearch(context.target);
|
|
2639
|
+
}
|
|
2640
|
+
console.log("");
|
|
2641
|
+
console.log(translate("cli.install.next-steps"));
|
|
2642
|
+
console.log("");
|
|
2643
|
+
console.log(paint.muted("More: docs/surfaces.md explains when to use CLI vs Skill vs MCP."));
|
|
2644
|
+
console.log("");
|
|
2645
|
+
console.log(translate("cli.install.restart-banner"));
|
|
2646
|
+
const finalSupports = detectClientSupports(context.target);
|
|
2647
|
+
this.printCapabilitySummary(finalSupports, context);
|
|
2648
|
+
return stageRan("guidance", [], []);
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
return stageFailedFromError("guidance", error);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
enableSemanticSearchAndReport(projectRoot, model) {
|
|
2654
|
+
const enabled = enableSemanticSearch(projectRoot, model === void 0 ? {} : { model });
|
|
2655
|
+
console.log("");
|
|
2656
|
+
if (enabled.alreadyEnabled) {
|
|
2657
|
+
console.log(
|
|
2658
|
+
paint.muted(t("cli.install.semantic.already-enabled", { model: enabled.model, path: enabled.configPath }))
|
|
2659
|
+
);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
for (const line of renderSemanticSearchInstructions(enabled.model)) {
|
|
2663
|
+
console.log(line);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async promptSemanticSearch(projectRoot) {
|
|
2667
|
+
const enable = await confirm({
|
|
2668
|
+
message: t("cli.install.semantic.prompt"),
|
|
2669
|
+
initialValue: false
|
|
2670
|
+
});
|
|
2671
|
+
if (isCancel2(enable) || !enable) {
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
const enabled = enableSemanticSearch(projectRoot);
|
|
2675
|
+
console.log("");
|
|
2676
|
+
if (enabled.alreadyEnabled) {
|
|
2677
|
+
console.log(
|
|
2678
|
+
paint.muted(t("cli.install.semantic.already-enabled", { model: enabled.model, path: enabled.configPath }))
|
|
2679
|
+
);
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
console.log(t("cli.install.semantic.enabled", { model: enabled.model }));
|
|
2683
|
+
await this.offerInstallFastembed();
|
|
2684
|
+
}
|
|
2685
|
+
async offerInstallFastembed() {
|
|
2686
|
+
const proceed = await confirm({
|
|
2687
|
+
message: t("cli.install.semantic.offer-install"),
|
|
2688
|
+
initialValue: true
|
|
2689
|
+
});
|
|
2690
|
+
if (isCancel2(proceed) || !proceed) {
|
|
2691
|
+
this.printSemanticManualSteps();
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
console.log(t("cli.install.semantic.installing"));
|
|
2695
|
+
try {
|
|
2696
|
+
execFileSync6("npm", ["i", "-g", "fastembed"], { stdio: ["ignore", "inherit", "inherit"] });
|
|
2697
|
+
console.log(paint.success(t("cli.install.semantic.installed")));
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
console.log(
|
|
2700
|
+
paint.warn(
|
|
2701
|
+
t("cli.install.semantic.install-failed", {
|
|
2702
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2703
|
+
})
|
|
2704
|
+
)
|
|
2705
|
+
);
|
|
2706
|
+
this.printSemanticManualSteps();
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
printSemanticManualSteps() {
|
|
2710
|
+
for (const line of t("cli.install.semantic.manual-steps").split("\n")) {
|
|
2711
|
+
console.log(line);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
printCapabilitySummary(supports, context) {
|
|
2715
|
+
const detected = supports.filter((s) => s.detected);
|
|
2716
|
+
if (detected.length === 0) {
|
|
2717
|
+
console.log(t("cli.install.capabilities.none"));
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
console.log(t("cli.install.capabilities.title"));
|
|
2721
|
+
const headers = {
|
|
2722
|
+
client: t("cli.install.capabilities.header.client"),
|
|
2723
|
+
bootstrap: t("cli.install.capabilities.header.bootstrap"),
|
|
2724
|
+
mcp: t("cli.install.capabilities.header.mcp"),
|
|
2725
|
+
hook: t("cli.install.capabilities.header.hook"),
|
|
2726
|
+
skill: t("cli.install.capabilities.header.skill"),
|
|
2727
|
+
followUp: t("cli.install.capabilities.header.follow-up")
|
|
2728
|
+
};
|
|
2729
|
+
const widths = {
|
|
2730
|
+
client: Math.max(6, ...detected.map((s) => s.label.length)),
|
|
2731
|
+
bootstrap: Math.max(8, 8),
|
|
2732
|
+
mcp: Math.max(3, 3),
|
|
2733
|
+
hook: Math.max(4, 4),
|
|
2734
|
+
skill: Math.max(5, 5),
|
|
2735
|
+
followUp: Math.max(9, 9)
|
|
2736
|
+
};
|
|
2737
|
+
const headerRow = [
|
|
2738
|
+
headers.client.padEnd(widths.client),
|
|
2739
|
+
headers.bootstrap.padEnd(widths.bootstrap),
|
|
2740
|
+
headers.mcp.padEnd(widths.mcp),
|
|
2741
|
+
headers.hook.padEnd(widths.hook),
|
|
2742
|
+
headers.skill.padEnd(widths.skill),
|
|
2743
|
+
headers.followUp.padEnd(widths.followUp)
|
|
2744
|
+
].join(" ");
|
|
2745
|
+
console.log(headerRow);
|
|
2746
|
+
const divider = [
|
|
2747
|
+
"".padEnd(widths.client, "-"),
|
|
2748
|
+
"".padEnd(widths.bootstrap, "-"),
|
|
2749
|
+
"".padEnd(widths.mcp, "-"),
|
|
2750
|
+
"".padEnd(widths.hook, "-"),
|
|
2751
|
+
"".padEnd(widths.skill, "-"),
|
|
2752
|
+
"".padEnd(widths.followUp, "-")
|
|
2753
|
+
].join(" ");
|
|
2754
|
+
console.log(divider);
|
|
2755
|
+
for (const support of detected) {
|
|
2756
|
+
const bootstrap = support.capabilities.bootstrap ? this.capabilityStatus(context.options.skipBootstrap ? "skipped" : "ran") : t("cli.install.capabilities.status.na");
|
|
2757
|
+
const mcp = support.capabilities.mcp ? this.capabilityStatus(context.options.skipMcp ? "skipped" : "ran") : t("cli.install.capabilities.status.na");
|
|
2758
|
+
const hook = this.capabilityInstallStatus(support, "hook");
|
|
2759
|
+
const skill = this.capabilityInstallStatus(support, "skill");
|
|
2760
|
+
const followUp = this.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");
|
|
2761
|
+
const row = [
|
|
2762
|
+
support.label.padEnd(widths.client),
|
|
2763
|
+
bootstrap.padEnd(widths.bootstrap),
|
|
2764
|
+
mcp.padEnd(widths.mcp),
|
|
2765
|
+
hook.padEnd(widths.hook),
|
|
2766
|
+
skill.padEnd(widths.skill),
|
|
2767
|
+
followUp.padEnd(widths.followUp)
|
|
2768
|
+
].join(" ");
|
|
2769
|
+
console.log(row);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
capabilityStatus(disposition) {
|
|
2773
|
+
switch (disposition) {
|
|
2774
|
+
case "ran":
|
|
2775
|
+
return t("cli.install.capabilities.status.ready");
|
|
2776
|
+
case "skipped":
|
|
2777
|
+
return t("cli.install.capabilities.status.skipped");
|
|
2778
|
+
case "failed":
|
|
2779
|
+
return t("cli.install.capabilities.status.failed");
|
|
2780
|
+
case null:
|
|
2781
|
+
return t("cli.install.capabilities.status.na");
|
|
2782
|
+
default:
|
|
2783
|
+
return t("cli.install.capabilities.status.ready");
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
capabilityInstallStatus(support, capability) {
|
|
2787
|
+
if (!support.capabilities[capability]) {
|
|
2788
|
+
return t("cli.install.capabilities.status.na");
|
|
2789
|
+
}
|
|
2790
|
+
return this.hasInstalledCapability(support, capability) ? t("cli.install.capabilities.status.installed") : t("cli.install.capabilities.status.supported");
|
|
2791
|
+
}
|
|
2792
|
+
hasInstalledCapability(support, capability) {
|
|
2793
|
+
return support.installedCapabilities?.[capability] === true;
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
// src/tui/StepCounter.tsx
|
|
2798
|
+
import { Box, Text } from "ink";
|
|
2799
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2800
|
+
var statusColors = {
|
|
2801
|
+
pending: "gray",
|
|
2802
|
+
running: "cyan",
|
|
2803
|
+
success: "green",
|
|
2804
|
+
error: "red",
|
|
2805
|
+
skipped: "yellow"
|
|
2806
|
+
};
|
|
2807
|
+
var statusSymbols = {
|
|
2808
|
+
pending: "\u25CB",
|
|
2809
|
+
running: "\u25CF",
|
|
2810
|
+
success: "\u2713",
|
|
2811
|
+
error: "\u2717",
|
|
2812
|
+
skipped: "\u25CB"
|
|
2813
|
+
};
|
|
2814
|
+
function StepCounter({
|
|
2815
|
+
current,
|
|
2816
|
+
total,
|
|
2817
|
+
label,
|
|
2818
|
+
status = "running"
|
|
2819
|
+
}) {
|
|
2820
|
+
const color = statusColors[status] || "cyan";
|
|
2821
|
+
const symbol = statusSymbols[status] || "\u25CF";
|
|
2822
|
+
return /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
|
|
2823
|
+
/* @__PURE__ */ jsx(Text, { color, bold: true, children: symbol }),
|
|
2824
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
2825
|
+
"(",
|
|
2826
|
+
current,
|
|
2827
|
+
"/",
|
|
2828
|
+
total,
|
|
2829
|
+
")"
|
|
2830
|
+
] }),
|
|
2831
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: label })
|
|
2832
|
+
] });
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/tui/StatusMessage.tsx
|
|
2836
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
2837
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2838
|
+
var typeColors = {
|
|
2839
|
+
success: "green",
|
|
2840
|
+
error: "red",
|
|
2841
|
+
warning: "yellow",
|
|
2842
|
+
info: "blue"
|
|
2843
|
+
};
|
|
2844
|
+
var typeLabels = {
|
|
2845
|
+
success: "\u2713",
|
|
2846
|
+
error: "\u2717",
|
|
2847
|
+
warning: "!",
|
|
2848
|
+
info: "\u2139"
|
|
2849
|
+
};
|
|
2850
|
+
function StatusMessage({
|
|
2851
|
+
message,
|
|
2852
|
+
type,
|
|
2853
|
+
timestamp = false
|
|
2854
|
+
}) {
|
|
2855
|
+
const color = typeColors[type] || "white";
|
|
2856
|
+
const label = typeLabels[type] || "\u2022";
|
|
2857
|
+
const timeStr = timestamp ? `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] ` : "";
|
|
2858
|
+
return /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
|
|
2859
|
+
/* @__PURE__ */ jsx2(Text2, { color, bold: true, children: label }),
|
|
2860
|
+
timestamp && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: timeStr }),
|
|
2861
|
+
/* @__PURE__ */ jsx2(Text2, { color: type === "error" ? "red" : void 0, children: message })
|
|
2862
|
+
] });
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// src/tui/SummaryCard.tsx
|
|
2866
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
2867
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2868
|
+
function DetailStatus({ status }) {
|
|
2869
|
+
if (!status) return null;
|
|
2870
|
+
const indicators = {
|
|
2871
|
+
success: { symbol: "\u2713", color: "green" },
|
|
2872
|
+
error: { symbol: "\u2717", color: "red" },
|
|
2873
|
+
skipped: { symbol: "\u25CB", color: "yellow" },
|
|
2874
|
+
info: { symbol: "\u2139", color: "blue" }
|
|
2875
|
+
};
|
|
2876
|
+
const { symbol, color } = indicators[status] || { symbol: "\u2022", color: "white" };
|
|
2877
|
+
return /* @__PURE__ */ jsx3(Text3, { color, bold: true, children: symbol });
|
|
2878
|
+
}
|
|
2879
|
+
function SummaryCard({ summary }) {
|
|
2880
|
+
const { title, successCount, skippedCount = 0, errorCount = 0, details = [] } = summary;
|
|
2881
|
+
const totalCount = successCount + skippedCount + errorCount;
|
|
2882
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
|
|
2883
|
+
/* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: title }) }),
|
|
2884
|
+
/* @__PURE__ */ jsxs3(Box3, { gap: 3, children: [
|
|
2885
|
+
successCount > 0 && /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
|
|
2886
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2713" }),
|
|
2887
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2888
|
+
successCount,
|
|
2889
|
+
" succeeded"
|
|
2890
|
+
] })
|
|
2891
|
+
] }),
|
|
2892
|
+
skippedCount > 0 && /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
|
|
2893
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: "\u25CB" }),
|
|
2894
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2895
|
+
skippedCount,
|
|
2896
|
+
" skipped"
|
|
2897
|
+
] })
|
|
2898
|
+
] }),
|
|
2899
|
+
errorCount > 0 && /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
|
|
2900
|
+
/* @__PURE__ */ jsx3(Text3, { color: "red", bold: true, children: "\u2717" }),
|
|
2901
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2902
|
+
errorCount,
|
|
2903
|
+
" failed"
|
|
2904
|
+
] })
|
|
2905
|
+
] })
|
|
2906
|
+
] }),
|
|
2907
|
+
details.length > 0 && /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: details.map((detail, index) => /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
|
|
2908
|
+
/* @__PURE__ */ jsx3(DetailStatus, { status: detail.status }),
|
|
2909
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2910
|
+
detail.label,
|
|
2911
|
+
":"
|
|
2912
|
+
] }),
|
|
2913
|
+
/* @__PURE__ */ jsx3(Text3, { children: detail.value })
|
|
2914
|
+
] }, index)) }),
|
|
2915
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: totalCount === successCount ? "All steps completed successfully" : errorCount > 0 ? `${errorCount} step${errorCount > 1 ? "s" : ""} failed` : `${successCount}/${totalCount} steps completed` }) })
|
|
2916
|
+
] });
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
// src/tui/ErrorBox.tsx
|
|
2920
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
2921
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2922
|
+
function ErrorBox({ error, showStack = false }) {
|
|
2923
|
+
const title = error.title || "Error";
|
|
2924
|
+
const code = "code" in error ? error.code : void 0;
|
|
2925
|
+
const hint = "hint" in error ? error.hint : void 0;
|
|
2926
|
+
const stack = "stack" in error ? error.stack : void 0;
|
|
2927
|
+
return /* @__PURE__ */ jsxs4(
|
|
2928
|
+
Box4,
|
|
2929
|
+
{
|
|
2930
|
+
flexDirection: "column",
|
|
2931
|
+
borderStyle: "round",
|
|
2932
|
+
borderColor: "red",
|
|
2933
|
+
paddingX: 1,
|
|
2934
|
+
children: [
|
|
2935
|
+
/* @__PURE__ */ jsxs4(Box4, { gap: 1, children: [
|
|
2936
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "red", bold: true, children: [
|
|
2937
|
+
"\u2717 ",
|
|
2938
|
+
title
|
|
2939
|
+
] }),
|
|
2940
|
+
code && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2941
|
+
"(",
|
|
2942
|
+
code,
|
|
2943
|
+
")"
|
|
2944
|
+
] })
|
|
2945
|
+
] }),
|
|
2946
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "red", children: error.message }) }),
|
|
2947
|
+
hint && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2948
|
+
"\u{1F4A1} ",
|
|
2949
|
+
hint
|
|
2950
|
+
] }) }),
|
|
2951
|
+
showStack && stack && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
|
|
2952
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Stack trace:" }),
|
|
2953
|
+
/* @__PURE__ */ jsx4(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, color: "gray", children: stack.split("\n").slice(0, 5).join("\n") }) })
|
|
2954
|
+
] })
|
|
2955
|
+
]
|
|
2956
|
+
}
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
function toErrorInfo(error) {
|
|
2960
|
+
if ("title" in error) {
|
|
2961
|
+
return error;
|
|
2962
|
+
}
|
|
2963
|
+
return {
|
|
2964
|
+
title: error.name || "Error",
|
|
2965
|
+
message: error.message,
|
|
2966
|
+
stack: error.stack
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// src/tui/Spinner.tsx
|
|
2971
|
+
import { useState, useEffect } from "react";
|
|
2972
|
+
import { Text as Text5 } from "ink";
|
|
2973
|
+
import { jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2974
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2975
|
+
function Spinner({ label }) {
|
|
2976
|
+
const [frame, setFrame] = useState(0);
|
|
2977
|
+
useEffect(() => {
|
|
2978
|
+
const timer = setInterval(() => {
|
|
2979
|
+
setFrame((prev) => (prev + 1) % FRAMES.length);
|
|
2980
|
+
}, 80);
|
|
2981
|
+
return () => clearInterval(timer);
|
|
2982
|
+
}, []);
|
|
2983
|
+
return /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
2984
|
+
FRAMES[frame],
|
|
2985
|
+
" ",
|
|
2986
|
+
label || "Loading..."
|
|
2987
|
+
] });
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// src/tui/ProgressBar.tsx
|
|
2991
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
2992
|
+
import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2993
|
+
|
|
2994
|
+
// src/tui/SectionHeader.tsx
|
|
2995
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
2996
|
+
import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2997
|
+
function SectionHeader({ title, subtitle }) {
|
|
2998
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom: 1, children: [
|
|
2999
|
+
/* @__PURE__ */ jsx6(Box6, { borderStyle: "classic", borderColor: "gray", children: /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text7, { bold: true, color: "cyan", children: title }) }) }),
|
|
3000
|
+
subtitle && /* @__PURE__ */ jsx6(Box6, { marginLeft: 2, children: /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: subtitle }) })
|
|
3001
|
+
] });
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// src/tui/StoreWizard.tsx
|
|
3005
|
+
import { useState as useState2 } from "react";
|
|
3006
|
+
import { Box as Box7, Text as Text8, useInput } from "ink";
|
|
3007
|
+
import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3008
|
+
|
|
3009
|
+
// src/tui/InputField.tsx
|
|
3010
|
+
import { useState as useState3 } from "react";
|
|
3011
|
+
import { Box as Box8, Text as Text9, useInput as useInput2, useApp } from "ink";
|
|
3012
|
+
import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3013
|
+
|
|
3014
|
+
// src/tui/StoreWizardFlow.tsx
|
|
3015
|
+
import { useState as useState4, useCallback } from "react";
|
|
3016
|
+
import { render } from "ink";
|
|
3017
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
3018
|
+
|
|
3019
|
+
// src/tui/InkOutputRenderer.ts
|
|
3020
|
+
import { render as render2 } from "ink";
|
|
3021
|
+
import React2 from "react";
|
|
3022
|
+
var InkOutputRenderer = class {
|
|
3023
|
+
config;
|
|
3024
|
+
currentStep = null;
|
|
3025
|
+
inkInstances = [];
|
|
3026
|
+
constructor(config = {}) {
|
|
3027
|
+
this.config = {
|
|
3028
|
+
colors: true,
|
|
3029
|
+
verbose: false,
|
|
3030
|
+
timestamps: false,
|
|
3031
|
+
...config
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Render a React component and wait for it to render
|
|
3036
|
+
*/
|
|
3037
|
+
renderComponent(node) {
|
|
3038
|
+
const { unmount } = render2(node);
|
|
3039
|
+
this.inkInstances.push({ unmount });
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Render a step progress indicator
|
|
3043
|
+
*/
|
|
3044
|
+
renderStep(step) {
|
|
3045
|
+
this.currentStep = step;
|
|
3046
|
+
if (step.status === "running") {
|
|
3047
|
+
this.renderComponent(
|
|
3048
|
+
React2.createElement(Spinner, { label: step.name })
|
|
3049
|
+
);
|
|
3050
|
+
} else {
|
|
3051
|
+
this.renderComponent(
|
|
3052
|
+
React2.createElement(StepCounter, {
|
|
3053
|
+
current: step.current,
|
|
3054
|
+
total: step.total,
|
|
3055
|
+
label: step.name,
|
|
3056
|
+
status: step.status
|
|
3057
|
+
})
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
if (step.detail) {
|
|
3061
|
+
this.renderComponent(
|
|
3062
|
+
React2.createElement(StatusMessage, {
|
|
3063
|
+
message: step.detail,
|
|
3064
|
+
type: step.status === "error" ? "error" : "info"
|
|
3065
|
+
})
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Render a success message
|
|
3071
|
+
*/
|
|
3072
|
+
renderSuccess(message) {
|
|
3073
|
+
this.renderComponent(
|
|
3074
|
+
React2.createElement(StatusMessage, {
|
|
3075
|
+
message,
|
|
3076
|
+
type: "success"
|
|
3077
|
+
})
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Render an error
|
|
3082
|
+
*/
|
|
3083
|
+
renderError(error) {
|
|
3084
|
+
const errorInfo = error instanceof Error ? toErrorInfo(error) : error;
|
|
3085
|
+
this.renderComponent(
|
|
3086
|
+
React2.createElement(ErrorBox, {
|
|
3087
|
+
error: errorInfo,
|
|
3088
|
+
showStack: this.config.verbose
|
|
3089
|
+
})
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
/**
|
|
3093
|
+
* Render a warning message
|
|
3094
|
+
*/
|
|
3095
|
+
renderWarning(message) {
|
|
3096
|
+
this.renderComponent(
|
|
3097
|
+
React2.createElement(StatusMessage, {
|
|
3098
|
+
message,
|
|
3099
|
+
type: "warning"
|
|
3100
|
+
})
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Render an info message
|
|
3105
|
+
*/
|
|
3106
|
+
renderInfo(message) {
|
|
3107
|
+
this.renderComponent(
|
|
3108
|
+
React2.createElement(StatusMessage, {
|
|
3109
|
+
message,
|
|
3110
|
+
type: "info"
|
|
3111
|
+
})
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Render a summary card
|
|
3116
|
+
*/
|
|
3117
|
+
renderSummaryCard(summary) {
|
|
3118
|
+
this.renderComponent(
|
|
3119
|
+
React2.createElement(SummaryCard, { summary })
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Render a section header
|
|
3124
|
+
*/
|
|
3125
|
+
renderSection(title) {
|
|
3126
|
+
this.renderComponent(
|
|
3127
|
+
React2.createElement(SectionHeader, { title })
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Render a final completion message
|
|
3132
|
+
*/
|
|
3133
|
+
renderComplete() {
|
|
3134
|
+
this.renderComponent(
|
|
3135
|
+
React2.createElement(StatusMessage, {
|
|
3136
|
+
message: "Done!",
|
|
3137
|
+
type: "success"
|
|
3138
|
+
})
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Clean up any pending renders
|
|
3143
|
+
*/
|
|
3144
|
+
async cleanup() {
|
|
3145
|
+
for (const instance of this.inkInstances) {
|
|
3146
|
+
try {
|
|
3147
|
+
instance.unmount();
|
|
3148
|
+
} catch {
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
this.inkInstances = [];
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
function createInkRenderer(config) {
|
|
3155
|
+
return new InkOutputRenderer(config);
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// src/commands/install-v2.ts
|
|
3159
|
+
var installCommand = defineCommand({
|
|
3160
|
+
meta: {
|
|
3161
|
+
name: "install",
|
|
3162
|
+
description: t("cli.install.description")
|
|
3163
|
+
},
|
|
3164
|
+
args: {
|
|
3165
|
+
debug: {
|
|
3166
|
+
type: "boolean",
|
|
3167
|
+
description: t("cli.install.args.debug.description"),
|
|
3168
|
+
default: false
|
|
3169
|
+
},
|
|
3170
|
+
"dry-run": {
|
|
3171
|
+
type: "boolean",
|
|
3172
|
+
description: t("cli.install.args.dry-run.description"),
|
|
3173
|
+
default: false
|
|
3174
|
+
},
|
|
3175
|
+
target: {
|
|
3176
|
+
type: "string",
|
|
3177
|
+
description: t("cli.install.args.target.description")
|
|
3178
|
+
},
|
|
3179
|
+
yes: {
|
|
3180
|
+
type: "boolean",
|
|
3181
|
+
description: t("cli.install.args.yes.description"),
|
|
3182
|
+
default: false
|
|
3183
|
+
},
|
|
3184
|
+
global: {
|
|
3185
|
+
type: "boolean",
|
|
3186
|
+
description: "Set up global Fabric (~/.fabric: uid + personal store + config)",
|
|
3187
|
+
default: false
|
|
3188
|
+
},
|
|
3189
|
+
url: {
|
|
3190
|
+
type: "string",
|
|
3191
|
+
description: "Clone + mount a shared store remote. In a project install: also binds it to this project and sets it as the write target. With --global: mounts it machine-wide only."
|
|
3192
|
+
},
|
|
3193
|
+
"enable-embed": {
|
|
3194
|
+
type: "boolean",
|
|
3195
|
+
description: t("cli.install.args.enable-embed.description"),
|
|
3196
|
+
default: false
|
|
3197
|
+
},
|
|
3198
|
+
"embed-model": {
|
|
3199
|
+
type: "string",
|
|
3200
|
+
description: t("cli.install.args.embed-model.description")
|
|
3201
|
+
}
|
|
3202
|
+
},
|
|
3203
|
+
async run({ args }) {
|
|
3204
|
+
await runInitCommand(args);
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
var install_v2_default = installCommand;
|
|
3208
|
+
async function runInitCommand(args) {
|
|
3209
|
+
const logger = createDebugLogger(args.debug);
|
|
3210
|
+
if (args.global === true) {
|
|
3211
|
+
if (args["dry-run"] === true) {
|
|
3212
|
+
console.log("Fabric install dry run: no global files will be written.");
|
|
3213
|
+
console.log("Planned: ensure global Fabric config and personal store exist.");
|
|
3214
|
+
if (args.url !== void 0) {
|
|
3215
|
+
console.log(`Planned: clone and mount store from ${args.url}.`);
|
|
3216
|
+
}
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
await runGlobalInstall({ url: args.url });
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
const resolution = resolveDevMode(args.target, process.cwd());
|
|
3223
|
+
logger(`init target source: ${resolution.source}`);
|
|
3224
|
+
for (const step of resolution.chain) {
|
|
3225
|
+
logger(step);
|
|
3226
|
+
}
|
|
3227
|
+
const terminalInteractive = isInteractiveInit();
|
|
3228
|
+
const renderer = shouldUseInstallRenderer(args, terminalInteractive) ? createInkRenderer({ verbose: args.debug }) : void 0;
|
|
3229
|
+
const context = createInstallContext(args, resolution.target, renderer);
|
|
3230
|
+
const pipeline = new InstallPipeline().addStage(new PreflightStage()).addStage(new EnvStage()).addStage(new StoreStage()).addStage(new HooksStage()).addStage(new McpStage()).addStage(new ValidateStage()).addStage(new GuidanceStage());
|
|
3231
|
+
const result = await pipeline.execute(context);
|
|
3232
|
+
if (renderer) {
|
|
3233
|
+
await renderer.cleanup();
|
|
3234
|
+
}
|
|
3235
|
+
if (!result.success) {
|
|
3236
|
+
if (result.error && !renderer) {
|
|
3237
|
+
console.error(paint.error(result.error.message));
|
|
3238
|
+
}
|
|
3239
|
+
process.exitCode = 1;
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
function createInstallContext(args, target, renderer) {
|
|
3244
|
+
const terminalInteractive = isInteractiveInit();
|
|
3245
|
+
const planOnly = args["dry-run"] === true;
|
|
3246
|
+
return {
|
|
3247
|
+
target,
|
|
3248
|
+
args,
|
|
3249
|
+
options: {
|
|
3250
|
+
planOnly,
|
|
3251
|
+
skipBootstrap: false,
|
|
3252
|
+
skipMcp: false,
|
|
3253
|
+
skipHooks: false
|
|
3254
|
+
},
|
|
3255
|
+
mcpInstallMode: "global",
|
|
3256
|
+
claudeMcpScope: "project",
|
|
3257
|
+
interactive: terminalInteractive && !args.yes,
|
|
3258
|
+
wizardEnabled: terminalInteractive && !args.yes && !planOnly,
|
|
3259
|
+
stageResults: [],
|
|
3260
|
+
rollbackStack: [],
|
|
3261
|
+
state: {},
|
|
3262
|
+
renderer
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function shouldUseInstallRenderer(args, terminalInteractive) {
|
|
3266
|
+
if (!terminalInteractive) {
|
|
3267
|
+
return false;
|
|
3268
|
+
}
|
|
3269
|
+
return args.yes === true || args["dry-run"] === true;
|
|
3270
|
+
}
|
|
3271
|
+
function isInteractiveInit() {
|
|
3272
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
3273
|
+
}
|
|
3274
|
+
export {
|
|
3275
|
+
install_v2_default as default,
|
|
3276
|
+
installCommand,
|
|
3277
|
+
runInitCommand,
|
|
3278
|
+
shouldUseInstallRenderer
|
|
3279
|
+
};
|