@contentful/experience-design-system-cli 2.12.4-dev-build-9c819d8.0 → 2.13.1-dev-build-847dead.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 (51) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/analyze/command.js +2 -22
  3. package/dist/src/analyze/select/command.js +1 -1
  4. package/dist/src/analyze/select/tui/components/FieldEditor.js +4 -1
  5. package/dist/src/analyze/select-agent/command.js +1 -1
  6. package/dist/src/apply/command.d.ts +1 -5
  7. package/dist/src/apply/command.js +0 -43
  8. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
  9. package/dist/src/import/tui/steps/GenerateReviewStep.js +20 -102
  10. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
  11. package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
  12. package/dist/src/import/tui/steps/preview-diff.js +0 -48
  13. package/dist/src/session/db.d.ts +0 -9
  14. package/dist/src/session/db.js +0 -46
  15. package/dist/src/types.d.ts +2 -76
  16. package/dist/src/types.js +1 -4
  17. package/package.json +3 -6
  18. package/dist/src/analyze/cycle-detection.d.ts +0 -68
  19. package/dist/src/analyze/cycle-detection.js +0 -285
  20. package/dist/src/analyze/extract/astro.d.ts +0 -5
  21. package/dist/src/analyze/extract/astro.js +0 -289
  22. package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
  23. package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
  24. package/dist/src/analyze/extract/pipeline.d.ts +0 -6
  25. package/dist/src/analyze/extract/pipeline.js +0 -314
  26. package/dist/src/analyze/extract/react.d.ts +0 -2
  27. package/dist/src/analyze/extract/react.js +0 -2041
  28. package/dist/src/analyze/extract/scoring.d.ts +0 -12
  29. package/dist/src/analyze/extract/scoring.js +0 -108
  30. package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
  31. package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
  32. package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
  33. package/dist/src/analyze/extract/slot-detection.js +0 -101
  34. package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
  35. package/dist/src/analyze/extract/source-inspection.js +0 -189
  36. package/dist/src/analyze/extract/stencil.d.ts +0 -2
  37. package/dist/src/analyze/extract/stencil.js +0 -296
  38. package/dist/src/analyze/extract/svelte.d.ts +0 -5
  39. package/dist/src/analyze/extract/svelte.js +0 -1626
  40. package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
  41. package/dist/src/analyze/extract/tsx-shared.js +0 -263
  42. package/dist/src/analyze/extract/validate.d.ts +0 -16
  43. package/dist/src/analyze/extract/validate.js +0 -94
  44. package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
  45. package/dist/src/analyze/extract/vue-tsx.js +0 -498
  46. package/dist/src/analyze/extract/vue.d.ts +0 -5
  47. package/dist/src/analyze/extract/vue.js +0 -653
  48. package/dist/src/analyze/extract/web-components.d.ts +0 -2
  49. package/dist/src/analyze/extract/web-components.js +0 -876
  50. package/dist/src/analyze/pre-classify.d.ts +0 -17
  51. package/dist/src/analyze/pre-classify.js +0 -211
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.12.4-dev-build-9c819d8.0",
3
+ "version": "2.13.1-dev-build-847dead.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,16 +37,13 @@
37
37
  "lint:fix": "nx lint:fix experience-design-system-cli"
38
38
  },
39
39
  "dependencies": {
40
+ "@contentful/experience-design-system-extraction": "workspace:*",
40
41
  "@contentful/experience-design-system-types": "workspace:*",
41
- "@vue/compiler-sfc": "^3.5.31",
42
42
  "commander": "^13.1.0",
43
43
  "ink": "^4.4.1",
44
44
  "react": "^18.3.1",
45
45
  "react-devtools-core": "^4.19.1",
46
- "react-dom": "^18.3.1",
47
- "svelte": "^5.56.4",
48
- "ts-morph": "^27.0.2",
49
- "typescript": "^5.9.3"
46
+ "react-dom": "^18.3.1"
50
47
  },
