@contentful/experience-design-system-cli 2.23.2-dev-build-753ceef.0 → 2.23.2-dev-build-e996ffb.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 (69) hide show
  1. package/README.md +2 -24
  2. package/dist/package.json +1 -2
  3. package/dist/src/analyze/command.js +2 -8
  4. package/dist/src/analyze/select/command.js +9 -13
  5. package/dist/src/analyze/select/tui/App.js +4 -1
  6. package/dist/src/analyze/select-agent/command.js +11 -14
  7. package/dist/src/apply/api-client.d.ts +5 -5
  8. package/dist/src/apply/api-client.js +8 -18
  9. package/dist/src/apply/command.d.ts +1 -1
  10. package/dist/src/apply/command.js +77 -81
  11. package/dist/src/apply/preview-utils.d.ts +5 -1
  12. package/dist/src/apply/preview-utils.js +5 -1
  13. package/dist/src/apply/tui/ServerApplyView.d.ts +6 -3
  14. package/dist/src/apply/tui/ServerApplyView.js +15 -6
  15. package/dist/src/apply/tui/ServerPreviewView.d.ts +2 -1
  16. package/dist/src/apply/tui/ServerPreviewView.js +3 -3
  17. package/dist/src/generate/command.js +9 -15
  18. package/dist/src/generate/edit/command.js +0 -1
  19. package/dist/src/import/command.js +6 -1
  20. package/dist/src/import/orchestrator.d.ts +1 -0
  21. package/dist/src/import/orchestrator.js +14 -18
  22. package/dist/src/import/tui/WizardApp.d.ts +2 -1
  23. package/dist/src/import/tui/WizardApp.js +8 -8
  24. package/dist/src/import/tui/final-review-host.d.ts +2 -1
  25. package/dist/src/import/tui/final-review-host.js +2 -2
  26. package/dist/src/import/tui/runLivePreview.d.ts +3 -0
  27. package/dist/src/import/tui/runLivePreview.js +4 -1
  28. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +2 -1
  29. package/dist/src/import/tui/steps/GenerateReviewStep.js +3 -3
  30. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -2
  31. package/dist/src/import/tui/steps/WizardPreviewStep.js +12 -3
  32. package/dist/src/import/tui/useFinalizePreview.d.ts +2 -0
  33. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  34. package/dist/src/import/tui/useLivePreview.d.ts +2 -0
  35. package/dist/src/import/tui/useLivePreview.js +1 -0
  36. package/dist/src/index.js +1 -6
  37. package/dist/src/lib/command-options.d.ts +1 -0
  38. package/dist/src/lib/command-options.js +3 -0
  39. package/dist/src/print/command.js +13 -16
  40. package/dist/src/program.js +1 -8
  41. package/dist/src/runs/modify-launcher.d.ts +2 -0
  42. package/dist/src/runs/modify-launcher.js +2 -0
  43. package/dist/src/runs/push-helpers.d.ts +1 -0
  44. package/dist/src/runs/push-helpers.js +3 -0
  45. package/dist/src/runs/replay-helpers.d.ts +4 -0
  46. package/dist/src/runs/replay-helpers.js +3 -0
  47. package/package.json +5 -6
  48. package/dist/src/analytics/apply.d.ts +0 -6
  49. package/dist/src/analytics/apply.js +0 -30
  50. package/dist/src/analytics/client.d.ts +0 -7
  51. package/dist/src/analytics/client.js +0 -68
  52. package/dist/src/analytics/constants.d.ts +0 -4
  53. package/dist/src/analytics/constants.js +0 -4
  54. package/dist/src/analytics/env.d.ts +0 -4
  55. package/dist/src/analytics/env.js +0 -14
  56. package/dist/src/analytics/exit.d.ts +0 -5
  57. package/dist/src/analytics/exit.js +0 -24
  58. package/dist/src/analytics/index.d.ts +0 -10
  59. package/dist/src/analytics/index.js +0 -9
  60. package/dist/src/analytics/normalize.d.ts +0 -3
  61. package/dist/src/analytics/normalize.js +0 -18
  62. package/dist/src/analytics/os.d.ts +0 -2
  63. package/dist/src/analytics/os.js +0 -13
  64. package/dist/src/analytics/session.d.ts +0 -3
  65. package/dist/src/analytics/session.js +0 -15
  66. package/dist/src/analytics/tracker.d.ts +0 -17
  67. package/dist/src/analytics/tracker.js +0 -126
  68. package/dist/src/analytics/types.d.ts +0 -28
  69. package/dist/src/analytics/types.js +0 -1
