@contentful/experience-design-system-cli 2.7.5-dev-build-752dc9c.0 → 2.10.1-dev-build-90df010.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 (34) 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 +13 -10
  5. package/dist/src/analyze/extract/validate.d.ts +13 -1
  6. package/dist/src/analyze/extract/validate.js +32 -5
  7. package/dist/src/analyze/select/command.d.ts +37 -0
  8. package/dist/src/analyze/select/command.js +200 -9
  9. package/dist/src/analyze/select/tui/App.js +1 -9
  10. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +1 -1
  11. package/dist/src/analyze/select/tui/components/Sidebar.js +26 -3
  12. package/dist/src/analyze/select-agent/command.js +22 -10
  13. package/dist/src/analyze/tui/AnalyzeView.d.ts +8 -0
  14. package/dist/src/analyze/tui/AnalyzeView.js +5 -2
  15. package/dist/src/apply/api-client.d.ts +20 -0
  16. package/dist/src/apply/api-client.js +71 -5
  17. package/dist/src/generate/command.js +21 -2
  18. package/dist/src/import/orchestrator.d.ts +21 -0
  19. package/dist/src/import/orchestrator.js +95 -14
  20. package/dist/src/import/tui/WizardApp.d.ts +13 -0
  21. package/dist/src/import/tui/WizardApp.js +224 -52
  22. package/dist/src/import/tui/steps/GateStep.d.ts +2 -1
  23. package/dist/src/import/tui/steps/GateStep.js +4 -2
  24. package/dist/src/import/tui/steps/PreviewValidationErrorStep.d.ts +11 -0
  25. package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +14 -0
  26. package/dist/src/import/tui/wizard-422-helpers.d.ts +48 -0
  27. package/dist/src/import/tui/wizard-422-helpers.js +60 -0
  28. package/dist/src/program.d.ts +17 -0
  29. package/dist/src/program.js +27 -4
  30. package/dist/src/session/db.d.ts +13 -0
  31. package/dist/src/session/db.js +50 -0
  32. package/dist/src/types.d.ts +1 -1
  33. package/package.json +2 -2
  34. package/skills/generate-components.md +2 -0
@@ -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 {};
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
4
- export function GateStep({ successMessage, summary, context, continueLabel = 'Continue', skipLabel = 'Approve all and skip', onContinue, onSkip, onQuit, showSkip = true, }) {
4
+ export function GateStep({ successMessage, summary, context, continueLabel = 'Continue', skipLabel = 'Approve all and skip', onContinue, onSkip, onQuit, showSkip = true, intent = 'success', }) {
5
5
  useImmediateInput((input, key) => {
6
6
  if (key.return) {
7
7
  onContinue();
@@ -16,5 +16,7 @@ export function GateStep({ successMessage, summary, context, continueLabel = 'Co
16
16
  return;
17
17
  }
18
18
  });
19
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 ", successMessage] }), summary && _jsx(Text, { dimColor: true, children: summary }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: context }) }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["[Enter] ", continueLabel] }), showSkip && onSkip && _jsxs(Text, { dimColor: true, children: ["[a] ", skipLabel] }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
19
+ const headerColor = intent === 'error' ? 'red' : 'green';
20
+ const headerIcon = intent === 'error' ? '✗' : '✓';
21
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { color: headerColor, children: [headerIcon, " ", successMessage] }), summary && _jsx(Text, { dimColor: true, children: summary }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: context }) }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["[Enter] ", continueLabel] }), showSkip && onSkip && _jsxs(Text, { dimColor: true, children: ["[a] ", skipLabel] }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
20
22
  }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { PreviewValidationError } from '../../../apply/api-client.js';
