@contentful/experience-design-system-cli 2.20.1-dev-build-938785e.0 → 2.20.1-dev-build-64d16a0.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 (38) hide show
  1. package/README.md +2 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/select/tui/App.js +4 -1
  4. package/dist/src/apply/api-client.d.ts +5 -3
  5. package/dist/src/apply/api-client.js +7 -43
  6. package/dist/src/apply/command.js +27 -10
  7. package/dist/src/apply/preview-utils.d.ts +5 -1
  8. package/dist/src/apply/preview-utils.js +5 -1
  9. package/dist/src/apply/tui/ServerApplyView.d.ts +6 -3
  10. package/dist/src/apply/tui/ServerApplyView.js +15 -6
  11. package/dist/src/apply/tui/ServerPreviewView.d.ts +2 -1
  12. package/dist/src/apply/tui/ServerPreviewView.js +3 -3
  13. package/dist/src/import/command.js +6 -1
  14. package/dist/src/import/orchestrator.d.ts +1 -0
  15. package/dist/src/import/orchestrator.js +2 -0
  16. package/dist/src/import/tui/WizardApp.d.ts +2 -1
  17. package/dist/src/import/tui/WizardApp.js +10 -14
  18. package/dist/src/import/tui/final-review-host.d.ts +2 -1
  19. package/dist/src/import/tui/final-review-host.js +2 -2
  20. package/dist/src/import/tui/runLivePreview.d.ts +3 -0
  21. package/dist/src/import/tui/runLivePreview.js +4 -1
  22. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +2 -1
  23. package/dist/src/import/tui/steps/GenerateReviewStep.js +3 -3
  24. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -2
  25. package/dist/src/import/tui/steps/WizardPreviewStep.js +12 -3
  26. package/dist/src/import/tui/useFinalizePreview.d.ts +2 -0
  27. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  28. package/dist/src/import/tui/useLivePreview.d.ts +2 -0
  29. package/dist/src/import/tui/useLivePreview.js +1 -0
  30. package/dist/src/lib/command-options.d.ts +1 -0
  31. package/dist/src/lib/command-options.js +3 -0
  32. package/dist/src/runs/modify-launcher.d.ts +2 -0
  33. package/dist/src/runs/modify-launcher.js +2 -0
  34. package/dist/src/runs/push-helpers.d.ts +1 -0
  35. package/dist/src/runs/push-helpers.js +3 -0
  36. package/dist/src/runs/replay-helpers.d.ts +4 -0
  37. package/dist/src/runs/replay-helpers.js +3 -0
  38. package/package.json +5 -5
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
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.20.1-dev-build-938785e.0",
3
+ "version": "2.20.1-dev-build-64d16a0.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 ?? '';
@@ -50,9 +50,11 @@ export declare class ImportApiClient {
50
50
  private headers;
51
51
  private requestWithRetry;
52
52
  validateToken(): Promise<void>;
53
- checkPreflight(): Promise<void>;
54
- previewImport(manifest: ManifestPayload): Promise<ServerPreviewResponse>;
55
- applyImport(manifest: ManifestPayload, acknowledgeBreakingChanges: boolean): Promise<ApplyOperationResponse>;
53
+ previewImport(manifest: ManifestPayload, allowDeletions?: boolean): Promise<ServerPreviewResponse>;
54
+ applyImport(manifest: ManifestPayload, options: {
55
+ acknowledgeBreakingChanges: boolean;
56
+ allowDeletions?: boolean;
57
+ }): Promise<ApplyOperationResponse>;
56
58
  pollOperation(operationId: string, opts?: {
57
59
  intervalMs?: number;
58
60
  maxIntervalMs?: number;
@@ -148,18 +148,6 @@ function sanitizePreviewResponse(res) {
148
148
  }
149
149
  return res;
150
150
  }
151
- function isApsDenialBody(body) {
152
- if (!body)
153
- return false;
154
- try {
155
- const parsed = JSON.parse(body);
156
- const id = parsed?.sys?.id;
157
- return id === 'NotFound' || id === 'AccessDenied';
158
- }
159
- catch {
160
- return false;
161
- }
162
- }
163
151
  async function request(url, options) {
164
152
  const headers = {
165
153
  Authorization: `Bearer ${options.token}`,
@@ -279,47 +267,21 @@ export class ImportApiClient {
279
267
  if (!res.ok) {
280
268
  throw new ApiError(`unexpected error validating token: ${res.status}`, res.status, await res.text());
281
269
  }
282
- await this.checkPreflight();
283
- }
284
- // Probe the sources-api preflight endpoint to catch APS-denied users up-front,
285
- // rather than letting them run a full apply flow that only fails at the write.
286
- // The endpoint gates Component + DesignToken permissions and returns:
287
- // 200 → user has apply permissions, proceed
288
- // 403 { sys.id: AccessDenied } → user lacks write; surface via error-parser
289
- // 404 { sys.id: NotFound } → user lacks read (APS hides existence)
290
- // 404 without a NotFound envelope → older backend deployment; treat as pass
291
- // 5xx / network → backend flake; treat as pass, do not block
292
- async checkPreflight() {
293
- const url = `${this.host}/spaces/${this.spaceId}/environments/${this.environmentId}/design_systems/preflight`;
294
- let res;
295
- try {
296
- res = await request(url, { token: this.token });
297
- }
298
- catch {
299
- return; // network error — don't block; the real call will surface it
300
- }
301
- if (res.ok)
302
- return;
303
- if (res.status >= 500)
304
- return; // backend flake — don't block
305
- const body = await res.text();
306
- if (res.status === 404 && !isApsDenialBody(body))
307
- return; // legacy backend without the endpoint
308
- throw new ApiError(`preflight failed: ${res.status}`, res.status, body);
309
270
  }
310
- async previewImport(manifest) {
271
+ async previewImport(manifest, allowDeletions = false) {
311
272
  const debug = getDebugLogger();
312
273
  const startedAt = Date.now();
313
274
  debug.event('apply', 'preview.request', {
314
275
  url: `${this.base()}/design_systems/imports/preview`,
315
276
  componentCount: manifest.components?.length ?? 0,
316
277
  tokenCount: manifest.designTokens?.length ?? 0,
278
+ allowDeletions,
317
279
  });
318
280
  const result = await this.requestWithRetry('preview', PREVIEW_ERROR_PREFIX, () => designSystemImportSourcelessPreview({
319
281
  baseUrl: this.host,
320
282
  headers: this.headers(),
321
283
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
322
- body: manifest,
284
+ body: { ...manifest, allowDeletions },
323
285
  parseAs: 'json',
324
286
  }));
325
287
  if (!result.response.ok) {
@@ -335,12 +297,14 @@ export class ImportApiClient {
335
297
  debug.event('apply', 'preview.ok', { status: result.response.status, durationMs: Date.now() - startedAt });
336
298
  return sanitizePreviewResponse(parsed);
337
299
  }
338
- async applyImport(manifest, acknowledgeBreakingChanges) {
300
+ async applyImport(manifest, options) {
301
+ const { acknowledgeBreakingChanges, allowDeletions = false } = options;
339
302
  const debug = getDebugLogger();
340
303
  const startedAt = Date.now();
341
304
  debug.event('apply', 'apply.request', {
342
305
  url: `${this.base()}/design_systems/imports/apply`,
343
306
  acknowledgeBreakingChanges,
307
+ allowDeletions,
344
308
  });
345
309
  let result;
346
310
  try {
@@ -348,7 +312,7 @@ export class ImportApiClient {
348
312
  baseUrl: this.host,
349
313
  headers: this.headers(),
350
314
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
351
- body: { ...manifest, acknowledgeBreakingChanges },
315
+ body: { ...manifest, acknowledgeBreakingChanges, allowDeletions },
352
316
  parseAs: 'json',
353
317
  });
354
318
  }
@@ -12,7 +12,7 @@ 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';
@@ -457,6 +457,7 @@ export function registerApplyCommand(program) {
457
457
  preview,
458
458
  spaceId,
459
459
  environmentId,
460
+ allowDeletions: false,
460
461
  }));
461
462
  await waitUntilExit();
462
463
  }
@@ -469,6 +470,7 @@ export function registerApplyCommand(program) {
469
470
  addArtifactInputOptions(pushCmd);
470
471
  addContentfulTargetOptions(pushCmd);
471
472
  addCompositionOptions(pushCmd);
473
+ addAllowDeletionsOption(pushCmd);
472
474
  pushCmd
473
475
  .option('--yes', 'Skip interactive confirmation')
474
476
  .option('--verbose', 'Show all entity progress including skipped/unchanged')
@@ -502,7 +504,7 @@ export function registerApplyCommand(program) {
502
504
  const manifest = buildManifest(components, tokens);
503
505
  let preview;
504
506
  try {
505
- preview = await client.previewImport(manifest);
507
+ preview = await client.previewImport(manifest, opts.allowDeletions === true);
506
508
  }
507
509
  catch (e) {
508
510
  if (e instanceof ApiError)
@@ -517,6 +519,7 @@ export function registerApplyCommand(program) {
517
519
  preview,
518
520
  spaceId,
519
521
  environmentId,
522
+ allowDeletions: opts.allowDeletions === true,
520
523
  }));
521
524
  await waitUntilExit();
522
525
  }
@@ -547,7 +550,10 @@ export function registerApplyCommand(program) {
547
550
  }
548
551
  let operation;
549
552
  try {
550
- 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
+ });
551
557
  }
552
558
  catch (e) {
553
559
  if (e instanceof ApiError)
@@ -569,7 +575,7 @@ export function registerApplyCommand(program) {
569
575
  return;
570
576
  }
571
577
  await new Promise((resolvePromise) => {
572
- const runApply = async (acknowledge) => {
578
+ const runApply = async (acknowledge, applyDeletions) => {
573
579
  instance.rerender(createElement(ServerApplyProgress, {
574
580
  spaceId,
575
581
  environmentId,
@@ -577,7 +583,10 @@ export function registerApplyCommand(program) {
577
583
  }));
578
584
  let operation;
579
585
  try {
580
- operation = await client.applyImport(manifest, acknowledge);
586
+ operation = await client.applyImport(manifest, {
587
+ acknowledgeBreakingChanges: acknowledge,
588
+ allowDeletions: applyDeletions,
589
+ });
581
590
  }
582
591
  catch (e) {
583
592
  if (e instanceof ApiError) {
@@ -625,8 +634,9 @@ export function registerApplyCommand(program) {
625
634
  spaceId,
626
635
  environmentId,
627
636
  breakingWithImpact,
628
- onConfirm: (acknowledge) => {
629
- void runApply(acknowledge);
637
+ allowDeletions: opts.allowDeletions === true,
638
+ onConfirm: (acknowledge, applyDeletions) => {
639
+ void runApply(acknowledge, applyDeletions);
630
640
  },
631
641
  onCancel: () => {
632
642
  process.exit(0);
@@ -640,6 +650,7 @@ export function registerApplyCommand(program) {
640
650
  addContentfulTargetOptions(selectCmd);
641
651
  addCompositionOptions(selectCmd);
642
652
  addSelectionOptions(selectCmd);
653
+ addAllowDeletionsOption(selectCmd);
643
654
  selectCmd.option('--force', 'Skip confirmation for breaking changes').action(async (opts) => {
644
655
  const nonInteractive = opts.selectAll || (opts.select ?? []).length > 0 || (opts.deselect ?? []).length > 0;
645
656
  if (!nonInteractive) {
@@ -669,7 +680,7 @@ export function registerApplyCommand(program) {
669
680
  const fullManifest = buildManifest(components, tokens);
670
681
  let preview;
671
682
  try {
672
- preview = await client.previewImport(fullManifest);
683
+ preview = await client.previewImport(fullManifest, opts.allowDeletions === true);
673
684
  }
674
685
  catch (e) {
675
686
  if (e instanceof ApiError)
@@ -707,7 +718,10 @@ export function registerApplyCommand(program) {
707
718
  }
708
719
  let operation;
709
720
  try {
710
- 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
+ });
711
725
  }
712
726
  catch (e) {
713
727
  if (e instanceof ApiError)
@@ -749,7 +763,10 @@ export function registerApplyCommand(program) {
749
763
  }));
750
764
  let operation;
751
765
  try {
752
- operation = await client.applyImport(filteredManifest, hasBreaking);
766
+ operation = await client.applyImport(filteredManifest, {
767
+ acknowledgeBreakingChanges: hasBreaking,
768
+ allowDeletions: opts.allowDeletions === true,
769
+ });
753
770
  }
754
771
  catch (e) {
755
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;
@@ -418,6 +418,8 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
418
418
  pushArgs.push('--host', opts.host);
419
419
  if (opts.verbose)
420
420
  pushArgs.push('--verbose');
421
+ if (opts.allowDeletions)
422
+ pushArgs.push('--allow-deletions');
421
423
  pushArgs.push('--yes');
422
424
  const pushStepId = createStep(db, sessionId, 'apply push', {
423
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;
@@ -138,7 +138,7 @@ function logStep(entry) {
138
138
  appendFileSync(WIZARD_LOG, line);
139
139
  getDebugLogger().event('wizard', 'step', entry);
140
140
  }
141
- export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope = false, autoRejectCycles = false, compositionMode = 'atomic', compositionMap, compositionAgent = false, compositionAgentMode, compositionRefresh = false, generateMap, promptOverrides, noCache = false, autoFilter = true, livePreview = true, noPush = false, noSave = false, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, } = {}) {
141
+ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope = false, autoRejectCycles = false, compositionMode = 'atomic', compositionMap, compositionAgent = false, compositionAgentMode, compositionRefresh = false, generateMap, promptOverrides, noCache = false, autoFilter = true, livePreview = true, noPush = false, noSave = false, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, allowDeletions = false, } = {}) {
142
142
  const defaultConfiguredHost = toConfiguredHost(host || process.env['EDS_HOST']) ?? DEFAULT_CONFIGURED_HOST;
143
143
  const resolveWizardHost = (hostValue) => hostValue || defaultConfiguredHost;
144
144
  const { stdout } = useStdout();
@@ -747,13 +747,9 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
747
747
  await advanceAfterCredentialsValidated();
748
748
  }
749
749
  catch (e) {
750
- if (e instanceof ApiError && (e.status === 401 || e.status === 403 || e.status === 404)) {
750
+ if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
751
751
  cancelGeneratePrefetch();
752
- update({
753
- step: 'credentials',
754
- credentialsValidating: false,
755
- credentialsError: formatApiError(e, process.env['EDSI_VERBOSE_ERRORS'] === '1'),
756
- });
752
+ update({ step: 'credentials', credentialsValidating: false, credentialsError: e.message });
757
753
  return;
758
754
  }
759
755
  const msg = e instanceof Error ? e.message : 'Credential check failed';
@@ -830,7 +826,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
830
826
  tokens = await readTokensFromPath('tokens', tokensPath);
831
827
  }
832
828
  let manifest = buildManifest(components, tokens, { deleteAllComponents: allowEmptyDeleteAllRef.current });
833
- let preview = await client.previewImport(manifest);
829
+ let preview = await client.previewImport(manifest, allowDeletions);
834
830
  if (extractSessionId) {
835
831
  let needsRepreview = false;
836
832
  const db = openPipelineDb();
@@ -852,7 +848,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
852
848
  if (needsRepreview) {
853
849
  components = loadCDFComponents(db, extractSessionId);
854
850
  manifest = buildManifest(components, tokens, { deleteAllComponents: allowEmptyDeleteAllRef.current });
855
- preview = await client.previewImport(manifest);
851
+ preview = await client.previewImport(manifest, allowDeletions);
856
852
  }
857
853
  }
858
854
  finally {
@@ -946,7 +942,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
946
942
  });
947
943
  }
948
944
  };
949
- const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, preview) => {
945
+ const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, allowDeletions, preview) => {
950
946
  if (shouldRefusePush(state)) {
951
947
  update(buildSkippedPushTransition());
952
948
  return;
@@ -986,7 +982,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
986
982
  environmentId,
987
983
  host: resolvedHost,
988
984
  });
989
- let operation = await client.applyImport(manifest, acknowledgeBreakingChanges);
985
+ let operation = await client.applyImport(manifest, { acknowledgeBreakingChanges, allowDeletions });
990
986
  try {
991
987
  logStep({
992
988
  applyResponse: {
@@ -1437,7 +1433,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1437
1433
  return (_jsx(RunningStep, { stepNumber: stepNum, totalSteps: totalSteps, title: "Generating definitions", description: `${formatAcceptanceSummary({ accepted: state.acceptedCount, autoRejected: state.autoRejectedCount })} ${state.agent} is mapping your TypeScript types to Contentful's CDF format.${hasTokens ? ' Using your design tokens for prop resolution.' : ''}`, detail: progressDetail }));
1438
1434
  }
1439
1435
  case 'final-review': {
1440
- return (_jsx(FinalReviewHost, { extractSessionId: state.extractSessionId, generatedCount: state.generatedCount, autoAccept: autoAcceptScope, compositionMode: compositionMode, livePreview: livePreview, spaceId: state.spaceId, environmentId: state.environmentId, cmaToken: state.cmaToken, host: state.host, tokensPath: state.tokensPath, initialFinalizeError: state.finalizeErrorBanner, onFinalize: (accepted, rejected, unresolved) => {
1436
+ return (_jsx(FinalReviewHost, { extractSessionId: state.extractSessionId, generatedCount: state.generatedCount, autoAccept: autoAcceptScope, compositionMode: compositionMode, livePreview: livePreview, spaceId: state.spaceId, environmentId: state.environmentId, cmaToken: state.cmaToken, host: state.host, tokensPath: state.tokensPath, initialFinalizeError: state.finalizeErrorBanner, allowDeletions: allowDeletions, onFinalize: (accepted, rejected, unresolved) => {
1441
1437
  process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected} Unresolved: ${unresolved}\n`);
1442
1438
  let acceptedCount = accepted;
1443
1439
  const detectAcceptedCycles = () => {
@@ -1605,8 +1601,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1605
1601
  // present-but-empty (only $schema), so editing would dead-end on the
1606
1602
  // "No generated definitions found" screen.
1607
1603
  const editableComponentCount = Object.keys(state.manifest?.componentsManifest ?? {}).filter((k) => k !== '$schema').length;
1608
- return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, onConfirm: (acknowledge) => {
1609
- void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, state.host, acknowledge, state.serverPreview);
1604
+ return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, allowDeletions: allowDeletions, onConfirm: (acknowledge, deleteMissing) => {
1605
+ void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, state.host, acknowledge, deleteMissing, state.serverPreview);
1610
1606
  }, ...(editableComponentCount > 0 ? { onEdit: () => void runEditFromPreview() } : {}), onSaveFiles: () => {
1611
1607
  void startSaveFlow();
1612
1608
  }, onQuit: () => process.exit(0) }));
@@ -14,5 +14,6 @@ export type FinalReviewHostProps = {
14
14
  host?: string;
15
15
  tokensPath?: string;
16
16
  initialFinalizeError?: string | null;
17
+ allowDeletions?: boolean;
17
18
  };
18
- export declare function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, compositionMode, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }: FinalReviewHostProps): React.ReactElement;
19
+ export declare function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, compositionMode, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, allowDeletions, }: FinalReviewHostProps): React.ReactElement;
@@ -4,7 +4,7 @@ import { PALETTE } from '../../analyze/select/tui/theme.js';
4
4
  import React from 'react';
5
5
  import { GenerateReviewStep } from './steps/GenerateReviewStep.js';
6
6
  import { AtomicGenerateReviewStep } from './steps/AtomicGenerateReviewStep.js';
7
- export function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, compositionMode = 'atomic', onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }) {
7
+ export function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, compositionMode = 'atomic', onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, allowDeletions, }) {
8
8
  if (!extractSessionId) {
9
9
  return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: PALETTE.error, children: "Error: no session ID \u2014 cannot load generated definitions." }) }));
10
10
  }
@@ -15,7 +15,7 @@ export function FinalReviewHost({ extractSessionId, generatedCount, autoAccept,
15
15
  // passes projectSlotGraph to FieldEditor and never walks closures/cycles, so
16
16
  // slot-composition editing and every hierarchy affordance stay absent.
17
17
  const StepComponent = compositionMode === 'atomic' ? AtomicGenerateReviewStep : GenerateReviewStep;
18
- return (_jsx(StepComponent, { extractSessionId: extractSessionId, onFinalize: onFinalize, onQuit: onQuit, livePreview: livePreview, spaceId: spaceId, environmentId: environmentId, cmaToken: cmaToken, host: host, tokensPath: tokensPath, initialFinalizeError: initialFinalizeError }));
18
+ return (_jsx(StepComponent, { extractSessionId: extractSessionId, onFinalize: onFinalize, onQuit: onQuit, livePreview: livePreview, spaceId: spaceId, environmentId: environmentId, cmaToken: cmaToken, host: host, tokensPath: tokensPath, initialFinalizeError: initialFinalizeError, ...(compositionMode !== 'atomic' ? { allowDeletions } : {}) }));
19
19
  }
20
20
  function FinalReviewAutoAccept({ generatedCount, onFinalize, }) {
21
21
  React.useEffect(() => {
@@ -26,6 +26,9 @@ export type RunLivePreviewOptions = {
26
26
  * full delete. Lets the Finalize dialog show exactly what the accepted push
27
27
  * would delete, independent of the session's on-disk generated rows. */
28
28
  acceptedKeys?: ReadonlySet<string>;
29
+ /** Forwarded verbatim to `previewImport`. Governs whether the response
30
+ * includes removed entities or a suppressed-count summary instead. */
31
+ allowDeletions?: boolean;
29
32
  };
30
33
  /**
31
34
  * Pure async helper used by `useLivePreview` to re-fire `previewImport` after a
@@ -60,7 +60,10 @@ export async function runLivePreview(opts) {
60
60
  timeoutHandle = setTimeout(() => reject(new TimeoutError()), timeoutMs);
61
61
  });
62
62
  try {
63
- const response = (await Promise.race([client.previewImport(manifest), timeoutPromise]));
63
+ const response = (await Promise.race([
64
+ client.previewImport(manifest, opts.allowDeletions === true),
65
+ timeoutPromise,
66
+ ]));
64
67
  if (process.env['EDS_VERBOSE']) {
65
68
  const durationMs = Date.now() - startedAt;
66
69
  try {
@@ -11,6 +11,7 @@ type GenerateReviewStepProps = {
11
11
  host?: string;
12
12
  tokensPath?: string;
13
13
  initialFinalizeError?: string | null;
14
+ allowDeletions?: boolean;
14
15
  };
15
16
  export declare function sortComponentsForSidebar<T extends {
16
17
  key: string;
@@ -33,4 +34,4 @@ export interface BreakingRow {
33
34
  }
34
35
  export declare function buildBreakingRows(breakingChanges: BreakingComponent[]): BreakingRow[];
35
36
  export declare function deriveBreakingChanges(response: ServerPreviewResponse): BreakingComponent[];
36
- export declare function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }: GenerateReviewStepProps): React.ReactElement;
37
+ export declare function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, allowDeletions, }: GenerateReviewStepProps): React.ReactElement;
@@ -174,7 +174,7 @@ export function deriveBreakingChanges(response) {
174
174
  }
175
175
  return out;
176
176
  }
177
- export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', initialFinalizeError = null, }) {
177
+ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', initialFinalizeError = null, allowDeletions = false, }) {
178
178
  const { stdout } = useStdout();
179
179
  const terminalWidth = stdout?.columns ?? 80;
180
180
  const [components, setComponents] = useState([]);
@@ -271,9 +271,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
271
271
  cmaToken,
272
272
  host,
273
273
  onResult: handleLivePreviewResult,
274
- // With nothing accepted, preview the delete-all diff so the review UI shows
275
- // which existing components a push would remove (instead of an empty preview).
276
274
  deleteAllComponents: acceptedCountForPreview === 0,
275
+ allowDeletions,
277
276
  });
278
277
  const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
279
278
  const [spinnerTick, setSpinnerTick] = useState(0);
@@ -352,6 +351,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
352
351
  cmaToken,
353
352
  host,
354
353
  acceptedKeys: new Set(components.filter((c) => c.status === 'accepted').map((c) => c.key)),
354
+ allowDeletions,
355
355
  });
356
356
  const handleFinalizeConfirm = () => {
357
357
  const acceptedCount = components.filter((c) => c.status === 'accepted').length;
@@ -12,10 +12,14 @@ type WizardPreviewStepProps = {
12
12
  environmentId: string;
13
13
  stepNumber: number;
14
14
  totalSteps: number;
15
- onConfirm: (acknowledge: boolean) => void;
15
+ /** The value the preview was actually fetched with — not a hint. When
16
+ * `false`, the server never returned removed entities, so there is
17
+ * nothing to render item-by-item or toggle over. */
18
+ allowDeletions?: boolean;
19
+ onConfirm: (acknowledge: boolean, allowDeletions: boolean) => void;
16
20
  onEdit?: () => void;
17
21
  onSaveFiles?: () => void;
18
22
  onQuit: () => void;
19
23
  };
20
- export declare function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }: WizardPreviewStepProps): React.ReactElement;
24
+ export declare function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, allowDeletions: fetchedAllowDeletions, onConfirm, onEdit, onSaveFiles, onQuit, }: WizardPreviewStepProps): React.ReactElement;
21
25
  export {};
@@ -89,10 +89,14 @@ export function buildPreviewDiffLines(preview) {
89
89
  }
90
90
  return lines;
91
91
  }
92
- export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }) {
92
+ export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, allowDeletions: fetchedAllowDeletions = false, onConfirm, onEdit, onSaveFiles, onQuit, }) {
93
93
  const breakingWithImpact = hasBreakingChangesWithImpact(preview);
94
94
  const [diffExpanded, setDiffExpanded] = useState(false);
95
95
  const [scrollOffset, setScrollOffset] = useState(0);
96
+ // Local state exists only to let the user opt OUT of a deletion the fetch
97
+ // already surfaced — it can never turn true when the fetch used false,
98
+ // because there's nothing in `preview` to reveal in that case.
99
+ const [allowDeletions, setAllowDeletions] = useState(fetchedAllowDeletions);
96
100
  const { stdout } = useStdout();
97
101
  const terminalRows = stdout?.rows ?? 40;
98
102
  const viewportHeight = Math.max(terminalRows - 14, 10);
@@ -105,9 +109,14 @@ export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber,
105
109
  }));
106
110
  }, [diffExpanded, preview]);
107
111
  const maxScroll = Math.max(0, allDiffLines.length - viewportHeight);
112
+ const removedCount = preview.components.removed.length + preview.tokens.removed.length;
108
113
  useImmediateInput((input, key) => {
109
114
  if (key.return) {
110
- onConfirm(breakingWithImpact);
115
+ onConfirm(breakingWithImpact, allowDeletions);
116
+ return;
117
+ }
118
+ if ((input === 'x' || input === 'X') && fetchedAllowDeletions && removedCount > 0) {
119
+ setAllowDeletions((prev) => !prev);
111
120
  return;
112
121
  }
113
122
  if (input === 'd' || input === 'D') {
@@ -156,5 +165,5 @@ export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber,
156
165
  })] })), components.changed.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.warning, children: " \uFF5E" }), _jsxs(Text, { children: [components.changed.length, " will be updated"] })] }), components.changed.map((item, i) => {
157
166
  const isBreaking = item.changeClassification?.classification === 'breaking';
158
167
  return (_jsxs(Text, { color: isBreaking ? PALETTE.error : PALETTE.warning, children: [' ', isBreaking ? '⚠' : '~', " ", item.current.name] }, `chg-${i}`));
159
- })] })), components.removed.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.error, children: " \u2717" }), _jsxs(Text, { children: [components.removed.length, " will be removed"] })] }), components.removed.map((item, i) => (_jsxs(Text, { color: PALETTE.error, children: [' ', "\u2717 ", item.name] }, `rm-${i}`)))] })), components.unchanged.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: " \u00B7" }), _jsxs(Text, { dimColor: true, children: [components.unchanged.length, " unchanged"] })] }))] })), hasTokens && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Box, { gap: 1, marginTop: 1, children: _jsx(Text, { bold: true, dimColor: true, children: "Design Tokens" }) }), tokens.new.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.success, children: " \uFF0B" }), _jsxs(Text, { children: [tokens.new.length, " will be created"] })] })), tokens.changed.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.warning, children: " \uFF5E" }), _jsxs(Text, { children: [tokens.changed.length, " will be updated"] })] })), tokens.removed.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.error, children: " \u2717" }), _jsxs(Text, { children: [tokens.removed.length, " will be removed"] })] })), tokens.unchanged.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: " \u00B7" }), _jsxs(Text, { dimColor: true, children: [tokens.unchanged.length, " unchanged"] })] }))] })), diffExpanded && allDiffLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Text, { dimColor: true, children: [' ', "Diff (", allDiffLines.length, " lines) \u2014 line ", scrollOffset + 1, "\u2013", Math.min(scrollOffset + viewportHeight, allDiffLines.length), " of ", allDiffLines.length] }), _jsx(Box, { flexDirection: "column", children: allDiffLines.slice(scrollOffset, scrollOffset + viewportHeight).map((line) => (_jsx(Box, { children: line.element }, line.key))) }), maxScroll > 0 && _jsx(Text, { dimColor: true, children: " \u2195 j/k to scroll, f/b to page" })] }))] })) : (_jsx(Text, { dimColor: true, children: "Nothing to push \u2014 everything is already up to date." })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Space:" }), _jsx(Text, { children: spaceId }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { dimColor: true, children: "Environment:" }), _jsx(Text, { children: environmentId })] }), breakingWithImpact && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: PALETTE.error, bold: true, children: "\u26A0 Breaking changes will affect downstream entities. Press Enter to acknowledge and apply." }) })), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Push to Contentful" }), _jsxs(Text, { dimColor: true, children: ["[d] ", diffExpanded ? 'Hide' : 'Show', " diff"] }), diffExpanded && _jsx(Text, { dimColor: true, children: "[j/k] Scroll [f/b] Page" }), onEdit && _jsx(Text, { dimColor: true, children: "[e] Edit definitions" }), onSaveFiles && _jsx(Text, { dimColor: true, children: "[s] Save files instead" }), _jsx(Text, { dimColor: true, children: "[q] Cancel" })] })] }));
168
+ })] })), components.removed.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: allowDeletions ? PALETTE.error : PALETTE.warning, children: allowDeletions ? ' ✗' : ' ⊘' }), _jsxs(Text, { children: [components.removed.length, " will be ", allowDeletions ? 'deleted' : 'skipped'] })] }), components.removed.map((item, i) => (_jsxs(Text, { color: allowDeletions ? PALETTE.error : PALETTE.warning, dimColor: !allowDeletions, children: [' ', allowDeletions ? '✗' : '⊘', " ", item.name] }, `rm-${i}`)))] })), components.unchanged.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: " \u00B7" }), _jsxs(Text, { dimColor: true, children: [components.unchanged.length, " unchanged"] })] }))] })), hasTokens && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Box, { gap: 1, marginTop: 1, children: _jsx(Text, { bold: true, dimColor: true, children: "Design Tokens" }) }), tokens.new.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.success, children: " \uFF0B" }), _jsxs(Text, { children: [tokens.new.length, " will be created"] })] })), tokens.changed.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.warning, children: " \uFF5E" }), _jsxs(Text, { children: [tokens.changed.length, " will be updated"] })] })), tokens.removed.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: allowDeletions ? PALETTE.error : PALETTE.warning, children: allowDeletions ? ' ✗' : ' ⊘' }), _jsxs(Text, { children: [tokens.removed.length, " will be ", allowDeletions ? 'deleted' : 'skipped'] })] })), tokens.unchanged.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: " \u00B7" }), _jsxs(Text, { dimColor: true, children: [tokens.unchanged.length, " unchanged"] })] }))] })), diffExpanded && allDiffLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Text, { dimColor: true, children: [' ', "Diff (", allDiffLines.length, " lines) \u2014 line ", scrollOffset + 1, "\u2013", Math.min(scrollOffset + viewportHeight, allDiffLines.length), " of ", allDiffLines.length] }), _jsx(Box, { flexDirection: "column", children: allDiffLines.slice(scrollOffset, scrollOffset + viewportHeight).map((line) => (_jsx(Box, { children: line.element }, line.key))) }), maxScroll > 0 && _jsx(Text, { dimColor: true, children: " \u2195 j/k to scroll, f/b to page" })] }))] })) : (_jsx(Text, { dimColor: true, children: "Nothing to push \u2014 everything is already up to date." })), !fetchedAllowDeletions && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.warning, children: " \u2298" }), _jsx(Text, { children: "Deletions are hidden by default \u2014 rerun with --allow-deletions to see them." })] })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Space:" }), _jsx(Text, { children: spaceId }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { dimColor: true, children: "Environment:" }), _jsx(Text, { children: environmentId })] }), breakingWithImpact && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: PALETTE.error, bold: true, children: "\u26A0 Breaking changes will affect downstream entities. Press Enter to acknowledge and apply." }) })), allowDeletions && removedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: PALETTE.error, bold: true, children: ["\u26A0 ", removedCount, " missing ", removedCount === 1 ? 'entity' : 'entities', " will be permanently deleted. Press Enter to confirm."] }) })), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Push to Contentful" }), _jsxs(Text, { dimColor: true, children: ["[d] ", diffExpanded ? 'Hide' : 'Show', " diff"] }), diffExpanded && _jsx(Text, { dimColor: true, children: "[j/k] Scroll [f/b] Page" }), onEdit && _jsx(Text, { dimColor: true, children: "[e] Edit definitions" }), onSaveFiles && _jsx(Text, { dimColor: true, children: "[s] Save files instead" }), fetchedAllowDeletions && removedCount > 0 && (_jsxs(Text, { dimColor: true, children: ["[x] ", allowDeletions ? '[✓]' : '[ ]', " Allow deletions"] })), _jsx(Text, { dimColor: true, children: "[q] Cancel" })] })] }));
160
169
  }
@@ -11,6 +11,8 @@ export type UseFinalizePreviewOptions = {
11
11
  host: string;
12
12
  /** Component keys the operator has accepted; drives the scoped preview. */
13
13
  acceptedKeys: ReadonlySet<string>;
14
+ /** Forwarded verbatim to `runLivePreview`/`previewImport`. */
15
+ allowDeletions?: boolean;
14
16
  };
15
17
  export type UseFinalizePreviewReturn = {
16
18
  status: FinalizePreviewStatus;
@@ -12,6 +12,8 @@ export type UseLivePreviewOptions = {
12
12
  /** Preview an empty-but-present manifest (delete-all diff) when nothing is
13
13
  * accepted, so the final-review UI can show what a push would delete. */
14
14
  deleteAllComponents?: boolean;
15
+ /** Forwarded verbatim to `runLivePreview`/`previewImport`. */
16
+ allowDeletions?: boolean;
15
17
  };
16
18
  export type LivePreviewStatus = 'idle' | 'running';
17
19
  export type UseLivePreviewReturn = {
@@ -82,6 +82,7 @@ export function useLivePreview(opts) {
82
82
  host: current.host,
83
83
  generation,
84
84
  deleteAllComponents: current.deleteAllComponents === true,
85
+ allowDeletions: current.allowDeletions === true,
85
86
  });
86
87
  // Discard stale responses (generation tag).
87
88
  if (result.generation !== latestRef.current)
@@ -3,3 +3,4 @@ export declare function addArtifactInputOptions(cmd: Command): Command;
3
3
  export declare function addContentfulTargetOptions(cmd: Command): Command;
4
4
  export declare function addCompositionOptions(cmd: Command): Command;
5
5
  export declare function addSelectionOptions(cmd: Command): Command;
6
+ export declare function addAllowDeletionsOption(cmd: Command): Command;
@@ -25,3 +25,6 @@ export function addSelectionOptions(cmd) {
25
25
  .option('--select <pattern>', 'Select entities by ID pattern (repeatable)', collectOptionValue, [])
26
26
  .option('--deselect <pattern>', 'Deselect entities by ID pattern (repeatable)', collectOptionValue, []);
27
27
  }
28
+ export function addAllowDeletionsOption(cmd) {
29
+ return cmd.option('--allow-deletions', 'Allow the push to delete remote ComponentTypes/DesignTokens missing from the manifest (default: skip them)');
30
+ }
@@ -26,5 +26,7 @@ export type ModifyLauncherInput = {
26
26
  initialHost?: string;
27
27
  /** Pre-fill CMA token (from credentials.json / env). */
28
28
  initialCmaToken?: string;
29
+ /** From `--allow-deletions` flag. Forwarded to wizard's push step. */
30
+ allowDeletions?: boolean;
29
31
  };
30
32
  export declare function launchModifyWizard(input: ModifyLauncherInput): Promise<void>;
@@ -35,6 +35,8 @@ export async function launchModifyWizard(input) {
35
35
  props.initialHost = input.initialHost;
36
36
  if (input.initialCmaToken)
37
37
  props.initialCmaToken = input.initialCmaToken;
38
+ if (input.allowDeletions !== undefined)
39
+ props.allowDeletions = input.allowDeletions;
38
40
  const { waitUntilExit } = render(createElement(WizardApp, props));
39
41
  await waitUntilExit();
40
42
  }
@@ -15,6 +15,7 @@ export type PushSessionOptions = {
15
15
  environmentId: string;
16
16
  cmaToken: string;
17
17
  host?: string;
18
+ allowDeletions?: boolean;
18
19
  };
19
20
  /**
20
21
  * Push a recorded pipeline.db session's components + tokens to Contentful by
@@ -60,6 +60,9 @@ export async function pushRunSession(opts) {
60
60
  if (opts.host) {
61
61
  args.push('--host', opts.host);
62
62
  }
63
+ if (opts.allowDeletions) {
64
+ args.push('--allow-deletions');
65
+ }
63
66
  const r = await runCli(args);
64
67
  if (r.exitCode === 0)
65
68
  return { ok: true };
@@ -16,6 +16,8 @@ export type ReplayRunOptions = {
16
16
  interactive?: boolean;
17
17
  /** When true, bypass the source/saved-file staleness check. */
18
18
  force?: boolean;
19
+ /** From `--allow-deletions` flag. Forwarded verbatim to the pushed session's apply request. */
20
+ allowDeletions?: boolean;
19
21
  /**
20
22
  * Test seam: replace the interactive prompt with a deterministic resolver.
21
23
  * The CLI surface never sets this; only used by tests.
@@ -57,6 +59,8 @@ export type ModifyRunOptions = {
57
59
  outDir?: string;
58
60
  /** When true, bypass the source/saved-file staleness check. */
59
61
  force?: boolean;
62
+ /** From `--allow-deletions` flag. Forwarded through the modify wizard's push step. */
63
+ allowDeletions?: boolean;
60
64
  };
61
65
  /**
62
66
  * Re-open the wizard with a prior run's extract/generate session pre-populated
@@ -83,6 +83,7 @@ export async function replayRun(opts) {
83
83
  environmentId,
84
84
  cmaToken,
85
85
  ...(host ? { host } : {}),
86
+ ...(opts.allowDeletions ? { allowDeletions: true } : {}),
86
87
  });
87
88
  if (!result.ok) {
88
89
  throw new Error(result.error);
@@ -98,6 +99,7 @@ export async function replayRun(opts) {
98
99
  environmentId,
99
100
  cmaToken,
100
101
  ...(host ? { host } : {}),
102
+ ...(opts.allowDeletions ? { allowDeletions: true } : {}),
101
103
  });
102
104
  if (!tokenResult.ok) {
103
105
  throw new Error(tokenResult.error);
@@ -157,5 +159,6 @@ export async function modifyRun(opts) {
157
159
  ...(mergedEnvironmentId ? { initialEnvironmentId: mergedEnvironmentId } : {}),
158
160
  ...(mergedHost ? { initialHost: mergedHost } : {}),
159
161
  ...(mergedCmaToken ? { initialCmaToken: mergedCmaToken } : {}),
162
+ ...(opts.allowDeletions !== undefined ? { allowDeletions: opts.allowDeletions } : {}),
160
163
  });
161
164
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.20.1-dev-build-938785e.0",
3
+ "version": "2.20.1-dev-build-64d16a0.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,10 +33,10 @@
33
33
  "react": "^18.3.1",
34
34
  "react-devtools-core": "^4.19.1",
35
35
  "react-dom": "^18.3.1",
36
- "@contentful/experience-design-system-extraction": "2.20.1-dev-build-938785e.0",
37
- "@contentful/experience-design-system-client": "2.20.1-dev-build-938785e.0",
38
- "@contentful/experience-design-system-types": "2.20.1-dev-build-938785e.0",
39
- "@contentful/experience-design-system-generation": "2.20.1-dev-build-938785e.0"
36
+ "@contentful/experience-design-system-extraction": "2.20.1-dev-build-64d16a0.0",
37
+ "@contentful/experience-design-system-types": "2.20.1-dev-build-64d16a0.0",
38
+ "@contentful/experience-design-system-generation": "2.20.1-dev-build-64d16a0.0",
39
+ "@contentful/experience-design-system-client": "2.20.1-dev-build-64d16a0.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@tsconfig/node24": "^24.0.3",