@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.
- package/README.md +64 -2
- package/dist/package.json +3 -2
- package/dist/src/analyze/command.d.ts +11 -0
- package/dist/src/analyze/command.js +298 -3
- package/dist/src/analyze/composite-closure.d.ts +25 -0
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
- package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
- package/dist/src/analyze/composition/apply-mapping.js +67 -0
- package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
- package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
- package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
- package/dist/src/analyze/composition/candidate-critic.js +63 -0
- package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
- package/dist/src/analyze/composition/candidate-files.js +131 -0
- package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
- package/dist/src/analyze/composition/composition-cache-key.js +19 -0
- package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
- package/dist/src/analyze/composition/interchange-schema.js +61 -0
- package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
- package/dist/src/analyze/composition/merge-edges.js +50 -0
- package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
- package/dist/src/analyze/composition/parse-map-edges.js +55 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
- package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
- package/dist/src/analyze/composition/resolve-mapping.js +94 -0
- package/dist/src/analyze/cycle-detection.d.ts +27 -0
- package/dist/src/analyze/cycle-detection.js +234 -0
- package/dist/src/analyze/cycle-view.d.ts +7 -0
- package/dist/src/analyze/cycle-view.js +28 -0
- package/dist/src/analyze/fuzzy-search.d.ts +3 -0
- package/dist/src/analyze/fuzzy-search.js +55 -0
- package/dist/src/analyze/issue-inheritance.d.ts +12 -0
- package/dist/src/analyze/issue-inheritance.js +108 -0
- package/dist/src/analyze/lineage.d.ts +29 -0
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
- package/dist/src/analyze/scope-gate-cascade.js +170 -0
- package/dist/src/analyze/search-neighborhood.d.ts +3 -0
- package/dist/src/analyze/search-neighborhood.js +56 -0
- package/dist/src/analyze/select/tui/App.js +5 -16
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
- package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
- package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
- package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
- package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
- package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
- package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
- package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
- package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
- package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
- package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
- package/dist/src/analyze/select/tui/theme.d.ts +15 -0
- package/dist/src/analyze/select/tui/theme.js +28 -0
- package/dist/src/analyze/selection-cascade.d.ts +3 -0
- package/dist/src/analyze/selection-cascade.js +19 -0
- package/dist/src/analyze/slot-graph.d.ts +11 -0
- package/dist/src/analyze/slot-graph.js +17 -0
- package/dist/src/apply/api-client.d.ts +2 -1
- package/dist/src/apply/api-client.js +40 -6
- package/dist/src/apply/command.d.ts +17 -1
- package/dist/src/apply/command.js +69 -11
- package/dist/src/apply/error-parser.d.ts +16 -0
- package/dist/src/apply/error-parser.js +117 -0
- package/dist/src/apply/tui/ServerPreviewView.js +3 -1
- package/dist/src/credentials-store.d.ts +6 -0
- package/dist/src/credentials-store.js +5 -1
- package/dist/src/generate/agent-runner.d.ts +6 -0
- package/dist/src/generate/agent-runner.js +24 -8
- package/dist/src/import/command.js +69 -31
- package/dist/src/import/cycle-auto-reject.d.ts +4 -0
- package/dist/src/import/cycle-auto-reject.js +14 -0
- package/dist/src/import/orchestrator.d.ts +20 -7
- package/dist/src/import/orchestrator.js +139 -35
- package/dist/src/import/strip-allowed-components.d.ts +18 -0
- package/dist/src/import/strip-allowed-components.js +22 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
- package/dist/src/import/tui/CustomPromptBanner.js +2 -1
- package/dist/src/import/tui/WizardApp.d.ts +10 -74
- package/dist/src/import/tui/WizardApp.js +234 -246
- package/dist/src/import/tui/ai-flag.d.ts +5 -0
- package/dist/src/import/tui/ai-flag.js +6 -0
- package/dist/src/import/tui/autocomplete.d.ts +5 -0
- package/dist/src/import/tui/autocomplete.js +32 -0
- package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
- package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
- package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
- package/dist/src/import/tui/components/CounterStrip.js +12 -0
- package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
- package/dist/src/import/tui/components/LegendEntry.js +6 -0
- package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
- package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
- package/dist/src/import/tui/final-review-host.d.ts +3 -7
- package/dist/src/import/tui/final-review-host.js +9 -4
- package/dist/src/import/tui/group-collapse.d.ts +2 -0
- package/dist/src/import/tui/group-collapse.js +11 -0
- package/dist/src/import/tui/history.d.ts +22 -0
- package/dist/src/import/tui/history.js +53 -0
- package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
- package/dist/src/import/tui/hooks/useLineage.js +50 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
- package/dist/src/import/tui/lineage-layout.d.ts +75 -0
- package/dist/src/import/tui/lineage-layout.js +82 -0
- package/dist/src/import/tui/runLivePreview.d.ts +11 -0
- package/dist/src/import/tui/runLivePreview.js +7 -1
- package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
- package/dist/src/import/tui/scope-gate-columns.js +94 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
- package/dist/src/import/tui/scope-gate-host.js +11 -3
- package/dist/src/import/tui/sidebar-width.d.ts +1 -0
- package/dist/src/import/tui/sidebar-width.js +3 -0
- package/dist/src/import/tui/step-filters.d.ts +19 -0
- package/dist/src/import/tui/step-filters.js +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
- package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
- package/dist/src/import/tui/steps/DoneStep.js +3 -2
- package/dist/src/import/tui/steps/ErrorStep.js +2 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
- package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
- package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
- package/dist/src/import/tui/steps/PreviewStep.js +2 -1
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
- package/dist/src/import/tui/steps/PushingStep.js +2 -1
- package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
- package/dist/src/import/tui/steps/RunningStep.js +3 -2
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
- package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
- package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
- package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
- package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
- package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
- package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +48 -5
- package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.d.ts +3 -0
- package/dist/src/import/tui/useLivePreview.js +1 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
- package/dist/src/import/tui/wizard-state-transitions.js +5 -60
- package/dist/src/lib/composition-mode.d.ts +28 -0
- package/dist/src/lib/composition-mode.js +37 -0
- package/dist/src/lib/prompt-overrides.d.ts +38 -0
- package/dist/src/lib/prompt-overrides.js +69 -0
- package/dist/src/print/command.js +19 -1
- package/dist/src/runs/modify-launcher.d.ts +3 -0
- package/dist/src/runs/modify-launcher.js +2 -0
- package/dist/src/runs/replay-helpers.js +1 -0
- package/dist/src/runs/run-picker.js +2 -18
- package/dist/src/runs/store.d.ts +4 -0
- package/dist/src/session/db.d.ts +23 -40
- package/dist/src/session/db.js +110 -138
- package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
- package/dist/src/setup/composition-mode-prompt.js +37 -0
- package/package.json +5 -4
- package/prompts/composition-dir-critic.md +7 -0
- package/prompts/composition-edges.md +8 -0
- package/prompts/composition-parser-repair-empty.md +1 -0
- package/prompts/composition-parser-repair-error.md +2 -0
- 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
|
-
//
|
|
170
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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:
|
|
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
|
|
1077
|
+
summary,
|
|
1142
1078
|
};
|
|
1143
1079
|
}
|
|
1144
1080
|
update({ step: 'done', pushResult });
|
|
1145
1081
|
}
|
|
1146
1082
|
catch (e) {
|
|
1147
|
-
|
|
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]);
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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:
|
|
1534
|
+
update({ generatedAcceptedCount: acceptedCount });
|
|
1518
1535
|
void startSaveFlow();
|
|
1519
1536
|
return;
|
|
1520
1537
|
}
|
|
1521
1538
|
if (noSave) {
|
|
1522
|
-
update({ generatedAcceptedCount:
|
|
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
|
-
|
|
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:
|
|
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
|
}
|