@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,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { PALETTE } from '../../analyze/select/tui/theme.js';
3
4
  import { Box, Text, useStdout } from 'ink';
4
5
  import { join, resolve } from 'node:path';
5
6
  import { appendFileSync, writeFileSync } from 'node:fs';
@@ -33,6 +34,10 @@ import { nextStateAfterPrint } from './run-print-files-helpers.js';
33
34
  import { PushDecisionGateStep } from './steps/PushDecisionGateStep.js';
34
35
  import { chooseGateAction } from './push-decision-gate-helpers.js';
35
36
  import { ImportApiClient, ApiError } from '../../apply/api-client.js';
37
+ import { detectSlotCycles, extractComponentsFromManifest, formatSlotCycleReport } from '../../apply/command.js';
38
+ import { findSlotCycles } from '../../analyze/cycle-detection.js';
39
+ import { buildComponentGraph } from '../../analyze/slot-graph.js';
40
+ import { parseEdsiError, formatParsedEdsiError } from '../../apply/error-parser.js';
36
41
  import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
37
42
  import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
38
43
  import { spawnGenerateChild } from './spawn-generate.js';
@@ -49,14 +54,12 @@ import { checkAgentAuth } from '../../generate/agent-runner.js';
49
54
  import { normalizePath } from '../path-utils.js';
50
55
  import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../../host-utils.js';
51
56
  import { writeExperiencesCredentials } from '../../credentials-store.js';
52
- import { nextStepAfterScopeGate, nextStepAfterCredentialsValidated, shouldBypassPreview, buildSkippedPreviewTransition, shouldRefusePush, buildSkippedPushTransition, shouldSkipFinalReviewAfterCredentials, resolveNoCacheForGenerate, } from './wizard-state-transitions.js';
57
+ import { nextStepAfterScopeGate, nextStepAfterCredentialsValidated, shouldBypassPreview, buildSkippedPreviewTransition, shouldRefusePush, buildSkippedPushTransition, shouldSkipFinalReviewAfterCredentials, resolveNoCacheForGenerate, resolveCycleGateAction, } from './wizard-state-transitions.js';
58
+ import { computeCycleAutoRejectTargets } from '../cycle-auto-reject.js';
53
59
  function findCliPath() {
54
60
  return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'bin', 'cli.js');
55
61
  }
56
62
  export function buildSelectAgentArgs(opts) {
57
- // Feature 3: the wizard auto-filter run should never fail-loud on validation
58
- // errors — those components surface in the AI-excluded section with a
59
- // synthesized reason, not an exit-1 abort. So we always pass --exclude-invalid.
60
63
  const args = ['analyze', 'select-agent', '--agent', opts.agent, '--session', opts.sessionId, '--exclude-invalid'];
61
64
  if (opts.model)
62
65
  args.push('--model', opts.model);
@@ -67,11 +70,6 @@ export function buildSelectAgentArgs(opts) {
67
70
  return args;
68
71
  }
69
72
  export function parseAutoFilterProgressLine(line) {
70
- // Format: progress=select-agent:N/M:<decision>:<name>:<url-encoded-reason>
71
- // Name and reason CANNOT contain `:` raw (name comes from component identifier
72
- // which forbids colons; reason is URL-encoded). We split with a limit so any
73
- // stray colon inside the URL-encoded reason wouldn't matter — but the encoder
74
- // also handles `:` as `%3A` so this is defensive.
75
73
  const prefix = 'progress=select-agent:';
76
74
  if (!line.startsWith(prefix))
77
75
  return null;
@@ -106,10 +104,6 @@ export function parseAutoFilterProgressLine(line) {
106
104
  };
107
105
  }
108
106
  export function buildGenerateComponentsArgs(opts) {
109
- // Default behavior preserves the SHA cache (re-runs only re-classify
110
- // changed components). The operator opts into a full re-classify pass via
111
- // `experiences import --no-cache`, which forwards through to the spawned
112
- // `generate components` subprocess. See wizard-cache.test.ts.
113
107
  const args = ['generate', 'components', '--agent', opts.agent, '--session', opts.sessionId];
114
108
  if (opts.tokensPath)
115
109
  args.push('--tokens', opts.tokensPath);
@@ -134,12 +128,6 @@ function runCli(args) {
134
128
  });
135
129
  });
136
130
  }
137
- /**
138
- * Parse the `wrote tokens.json (N tokens)` confirmation line emitted by
139
- * `experiences print tokens`. Returns 0 when the line cannot be parsed —
140
- * the wizard still records the run; downstream consumers should treat 0 as
141
- * "unknown" rather than "no tokens".
142
- */
143
131
  export function parsePrintTokensCount(stdout) {
144
132
  const m = /\((\d+)\s+token/.exec(stdout);
145
133
  return m ? Number(m[1]) : 0;
@@ -148,10 +136,9 @@ const WIZARD_LOG = join(tmpdir(), 'experiences-import-wizard.log');
148
136
  function logStep(entry) {
149
137
  const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
150
138
  appendFileSync(WIZARD_LOG, line);
151
- // Tee into the unified debug log when enabled — cheap no-op otherwise.
152
139
  getDebugLogger().event('wizard', 'step', entry);
153
140
  }
154
- export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope = false, noCache = false, autoFilter = true, livePreview = true, noPush = false, noSave = false, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, } = {}) {
141
+ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope = false, autoRejectCycles = false, compositionMode = 'atomic', compositionMap, compositionAgent = false, compositionAgentMode, compositionRefresh = false, generateMap, promptOverrides, noCache = false, autoFilter = true, livePreview = true, noPush = false, noSave = false, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, } = {}) {
155
142
  const defaultConfiguredHost = toConfiguredHost(host || process.env['EDS_HOST']) ?? DEFAULT_CONFIGURED_HOST;
156
143
  const resolveWizardHost = (hostValue) => hostValue || defaultConfiguredHost;
157
144
  const { stdout } = useStdout();
@@ -166,39 +153,19 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
166
153
  extractSessionId: null,
167
154
  tokensPath: '',
168
155
  });
169
- // Feature 3: holds the spawned `analyze select-agent` subprocess so the
170
- // scope-gate's `q` (during running) can SIGTERM it for cancellation.
156
+ // Set at finalize when zero components are accepted (confirmed via the
157
+ // FinalizeDialog warning). Read by preview/push so buildManifest emits an
158
+ // empty-but-present components manifest → server deletes all. A ref (not
159
+ // state) so the async preview/push closures see the confirmed value.
160
+ const allowEmptyDeleteAllRef = useRef(false);
171
161
  const autoFilterChildRef = useRef(null);
172
- // Promise that resolves when the auto-filter subprocess fully exits. Used
173
- // by `cancelAutoFilterAndWait` so scope-gate confirm can guarantee its
174
- // snapshot write goes AFTER the subprocess's last write.
175
162
  const autoFilterDonePromiseRef = useRef(null);
