@contentful/experience-design-system-cli 2.23.2-dev-build-8b2cfc9.0 → 2.23.2-dev-build-09d87bb.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 (55) hide show
  1. package/README.md +2 -24
  2. package/dist/package.json +1 -2
  3. package/dist/src/analyze/command.js +0 -4
  4. package/dist/src/analyze/select/tui/App.js +4 -1
  5. package/dist/src/apply/api-client.d.ts +5 -5
  6. package/dist/src/apply/api-client.js +8 -18
  7. package/dist/src/apply/command.js +32 -32
  8. package/dist/src/apply/preview-utils.d.ts +5 -1
  9. package/dist/src/apply/preview-utils.js +5 -1
  10. package/dist/src/apply/tui/ServerApplyView.d.ts +6 -3
  11. package/dist/src/apply/tui/ServerApplyView.js +15 -6
  12. package/dist/src/apply/tui/ServerPreviewView.d.ts +2 -1
  13. package/dist/src/apply/tui/ServerPreviewView.js +3 -3
  14. package/dist/src/import/command.js +6 -1
  15. package/dist/src/import/orchestrator.d.ts +1 -0
  16. package/dist/src/import/orchestrator.js +3 -6
  17. package/dist/src/import/tui/WizardApp.d.ts +2 -1
  18. package/dist/src/import/tui/WizardApp.js +8 -8
  19. package/dist/src/import/tui/final-review-host.d.ts +2 -1
  20. package/dist/src/import/tui/final-review-host.js +2 -2
  21. package/dist/src/import/tui/runLivePreview.d.ts +3 -0
  22. package/dist/src/import/tui/runLivePreview.js +4 -1
  23. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +2 -1
  24. package/dist/src/import/tui/steps/GenerateReviewStep.js +3 -3
  25. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -2
  26. package/dist/src/import/tui/steps/WizardPreviewStep.js +12 -3
  27. package/dist/src/import/tui/useFinalizePreview.d.ts +2 -0
  28. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  29. package/dist/src/import/tui/useLivePreview.d.ts +2 -0
  30. package/dist/src/import/tui/useLivePreview.js +1 -0
  31. package/dist/src/index.js +1 -5
  32. package/dist/src/lib/command-options.d.ts +1 -0
  33. package/dist/src/lib/command-options.js +3 -0
  34. package/dist/src/program.js +1 -8
  35. package/dist/src/runs/modify-launcher.d.ts +2 -0
  36. package/dist/src/runs/modify-launcher.js +2 -0
  37. package/dist/src/runs/push-helpers.d.ts +1 -0
  38. package/dist/src/runs/push-helpers.js +3 -0
  39. package/dist/src/runs/replay-helpers.d.ts +4 -0
  40. package/dist/src/runs/replay-helpers.js +3 -0
  41. package/package.json +5 -6
  42. package/dist/src/analytics/apply.d.ts +0 -6
  43. package/dist/src/analytics/apply.js +0 -30
  44. package/dist/src/analytics/client.d.ts +0 -3
  45. package/dist/src/analytics/client.js +0 -39
  46. package/dist/src/analytics/index.d.ts +0 -6
  47. package/dist/src/analytics/index.js +0 -5
  48. package/dist/src/analytics/normalize.d.ts +0 -3
  49. package/dist/src/analytics/normalize.js +0 -17
  50. package/dist/src/analytics/os.d.ts +0 -2
  51. package/dist/src/analytics/os.js +0 -13
  52. package/dist/src/analytics/tracker.d.ts +0 -19
  53. package/dist/src/analytics/tracker.js +0 -157
  54. package/dist/src/analytics/types.d.ts +0 -29
  55. package/dist/src/analytics/types.js +0 -1
package/README.md CHANGED
@@ -214,6 +214,7 @@ Pass `--select-prompt-path <path>` and/or `--generate-prompt-path <path>` to swa
214
214
  | `--host <url>` | `https://api.contentful.com` | Override API base URL |
215
215
  | `--on-conflict <mode>` | _(prompt via `<SaveConflictGate>`)_ | Headless conflict resolution when a file already exists at the save path: `overwrite`, `skip`, or `fail`. Bypasses the wizard's interactive save-conflict gate. Mutex with `--no-save`. |
