@contentful/experience-design-system-cli 2.12.4-dev-build-9c819d8.0 → 2.12.4-dev-build-ff57340.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/dist/package.json +1 -1
- package/dist/src/analyze/cycle-detection.d.ts +25 -0
- package/dist/src/analyze/cycle-detection.js +41 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +24 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +136 -6
- package/dist/src/apply/command.d.ts +29 -0
- package/dist/src/apply/command.js +44 -6
- package/dist/src/apply/error-parser.d.ts +43 -0
- package/dist/src/apply/error-parser.js +163 -0
- package/dist/src/import/tui/WizardApp.js +59 -10
- package/dist/src/import/tui/steps/GenerateReviewStep.js +115 -7
- package/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -64,5 +64,30 @@ export declare function suggestCycleBreakEdge(cycle: SlotCycle, allCycles: SlotC
|
|
|
64
64
|
* Truncates at `maxHops` edges (default 8), inserting `…` mid-path so the
|
|
65
65
|
* beginning and end remain visible. A hop is one edge; the returned string
|
|
66
66
|
* for an n-edge cycle contains n+1 component names.
|
|
67
|
+
*
|
|
68
|
+
* Kept for backend/log/error use where a single string is required (see
|
|
69
|
+
* `assertNoSlotCycles` in `apply/command.ts`). The TUI banner + detail panel
|
|
70
|
+
* use `formatCyclePathSegments` instead so slot names can be visually
|
|
71
|
+
* distinguished from component names.
|
|
67
72
|
*/
|
|
68
73
|
export declare function formatCyclePath(cycle: SlotCycle, maxHops?: number): string;
|
|
74
|
+
/**
|
|
75
|
+
* A single token in the rendered cycle path — either a component name, a
|
|
76
|
+
* slot name (wrapped in brackets for monochrome legibility), or the arrow
|
|
77
|
+
* separator between them. The TUI colorizes each kind independently.
|
|
78
|
+
*
|
|
79
|
+
* Slots are rendered as `[slotName]` so the visual distinction survives when
|
|
80
|
+
* ANSI colors are stripped (log files, redirection, dumb terminals).
|
|
81
|
+
*/
|
|
82
|
+
export interface CyclePathSegment {
|
|
83
|
+
kind: 'component' | 'slot' | 'arrow';
|
|
84
|
+
text: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Segment-oriented counterpart to `formatCyclePath`. Preserves the same
|
|
88
|
+
* 8-hop truncation semantics but returns each token classified so the
|
|
89
|
+
* renderer can color slots (cyan) and components (default) independently.
|
|
90
|
+
* The truncation marker `…` is emitted as a bare component segment because
|
|
91
|
+
* it stands in for one or more component nodes.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatCyclePathSegments(cycle: SlotCycle, maxHops?: number): CyclePathSegment[];
|
|
@@ -260,6 +260,11 @@ export function suggestCycleBreakEdge(cycle, allCycles) {
|
|
|
260
260
|
* Truncates at `maxHops` edges (default 8), inserting `…` mid-path so the
|
|
261
261
|
* beginning and end remain visible. A hop is one edge; the returned string
|
|
262
262
|
* for an n-edge cycle contains n+1 component names.
|
|
263
|
+
*
|
|
264
|
+
* Kept for backend/log/error use where a single string is required (see
|
|
265
|
+
* `assertNoSlotCycles` in `apply/command.ts`). The TUI banner + detail panel
|
|
266
|
+
* use `formatCyclePathSegments` instead so slot names can be visually
|
|
267
|
+
* distinguished from component names.
|
|
263
268
|
*/
|
|
264
269
|
export function formatCyclePath(cycle, maxHops = 8) {
|
|
265
270
|
const arrow = ' → ';
|
|
@@ -283,3 +288,39 @@ export function formatCyclePath(cycle, maxHops = 8) {
|
|
|
283
288
|
const tail = parts[parts.length - 1];
|
|
284
289
|
return `${head}${arrow}…${arrow}${tail}`;
|
|
285
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Segment-oriented counterpart to `formatCyclePath`. Preserves the same
|
|
293
|
+
* 8-hop truncation semantics but returns each token classified so the
|
|
294
|
+
* renderer can color slots (cyan) and components (default) independently.
|
|
295
|
+
* The truncation marker `…` is emitted as a bare component segment because
|
|
296
|
+
* it stands in for one or more component nodes.
|
|
297
|
+
*/
|
|
298
|
+
export function formatCyclePathSegments(cycle, maxHops = 8) {
|
|
299
|
+
// Interleaved [component, slot, component, slot, ..., component] token list
|
|
300
|
+
// matching `formatCyclePath`'s parts array, but classified.
|
|
301
|
+
const raw = [];
|
|
302
|
+
for (let i = 0; i < cycle.edges.length; i += 1) {
|
|
303
|
+
raw.push({ kind: 'component', text: cycle.path[i] });
|
|
304
|
+
raw.push({ kind: 'slot', text: `[${cycle.edges[i].slotName}]` });
|
|
305
|
+
}
|
|
306
|
+
raw.push({ kind: 'component', text: cycle.path[cycle.path.length - 1] });
|
|
307
|
+
const withArrows = (segs) => {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (let i = 0; i < segs.length; i += 1) {
|
|
310
|
+
if (i > 0)
|
|
311
|
+
out.push({ kind: 'arrow', text: ' → ' });
|
|
312
|
+
out.push(segs[i]);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
};
|
|
316
|
+
if (cycle.edges.length <= maxHops) {
|
|
317
|
+
return withArrows(raw);
|
|
318
|
+
}
|
|
319
|
+
// Match `formatCyclePath` truncation: keep the first (maxHops-1)*2 tokens
|
|
320
|
+
// plus a "…" placeholder plus the trailing component.
|
|
321
|
+
const keepHops = Math.max(1, maxHops - 1);
|
|
322
|
+
const keepTokens = keepHops * 2;
|
|
323
|
+
const head = raw.slice(0, keepTokens);
|
|
324
|
+
const tail = raw[raw.length - 1];
|
|
325
|
+
return withArrows([...head, { kind: 'component', text: '…' }, tail]);
|
|
326
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { findSlotCycles, type ComponentSlotInfo } from '../../../cycle-detection.js';
|
|
2
3
|
/** Per-prop / per-component metadata captured by the extractor + generate phase.
|
|
3
4
|
* Optional; when omitted, the FieldEditor renders without rationale/source
|
|
4
5
|
* affordances (backwards-compatible with mounts that pre-date Feature 1). */
|
|
@@ -63,6 +64,28 @@ type FieldEditorProps = {
|
|
|
63
64
|
* description editors, string default editors, and value-list text entry.
|
|
64
65
|
*/
|
|
65
66
|
onTextEntryActiveChange?: (active: boolean) => void;
|
|
67
|
+
/**
|
|
68
|
+
* INTEG-4401: project-wide slot graph. When supplied, the
|
|
69
|
+
* $allowedComponents add-mode shows a cycle-filtered picker. Optional.
|
|
70
|
+
*/
|
|
71
|
+
projectSlotGraph?: ComponentSlotInfo[];
|
|
72
|
+
/**
|
|
73
|
+
* INTEG-4401: name of the component being edited. Self-loop guard +
|
|
74
|
+
* self-entry replacement in the cycle simulation.
|
|
75
|
+
*/
|
|
76
|
+
currentComponentName?: string;
|
|
66
77
|
};
|
|
67
|
-
|
|
78
|
+
/** Simulate adding a candidate to a slot; replaces self-entry with live edits. */
|
|
79
|
+
export declare function simulateGraphWithCandidate(projectSlotGraph: ComponentSlotInfo[], selfName: string, currentSlots: {
|
|
80
|
+
name: string;
|
|
81
|
+
allowedComponents: string[];
|
|
82
|
+
}[], targetSlotName: string, candidate: string): ComponentSlotInfo[];
|
|
83
|
+
/** True iff `next` contains a cycle whose canonical edge set is absent in `before`. */
|
|
84
|
+
export declare function introducesNewCycle(before: ReturnType<typeof findSlotCycles>, next: ReturnType<typeof findSlotCycles>): boolean;
|
|
85
|
+
/** Candidate names that can be added to a slot without creating a new cycle. */
|
|
86
|
+
export declare function computeAllowedComponentCandidates(projectSlotGraph: ComponentSlotInfo[], selfName: string, currentSlots: {
|
|
87
|
+
name: string;
|
|
88
|
+
allowedComponents: string[];
|
|
89
|
+
}[], targetSlotName: string): string[];
|
|
90
|
+
export declare function FieldEditor({ value, width, height, active, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, projectSlotGraph, currentComponentName, }: FieldEditorProps): React.ReactElement;
|
|
68
91
|
export {};
|
|
@@ -4,6 +4,7 @@ import { Box, Text } from 'ink';
|
|
|
4
4
|
import { CDF_PROPERTY_TYPES, CDF_PROPERTY_CATEGORIES, DESIGN_TOKEN_TYPES, } from '@contentful/experience-design-system-types';
|
|
5
5
|
import { useImmediateInput } from '../hooks/useImmediateInput.js';
|
|
6
6
|
import { computeNextScrollOffset } from '../hooks/scroll-offset.js';
|
|
7
|
+
import { findSlotCycles } from '../../../cycle-detection.js';
|
|
7
8
|
// Section indices for top-level navigation
|
|
8
9
|
// 0 = component $description row
|
|
9
10
|
// 1 = $properties header (then props[0..n-1])
|
|
@@ -201,18 +202,34 @@ function PropRow({ prop, selected, activeField, textCursor, valueCursor, cursorV
|
|
|
201
202
|
return (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: isActiveCursor ? 'cyan' : 'white', children: isActiveCursor ? `▶ ${v}` : ` ${v}` }) }, i));
|
|
202
203
|
}), editingValue?.mode === 'add' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }))] }))] }));
|
|
203
204
|
}
|
|
204
|
-
function SlotRow({ slot, selected, activeField, textCursor, valueCursor, cursorVisible, editingValue, valueText, width, }) {
|
|
205
|
+
function SlotRow({ slot, selected, activeField, textCursor, valueCursor, cursorVisible, editingValue, valueText, width, pickerCandidates, pickerCursor, }) {
|
|
205
206
|
const cursor = cursorVisible ? '█' : ' ';
|
|
206
207
|
const bg = selected ? 'blue' : undefined;
|
|
207
208
|
const nameDisplay = slot.name.length > 14 ? slot.name.slice(0, 13) + '…' : slot.name.padEnd(14);
|
|
208
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: slot.required, active: true })) : (_jsx(Toggle, { value: slot.required, active: false }))] }), selected && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "allowed:" }), activeField === 'allowedComponents' && (_jsx(Text, { dimColor: true, children: ' [a]dd [e]dit [r]emove [↑↓] navigate [K/J] reorder' }))] }), slot.allowedComponents.length === 0 && !editingValue && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: activeField === 'allowedComponents' ? '(any — press [a] to add)' : '(any)' }) })), slot.allowedComponents.map((v, i) => {
|
|
209
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: slot.required, active: true })) : (_jsx(Toggle, { value: slot.required, active: false }))] }), !selected && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "allowed:" }), slot.allowedComponents.length === 0 ? (_jsx(Text, { dimColor: true, children: "(any)" })) : (_jsx(Text, { color: "cyan", children: slot.allowedComponents.join(', ') }))] })), selected && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "allowed:" }), activeField === 'allowedComponents' && (_jsx(Text, { dimColor: true, children: ' [a]dd [e]dit [r]emove [↑↓] navigate [K/J] reorder' }))] }), slot.allowedComponents.length === 0 && !editingValue && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: activeField === 'allowedComponents' ? '(any — press [a] to add)' : '(any)' }) })), slot.allowedComponents.map((v, i) => {
|
|
209
210
|
const isActiveCursor = activeField === 'allowedComponents' && valueCursor === i;
|
|
210
211
|
const isBeingEdited = editingValue?.mode === 'edit' && editingValue.index === i;
|
|
211
212
|
if (isBeingEdited) {
|
|
212
213
|
return (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '✎ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }, i));
|
|
213
214
|
}
|
|
214
215
|
return (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: isActiveCursor ? 'cyan' : 'white', children: isActiveCursor ? `▶ ${v}` : ` ${v}` }) }, i));
|
|
215
|
-
}), editingValue?.mode === 'add' && activeField === 'allowedComponents' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }))
|
|
216
|
+
}), editingValue?.mode === 'add' && activeField === 'allowedComponents' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] })), editingValue?.mode === 'add' && activeField === 'allowedComponents' && pickerCandidates !== null && (_jsx(Box, { paddingLeft: 2, flexDirection: "column", children: pickerCandidates.length === 0 ? (_jsx(Text, { dimColor: true, children: '(no valid components to add — all remaining candidates would create cycles)' })) : ((() => {
|
|
217
|
+
const filtered = valueText.length === 0
|
|
218
|
+
? pickerCandidates
|
|
219
|
+
: pickerCandidates.filter((n) => n.toLowerCase().includes(valueText.toLowerCase()));
|
|
220
|
+
if (filtered.length === 0) {
|
|
221
|
+
return _jsx(Text, { dimColor: true, children: '(no candidates match — Enter to add as free text)' });
|
|
222
|
+
}
|
|
223
|
+
const cursor = pickerCursor % filtered.length;
|
|
224
|
+
const MAX_VISIBLE = 5;
|
|
225
|
+
const start = Math.max(0, Math.min(filtered.length - MAX_VISIBLE, cursor - 2));
|
|
226
|
+
const slice = filtered.slice(start, start + MAX_VISIBLE);
|
|
227
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: ' candidates (↑↓ cycle, Enter to add):' }), slice.map((name, i) => {
|
|
228
|
+
const absIdx = start + i;
|
|
229
|
+
const isCursor = absIdx === cursor;
|
|
230
|
+
return (_jsx(Text, { color: isCursor ? 'cyan' : undefined, dimColor: !isCursor, children: isCursor ? ` ▶ ${name}` : ` ${name}` }, name));
|
|
231
|
+
})] }));
|
|
232
|
+
})()) }))] })), selected && activeField === 'description' && (_jsxs(Box, { paddingLeft: 2, flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: slot.description.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: slot.description[textCursor] ?? cursor }), _jsx(Text, { children: slot.description.slice(textCursor + 1) })] })] })), selected && activeField !== 'description' && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsx(Text, { color: "green", children: slot.description || '—' })] }))] }));
|
|
216
233
|
}
|
|
217
234
|
// Field order is intentional: description is LAST so it's the "edge" of the
|
|
218
235
|
// field cycle. The user reaches description by walking through type → category
|
|
@@ -238,8 +255,59 @@ function propFields(prop) {
|
|
|
238
255
|
// components is a list-typed field that swallows j/k similarly to enum $values
|
|
239
256
|
// — placing it before description preserves description-as-last.
|
|
240
257
|
const SLOT_FIELDS = ['required', 'allowedComponents', 'description'];
|
|
258
|
+
// ── INTEG-4401: cycle-aware $allowedComponents picker helpers ─────────────
|
|
259
|
+
/** Simulate adding a candidate to a slot; replaces self-entry with live edits. */
|
|
260
|
+
export function simulateGraphWithCandidate(projectSlotGraph, selfName, currentSlots, targetSlotName, candidate) {
|
|
261
|
+
const selfEntry = {
|
|
262
|
+
name: selfName,
|
|
263
|
+
slots: currentSlots.map((s) => ({
|
|
264
|
+
name: s.name,
|
|
265
|
+
allowedComponents: s.name === targetSlotName ? [...s.allowedComponents, candidate] : [...s.allowedComponents],
|
|
266
|
+
})),
|
|
267
|
+
};
|
|
268
|
+
const withoutSelf = projectSlotGraph.filter((c) => c.name !== selfName);
|
|
269
|
+
return [...withoutSelf, selfEntry];
|
|
270
|
+
}
|
|
271
|
+
/** True iff `next` contains a cycle whose canonical edge set is absent in `before`. */
|
|
272
|
+
export function introducesNewCycle(before, next) {
|
|
273
|
+
const key = (edges) => {
|
|
274
|
+
if (edges.length === 0)
|
|
275
|
+
return '';
|
|
276
|
+
const encoded = edges.map((e) => `${e.fromComponent}|${e.slotName}|${e.toComponent}`);
|
|
277
|
+
let minIdx = 0;
|
|
278
|
+
for (let i = 1; i < encoded.length; i += 1)
|
|
279
|
+
if (encoded[i] < encoded[minIdx])
|
|
280
|
+
minIdx = i;
|
|
281
|
+
return [...encoded.slice(minIdx), ...encoded.slice(0, minIdx)].join(';');
|
|
282
|
+
};
|
|
283
|
+
const beforeSet = new Set(before.map((c) => key(c.edges)));
|
|
284
|
+
for (const cycle of next)
|
|
285
|
+
if (!beforeSet.has(key(cycle.edges)))
|
|
286
|
+
return true;
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
/** Candidate names that can be added to a slot without creating a new cycle. */
|
|
290
|
+
export function computeAllowedComponentCandidates(projectSlotGraph, selfName, currentSlots, targetSlotName) {
|
|
291
|
+
const targetSlot = currentSlots.find((s) => s.name === targetSlotName);
|
|
292
|
+
const alreadyAdded = new Set(targetSlot?.allowedComponents ?? []);
|
|
293
|
+
const universe = new Set();
|
|
294
|
+
for (const c of projectSlotGraph)
|
|
295
|
+
universe.add(c.name);
|
|
296
|
+
universe.delete(selfName);
|
|
297
|
+
for (const added of alreadyAdded)
|
|
298
|
+
universe.delete(added);
|
|
299
|
+
const baseline = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, selfName, currentSlots, '', ''));
|
|
300
|
+
const safe = [];
|
|
301
|
+
for (const candidate of universe) {
|
|
302
|
+
const nextCycles = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, selfName, currentSlots, targetSlotName, candidate));
|
|
303
|
+
if (!introducesNewCycle(baseline, nextCycles))
|
|
304
|
+
safe.push(candidate);
|
|
305
|
+
}
|
|
306
|
+
safe.sort((a, b) => a.localeCompare(b));
|
|
307
|
+
return safe;
|
|
308
|
+
}
|
|
241
309
|
// ── Main component ────────────────────────────────────────────────────────────
|
|
242
|
-
export function FieldEditor({ value, width, height, active = true, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, }) {
|
|
310
|
+
export function FieldEditor({ value, width, height, active = true, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, projectSlotGraph, currentComponentName, }) {
|
|
243
311
|
const { state: initialState, error: parseError } = parseToState(value);
|
|
244
312
|
const [editorState, setEditorState] = useState(initialState);
|
|
245
313
|
const [parseErr] = useState(parseError);
|
|
@@ -291,6 +359,8 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
|
|
|
291
359
|
// mode='add' — append on Enter; mode='edit' — replace at index on Enter.
|
|
292
360
|
const [editingValue, setEditingValue] = useState(null);
|
|
293
361
|
const [valueText, setValueText] = useState('');
|
|
362
|
+
// INTEG-4401: picker cursor for the cycle-filtered $allowedComponents list.
|
|
363
|
+
const [pickerCursor, setPickerCursor] = useState(0);
|
|
294
364
|
const [validationError, setValidationError] = useState(null);
|
|
295
365
|
const [cursorVisible] = useState(true);
|
|
296
366
|
// Feature 1: source-view panel toggle. Opens with `s`, closes with `s` or Esc.
|
|
@@ -359,11 +429,57 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
|
|
|
359
429
|
// same add/edit/remove/reorder list shape.
|
|
360
430
|
const isPropValuesEntry = editingValue && currentProp && activeField === 'values' && !inSlots;
|
|
361
431
|
const isSlotAllowedEntry = editingValue && currentSlot && activeField === 'allowedComponents' && inSlots;
|
|
432
|
+
// INTEG-4401: cycle-filtered candidates for slot-add.
|
|
433
|
+
const slotAddCandidates = isSlotAllowedEntry && editingValue?.mode === 'add' && projectSlotGraph && currentSlot && currentComponentName
|
|
434
|
+
? computeAllowedComponentCandidates(projectSlotGraph, currentComponentName, slots, currentSlot.name)
|
|
435
|
+
: null;
|
|
436
|
+
const filteredCandidates = slotAddCandidates
|
|
437
|
+
? valueText.length === 0
|
|
438
|
+
? slotAddCandidates
|
|
439
|
+
: slotAddCandidates.filter((n) => n.toLowerCase().includes(valueText.toLowerCase()))
|
|
440
|
+
: null;
|
|
362
441
|
if (isPropValuesEntry || isSlotAllowedEntry) {
|
|
363
442
|
const vals = isPropValuesEntry ? currentProp.values : currentSlot.allowedComponents;
|
|
443
|
+
const pickerActive = isSlotAllowedEntry && editingValue?.mode === 'add' && slotAddCandidates !== null;
|
|
444
|
+
if (pickerActive && (key.upArrow || key.downArrow)) {
|
|
445
|
+
const len = filteredCandidates.length;
|
|
446
|
+
if (len > 0) {
|
|
447
|
+
setPickerCursor((c) => (key.upArrow ? (c - 1 + len) % len : (c + 1) % len));
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
364
451
|
if (key.return) {
|
|
365
|
-
|
|
452
|
+
let chosen = null;
|
|
453
|
+
if (pickerActive && filteredCandidates && filteredCandidates.length > 0) {
|
|
454
|
+
if (valueText.trim() === '') {
|
|
455
|
+
chosen = filteredCandidates[pickerCursor % filteredCandidates.length] ?? null;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const exact = filteredCandidates.find((n) => n === valueText.trim());
|
|
459
|
+
if (exact)
|
|
460
|
+
chosen = exact;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const trimmed = chosen ?? valueText.trim();
|
|
366
464
|
if (trimmed) {
|
|
465
|
+
// Cycle-aware free-text validation. Picker path is safe by construction.
|
|
466
|
+
if (isSlotAllowedEntry &&
|
|
467
|
+
editingValue?.mode === 'add' &&
|
|
468
|
+
projectSlotGraph &&
|
|
469
|
+
currentSlot &&
|
|
470
|
+
currentComponentName &&
|
|
471
|
+
chosen === null) {
|
|
472
|
+
if (trimmed === currentComponentName) {
|
|
473
|
+
setValidationError(`"${trimmed}" cannot be added to its own slot (self-loop).`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const baseline = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, currentComponentName, slots, '', ''));
|
|
477
|
+
const next = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, currentComponentName, slots, currentSlot.name, trimmed));
|
|
478
|
+
if (introducesNewCycle(baseline, next)) {
|
|
479
|
+
setValidationError(`Adding "${trimmed}" to slot "${currentSlot.name}" would create a slot-dependency cycle. Choose a different component.`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
367
483
|
let nextVals;
|
|
368
484
|
let cursorAfter;
|
|
369
485
|
if (editingValue.mode === 'add') {
|
|
@@ -384,22 +500,28 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
|
|
|
384
500
|
commit({ ...editorState, slots: nextSlots });
|
|
385
501
|
}
|
|
386
502
|
setValueCursor(cursorAfter);
|
|
503
|
+
setValidationError(null);
|
|
387
504
|
}
|
|
388
505
|
setEditingValue(null);
|
|
389
506
|
setValueText('');
|
|
507
|
+
setPickerCursor(0);
|
|
390
508
|
return;
|
|
391
509
|
}
|
|
392
510
|
if (key.escape) {
|
|
393
511
|
setEditingValue(null);
|
|
394
512
|
setValueText('');
|
|
513
|
+
setPickerCursor(0);
|
|
514
|
+
setValidationError(null);
|
|
395
515
|
return;
|
|
396
516
|
}
|
|
397
517
|
if (key.backspace) {
|
|
398
518
|
setValueText((t) => t.slice(0, -1));
|
|
519
|
+
setPickerCursor(0);
|
|
399
520
|
return;
|
|
400
521
|
}
|
|
401
522
|
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
402
523
|
setValueText((t) => t + input);
|
|
524
|
+
setPickerCursor(0);
|
|
403
525
|
return;
|
|
404
526
|
}
|
|
405
527
|
return;
|
|
@@ -985,7 +1107,15 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
|
|
|
985
1107
|
// slot row
|
|
986
1108
|
const s = slots[row.idx];
|
|
987
1109
|
const isSelected = inSlots && row.idx === slotIdx;
|
|
988
|
-
|
|
1110
|
+
// INTEG-4401: compute picker candidates for selected slot in add-mode.
|
|
1111
|
+
const slotPickerCandidates = isSelected &&
|
|
1112
|
+
editingValue?.mode === 'add' &&
|
|
1113
|
+
activeField === 'allowedComponents' &&
|
|
1114
|
+
projectSlotGraph &&
|
|
1115
|
+
currentComponentName
|
|
1116
|
+
? computeAllowedComponentCandidates(projectSlotGraph, currentComponentName, slots, s.name)
|
|
1117
|
+
: null;
|
|
1118
|
+
return (_jsx(SlotRow, { slot: s, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, valueCursor: valueCursor, cursorVisible: cursorVisible, editingValue: isSelected ? editingValue : null, valueText: isSelected ? valueText : '', width: innerWidth, pickerCandidates: slotPickerCandidates, pickerCursor: pickerCursor }, `slot-${row.idx}`));
|
|
989
1119
|
}) }), sourceOpen &&
|
|
990
1120
|
!onToggleSourceExternal &&
|
|
991
1121
|
(() => {
|
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { CDFComponentEntry, DTCGTokenEntry } from '@contentful/experience-design-system-types';
|
|
3
|
+
import { findSlotCycles } from '../analyze/cycle-detection.js';
|
|
3
4
|
import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
|
|
4
5
|
export declare function readTokensFromPath(flag: string, p: string): Promise<DTCGTokenEntry[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Pure cycle detection — returns [] when the graph is acyclic. Callers own
|
|
8
|
+
* how to react (CLI standalone uses `assertNoSlotCycles` which exits 1; the
|
|
9
|
+
* wizard uses `detectSlotCycles` directly to route to an in-TUI error step
|
|
10
|
+
* without ever POSTing to EDSI).
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectSlotCycles(components: Array<{
|
|
13
|
+
key: string;
|
|
14
|
+
entry: CDFComponentEntry;
|
|
15
|
+
}>): ReturnType<typeof findSlotCycles>;
|
|
16
|
+
/**
|
|
17
|
+
* Build a stderr-style message block for a set of cycles. Extracted so the
|
|
18
|
+
* wizard can render the same text in an in-TUI error panel and any future
|
|
19
|
+
* headless surface can log identical output.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatSlotCycleReport(cycles: ReturnType<typeof findSlotCycles>): string[];
|
|
5
22
|
export declare function assertNoSlotCycles(components: Array<{
|
|
6
23
|
key: string;
|
|
7
24
|
entry: CDFComponentEntry;
|
|
8
25
|
}>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Reconstruct the `{key, entry}` shape from a built `ManifestPayload`. The
|
|
28
|
+
* wizard's push path holds a serialized manifest rather than the underlying
|
|
29
|
+
* CDF records, so we unwrap it here to feed `detectSlotCycles`. Skips the
|
|
30
|
+
* `$schema` sentinel key. Safe on undefined/empty manifests (returns []).
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractComponentsFromManifest(manifest: {
|
|
33
|
+
componentsManifest?: Record<string, unknown>;
|
|
34
|
+
} | null | undefined): Array<{
|
|
35
|
+
key: string;
|
|
36
|
+
entry: CDFComponentEntry;
|
|
37
|
+
}>;
|
|
9
38
|
export declare function hasBreakingChangesWithImpact(preview: ServerPreviewResponse): boolean;
|
|
10
39
|
export declare function registerApplyCommand(program: Command): void;
|
|
@@ -177,10 +177,17 @@ async function resolveSharedInputs(opts) {
|
|
|
177
177
|
// by then the request has already been serialized, transported, and partially
|
|
178
178
|
// applied — leading to confusing partial-failure states. Detecting the same
|
|
179
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
|
|
180
|
+
// side-effects. Called from the `apply push` and `apply select` flows AND
|
|
181
|
+
// from the wizard's `experiences import` push path (via `detectSlotCycles`).
|
|
181
182
|
// `apply preview` is read-only and still runs (its diff is used to warn but
|
|
182
183
|
// not to block).
|
|
183
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Pure cycle detection — returns [] when the graph is acyclic. Callers own
|
|
186
|
+
* how to react (CLI standalone uses `assertNoSlotCycles` which exits 1; the
|
|
187
|
+
* wizard uses `detectSlotCycles` directly to route to an in-TUI error step
|
|
188
|
+
* without ever POSTing to EDSI).
|
|
189
|
+
*/
|
|
190
|
+
export function detectSlotCycles(components) {
|
|
184
191
|
const cycleInput = components.map(({ key, entry }) => ({
|
|
185
192
|
name: key,
|
|
186
193
|
slots: Object.entries(entry.$slots ?? {}).map(([slotName, slotDef]) => ({
|
|
@@ -188,9 +195,14 @@ export function assertNoSlotCycles(components) {
|
|
|
188
195
|
allowedComponents: slotDef.$allowedComponents ?? [],
|
|
189
196
|
})),
|
|
190
197
|
}));
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
return findSlotCycles(cycleInput);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build a stderr-style message block for a set of cycles. Extracted so the
|
|
202
|
+
* wizard can render the same text in an in-TUI error panel and any future
|
|
203
|
+
* headless surface can log identical output.
|
|
204
|
+
*/
|
|
205
|
+
export function formatSlotCycleReport(cycles) {
|
|
194
206
|
const lines = [];
|
|
195
207
|
lines.push(`Error: manifest:components/slot-cycles — ${cycles.length} slot dependency cycle(s) detected. Push refused.`);
|
|
196
208
|
for (let i = 0; i < cycles.length; i += 1) {
|
|
@@ -199,9 +211,35 @@ export function assertNoSlotCycles(components) {
|
|
|
199
211
|
const suggested = suggestCycleBreakEdge(cycle, cycles);
|
|
200
212
|
lines.push(` Fix: remove '${suggested.toComponent}' from ${suggested.fromComponent}.$slots.${suggested.slotName}.$allowedComponents`);
|
|
201
213
|
}
|
|
202
|
-
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
export function assertNoSlotCycles(components) {
|
|
217
|
+
const cycles = detectSlotCycles(components);
|
|
218
|
+
if (cycles.length === 0)
|
|
219
|
+
return;
|
|
220
|
+
process.stderr.write(formatSlotCycleReport(cycles).join('\n') + '\n');
|
|
203
221
|
process.exit(1);
|
|
204
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Reconstruct the `{key, entry}` shape from a built `ManifestPayload`. The
|
|
225
|
+
* wizard's push path holds a serialized manifest rather than the underlying
|
|
226
|
+
* CDF records, so we unwrap it here to feed `detectSlotCycles`. Skips the
|
|
227
|
+
* `$schema` sentinel key. Safe on undefined/empty manifests (returns []).
|
|
228
|
+
*/
|
|
229
|
+
export function extractComponentsFromManifest(manifest) {
|
|
230
|
+
const componentsManifest = manifest?.componentsManifest;
|
|
231
|
+
if (!componentsManifest)
|
|
232
|
+
return [];
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const [key, value] of Object.entries(componentsManifest)) {
|
|
235
|
+
if (key === '$schema')
|
|
236
|
+
continue;
|
|
237
|
+
if (!value || typeof value !== 'object')
|
|
238
|
+
continue;
|
|
239
|
+
out.push({ key, entry: value });
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
205
243
|
// --- Output helpers ---
|
|
206
244
|
export function hasBreakingChangesWithImpact(preview) {
|
|
207
245
|
const allChanged = [...preview.components.changed, ...preview.tokens.changed];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INTEG-4401 Fix C — parse EDSI failure bodies into a structured shape the
|
|
3
|
+
* TUI can render without dumping timestamps + trace ids at operators.
|
|
4
|
+
*
|
|
5
|
+
* EDSI failure bodies come in three broad shapes today:
|
|
6
|
+
*
|
|
7
|
+
* 1. Plain JSON error:
|
|
8
|
+
* `{"code":"TopoSortCycleError","message":"...","cycle":["CycleA","CycleB"]}`
|
|
9
|
+
*
|
|
10
|
+
* 2. Wrapper JSON with `details`:
|
|
11
|
+
* `{"sys":{"type":"Error"},"message":"...","details":{"code":"...","cycle":[...]}}`
|
|
12
|
+
*
|
|
13
|
+
* 3. Lambda log-line spill (the response body is the CloudWatch log
|
|
14
|
+
* formatting the worker's uncaught exception):
|
|
15
|
+
* `2026-07-07T22:26:26.479Z\t<request-id>\tERROR\t[dd.trace_id=... dd.span_id=...] <message> {\n operationId: '...',\n code: 'TopoSortCycleError',\n cycle: [ 'CycleA', 'CycleB' ]\n}`
|
|
16
|
+
*
|
|
17
|
+
* We parse each shape best-effort and fall back to a cleaned raw message so
|
|
18
|
+
* the ErrorStep never has to show the operator a timestamp + trace id.
|
|
19
|
+
*/
|
|
20
|
+
export interface ParsedEdsiError {
|
|
21
|
+
/** Server-side error code, if we could extract one. */
|
|
22
|
+
code: string | null;
|
|
23
|
+
/** Human-readable message, stripped of log/trace decoration. */
|
|
24
|
+
message: string;
|
|
25
|
+
/** Cycle participants, when `code === 'TopoSortCycleError'`. */
|
|
26
|
+
cycle: string[] | null;
|
|
27
|
+
/** True when the message survived cleaning as-is (no parseable structure). */
|
|
28
|
+
raw: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Strip the Lambda/CloudWatch log-line decoration from the head of a body so
|
|
32
|
+
* the operator-facing message reads as an error sentence, not a log entry.
|
|
33
|
+
*/
|
|
34
|
+
export declare function stripLambdaLogPrefix(body: string): string;
|
|
35
|
+
export declare function parseEdsiError(rawInput: string | undefined | null): ParsedEdsiError;
|
|
36
|
+
/**
|
|
37
|
+
* Render a parsed error into a short multi-line block for the ErrorStep.
|
|
38
|
+
* Callers can pass `verbose: true` to include the raw body underneath.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatParsedEdsiError(parsed: ParsedEdsiError, opts?: {
|
|
41
|
+
verbose?: boolean;
|
|
42
|
+
raw?: string;
|
|
43
|
+
}): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INTEG-4401 Fix C — parse EDSI failure bodies into a structured shape the
|
|
3
|
+
* TUI can render without dumping timestamps + trace ids at operators.
|
|
4
|
+
*
|
|
5
|
+
* EDSI failure bodies come in three broad shapes today:
|
|
6
|
+
*
|
|
7
|
+
* 1. Plain JSON error:
|
|
8
|
+
* `{"code":"TopoSortCycleError","message":"...","cycle":["CycleA","CycleB"]}`
|
|
9
|
+
*
|
|
10
|
+
* 2. Wrapper JSON with `details`:
|
|
11
|
+
* `{"sys":{"type":"Error"},"message":"...","details":{"code":"...","cycle":[...]}}`
|
|
12
|
+
*
|
|
13
|
+
* 3. Lambda log-line spill (the response body is the CloudWatch log
|
|
14
|
+
* formatting the worker's uncaught exception):
|
|
15
|
+
* `2026-07-07T22:26:26.479Z\t<request-id>\tERROR\t[dd.trace_id=... dd.span_id=...] <message> {\n operationId: '...',\n code: 'TopoSortCycleError',\n cycle: [ 'CycleA', 'CycleB' ]\n}`
|
|
16
|
+
*
|
|
17
|
+
* We parse each shape best-effort and fall back to a cleaned raw message so
|
|
18
|
+
* the ErrorStep never has to show the operator a timestamp + trace id.
|
|
19
|
+
*/
|
|
20
|
+
const LAMBDA_LOG_PREFIX_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s+[0-9a-f-]+\s+ERROR\s+(?:\[dd\.[^\]]*\]\s*)?/;
|
|
21
|
+
const DD_TAG_RE = /\[dd\.(?:trace_id|span_id)=[^\]]*\]\s*/g;
|
|
22
|
+
/**
|
|
23
|
+
* Strip the Lambda/CloudWatch log-line decoration from the head of a body so
|
|
24
|
+
* the operator-facing message reads as an error sentence, not a log entry.
|
|
25
|
+
*/
|
|
26
|
+
export function stripLambdaLogPrefix(body) {
|
|
27
|
+
let out = body.replace(LAMBDA_LOG_PREFIX_RE, '');
|
|
28
|
+
out = out.replace(DD_TAG_RE, '');
|
|
29
|
+
return out.trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract a JS-object-literal tail like `{ operationId: '...', code: '...', cycle: [ 'A', 'B' ] }`
|
|
33
|
+
* that Lambda-spilled errors trail with. Returns `null` if no such structure
|
|
34
|
+
* is present or the fields we care about can't be recovered.
|
|
35
|
+
*/
|
|
36
|
+
function parseObjectLiteralTail(body) {
|
|
37
|
+
const braceStart = body.lastIndexOf('{');
|
|
38
|
+
if (braceStart === -1)
|
|
39
|
+
return null;
|
|
40
|
+
const tail = body.slice(braceStart);
|
|
41
|
+
const codeMatch = tail.match(/code:\s*['"]([^'"]+)['"]/);
|
|
42
|
+
const cycleMatch = tail.match(/cycle:\s*\[\s*([^\]]*)\s*\]/);
|
|
43
|
+
if (!codeMatch && !cycleMatch)
|
|
44
|
+
return null;
|
|
45
|
+
const code = codeMatch ? codeMatch[1] : null;
|
|
46
|
+
let cycle = null;
|
|
47
|
+
if (cycleMatch) {
|
|
48
|
+
cycle = cycleMatch[1]
|
|
49
|
+
.split(',')
|
|
50
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
51
|
+
.filter((s) => s.length > 0);
|
|
52
|
+
if (cycle.length === 0)
|
|
53
|
+
cycle = null;
|
|
54
|
+
}
|
|
55
|
+
return { code, cycle };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort JSON parse walking one level deep into `details`. Fields we
|
|
59
|
+
* accept anywhere in the top-level or `details` object: `code`, `message`,
|
|
60
|
+
* `cycle`. Returns `null` when the input isn't JSON.
|
|
61
|
+
*/
|
|
62
|
+
function parseJsonBody(body) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(body);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== 'object')
|
|
71
|
+
return null;
|
|
72
|
+
const p = parsed;
|
|
73
|
+
const details = (p.details && typeof p.details === 'object' ? p.details : {});
|
|
74
|
+
const pick = (k) => (p[k] !== undefined ? p[k] : details[k]);
|
|
75
|
+
const codeRaw = pick('code');
|
|
76
|
+
const messageRaw = pick('message');
|
|
77
|
+
const cycleRaw = pick('cycle');
|
|
78
|
+
const out = {};
|
|
79
|
+
if (typeof codeRaw === 'string')
|
|
80
|
+
out.code = codeRaw;
|
|
81
|
+
if (typeof messageRaw === 'string')
|
|
82
|
+
out.message = messageRaw;
|
|
83
|
+
if (Array.isArray(cycleRaw)) {
|
|
84
|
+
const strs = cycleRaw.filter((x) => typeof x === 'string');
|
|
85
|
+
if (strs.length > 0)
|
|
86
|
+
out.cycle = strs;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse an EDSI error body (the raw response body, or an `ApiError.message`
|
|
92
|
+
* that has the body appended). Always returns a value — the fallback is the
|
|
93
|
+
* cleaned raw message with `code: null`.
|
|
94
|
+
*/
|
|
95
|
+
// ApiError.message shape: `${phasePrefix}\n${body}` where phasePrefix looks
|
|
96
|
+
// like `apply failed: 400`, `preview failed: 422`, `poll failed: 500`. We
|
|
97
|
+
// only strip the prefix line when the first line matches this shape —
|
|
98
|
+
// otherwise a raw body that happens to contain a newline (e.g. a Lambda log
|
|
99
|
+
// spill with a multi-line object literal) gets truncated mid-structure.
|
|
100
|
+
const API_ERROR_PREFIX_RE = /^(?:apply|preview|poll) failed: \d+$/;
|
|
101
|
+
export function parseEdsiError(rawInput) {
|
|
102
|
+
if (!rawInput)
|
|
103
|
+
return { code: null, message: '', cycle: null, raw: true };
|
|
104
|
+
const nlIndex = rawInput.indexOf('\n');
|
|
105
|
+
let body = rawInput;
|
|
106
|
+
let prefix = '';
|
|
107
|
+
if (nlIndex !== -1) {
|
|
108
|
+
const firstLine = rawInput.slice(0, nlIndex);
|
|
109
|
+
if (API_ERROR_PREFIX_RE.test(firstLine)) {
|
|
110
|
+
prefix = firstLine;
|
|
111
|
+
body = rawInput.slice(nlIndex + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const cleaned = stripLambdaLogPrefix(body);
|
|
115
|
+
const json = parseJsonBody(cleaned) ?? parseJsonBody(body);
|
|
116
|
+
if (json && (json.code || json.message || json.cycle)) {
|
|
117
|
+
return {
|
|
118
|
+
code: json.code ?? null,
|
|
119
|
+
message: json.message ?? (cleaned || prefix),
|
|
120
|
+
cycle: json.cycle ?? null,
|
|
121
|
+
raw: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const literal = parseObjectLiteralTail(cleaned);
|
|
125
|
+
if (literal && (literal.code || literal.cycle)) {
|
|
126
|
+
// Grab the human-readable head — text before the trailing `{`. This is
|
|
127
|
+
// the sentence part of a Lambda log-line spill (e.g. "Apply operation
|
|
128
|
+
// <id> rejected: ComponentType slot dependency cycle detected among:
|
|
129
|
+
// CycleA, CycleB. Break the cycle by...").
|
|
130
|
+
const braceStart = cleaned.lastIndexOf('{');
|
|
131
|
+
const head = braceStart === -1 ? cleaned : cleaned.slice(0, braceStart).trim();
|
|
132
|
+
return {
|
|
133
|
+
code: literal.code ?? null,
|
|
134
|
+
message: head || cleaned,
|
|
135
|
+
cycle: literal.cycle ?? null,
|
|
136
|
+
raw: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { code: null, message: cleaned || prefix || rawInput, cycle: null, raw: true };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Render a parsed error into a short multi-line block for the ErrorStep.
|
|
143
|
+
* Callers can pass `verbose: true` to include the raw body underneath.
|
|
144
|
+
*/
|
|
145
|
+
export function formatParsedEdsiError(parsed, opts = {}) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
if (parsed.code) {
|
|
148
|
+
lines.push(`[${parsed.code}]`);
|
|
149
|
+
}
|
|
150
|
+
if (parsed.message) {
|
|
151
|
+
lines.push(parsed.message);
|
|
152
|
+
}
|
|
153
|
+
if (parsed.cycle && parsed.cycle.length > 0) {
|
|
154
|
+
lines.push(`Cycle: ${parsed.cycle.join(' → ')} → ${parsed.cycle[0]}`);
|
|
155
|
+
lines.push('Break the cycle by removing at least one $allowedComponents entry.');
|
|
156
|
+
}
|
|
157
|
+
if (opts.verbose && opts.raw) {
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push('--- raw ---');
|
|
160
|
+
lines.push(opts.raw);
|
|
161
|
+
}
|
|
162
|
+
return lines.filter(Boolean).join('\n');
|
|
163
|
+
}
|
|
@@ -33,6 +33,8 @@ import { nextStateAfterPrint } from './run-print-files-helpers.js';
|
|
|
33
33
|
import { PushDecisionGateStep } from './steps/PushDecisionGateStep.js';
|
|
34
34
|
import { chooseGateAction } from './push-decision-gate-helpers.js';
|
|
35
35
|
import { ImportApiClient, ApiError } from '../../apply/api-client.js';
|
|
36
|
+
import { detectSlotCycles, extractComponentsFromManifest, formatSlotCycleReport } from '../../apply/command.js';
|
|
37
|
+
import { parseEdsiError, formatParsedEdsiError } from '../../apply/error-parser.js';
|
|
36
38
|
import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
|
|
37
39
|
import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
|
|
38
40
|
import { spawnGenerateChild } from './spawn-generate.js';
|
|
@@ -1034,6 +1036,23 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
1034
1036
|
return;
|
|
1035
1037
|
}
|
|
1036
1038
|
}
|
|
1039
|
+
// INTEG-4401 Fix A — pre-push slot-cycle hard block for the wizard's
|
|
1040
|
+
// direct-API push path. The standalone `apply push` / `apply select`
|
|
1041
|
+
// commands run `assertNoSlotCycles` before ever constructing an API
|
|
1042
|
+
// client, but `experiences import` calls `client.applyImport` from here
|
|
1043
|
+
// without shelling out — so the guard has to run again on this path.
|
|
1044
|
+
// Otherwise a cyclic graph reaches EDSI and the operator sees a raw
|
|
1045
|
+
// Lambda error dump (see Fix C) instead of the clear local report.
|
|
1046
|
+
const cycles = detectSlotCycles(extractComponentsFromManifest(manifest));
|
|
1047
|
+
if (cycles.length > 0) {
|
|
1048
|
+
update({
|
|
1049
|
+
step: 'error',
|
|
1050
|
+
errorStep: 'apply push',
|
|
1051
|
+
errorMessage: formatSlotCycleReport(cycles).join('\n'),
|
|
1052
|
+
errorAllowCredentialRetry: false,
|
|
1053
|
+
});
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1037
1056
|
update({ step: 'pushing', pushProgress: null });
|
|
1038
1057
|
try {
|
|
1039
1058
|
const resolvedHost = resolveWizardHost(host);
|
|
@@ -1124,27 +1143,57 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
1124
1143
|
};
|
|
1125
1144
|
}
|
|
1126
1145
|
else {
|
|
1127
|
-
// API didn't return items
|
|
1146
|
+
// API didn't return items. INTEG-4401 Fix B — do NOT report preview
|
|
1147
|
+
// counts as "created": if the server said something failed, echo it
|
|
1148
|
+
// truthfully so the summary can't lie. Historically this branch used
|
|
1149
|
+
// preview.new/changed/removed as success counts and only surfaced
|
|
1150
|
+
// failed:0, which meant a cycle rejection (summary: 0 succeeded /
|
|
1151
|
+
// 11 failed, empty items) rendered as "Done ✓ 2 Component Types
|
|
1152
|
+
// created" — the exact regression this fix guards against.
|
|
1153
|
+
const summary = operation.summary ?? { total: 0, pending: 0, succeeded: 0, failed: 0 };
|
|
1154
|
+
const anyFailure = summary.failed > 0 || operation.sys.status === 'failed' || operation.sys.status === 'partial';
|
|
1128
1155
|
pushResult = {
|
|
1129
1156
|
componentTypes: {
|
|
1130
|
-
created: preview?.components.new.length ?? 0,
|
|
1131
|
-
updated: preview?.components.changed.length ?? 0,
|
|
1132
|
-
removed: preview?.components.removed.length ?? 0,
|
|
1133
|
-
|
|
1157
|
+
created: anyFailure ? 0 : (preview?.components.new.length ?? 0),
|
|
1158
|
+
updated: anyFailure ? 0 : (preview?.components.changed.length ?? 0),
|
|
1159
|
+
removed: anyFailure ? 0 : (preview?.components.removed.length ?? 0),
|
|
1160
|
+
// Attribute failures to component types by default — without a
|
|
1161
|
+
// per-item breakdown we can't split components vs tokens, and
|
|
1162
|
+
// components are the dominant entity in an import.
|
|
1163
|
+
failed: summary.failed,
|
|
1134
1164
|
},
|
|
1135
1165
|
designTokens: {
|
|
1136
|
-
created: preview?.tokens.new.length ?? 0,
|
|
1137
|
-
updated: preview?.tokens.changed.length ?? 0,
|
|
1138
|
-
removed: preview?.tokens.removed.length ?? 0,
|
|
1166
|
+
created: anyFailure ? 0 : (preview?.tokens.new.length ?? 0),
|
|
1167
|
+
updated: anyFailure ? 0 : (preview?.tokens.changed.length ?? 0),
|
|
1168
|
+
removed: anyFailure ? 0 : (preview?.tokens.removed.length ?? 0),
|
|
1139
1169
|
failed: 0,
|
|
1140
1170
|
},
|
|
1141
|
-
summary
|
|
1171
|
+
summary,
|
|
1142
1172
|
};
|
|
1143
1173
|
}
|
|
1144
1174
|
update({ step: 'done', pushResult });
|
|
1145
1175
|
}
|
|
1146
1176
|
catch (e) {
|
|
1147
|
-
|
|
1177
|
+
// INTEG-4401 Fix C — parse EDSI error bodies into a `[CODE] message`
|
|
1178
|
+
// block before handing off to ErrorStep, so cycle rejections and other
|
|
1179
|
+
// structured failures don't render as raw Lambda log lines
|
|
1180
|
+
// (timestamp / request-id / dd.trace_id / etc.).
|
|
1181
|
+
let msg;
|
|
1182
|
+
if (e instanceof ApiError) {
|
|
1183
|
+
const parsed = parseEdsiError(e.body || e.message);
|
|
1184
|
+
msg = formatParsedEdsiError(parsed, {
|
|
1185
|
+
verbose: process.env['EDSI_VERBOSE_ERRORS'] === '1',
|
|
1186
|
+
raw: e.body,
|
|
1187
|
+
});
|
|
1188
|
+
if (!msg)
|
|
1189
|
+
msg = e.message;
|
|
1190
|
+
}
|
|
1191
|
+
else if (e instanceof Error) {
|
|
1192
|
+
msg = e.message;
|
|
1193
|
+
}
|
|
1194
|
+
else {
|
|
1195
|
+
msg = 'Push failed';
|
|
1196
|
+
}
|
|
1148
1197
|
update({
|
|
1149
1198
|
step: 'error',
|
|
1150
1199
|
errorStep: 'apply push',
|
|
@@ -8,8 +8,8 @@ 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 {
|
|
11
|
+
import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, loadSlotCycles, storeSlotCycles, } from '../../../session/db.js';
|
|
12
|
+
import { formatCyclePathSegments, findSlotCycles, suggestCycleBreakEdge } from '../../../analyze/cycle-detection.js';
|
|
13
13
|
import { RationalePanel } from '../../../analyze/select/tui/components/RationalePanel.js';
|
|
14
14
|
import { ComponentRationalePanel } from '../../../analyze/select/tui/components/ComponentRationalePanel.js';
|
|
15
15
|
import { applyPreviewAnnotations } from '../../../analyze/select/preview-annotations.js';
|
|
@@ -253,6 +253,66 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
253
253
|
}
|
|
254
254
|
onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
|
|
255
255
|
};
|
|
256
|
+
/**
|
|
257
|
+
* INTEG-4401 (Fix 3/4): re-run cycle detection against the current in-memory
|
|
258
|
+
* component state and, if the result differs from the persisted `slotCycles`
|
|
259
|
+
* state, update both React state and the session DB. Called from:
|
|
260
|
+
* - `handleEditSave` after a FieldEditor save mutates a slot's
|
|
261
|
+
* `$allowedComponents`, so the banner / sidebar badges / [c] panel reflect
|
|
262
|
+
* reality instead of the stale extract-time snapshot.
|
|
263
|
+
* - The `r` reject keystroke, since dropping a component from the manifest
|
|
264
|
+
* can collapse cycles that routed through it.
|
|
265
|
+
*
|
|
266
|
+
* Rejected components are excluded from the graph — they will never ship, so
|
|
267
|
+
* they cannot contribute to a real push-time cycle even if their slot config
|
|
268
|
+
* still references other components locally.
|
|
269
|
+
*
|
|
270
|
+
* Cheap for typical N: findSlotCycles is O((V+E)(C+1)) and most manifests
|
|
271
|
+
* have zero or a handful of cycles. Wrapped in try/catch so a malformed
|
|
272
|
+
* $slots shape can't crash the render pipeline.
|
|
273
|
+
*/
|
|
274
|
+
const recomputeCycles = (currentComponents) => {
|
|
275
|
+
try {
|
|
276
|
+
const graph = currentComponents
|
|
277
|
+
.filter((c) => c.status !== 'rejected')
|
|
278
|
+
.map((c) => ({
|
|
279
|
+
name: c.key,
|
|
280
|
+
slots: Object.entries(c.entry.$slots ?? {}).map(([slotName, slotDef]) => ({
|
|
281
|
+
name: slotName,
|
|
282
|
+
allowedComponents: Array.isArray(slotDef?.$allowedComponents)
|
|
283
|
+
? slotDef.$allowedComponents.filter((v) => typeof v === 'string')
|
|
284
|
+
: [],
|
|
285
|
+
})),
|
|
286
|
+
}));
|
|
287
|
+
const rawCycles = findSlotCycles(graph);
|
|
288
|
+
const next = rawCycles.map((cycle) => ({
|
|
289
|
+
path: cycle.path,
|
|
290
|
+
edges: cycle.edges,
|
|
291
|
+
suggestedBreak: cycle.edges.length > 0 ? suggestCycleBreakEdge(cycle, rawCycles) : null,
|
|
292
|
+
}));
|
|
293
|
+
// Cheap structural equality via JSON.stringify — cycle count is tiny and
|
|
294
|
+
// this only fires on user actions, not on every render.
|
|
295
|
+
const prevSerialized = JSON.stringify(slotCycles);
|
|
296
|
+
const nextSerialized = JSON.stringify(next);
|
|
297
|
+
if (prevSerialized === nextSerialized)
|
|
298
|
+
return;
|
|
299
|
+
setSlotCycles(next);
|
|
300
|
+
const db = openPipelineDb();
|
|
301
|
+
try {
|
|
302
|
+
storeSlotCycles(db, extractSessionId, next);
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
db.close();
|
|
306
|
+
}
|
|
307
|
+
// If the new cycle set is empty, clear any lingering finalize error the
|
|
308
|
+
// [F] guard may have surfaced. Otherwise leave it alone.
|
|
309
|
+
if (next.length === 0)
|
|
310
|
+
setFinalizeError(null);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Defensive: swallow — never let cycle detection crash the review UI.
|
|
314
|
+
}
|
|
315
|
+
};
|
|
256
316
|
const handleEditSave = () => {
|
|
257
317
|
const current = components[selectedIdx];
|
|
258
318
|
if (!current)
|
|
@@ -268,7 +328,11 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
268
328
|
setSaveError('Invalid CDF entry: must have $type: "component" and $properties object');
|
|
269
329
|
return;
|
|
270
330
|
}
|
|
271
|
-
|
|
331
|
+
let updatedComponents = [];
|
|
332
|
+
setComponents((prev) => {
|
|
333
|
+
updatedComponents = prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c);
|
|
334
|
+
return updatedComponents;
|
|
335
|
+
});
|
|
272
336
|
setDraftValue('');
|
|
273
337
|
setSaveError(null);
|
|
274
338
|
const db = openPipelineDb();
|
|
@@ -278,6 +342,12 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
278
342
|
finally {
|
|
279
343
|
db.close();
|
|
280
344
|
}
|
|
345
|
+
// INTEG-4401 (Fix 3/4): recompute cycles against the freshly-edited
|
|
346
|
+
// component set so banner/badges/[c]-panel stay in sync. The FieldEditor
|
|
347
|
+
// picker filters cycle-forming candidates, but free-text edits + JSON
|
|
348
|
+
// pastes still slip through, and edits that BREAK an existing cycle
|
|
349
|
+
// won't propagate without this call.
|
|
350
|
+
recomputeCycles(updatedComponents);
|
|
281
351
|
// Feature 2: re-fire the live preview now that pipeline.db reflects
|
|
282
352
|
// the new state. The hook owns debounce + cred-missing short-circuit.
|
|
283
353
|
livePreviewHook.trigger();
|
|
@@ -460,6 +530,14 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
460
530
|
return;
|
|
461
531
|
}
|
|
462
532
|
if (input === 'F') {
|
|
533
|
+
// INTEG-4401: refuse to open the finalize dialog while any slot cycle
|
|
534
|
+
// is unresolved. The push-time hard block (`assertNoSlotCycles`) would
|
|
535
|
+
// catch this downstream, but showing an inline banner here saves the
|
|
536
|
+
// operator from confirming the dialog only to hit a stderr crash.
|
|
537
|
+
if (slotCycles.length > 0) {
|
|
538
|
+
setFinalizeError('Cannot finalize — resolve slot dependency cycle(s) first (press [c] for detail)');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
463
541
|
setShowFinalize(true);
|
|
464
542
|
return;
|
|
465
543
|
}
|
|
@@ -469,7 +547,14 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
469
547
|
return;
|
|
470
548
|
}
|
|
471
549
|
if (input === 'r') {
|
|
472
|
-
|
|
550
|
+
// INTEG-4401 (Fix 4): rejecting removes the component from the effective
|
|
551
|
+
// manifest, so recompute cycles across the reduced graph — any cycle
|
|
552
|
+
// that routed through this component collapses. We build the "next"
|
|
553
|
+
// components array inline (mirroring updateStatus) so recomputeCycles
|
|
554
|
+
// sees the post-update graph without waiting for a render tick.
|
|
555
|
+
const next = components.map((c, i) => i === selectedIdx ? { ...c, status: 'rejected' } : c);
|
|
556
|
+
setComponents(next);
|
|
557
|
+
recomputeCycles(next);
|
|
473
558
|
return;
|
|
474
559
|
}
|
|
475
560
|
if (input === 'A') {
|
|
@@ -555,6 +640,21 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
555
640
|
// sidebar width doesn't jitter as live-preview annotations flip in/out.
|
|
556
641
|
const sidebarWidth = Math.min(Math.max(longestName + 5, 14), 30);
|
|
557
642
|
const panelWidth = Math.max(10, terminalWidth - sidebarWidth - 4);
|
|
643
|
+
// INTEG-4401: project-wide slot graph passed to FieldEditor so its
|
|
644
|
+
// $allowedComponents picker can filter out cycle-forming candidates. Built
|
|
645
|
+
// from every review entry's $slots — includes accepted, rejected, and
|
|
646
|
+
// needs-review components so the graph reflects what will actually be
|
|
647
|
+
// pushed. The FieldEditor replaces its own entry in-simulation with its
|
|
648
|
+
// live editor state, so pending edits are always reflected.
|
|
649
|
+
const projectSlotGraph = components.map((c) => ({
|
|
650
|
+
name: c.key,
|
|
651
|
+
slots: Object.entries(c.entry.$slots ?? {}).map(([slotName, slotDef]) => ({
|
|
652
|
+
name: slotName,
|
|
653
|
+
allowedComponents: Array.isArray(slotDef?.$allowedComponents)
|
|
654
|
+
? slotDef.$allowedComponents.filter((v) => typeof v === 'string')
|
|
655
|
+
: [],
|
|
656
|
+
})),
|
|
657
|
+
}));
|
|
558
658
|
const accepted = components.filter((c) => c.status === 'accepted').length;
|
|
559
659
|
const rejected = components.filter((c) => c.status === 'rejected').length;
|
|
560
660
|
const needsReview = components.filter((c) => c.status === 'needs-review').length;
|
|
@@ -575,7 +675,12 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
575
675
|
slotCycles.forEach((cycle, idx) => {
|
|
576
676
|
const nodeCount = new Set(cycle.path).size;
|
|
577
677
|
lines.push(_jsx(Text, { bold: true, children: `▸ Cycle ${idx + 1} (${nodeCount} component${nodeCount === 1 ? '' : 's'}):` }, `cyc-h-${idx}`));
|
|
578
|
-
|
|
678
|
+
// Colorize slots (cyan, bracketed) distinctly from components so
|
|
679
|
+
// the operator can visually parse the alternating structure.
|
|
680
|
+
// Brackets on slot names ensure the distinction survives when
|
|
681
|
+
// ANSI is stripped (logs, redirected output).
|
|
682
|
+
const segs = formatCyclePathSegments(cycle, 16);
|
|
683
|
+
lines.push(_jsxs(Text, { children: [' ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: "cyan", children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { children: seg.text }, si)))] }, `cyc-p-${idx}`));
|
|
579
684
|
if (cycle.suggestedBreak) {
|
|
580
685
|
const b = cycle.suggestedBreak;
|
|
581
686
|
lines.push(_jsx(Text, { dimColor: true, children: ` Suggested fix: remove '${b.toComponent}' from ${b.fromComponent}.$slots.${b.slotName}.$allowedComponents` }, `cyc-f-${idx}`));
|
|
@@ -607,7 +712,10 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
607
712
|
if (!hasCounts)
|
|
608
713
|
return null;
|
|
609
714
|
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) =>
|
|
715
|
+
})(), !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) => {
|
|
716
|
+
const segs = formatCyclePathSegments(cycle);
|
|
717
|
+
return (_jsxs(Text, { color: "yellow", children: [' Cycle: ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: "cyan", children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { color: "yellow", children: seg.text }, si)))] }, `cyc-banner-${idx}`));
|
|
718
|
+
}), 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) => {
|
|
611
719
|
const idx = components.findIndex((c) => c.key === id);
|
|
612
720
|
if (idx >= 0) {
|
|
613
721
|
setSelectedIdx(idx);
|
|
@@ -656,7 +764,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
656
764
|
}, onToggleSourceExternal: () => {
|
|
657
765
|
setPanelOpen('source');
|
|
658
766
|
setPanelScrollOffset(() => 0);
|
|
659
|
-
}, onTextEntryActiveChange: setTextEntryActive }, selected.key)), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
|
|
767
|
+
}, onTextEntryActiveChange: setTextEntryActive, projectSlotGraph: projectSlotGraph, currentComponentName: selected.key }, selected.key)), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
|
|
660
768
|
? ' [a] accept [r] reject [A] accept all [J] ' +
|
|
661
769
|
(showJson ? 'hide JSON' : 'show JSON') +
|
|
662
770
|
' [F] finalize [e/Tab] focus panel' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.12.4-dev-build-
|
|
3
|
+
"version": "2.12.4-dev-build-ff57340.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"svelte": "^5.56.4",
|
|
38
38
|
"ts-morph": "^27.0.2",
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
|
-
"@contentful/experience-design-system-types": "2.12.4-dev-build-
|
|
40
|
+
"@contentful/experience-design-system-types": "2.12.4-dev-build-ff57340.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@tsconfig/node24": "^24.0.3",
|