51
48
  "devDependencies": {
52
49
  "@tsconfig/node24": "^24.0.3",
@@ -2,17 +2,11 @@ import { createElement } from 'react';
2
2
  import { render } from 'ink';
3
3
  import { mkdir, readdir, stat } from 'node:fs/promises';
4
4
  import { isAbsolute, join, relative, resolve } from 'node:path';
5
- import { extractComponents } from './extract/pipeline.js';
5
+ import { extractComponents, preClassifyComponent, isNonAuthorableComponent, computeExtractionScore, deriveNeedsReview, describeReviewReasons, inspectComponentSource, validateExtractedComponents, } from '@contentful/experience-design-system-extraction';
6
6
  import { AnalyzeView } from './tui/AnalyzeView.js';
7
7
  import { registerAnalyzeEditCommand } from './select/command.js';
8
8
  import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
9
- import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, storeSlotCycles, } from '../session/db.js';
10
- import { findSlotCycles, suggestCycleBreakEdge } from './cycle-detection.js';
11
- import { preClassifyComponent } from './pre-classify.js';
12
- import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
13
- import { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
14
- import { describeReviewReasons, inspectComponentSource } from './extract/source-inspection.js';
15
- import { validateExtractedComponents } from './extract/validate.js';
9
+ import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, } from '../session/db.js';
16
10
  import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
17
11
  const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
