@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
@@ -2,6 +2,7 @@ import { resolve, join } from 'node:path';
2
2
  import { runPipeline } from './orchestrator.js';
3
3
  import { resolveAutoFilter } from './auto-filter-resolve.js';
4
4
  import { resolveAgent, resolveModel } from './agent-model-resolve.js';
5
+ import { resolveCompositionMode } from '../lib/composition-mode.js';
5
6
  import { readExperiencesCredentials } from '../credentials-store.js';
6
7
  import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
7
8
  import { replayRun, modifyRun } from '../runs/replay-helpers.js';
@@ -28,7 +29,7 @@ export function registerImportCommand(program) {
28
29
  .option('--skip-analyze', 'Skip the analyze step (uses most recent extract session)')
29
30
  .option('--skip-generate', 'Skip the generate step (uses most recent generate session)')
30
31
  .option('--print', 'Write components.json to --out after generation')
31
- .option('--skip-apply', 'Skip pushing to Contentful (stops after generate)')
32
+ .option('--skip-apply', '(deprecated alias for --no-push) Skip pushing to Contentful')
32
33
  .option('--no-cache', 'Re-run all steps even if output already exists')
33
34
  .option('--yes', 'Skip interactive confirmation in apply push')
34
35
  .option('--verbose', 'Show full agent output and all entity progress')
@@ -38,10 +39,19 @@ export function registerImportCommand(program) {
38
39
  .option('--dry-run', "(deprecated, will change semantics in a future release) Print generate components prompt without invoking the agent. Use --print-prompt for the same behaviour explicitly, or '--dry-run --no-push' for the upcoming manifest-preview semantics.")
39
40
  .option('--print-prompt', 'Print the generate components prompt without invoking the agent. Replaces the legacy --dry-run prompt-print behaviour on this command.')
40
41
  .option('--auto-accept-scope', 'Accept all extracted components without prompting (for scripted/non-TTY callers)')
42
+ .option('--composite', 'Import embedded-component hierarchy (opt in; default is atomic)')
43
+ .option('--atomic', 'Import flat components with no embedded-component hierarchy (default)')
44
+ .option('--composition-map <path>', 'Consume a hand-authored parent→children interchange map (implies --composite)')
45
+ .option('--composition-agent', 'Opt into agentic mapping resolution when deterministic sources find no groups (implies --composite)')
46
+ .option('--composition-refresh', 'Bypass the composition cache and re-resolve from scratch, forcing the agent to run (implies --composite)')
47
+ .option('--composition-agent-mode <mode>', "Agent mode: 'parser' (agent writes a sandboxed parser, default) or 'edges' (agent lists edges)")
48
+ .option('--generate-map <path>', 'Also write a composition-map skeleton from resolved edges during extract (implies --composite)')
49
+ .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], [])
50
+ .option('--auto-reject-cycles', 'Automatically reject components involved in slot cycles and retry')
41
51
  .option('--auto-filter', 'Force the AI auto-filter ON (overrides the credentials.json autoFilter preference)')
42
52
  .option('--no-auto-filter', 'Skip the automatic AI pre-filter; jump straight to manual scope-gate (overrides the credentials.json autoFilter preference; no-op when paired with --auto-accept-scope)')
43
53
  .option('--no-live-preview', 'Skip the automatic preview re-run after each FieldEditor save (no-op when paired with --auto-accept-scope)')
44
- .option('--no-push', 'Run extract → scope-gate → generate → final-review and exit without pushing to Contentful (no credentials prompt; live preview disabled)')
54
+ .option('--no-push', 'Import without pushing to Contentful. Interactive: runs the full wizard (extract → scope-gate → generate → final-review) and stops before push. Non-interactive (piped/CI): runs headless through generate. No credentials needed either way.')
45
55
  .option('--no-save', 'Push without writing components.json / tokens.json to disk (default: save AND push)')
46
56
  .option('--out-dir <path>', 'Save components.json / tokens.json to this directory; bypasses the inline save-path prompt')