@@ -12,14 +12,13 @@ import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerAppl
12
12
  import { SelectView, makeSelectKey } from './tui/SelectView.js';
13
13
  import { buildPostPushUrl } from '../lib/contentful-urls.js';
14
14
  import { resolveCompositionMode } from '../lib/composition-mode.js';
15
- import { addArtifactInputOptions, addCompositionOptions, addContentfulTargetOptions, addSelectionOptions, } from '../lib/command-options.js';
15
+ import { addAllowDeletionsOption, addArtifactInputOptions, addCompositionOptions, addContentfulTargetOptions, addSelectionOptions, } from '../lib/command-options.js';
16
16
  import { stripAllowedComponents } from '../import/strip-allowed-components.js';
17
17
  import { readExperiencesCredentials } from '../credentials-store.js';
18
18
  import { getInteractiveTerminalSupport, requireInteractiveTerminal } from '../lib/terminal-capabilities.js';
19
- import { bindAnalyticsSessionId, exitWithAnalytics, failureFromApiError, recordApplyOutcome, recordContentfulContext, } from '../analytics/index.js';
20
- async function die(message, fields = {}) {
19
+ function die(message) {
21
20
  process.stderr.write(`${message}\n`);
22
- return exitWithAnalytics(1, fields);
21
+ process.exit(1);
23
22
  }
24
23
  async function pathExists(p) {
25
24
  return access(p)
@@ -28,7 +27,7 @@ async function pathExists(p) {
28
27
  }
29
28
  async function assertFileExists(flag, p) {
30
29
  if (!(await pathExists(p)))
31
- return await die(`Error: file not found: ${p} (from ${flag})`);
30
+ die(`Error: file not found: ${p} (from ${flag})`);
32
31
  }
33
32
  async function readJsonFile(flag, p) {
34
33
  let text;
@@ -36,13 +35,13 @@ async function readJsonFile(flag, p) {
36
35
  text = await readFile(p, 'utf8');
37
36
  }
38
37
  catch {
39
- return await die(`Error: file not found: ${p} (from ${flag})`);
38
+ die(`Error: file not found: ${p} (from ${flag})`);
40
39
  }
41
40
  try {
42
41
  return JSON.parse(text);
43
42
  }
44
43
  catch {
45
- return await die(`Error: ${flag} is not valid JSON: ${p}`);
44
+ die(`Error: ${flag} is not valid JSON: ${p}`);
46
45
  }
47
46
  }
48
47
  const IGNORE_TOKEN_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.nuxt', '.git']);
@@ -84,12 +83,12 @@ export async function readTokensFromPath(flag, p) {
84
83
  s = await stat(p);
85
84
  }
86
85
  catch {
87
- return await die(`Error: file not found: ${p} (from ${flag})`);
86
+ die(`Error: file not found: ${p} (from ${flag})`);
88
87
  }
89
88
  if (s.isDirectory()) {
90
89
  const files = await collectJsonFiles(p);
91
90
  if (files.length === 0)
92
- return await die(`Error: no .json files found in directory: ${p} (from ${flag})`);
91
+ die(`Error: no .json files found in directory: ${p} (from ${flag})`);
93
92
  const merged = {};
94
93
  for (const file of files.sort()) {
95
94
  let text;
@@ -112,36 +111,36 @@ export async function readTokensFromPath(flag, p) {
112
111
  }
113
112
  const { valid, errors } = validateDTCG(merged);
114
113
  if (!valid)
115
- return await die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
114
+ die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
116
115
  return flattenDTCG(merged, '');
117
116
  }
118
117
  const raw = await readJsonFile(flag, p);
119
118
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
120
- return await die(`Error: ${flag} is not valid JSON: expected an object`);
119
+ die(`Error: ${flag} is not valid JSON: expected an object`);
121
120
  }
122
121
  const { valid, errors } = validateDTCG(raw);
123
122
  if (!valid)
124
- return await die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
123
+ die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
125
124
  return flattenDTCG(raw, '');
126
125
  }
127
126
  async function resolveSharedInputs(opts) {
128
127
  if (!opts.components && !opts.tokens && !opts.session) {
129
- return await die('Error: at least one of --components, --tokens, or --session is required');
128
+ die('Error: at least one of --components, --tokens, or --session is required');
130
129
  }
131
130
  if (opts.session && opts.components) {
132
- return await die('Error: --session and --components are mutually exclusive');
131
+ die('Error: --session and --components are mutually exclusive');
133
132
  }
134
133
  const spaceId = opts.spaceId ?? process.env.CONTENTFUL_SPACE_ID;
135
134
  const environmentId = opts.environmentId ?? process.env.CONTENTFUL_ENVIRONMENT_ID;
136
135
  if (!spaceId)
137
- return await die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
136
+ die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
138
137
  if (!environmentId)
139
- return await die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
138
+ die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
140
139
  opts.spaceId = spaceId;
141
140
  opts.environmentId = environmentId;
142
141
  const cmaToken = opts.cmaToken ?? process.env.CONTENTFUL_MANAGEMENT_TOKEN;
143
142
  if (!cmaToken) {
144
- return await die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
143
+ die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
145
144
  }
146
145
  if (opts.components)
147
146
  await assertFileExists('--components', opts.components);
@@ -155,14 +154,14 @@ async function resolveSharedInputs(opts) {
155
154
  db.close();
156
155
  }
157
156
  if (components.length === 0) {
158
- return await die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
157
+ die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
159
158
  }
160
159
  }
161
160
  else if (opts.components) {
162
161
  const raw = await readJsonFile('--components', opts.components);
163
162
  const result = validateCDF(raw);
164
163
  if (!result.valid) {
165
- return await die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
164
+ die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
166
165
  }
167
166
  components = result.components;
168
167
  }
@@ -214,12 +213,12 @@ export function formatSlotCycleReport(cycles) {
214
213
  }
215
214
  return lines;
216
215
  }