18
12
  const IGNORED_DIRECTORY_NAMES = new Set([
@@ -192,20 +186,6 @@ export function registerAnalyzeCommand(program) {
192
186
  }
193
187
  const validatedComponents = validateExtractedComponents(filteredComponents);
194
188
  storeRawComponents(db, sessionId, validatedComponents);
195
- // INTEG-4401: post-extract slot-dependency cycle detection. The apply
196
- // worker rejects cyclic slot graphs at push time, so we surface the
197
- // same violation locally as a soft warning here (TUI reads
198
- // slot_cycles) and as a hard block at manifest finalization.
199
- const cycleInput = validatedComponents.map((c) => ({
200
- name: c.name,
201
- slots: c.slots.map((s) => ({ name: s.name, allowedComponents: s.allowedComponents })),
202
- }));
203
- const cycles = findSlotCycles(cycleInput);
204
- const withBreaks = cycles.map((cycle) => ({
205
- ...cycle,
206
- suggestedBreak: suggestCycleBreakEdge(cycle, cycles),
207
- }));
208
- storeSlotCycles(db, sessionId, withBreaks);
209
189
  storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
210
190
  updateStep(db, stepId, 'complete', { sessionId });
211
191
  db.close();
@@ -6,7 +6,7 @@ import { getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, sav
6
6
  import { loadReviewInput } from './parser.js';
7
7
  import { App } from './tui/App.js';
8
8
  import { openPipelineDb, loadRawComponents, storeRawComponents, createStep, updateStep } from '../../session/db.js';
9
- import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
9
+ import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
10
10
  const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
11
11
  const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
12
12
  function applyDotPath(obj, path, value) {
@@ -924,7 +924,10 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
924
924
  if (props.length === 0 && slots.length === 0) {
925
925
  return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "yellow", children: [_jsx(Text, { bold: true, color: "yellow", children: "FIELD EDITOR \u2014 no fields" }), _jsx(Text, { color: "yellow", children: "⚠ No properties classified for this component. The LLM didn't find anything to classify." }), _jsx(Text, { dimColor: true, children: "You can add fields manually below or reject this component." }), _jsx(Text, { dimColor: true, children: "Ctrl+S to save \u00B7 Esc to discard" })] }));
926
926
  }
927
- const hasEmptyProperties = props.length === 0 && slots.length === 0;
927
+ // When $properties is empty but $slots exist, surface the same warning
928
+ // prominently in the panel so the user understands why the component looks
929
+ // sparse — and can act on it (manually add a prop or reject).
930
+ const hasEmptyProperties = props.length === 0;
928
931
  const modeLabel = (() => {
929
932
  if (editingValue) {
930
933
  return editingValue.mode === 'add' ? 'Enter to add · Esc to cancel' : 'Enter to save edit · Esc to cancel';
@@ -11,7 +11,7 @@ import { OutputFormatter, c } from '../../output/format.js';
11
11
  import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
12
12
  import { runShowRationale } from './show-rationale.js';
13
13
  import { isAbsolute, resolve } from 'node:path';
14
- import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
14
+ import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
15
15
  const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
16
16
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
17
17
  export const DEFAULT_CONCURRENCY = 10;
@@ -1,10 +1,6 @@
1
1
  import type { Command } from 'commander';
2
- import type { CDFComponentEntry, DTCGTokenEntry } from '@contentful/experience-design-system-types';
2
+ import type { DTCGTokenEntry } from '@contentful/experience-design-system-types';
3
3
  import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
4
4
  export declare function readTokensFromPath(flag: string, p: string): Promise<DTCGTokenEntry[]>;
5
- export declare function assertNoSlotCycles(components: Array<{
6
- key: string;
7
- entry: CDFComponentEntry;
8
- }>): void;
9
5
  export declare function hasBreakingChangesWithImpact(preview: ServerPreviewResponse): boolean;
10
6
  export declare function registerApplyCommand(program: Command): void;
@@ -5,7 +5,6 @@ import { join } from 'node:path';
5
5
  import { validateCDF, flattenDTCG, validateDTCG, buildManifest, buildFilteredManifest, } from '@contentful/experience-design-system-types';
6
6
  import { ApiError, ImportApiClient } from './api-client.js';
7
7
  import { openPipelineDb, loadCDFComponents } from '../session/db.js';
8
- import { findSlotCycles, suggestCycleBreakEdge, formatCyclePath } from '../analyze/cycle-detection.js';
9
8
  import { isEmptyPreview } from './preview-utils.js';
10
9
  import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
11
10
  import { SelectView, makeSelectKey } from './tui/SelectView.js';
@@ -171,37 +170,6 @@ async function resolveSharedInputs(opts) {
171
170
  });
172
171
  return { components, tokens, client };
173
172
  }
174
- // --- INTEG-4401: pre-push slot-cycle hard block ---
175
- //
176
- // The backend apply worker rejects cyclic slot graphs at topo-sort time, but
177
- // by then the request has already been serialized, transported, and partially
178
- // applied — leading to confusing partial-failure states. Detecting the same
179
- // violation locally lets us fail fast with a clear message and zero
180
- // side-effects. Called from the `apply push` and `apply select` flows only;
181
- // `apply preview` is read-only and still runs (its diff is used to warn but
182
- // not to block).
183
- export function assertNoSlotCycles(components) {
184
- const cycleInput = components.map(({ key, entry }) => ({
185
- name: key,
186
- slots: Object.entries(entry.$slots ?? {}).map(([slotName, slotDef]) => ({
187
- name: slotName,
188
- allowedComponents: slotDef.$allowedComponents ?? [],
189
- })),
190
- }));
191
- const cycles = findSlotCycles(cycleInput);
192
- if (cycles.length === 0)
193
- return;
194
- const lines = [];
195
- lines.push(`Error: manifest:components/slot-cycles — ${cycles.length} slot dependency cycle(s) detected. Push refused.`);
196
- for (let i = 0; i < cycles.length; i += 1) {
197
- const cycle = cycles[i];
198
- lines.push(` Cycle ${i + 1}: ${formatCyclePath(cycle)}`);
199
- const suggested = suggestCycleBreakEdge(cycle, cycles);
200
- lines.push(` Fix: remove '${suggested.toComponent}' from ${suggested.fromComponent}.$slots.${suggested.slotName}.$allowedComponents`);
201
- }
202
- process.stderr.write(lines.join('\n') + '\n');
203
- process.exit(1);
204
- }
205
173
  // --- Output helpers ---
206
174
  export function hasBreakingChangesWithImpact(preview) {
207
175
  const allChanged = [...preview.components.changed, ...preview.tokens.changed];
@@ -477,12 +445,6 @@ export function registerApplyCommand(program) {
477
445
  throw e;
478
446
  }
479
447
  const { components, tokens, client } = inputs;
480
- // INTEG-4401: refuse before spending any credentials or bytes on a
481
- // manifest the backend will reject. Cycle detection is deterministic
482
- // and cheap so we run it here (not just at extract time) — this also
483
- // guards `apply push --components` where extract-time cycles are not
484
- // in the session DB.
485
- assertNoSlotCycles(components);
486
448
  try {
487
449
  await client.validateToken();
488
450
  }
@@ -661,11 +623,6 @@ export function registerApplyCommand(program) {
661
623
  throw e;
662
624
  }
663
625
  const { components, tokens, client } = inputs;
664
- // INTEG-4401: same pre-push cycle block as `apply push`. Running the
665
- // check on the FULL component set (not the filtered selection) is
666
- // deliberate: partial selection cannot resolve a cycle whose
667
- // participants are all still present in the target space.
668
- assertNoSlotCycles(components);
669
626
  try {
670
627
  await client.validateToken();
671
628
  }
@@ -37,6 +37,6 @@ type GenerateReviewStepProps = {
37
37
  export declare function sortComponentsForSidebar<T extends {
38
38
  key: string;
39
39
  entry: CDFComponentEntry;
40
- }>(components: T[], cycleParticipants?: Set<string>): T[];
40
+ }>(components: T[]): T[];
41
41
  export declare function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }: GenerateReviewStepProps): React.ReactElement;
42
42
  export {};
@@ -8,8 +8,7 @@ import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
8
8
  import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
9
9
  import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
10
10
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
11
- import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, loadSlotCycles, } from '../../../session/db.js';
12
- import { formatCyclePath } from '../../../analyze/cycle-detection.js';
11
+ import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, } from '../../../session/db.js';
13
12
  import { RationalePanel } from '../../../analyze/select/tui/components/RationalePanel.js';