216
216
  | `--print-prompt` | — | Print the generate prompt to stdout and exit. Replaces the prompt-print semantics of `--dry-run`. |
217
+ | `--allow-deletions` | off (non-destructive) | Allow the push to delete remote ComponentTypes/DesignTokens missing from the manifest. Default skips them instead of deleting. Without this flag, preview responses suppress the removed-entity list and return a count instead; interactive confirm screens show an opt-out toggle (never opt-in) only when the flag is passed. Forwarded to headless subprocess pushes and `--push-from-run`. |
217
218
  | `--dry-run` | _(deprecated)_ | Deprecated alias for `--print-prompt`. Emits a stderr deprecation notice; prompt-print semantics will be removed in a future release. |
218
219
 
219
220
  ### Run-picker at wizard start
@@ -418,7 +419,7 @@ experiences apply select --space-id <id> --environment-id <env> --session <id>
418
419
  experiences apply push --space-id <id> --environment-id <env> --session <id> [--yes]
419
420
  ```
420
421
 
421
- Shared flags: `--components`, `--tokens`, `--session`, `--space-id`, `--environment-id`, `--cma-token`, `--host`, `--viewports`. `apply preview` adds `--include-unchanged`. `apply select` adds `--select-all`, `--select`, `--deselect`, `--force`. `apply push` adds `--yes`, `--verbose`, `--force`, `--dry-run`.
422
+ Shared flags: `--components`, `--tokens`, `--session`, `--space-id`, `--environment-id`, `--cma-token`, `--host`, `--viewports`. `apply preview` adds `--include-unchanged`. `apply select` adds `--select-all`, `--select`, `--deselect`, `--force`, `--allow-deletions`. `apply push` adds `--yes`, `--verbose`, `--force`, `--dry-run`, `--allow-deletions`. By default, remote ComponentTypes and DesignTokens missing from the pushed manifest are skipped, not deleted; pass `--allow-deletions` to restore the prior delete behavior.
422
423
 
423
424
  Design tokens are written first (component types may reference token kinds). Each entity write is recorded in the session database atomically — interrupted pushes resume from where they left off.
424
425
 
@@ -450,29 +451,6 @@ Wizard run history is separate: `~/.config/experiences/runs.json`.
450
451
 
451
452
  ---
452
453
 
453
- ## Usage data
454
-
455
- The CLI collects **anonymous usage data** to help us understand which commands are used and where the import workflow succeeds or fails. This data does **not** include your source code, file paths, credentials, prompts, or any content you author.
456
-
457
- What may be included:
458
-
459
- - Command name and duration
460
- - CLI, Node.js, and operating-system version
461
- - Anonymous session identifiers that link steps within a single import run
462
- - Structural counts (for example, how many components were extracted or accepted)
463
- - Space and environment IDs you pass on the command line
464
- - Contentful request IDs from API responses (to correlate failures with server logs)
465
-
466
- You can turn this off at any time:
467
-
468
- ```bash
469
- DISABLE_ANALYTICS=1 experiences import --project ./my-app
470
- ```
471
-
472
- Setting `DISABLE_ANALYTICS` to any value disables collection for that invocation.
473
-
474
- ---
475
-
476
454
  ## Development
477
455
 
478
456
  ```bash
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.23.2-dev-build-8b2cfc9.0",
3
+ "version": "2.23.2-dev-build-09d87bb.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,6 @@
41
41
  "@contentful/experience-design-system-extraction": "workspace:*",
42
42
  "@contentful/experience-design-system-generation": "workspace:*",
43
43
  "@contentful/experience-design-system-types": "workspace:*",
44
- "@segment/analytics-node": "^3.0.0",
45
44
  "commander": "^13.1.0",
46
45
  "ink": "^4.4.1",
47
46
  "react": "^18.3.1",
@@ -24,7 +24,6 @@ import { DEFAULT_AGENT_NAME, isAgentName, runAgent, } from '@contentful/experien
24
24
  import { readExperiencesCredentials } from '../credentials-store.js';
25
25
  import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