47
57
  .option('--on-conflict <mode>', "How to handle existing components.json / tokens.json at the save path: 'overwrite' replaces files, 'skip' writes to a timestamped subdirectory, 'fail' exits non-zero. Skips the wizard's interactive conflict gate when set.", (value) => {
@@ -59,9 +69,31 @@ export function registerImportCommand(program) {
59
69
  .option('--save-as-new', 'Only valid with --modify: always save to a new path (prompts for one)')
60
70
  .option('--force', 'Bypass staleness checks when paired with --push-from-run or --modify.')
61
71
  .action(async (opts) => {
62
- // ── --push-from-run handling ───────────────────────────────────────
63
- // Push-only replay of a prior run. Mutex checks happen *before* any
64
- // side effects (no DB reads, no wizard render).
72
+ // --modify and --push-from-run resume a recorded session; the composition
73
+ // mode comes from that run's record, so composition flags on the command
74
+ // line don't apply. Warn and clear them rather than let them mislead.
75
+ if (opts.modify !== undefined || opts.pushFromRun !== undefined) {
76
+ const passedCompositionFlags = [
77
+ opts.composite ? '--composite' : null,
78
+ opts.atomic ? '--atomic' : null,
79
+ opts.compositionMap ? '--composition-map' : null,
80
+ opts.compositionAgent ? '--composition-agent' : null,
81
+ opts.compositionAgentMode ? '--composition-agent-mode' : null,
82
+ opts.compositionRefresh ? '--composition-refresh' : null,
83
+ opts.generateMap ? '--generate-map' : null,
84
+ ].filter((f) => f !== null);
85
+ if (passedCompositionFlags.length > 0) {
86
+ const entry = opts.modify !== undefined ? '--modify' : '--push-from-run';
87
+ process.stderr.write(`Note: ${passedCompositionFlags.join(', ')} ignored with ${entry} — composition mode comes from the recorded run.\n`);
88
+ opts.composite = undefined;
89
+ opts.atomic = undefined;
90
+ opts.compositionMap = undefined;
91
+ opts.compositionAgent = undefined;
92
+ opts.compositionAgentMode = undefined;
93
+ opts.compositionRefresh = undefined;
94
+ opts.generateMap = undefined;
95
+ }
96
+ }
65
97
  if (opts.pushFromRun !== undefined) {
66
98
  if (opts.modify !== undefined) {
67
99
  process.stderr.write('Error: --push-from-run and --modify are mutually exclusive. --push-from-run pushes the recorded session; --modify re-opens the wizard for edits.\n');
@@ -106,7 +138,6 @@ export function registerImportCommand(program) {
106
138
  return;
107
139
  }
108
140
  }
109
- // ── --modify handling ──────────────────────────────────────────────
110
141
  if (opts.modify !== undefined) {
111
142
  if (opts.project !== '.') {
112
143
  process.stderr.write('Error: --modify and --project are mutually exclusive. The project path is read from the recorded run.\n');
@@ -154,10 +185,6 @@ export function registerImportCommand(program) {
154
185
  process.exit(1);
155
186
  return;
156
187
  }
157
- // ── --raw-tokens validation ─────────────────────────────────────────
158
- // The raw-tokens path is a source file the wizard hands to
159
- // `generate tokens --raw-tokens <path>`. Validate at parse time so
160
- // operators get an immediate error before the wizard renders.
161
188
  if (opts.rawTokens !== undefined) {
162
189
  if (opts.tokens !== undefined) {
163
190
  process.stderr.write('Error: --raw-tokens and --tokens are mutually exclusive: --raw-tokens is the source file to classify, --tokens is a pre-classified DTCG sidecar.\n');
@@ -182,9 +209,16 @@ export function registerImportCommand(program) {
182
209
  process.stderr.write(promptFlags.deprecationNotice);
183
210
  }
184
211
  const dryRunForward = promptFlags.forwardDryRun;
212
+ // "Don't push" is one user intent (--no-push); --skip-apply is a
213
+ // deprecated alias. Interactive runs (TTY) take the wizard and stop
214
+ // before push; non-interactive runs take the headless pipeline and stop
215
+ // after generate. Either way no credentials are required.
216
+ const noPushRequested = opts.push === false || opts.skipApply === true;
185
217
  const isHeadless = opts.skipAnalyze ||
186
218
  opts.skipGenerate ||
187
- opts.skipApply ||
219
+ // A "don't push" request on a non-TTY is a headless intent (the wizard
220
+ // needs a TTY); in a TTY it stays interactive and is NOT headless.
221
+ (noPushRequested && !process.stdout.isTTY) ||
188
222
  !!opts.spaceId ||
189
223
  !!opts.environmentId ||
190
224
  !!opts.cmaToken ||
@@ -193,7 +227,7 @@ export function registerImportCommand(program) {
193
227
  false;
194
228
  const autoAcceptScope = opts.autoAcceptScope ?? false;
195
229
  if (!process.stdout.isTTY && !isHeadless && !autoAcceptScope) {
196
- process.stderr.write('Error: experiences import is interactive. Pass --auto-accept-scope, or use a headless mode by providing credentials (--space-id, --environment-id, --cma-token) or one of --skip-analyze, --skip-generate, --skip-apply, --yes, --dry-run, --print-prompt.\n');
230
+ process.stderr.write('Error: experiences import is interactive. Pass --auto-accept-scope, or use a headless mode by providing credentials (--space-id, --environment-id, --cma-token) or one of --no-push, --skip-analyze, --skip-generate, --yes, --dry-run, --print-prompt.\n');
197
231
  process.exit(1);
198
232
  return;
199
233
  }
@@ -202,15 +236,9 @@ export function registerImportCommand(program) {
202
236
  const { createElement } = await import('react');
203
237
  const { WizardApp } = await import('./tui/WizardApp.js');
204
238
  const creds = await readExperiencesCredentials();
205
- // Parity-audit Q4: resolve --agent / --model overrides against the
206
- // stored credentials.json so both flags are functional for the
207
- // wizard path. Flag wins, then stored value, then default.
208
239
  const resolvedAgent = resolveAgent(opts.agent, creds.agent);
209
240
  const resolvedModel = resolveModel(opts.model, creds.agentModel);
210
- // ── Run picker decision ─────────────────────────────────────────
211
- // When runs.json has prior entries and the operator didn't ask for
212
- // a specific entry point, open the wizard with the run picker. The
213
- // helper enforces every gate (TTY, conflicting flags, file state).
241
+ const resolvedCompositionMode = resolveCompositionMode(opts, creds.compositionMode);
214
242
  const pickerDecision = await shouldShowRunPicker({
215
243
  flags: {
216
244
  ...(opts.pushFromRun !== undefined ? { pushFromRun: opts.pushFromRun } : {}),
@@ -222,10 +250,6 @@ export function registerImportCommand(program) {
222
250
  isTTY: !!process.stdin.isTTY,
223
251
  });
224
252
  let pickerSelection = null;
225
- // Ink `unmount` handle captured after `render(...)` below so the
226
- // picker callback can tear down the wizard cleanly. Prior to this
227
- // fix the callback did `setImmediate(() => process.exit(0))`, which
228
- // killed the process before `dispatchPickerSelection` could run.
229
253
  let unmountInk = null;
230
254
  const pickerProps = {};
231
255
  if (pickerDecision.shouldShow) {
@@ -245,10 +269,18 @@ export function registerImportCommand(program) {
245
269
  initialProjectPath: opts.project !== '.' ? resolve(opts.project) : undefined,
246
270
  host: opts.host,
247
271
  autoAcceptScope,
272
+ autoRejectCycles: opts.autoRejectCycles ?? false,
273
+ compositionMode: resolvedCompositionMode,
274
+ ...(opts.compositionMap ? { compositionMap: opts.compositionMap } : {}),
275
+ ...(opts.compositionAgent ? { compositionAgent: true } : {}),
276
+ ...(opts.compositionAgentMode ? { compositionAgentMode: opts.compositionAgentMode } : {}),
277
+ ...(opts.compositionRefresh ? { compositionRefresh: true } : {}),
278
+ ...(opts.generateMap ? { generateMap: opts.generateMap } : {}),
279
+ ...(opts.prompt && opts.prompt.length > 0 ? { promptOverrides: opts.prompt } : {}),
248
280
  noCache: opts.cache === false,
249
281
  autoFilter: resolveAutoFilter({ autoFilter: opts.autoFilter }, creds.autoFilter),
250
282
  livePreview: opts.livePreview !== false,
251
- noPush: opts.push === false,
283
+ noPush: noPushRequested,
252
284
  noSave: opts.save === false,
253
285
  ...(opts.outDir ? { outDirOverride: resolve(opts.outDir) } : {}),
254
286
  ...(opts.onConflict ? { onConflictMode: opts.onConflict } : {}),
@@ -259,10 +291,6 @@ export function registerImportCommand(program) {
259
291
  }));
260
292
  unmountInk = unmount;
261
293
  await waitUntilExit();
262
- // ── Picker dispatch ─────────────────────────────────────────────
263
- // If the operator picked a run, route into replayRun / modifyRun.
264
- // dispatchPickerSelection lives in ./picker-dispatch.ts so this
265
- // decision is testable without an Ink runtime.
266
294
  if (pickerSelection) {
267
295
  await dispatchPickerSelection(pickerSelection, {
268
296
  ...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
@@ -278,21 +306,23 @@ export function registerImportCommand(program) {
278
306
  }
279
307
  return;
280
308
  }
281
- const skipApply = opts.skipApply ?? false;
309
+ // --no-push (and its deprecated alias --skip-apply) both mean "stop
310
+ // before the push" on the headless path — so neither requires credentials.
311
+ const skipApply = noPushRequested;
282
312
  const spaceId = opts.spaceId ?? process.env['CONTENTFUL_SPACE_ID'];
283
313
  const environmentId = opts.environmentId ?? process.env['CONTENTFUL_ENVIRONMENT_ID'];
284
314
  const cmaToken = opts.cmaToken ?? process.env['CONTENTFUL_MANAGEMENT_TOKEN'];
285
315
  if (!skipApply && (!spaceId || !environmentId || !cmaToken)) {
286
- process.stderr.write('Error: --space-id (or CONTENTFUL_SPACE_ID), --environment-id (or CONTENTFUL_ENVIRONMENT_ID), and --cma-token (or CONTENTFUL_MANAGEMENT_TOKEN) are required unless --skip-apply is set.\n');
316
+ process.stderr.write('Error: --space-id (or CONTENTFUL_SPACE_ID), --environment-id (or CONTENTFUL_ENVIRONMENT_ID), and --cma-token (or CONTENTFUL_MANAGEMENT_TOKEN) are required unless --no-push is set.\n');
287
317
  process.exit(1);
288
318
  return;
289
319
  }
290
320
  const projectRoot = resolve(opts.project);
291
321
  const outDir = opts.out ? resolve(opts.out) : join(projectRoot, '.contentful');
292
- // Parity-audit Q4: also honor stored agent/model in headless mode.
293
322
  const headlessCreds = await readExperiencesCredentials();
294
323
  const headlessAgent = resolveAgent(opts.agent, headlessCreds.agent);
295
324
  const headlessModel = resolveModel(opts.model, headlessCreds.agentModel);
325
+ const headlessCompositionMode = resolveCompositionMode(opts, headlessCreds.compositionMode);
296
326
  const result = await runPipeline({
297
327
  project: projectRoot,
298
328
  out: outDir,
@@ -317,6 +347,14 @@ export function registerImportCommand(program) {
317
347
  host: opts.host,
318
348
  dryRun: dryRunForward,
319
349
  selectPromptPath: opts.selectPromptPath,
350
+ autoRejectCycles: opts.autoRejectCycles ?? false,
351
+ compositionMode: headlessCompositionMode,
352
+ ...(opts.compositionMap ? { compositionMap: opts.compositionMap } : {}),
353
+ ...(opts.compositionAgent ? { compositionAgent: true } : {}),
354
+ ...(opts.compositionAgentMode ? { compositionAgentMode: opts.compositionAgentMode } : {}),
355
+ ...(opts.compositionRefresh ? { compositionRefresh: true } : {}),
356
+ ...(opts.generateMap ? { generateMap: opts.generateMap } : {}),
357
+ ...(opts.prompt && opts.prompt.length > 0 ? { promptOverrides: opts.prompt } : {}),
320
358
  }, (line) => process.stderr.write(line + '\n'));
321
359
  const hasFailed = result.steps.some((s) => s.status === 'failed');
322
360
  if (!process.stdout.isTTY) {
@@ -0,0 +1,4 @@
1
+ import type { ComponentGraphNode } from '../analyze/composite-closure.js';
2
+ export declare function computeCycleAutoRejectTargets(slotCycles: Array<{
3
+ path: string[];
4
+ }>, graph: ComponentGraphNode[]): Set<string>;
@@ -0,0 +1,14 @@
1
+ import { findAllAncestors } from '../analyze/lineage.js';
2
+ export function computeCycleAutoRejectTargets(slotCycles, graph) {
3
+ const targets = new Set();
4
+ const participants = new Set();
5
+ for (const cyc of slotCycles)
6
+ for (const p of cyc.path)
7
+ participants.add(p);
8
+ for (const p of participants) {
9
+ targets.add(p);
10
+ for (const anc of findAllAncestors(p, graph))
11
+ targets.add(anc);
12
+ }
13
+ return targets;
14
+ }
@@ -1,3 +1,4 @@
1
+ import type { CompositionMode } from '../lib/composition-mode.js';
1
2
  export interface PipelineOptions {
2
3
  project: string;
3
4
  out: string;
@@ -23,6 +24,15 @@ export interface PipelineOptions {
23
24
  deselect?: string[];
24
25
  /** Forwarded to the spawned `analyze select-agent` subprocess. */
25
26
  selectPromptPath?: string;
27
+ /** When true, auto-reject cycle participants and retry push instead of surfacing an error. */
28
+ autoRejectCycles?: boolean;
29
+ compositionMode?: CompositionMode;
30
+ compositionMap?: string;
31
+ compositionAgent?: boolean;
32
+ compositionAgentMode?: string;
33
+ compositionRefresh?: boolean;
34
+ generateMap?: string;
35
+ promptOverrides?: string[];
26
36
  }
27
37
  export interface StepResult {
28
38
  step: string;
@@ -36,19 +46,22 @@ export interface PipelineResult {
36
46
  session: string;
37
47
  project: string;
38
48
  steps: StepResult[];
49
+ cycleError?: {
50
+ report: string[];
51
+ };
39
52
  }
53
+ export declare const SLOT_CYCLE_MARKER = "manifest:components/slot-cycles";
54
+ export declare function isSlotCycleError(result: {
55
+ exitCode: number;
56
+ stderr: string;
57
+ }): boolean;
58
+ export declare function extractCycleReport(stderr: string): string[];
59
+ export declare function parseCycleComponentNames(report: string[]): string[];
40
60
  export declare function isPreviewValidationError(result: {
41
61
  exitCode: number;
42
62
  stderr: string;
43
63
  }): boolean;
44
64
  export declare function parseOffendingComponentNames(output: string): string[];
45
- /**
46
- * Build the apply-push StepResult record. Centralizes the success/failure
47
- * shape so excludedByValidationRetry is recorded consistently in both
48
- * branches — previously the total-failure branch dropped it, leaving users
49
- * with a failed pipeline and no audit trail of what was auto-excluded
50
- * before the retry loop gave up.
51
- */
52
65
  export declare function buildPushStepResult(args: {
53
66
  created: number;
54
67
  updated: number;
@@ -2,7 +2,8 @@ import { mkdir } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { execFile } from 'node:child_process';
5
- import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, } from '../session/db.js';
5
+ import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, loadCDFComponents, } from '../session/db.js';
6
+ import { detectSlotCycles, formatSlotCycleReport } from '../apply/command.js';
6
7
  import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
7
8
  import { buildPostPushUrl } from '../lib/contentful-urls.js';
8
9
  import { getDebugLogger, debugEnvForSubprocess } from '../lib/debug-logger.js';
@@ -43,27 +44,34 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
43
44
  });
44
45
  }
45
46
  const MAX_VALIDATION_RETRIES = Number(process.env['EDS_MAX_VALIDATION_RETRIES'] ?? 2);
47
+ export const SLOT_CYCLE_MARKER = 'manifest:components/slot-cycles';
48
+ export function isSlotCycleError(result) {
49
+ return result.exitCode !== 0 && result.stderr.includes(SLOT_CYCLE_MARKER);
50
+ }
51
+ export function extractCycleReport(stderr) {
52
+ return stderr.split('\n').filter((line) => line.trim());
53
+ }
54
+ export function parseCycleComponentNames(report) {
55
+ const names = [];
56
+ for (const line of report) {
57
+ const m = line.match(/Fix: remove '([^']+)' from /);
58
+ if (m)
59
+ names.push(m[1]);
60
+ }
61
+ return [...new Set(names)];
62
+ }
46
63
  export function isPreviewValidationError(result) {
47
64
  return (result.exitCode !== 0 &&
48
65
  result.stderr.includes(`${PREVIEW_ERROR_PREFIX} 422`) &&
49
66
  result.stderr.includes(VALIDATION_FAILED_CODE));
50
67
  }
51
68
  export function parseOffendingComponentNames(output) {
52
- // The 422 body is appended to the ApiError message by the constructor and
53
- // written to stderr by die(). Extract the JSON portion by finding the first '{'.
54
69
  const jsonStart = output.indexOf('{');
55
70
  if (jsonStart === -1)
56
71
  return [];
57
72
  const errors = parsePreviewValidationErrors(output.slice(jsonStart));
58
73
  return [...new Set(errors.map((e) => e.componentName))];
59
74
  }
60
- /**
61
- * Build the apply-push StepResult record. Centralizes the success/failure
62
- * shape so excludedByValidationRetry is recorded consistently in both
63
- * branches — previously the total-failure branch dropped it, leaving users
64
- * with a failed pipeline and no audit trail of what was auto-excluded
65
- * before the retry loop gave up.
66
- */
67
75
  export function buildPushStepResult(args) {
68
76
  const { created, updated, failed, durationMs, stderr, excludedByRetry, totalFailure } = args;
69
77
  const excludedDetail = excludedByRetry.length > 0 ? { excludedByValidationRetry: excludedByRetry } : {};
@@ -106,13 +114,11 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
106
114
  await mkdir(outDir, { recursive: true });
107
115
  const steps = [];
108
116
  let stepNum = 0;
109
- // print components is an optional step; adjust total accordingly
110
117
  const totalSteps = 4 + (opts.print ? 1 : 0);
111
118
  function stepLabel(name) {
112
119
  stepNum++;
113
120
  return ` Step ${stepNum}/${totalSteps} ${name} `;
114
121
  }
115
- // ── Step 1: analyze extract ──────────────────────────────────────────────
116
122
  const analyzeLabel = stepLabel('Statically analyzing project');
117
123
  const analyzeSkipped = !opts.noCache && opts.skipAnalyze;
118
124
  let extractSessionId = null;
@@ -123,7 +129,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
123
129
  status: 'skipped',
124
130
  reason: '--skip-analyze',
125
131
  });
126
- // Try to find a prior extract session to hand to downstream steps
127
132
  const prior = findLatestSessionForCommand(db, 'analyze extract');
128
133
  extractSessionId = prior ?? null;
129
134
  }
@@ -133,6 +138,23 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
133
138
  });
134
139
  const t0 = Date.now();
135
140
  const analyzeArgs = ['analyze', 'extract', '--project', projectRoot];
141
+ if (opts.compositionMode === 'composite') {
142
+ analyzeArgs.push('--composite');
143
+ if (opts.compositionMap)
144
+ analyzeArgs.push('--composition-map', opts.compositionMap);
145
+ if (opts.compositionAgent)
146
+ analyzeArgs.push('--composition-agent');
147
+ if (opts.compositionAgentMode)
148
+ analyzeArgs.push('--composition-agent-mode', opts.compositionAgentMode);
149
+ if (opts.compositionRefresh)
150
+ analyzeArgs.push('--composition-refresh');
151
+ if (opts.generateMap)
152
+ analyzeArgs.push('--generate-map', opts.generateMap);
153
+ for (const p of opts.promptOverrides ?? [])
154
+ analyzeArgs.push('--prompt', p);
155
+ if (opts.agent)
156
+ analyzeArgs.push('--agent', opts.agent);
157
+ }
136
158
  const r = await runStep(analyzeArgs, cliPath);
137
159
  const durationMs = Date.now() - t0;
138
160
  if (r.exitCode !== 0) {
@@ -149,7 +171,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
149
171
  db.close();
150
172
  return { session: sessionId, project: projectRoot, steps };
151
173
  }
152
- // Read session ID from DB — avoids dependence on subprocess stdout format.
153
174
  extractSessionId = findLatestSessionForCommand(db, 'analyze extract') ?? null;
154
175
  const componentMatch = /Extracted (\d+) component/.exec(r.stderr);
155
176
  const componentCount = componentMatch ? Number(componentMatch[1]) : 0;
@@ -164,7 +185,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
164
185
  detail: { components: componentCount },
165
186
  });
166
187
  }
167
- // ── Step 2: analyze select ───────────────────────────────────────────────
168
188
  const editLabel = stepLabel('Filtering components');
169
189
  if (analyzeSkipped) {
170
190
  progressWriter(`${editLabel}– skipped (--skip-analyze)`);
@@ -179,7 +199,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
179
199
  extractSession: extractSessionId,
180
200
  });
181
201
  const t0Edit = Date.now();
182
- // Use agentic select when an agent is available and no manual select/deselect patterns are given.
183
202
  const useAgentSelect = !opts.selectAll && (!opts.select || opts.select.length === 0) && (!opts.deselect || opts.deselect.length === 0);
184
203
  let editArgs;
185
204
  if (useAgentSelect) {
@@ -202,7 +221,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
202
221
  else if (opts.deselect && opts.deselect.length > 0) {
203
222
  for (const p of opts.deselect)
204
223
  editArgs.push('--deselect', p);
205
- editArgs.push('--select-all'); // select-all with deselect patterns = select all except matches
224
+ editArgs.push('--select-all');
206
225
  }
207
226
  else {
208
227
  editArgs.push('--select-all');
@@ -245,7 +264,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
245
264
  reason: 'no extract session',
246
265
  });
247
266
  }
248
- // ── Step 3: generate components ──────────────────────────────────────────
249
267
  if (opts.skipGenerate) {
250
268
  const generateLabel = stepLabel('Categorizing component props');
251
269
  progressWriter(`${generateLabel}– skipped (--skip-generate)`);
@@ -301,7 +319,29 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
301
319
  progressWriter(`${generateLabel}✓ components stored locally (${(durationMs / 1000).toFixed(1)}s)`);
302
320
  steps.push({ step: 'generate components', status: 'complete', durationMs });
303
321
  }
304
- // ── Step 4 (optional): print components ─────────────────────────────────
322
+ // Block save or push when the accepted set still contains a slot cycle.
323
+ // Skipped under --auto-reject-cycles, which delegates to the push-path
324
+ // retry loop below (reject cycle members, then retry) instead of blocking.
325
+ if (extractSessionId && !opts.autoRejectCycles) {
326
+ const acceptedCycles = (() => {
327
+ try {
328
+ return detectSlotCycles(loadCDFComponents(db, extractSessionId));
329
+ }
330
+ catch {
331
+ return [];
332
+ }
333
+ })();
334
+ if (acceptedCycles.length > 0) {
335
+ const report = formatSlotCycleReport(acceptedCycles);
336
+ process.stderr.write(report.join('\n') + '\n');
337
+ progressWriter('Slot cycle detected. The accepted components contain circular slot references — refusing to save or push.');
338
+ for (const line of report)
339
+ progressWriter(line);
340
+ steps.push({ step: 'cycle gate', status: 'failed', error: report.join('\n') });
341
+ db.close();
342
+ return { session: sessionId, project: projectRoot, steps, cycleError: { report } };
343
+ }
344
+ }
305
345
  if (opts.print) {
306
346
  const printLabel = stepLabel('Writing components.json');
307
347
  const printArgs = ['print', 'components', '--out', componentsPath];
@@ -331,7 +371,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
331
371
  progressWriter(`${printLabel}✓ components.json written (${(durationMs / 1000).toFixed(1)}s)`);
332
372
  steps.push({ step: 'print components', status: 'complete', durationMs });
333
373
  }
334
- // ── Step 4/5: apply push ─────────────────────────────────────────────────
335
374
  const applyLabelText = opts.spaceId && opts.environmentId
336
375
  ? `Applying changes to Space: ${opts.spaceId} Environment: ${opts.environmentId}`
337
376
  : 'Applying changes to Contentful';
@@ -361,10 +400,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
361
400
  '--cma-token',
362
401
  opts.cmaToken,
363
402
  ];
364
- // Components are stored under the extract session ID (generate command uses resolveSessionId
365
- // which returns the passed --session value, i.e. the extract session). Pass that directly so
366
- // apply push reads from the DB without needing a components.json file.
367
- // Fall back to --components so the step fails with a clear error rather than a generic one.
368
403
  if (extractSessionId) {
369
404
  pushArgs.push('--session', extractSessionId);
370
405
  }
@@ -379,28 +414,20 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
379
414
  pushArgs.push('--host', opts.host);
380
415
  if (opts.verbose)
381
416
  pushArgs.push('--verbose');
382
- pushArgs.push('--yes'); // always non-interactive in subprocess context
417
+ pushArgs.push('--yes');
383
418
  const pushStepId = createStep(db, sessionId, 'apply push', {
384
419
  components: componentsPath,
385
420
  });
386
421
  const t0 = Date.now();
387
422
  let r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
388
- // Bounded retry loop: on a preview-phase 422 with a parseable ValidationFailed
389
- // body, exclude the offending components and re-run apply push.
390
423
  const excludedByRetry = [];
391
424
  let validationRetryCount = 0;
392
425
  while (validationRetryCount < MAX_VALIDATION_RETRIES && isPreviewValidationError(r) && extractSessionId) {
393
426
  const offenders = parseOffendingComponentNames(r.stderr + r.stdout);
394
427
  if (offenders.length === 0)
395
- break; // unparseable body — give up and surface original error
428
+ break;
396
429
  process.stderr.write(`[retry ${validationRetryCount + 1}/${MAX_VALIDATION_RETRIES}] Preview validation failed — excluding ${offenders.join(', ')} and retrying\n`);
397
430
  excludedByRetry.push(...offenders);
398
- // No --select-all here: that would route through runNonInteractive's
399
- // rebuild path, which DELETEs all rows and re-inserts with default
400
- // status='extracted' — wiping the post-`generate components` state
401
- // (status='generated' + raw_props.cdf_type) the next apply push reads.
402
- // The bare --exclude-components form takes the rejectComponentsByName
403
- // early-return: pure UPDATE, no rebuild.
404
431
  const rejectArgs = [
405
432
  'analyze',
406
433
  'select',
@@ -411,7 +438,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
411
438
  ];
412
439
  const rejectResult = await runStep(rejectArgs, cliPath);
413
440
  if (rejectResult.exitCode !== 0)
414
- break; // selection step failed — give up
441
+ break;
415
442
  r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
416
443
  validationRetryCount++;
417
444
  }
@@ -433,6 +460,83 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
433
460
  ? (pushResult.componentTypes?.failed ?? 0) + (pushResult.designTokens?.failed ?? 0)
434
461
  : Number(/(\d+) failed/.exec(r.stdout + r.stderr)?.[1] ?? 0);
435
462
  const totalPushed = created + updated + failed;
463
+ if (isSlotCycleError(r)) {
464
+ const report = extractCycleReport(r.stderr);
465
+ if (opts.autoRejectCycles && extractSessionId) {
466
+ const cycleNames = parseCycleComponentNames(report);
467
+ if (cycleNames.length > 0) {
468
+ process.stderr.write(`[cycle-retry] Slot cycle detected — excluding ${cycleNames.join(', ')} and retrying\n`);
469
+ const rejectArgs = [
470
+ 'analyze',
471
+ 'select',
472
+ '--session',
473
+ extractSessionId,
474
+ '--exclude-components',
475
+ cycleNames.join(','),
476
+ ];
477
+ const rejectResult = await runStep(rejectArgs, cliPath);
478
+ if (rejectResult.exitCode === 0) {
479
+ const retryT0 = Date.now();
480
+ const retryR = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
481
+ const retryDurationMs = Date.now() - t0 + (Date.now() - retryT0);
482
+ if (isSlotCycleError(retryR)) {
483
+ const retryReport = extractCycleReport(retryR.stderr);
484
+ updateStep(db, pushStepId, 'failed', {}, retryR.stderr);
485
+ progressWriter(`${pushLabel}✗ failed (${(retryDurationMs / 1000).toFixed(1)}s)`);
486
+ progressWriter('Slot cycle detected after auto-reject retry. The manifest still contains circular slot references.');
487
+ for (const line of retryReport)
488
+ progressWriter(line);
489
+ steps.push(buildPushStepResult({
490
+ created,
491
+ updated,
492
+ failed,
493
+ durationMs: retryDurationMs,
494
+ stderr: retryR.stderr,
495
+ excludedByRetry: [...excludedByRetry, ...cycleNames],
496
+ totalFailure: true,
497
+ }));
498
+ db.close();
499
+ return { session: sessionId, project: projectRoot, steps, cycleError: { report: retryReport } };
500
+ }
501
+ if (retryR.exitCode !== 0) {
502
+ updateStep(db, pushStepId, 'failed', {}, retryR.stderr);
503
+ progressWriter(`${pushLabel}✗ failed (${(retryDurationMs / 1000).toFixed(1)}s)`);
504
+ steps.push(buildPushStepResult({
505
+ created,
506
+ updated,
507
+ failed,
508
+ durationMs: retryDurationMs,
509
+ stderr: retryR.stderr,
510
+ excludedByRetry: [...excludedByRetry, ...cycleNames],
511
+ totalFailure: true,
512
+ }));
513
+ db.close();
514
+ return { session: sessionId, project: projectRoot, steps };
515
+ }
516
+ excludedByRetry.push(...cycleNames);
517
+ r = retryR;
518
+ }
519
+ }
520
+ }
521
+ if (isSlotCycleError(r)) {
522
+ updateStep(db, pushStepId, 'failed', {}, r.stderr);
523
+ progressWriter(`${pushLabel}✗ failed (${(durationMs / 1000).toFixed(1)}s)`);
524
+ progressWriter('Slot cycle detected. The manifest contains circular slot references that would block the push.');
525
+ for (const line of report)
526
+ progressWriter(line);
527
+ steps.push(buildPushStepResult({
528
+ created,
529
+ updated,
530
+ failed,
531
+ durationMs,
532
+ stderr: r.stderr,
533
+ excludedByRetry,
534
+ totalFailure: true,
535
+ }));
536
+ db.close();
537
+ return { session: sessionId, project: projectRoot, steps, cycleError: { report } };
538
+ }
539
+ }
436
540
  if (r.exitCode !== 0 && (totalPushed === 0 || failed === totalPushed)) {
437
541
  // Total failure — nothing was pushed
438
542
  updateStep(db, pushStepId, 'failed', {}, r.stderr);
@@ -0,0 +1,18 @@
1
+ import type { CDFComponentEntry } from '@contentful/experience-design-system-types';
2
+ /**
3
+ * Atomic-mode normalization (spec T8, option a): keep every slot's structure
4
+ * but drop its `$allowedComponents` composition constraint. Applied over the
5
+ * loaded component array regardless of source (`--session` or `--components`),
6
+ * so the atomic bypass cannot leak a live composition link into the pushed
7
+ * manifest. Starving `$allowedComponents` at this single point also makes
8
+ * slot-cycle detection structurally return zero (see T12).
9
+ *
10
+ * Returns a new array with cloned entries; inputs are not mutated.
11
+ */
12
+ export declare function stripAllowedComponents(components: Array<{
13
+ key: string;
14
+ entry: CDFComponentEntry;
15
+ }>): Array<{
16
+ key: string;
17
+ entry: CDFComponentEntry;
18
+ }>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Atomic-mode normalization (spec T8, option a): keep every slot's structure
3
+ * but drop its `$allowedComponents` composition constraint. Applied over the
4
+ * loaded component array regardless of source (`--session` or `--components`),
5
+ * so the atomic bypass cannot leak a live composition link into the pushed
6
+ * manifest. Starving `$allowedComponents` at this single point also makes
7
+ * slot-cycle detection structurally return zero (see T12).
8
+ *
9
+ * Returns a new array with cloned entries; inputs are not mutated.
10
+ */
11
+ export function stripAllowedComponents(components) {
12
+ return components.map(({ key, entry }) => {
13
+ if (!entry.$slots)
14
+ return { key, entry };
15
+ const $slots = {};
16
+ for (const [slotName, slotDef] of Object.entries(entry.$slots)) {
17
+ const { $allowedComponents: _dropped, ...rest } = slotDef;
18
+ $slots[slotName] = rest;
19
+ }
20
+ return { key, entry: { ...entry, $slots } };
21
+ });
22
+ }