14
13
  import { ComponentRationalePanel } from '../../../analyze/select/tui/components/ComponentRationalePanel.js';
15
14
  import { applyPreviewAnnotations } from '../../../analyze/select/preview-annotations.js';
@@ -25,22 +24,12 @@ import { computeNextScrollOffset } from '../../../analyze/select/tui/hooks/scrol
25
24
  *
26
25
  * Within each tier (empty / non-empty) we tie-break alphabetically by `key`.
27
26
  */
28
- export function sortComponentsForSidebar(components, cycleParticipants) {
29
- const isEmpty = (entry) => Object.keys(entry.$properties ?? {}).length === 0 && Object.keys(entry.$slots ?? {}).length === 0;
30
- // Tier order: cycle members first (they block push — surface loudest),
31
- // then empty (soft warning), then everything else. Ties broken alpha.
32
- const tier = (c) => {
33
- if (cycleParticipants?.has(c.key))
34
- return 0;
35
- if (isEmpty(c.entry))
36
- return 1;
37
- return 2;
38
- };
27
+ export function sortComponentsForSidebar(components) {
39
28
  return [...components].sort((a, b) => {
40
- const at = tier(a);
41
- const bt = tier(b);
42
- if (at !== bt)
43
- return at - bt;
29
+ const aEmpty = Object.keys(a.entry.$properties ?? {}).length === 0;
30
+ const bEmpty = Object.keys(b.entry.$properties ?? {}).length === 0;
31
+ if (aEmpty !== bEmpty)
32
+ return aEmpty ? -1 : 1;
44
33
  return a.key.localeCompare(b.key);
45
34
  });
46
35
  }
@@ -86,12 +75,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
86
75
  // Tracks the first `g` of a potential `gg` double-tap (jumps to top in
87
76
  // JSON-view + panel-focused state). Reset on any non-`g` key.
88
77
  const pendingGRef = useRef(false);
89
- // INTEG-4401: slot-dependency cycles loaded from the session DB. Non-empty
90
- // triggers sidebar (cycle) badges, banner + [c] detail-panel affordance,
91
- // and (at push time) a hard block.
92
- const [slotCycles, setSlotCycles] = useState([]);
93
- const [showCyclePanel, setShowCyclePanel] = useState(false);
94
- const [cyclePanelScroll, setCyclePanelScroll] = useState(0);
95
78
  const handleLivePreviewResult = (response) => {
96
79
  if (!response)
97
80
  return;
@@ -123,10 +106,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
123
106
  async function load() {
124
107
  const db = openPipelineDb();
125
108
  let cdfComponents;
126
- let cycles = [];
127
109
  try {
128
110
  cdfComponents = loadCDFComponents(db, extractSessionId);
129
- cycles = loadSlotCycles(db, extractSessionId);
130
111
  }
131
112
  finally {
132
113
  db.close();
@@ -141,12 +122,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
141
122
  entry,
142
123
  status: 'needs-review',
143
124
  }));
144
- const cycleParticipants = new Set();
145
- for (const c of cycles)
146
- for (const p of c.path)
147
- cycleParticipants.add(p);
148
- setSlotCycles(cycles);
149
- setComponents(sortComponentsForSidebar(reviewEntries, cycleParticipants));
125
+ setComponents(sortComponentsForSidebar(reviewEntries));
150
126
  setLoading(false);
151
127
  }
