@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
@@ -1,45 +1,9 @@
1
- /**
2
- * Pure state-machine helpers for the wizard. Extracted from WizardApp.tsx so the
3
- * transitions can be unit-tested without rendering ink.
4
- *
5
- * Bug fix (2026-06): credentials moved BEFORE generate so:
6
- * 1. Bad creds (401/403) are caught upfront — before the operator pays the cost
7
- * of extract + LLM classification (potentially many minutes).
8
- * 2. Live preview in final-review actually fires, because the validated creds
9
- * are present in WizardApp state when GenerateReviewStep mounts.
10
- *
11
- * New order:
12
- * welcome → token-input → path-validation → extracting → scope-gate
13
- * → credentials (validates inline) → generating
14
- * → generating → final-review → push-decision-gate → previewing
15
- * → preview-gate → pushing → done
16
- *
17
- * `--no-push` short-circuits the credentials/preview/push branch — the operator
18
- * just wants extract + classify + final-review without ever touching Contentful.
19
- */
20
- /**
21
- * Transition target after the scope-gate confirms.
22
- *
23
- * - acceptedCount > 0:
24
- * - !noPush → `credentials` (gather + validate creds upfront, before LLM cost)
25
- * - noPush → `generating` (skip creds entirely; live preview no-ops gracefully)
26
- * - acceptedCount === 0 (everything rejected):
27
- * - !noPush → `credentials` (still need creds to push tokens / removals)
28
- * - noPush → `print-gate` (nothing to do; let the operator save files and exit)
29
- */
30
1
  export function nextStepAfterScopeGate(opts) {
31
2
  if (opts.acceptedCount > 0) {
32
3
  return opts.noPush ? 'generating' : 'credentials';
33
4
  }
34
5
  return opts.noPush ? 'print-gate' : 'credentials';
35
6
  }
36
- /**
37
- * Transition target after credentials are validated (or skipped via the
38
- * inline validation pings the API). Drives the post-credentials branch:
39
- * - acceptedCount > 0 → `generating` (run the generator now that creds are good)
40
- * - acceptedCount === 0 → `push-decision-gate` (no components to generate;
41
- * skip straight to push-decision and preview)
42
- */
43
7
  export function nextStepAfterCredentialsValidated(opts) {
44
8
  return opts.acceptedCount > 0 ? 'generating' : 'push-decision-gate';
45
9
  }
@@ -47,38 +11,19 @@ export function shouldSkipFinalReviewAfterCredentials(state) {
47
11
  return state.finalReviewPassed && state.generateSessionId != null;
48
12
  }
49
13
  export function resolveNoCacheForGenerate(opts) {
50
- if (opts.isFreshSession)
51
- return true;
52
14
  return opts.cliNoCache;
53
15
  }
54
- /**
55
- * Skip-credentials spec — Task 2. When the operator pressed `s` on the
56
- * credentials screen, the wizard advanced without validating creds. That
57
- * makes `previewImport` impossible (no working token) and undesirable (the
58
- * whole point is to inspect locally without touching the server). Callers
59
- * check this before issuing the API call and route to
60
- * `buildSkippedPreviewTransition()` instead.
61
- */
62
16
  export function shouldBypassPreview(state) {
63
17
  return state.credentialsSkipped === true;
64
18
  }
65
- /**
66
- * State patch applied when `shouldBypassPreview` is true: skip the preview
67
- * step entirely, surface no server preview to downstream consumers, and
68
- * hand control to the push-decision-gate (which will render with push
69
- * disabled — see Task 3).
70
- */
71
19
  export function buildSkippedPreviewTransition() {
72
20
  return { step: 'push-decision-gate', serverPreview: null };
73
21
  }
74
- /**
75
- * Skip-credentials spec — Task 4 (defensive guard). The push-decision-gate
76
- * disables "Save AND push" and "Push only" when credentialsSkipped is true,
77
- * so `runPush` should never be reached. If a state-machine regression ever
78
- * routed an operator past that guard, `runPush` checks this helper and
79
- * refuses to issue the API call — instead it routes back to the print-files
80
- * local-save path via `buildSkippedPushTransition`.
81
- */
22
+ export function resolveCycleGateAction(opts) {
23
+ if (!opts.hasCycles)
24
+ return 'proceed';
25
+ return opts.autoRejectCycles ? 'auto-reject' : 'block';
26
+ }
82
27
  export function shouldRefusePush(state) {
83
28
  return state.credentialsSkipped === true;
84
29
  }