176
- // Background `generate components` subprocess spawned from scope-gate
177
- // confirm so its LLM call overlaps with the operator's credential entry.
178
- // Held in refs so we can SIGTERM on credential failure / quit, and await
179
- // the promise from inside `validateCredentials` once creds come back OK.
180
163
  const generateChildRef = useRef(null);
181
164
  const generatePromiseRef = useRef(null);
182
- // Modify-entry short-circuit: when the launcher passes seed session IDs
183
- // and `initialStep: 'final-review'`, skip the welcome/token-input bootstrap
184
- // and land directly on the post-generate review screen. The DB-backed
185
- // GenerateReviewStep loads its data off `state.extractSessionId`, so all
186
- // we need to do here is seed the IDs and the step.
187
165
  const modifyEntryReady = !!seedExtractSessionId && initialStep === 'final-review';
188
- // Push-from-picker entry: the run-picker's Push action mounts the wizard
189
- // with seeded session IDs and `initialStep: 'push-from-picker'` so the
190
- // operator sees the same preview + push UX as a fresh import. Skips
191
- // welcome, token-input, scope-gate, final-review, and push-decision-gate;
192
- // an effect below dispatches `runPreview` on mount which drives
193
- // previewing → pushing → done.
194
166
  const pushFromPickerReady = !!seedExtractSessionId && initialStep === 'push-from-picker';
195
- // Headless raw-tokens entry: when the operator passed `--raw-tokens <path>`
196
- // the CLI seeds this prop. Skip welcome + token-input and land on the
197
- // `generating-tokens` step which already drives the token-classification
198
- // subprocess off `state.rawTokensPath`. Modify-entry wins if both are set.
199
167
  const rawTokensEntryReady = !modifyEntryReady && !pushFromPickerReady && !!initialRawTokensPath;
200
- const isFreshSession = !seedExtractSessionId;
201
- const effectiveNoCache = resolveNoCacheForGenerate({ isFreshSession, cliNoCache: noCache });
168
+ const effectiveNoCache = resolveNoCacheForGenerate({ cliNoCache: noCache });
202
169
  const initialStepResolved = modifyEntryReady
203
170
  ? 'final-review'
204
171
  : pushFromPickerReady
@@ -209,7 +176,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
209
176
  ? 'token-input'
210
177
  : 'welcome';
211
178
  const initialOutDir = initialProjectPath ? join(resolve(initialProjectPath), '.contentful') : '';
212
- const initialTokensPath = (modifyEntryReady || pushFromPickerReady) && initialOutDir ? join(initialOutDir, 'tokens.json') : '';
179
+ // Only point at tokens.json when the run actually had a tokens session. A run
180
+ // saved without tokens has no tokens.json on disk, so assuming one exists made
181
+ // modify/push-from-picker fail with "file not found: .../tokens.json".
182
+ const initialTokensPath = (modifyEntryReady || pushFromPickerReady) && initialOutDir && seedTokenSessionId
183
+ ? join(initialOutDir, 'tokens.json')
184
+ : '';
213
185
  const [state, setState] = useState({
214
186
  step: modifyEntryReady || rawTokensEntryReady || pushFromPickerReady
215
187
  ? initialStepResolved
@@ -236,6 +208,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
236
208
  renamedSlotsCount: 0,
237
209
  generateProgress: null,
238
210
  extractProgress: null,
211
+ compositionPhase: null,
239
212
  componentsPath: '',
240
213
  spaceId: initialSpaceId,
241
214
  environmentId: initialEnvironmentId,
@@ -266,6 +239,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
266
239
  lastRunId: null,
267
240
  finalizeErrorBanner: null,
268
241
  finalReviewPassed: modifyEntryReady || pushFromPickerReady,
242
+ allowEmptyDeleteAll: false,
269
243
  });
270
244
  useEffect(() => {
271
245
  sessionRef.current = {
@@ -320,7 +294,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
320
294
  logStep({ update: sanitized });
321
295
  setState((prev) => ({ ...prev, ...partial }));
322
296
  };
323
- // ── Agent auth pre-flight ───────────────────────────────────────────────────────
324
297
  const runAgentAuthCheck = async (nextStep) => {
325
298
  const authCheckStepNumber = nextStep === 'generating-tokens' ? 1 : state.tokensPath ? 4 : 3;
326
299
  update({ step: 'checking-claude-auth', authCheckStepNumber });
@@ -347,7 +320,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
347
320
  update({ step: nextStep });
348
321
  return true;
349
322
  };
350
- // ── Step runners ────────────────────────────────────────────────────────
351
323
  const runGenerateTokens = async (rawTokensPath, outDir) => {
352
324
  const result = await new Promise((res) => {
353
325
  const tokenArgs = [findCliPath(), 'generate', 'tokens', '--agent', state.agent, '--raw-tokens', rawTokensPath];
@@ -392,9 +364,28 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
392
364
  };
393
365
  const runExtract = async (projectPath) => {
394
366
  const outDir = join(resolve(projectPath), '.contentful');
395
- update({ step: 'extracting', outDir, extractProgress: null });
367
+ update({ step: 'extracting', outDir, extractProgress: null, compositionPhase: null });
396
368
  const r = await new Promise((res) => {
397
- const child = spawn('node', [findCliPath(), 'analyze', 'extract', '--project', projectPath]);
369
+ const extractArgs = [findCliPath(), 'analyze', 'extract', '--project', projectPath];
370
+ if (compositionMode === 'composite') {
371
+ extractArgs.push('--composite');
372
+ if (compositionMap)
373
+ extractArgs.push('--composition-map', compositionMap);
374
+ if (compositionAgent)
375
+ extractArgs.push('--composition-agent');
376
+ if (compositionAgentMode)
377
+ extractArgs.push('--composition-agent-mode', compositionAgentMode);
378
+ if (compositionRefresh)
379
+ extractArgs.push('--composition-refresh');
380
+ if (generateMap)
381
+ extractArgs.push('--generate-map', generateMap);
382
+ for (const p of promptOverrides ?? [])
383
+ extractArgs.push('--prompt', p);
384
+ // Composition resolution uses the same agent the user picked for the run.
385
+ if (state.agent)
386
+ extractArgs.push('--agent', state.agent);
387
+ }
388
+ const child = spawn('node', extractArgs);
398
389
  let stdout = '';
399
390
  let stderr = '';
400
391
  child.stdout.on('data', (d) => {
@@ -432,6 +423,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
432
423
  componentsFound,
433
424
  },
434
425
  }));
426
+ continue;
427
+ }
428
+ const compositionMatch = /^progress=composition:(.+)$/.exec(line.trim());
429
+ if (compositionMatch) {
430
+ const phase = compositionMatch[1];
431
+ setState((prev) => ({ ...prev, compositionPhase: phase }));
435
432
  }
436
433
  }
437
434
  });
@@ -470,11 +467,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
470
467
  autoFilterDonePromiseRef.current = runAutoFilter(extractSessionId);
471
468
  }
