@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.
Files changed (38) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/analyze/build-analyze-view-rows.d.ts +23 -0
  3. package/dist/src/analyze/build-analyze-view-rows.js +39 -0
  4. package/dist/src/analyze/command.js +16 -11
  5. package/dist/src/analyze/extract/validate.d.ts +16 -0
  6. package/dist/src/analyze/extract/validate.js +94 -0
  7. package/dist/src/analyze/select/command.d.ts +47 -0
  8. package/dist/src/analyze/select/command.js +238 -14
  9. package/dist/src/analyze/select/tui/App.js +15 -17
  10. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +4 -1
  11. package/dist/src/analyze/select/tui/components/Sidebar.js +42 -6
  12. package/dist/src/analyze/select/types.d.ts +6 -0
  13. package/dist/src/analyze/select/types.js +24 -7
  14. package/dist/src/analyze/select-agent/command.js +43 -3
  15. package/dist/src/analyze/tui/AnalyzeView.d.ts +8 -0
  16. package/dist/src/analyze/tui/AnalyzeView.js +5 -2
  17. package/dist/src/apply/api-client.d.ts +20 -0
  18. package/dist/src/apply/api-client.js +71 -5
  19. package/dist/src/generate/command.js +21 -2
  20. package/dist/src/import/command.js +2 -0
  21. package/dist/src/import/orchestrator.d.ts +22 -0
  22. package/dist/src/import/orchestrator.js +97 -14
  23. package/dist/src/import/tui/WizardApp.d.ts +13 -0
  24. package/dist/src/import/tui/WizardApp.js +224 -52
  25. package/dist/src/import/tui/steps/GateStep.d.ts +2 -1
  26. package/dist/src/import/tui/steps/GateStep.js +4 -2
  27. package/dist/src/import/tui/steps/GenerateReviewStep.js +2 -0
  28. package/dist/src/import/tui/steps/PreviewValidationErrorStep.d.ts +11 -0
  29. package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +14 -0
  30. package/dist/src/import/tui/wizard-422-helpers.d.ts +48 -0
  31. package/dist/src/import/tui/wizard-422-helpers.js +60 -0
  32. package/dist/src/program.d.ts +17 -0
  33. package/dist/src/program.js +27 -4
  34. package/dist/src/session/db.d.ts +13 -0
  35. package/dist/src/session/db.js +50 -0
  36. package/dist/src/types.d.ts +8 -0
  37. package/package.json +2 -2
  38. 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
- const r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
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
- step: 'apply push',
348
- status: 'failed',
422
+ steps.push(buildPushStepResult({
423
+ created,
424
+ updated,
425
+ failed,
349
426
  durationMs,
350
- error: r.stderr,
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 stepStatus = failed > 0 ? 'failed' : 'complete';
356
- updateStep(db, pushStepId, stepStatus === 'complete' ? 'complete' : 'failed', { components: componentsPath });
357
- const statusIcon = failed > 0 ? '⚠' : '✓';
358
- progressWriter(`${pushLabel}${statusIcon} ${created} created, ${updated} updated, ${failed} failed (${(durationMs / 1000).toFixed(1)}s)`);
359
- steps.push({
360
- step: 'apply push',
361
- status: stepStatus,
434
+ const stepResult = buildPushStepResult({
435
+ created,
436
+ updated,
437
+ failed,
362
438
  durationMs,
363
- detail: { created, updated, failed },
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({ step: 'error', errorStep: 'generate tokens', errorMessage: result.stderr.trim() || 'Unknown error' });
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({ step: 'error', errorStep: 'print tokens', errorMessage: r.stderr.trim() || 'Unknown error' });
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({ step: 'error', errorStep: 'analyze extract', errorMessage: r.stderr.trim() || 'Unknown error' });
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({ step: 'review-extraction-gate', extractSessionId, extractedCount });
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({ fn: 'runAnalyzeSelect:enter', sessionId, extractedCount, acceptAll });
330
+ logStep({
331
+ fn: 'runAnalyzeSelect:enter',
332
+ sessionId,
333
+ extractedCount,
334
+ acceptAll,
335
+ });
281
336
  let acceptedCount;
282
- if (acceptAll) {
283
- logStep({ fn: 'runAnalyzeSelect:acceptAll-skip-spawn' });
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
- else {
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
- const r = await runCliInteractive(['analyze', 'select', '--session', sessionId]);
292
- logStep({ fn: 'runAnalyzeSelect:post-spawn', exitCode: r.exitCode });
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
- if (r.exitCode !== 0) {
295
- update({ step: 'error', errorStep: 'analyze select', errorMessage: r.stderr.trim() || 'Unknown error' });
296
- return;
297
- }
298
- // Read accepted count from the review state file (since TUI subprocess inherits stdio)
299
- const artifactsRoot = process.env['EDS_REVIEW_ARTIFACTS_DIR']
300
- ? resolve(process.env['EDS_REVIEW_ARTIFACTS_DIR'])
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
- update({ acceptedCount });
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({ generateProgress: { done: Number(m[1]), total: Number(m[2]), current: m[3].trim() } });
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
- update({ step: 'review-generated-gate', generateSessionId, generatedCount, generateProgress: null });
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({ step: 'error', errorStep: 'generate edit', errorMessage: r.stderr.trim() || 'Unknown error' });
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({ step: 'error', errorStep: 'edit definitions', errorMessage: 'No session available for editing' });
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({ step: 'error', errorStep: 'edit definitions', errorMessage: 'Editor exited with an error' });
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({ spaceId, environmentId, cmaToken, host: resolvedHost });
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({ cmaToken, spaceId, environmentId, host: resolvedHost });
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({ cmaToken, spaceId, environmentId, host: resolvedHost });
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
- update({ step: 'error', errorStep: 'apply preview', errorMessage: e.message, errorAllowCredentialRetry: true });
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({ step: 'error', errorStep: 'apply preview', errorMessage: msg, errorAllowCredentialRetry: true });
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({ cmaToken, spaceId, environmentId, host: resolvedHost });
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: { status: operation?.sys?.status, id: operation?.sys?.id, keys: Object.keys(operation ?? {}) },
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({ pushProgress: `Queued (operation ${operation.sys.id.slice(0, 8)}...)` });
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({ pollTick: { attempt: pollCount, status: op.sys.status, summary: op.summary } });
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({ step: 'error', errorStep: 'apply push', errorMessage: msg, errorAllowCredentialRetry: true });
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({ step: 'error', errorStep: 'print components', errorMessage: r.stderr.trim() || 'Unknown error' });
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({ step: 'token-reuse-gate', tokensPath: existingTokensPath, tokenSourceChanged: sourceChanged });
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} 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 }));
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: `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: () => {
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({ generatedAcceptedCount: accepted, step: 'push-decision-gate' });
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 {};