152
128
  load().catch((e) => {
@@ -306,31 +282,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
306
282
  }
307
283
  return;
308
284
  }
309
- // INTEG-4401: slot-cycle detail panel. Same modal-swallow rules as
310
- // showRemovedPanel; q/Esc close, ↑↓ scroll.
311
- if (showCyclePanel) {
312
- if (input === 'c' || input === 'q' || key.escape) {
313
- setShowCyclePanel(false);
314
- setCyclePanelScroll(0);
315
- return;
316
- }
317
- if (key.upArrow || input === 'k') {
318
- setCyclePanelScroll((v) => Math.max(0, v - 1));
319
- return;
320
- }
321
- if (key.downArrow || input === 'j') {
322
- setCyclePanelScroll((v) => v + 1);
323
- return;
324
- }
325
- return;
326
- }
327
- // Open the cycle panel from sidebar-focused state when there is at
328
- // least one cycle to display.
329
- if (input === 'c' && sidebarFocused && slotCycles.length > 0) {
330
- setShowCyclePanel(true);
331
- setCyclePanelScroll(0);
332
- return;
333
- }
334
285
  // `d` opens the panel only when live-preview is enabled and there is at
335
286
  // least one removed component to display. Sidebar-focused only so it
336
287
  // doesn't collide with FieldEditor input.
@@ -522,34 +473,26 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
522
473
  }
523
474
  const selected = components[selectedIdx] ?? null;
524
475
  const selectedJson = selected ? JSON.stringify({ [selected.key]: selected.entry }, null, 2) : '';
525
- const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0 && Object.keys(c.entry.$slots ?? {}).length === 0;
476
+ // A component with zero classified $properties is a real defensibility issue
477
+ // it can't be pushed to Contentful (no fields). Surface it in the sidebar via
478
+ // the existing warning-color path (yellow) and a "(empty)" suffix so the user
479
+ // can see what went wrong. They can manually add props in FieldEditor or
480
+ // explicitly reject the component.
481
+ const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0;
526
482
  const emptyCount = components.filter(isEmpty).length;
527
- // INTEG-4401: cycle-participant set drives sidebar `(cycle)` badges plus
528
- // the banner counts. Recomputed on every render — cheap for typical N.
529
- const cycleParticipantSet = new Set();
530
- for (const cycle of slotCycles)
531
- for (const p of cycle.path)
532
- cycleParticipantSet.add(p);
533
- const sidebarSuffix = (c) => {
534
- if (cycleParticipantSet.has(c.key))
535
- return ' (cycle)';
536
- if (isEmpty(c))
537
- return ' (empty)';
538
- return '';
539
- };
540
483
  const sidebarItems = components.map((c) => ({
541
484
  id: c.key,
542
- name: `${c.key}${sidebarSuffix(c)}`,
485
+ name: isEmpty(c) ? `${c.key} (empty)` : c.key,
543
486
  status: c.status,
544
487
  previewAnnotation: previewAnnotations.get(c.key),
545
488
  extractionConfidence: null,
546
489
  needsReview: false,
547
490
  validationErrorCount: 0,
548
- validationWarningCount: sidebarSuffix(c) !== '' ? 1 : 0,
491
+ validationWarningCount: isEmpty(c) ? 1 : 0,
549
492
  }));
550
- // Account for the "(cycle)" / "(empty)" suffix added to badged component
551
- // names so the sidebar doesn't truncate them.
552
- const longestName = components.reduce((m, c) => Math.max(m, c.key.length + sidebarSuffix(c).length), 0);
493
+ // Account for the "(empty)" suffix added to zero-prop component names so the
494
+ // sidebar doesn't truncate it.
495
+ const longestName = components.reduce((m, c) => Math.max(m, c.key.length + (isEmpty(c) ? ' (empty)'.length : 0)), 0);
553
496
  // +5 = border (1) + status icon (1) + badge column (1) + space (1) + border (1).
554
497
  // The badge column is reserved even when no annotation is present so the
555
498
  // sidebar width doesn't jitter as live-preview annotations flip in/out.