@@ -0,0 +1,28 @@
1
+ export type CompositionMode = 'composite' | 'atomic';
2
+ /**
3
+ * Options that, when present, IMPLY composite mode — passing any of them is a
4
+ * clear opt-in, so requiring an explicit `--composite` alongside is redundant.
5
+ */
6
+ export type CompositionSourceOptions = {
7
+ compositionMap?: string;
8
+ compositionAgent?: boolean;
9
+ compositionRefresh?: boolean;
10
+ generateMap?: string;
11
+ };
12
+ /**
13
+ * Resolve the effective composition mode with precedence
14
+ * `explicit flag > implied source > env > persisted config > default`.
15
+ * Default is `atomic` (composition OFF); `--composite` opts into the hierarchy
16
+ * machinery.
17
+ *
18
+ * `--atomic` is accepted for symmetry and wins when passed — even alongside a
19
+ * composition source, since it's an explicit "no" that overrides the implicit
20
+ * opt-in. If both `--composite` and `--atomic` are present, `--composite`
21
+ * wins. Passing a composition source (`--composition-map/-agent`,
22
+ * `--composition-refresh`, or `--generate-map`) implies composite without a
23
+ * separate `--composite`.
24
+ */
25
+ export declare function resolveCompositionMode(opts: {
26
+ composite?: boolean;
27
+ atomic?: boolean;
28
+ } & CompositionSourceOptions, configMode?: CompositionMode): CompositionMode;
@@ -0,0 +1,37 @@
1
+ function hasCompositionSource(opts) {
2
+ return !!opts.compositionMap || !!opts.compositionAgent || !!opts.compositionRefresh || !!opts.generateMap;
3
+ }
4
+ /**
5
+ * Resolve the effective composition mode with precedence
6
+ * `explicit flag > implied source > env > persisted config > default`.
7
+ * Default is `atomic` (composition OFF); `--composite` opts into the hierarchy
8
+ * machinery.
9
+ *
10
+ * `--atomic` is accepted for symmetry and wins when passed — even alongside a
11
+ * composition source, since it's an explicit "no" that overrides the implicit
12
+ * opt-in. If both `--composite` and `--atomic` are present, `--composite`
13
+ * wins. Passing a composition source (`--composition-map/-agent`,
14
+ * `--composition-refresh`, or `--generate-map`) implies composite without a
15
+ * separate `--composite`.
16
+ */
17
+ export function resolveCompositionMode(opts, configMode) {
18
+ if (opts.composite)
19
+ return 'composite';
20
+ if (opts.atomic)
21
+ return 'atomic';
22
+ // A composition source is an explicit opt-in for this invocation — it beats
23
+ // env/config, but not an explicit --atomic (handled above).
24
+ if (hasCompositionSource(opts))
25
+ return 'composite';
26
+ const env = process.env['EXPERIENCES_COMPOSITION_MODE'];
27
+ if (env !== undefined && env !== '') {
28
+ const v = env.toLowerCase();
29
+ if (v === 'composite')
30
+ return 'composite';
31
+ if (v === 'atomic')
32
+ return 'atomic';
33
+ }
34
+ if (configMode === 'composite' || configMode === 'atomic')
35
+ return configMode;
36
+ return 'atomic';
37
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Generic `--prompt <stage>=<value>` override parser.
3
+ *
4
+ * One repeatable flag overrides the prompt for any named pipeline stage
5
+ * (e.g. `composition`, `select`, `generate`). The value is classified by
6
+ * string SHAPE only — no filesystem access here: a value that looks like a
7
+ * path is resolved+read by the consumer; otherwise it is treated as the
8
+ * literal prompt text. This keeps the parser pure and testable.
9
+ */
10
+ export type PromptOverride = {
11
+ kind: 'path';
12
+ value: string;
13
+ } | {
14
+ kind: 'text';
15
+ value: string;
16
+ };
17
+ /**
18
+ * Heuristic: does this value look like a file path (vs. literal prompt text)?
19
+ * Path if it contains a path separator, starts with `~`, or ends with a known
20
+ * prompt-file extension. Deliberately does NOT touch the filesystem.
21
+ */
22
+ export declare function looksLikePath(value: string): boolean;
23
+ export type ParsePromptOverridesResult = {
24
+ overrides: Map<string, PromptOverride>;
25
+ errors: string[];
26
+ };
27
+ /**
28
+ * Parse repeated `stage=value` flag inputs into a stage→override map. Splits on
29
+ * the FIRST `=` so values may contain `=`. Last write wins for a repeated
30
+ * stage. Malformed entries are collected in `errors` (caller decides fatality).
31
+ */
32
+ export declare function parsePromptOverrides(inputs: string[]): ParsePromptOverridesResult;
33
+ /**
34
+ * Resolve an override to its prompt text: read the file for a `path` override
35
+ * (resolved against cwd), or return the literal `text` value. Throws with a
36
+ * clear message if a path override can't be read.
37
+ */
38
+ export declare function resolvePromptOverride(override: PromptOverride): Promise<string>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Generic `--prompt <stage>=<value>` override parser.
3
+ *
4
+ * One repeatable flag overrides the prompt for any named pipeline stage
5
+ * (e.g. `composition`, `select`, `generate`). The value is classified by
6
+ * string SHAPE only — no filesystem access here: a value that looks like a
7
+ * path is resolved+read by the consumer; otherwise it is treated as the
8
+ * literal prompt text. This keeps the parser pure and testable.
9
+ */
10
+ import { readFile } from 'node:fs/promises';
11
+ import { resolve } from 'node:path';
12
+ const PROMPT_FILE_EXTENSIONS = ['.md', '.txt', '.prompt'];
13
+ /**
14
+ * Heuristic: does this value look like a file path (vs. literal prompt text)?
15
+ * Path if it contains a path separator, starts with `~`, or ends with a known
16
+ * prompt-file extension. Deliberately does NOT touch the filesystem.
17
+ */
18
+ export function looksLikePath(value) {
19
+ if (value.includes('/') || value.includes('\\'))
20
+ return true;
21
+ if (value.startsWith('~'))
22
+ return true;
23
+ const lower = value.toLowerCase();
24
+ return PROMPT_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
25
+ }
26
+ /**
27
+ * Parse repeated `stage=value` flag inputs into a stage→override map. Splits on
28
+ * the FIRST `=` so values may contain `=`. Last write wins for a repeated
29
+ * stage. Malformed entries are collected in `errors` (caller decides fatality).
30
+ */
31
+ export function parsePromptOverrides(inputs) {
32
+ const overrides = new Map();
33
+ const errors = [];
34
+ for (const raw of inputs) {
35
+ const eq = raw.indexOf('=');
36
+ if (eq === -1) {
37
+ errors.push(`--prompt "${raw}" must be in the form stage=value`);
38
+ continue;
39
+ }
40
+ const stage = raw.slice(0, eq).trim();
41
+ const value = raw.slice(eq + 1);
42
+ if (stage === '') {
43
+ errors.push(`--prompt "${raw}" has an empty stage (expected stage=value)`);
44
+ continue;
45
+ }
46
+ if (value === '') {
47
+ errors.push(`--prompt "${raw}" has an empty value`);
48
+ continue;
49
+ }
50
+ overrides.set(stage, looksLikePath(value) ? { kind: 'path', value } : { kind: 'text', value });
51
+ }
52
+ return { overrides, errors };
53
+ }
54
+ /**
55
+ * Resolve an override to its prompt text: read the file for a `path` override
56
+ * (resolved against cwd), or return the literal `text` value. Throws with a
57
+ * clear message if a path override can't be read.
58
+ */
59
+ export async function resolvePromptOverride(override) {
60
+ if (override.kind === 'text')
61
+ return override.value;
62
+ const abs = resolve(override.value);
63
+ try {
64
+ return await readFile(abs, 'utf8');
65
+ }
66
+ catch {
67
+ throw new Error(`--prompt: could not read prompt file: ${abs}`);
68
+ }
69
+ }
@@ -80,6 +80,7 @@ export function registerPrintCommand(program) {
80
80
  .description('Write generated components to a CDF JSON file')
81
81
  .option('--session <id>', 'Session ID to print from (default: most recent generate components session)')
82
82
  .option('--out <path>', 'Output file path', 'components.json')
83
+ .option('--allow-empty', 'Write an empty-but-present components manifest when no components are accepted (a subsequent push then removes ALL components from the target space). Without this, an empty accepted set is an error.')
83
84
  .action(async (opts) => {
84
85
  const outPath = resolve(opts.out);
85
86
  await assertOutIsNotDirectory(outPath);
@@ -87,18 +88,35 @@ export function registerPrintCommand(program) {
87
88
  const db = openPipelineDb();
88
89
  let components;
89
90
  let generateStepStatus = null;
91
+ let rejectedCount = 0;
90
92
  try {
91
93
  components = loadCDFComponents(db, sessionId);
92
94
  const stepRow = db
93
95
  .prepare(`SELECT status FROM steps WHERE session_id = ? AND command = 'generate components' ORDER BY id DESC LIMIT 1`)
94
96
  .get(sessionId);
95
97
  generateStepStatus = stepRow?.status ?? null;
98
+ const rejectedRow = db
99
+ .prepare(`SELECT COUNT(*) AS n FROM raw_components WHERE session_id = ? AND status = 'generate-rejected'`)
100
+ .get(sessionId);
101
+ rejectedCount = rejectedRow?.n ?? 0;
96
102
  }
97
103
  finally {
98
104
  db.close();
99
105
  }
100
106
  if (components.length === 0) {
101
- die(`Error: no generated components in session '${sessionId}'. Run generate components first.`);
107
+ if (rejectedCount > 0) {
108
+ // Components WERE generated, but final review left none accepted — every
109
+ // component was rejected or left unresolved. This is a legitimate
110
+ // "clear the space" intent, but it's destructive, so require --allow-empty.
111
+ if (!opts.allowEmpty) {
112
+ die(`Error: all ${rejectedCount} generated component${rejectedCount === 1 ? ' was' : 's were'} rejected or left unresolved at final review in session '${sessionId}', so there is nothing to save. Accept at least one component (press [a] on a row, or [A] to accept all), or pass --allow-empty to write an empty manifest that will DELETE all components from the target space on push.`);
113
+ }
114
+ // Fall through: write an empty-but-present components manifest so a
115
+ // subsequent push removes every component from the target space.
116
+ }
117
+ else {
118
+ die(`Error: no generated components in session '${sessionId}'. Run generate components first.`);
119
+ }
102
120
  }
103
121
  if (generateStepStatus === 'failed') {
104
122
  process.stderr.write(`Warning: session '${sessionId}' generate step failed — output may be incomplete (${components.length} components found)\n`);
@@ -13,6 +13,9 @@ export type ModifyLauncherInput = {
13
13
  savePath: string;
14
14
  entryStep: 'scope-gate' | 'final-review';
15
15
  saveMode: 'overwrite' | 'new' | 'prompt';
16
+ /** Composition mode from the run record, so the modify wizard resumes in the
17
+ * same mode. Omitted → the wizard's default (`atomic`). */
18
+ compositionMode?: 'composite' | 'atomic';
16
19
  outDirOverride?: string;
17
20
  /** Pre-fill space id (from the run record's pushedTo). */
18
21
  initialSpaceId?: string;
@@ -21,6 +21,8 @@ export async function launchModifyWizard(input) {
21
21
  props.seedGenerateSessionId = input.generateSessionId;
22
22
  if (input.tokenSessionId)
23
23
  props.seedTokenSessionId = input.tokenSessionId;
24
+ if (input.compositionMode)
25
+ props.compositionMode = input.compositionMode;
24
26
  if (input.saveMode === 'overwrite')
25
27
  props.outDirOverride = input.savePath;
26
28
  if (input.outDirOverride)
@@ -151,6 +151,7 @@ export async function modifyRun(opts) {
151
151
  savePath: run.savePath,
152
152
  entryStep: 'final-review',
153
153
  saveMode,
154
+ ...(run.compositionMode ? { compositionMode: run.compositionMode } : {}),
154
155
  ...(opts.outDir ? { outDirOverride: resolve(opts.outDir) } : {}),
155
156
  ...(mergedSpaceId ? { initialSpaceId: mergedSpaceId } : {}),
156
157
  ...(mergedEnvironmentId ? { initialEnvironmentId: mergedEnvironmentId } : {}),
@@ -2,18 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { useImmediateInput } from '../analyze/select/tui/hooks/useImmediateInput.js';
5
- const COLLAPSED_LIMIT = 3;
6
- /**
7
- * The picker collapses to top 3 + Show all only when there are 5+ total.
8
- * Four or fewer runs render in full.
9
- */
10
- const COLLAPSE_THRESHOLD = 5;
11
- /**
12
- * Format the createdAt timestamp as `YYYY-MM-DD HH:MM` in the operator's
13
- * local timezone. We intentionally use local time (not UTC) because the
14
- * picker is for human recognition — operators expect to see times in the
15
- * same zone they were when the run happened.
16
- */
5
+ const COLLAPSED_LIMIT = 10;
6
+ const COLLAPSE_THRESHOLD = 12;
17
7
  function formatCreatedAt(iso) {
18
8
  const d = new Date(iso);
19
9
  if (Number.isNaN(d.getTime()))
@@ -78,15 +68,12 @@ function buildRows(runs, expanded) {
78
68
  export function RunPicker({ runs, staleRunIds, onSelect, onCancel }) {
79
69
  const [expanded, setExpanded] = useState(false);
80
70
  const [focusIdx, setFocusIdx] = useState(0);
81
- // When non-null, render the Push/Modify/Cancel sub-screen for this run.
82
71
  const [actionRunId, setActionRunId] = useState(null);
83
72
  const rows = buildRows(runs, expanded);
84
- // Clamp focus if the row count shrinks (e.g. after expansion changes layout).
85
73
  const clampedFocus = Math.min(focusIdx, rows.length - 1);
86
74
  const selectRow = (row) => {
87
75
  if (row.kind === 'show-all') {
88
76
  setExpanded(true);
89
- // Keep cursor on the first newly-revealed run for continuity.
90
77
  setFocusIdx(COLLAPSED_LIMIT);
91
78
  return;
92
79
  }
@@ -98,9 +85,6 @@ export function RunPicker({ runs, staleRunIds, onSelect, onCancel }) {
98
85
  };
99
86
  useImmediateInput((rawInput, key) => {
100
87
  if (actionRunId !== null) {
101
- // Sub-screen owns input while it's active. (ActionScreen also installs
102
- // its own useImmediateInput; only one is mounted at a time so there's
103
- // no double-handle.)
104
88
  return;
105
89
  }
106
90
  if (key.upArrow || rawInput === 'k') {
@@ -55,6 +55,10 @@ export type RunRecord = {
55
55
  * writers always populate. Added in runs.json v3. Optional for the same
56
56
  * reason as `sourceFingerprint`. */
57
57
  savedFingerprint?: SavedFingerprint | null;
58
+ /** Composition mode the run was produced in, so modify/replay resumes in the
59
+ * same mode (`composite` vs `atomic`). Absent on records written before this
60
+ * field existed; callers treat a missing value as `atomic` (the default). */
61
+ compositionMode?: 'composite' | 'atomic';
58
62
  notes?: string;
59
63
  };
60
64
  export type RunsFile = {
@@ -3,6 +3,7 @@ import type { RawComponentDefinition } from '../types.js';
3
3
  import type { CDFComponentEntry, DTCGTokenEntry, DTCGTokenGroup } from '@contentful/experience-design-system-types';
4
4
  import type { ToolCall, TokenToolCall } from '../generate/agent-runner.js';
5
5
  import type { ComponentTypeSummary } from '@contentful/experience-design-system-types';
6
+ import type { SlotCycle, SlotEdge } from '../analyze/cycle-detection.js';
6
7
  export type StepStatus = 'pending' | 'complete' | 'failed' | 'interrupted';
7
8
  export type CommandName = 'analyze extract' | 'analyze select' | 'generate components' | 'generate tokens' | 'generate edit' | 'apply preview' | 'apply select' | 'apply push' | 'print components' | 'print tokens' | 'import';
8
9
  export interface SessionRow {
@@ -31,12 +32,6 @@ export interface ApplyToolCallsResult {
31
32
  slots: number;
32
33
  warnings: string[];
33
34
  }
34
- /**
35
- * Feature 1: load per-component metadata for surfacing in the review UI —
36
- * source path, full source text, and per-prop rationale + source-location.
37
- * Returns null when no row matches. Decoupled from loadCDFComponents so
38
- * the CDF projection stays unaffected.
39
- */
40
35
  export interface ComponentReviewMetadata {
41
36
  sourcePath: string | null;
42
37
  componentSource: string | null;
@@ -65,13 +60,6 @@ export interface ComponentRationale {
65
60
  rationale: string | null;
66
61
  }>;
67
62
  }
68
- /**
69
- * Component-rationale loader: data contract for the `I` ComponentRationalePanel.
70
- * Returns the component's description + the three component-level rationale
71
- * strings (why-description / why-props / why-slots), plus per-prop and per-slot
72
- * rationale rows for the operator-facing panel. Returns null when no component
73
- * matches.
74
- */
75
63
  export declare function loadComponentRationale(db: DatabaseSync, sessionId: string, componentName: string): ComponentRationale | null;
76
64
  export declare function applyToolCalls(db: DatabaseSync, sessionId: string, componentId: string, componentName: string, calls: ToolCall[], incomingWarnings: string[]): ApplyToolCallsResult;
77
65
  export interface MatchHints {
@@ -95,12 +83,6 @@ export type RawComponentWithId = RawComponentDefinition & {
95
83
  component_id: string;
96
84
  };
97
85
  export declare function loadRawComponents(db: DatabaseSync, sessionId: string, allowedNames?: Set<string>): RawComponentWithId[];
98
- /**
99
- * Rename empty-named slots in the DB to heuristic names before generation.
100
- * The LLM classify_slot tool call is matched back to the DB row by name, so a
101
- * row with name="" can never be updated by the LLM — renaming it here makes it
102
- * classifiable. Returns one warning string per renamed slot.
103
- */
104
86
  export declare function renameEmptySlots(db: DatabaseSync, sessionId: string, componentId: string, componentName: string, slotCount: number): {
105
87
  renames: Array<{
106
88
  oldName: string;
@@ -121,6 +103,10 @@ export type ScopeComponentRow = {
121
103
  componentId: string;
122
104
  aiDecision: 'accepted' | 'rejected' | null;
123
105
  aiReason: string | null;
106
+ slots: Array<{
107
+ name: string;
108
+ allowedComponents: string[];
109
+ }>;
124
110
  };
125
111
  export declare function loadScopeComponents(db: DatabaseSync, sessionId: string): ScopeComponentRow[];
126
112
  export declare function applyScopeDecisions(db: DatabaseSync, sessionId: string, decisions: {
@@ -141,22 +127,10 @@ export declare function loadDTCGTokens(db: DatabaseSync, sessionId: string): {
141
127
  export declare function findLatestSessionForCommand(db: DatabaseSync, command: CommandName): string | null;
142
128
  export declare function seedCDFFromPriorSession(db: DatabaseSync, targetSessionId: string): number;
143
129
  export declare function seedCDFFromPreviewResponse(db: DatabaseSync, sessionId: string, removedItems: ComponentTypeSummary[]): number;
144
- /**
145
- * Seeds server-side metadata for changed components:
146
- * 1. Fills in defaults the server has but we don't (prevents accidental removal)
147
- * 2. Seeds cdf_type/cdf_category for props that exist on server but lack CDF locally
148
- * (e.g. props with non-standard source types the generation step couldn't classify)
149
- */
150
130
  export declare function seedDefaultsFromChangedItems(db: DatabaseSync, sessionId: string, changedItems: Array<{
151
131
  current: ComponentTypeSummary;
152
132
  proposed: object;
153
133
  }>): number;
154
- /**
155
- * Ensures all props on generated components have a cdf_type.
156
- * Uses the pre-classification `category` column when available.
157
- * Falls back to cdf_type: 'string', cdf_category: 'content' for props
158
- * with no pre-classification (safe default — content is the most common category).
159
- */
160
134
  export declare function backfillUnclassifiedProps(db: DatabaseSync, sessionId: string): number;
161
135
  export interface CacheEntry {
162
136
  inputHash: string;
@@ -164,7 +138,6 @@ export interface CacheEntry {
164
138
  entityId: string;
165
139
  sourceSessionId: string;
166
140
  humanEdited: boolean;
167
- /** sha256 of the prompt content. '' for pre-upgrade rows that predate the fine-grained cache. */
168
141
  promptHash: string;
169
142
  createdAt: string;
170
143
  updatedAt: string;
@@ -176,17 +149,17 @@ export declare function lookupCacheByEntity(db: DatabaseSync, entityType: 'compo
176
149
  export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean, promptHash?: string): void;
177
150
  export declare function storeScannedFiles(db: DatabaseSync, sessionId: string, filePaths: string[]): void;
178
151
  export declare function loadScannedFiles(db: DatabaseSync, sessionId: string): string[];
152
+ export declare function storeSlotCycles(db: DatabaseSync, sessionId: string, cycles: Array<SlotCycle & {
153
+ suggestedBreak?: SlotEdge | null;
154
+ }>): void;
155
+ export interface StoredSlotCycle extends SlotCycle {
156
+ suggestedBreak: SlotEdge | null;
157
+ }
158
+ export declare function loadSlotCycles(db: DatabaseSync, sessionId: string): StoredSlotCycle[];
159
+ export declare function clearSlotCycles(db: DatabaseSync, sessionId: string): void;
179
160
  export declare function markCacheHumanEdited(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): void;
180
161
  export declare function copyComponentFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string, componentId: string): void;
181
162
  export declare function copyTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): void;
182
- /**
183
- * Returns the CLI cache version used to namespace cache rows. To avoid busting
184
- * the cache on every npm patch bump, this hashes the content of the bundled
185
- * skill prompts. When the prompts change, cache invalidates; when only library
186
- * code or test fixtures change, the cache stays warm.
187
- *
188
- * Memoized per-process. Falls back to package version on any I/O error.
189
- */
190
163
  export declare function getCliCacheVersion(): Promise<string>;
191
164
  export interface ExtractCacheEntry {
192
165
  filePath: string;
@@ -198,6 +171,16 @@ export interface ExtractCacheEntry {
198
171
  }
199
172
  export declare function storeExtractCache(db: DatabaseSync, filePath: string, fileHash: string, cliVersion: string, components: RawComponentDefinition[]): void;
200
173
  export declare function lookupExtractCache(db: DatabaseSync, fileHash: string, cliVersion: string): ExtractCacheEntry | null;
174
+ /**
175
+ * Content-addressed cache for the composition resolver's raw agent output
176
+ * (authored parser source or emitted-edge JSONL). Keyed by a hash of the
177
+ * candidate files + producer identity (`composition-cache-key.ts`), scoped by
178
+ * `cli_version` so a prompt/skill change invalidates stale entries. Replaces
179
+ * the former on-disk `~/.contentful/.../composition-cache` store so all CLI
180
+ * caches live in the one pipeline DB.
181
+ */
182
+ export declare function storeCompositionCache(db: DatabaseSync, inputHash: string, cliVersion: string, agentOutput: string): void;
183
+ export declare function lookupCompositionCache(db: DatabaseSync, inputHash: string, cliVersion: string): string | null;
201
184
  export type SelectDecision = 'accepted' | 'rejected';
202
185
  export interface SelectCacheEntry {
203
186
  componentHash: string;