472
469
  };
473
- // Feature 3: spawn `analyze select-agent` after extract and stream decisions
474
- // into wizard state via stderr progress lines. The subprocess writes
475
- // `raw_components.status` + `reject_reason` itself (see Task 2), so the
476
- // scope-gate UI re-loads via `loadScopeComponents` to get fresh data on every
477
- // render — but we also keep a memory-side `aiDecisions` map for streaming UX.
478
470
  const runAutoFilter = (sessionId) => {
479
471
  return new Promise((res) => {
480
472
  const args = buildSelectAgentArgs({
@@ -507,12 +499,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
507
499
  }));
508
500
  }
509
501
  });
510
- // Use 'close' (not 'exit') so the status flip happens after the child's
511
- // stdio AND its inherited SQLite WAL/SHM file handles have been fully
512
- // released by the OS. 'exit' fires the instant the process terminates;
513
- // setting state then triggers a wizard re-render that re-opens
514
- // pipeline.db from the scope-gate step, and the lock from the dead
515
- // child's still-mapped WAL handle surfaces as "database is locked".
516
502
  child.on('close', (code, signal) => {
517
503
  autoFilterChildRef.current = null;
518
504
  if (signal === 'SIGTERM') {
@@ -553,16 +539,10 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
553
539
  }
554
540
  }
555
541
  };
556
- // Variant that returns a Promise resolving when the subprocess has fully
557
- // exited. Used by scope-gate confirm to guarantee operator-write-last
558
- // ordering on the review-state snapshot (PR #43 race fix).
559
542
  const cancelAutoFilterAndWait = async () => {
560
543
  const child = autoFilterChildRef.current;
561
544
  const donePromise = autoFilterDonePromiseRef.current;
562
545
  if (!child || child.killed) {
563
- // Subprocess already gone (or never started). If the Promise is still
564
- // pending — e.g. exit handler hasn't fired yet — await it; otherwise
565
- // resolve immediately.
566
546
  if (donePromise)
567
547
  await donePromise;
568
548
  return;
@@ -576,9 +556,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
576
556
  if (donePromise)
577
557
  await donePromise;
578
558
  };
579
- // Cancel a running generation prefetch (SIGTERM the child + clear refs +
580
- // reset state). Best-effort — if the child has already exited, this is a
581
- // no-op aside from clearing the prefetch status.
582
559
  const cancelGeneratePrefetch = () => {
583
560
  const child = generateChildRef.current;
584
561
  if (child && !child.killed) {
@@ -598,9 +575,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
598
575
  generateProgress: null,
599
576
  }));
600
577
  };
601
- // Spawn `generate components` in the background. Wires up the same
602
- // stderr-progress streaming as `runGenerate` but stores the in-flight
603
- // promise in a ref so the post-credentials path can await it.
604
578
  const startGeneratePrefetch = (extractSessionId, tokensPath) => {
605
579
  const args = [
606
580
  findCliPath(),
@@ -633,10 +607,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
633
607
  }));
634
608
  donePromise
