@fenglimg/fabric-cli 2.2.0-rc.1 → 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.
Files changed (83) hide show
  1. package/README.md +8 -5
  2. package/dist/chunk-27HK6H5Y.js +69 -0
  3. package/dist/{chunk-AOE6AYI7.js → chunk-2KBCTMID.js} +31 -8
  4. package/dist/chunk-3D7B2UAZ.js +149 -0
  5. package/dist/{chunk-XC5RUHLK.js → chunk-3IOLS5EK.js} +23 -38
  6. package/dist/{plan-context-hint-FC6P3WFE.js → chunk-722JU5BP.js} +52 -12
  7. package/dist/{chunk-2R55HNVD.js → chunk-7ZDXBOOU.js} +234 -206
  8. package/dist/{doctor-YONYXDX6.js → chunk-E7HJUU34.js} +215 -52
  9. package/dist/chunk-EOT63RDH.js +36 -0
  10. package/dist/chunk-FNHDQTPC.js +16 -0
  11. package/dist/{chunk-2CY4BMTH.js → chunk-HORSMSZL.js} +9 -5
  12. package/dist/{chunk-BO4XIZWZ.js → chunk-NLNH64A3.js} +5 -18
  13. package/dist/{chunk-WU6GAPKH.js → chunk-PTGQAZEW.js} +12 -4
  14. package/dist/chunk-QFIVFZRH.js +13 -0
  15. package/dist/chunk-QPAW6IYT.js +387 -0
  16. package/dist/{chunk-COI5VDFU.js → chunk-WA3DYGSY.js} +1 -2
  17. package/dist/{config-XYRBZJDU.js → config-A3LTECAY.js} +4 -3
  18. package/dist/context-UJCGYOT6.js +117 -0
  19. package/dist/doctor-MDTZWKBK.js +24 -0
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +133 -22
  22. package/dist/info-7FKBTMVO.js +139 -0
  23. package/dist/install-v2-RINEA24K.js +3279 -0
  24. package/dist/{metrics-RER6NLFC.js → metrics-HMFH4YHK.js} +1 -1
  25. package/dist/{onboard-coverage-JWQWDZW7.js → onboard-coverage-XSG77LL3.js} +48 -27
  26. package/dist/plan-context-hint-5TNGH3R4.js +12 -0
  27. package/dist/{scope-explain-CDIZESP5.js → scope-explain-HLJZ2M33.js} +17 -6
  28. package/dist/status-4R3TM4FJ.js +37 -0
  29. package/dist/store-HOCORVL3.js +563 -0
  30. package/dist/{sync-UJ4BBCZJ.js → sync-DT5UJMMR.js} +197 -30
  31. package/dist/{uninstall-C3QXKOO6.js → uninstall-IFN2KYBK.js} +97 -140
  32. package/dist/whoami-ITGEFWH4.js +49 -0
  33. package/package.json +7 -5
  34. package/templates/hooks/cite-policy-evict.cjs +412 -160
  35. package/templates/hooks/configs/README.md +14 -27
  36. package/templates/hooks/configs/claude-code.json +17 -2
  37. package/templates/hooks/configs/codex-hooks.json +15 -3
  38. package/templates/hooks/fabric-hint.cjs +477 -176
  39. package/templates/hooks/knowledge-hint-broad.cjs +577 -274
  40. package/templates/hooks/knowledge-hint-narrow.cjs +113 -73
  41. package/templates/hooks/lib/banner-i18n.cjs +31 -0
  42. package/templates/hooks/lib/bindings-snapshot-reader.cjs +118 -7
  43. package/templates/hooks/lib/cite-line-parser.cjs +12 -20
  44. package/templates/hooks/lib/client-adapter.cjs +66 -7
  45. package/templates/hooks/lib/nudge-policy.cjs +117 -0
  46. package/templates/hooks/lib/state-store.cjs +60 -0
  47. package/templates/hooks/post-tooluse-mutation.cjs +386 -0
  48. package/templates/hooks/session-end-marker.cjs +140 -0
  49. package/templates/skills/fabric/SKILL.md +100 -0
  50. package/templates/skills/fabric-archive/SKILL.md +35 -24
  51. package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
  52. package/templates/skills/fabric-archive/ref/i18n-policy.md +2 -3
  53. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +2 -3
  54. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +1 -1
  55. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
  56. package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +18 -0
  57. package/templates/skills/fabric-archive/ref/phase-3-7-semantic-scope.md +47 -0
  58. package/templates/skills/fabric-audit/SKILL.md +13 -3
  59. package/templates/skills/fabric-connect/SKILL.md +3 -3
  60. package/templates/skills/fabric-import/SKILL.md +7 -7
  61. package/templates/skills/fabric-import/ref/i18n-policy.md +2 -3
  62. package/templates/skills/fabric-import/ref/state-recovery.md +1 -2
  63. package/templates/skills/fabric-review/SKILL.md +14 -5
  64. package/templates/skills/fabric-review/ref/cite-contract.md +1 -1
  65. package/templates/skills/fabric-review/ref/i18n-policy.md +2 -3
  66. package/templates/skills/fabric-review/ref/output-contract.md +1 -1
  67. package/templates/skills/fabric-review/ref/per-mode-flows.md +2 -2
  68. package/templates/skills/fabric-review/ref/worked-examples.md +1 -1
  69. package/templates/skills/fabric-store/SKILL.md +1 -1
  70. package/templates/skills/fabric-sync/SKILL.md +1 -1
  71. package/templates/skills/lib/shared-policy.md +2 -2
  72. package/dist/chunk-4R2CYEA4.js +0 -116
  73. package/dist/chunk-L4Q55UC4.js +0 -52
  74. package/dist/chunk-LFIKMVY7.js +0 -27
  75. package/dist/chunk-RYAFBNES.js +0 -33
  76. package/dist/chunk-T5RPGCCM.js +0 -40
  77. package/dist/install-74ANPCCP.js +0 -2737
  78. package/dist/status-GLQWLWH6.js +0 -23
  79. package/dist/store-XB3ADT65.js +0 -144
  80. package/dist/whoami-2MLO4Y37.js +0 -36
  81. package/templates/hooks/configs/cursor-hooks.json +0 -18
  82. package/templates/hooks/lib/cite-contract-reminder.cjs +0 -179
  83. 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
+ };