@@ -560,31 +503,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
560
503
  const needsReview = components.filter((c) => c.status === 'needs-review').length;
561
504
  const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
562
505
  const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
563
- return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showRemovedPanel && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: `Removed components (${removedComponents.length})` }), _jsx(Text, { dimColor: true, children: "these will be DELETED from the target space" }), _jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: `- ${rc.name}${rc.id ? ` (${rc.id})` : ''}` }, rc.id))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "press d or Esc to close" })] })), showCyclePanel &&
564
- !dialogOpen &&
565
- (() => {
566
- // Materialize the full panel body as a flat list of Text lines,
567
- // then slice by cyclePanelScroll so ↑↓ can walk arbitrarily long
568
- // content. Each cycle contributes 3-4 lines: heading, path,
569
- // suggested fix (if any), and a blank separator.
570
- const PANEL_H = 20;
571
- const lines = [];
572
- lines.push(_jsx(Text, { bold: true, color: "yellow", children: `SLOT DEPENDENCY CYCLES (${slotCycles.length})` }, "cyc-title"));
573
- lines.push(_jsx(Text, { dimColor: true, children: 'push will fail until these are resolved' }, "cyc-sub"));
574
- lines.push(_jsx(Text, { children: " " }, "cyc-space"));
575
- slotCycles.forEach((cycle, idx) => {
576
- const nodeCount = new Set(cycle.path).size;
577
- lines.push(_jsx(Text, { bold: true, children: `▸ Cycle ${idx + 1} (${nodeCount} component${nodeCount === 1 ? '' : 's'}):` }, `cyc-h-${idx}`));
578
- lines.push(_jsx(Text, { children: ` ${formatCyclePath(cycle, 16)}` }, `cyc-p-${idx}`));
579
- if (cycle.suggestedBreak) {
580
- const b = cycle.suggestedBreak;
581
- lines.push(_jsx(Text, { dimColor: true, children: ` Suggested fix: remove '${b.toComponent}' from ${b.fromComponent}.$slots.${b.slotName}.$allowedComponents` }, `cyc-f-${idx}`));
582
- }
583
- lines.push(_jsx(Text, { children: " " }, `cyc-s-${idx}`));
584
- });
585
- const visible = lines.slice(cyclePanelScroll, cyclePanelScroll + PANEL_H);
586
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [visible, _jsx(Text, { dimColor: true, children: '[↑↓/j/k] scroll [c/q/Esc] close' })] }));
587
- })(), !dialogOpen &&
506
+ return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showRemovedPanel && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: `Removed components (${removedComponents.length})` }), _jsx(Text, { dimColor: true, children: "these will be DELETED from the target space" }), _jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: `- ${rc.name}${rc.id ? ` (${rc.id})` : ''}` }, rc.id))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "press d or Esc to close" })] })), !dialogOpen &&
588
507
  livePreview &&
