@fenglimg/fabric-cli 2.2.0-rc.4 → 2.2.0-rc.8

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