635
609
  .then((result) => {
636
- // Clear the child ref regardless of how it ended.
637
610
  generateChildRef.current = null;
638
611
  if (result.signal === 'SIGTERM') {
639
- // Caller already reset state via cancelGeneratePrefetch.
640
612
  return;
641
613
  }
642
614
  if (result.exitCode !== 0) {
@@ -655,8 +627,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
655
627
  const generatedCount = countMatch ? Number(countMatch[1]) : 0;
656
628
  const renamedMatch = /^renamed-slots:\s*(\d+)$/m.exec(result.stdout);
657
629
  const renamedSlotsCount = renamedMatch ? Number(renamedMatch[1]) : 0;
658
- // Persist generated state but DO NOT auto-advance — the credentials
659
- // path will pick this up via `advanceAfterCredentialsValidated`.
660
630
  setState((prev) => ({
661
631
  ...prev,
662
632
  generateSessionId,
@@ -727,7 +697,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
727
697
  update({ generatedAcceptedCount, step: 'credentials' });
728
698
  };
729
699
  const runEditFromPreview = async () => {
730
- // Post-preview edits land in the unified final-review screen.
731
700
  update({ step: 'final-review' });
732
701
  };
733
702
  const runSkipValidationErrorsAndRetry = async (errors) => {
@@ -738,11 +707,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
738
707
  const advanceWithCredentials = (spaceId, environmentId, cmaToken, host) => {
739
708
  const resolvedHost = resolveWizardHost(host);
740
709
  credentialsRef.current = { spaceId, environmentId, cmaToken };
741
- // The dedicated `credential-test-gate` screen was dropped — credentials are
742
- // now always validated immediately after they're persisted. The inline
743
- // `credentialsValidating` status (PR #54) handles the visual feedback while
744
- // the API ping is in flight. The literal `'credential-test-gate'` is kept
745
- // in the WizardStep union for back-compat, but is never set as a step.
746
710
  void validateCredentials(spaceId, environmentId, cmaToken, resolvedHost);
747
711
  };
748
712
  const confirmCredentials = async (spaceId, environmentId, cmaToken, host) => {
@@ -769,8 +733,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
769
733
  }
770
734
  };
771
735
  const validateCredentials = async (spaceId, environmentId, cmaToken, host) => {
772
- // Inline validation: stay on the credentials step, flip the validating
773
- // boolean so CredentialsStep locks input + renders an inline status.
774
736
  update({ step: 'credentials', credentialsValidating: true, credentialsError: '' });
775
737
  try {
776
738
  const resolvedHost = resolveWizardHost(host);
@@ -786,9 +748,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
786
748
  }
787
749
  catch (e) {
788
750
  if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
789
- // Validation failure: cancel any in-flight generate prefetch so we
790
- // don't leave an orphaned subprocess running after the operator backs
791
- // out (Risk #1 in the spec).
792
751
  cancelGeneratePrefetch();
793
752
  update({ step: 'credentials', credentialsValidating: false, credentialsError: e.message });
794
753
  return;
@@ -804,10 +763,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
804
763
  });
805
764
  }
806
765
  };
807
- // Branch after credentials are known good (validated, or skipped via the
808
- // credential-test-gate skip path). Either runs the generator (if there are
809
- // accepted components) or jumps straight to push-decision-gate (if scope
810
- // rejected everything but tokens/removals still need to be pushed).
811
766
  const advanceAfterCredentialsValidated = async () => {
812
767
  if (shouldSkipFinalReviewAfterCredentials(state)) {
813
768
  update({ step: 'push-decision-gate' });
@@ -824,25 +779,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
824
779
  });
825
780
  return;
826
781
  }
827
- // Prefetch path: if a background generate is already running or has
828
- // already completed, await it (or use its result) instead of spawning
829
- // a second LLM call. Transition to 'generating' first so the operator
830
- // sees the familiar RunningStep progress screen while we wait —
831
- // matching the no-prefetch flow's visual.
832
782
  const inflight = generatePromiseRef.current;
833
783
  if (inflight) {
834
784
  update({ step: 'generating' });
835
785
  const result = await inflight;
836
786
  generatePromiseRef.current = null;
837
787
  if (result.exitCode === 0 && result.signal !== 'SIGTERM') {
838
- // The donePromise.then() handler already populated
839
- // generateSessionId / generatedCount in state. Advance to
840
- // final-review using the latest values via functional setState so
841
- // we read whichever update landed last.
842
788
  setState((prev) => ({ ...prev, step: 'final-review' }));
843
789
  return;
844
790
  }
845
- // Prefetch failed — fall through to retry via a fresh runGenerate.
846
791
  }
847
792
  if (await runAgentAuthCheck('generating')) {
848
793
  void runGenerate(sid, state.tokensPath, state.acceptedCount);
@@ -852,11 +797,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
852
797
  update({ step: 'push-decision-gate' });
853
798
  };
854
799
  const runPreview = async (extractSessionId, tokensPath, spaceId, environmentId, cmaToken, host) => {
855
- // Skip-credentials short-circuit. When the operator pressed `s` on the
856
- // credentials screen, we never got a working token — calling
857
- // previewImport would 401/403 (or worse, send a half-formed manifest
858
- // somewhere). Jump straight to the push-decision-gate; Task 3 disables
859
- // the push options downstream.
860
800
  if (shouldBypassPreview(state)) {
861
801
  update(buildSkippedPreviewTransition());
862
802
  return;
@@ -885,14 +825,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
885
825
  if (tokensPath) {
886
826
  tokens = await readTokensFromPath('tokens', tokensPath);
887
827
  }
888
- let manifest = buildManifest(components, tokens);
828
+ let manifest = buildManifest(components, tokens, { deleteAllComponents: allowEmptyDeleteAllRef.current });
889
829
  let preview = await client.previewImport(manifest);
890
- // Second pass: seed CDF from false removals + preserve defaults from changed items
891
830
  if (extractSessionId) {
892
831
  let needsRepreview = false;
893
832
  const db = openPipelineDb();
894
833
  try {
895
- // Seed CDF for components server thinks are removed but exist locally
896
834
  if (preview.components.removed.length > 0) {
897
835
  const localNames = new Set(db.prepare(`SELECT name FROM raw_components WHERE session_id = ?`).all(extractSessionId).map((r) => r.name));
898
836
  const falseRemovals = preview.components.removed.filter((r) => localNames.has(r.name));
@@ -902,7 +840,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
902
840
  needsRepreview = true;
903
841
  }
904
842
  }
905
- // Preserve server-side defaults so we don't accidentally propose removing them
906
843
  if (preview.components.changed.length > 0) {
907
844
  const seededDefaults = seedDefaultsFromChangedItems(db, extractSessionId, preview.components.changed);
908
845
  if (seededDefaults > 0)
@@ -910,7 +847,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
910
847
  }
911
848
  if (needsRepreview) {
912
849
  components = loadCDFComponents(db, extractSessionId);
913
- manifest = buildManifest(components, tokens);
850
+ manifest = buildManifest(components, tokens, { deleteAllComponents: allowEmptyDeleteAllRef.current });
914
851
  preview = await client.previewImport(manifest);
915
852
  }
916
853
  }
@@ -918,16 +855,19 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
918
855
  db.close();
919
856
  }
920
857
  }
921
- // INTEG-4411 refined: preview-aware finalize guard. If the resulting
922
- // push would be a pure no-op across every diff bucket, don't send an
923
- // empty manifest to EDSI — route back to `final-review` with an inline
924
- // banner. Accepted components, rejections that remove server-side
925
- // components, and token-only diffs all keep this branch from firing.
926
858
  if (isEmptyPreview(preview)) {
859
+ // No-op push: the accepted set already matches the target space, so
860
+ // there is nothing to create/update/remove. Route to a terminal "done"
861
+ // state rather than bouncing back to final-review — the review screen
862
+ // resets every component to needs-review on remount, which otherwise
863
+ // traps the operator in an accept → empty-preview → reset loop.
927
864
  update({
928
- step: 'final-review',
929
- finalizeErrorBanner: 'Nothing to push — accept a component, reject a component that exists in Contentful, or quit.',
865
+ step: 'done',
930
866
  serverPreview: preview,
867
+ pushResult: {
868
+ componentTypes: { created: 0, updated: 0, removed: 0, failed: 0 },
869
+ designTokens: { created: 0, updated: 0, removed: 0, failed: 0 },
870
+ },
931
871
  ...clearedValidationErrorState(),
932
872
  });
933
873
  return;
@@ -950,8 +890,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
950
890
  catch {
951
891
  /* non-JSON */
952
892
  }
953
- // Space-level config errors (e.g. "Design system public CMA is disabled") cannot be
954
- // fixed by re-entering credentials — send to error screen.
955
893
  if (bodyMsg && /disabled/i.test(bodyMsg)) {
956
894
  update({
957
895
  step: 'error',
@@ -965,9 +903,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
965
903
  return;
966
904
  }
967
905
  if (e.status === 404) {
968
- // 404 from previewImport means the design systems endpoint doesn't exist for this
969
- // space/environment (typically wrong --host or wrong space/env). Not a credentials
970
- // problem — show a clear error instead of looping.
971
906
  update({
972
907
  step: 'error',
973
908
  errorStep: 'apply preview',
@@ -989,7 +924,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
989
924
  });
990
925
  return;
991
926
  }
992
- // 'unparseable' and 'not-422' both fall through to the generic error branch below.
993
927
  update({
994
928
  step: 'error',
995
929
  errorStep: 'apply preview',
@@ -1008,12 +942,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1008
942
  }
1009
943
  };
1010
944
  const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, preview) => {
1011
- // Skip-credentials defensive guard. The push-decision-gate disables
1012
- // push-emitting choices when `credentialsSkipped` is true, so we
1013
- // should never get here in practice. But if a state-machine bug or
1014
- // future regression ever did, refuse to issue the API call — there
1015
- // is no validated token and the operator explicitly opted out of
1016
- // push. Route back to the local-save (print-gate) path instead.
1017
945
  if (shouldRefusePush(state)) {
1018
946
  update(buildSkippedPushTransition());
1019
947
  return;
@@ -1034,6 +962,16 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1034
962
  return;
1035
963
  }
1036
964
  }
965
+ const cycles = detectSlotCycles(extractComponentsFromManifest(manifest));
966
+ if (cycles.length > 0) {
967
+ update({
968
+ step: 'error',
969
+ errorStep: 'apply push',
970
+ errorMessage: formatSlotCycleReport(cycles).join('\n'),
971
+ errorAllowCredentialRetry: false,
972
+ });
973
+ return;
974
+ }
1037
975
  update({ step: 'pushing', pushProgress: null });
1038
976
  try {
1039
977
  const resolvedHost = resolveWizardHost(host);
@@ -1067,9 +1005,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1067
1005
  if (s) {
1068
1006
  const done = s.total - s.pending;
1069
1007
  const items = op.items ?? [];
1070
- // Best-effort fresh signal: pick the most recently succeeded item
1071
- // (the API does not surface an in-progress status today). Falls
1072
- // back to null and the PushingStep hides the line.
1073
1008
  const lastDone = items.length > 0 ? items[items.length - 1] : null;
1074
1009
  const current = lastDone && lastDone.status === 'succeeded' ? lastDone.id : null;
1075
1010
  update({
@@ -1124,27 +1059,43 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1124
1059
  };
1125
1060
  }
1126
1061
  else {
1127
- // API didn't return items fall back to summary + preview counts
1062
+ const summary = operation.summary ?? { total: 0, pending: 0, succeeded: 0, failed: 0 };
1063
+ const anyFailure = summary.failed > 0 || operation.sys.status === 'failed' || operation.sys.status === 'partial';
1128
1064
  pushResult = {
1129
1065
  componentTypes: {
1130
- created: preview?.components.new.length ?? 0,
1131
- updated: preview?.components.changed.length ?? 0,
1132
- removed: preview?.components.removed.length ?? 0,
1133
- failed: 0,
1066
+ created: anyFailure ? 0 : (preview?.components.new.length ?? 0),
1067
+ updated: anyFailure ? 0 : (preview?.components.changed.length ?? 0),
1068
+ removed: anyFailure ? 0 : (preview?.components.removed.length ?? 0),
1069
+ failed: summary.failed,
1134
1070
  },
1135
1071
  designTokens: {
1136
- created: preview?.tokens.new.length ?? 0,
1137
- updated: preview?.tokens.changed.length ?? 0,
1138
- removed: preview?.tokens.removed.length ?? 0,
1072
+ created: anyFailure ? 0 : (preview?.tokens.new.length ?? 0),
1073
+ updated: anyFailure ? 0 : (preview?.tokens.changed.length ?? 0),
1074
+ removed: anyFailure ? 0 : (preview?.tokens.removed.length ?? 0),
1139
1075
  failed: 0,
1140
1076
  },
1141
- summary: operation.summary,
1077
+ summary,
1142
1078
  };
1143
1079
  }
1144
1080
  update({ step: 'done', pushResult });
1145
1081
  }
1146
1082
  catch (e) {
1147
- const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : 'Push failed';
1083
+ let msg;
1084
+ if (e instanceof ApiError) {
1085
+ const parsed = parseEdsiError(e.body || e.message);
1086
+ msg = formatParsedEdsiError(parsed, {
1087
+ verbose: process.env['EDSI_VERBOSE_ERRORS'] === '1',
1088
+ raw: e.body,
1089
+ });
1090
+ if (!msg)
1091
+ msg = e.message;
1092
+ }
1093
+ else if (e instanceof Error) {
1094
+ msg = e.message;
1095
+ }
1096
+ else {
1097
+ msg = 'Push failed';
1098
+ }
1148
1099
  update({
1149
1100
  step: 'error',
1150
1101
  errorStep: 'apply push',
@@ -1159,6 +1110,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1159
1110
  const printArgs = ['print', 'components', '--out', componentsPath];
1160
1111
  if (extractSessionId)
1161
1112
  printArgs.push('--session', extractSessionId);
1113
+ if (opts.allowEmpty)
1114
+ printArgs.push('--allow-empty');
1162
1115
  const r = await runCli(printArgs);
1163
1116
  if (r.exitCode !== 0) {
1164
1117
  update({
@@ -1168,9 +1121,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1168
1121
  });
1169
1122
  return { ok: false };
1170
1123
  }
1171
- // Co-locate tokens.json with components.json. Without this, `--out-dir`
1172
- // would move components.json to the operator's chosen path while leaving
1173
- // tokens.json behind at <projectPath>/.contentful/tokens.json.
1174
1124
  let emittedTokensPath;
1175
1125
  let emittedTokenCount;
1176
1126
  if (opts.tokenSessionId) {
@@ -1198,11 +1148,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1198
1148
  const runSaveAndPush = async () => {
1199
1149
  await startSaveFlow({ skipGate: true, andPush: true });
1200
1150
  };
1201
- // ── Task 4: save-path orchestration ────────────────────────────────────────
1202
- // Wraps every `runPrintFiles` site. When `--out-dir` is set we skip the
1203
- // inline prompt and conflict gate entirely. Otherwise the wizard transitions
1204
- // to the path-prompt step; the operator's submit handler calls back into
1205
- // `proceedToWrite` (which may surface the conflict gate).
1206
1151
  const pendingSaveOptionsRef = useRef({});
1207
1152
  const startSaveFlow = async (opts = {}) => {
1208
1153
  pendingSaveOptionsRef.current = opts;
@@ -1234,20 +1179,14 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1234
1179
  const result = await runPrintFiles(extractSessionId, path, {
1235
1180
  ...(skipGate ? { skipGate: true } : {}),
1236
1181
  ...(state.tokenSessionId ? { tokenSessionId: state.tokenSessionId } : {}),
1182
+ ...(allowEmptyDeleteAllRef.current ? { allowEmpty: true } : {}),
1237
1183
  });
1238
1184
  if (!result.ok)
1239
1185
  return;
1240
- // Prefer the freshly emitted path/count (covers --out-dir); fall back to
1241
- // the values captured during the original generate-tokens step.
1242
1186
  const recordedTokensPath = result.tokensPath ?? (state.tokenSessionId ? tokensPath || null : null);
1243
1187
  const recordedTokenCount = result.tokenCount ?? state.tokenCount;
1244
1188
  if (result.ok) {
1245
- // Append a run record on every successful write. Best-effort: append
1246
- // failures must not break the wizard flow (they surface on stderr).
1247
1189
  try {
1248
- // Build the v3 fingerprints. Both are best-effort: if any step
1249
- // throws (missing source file, hash failure, db lookup error) we
1250
- // fall back to null fingerprints rather than aborting the save.
1251
1190
  let sourceFingerprint = null;
1252
1191
  let savedFingerprint = null;
1253
1192
  try {
@@ -1292,6 +1231,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1292
1231
  generateSessionId: state.generateSessionId,
1293
1232
  sourceFingerprint,
1294
1233
  savedFingerprint,
1234
+ compositionMode,
1295
1235
  });
1296
1236
  setState((prev) => ({ ...prev, lastRunId: record.id }));
1297
1237
  }
@@ -1303,7 +1243,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1303
1243
  void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
1304
1244
  }
1305
1245
  };
1306
- // ── Effect: kick off automatic steps ───────────────────────────────────────────────
1307
1246
  const tokenReuseChecked = useRef(false);
1308
1247
  useEffect(() => {
1309
1248
  if (state.step === 'generating-tokens') {
@@ -1326,21 +1265,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1326
1265
  });
1327
1266
  }
1328
1267
  catch {
1329
- // No existing tokens — need LLM to generate
1330
1268
  if (await runAgentAuthCheck('generating-tokens')) {
1331
1269
  void runGenerateTokens(state.rawTokensPath, state.outDir);
1332
1270
  }
1333
1271
  }
1334
1272
  })();
1335
1273
  }
1336
- }, [state.step]); // intentional: only re-run when step changes
1337
- // Push-from-picker entry: on mount, dispatch runPreview to jump through
1338
- // previewing → preview-gate → pushing → done. We DON'T wait for the
1339
- // operator to interact with any pre-preview screen; the run-picker Push
1340
- // action is a "click Push and watch it happen" flow. `preview-gate` still
1341
- // renders after runPreview resolves — that's the diff-review screen where
1342
- // the operator can confirm/quit/edit. We route straight past
1343
- // push-decision-gate (which is for save-vs-push decisions on a fresh run).
1274
+ }, [state.step]);
1344
1275
  const pushFromPickerDispatched = useRef(false);
1345
1276
  useEffect(() => {
1346
1277
  if (state.step !== 'push-from-picker')
@@ -1350,7 +1281,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1350
1281
  pushFromPickerDispatched.current = true;
1351
1282
  void runPreview(state.extractSessionId, state.tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
1352
1283
  }, [state.step]);
1353
- // ── Render ────────────────────────────────────────────────────────────────────────────
1354
1284
  const noQuitSteps = [
1355
1285
  'run-picker',
1356
1286
  'checking-claude-auth',
@@ -1364,7 +1294,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1364
1294
  'pushing',
1365
1295
  ];
1366
1296
  const hints = noQuitSteps.includes(state.step) ? [] : [{ key: 'q', label: 'quit' }];
1367
- // step count: tokens step adds 1, components steps add 2 (extract + generate)
1368
1297
  const hasTokens = !!state.tokensPath;
1369
1298
  const hasComponents = !state.skipComponents;
1370
1299
  const totalSteps = 3 + (hasTokens ? 1 : 0) + (hasComponents ? 2 : 0);
@@ -1376,10 +1305,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1376
1305
  update({ step: 'welcome' });
1377
1306
  return;
1378
1307
  }
1379
- // Push / modify routing exits the wizard back into the CLI
1380
- // surface so `replayRun` / `modifyRun` (which spin their own
1381
- // UI / spawn their own Ink trees) can take over. The CLI
1382
- // entry point in `command.ts` provides `onRunPicked`.
1383
1308
  onRunPicked?.(selection);
1384
1309
  }, onCancel: () => process.exit(0) }));