26
26
  import { getInteractiveTerminalSupport } from '../lib/terminal-capabilities.js';
27
- import { bindAnalyticsSession, emitSessionStarted, enrichCommandResult } from '../analytics/index.js';
28
27
  import { getDebugLogger } from '../lib/debug-logger.js';
29
28
  const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
30
29
  /**
@@ -265,8 +264,6 @@ export function registerAnalyzeCommand(program) {
265
264
  inputPath: projectRoot,
266
265
  outDir,
267
266
  });
268
- await bindAnalyticsSession(sessionId);
269
- await emitSessionStarted('analyze_extract');
270
267
  const stepId = createStep(db, sessionId, 'analyze extract', {
271
268
  project: projectRoot,
272
269
  });
@@ -540,7 +537,6 @@ export function registerAnalyzeCommand(program) {
540
537
  storeSlotCycles(db, sessionId, withBreaks);
541
538
  storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
542
539
  updateStep(db, stepId, 'complete', { sessionId });
543
- enrichCommandResult({ extracted_component_count: validatedComponents.length });
544
540
  db.close();
545
541
  const allWarnings = [...extraction.warnings, ...filterWarnings];
546
542
  const { rows: componentRows, totalErrors } = buildAnalyzeViewRows(filteredComponents, validatedComponents, allWarnings);
@@ -80,7 +80,10 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
80
80
  if (!manifest.componentsManifest)
81
81
  manifest.componentsManifest = {};
82
82
  const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
83
- const preview = await client.previewImport(manifest);
83
+ // This preview is display-only (component-picker annotations); it never
84
+ // triggers an apply, so always request the full diff to keep the
85
+ // existing "removed" annotation working.
86
+ const preview = await client.previewImport(manifest, true);
84
87
  const annotations = {};
85
88
  for (const item of preview.components.new) {
86
89
  const name = item.name ?? '';
@@ -45,17 +45,17 @@ export declare class ImportApiClient {
45
45
  private spaceId;
46
46
  private environmentId;
47
47
  private retry;
48
- private lastRequestId?;
49
- getLastRequestId(): string | undefined;
50
- private noteRequestId;
51
48
  constructor(opts: ApiClientOptions);
52
49
  private base;
53
50
  private headers;
54
51
  private requestWithRetry;
55
52
  validateToken(): Promise<void>;
56
53
  checkPreflight(): Promise<void>;
57
- previewImport(manifest: ManifestPayload): Promise<ServerPreviewResponse>;
58
- applyImport(manifest: ManifestPayload, acknowledgeBreakingChanges: boolean): Promise<ApplyOperationResponse>;
54
+ previewImport(manifest: ManifestPayload, allowDeletions?: boolean): Promise<ServerPreviewResponse>;
55
+ applyImport(manifest: ManifestPayload, options: {
56
+ acknowledgeBreakingChanges: boolean;
57
+ allowDeletions?: boolean;
58
+ }): Promise<ApplyOperationResponse>;
59
59
  pollOperation(operationId: string, opts?: {
60
60
  intervalMs?: number;
61
61
  maxIntervalMs?: number;
@@ -180,15 +180,6 @@ export class ImportApiClient {
180
180
  spaceId;
181
181
  environmentId;
182
182
  retry;
183
- lastRequestId;
184
- getLastRequestId() {
185
- return this.lastRequestId;
186
- }
187
- noteRequestId(response) {
188
- const requestId = response.headers.get('x-contentful-request-id');
189
- if (requestId)
190
- this.lastRequestId = requestId;
191
- }
192
183
  constructor(opts) {
193
184
  this.host = toApiHost(opts.host);
194
185
  this.token = opts.cmaToken;
@@ -246,10 +237,8 @@ export class ImportApiClient {
246
237
  await this.retry.sleep(delayMs);
247
238
  continue;
248
239
  }
249
- if (!isTransientStatus(result.response.status)) {
250
- this.noteRequestId(result.response);
240
+ if (!isTransientStatus(result.response.status))
251
241
  return result;
252
- }
253
242
  if (attempt === this.retry.maxAttempts) {
254
243
  const body = stringifyError(result.error);
255
244
  const guidance = `The ${phase} request failed after ${attempt} attempts because the service remained unavailable. ` +
@@ -284,7 +273,6 @@ export class ImportApiClient {
284
273
  // false positives that don't apply to the design-systems API authorization path.
285
274
  const url = `${this.host}/users/me`;
286
275
  const res = await request(url, { token: this.token });
287
- this.noteRequestId(res);
288
276
  if (res.status === 401) {
289
277
  throw new ApiError('CMA token is invalid or revoked', res.status, await res.text());
290
278
  }
@@ -310,7 +298,6 @@ export class ImportApiClient {
310
298
  catch {
311
299
  return; // network error — don't block; the real call will surface it
312
300
  }
313
- this.noteRequestId(res);
314
301
  if (res.ok)
315
302
  return;
316
303
  if (res.status >= 500)
@@ -320,19 +307,20 @@ export class ImportApiClient {
320
307
  return; // legacy backend without the endpoint
321
308
  throw new ApiError(`preflight failed: ${res.status}`, res.status, body);
322
309
  }
323
- async previewImport(manifest) {
310
+ async previewImport(manifest, allowDeletions = false) {
324
311
  const debug = getDebugLogger();
325
312
  const startedAt = Date.now();
326
313
  debug.event('apply', 'preview.request', {
327
314
  url: `${this.base()}/design_systems/imports/preview`,
328
315
  componentCount: manifest.components?.length ?? 0,
329
316
  tokenCount: manifest.designTokens?.length ?? 0,
317
+ allowDeletions,
330
318
  });
331
319
  const result = await this.requestWithRetry('preview', PREVIEW_ERROR_PREFIX, () => designSystemImportSourcelessPreview({
332
320
  baseUrl: this.host,
333
321
  headers: this.headers(),
334
322
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
335
- body: manifest,
323
+ body: { ...manifest, allowDeletions },
336
324
  parseAs: 'json',
337
325
  }));
338
326
  if (!result.response.ok) {
@@ -348,12 +336,14 @@ export class ImportApiClient {
348
336
  debug.event('apply', 'preview.ok', { status: result.response.status, durationMs: Date.now() - startedAt });
349
337
  return sanitizePreviewResponse(parsed);
350
338
  }
351
- async applyImport(manifest, acknowledgeBreakingChanges) {
339
+ async applyImport(manifest, options) {
340
+ const { acknowledgeBreakingChanges, allowDeletions = false } = options;
352
341
  const debug = getDebugLogger();
353
342
  const startedAt = Date.now();
354
343
  debug.event('apply', 'apply.request', {
355
344
  url: `${this.base()}/design_systems/imports/apply`,
356
345
  acknowledgeBreakingChanges,
346
+ allowDeletions,
357
347
  });
358
348
  let result;
359
349
  try {
@@ -361,7 +351,7 @@ export class ImportApiClient {
361
351
  baseUrl: this.host,
362
352
  headers: this.headers(),
363
353
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
364
- body: { ...manifest, acknowledgeBreakingChanges },
354
+ body: { ...manifest, acknowledgeBreakingChanges, allowDeletions },
365
355
  parseAs: 'json',
366
356
  });
367
357
  }
@@ -12,13 +12,11 @@ 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, completeActiveCommand, failActiveCommand, recordApplyOutcome, recordContentfulContext, } from '../analytics/index.js';
20
19
  function die(message) {
21
- void failActiveCommand({ exit_code: 1 });
22
20
  process.stderr.write(`${message}\n`);
23
21
  process.exit(1);
24
22
  }
@@ -433,12 +431,6 @@ export function registerApplyCommand(program) {
433
431
  throw e;
434
432
  }
435
433
  const { components, tokens, client } = inputs;
436
- const spaceId = opts.spaceId;
437
- const environmentId = opts.environmentId;
438
- await bindAnalyticsSessionId(opts.session, {
439
- space_key: spaceId,
440
- environment_key: environmentId,
441
- });
442
434
  try {
443
435
  await client.validateToken();
444
436
  }
@@ -458,12 +450,14 @@ export function registerApplyCommand(program) {
458
450
  die(`Error: ${formatApiError(e)}`);
459
451
  throw e;
460
452
  }
461
- recordContentfulContext(client, spaceId, environmentId);
453
+ const spaceId = opts.spaceId;
454
+ const environmentId = opts.environmentId;
462
455
  if (getInteractiveTerminalSupport().supported) {
463
456
  const { waitUntilExit } = render(createElement(ServerPreviewApp, {
464
457
  preview,
465
458
  spaceId,
466
459
  environmentId,
460
+ allowDeletions: false,
467
461
  }));
468
462
  await waitUntilExit();
469
463
  }
@@ -476,6 +470,7 @@ export function registerApplyCommand(program) {
476
470
  addArtifactInputOptions(pushCmd);
477
471
  addContentfulTargetOptions(pushCmd);
478
472
  addCompositionOptions(pushCmd);
473
+ addAllowDeletionsOption(pushCmd);
479
474
  pushCmd
480
475
  .option('--yes', 'Skip interactive confirmation')
481
476
  .option('--verbose', 'Show all entity progress including skipped/unchanged')
@@ -497,12 +492,6 @@ export function registerApplyCommand(program) {
497
492
  throw e;
498
493
  }
499
494
  const { components, tokens, client } = inputs;
500
- const spaceId = opts.spaceId;
501
- const environmentId = opts.environmentId;
502
- await bindAnalyticsSessionId(opts.session, {
503
- space_key: spaceId,
504
- environment_key: environmentId,
505
- });
506
495
  assertNoSlotCycles(components);
507
496
  try {
508
497
  await client.validateToken();
@@ -515,19 +504,22 @@ export function registerApplyCommand(program) {
515
504
  const manifest = buildManifest(components, tokens);
516
505
  let preview;
517
506
  try {
518
- preview = await client.previewImport(manifest);
507
+ preview = await client.previewImport(manifest, opts.allowDeletions === true);
519
508
  }
520
509
  catch (e) {
521
510
  if (e instanceof ApiError)
522
511
  die(`Error: ${formatApiError(e, opts.verbose)}`);
523
512
  throw e;
524
513
  }
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
  }
@@ -558,7 +550,10 @@ 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)
@@ -575,18 +570,12 @@ export function registerApplyCommand(program) {
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
- if (exitCode === 0)
582
- await completeActiveCommand();
583
- else
584
- await failActiveCommand({ exit_code: exitCode });
585
- process.exit(exitCode);
574
+ process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
586
575
  return;
587
576
  }
588
577
  await new Promise((resolvePromise) => {
589
- const runApply = async (acknowledge) => {
578
+ const runApply = async (acknowledge, applyDeletions) => {
590
579
  instance.rerender(createElement(ServerApplyProgress, {
591
580
  spaceId,
592
581
  environmentId,
@@ -594,7 +583,10 @@ export function registerApplyCommand(program) {
594
583
  }));
595
584
  let operation;
596
585
  try {
597
- operation = await client.applyImport(manifest, acknowledge);
586
+ operation = await client.applyImport(manifest, {
587
+ acknowledgeBreakingChanges: acknowledge,
588
+ allowDeletions: applyDeletions,
589
+ });
598
590
  }
599
591
  catch (e) {
600
592
  if (e instanceof ApiError) {
@@ -642,8 +634,9 @@ export function registerApplyCommand(program) {
642
634
  spaceId,
643
635
  environmentId,
644
636
  breakingWithImpact,
645
- onConfirm: (acknowledge) => {
646
- void runApply(acknowledge);
637
+ allowDeletions: opts.allowDeletions === true,
638
+ onConfirm: (acknowledge, applyDeletions) => {
639
+ void runApply(acknowledge, applyDeletions);
647
640
  },
648
641
  onCancel: () => {
649
642
  process.exit(0);
@@ -657,6 +650,7 @@ export function registerApplyCommand(program) {
657
650
  addContentfulTargetOptions(selectCmd);
658
651
  addCompositionOptions(selectCmd);
659
652
  addSelectionOptions(selectCmd);
653
+ addAllowDeletionsOption(selectCmd);
660
654
  selectCmd.option('--force', 'Skip confirmation for breaking changes').action(async (opts) => {
661
655
  const nonInteractive = opts.selectAll || (opts.select ?? []).length > 0 || (opts.deselect ?? []).length > 0;
662
656
  if (!nonInteractive) {
@@ -686,7 +680,7 @@ export function registerApplyCommand(program) {
686
680
  const fullManifest = buildManifest(components, tokens);
687
681
  let preview;
688
682
  try {
689
- preview = await client.previewImport(fullManifest);
683
+ preview = await client.previewImport(fullManifest, opts.allowDeletions === true);
690
684
  }
691
685
  catch (e) {
692
686
  if (e instanceof ApiError)
@@ -724,7 +718,10 @@ export function registerApplyCommand(program) {
724
718
  }
725
719
  let operation;
726
720
  try {
727
- 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
+ });
728
725
  }
729
726
  catch (e) {
730
727
  if (e instanceof ApiError)
@@ -766,7 +763,10 @@ export function registerApplyCommand(program) {
766
763
  }));
767
764
  let operation;
768
765
  try {
769
- operation = await client.applyImport(filteredManifest, hasBreaking);
766
+ operation = await client.applyImport(filteredManifest, {
767
+ acknowledgeBreakingChanges: hasBreaking,
768
+ allowDeletions: opts.allowDeletions === true,
769
+ });
770
770
  }
771
771
  catch (e) {
772
772
  if (e instanceof ApiError) {
@@ -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
  }
@@ -4,7 +4,7 @@ import { resolveAutoFilter } from './auto-filter-resolve.js';
4
4
  import { resolveAgent, resolveModel } from './agent-model-resolve.js';
5
5
  import { addAgentModelOptions } from '../lib/agent-model-options.js';
6
6
  import { resolveCompositionMode } from '../lib/composition-mode.js';
7
- import { addCompositionOptions } from '../lib/command-options.js';
7
+ import { addAllowDeletionsOption, addCompositionOptions } from '../lib/command-options.js';
8
8
  import { isConflictMode } from '../runs/save-path-resolver.js';
9
9
  import { readExperiencesCredentials } from '../credentials-store.js';
10
10
  import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
@@ -46,6 +46,7 @@ export function registerImportCommand(program) {
46
46
  .option('--print-prompt', 'Print the generate components prompt without invoking the agent. Replaces the legacy --dry-run prompt-print behaviour on this command.')
47
47
  .option('--auto-accept-scope', 'Accept all extracted components without prompting (for scripted/non-TTY callers)');
48
48
  addCompositionOptions(cmd);
49
+ addAllowDeletionsOption(cmd);
49
50
  cmd
50
51
  .option('--composition-map <path>', 'Consume a hand-authored parent→children interchange map (implies --composite)')
51
52
  .option('--composition-agent', 'Opt into agentic mapping resolution when deterministic sources find no groups (implies --composite)')
@@ -136,6 +137,7 @@ export function registerImportCommand(program) {
136
137
  ...(opts.host ? { host: opts.host } : {}),
137
138
  interactive: interactiveTerminalSupported,
138
139
  ...(opts.force ? { force: true } : {}),
140
+ ...(opts.allowDeletions ? { allowDeletions: true } : {}),
139
141
  });
140
142
  return;
141
143
  }
@@ -166,6 +168,7 @@ export function registerImportCommand(program) {
166
168
  ...(opts.saveAsNew ? { saveAsNew: true } : {}),
167
169
  ...(opts.outDir ? { outDir: opts.outDir } : {}),
168
170
  ...(opts.force ? { force: true } : {}),
171
+ ...(opts.allowDeletions ? { allowDeletions: true } : {}),
169
172
  });
170
173
  return;
171
174
  }
@@ -297,6 +300,7 @@ export function registerImportCommand(program) {
297
300
  selectPromptPath: opts.selectPromptPath ?? creds.selectPromptPath,
298
301
  generatePromptPath: opts.generatePromptPath ?? creds.generatePromptPath,
299
302
  ...(opts.rawTokens ? { initialRawTokensPath: resolve(opts.rawTokens) } : {}),
303
+ allowDeletions: opts.allowDeletions === true,
300
304
  ...pickerProps,
301
305
  }));
302
306
  unmountInk = unmount;
@@ -357,6 +361,7 @@ export function registerImportCommand(program) {
357
361
  dryRun: dryRunForward,
358
362
  selectPromptPath: opts.selectPromptPath,
359
363
  autoRejectCycles: opts.autoRejectCycles ?? false,
364
+ allowDeletions: opts.allowDeletions ?? false,
360
365
  compositionMode: headlessCompositionMode,
361
366
  ...(opts.compositionMap ? { compositionMap: opts.compositionMap } : {}),
362
367
  ...(opts.compositionAgent ? { compositionAgent: true } : {}),
@@ -26,6 +26,7 @@ export interface PipelineOptions {
26
26
  selectPromptPath?: string;
27
27
  /** When true, auto-reject cycle participants and retry push instead of surfacing an error. */