3
+ type PreviewValidationErrorStepProps = {
4
+ errors: PreviewValidationError[];
5
+ missingNames: string[];
6
+ onEdit: () => void;
7
+ onSkip: () => void;
8
+ onQuit: () => void;
9
+ };
10
+ export declare function PreviewValidationErrorStep({ errors, missingNames, onEdit, onSkip, onQuit, }: PreviewValidationErrorStepProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { GateStep } from './GateStep.js';
3
+ export function PreviewValidationErrorStep({ errors, missingNames, onEdit, onSkip, onQuit, }) {
4
+ const uniqueNames = [...new Set(errors.map((e) => e.componentName))];
5
+ const matchedNames = uniqueNames.filter((n) => !missingNames.includes(n));
6
+ const errorLines = errors.map((e) => ` ${e.componentName}: ${e.message}`).join('\n');
7
+ const missingNote = missingNames.length > 0
8
+ ? `\n\nNote: ${missingNames.length} component name${missingNames.length === 1 ? '' : 's'} from the server (${missingNames.join(', ')}) ${missingNames.length === 1 ? 'does' : 'do'} not match anything in this session — they cannot be edited or skipped from here.`
9
+ : '';
10
+ const skipLabel = matchedNames.length === 0
11
+ ? 'No matching components to skip'
12
+ : `Skip ${matchedNames.length === 1 ? matchedNames[0] : `${matchedNames.length} components`} and retry`;
13
+ return (_jsx(GateStep, { intent: "error", successMessage: "Preview validation failed", summary: errorLines, context: `${uniqueNames.length} component${uniqueNames.length === 1 ? '' : 's'} failed server validation. Edit their definitions in the review TUI, or skip them and retry preview without them.${missingNote}`, continueLabel: "Edit definitions", skipLabel: skipLabel, showSkip: matchedNames.length > 0, onContinue: onEdit, onSkip: onSkip, onQuit: onQuit }));
14
+ }
@@ -0,0 +1,48 @@
1
+ import { ApiError, type PreviewValidationError } from '../../apply/api-client.js';
2
+ import { patchReviewStateWithValidationErrors, rejectComponentsByName } from '../../analyze/select/command.js';
3
+ export type Preview422Outcome = {
4
+ kind: 'validation-error';
5
+ errors: PreviewValidationError[];
6
+ missingNames: string[];
7
+ } | {
8
+ kind: 'unparseable';
9
+ message: string;
10
+ } | {
11
+ kind: 'not-422';
12
+ };
13
+ /**
14
+ * Decide what to do with an `ApiError` raised by `previewImport()`.
15
+ * Returns `not-422` if the caller should fall through to its other branches
16
+ * (404 / 401 / 403 / generic), `unparseable` if the body cannot be parsed
17
+ * into structured component-level errors (caller should route to the generic
18
+ * error step), or `validation-error` with the patched-state side effects
19
+ * already applied.
20
+ *
21
+ * Pure decision logic + a single delegated side effect to
22
+ * `patchReviewStateWithValidationErrors`. Extracted from `WizardApp.runPreview`
23
+ * so the routing is unit-testable without driving the full TUI.
24
+ */
25
+ export declare function handlePreview422(err: ApiError, extractSessionId: string | null, patchFn?: typeof patchReviewStateWithValidationErrors): Promise<Preview422Outcome>;
26
+ /**
27
+ * The WizardState patch the wizard should spread into any update that
28
+ * transitions OUT of the preview-validation-error step (e.g. into
29
+ * preview-gate after a successful retry). Without this, the validation-error
30
+ * arrays linger in state and any future code path that reads them sees stale
31
+ * data from a prior failed attempt.
32
+ */
33
+ export declare function clearedValidationErrorState(): {
34
+ previewValidationErrors: PreviewValidationError[];
35
+ previewValidationMissingNames: string[];
36
+ };
37
+ /**
38
+ * Run the "skip and retry" side effect: dedup the offending component
39
+ * names from a list of errors and call `rejectComponentsByName`. Returns
40
+ * the deduped names so the caller can log them or include them in test
41
+ * assertions. No-op when sessionId is null or errors are empty.
42
+ *
43
+ * The DB update inside `rejectComponentsByName` is what makes the next
44
+ * preview see a manifest with the offenders excluded — see SP-3 retro
45
+ * Bug 1: writing to the JSON state file alone is not enough because
46
+ * `loadCDFComponents` reads from the pipeline DB.
47
+ */
48
+ export declare function applySkipValidationErrors(sessionId: string | null, errors: PreviewValidationError[], rejectFn?: typeof rejectComponentsByName): Promise<string[]>;
@@ -0,0 +1,60 @@
1
+ import { parsePreviewValidationErrors } from '../../apply/api-client.js';
2
+ import { patchReviewStateWithValidationErrors, rejectComponentsByName } from '../../analyze/select/command.js';
3
+ /**
4
+ * Decide what to do with an `ApiError` raised by `previewImport()`.
5
+ * Returns `not-422` if the caller should fall through to its other branches
6
+ * (404 / 401 / 403 / generic), `unparseable` if the body cannot be parsed
7
+ * into structured component-level errors (caller should route to the generic
8
+ * error step), or `validation-error` with the patched-state side effects
9
+ * already applied.
10
+ *
11
+ * Pure decision logic + a single delegated side effect to
12
+ * `patchReviewStateWithValidationErrors`. Extracted from `WizardApp.runPreview`
13
+ * so the routing is unit-testable without driving the full TUI.
14
+ */
15
+ export async function handlePreview422(err, extractSessionId, patchFn = patchReviewStateWithValidationErrors) {
16
+ if (err.status !== 422)
17
+ return { kind: 'not-422' };
18
+ const errors = parsePreviewValidationErrors(err.body);
19
+ if (errors.length === 0)
20
+ return { kind: 'unparseable', message: err.message };
21
+ let missingNames = [];
22
+ if (extractSessionId) {
23
+ const result = await patchFn(extractSessionId, errors);
24
+ missingNames = result.missingNames;
25
+ }
26
+ return { kind: 'validation-error', errors, missingNames };
27
+ }
28
+ /**
29
+ * The WizardState patch the wizard should spread into any update that
30
+ * transitions OUT of the preview-validation-error step (e.g. into
31
+ * preview-gate after a successful retry). Without this, the validation-error
32
+ * arrays linger in state and any future code path that reads them sees stale
33
+ * data from a prior failed attempt.
34
+ */
35
+ export function clearedValidationErrorState() {
36
+ return {
37
+ previewValidationErrors: [],
38
+ previewValidationMissingNames: [],
39
+ };
40
+ }
41
+ /**
42
+ * Run the "skip and retry" side effect: dedup the offending component
43
+ * names from a list of errors and call `rejectComponentsByName`. Returns
44
+ * the deduped names so the caller can log them or include them in test
45
+ * assertions. No-op when sessionId is null or errors are empty.
46
+ *
47
+ * The DB update inside `rejectComponentsByName` is what makes the next
48
+ * preview see a manifest with the offenders excluded — see SP-3 retro
49
+ * Bug 1: writing to the JSON state file alone is not enough because
50
+ * `loadCDFComponents` reads from the pipeline DB.
51
+ */
52
+ export async function applySkipValidationErrors(sessionId, errors, rejectFn = rejectComponentsByName) {
53
+ if (!sessionId)
54
+ return [];
55
+ const names = [...new Set(errors.map((e) => e.componentName))];
56
+ if (names.length === 0)
57
+ return [];
58
+ await rejectFn(sessionId, names);
59
+ return names;
60
+ }