@contentful/experience-design-system-cli 2.14.2 → 2.15.0

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 (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. package/prompts/composition-parser.md +42 -0
package/README.md CHANGED
@@ -34,6 +34,46 @@ All intermediate data flows through a local SQLite session database (`~/.content
34
34
 
35
35
  ---
36
36
 
37
+ ## Composite components & composition
38
+
39
+ A **composite component** is one that renders other components inside it — a `Card` that slots a `Button` and an `Icon`, a `Tabs` that slots `Tab` panels. When you import composite components, the CLI can populate each slot's `$allowedComponents` so the parent→child relationships survive into Contentful.
40
+
41
+ ### Atomic vs. composite
42
+
43
+ Imports are **atomic by default** — flat components, no embedded hierarchy. This is the right choice when you just want each component registered on its own. Opt into hierarchy resolution with `--composite` (or any composition flag, which implies it).
44
+
45
+ | Mode | Flag | Behavior |
46
+ |---|---|---|
47
+ | Atomic | `--atomic` (default) | Flat import; composition stripped before push |
48
+ | Composite | `--composite` | Resolve and import the parent→child hierarchy |
49
+
50
+ ### How composition is resolved
51
+
52
+ Under `--composite`, relationships are resolved from the highest-confidence source available, in this precedence order:
53
+
54
+ 1. **Typed slots (code)** — slots the source already declares, e.g. React `ReactElement<XProps>` / `children`, Svelte `Snippet<[XProps]>`, or an explicit `@allowedComponents` JSDoc tag. Fully deterministic; picked up automatically.
55
+ 2. **Mapping map** — a hand-authored parent→children interchange map you feed with `--composition-map <path>`. Use `--generate-map <path>` to emit a skeleton from whatever was resolved, then hand-edit it and feed it back.
56
+ 3. **Agent (`--composition-agent`)** — for codebases that encode composition in *code patterns* rather than typed slots (common in real-world design systems). Only runs when the deterministic sources above find nothing.
57
+
58
+ When more than one source speaks to the same relationship, the higher-precedence one wins (**code slots > map > agent**).
59
+
60
+ ### The composition agent
61
+
62
+ `--composition-agent` doesn't ask the model to *list* relationships (which would be non-deterministic and unauditable). Instead the agent **writes a small parser** — a pure `(ctx) => Edge[]` function — which the CLI runs in a locked-down sandbox (separate process, no filesystem/network, memory + wall-clock limits), caches by the parser's own source, and replays. Repeated runs over the same code are stable and inspectable.
63
+
64
+ - `--composition-agent-mode <parser|edges>` — `parser` (default; the sandboxed-parser design above) or `edges` (agent lists relationships directly; less robust).
65
+ - `--composition-refresh` — ignore the cache and re-resolve from scratch, forcing the agent to run.
66
+ - `--agent <name>` — which coding agent authors the parser (`claude`, `codex`, `opencode`, `cursor`).
67
+ - `--prompt composition=<file-or-text>` — override the composition stage's prompt.
68
+
69
+ Because the agent path spawns a coding agent, it adds latency and cost and is best-effort (parser quality can vary run to run). For reproducible results, prefer `--composition-map`.
70
+
71
+ ### Slot cycles
72
+
73
+ If the resolved graph contains a circular slot dependency (A slots B, B slots A), it is detected before push. `apply push` and the wizard's push path **refuse to send a manifest with cycles**; the cycle path is reported so you can break it. In headless `import`, pass `--auto-reject-cycles` to auto-reject the components in a cycle and retry instead of failing.
74
+
75
+ ---
76
+
37
77
  ## Prerequisites
38
78
 
39
79
  ### Coding agent
@@ -80,7 +120,8 @@ experiences import [flags]
80
120
  ```
81
121
  welcome
82
122
 
83
- extracting — runs analyze extract; spawns generate in parallel (prefetch)
123
+ extracting — runs analyze extract (atomic by default; resolves composition
124
+ under --composite, see below); spawns generate in parallel (prefetch)
84
125
 
85
126
  [auto-filter] — analyze select-agent runs automatically (skip with --no-auto-filter)
86
127
 
@@ -139,6 +180,15 @@ Pass `--select-prompt-path <path>` and/or `--generate-prompt-path <path>` to swa
139
180
  | `--model <name>` | agent default | Model name |
140
181
  | `--tokens <path>` | — | DTCG `tokens.json` to push alongside generated components |
141
182
  | `--auto-accept-scope` | off | Accept all extracted components without prompting (required for non-TTY without other headless flags) |
183
+ | `--atomic` | **default** | Flat import, no embedded-component hierarchy (composition stripped on push) |
184
+ | `--composite` | — | Import the embedded-component hierarchy (any composition flag implies this) |
185
+ | `--composition-map <path>` | — | Consume a hand-authored parent→children interchange map (implies `--composite`) |
186
+ | `--generate-map <path>` | — | Also write a composition-map skeleton from the resolved composition (implies `--composite`) |
187
+ | `--composition-agent` | — | Opt into agentic resolution when deterministic sources find no groups (implies `--composite`) |
188
+ | `--composition-refresh` | — | Bypass the composition cache and re-resolve from scratch, forcing the agent to run (implies `--composite`) |
189
+ | `--composition-agent-mode <mode>` | `parser` | `parser` (agent writes a sandboxed parser) or `edges` (agent lists edges directly) |
190
+ | `--prompt <stage=value>` | — | Override a stage prompt (repeatable); value is a file path or literal text, e.g. `--prompt composition=./p.md` |
191
+ | `--auto-reject-cycles` | off (fail loud) | Auto-reject components in slot cycles and retry, instead of stopping with the cycle path |
142
192
  | `--auto-filter` / `--no-auto-filter` | persisted in `credentials.json` | Force AI auto-filter on or off; overrides saved preference |
143
193
  | `--no-live-preview` | live preview on | Skip the automatic preview re-run after each FieldEditor save |
144
194
  | `--no-push` | push on | Run extract → scope-gate → generate → final-review and exit without pushing |
@@ -224,13 +274,23 @@ The standalone subcommands below are pinned by snapshot test (`test/analyze/sele
224
274
  Extract component definitions from a project source tree.
225
275
 
226
276
  ```bash
227
- experiences analyze extract --project <path> [--dir <src-dir>]
277
+ experiences analyze extract --project <path> [--dir <src-dir>] [composition flags]
228
278
  ```
229
279
 
230
280
  | Option | Default | Description |
231
281
  |---|---|---|
232
282
  | `--project <path>` | _(required)_ | Path to the project root |
233
283
  | `--dir <path>` | `src` (falls back to project root) | Source directory relative to project root |
284
+ | `--resolve-unreachable <mode>` | `auto` | Retry pass for unresolved Svelte `Props` types: `auto`, `always`, or `never` |
285
+ | `--atomic` | **default** | Skip composition resolution — flat components only |
286
+ | `--composite` | — | Resolve embedded-component composition (any composition flag implies this) |
287
+ | `--composition-map <path>` | — | Consume a hand-authored parent→children interchange map (implies `--composite`) |
288
+ | `--generate-map <path>` | — | Write a skeleton interchange map from the resolved composition (implies `--composite`) |
289
+ | `--composition-agent` | — | Opt into agentic resolution when deterministic sources find no groups (implies `--composite`) |
290
+ | `--composition-refresh` | — | Bypass the composition cache and re-resolve from scratch, forcing the agent to run (implies `--composite`) |
291
+ | `--composition-agent-mode <mode>` | `parser` | `parser` (agent writes a sandboxed parser — deterministic) or `edges` (agent lists edges directly) |
292
+ | `--agent <name>` | saved by setup | Coding agent for composition resolution: `claude`, `codex`, `opencode`, `cursor` |
293
+ | `--prompt <stage=value>` | — | Override a stage prompt (repeatable); value is a file path or literal text, e.g. `--prompt composition=./p.md` |
234
294
 
235
295
  Scans `.tsx`, `.ts`, `.jsx`, `.js`, `.vue`, and `.astro` files. Ignores `node_modules`, `dist`, `build`, `.next`, `.nuxt`, `coverage`, `storybook-static`, `out`, `demo(s)`, and `example(s)` directories. Also ignores `*.stories.*`, `*.story.*`, `*.spec.*`, and `*.test.*` files.
236
296
 
@@ -238,6 +298,8 @@ Writes extracted components to the session database and prints `session=<id>` to
238
298
 
239
299
  The deterministic non-authorable filter drops infrastructure components with no authoring surface (Context providers, refs-only wrappers, etc.); each drop is reported as a warning so the operator can audit.
240
300
 
301
+ Extraction is **atomic by default** — flat components, no embedded hierarchy. See [Composite components & composition](#composite-components--composition) for how `--composite` and the composition flags resolve parent→child relationships.
302
+
241
303
  ---
242
304
 
243
305
  ### `analyze select`
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.14.2",
3
+ "version": "2.15.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "files": [
26
26
  "bin/",
27
27
  "dist/",
28
- "skills/"
28
+ "skills/",
29
+ "prompts/"
29
30
  ],
30
31
  "scripts": {
31
32
  "build": "nx build experience-design-system-cli && BINDIR=$(dirname $(which node)) && for cmd in exo experiences experience-design-system-cli; do ln -sf \"$(pwd)/bin/cli.js\" \"$BINDIR/$cmd\"; done",
@@ -1,3 +1,14 @@
1
1
  import type { Command } from 'commander';
2
+ import type { InterchangeMap } from './composition/interchange-schema.js';
3
+ import type { RawSlotDefinition } from '../types.js';
2
4
  export declare function collectSourceFiles(directory: string, onProgress?: (scannedCount: number) => void): Promise<string[]>;
5
+ /**
6
+ * Build a { version, groups } interchange skeleton (spec T1) from the resolved
7
+ * components' slot allowedComponents — reflecting BOTH typed-slot edges the
8
+ * extractor found and anything the mapping resolver added.
9
+ */
10
+ export declare function componentsToInterchangeMap(components: Array<{
11
+ name: string;
12
+ slots: RawSlotDefinition[];
13
+ }>): InterchangeMap;
3
14
  export declare function registerAnalyzeCommand(program: Command): void;
@@ -1,12 +1,25 @@
1
1
  import { createElement } from 'react';
2
2
  import { render } from 'ink';
3
- import { mkdir, readdir, stat } from 'node:fs/promises';
3
+ import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
4
4
  import { isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { extractComponents, preClassifyComponent, isNonAuthorableComponent, computeExtractionScore, deriveNeedsReview, describeReviewReasons, inspectComponentSource, validateExtractedComponents, } from '@contentful/experience-design-system-extraction';
6
6
  import { AnalyzeView } from './tui/AnalyzeView.js';
7
7
  import { registerAnalyzeEditCommand } from './select/command.js';
8
8
  import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
9
- import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, } from '../session/db.js';
9
+ import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, storeSlotCycles, getCliCacheVersion, lookupCompositionCache, storeCompositionCache, } from '../session/db.js';
10
+ import { findSlotCycles, suggestCycleBreakEdge } from './cycle-detection.js';
11
+ import { resolveCompositionMode } from '../lib/composition-mode.js';
12
+ import { resolveMapping } from './composition/resolve-mapping.js';
13
+ import { loadUserMap, resolveCompositionSources } from './composition/resolve-mapping-cli.js';
14
+ import { selectCandidateFiles, capCandidatesToPromptBudget } from './composition/candidate-files.js';
15
+ import { critiqueCandidates } from './composition/candidate-critic.js';
16
+ import { buildDirCriticPrompt, parseDirCriticReply } from './composition/candidate-critic-agent.js';
17
+ import { buildCompositionInputHash } from './composition/composition-cache-key.js';
18
+ import { runParserInSandbox } from './composition/agent-parser/sandbox.js';
19
+ import { resolveViaAgentParser } from './composition/agent-parser/resolve-via-parser.js';
20
+ import { parsePromptOverrides, resolvePromptOverride } from '../lib/prompt-overrides.js';
21
+ import { runAgent } from '../generate/agent-runner.js';
22
+ import { readExperiencesCredentials } from '../credentials-store.js';
10
23
  import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
11
24
  const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
12
25
  const IGNORED_DIRECTORY_NAMES = new Set([
@@ -85,6 +98,69 @@ export async function collectSourceFiles(directory, onProgress) {
85
98
  await visit(directory);
86
99
  return files.sort();
87
100
  }
101
+ /** Read the persisted default composition mode; missing config is fine. */
102
+ async function safeReadCompositionMode() {
103
+ try {
104
+ return (await readExperiencesCredentials()).compositionMode;
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ /**
111
+ * Build a { version, groups } interchange skeleton (spec T1) from the resolved
112
+ * components' slot allowedComponents — reflecting BOTH typed-slot edges the
113
+ * extractor found and anything the mapping resolver added.
114
+ */
115
+ export function componentsToInterchangeMap(components) {
116
+ const groups = {};
117
+ for (const c of components) {
118
+ const children = new Set();
119
+ for (const slot of c.slots) {
120
+ for (const child of slot.allowedComponents ?? [])
121
+ children.add(child);
122
+ }
123
+ if (children.size > 0)
124
+ groups[c.name] = [...children].sort();
125
+ }
126
+ const sorted = {};
127
+ for (const parent of Object.keys(groups).sort())
128
+ sorted[parent] = groups[parent];
129
+ return { version: 1, groups: sorted };
130
+ }
131
+ /** Resolve which coding-agent runs mapping resolution: `--agent` flag > env > default. */
132
+ function resolveCompositionAgentName(flagValue) {
133
+ const valid = ['claude', 'codex', 'opencode', 'cursor'];
134
+ if (flagValue && valid.includes(flagValue))
135
+ return flagValue;
136
+ const env = process.env['EDS_COMPOSITION_AGENT'];
137
+ if (env && valid.includes(env))
138
+ return env;
139
+ return 'claude';
140
+ }
141
+ /**
142
+ * Read the source files of the extracted components plus nearby mapping/meta
143
+ * files, so the candidate pre-filter (T3) can pick the relevant ones. Reads
144
+ * each unique `sourcePath` once; missing files are skipped.
145
+ */
146
+ async function readCandidateFiles(components, extraFiles = []) {
147
+ const paths = new Set(extraFiles);
148
+ for (const c of components) {
149
+ if (c.sourcePath)
150
+ paths.add(c.sourcePath);
151
+ }
152
+ const out = [];
153
+ await Promise.all([...paths].map(async (p) => {
154
+ try {
155
+ const content = await readFile(p, 'utf8');
156
+ out.push({ path: p, content });
157
+ }
158
+ catch {
159
+ void 0;
160
+ }
161
+ }));
162
+ return out;
163
+ }
88
164
  export function registerAnalyzeCommand(program) {
89
165
  const analyze = program
90
166
  .command('analyze')
@@ -95,6 +171,15 @@ export function registerAnalyzeCommand(program) {
95
171
  .requiredOption('--project <path>', 'Path to the project root')
96
172
  .option('--dir <path>', 'Path to the component source directory relative to the project root')
97
173
  .option('--resolve-unreachable <mode>', "Retry pass for unresolved Svelte Props types: 'auto' (default), 'always', or 'never'", 'auto')
174
+ .option('--composite', 'Resolve embedded-component composition (opt in; default is atomic)')
175
+ .option('--atomic', 'Skip composition resolution — flat components only (default)')
176
+ .option('--composition-map <path>', 'Consume a hand-authored parent→children interchange map (implies --composite)')
177
+ .option('--composition-agent', 'Opt into agentic mapping resolution when deterministic sources find no groups (implies --composite)')
178
+ .option('--composition-refresh', 'Force the mapping agent to run even where deterministic sources answered (implies --composite)')
179
+ .option('--generate-map <path>', 'Write a skeleton interchange map from resolved composition (implies --composite)')
180
+ .option('--prompt <stage=value>', 'Override a stage prompt (repeatable). value is a file path or literal text, e.g. --prompt composition=./p.md', (v, acc) => [...acc, v], [])
181
+ .option('--agent <name>', 'Coding agent for composition mapping resolution (claude|codex|opencode|cursor)')
182
+ .option('--composition-agent-mode <mode>', "Agent resolution mode: 'parser' (agent writes a sandboxed parser — deterministic, default) or 'edges' (agent lists edges directly)", 'parser')
98
183
  .action(async (opts) => {
99
184
  const resolveUnreachable = (() => {
100
185
  const v = opts.resolveUnreachable ?? 'auto';
@@ -184,8 +269,218 @@ export function registerAnalyzeCommand(program) {
184
269
  (component.needsReview ?? false),
185
270
  });
186
271
  }
187
- const validatedComponents = validateExtractedComponents(filteredComponents);
272
+ let validatedComponents = validateExtractedComponents(filteredComponents);
273
+ // Composition mapping resolution (spec U2). Only in composite mode and
274
+ // only when a source is provided (user map / agent opt-in).
275
+ // Atomic (default) never resolves — it would only be stripped later.
276
+ const compositionMode = resolveCompositionMode(opts, (await safeReadCompositionMode()) ?? undefined);
277
+ if (compositionMode === 'composite') {
278
+ const sources = resolveCompositionSources(opts);
279
+ const { overrides: promptOverrides, errors: promptErrors } = parsePromptOverrides(opts.prompt ?? []);
280
+ for (const err of promptErrors) {
281
+ process.stderr.write(`Error: ${err}\n`);
282
+ process.exit(1);
283
+ }
284
+ let compositionPrompt;
285
+ const compositionOverride = promptOverrides.get('composition');
286
+ if (compositionOverride) {
287
+ try {
288
+ compositionPrompt = await resolvePromptOverride(compositionOverride);
289
+ }
290
+ catch (e) {
291
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
292
+ process.exit(1);
293
+ }
294
+ }
295
+ let userMap;
296
+ if (opts.compositionMap) {
297
+ const loaded = await loadUserMap(opts.compositionMap);
298
+ if (!loaded.ok) {
299
+ process.stderr.write(`Error: ${loaded.error}\n`);
300
+ process.exit(1);
301
+ }
302
+ userMap = loaded.map;
303
+ }
304
+ const hasSource = !!userMap || sources.useAgent || sources.forceAgent;
305
+ if (hasSource || opts.generateMap) {
306
+ // Composition progress mirrors the scan/extract progress convention:
307
+ // emit `progress=composition:<phase>` on stderr so the wizard can
308
+ // render a second progress line during the (potentially slow, agent-
309
+ // backed) resolution instead of appearing frozen.
310
+ const emitCompositionProgress = (phase) => {
311
+ if (!process.stdout.isTTY)
312
+ process.stderr.write(`progress=composition:${phase}\n`);
313
+ };
314
+ emitCompositionProgress('resolving');
315
+ const allFiles = await readCandidateFiles(validatedComponents, sourceFiles);
316
+ // Decouple the two file sets (design: candidate-heuristic fragility):
317
+ // - `promptFiles`: a bounded candidate SAMPLE inlined into the agent
318
+ // prompt so it sees the convention without ingesting the whole repo.
319
+ // - `allFiles`: EVERY scanned file, handed to the authored parser at
320
+ // runtime. The parser is deterministic code — give it everything so
321
+ // a candidate-filter miss can never starve it of a definition file.
322
+ const selectedCandidates = selectCandidateFiles(allFiles).map((c) => ({ path: c.path, content: c.content }));
323
+ // Cap the inlined set so a large design system can't overflow the
324
+ // agent's context window and fail resolution outright (see the budget
325
+ // constant). The runtime parser still sees EVERY file.
326
+ const capped = capCandidatesToPromptBudget(selectedCandidates);
327
+ let promptFiles = capped.kept;
328
+ if (capped.dropped.length > 0) {
329
+ process.stderr.write(`Warning: composition — ${capped.dropped.length} candidate file(s) omitted from the agent prompt to fit the context budget; resolution runs on the ${promptFiles.length} highest-value files.\n`);
330
+ }
331
+ const runtimeFiles = allFiles.map((c) => ({ path: c.path, content: c.content }));
332
+ const resolverAgent = resolveCompositionAgentName(opts.agent);
333
+ const parserMode = (opts.compositionAgentMode ?? 'parser') !== 'edges';
334
+ const componentNameSet = new Set(validatedComponents.map((c) => c.name));
335
+ const cacheVersion = await getCliCacheVersion();
336
+ // Tracks the exit code of the most recent spawnAgent call so the
337
+ // caching sites can refuse to persist a failed run (a non-zero exit —
338
+ // e.g. a context-window overflow — otherwise poisons the cache and
339
+ // replays the error on every subsequent run).
340
+ let lastAgentExitCode = 0;
341
+ const spawnAgent = async (prompt) => {
342
+ const res = await runAgent({
343
+ agent: resolverAgent,
344
+ prompt,
345
+ interactive: false,
346
+ timeoutMs: 120_000,
347
+ promptViaStdin: true,
348
+ });
349
+ lastAgentExitCode = res.exitCode;
350
+ if (res.exitCode !== 0 && res.stderr.trim()) {
351
+ process.stderr.write(`Warning: composition — agent exited ${res.exitCode}: ${res.stderr.trim()}\n`);
352
+ }
353
+ return res.stdout;
354
+ };
355
+ // Completeness critic: let the agent flag composition-relevant dirs
356
+ // the keyword filter missed (by path/name alone — cheap). It can only
357
+ // ADD to the prompt sample. Runs ONLY on a genuine authoring pass (see
358
+ // the cache-miss branch below) so a parser cache hit pays nothing.
359
+ const runCandidateCritic = async () => {
360
+ const critic = await critiqueCandidates(runtimeFiles, promptFiles, async (dirs) => parseDirCriticReply(await spawnAgent(buildDirCriticPrompt(dirs)), dirs));
361
+ if (critic.addedDirs.length > 0) {
362
+ promptFiles = critic.files;
363
+ if (process.env['EDS_DEBUG']) {
364
+ process.stderr.write(`[composition-debug] critic added dirs: ${critic.addedDirs.join(', ')}\n`);
365
+ }
366
+ }
367
+ };
368
+ // Agent-authored parser path (default): the agent writes a sandboxed
369
+ // (ctx) => Edge[] parser we run deterministically, cached by parser
370
+ // SOURCE. Falls back to direct edge-emission if authoring fails.
371
+ let parserEdges;
372
+ if (sources.useAgent && parserMode) {
373
+ const parserCacheKey = buildCompositionInputHash({
374
+ files: runtimeFiles,
375
+ agent: resolverAgent,
376
+ kind: 'parser',
377
+ });
378
+ const cachedSource = opts.compositionRefresh
379
+ ? null
380
+ : lookupCompositionCache(db, parserCacheKey, cacheVersion);
381
+ if (cachedSource !== null) {
382
+ emitCompositionProgress('cache-hit');
383
+ const ran = await runParserInSandbox(cachedSource, {
384
+ files: runtimeFiles,
385
+ componentNames: [...componentNameSet],
386
+ });
387
+ if (!ran.error) {
388
+ parserEdges = ran.edges.filter((e) => e.parent !== e.child && componentNameSet.has(e.parent) && componentNameSet.has(e.child));
389
+ }
390
+ }
391
+ if (parserEdges === undefined) {
392
+ await runCandidateCritic();
393
+ const pr = await resolveViaAgentParser({
394
+ files: promptFiles,
395
+ runtimeFiles,
396
+ componentNames: componentNameSet,
397
+ runAgentFn: ({ prompt }) => spawnAgent(prompt),
398
+ ...(compositionPrompt ? { instructionOverride: compositionPrompt } : {}),
399
+ onPhase: (phase) => emitCompositionProgress(phase),
400
+ // The candidate filter selected these files because they carry
401
+ // composition markers, so a clean 0-edge parse is suspicious —
402
+ // let the resolver spend its one repair round on it.
403
+ retryOnEmpty: promptFiles.length > 0,
404
+ });
405
+ for (const w of pr.warnings)
406
+ process.stderr.write(`Warning: composition — ${w}\n`);
407
+ if (!pr.usedFallback) {
408
+ parserEdges = pr.edges;
409
+ if (pr.parserSource && lastAgentExitCode === 0) {
410
+ storeCompositionCache(db, parserCacheKey, cacheVersion, pr.parserSource);
411
+ }
412
+ }
413
+ else {
414
+ process.stderr.write('Warning: composition — parser mode failed; falling back to edge emission\n');
415
+ }
416
+ }
417
+ }
418
+ if (process.env['EDS_DEBUG'] && parserEdges) {
419
+ process.stderr.write(`[composition-debug] prompt files: ${promptFiles.length}; runtime files: ${runtimeFiles.length}; componentNames: ${componentNameSet.size}; parser edges: ${parserEdges.length}\n`);
420
+ for (const e of parserEdges)
421
+ process.stderr.write(`[composition-debug] edge ${e.parent} -> ${e.child}\n`);
422
+ }
423
+ // Edge-emission cache (used for both explicit edges-mode and the
424
+ // parser-mode fallback). Keyed on prompt files + agent identity — the
425
+ // agent emits edges directly from what it reads in the prompt.
426
+ const agentCacheKey = buildCompositionInputHash({
427
+ files: promptFiles,
428
+ agent: resolverAgent,
429
+ kind: 'edges',
430
+ });
431
+ const useEdgeEmission = sources.useAgent && parserEdges === undefined;
432
+ const result = await resolveMapping({
433
+ components: validatedComponents,
434
+ ...(userMap ? { userMap } : {}),
435
+ ...(parserEdges ? { extraEdges: parserEdges } : {}),
436
+ useAgent: useEdgeEmission,
437
+ forceAgent: sources.forceAgent && useEdgeEmission,
438
+ files: promptFiles,
439
+ ...(compositionPrompt ? { promptOverride: compositionPrompt } : {}),
440
+ runAgentFn: async ({ prompt }) => {
441
+ if (!opts.compositionRefresh) {
442
+ const cached = lookupCompositionCache(db, agentCacheKey, cacheVersion);
443
+ if (cached !== null) {
444
+ emitCompositionProgress('cache-hit');
445
+ return cached;
446
+ }
447
+ }
448
+ emitCompositionProgress(`agent:${resolverAgent}`);
449
+ const stdout = await spawnAgent(prompt);
450
+ if (lastAgentExitCode === 0) {
451
+ storeCompositionCache(db, agentCacheKey, cacheVersion, stdout);
452
+ }
453
+ return stdout;
454
+ },
455
+ });
456
+ emitCompositionProgress('done');
457
+ for (const w of result.warnings)
458
+ process.stderr.write(`Warning: composition — ${w}\n`);
459
+ for (const c of result.conflicts) {
460
+ process.stderr.write(`Warning: composition conflict on ${c.parent}→${c.child}: kept ${c.winner}, dropped ${c.loser}\n`);
461
+ }
462
+ validatedComponents = result.components;
463
+ if (opts.generateMap) {
464
+ // Reflect the FULL resolved composition — typed-slot edges already
465
+ // on the extracted components PLUS anything the resolver added — not
466
+ // just the resolver's own contributed edges.
467
+ const skeleton = componentsToInterchangeMap(validatedComponents);
468
+ await writeFile(opts.generateMap, JSON.stringify(skeleton, null, 2) + '\n');
469
+ process.stderr.write(`Wrote composition map skeleton to ${opts.generateMap}\n`);
470
+ }
471
+ }
472
+ }
188
473
  storeRawComponents(db, sessionId, validatedComponents);
474
+ const cycleInput = validatedComponents.map((c) => ({
475
+ name: c.name,
476
+ slots: c.slots.map((s) => ({ name: s.name, allowedComponents: s.allowedComponents })),
477
+ }));
478
+ const cycles = findSlotCycles(cycleInput);
479
+ const withBreaks = cycles.map((cycle) => ({
480
+ ...cycle,
481
+ suggestedBreak: suggestCycleBreakEdge(cycle, cycles),
482
+ }));
483
+ storeSlotCycles(db, sessionId, withBreaks);
189
484
  storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
190
485
  updateStep(db, stepId, 'complete', { sessionId });
191
486
  db.close();
@@ -0,0 +1,25 @@
1
+ export interface ComponentGraphNode {
2
+ name: string;
3
+ slots: Array<{
4
+ name: string;
5
+ allowedComponents?: string[];
6
+ }>;
7
+ }
8
+ export interface ClosureNode {
9
+ name: string;
10
+ depth: number;
11
+ path: string[];
12
+ parents: string[];
13
+ }
14
+ export interface Closure {
15
+ root: string;
16
+ nodes: ClosureNode[];
17
+ containsCycle: boolean;
18
+ cyclePath?: string[];
19
+ }
20
+ export type NodeStatus = 'ok' | 'warning' | 'error';
21
+ export declare function findRoots(components: ComponentGraphNode[], selected?: Set<string>): string[];
22
+ export declare function computeClosure(root: string, components: ComponentGraphNode[]): Closure;
23
+ export declare function computeAllClosures(components: ComponentGraphNode[], selected?: Set<string>): Map<string, Closure>;
24
+ export declare function aggregateStatus(closure: Closure, perNodeStatus: Map<string, NodeStatus>): NodeStatus;
25
+ export declare function findSharedDeps(closures: Map<string, Closure>): Map<string, string[]>;
@@ -0,0 +1,4 @@
1
+ export declare function buildAuthorPrompt(files: Array<{
2
+ path: string;
3
+ content: string;
4
+ }>, componentNames: string[], instructionOverride?: string): string;
@@ -0,0 +1,22 @@
1
+ import { loadPrompt } from './load-prompt.js';
2
+ /**
3
+ * Build the prompt that asks the agent to WRITE a pure parser function (spec:
4
+ * agent-authored-parser, Phase 2) rather than list edges. The static
5
+ * instruction + contract live in `prompts/composition-parser.md`; this
6
+ * appends the dynamic candidate files + component names. The authored parser
7
+ * runs in the sandbox (sandbox.ts), so the contract forbids all I/O.
8
+ */
9
+ const PROMPT_FILE = 'composition-parser.md';
10
+ export function buildAuthorPrompt(files, componentNames, instructionOverride) {
11
+ const fileBlocks = files.map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');
12
+ const instruction = instructionOverride?.trim() ? instructionOverride.trim() : loadPrompt(PROMPT_FILE).trim();
13
+ return [
14
+ instruction,
15
+ '',
16
+ 'Component names (use ONLY these):',
17
+ componentNames.join(', '),
18
+ '',
19
+ 'Candidate files:',
20
+ fileBlocks,
21
+ ].join('\n');
22
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Cheap structural check that source matches the parser contract:
3
+ * `export default` of a function (declaration or arrow) that takes at least
4
+ * one parameter. Not a full parse — just enough to reject JSON samples, prose,
5
+ * and no-arg functions before we spend a sandbox spawn on them.
6
+ */
7
+ export declare function looksLikeParser(source: string): boolean;
8
+ export declare function extractParserSource(stdout: string): string | null;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pull the parser function source out of the agent's stdout (spec:
3
+ * agent-authored-parser, Phase 2). Agents typically wrap code in a fenced
4
+ * block, but often emit a non-parser block first (a JSON sample, prose). We
5
+ * return the first block that is actually SHAPED like the contract — a
6
+ * default-exported function taking one param — so a stray sample never reaches
7
+ * the sandbox. Lenient by design: no parser-shaped source → null → caller
8
+ * falls back.
9
+ */
10
+ const FENCE = /```(?:[a-zA-Z]+)?\n([\s\S]*?)```/g;
11
+ /**
12
+ * Cheap structural check that source matches the parser contract:
13
+ * `export default` of a function (declaration or arrow) that takes at least
14
+ * one parameter. Not a full parse — just enough to reject JSON samples, prose,
15
+ * and no-arg functions before we spend a sandbox spawn on them.
16
+ */
17
+ export function looksLikeParser(source) {
18
+ const s = source.trim();
19
+ if (!/export\s+default\b/.test(s))
20
+ return false;
21
+ // export default function [name] (param...) { ... }
22
+ const fnDecl = /export\s+default\s+(?:async\s+)?function\b[^(]*\(\s*[A-Za-z_$][\w$]*/;
23
+ // export default (param...) => ... OR export default param => ...
24
+ const arrow = /export\s+default\s+(?:async\s+)?(?:\(\s*[A-Za-z_$][\w$]*|[A-Za-z_$][\w$]*\s*=>)/;
25
+ return fnDecl.test(s) || arrow.test(s);
26
+ }
27
+ export function extractParserSource(stdout) {
28
+ FENCE.lastIndex = 0;
29
+ let m;
30
+ while ((m = FENCE.exec(stdout)) !== null) {
31
+ const candidate = m[1].trim();
32
+ if (candidate !== '' && looksLikeParser(candidate))
33
+ return candidate;
34
+ }
35
+ // No parser-shaped fenced block — accept bare source only if it, too, is
36
+ // shaped like the contract.
37
+ const idx = stdout.indexOf('export default');
38
+ if (idx !== -1) {
39
+ const bare = stdout.slice(idx).trim();
40
+ if (looksLikeParser(bare))
41
+ return bare;
42
+ }
43
+ return null;
44
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolve a bundled prompt file from the package-root `prompts/` directory.
3
+ * Walks up from this module so it works from both `src/` and `dist/src/`,
4
+ * mirroring `resolveSkillPath` for the skills/ directory.
5
+ */
6
+ export declare function resolvePromptPath(fileName: string): string;
7
+ export declare function loadPrompt(fileName: string): string;
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * Resolve a bundled prompt file from the package-root `prompts/` directory.
6
+ * Walks up from this module so it works from both `src/` and `dist/src/`,
7
+ * mirroring `resolveSkillPath` for the skills/ directory.
8
+ */
9
+ export function resolvePromptPath(fileName) {
10
+ const thisDir = dirname(fileURLToPath(import.meta.url));
11
+ let dir = thisDir;
12
+ for (;;) {
13
+ const candidate = join(dir, 'prompts');
14
+ if (existsSync(candidate))
15
+ return join(candidate, fileName);
16
+ const parent = resolve(dir, '..');
17
+ if (parent === dir) {
18
+ throw new Error(`prompt file missing from CLI installation (could not locate prompts/ directory from: ${thisDir})`);
19
+ }
20
+ dir = parent;
21
+ }
22
+ }
23
+ export function loadPrompt(fileName) {
24
+ const path = resolvePromptPath(fileName);
25
+ try {
26
+ return readFileSync(path, 'utf8');
27
+ }
28
+ catch {
29
+ throw new Error(`prompt file missing from CLI installation — try reinstalling the CLI (looked for: ${path})`);
30
+ }
31
+ }