28
28
  autoRejectCycles?: boolean;
29
+ allowDeletions?: boolean;
29
30
  compositionMode?: CompositionMode;
30
31
  compositionMap?: string;
31
32
  compositionAgent?: boolean;
@@ -7,7 +7,6 @@ import { detectSlotCycles, formatSlotCycleReport } from '../apply/command.js';
7
7
  import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
8
8
  import { buildPostPushUrl } from '../lib/contentful-urls.js';
9
9
  import { getDebugLogger, debugEnvForSubprocess } from '../lib/debug-logger.js';
10
- import { bindAnalyticsSession, emitSessionStarted } from '../analytics/index.js';
11
10
  function findCliPath() {
12
11
  return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
13
12
  }
@@ -17,7 +16,7 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
17
16
  debug.event('import', 'subprocess.spawn', { cliPath, args });
18
17
  return new Promise((res) => {
19
18
  const child = execFile('node', [cliPath, ...args], {
20
- env: debugEnvForSubprocess({ ...process.env, ...env, EDS_IMPORT_PIPELINE: '1' }),
19
+ env: debugEnvForSubprocess({ ...process.env, ...env }),
21
20
  });
22
21
  let stdout = '';
23
22
  let stderr = '';
@@ -109,10 +108,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
109
108
  inputPath: projectRoot,
110
109
  outDir,
111
110
  });
