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