217
- export async function assertNoSlotCycles(components) {
216
+ export function assertNoSlotCycles(components) {
218
217
  const cycles = detectSlotCycles(components);
219
218
  if (cycles.length === 0)
220
219
  return;
221
220
  process.stderr.write(formatSlotCycleReport(cycles).join('\n') + '\n');
222
- await exitWithAnalytics(1);
221
+ process.exit(1);
223
222
  }
224
223
  export function extractComponentsFromManifest(manifest) {
225
224
  const componentsManifest = manifest?.componentsManifest;
@@ -428,24 +427,18 @@ export function registerApplyCommand(program) {
428
427
  }
429
428
  catch (e) {
430
429
  if (e instanceof ApiError)
431
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
430
+ die(`Error: ${formatApiError(e)}`);
432
431
  throw e;
433
432
  }
434
433
  const { components, tokens, client } = inputs;
435
- const spaceId = opts.spaceId;
436
- const environmentId = opts.environmentId;
437
- await bindAnalyticsSessionId(opts.session, {
438
- space_key: spaceId,
439
- environment_key: environmentId,
440
- });
441
434
  try {
442
435
  await client.validateToken();
443
436
  }
444
437
  catch (e) {
445
438
  if (e instanceof ApiError)
446
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
439
+ die(`Error: ${formatApiError(e)}`);
447
440
  const cause = e instanceof Error && e.cause instanceof Error ? e.cause.message : '';
448
- return await die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
441
+ die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
449
442
  }
450
443
  const manifest = buildManifest(components, tokens);
451
444
  let preview;
@@ -454,27 +447,30 @@ export function registerApplyCommand(program) {
454
447
  }
455
448
  catch (e) {
456
449
  if (e instanceof ApiError)
457
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
450
+ die(`Error: ${formatApiError(e)}`);
458
451
  throw e;
459
452
  }
460
- recordContentfulContext(client, spaceId, environmentId);
453
+ const spaceId = opts.spaceId;
454
+ const environmentId = opts.environmentId;
461
455
  if (getInteractiveTerminalSupport().supported) {
462
456
  const { waitUntilExit } = render(createElement(ServerPreviewApp, {
463
457
  preview,
464
458
  spaceId,
465
459
  environmentId,
460
+ allowDeletions: false,
466
461
  }));
467
462
  await waitUntilExit();
468
463
  }
469
464
  else {
470
465
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
471
- await exitWithAnalytics(0);
466
+ process.exit(0);
472
467
  }
473
468
  });
474
469
  const pushCmd = applyCmd.command('push').description('Write component types and design tokens to Contentful ExO');
475
470
  addArtifactInputOptions(pushCmd);
