@contentful/experience-design-system-cli 2.7.4 → 2.11.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/dist/package.json +1 -1
- package/dist/src/analyze/build-analyze-view-rows.d.ts +23 -0
- package/dist/src/analyze/build-analyze-view-rows.js +39 -0
- package/dist/src/analyze/command.js +16 -11
- package/dist/src/analyze/extract/validate.d.ts +16 -0
- package/dist/src/analyze/extract/validate.js +94 -0
- package/dist/src/analyze/select/command.d.ts +47 -0
- package/dist/src/analyze/select/command.js +238 -14
- package/dist/src/analyze/select/tui/App.js +15 -17
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +4 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +42 -6
- package/dist/src/analyze/select/types.d.ts +6 -0
- package/dist/src/analyze/select/types.js +24 -7
- package/dist/src/analyze/select-agent/command.js +43 -3
- package/dist/src/analyze/tui/AnalyzeView.d.ts +8 -0
- package/dist/src/analyze/tui/AnalyzeView.js +5 -2
- package/dist/src/apply/api-client.d.ts +20 -0
- package/dist/src/apply/api-client.js +71 -5
- package/dist/src/generate/command.js +21 -2
- package/dist/src/import/command.js +2 -0
- package/dist/src/import/orchestrator.d.ts +22 -0
- package/dist/src/import/orchestrator.js +97 -14
- package/dist/src/import/tui/WizardApp.d.ts +13 -0
- package/dist/src/import/tui/WizardApp.js +224 -52
- package/dist/src/import/tui/steps/GateStep.d.ts +2 -1
- package/dist/src/import/tui/steps/GateStep.js +4 -2
- package/dist/src/import/tui/steps/GenerateReviewStep.js +2 -0
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.d.ts +11 -0
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +14 -0
- package/dist/src/import/tui/wizard-422-helpers.d.ts +48 -0
- package/dist/src/import/tui/wizard-422-helpers.js +60 -0
- package/dist/src/program.d.ts +17 -0
- package/dist/src/program.js +27 -4
- package/dist/src/session/db.d.ts +13 -0
- package/dist/src/session/db.js +50 -0
- package/dist/src/types.d.ts +8 -0
- package/package.json +2 -2
- package/skills/generate-components.md +2 -0
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, } from '../session/db.js';
|
|
6
|
+
import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
|
|
6
7
|
function findCliPath() {
|
|
7
8
|
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
|
|
8
9
|
}
|
|
@@ -27,6 +28,49 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
|
|
|
27
28
|
});
|
|
28
29
|
});
|
|
29
30
|
}
|
|
31
|
+
const MAX_VALIDATION_RETRIES = Number(process.env['EDS_MAX_VALIDATION_RETRIES'] ?? 2);
|
|
32
|
+
export function isPreviewValidationError(result) {
|
|
33
|
+
return (result.exitCode !== 0 &&
|
|
34
|
+
result.stderr.includes(`${PREVIEW_ERROR_PREFIX} 422`) &&
|
|
35
|
+
result.stderr.includes(VALIDATION_FAILED_CODE));
|
|
36
|
+
}
|
|
37
|
+
export function parseOffendingComponentNames(output) {
|
|
38
|
+
// The 422 body is appended to the ApiError message by the constructor and
|
|
39
|
+
// written to stderr by die(). Extract the JSON portion by finding the first '{'.
|
|
40
|
+
const jsonStart = output.indexOf('{');
|
|
41
|
+
if (jsonStart === -1)
|
|
42
|
+
return [];
|
|
43
|
+
const errors = parsePreviewValidationErrors(output.slice(jsonStart));
|
|
44
|
+
return [...new Set(errors.map((e) => e.componentName))];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the apply-push StepResult record. Centralizes the success/failure
|
|
48
|
+
* shape so excludedByValidationRetry is recorded consistently in both
|
|
49
|
+
* branches — previously the total-failure branch dropped it, leaving users
|
|
50
|
+
* with a failed pipeline and no audit trail of what was auto-excluded
|
|
51
|
+
* before the retry loop gave up.
|
|
52
|
+
*/
|
|
53
|
+
export function buildPushStepResult(args) {
|
|
54
|
+
const { created, updated, failed, durationMs, stderr, excludedByRetry, totalFailure } = args;
|
|
55
|
+
const excludedDetail = excludedByRetry.length > 0 ? { excludedByValidationRetry: excludedByRetry } : {};
|
|
56
|
+
if (totalFailure) {
|
|
57
|
+
const detail = excludedByRetry.length > 0 ? { ...excludedDetail } : undefined;
|
|
58
|
+
return {
|
|
59
|
+
step: 'apply push',
|
|
60
|
+
status: 'failed',
|
|
61
|
+
durationMs,
|
|
62
|
+
error: stderr,
|
|
63
|
+
...(detail ? { detail } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const status = failed > 0 ? 'failed' : 'complete';
|
|
67
|
+
return {
|
|
68
|
+
step: 'apply push',
|
|
69
|
+
status,
|
|
70
|
+
durationMs,
|
|
71
|
+
detail: { created, updated, failed, ...excludedDetail },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
30
74
|
export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
31
75
|
const projectRoot = resolve(opts.project);
|
|
32
76
|
const outDir = resolve(opts.out);
|
|
@@ -128,6 +172,8 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
128
172
|
editArgs = ['analyze', 'select-agent', '--session', extractSessionId, '--agent', opts.agent];
|
|
129
173
|
if (opts.model)
|
|
130
174
|
editArgs.push('--model', opts.model);
|
|
175
|
+
if (opts.excludeInvalid)
|
|
176
|
+
editArgs.push('--exclude-invalid');
|
|
131
177
|
}
|
|
132
178
|
else {
|
|
133
179
|
editArgs = ['analyze', 'select', '--session', extractSessionId];
|
|
@@ -320,7 +366,37 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
320
366
|
components: componentsPath,
|
|
321
367
|
});
|
|
322
368
|
const t0 = Date.now();
|
|
323
|
-
|
|
369
|
+
let r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
370
|
+
// Bounded retry loop: on a preview-phase 422 with a parseable ValidationFailed
|
|
371
|
+
// body, exclude the offending components and re-run apply push.
|
|
372
|
+
const excludedByRetry = [];
|
|
373
|
+
let validationRetryCount = 0;
|
|
374
|
+
while (validationRetryCount < MAX_VALIDATION_RETRIES && isPreviewValidationError(r) && extractSessionId) {
|
|
375
|
+
const offenders = parseOffendingComponentNames(r.stderr + r.stdout);
|
|
376
|
+
if (offenders.length === 0)
|
|
377
|
+
break; // unparseable body — give up and surface original error
|
|
378
|
+
process.stderr.write(`[retry ${validationRetryCount + 1}/${MAX_VALIDATION_RETRIES}] Preview validation failed — excluding ${offenders.join(', ')} and retrying\n`);
|
|
379
|
+
excludedByRetry.push(...offenders);
|
|
380
|
+
// No --select-all here: that would route through runNonInteractive's
|
|
381
|
+
// rebuild path, which DELETEs all rows and re-inserts with default
|
|
382
|
+
// status='extracted' — wiping the post-`generate components` state
|
|
383
|
+
// (status='generated' + raw_props.cdf_type) the next apply push reads.
|
|
384
|
+
// The bare --exclude-components form takes the rejectComponentsByName
|
|
385
|
+
// early-return: pure UPDATE, no rebuild.
|
|
386
|
+
const rejectArgs = [
|
|
387
|
+
'analyze',
|
|
388
|
+
'select',
|
|
389
|
+
'--session',
|
|
390
|
+
extractSessionId,
|
|
391
|
+
'--exclude-components',
|
|
392
|
+
offenders.join(','),
|
|
393
|
+
];
|
|
394
|
+
const rejectResult = await runStep(rejectArgs, cliPath);
|
|
395
|
+
if (rejectResult.exitCode !== 0)
|
|
396
|
+
break; // selection step failed — give up
|
|
397
|
+
r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
398
|
+
validationRetryCount++;
|
|
399
|
+
}
|
|
324
400
|
const durationMs = Date.now() - t0;
|
|
325
401
|
let pushResult = null;
|
|
326
402
|
try {
|
|
@@ -343,25 +419,32 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
343
419
|
// Total failure — nothing was pushed
|
|
344
420
|
updateStep(db, pushStepId, 'failed', {}, r.stderr);
|
|
345
421
|
progressWriter(`${pushLabel}✗ failed (${(durationMs / 1000).toFixed(1)}s)`);
|
|
346
|
-
steps.push({
|
|
347
|
-
|
|
348
|
-
|
|
422
|
+
steps.push(buildPushStepResult({
|
|
423
|
+
created,
|
|
424
|
+
updated,
|
|
425
|
+
failed,
|
|
349
426
|
durationMs,
|
|
350
|
-
|
|
351
|
-
|
|
427
|
+
stderr: r.stderr,
|
|
428
|
+
excludedByRetry,
|
|
429
|
+
totalFailure: true,
|
|
430
|
+
}));
|
|
352
431
|
db.close();
|
|
353
432
|
return { session: sessionId, project: projectRoot, steps };
|
|
354
433
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
steps.push({
|
|
360
|
-
step: 'apply push',
|
|
361
|
-
status: stepStatus,
|
|
434
|
+
const stepResult = buildPushStepResult({
|
|
435
|
+
created,
|
|
436
|
+
updated,
|
|
437
|
+
failed,
|
|
362
438
|
durationMs,
|
|
363
|
-
|
|
439
|
+
stderr: r.stderr,
|
|
440
|
+
excludedByRetry,
|
|
364
441
|
});
|
|
442
|
+
updateStep(db, pushStepId, stepResult.status === 'complete' ? 'complete' : 'failed', {
|
|
443
|
+
components: componentsPath,
|
|
444
|
+
});
|
|
445
|
+
const statusIcon = failed > 0 ? '⚠' : '✓';
|
|
446
|
+
progressWriter(`${pushLabel}${statusIcon} ${created} created, ${updated} updated, ${failed} failed (${(durationMs / 1000).toFixed(1)}s)`);
|
|
447
|
+
steps.push(stepResult);
|
|
365
448
|
}
|
|
366
449
|
progressWriter('');
|
|
367
450
|
progressWriter(`Pipeline complete. Session: ${sessionId}`);
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
export declare function buildAnalyzeSelectArgs(opts: {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
acceptAll: boolean;
|
|
5
|
+
}): string[];
|
|
6
|
+
export declare function formatAcceptanceSummary(opts: {
|
|
7
|
+
accepted: number;
|
|
8
|
+
autoRejected: number;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function formatGeneratedSummary(opts: {
|
|
11
|
+
generated: number;
|
|
12
|
+
renamedSlots: number;
|
|
13
|
+
autoRejected: number;
|
|
14
|
+
}): string;
|
|
2
15
|
export type WizardAppProps = {
|
|
3
16
|
initialSpaceId?: string;
|
|
4
17
|
initialEnvironmentId?: string;
|
|
@@ -18,7 +18,9 @@ import { DoneStep } from './steps/DoneStep.js';
|
|
|
18
18
|
import { ErrorStep } from './steps/ErrorStep.js';
|
|
19
19
|
import { TokenInputStep } from './steps/TokenInputStep.js';
|
|
20
20
|
import { GenerateReviewStep } from './steps/GenerateReviewStep.js';
|
|
21
|
+
import { PreviewValidationErrorStep } from './steps/PreviewValidationErrorStep.js';
|
|
21
22
|
import { ImportApiClient, ApiError } from '../../apply/api-client.js';
|
|
23
|
+
import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
|
|
22
24
|
import { readTokensFromPath, hasBreakingChangesWithImpact } from '../../apply/manifest.js';
|
|
23
25
|
import { buildManifest } from '@contentful/experience-design-system-types';
|
|
24
26
|
import { openPipelineDb, loadCDFComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
|
|
@@ -29,6 +31,34 @@ import { writeExperiencesCredentials } from '../../credentials-store.js';
|
|
|
29
31
|
function findCliPath() {
|
|
30
32
|
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'bin', 'cli.js');
|
|
31
33
|
}
|
|
34
|
+
export function buildAnalyzeSelectArgs(opts) {
|
|
35
|
+
const args = ['analyze', 'select', '--session', opts.sessionId];
|
|
36
|
+
if (opts.acceptAll) {
|
|
37
|
+
// The wizard pre-shows validation errors in the analyze-extract TUI before
|
|
38
|
+
// reaching this gate, so the user has already seen what will be excluded.
|
|
39
|
+
// Pass --exclude-invalid to bypass the headless fail-loud gate (which is
|
|
40
|
+
// for CI / scripted callers that need to fail loud) — the wizard surfaces
|
|
41
|
+
// the auto-rejection via formatAcceptanceSummary on the next screen.
|
|
42
|
+
args.push('--select-all', '--exclude-invalid');
|
|
43
|
+
}
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
export function formatAcceptanceSummary(opts) {
|
|
47
|
+
const acceptedClause = `${opts.accepted} component${opts.accepted === 1 ? '' : 's'} accepted`;
|
|
48
|
+
if (opts.autoRejected === 0)
|
|
49
|
+
return `${acceptedClause}.`;
|
|
50
|
+
return `${acceptedClause}, ${opts.autoRejected} excluded due to validation errors.`;
|
|
51
|
+
}
|
|
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
|
+
}
|
|
32
62
|
function runCli(args) {
|
|
33
63
|
return new Promise((res) => {
|
|
34
64
|
execFile('node', [findCliPath(), ...args], (error, stdout, stderr) => {
|
|
@@ -70,8 +100,10 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
70
100
|
generateSessionId: null,
|
|
71
101
|
extractedCount: 0,
|
|
72
102
|
acceptedCount: 0,
|
|
103
|
+
autoRejectedCount: 0,
|
|
73
104
|
generatedCount: 0,
|
|
74
105
|
generatedAcceptedCount: 0,
|
|
106
|
+
renamedSlotsCount: 0,
|
|
75
107
|
generateProgress: null,
|
|
76
108
|
extractProgress: null,
|
|
77
109
|
componentsPath: '',
|
|
@@ -91,6 +123,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
91
123
|
errorMessage: '',
|
|
92
124
|
errorAllowCredentialRetry: false,
|
|
93
125
|
authCheckStepNumber: 1,
|
|
126
|
+
previewValidationErrors: [],
|
|
127
|
+
previewValidationMissingNames: [],
|
|
94
128
|
});
|
|
95
129
|
useEffect(() => {
|
|
96
130
|
sessionRef.current = {
|
|
@@ -195,7 +229,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
195
229
|
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
196
230
|
});
|
|
197
231
|
if (result.exitCode !== 0) {
|
|
198
|
-
update({
|
|
232
|
+
update({
|
|
233
|
+
step: 'error',
|
|
234
|
+
errorStep: 'generate tokens',
|
|
235
|
+
errorMessage: result.stderr.trim() || 'Unknown error',
|
|
236
|
+
});
|
|
199
237
|
return;
|
|
200
238
|
}
|
|
201
239
|
const sessionMatch = /^session:\s*(.+)$/m.exec(result.stdout);
|
|
@@ -206,7 +244,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
206
244
|
printArgs.push('--session', tokenSessionId);
|
|
207
245
|
const r = await runCli(printArgs);
|
|
208
246
|
if (r.exitCode !== 0) {
|
|
209
|
-
update({
|
|
247
|
+
update({
|
|
248
|
+
step: 'error',
|
|
249
|
+
errorStep: 'print tokens',
|
|
250
|
+
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
251
|
+
});
|
|
210
252
|
return;
|
|
211
253
|
}
|
|
212
254
|
update({ step: 'path-validation', tokensPath, tokenSessionId });
|
|
@@ -259,7 +301,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
259
301
|
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
260
302
|
});
|
|
261
303
|
if (r.exitCode !== 0) {
|
|
262
|
-
update({
|
|
304
|
+
update({
|
|
305
|
+
step: 'error',
|
|
306
|
+
errorStep: 'analyze extract',
|
|
307
|
+
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
308
|
+
});
|
|
263
309
|
return;
|
|
264
310
|
}
|
|
265
311
|
const sessionMatch = /^session=(.+)$/m.exec(r.stdout);
|
|
@@ -274,41 +320,57 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
274
320
|
});
|
|
275
321
|
return;
|
|
276
322
|
}
|
|
277
|
-
update({
|
|
323
|
+
update({
|
|
324
|
+
step: 'review-extraction-gate',
|
|
325
|
+
extractSessionId,
|
|
326
|
+
extractedCount,
|
|
327
|
+
});
|
|
278
328
|
};
|
|
279
329
|
const runAnalyzeSelect = async (sessionId, extractedCount, tokensPath, acceptAll) => {
|
|
280
|
-
logStep({
|
|
330
|
+
logStep({
|
|
331
|
+
fn: 'runAnalyzeSelect:enter',
|
|
332
|
+
sessionId,
|
|
333
|
+
extractedCount,
|
|
334
|
+
acceptAll,
|
|
335
|
+
});
|
|
281
336
|
let acceptedCount;
|
|
282
|
-
if (acceptAll) {
|
|
283
|
-
|
|
284
|
-
acceptedCount = extractedCount;
|
|
337
|
+
if (!acceptAll && state.serverPreview && !process.env['EDS_PREVIEW_ANNOTATIONS']) {
|
|
338
|
+
process.env['EDS_PREVIEW_ANNOTATIONS'] = JSON.stringify(buildPreviewAnnotations(state.serverPreview));
|
|
285
339
|
}
|
|
286
|
-
|
|
287
|
-
if (state.serverPreview && !process.env['EDS_PREVIEW_ANNOTATIONS']) {
|
|
288
|
-
process.env['EDS_PREVIEW_ANNOTATIONS'] = JSON.stringify(buildPreviewAnnotations(state.serverPreview));
|
|
289
|
-
}
|
|
340
|
+
if (!acceptAll) {
|
|
290
341
|
update({ step: 'analyze-select' });
|
|
291
|
-
|
|
292
|
-
|
|
342
|
+
}
|
|
343
|
+
const args = buildAnalyzeSelectArgs({ sessionId, acceptAll });
|
|
344
|
+
const r = acceptAll ? await runCli(args) : await runCliInteractive(args);
|
|
345
|
+
logStep({ fn: 'runAnalyzeSelect:post-spawn', exitCode: r.exitCode, args });
|
|
346
|
+
if (!acceptAll)
|
|
293
347
|
clearPreviewEnvVars();
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
: resolve(homedir(), '.contentful', 'experience-design-system-cli', 'reviews');
|
|
302
|
-
const reviewStatePath = resolve(artifactsRoot, sessionId, 'current-review-state.json');
|
|
303
|
-
try {
|
|
304
|
-
const reviewState = JSON.parse(await readFile(reviewStatePath, 'utf8'));
|
|
305
|
-
acceptedCount = reviewState.components.filter((c) => c.status === 'accepted').length;
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
acceptedCount = extractedCount;
|
|
309
|
-
}
|
|
348
|
+
if (r.exitCode !== 0) {
|
|
349
|
+
update({
|
|
350
|
+
step: 'error',
|
|
351
|
+
errorStep: 'analyze select',
|
|
352
|
+
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
310
355
|
}
|
|
311
|
-
|
|
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
|
+
try {
|
|
366
|
+
const reviewState = JSON.parse(await readFile(reviewStatePath, 'utf8'));
|
|
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;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
acceptedCount = extractedCount;
|
|
372
|
+
}
|
|
373
|
+
update({ acceptedCount, autoRejectedCount });
|
|
312
374
|
if (acceptedCount > 0) {
|
|
313
375
|
if (await runAgentAuthCheck('generating')) {
|
|
314
376
|
void runGenerate(sessionId, tokensPath, acceptedCount);
|
|
@@ -335,7 +397,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
335
397
|
for (const line of chunk.split('\n')) {
|
|
336
398
|
const m = /\[(\d+)\/(\d+)\]\s+(.+)/.exec(line);
|
|
337
399
|
if (m)
|
|
338
|
-
update({
|
|
400
|
+
update({
|
|
401
|
+
generateProgress: {
|
|
402
|
+
done: Number(m[1]),
|
|
403
|
+
total: Number(m[2]),
|
|
404
|
+
current: m[3].trim(),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
339
407
|
}
|
|
340
408
|
});
|
|
341
409
|
child.on('exit', (code) => res({ exitCode: code ?? 0, stdout, stderr }));
|
|
@@ -352,7 +420,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
352
420
|
const generateSessionId = sessionMatch ? sessionMatch[1].trim() : null;
|
|
353
421
|
const countMatch = /(\d+) components?/.exec(result.stderr);
|
|
354
422
|
const generatedCount = countMatch ? Number(countMatch[1]) : acceptedCount;
|
|
355
|
-
|
|
423
|
+
const renamedMatch = /^renamed-slots:\s*(\d+)$/m.exec(result.stdout);
|
|
424
|
+
const renamedSlotsCount = renamedMatch ? Number(renamedMatch[1]) : 0;
|
|
425
|
+
update({
|
|
426
|
+
step: 'review-generated-gate',
|
|
427
|
+
generateSessionId,
|
|
428
|
+
generatedCount,
|
|
429
|
+
renamedSlotsCount,
|
|
430
|
+
generateProgress: null,
|
|
431
|
+
});
|
|
356
432
|
};
|
|
357
433
|
const advanceToPushFlow = (generatedAcceptedCount) => {
|
|
358
434
|
update({ generatedAcceptedCount, step: 'credentials' });
|
|
@@ -367,7 +443,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
367
443
|
if (sessionId) {
|
|
368
444
|
const r = await runCliInteractive(['generate', 'components', 'edit', '--session', sessionId]);
|
|
369
445
|
if (r.exitCode !== 0) {
|
|
370
|
-
update({
|
|
446
|
+
update({
|
|
447
|
+
step: 'error',
|
|
448
|
+
errorStep: 'generate edit',
|
|
449
|
+
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
450
|
+
});
|
|
371
451
|
return;
|
|
372
452
|
}
|
|
373
453
|
const acceptedMatch = /Accepted: (\d+)/.exec(r.stderr);
|
|
@@ -409,7 +489,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
409
489
|
const runEditFromPreview = async (preview) => {
|
|
410
490
|
const sessionId = state.extractSessionId;
|
|
411
491
|
if (!sessionId) {
|
|
412
|
-
update({
|
|
492
|
+
update({
|
|
493
|
+
step: 'error',
|
|
494
|
+
errorStep: 'edit definitions',
|
|
495
|
+
errorMessage: 'No session available for editing',
|
|
496
|
+
});
|
|
413
497
|
return;
|
|
414
498
|
}
|
|
415
499
|
process.env['EDS_PREVIEW_ANNOTATIONS'] = JSON.stringify(buildPreviewAnnotations(preview));
|
|
@@ -430,13 +514,22 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
430
514
|
const r = await runCliInteractive(['analyze', 'select', '--session', sessionId]);
|
|
431
515
|
clearPreviewEnvVars();
|
|
432
516
|
if (r.exitCode !== 0) {
|
|
433
|
-
update({
|
|
517
|
+
update({
|
|
518
|
+
step: 'error',
|
|
519
|
+
errorStep: 'edit definitions',
|
|
520
|
+
errorMessage: 'Editor exited with an error',
|
|
521
|
+
});
|
|
434
522
|
return;
|
|
435
523
|
}
|
|
436
524
|
// Re-preview with updated definitions
|
|
437
525
|
const { extractSessionId: sid, tokensPath: tp } = sessionRef.current;
|
|
438
526
|
void runPreview(sid, tp, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
439
527
|
};
|
|
528
|
+
const runSkipValidationErrorsAndRetry = async (errors) => {
|
|
529
|
+
await applySkipValidationErrors(state.extractSessionId, errors);
|
|
530
|
+
const { extractSessionId: sid, tokensPath: tp } = sessionRef.current;
|
|
531
|
+
void runPreview(sid, tp, state.spaceId, state.environmentId, state.cmaToken, state.host);
|
|
532
|
+
};
|
|
440
533
|
const advanceWithCredentials = (spaceId, environmentId, cmaToken, host) => {
|
|
441
534
|
const resolvedHost = resolveWizardHost(host);
|
|
442
535
|
credentialsRef.current = { spaceId, environmentId, cmaToken };
|
|
@@ -452,7 +545,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
452
545
|
const confirmCredentials = async (spaceId, environmentId, cmaToken, host) => {
|
|
453
546
|
const resolvedHost = resolveWizardHost(host);
|
|
454
547
|
try {
|
|
455
|
-
await writeExperiencesCredentials({
|
|
548
|
+
await writeExperiencesCredentials({
|
|
549
|
+
spaceId,
|
|
550
|
+
environmentId,
|
|
551
|
+
cmaToken,
|
|
552
|
+
host: resolvedHost,
|
|
553
|
+
});
|
|
456
554
|
advanceWithCredentials(spaceId, environmentId, cmaToken, resolvedHost);
|
|
457
555
|
}
|
|
458
556
|
catch (e) {
|
|
@@ -471,7 +569,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
471
569
|
update({ step: 'validating-credentials' });
|
|
472
570
|
try {
|
|
473
571
|
const resolvedHost = resolveWizardHost(host);
|
|
474
|
-
const client = new ImportApiClient({
|
|
572
|
+
const client = new ImportApiClient({
|
|
573
|
+
cmaToken,
|
|
574
|
+
spaceId,
|
|
575
|
+
environmentId,
|
|
576
|
+
host: resolvedHost,
|
|
577
|
+
});
|
|
475
578
|
await client.validateToken();
|
|
476
579
|
const { extractSessionId, tokensPath } = sessionRef.current;
|
|
477
580
|
void runPreview(extractSessionId, tokensPath, spaceId, environmentId, cmaToken, resolvedHost);
|
|
@@ -494,7 +597,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
494
597
|
update({ step: 'previewing' });
|
|
495
598
|
const resolvedHost = resolveWizardHost(host);
|
|
496
599
|
try {
|
|
497
|
-
const client = new ImportApiClient({
|
|
600
|
+
const client = new ImportApiClient({
|
|
601
|
+
cmaToken,
|
|
602
|
+
spaceId,
|
|
603
|
+
environmentId,
|
|
604
|
+
host: resolvedHost,
|
|
605
|
+
});
|
|
498
606
|
let components = [];
|
|
499
607
|
if (extractSessionId) {
|
|
500
608
|
const db = openPipelineDb();
|
|
@@ -543,7 +651,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
543
651
|
db.close();
|
|
544
652
|
}
|
|
545
653
|
}
|
|
546
|
-
update({ step: 'preview-gate', serverPreview: preview, manifest });
|
|
654
|
+
update({ step: 'preview-gate', serverPreview: preview, manifest, ...clearedValidationErrorState() });
|
|
547
655
|
}
|
|
548
656
|
catch (e) {
|
|
549
657
|
if (e instanceof ApiError) {
|
|
@@ -585,11 +693,31 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
585
693
|
});
|
|
586
694
|
return;
|
|
587
695
|
}
|
|
588
|
-
|
|
696
|
+
const outcome = await handlePreview422(e, extractSessionId);
|
|
697
|
+
if (outcome.kind === 'validation-error') {
|
|
698
|
+
update({
|
|
699
|
+
step: 'preview-validation-error',
|
|
700
|
+
previewValidationErrors: outcome.errors,
|
|
701
|
+
previewValidationMissingNames: outcome.missingNames,
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// 'unparseable' and 'not-422' both fall through to the generic error branch below.
|
|
706
|
+
update({
|
|
707
|
+
step: 'error',
|
|
708
|
+
errorStep: 'apply preview',
|
|
709
|
+
errorMessage: e.message,
|
|
710
|
+
errorAllowCredentialRetry: true,
|
|
711
|
+
});
|
|
589
712
|
return;
|
|
590
713
|
}
|
|
591
714
|
const msg = e instanceof Error ? e.message : 'Preview failed';
|
|
592
|
-
update({
|
|
715
|
+
update({
|
|
716
|
+
step: 'error',
|
|
717
|
+
errorStep: 'apply preview',
|
|
718
|
+
errorMessage: msg,
|
|
719
|
+
errorAllowCredentialRetry: true,
|
|
720
|
+
});
|
|
593
721
|
}
|
|
594
722
|
};
|
|
595
723
|
const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, preview) => {
|
|
@@ -612,17 +740,28 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
612
740
|
update({ step: 'pushing' });
|
|
613
741
|
try {
|
|
614
742
|
const resolvedHost = resolveWizardHost(host);
|
|
615
|
-
const client = new ImportApiClient({
|
|
743
|
+
const client = new ImportApiClient({
|
|
744
|
+
cmaToken,
|
|
745
|
+
spaceId,
|
|
746
|
+
environmentId,
|
|
747
|
+
host: resolvedHost,
|
|
748
|
+
});
|
|
616
749
|
let operation = await client.applyImport(manifest, acknowledgeBreakingChanges);
|
|
617
750
|
try {
|
|
618
751
|
logStep({
|
|
619
|
-
applyResponse: {
|
|
752
|
+
applyResponse: {
|
|
753
|
+
status: operation?.sys?.status,
|
|
754
|
+
id: operation?.sys?.id,
|
|
755
|
+
keys: Object.keys(operation ?? {}),
|
|
756
|
+
},
|
|
620
757
|
});
|
|
621
758
|
}
|
|
622
759
|
catch (err) {
|
|
623
760
|
process.stderr.write(`[eds] log write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
624
761
|
}
|
|
625
|
-
update({
|
|
762
|
+
update({
|
|
763
|
+
pushProgress: `Queued (operation ${operation.sys.id.slice(0, 8)}...)`,
|
|
764
|
+
});
|
|
626
765
|
let pollCount = 0;
|
|
627
766
|
operation = await client.pollOperation(operation.sys.id, {
|
|
628
767
|
onProgress: (op) => {
|
|
@@ -633,7 +772,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
633
772
|
update({ pushProgress: `${done}/${s.total} entities processed` });
|
|
634
773
|
}
|
|
635
774
|
try {
|
|
636
|
-
logStep({
|
|
775
|
+
logStep({
|
|
776
|
+
pollTick: {
|
|
777
|
+
attempt: pollCount,
|
|
778
|
+
status: op.sys.status,
|
|
779
|
+
summary: op.summary,
|
|
780
|
+
},
|
|
781
|
+
});
|
|
637
782
|
}
|
|
638
783
|
catch (err) {
|
|
639
784
|
process.stderr.write(`[eds] log write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -691,7 +836,12 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
691
836
|
}
|
|
692
837
|
catch (e) {
|
|
693
838
|
const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : 'Push failed';
|
|
694
|
-
update({
|
|
839
|
+
update({
|
|
840
|
+
step: 'error',
|
|
841
|
+
errorStep: 'apply push',
|
|
842
|
+
errorMessage: msg,
|
|
843
|
+
errorAllowCredentialRetry: true,
|
|
844
|
+
});
|
|
695
845
|
}
|
|
696
846
|
};
|
|
697
847
|
const runPrintFiles = async (extractSessionId, outDir) => {
|
|
@@ -702,7 +852,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
702
852
|
printArgs.push('--session', extractSessionId);
|
|
703
853
|
const r = await runCli(printArgs);
|
|
704
854
|
if (r.exitCode !== 0) {
|
|
705
|
-
update({
|
|
855
|
+
update({
|
|
856
|
+
step: 'error',
|
|
857
|
+
errorStep: 'print components',
|
|
858
|
+
errorMessage: r.stderr.trim() || 'Unknown error',
|
|
859
|
+
});
|
|
706
860
|
return;
|
|
707
861
|
}
|
|
708
862
|
// tokensPath is already on disk from generate-tokens step; just record it
|
|
@@ -724,7 +878,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
724
878
|
stat(state.rawTokensPath).catch(() => null),
|
|
725
879
|
]);
|
|
726
880
|
const sourceChanged = sourceStat ? sourceStat.mtimeMs > tokensStat.mtimeMs : false;
|
|
727
|
-
update({
|
|
881
|
+
update({
|
|
882
|
+
step: 'token-reuse-gate',
|
|
883
|
+
tokensPath: existingTokensPath,
|
|
884
|
+
tokenSourceChanged: sourceChanged,
|
|
885
|
+
});
|
|
728
886
|
}
|
|
729
887
|
catch {
|
|
730
888
|
// No existing tokens — need LLM to generate
|
|
@@ -828,11 +986,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
828
986
|
const progressDetail = p
|
|
829
987
|
? `[${p.done}/${p.total}] ${p.current} — this can take 10–30 minutes for large libraries`
|
|
830
988
|
: `Starting up ${state.agent}... (this can take 10–30 minutes for large libraries — grab a coffee)`;
|
|
831
|
-
return (_jsx(RunningStep, { stepNumber: stepNum, totalSteps: totalSteps, title: "Generating definitions", description: `${state.acceptedCount
|
|
989
|
+
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 }));
|
|
832
990
|
}
|
|
833
991
|
case 'review-generated-gate': {
|
|
834
992
|
const stepNum = hasTokens ? 4 : 3;
|
|
835
|
-
return (_jsx(GateStep, { successMessage: `Step ${stepNum} complete — definitions generated`, summary:
|
|
993
|
+
return (_jsx(GateStep, { successMessage: `Step ${stepNum} complete — definitions generated`, summary: formatGeneratedSummary({
|
|
994
|
+
generated: state.generatedCount,
|
|
995
|
+
renamedSlots: state.renamedSlotsCount,
|
|
996
|
+
autoRejected: state.autoRejectedCount,
|
|
997
|
+
}), 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: () => {
|
|
836
998
|
update({ step: 'generate-review' });
|
|
837
999
|
}, onSkip: () => {
|
|
838
1000
|
void runGenerateEdit(state.generateSessionId, state.generatedCount, true);
|
|
@@ -843,7 +1005,10 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
843
1005
|
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: "red", children: "Error: no session ID \u2014 cannot load generated definitions." }) }));
|
|
844
1006
|
}
|
|
845
1007
|
return (_jsx(GenerateReviewStep, { extractSessionId: state.extractSessionId, onFinalize: (accepted, rejected) => {
|
|
846
|
-
update({
|
|
1008
|
+
update({
|
|
1009
|
+
generatedAcceptedCount: accepted,
|
|
1010
|
+
step: 'push-decision-gate',
|
|
1011
|
+
});
|
|
847
1012
|
void Promise.resolve();
|
|
848
1013
|
// log so orchestrator can read it
|
|
849
1014
|
process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected}\n`);
|
|
@@ -905,6 +1070,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
905
1070
|
const totalFailed = state.pushResult.componentTypes.failed + state.pushResult.designTokens.failed;
|
|
906
1071
|
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) }));
|
|
907
1072
|
}
|
|
1073
|
+
case 'preview-validation-error': {
|
|
1074
|
+
return (_jsx(PreviewValidationErrorStep, { errors: state.previewValidationErrors, missingNames: state.previewValidationMissingNames, onEdit: () => {
|
|
1075
|
+
void runEditFromPreview(null);
|
|
1076
|
+
}, onSkip: () => {
|
|
1077
|
+
void runSkipValidationErrorsAndRetry(state.previewValidationErrors);
|
|
1078
|
+
}, onQuit: () => process.exit(0) }));
|
|
1079
|
+
}
|
|
908
1080
|
case 'error':
|
|
909
1081
|
return (_jsx(ErrorStep, { stepName: state.errorStep, message: state.errorMessage, onExit: () => process.exit(1), onRetryCredentials: state.errorAllowCredentialRetry ? () => update({ step: 'credentials', credentialsError: '' }) : undefined }));
|
|
910
1082
|
}
|
|
@@ -9,6 +9,7 @@ type GateStepProps = {
|
|
|
9
9
|
onSkip?: () => void;
|
|
10
10
|
onQuit: () => void;
|
|
11
11
|
showSkip?: boolean;
|
|
12
|
+
intent?: 'success' | 'error';
|
|
12
13
|
};
|
|
13
|
-
export declare function GateStep({ successMessage, summary, context, continueLabel, skipLabel, onContinue, onSkip, onQuit, showSkip, }: GateStepProps): React.ReactElement;
|
|
14
|
+
export declare function GateStep({ successMessage, summary, context, continueLabel, skipLabel, onContinue, onSkip, onQuit, showSkip, intent, }: GateStepProps): React.ReactElement;
|
|
14
15
|
export {};
|