1385
1310
  case 'welcome':
@@ -1411,9 +1336,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1411
1336
  return (_jsx(PathValidationStep, { projectPath: state.projectPath, onConfirm: (path) => {
1412
1337
  void runExtract(path);
1413
1338
  }, onSkipComponents: () => {
1414
- // No components — only design tokens to push. Still gather creds
1415
- // first (unless --no-push, in which case there is nothing to do
1416
- // and we save files instead).
1417
1339
  if (noPush) {
1418
1340
  update({ skipComponents: true, acceptedCount: 0 });
1419
1341
  void startSaveFlow();
@@ -1433,11 +1355,29 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1433
1355
  else {
1434
1356
  extractDetail = 'Scanning...';
1435
1357
  }
1436
- return (_jsx(RunningStep, { stepNumber: hasTokens ? 2 : 1, totalSteps: totalSteps, title: "Extracting components", description: "I'm scanning your files and figuring out what components exist, what props they have, and how they're structured. This is fully automatic \u2014 sit tight.", detail: extractDetail }));
1358
+ const compositionDetail = (() => {
1359
+ const phase = state.compositionPhase;
1360
+ if (!phase)
1361
+ return undefined;
1362
+ if (phase === 'resolving')
1363
+ return 'Resolving composition mapping...';
1364
+ if (phase === 'cache-hit')
1365
+ return 'Composition mapping (cached)...';
1366
+ if (phase === 'authoring')
1367
+ return 'Writing a composition parser...';
1368
+ if (phase === 'parsing')
1369
+ return 'Running the composition parser...';
1370
+ if (phase.startsWith('agent:'))
1371
+ return `Resolving composition via ${phase.slice('agent:'.length)} agent...`;
1372
+ if (phase === 'done')
1373
+ return 'Composition mapping resolved ✓';
1374
+ return `Composition: ${phase}`;
1375
+ })();
1376
+ return (_jsx(RunningStep, { stepNumber: hasTokens ? 2 : 1, totalSteps: totalSteps, title: "Extracting components", description: "I'm scanning your files and figuring out what components exist, what props they have, and how they're structured. This is fully automatic \u2014 sit tight.", detail: extractDetail, secondaryDetail: compositionDetail }));
1437
1377
  }
1438
1378
  case 'scope-gate': {
1439
1379
  if (!state.extractSessionId) {
1440
- return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: "red", children: "Error: extract session ID missing \u2014 please re-run." }) }));
1380
+ return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: PALETTE.error, children: "Error: extract session ID missing \u2014 please re-run." }) }));
1441
1381
  }
