@contentful/experience-design-system-cli 2.7.4-dev-build-595febb.0 → 2.7.5-dev-build-752dc9c.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.7.4-dev-build-595febb.0",
3
+ "version": "2.7.5-dev-build-752dc9c.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,7 @@ import { preClassifyComponent } from './pre-classify.js';
11
11
  import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
12
12
  import { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
13
13
  import { describeReviewReasons, inspectComponentSource } from './extract/source-inspection.js';
14
+ import { validateExtractedComponents } from './extract/validate.js';
14
15
  const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
15
16
  const IGNORED_DIRECTORY_NAMES = new Set([
16
17
  '.git',
@@ -168,7 +169,8 @@ export function registerAnalyzeCommand(program) {
168
169
  needsReview: deriveNeedsReview(confidence) || inspection.wrapperConfidence >= 4 || inspection.keepDespiteZeroSurface,
169
170
  });
170
171
  }
171
- storeRawComponents(db, sessionId, filteredComponents);
172
+ const validatedComponents = validateExtractedComponents(filteredComponents);
173
+ storeRawComponents(db, sessionId, validatedComponents);
172
174
  storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
173
175
  updateStep(db, stepId, 'complete', { sessionId });
174
176
  db.close();
@@ -0,0 +1,4 @@
1
+ import type { RawComponentDefinition } from '../../types.js';
2
+ export type { ExtractionValidationIssue } from '../../types.js';
3
+ export declare function validateExtractedComponents(components: RawComponentDefinition[]): RawComponentDefinition[];
4
+ export declare function shouldExcludeDueToValidation(component: RawComponentDefinition): boolean;
@@ -0,0 +1,67 @@
1
+ export function validateExtractedComponents(components) {
2
+ const nameCounts = new Map();
3
+ for (const component of components) {
4
+ const key = component.name.trim();
5
+ nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
6
+ }
7
+ return components.map((component) => {
8
+ const issues = [];
9
+ if (!component.name.trim()) {
10
+ issues.push({
11
+ severity: 'error',
12
+ code: 'EMPTY_COMPONENT_NAME',
13
+ message: 'Component name must not be empty',
14
+ });
15
+ }
16
+ for (let i = 0; i < component.props.length; i++) {
17
+ if (!component.props[i].name.trim()) {
18
+ issues.push({
19
+ severity: 'error',
20
+ code: 'EMPTY_PROP_NAME',
21
+ message: `Prop at index ${i} has an empty name`,
22
+ field: `props[${i}].name`,
23
+ });
24
+ }
25
+ }
26
+ for (let i = 0; i < component.slots.length; i++) {
27
+ if (!component.slots[i].name.trim()) {
28
+ issues.push({
29
+ severity: 'error',
30
+ code: 'EMPTY_SLOT_NAME',
31
+ message: `Slot at index ${i} has an empty name`,
32
+ field: `slots[${i}].name`,
33
+ });
34
+ }
35
+ }
36
+ const propNames = new Set(component.props.map((p) => p.name));
37
+ for (let i = 0; i < component.slots.length; i++) {
38
+ if (component.slots[i].name && propNames.has(component.slots[i].name)) {
39
+ issues.push({
40
+ severity: 'error',
41
+ code: 'PROP_SLOT_NAME_COLLISION',
42
+ message: `"${component.slots[i].name}" is used as both a prop name and a slot name`,
43
+ field: `slots[${i}].name`,
44
+ });
45
+ }
46
+ }
47
+ const nameKey = component.name.trim();
48
+ if (nameKey && (nameCounts.get(nameKey) ?? 0) > 1) {
49
+ issues.push({
50
+ severity: 'warning',
51
+ code: 'DUPLICATE_COMPONENT_NAME',
52
+ message: `Component name "${component.name}" appears more than once in the extracted set`,
53
+ });
54
+ }
55
+ if (component.props.length === 0 && component.slots.length === 0) {
56
+ issues.push({
57
+ severity: 'warning',
58
+ code: 'EMPTY_COMPONENT',
59
+ message: 'Component has no props or slots and will be filtered out during generation',
60
+ });
61
+ }
62
+ return { ...component, validationIssues: issues };
63
+ });
64
+ }
65
+ export function shouldExcludeDueToValidation(component) {
66
+ return (component.validationIssues ?? []).some((i) => i.severity === 'error');
67
+ }
@@ -1,2 +1,12 @@
1
1
  import type { Command } from 'commander';
2
+ import type { ReviewSessionSnapshot } from './types.js';
3
+ /**
4
+ * Load components from the pipeline DB and re-run extraction validation.
5
+ *
6
+ * `validationIssues` is intentionally not persisted (the validator is pure
7
+ * and cheap to re-run), so any cold-start of `analyze select` from a prior
8
+ * `analyze extract` session needs to recompute it before building the
9
+ * review snapshot — otherwise the TUI sees no validation errors at all.
10
+ */
11
+ export declare function loadAndValidateForReview(sessionId: string, projectRoot: string | undefined): Promise<ReviewSessionSnapshot>;
2
12
  export declare function registerAnalyzeEditCommand(program: Command): void;
@@ -6,6 +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 } from '../extract/validate.js';
9
10
  const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