476
471
  addContentfulTargetOptions(pushCmd);
477
472
  addCompositionOptions(pushCmd);
473
+ addAllowDeletionsOption(pushCmd);
478
474
  pushCmd
479
475
  .option('--yes', 'Skip interactive confirmation')
480
476
  .option('--verbose', 'Show all entity progress including skipped/unchanged')
@@ -484,7 +480,7 @@ export function registerApplyCommand(program) {
484
480
  const isTTY = getInteractiveTerminalSupport().supported;
485
481
  if (!isTTY && !opts.yes) {
486
482
  process.stderr.write('Error: apply push requires --yes in non-interactive mode\n');
487
- await exitWithAnalytics(1);
483
+ process.exit(1);
488
484
  }
489
485
  let inputs;
490
486
  try {
@@ -492,49 +488,45 @@ export function registerApplyCommand(program) {
492
488
  }
493
489
  catch (e) {
494
490
  if (e instanceof ApiError)
495
- return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
491
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
496
492
  throw e;
497
493
  }
498
494
  const { components, tokens, client } = inputs;
499
- const spaceId = opts.spaceId;
500
- const environmentId = opts.environmentId;
501
- await bindAnalyticsSessionId(opts.session, {
502
- space_key: spaceId,
503
- environment_key: environmentId,
504
- });
505
- await assertNoSlotCycles(components);
495
+ assertNoSlotCycles(components);
506
496
  try {
507
497
  await client.validateToken();
508
498
  }
509
499
  catch (e) {
510
500
  if (e instanceof ApiError)
511
- return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
501
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
512
502
  throw e;
513
503
  }
514
504
  const manifest = buildManifest(components, tokens);
515
505
  let preview;
516
506
  try {
517
- preview = await client.previewImport(manifest);
507
+ preview = await client.previewImport(manifest, opts.allowDeletions === true);
518
508
  }
519
509
  catch (e) {
520
510
  if (e instanceof ApiError)
521
- return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
511
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
522
512
  throw e;
523
513
  }
524
- recordContentfulContext(client, spaceId, environmentId);
514
+ const spaceId = opts.spaceId;
515
+ const environmentId = opts.environmentId;
525
516
  if (opts.dryRun) {
526
517
  if (isTTY) {
527
518
  const { waitUntilExit } = render(createElement(ServerPreviewApp, {
528
519
  preview,
529
520
  spaceId,
530
521
  environmentId,
522
+ allowDeletions: opts.allowDeletions === true,
531
523
  }));
532
524
  await waitUntilExit();
533
525
  }
534
526
  else {
535
527
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
536
528
  }
537
- await exitWithAnalytics(0);
529
+ process.exit(0);
538
530
  }
539
531
  if (isEmptyPreview(preview)) {
540
532
  if (isTTY && !opts.yes) {
@@ -543,14 +535,14 @@ export function registerApplyCommand(program) {
543
535
  else {
544
536
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
545
537
  }
546
- await exitWithAnalytics(0);
538
+ process.exit(0);
547
539
  }
548
540
  const breakingWithImpact = hasBreakingChangesWithImpact(preview);
549
541
  if (!isTTY || opts.yes) {
550
542
  if (breakingWithImpact && !opts.force) {
551
543
  process.stderr.write('Error: breaking changes with downstream impact detected. Use --force to acknowledge.\n');
552
544
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
553
- await exitWithAnalytics(1);
545
+ process.exit(1);
554
546
  }
555
547
  const verbose = opts.verbose ?? false;
556
548
  if (verbose) {
@@ -558,11 +550,14 @@ export function registerApplyCommand(program) {
558
550
  }
559
551
  let operation;
560
552
  try {
561
- operation = await client.applyImport(manifest, breakingWithImpact || opts.force === true);
553
+ operation = await client.applyImport(manifest, {
554
+ acknowledgeBreakingChanges: breakingWithImpact || opts.force === true,
555
+ allowDeletions: opts.allowDeletions === true,
556
+ });
562
557
  }
563
558
  catch (e) {
564
559
  if (e instanceof ApiError)
565
- return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
560
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
566
561
  throw e;
567
562
  }
568
563
  process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
@@ -571,18 +566,16 @@ export function registerApplyCommand(program) {
571
566
  }
572
567
  catch (e) {
573
568
  if (e instanceof ApiError)
574
- return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
569
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
575
570
  throw e;
576
571
  }
577
572
  const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
578
- recordApplyOutcome(client, spaceId, environmentId, operation);
579
573
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
580
- const exitCode = operation.sys.status === 'succeeded' ? 0 : 1;
581
- await exitWithAnalytics(exitCode);
574
+ process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
582
575
  return;
583
576
  }
584
577
  await new Promise((resolvePromise) => {
585
- const runApply = async (acknowledge) => {
578
+ const runApply = async (acknowledge, applyDeletions) => {
586
579
  instance.rerender(createElement(ServerApplyProgress, {
587
580
  spaceId,
588
581
  environmentId,
@@ -590,7 +583,10 @@ export function registerApplyCommand(program) {
590
583
  }));
591
584
  let operation;
592
585
  try {
593
- operation = await client.applyImport(manifest, acknowledge);
586
+ operation = await client.applyImport(manifest, {
587
+ acknowledgeBreakingChanges: acknowledge,
588
+ allowDeletions: applyDeletions,
589
+ });
594
590
  }
595
591
  catch (e) {
596
592
  if (e instanceof ApiError) {
@@ -625,7 +621,6 @@ export function registerApplyCommand(program) {
625
621
  }
626
622
  throw e;
627
623
  }
628
- recordApplyOutcome(client, spaceId, environmentId, operation);
629
624
  instance.rerender(createElement(ServerApplyDone, {
630
625
  operation,
631
626
  spaceId,
@@ -639,11 +634,12 @@ export function registerApplyCommand(program) {
639
634
  spaceId,
640
635
  environmentId,
641
636
  breakingWithImpact,
642
- onConfirm: (acknowledge) => {
643
- void runApply(acknowledge);
637
+ allowDeletions: opts.allowDeletions === true,
638
+ onConfirm: (acknowledge, applyDeletions) => {
639
+ void runApply(acknowledge, applyDeletions);
644
640
  },
645
641
  onCancel: () => {
646
- void exitWithAnalytics(0);
642
+ process.exit(0);
647
643
  },
648
644
  }));
649
645
  void instance.waitUntilExit().then(() => resolvePromise());
@@ -654,6 +650,7 @@ export function registerApplyCommand(program) {
654
650
  addContentfulTargetOptions(selectCmd);
655
651
  addCompositionOptions(selectCmd);
656
652
  addSelectionOptions(selectCmd);
653
+ addAllowDeletionsOption(selectCmd);
657
654
  selectCmd.option('--force', 'Skip confirmation for breaking changes').action(async (opts) => {
658
655
  const nonInteractive = opts.selectAll || (opts.select ?? []).length > 0 || (opts.deselect ?? []).length > 0;
659
656
  if (!nonInteractive) {
@@ -667,46 +664,41 @@ export function registerApplyCommand(program) {
667
664
  }
668
665
  catch (e) {
669
666
  if (e instanceof ApiError)
670
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
667
+ die(`Error: ${formatApiError(e)}`);
671
668
  throw e;
672
669
  }
673
670
  const { components, tokens, client } = inputs;
674
- await assertNoSlotCycles(components);
671
+ assertNoSlotCycles(components);
675
672
  try {
676
673
  await client.validateToken();
677
674
  }
678
675
  catch (e) {
679
676
  if (e instanceof ApiError)
680
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
677
+ die(`Error: ${formatApiError(e)}`);
681
678
  throw e;
682
679
  }
683
680
  const fullManifest = buildManifest(components, tokens);
684
681
  let preview;
685
682
  try {
686
- preview = await client.previewImport(fullManifest);
683
+ preview = await client.previewImport(fullManifest, opts.allowDeletions === true);
687
684
  }
688
685
  catch (e) {
689
686
  if (e instanceof ApiError)
690
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
687
+ die(`Error: ${formatApiError(e)}`);
691
688
  throw e;
692
689
  }
693
690
  const spaceId = opts.spaceId;
694
691
  const environmentId = opts.environmentId;
695
- await bindAnalyticsSessionId(opts.session, {
696
- space_key: spaceId,
697
- environment_key: environmentId,
698
- });
699
- recordContentfulContext(client, spaceId, environmentId);
700
692
  const entities = getSelectableEntities(preview);
701
693
  if (entities.length === 0) {
702
694
  process.stderr.write('Nothing to change — design system is up to date.\n');
703
- await exitWithAnalytics(0);
695
+ process.exit(0);
704
696
  }
705
697
  if (nonInteractive) {
706
698
  const selectedKeys = resolveNonInteractiveSelection(entities, opts);
707
699
  if (selectedKeys.size === 0) {
708
700
  process.stderr.write('No entities matched selection criteria.\n');
709
- await exitWithAnalytics(0);
701
+ process.exit(0);
710
702
  }
711
703
  const selectedComponentKeys = new Set();
712
704
  const selectedTokenPaths = new Set();
@@ -722,15 +714,18 @@ export function registerApplyCommand(program) {
722
714
  const hasBreaking = entities.some((e) => e.isBreaking && selectedKeys.has(makeSelectKey(e.kind, e.id)));
723
715
  if (hasBreaking && !opts.force) {
724
716
  process.stderr.write('Error: selection includes breaking changes. Use --force to acknowledge.\n');
725
- await exitWithAnalytics(1);
717
+ process.exit(1);
726
718
  }
727
719
  let operation;
728
720
  try {
729
- operation = await client.applyImport(filteredManifest, hasBreaking || opts.force === true);
721
+ operation = await client.applyImport(filteredManifest, {
722
+ acknowledgeBreakingChanges: hasBreaking || opts.force === true,
723
+ allowDeletions: opts.allowDeletions === true,
724
+ });
730
725
  }
731
726
  catch (e) {
732
727
  if (e instanceof ApiError)
733
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
728
+ die(`Error: ${formatApiError(e)}`);
734
729
  throw e;
735
730
  }
736
731
  process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
@@ -739,13 +734,12 @@ export function registerApplyCommand(program) {
739
734
  }
740
735
  catch (e) {
741
736
  if (e instanceof ApiError)
742
- return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
737
+ die(`Error: ${formatApiError(e)}`);
743
738
  throw e;
744
739
  }
745
740
  const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
746
- recordApplyOutcome(client, spaceId, environmentId, operation);
747
741
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
748
- await exitWithAnalytics(operation.sys.status === 'succeeded' ? 0 : 1);
742
+ process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
749
743
  return;
750
744
  }
751
745
  await new Promise((resolvePromise) => {
@@ -769,7 +763,10 @@ export function registerApplyCommand(program) {
769
763
  }));
770
764
  let operation;
771
765
  try {
772
- operation = await client.applyImport(filteredManifest, hasBreaking);
766
+ operation = await client.applyImport(filteredManifest, {
767
+ acknowledgeBreakingChanges: hasBreaking,
768
+ allowDeletions: opts.allowDeletions === true,
769
+ });
773
770
  }
774
771
  catch (e) {
775
772
  if (e instanceof ApiError) {
@@ -804,7 +801,6 @@ export function registerApplyCommand(program) {
804
801
  }
805
802
  throw e;
806
803
  }
807
- recordApplyOutcome(client, spaceId, environmentId, operation);
808
804
  instance.rerender(createElement(ServerApplyDone, {
809
805
  operation,
810
806
  spaceId,
@@ -7,6 +7,10 @@ import type { ServerPreviewResponse } from '@contentful/experience-design-system
7
7
  * would be a pure no-op (INTEG-4411 refined guard).
8
8
  *
9
9
  * A push that produces ANY entry in ANY bucket — including a rejection that
10
- * removes a server-side component — is NOT empty.
10
+ * removes a server-side component — is NOT empty. Note the server hides
11
+ * removals entirely (empty removed[]) unless the preview was fetched with
12
+ * allowDeletions: true, so "empty" here also correctly describes a preview
13
+ * with pending-but-hidden deletions: pushing without --allow-deletions is a
14
+ * genuine no-op in that case too, since the apply gate hides them the same way.
11
15
  */
12
16
  export declare function isEmptyPreview(preview: ServerPreviewResponse): boolean;
@@ -6,7 +6,11 @@
6
6
  * would be a pure no-op (INTEG-4411 refined guard).
7
7
  *
8
8
  * A push that produces ANY entry in ANY bucket — including a rejection that
9
- * removes a server-side component — is NOT empty.
9
+ * removes a server-side component — is NOT empty. Note the server hides
10
+ * removals entirely (empty removed[]) unless the preview was fetched with
11
+ * allowDeletions: true, so "empty" here also correctly describes a preview
12
+ * with pending-but-hidden deletions: pushing without --allow-deletions is a
13
+ * genuine no-op in that case too, since the apply gate hides them the same way.
10
14
  */
11
15
  export function isEmptyPreview(preview) {
12
16
  const { components, tokens, taxonomies } = preview;
@@ -5,16 +5,19 @@ interface ServerPreviewConfirmProps {
5
5
  spaceId: string;
6
6
  environmentId: string;
7
7
  breakingWithImpact: boolean;
8
- onConfirm: (acknowledge: boolean) => void;
8
+ /** The value the preview was actually fetched with. */
9
+ allowDeletions: boolean;
10
+ onConfirm: (acknowledge: boolean, allowDeletions: boolean) => void;
9
11
  onCancel: () => void;
10
12
  }
11
- export declare function ServerPreviewConfirm({ preview, spaceId, environmentId, breakingWithImpact, onConfirm, onCancel, }: ServerPreviewConfirmProps): React.ReactElement;
13
+ export declare function ServerPreviewConfirm({ preview, spaceId, environmentId, breakingWithImpact, allowDeletions: fetchedAllowDeletions, onConfirm, onCancel, }: ServerPreviewConfirmProps): React.ReactElement;
12
14
  interface ServerPreviewAppProps {
13
15
  preview: ServerPreviewResponse;
14
16
  spaceId: string;
15
17
  environmentId: string;
18
+ allowDeletions: boolean;
16
19
  }
17
- export declare function ServerPreviewApp({ preview, spaceId, environmentId }: ServerPreviewAppProps): React.ReactElement;
20
+ export declare function ServerPreviewApp({ preview, spaceId, environmentId, allowDeletions, }: ServerPreviewAppProps): React.ReactElement;
18
21
  interface ServerApplyProgressProps {
19
22
  spaceId: string;
20
23
  environmentId: string;
@@ -1,23 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import { Box, Text, useInput } from 'ink';
3
4
  import { ServerPreviewView } from './ServerPreviewView.js';
4
5
  import { buildPostPushUrl } from '../../lib/contentful-urls.js';
5
6
  import { formatEdsiError } from '../../lib/error-parser.js';
6
- export function ServerPreviewConfirm({ preview, spaceId, environmentId, breakingWithImpact, onConfirm, onCancel, }) {
7
+ export function ServerPreviewConfirm({ preview, spaceId, environmentId, breakingWithImpact, allowDeletions: fetchedAllowDeletions, onConfirm, onCancel, }) {
8
+ const [allowDeletions, setAllowDeletions] = useState(fetchedAllowDeletions);
9
+ const removedCount = preview.components.removed.length + preview.tokens.removed.length;
7
10
  useInput((input, key) => {
8
- if (key.return)
9
- onConfirm(breakingWithImpact);
11
+ if (key.return) {
12
+ onConfirm(breakingWithImpact, allowDeletions);
13
+ return;
14
+ }
15
+ if ((input === 'x' || input === 'X') && fetchedAllowDeletions && removedCount > 0) {
16
+ setAllowDeletions((prev) => !prev);
17
+ return;
18
+ }
10
19
  if (key.escape || input === 'q')
11
20
  onCancel();
12
21
  });
13
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ServerPreviewView, { preview: preview, spaceId: spaceId, environmentId: environmentId }), _jsxs(Box, { paddingX: 2, flexDirection: "column", children: [breakingWithImpact && (_jsxs(Text, { color: "red", bold: true, children: [' ', "\u26A0 Breaking changes will affect downstream entities. Press Enter to acknowledge and apply."] })), _jsxs(Text, { children: [' ', "Press ", _jsx(Text, { bold: true, children: "Enter" }), " to apply, ", _jsx(Text, { bold: true, children: "Esc" }), " to cancel"] })] })] }));
22
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ServerPreviewView, { preview: preview, spaceId: spaceId, environmentId: environmentId, allowDeletions: allowDeletions }), _jsxs(Box, { paddingX: 2, flexDirection: "column", children: [breakingWithImpact && (_jsxs(Text, { color: "red", bold: true, children: [' ', "\u26A0 Breaking changes will affect downstream entities. Press Enter to acknowledge and apply."] })), allowDeletions && removedCount > 0 && (_jsxs(Text, { color: "red", bold: true, children: [' ', "\u26A0 ", removedCount, " missing ", removedCount === 1 ? 'entity' : 'entities', " will be permanently deleted. Press Enter to confirm."] })), fetchedAllowDeletions && removedCount > 0 && (_jsxs(Text, { dimColor: true, children: [" [x] ", allowDeletions ? '[✓]' : '[ ]', " Allow deletions"] })), _jsxs(Text, { children: [' ', "Press ", _jsx(Text, { bold: true, children: "Enter" }), " to apply, ", _jsx(Text, { bold: true, children: "Esc" }), " to cancel"] })] })] }));
14
23
  }
15
- export function ServerPreviewApp({ preview, spaceId, environmentId }) {
24
+ export function ServerPreviewApp({ preview, spaceId, environmentId, allowDeletions, }) {
16
25
  useInput((input, key) => {
17
26
  if (key.escape || input === 'q')
18
27
  process.exit(0);
19
28
  });
20
- return _jsx(ServerPreviewView, { preview: preview, spaceId: spaceId, environmentId: environmentId });
29
+ return (_jsx(ServerPreviewView, { preview: preview, spaceId: spaceId, environmentId: environmentId, allowDeletions: allowDeletions }));
21
30
  }
22
31
  export function ServerApplyProgress({ spaceId, environmentId, status, operationId, error, }) {
23
32
  useInput((_input, key) => {
@@ -4,6 +4,7 @@ interface ServerPreviewViewProps {
4
4
  preview: ServerPreviewResponse;
5
5
  spaceId: string;
6
6
  environmentId: string;
7
+ allowDeletions: boolean;
7
8
  }
8
- export declare function ServerPreviewView({ preview, spaceId, environmentId }: ServerPreviewViewProps): React.ReactElement;
9
+ export declare function ServerPreviewView({ preview, spaceId, environmentId, allowDeletions, }: ServerPreviewViewProps): React.ReactElement;
9
10
  export {};
@@ -13,11 +13,11 @@ function DraftWarning({ hasDraft }) {
13
13
  return null;
14
14
  return _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" });
15
15
  }
16
- export function ServerPreviewView({ preview, spaceId, environmentId }) {
16
+ export function ServerPreviewView({ preview, spaceId, environmentId, allowDeletions, }) {
17
17
  const { components, tokens } = preview;
18
18
  const totalComponents = components.new.length + components.changed.length + components.unchanged.length + components.removed.length;
19
19
  const totalTokens = tokens.new.length + tokens.changed.length + tokens.unchanged.length + tokens.removed.length;
20
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Preview \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), totalComponents > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Component Types (", totalComponents, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2746 ", components.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", components.changed.length, " to update"] }), _jsxs(Text, { color: "red", children: [" \u2717 ", components.removed.length, " to remove"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", components.unchanged.length, " unchanged"] }), components.changed.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: item.hasPendingDraftChanges })] }), _jsx(BreakingBadge, { item: item })] }, i))), _jsx(Text, { children: " " })] })), totalTokens > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Design Tokens (", totalTokens, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2746 ", tokens.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", tokens.changed.length, " to update"] }), _jsxs(Text, { color: "red", children: [" \u2717 ", tokens.removed.length, " to remove"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", tokens.unchanged.length, " unchanged"] }), tokens.changed
20
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Preview \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), totalComponents > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Component Types (", totalComponents, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2746 ", components.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", components.changed.length, " to update"] }), _jsxs(Text, { color: allowDeletions ? 'red' : 'yellow', children: [' ', allowDeletions ? '✗' : '⊘', " ", components.removed.length, " to ", allowDeletions ? 'delete' : 'skip'] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", components.unchanged.length, " unchanged"] }), components.changed.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: item.hasPendingDraftChanges })] }), _jsx(BreakingBadge, { item: item })] }, i))), _jsx(Text, { children: " " })] })), totalTokens > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Design Tokens (", totalTokens, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2746 ", tokens.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", tokens.changed.length, " to update"] }), _jsxs(Text, { color: allowDeletions ? 'red' : 'yellow', children: [' ', allowDeletions ? '✗' : '⊘', " ", tokens.removed.length, " to ", allowDeletions ? 'delete' : 'skip'] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", tokens.unchanged.length, " unchanged"] }), tokens.changed
21
21
  .filter((t) => t.hasPendingDraftChanges)
22
- .map((item, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: true })] }, i))), _jsx(Text, { children: " " })] })), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
22
+ .map((item, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: true })] }, i))), _jsx(Text, { children: " " })] })), !allowDeletions && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: " \u2298 Deletions are hidden by default \u2014 rerun with --allow-deletions to see them." }) })), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
23
23
  }