1442
1382
  const sessionId = state.extractSessionId;
1443
1383
  const db = openPipelineDb();
@@ -1448,12 +1388,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1448
1388
  finally {
1449
1389
  db.close();
1450
1390
  }
1451
- // INTEG-4318: overlay the streamed auto-filter decisions (from
1452
- // stderr progress lines) onto DB-loaded rows so 'failed' components
1453
- // (LLM omitted a tool call in a batch) surface in the scope-gate
1454
- // instead of silently defaulting to included.
1455
1391
  components = mergeAiDecisions(components, state.aiDecisions);
1456
- return (_jsx(ScopeGateHost, { components: components, autoAccept: autoAcceptScope, aiFilterStatus: state.aiFilterStatus, aiFilterProgress: state.aiFilterProgress, aiFilterError: state.aiFilterError, onCancelAutoFilter: cancelAutoFilter, onConfirm: (decisions) => {
1392
+ return (_jsx(ScopeGateHost, { components: components, autoAccept: autoAcceptScope, compositionMode: compositionMode, aiFilterStatus: state.aiFilterStatus, aiFilterProgress: state.aiFilterProgress, aiFilterError: state.aiFilterError, onCancelAutoFilter: cancelAutoFilter, onConfirm: (decisions) => {
1457
1393
  void runScopeGate({
1458
1394
  sessionId,
1459
1395
  decisions,
@@ -1467,12 +1403,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1467
1403
  }
1468
1404
  return;
1469
1405
  }
1470
- // next === 'credentials' (push enabled). Kick off the
1471
- // generate child in the background so the LLM call overlaps
1472
- // with the operator's credential entry. We only prefetch
1473
- // when we have accepted components to classify AND push is
1474
- // enabled (noPush path is already excluded — the scope-gate
1475
- // helper would have returned 'generating' there).
1476
1406
  if (acceptedCount > 0 && !noPush) {
1477
1407
  if (await runAgentAuthCheck('credentials')) {
1478
1408
  startGeneratePrefetch(sid, state.tokensPath);
@@ -1487,7 +1417,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1487
1417
  void startSaveFlow();
1488
1418
  return;
1489
1419
  }
1490
- // 'credentials' — still need creds for tokens/removals push.
1491
1420
  advanceToPushFlow(count);
1492
1421
  },
1493
1422
  });
@@ -1502,38 +1431,122 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1502
1431
  return (_jsx(RunningStep, { stepNumber: stepNum, totalSteps: totalSteps, title: "Generating definitions", description: `${formatAcceptanceSummary({ accepted: state.acceptedCount, autoRejected: state.autoRejectedCount })} ${state.agent} is mapping your TypeScript types to Contentful's CDF format.${hasTokens ? ' Using your design tokens for prop resolution.' : ''}`, detail: progressDetail }));
1503
1432
  }
1504
1433
  case 'final-review': {
1505
- return (_jsx(FinalReviewHost, { extractSessionId: state.extractSessionId, generatedCount: state.generatedCount, autoAccept: autoAcceptScope, livePreview: livePreview, spaceId: state.spaceId, environmentId: state.environmentId, cmaToken: state.cmaToken, host: state.host, tokensPath: state.tokensPath, initialFinalizeError: state.finalizeErrorBanner, onFinalize: (accepted, rejected, unresolved) => {
1434
+ return (_jsx(FinalReviewHost, { extractSessionId: state.extractSessionId, generatedCount: state.generatedCount, autoAccept: autoAcceptScope, compositionMode: compositionMode, livePreview: livePreview, spaceId: state.spaceId, environmentId: state.environmentId, cmaToken: state.cmaToken, host: state.host, tokensPath: state.tokensPath, initialFinalizeError: state.finalizeErrorBanner, onFinalize: (accepted, rejected, unresolved) => {
1506
1435
  process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected} Unresolved: ${unresolved}\n`);
1507
- update({ finalReviewPassed: true });
1508
- // INTEG-4411 refined: no `accepted === 0` up-front block here.
1509
- // A zero-accepted finalize can still be a valid push when the
1510
- // operator explicitly rejected component(s) that exist server-
1511
- // side (→ REMOVALS) or when tokens carry a diff. The load-
1512
- // bearing no-op check consults the preview response inside
1513
- // `runPreview` below (see `isEmptyPreview` branch) and routes
1514
- // back to `final-review` with `finalizeErrorBanner` set when
1515
- // the diff is truly empty.
1436
+ let acceptedCount = accepted;
1437
+ const detectAcceptedCycles = () => {
1438
+ if (!state.extractSessionId)
1439
+ return [];
1440
+ try {
1441
+ const db = openPipelineDb();
1442
+ try {
1443
+ const acceptedComponents = loadCDFComponents(db, state.extractSessionId);
1444
+ return findSlotCycles(buildComponentGraph(acceptedComponents));
1445
+ }
1446
+ finally {
1447
+ db.close();
1448
+ }
1449
+ }
1450
+ catch {
1451
+ return [];
1452
+ }
1453
+ };
1454
+ let acceptedCycles = detectAcceptedCycles();
1455
+ const gateAction = resolveCycleGateAction({
1456
+ hasCycles: acceptedCycles.length > 0,
1457
+ autoRejectCycles,
1458
+ });
1459
+ const routeToCycleError = () => {
1460
+ update({
1461
+ step: 'error',
1462
+ errorStep: 'final review',
1463
+ errorMessage: formatSlotCycleReport(acceptedCycles).join('\n'),
1464
+ errorAllowCredentialRetry: false,
1465
+ });
1466
+ };
1467
+ if (gateAction === 'block') {
1468
+ routeToCycleError();
1469
+ return;
1470
+ }
1471
+ if (gateAction === 'auto-reject' && state.extractSessionId) {
1472
+ const sessionId = state.extractSessionId;
1473
+ let excluded = [];
1474
+ try {
1475
+ const db = openPipelineDb();
1476
+ try {
1477
+ const acceptedComponents = loadCDFComponents(db, sessionId);
1478
+ const targets = computeCycleAutoRejectTargets(acceptedCycles, buildComponentGraph(acceptedComponents));
1479
+ excluded = [...targets];
1480
+ if (excluded.length > 0) {
1481
+ const stmt = db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name = ?`);
1482
+ db.exec('BEGIN');
1483
+ try {
1484
+ for (const name of excluded) {
1485
+ stmt.run(sessionId, name);
1486
+ }
1487
+ db.exec('COMMIT');
1488
+ }
1489
+ catch (e) {
1490
+ db.exec('ROLLBACK');
1491
+ throw e;
1492
+ }
1493
+ }
1494
+ }
1495
+ finally {
1496
+ db.close();
1497
+ }
1498
+ }
1499
+ catch {
1500
+ routeToCycleError();
1501
+ return;
1502
+ }
1503
+ if (excluded.length > 0) {
1504
+ process.stderr.write(`Auto-rejected cycle participants: ${excluded.join(', ')}\n`);
1505
+ logStep({ event: 'cycle-auto-reject', excluded });
1506
+ }
1507
+ acceptedCycles = detectAcceptedCycles();
1508
+ if (acceptedCycles.length > 0) {
1509
+ routeToCycleError();
1510
+ return;
1511
+ }
1512
+ const remaining = (() => {
1513
+ if (!state.extractSessionId)
1514
+ return acceptedCount;
1515
+ try {
1516
+ const db = openPipelineDb();
1517
+ try {
1518
+ return loadCDFComponents(db, sessionId).length;
1519
+ }
1520
+ finally {
1521
+ db.close();
1522
+ }
1523
+ }
1524
+ catch {
1525
+ return acceptedCount;
1526
+ }
1527
+ })();
1528
+ acceptedCount = remaining;
1529
+ }
1530
+ const allowEmptyDeleteAll = acceptedCount === 0;
1531
+ allowEmptyDeleteAllRef.current = allowEmptyDeleteAll;
1532
+ update({ finalReviewPassed: true, allowEmptyDeleteAll });
1516
1533
  if (noPush) {
1517
- update({ generatedAcceptedCount: accepted });
1534
+ update({ generatedAcceptedCount: acceptedCount });
1518
1535
  void startSaveFlow();
1519
1536
  return;
1520
1537
  }
1521
1538
  if (noSave) {
1522
- update({ generatedAcceptedCount: accepted });
1539
+ update({ generatedAcceptedCount: acceptedCount });
1523
1540
  const { extractSessionId, tokensPath } = sessionRef.current;
1524
1541
  void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
1525
1542
  return;
1526
1543
  }
1527
1544
  if (autoAcceptScope) {
1528
- // Headless run with neither --no-save nor --no-push: pick the
1529
- // default "both" path automatically to preserve scripted
1530
- // (auto-accept-scope) UX from before the gate gained a third
1531
- // option. Operators who want push-only must pass --no-save.
1532
- update({ generatedAcceptedCount: accepted });
1545
+ update({ generatedAcceptedCount: acceptedCount });
1533
1546
  void runSaveAndPush();
1534
1547
  return;
1535
1548
  }
1536
- update({ generatedAcceptedCount: accepted, step: 'push-decision-gate' });
1549
+ update({ generatedAcceptedCount: acceptedCount, step: 'push-decision-gate' });
1537
1550
  }, onQuit: () => process.exit(0) }));
1538
1551
  }
1539
1552
  case 'push-decision-gate': {
@@ -1557,22 +1570,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1557
1570
  void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
1558
1571
  return;
1559
1572
  }
1560
- // save-only
1561
1573
  void startSaveFlow();
1562
1574
  }, onQuit: () => process.exit(0) }));
