@contentful/experience-design-system-cli 2.12.0 → 2.12.1-dev-build-589b615.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 +214 -357
- package/dist/package.json +1 -1
- package/dist/src/analyze/extract/astro.js +9 -0
- package/dist/src/analyze/extract/react.js +23 -0
- package/dist/src/analyze/extract/stencil.js +3 -0
- package/dist/src/analyze/extract/vue.js +6 -0
- package/dist/src/analyze/extract/web-components.js +10 -0
- package/dist/src/analyze/pre-classify.d.ts +1 -1
- package/dist/src/analyze/pre-classify.js +3 -3
- package/dist/src/analyze/select/persistence.d.ts +13 -0
- package/dist/src/analyze/select/persistence.js +38 -0
- package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
- package/dist/src/analyze/select/preview-annotations.js +62 -0
- package/dist/src/analyze/select/tui/App.js +5 -2
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
- package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
- package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
- package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +259 -45
- package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
- package/dist/src/analyze/select-agent/show-rationale.js +92 -0
- package/dist/src/apply/command.js +7 -3
- package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
- package/dist/src/apply/tui/ServerApplyView.js +3 -2
- package/dist/src/credentials-store.d.ts +5 -0
- package/dist/src/credentials-store.js +7 -1
- package/dist/src/generate/agent-runner.d.ts +14 -0
- package/dist/src/generate/agent-runner.js +16 -0
- package/dist/src/generate/command.d.ts +6 -0
- package/dist/src/generate/command.js +45 -10
- package/dist/src/generate/progress.d.ts +9 -0
- package/dist/src/generate/progress.js +11 -0
- package/dist/src/generate/prompt-builder.d.ts +8 -0
- package/dist/src/generate/prompt-builder.js +22 -11
- package/dist/src/import/agent-model-resolve.d.ts +23 -0
- package/dist/src/import/agent-model-resolve.js +35 -0
- package/dist/src/import/auto-filter-resolve.d.ts +12 -0
- package/dist/src/import/auto-filter-resolve.js +16 -0
- package/dist/src/import/command.js +248 -8
- package/dist/src/import/orchestrator.d.ts +2 -0
- package/dist/src/import/orchestrator.js +11 -3
- package/dist/src/import/print-prompt.d.ts +35 -0
- package/dist/src/import/print-prompt.js +31 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
- package/dist/src/import/tui/CustomPromptBanner.js +7 -0
- package/dist/src/import/tui/WizardApp.d.ts +112 -8
- package/dist/src/import/tui/WizardApp.js +814 -333
- package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
- package/dist/src/import/tui/auto-filter-error.js +29 -0
- package/dist/src/import/tui/final-review-host.d.ts +15 -0
- package/dist/src/import/tui/final-review-host.js +20 -0
- package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
- package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
- package/dist/src/import/tui/push-progress.d.ts +23 -0
- package/dist/src/import/tui/push-progress.js +14 -0
- package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
- package/dist/src/import/tui/run-print-files-helpers.js +8 -0
- package/dist/src/import/tui/run-teaser.d.ts +5 -0
- package/dist/src/import/tui/run-teaser.js +9 -0
- package/dist/src/import/tui/runLivePreview.d.ts +35 -0
- package/dist/src/import/tui/runLivePreview.js +73 -0
- package/dist/src/import/tui/runScopeGate.d.ts +22 -0
- package/dist/src/import/tui/runScopeGate.js +28 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
- package/dist/src/import/tui/scope-gate-host.js +20 -0
- package/dist/src/import/tui/spawn-generate.d.ts +33 -0
- package/dist/src/import/tui/spawn-generate.js +53 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
- package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
- package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
- package/dist/src/import/tui/steps/DoneStep.js +10 -4
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
- package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
- package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PushingStep.js +40 -0
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
- package/dist/src/import/tui/useLivePreview.d.ts +35 -0
- package/dist/src/import/tui/useLivePreview.js +120 -0
- package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
- package/dist/src/import/tui/wizard-generate-progress.js +25 -0
- package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
- package/dist/src/import/tui/wizard-save-flow.js +20 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
- package/dist/src/import/tui/wizard-state-transitions.js +79 -0
- package/dist/src/lib/contentful-urls.d.ts +34 -0
- package/dist/src/lib/contentful-urls.js +41 -0
- package/dist/src/program.js +2 -0
- package/dist/src/runs/export-helpers.d.ts +30 -0
- package/dist/src/runs/export-helpers.js +59 -0
- package/dist/src/runs/fingerprint.d.ts +47 -0
- package/dist/src/runs/fingerprint.js +75 -0
- package/dist/src/runs/ls-command.d.ts +12 -0
- package/dist/src/runs/ls-command.js +137 -0
- package/dist/src/runs/modify-launcher.d.ts +24 -0
- package/dist/src/runs/modify-launcher.js +36 -0
- package/dist/src/runs/path-prompt.d.ts +8 -0
- package/dist/src/runs/path-prompt.js +72 -0
- package/dist/src/runs/push-creds-prompt.d.ts +25 -0
- package/dist/src/runs/push-creds-prompt.js +39 -0
- package/dist/src/runs/push-helpers.d.ts +29 -0
- package/dist/src/runs/push-helpers.js +69 -0
- package/dist/src/runs/replay-helpers.d.ts +67 -0
- package/dist/src/runs/replay-helpers.js +148 -0
- package/dist/src/runs/resolve-run-target.d.ts +12 -0
- package/dist/src/runs/resolve-run-target.js +45 -0
- package/dist/src/runs/run-picker-mount.d.ts +46 -0
- package/dist/src/runs/run-picker-mount.js +72 -0
- package/dist/src/runs/run-picker.d.ts +17 -0
- package/dist/src/runs/run-picker.js +145 -0
- package/dist/src/runs/save-conflict.d.ts +8 -0
- package/dist/src/runs/save-conflict.js +56 -0
- package/dist/src/runs/save-path-resolver.d.ts +55 -0
- package/dist/src/runs/save-path-resolver.js +71 -0
- package/dist/src/runs/staleness.d.ts +38 -0
- package/dist/src/runs/staleness.js +140 -0
- package/dist/src/runs/store.d.ts +78 -0
- package/dist/src/runs/store.js +136 -0
- package/dist/src/session/cache-keys.d.ts +15 -0
- package/dist/src/session/cache-keys.js +38 -0
- package/dist/src/session/db.d.ts +89 -3
- package/dist/src/session/db.js +447 -104
- package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
- package/dist/src/setup/auto-filter-prompt.js +24 -0
- package/dist/src/setup/command.d.ts +7 -0
- package/dist/src/setup/command.js +58 -2
- package/dist/src/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/generate-components.md +54 -6
- package/skills/select-components.md +13 -4
|
@@ -4,10 +4,19 @@ import { Box, Text, useStdout } from 'ink';
|
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { appendFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { access, readFile, stat } from 'node:fs/promises';
|
|
7
|
-
import {
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { execFile, spawn } from 'node:child_process';
|
|
10
|
+
import { mkdir } from 'node:fs/promises';
|
|
11
|
+
import { buildRunTeaserLine } from './run-teaser.js';
|
|
12
|
+
import { PathPrompt } from '../../runs/path-prompt.js';
|
|
13
|
+
import { RunPicker } from '../../runs/run-picker.js';
|
|
14
|
+
import { SaveConflictGate } from '../../runs/save-conflict.js';
|
|
15
|
+
import { detectSaveConflict, buildTimestampedSubdir, resolveSavePath, } from '../../runs/save-path-resolver.js';
|
|
16
|
+
import { appendRun } from '../../runs/store.js';
|
|
17
|
+
import { buildSourceFingerprint, buildSavedFingerprint } from '../../runs/fingerprint.js';
|
|
10
18
|
import { TopBar } from '../../analyze/select/tui/components/TopBar.js';
|
|
19
|
+
import { CustomPromptBanner } from './CustomPromptBanner.js';
|
|
11
20
|
import { WelcomeStep } from './steps/WelcomeStep.js';
|
|
12
21
|
import { PathValidationStep } from './steps/PathValidationStep.js';
|
|
13
22
|
import { RunningStep } from './steps/RunningStep.js';
|
|
@@ -17,30 +26,97 @@ import { WizardPreviewStep } from './steps/WizardPreviewStep.js';
|
|
|
17
26
|
import { DoneStep } from './steps/DoneStep.js';
|
|
18
27
|
import { ErrorStep } from './steps/ErrorStep.js';
|
|
19
28
|
import { TokenInputStep } from './steps/TokenInputStep.js';
|
|
20
|
-
import { GenerateReviewStep } from './steps/GenerateReviewStep.js';
|
|
21
29
|
import { PreviewValidationErrorStep } from './steps/PreviewValidationErrorStep.js';
|
|
30
|
+
import { PushingStep } from './steps/PushingStep.js';
|
|
31
|
+
import { computePushExpected } from './push-progress.js';
|
|
32
|
+
import { nextStateAfterPrint } from './run-print-files-helpers.js';
|
|
33
|
+
import { PushDecisionGateStep } from './steps/PushDecisionGateStep.js';
|
|
34
|
+
import { chooseGateAction } from './push-decision-gate-helpers.js';
|
|
22
35
|
import { ImportApiClient, ApiError } from '../../apply/api-client.js';
|
|
23
36
|
import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
|
|
37
|
+
import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
|
|
38
|
+
import { spawnGenerateChild } from './spawn-generate.js';
|
|
24
39
|
import { readTokensFromPath, hasBreakingChangesWithImpact } from '../../apply/manifest.js';
|
|
25
40
|
import { buildManifest } from '@contentful/experience-design-system-types';
|
|
26
|
-
import { openPipelineDb, loadCDFComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
|
|
41
|
+
import { openPipelineDb, loadCDFComponents, loadScopeComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
|
|
42
|
+
import { ScopeGateHost } from './scope-gate-host.js';
|
|
43
|
+
import { FinalReviewHost } from './final-review-host.js';
|
|
44
|
+
import { runScopeGate } from './runScopeGate.js';
|
|
45
|
+
import { buildAutoFilterErrorTail } from './auto-filter-error.js';
|
|
27
46
|
import { checkAgentAuth } from '../../generate/agent-runner.js';
|
|
28
47
|
import { normalizePath } from '../path-utils.js';
|
|
29
48
|
import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../../host-utils.js';
|
|
30
49
|
import { writeExperiencesCredentials } from '../../credentials-store.js';
|
|
50
|
+
import { nextStepAfterScopeGate, nextStepAfterCredentialsValidated, shouldBypassPreview, buildSkippedPreviewTransition, shouldRefusePush, buildSkippedPushTransition, } from './wizard-state-transitions.js';
|
|
31
51
|
function findCliPath() {
|
|
32
52
|
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'bin', 'cli.js');
|
|
33
53
|
}
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
args.push('--select-
|
|
54
|
+
export function buildSelectAgentArgs(opts) {
|
|
55
|
+
// Feature 3: the wizard auto-filter run should never fail-loud on validation
|
|
56
|
+
// errors — those components surface in the AI-excluded section with a
|
|
57
|
+
// synthesized reason, not an exit-1 abort. So we always pass --exclude-invalid.
|
|
58
|
+
const args = ['analyze', 'select-agent', '--agent', opts.agent, '--session', opts.sessionId, '--exclude-invalid'];
|
|
59
|
+
if (opts.model)
|
|
60
|
+
args.push('--model', opts.model);
|
|
61
|
+
if (opts.selectPromptPath)
|
|
62
|
+
args.push('--select-prompt-path', opts.selectPromptPath);
|
|
63
|
+
if (opts.noCache)
|
|
64
|
+
args.push('--no-cache');
|
|
65
|
+
return args;
|
|
66
|
+
}
|
|
67
|
+
export function parseAutoFilterProgressLine(line) {
|
|
68
|
+
// Format: progress=select-agent:N/M:<decision>:<name>:<url-encoded-reason>
|
|
69
|
+
// Name and reason CANNOT contain `:` raw (name comes from component identifier
|
|
70
|
+
// which forbids colons; reason is URL-encoded). We split with a limit so any
|
|
71
|
+
// stray colon inside the URL-encoded reason wouldn't matter — but the encoder
|
|
72
|
+
// also handles `:` as `%3A` so this is defensive.
|
|
73
|
+
const prefix = 'progress=select-agent:';
|
|
74
|
+
if (!line.startsWith(prefix))
|
|
75
|
+
return null;
|
|
76
|
+
const rest = line.slice(prefix.length);
|
|
77
|
+
const parts = rest.split(':');
|
|
78
|
+
if (parts.length < 4)
|
|
79
|
+
return null;
|
|
80
|
+
const [counter, decision, name, ...reasonParts] = parts;
|
|
81
|
+
if (!counter)
|
|
82
|
+
return null;
|
|
83
|
+
const counterMatch = /^(\d+)\/(\d+)$/.exec(counter);
|
|
84
|
+
if (!counterMatch)
|
|
85
|
+
return null;
|
|
86
|
+
if (decision !== 'accepted' && decision !== 'rejected')
|
|
87
|
+
return null;
|
|
88
|
+
if (!name)
|
|
89
|
+
return null;
|
|
90
|
+
const encodedReason = reasonParts.join(':');
|
|
91
|
+
let reason = '';
|
|
92
|
+
try {
|
|
93
|
+
reason = decodeURIComponent(encodedReason);
|
|
43
94
|
}
|
|
95
|
+
catch {
|
|
96
|
+
reason = encodedReason;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
n: Number(counterMatch[1]),
|
|
100
|
+
total: Number(counterMatch[2]),
|
|
101
|
+
decision,
|
|
102
|
+
name,
|
|
103
|
+
reason,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function buildGenerateComponentsArgs(opts) {
|
|
107
|
+
// Default behavior preserves the SHA cache (re-runs only re-classify
|
|
108
|
+
// changed components). The operator opts into a full re-classify pass via
|
|
109
|
+
// `experiences import --no-cache`, which forwards through to the spawned
|
|
110
|
+
// `generate components` subprocess. See wizard-cache.test.ts.
|
|
111
|
+
const args = ['generate', 'components', '--agent', opts.agent, '--session', opts.sessionId];
|
|
112
|
+
if (opts.tokensPath)
|
|
113
|
+
args.push('--tokens', opts.tokensPath);
|
|
114
|
+
if (opts.model)
|
|
115
|
+
args.push('--model', opts.model);
|
|
116
|
+
if (opts.noCache)
|
|
117
|
+
args.push('--no-cache');
|
|
118
|
+
if (opts.generatePromptPath)
|
|
119
|
+
args.push('--generate-prompt-path', opts.generatePromptPath);
|
|
44
120
|
return args;
|
|
45
121
|
}
|
|
46
122
|
export function formatAcceptanceSummary(opts) {
|
|
@@ -49,16 +125,6 @@ export function formatAcceptanceSummary(opts) {
|
|
|
49
125
|
return `${acceptedClause}.`;
|
|
50
126
|
return `${acceptedClause}, ${opts.autoRejected} excluded due to validation errors.`;
|
|
51
127
|
}
|
|
52
|
-
export function formatGeneratedSummary(opts) {
|
|
53
|
-
const generatedClause = `Generated definitions for ${opts.generated} component${opts.generated === 1 ? '' : 's'}.`;
|
|
54
|
-
const renamedClause = opts.renamedSlots > 0
|
|
55
|
-
? ` ${opts.renamedSlots} unnamed slot${opts.renamedSlots === 1 ? '' : 's'} renamed (children / slot_<n>) so the LLM could classify them.`
|
|
56
|
-
: '';
|
|
57
|
-
const exclusionClause = opts.autoRejected > 0
|
|
58
|
-
? ` ${opts.autoRejected} component${opts.autoRejected === 1 ? '' : 's'} excluded earlier due to validation errors.`
|
|
59
|
-
: '';
|
|
60
|
-
return `${generatedClause}${renamedClause}${exclusionClause}`;
|
|
61
|
-
}
|
|
62
128
|
function runCli(args) {
|
|
63
129
|
return new Promise((res) => {
|
|
64
130
|
execFile('node', [findCliPath(), ...args], (error, stdout, stderr) => {
|
|
@@ -66,12 +132,22 @@ function runCli(args) {
|
|
|
66
132
|
});
|
|
67
133
|
});
|
|
68
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse the `wrote tokens.json (N tokens)` confirmation line emitted by
|
|
137
|
+
* `experiences print tokens`. Returns 0 when the line cannot be parsed —
|
|
138
|
+
* the wizard still records the run; downstream consumers should treat 0 as
|
|
139
|
+
* "unknown" rather than "no tokens".
|
|
140
|
+
*/
|
|
141
|
+
export function parsePrintTokensCount(stdout) {
|
|
142
|
+
const m = /\((\d+)\s+token/.exec(stdout);
|
|
143
|
+
return m ? Number(m[1]) : 0;
|
|
144
|
+
}
|
|
69
145
|
const WIZARD_LOG = join(tmpdir(), 'experiences-import-wizard.log');
|
|
70
146
|
function logStep(entry) {
|
|
71
147
|
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
|
|
72
148
|
appendFileSync(WIZARD_LOG, line);
|
|
73
149
|
}
|
|
74
|
-
export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialProjectPath, host, } = {}) {
|
|
150
|
+
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, initialStep, initialRawTokensPath, initialRuns, onRunPicked, } = {}) {
|
|
75
151
|
const defaultConfiguredHost = toConfiguredHost(host || process.env['EDS_HOST']) ?? DEFAULT_CONFIGURED_HOST;
|
|
76
152
|
const resolveWizardHost = (hostValue) => hostValue || defaultConfiguredHost;
|
|
77
153
|
const { stdout } = useStdout();
|
|
@@ -86,18 +162,57 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
86
162
|
extractSessionId: null,
|
|
87
163
|
tokensPath: '',
|
|
88
164
|
});
|
|
165
|
+
// Feature 3: holds the spawned `analyze select-agent` subprocess so the
|
|
166
|
+
// scope-gate's `q` (during running) can SIGTERM it for cancellation.
|
|
167
|
+
const autoFilterChildRef = useRef(null);
|
|
168
|
+
// Promise that resolves when the auto-filter subprocess fully exits. Used
|
|
169
|
+
// by `cancelAutoFilterAndWait` so scope-gate confirm can guarantee its
|
|
170
|
+
// snapshot write goes AFTER the subprocess's last write.
|
|
171
|
+
const autoFilterDonePromiseRef = useRef(null);
|
|
172
|
+
// Background `generate components` subprocess spawned from scope-gate
|
|
173
|
+
// confirm so its LLM call overlaps with the operator's credential entry.
|
|
174
|
+
// Held in refs so we can SIGTERM on credential failure / quit, and await
|
|
175
|
+
// the promise from inside `validateCredentials` once creds come back OK.
|
|
176
|
+
const generateChildRef = useRef(null);
|
|
177
|
+
const generatePromiseRef = useRef(null);
|
|
178
|
+
// Modify-entry short-circuit: when the launcher passes seed session IDs
|
|
179
|
+
// and `initialStep: 'final-review'`, skip the welcome/token-input bootstrap
|
|
180
|
+
// and land directly on the post-generate review screen. The DB-backed
|
|
181
|
+
// GenerateReviewStep loads its data off `state.extractSessionId`, so all
|
|
182
|
+
// we need to do here is seed the IDs and the step.
|
|
183
|
+
const modifyEntryReady = !!seedExtractSessionId && initialStep === 'final-review';
|
|
184
|
+
// Headless raw-tokens entry: when the operator passed `--raw-tokens <path>`
|
|
185
|
+
// the CLI seeds this prop. Skip welcome + token-input and land on the
|
|
186
|
+
// `generating-tokens` step which already drives the token-classification
|
|
187
|
+
// subprocess off `state.rawTokensPath`. Modify-entry wins if both are set.
|
|
188
|
+
const rawTokensEntryReady = !modifyEntryReady && !!initialRawTokensPath;
|
|
189
|
+
const initialStepResolved = modifyEntryReady
|
|
190
|
+
? 'final-review'
|
|
191
|
+
: rawTokensEntryReady
|
|
192
|
+
? 'generating-tokens'
|
|
193
|
+
: initialProjectPath
|
|
194
|
+
? 'token-input'
|
|
195
|
+
: 'welcome';
|
|
196
|
+
const initialOutDir = initialProjectPath ? join(resolve(initialProjectPath), '.contentful') : '';
|
|
197
|
+
const initialTokensPath = modifyEntryReady && initialOutDir ? join(initialOutDir, 'tokens.json') : '';
|
|
89
198
|
const [state, setState] = useState({
|
|
90
|
-
step:
|
|
199
|
+
step: modifyEntryReady || rawTokensEntryReady
|
|
200
|
+
? initialStepResolved
|
|
201
|
+
: initialRuns && initialRuns.length > 0
|
|
202
|
+
? 'run-picker'
|
|
203
|
+
: initialStepResolved,
|
|
91
204
|
agent: initialAgent ?? 'claude',
|
|
205
|
+
...(initialModel ? { agentModel: initialModel } : {}),
|
|
92
206
|
projectPath: initialProjectPath ?? '',
|
|
93
|
-
outDir:
|
|
94
|
-
rawTokensPath: '',
|
|
95
|
-
tokensPath:
|
|
207
|
+
outDir: initialOutDir,
|
|
208
|
+
rawTokensPath: rawTokensEntryReady ? initialRawTokensPath : '',
|
|
209
|
+
tokensPath: initialTokensPath,
|
|
96
210
|
tokenSourceChanged: null,
|
|
97
211
|
skipComponents: false,
|
|
98
|
-
tokenSessionId: null,
|
|
99
|
-
|
|
100
|
-
|
|
212
|
+
tokenSessionId: seedTokenSessionId ?? null,
|
|
213
|
+
tokenCount: 0,
|
|
214
|
+
extractSessionId: seedExtractSessionId ?? null,
|
|
215
|
+
generateSessionId: seedGenerateSessionId ?? null,
|
|
101
216
|
extractedCount: 0,
|
|
102
217
|
acceptedCount: 0,
|
|
103
218
|
autoRejectedCount: 0,
|
|
@@ -115,9 +230,10 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
115
230
|
serverPreview: null,
|
|
116
231
|
manifest: null,
|
|
117
232
|
pushProgress: null,
|
|
233
|
+
pushExpected: null,
|
|
118
234
|
pushResult: {
|
|
119
|
-
componentTypes: { created: 0, updated: 0, failed: 0 },
|
|
120
|
-
designTokens: { created: 0, updated: 0, failed: 0 },
|
|
235
|
+
componentTypes: { created: 0, updated: 0, removed: 0, failed: 0 },
|
|
236
|
+
designTokens: { created: 0, updated: 0, removed: 0, failed: 0 },
|
|
121
237
|
},
|
|
122
238
|
errorStep: '',
|
|
123
239
|
errorMessage: '',
|
|
@@ -125,6 +241,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
125
241
|
authCheckStepNumber: 1,
|
|
126
242
|
previewValidationErrors: [],
|
|
127
243
|
previewValidationMissingNames: [],
|
|
244
|
+
aiFilterStatus: 'idle',
|
|
245
|
+
aiFilterProgress: null,
|
|
246
|
+
aiDecisions: {},
|
|
247
|
+
aiFilterError: null,
|
|
248
|
+
credentialsValidating: false,
|
|
249
|
+
generatePrefetchStatus: 'idle',
|
|
250
|
+
generatePrefetchError: null,
|
|
251
|
+
credentialsSkipped: false,
|
|
252
|
+
lastRunId: null,
|
|
128
253
|
});
|
|
129
254
|
useEffect(() => {
|
|
130
255
|
sessionRef.current = {
|
|
@@ -209,15 +334,10 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
209
334
|
// ── Step runners ────────────────────────────────────────────────────────
|
|
210
335
|
const runGenerateTokens = async (rawTokensPath, outDir) => {
|
|
211
336
|
const result = await new Promise((res) => {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
'
|
|
215
|
-
|
|
216
|
-
'--agent',
|
|
217
|
-
state.agent,
|
|
218
|
-
'--raw-tokens',
|
|
219
|
-
rawTokensPath,
|
|
220
|
-
]);
|
|
337
|
+
const tokenArgs = [findCliPath(), 'generate', 'tokens', '--agent', state.agent, '--raw-tokens', rawTokensPath];
|
|
338
|
+
if (state.agentModel)
|
|
339
|
+
tokenArgs.push('--model', state.agentModel);
|
|
340
|
+
const child = spawn('node', tokenArgs);
|
|
221
341
|
let stdout = '';
|
|
222
342
|
let stderr = '';
|
|
223
343
|
child.stdout.on('data', (d) => {
|
|
@@ -251,7 +371,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
251
371
|
});
|
|
252
372
|
return;
|
|
253
373
|
}
|
|
254
|
-
|
|
374
|
+
const tokenCount = parsePrintTokensCount(r.stdout);
|
|
375
|
+
update({ step: 'path-validation', tokensPath, tokenSessionId, tokenCount });
|
|
255
376
|
};
|
|
256
377
|
const runExtract = async (projectPath) => {
|
|
257
378
|
const outDir = join(resolve(projectPath), '.contentful');
|
|
@@ -321,93 +442,249 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
321
442
|
return;
|
|
322
443
|
}
|
|
323
444
|
update({
|
|
324
|
-
step: '
|
|
445
|
+
step: 'scope-gate',
|
|
325
446
|
extractSessionId,
|
|
326
447
|
extractedCount,
|
|
448
|
+
aiFilterStatus: autoFilter ? 'running' : 'idle',
|
|
449
|
+
aiFilterProgress: autoFilter ? { done: 0, total: extractedCount } : null,
|
|
450
|
+
aiDecisions: {},
|
|
451
|
+
aiFilterError: null,
|
|
327
452
|
});
|
|
453
|
+
if (autoFilter && extractSessionId) {
|
|
454
|
+
autoFilterDonePromiseRef.current = runAutoFilter(extractSessionId);
|
|
455
|
+
}
|
|
328
456
|
};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
457
|
+
// Feature 3: spawn `analyze select-agent` after extract and stream decisions
|
|
458
|
+
// into wizard state via stderr progress lines. The subprocess writes
|
|
459
|
+
// `raw_components.status` + `reject_reason` itself (see Task 2), so the
|
|
460
|
+
// scope-gate UI re-loads via `loadScopeComponents` to get fresh data on every
|
|
461
|
+
// render — but we also keep a memory-side `aiDecisions` map for streaming UX.
|
|
462
|
+
const runAutoFilter = (sessionId) => {
|
|
463
|
+
return new Promise((res) => {
|
|
464
|
+
const args = buildSelectAgentArgs({
|
|
465
|
+
sessionId,
|
|
466
|
+
agent: state.agent,
|
|
467
|
+
...(state.agentModel ? { model: state.agentModel } : {}),
|
|
468
|
+
selectPromptPath,
|
|
469
|
+
noCache,
|
|
470
|
+
});
|
|
471
|
+
const child = spawn('node', [findCliPath(), ...args]);
|
|
472
|
+
autoFilterChildRef.current = child;
|
|
473
|
+
let stderr = '';
|
|
474
|
+
child.stderr.on('data', (d) => {
|
|
475
|
+
const chunk = String(d);
|
|
476
|
+
stderr += chunk;
|
|
477
|
+
for (const line of chunk.split('\n')) {
|
|
478
|
+
const trimmed = line.trim();
|
|
479
|
+
if (!trimmed)
|
|
480
|
+
continue;
|
|
481
|
+
const parsed = parseAutoFilterProgressLine(trimmed);
|
|
482
|
+
if (!parsed)
|
|
483
|
+
continue;
|
|
484
|
+
setState((prev) => ({
|
|
485
|
+
...prev,
|
|
486
|
+
aiFilterProgress: { done: parsed.n, total: parsed.total },
|
|
487
|
+
aiDecisions: {
|
|
488
|
+
...prev.aiDecisions,
|
|
489
|
+
[parsed.name]: { decision: parsed.decision, reason: parsed.reason },
|
|
490
|
+
},
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
// Use 'close' (not 'exit') so the status flip happens after the child's
|
|
495
|
+
// stdio AND its inherited SQLite WAL/SHM file handles have been fully
|
|
496
|
+
// released by the OS. 'exit' fires the instant the process terminates;
|
|
497
|
+
// setting state then triggers a wizard re-render that re-opens
|
|
498
|
+
// pipeline.db from the scope-gate step, and the lock from the dead
|
|
499
|
+
// child's still-mapped WAL handle surfaces as "database is locked".
|
|
500
|
+
child.on('close', (code, signal) => {
|
|
501
|
+
autoFilterChildRef.current = null;
|
|
502
|
+
if (signal === 'SIGTERM') {
|
|
503
|
+
setState((prev) => ({ ...prev, aiFilterStatus: 'cancelled' }));
|
|
504
|
+
}
|
|
505
|
+
else if ((code ?? 0) !== 0) {
|
|
506
|
+
const tail = buildAutoFilterErrorTail(stderr);
|
|
507
|
+
setState((prev) => ({
|
|
508
|
+
...prev,
|
|
509
|
+
aiFilterStatus: 'failed',
|
|
510
|
+
aiFilterError: tail || `exit ${code}`,
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
setState((prev) => ({ ...prev, aiFilterStatus: 'complete' }));
|
|
515
|
+
}
|
|
516
|
+
res();
|
|
517
|
+
});
|
|
518
|
+
child.on('error', (err) => {
|
|
519
|
+
autoFilterChildRef.current = null;
|
|
520
|
+
setState((prev) => ({
|
|
521
|
+
...prev,
|
|
522
|
+
aiFilterStatus: 'failed',
|
|
523
|
+
aiFilterError: err.message,
|
|
524
|
+
}));
|
|
525
|
+
res();
|
|
526
|
+
});
|
|
335
527
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
528
|
+
};
|
|
529
|
+
const cancelAutoFilter = () => {
|
|
530
|
+
const child = autoFilterChildRef.current;
|
|
531
|
+
if (child && !child.killed) {
|
|
532
|
+
try {
|
|
533
|
+
child.kill('SIGTERM');
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// best-effort
|
|
537
|
+
}
|
|
342
538
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
539
|
+
};
|
|
540
|
+
// Variant that returns a Promise resolving when the subprocess has fully
|
|
541
|
+
// exited. Used by scope-gate confirm to guarantee operator-write-last
|
|
542
|
+
// ordering on the review-state snapshot (PR #43 race fix).
|
|
543
|
+
const cancelAutoFilterAndWait = async () => {
|
|
544
|
+
const child = autoFilterChildRef.current;
|
|
545
|
+
const donePromise = autoFilterDonePromiseRef.current;
|
|
546
|
+
if (!child || child.killed) {
|
|
547
|
+
// Subprocess already gone (or never started). If the Promise is still
|
|
548
|
+
// pending — e.g. exit handler hasn't fired yet — await it; otherwise
|
|
549
|
+
// resolve immediately.
|
|
550
|
+
if (donePromise)
|
|
551
|
+
await donePromise;
|
|
354
552
|
return;
|
|
355
553
|
}
|
|
356
|
-
// Read accepted count from the review state file (since TUI subprocess inherits stdio
|
|
357
|
-
// in the manual review path; in the bulk path we also persist to the same file).
|
|
358
|
-
// Also extract auto-rejected count (components with error-severity validation issues
|
|
359
|
-
// dropped by the gate) so the user sees what was excluded on the next step.
|
|
360
|
-
const artifactsRoot = process.env['EDS_REVIEW_ARTIFACTS_DIR']
|
|
361
|
-
? resolve(process.env['EDS_REVIEW_ARTIFACTS_DIR'])
|
|
362
|
-
: resolve(homedir(), '.contentful', 'experience-design-system-cli', 'reviews');
|
|
363
|
-
const reviewStatePath = resolve(artifactsRoot, sessionId, 'current-review-state.json');
|
|
364
|
-
let autoRejectedCount = 0;
|
|
365
554
|
try {
|
|
366
|
-
|
|
367
|
-
acceptedCount = reviewState.components.filter((c) => c.status === 'accepted').length;
|
|
368
|
-
autoRejectedCount = reviewState.components.filter((c) => c.status === 'rejected' && (c.originalProposal.validationIssues ?? []).some((i) => i.severity === 'error')).length;
|
|
555
|
+
child.kill('SIGTERM');
|
|
369
556
|
}
|
|
370
557
|
catch {
|
|
371
|
-
|
|
558
|
+
// best-effort
|
|
372
559
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
560
|
+
if (donePromise)
|
|
561
|
+
await donePromise;
|
|
562
|
+
};
|
|
563
|
+
// Cancel a running generation prefetch (SIGTERM the child + clear refs +
|
|
564
|
+
// reset state). Best-effort — if the child has already exited, this is a
|
|
565
|
+
// no-op aside from clearing the prefetch status.
|
|
566
|
+
const cancelGeneratePrefetch = () => {
|
|
567
|
+
const child = generateChildRef.current;
|
|
568
|
+
if (child && !child.killed) {
|
|
569
|
+
try {
|
|
570
|
+
child.kill('SIGTERM');
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// best-effort
|
|
377
574
|
}
|
|
378
575
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
576
|
+
generateChildRef.current = null;
|
|
577
|
+
generatePromiseRef.current = null;
|
|
578
|
+
setState((prev) => ({
|
|
579
|
+
...prev,
|
|
580
|
+
generatePrefetchStatus: 'idle',
|
|
581
|
+
generatePrefetchError: null,
|
|
582
|
+
generateProgress: null,
|
|
583
|
+
}));
|
|
584
|
+
};
|
|
585
|
+
// Spawn `generate components` in the background. Wires up the same
|
|
586
|
+
// stderr-progress streaming as `runGenerate` but stores the in-flight
|
|
587
|
+
// promise in a ref so the post-credentials path can await it.
|
|
588
|
+
const startGeneratePrefetch = (extractSessionId, tokensPath) => {
|
|
589
|
+
const args = [
|
|
590
|
+
findCliPath(),
|
|
591
|
+
...buildGenerateComponentsArgs({
|
|
592
|
+
sessionId: extractSessionId,
|
|
593
|
+
tokensPath,
|
|
594
|
+
agent: state.agent,
|
|
595
|
+
...(state.agentModel ? { model: state.agentModel } : {}),
|
|
596
|
+
noCache,
|
|
597
|
+
}),
|
|
598
|
+
];
|
|
599
|
+
let progressCursor = null;
|
|
600
|
+
const { child, donePromise } = spawnGenerateChild({
|
|
601
|
+
command: 'node',
|
|
602
|
+
args,
|
|
603
|
+
onStderr: (chunk) => {
|
|
604
|
+
const nextProgress = parseGenerateStderrChunk(chunk, progressCursor);
|
|
605
|
+
if (nextProgress !== progressCursor) {
|
|
606
|
+
progressCursor = nextProgress;
|
|
607
|
+
setState((prev) => ({ ...prev, generateProgress: nextProgress }));
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
generateChildRef.current = child;
|
|
612
|
+
generatePromiseRef.current = donePromise;
|
|
613
|
+
setState((prev) => ({
|
|
614
|
+
...prev,
|
|
615
|
+
generatePrefetchStatus: 'running',
|
|
616
|
+
generatePrefetchError: null,
|
|
617
|
+
}));
|
|
618
|
+
donePromise
|
|
619
|
+
.then((result) => {
|
|
620
|
+
// Clear the child ref regardless of how it ended.
|
|
621
|
+
generateChildRef.current = null;
|
|
622
|
+
if (result.signal === 'SIGTERM') {
|
|
623
|
+
// Caller already reset state via cancelGeneratePrefetch.
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (result.exitCode !== 0) {
|
|
627
|
+
const tail = result.stderr.trim().split('\n').slice(-3).join('\n') || `exit ${result.exitCode}`;
|
|
628
|
+
setState((prev) => ({
|
|
629
|
+
...prev,
|
|
630
|
+
generatePrefetchStatus: 'failed',
|
|
631
|
+
generatePrefetchError: tail,
|
|
632
|
+
generateProgress: null,
|
|
633
|
+
}));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const sessionMatch = /^session=(.+)$/m.exec(result.stdout);
|
|
637
|
+
const generateSessionId = sessionMatch ? sessionMatch[1].trim() : null;
|
|
638
|
+
const countMatch = /(\d+) components?/.exec(result.stderr);
|
|
639
|
+
const generatedCount = countMatch ? Number(countMatch[1]) : 0;
|
|
640
|
+
const renamedMatch = /^renamed-slots:\s*(\d+)$/m.exec(result.stdout);
|
|
641
|
+
const renamedSlotsCount = renamedMatch ? Number(renamedMatch[1]) : 0;
|
|
642
|
+
// Persist generated state but DO NOT auto-advance — the credentials
|
|
643
|
+
// path will pick this up via `advanceAfterCredentialsValidated`.
|
|
644
|
+
setState((prev) => ({
|
|
645
|
+
...prev,
|
|
646
|
+
generateSessionId,
|
|
647
|
+
generatedCount,
|
|
648
|
+
renamedSlotsCount,
|
|
649
|
+
generateProgress: null,
|
|
650
|
+
generatePrefetchStatus: 'complete',
|
|
651
|
+
}));
|
|
652
|
+
})
|
|
653
|
+
.catch(() => {
|
|
654
|
+
generateChildRef.current = null;
|
|
655
|
+
setState((prev) => ({
|
|
656
|
+
...prev,
|
|
657
|
+
generatePrefetchStatus: 'failed',
|
|
658
|
+
generatePrefetchError: 'subprocess error',
|
|
659
|
+
}));
|
|
660
|
+
});
|
|
661
|
+
return donePromise;
|
|
382
662
|
};
|
|
383
663
|
const runGenerate = async (extractSessionId, tokensPath, acceptedCount) => {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
current: m[3].trim(),
|
|
405
|
-
},
|
|
406
|
-
});
|
|
664
|
+
const args = [
|
|
665
|
+
findCliPath(),
|
|
666
|
+
...buildGenerateComponentsArgs({
|
|
667
|
+
sessionId: extractSessionId,
|
|
668
|
+
tokensPath,
|
|
669
|
+
agent: state.agent,
|
|
670
|
+
...(state.agentModel ? { model: state.agentModel } : {}),
|
|
671
|
+
noCache,
|
|
672
|
+
generatePromptPath,
|
|
673
|
+
}),
|
|
674
|
+
];
|
|
675
|
+
let progressCursor = state.generateProgress;
|
|
676
|
+
const { donePromise } = spawnGenerateChild({
|
|
677
|
+
command: 'node',
|
|
678
|
+
args,
|
|
679
|
+
onStderr: (chunk) => {
|
|
680
|
+
const nextProgress = parseGenerateStderrChunk(chunk, progressCursor);
|
|
681
|
+
if (nextProgress !== progressCursor) {
|
|
682
|
+
progressCursor = nextProgress;
|
|
683
|
+
update({ generateProgress: nextProgress });
|
|
407
684
|
}
|
|
408
|
-
}
|
|
409
|
-
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
685
|
+
},
|
|
410
686
|
});
|
|
687
|
+
const result = await donePromise;
|
|
411
688
|
if (result.exitCode !== 0) {
|
|
412
689
|
update({
|
|
413
690
|
step: 'error',
|
|
@@ -423,7 +700,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
423
700
|
const renamedMatch = /^renamed-slots:\s*(\d+)$/m.exec(result.stdout);
|
|
424
701
|
const renamedSlotsCount = renamedMatch ? Number(renamedMatch[1]) : 0;
|
|
425
702
|
update({
|
|
426
|
-
step: 'review
|
|
703
|
+
step: 'final-review',
|
|
427
704
|
generateSessionId,
|
|
428
705
|
generatedCount,
|
|
429
706
|
renamedSlotsCount,
|
|
@@ -433,97 +710,9 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
433
710
|
const advanceToPushFlow = (generatedAcceptedCount) => {
|
|
434
711
|
update({ generatedAcceptedCount, step: 'credentials' });
|
|
435
712
|
};
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
generatedAcceptedCount = generatedCount;
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
update({ step: 'generate-edit' });
|
|
443
|
-
if (sessionId) {
|
|
444
|
-
const r = await runCliInteractive(['generate', 'components', 'edit', '--session', sessionId]);
|
|
445
|
-
if (r.exitCode !== 0) {
|
|
446
|
-
update({
|
|
447
|
-
step: 'error',
|
|
448
|
-
errorStep: 'generate edit',
|
|
449
|
-
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
450
|
-
});
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
const acceptedMatch = /Accepted: (\d+)/.exec(r.stderr);
|
|
454
|
-
generatedAcceptedCount = acceptedMatch ? Number(acceptedMatch[1]) : generatedCount;
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
generatedAcceptedCount = generatedCount;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (returnToPreview) {
|
|
461
|
-
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
462
|
-
void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
advanceToPushFlow(generatedAcceptedCount);
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const setPreviewEnvVars = () => {
|
|
469
|
-
const creds = credentialsRef.current;
|
|
470
|
-
const cmaToken = creds?.cmaToken || state.cmaToken;
|
|
471
|
-
const spaceId = creds?.spaceId || state.spaceId;
|
|
472
|
-
const environmentId = creds?.environmentId || state.environmentId;
|
|
473
|
-
if (cmaToken)
|
|
474
|
-
process.env['EDS_CMA_TOKEN'] = cmaToken;
|
|
475
|
-
if (spaceId)
|
|
476
|
-
process.env['EDS_SPACE_ID'] = spaceId;
|
|
477
|
-
if (environmentId)
|
|
478
|
-
process.env['EDS_ENVIRONMENT_ID'] = environmentId;
|
|
479
|
-
process.env['EDS_TOKENS_PATH'] = state.tokensPath || '';
|
|
480
|
-
};
|
|
481
|
-
const clearPreviewEnvVars = () => {
|
|
482
|
-
delete process.env['EDS_PREVIEW_ANNOTATIONS'];
|
|
483
|
-
delete process.env['EDS_PREVIEW_COUNTS'];
|
|
484
|
-
delete process.env['EDS_CMA_TOKEN'];
|
|
485
|
-
delete process.env['EDS_SPACE_ID'];
|
|
486
|
-
delete process.env['EDS_ENVIRONMENT_ID'];
|
|
487
|
-
delete process.env['EDS_TOKENS_PATH'];
|
|
488
|
-
};
|
|
489
|
-
const runEditFromPreview = async (preview) => {
|
|
490
|
-
const sessionId = state.extractSessionId;
|
|
491
|
-
if (!sessionId) {
|
|
492
|
-
update({
|
|
493
|
-
step: 'error',
|
|
494
|
-
errorStep: 'edit definitions',
|
|
495
|
-
errorMessage: 'No session available for editing',
|
|
496
|
-
});
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
process.env['EDS_PREVIEW_ANNOTATIONS'] = JSON.stringify(buildPreviewAnnotations(preview));
|
|
500
|
-
if (preview) {
|
|
501
|
-
process.env['EDS_PREVIEW_COUNTS'] = JSON.stringify({
|
|
502
|
-
compNew: preview.components.new.length,
|
|
503
|
-
compChanged: preview.components.changed.length,
|
|
504
|
-
compRemoved: preview.components.removed.length,
|
|
505
|
-
compUnchanged: preview.components.unchanged.length,
|
|
506
|
-
tokNew: preview.tokens.new.length,
|
|
507
|
-
tokChanged: preview.tokens.changed.length,
|
|
508
|
-
tokRemoved: preview.tokens.removed.length,
|
|
509
|
-
tokUnchanged: preview.tokens.unchanged.length,
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
setPreviewEnvVars();
|
|
513
|
-
update({ step: 'analyze-select' });
|
|
514
|
-
const r = await runCliInteractive(['analyze', 'select', '--session', sessionId]);
|
|
515
|
-
clearPreviewEnvVars();
|
|
516
|
-
if (r.exitCode !== 0) {
|
|
517
|
-
update({
|
|
518
|
-
step: 'error',
|
|
519
|
-
errorStep: 'edit definitions',
|
|
520
|
-
errorMessage: 'Editor exited with an error',
|
|
521
|
-
});
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
// Re-preview with updated definitions
|
|
525
|
-
const { extractSessionId: sid, tokensPath: tp } = sessionRef.current;
|
|
526
|
-
void runPreview(sid, tp, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
713
|
+
const runEditFromPreview = async () => {
|
|
714
|
+
// Post-preview edits land in the unified final-review screen.
|
|
715
|
+
update({ step: 'final-review' });
|
|
527
716
|
};
|
|
528
717
|
const runSkipValidationErrorsAndRetry = async (errors) => {
|
|
529
718
|
await applySkipValidationErrors(state.extractSessionId, errors);
|
|
@@ -533,14 +722,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
533
722
|
const advanceWithCredentials = (spaceId, environmentId, cmaToken, host) => {
|
|
534
723
|
const resolvedHost = resolveWizardHost(host);
|
|
535
724
|
credentialsRef.current = { spaceId, environmentId, cmaToken };
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
step: 'credential-test-gate',
|
|
543
|
-
});
|
|
725
|
+
// The dedicated `credential-test-gate` screen was dropped — credentials are
|
|
726
|
+
// now always validated immediately after they're persisted. The inline
|
|
727
|
+
// `credentialsValidating` status (PR #54) handles the visual feedback while
|
|
728
|
+
// the API ping is in flight. The literal `'credential-test-gate'` is kept
|
|
729
|
+
// in the WizardStep union for back-compat, but is never set as a step.
|
|
730
|
+
void validateCredentials(spaceId, environmentId, cmaToken, resolvedHost);
|
|
544
731
|
};
|
|
545
732
|
const confirmCredentials = async (spaceId, environmentId, cmaToken, host) => {
|
|
546
733
|
const resolvedHost = resolveWizardHost(host);
|
|
@@ -566,7 +753,9 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
566
753
|
}
|
|
567
754
|
};
|
|
568
755
|
const validateCredentials = async (spaceId, environmentId, cmaToken, host) => {
|
|
569
|
-
|
|
756
|
+
// Inline validation: stay on the credentials step, flip the validating
|
|
757
|
+
// boolean so CredentialsStep locks input + renders an inline status.
|
|
758
|
+
update({ step: 'credentials', credentialsValidating: true, credentialsError: '' });
|
|
570
759
|
try {
|
|
571
760
|
const resolvedHost = resolveWizardHost(host);
|
|
572
761
|
const client = new ImportApiClient({
|
|
@@ -576,24 +765,89 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
576
765
|
host: resolvedHost,
|
|
577
766
|
});
|
|
578
767
|
await client.validateToken();
|
|
579
|
-
|
|
580
|
-
|
|
768
|
+
update({ credentialsValidating: false });
|
|
769
|
+
await advanceAfterCredentialsValidated();
|
|
581
770
|
}
|
|
582
771
|
catch (e) {
|
|
583
772
|
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
|
|
584
|
-
|
|
773
|
+
// Validation failure: cancel any in-flight generate prefetch so we
|
|
774
|
+
// don't leave an orphaned subprocess running after the operator backs
|
|
775
|
+
// out (Risk #1 in the spec).
|
|
776
|
+
cancelGeneratePrefetch();
|
|
777
|
+
update({ step: 'credentials', credentialsValidating: false, credentialsError: e.message });
|
|
585
778
|
return;
|
|
586
779
|
}
|
|
587
780
|
const msg = e instanceof Error ? e.message : 'Credential check failed';
|
|
781
|
+
cancelGeneratePrefetch();
|
|
588
782
|
update({
|
|
589
783
|
step: 'error',
|
|
590
784
|
errorStep: 'validating-credentials',
|
|
591
785
|
errorMessage: msg,
|
|
592
786
|
errorAllowCredentialRetry: false,
|
|
787
|
+
credentialsValidating: false,
|
|
593
788
|
});
|
|
594
789
|
}
|
|
595
790
|
};
|
|
791
|
+
// Branch after credentials are known good (validated, or skipped via the
|
|
792
|
+
// credential-test-gate skip path). Either runs the generator (if there are
|
|
793
|
+
// accepted components) or jumps straight to push-decision-gate (if scope
|
|
794
|
+
// rejected everything but tokens/removals still need to be pushed).
|
|
795
|
+
const advanceAfterCredentialsValidated = async () => {
|
|
796
|
+
// If we already ran the generator (re-entering credentials from a late
|
|
797
|
+
// 401/403 raised by runPreview), skip back to push-decision-gate rather
|
|
798
|
+
// than re-running the LLM.
|
|
799
|
+
if (state.generateSessionId) {
|
|
800
|
+
update({ step: 'push-decision-gate' });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const next = nextStepAfterCredentialsValidated({ acceptedCount: state.acceptedCount });
|
|
804
|
+
if (next === 'generating') {
|
|
805
|
+
const sid = sessionRef.current.extractSessionId;
|
|
806
|
+
if (!sid) {
|
|
807
|
+
update({
|
|
808
|
+
step: 'error',
|
|
809
|
+
errorStep: 'post-credentials',
|
|
810
|
+
errorMessage: 'Internal error: extract session ID missing after credential validation.',
|
|
811
|
+
});
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// Prefetch path: if a background generate is already running or has
|
|
815
|
+
// already completed, await it (or use its result) instead of spawning
|
|
816
|
+
// a second LLM call. Transition to 'generating' first so the operator
|
|
817
|
+
// sees the familiar RunningStep progress screen while we wait —
|
|
818
|
+
// matching the no-prefetch flow's visual.
|
|
819
|
+
const inflight = generatePromiseRef.current;
|
|
820
|
+
if (inflight) {
|
|
821
|
+
update({ step: 'generating' });
|
|
822
|
+
const result = await inflight;
|
|
823
|
+
generatePromiseRef.current = null;
|
|
824
|
+
if (result.exitCode === 0 && result.signal !== 'SIGTERM') {
|
|
825
|
+
// The donePromise.then() handler already populated
|
|
826
|
+
// generateSessionId / generatedCount in state. Advance to
|
|
827
|
+
// final-review using the latest values via functional setState so
|
|
828
|
+
// we read whichever update landed last.
|
|
829
|
+
setState((prev) => ({ ...prev, step: 'final-review' }));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
// Prefetch failed — fall through to retry via a fresh runGenerate.
|
|
833
|
+
}
|
|
834
|
+
if (await runAgentAuthCheck('generating')) {
|
|
835
|
+
void runGenerate(sid, state.tokensPath, state.acceptedCount);
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
update({ step: 'push-decision-gate' });
|
|
840
|
+
};
|
|
596
841
|
const runPreview = async (extractSessionId, tokensPath, spaceId, environmentId, cmaToken, host) => {
|
|
842
|
+
// Skip-credentials short-circuit. When the operator pressed `s` on the
|
|
843
|
+
// credentials screen, we never got a working token — calling
|
|
844
|
+
// previewImport would 401/403 (or worse, send a half-formed manifest
|
|
845
|
+
// somewhere). Jump straight to the push-decision-gate; Task 3 disables
|
|
846
|
+
// the push options downstream.
|
|
847
|
+
if (shouldBypassPreview(state)) {
|
|
848
|
+
update(buildSkippedPreviewTransition());
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
597
851
|
update({ step: 'previewing' });
|
|
598
852
|
const resolvedHost = resolveWizardHost(host);
|
|
599
853
|
try {
|
|
@@ -721,6 +975,16 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
721
975
|
}
|
|
722
976
|
};
|
|
723
977
|
const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, preview) => {
|
|
978
|
+
// Skip-credentials defensive guard. The push-decision-gate disables
|
|
979
|
+
// push-emitting choices when `credentialsSkipped` is true, so we
|
|
980
|
+
// should never get here in practice. But if a state-machine bug or
|
|
981
|
+
// future regression ever did, refuse to issue the API call — there
|
|
982
|
+
// is no validated token and the operator explicitly opted out of
|
|
983
|
+
// push. Route back to the local-save (print-gate) path instead.
|
|
984
|
+
if (shouldRefusePush(state)) {
|
|
985
|
+
update(buildSkippedPushTransition());
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
724
988
|
if (preview) {
|
|
725
989
|
const hasComponentChanges = preview.components.new.length > 0 ||
|
|
726
990
|
preview.components.changed.length > 0 ||
|
|
@@ -730,14 +994,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
730
994
|
update({
|
|
731
995
|
step: 'done',
|
|
732
996
|
pushResult: {
|
|
733
|
-
componentTypes: { created: 0, updated: 0, failed: 0 },
|
|
734
|
-
designTokens: { created: 0, updated: 0, failed: 0 },
|
|
997
|
+
componentTypes: { created: 0, updated: 0, removed: 0, failed: 0 },
|
|
998
|
+
designTokens: { created: 0, updated: 0, removed: 0, failed: 0 },
|
|
735
999
|
},
|
|
736
1000
|
});
|
|
737
1001
|
return;
|
|
738
1002
|
}
|
|
739
1003
|
}
|
|
740
|
-
|
|
1004
|
+
const pushExpected = preview ? computePushExpected(preview) : null;
|
|
1005
|
+
update({ step: 'pushing', pushExpected, pushProgress: null });
|
|
741
1006
|
try {
|
|
742
1007
|
const resolvedHost = resolveWizardHost(host);
|
|
743
1008
|
const client = new ImportApiClient({
|
|
@@ -760,7 +1025,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
760
1025
|
process.stderr.write(`[eds] log write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
761
1026
|
}
|
|
762
1027
|
update({
|
|
763
|
-
pushProgress:
|
|
1028
|
+
pushProgress: { kind: 'queued', operationId: operation.sys.id },
|
|
764
1029
|
});
|
|
765
1030
|
let pollCount = 0;
|
|
766
1031
|
operation = await client.pollOperation(operation.sys.id, {
|
|
@@ -769,7 +1034,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
769
1034
|
const s = op.summary;
|
|
770
1035
|
if (s) {
|
|
771
1036
|
const done = s.total - s.pending;
|
|
772
|
-
|
|
1037
|
+
const items = op.items ?? [];
|
|
1038
|
+
// Best-effort fresh signal: pick the most recently succeeded item
|
|
1039
|
+
// (the API does not surface an in-progress status today). Falls
|
|
1040
|
+
// back to null and the PushingStep hides the line.
|
|
1041
|
+
const lastDone = items.length > 0 ? items[items.length - 1] : null;
|
|
1042
|
+
const current = lastDone && lastDone.status === 'succeeded' ? lastDone.id : null;
|
|
1043
|
+
update({
|
|
1044
|
+
pushProgress: { kind: 'progress', processed: done, total: s.total, current },
|
|
1045
|
+
});
|
|
773
1046
|
}
|
|
774
1047
|
try {
|
|
775
1048
|
logStep({
|
|
@@ -806,11 +1079,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
806
1079
|
componentTypes: {
|
|
807
1080
|
created: items.filter((i) => i.entityType === 'ComponentType' && i.action === 'create' && i.status === 'succeeded').length,
|
|
808
1081
|
updated: items.filter((i) => i.entityType === 'ComponentType' && i.action === 'update' && i.status === 'succeeded').length,
|
|
1082
|
+
removed: items.filter((i) => i.entityType === 'ComponentType' && i.action === 'delete' && i.status === 'succeeded').length,
|
|
809
1083
|
failed: items.filter((i) => i.entityType === 'ComponentType' && i.status === 'failed').length,
|
|
810
1084
|
},
|
|
811
1085
|
designTokens: {
|
|
812
1086
|
created: items.filter((i) => i.entityType === 'DesignToken' && i.action === 'create' && i.status === 'succeeded').length,
|
|
813
1087
|
updated: items.filter((i) => i.entityType === 'DesignToken' && i.action === 'update' && i.status === 'succeeded').length,
|
|
1088
|
+
removed: items.filter((i) => i.entityType === 'DesignToken' && i.action === 'delete' && i.status === 'succeeded').length,
|
|
814
1089
|
failed: items.filter((i) => i.entityType === 'DesignToken' && i.status === 'failed').length,
|
|
815
1090
|
},
|
|
816
1091
|
summary: operation.summary,
|
|
@@ -822,11 +1097,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
822
1097
|
componentTypes: {
|
|
823
1098
|
created: preview?.components.new.length ?? 0,
|
|
824
1099
|
updated: preview?.components.changed.length ?? 0,
|
|
1100
|
+
removed: preview?.components.removed.length ?? 0,
|
|
825
1101
|
failed: 0,
|
|
826
1102
|
},
|
|
827
1103
|
designTokens: {
|
|
828
1104
|
created: preview?.tokens.new.length ?? 0,
|
|
829
1105
|
updated: preview?.tokens.changed.length ?? 0,
|
|
1106
|
+
removed: preview?.tokens.removed.length ?? 0,
|
|
830
1107
|
failed: 0,
|
|
831
1108
|
},
|
|
832
1109
|
summary: operation.summary,
|
|
@@ -844,7 +1121,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
844
1121
|
});
|
|
845
1122
|
}
|
|
846
1123
|
};
|
|
847
|
-
const runPrintFiles = async (extractSessionId, outDir) => {
|
|
1124
|
+
const runPrintFiles = async (extractSessionId, outDir, opts = {}) => {
|
|
848
1125
|
update({ step: 'printing' });
|
|
849
1126
|
const componentsPath = join(outDir, 'components.json');
|
|
850
1127
|
const printArgs = ['print', 'components', '--out', componentsPath];
|
|
@@ -857,10 +1134,142 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
857
1134
|
errorStep: 'print components',
|
|
858
1135
|
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
859
1136
|
});
|
|
1137
|
+
return { ok: false };
|
|
1138
|
+
}
|
|
1139
|
+
// Co-locate tokens.json with components.json. Without this, `--out-dir`
|
|
1140
|
+
// would move components.json to the operator's chosen path while leaving
|
|
1141
|
+
// tokens.json behind at <projectPath>/.contentful/tokens.json.
|
|
1142
|
+
let emittedTokensPath;
|
|
1143
|
+
let emittedTokenCount;
|
|
1144
|
+
if (opts.tokenSessionId) {
|
|
1145
|
+
const tokensOut = join(outDir, 'tokens.json');
|
|
1146
|
+
const tokenArgs = ['print', 'tokens', '--out', tokensOut, '--session', opts.tokenSessionId];
|
|
1147
|
+
const tr = await runCli(tokenArgs);
|
|
1148
|
+
if (tr.exitCode !== 0) {
|
|
1149
|
+
update({
|
|
1150
|
+
step: 'error',
|
|
1151
|
+
errorStep: 'print tokens',
|
|
1152
|
+
errorMessage: tr.stderr.trim() || 'Unknown error',
|
|
1153
|
+
});
|
|
1154
|
+
return { ok: false };
|
|
1155
|
+
}
|
|
1156
|
+
emittedTokensPath = tokensOut;
|
|
1157
|
+
emittedTokenCount = parsePrintTokensCount(tr.stdout);
|
|
1158
|
+
}
|
|
1159
|
+
update(nextStateAfterPrint({ skipGate: opts.skipGate, componentsPath }));
|
|
1160
|
+
return {
|
|
1161
|
+
ok: true,
|
|
1162
|
+
...(emittedTokensPath ? { tokensPath: emittedTokensPath } : {}),
|
|
1163
|
+
...(typeof emittedTokenCount === 'number' ? { tokenCount: emittedTokenCount } : {}),
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1166
|
+
const runSaveAndPush = async () => {
|
|
1167
|
+
await startSaveFlow({ skipGate: true, andPush: true });
|
|
1168
|
+
};
|
|
1169
|
+
// ── Task 4: save-path orchestration ────────────────────────────────────────
|
|
1170
|
+
// Wraps every `runPrintFiles` site. When `--out-dir` is set we skip the
|
|
1171
|
+
// inline prompt and conflict gate entirely. Otherwise the wizard transitions
|
|
1172
|
+
// to the path-prompt step; the operator's submit handler calls back into
|
|
1173
|
+
// `proceedToWrite` (which may surface the conflict gate).
|
|
1174
|
+
const pendingSaveOptionsRef = useRef({});
|
|
1175
|
+
const startSaveFlow = async (opts = {}) => {
|
|
1176
|
+
pendingSaveOptionsRef.current = opts;
|
|
1177
|
+
if (outDirOverride) {
|
|
1178
|
+
await mkdir(outDirOverride, { recursive: true });
|
|
1179
|
+
if (onConflictMode) {
|
|
1180
|
+
const resolved = await resolveSavePath(outDirOverride, { onConflict: onConflictMode });
|
|
1181
|
+
if (resolved.kind === 'fail') {
|
|
1182
|
+
const files = resolved.conflict.files.join(', ');
|
|
1183
|
+
process.stderr.write(`Error: --on-conflict fail — refusing to overwrite ${files} at ${resolved.conflict.path}.\n`);
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (resolved.kind === 'write') {
|
|
1188
|
+
await mkdir(resolved.path, { recursive: true });
|
|
1189
|
+
await proceedToWrite(resolved.path);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
await proceedToWrite(outDirOverride);
|
|
860
1194
|
return;
|
|
861
1195
|
}
|
|
862
|
-
|
|
863
|
-
|
|
1196
|
+
setState((prev) => ({ ...prev, step: 'path-prompt' }));
|
|
1197
|
+
};
|
|
1198
|
+
const proceedToWrite = async (path) => {
|
|
1199
|
+
setState((prev) => ({ ...prev, outDir: path }));
|
|
1200
|
+
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
1201
|
+
const { skipGate, andPush } = pendingSaveOptionsRef.current;
|
|
1202
|
+
const result = await runPrintFiles(extractSessionId, path, {
|
|
1203
|
+
...(skipGate ? { skipGate: true } : {}),
|
|
1204
|
+
...(state.tokenSessionId ? { tokenSessionId: state.tokenSessionId } : {}),
|
|
1205
|
+
});
|
|
1206
|
+
if (!result.ok)
|
|
1207
|
+
return;
|
|
1208
|
+
// Prefer the freshly emitted path/count (covers --out-dir); fall back to
|
|
1209
|
+
// the values captured during the original generate-tokens step.
|
|
1210
|
+
const recordedTokensPath = result.tokensPath ?? (state.tokenSessionId ? tokensPath || null : null);
|
|
1211
|
+
const recordedTokenCount = result.tokenCount ?? state.tokenCount;
|
|
1212
|
+
if (result.ok) {
|
|
1213
|
+
// Append a run record on every successful write. Best-effort: append
|
|
1214
|
+
// failures must not break the wizard flow (they surface on stderr).
|
|
1215
|
+
try {
|
|
1216
|
+
// Build the v3 fingerprints. Both are best-effort: if any step
|
|
1217
|
+
// throws (missing source file, hash failure, db lookup error) we
|
|
1218
|
+
// fall back to null fingerprints rather than aborting the save.
|
|
1219
|
+
let sourceFingerprint = null;
|
|
1220
|
+
let savedFingerprint = null;
|
|
1221
|
+
try {
|
|
1222
|
+
if (state.extractSessionId) {
|
|
1223
|
+
const db = openPipelineDb();
|
|
1224
|
+
try {
|
|
1225
|
+
sourceFingerprint = await buildSourceFingerprint({
|
|
1226
|
+
db,
|
|
1227
|
+
extractSessionId: state.extractSessionId,
|
|
1228
|
+
rawTokensPath: state.rawTokensPath || null,
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
finally {
|
|
1232
|
+
db.close();
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
process.stderr.write(`Warning: failed to compute source fingerprint: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const componentsBuf = await readFile(join(path, 'components.json')).catch(() => null);
|
|
1241
|
+
const tokensBuf = recordedTokensPath ? await readFile(recordedTokensPath).catch(() => null) : null;
|
|
1242
|
+
savedFingerprint = buildSavedFingerprint({
|
|
1243
|
+
componentsJson: componentsBuf,
|
|
1244
|
+
tokensJson: tokensBuf,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
catch (err) {
|
|
1248
|
+
process.stderr.write(`Warning: failed to compute saved fingerprint: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1249
|
+
}
|
|
1250
|
+
const record = await appendRun({
|
|
1251
|
+
projectPath: state.projectPath,
|
|
1252
|
+
savePath: path,
|
|
1253
|
+
componentCount: state.generatedAcceptedCount || state.generatedCount,
|
|
1254
|
+
tokenCount: recordedTokenCount,
|
|
1255
|
+
tokensPath: recordedTokensPath || null,
|
|
1256
|
+
tokenSessionId: state.tokenSessionId,
|
|
1257
|
+
agent: state.agent,
|
|
1258
|
+
pushedTo: null,
|
|
1259
|
+
extractSessionId: state.extractSessionId ?? '',
|
|
1260
|
+
generateSessionId: state.generateSessionId,
|
|
1261
|
+
sourceFingerprint,
|
|
1262
|
+
savedFingerprint,
|
|
1263
|
+
});
|
|
1264
|
+
setState((prev) => ({ ...prev, lastRunId: record.id }));
|
|
1265
|
+
}
|
|
1266
|
+
catch (err) {
|
|
1267
|
+
process.stderr.write(`Warning: failed to record run: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (andPush) {
|
|
1271
|
+
void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
1272
|
+
}
|
|
864
1273
|
};
|
|
865
1274
|
// ── Effect: kick off automatic steps ───────────────────────────────────────────────
|
|
866
1275
|
const tokenReuseChecked = useRef(false);
|
|
@@ -895,6 +1304,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
895
1304
|
}, [state.step]); // intentional: only re-run when step changes
|
|
896
1305
|
// ── Render ────────────────────────────────────────────────────────────────────────────
|
|
897
1306
|
const noQuitSteps = [
|
|
1307
|
+
'run-picker',
|
|
898
1308
|
'checking-claude-auth',
|
|
899
1309
|
'validating-credentials',
|
|
900
1310
|
'generating-tokens',
|
|
@@ -911,6 +1321,18 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
911
1321
|
const totalSteps = 3 + (hasTokens ? 1 : 0) + (hasComponents ? 2 : 0);
|
|
912
1322
|
const stepContent = (() => {
|
|
913
1323
|
switch (state.step) {
|
|
1324
|
+
case 'run-picker':
|
|
1325
|
+
return (_jsx(RunPicker, { runs: initialRuns ?? [], onSelect: (selection) => {
|
|
1326
|
+
if (selection.action === 'new') {
|
|
1327
|
+
update({ step: 'welcome' });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
// Push / modify routing exits the wizard back into the CLI
|
|
1331
|
+
// surface so `replayRun` / `modifyRun` (which spin their own
|
|
1332
|
+
// UI / spawn their own Ink trees) can take over. The CLI
|
|
1333
|
+
// entry point in `command.ts` provides `onRunPicked`.
|
|
1334
|
+
onRunPicked?.(selection);
|
|
1335
|
+
}, onCancel: () => process.exit(0) }));
|
|
914
1336
|
case 'welcome':
|
|
915
1337
|
return (_jsx(WelcomeStep, { onContinue: (path) => {
|
|
916
1338
|
const projectPath = normalizePath(path);
|
|
@@ -939,7 +1361,17 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
939
1361
|
case 'path-validation':
|
|
940
1362
|
return (_jsx(PathValidationStep, { projectPath: state.projectPath, onConfirm: (path) => {
|
|
941
1363
|
void runExtract(path);
|
|
942
|
-
}, onSkipComponents: () =>
|
|
1364
|
+
}, onSkipComponents: () => {
|
|
1365
|
+
// No components — only design tokens to push. Still gather creds
|
|
1366
|
+
// first (unless --no-push, in which case there is nothing to do
|
|
1367
|
+
// and we save files instead).
|
|
1368
|
+
if (noPush) {
|
|
1369
|
+
update({ skipComponents: true, acceptedCount: 0 });
|
|
1370
|
+
void startSaveFlow();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
update({ step: 'credentials', skipComponents: true, acceptedCount: 0 });
|
|
1374
|
+
}, onChangePath: () => update({ step: 'welcome' }), onQuit: () => process.exit(0) }));
|
|
943
1375
|
case 'extracting': {
|
|
944
1376
|
const ep = state.extractProgress;
|
|
945
1377
|
let extractDetail;
|
|
@@ -954,32 +1386,59 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
954
1386
|
}
|
|
955
1387
|
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 }));
|
|
956
1388
|
}
|
|
957
|
-
case '
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
})
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1389
|
+
case 'scope-gate': {
|
|
1390
|
+
if (!state.extractSessionId) {
|
|
1391
|
+
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: "red", children: "Error: extract session ID missing \u2014 please re-run." }) }));
|
|
1392
|
+
}
|
|
1393
|
+
const sessionId = state.extractSessionId;
|
|
1394
|
+
const db = openPipelineDb();
|
|
1395
|
+
let components;
|
|
1396
|
+
try {
|
|
1397
|
+
components = loadScopeComponents(db, sessionId);
|
|
1398
|
+
}
|
|
1399
|
+
finally {
|
|
1400
|
+
db.close();
|
|
1401
|
+
}
|
|
1402
|
+
return (_jsx(ScopeGateHost, { components: components, autoAccept: autoAcceptScope, aiFilterStatus: state.aiFilterStatus, aiFilterProgress: state.aiFilterProgress, aiFilterError: state.aiFilterError, onCancelAutoFilter: cancelAutoFilter, onConfirm: (decisions) => {
|
|
1403
|
+
void runScopeGate({
|
|
1404
|
+
sessionId,
|
|
1405
|
+
decisions,
|
|
1406
|
+
cancelAutoFilter: state.aiFilterStatus === 'running' ? cancelAutoFilterAndWait : undefined,
|
|
1407
|
+
onAdvanceToGenerate: async ({ sessionId: sid, acceptedCount }) => {
|
|
1408
|
+
update({ acceptedCount, autoRejectedCount: 0 });
|
|
1409
|
+
const next = nextStepAfterScopeGate({ acceptedCount, noPush });
|
|
1410
|
+
if (next === 'generating') {
|
|
1411
|
+
if (await runAgentAuthCheck('generating')) {
|
|
1412
|
+
void runGenerate(sid, state.tokensPath, acceptedCount);
|
|
1413
|
+
}
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
// next === 'credentials' (push enabled). Kick off the
|
|
1417
|
+
// generate child in the background so the LLM call overlaps
|
|
1418
|
+
// with the operator's credential entry. We only prefetch
|
|
1419
|
+
// when we have accepted components to classify AND push is
|
|
1420
|
+
// enabled (noPush path is already excluded — the scope-gate
|
|
1421
|
+
// helper would have returned 'generating' there).
|
|
1422
|
+
if (acceptedCount > 0 && !noPush) {
|
|
1423
|
+
if (await runAgentAuthCheck('credentials')) {
|
|
1424
|
+
startGeneratePrefetch(sid, state.tokensPath);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
update({ step: 'credentials' });
|
|
1428
|
+
},
|
|
1429
|
+
onAdvanceToPushFlow: (count) => {
|
|
1430
|
+
update({ acceptedCount: count, autoRejectedCount: 0 });
|
|
1431
|
+
const next = nextStepAfterScopeGate({ acceptedCount: count, noPush });
|
|
1432
|
+
if (next === 'print-gate') {
|
|
1433
|
+
void startSaveFlow();
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
// 'credentials' — still need creds for tokens/removals push.
|
|
1437
|
+
advanceToPushFlow(count);
|
|
1438
|
+
},
|
|
1439
|
+
});
|
|
979
1440
|
}, onQuit: () => process.exit(0) }));
|
|
980
1441
|
}
|
|
981
|
-
case 'analyze-select':
|
|
982
|
-
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Launching component review... (the TUI will appear shortly)" }) }));
|
|
983
1442
|
case 'generating': {
|
|
984
1443
|
const p = state.generateProgress;
|
|
985
1444
|
const stepNum = hasTokens ? 4 : 3;
|
|
@@ -988,34 +1447,32 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
988
1447
|
: `Starting up ${state.agent}... (this can take 10–30 minutes for large libraries — grab a coffee)`;
|
|
989
1448
|
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 }));
|
|
990
1449
|
}
|
|
991
|
-
case 'review
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1450
|
+
case 'final-review': {
|
|
1451
|
+
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, onFinalize: (accepted, rejected, unresolved) => {
|
|
1452
|
+
process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected} Unresolved: ${unresolved}\n`);
|
|
1453
|
+
if (noPush) {
|
|
1454
|
+
update({ generatedAcceptedCount: accepted });
|
|
1455
|
+
void startSaveFlow();
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (noSave) {
|
|
1459
|
+
update({ generatedAcceptedCount: accepted });
|
|
1460
|
+
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
1461
|
+
void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (autoAcceptScope) {
|
|
1465
|
+
// Headless run with neither --no-save nor --no-push: pick the
|
|
1466
|
+
// default "both" path automatically to preserve scripted
|
|
1467
|
+
// (auto-accept-scope) UX from before the gate gained a third
|
|
1468
|
+
// option. Operators who want push-only must pass --no-save.
|
|
1469
|
+
update({ generatedAcceptedCount: accepted });
|
|
1470
|
+
void runSaveAndPush();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
update({ generatedAcceptedCount: accepted, step: 'push-decision-gate' });
|
|
1015
1474
|
}, onQuit: () => process.exit(0) }));
|
|
1016
1475
|
}
|
|
1017
|
-
case 'generate-edit':
|
|
1018
|
-
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Launching definition review... (the TUI will appear shortly)" }) }));
|
|
1019
1476
|
case 'push-decision-gate': {
|
|
1020
1477
|
const tokenDesc = hasTokens ? 'tokens.json' : null;
|
|
1021
1478
|
const compDesc = hasComponents ? 'components.json' : null;
|
|
@@ -1026,96 +1483,120 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
1026
1483
|
: hasTokens
|
|
1027
1484
|
? 'Design tokens ready.'
|
|
1028
1485
|
: 'Ready to continue.';
|
|
1029
|
-
return (_jsx(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1486
|
+
return (_jsx(PushDecisionGateStep, { summary: summary, context: `Save ${files || 'output files'} to disk, push to your Contentful space, or both.`, fileList: files || 'files', pushDisabled: state.credentialsSkipped, onChoice: (choice) => {
|
|
1487
|
+
const action = chooseGateAction(choice);
|
|
1488
|
+
if (action === 'save-and-push') {
|
|
1489
|
+
void runSaveAndPush();
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (action === 'push-only') {
|
|
1493
|
+
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
1494
|
+
void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
// save-only
|
|
1498
|
+
void startSaveFlow();
|
|
1033
1499
|
}, onQuit: () => process.exit(0) }));
|
|
1034
1500
|
}
|
|
1035
1501
|
case 'credentials':
|
|
1036
|
-
return (_jsx(CredentialsStep, { initialSpaceId: state.spaceId, initialEnvironmentId: state.environmentId, initialCmaToken: state.cmaToken, initialHost: state.host, error: state.credentialsError || undefined, onConfirm: (spaceId, environmentId, cmaToken, host) => {
|
|
1502
|
+
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) => {
|
|
1037
1503
|
void confirmCredentials(spaceId, environmentId, cmaToken, host);
|
|
1038
|
-
}, onContinue: advanceWithCredentials,
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1504
|
+
}, onContinue: advanceWithCredentials, onRetryPrefetch: state.generatePrefetchStatus === 'failed' && sessionRef.current.extractSessionId
|
|
1505
|
+
? () => {
|
|
1506
|
+
const sid = sessionRef.current.extractSessionId;
|
|
1507
|
+
void startGeneratePrefetch(sid, state.tokensPath);
|
|
1508
|
+
}
|
|
1509
|
+
: undefined, onSkip: () => {
|
|
1510
|
+
// Skip-credentials: mark the wizard as skipped, clear any
|
|
1511
|
+
// stale credentialsError banner, and advance through the
|
|
1512
|
+
// normal post-credentials branch. The in-flight generate
|
|
1513
|
+
// prefetch (PR #54) is intentionally NOT cancelled here —
|
|
1514
|
+
// the operator still wants to see classifications.
|
|
1515
|
+
update({ credentialsSkipped: true, credentialsError: '' });
|
|
1516
|
+
void advanceAfterCredentialsValidated();
|
|
1517
|
+
}, onQuit: () => {
|
|
1518
|
+
cancelGeneratePrefetch();
|
|
1519
|
+
process.exit(0);
|
|
1520
|
+
} }));
|
|
1521
|
+
// 'credential-test-gate' is intentionally NOT rendered as a dedicated
|
|
1522
|
+
// screen any more (the post-#54 inline credentials-validating status made
|
|
1523
|
+
// the gate redundant). The step value is kept in the union for back-compat
|
|
1524
|
+
// with existing imports/tests but is never actually set as a step —
|
|
1525
|
+
// `advanceWithCredentials` calls `validateCredentials` directly.
|
|
1526
|
+
//
|
|
1527
|
+
// 'validating-credentials' is intentionally NOT rendered as a dedicated
|
|
1528
|
+
// screen any more (see CredentialsStep `validating` prop). The step
|
|
1529
|
+
// value is kept in the union for back-compat with existing imports/tests
|
|
1530
|
+
// but should never actually be set — `validateCredentials` keeps the
|
|
1531
|
+
// step on 'credentials' and toggles `credentialsValidating` instead.
|
|
1048
1532
|
case 'previewing':
|
|
1049
1533
|
return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Computing diff", description: "Computing diff against your Contentful space..." }));
|
|
1050
1534
|
case 'preview-gate':
|
|
1051
1535
|
return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, onConfirm: (acknowledge) => {
|
|
1052
1536
|
void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, state.host, acknowledge, state.serverPreview);
|
|
1053
1537
|
}, onEdit: () => {
|
|
1054
|
-
void runEditFromPreview(
|
|
1538
|
+
void runEditFromPreview();
|
|
1055
1539
|
}, onSaveFiles: () => {
|
|
1056
|
-
void
|
|
1540
|
+
void startSaveFlow();
|
|
1057
1541
|
}, onQuit: () => process.exit(0) }));
|
|
1058
1542
|
case 'pushing':
|
|
1059
|
-
return (_jsx(
|
|
1543
|
+
return (_jsx(PushingStep, { stepNumber: totalSteps, totalSteps: totalSteps, expected: state.pushExpected, progress: state.pushProgress }));
|
|
1544
|
+
case 'path-prompt':
|
|
1545
|
+
return (_jsx(PathPrompt, { defaultPath: state.outDir, onSubmit: (submitted) => {
|
|
1546
|
+
void (async () => {
|
|
1547
|
+
await mkdir(submitted, { recursive: true });
|
|
1548
|
+
const hasConflict = await detectSaveConflict(submitted);
|
|
1549
|
+
if (hasConflict) {
|
|
1550
|
+
setState((prev) => ({ ...prev, step: 'save-conflict-gate', outDir: submitted }));
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
await proceedToWrite(submitted);
|
|
1554
|
+
})();
|
|
1555
|
+
}, onCancel: () => process.exit(0) }));
|
|
1556
|
+
case 'save-conflict-gate':
|
|
1557
|
+
return (_jsx(SaveConflictGate, { path: state.outDir, onOverwrite: () => {
|
|
1558
|
+
void proceedToWrite(state.outDir);
|
|
1559
|
+
}, onNew: () => {
|
|
1560
|
+
const subdir = buildTimestampedSubdir(state.outDir);
|
|
1561
|
+
void (async () => {
|
|
1562
|
+
await mkdir(subdir, { recursive: true });
|
|
1563
|
+
await proceedToWrite(subdir);
|
|
1564
|
+
})();
|
|
1565
|
+
}, onCancel: () => setState((prev) => ({ ...prev, step: 'path-prompt' })) }));
|
|
1060
1566
|
case 'printing':
|
|
1061
1567
|
return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Writing files", description: "Writing output files to disk..." }));
|
|
1062
|
-
case 'print-gate':
|
|
1568
|
+
case 'print-gate': {
|
|
1569
|
+
const teaser = buildRunTeaserLine(state.lastRunId);
|
|
1063
1570
|
return (_jsx(GateStep, { successMessage: "Files saved", summary: [
|
|
1064
1571
|
hasComponents && state.componentsPath ? `components.json → ${state.componentsPath}` : null,
|
|
1065
1572
|
hasTokens && state.tokensPath ? `tokens.json → ${state.tokensPath}` : null,
|
|
1066
1573
|
]
|
|
1067
1574
|
.filter(Boolean)
|
|
1068
|
-
.join('\n'), context:
|
|
1575
|
+
.join('\n'), context: teaser
|
|
1576
|
+
? `Your files are saved to disk. ${teaser}`
|
|
1577
|
+
: "Your files are saved to disk. Run `experiences import` again when you're ready to push to Contentful.", continueLabel: "Exit", showSkip: false, onContinue: () => process.exit(0), onQuit: () => process.exit(0) }));
|
|
1578
|
+
}
|
|
1069
1579
|
case 'done': {
|
|
1070
1580
|
const totalFailed = state.pushResult.componentTypes.failed + state.pushResult.designTokens.failed;
|
|
1071
|
-
|
|
1581
|
+
const teaser = buildRunTeaserLine(state.lastRunId);
|
|
1582
|
+
return (_jsx(DoneStep, { componentTypes: state.pushResult.componentTypes, designTokens: state.pushResult.designTokens, summary: state.pushResult.summary, spaceId: state.spaceId, environmentId: state.environmentId, host: state.host, ...(teaser ? { runTeaser: teaser } : {}), onExit: () => process.exit(totalFailed > 0 ? 1 : 0) }));
|
|
1072
1583
|
}
|
|
1073
1584
|
case 'preview-validation-error': {
|
|
1074
1585
|
return (_jsx(PreviewValidationErrorStep, { errors: state.previewValidationErrors, missingNames: state.previewValidationMissingNames, onEdit: () => {
|
|
1075
|
-
void runEditFromPreview(
|
|
1586
|
+
void runEditFromPreview();
|
|
1076
1587
|
}, onSkip: () => {
|
|
1077
1588
|
void runSkipValidationErrorsAndRetry(state.previewValidationErrors);
|
|
1078
1589
|
}, onQuit: () => process.exit(0) }));
|
|
1079
1590
|
}
|
|
1080
1591
|
case 'error':
|
|
1081
1592
|
return (_jsx(ErrorStep, { stepName: state.errorStep, message: state.errorMessage, onExit: () => process.exit(1), onRetryCredentials: state.errorAllowCredentialRetry ? () => update({ step: 'credentials', credentialsError: '' }) : undefined }));
|
|
1593
|
+
// 'validating-credentials' kept in the WizardStep union for back-compat
|
|
1594
|
+
// but is no longer reachable as a step (validateCredentials toggles the
|
|
1595
|
+
// `credentialsValidating` boolean while leaving step on 'credentials').
|
|
1596
|
+
// Default-return null so the switch is exhaustive.
|
|
1597
|
+
default:
|
|
1598
|
+
return null;
|
|
1082
1599
|
}
|
|
1083
1600
|
})();
|
|
1084
|
-
return (_jsxs(Box, { flexDirection: "column", width: terminalWidth, children: [_jsx(TopBar, { subcommand: "import", hints: hints }), stepContent] }));
|
|
1085
|
-
}
|
|
1086
|
-
function buildPreviewAnnotations(preview) {
|
|
1087
|
-
const annotations = {};
|
|
1088
|
-
if (!preview)
|
|
1089
|
-
return annotations;
|
|
1090
|
-
for (const item of preview.components.new) {
|
|
1091
|
-
const key = item.key ?? '';
|
|
1092
|
-
if (key)
|
|
1093
|
-
annotations[key] = 'new';
|
|
1094
|
-
}
|
|
1095
|
-
for (const item of preview.components.removed) {
|
|
1096
|
-
annotations[item.name] = 'removed';
|
|
1097
|
-
}
|
|
1098
|
-
for (const item of preview.components.changed) {
|
|
1099
|
-
if (item.changeClassification?.classification === 'breaking') {
|
|
1100
|
-
annotations[item.current.name] = 'breaking';
|
|
1101
|
-
}
|
|
1102
|
-
else {
|
|
1103
|
-
annotations[item.current.name] = 'changed';
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
return annotations;
|
|
1107
|
-
}
|
|
1108
|
-
// Interactive subprocess helper — uses spawn with inherited stdio so child isTTY is true
|
|
1109
|
-
function runCliInteractive(args) {
|
|
1110
|
-
logStep({
|
|
1111
|
-
fn: 'runCliInteractive:spawn',
|
|
1112
|
-
args,
|
|
1113
|
-
});
|
|
1114
|
-
return new Promise((res) => {
|
|
1115
|
-
const child = spawn('node', [findCliPath(), ...args], { stdio: 'inherit' });
|
|
1116
|
-
child.on('exit', (code) => {
|
|
1117
|
-
logStep({ fn: 'runCliInteractive:exit', args, exitCode: code ?? 0 });
|
|
1118
|
-
res({ exitCode: code ?? 0, stdout: '', stderr: '' });
|
|
1119
|
-
});
|
|
1120
|
-
});
|
|
1601
|
+
return (_jsxs(Box, { flexDirection: "column", width: terminalWidth, children: [_jsx(TopBar, { subcommand: "import", hints: hints }), _jsx(CustomPromptBanner, { selectPromptPath: selectPromptPath, generatePromptPath: generatePromptPath }), stepContent] }));
|
|
1121
1602
|
}
|