@contentful/experience-design-system-cli 2.7.5-dev-build-752dc9c.0 → 2.10.1-dev-build-90df010.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/build-analyze-view-rows.d.ts +23 -0
- package/dist/src/analyze/build-analyze-view-rows.js +39 -0
- package/dist/src/analyze/command.js +13 -10
- package/dist/src/analyze/extract/validate.d.ts +13 -1
- package/dist/src/analyze/extract/validate.js +32 -5
- package/dist/src/analyze/select/command.d.ts +37 -0
- package/dist/src/analyze/select/command.js +200 -9
- package/dist/src/analyze/select/tui/App.js +1 -9
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +1 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +26 -3
- package/dist/src/analyze/select-agent/command.js +22 -10
- package/dist/src/analyze/tui/AnalyzeView.d.ts +8 -0
- package/dist/src/analyze/tui/AnalyzeView.js +5 -2
- package/dist/src/apply/api-client.d.ts +20 -0
- package/dist/src/apply/api-client.js +71 -5
- package/dist/src/generate/command.js +21 -2
- package/dist/src/import/orchestrator.d.ts +21 -0
- package/dist/src/import/orchestrator.js +95 -14
- package/dist/src/import/tui/WizardApp.d.ts +13 -0
- package/dist/src/import/tui/WizardApp.js +224 -52
- package/dist/src/import/tui/steps/GateStep.d.ts +2 -1
- package/dist/src/import/tui/steps/GateStep.js +4 -2
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.d.ts +11 -0
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +14 -0
- package/dist/src/import/tui/wizard-422-helpers.d.ts +48 -0
- package/dist/src/import/tui/wizard-422-helpers.js +60 -0
- package/dist/src/program.d.ts +17 -0
- package/dist/src/program.js +27 -4
- package/dist/src/session/db.d.ts +13 -0
- package/dist/src/session/db.js +50 -0
- package/dist/src/types.d.ts +1 -1
- package/package.json +2 -2
- package/skills/generate-components.md +2 -0
|
@@ -6,7 +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
|
+
import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
|
|
10
10
|
const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
|
|
11
11
|
const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
|
|
12
12
|
const DEFAULT_CONCURRENCY = 5;
|
|
@@ -178,7 +178,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
178
178
|
.option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
|
|
179
179
|
.option('--verbose', 'Show full agent output including reasoning text')
|
|
180
180
|
.option('--dry-run', 'Print the prompt for the first component without invoking the agent')
|
|
181
|
-
.option('--exclude-invalid', '
|
|
181
|
+
.option('--exclude-invalid', 'Auto-reject components with validation errors instead of failing loud (LLM cannot fix structural issues)')
|
|
182
182
|
.action(async (opts) => {
|
|
183
183
|
if (!VALID_AGENTS.has(opts.agent)) {
|
|
184
184
|
process.stderr.write(`Error: unknown agent '${opts.agent}'. Accepted values: claude, codex, opencode, cursor\n`);
|
|
@@ -205,20 +205,32 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
205
205
|
}
|
|
206
206
|
// Re-run validation (not persisted to DB, so always recompute).
|
|
207
207
|
const validatedComponents = validateExtractedComponents(rawComponents);
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
// Fail-loud gate: if any component has validation errors, refuse to
|
|
209
|
+
// proceed unless --exclude-invalid is set. The LLM cannot fix
|
|
210
|
+
// structural issues (empty names, slot/prop collisions, etc.) — silent
|
|
211
|
+
// exclusion in this CI-style command would drop components without the
|
|
212
|
+
// caller noticing.
|
|
213
|
+
const invalidComponents = validatedComponents.filter(shouldExcludeDueToValidation);
|
|
214
|
+
if (invalidComponents.length > 0 && !opts.excludeInvalid) {
|
|
215
|
+
const lines = [
|
|
216
|
+
`Error: ${invalidComponents.length} component(s) failed validation; refusing select-agent without --exclude-invalid:`,
|
|
217
|
+
];
|
|
215
218
|
for (const comp of invalidComponents) {
|
|
216
219
|
const codes = (comp.validationIssues ?? [])
|
|
217
220
|
.filter((i) => i.severity === 'error')
|
|
218
221
|
.map((i) => i.code)
|
|
219
222
|
.join(', ');
|
|
220
|
-
|
|
223
|
+
lines.push(` ✗ ${comp.name} ${codes}`);
|
|
221
224
|
}
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push('Re-run with --exclude-invalid to auto-reject these components, or fix them in source first.');
|
|
227
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const componentsForAgent = validatedComponents.filter((comp) => !shouldExcludeDueToValidation(comp));
|
|
232
|
+
if (invalidComponents.length > 0) {
|
|
233
|
+
process.stderr.write(c.yellow(formatExclusionWarning(invalidComponents)));
|
|
222
234
|
}
|
|
223
235
|
if (selectionRoot && scannedFiles.length > 0) {
|
|
224
236
|
scannedFiles = scannedFiles.map((f) => (isAbsolute(f) ? f : resolve(selectionRoot, f)));
|
|
@@ -9,10 +9,18 @@ export type AnalyzeViewResult = {
|
|
|
9
9
|
propCount: number;
|
|
10
10
|
slotCount: number;
|
|
11
11
|
warnings: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Error-severity validation messages. Components with any errors will be
|
|
14
|
+
* auto-rejected by the next step (`analyze select --select-all`), so the
|
|
15
|
+
* extract TUI surfaces them prominently — both with a per-row ✗ badge
|
|
16
|
+
* and a dedicated Errors section below the component list.
|
|
17
|
+
*/
|
|
18
|
+
errors: string[];
|
|
12
19
|
extractionConfidence: number | null;
|
|
13
20
|
needsReview: boolean;
|
|
14
21
|
}>;
|
|
15
22
|
totalWarnings: number;
|
|
23
|
+
totalErrors: number;
|
|
16
24
|
};
|
|
17
25
|
type AnalyzeViewProps = {
|
|
18
26
|
result: AnalyzeViewResult;
|
|
@@ -53,8 +53,11 @@ export function AnalyzeView({ result, onExit }) {
|
|
|
53
53
|
const conf = component.extractionConfidence;
|
|
54
54
|
const confColor = conf === null ? 'gray' : component.needsReview ? 'red' : conf >= 4 ? 'white' : conf >= 3 ? 'yellow' : 'red';
|
|
55
55
|
const confLabel = conf === null ? '—' : (component.needsReview ? '⚑ ' : '') + String(conf);
|
|
56
|
-
return (_jsxs(Box, { children: [component.warnings.length > 0 && _jsx(Text, { color: "yellow", children: "\u26A0 " }), component.warnings.length === 0 && _jsx(Text, { children: " " }), _jsx(Text, { children: truncateName(component.name).padEnd(20) }), _jsx(Text, { dimColor: true, children: component.framework.padEnd(10) }), _jsx(Text, { children: (component.propCount + ' props').padEnd(10) }), _jsx(Text, { children: (component.slotCount + ' ' + (component.slotCount === 1 ? 'slot' : 'slots')).padEnd(8) }), _jsx(Text, { color: confColor, children: confLabel }), component.warnings.length > 0 && (_jsx(Text, { color: "yellow", children: ' ⚠ ' + component.warnings.length + ' warning' + (component.warnings.length === 1 ? '' : 's') }))] }, component.name));
|
|
57
|
-
}), showScrollDown && _jsx(Text, { dimColor: true, children: " \u25BC scroll down" }), result.
|
|
56
|
+
return (_jsxs(Box, { children: [component.errors.length > 0 && _jsx(Text, { color: "red", children: "\u2717 " }), component.errors.length === 0 && component.warnings.length > 0 && _jsx(Text, { color: "yellow", children: "\u26A0 " }), component.errors.length === 0 && component.warnings.length === 0 && _jsx(Text, { children: " " }), _jsx(Text, { children: truncateName(component.name).padEnd(20) }), _jsx(Text, { dimColor: true, children: component.framework.padEnd(10) }), _jsx(Text, { children: (component.propCount + ' props').padEnd(10) }), _jsx(Text, { children: (component.slotCount + ' ' + (component.slotCount === 1 ? 'slot' : 'slots')).padEnd(8) }), _jsx(Text, { color: confColor, children: confLabel }), component.errors.length > 0 && (_jsx(Text, { color: "red", children: ' ✗ ' + component.errors.length + ' error' + (component.errors.length === 1 ? '' : 's') })), component.errors.length === 0 && component.warnings.length > 0 && (_jsx(Text, { color: "yellow", children: ' ⚠ ' + component.warnings.length + ' warning' + (component.warnings.length === 1 ? '' : 's') }))] }, component.name));
|
|
57
|
+
}), showScrollDown && _jsx(Text, { dimColor: true, children: " \u25BC scroll down" }), result.totalErrors > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, color: "red", children: 'Errors (' + result.totalErrors + ')' }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), result.components
|
|
58
|
+
.filter((c) => c.errors.length > 0)
|
|
59
|
+
.flatMap((c) => c.errors.map((e) => ({ component: c.name, error: e })))
|
|
60
|
+
.map((e, i) => (_jsx(Text, { color: "red", children: ' ✗ ' + e.component + ': ' + e.error }, i)))] })), result.totalWarnings > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, color: "yellow", children: 'Warnings (' + result.totalWarnings + ')' }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), result.components
|
|
58
61
|
.filter((c) => c.warnings.length > 0)
|
|
59
62
|
.flatMap((c) => c.warnings.map((w) => ({ component: c.name, warning: w })))
|
|
60
63
|
.map((w, i) => (_jsx(Text, { color: "yellow", children: ' ⚠ ' + w.component + ': ' + w.warning }, i)))] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: 'Run: analyze select --session ' + result.sessionId }), _jsx(Text, { children: " " })] }), _jsx(Box, { borderStyle: "single", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter or q to exit" }) })] }));
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ManifestPayload, ServerPreviewResponse, ApplyOperationResponse } from '@contentful/experience-design-system-types';
|
|
2
2
|
export declare const DEFAULT_HOST = "https://api.contentful.com";
|
|
3
|
+
export declare const PREVIEW_ERROR_PREFIX = "preview failed:";
|
|
4
|
+
export declare const APPLY_ERROR_PREFIX = "apply failed:";
|
|
5
|
+
export declare const VALIDATION_FAILED_CODE = "\"ValidationFailed\"";
|
|
3
6
|
export interface ApiClientOptions {
|
|
4
7
|
host?: string;
|
|
5
8
|
cmaToken: string;
|
|
@@ -11,6 +14,23 @@ export declare class ApiError extends Error {
|
|
|
11
14
|
readonly body: string;
|
|
12
15
|
constructor(message: string, status: number, body: string);
|
|
13
16
|
}
|
|
17
|
+
export interface PreviewValidationError {
|
|
18
|
+
componentName: string;
|
|
19
|
+
path: string;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse the JSON body of a 422 from `previewImport()` into structured
|
|
24
|
+
* per-component validation errors. Returns [] for any malformed input
|
|
25
|
+
* so callers can fall back to the generic error path without try/catch.
|
|
26
|
+
*
|
|
27
|
+
* Path shape: `manifest:components/<Name>/$slots/<key>` or
|
|
28
|
+
* `manifest:components/<Name>/$properties/<key>`. Only the component
|
|
29
|
+
* name is extracted today; `path` and `message` are kept verbatim so
|
|
30
|
+
* future surfaces (debug logging, headless retry in SP-4) can render
|
|
31
|
+
* the field-level detail.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parsePreviewValidationErrors(body: string): PreviewValidationError[];
|
|
14
34
|
export declare class ImportApiClient {
|
|
15
35
|
private host;
|
|
16
36
|
private token;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { DEFAULT_API_HOST, toApiHost } from '../host-utils.js';
|
|
2
2
|
export const DEFAULT_HOST = DEFAULT_API_HOST;
|
|
3
|
+
// Phase-prefix constants used at the two ApiError throw sites below and
|
|
4
|
+
// imported by orchestrator.ts to identify preview-phase 422s for retry.
|
|
5
|
+
export const PREVIEW_ERROR_PREFIX = 'preview failed:';
|
|
6
|
+
export const APPLY_ERROR_PREFIX = 'apply failed:';
|
|
7
|
+
// Substring match the orchestrator uses to distinguish a parseable
|
|
8
|
+
// component-level validation failure from generic 422s. Quoted because the
|
|
9
|
+
// match runs against the raw JSON body (which contains `"code":"ValidationFailed"`).
|
|
10
|
+
// If the server ever changes the casing or naming, isPreviewValidationError
|
|
11
|
+
// silently returns false and the retry loop never fires — so this lives next
|
|
12
|
+
// to the prefixes as a deliberate, named contract rather than an inline
|
|
13
|
+
// magic string in the orchestrator.
|
|
14
|
+
export const VALIDATION_FAILED_CODE = '"ValidationFailed"';
|
|
15
|
+
// Cap on the body slice appended to ApiError.message. Bumped from 1000 →
|
|
16
|
+
// 16384 so realistic 422 ValidationFailed reports (which list every
|
|
17
|
+
// offending component, ~100 chars per error, easily exceeds 1KB once you
|
|
18
|
+
// cross ~10 components) survive intact through subprocess stderr. The
|
|
19
|
+
// orchestrator's parseOffendingComponentNames does JSON.parse on this slice
|
|
20
|
+
// and silently fails to recover any offenders if the JSON is mid-truncated.
|
|
21
|
+
// The cap stays in place to keep a runaway server response from blowing up
|
|
22
|
+
// log output.
|
|
23
|
+
const ERROR_BODY_LOG_CAP = 16384;
|
|
3
24
|
export class ApiError extends Error {
|
|
4
25
|
status;
|
|
5
26
|
body;
|
|
@@ -8,13 +29,58 @@ export class ApiError extends Error {
|
|
|
8
29
|
this.status = status;
|
|
9
30
|
this.body = body;
|
|
10
31
|
if (body) {
|
|
11
|
-
// Append a trimmed version of the response body so callers
|
|
12
|
-
// log e.message don't silently swallow the server's error
|
|
13
|
-
|
|
32
|
+
// Append a (possibly trimmed) version of the response body so callers
|
|
33
|
+
// that only log e.message don't silently swallow the server's error
|
|
34
|
+
// detail.
|
|
35
|
+
const trimmed = body.length > ERROR_BODY_LOG_CAP ? body.slice(0, ERROR_BODY_LOG_CAP) + '…' : body;
|
|
14
36
|
this.message = `${message}\n${trimmed}`;
|
|
15
37
|
}
|
|
16
38
|
}
|
|
17
39
|
}
|
|
40
|
+
const COMPONENT_PATH_PREFIX = 'manifest:components/';
|
|
41
|
+
/**
|
|
42
|
+
* Parse the JSON body of a 422 from `previewImport()` into structured
|
|
43
|
+
* per-component validation errors. Returns [] for any malformed input
|
|
44
|
+
* so callers can fall back to the generic error path without try/catch.
|
|
45
|
+
*
|
|
46
|
+
* Path shape: `manifest:components/<Name>/$slots/<key>` or
|
|
47
|
+
* `manifest:components/<Name>/$properties/<key>`. Only the component
|
|
48
|
+
* name is extracted today; `path` and `message` are kept verbatim so
|
|
49
|
+
* future surfaces (debug logging, headless retry in SP-4) can render
|
|
50
|
+
* the field-level detail.
|
|
51
|
+
*/
|
|
52
|
+
export function parsePreviewValidationErrors(body) {
|
|
53
|
+
if (!body)
|
|
54
|
+
return [];
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(body);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const details = parsed?.details;
|
|
63
|
+
const errors = details?.errors;
|
|
64
|
+
if (!Array.isArray(errors))
|
|
65
|
+
return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const raw of errors) {
|
|
68
|
+
if (typeof raw !== 'object' || raw === null)
|
|
69
|
+
continue;
|
|
70
|
+
const entry = raw;
|
|
71
|
+
if (typeof entry.path !== 'string' || typeof entry.message !== 'string')
|
|
72
|
+
continue;
|
|
73
|
+
if (!entry.path.startsWith(COMPONENT_PATH_PREFIX))
|
|
74
|
+
continue;
|
|
75
|
+
const tail = entry.path.slice(COMPONENT_PATH_PREFIX.length);
|
|
76
|
+
const slash = tail.indexOf('/');
|
|
77
|
+
const componentName = slash === -1 ? tail : tail.slice(0, slash);
|
|
78
|
+
if (!componentName)
|
|
79
|
+
continue;
|
|
80
|
+
out.push({ componentName, path: entry.path, message: entry.message });
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
18
84
|
async function request(url, options) {
|
|
19
85
|
const headers = {
|
|
20
86
|
Authorization: `Bearer ${options.token}`,
|
|
@@ -68,7 +134,7 @@ export class ImportApiClient {
|
|
|
68
134
|
body: JSON.stringify(manifest),
|
|
69
135
|
});
|
|
70
136
|
if (!res.ok) {
|
|
71
|
-
throw new ApiError(
|
|
137
|
+
throw new ApiError(`${PREVIEW_ERROR_PREFIX} ${res.status}`, res.status, await res.text());
|
|
72
138
|
}
|
|
73
139
|
return (await res.json());
|
|
74
140
|
}
|
|
@@ -80,7 +146,7 @@ export class ImportApiClient {
|
|
|
80
146
|
body: JSON.stringify({ ...manifest, acknowledgeBreakingChanges }),
|
|
81
147
|
});
|
|
82
148
|
if (!res.ok) {
|
|
83
|
-
throw new ApiError(
|
|
149
|
+
throw new ApiError(`${APPLY_ERROR_PREFIX} ${res.status}`, res.status, await res.text());
|
|
84
150
|
}
|
|
85
151
|
return (await res.json());
|
|
86
152
|
}
|
|
@@ -9,7 +9,7 @@ import { OutputFormatter, c } from '../output/format.js';
|
|
|
9
9
|
import { buildPrompt, resolveSkillPath } from './prompt-builder.js';
|
|
10
10
|
import { GenerateView } from './tui/GenerateView.js';
|
|
11
11
|
import { registerGenerateEditCommand } from './edit/command.js';
|
|
12
|
-
import { openPipelineDb, loadRawComponents, applyToolCalls, applyTokenToolCalls, computeComponentInputHash, computeTokenInputHash, lookupCache, lookupCacheByEntity, storeCache, copyComponentFromCache, copyTokensFromCache, } from '../session/db.js';
|
|
12
|
+
import { openPipelineDb, loadRawComponents, applyToolCalls, applyTokenToolCalls, computeComponentInputHash, computeTokenInputHash, lookupCache, lookupCacheByEntity, storeCache, copyComponentFromCache, copyTokensFromCache, renameEmptySlots, } from '../session/db.js';
|
|
13
13
|
import { getRefineArtifactsRoot, getRefineSessionPaths } from '../analyze/select/persistence.js';
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
15
|
const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
|
|
@@ -119,6 +119,7 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
|
|
|
119
119
|
warnings: [],
|
|
120
120
|
failed: false,
|
|
121
121
|
cached: true,
|
|
122
|
+
renamedSlotsCount: 0,
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
125
|
// Check for pinned (human-edited) entry with a different hash
|
|
@@ -134,16 +135,28 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
|
|
|
134
135
|
warnings: [`${component.name}: source changed but human edits preserved`],
|
|
135
136
|
failed: false,
|
|
136
137
|
cached: true,
|
|
138
|
+
renamedSlotsCount: 0,
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
}
|
|
142
|
+
// Rename empty-named slots in the DB and patch the in-memory component so the
|
|
143
|
+
// prompt sees the heuristic names. applyToolCalls matches by name — a row with
|
|
144
|
+
// name="" would never be reachable by a classify_slot call otherwise.
|
|
145
|
+
const { renames, warnings: renameWarnings } = renameEmptySlots(db, sessionId, component.component_id, component.name, component.slots.length);
|
|
146
|
+
let effectiveSlots = component.slots;
|
|
147
|
+
if (renames.length > 0) {
|
|
148
|
+
const renameMap = new Map(renames.map((r) => [r.oldName, r.newName]));
|
|
149
|
+
effectiveSlots = component.slots.map((s) => (renameMap.has(s.name) ? { ...s, name: renameMap.get(s.name) } : s));
|
|
150
|
+
for (const w of renameWarnings)
|
|
151
|
+
process.stderr.write(` ${c.yellow('⚠')} ${w}\n`);
|
|
152
|
+
}
|
|
140
153
|
const rawComponentsInline = JSON.stringify([
|
|
141
154
|
{
|
|
142
155
|
name: component.name,
|
|
143
156
|
source: component.source,
|
|
144
157
|
framework: component.framework,
|
|
145
158
|
props: component.props,
|
|
146
|
-
slots:
|
|
159
|
+
slots: effectiveSlots,
|
|
147
160
|
},
|
|
148
161
|
], null, 2);
|
|
149
162
|
const prompt = await buildPrompt({
|
|
@@ -186,6 +199,7 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
|
|
|
186
199
|
warnings: [],
|
|
187
200
|
failed: true,
|
|
188
201
|
error: `timed out after ${DEFAULT_TIMEOUT_MS / 60000} minutes`,
|
|
202
|
+
renamedSlotsCount: renames.length,
|
|
189
203
|
};
|
|
190
204
|
}
|
|
191
205
|
if (result.exitCode !== 0) {
|
|
@@ -209,6 +223,7 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
|
|
|
209
223
|
slots: applied.slots,
|
|
210
224
|
warnings: applied.warnings,
|
|
211
225
|
failed: false,
|
|
226
|
+
renamedSlotsCount: renames.length,
|
|
212
227
|
};
|
|
213
228
|
}
|
|
214
229
|
return {
|
|
@@ -219,6 +234,7 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
|
|
|
219
234
|
warnings: [],
|
|
220
235
|
failed: true,
|
|
221
236
|
error: lastError,
|
|
237
|
+
renamedSlotsCount: renames.length,
|
|
222
238
|
};
|
|
223
239
|
}
|
|
224
240
|
async function runAllComponents(agent, model, db, sessionId, components, tokensInline, tokenMapInline, verbose, noCache) {
|
|
@@ -390,6 +406,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
390
406
|
}
|
|
391
407
|
const totalClassified = generated.reduce((s, r) => s + r.classified, 0);
|
|
392
408
|
const totalExcluded = generated.reduce((s, r) => s + r.excluded, 0);
|
|
409
|
+
const totalRenamedSlots = componentResults.reduce((s, r) => s + r.renamedSlotsCount, 0);
|
|
393
410
|
const allOk = failed.length === 0;
|
|
394
411
|
const cachedNote = cachedResults.length > 0 ? c.dim(` (${cachedResults.length} cached)`) : '';
|
|
395
412
|
process.stderr.write((allOk ? c.green('✓') : c.yellow('⚠')) +
|
|
@@ -397,6 +414,8 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
397
414
|
cachedNote +
|
|
398
415
|
c.dim(` ${totalClassified} classified, ${totalExcluded} excluded`) +
|
|
399
416
|
'\n');
|
|
417
|
+
// Machine-parseable summary on stdout for the wizard / orchestrator.
|
|
418
|
+
process.stdout.write(`renamed-slots: ${totalRenamedSlots}\n`);
|
|
400
419
|
if (generated.length === 0 && cachedResults.length === 0) {
|
|
401
420
|
die('Error: all components failed to generate — check agent output above');
|
|
402
421
|
}
|
|
@@ -35,4 +35,25 @@ export interface PipelineResult {
|
|
|
35
35
|
project: string;
|
|
36
36
|
steps: StepResult[];
|
|
37
37
|
}
|
|
38
|
+
export declare function isPreviewValidationError(result: {
|
|
39
|
+
exitCode: number;
|
|
40
|
+
stderr: string;
|
|
41
|
+
}): boolean;
|
|
42
|
+
export declare function parseOffendingComponentNames(output: string): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Build the apply-push StepResult record. Centralizes the success/failure
|
|
45
|
+
* shape so excludedByValidationRetry is recorded consistently in both
|
|
46
|
+
* branches — previously the total-failure branch dropped it, leaving users
|
|
47
|
+
* with a failed pipeline and no audit trail of what was auto-excluded
|
|
48
|
+
* before the retry loop gave up.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildPushStepResult(args: {
|
|
51
|
+
created: number;
|
|
52
|
+
updated: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
durationMs: number;
|
|
55
|
+
stderr: string;
|
|
56
|
+
excludedByRetry: string[];
|
|
57
|
+
totalFailure?: boolean;
|
|
58
|
+
}): StepResult;
|
|
38
59
|
export declare function runPipeline(opts: PipelineOptions, progressWriter: (line: string) => void, cliPathOverride?: string): Promise<PipelineResult>;
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, } from '../session/db.js';
|
|
6
|
+
import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
|
|
6
7
|
function findCliPath() {
|
|
7
8
|
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
|
|
8
9
|
}
|
|
@@ -27,6 +28,49 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
|
|
|
27
28
|
});
|
|
28
29
|
});
|
|
29
30
|
}
|
|
31
|
+
const MAX_VALIDATION_RETRIES = Number(process.env['EDS_MAX_VALIDATION_RETRIES'] ?? 2);
|
|
32
|
+
export function isPreviewValidationError(result) {
|
|
33
|
+
return (result.exitCode !== 0 &&
|
|
34
|
+
result.stderr.includes(`${PREVIEW_ERROR_PREFIX} 422`) &&
|
|
35
|
+
result.stderr.includes(VALIDATION_FAILED_CODE));
|
|
36
|
+
}
|
|
37
|
+
export function parseOffendingComponentNames(output) {
|
|
38
|
+
// The 422 body is appended to the ApiError message by the constructor and
|
|
39
|
+
// written to stderr by die(). Extract the JSON portion by finding the first '{'.
|
|
40
|
+
const jsonStart = output.indexOf('{');
|
|
41
|
+
if (jsonStart === -1)
|
|
42
|
+
return [];
|
|
43
|
+
const errors = parsePreviewValidationErrors(output.slice(jsonStart));
|
|
44
|
+
return [...new Set(errors.map((e) => e.componentName))];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the apply-push StepResult record. Centralizes the success/failure
|
|
48
|
+
* shape so excludedByValidationRetry is recorded consistently in both
|
|
49
|
+
* branches — previously the total-failure branch dropped it, leaving users
|
|
50
|
+
* with a failed pipeline and no audit trail of what was auto-excluded
|
|
51
|
+
* before the retry loop gave up.
|
|
52
|
+
*/
|
|
53
|
+
export function buildPushStepResult(args) {
|
|
54
|
+
const { created, updated, failed, durationMs, stderr, excludedByRetry, totalFailure } = args;
|
|
55
|
+
const excludedDetail = excludedByRetry.length > 0 ? { excludedByValidationRetry: excludedByRetry } : {};
|
|
56
|
+
if (totalFailure) {
|
|
57
|
+
const detail = excludedByRetry.length > 0 ? { ...excludedDetail } : undefined;
|
|
58
|
+
return {
|
|
59
|
+
step: 'apply push',
|
|
60
|
+
status: 'failed',
|
|
61
|
+
durationMs,
|
|
62
|
+
error: stderr,
|
|
63
|
+
...(detail ? { detail } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const status = failed > 0 ? 'failed' : 'complete';
|
|
67
|
+
return {
|
|
68
|
+
step: 'apply push',
|
|
69
|
+
status,
|
|
70
|
+
durationMs,
|
|
71
|
+
detail: { created, updated, failed, ...excludedDetail },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
30
74
|
export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
31
75
|
const projectRoot = resolve(opts.project);
|
|
32
76
|
const outDir = resolve(opts.out);
|
|
@@ -322,7 +366,37 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
322
366
|
components: componentsPath,
|
|
323
367
|
});
|
|
324
368
|
const t0 = Date.now();
|
|
325
|
-
|
|
369
|
+
let r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
370
|
+
// Bounded retry loop: on a preview-phase 422 with a parseable ValidationFailed
|
|
371
|
+
// body, exclude the offending components and re-run apply push.
|
|
372
|
+
const excludedByRetry = [];
|
|
373
|
+
let validationRetryCount = 0;
|
|
374
|
+
while (validationRetryCount < MAX_VALIDATION_RETRIES && isPreviewValidationError(r) && extractSessionId) {
|
|
375
|
+
const offenders = parseOffendingComponentNames(r.stderr + r.stdout);
|
|
376
|
+
if (offenders.length === 0)
|
|
377
|
+
break; // unparseable body — give up and surface original error
|
|
378
|
+
process.stderr.write(`[retry ${validationRetryCount + 1}/${MAX_VALIDATION_RETRIES}] Preview validation failed — excluding ${offenders.join(', ')} and retrying\n`);
|
|
379
|
+
excludedByRetry.push(...offenders);
|
|
380
|
+
// No --select-all here: that would route through runNonInteractive's
|
|
381
|
+
// rebuild path, which DELETEs all rows and re-inserts with default
|
|
382
|
+
// status='extracted' — wiping the post-`generate components` state
|
|
383
|
+
// (status='generated' + raw_props.cdf_type) the next apply push reads.
|
|
384
|
+
// The bare --exclude-components form takes the rejectComponentsByName
|
|
385
|
+
// early-return: pure UPDATE, no rebuild.
|
|
386
|
+
const rejectArgs = [
|
|
387
|
+
'analyze',
|
|
388
|
+
'select',
|
|
389
|
+
'--session',
|
|
390
|
+
extractSessionId,
|
|
391
|
+
'--exclude-components',
|
|
392
|
+
offenders.join(','),
|
|
393
|
+
];
|
|
394
|
+
const rejectResult = await runStep(rejectArgs, cliPath);
|
|
395
|
+
if (rejectResult.exitCode !== 0)
|
|
396
|
+
break; // selection step failed — give up
|
|
397
|
+
r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
398
|
+
validationRetryCount++;
|
|
399
|
+
}
|
|
326
400
|
const durationMs = Date.now() - t0;
|
|
327
401
|
let pushResult = null;
|
|
328
402
|
try {
|
|
@@ -345,25 +419,32 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
345
419
|
// Total failure — nothing was pushed
|
|
346
420
|
updateStep(db, pushStepId, 'failed', {}, r.stderr);
|
|
347
421
|
progressWriter(`${pushLabel}✗ failed (${(durationMs / 1000).toFixed(1)}s)`);
|
|
348
|
-
steps.push({
|
|
349
|
-
|
|
350
|
-
|
|
422
|
+
steps.push(buildPushStepResult({
|
|
423
|
+
created,
|
|
424
|
+
updated,
|
|
425
|
+
failed,
|
|
351
426
|
durationMs,
|
|
352
|
-
|
|
353
|
-
|
|
427
|
+
stderr: r.stderr,
|
|
428
|
+
excludedByRetry,
|
|
429
|
+
totalFailure: true,
|
|
430
|
+
}));
|
|
354
431
|
db.close();
|
|
355
432
|
return { session: sessionId, project: projectRoot, steps };
|
|
356
433
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
steps.push({
|
|
362
|
-
step: 'apply push',
|
|
363
|
-
status: stepStatus,
|
|
434
|
+
const stepResult = buildPushStepResult({
|
|
435
|
+
created,
|
|
436
|
+
updated,
|
|
437
|
+
failed,
|
|
364
438
|
durationMs,
|
|
365
|
-
|
|
439
|
+
stderr: r.stderr,
|
|
440
|
+
excludedByRetry,
|
|
366
441
|
});
|
|
442
|
+
updateStep(db, pushStepId, stepResult.status === 'complete' ? 'complete' : 'failed', {
|
|
443
|
+
components: componentsPath,
|
|
444
|
+
});
|
|
445
|
+
const statusIcon = failed > 0 ? '⚠' : '✓';
|
|
446
|
+
progressWriter(`${pushLabel}${statusIcon} ${created} created, ${updated} updated, ${failed} failed (${(durationMs / 1000).toFixed(1)}s)`);
|
|
447
|
+
steps.push(stepResult);
|
|
367
448
|
}
|
|
368
449
|
progressWriter('');
|
|
369
450
|
progressWriter(`Pipeline complete. Session: ${sessionId}`);
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
export declare function buildAnalyzeSelectArgs(opts: {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
acceptAll: boolean;
|
|
5
|
+
}): string[];
|
|
6
|
+
export declare function formatAcceptanceSummary(opts: {
|
|
7
|
+
accepted: number;
|
|
8
|
+
autoRejected: number;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function formatGeneratedSummary(opts: {
|
|
11
|
+
generated: number;
|
|
12
|
+
renamedSlots: number;
|
|
13
|
+
autoRejected: number;
|
|
14
|
+
}): string;
|
|
2
15
|
export type WizardAppProps = {
|
|
3
16
|
initialSpaceId?: string;
|
|
4
17
|
initialEnvironmentId?: string;
|