589
508
  (() => {
590
509
  // Pilot-2026-06-23 R2: at-a-glance diff summary at the top of the
@@ -607,7 +526,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
607
526
  if (!hasCounts)
608
527
  return null;
609
528
  return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color: "green", children: `${counts.new} new` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "yellow", children: `${counts.changed} changed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { dimColor: true, children: `${counts.removed} removed` }), removedComponents.length > 0 && _jsx(Text, { dimColor: true, children: ' ([d] removed list)' }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "red", bold: true, children: `${counts.breaking} breaking` })] }));
610
- })(), !dialogOpen && slotCycles.length > 0 && !showCyclePanel && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: `⚠ ${slotCycles.length} slot dependency cycle${slotCycles.length === 1 ? '' : 's'} detected — push will fail` }), slotCycles.slice(0, 3).map((cycle, idx) => (_jsx(Text, { color: "yellow", children: ` Cycle: ${formatCyclePath(cycle)}` }, `cyc-banner-${idx}`))), slotCycles.length > 3 && _jsx(Text, { color: "yellow", children: ` …${slotCycles.length - 3} more` }), _jsx(Text, { dimColor: true, children: ' press [c] for detail' })] })), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: "red", children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
529
+ })(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: "red", children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
611
530
  const idx = components.findIndex((c) => c.key === id);
612
531
  if (idx >= 0) {
613
532
  setSelectedIdx(idx);
@@ -661,7 +580,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
661
580
  (showJson ? 'hide JSON' : 'show JSON') +
662
581
  ' [F] finalize [e/Tab] focus panel' +
663
582
  (livePreview && removedComponents.length > 0 ? ' [d] removed list' : '') +
664
- (slotCycles.length > 0 ? ' [c] cycles' : '') +
665
583
  ' [q] quit'
666
584
  : showJson
667
585
  ? ' [j/k] scroll [Ctrl+u/d] half-page [gg/G] top/bottom [Tab] focus list'
@@ -1,15 +1,5 @@
1
1
  import React from 'react';
2
2
  import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
3
- export interface PreviewDiffLine {
4
- key: string;
5
- color: 'green' | 'red' | 'yellow' | 'gray';
6
- text: string;
7
- }
8
- /**
9
- * Pure builder for the wizard preview diff lines. Extracted from the render
10
- * layer so it can be unit-tested independently of Ink.
11
- */
12
- export declare function buildPreviewDiffLines(preview: ServerPreviewResponse): PreviewDiffLine[];
13
3
  type WizardPreviewStepProps = {
14
4
  preview: ServerPreviewResponse;
15
5
  spaceId: string;
@@ -1,99 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useMemo } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
4
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
5
5
  import { hasBreakingChangesWithImpact } from '../../../apply/manifest.js';
6
6
  import { computeComponentDiffLines } from './preview-diff.js';
7
- /**
8
- * Pure builder for the wizard preview diff lines. Extracted from the render
9
- * layer so it can be unit-tested independently of Ink.
10
- */
11
- export function buildPreviewDiffLines(preview) {
12
- const lines = [];
13
- const { components, tokens } = preview;
14
- for (const item of components.new) {
15
- const raw = item;
16
- const name = raw.key ?? raw.$name ?? 'unknown';
17
- lines.push({ key: `comp-new-${name}`, color: 'green', text: ` + ${name}` });
18
- const slots = (raw.$slots ?? {});
19
- for (const slotName of Object.keys(slots).sort()) {
20
- lines.push({
21
- key: `comp-new-${name}-slot-${slotName}`,
22
- color: 'green',
23
- text: ` slot: ${slotName}`,
24
- });
25
- const allowed = slots[slotName]?.['$allowedComponents'];
26
- if (Array.isArray(allowed) && allowed.length > 0) {
27
- const names = allowed.filter((n) => typeof n === 'string');
28
- lines.push({
29
- key: `comp-new-${name}-slot-${slotName}-allow`,
30
- color: 'green',
31
- text: ` allowedComponents: [${names.join(', ')}]`,
32
- });
33
- }
34
- }
35
- }
36
- for (const item of components.removed) {
37
- lines.push({ key: `comp-rm-${item.name}`, color: 'red', text: ` - ${item.name}` });
38
- }
39
- for (const item of components.changed) {
40
- lines.push({
41
- key: `comp-h-${item.current.name}`,
42
- color: 'yellow',
43
- text: ` ~ ${item.current.name}${item.hasPendingDraftChanges ? ' ⚡ has pending draft changes' : ''}`,
44
- });
45
- if (item.changeClassification?.classification === 'breaking') {
46
- const reasons = item.changeClassification.breakingChanges
47
- .map((bc) => `${bc.propertyId}: ${bc.reason}`)
48
- .join(', ');
49
- lines.push({ key: `comp-b-${item.current.name}`, color: 'red', text: ` ⚠ BREAKING: ${reasons}` });
50
- }
51
- const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
52
- for (const d of diffLines) {
53
- lines.push({ key: `comp-d-${item.current.name}-${d.key}`, color: d.color, text: ` ${d.text}` });
54
- }
55
- // Also enumerate slots + allowedComponents from the proposed side so the
56
- // reviewer sees the shape they're pushing, not just the delta.
57
- const proposedSlots = item.proposed['$slots'] ?? {};
58
- for (const slotName of Object.keys(proposedSlots).sort()) {
59
- const allowed = proposedSlots[slotName]?.['$allowedComponents'];
60
- if (Array.isArray(allowed) && allowed.length > 0) {
61
- const names = allowed.filter((n) => typeof n === 'string');
62
- const key = `comp-d-${item.current.name}-slot-${slotName}-allow-list`;
63
- if (!lines.some((l) => l.key === key)) {
64
- lines.push({
65
- key,
66
- color: 'gray',
67
- text: ` slot ${slotName} allowedComponents: [${names.join(', ')}]`,
68
- });
69
- }
70
- }
71
- }
72
- }
73
- for (const item of tokens.new) {
74
- const raw = item;
75
- const name = raw.name ?? raw.path ?? 'unknown';
76
- lines.push({ key: `tok-new-${name}`, color: 'green', text: ` + ${name}` });
77
- }
78
- for (const item of tokens.removed) {
79
- lines.push({ key: `tok-rm-${item.name}`, color: 'red', text: ` - ${item.name}` });
80
- }
81
- for (const item of tokens.changed) {
82
- const tokenName = item.current.name;
83
- lines.push({
84
- key: `tok-h-${tokenName}`,
85
- color: 'yellow',
86
- text: ` ~ ${tokenName}${item.hasPendingDraftChanges ? ' ⚡ has pending draft changes' : ''}`,
87
- });
88
- if (item.changeClassification?.classification === 'breaking') {
89
- const reasons = item.changeClassification.breakingChanges
90
- .map((bc) => `${bc.propertyId}: ${bc.reason}`)
91
- .join(', ');
92
- lines.push({ key: `tok-b-${tokenName}`, color: 'red', text: ` ⚠ BREAKING: ${reasons}` });
93
- }
94
- }
95
- return lines;
96
- }
97
7
  export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }) {
98
8
  const breakingWithImpact = hasBreakingChangesWithImpact(preview);
99
9
  const [diffExpanded, setDiffExpanded] = useState(false);
@@ -104,10 +14,56 @@ export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber,
104
14
  const allDiffLines = useMemo(() => {
105
15
  if (!diffExpanded)
106
16
  return [];
107
- return buildPreviewDiffLines(preview).map((line) => ({
108
- key: line.key,
109
- element: (_jsx(Text, { color: line.color === 'gray' ? undefined : line.color, dimColor: line.color === 'gray', children: line.text })),
110
- }));
17
+ const lines = [];
18
+ const { components, tokens } = preview;
19
+ for (const item of components.new) {
20
+ const name = item.key ??
21
+ item.$name ??
22
+ 'unknown';
23
+ lines.push({ key: `comp-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
24
+ }
25
+ for (const item of components.removed) {
26
+ lines.push({ key: `comp-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
27
+ }
28
+ for (const item of components.changed) {
29
+ lines.push({
30
+ key: `comp-h-${item.current.name}`,
31
+ element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", item.current.name, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
32
+ });
33
+ if (item.changeClassification?.classification === 'breaking') {
34
+ const reasons = item.changeClassification.breakingChanges
35
+ .map((bc) => `${bc.propertyId}: ${bc.reason}`)
36
+ .join(', ');
37
+ lines.push({ key: `comp-b-${item.current.name}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
38
+ }
39
+ const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
40
+ for (const d of diffLines) {
41
+ lines.push({ key: `comp-d-${item.current.name}-${d.key}`, element: _jsxs(Text, { color: d.color, children: [" ", d.text] }) });
42
+ }
43
+ }
44
+ for (const item of tokens.new) {
45
+ const name = item.name ??
46
+ item.path ??
47
+ 'unknown';
48
+ lines.push({ key: `tok-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
49
+ }
50
+ for (const item of tokens.removed) {
51
+ lines.push({ key: `tok-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
52
+ }
53
+ for (const item of tokens.changed) {
54
+ const tokenName = item.current.name;
55
+ lines.push({
56
+ key: `tok-h-${tokenName}`,
57
+ element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", tokenName, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
58
+ });
59
+ if (item.changeClassification?.classification === 'breaking') {
60
+ const reasons = item.changeClassification.breakingChanges
61
+ .map((bc) => `${bc.propertyId}: ${bc.reason}`)
62
+ .join(', ');
63
+ lines.push({ key: `tok-b-${tokenName}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
64
+ }
65
+ }
66
+ return lines;
111
67
  }, [diffExpanded, preview]);
112
68
  const maxScroll = Math.max(0, allDiffLines.length - viewportHeight);
113
69
  useImmediateInput((input, key) => {