@contentful/experience-design-system-cli 2.23.2-dev-build-5146e8a.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.
package/README.md CHANGED
@@ -214,7 +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. Forwarded to headless subprocess pushes and `--push-from-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`. |
218
218
  | `--dry-run` | _(deprecated)_ | Deprecated alias for `--print-prompt`. Emits a stderr deprecation notice; prompt-print semantics will be removed in a future release. |
219
219
 
220
220
  ### Run-picker at wizard start
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-5146e8a.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",
@@ -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 ?? '';
@@ -51,7 +51,7 @@ export declare class ImportApiClient {
51
51
  private requestWithRetry;
52
52
  validateToken(): Promise<void>;
53
53
  checkPreflight(): Promise<void>;
54
- previewImport(manifest: ManifestPayload): Promise<ServerPreviewResponse>;
54
+ previewImport(manifest: ManifestPayload, allowDeletions?: boolean): Promise<ServerPreviewResponse>;
55
55
  applyImport(manifest: ManifestPayload, options: {
56
56
  acknowledgeBreakingChanges: boolean;
57
57
  allowDeletions?: boolean;
@@ -307,19 +307,20 @@ export class ImportApiClient {
307
307
  return; // legacy backend without the endpoint
308
308
  throw new ApiError(`preflight failed: ${res.status}`, res.status, body);
309
309
  }
310
- async previewImport(manifest) {
310
+ async previewImport(manifest, allowDeletions = false) {
311
311
  const debug = getDebugLogger();
312
312
  const startedAt = Date.now();
313
313
  debug.event('apply', 'preview.request', {
314
314
  url: `${this.base()}/design_systems/imports/preview`,
315
315
  componentCount: manifest.components?.length ?? 0,
316
316
  tokenCount: manifest.designTokens?.length ?? 0,
317
+ allowDeletions,
317
318
  });
318
319
  const result = await this.requestWithRetry('preview', PREVIEW_ERROR_PREFIX, () => designSystemImportSourcelessPreview({
319
320
  baseUrl: this.host,
320
321
  headers: this.headers(),
321
322
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
322
- body: manifest,
323
+ body: { ...manifest, allowDeletions },
323
324
  parseAs: 'json',
324
325
  }));
325
326
  if (!result.response.ok) {
@@ -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
  }
@@ -503,7 +504,7 @@ export function registerApplyCommand(program) {
503
504
  const manifest = buildManifest(components, tokens);
504
505
  let preview;
505
506
  try {
506
- preview = await client.previewImport(manifest);
507
+ preview = await client.previewImport(manifest, opts.allowDeletions === true);
507
508
  }
508
509
  catch (e) {
509
510
  if (e instanceof ApiError)
@@ -518,6 +519,7 @@ export function registerApplyCommand(program) {
518
519
  preview,
519
520
  spaceId,
520
521
  environmentId,
522
+ allowDeletions: opts.allowDeletions === true,
521
523
  }));
522
524
  await waitUntilExit();
523
525
  }
@@ -573,7 +575,7 @@ export function registerApplyCommand(program) {
573
575
  return;
574
576
  }
575
577
  await new Promise((resolvePromise) => {
576
- const runApply = async (acknowledge) => {
578
+ const runApply = async (acknowledge, applyDeletions) => {
577
579
  instance.rerender(createElement(ServerApplyProgress, {
578
580
  spaceId,
579
581
  environmentId,
@@ -583,7 +585,7 @@ export function registerApplyCommand(program) {
583
585
  try {
584
586
  operation = await client.applyImport(manifest, {
585
587
  acknowledgeBreakingChanges: acknowledge,
586
- allowDeletions: opts.allowDeletions === true,
588
+ allowDeletions: applyDeletions,
587
589
  });
588
590
  }
589
591
  catch (e) {
@@ -632,8 +634,9 @@ export function registerApplyCommand(program) {
632
634
  spaceId,
633
635
  environmentId,
634
636
  breakingWithImpact,
635
- onConfirm: (acknowledge) => {
636
- void runApply(acknowledge);
637
+ allowDeletions: opts.allowDeletions === true,
638
+ onConfirm: (acknowledge, applyDeletions) => {
639
+ void runApply(acknowledge, applyDeletions);
637
640
  },
638
641
  onCancel: () => {
639
642
  process.exit(0);
@@ -677,7 +680,7 @@ export function registerApplyCommand(program) {
677
680
  const fullManifest = buildManifest(components, tokens);
678
681
  let preview;
679
682
  try {
680
- preview = await client.previewImport(fullManifest);
683
+ preview = await client.previewImport(fullManifest, opts.allowDeletions === true);
681
684
  }
682
685
  catch (e) {
683
686
  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
  }
@@ -830,7 +830,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
830
830
  tokens = await readTokensFromPath('tokens', tokensPath);
831
831
  }
832
832
  let manifest = buildManifest(components, tokens, { deleteAllComponents: allowEmptyDeleteAllRef.current });
833
- let preview = await client.previewImport(manifest);
833
+ let preview = await client.previewImport(manifest, allowDeletions);
834
834
  if (extractSessionId) {
835
835
  let needsRepreview = false;
836
836
  const db = openPipelineDb();
@@ -852,7 +852,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
852
852
  if (needsRepreview) {
853
853
  components = loadCDFComponents(db, extractSessionId);
854
854
  manifest = buildManifest(components, tokens, { deleteAllComponents: allowEmptyDeleteAllRef.current });
855
- preview = await client.previewImport(manifest);
855
+ preview = await client.previewImport(manifest, allowDeletions);
856
856
  }
857
857
  }
858
858
  finally {
@@ -946,7 +946,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
946
946
  });
947
947
  }
948
948
  };
949
- const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, preview) => {
949
+ const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, allowDeletions, preview) => {
950
950
  if (shouldRefusePush(state)) {
951
951
  update(buildSkippedPushTransition());
952
952
  return;
@@ -1437,7 +1437,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1437
1437
  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
1438
  }
1439
1439
  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) => {
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, allowDeletions: allowDeletions, onFinalize: (accepted, rejected, unresolved) => {
1441
1441
  process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected} Unresolved: ${unresolved}\n`);
1442
1442
  let acceptedCount = accepted;
1443
1443
  const detectAcceptedCycles = () => {
@@ -1605,8 +1605,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1605
1605
  // present-but-empty (only $schema), so editing would dead-end on the
1606
1606
  // "No generated definitions found" screen.
1607
1607
  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);
1608
+ return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, allowDeletions: allowDeletions, onConfirm: (acknowledge, deleteMissing) => {
1609
+ void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, state.host, acknowledge, deleteMissing, state.serverPreview);
1610
1610
  }, ...(editableComponentCount > 0 ? { onEdit: () => void runEditFromPreview() } : {}), onSaveFiles: () => {
1611
1611
  void startSaveFlow();
1612
1612
  }, 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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.23.2-dev-build-5146e8a.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",
@@ -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-client": "2.23.2-dev-build-5146e8a.0",
37
- "@contentful/experience-design-system-types": "2.23.2-dev-build-5146e8a.0",
38
- "@contentful/experience-design-system-generation": "2.23.2-dev-build-5146e8a.0",
39
- "@contentful/experience-design-system-extraction": "2.23.2-dev-build-5146e8a.0"
36
+ "@contentful/experience-design-system-client": "2.23.2-dev-build-09d87bb.0",
37
+ "@contentful/experience-design-system-generation": "2.23.2-dev-build-09d87bb.0",
38
+ "@contentful/experience-design-system-types": "2.23.2-dev-build-09d87bb.0",
39
+ "@contentful/experience-design-system-extraction": "2.23.2-dev-build-09d87bb.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@tsconfig/node24": "^24.0.4",