10
11
  const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
11
12
  function applyDotPath(obj, path, value) {
@@ -84,6 +85,9 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
84
85
  if (rejectPatterns.some((p) => nameLower.includes(p))) {
85
86
  return { ...c, status: 'rejected' };
86
87
  }
88
+ if (selectAll && opts.excludeInvalid && shouldExcludeDueToValidation(c.originalProposal)) {
89
+ return { ...c, status: 'rejected' };
90
+ }
87
91
  if (selectAll || selectPatterns.some((p) => nameLower.includes(p))) {
88
92
  return { ...c, status: 'accepted' };
89
93
  }
@@ -134,6 +138,26 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
134
138
  }
135
139
  process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
136
140
  }
141
+ /**
142
+ * Load components from the pipeline DB and re-run extraction validation.
143
+ *
144
+ * `validationIssues` is intentionally not persisted (the validator is pure
145
+ * and cheap to re-run), so any cold-start of `analyze select` from a prior
146
+ * `analyze extract` session needs to recompute it before building the
147
+ * review snapshot — otherwise the TUI sees no validation errors at all.
148
+ */
149
+ export async function loadAndValidateForReview(sessionId, projectRoot) {
150
+ const db = openPipelineDb();
151
+ let rawComponents;
152
+ try {
153
+ rawComponents = loadRawComponents(db, sessionId);
154
+ }
155
+ finally {
156
+ db.close();
157
+ }
158
+ const validatedComponents = validateExtractedComponents(rawComponents);
159
+ return loadReviewInput(validatedComponents, { reviewRoot: projectRoot });
160
+ }
137
161
  function resolveSessionId(sessionFlag) {
138
162
  if (sessionFlag)
139
163
  return sessionFlag;
@@ -170,17 +194,18 @@ export function registerAnalyzeEditCommand(program) {
170
194
  .option('--accept-all', 'Alias for --select-all', false)
171
195
  .option('--reject <pattern>', 'Alias for --deselect <pattern> (repeatable)', collect, [])
172
196
  .option('--patch <path>', 'Path to a JSON patch file for structured component overrides')
173
- .action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, }) => {
197
+ .option('--exclude-invalid', 'Auto-reject components with validation errors when bulk-selecting (no-op without --select-all)')
198
+ .action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, excludeInvalid, }) => {
174
199
  const sessionId = resolveSessionId(sessionFlag);
175
200
  const db = openPipelineDb();
176
- let rawComponents;
201
+ let rawComponentCount = 0;
177
202
  try {
178
- rawComponents = loadRawComponents(db, sessionId);
203
+ rawComponentCount = loadRawComponents(db, sessionId).length;
179
204
  }
180
205
  finally {
181
206
  db.close();
182
207
  }
183
- if (rawComponents.length === 0) {
208
+ if (rawComponentCount === 0) {
184
209
  process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
185
210
  process.exit(1);
186
211
  return;
@@ -195,9 +220,7 @@ export function registerAnalyzeEditCommand(program) {
195
220
  let paths;
196
221
  let snapshot;
197
222
  try {
198
- snapshot = await loadReviewInput(rawComponents, {
199
- reviewRoot: projectRoot,
200
- });
223
+ snapshot = await loadAndValidateForReview(sessionId, projectRoot);
201
224
  paths = await getRefineSessionPaths(sessionId, artifactsRoot);
202
225
  if (!nonInteractive) {
203
226
  snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
@@ -216,7 +239,17 @@ export function registerAnalyzeEditCommand(program) {
216
239
  const stepDb = openPipelineDb();
217
240
  const stepId = createStep(stepDb, sessionId, 'analyze select', { sessionId });
218
241
  try {
219
- await runNonInteractive(snapshot, { session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch }, paths, sessionId);
242
+ await runNonInteractive(snapshot, {
243
+ session: sessionFlag,
244
+ projectRoot,
245
+ acceptAll,
246
+ selectAll,
247
+ reject,
248
+ deselect,
249
+ select,
250
+ patch,
251
+ excludeInvalid,
252
+ }, paths, sessionId);
220
253
  updateStep(stepDb, stepId, 'complete', { sessionId });
221
254
  }
222
255
  catch (err) {
@@ -4,7 +4,8 @@ import { Box, Text, useStdout } from 'ink';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { createReviewSessionDetail } from '../types.js';
6
6
  import { TopBar } from './components/TopBar.js';
7
- import { Sidebar } from './components/Sidebar.js';
7
+ import { Sidebar, sortComponentsForSidebar } from './components/Sidebar.js';
8
+ import { countValidationIssues } from '../types.js';
8
9
  import { ComponentDetail } from './components/ComponentDetail.js';
9
10
  import { StatusBar } from './components/StatusBar.js';
10
11
  import { HelpOverlay } from './components/HelpOverlay.js';
@@ -316,22 +317,27 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
316
317
  onToggleHelp: () => setShowHelp((prev) => !prev),
317
318
  });
318
319
  // Must be before early returns — Rules of Hooks
319
- const sessionSummary = useMemo(() => (session?.components ?? [])
320
- .map((c) => ({
321
- id: c.id,
322
- name: c.name,
323
- status: c.status,
324
- previewAnnotation: previewAnnotations[c.name],
325
- extractionConfidence: c.originalProposal.extractionConfidence ?? null,
326
- needsReview: c.originalProposal.needsReview ?? false,
327
- }))
320
+ const sessionSummary = useMemo(() => sortComponentsForSidebar((session?.components ?? [])
321
+ .map((c) => {
322
+ const counts = countValidationIssues(c.originalProposal);
323
+ return {
324
+ id: c.id,
325
+ name: c.name,
326
+ status: c.status,
327
+ previewAnnotation: previewAnnotations[c.name],
328
+ extractionConfidence: c.originalProposal.extractionConfidence ?? null,
329
+ needsReview: c.originalProposal.needsReview ?? false,
330
+ validationErrorCount: counts.errors,
331
+ validationWarningCount: counts.warnings,
332
+ };
333
+ })
328
334
  .sort((a, b) => {
329
335
  const aF = a.needsReview && a.status === 'needs-review' ? 0 : 1;
330
336
  const bF = b.needsReview && b.status === 'needs-review' ? 0 : 1;
331
337
  if (aF !== bF)
332
338
  return aF - bF;
333
339
  return (a.extractionConfidence ?? 6) - (b.extractionConfidence ?? 6);
334
- }), [session?.components, previewAnnotations]);
340
+ })), [session?.components, previewAnnotations]);
335
341
  if (loading) {
336
342
  return _jsx(Text, { children: "Loading session..." });
337
343
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { ReviewComponentSummary } from '../../types.js';
2
+ import type { ReviewComponentSummary, ReviewComponentStatus } from '../../types.js';
3
3
  type SidebarProps = {
4
4
  components: ReviewComponentSummary[];
5
5
  selectedId: string | null;
@@ -11,5 +11,8 @@ type SidebarProps = {
11
11
  collapsed?: boolean;
12
12
  width?: number;
13
13
  };
14
+ export declare function statusIcon(status: ReviewComponentStatus, validationErrorCount: number, validationWarningCount: number): string;
15
+ export declare function statusColor(status: ReviewComponentStatus, validationErrorCount: number, validationWarningCount: number): string;
16
+ export declare function sortComponentsForSidebar(components: ReviewComponentSummary[]): ReviewComponentSummary[];
14
17
  export declare function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed, width: widthProp, }: SidebarProps): React.ReactElement;
15
18
  export {};
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- function statusIcon(status) {
3
+ export function statusIcon(status, validationErrorCount, validationWarningCount) {
4
+ if (validationErrorCount > 0 || validationWarningCount > 0)
5
+ return '⚠';
4
6
  switch (status) {
5
7
  case 'accepted':
6
8
  return '✓';
@@ -12,7 +14,11 @@ function statusIcon(status) {
12
14
  return '·';
13
15
  }
14
16
  }
15
- function statusColor(status) {
17
+ export function statusColor(status, validationErrorCount, validationWarningCount) {
18
+ if (validationErrorCount > 0)
19
+ return 'red';
20
+ if (validationWarningCount > 0)
21
+ return 'yellow';
16
22
  switch (status) {
17
23
  case 'accepted':
18
24
  return 'green';
@@ -29,15 +35,22 @@ function truncateName(name, maxLen) {
29
35
  return name;
30
36
  return name.slice(0, maxLen) + '…';
31
37
  }
38
+ export function sortComponentsForSidebar(components) {
39
+ const withErrors = components.filter((c) => c.validationErrorCount > 0);
40
+ const withWarnings = components.filter((c) => c.validationErrorCount === 0 && c.validationWarningCount > 0);
41
+ const clean = components.filter((c) => c.validationErrorCount === 0 && c.validationWarningCount === 0);
42
+ return [...withErrors, ...withWarnings, ...clean];
43
+ }
32
44
  export function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed = false, width: widthProp, }) {
33
- const visible = components.slice(scrollOffset, scrollOffset + visibleCount);
45
+ const sorted = sortComponentsForSidebar(components);
46
+ const visible = sorted.slice(scrollOffset, scrollOffset + visibleCount);
34
47
  const showScrollUp = scrollOffset > 0;
35
- const showScrollDown = scrollOffset + visibleCount < components.length;
48
+ const showScrollDown = scrollOffset + visibleCount < sorted.length;
36
49
  const width = collapsed ? 3 : (widthProp ?? 18);
37
50
  return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: focused ? 'white' : undefined, children: [showScrollUp && !collapsed && _jsx(Text, { dimColor: true, children: "\u25B2" }), visible.map((component) => {
38
51
  const isSelected = component.id === selectedId;
39
- const icon = statusIcon(component.status);
40
- const color = statusColor(component.status);
52
+ const icon = statusIcon(component.status, component.validationErrorCount, component.validationWarningCount);
53
+ const color = statusColor(component.status, component.validationErrorCount, component.validationWarningCount);
41
54
  const maxNameLen = Math.max(1, width - 4);
42
55
  const name = truncateName(component.name, maxNameLen);
43
56
  if (collapsed) {
@@ -24,6 +24,8 @@ export type ReviewComponentSummary = {
24
24
  previewAnnotation?: PreviewAnnotation;
25
25
  extractionConfidence: number | null;
26
26
  needsReview: boolean;
27
+ validationErrorCount: number;
28
+ validationWarningCount: number;
27
29
  };
28
30
  export type ReviewSessionSnapshot = {
29
31
  components: ReviewComponentRecord[];
@@ -44,5 +46,9 @@ export type ReviewSessionPaths = {
44
46
  eventsPath: string;
45
47
  statePath: string;
46
48
  };
49
+ export declare function countValidationIssues(component: RawComponentDefinition): {
50
+ errors: number;
51
+ warnings: number;
52
+ };
47
53
  export declare function createReviewSessionSummary(session: ReviewSessionSnapshot): ReviewSessionSummary;
48
54
  export declare function createReviewSessionDetail(session: ReviewSessionSnapshot): ReviewSessionDetail;
@@ -1,12 +1,29 @@
1
+ export function countValidationIssues(component) {
2
+ const issues = component.validationIssues ?? [];
3
+ let errors = 0;
4
+ let warnings = 0;
5
+ for (const issue of issues) {
6
+ if (issue.severity === 'error')
7
+ errors++;
8
+ else if (issue.severity === 'warning')
9
+ warnings++;
10
+ }
11
+ return { errors, warnings };
12
+ }
1
13
  export function createReviewSessionSummary(session) {
2
14
  return {
3
- components: session.components.map((component) => ({
4
- id: component.id,
5
- name: component.name,
6
- status: component.status,
7
- extractionConfidence: component.originalProposal.extractionConfidence ?? null,
8
- needsReview: component.originalProposal.needsReview ?? false,
9
- })),
15
+ components: session.components.map((component) => {
16
+ const counts = countValidationIssues(component.originalProposal);
17
+ return {
18
+ id: component.id,
19
+ name: component.name,
20
+ status: component.status,
21
+ extractionConfidence: component.originalProposal.extractionConfidence ?? null,
22
+ needsReview: component.originalProposal.needsReview ?? false,
23
+ validationErrorCount: counts.errors,
24
+ validationWarningCount: counts.warnings,
25
+ };
26
+ }),
10
27
  };
11
28
  }
12
29
  export function createReviewSessionDetail(session) {
@@ -6,6 +6,7 @@ import { parseSelectToolCallLines, runAgent } from '../../generate/agent-runner.
6
6
  import { OutputFormatter, c } from '../../output/format.js';
7
7
  import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
8
8
  import { isAbsolute, resolve } from 'node:path';
9
+ import { validateExtractedComponents, shouldExcludeDueToValidation } from '../extract/validate.js';
9
10
  const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
10
11
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
11
12
  const DEFAULT_CONCURRENCY = 5;
@@ -177,6 +178,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
177
178
  .option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
178
179
  .option('--verbose', 'Show full agent output including reasoning text')
179
180
  .option('--dry-run', 'Print the prompt for the first component without invoking the agent')
181
+ .option('--exclude-invalid', 'Automatically reject components with validation errors (empty names, collisions)')
180
182
  .action(async (opts) => {
181
183
  if (!VALID_AGENTS.has(opts.agent)) {
182
184
  process.stderr.write(`Error: unknown agent '${opts.agent}'. Accepted values: claude, codex, opencode, cursor\n`);
@@ -201,6 +203,23 @@ export function registerAnalyzeSelectAgentCommand(program) {
201
203
  process.exit(1);
202
204
  return;
203
205
  }
206
+ // Re-run validation (not persisted to DB, so always recompute).
207
+ const validatedComponents = validateExtractedComponents(rawComponents);
208
+ // If --exclude-invalid, split out errored components before sending to LLM.
209
+ const invalidComponents = opts.excludeInvalid ? validatedComponents.filter(shouldExcludeDueToValidation) : [];
210
+ const componentsForAgent = opts.excludeInvalid
211
+ ? validatedComponents.filter((comp) => !shouldExcludeDueToValidation(comp))
212
+ : validatedComponents;
213
+ if (invalidComponents.length > 0) {
214
+ process.stderr.write(c.yellow(`Auto-rejecting ${invalidComponents.length} component(s) with validation errors (--exclude-invalid):\n`));
215
+ for (const comp of invalidComponents) {
216
+ const codes = (comp.validationIssues ?? [])
217
+ .filter((i) => i.severity === 'error')
218
+ .map((i) => i.code)
219
+ .join(', ');
220
+ process.stderr.write(` ✗ ${comp.name} ${codes}\n`);
221
+ }
222
+ }
204
223
  if (selectionRoot && scannedFiles.length > 0) {
205
224
  scannedFiles = scannedFiles.map((f) => (isAbsolute(f) ? f : resolve(selectionRoot, f)));
206
225
  }
@@ -208,17 +227,22 @@ export function registerAnalyzeSelectAgentCommand(program) {
208
227
  process.stderr.write('warn: session has no scanned-files index (likely extracted on an older CLI version). ' +
209
228
  'Re-run `analyze extract` to enable data-fetch wrapper detection during selection.\n');
210
229
  }
211
- let selectionCandidates = rawComponents.map((component) => ({ component }));
230
+ let selectionCandidates = componentsForAgent.map((component) => ({ component }));
212
231
  if (selectionRoot && scannedFiles.length > 0) {
213
232
  const repoIndex = await buildRepoContextIndex(selectionRoot, scannedFiles).catch(() => null);
214
233
  if (repoIndex) {
215
- selectionCandidates = rawComponents.map((component) => ({
234
+ selectionCandidates = componentsForAgent.map((component) => ({
216
235
  component,
217
236
  selectionContext: buildSelectionContext(repoIndex, component),
218
237
  }));
219
238
  }
220
239
  }
221
240
  if (opts.dryRun) {
241
+ if (selectionCandidates.length === 0) {
242
+ process.stderr.write('No valid components to preview — all components were excluded due to validation errors.\n');
243
+ process.exit(0);
244
+ return;
245
+ }
222
246
  const first = selectionCandidates[0];
223
247
  const prompt = await buildPrompt({
224
248
  skill: 'select',
@@ -231,14 +255,18 @@ export function registerAnalyzeSelectAgentCommand(program) {
231
255
  return;
232
256
  }
233
257
  const selectResults = await selectAllComponents(agent, opts.model, selectionCandidates, opts.verbose ?? false);
258
+ // Build decision map from results. Seed auto-rejections from validation exclusions first.
234
259
  const decisions = new Map();
260
+ for (const comp of invalidComponents) {
261
+ decisions.set(componentKey(comp), 'rejected');
262
+ }
235
263
  for (const r of selectResults) {
236
264
  decisions.set(r.componentKey, r.decision);
237
265
  }
238
266
  const artifactsRoot = getRefineArtifactsRoot();
239
267
  let snapshot;
240
268
  try {
241
- snapshot = await loadReviewInput(rawComponents, {
269
+ snapshot = await loadReviewInput(validatedComponents, {
242
270
  reviewRoot: selectionRoot ?? undefined,
243
271
  });
244
272
  snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
@@ -24,6 +24,7 @@ export function registerImportCommand(program) {
24
24
  .option('--no-cache', 'Re-run all steps even if output already exists')
25
25
  .option('--yes', 'Skip interactive confirmation in apply push')
26
26
  .option('--verbose', 'Show full agent output and all entity progress')
27
+ .option('--exclude-invalid', 'Automatically reject components with validation errors (empty names, collisions)')
27
28
  .option('--viewports <path>', 'JSON file with viewport array (passed to apply push)')
28
29
  .option('--host <url>', 'Override API base URL (passed to apply push)')
29
30
  .option('--dry-run', 'Print generate components prompt without invoking the agent')
@@ -84,6 +85,7 @@ export function registerImportCommand(program) {
84
85
  noCache: opts.cache === false,
85
86
  yes: opts.yes ?? false,
86
87
  verbose: opts.verbose ?? false,
88
+ excludeInvalid: opts.excludeInvalid ?? false,
87
89
  viewports: opts.viewports,
88
90
  host: opts.host,
89
91
  dryRun: opts.dryRun,
@@ -17,6 +17,7 @@ export interface PipelineOptions {
17
17
  viewports?: string;
18
18
  host?: string;
19
19
  dryRun?: boolean;
20
+ excludeInvalid?: boolean;
20
21
  selectAll?: boolean;
21
22
  select?: string[];
22
23
  deselect?: string[];
@@ -128,6 +128,8 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
128
128
  editArgs = ['analyze', 'select-agent', '--session', extractSessionId, '--agent', opts.agent];
129
129
  if (opts.model)
130
130
  editArgs.push('--model', opts.model);
131
+ if (opts.excludeInvalid)
132
+ editArgs.push('--exclude-invalid');
131
133
  }
132
134
  else {
133
135
  editArgs = ['analyze', 'select', '--session', extractSessionId];
@@ -187,6 +187,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
187
187
  status: c.status,
188
188
  extractionConfidence: null,
189
189
  needsReview: false,
190
+ validationErrorCount: 0,
191
+ validationWarningCount: 0,
190
192
  }));
191
193
  const longestName = components.reduce((m, c) => Math.max(m, c.key.length), 0);
192
194
  const sidebarWidth = Math.min(Math.max(longestName + 4, 14), 22);
@@ -1,4 +1,11 @@
1
1
  import type { DesignTokenType } from '@contentful/experience-design-system-types';
2
+ export type ExtractionValidationIssueCode = 'EMPTY_COMPONENT_NAME' | 'EMPTY_PROP_NAME' | 'EMPTY_SLOT_NAME' | 'PROP_SLOT_NAME_COLLISION' | 'DUPLICATE_COMPONENT_NAME' | 'EMPTY_COMPONENT';
3
+ export type ExtractionValidationIssue = {
4
+ severity: 'error' | 'warning';
5
+ code: ExtractionValidationIssueCode;
6
+ message: string;
7
+ field?: string;
8
+ };
2
9
  export interface RawTokenDefinition {
3
10
  name: string;
4
11
  value: string;
@@ -37,6 +44,7 @@ export interface RawComponentDefinition {
37
44
  extractionConfidence?: number | null;
38
45
  reviewReasons?: string[];
39
46
  needsReview?: boolean;
47
+ validationIssues?: ExtractionValidationIssue[];
40
48
  }
41
49
  export interface ComponentExtractionResult {
42
50
  components: RawComponentDefinition[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.7.4-dev-build-595febb.0",
3
+ "version": "2.7.5-dev-build-752dc9c.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "react-dom": "^18.3.1",
37
37
  "ts-morph": "^27.0.2",
38
38
  "typescript": "^5.9.3",
39
- "@contentful/experience-design-system-types": "2.7.4-dev-build-595febb.0"
39
+ "@contentful/experience-design-system-types": "2.7.5-dev-build-752dc9c.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@tsconfig/node24": "^24.0.3",