@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.
- package/dist/package.json +3 -6
- package/dist/src/analyze/command.js +2 -22
- package/dist/src/analyze/select/command.js +1 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +4 -1
- package/dist/src/analyze/select-agent/command.js +1 -1
- package/dist/src/apply/command.d.ts +1 -5
- package/dist/src/apply/command.js +0 -43
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.js +20 -102
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
- package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
- package/dist/src/import/tui/steps/preview-diff.js +0 -48
- package/dist/src/session/db.d.ts +0 -9
- package/dist/src/session/db.js +0 -46
- package/dist/src/types.d.ts +2 -76
- package/dist/src/types.js +1 -4
- package/package.json +3 -6
- package/dist/src/analyze/cycle-detection.d.ts +0 -68
- package/dist/src/analyze/cycle-detection.js +0 -285
- package/dist/src/analyze/extract/astro.d.ts +0 -5
- package/dist/src/analyze/extract/astro.js +0 -289
- package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
- package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
- package/dist/src/analyze/extract/pipeline.d.ts +0 -6
- package/dist/src/analyze/extract/pipeline.js +0 -314
- package/dist/src/analyze/extract/react.d.ts +0 -2
- package/dist/src/analyze/extract/react.js +0 -2041
- package/dist/src/analyze/extract/scoring.d.ts +0 -12
- package/dist/src/analyze/extract/scoring.js +0 -108
- package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
- package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
- package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
- package/dist/src/analyze/extract/slot-detection.js +0 -101
- package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
- package/dist/src/analyze/extract/source-inspection.js +0 -189
- package/dist/src/analyze/extract/stencil.d.ts +0 -2
- package/dist/src/analyze/extract/stencil.js +0 -296
- package/dist/src/analyze/extract/svelte.d.ts +0 -5
- package/dist/src/analyze/extract/svelte.js +0 -1626
- package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
- package/dist/src/analyze/extract/tsx-shared.js +0 -263
- package/dist/src/analyze/extract/validate.d.ts +0 -16
- package/dist/src/analyze/extract/validate.js +0 -94
- package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
- package/dist/src/analyze/extract/vue-tsx.js +0 -498
- package/dist/src/analyze/extract/vue.d.ts +0 -5
- package/dist/src/analyze/extract/vue.js +0 -653
- package/dist/src/analyze/extract/web-components.d.ts +0 -2
- package/dist/src/analyze/extract/web-components.js +0 -876
- package/dist/src/analyze/pre-classify.d.ts +0 -17
- 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.
|
|
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 '
|
|
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,
|
|
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 '
|
|
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
|
-
|
|
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 '
|
|
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 {
|
|
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[]
|
|
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,
|
|
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
|
|
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
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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:
|
|
491
|
+
validationWarningCount: isEmpty(c) ? 1 : 0,
|
|
549
492
|
}));
|
|
550
|
-
// Account for the "(
|
|
551
|
-
//
|
|
552
|
-
const longestName = components.reduce((m, c) => Math.max(m, c.key.length +
|
|
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" })] })),
|
|
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 &&
|
|
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 {
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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) => {
|