@contentful/experience-design-system-cli 2.2.1
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 +532 -0
- package/bin/cli.js +58 -0
- package/dist/package.json +56 -0
- package/dist/src/analyze/command.d.ts +3 -0
- package/dist/src/analyze/command.js +175 -0
- package/dist/src/analyze/extract/astro.d.ts +5 -0
- package/dist/src/analyze/extract/astro.js +280 -0
- package/dist/src/analyze/extract/pipeline.d.ts +6 -0
- package/dist/src/analyze/extract/pipeline.js +298 -0
- package/dist/src/analyze/extract/react.d.ts +2 -0
- package/dist/src/analyze/extract/react.js +1949 -0
- package/dist/src/analyze/extract/slot-detection.d.ts +35 -0
- package/dist/src/analyze/extract/slot-detection.js +101 -0
- package/dist/src/analyze/extract/stencil.d.ts +2 -0
- package/dist/src/analyze/extract/stencil.js +293 -0
- package/dist/src/analyze/extract/tsx-shared.d.ts +8 -0
- package/dist/src/analyze/extract/tsx-shared.js +263 -0
- package/dist/src/analyze/extract/vue-tsx.d.ts +2 -0
- package/dist/src/analyze/extract/vue-tsx.js +498 -0
- package/dist/src/analyze/extract/vue.d.ts +5 -0
- package/dist/src/analyze/extract/vue.js +647 -0
- package/dist/src/analyze/extract/web-components.d.ts +2 -0
- package/dist/src/analyze/extract/web-components.js +866 -0
- package/dist/src/analyze/pre-classify.d.ts +17 -0
- package/dist/src/analyze/pre-classify.js +144 -0
- package/dist/src/analyze/select/command.d.ts +2 -0
- package/dist/src/analyze/select/command.js +256 -0
- package/dist/src/analyze/select/index.d.ts +6 -0
- package/dist/src/analyze/select/index.js +5 -0
- package/dist/src/analyze/select/parser.d.ts +6 -0
- package/dist/src/analyze/select/parser.js +53 -0
- package/dist/src/analyze/select/persistence.d.ts +9 -0
- package/dist/src/analyze/select/persistence.js +42 -0
- package/dist/src/analyze/select/stdout.d.ts +7 -0
- package/dist/src/analyze/select/stdout.js +3 -0
- package/dist/src/analyze/select/tui/App.d.ts +8 -0
- package/dist/src/analyze/select/tui/App.js +491 -0
- package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +20 -0
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +43 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.js +531 -0
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +10 -0
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +15 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +11 -0
- package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/JsonEditor.js +154 -0
- package/dist/src/analyze/select/tui/components/JsonPanel.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/JsonPanel.js +62 -0
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.d.ts +8 -0
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +29 -0
- package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +8 -0
- package/dist/src/analyze/select/tui/components/QuitDialog.js +14 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +15 -0
- package/dist/src/analyze/select/tui/components/Sidebar.js +48 -0
- package/dist/src/analyze/select/tui/components/SourcePanel.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/SourcePanel.js +52 -0
- package/dist/src/analyze/select/tui/components/StatusBar.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/StatusBar.js +6 -0
- package/dist/src/analyze/select/tui/components/TopBar.d.ts +10 -0
- package/dist/src/analyze/select/tui/components/TopBar.js +5 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +24 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +68 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +24 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -0
- package/dist/src/analyze/select/tui/hooks/useSession.d.ts +19 -0
- package/dist/src/analyze/select/tui/hooks/useSession.js +52 -0
- package/dist/src/analyze/select/tui/hooks/useUndo.d.ts +8 -0
- package/dist/src/analyze/select/tui/hooks/useUndo.js +26 -0
- package/dist/src/analyze/select/types.d.ts +46 -0
- package/dist/src/analyze/select/types.js +20 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +208 -0
- package/dist/src/analyze/tui/AnalyzeView.d.ts +24 -0
- package/dist/src/analyze/tui/AnalyzeView.js +38 -0
- package/dist/src/apply/api-client.d.ts +35 -0
- package/dist/src/apply/api-client.js +143 -0
- package/dist/src/apply/command.d.ts +6 -0
- package/dist/src/apply/command.js +787 -0
- package/dist/src/apply/manifest.d.ts +1 -0
- package/dist/src/apply/manifest.js +1 -0
- package/dist/src/apply/tui/SelectView.d.ts +18 -0
- package/dist/src/apply/tui/SelectView.js +34 -0
- package/dist/src/apply/tui/ServerApplyView.d.ts +32 -0
- package/dist/src/apply/tui/ServerApplyView.js +42 -0
- package/dist/src/apply/tui/ServerPreviewView.d.ts +9 -0
- package/dist/src/apply/tui/ServerPreviewView.js +21 -0
- package/dist/src/credentials-store.d.ts +8 -0
- package/dist/src/credentials-store.js +30 -0
- package/dist/src/generate/agent-runner.d.ts +86 -0
- package/dist/src/generate/agent-runner.js +314 -0
- package/dist/src/generate/command.d.ts +2 -0
- package/dist/src/generate/command.js +545 -0
- package/dist/src/generate/edit/command.d.ts +2 -0
- package/dist/src/generate/edit/command.js +126 -0
- package/dist/src/generate/prompt-builder.d.ts +18 -0
- package/dist/src/generate/prompt-builder.js +202 -0
- package/dist/src/generate/tui/GenerateView.d.ts +12 -0
- package/dist/src/generate/tui/GenerateView.js +10 -0
- package/dist/src/import/command.d.ts +2 -0
- package/dist/src/import/command.js +96 -0
- package/dist/src/import/orchestrator.d.ts +37 -0
- package/dist/src/import/orchestrator.js +374 -0
- package/dist/src/import/path-utils.d.ts +15 -0
- package/dist/src/import/path-utils.js +30 -0
- package/dist/src/import/tui/WizardApp.d.ts +10 -0
- package/dist/src/import/tui/WizardApp.js +906 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +15 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +79 -0
- package/dist/src/import/tui/steps/DoneStep.d.ts +20 -0
- package/dist/src/import/tui/steps/DoneStep.js +17 -0
- package/dist/src/import/tui/steps/ErrorStep.d.ts +8 -0
- package/dist/src/import/tui/steps/ErrorStep.js +11 -0
- package/dist/src/import/tui/steps/GateStep.d.ts +14 -0
- package/dist/src/import/tui/steps/GateStep.js +20 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.js +208 -0
- package/dist/src/import/tui/steps/PathValidationStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PathValidationStep.js +151 -0
- package/dist/src/import/tui/steps/PreviewStep.d.ts +21 -0
- package/dist/src/import/tui/steps/PreviewStep.js +36 -0
- package/dist/src/import/tui/steps/RunningStep.d.ts +10 -0
- package/dist/src/import/tui/steps/RunningStep.js +20 -0
- package/dist/src/import/tui/steps/TokenInputStep.d.ts +8 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +70 -0
- package/dist/src/import/tui/steps/WelcomeStep.d.ts +7 -0
- package/dist/src/import/tui/steps/WelcomeStep.js +33 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +15 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +121 -0
- package/dist/src/import/tui/steps/preview-diff.d.ts +10 -0
- package/dist/src/import/tui/steps/preview-diff.js +132 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/output/format.d.ts +23 -0
- package/dist/src/output/format.js +110 -0
- package/dist/src/print/command.d.ts +2 -0
- package/dist/src/print/command.js +199 -0
- package/dist/src/print/validate/tui/ValidateView.d.ts +15 -0
- package/dist/src/print/validate/tui/ValidateView.js +37 -0
- package/dist/src/print/validate/validators/cdf-validator.d.ts +2 -0
- package/dist/src/print/validate/validators/cdf-validator.js +104 -0
- package/dist/src/print/validate/validators/dtcg-validator.d.ts +2 -0
- package/dist/src/print/validate/validators/dtcg-validator.js +110 -0
- package/dist/src/print/validate/validators/format-errors.d.ts +12 -0
- package/dist/src/print/validate/validators/format-errors.js +18 -0
- package/dist/src/program.d.ts +2 -0
- package/dist/src/program.js +25 -0
- package/dist/src/session/command.d.ts +2 -0
- package/dist/src/session/command.js +261 -0
- package/dist/src/session/db.d.ts +111 -0
- package/dist/src/session/db.js +1114 -0
- package/dist/src/session/migration.d.ts +4 -0
- package/dist/src/session/migration.js +117 -0
- package/dist/src/session/session-id.d.ts +1 -0
- package/dist/src/session/session-id.js +212 -0
- package/dist/src/session/stats.d.ts +27 -0
- package/dist/src/session/stats.js +89 -0
- package/dist/src/setup/command.d.ts +2 -0
- package/dist/src/setup/command.js +765 -0
- package/dist/src/types.d.ts +48 -0
- package/dist/src/types.js +1 -0
- package/package.json +55 -0
- package/skills/generate-components.md +361 -0
- package/skills/generate-tokens.md +194 -0
- package/skills/select-components.md +180 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { appendFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { access, readFile, stat } from 'node:fs/promises';
|
|
7
|
+
import { homedir, tmpdir } from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { execFile, spawn } from 'node:child_process';
|
|
10
|
+
import { TopBar } from '../../analyze/select/tui/components/TopBar.js';
|
|
11
|
+
import { WelcomeStep } from './steps/WelcomeStep.js';
|
|
12
|
+
import { PathValidationStep } from './steps/PathValidationStep.js';
|
|
13
|
+
import { RunningStep } from './steps/RunningStep.js';
|
|
14
|
+
import { GateStep } from './steps/GateStep.js';
|
|
15
|
+
import { CredentialsStep } from './steps/CredentialsStep.js';
|
|
16
|
+
import { WizardPreviewStep } from './steps/WizardPreviewStep.js';
|
|
17
|
+
import { DoneStep } from './steps/DoneStep.js';
|
|
18
|
+
import { ErrorStep } from './steps/ErrorStep.js';
|
|
19
|
+
import { TokenInputStep } from './steps/TokenInputStep.js';
|
|
20
|
+
import { GenerateReviewStep } from './steps/GenerateReviewStep.js';
|
|
21
|
+
import { ImportApiClient, ApiError } from '../../apply/api-client.js';
|
|
22
|
+
import { readTokensFromPath, hasBreakingChangesWithImpact } from '../../apply/manifest.js';
|
|
23
|
+
import { buildManifest } from '@contentful/experience-design-system-types';
|
|
24
|
+
import { openPipelineDb, loadCDFComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
|
|
25
|
+
import { checkAgentAuth } from '../../generate/agent-runner.js';
|
|
26
|
+
import { normalizePath } from '../path-utils.js';
|
|
27
|
+
function findCliPath() {
|
|
28
|
+
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'bin', 'cli.js');
|
|
29
|
+
}
|
|
30
|
+
function runCli(args) {
|
|
31
|
+
return new Promise((res) => {
|
|
32
|
+
execFile('node', [findCliPath(), ...args], (error, stdout, stderr) => {
|
|
33
|
+
res({ exitCode: error?.code ? Number(error.code) : 0, stdout, stderr });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const WIZARD_LOG = join(tmpdir(), 'exo-import-wizard.log');
|
|
38
|
+
function logStep(entry) {
|
|
39
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
|
|
40
|
+
appendFileSync(WIZARD_LOG, line);
|
|
41
|
+
}
|
|
42
|
+
export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialAgent, initialProjectPath, host, } = {}) {
|
|
43
|
+
const apiHost = host ?? process.env['EDS_HOST'];
|
|
44
|
+
const { stdout } = useStdout();
|
|
45
|
+
const terminalWidth = stdout?.columns ?? 80;
|
|
46
|
+
const logInit = useRef(false);
|
|
47
|
+
if (!logInit.current) {
|
|
48
|
+
writeFileSync(WIZARD_LOG, `--- exo import session ${new Date().toISOString()} ---\n`);
|
|
49
|
+
logInit.current = true;
|
|
50
|
+
}
|
|
51
|
+
const credentialsRef = useRef(null);
|
|
52
|
+
const sessionRef = useRef({
|
|
53
|
+
extractSessionId: null,
|
|
54
|
+
tokensPath: '',
|
|
55
|
+
});
|
|
56
|
+
const [state, setState] = useState({
|
|
57
|
+
step: initialProjectPath ? 'token-input' : 'welcome',
|
|
58
|
+
agent: initialAgent ?? 'claude',
|
|
59
|
+
projectPath: initialProjectPath ?? '',
|
|
60
|
+
outDir: initialProjectPath ? join(resolve(initialProjectPath), '.contentful') : '',
|
|
61
|
+
rawTokensPath: '',
|
|
62
|
+
tokensPath: '',
|
|
63
|
+
tokenSourceChanged: null,
|
|
64
|
+
skipComponents: false,
|
|
65
|
+
tokenSessionId: null,
|
|
66
|
+
extractSessionId: null,
|
|
67
|
+
generateSessionId: null,
|
|
68
|
+
extractedCount: 0,
|
|
69
|
+
acceptedCount: 0,
|
|
70
|
+
generatedCount: 0,
|
|
71
|
+
generatedAcceptedCount: 0,
|
|
72
|
+
generateProgress: null,
|
|
73
|
+
extractProgress: null,
|
|
74
|
+
componentsPath: '',
|
|
75
|
+
spaceId: initialSpaceId,
|
|
76
|
+
environmentId: initialEnvironmentId,
|
|
77
|
+
cmaToken: initialCmaToken,
|
|
78
|
+
credentialsError: '',
|
|
79
|
+
serverPreview: null,
|
|
80
|
+
manifest: null,
|
|
81
|
+
pushProgress: null,
|
|
82
|
+
pushResult: {
|
|
83
|
+
componentTypes: { created: 0, updated: 0, failed: 0 },
|
|
84
|
+
designTokens: { created: 0, updated: 0, failed: 0 },
|
|
85
|
+
},
|
|
86
|
+
errorStep: '',
|
|
87
|
+
errorMessage: '',
|
|
88
|
+
});
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
sessionRef.current = {
|
|
91
|
+
extractSessionId: state.extractSessionId,
|
|
92
|
+
tokensPath: state.tokensPath,
|
|
93
|
+
};
|
|
94
|
+
}, [state.extractSessionId, state.tokensPath]);
|
|
95
|
+
const update = (partial) => {
|
|
96
|
+
const sanitized = { ...partial };
|
|
97
|
+
if (sanitized.serverPreview) {
|
|
98
|
+
const p = sanitized.serverPreview;
|
|
99
|
+
sanitized.serverPreview = {
|
|
100
|
+
components: {
|
|
101
|
+
new: p.components.new.length,
|
|
102
|
+
newNames: p.components.new.map((c) => c.name ?? JSON.stringify(c).slice(0, 80)),
|
|
103
|
+
changed: p.components.changed.length,
|
|
104
|
+
removed: p.components.removed.length,
|
|
105
|
+
removedNames: p.components.removed.map((c) => c.name),
|
|
106
|
+
unchanged: p.components.unchanged.length,
|
|
107
|
+
},
|
|
108
|
+
tokens: {
|
|
109
|
+
new: p.tokens.new.length,
|
|
110
|
+
changed: p.tokens.changed.length,
|
|
111
|
+
removed: p.tokens.removed.length,
|
|
112
|
+
unchanged: p.tokens.unchanged.length,
|
|
113
|
+
},
|
|
114
|
+
changedComponentDetails: p.components.changed.map((c) => ({
|
|
115
|
+
name: c.current.name,
|
|
116
|
+
hasPendingDraftChanges: c.hasPendingDraftChanges,
|
|
117
|
+
classification: c.changeClassification?.classification,
|
|
118
|
+
breakingChanges: c.changeClassification?.breakingChanges,
|
|
119
|
+
impact: c.impact,
|
|
120
|
+
})),
|
|
121
|
+
changedTokenDetails: p.tokens.changed.slice(0, 5).map((t) => ({
|
|
122
|
+
name: t.current.name,
|
|
123
|
+
hasPendingDraftChanges: t.hasPendingDraftChanges,
|
|
124
|
+
classification: t.changeClassification?.classification,
|
|
125
|
+
breakingChanges: t.changeClassification?.breakingChanges,
|
|
126
|
+
impact: t.impact,
|
|
127
|
+
})),
|
|
128
|
+
tokenDiffs: p.tokens.changed.slice(0, 3).map((t) => ({
|
|
129
|
+
current: t.current,
|
|
130
|
+
proposed: t.proposed,
|
|
131
|
+
})),
|
|
132
|
+
hasBreakingWithImpact: hasBreakingChangesWithImpact(p),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (sanitized.manifest)
|
|
136
|
+
sanitized.manifest = '[manifest]';
|
|
137
|
+
if (sanitized.cmaToken)
|
|
138
|
+
sanitized.cmaToken = '[redacted]';
|
|
139
|
+
logStep({ update: sanitized });
|
|
140
|
+
setState((prev) => ({ ...prev, ...partial }));
|
|
141
|
+
};
|
|
142
|
+
// ── Agent auth pre-flight ───────────────────────────────────────────────────
|
|
143
|
+
const runAgentAuthCheck = async (nextStep) => {
|
|
144
|
+
update({ step: 'checking-claude-auth' });
|
|
145
|
+
const status = await checkAgentAuth(state.agent);
|
|
146
|
+
if (status === 'not-found') {
|
|
147
|
+
update({
|
|
148
|
+
step: 'error',
|
|
149
|
+
errorStep: `${state.agent} auth check`,
|
|
150
|
+
errorMessage: `The \`${state.agent}\` CLI was not found on your PATH.\n\nInstall it, then re-run \`exo import\`.`,
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (status === 'unauthenticated') {
|
|
155
|
+
update({
|
|
156
|
+
step: 'error',
|
|
157
|
+
errorStep: `${state.agent} auth check`,
|
|
158
|
+
errorMessage: `${state.agent} is not authenticated.\n\n` +
|
|
159
|
+
`Run \`${state.agent}\` in your terminal to log in, then re-run \`exo import\`.\n\n` +
|
|
160
|
+
'If you are using AWS Bedrock, run:\n' +
|
|
161
|
+
' aws sso login --profile <your-profile>',
|
|
162
|
+
});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
update({ step: nextStep });
|
|
166
|
+
return true;
|
|
167
|
+
};
|
|
168
|
+
// ── Step runners ──────────────────────────────────────────────────────────
|
|
169
|
+
const runGenerateTokens = async (rawTokensPath, outDir) => {
|
|
170
|
+
const result = await new Promise((res) => {
|
|
171
|
+
const child = spawn('node', [
|
|
172
|
+
findCliPath(),
|
|
173
|
+
'generate',
|
|
174
|
+
'tokens',
|
|
175
|
+
'--agent',
|
|
176
|
+
state.agent,
|
|
177
|
+
'--raw-tokens',
|
|
178
|
+
rawTokensPath,
|
|
179
|
+
]);
|
|
180
|
+
let stdout = '';
|
|
181
|
+
let stderr = '';
|
|
182
|
+
child.stdout.on('data', (d) => {
|
|
183
|
+
stdout += String(d);
|
|
184
|
+
});
|
|
185
|
+
child.stderr.on('data', (d) => {
|
|
186
|
+
stderr += String(d);
|
|
187
|
+
});
|
|
188
|
+
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
189
|
+
});
|
|
190
|
+
if (result.exitCode !== 0) {
|
|
191
|
+
update({ step: 'error', errorStep: 'generate tokens', errorMessage: result.stderr.trim() || 'Unknown error' });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const sessionMatch = /^session:\s*(.+)$/m.exec(result.stdout);
|
|
195
|
+
const tokenSessionId = sessionMatch ? sessionMatch[1].trim() : null;
|
|
196
|
+
const tokensPath = join(outDir, 'tokens.json');
|
|
197
|
+
const printArgs = ['print', 'tokens', '--out', tokensPath];
|
|
198
|
+
if (tokenSessionId)
|
|
199
|
+
printArgs.push('--session', tokenSessionId);
|
|
200
|
+
const r = await runCli(printArgs);
|
|
201
|
+
if (r.exitCode !== 0) {
|
|
202
|
+
update({ step: 'error', errorStep: 'print tokens', errorMessage: r.stderr.trim() || 'Unknown error' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
update({ step: 'path-validation', tokensPath, tokenSessionId });
|
|
206
|
+
};
|
|
207
|
+
const runExtract = async (projectPath) => {
|
|
208
|
+
const outDir = join(resolve(projectPath), '.contentful');
|
|
209
|
+
update({ step: 'extracting', outDir, extractProgress: null });
|
|
210
|
+
const r = await new Promise((res) => {
|
|
211
|
+
const child = spawn('node', [findCliPath(), 'analyze', 'extract', '--project', projectPath]);
|
|
212
|
+
let stdout = '';
|
|
213
|
+
let stderr = '';
|
|
214
|
+
child.stdout.on('data', (d) => {
|
|
215
|
+
stdout += String(d);
|
|
216
|
+
});
|
|
217
|
+
child.stderr.on('data', (d) => {
|
|
218
|
+
const chunk = String(d);
|
|
219
|
+
stderr += chunk;
|
|
220
|
+
for (const line of chunk.split('\n')) {
|
|
221
|
+
const scanMatch = /^progress=scan:(\d+)$/.exec(line.trim());
|
|
222
|
+
if (scanMatch) {
|
|
223
|
+
const scanned = Number(scanMatch[1]);
|
|
224
|
+
setState((prev) => ({
|
|
225
|
+
...prev,
|
|
226
|
+
extractProgress: {
|
|
227
|
+
scanned,
|
|
228
|
+
filesProcessed: prev.extractProgress?.filesProcessed ?? 0,
|
|
229
|
+
totalFiles: prev.extractProgress?.totalFiles ?? 0,
|
|
230
|
+
componentsFound: prev.extractProgress?.componentsFound ?? 0,
|
|
231
|
+
},
|
|
232
|
+
}));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const extractMatch = /^progress=extract:(\d+)\/(\d+):(\d+)$/.exec(line.trim());
|
|
236
|
+
if (extractMatch) {
|
|
237
|
+
const filesProcessed = Number(extractMatch[1]);
|
|
238
|
+
const totalFiles = Number(extractMatch[2]);
|
|
239
|
+
const componentsFound = Number(extractMatch[3]);
|
|
240
|
+
setState((prev) => ({
|
|
241
|
+
...prev,
|
|
242
|
+
extractProgress: {
|
|
243
|
+
scanned: prev.extractProgress?.scanned ?? 0,
|
|
244
|
+
filesProcessed,
|
|
245
|
+
totalFiles,
|
|
246
|
+
componentsFound,
|
|
247
|
+
},
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
253
|
+
});
|
|
254
|
+
if (r.exitCode !== 0) {
|
|
255
|
+
update({ step: 'error', errorStep: 'analyze extract', errorMessage: r.stderr.trim() || 'Unknown error' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const sessionMatch = /^session=(.+)$/m.exec(r.stdout);
|
|
259
|
+
const extractSessionId = sessionMatch ? sessionMatch[1].trim() : null;
|
|
260
|
+
const countMatch = /Extracted (\d+) components?/.exec(r.stderr);
|
|
261
|
+
const extractedCount = countMatch ? Number(countMatch[1]) : 0;
|
|
262
|
+
if (extractedCount === 0) {
|
|
263
|
+
update({
|
|
264
|
+
step: 'error',
|
|
265
|
+
errorStep: 'analyze extract',
|
|
266
|
+
errorMessage: `No components found in ${projectPath}.\n\nMake sure this path contains TypeScript/React/Vue component files (.tsx, .ts, .vue, etc.).`,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
update({ step: 'review-extraction-gate', extractSessionId, extractedCount });
|
|
271
|
+
};
|
|
272
|
+
const runAnalyzeSelect = async (sessionId, extractedCount, tokensPath, acceptAll) => {
|
|
273
|
+
logStep({ fn: 'runAnalyzeSelect:enter', sessionId, extractedCount, acceptAll });
|
|
274
|
+
let acceptedCount;
|
|
275
|
+
if (acceptAll) {
|
|
276
|
+
logStep({ fn: 'runAnalyzeSelect:acceptAll-skip-spawn' });
|
|
277
|
+
acceptedCount = extractedCount;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
if (state.serverPreview && !process.env['EDS_PREVIEW_ANNOTATIONS']) {
|
|
281
|
+
process.env['EDS_PREVIEW_ANNOTATIONS'] = JSON.stringify(buildPreviewAnnotations(state.serverPreview));
|
|
282
|
+
}
|
|
283
|
+
update({ step: 'analyze-select' });
|
|
284
|
+
const r = await runCliInteractive(['analyze', 'select', '--session', sessionId]);
|
|
285
|
+
logStep({ fn: 'runAnalyzeSelect:post-spawn', exitCode: r.exitCode });
|
|
286
|
+
clearPreviewEnvVars();
|
|
287
|
+
if (r.exitCode !== 0) {
|
|
288
|
+
update({ step: 'error', errorStep: 'analyze select', errorMessage: r.stderr.trim() || 'Unknown error' });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Read accepted count from the review state file (since TUI subprocess inherits stdio)
|
|
292
|
+
const artifactsRoot = process.env['EDS_REVIEW_ARTIFACTS_DIR']
|
|
293
|
+
? resolve(process.env['EDS_REVIEW_ARTIFACTS_DIR'])
|
|
294
|
+
: resolve(homedir(), '.contentful', 'experience-design-system-cli', 'reviews');
|
|
295
|
+
const reviewStatePath = resolve(artifactsRoot, sessionId, 'current-review-state.json');
|
|
296
|
+
try {
|
|
297
|
+
const reviewState = JSON.parse(await readFile(reviewStatePath, 'utf8'));
|
|
298
|
+
acceptedCount = reviewState.components.filter((c) => c.status === 'accepted').length;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
acceptedCount = extractedCount;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
update({ acceptedCount });
|
|
305
|
+
if (acceptedCount > 0) {
|
|
306
|
+
if (await runAgentAuthCheck('generating')) {
|
|
307
|
+
void runGenerate(sessionId, tokensPath, acceptedCount);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
advanceToPushFlow(0);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const runGenerate = async (extractSessionId, tokensPath, acceptedCount) => {
|
|
315
|
+
const result = await new Promise((res) => {
|
|
316
|
+
const args = [findCliPath(), 'generate', 'components', '--agent', state.agent, '--session', extractSessionId];
|
|
317
|
+
if (tokensPath)
|
|
318
|
+
args.push('--tokens', tokensPath);
|
|
319
|
+
const child = spawn('node', args);
|
|
320
|
+
let stdout = '';
|
|
321
|
+
let stderr = '';
|
|
322
|
+
child.stdout.on('data', (d) => {
|
|
323
|
+
stdout += String(d);
|
|
324
|
+
});
|
|
325
|
+
child.stderr.on('data', (d) => {
|
|
326
|
+
const chunk = String(d);
|
|
327
|
+
stderr += chunk;
|
|
328
|
+
for (const line of chunk.split('\n')) {
|
|
329
|
+
const m = /\[(\d+)\/(\d+)\]\s+(.+)/.exec(line);
|
|
330
|
+
if (m)
|
|
331
|
+
update({ generateProgress: { done: Number(m[1]), total: Number(m[2]), current: m[3].trim() } });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
335
|
+
});
|
|
336
|
+
if (result.exitCode !== 0) {
|
|
337
|
+
update({
|
|
338
|
+
step: 'error',
|
|
339
|
+
errorStep: 'generate components',
|
|
340
|
+
errorMessage: result.stderr.trim() || 'Unknown error',
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const sessionMatch = /^session=(.+)$/m.exec(result.stdout);
|
|
345
|
+
const generateSessionId = sessionMatch ? sessionMatch[1].trim() : null;
|
|
346
|
+
const countMatch = /(\d+) components?/.exec(result.stderr);
|
|
347
|
+
const generatedCount = countMatch ? Number(countMatch[1]) : acceptedCount;
|
|
348
|
+
update({ step: 'review-generated-gate', generateSessionId, generatedCount, generateProgress: null });
|
|
349
|
+
};
|
|
350
|
+
const advanceToPushFlow = (generatedAcceptedCount) => {
|
|
351
|
+
update({ generatedAcceptedCount, step: 'credentials' });
|
|
352
|
+
};
|
|
353
|
+
const runGenerateEdit = async (sessionId, generatedCount, acceptAll, returnToPreview = false) => {
|
|
354
|
+
let generatedAcceptedCount;
|
|
355
|
+
if (acceptAll) {
|
|
356
|
+
generatedAcceptedCount = generatedCount;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
update({ step: 'generate-edit' });
|
|
360
|
+
if (sessionId) {
|
|
361
|
+
const r = await runCliInteractive(['generate', 'components', 'edit', '--session', sessionId]);
|
|
362
|
+
if (r.exitCode !== 0) {
|
|
363
|
+
update({ step: 'error', errorStep: 'generate edit', errorMessage: r.stderr.trim() || 'Unknown error' });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const acceptedMatch = /Accepted: (\d+)/.exec(r.stderr);
|
|
367
|
+
generatedAcceptedCount = acceptedMatch ? Number(acceptedMatch[1]) : generatedCount;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
generatedAcceptedCount = generatedCount;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (returnToPreview) {
|
|
374
|
+
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
375
|
+
void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
advanceToPushFlow(generatedAcceptedCount);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const setPreviewEnvVars = () => {
|
|
382
|
+
const creds = credentialsRef.current;
|
|
383
|
+
const cmaToken = creds?.cmaToken || state.cmaToken;
|
|
384
|
+
const spaceId = creds?.spaceId || state.spaceId;
|
|
385
|
+
const environmentId = creds?.environmentId || state.environmentId;
|
|
386
|
+
if (cmaToken)
|
|
387
|
+
process.env['EDS_CMA_TOKEN'] = cmaToken;
|
|
388
|
+
if (spaceId)
|
|
389
|
+
process.env['EDS_SPACE_ID'] = spaceId;
|
|
390
|
+
if (environmentId)
|
|
391
|
+
process.env['EDS_ENVIRONMENT_ID'] = environmentId;
|
|
392
|
+
process.env['EDS_TOKENS_PATH'] = state.tokensPath || '';
|
|
393
|
+
};
|
|
394
|
+
const clearPreviewEnvVars = () => {
|
|
395
|
+
delete process.env['EDS_PREVIEW_ANNOTATIONS'];
|
|
396
|
+
delete process.env['EDS_PREVIEW_COUNTS'];
|
|
397
|
+
delete process.env['EDS_CMA_TOKEN'];
|
|
398
|
+
delete process.env['EDS_SPACE_ID'];
|
|
399
|
+
delete process.env['EDS_ENVIRONMENT_ID'];
|
|
400
|
+
delete process.env['EDS_TOKENS_PATH'];
|
|
401
|
+
};
|
|
402
|
+
const runEditFromPreview = async (preview) => {
|
|
403
|
+
const sessionId = state.extractSessionId;
|
|
404
|
+
if (!sessionId) {
|
|
405
|
+
update({ step: 'error', errorStep: 'edit definitions', errorMessage: 'No session available for editing' });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
process.env['EDS_PREVIEW_ANNOTATIONS'] = JSON.stringify(buildPreviewAnnotations(preview));
|
|
409
|
+
if (preview) {
|
|
410
|
+
process.env['EDS_PREVIEW_COUNTS'] = JSON.stringify({
|
|
411
|
+
compNew: preview.components.new.length,
|
|
412
|
+
compChanged: preview.components.changed.length,
|
|
413
|
+
compRemoved: preview.components.removed.length,
|
|
414
|
+
compUnchanged: preview.components.unchanged.length,
|
|
415
|
+
tokNew: preview.tokens.new.length,
|
|
416
|
+
tokChanged: preview.tokens.changed.length,
|
|
417
|
+
tokRemoved: preview.tokens.removed.length,
|
|
418
|
+
tokUnchanged: preview.tokens.unchanged.length,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
setPreviewEnvVars();
|
|
422
|
+
update({ step: 'analyze-select' });
|
|
423
|
+
const r = await runCliInteractive(['analyze', 'select', '--session', sessionId]);
|
|
424
|
+
clearPreviewEnvVars();
|
|
425
|
+
if (r.exitCode !== 0) {
|
|
426
|
+
update({ step: 'error', errorStep: 'edit definitions', errorMessage: 'Editor exited with an error' });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Re-preview with updated definitions
|
|
430
|
+
const { extractSessionId: sid, tokensPath: tp } = sessionRef.current;
|
|
431
|
+
void runPreview(sid, tp, state.spaceId, state.environmentId, state.cmaToken);
|
|
432
|
+
};
|
|
433
|
+
const confirmCredentials = (spaceId, environmentId, cmaToken) => {
|
|
434
|
+
credentialsRef.current = { spaceId, environmentId, cmaToken };
|
|
435
|
+
update({ spaceId, environmentId, cmaToken, step: 'credential-test-gate' });
|
|
436
|
+
};
|
|
437
|
+
const validateCredentials = async (spaceId, environmentId, cmaToken) => {
|
|
438
|
+
update({ step: 'validating-credentials' });
|
|
439
|
+
try {
|
|
440
|
+
const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
|
|
441
|
+
await client.resolveOrganizationId();
|
|
442
|
+
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
443
|
+
void runPreview(extractSessionId, tokensPath, spaceId, environmentId, cmaToken);
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
|
|
447
|
+
update({ step: 'credentials', credentialsError: e.message });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const msg = e instanceof Error ? e.message : 'Credential check failed';
|
|
451
|
+
update({ step: 'credentials', credentialsError: msg });
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const runPreview = async (extractSessionId, tokensPath, spaceId, environmentId, cmaToken) => {
|
|
455
|
+
update({ step: 'previewing' });
|
|
456
|
+
try {
|
|
457
|
+
const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
|
|
458
|
+
const orgId = await client.resolveOrganizationId();
|
|
459
|
+
client.setOrganizationId(orgId);
|
|
460
|
+
let components = [];
|
|
461
|
+
if (extractSessionId) {
|
|
462
|
+
const db = openPipelineDb();
|
|
463
|
+
try {
|
|
464
|
+
backfillUnclassifiedProps(db, extractSessionId);
|
|
465
|
+
components = loadCDFComponents(db, extractSessionId);
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
db.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
let tokens = [];
|
|
472
|
+
if (tokensPath) {
|
|
473
|
+
tokens = await readTokensFromPath('tokens', tokensPath);
|
|
474
|
+
}
|
|
475
|
+
let manifest = buildManifest(components, tokens);
|
|
476
|
+
let preview = await client.previewImport(manifest);
|
|
477
|
+
// Second pass: seed CDF from false removals + preserve defaults from changed items
|
|
478
|
+
if (extractSessionId) {
|
|
479
|
+
let needsRepreview = false;
|
|
480
|
+
const db = openPipelineDb();
|
|
481
|
+
try {
|
|
482
|
+
// Seed CDF for components server thinks are removed but exist locally
|
|
483
|
+
if (preview.components.removed.length > 0) {
|
|
484
|
+
const localNames = new Set(db.prepare(`SELECT name FROM raw_components WHERE session_id = ?`).all(extractSessionId).map((r) => r.name));
|
|
485
|
+
const falseRemovals = preview.components.removed.filter((r) => localNames.has(r.name));
|
|
486
|
+
if (falseRemovals.length > 0) {
|
|
487
|
+
const seeded = seedCDFFromPreviewResponse(db, extractSessionId, falseRemovals);
|
|
488
|
+
if (seeded > 0)
|
|
489
|
+
needsRepreview = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Preserve server-side defaults so we don't accidentally propose removing them
|
|
493
|
+
if (preview.components.changed.length > 0) {
|
|
494
|
+
const seededDefaults = seedDefaultsFromChangedItems(db, extractSessionId, preview.components.changed);
|
|
495
|
+
if (seededDefaults > 0)
|
|
496
|
+
needsRepreview = true;
|
|
497
|
+
}
|
|
498
|
+
if (needsRepreview) {
|
|
499
|
+
components = loadCDFComponents(db, extractSessionId);
|
|
500
|
+
manifest = buildManifest(components, tokens);
|
|
501
|
+
preview = await client.previewImport(manifest);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
db.close();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
update({ step: 'preview-gate', serverPreview: preview, manifest });
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
if (e instanceof ApiError) {
|
|
512
|
+
if (e.status === 401 || e.status === 403) {
|
|
513
|
+
let bodyMsg = '';
|
|
514
|
+
try {
|
|
515
|
+
bodyMsg = JSON.parse(e.body)['message'];
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
/* non-JSON */
|
|
519
|
+
}
|
|
520
|
+
// Space-level config errors (e.g. "Design system public CMA is disabled") cannot be
|
|
521
|
+
// fixed by re-entering credentials — send to error screen.
|
|
522
|
+
if (bodyMsg && /disabled/i.test(bodyMsg)) {
|
|
523
|
+
update({ step: 'error', errorStep: 'apply preview', errorMessage: `Preview failed: ${bodyMsg}` });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
update({ step: 'credentials', credentialsError: e.message });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (e.status === 404) {
|
|
530
|
+
// 404 can come from two places:
|
|
531
|
+
// - resolveOrganizationId: space not found on this host (common when using --host
|
|
532
|
+
// with a staging/preview host but prod space IDs)
|
|
533
|
+
// - previewImport: design systems endpoint doesn't exist for this space/environment
|
|
534
|
+
// Neither is a credentials problem — show a clear error instead of looping.
|
|
535
|
+
update({
|
|
536
|
+
step: 'error',
|
|
537
|
+
errorStep: 'apply preview',
|
|
538
|
+
errorMessage: `Not found (404). Check that the space ID, environment ID, and host are correct.\n\n` +
|
|
539
|
+
` Space: ${spaceId}\n` +
|
|
540
|
+
` Environment: ${environmentId}\n` +
|
|
541
|
+
(apiHost ? ` Host: ${apiHost}\n` : '') +
|
|
542
|
+
`\nIf using a custom --host, make sure the space exists on that host.`,
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
update({ step: 'error', errorStep: 'apply preview', errorMessage: e.message });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const msg = e instanceof Error ? e.message : 'Preview failed';
|
|
550
|
+
update({ step: 'error', errorStep: 'apply preview', errorMessage: msg });
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
const runPush = async (manifest, spaceId, environmentId, cmaToken, acknowledgeBreakingChanges, preview) => {
|
|
554
|
+
if (preview) {
|
|
555
|
+
const hasComponentChanges = preview.components.new.length > 0 ||
|
|
556
|
+
preview.components.changed.length > 0 ||
|
|
557
|
+
preview.components.removed.length > 0;
|
|
558
|
+
const hasTokenChanges = preview.tokens.new.length > 0 || preview.tokens.changed.length > 0 || preview.tokens.removed.length > 0;
|
|
559
|
+
if (!hasComponentChanges && !hasTokenChanges) {
|
|
560
|
+
update({
|
|
561
|
+
step: 'done',
|
|
562
|
+
pushResult: {
|
|
563
|
+
componentTypes: { created: 0, updated: 0, failed: 0 },
|
|
564
|
+
designTokens: { created: 0, updated: 0, failed: 0 },
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
update({ step: 'pushing' });
|
|
571
|
+
try {
|
|
572
|
+
const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
|
|
573
|
+
const orgId = await client.resolveOrganizationId();
|
|
574
|
+
client.setOrganizationId(orgId);
|
|
575
|
+
let operation = await client.applyImport(manifest, acknowledgeBreakingChanges);
|
|
576
|
+
try {
|
|
577
|
+
logStep({
|
|
578
|
+
applyResponse: { status: operation?.sys?.status, id: operation?.sys?.id, keys: Object.keys(operation ?? {}) },
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
process.stderr.write(`[eds] log write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
583
|
+
}
|
|
584
|
+
update({ pushProgress: `Queued (operation ${operation.sys.id.slice(0, 8)}...)` });
|
|
585
|
+
let pollCount = 0;
|
|
586
|
+
operation = await client.pollOperation(operation.sys.id, {
|
|
587
|
+
onProgress: (op) => {
|
|
588
|
+
pollCount++;
|
|
589
|
+
const s = op.summary;
|
|
590
|
+
if (s) {
|
|
591
|
+
const done = s.total - s.pending;
|
|
592
|
+
update({ pushProgress: `${done}/${s.total} entities processed` });
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
logStep({ pollTick: { attempt: pollCount, status: op.sys.status, summary: op.summary } });
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
process.stderr.write(`[eds] log write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
try {
|
|
603
|
+
logStep({
|
|
604
|
+
pollResult: {
|
|
605
|
+
status: operation?.sys?.status,
|
|
606
|
+
keys: Object.keys(operation ?? {}),
|
|
607
|
+
itemCount: operation.items?.length,
|
|
608
|
+
summary: operation.summary,
|
|
609
|
+
sampleItems: operation.items?.slice(0, 3),
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
catch (logErr) {
|
|
614
|
+
logStep({ pollLogError: String(logErr) });
|
|
615
|
+
}
|
|
616
|
+
const items = operation.items ?? [];
|
|
617
|
+
let pushResult;
|
|
618
|
+
if (items.length > 0) {
|
|
619
|
+
pushResult = {
|
|
620
|
+
componentTypes: {
|
|
621
|
+
created: items.filter((i) => i.entityType === 'ComponentType' && i.action === 'create' && i.status === 'succeeded').length,
|
|
622
|
+
updated: items.filter((i) => i.entityType === 'ComponentType' && i.action === 'update' && i.status === 'succeeded').length,
|
|
623
|
+
failed: items.filter((i) => i.entityType === 'ComponentType' && i.status === 'failed').length,
|
|
624
|
+
},
|
|
625
|
+
designTokens: {
|
|
626
|
+
created: items.filter((i) => i.entityType === 'DesignToken' && i.action === 'create' && i.status === 'succeeded').length,
|
|
627
|
+
updated: items.filter((i) => i.entityType === 'DesignToken' && i.action === 'update' && i.status === 'succeeded').length,
|
|
628
|
+
failed: items.filter((i) => i.entityType === 'DesignToken' && i.status === 'failed').length,
|
|
629
|
+
},
|
|
630
|
+
summary: operation.summary,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// API didn't return items — fall back to summary + preview counts
|
|
635
|
+
pushResult = {
|
|
636
|
+
componentTypes: {
|
|
637
|
+
created: preview?.components.new.length ?? 0,
|
|
638
|
+
updated: preview?.components.changed.length ?? 0,
|
|
639
|
+
failed: 0,
|
|
640
|
+
},
|
|
641
|
+
designTokens: {
|
|
642
|
+
created: preview?.tokens.new.length ?? 0,
|
|
643
|
+
updated: preview?.tokens.changed.length ?? 0,
|
|
644
|
+
failed: 0,
|
|
645
|
+
},
|
|
646
|
+
summary: operation.summary,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
update({ step: 'done', pushResult });
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : 'Push failed';
|
|
653
|
+
update({ step: 'error', errorStep: 'apply push', errorMessage: msg });
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
const runPrintFiles = async (extractSessionId, outDir) => {
|
|
657
|
+
update({ step: 'printing' });
|
|
658
|
+
const componentsPath = join(outDir, 'components.json');
|
|
659
|
+
const printArgs = ['print', 'components', '--out', componentsPath];
|
|
660
|
+
if (extractSessionId)
|
|
661
|
+
printArgs.push('--session', extractSessionId);
|
|
662
|
+
const r = await runCli(printArgs);
|
|
663
|
+
if (r.exitCode !== 0) {
|
|
664
|
+
update({ step: 'error', errorStep: 'print components', errorMessage: r.stderr.trim() || 'Unknown error' });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
// tokensPath is already on disk from generate-tokens step; just record it
|
|
668
|
+
update({ step: 'print-gate', componentsPath });
|
|
669
|
+
};
|
|
670
|
+
// ── Effect: kick off automatic steps ─────────────────────────────────────
|
|
671
|
+
const tokenReuseChecked = useRef(false);
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
if (state.step === 'generating-tokens') {
|
|
674
|
+
if (tokenReuseChecked.current)
|
|
675
|
+
return; // already checked or user chose regenerate
|
|
676
|
+
tokenReuseChecked.current = true;
|
|
677
|
+
const existingTokensPath = join(state.outDir, 'tokens.json');
|
|
678
|
+
(async () => {
|
|
679
|
+
try {
|
|
680
|
+
await access(existingTokensPath);
|
|
681
|
+
const [tokensStat, sourceStat] = await Promise.all([
|
|
682
|
+
stat(existingTokensPath),
|
|
683
|
+
stat(state.rawTokensPath).catch(() => null),
|
|
684
|
+
]);
|
|
685
|
+
const sourceChanged = sourceStat ? sourceStat.mtimeMs > tokensStat.mtimeMs : false;
|
|
686
|
+
update({ step: 'token-reuse-gate', tokensPath: existingTokensPath, tokenSourceChanged: sourceChanged });
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
// No existing tokens — need LLM to generate
|
|
690
|
+
if (await runAgentAuthCheck('generating-tokens')) {
|
|
691
|
+
void runGenerateTokens(state.rawTokensPath, state.outDir);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
})();
|
|
695
|
+
}
|
|
696
|
+
}, [state.step]); // intentional: only re-run when step changes
|
|
697
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
698
|
+
const noQuitSteps = [
|
|
699
|
+
'checking-claude-auth',
|
|
700
|
+
'validating-credentials',
|
|
701
|
+
'generating-tokens',
|
|
702
|
+
'extracting',
|
|
703
|
+
'generating',
|
|
704
|
+
'printing',
|
|
705
|
+
'previewing',
|
|
706
|
+
'pushing',
|
|
707
|
+
];
|
|
708
|
+
const hints = noQuitSteps.includes(state.step) ? [] : [{ key: 'q', label: 'quit' }];
|
|
709
|
+
// step count: tokens step adds 1, components steps add 2 (extract + generate)
|
|
710
|
+
const hasTokens = !!state.tokensPath;
|
|
711
|
+
const hasComponents = !state.skipComponents;
|
|
712
|
+
const totalSteps = 3 + (hasTokens ? 1 : 0) + (hasComponents ? 2 : 0);
|
|
713
|
+
const stepContent = (() => {
|
|
714
|
+
switch (state.step) {
|
|
715
|
+
case 'welcome':
|
|
716
|
+
return (_jsx(WelcomeStep, { onContinue: (path) => {
|
|
717
|
+
const projectPath = normalizePath(path);
|
|
718
|
+
const outDir = join(projectPath, '.contentful');
|
|
719
|
+
update({ step: 'token-input', projectPath, outDir });
|
|
720
|
+
}, onQuit: () => process.exit(0) }));
|
|
721
|
+
case 'token-input':
|
|
722
|
+
return (_jsx(TokenInputStep, { onConfirm: (rawTokensPath) => {
|
|
723
|
+
update({ rawTokensPath, step: 'generating-tokens' });
|
|
724
|
+
}, onSkip: () => update({ step: 'path-validation' }), onQuit: () => process.exit(0) }));
|
|
725
|
+
case 'token-reuse-gate':
|
|
726
|
+
return (_jsx(GateStep, { successMessage: "Existing tokens.json found", summary: state.tokenSourceChanged
|
|
727
|
+
? `Source file has been modified since tokens were last generated.\n ${state.tokensPath}`
|
|
728
|
+
: `Source file has not changed since tokens were last generated.\n ${state.tokensPath}`, context: state.tokenSourceChanged
|
|
729
|
+
? 'The source tokens file changed — regenerating is recommended.'
|
|
730
|
+
: 'No changes detected — reusing the existing tokens avoids nondeterministic AI drift.', continueLabel: "Reuse existing tokens", skipLabel: "Regenerate tokens", showSkip: true, onContinue: () => update({ step: 'path-validation', tokenSessionId: null }), onSkip: async () => {
|
|
731
|
+
update({ tokenSourceChanged: null });
|
|
732
|
+
if (await runAgentAuthCheck('generating-tokens')) {
|
|
733
|
+
void runGenerateTokens(state.rawTokensPath, state.outDir);
|
|
734
|
+
}
|
|
735
|
+
}, onQuit: () => process.exit(0) }));
|
|
736
|
+
case 'checking-claude-auth':
|
|
737
|
+
return (_jsx(RunningStep, { stepNumber: 1, totalSteps: totalSteps, title: `Checking ${state.agent}`, description: `Verifying ${state.agent} is installed and authenticated...` }));
|
|
738
|
+
case 'generating-tokens':
|
|
739
|
+
return (_jsx(RunningStep, { stepNumber: 1, totalSteps: totalSteps, title: "Generating token definitions", description: `${state.agent} is mapping your design tokens to DTCG format and writing tokens.json. This may take a few minutes.` }));
|
|
740
|
+
case 'path-validation':
|
|
741
|
+
return (_jsx(PathValidationStep, { projectPath: state.projectPath, onConfirm: (path) => {
|
|
742
|
+
void runExtract(path);
|
|
743
|
+
}, onSkipComponents: () => update({ step: 'push-decision-gate', skipComponents: true }), onChangePath: () => update({ step: 'welcome' }), onQuit: () => process.exit(0) }));
|
|
744
|
+
case 'extracting': {
|
|
745
|
+
const ep = state.extractProgress;
|
|
746
|
+
let extractDetail;
|
|
747
|
+
if (ep && ep.totalFiles > 0) {
|
|
748
|
+
extractDetail = `Analyzing ${ep.filesProcessed}/${ep.totalFiles} files · ${ep.componentsFound} component${ep.componentsFound === 1 ? '' : 's'} found`;
|
|
749
|
+
}
|
|
750
|
+
else if (ep && ep.scanned > 0) {
|
|
751
|
+
extractDetail = `Scanned ${ep.scanned} file${ep.scanned === 1 ? '' : 's'}...`;
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
extractDetail = 'Scanning...';
|
|
755
|
+
}
|
|
756
|
+
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 }));
|
|
757
|
+
}
|
|
758
|
+
case 'review-extraction-gate': {
|
|
759
|
+
const stepNum = hasTokens ? 2 : 1;
|
|
760
|
+
return (_jsx(GateStep, { successMessage: `Step ${stepNum} complete`, summary: `Found ${state.extractedCount} component${state.extractedCount === 1 ? '' : 's'}.`, context: "Ready to review what was extracted? You can correct any props the extractor got wrong, or approve everything and move on.", continueLabel: "Review components", skipLabel: "Approve all and skip", onContinue: () => {
|
|
761
|
+
if (!state.extractSessionId) {
|
|
762
|
+
update({
|
|
763
|
+
step: 'error',
|
|
764
|
+
errorStep: 'analyze extract',
|
|
765
|
+
errorMessage: 'Extract session ID missing — please re-run.',
|
|
766
|
+
});
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
void runAnalyzeSelect(state.extractSessionId, state.extractedCount, state.tokensPath, false);
|
|
770
|
+
}, onSkip: () => {
|
|
771
|
+
if (!state.extractSessionId) {
|
|
772
|
+
update({
|
|
773
|
+
step: 'error',
|
|
774
|
+
errorStep: 'analyze extract',
|
|
775
|
+
errorMessage: 'Extract session ID missing — please re-run.',
|
|
776
|
+
});
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
void runAnalyzeSelect(state.extractSessionId, state.extractedCount, state.tokensPath, true);
|
|
780
|
+
}, onQuit: () => process.exit(0) }));
|
|
781
|
+
}
|
|
782
|
+
case 'analyze-select':
|
|
783
|
+
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Launching component review... (the TUI will appear shortly)" }) }));
|
|
784
|
+
case 'generating': {
|
|
785
|
+
const p = state.generateProgress;
|
|
786
|
+
const stepNum = hasTokens ? 4 : 3;
|
|
787
|
+
const progressDetail = p
|
|
788
|
+
? `[${p.done}/${p.total}] ${p.current} — this can take 10–30 minutes for large libraries`
|
|
789
|
+
: `Starting up ${state.agent}... (this can take 10–30 minutes for large libraries — grab a coffee)`;
|
|
790
|
+
return (_jsx(RunningStep, { stepNumber: stepNum, totalSteps: totalSteps, title: "Generating definitions", description: `${state.acceptedCount} component${state.acceptedCount === 1 ? '' : 's'} accepted. ${state.agent} is mapping your TypeScript types to Contentful's CDF format.${hasTokens ? ' Using your design tokens for prop resolution.' : ''}`, detail: progressDetail }));
|
|
791
|
+
}
|
|
792
|
+
case 'review-generated-gate': {
|
|
793
|
+
const stepNum = hasTokens ? 4 : 3;
|
|
794
|
+
return (_jsx(GateStep, { successMessage: `Step ${stepNum} complete — definitions generated`, summary: `Generated definitions for ${state.generatedCount} component${state.generatedCount === 1 ? '' : 's'}.`, context: "Take a final look before pushing to Contentful. You can accept, reject, or inspect each component's generated definition.", continueLabel: "Review definitions", skipLabel: "Approve all and skip", onContinue: () => {
|
|
795
|
+
update({ step: 'generate-review' });
|
|
796
|
+
}, onSkip: () => {
|
|
797
|
+
void runGenerateEdit(state.generateSessionId, state.generatedCount, true);
|
|
798
|
+
}, onQuit: () => process.exit(0) }));
|
|
799
|
+
}
|
|
800
|
+
case 'generate-review': {
|
|
801
|
+
if (!state.extractSessionId) {
|
|
802
|
+
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: "red", children: "Error: no session ID \u2014 cannot load generated definitions." }) }));
|
|
803
|
+
}
|
|
804
|
+
return (_jsx(GenerateReviewStep, { extractSessionId: state.extractSessionId, onFinalize: (accepted, rejected) => {
|
|
805
|
+
update({ generatedAcceptedCount: accepted, step: 'push-decision-gate' });
|
|
806
|
+
void Promise.resolve();
|
|
807
|
+
// log so orchestrator can read it
|
|
808
|
+
process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected}\n`);
|
|
809
|
+
}, onQuit: () => process.exit(0) }));
|
|
810
|
+
}
|
|
811
|
+
case 'generate-edit':
|
|
812
|
+
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Launching definition review... (the TUI will appear shortly)" }) }));
|
|
813
|
+
case 'push-decision-gate': {
|
|
814
|
+
const tokenDesc = hasTokens ? 'tokens.json' : null;
|
|
815
|
+
const compDesc = hasComponents ? 'components.json' : null;
|
|
816
|
+
const files = [tokenDesc, compDesc].filter(Boolean).join(' and ');
|
|
817
|
+
const count = state.generatedAcceptedCount > 0 ? state.generatedAcceptedCount : state.generatedCount;
|
|
818
|
+
const summary = hasComponents
|
|
819
|
+
? `${count} component definition${count !== 1 ? 's' : ''} ready${hasTokens ? ', design tokens ready' : ''}.`
|
|
820
|
+
: hasTokens
|
|
821
|
+
? 'Design tokens ready.'
|
|
822
|
+
: 'Ready to continue.';
|
|
823
|
+
return (_jsx(GateStep, { successMessage: "Generation complete", summary: summary, context: `Push directly to your Contentful space now, or save ${files || 'the output files'} to disk first.`, continueLabel: "Push to Contentful", skipLabel: `Save ${files || 'files'} to disk`, onContinue: () => {
|
|
824
|
+
update({ step: 'credentials' });
|
|
825
|
+
}, onSkip: () => {
|
|
826
|
+
void runPrintFiles(state.extractSessionId, state.outDir);
|
|
827
|
+
}, onQuit: () => process.exit(0) }));
|
|
828
|
+
}
|
|
829
|
+
case 'credentials':
|
|
830
|
+
return (_jsx(CredentialsStep, { initialSpaceId: state.spaceId, initialEnvironmentId: state.environmentId, initialCmaToken: state.cmaToken, error: state.credentialsError || undefined, onConfirm: confirmCredentials, onContinue: confirmCredentials, onQuit: () => process.exit(0) }));
|
|
831
|
+
case 'credential-test-gate':
|
|
832
|
+
return (_jsx(GateStep, { successMessage: "Credentials entered", summary: `Space: ${state.spaceId} · Environment: ${state.environmentId}`, context: "Verify your credentials work before running the import, or skip and find out during the push step.", continueLabel: "Test credentials", skipLabel: "Skip and continue", showSkip: true, onContinue: () => {
|
|
833
|
+
void validateCredentials(state.spaceId, state.environmentId, state.cmaToken);
|
|
834
|
+
}, onSkip: () => {
|
|
835
|
+
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
836
|
+
void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken);
|
|
837
|
+
}, onQuit: () => process.exit(0) }));
|
|
838
|
+
case 'validating-credentials':
|
|
839
|
+
return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Validating credentials", description: "Checking that your space ID and CMA token are valid..." }));
|
|
840
|
+
case 'previewing':
|
|
841
|
+
return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Computing diff", description: "Computing diff against your Contentful space..." }));
|
|
842
|
+
case 'preview-gate':
|
|
843
|
+
return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, onConfirm: (acknowledge) => {
|
|
844
|
+
void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, acknowledge, state.serverPreview);
|
|
845
|
+
}, onEdit: () => {
|
|
846
|
+
void runEditFromPreview(state.serverPreview);
|
|
847
|
+
}, onSaveFiles: () => {
|
|
848
|
+
void runPrintFiles(state.extractSessionId, state.outDir);
|
|
849
|
+
}, onQuit: () => process.exit(0) }));
|
|
850
|
+
case 'pushing':
|
|
851
|
+
return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Push to Contentful", description: "Writing component types and design tokens to your Contentful space...", detail: state.pushProgress ?? undefined }));
|
|
852
|
+
case 'printing':
|
|
853
|
+
return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Writing files", description: "Writing output files to disk..." }));
|
|
854
|
+
case 'print-gate':
|
|
855
|
+
return (_jsx(GateStep, { successMessage: "Files saved", summary: [
|
|
856
|
+
hasComponents && state.componentsPath ? `components.json → ${state.componentsPath}` : null,
|
|
857
|
+
hasTokens && state.tokensPath ? `tokens.json → ${state.tokensPath}` : null,
|
|
858
|
+
]
|
|
859
|
+
.filter(Boolean)
|
|
860
|
+
.join('\n'), context: "Your files are saved to disk. Run `exo import` again when you're ready to push to Contentful.", continueLabel: "Exit", showSkip: false, onContinue: () => process.exit(0), onQuit: () => process.exit(0) }));
|
|
861
|
+
case 'done': {
|
|
862
|
+
const totalFailed = state.pushResult.componentTypes.failed + state.pushResult.designTokens.failed;
|
|
863
|
+
return (_jsx(DoneStep, { componentTypes: state.pushResult.componentTypes, designTokens: state.pushResult.designTokens, summary: state.pushResult.summary, spaceId: state.spaceId, environmentId: state.environmentId, onExit: () => process.exit(totalFailed > 0 ? 1 : 0) }));
|
|
864
|
+
}
|
|
865
|
+
case 'error':
|
|
866
|
+
return _jsx(ErrorStep, { stepName: state.errorStep, message: state.errorMessage, onExit: () => process.exit(1) });
|
|
867
|
+
}
|
|
868
|
+
})();
|
|
869
|
+
return (_jsxs(Box, { flexDirection: "column", width: terminalWidth, children: [_jsx(TopBar, { subcommand: "import", hints: hints }), stepContent] }));
|
|
870
|
+
}
|
|
871
|
+
function buildPreviewAnnotations(preview) {
|
|
872
|
+
const annotations = {};
|
|
873
|
+
if (!preview)
|
|
874
|
+
return annotations;
|
|
875
|
+
for (const item of preview.components.new) {
|
|
876
|
+
const key = item.key ?? '';
|
|
877
|
+
if (key)
|
|
878
|
+
annotations[key] = 'new';
|
|
879
|
+
}
|
|
880
|
+
for (const item of preview.components.removed) {
|
|
881
|
+
annotations[item.name] = 'removed';
|
|
882
|
+
}
|
|
883
|
+
for (const item of preview.components.changed) {
|
|
884
|
+
if (item.changeClassification?.classification === 'breaking') {
|
|
885
|
+
annotations[item.current.name] = 'breaking';
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
annotations[item.current.name] = 'changed';
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return annotations;
|
|
892
|
+
}
|
|
893
|
+
// Interactive subprocess helper — uses spawn with inherited stdio so child isTTY is true
|
|
894
|
+
function runCliInteractive(args) {
|
|
895
|
+
logStep({
|
|
896
|
+
fn: 'runCliInteractive:spawn',
|
|
897
|
+
args,
|
|
898
|
+
});
|
|
899
|
+
return new Promise((res) => {
|
|
900
|
+
const child = spawn('node', [findCliPath(), ...args], { stdio: 'inherit' });
|
|
901
|
+
child.on('exit', (code) => {
|
|
902
|
+
logStep({ fn: 'runCliInteractive:exit', args, exitCode: code ?? 0 });
|
|
903
|
+
res({ exitCode: code ?? 0, stdout: '', stderr: '' });
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
}
|