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