1563
1575
  }
1564
1576
  case 'credentials':
1565
1577
  return (_jsx(CredentialsStep, { initialSpaceId: state.spaceId, initialEnvironmentId: state.environmentId, initialCmaToken: state.cmaToken, initialHost: state.host, error: state.credentialsError || undefined, validating: state.credentialsValidating, generatePrefetchStatus: state.generatePrefetchStatus, generatePrefetchError: state.generatePrefetchError, onConfirm: (spaceId, environmentId, cmaToken, host) => {
1566
1578
  void confirmCredentials(spaceId, environmentId, cmaToken, host);
1567
- },
1568
- // INTEG-4410: unify the unchanged-form path with the changed-form
1569
- // path so credentials are ALWAYS persisted to disk on submit.
1570
- // The pre-fix wiring routed `onContinue` at
1571
- // `advanceWithCredentials`, which only mutated state — so on
1572
- // `--modify` (where the wizard seeds `run.pushedTo` into the
1573
- // form, not disk) an operator pressing Enter never wrote to
1574
- // ~/.config/experiences/credentials.json and disk stayed stale.
1575
- onContinue: (spaceId, environmentId, cmaToken, host) => {
1579
+ }, onContinue: (spaceId, environmentId, cmaToken, host) => {
1576
1580
  void confirmCredentials(spaceId, environmentId, cmaToken, host);
1577
1581
  }, onRetryPrefetch: state.generatePrefetchStatus === 'failed' && sessionRef.current.extractSessionId
1578
1582
  ? () => {
@@ -1580,39 +1584,27 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1580
1584
  void startGeneratePrefetch(sid, state.tokensPath);
1581
1585
  }
1582
1586
  : undefined, onSkip: () => {
1583
- // Skip-credentials: mark the wizard as skipped, clear any
1584
- // stale credentialsError banner, and advance through the
1585
- // normal post-credentials branch. The in-flight generate
1586
- // prefetch (PR #54) is intentionally NOT cancelled here —
1587
- // the operator still wants to see classifications.
1588
1587
  update({ credentialsSkipped: true, credentialsError: '' });
1589
1588
  void advanceAfterCredentialsValidated();
1590
1589
  }, onQuit: () => {
1591
1590
  cancelGeneratePrefetch();
1592
1591
  process.exit(0);
1593
1592
  } }));