112
- await bindAnalyticsSession(sessionId, {
113
- ...(opts.spaceId ? { space_key: opts.spaceId, environment_key: opts.environmentId } : {}),
114
- });
115
- await emitSessionStarted('import');
116
111
  progressWriter(`Experience Design System CLI — Pipeline Import`);
117
112
  progressWriter(`Project: ${projectRoot}`);
118
113
  progressWriter(`Output: ${outDir}`);
@@ -423,6 +418,8 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
423
418
  pushArgs.push('--host', opts.host);
424
419
  if (opts.verbose)
425
420
  pushArgs.push('--verbose');
421
+ if (opts.allowDeletions)
422
+ pushArgs.push('--allow-deletions');
426
423
  pushArgs.push('--yes');
427
424
  const pushStepId = createStep(db, sessionId, 'apply push', {
428
425
  components: componentsPath,
@@ -66,5 +66,6 @@ export type WizardAppProps = {
66
66
  initialRawTokensPath?: string;
67
67
  initialRuns?: RunRecord[];
68
68
  onRunPicked?: (selection: RunPickerSelection) => void;
69
+ allowDeletions?: boolean;
69
70
  };
70
- export declare function WizardApp({ initialSpaceId, initialEnvironmentId, initialCmaToken, initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope, autoRejectCycles, compositionMode, compositionMap, compositionAgent, compositionAgentMode, compositionRefresh, generateMap, promptOverrides, noCache, autoFilter, livePreview, noPush, noSave, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, }?: WizardAppProps): React.ReactElement;
71
+ export declare function WizardApp({ initialSpaceId, initialEnvironmentId, initialCmaToken, initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope, autoRejectCycles, compositionMode, compositionMap, compositionAgent, compositionAgentMode, compositionRefresh, generateMap, promptOverrides, noCache, autoFilter, livePreview, noPush, noSave, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, allowDeletions, }?: WizardAppProps): React.ReactElement;