@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.
- 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 +13 -10
- package/dist/src/analyze/extract/validate.d.ts +13 -1
- package/dist/src/analyze/extract/validate.js +32 -5
- package/dist/src/analyze/select/command.d.ts +37 -0
- package/dist/src/analyze/select/command.js +200 -9
- package/dist/src/analyze/select/tui/App.js +1 -9
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +1 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +26 -3
- package/dist/src/analyze/select-agent/command.js +22 -10
- 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/orchestrator.d.ts +21 -0
- package/dist/src/import/orchestrator.js +95 -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/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 +1 -1
- package/package.json +2 -2
- 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({
|
|
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 {};
|
|
@@ -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
|
-
|
|
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
|
+
}
|