@contentful/experience-design-system-cli 2.12.1-dev-build-589b615.0 → 2.12.1-dev-build-0b7de73.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.12.1-dev-build-589b615.0",
3
+ "version": "2.12.1-dev-build-0b7de73.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -167,6 +167,7 @@ async function selectBatch(agent, model, batch, total, verbose, skillPathOverrid
167
167
  const call = calls.find((toolCall) => toolCall.name === component.name);
168
168
  if (!call) {
169
169
  process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('no tool call')}\n`);
170
+ emitProgress(item, 'failed', 'no-tool-call-from-agent');
170
171
  results.push({
171
172
  componentKey: componentKey(component),
172
173
  componentName: component.name,
@@ -7,6 +7,7 @@ import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
7
7
  import { replayRun, modifyRun } from '../runs/replay-helpers.js';
8
8
  import { resolvePromptFlags } from './print-prompt.js';
9
9
  import { shouldShowRunPicker } from '../runs/run-picker-mount.js';
10
+ import { dispatchPickerSelection } from './picker-dispatch.js';
10
11
  export function registerImportCommand(program) {
11
12
  program
12
13
  .command('import')
@@ -220,15 +221,20 @@ export function registerImportCommand(program) {
220
221
  isTTY: !!process.stdin.isTTY,
221
222
  });
222
223
  let pickerSelection = null;
224
+ // Ink `unmount` handle captured after `render(...)` below so the
225
+ // picker callback can tear down the wizard cleanly. Prior to this
226
+ // fix the callback did `setImmediate(() => process.exit(0))`, which
227
+ // killed the process before `dispatchPickerSelection` could run.
228
+ let unmountInk = null;
223
229
  const pickerProps = {};
224
230
  if (pickerDecision.shouldShow) {
225
231
  pickerProps.initialRuns = pickerDecision.runs;
226
232
  pickerProps.onRunPicked = (selection) => {
227
233
  pickerSelection = selection;
228
- setImmediate(() => process.exit(0));
234
+ unmountInk?.();
229
235
  };
230
236
  }
231
- const { waitUntilExit } = render(createElement(WizardApp, {
237
+ const { waitUntilExit, unmount } = render(createElement(WizardApp, {
232
238
  initialSpaceId: creds.spaceId,
233
239
  initialEnvironmentId: creds.environmentId || 'master',
234
240
  initialCmaToken: creds.cmaToken,
@@ -250,43 +256,24 @@ export function registerImportCommand(program) {
250
256
  ...(opts.rawTokens ? { initialRawTokensPath: resolve(opts.rawTokens) } : {}),
251
257
  ...pickerProps,
252
258
  }));
253
- try {
254
- await waitUntilExit();
255
- }
256
- catch {
257
- /* Ink throws on process.exit; swallow so picker dispatch can run. */
258
- }
259
+ unmountInk = unmount;
260
+ await waitUntilExit();
259
261
  // ── Picker dispatch ─────────────────────────────────────────────
260
- // If the operator picked a run, route into the existing entry
261
- // points so credential resolution and mutex checks stay in one
262
- // place. The --modify path resolves the run record via
263
- // resolveRunTarget and threads its session IDs through
264
- // launchModifyWizard (see runs/replay-helpers.ts).
262
+ // If the operator picked a run, route into replayRun / modifyRun.
263
+ // dispatchPickerSelection lives in ./picker-dispatch.ts so this
264
+ // decision is testable without an Ink runtime.
265
265
  if (pickerSelection) {
266
- const sel = pickerSelection;
267
- if (sel.action === 'push' && sel.runId) {
268
- await replayRun({
269
- runIdOrPath: sel.runId,
270
- ...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
271
- ...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
272
- ...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
273
- ...(opts.host ? { host: opts.host } : {}),
274
- interactive: !!process.stdout.isTTY,
275
- ...(opts.force ? { force: true } : {}),
276
- });
277
- return;
278
- }
279
- if (sel.action === 'modify' && sel.runId) {
280
- await modifyRun({
281
- runIdOrPath: sel.runId,
282
- ...(opts.outDir ? { outDir: opts.outDir } : {}),
283
- ...(opts.overwrite ? { overwrite: true } : {}),
284
- ...(opts.saveAsNew ? { saveAsNew: true } : {}),
285
- ...(opts.force ? { force: true } : {}),
286
- });
287
- return;
288
- }
289
- // action === 'new' falls through — the wizard already advanced.
266
+ await dispatchPickerSelection(pickerSelection, {
267
+ ...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
268
+ ...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
269
+ ...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
270
+ ...(opts.host ? { host: opts.host } : {}),
271
+ interactive: !!process.stdout.isTTY,
272
+ ...(opts.outDir ? { outDir: opts.outDir } : {}),
273
+ ...(opts.overwrite ? { overwrite: true } : {}),
274
+ ...(opts.saveAsNew ? { saveAsNew: true } : {}),
275
+ ...(opts.force ? { force: true } : {}),
276
+ }, { replayRun, modifyRun });
290
277
  }
291
278
  return;
292
279
  }
@@ -0,0 +1,18 @@
1
+ import type { RunPickerSelection } from '../runs/run-picker.js';
2
+ import type { replayRun as replayRunFn, modifyRun as modifyRunFn } from '../runs/replay-helpers.js';
3
+ export type PickerDispatchOptions = {
4
+ spaceId?: string;
5
+ environmentId?: string;
6
+ cmaToken?: string;
7
+ host?: string;
8
+ interactive?: boolean;
9
+ outDir?: string;
10
+ overwrite?: boolean;
11
+ saveAsNew?: boolean;
12
+ force?: boolean;
13
+ };
14
+ export type PickerDispatchDeps = {
15
+ replayRun: typeof replayRunFn;
16
+ modifyRun: typeof modifyRunFn;
17
+ };
18
+ export declare function dispatchPickerSelection(selection: RunPickerSelection, opts: PickerDispatchOptions, deps: PickerDispatchDeps): Promise<void>;
@@ -0,0 +1,29 @@
1
+ // Route a resolved RunPickerSelection into replayRun / modifyRun. Extracted
2
+ // from command.ts so the dispatch decision is testable without an Ink runtime
3
+ // and so the picker callback in command.ts is a simple state-capture (no
4
+ // process.exit shenanigans that would kill the process before dispatch runs).
5
+ export async function dispatchPickerSelection(selection, opts, deps) {
6
+ if (selection.action === 'push' && selection.runId) {
7
+ await deps.replayRun({
8
+ runIdOrPath: selection.runId,
9
+ ...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
10
+ ...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
11
+ ...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
12
+ ...(opts.host ? { host: opts.host } : {}),
13
+ ...(opts.interactive !== undefined ? { interactive: opts.interactive } : {}),
14
+ ...(opts.force ? { force: true } : {}),
15
+ });
16
+ return;
17
+ }
18
+ if (selection.action === 'modify' && selection.runId) {
19
+ await deps.modifyRun({
20
+ runIdOrPath: selection.runId,
21
+ ...(opts.outDir ? { outDir: opts.outDir } : {}),
22
+ ...(opts.overwrite ? { overwrite: true } : {}),
23
+ ...(opts.saveAsNew ? { saveAsNew: true } : {}),
24
+ ...(opts.force ? { force: true } : {}),
25
+ });
26
+ return;
27
+ }
28
+ // action === 'new' → no-op; the wizard already advanced past the picker.
29
+ }
@@ -19,7 +19,7 @@ export declare function buildSelectAgentArgs(opts: {
19
19
  export type AutoFilterProgress = {
20
20
  n: number;
21
21
  total: number;
22
- decision: 'accepted' | 'rejected';
22
+ decision: 'accepted' | 'rejected' | 'failed';
23
23
  name: string;
24
24
  reason: string;
25
25
  };
@@ -40,6 +40,7 @@ import { readTokensFromPath, hasBreakingChangesWithImpact } from '../../apply/ma
40
40
  import { buildManifest } from '@contentful/experience-design-system-types';
41
41
  import { openPipelineDb, loadCDFComponents, loadScopeComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
42
42
  import { ScopeGateHost } from './scope-gate-host.js';
43
+ import { mergeAiDecisions } from './merge-ai-decisions.js';
43
44
  import { FinalReviewHost } from './final-review-host.js';
44
45
  import { runScopeGate } from './runScopeGate.js';
45
46
  import { buildAutoFilterErrorTail } from './auto-filter-error.js';
@@ -83,7 +84,7 @@ export function parseAutoFilterProgressLine(line) {
83
84
  const counterMatch = /^(\d+)\/(\d+)$/.exec(counter);
84
85
  if (!counterMatch)
85
86
  return null;
86
- if (decision !== 'accepted' && decision !== 'rejected')
87
+ if (decision !== 'accepted' && decision !== 'rejected' && decision !== 'failed')
87
88
  return null;
88
89
  if (!name)
89
90
  return null;
@@ -1399,6 +1400,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1399
1400
  finally {
1400
1401
  db.close();
1401
1402
  }
1403
+ // INTEG-4318: overlay the streamed auto-filter decisions (from
1404
+ // stderr progress lines) onto DB-loaded rows so 'failed' components
1405
+ // (LLM omitted a tool call in a batch) surface in the scope-gate
1406
+ // instead of silently defaulting to included.
1407
+ components = mergeAiDecisions(components, state.aiDecisions);
1402
1408
  return (_jsx(ScopeGateHost, { components: components, autoAccept: autoAcceptScope, aiFilterStatus: state.aiFilterStatus, aiFilterProgress: state.aiFilterProgress, aiFilterError: state.aiFilterError, onCancelAutoFilter: cancelAutoFilter, onConfirm: (decisions) => {
1403
1409
  void runScopeGate({
1404
1410
  sessionId,
@@ -0,0 +1,5 @@
1
+ import type { ScopeComponent } from './steps/ScopeGateStep.js';
2
+ export declare function mergeAiDecisions(components: ReadonlyArray<ScopeComponent>, aiDecisions: Record<string, {
3
+ decision: 'accepted' | 'rejected' | 'failed';
4
+ reason: string;
5
+ }>): ScopeComponent[];
@@ -0,0 +1,22 @@
1
+ // INTEG-4318: overlay streamed auto-filter decisions (from the select-agent
2
+ // child's stderr progress lines) onto components loaded from raw_components.
3
+ // Only fills gaps where the DB has no decision — the DB row is authoritative
4
+ // once the child persists its status. This is what lets the scope-gate see
5
+ // a 'failed' status for components where the LLM omitted a tool call in a
6
+ // batch (the select-agent does not persist a 'failed' status to the DB).
7
+ export function mergeAiDecisions(components, aiDecisions) {
8
+ return components.map((component) => {
9
+ if (component.aiDecision !== null && component.aiDecision !== undefined) {
10
+ return component;
11
+ }
12
+ const streamed = aiDecisions[component.name];
13
+ if (!streamed) {
14
+ return component;
15
+ }
16
+ return {
17
+ ...component,
18
+ aiDecision: streamed.decision,
19
+ aiReason: streamed.reason,
20
+ };
21
+ });
22
+ }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  export type ScopeComponent = {
3
3
  name: string;
4
4
  componentId: string;
5
- aiDecision?: 'accepted' | 'rejected' | null;
5
+ aiDecision?: 'accepted' | 'rejected' | 'failed' | null;
6
6
  aiReason?: string | null;
7
7
  };
8
8
  export type ScopeGateStepProps = {
@@ -20,7 +20,11 @@ function truncateReason(reason) {
20
20
  return reason.slice(0, REASON_DISPLAY_MAX - 1).trimEnd() + '…';
21
21
  }
22
22
  function isAiFlagged(row) {
23
- return row.aiDecision === 'rejected';
23
+ // INTEG-4318: `failed` means the LLM omitted a decision for this component
24
+ // (e.g. batch under-emit). Surface these in the AI-recommended-exclusions
25
+ // section so the operator sees them and can override — silent inclusion was
26
+ // the bug.
27
+ return row.aiDecision === 'rejected' || row.aiDecision === 'failed';
24
28
  }
25
29
  export function ScopeGateStep({ components, onConfirm, onQuit, aiFilterStatus = 'idle', aiFilterProgress = null, aiFilterError = null, onCancelAutoFilter, }) {
26
30
  // Pilot-2026-06-25: scope-gate UX overhaul — single unified list.
@@ -60,7 +64,10 @@ export function ScopeGateStep({ components, onConfirm, onQuit, aiFilterStatus =
60
64
  return false;
61
65
  if (userUnExcluded.has(row.name))
62
66
  return true;
63
- return row.aiDecision !== 'rejected';
67
+ // INTEG-4318: exclude on 'rejected' AND 'failed'. Missing/null aiDecision
68
+ // (auto-filter not run, or component never seen) still defaults to
69
+ // included so --no-auto-filter and skip-credentials flows are unchanged.
70
+ return row.aiDecision !== 'rejected' && row.aiDecision !== 'failed';
64
71
  };
65
72
  const partition = () => {
66
73
  const accepted = [];
@@ -216,7 +223,7 @@ export function ScopeGateStep({ components, onConfirm, onQuit, aiFilterStatus =
216
223
  const showCancelledBanner = aiFilterStatus === 'cancelled';
217
224
  const showFailedBanner = aiFilterStatus === 'failed';
218
225
  const allRejected = aiFilterStatus === 'complete' && total > 0 && flatList.every((c) => !isIncluded(c));
219
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "green", children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", total, " component", total === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), showRunningHeader && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { color: "cyan", children: ["[AI filtering (", aiFilterProgress.done, "/", aiFilterProgress.total, ")\u2026] ", _jsx(Text, { dimColor: true, children: "[q] cancels" })] }) })), showCancelledBanner && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["AI auto-filter cancelled", aiFilterProgress ? ` at ${aiFilterProgress.done}/${aiFilterProgress.total}` : '', ". Review remaining manually."] }) })), showFailedBanner && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["AI auto-filter failed: ", aiFilterError ?? 'unknown error', ". Continuing without AI suggestions."] }) })), reasonPanelOpen && flatList[cursor]?.aiDecision === 'rejected' && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `AI rejection reason: ${flatList[cursor].name}` }), _jsx(Text, { children: flatList[cursor].aiReason ?? '<no reason given>' }), _jsx(Text, { dimColor: true, children: "[s] close \u00B7 [Esc] close" })] })), allRejected ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "AI excluded all components \u2014 press [a] to override or [q] to quit" }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [above > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", above, " above"] }), visible.map((c, vi) => {
226
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "green", children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", total, " component", total === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), showRunningHeader && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { color: "cyan", children: ["[AI filtering (", aiFilterProgress.done, "/", aiFilterProgress.total, ")\u2026] ", _jsx(Text, { dimColor: true, children: "[q] cancels" })] }) })), showCancelledBanner && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["AI auto-filter cancelled", aiFilterProgress ? ` at ${aiFilterProgress.done}/${aiFilterProgress.total}` : '', ". Review remaining manually."] }) })), showFailedBanner && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["AI auto-filter failed: ", aiFilterError ?? 'unknown error', ". Continuing without AI suggestions."] }) })), reasonPanelOpen && flatList[cursor] !== undefined && isAiFlagged(flatList[cursor]) && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `AI rejection reason: ${flatList[cursor].name}` }), _jsx(Text, { children: flatList[cursor].aiReason ?? '<no reason given>' }), _jsx(Text, { dimColor: true, children: "[s] close \u00B7 [Esc] close" })] })), allRejected ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "AI excluded all components \u2014 press [a] to override or [q] to quit" }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [above > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", above, " above"] }), visible.map((c, vi) => {
220
227
  const i = vi + scrollOffset;
221
228
  const isCursor = i === cursor;
222
229
  const included = isIncluded(c);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.12.1-dev-build-589b615.0",
3
+ "version": "2.12.1-dev-build-0b7de73.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.1-dev-build-589b615.0"
40
+ "@contentful/experience-design-system-types": "2.12.1-dev-build-0b7de73.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tsconfig/node24": "^24.0.3",