1594
- // 'credential-test-gate' is intentionally NOT rendered as a dedicated
1595
- // screen any more (the post-#54 inline credentials-validating status made
1596
- // the gate redundant). The step value is kept in the union for back-compat
1597
- // with existing imports/tests but is never actually set as a step —
1598
- // `advanceWithCredentials` calls `validateCredentials` directly.
1599
- //
1600
- // 'validating-credentials' is intentionally NOT rendered as a dedicated
1601
- // screen any more (see CredentialsStep `validating` prop). The step
1602
- // value is kept in the union for back-compat with existing imports/tests
1603
- // but should never actually be set — `validateCredentials` keeps the
1604
- // step on 'credentials' and toggles `credentialsValidating` instead.
1605
1593
  case 'push-from-picker':
1606
1594
  case 'previewing':
1607
1595
  return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Computing diff", description: "Computing diff against your Contentful space..." }));
1608
- case 'preview-gate':
1596
+ case 'preview-gate': {
1597
+ // Only offer "[e] Edit definitions" when the manifest actually has
1598
+ // components to edit. In the delete-all / empty case the manifest is
1599
+ // present-but-empty (only $schema), so editing would dead-end on the
1600
+ // "No generated definitions found" screen.
1601
+ const editableComponentCount = Object.keys(state.manifest?.componentsManifest ?? {}).filter((k) => k !== '$schema').length;
1609
1602
  return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, onConfirm: (acknowledge) => {
1610
1603
  void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, state.host, acknowledge, state.serverPreview);
1611
- }, onEdit: () => {
1612
- void runEditFromPreview();
1613
- }, onSaveFiles: () => {
1604
+ }, ...(editableComponentCount > 0 ? { onEdit: () => void runEditFromPreview() } : {}), onSaveFiles: () => {
1614
1605
  void startSaveFlow();
1615
1606
  }, onQuit: () => process.exit(0) }));
1607
+ }
1616
1608
  case 'pushing':
1617
1609
  return _jsx(PushingStep, { stepNumber: totalSteps, totalSteps: totalSteps, progress: state.pushProgress });
1618
1610
  case 'path-prompt':
@@ -1664,10 +1656,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1664
1656
  }
1665
1657
  case 'error':
1666
1658
  return (_jsx(ErrorStep, { stepName: state.errorStep, message: state.errorMessage, onExit: () => process.exit(1), onRetryCredentials: state.errorAllowCredentialRetry ? () => update({ step: 'credentials', credentialsError: '' }) : undefined }));
1667
- // 'validating-credentials' kept in the WizardStep union for back-compat
1668
- // but is no longer reachable as a step (validateCredentials toggles the
1669
- // `credentialsValidating` boolean while leaving step on 'credentials').
1670
- // Default-return null so the switch is exhaustive.
1671
1659
  default:
1672
1660
  return null;
1673
1661
  }