@contentful/experience-design-system-cli 2.14.2 → 2.15.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/README.md +64 -2
- package/dist/package.json +3 -2
- package/dist/src/analyze/command.d.ts +11 -0
- package/dist/src/analyze/command.js +298 -3
- package/dist/src/analyze/composite-closure.d.ts +25 -0
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
- package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
- package/dist/src/analyze/composition/apply-mapping.js +67 -0
- package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
- package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
- package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
- package/dist/src/analyze/composition/candidate-critic.js +63 -0
- package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
- package/dist/src/analyze/composition/candidate-files.js +131 -0
- package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
- package/dist/src/analyze/composition/composition-cache-key.js +19 -0
- package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
- package/dist/src/analyze/composition/interchange-schema.js +61 -0
- package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
- package/dist/src/analyze/composition/merge-edges.js +50 -0
- package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
- package/dist/src/analyze/composition/parse-map-edges.js +55 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
- package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
- package/dist/src/analyze/composition/resolve-mapping.js +94 -0
- package/dist/src/analyze/cycle-detection.d.ts +27 -0
- package/dist/src/analyze/cycle-detection.js +234 -0
- package/dist/src/analyze/cycle-view.d.ts +7 -0
- package/dist/src/analyze/cycle-view.js +28 -0
- package/dist/src/analyze/fuzzy-search.d.ts +3 -0
- package/dist/src/analyze/fuzzy-search.js +55 -0
- package/dist/src/analyze/issue-inheritance.d.ts +12 -0
- package/dist/src/analyze/issue-inheritance.js +108 -0
- package/dist/src/analyze/lineage.d.ts +29 -0
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
- package/dist/src/analyze/scope-gate-cascade.js +170 -0
- package/dist/src/analyze/search-neighborhood.d.ts +3 -0
- package/dist/src/analyze/search-neighborhood.js +56 -0
- package/dist/src/analyze/select/tui/App.js +5 -16
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
- package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
- package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
- package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
- package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
- package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
- package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
- package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
- package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
- package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
- package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
- package/dist/src/analyze/select/tui/theme.d.ts +15 -0
- package/dist/src/analyze/select/tui/theme.js +28 -0
- package/dist/src/analyze/selection-cascade.d.ts +3 -0
- package/dist/src/analyze/selection-cascade.js +19 -0
- package/dist/src/analyze/slot-graph.d.ts +11 -0
- package/dist/src/analyze/slot-graph.js +17 -0
- package/dist/src/apply/api-client.d.ts +2 -1
- package/dist/src/apply/api-client.js +40 -6
- package/dist/src/apply/command.d.ts +17 -1
- package/dist/src/apply/command.js +69 -11
- package/dist/src/apply/error-parser.d.ts +16 -0
- package/dist/src/apply/error-parser.js +117 -0
- package/dist/src/apply/tui/ServerPreviewView.js +3 -1
- package/dist/src/credentials-store.d.ts +6 -0
- package/dist/src/credentials-store.js +5 -1
- package/dist/src/generate/agent-runner.d.ts +6 -0
- package/dist/src/generate/agent-runner.js +24 -8
- package/dist/src/import/command.js +69 -31
- package/dist/src/import/cycle-auto-reject.d.ts +4 -0
- package/dist/src/import/cycle-auto-reject.js +14 -0
- package/dist/src/import/orchestrator.d.ts +20 -7
- package/dist/src/import/orchestrator.js +139 -35
- package/dist/src/import/strip-allowed-components.d.ts +18 -0
- package/dist/src/import/strip-allowed-components.js +22 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
- package/dist/src/import/tui/CustomPromptBanner.js +2 -1
- package/dist/src/import/tui/WizardApp.d.ts +10 -74
- package/dist/src/import/tui/WizardApp.js +234 -246
- package/dist/src/import/tui/ai-flag.d.ts +5 -0
- package/dist/src/import/tui/ai-flag.js +6 -0
- package/dist/src/import/tui/autocomplete.d.ts +5 -0
- package/dist/src/import/tui/autocomplete.js +32 -0
- package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
- package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
- package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
- package/dist/src/import/tui/components/CounterStrip.js +12 -0
- package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
- package/dist/src/import/tui/components/LegendEntry.js +6 -0
- package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
- package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
- package/dist/src/import/tui/final-review-host.d.ts +3 -7
- package/dist/src/import/tui/final-review-host.js +9 -4
- package/dist/src/import/tui/group-collapse.d.ts +2 -0
- package/dist/src/import/tui/group-collapse.js +11 -0
- package/dist/src/import/tui/history.d.ts +22 -0
- package/dist/src/import/tui/history.js +53 -0
- package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
- package/dist/src/import/tui/hooks/useLineage.js +50 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
- package/dist/src/import/tui/lineage-layout.d.ts +75 -0
- package/dist/src/import/tui/lineage-layout.js +82 -0
- package/dist/src/import/tui/runLivePreview.d.ts +11 -0
- package/dist/src/import/tui/runLivePreview.js +7 -1
- package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
- package/dist/src/import/tui/scope-gate-columns.js +94 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
- package/dist/src/import/tui/scope-gate-host.js +11 -3
- package/dist/src/import/tui/sidebar-width.d.ts +1 -0
- package/dist/src/import/tui/sidebar-width.js +3 -0
- package/dist/src/import/tui/step-filters.d.ts +19 -0
- package/dist/src/import/tui/step-filters.js +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
- package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
- package/dist/src/import/tui/steps/DoneStep.js +3 -2
- package/dist/src/import/tui/steps/ErrorStep.js +2 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
- package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
- package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
- package/dist/src/import/tui/steps/PreviewStep.js +2 -1
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
- package/dist/src/import/tui/steps/PushingStep.js +2 -1
- package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
- package/dist/src/import/tui/steps/RunningStep.js +3 -2
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
- package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
- package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
- package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
- package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
- package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
- package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +48 -5
- package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.d.ts +3 -0
- package/dist/src/import/tui/useLivePreview.js +1 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
- package/dist/src/import/tui/wizard-state-transitions.js +5 -60
- package/dist/src/lib/composition-mode.d.ts +28 -0
- package/dist/src/lib/composition-mode.js +37 -0
- package/dist/src/lib/prompt-overrides.d.ts +38 -0
- package/dist/src/lib/prompt-overrides.js +69 -0
- package/dist/src/print/command.js +19 -1
- package/dist/src/runs/modify-launcher.d.ts +3 -0
- package/dist/src/runs/modify-launcher.js +2 -0
- package/dist/src/runs/replay-helpers.js +1 -0
- package/dist/src/runs/run-picker.js +2 -18
- package/dist/src/runs/store.d.ts +4 -0
- package/dist/src/session/db.d.ts +23 -40
- package/dist/src/session/db.js +110 -138
- package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
- package/dist/src/setup/composition-mode-prompt.js +37 -0
- package/package.json +5 -4
- package/prompts/composition-dir-critic.md +7 -0
- package/prompts/composition-edges.md +8 -0
- package/prompts/composition-parser-repair-empty.md +1 -0
- package/prompts/composition-parser-repair-error.md +2 -0
- package/prompts/composition-parser.md +42 -0
|
@@ -5,10 +5,14 @@ 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';
|
|
8
9
|
import { isEmptyPreview } from './preview-utils.js';
|
|
9
10
|
import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
|
|
10
11
|
import { SelectView, makeSelectKey } from './tui/SelectView.js';
|
|
11
12
|
import { buildPostPushUrl } from '../lib/contentful-urls.js';
|
|
13
|
+
import { resolveCompositionMode } from '../lib/composition-mode.js';
|
|
14
|
+
import { stripAllowedComponents } from '../import/strip-allowed-components.js';
|
|
15
|
+
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
12
16
|
function die(message) {
|
|
13
17
|
process.stderr.write(`${message}\n`);
|
|
14
18
|
process.exit(1);
|
|
@@ -158,6 +162,21 @@ async function resolveSharedInputs(opts) {
|
|
|
158
162
|
}
|
|
159
163
|
components = result.components;
|
|
160
164
|
}
|
|
165
|
+
// Atomic mode (spec T8/T12): strip embedded-component composition at the
|
|
166
|
+
// single serialization boundary, regardless of load path. Normalizing here
|
|
167
|
+
// (rather than only in loadCDFComponents) also covers hand-authored
|
|
168
|
+
// `--components` files. Starving `$allowedComponents` at this one point
|
|
169
|
+
// means slot-cycle detection downstream structurally returns zero.
|
|
170
|
+
let configMode;
|
|
171
|
+
try {
|
|
172
|
+
configMode = (await readExperiencesCredentials()).compositionMode;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Missing credentials.json → resolver falls through to default (atomic).
|
|
176
|
+
}
|
|
177
|
+
if (resolveCompositionMode(opts, configMode) === 'atomic') {
|
|
178
|
+
components = stripAllowedComponents(components);
|
|
179
|
+
}
|
|
161
180
|
let tokens = [];
|
|
162
181
|
if (opts.tokens) {
|
|
163
182
|
tokens = await readTokensFromPath('--tokens', opts.tokens);
|
|
@@ -170,7 +189,48 @@ async function resolveSharedInputs(opts) {
|
|
|
170
189
|
});
|
|
171
190
|
return { components, tokens, client };
|
|
172
191
|
}
|
|
173
|
-
|
|
192
|
+
export function detectSlotCycles(components) {
|
|
193
|
+
const cycleInput = components.map(({ key, entry }) => ({
|
|
194
|
+
name: key,
|
|
195
|
+
slots: Object.entries(entry.$slots ?? {}).map(([slotName, slotDef]) => ({
|
|
196
|
+
name: slotName,
|
|
197
|
+
allowedComponents: slotDef.$allowedComponents ?? [],
|
|
198
|
+
})),
|
|
199
|
+
}));
|
|
200
|
+
return findSlotCycles(cycleInput);
|
|
201
|
+
}
|
|
202
|
+
export function formatSlotCycleReport(cycles) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push(`Error: manifest:components/slot-cycles — ${cycles.length} slot dependency cycle(s) detected. Push refused.`);
|
|
205
|
+
for (let i = 0; i < cycles.length; i += 1) {
|
|
206
|
+
const cycle = cycles[i];
|
|
207
|
+
lines.push(` Cycle ${i + 1}: ${formatCyclePath(cycle)}`);
|
|
208
|
+
const suggested = suggestCycleBreakEdge(cycle, cycles);
|
|
209
|
+
lines.push(` Fix: remove '${suggested.toComponent}' from ${suggested.fromComponent}.$slots.${suggested.slotName}.$allowedComponents`);
|
|
210
|
+
}
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
export function assertNoSlotCycles(components) {
|
|
214
|
+
const cycles = detectSlotCycles(components);
|
|
215
|
+
if (cycles.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
process.stderr.write(formatSlotCycleReport(cycles).join('\n') + '\n');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
export function extractComponentsFromManifest(manifest) {
|
|
221
|
+
const componentsManifest = manifest?.componentsManifest;
|
|
222
|
+
if (!componentsManifest)
|
|
223
|
+
return [];
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const [key, value] of Object.entries(componentsManifest)) {
|
|
226
|
+
if (key === '$schema')
|
|
227
|
+
continue;
|
|
228
|
+
if (!value || typeof value !== 'object')
|
|
229
|
+
continue;
|
|
230
|
+
out.push({ key, entry: value });
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
174
234
|
export function hasBreakingChangesWithImpact(preview) {
|
|
175
235
|
const allChanged = [...preview.components.changed, ...preview.tokens.changed];
|
|
176
236
|
return allChanged.some((c) => c.changeClassification?.classification === 'breaking' &&
|
|
@@ -233,7 +293,6 @@ function buildApplyOutput(operation, spaceId, environmentId, host) {
|
|
|
233
293
|
})),
|
|
234
294
|
};
|
|
235
295
|
}
|
|
236
|
-
// --- Selection helpers ---
|
|
237
296
|
function getSelectableEntities(preview) {
|
|
238
297
|
const entities = [];
|
|
239
298
|
for (const token of preview.tokens.new) {
|
|
@@ -350,7 +409,6 @@ function SelectApp({ entities, spaceId, environmentId, onApply }) {
|
|
|
350
409
|
importing,
|
|
351
410
|
});
|
|
352
411
|
}
|
|
353
|
-
// --- Command registration ---
|
|
354
412
|
function collect(val, prev) {
|
|
355
413
|
return [...prev, val];
|
|
356
414
|
}
|
|
@@ -358,7 +416,6 @@ export function registerApplyCommand(program) {
|
|
|
358
416
|
const applyCmd = program
|
|
359
417
|
.command('apply')
|
|
360
418
|
.description('Preview, select, or push design system entities to Contentful ExO');
|
|
361
|
-
// --- apply preview ---
|
|
362
419
|
applyCmd
|
|
363
420
|
.command('preview')
|
|
364
421
|
.description('Show a read-only diff of what apply push would do')
|
|
@@ -369,6 +426,8 @@ export function registerApplyCommand(program) {
|
|
|
369
426
|
.requiredOption('--environment-id <id>', 'Contentful environment ID')
|
|
370
427
|
.option('--cma-token <token>', 'CMA personal access token (or set CONTENTFUL_MANAGEMENT_TOKEN)')
|
|
371
428
|
.option('--host <url>', 'Override API base URL')
|
|
429
|
+
.option('--composite', 'Import embedded-component hierarchy (opt in; default is atomic)')
|
|
430
|
+
.option('--atomic', 'Import flat components with no embedded-component hierarchy (default)')
|
|
372
431
|
.action(async (opts) => {
|
|
373
432
|
let inputs;
|
|
374
433
|
try {
|
|
@@ -414,7 +473,6 @@ export function registerApplyCommand(program) {
|
|
|
414
473
|
process.exit(0);
|
|
415
474
|
}
|
|
416
475
|
});
|
|
417
|
-
// --- apply push ---
|
|
418
476
|
applyCmd
|
|
419
477
|
.command('push')
|
|
420
478
|
.description('Write component types and design tokens to Contentful ExO')
|
|
@@ -425,6 +483,8 @@ export function registerApplyCommand(program) {
|
|
|
425
483
|
.requiredOption('--environment-id <id>', 'Contentful environment ID')
|
|
426
484
|
.option('--cma-token <token>', 'CMA personal access token (or set CONTENTFUL_MANAGEMENT_TOKEN)')
|
|
427
485
|
.option('--host <url>', 'Override API base URL')
|
|
486
|
+
.option('--composite', 'Import embedded-component hierarchy (opt in; default is atomic)')
|
|
487
|
+
.option('--atomic', 'Import flat components with no embedded-component hierarchy (default)')
|
|
428
488
|
.option('--yes', 'Skip interactive confirmation')
|
|
429
489
|
.option('--verbose', 'Show all entity progress including skipped/unchanged')
|
|
430
490
|
.option('--force', 'Skip confirmation for breaking changes (for CI)')
|
|
@@ -445,6 +505,7 @@ export function registerApplyCommand(program) {
|
|
|
445
505
|
throw e;
|
|
446
506
|
}
|
|
447
507
|
const { components, tokens, client } = inputs;
|
|
508
|
+
assertNoSlotCycles(components);
|
|
448
509
|
try {
|
|
449
510
|
await client.validateToken();
|
|
450
511
|
}
|
|
@@ -465,7 +526,6 @@ export function registerApplyCommand(program) {
|
|
|
465
526
|
}
|
|
466
527
|
const spaceId = opts.spaceId;
|
|
467
528
|
const environmentId = opts.environmentId;
|
|
468
|
-
// --- Dry run: print preview and exit ---
|
|
469
529
|
if (opts.dryRun) {
|
|
470
530
|
if (isTTY) {
|
|
471
531
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
@@ -480,7 +540,6 @@ export function registerApplyCommand(program) {
|
|
|
480
540
|
}
|
|
481
541
|
process.exit(0);
|
|
482
542
|
}
|
|
483
|
-
// --- Nothing to do: exit early without calling apply ---
|
|
484
543
|
if (isEmptyPreview(preview)) {
|
|
485
544
|
if (isTTY && !opts.yes) {
|
|
486
545
|
process.stderr.write('Nothing to change — design system is up to date.\n');
|
|
@@ -491,7 +550,6 @@ export function registerApplyCommand(program) {
|
|
|
491
550
|
process.exit(0);
|
|
492
551
|
}
|
|
493
552
|
const breakingWithImpact = hasBreakingChangesWithImpact(preview);
|
|
494
|
-
// --- Non-interactive: require --force for breaking changes ---
|
|
495
553
|
if (!isTTY || opts.yes) {
|
|
496
554
|
if (breakingWithImpact && !opts.force) {
|
|
497
555
|
process.stderr.write('Error: breaking changes with downstream impact detected. Use --force to acknowledge.\n');
|
|
@@ -525,7 +583,6 @@ export function registerApplyCommand(program) {
|
|
|
525
583
|
process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
|
|
526
584
|
return;
|
|
527
585
|
}
|
|
528
|
-
// --- Interactive (TTY, no --yes) flow ---
|
|
529
586
|
await new Promise((resolvePromise) => {
|
|
530
587
|
const runApply = async (acknowledge) => {
|
|
531
588
|
instance.rerender(createElement(ServerApplyProgress, {
|
|
@@ -593,7 +650,6 @@ export function registerApplyCommand(program) {
|
|
|
593
650
|
void instance.waitUntilExit().then(() => resolvePromise());
|
|
594
651
|
});
|
|
595
652
|
});
|
|
596
|
-
// --- apply select ---
|
|
597
653
|
applyCmd
|
|
598
654
|
.command('select')
|
|
599
655
|
.description('Select a subset of entities and push to Contentful ExO')
|
|
@@ -604,6 +660,8 @@ export function registerApplyCommand(program) {
|
|
|
604
660
|
.requiredOption('--environment-id <id>', 'Contentful environment ID')
|
|
605
661
|
.option('--cma-token <token>', 'CMA personal access token (or set CONTENTFUL_MANAGEMENT_TOKEN)')
|
|
606
662
|
.option('--host <url>', 'Override API base URL')
|
|
663
|
+
.option('--composite', 'Import embedded-component hierarchy (opt in; default is atomic)')
|
|
664
|
+
.option('--atomic', 'Import flat components with no embedded-component hierarchy (default)')
|
|
607
665
|
.option('--select-all', 'Select all entities without launching TUI')
|
|
608
666
|
.option('--select <pattern>', 'Select entities by ID pattern (repeatable)', collect, [])
|
|
609
667
|
.option('--deselect <pattern>', 'Deselect entities by ID pattern (repeatable)', collect, [])
|
|
@@ -623,6 +681,7 @@ export function registerApplyCommand(program) {
|
|
|
623
681
|
throw e;
|
|
624
682
|
}
|
|
625
683
|
const { components, tokens, client } = inputs;
|
|
684
|
+
assertNoSlotCycles(components);
|
|
626
685
|
try {
|
|
627
686
|
await client.validateToken();
|
|
628
687
|
}
|
|
@@ -693,7 +752,6 @@ export function registerApplyCommand(program) {
|
|
|
693
752
|
process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
|
|
694
753
|
return;
|
|
695
754
|
}
|
|
696
|
-
// --- Interactive flow ---
|
|
697
755
|
await new Promise((resolvePromise) => {
|
|
698
756
|
const runSelectApply = async (selectedKeys) => {
|
|
699
757
|
const selectedComponentKeys = new Set();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ParsedEdsiError {
|
|
2
|
+
/** Server-side error code, if we could extract one. */
|
|
3
|
+
code: string | null;
|
|
4
|
+
/** Human-readable message, stripped of log/trace decoration. */
|
|
5
|
+
message: string;
|
|
6
|
+
/** Cycle participants, when `code === 'TopoSortCycleError'`. */
|
|
7
|
+
cycle: string[] | null;
|
|
8
|
+
/** True when the message survived cleaning as-is (no parseable structure). */
|
|
9
|
+
raw: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function stripLambdaLogPrefix(body: string): string;
|
|
12
|
+
export declare function parseEdsiError(rawInput: string | undefined | null): ParsedEdsiError;
|
|
13
|
+
export declare function formatParsedEdsiError(parsed: ParsedEdsiError, opts?: {
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
raw?: string;
|
|
16
|
+
}): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const LAMBDA_LOG_PREFIX_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s+[0-9a-f-]+\s+ERROR\s+(?:\[dd\.[^\]]*\]\s*)?/;
|
|
2
|
+
const DD_TAG_RE = /\[dd\.(?:trace_id|span_id)=[^\]]*\]\s*/g;
|
|
3
|
+
export function stripLambdaLogPrefix(body) {
|
|
4
|
+
let out = body.replace(LAMBDA_LOG_PREFIX_RE, '');
|
|
5
|
+
out = out.replace(DD_TAG_RE, '');
|
|
6
|
+
return out.trim();
|
|
7
|
+
}
|
|
8
|
+
function parseObjectLiteralTail(body) {
|
|
9
|
+
const braceStart = body.lastIndexOf('{');
|
|
10
|
+
if (braceStart === -1)
|
|
11
|
+
return null;
|
|
12
|
+
const tail = body.slice(braceStart);
|
|
13
|
+
const codeMatch = tail.match(/code:\s*['"]([^'"]+)['"]/);
|
|
14
|
+
const cycleMatch = tail.match(/cycle:\s*\[\s*([^\]]*)\s*\]/);
|
|
15
|
+
if (!codeMatch && !cycleMatch)
|
|
16
|
+
return null;
|
|
17
|
+
const code = codeMatch ? codeMatch[1] : null;
|
|
18
|
+
let cycle = null;
|
|
19
|
+
if (cycleMatch) {
|
|
20
|
+
cycle = cycleMatch[1]
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
23
|
+
.filter((s) => s.length > 0);
|
|
24
|
+
if (cycle.length === 0)
|
|
25
|
+
cycle = null;
|
|
26
|
+
}
|
|
27
|
+
return { code, cycle };
|
|
28
|
+
}
|
|
29
|
+
function parseJsonBody(body) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(body);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!parsed || typeof parsed !== 'object')
|
|
38
|
+
return null;
|
|
39
|
+
const p = parsed;
|
|
40
|
+
const details = (p.details && typeof p.details === 'object' ? p.details : {});
|
|
41
|
+
const pick = (k) => (p[k] !== undefined ? p[k] : details[k]);
|
|
42
|
+
const codeRaw = pick('code');
|
|
43
|
+
const messageRaw = pick('message');
|
|
44
|
+
const cycleRaw = pick('cycle');
|
|
45
|
+
const out = {};
|
|
46
|
+
if (typeof codeRaw === 'string')
|
|
47
|
+
out.code = codeRaw;
|
|
48
|
+
if (typeof messageRaw === 'string')
|
|
49
|
+
out.message = messageRaw;
|
|
50
|
+
if (Array.isArray(cycleRaw)) {
|
|
51
|
+
const strs = cycleRaw.filter((x) => typeof x === 'string');
|
|
52
|
+
if (strs.length > 0)
|
|
53
|
+
out.cycle = strs;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
// ApiError.message shape: `${phasePrefix}\n${body}` where phasePrefix looks
|
|
58
|
+
// like `apply failed: 400`, `preview failed: 422`, `poll failed: 500`. We
|
|
59
|
+
// only strip the prefix line when the first line matches this shape —
|
|
60
|
+
// otherwise a raw body that happens to contain a newline (e.g. a Lambda log
|
|
61
|
+
// spill with a multi-line object literal) gets truncated mid-structure.
|
|
62
|
+
const API_ERROR_PREFIX_RE = /^(?:apply|preview|poll) failed: \d+$/;
|
|
63
|
+
export function parseEdsiError(rawInput) {
|
|
64
|
+
if (!rawInput)
|
|
65
|
+
return { code: null, message: '', cycle: null, raw: true };
|
|
66
|
+
const nlIndex = rawInput.indexOf('\n');
|
|
67
|
+
let body = rawInput;
|
|
68
|
+
let prefix = '';
|
|
69
|
+
if (nlIndex !== -1) {
|
|
70
|
+
const firstLine = rawInput.slice(0, nlIndex);
|
|
71
|
+
if (API_ERROR_PREFIX_RE.test(firstLine)) {
|
|
72
|
+
prefix = firstLine;
|
|
73
|
+
body = rawInput.slice(nlIndex + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const cleaned = stripLambdaLogPrefix(body);
|
|
77
|
+
const json = parseJsonBody(cleaned) ?? parseJsonBody(body);
|
|
78
|
+
if (json && (json.code || json.message || json.cycle)) {
|
|
79
|
+
return {
|
|
80
|
+
code: json.code ?? null,
|
|
81
|
+
message: json.message ?? (cleaned || prefix),
|
|
82
|
+
cycle: json.cycle ?? null,
|
|
83
|
+
raw: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const literal = parseObjectLiteralTail(cleaned);
|
|
87
|
+
if (literal && (literal.code || literal.cycle)) {
|
|
88
|
+
const braceStart = cleaned.lastIndexOf('{');
|
|
89
|
+
const head = braceStart === -1 ? cleaned : cleaned.slice(0, braceStart).trim();
|
|
90
|
+
return {
|
|
91
|
+
code: literal.code ?? null,
|
|
92
|
+
message: head || cleaned,
|
|
93
|
+
cycle: literal.cycle ?? null,
|
|
94
|
+
raw: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { code: null, message: cleaned || prefix || rawInput, cycle: null, raw: true };
|
|
98
|
+
}
|
|
99
|
+
export function formatParsedEdsiError(parsed, opts = {}) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
if (parsed.code) {
|
|
102
|
+
lines.push(`[${parsed.code}]`);
|
|
103
|
+
}
|
|
104
|
+
if (parsed.message) {
|
|
105
|
+
lines.push(parsed.message);
|
|
106
|
+
}
|
|
107
|
+
if (parsed.cycle && parsed.cycle.length > 0) {
|
|
108
|
+
lines.push(`Cycle: ${parsed.cycle.join(' → ')} → ${parsed.cycle[0]}`);
|
|
109
|
+
lines.push('Break the cycle by removing at least one $allowedComponents entry.');
|
|
110
|
+
}
|
|
111
|
+
if (opts.verbose && opts.raw) {
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push('--- raw ---');
|
|
114
|
+
lines.push(opts.raw);
|
|
115
|
+
}
|
|
116
|
+
return lines.filter(Boolean).join('\n');
|
|
117
|
+
}
|
|
@@ -3,7 +3,9 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
function BreakingBadge({ item }) {
|
|
4
4
|
if (!item.changeClassification || item.changeClassification.classification !== 'breaking')
|
|
5
5
|
return null;
|
|
6
|
-
const reasons = item.changeClassification.breakingChanges
|
|
6
|
+
const reasons = item.changeClassification.breakingChanges
|
|
7
|
+
.map((bc) => `${'slotId' in bc ? bc.slotId : bc.propertyId}: ${bc.reason}`)
|
|
8
|
+
.join(', ');
|
|
7
9
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }), item.impact && (item.impact.affectedFragments > 0 || item.impact.affectedExperiences > 0) && (_jsxs(Text, { color: "red", children: [' ', "\u2192 affects ", item.impact.affectedFragments, " Fragments, ", item.impact.affectedExperiences, " Experiences"] }))] }));
|
|
8
10
|
}
|
|
9
11
|
function DraftWarning({ hasDraft }) {
|
|
@@ -12,6 +12,12 @@ export type ExperiencesCredentials = {
|
|
|
12
12
|
autoFilter?: boolean;
|
|
13
13
|
/** Feature: default debug-mode (writes JSONL trace of every decision) for all commands. */
|
|
14
14
|
debug?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Feature (atomic mode): default composition mode. `atomic` (default) imports
|
|
17
|
+
* flat components with no embedded-component hierarchy; `composite` opts into
|
|
18
|
+
* the slot-graph machinery. Resolved `flag > env > this > default`.
|
|
19
|
+
*/
|
|
20
|
+
compositionMode?: 'composite' | 'atomic';
|
|
15
21
|
};
|
|
16
22
|
/**
|
|
17
23
|
* Read persisted Contentful credentials.
|
|
@@ -35,6 +35,9 @@ export async function readExperiencesCredentials() {
|
|
|
35
35
|
...(parsed.generatePromptPath ? { generatePromptPath: parsed.generatePromptPath } : {}),
|
|
36
36
|
...(typeof parsed.autoFilter === 'boolean' ? { autoFilter: parsed.autoFilter } : {}),
|
|
37
37
|
...(typeof parsed.debug === 'boolean' ? { debug: parsed.debug } : {}),
|
|
38
|
+
...(parsed.compositionMode === 'composite' || parsed.compositionMode === 'atomic'
|
|
39
|
+
? { compositionMode: parsed.compositionMode }
|
|
40
|
+
: {}),
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
catch {
|
|
@@ -48,7 +51,7 @@ export async function readExperiencesCredentials() {
|
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
export async function writeExperiencesCredentials(creds) {
|
|
51
|
-
const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, ...rest } = creds;
|
|
54
|
+
const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, compositionMode, ...rest } = creds;
|
|
52
55
|
const host = toConfiguredHost(creds.host);
|
|
53
56
|
await mkdir(CREDENTIALS_DIR, { recursive: true });
|
|
54
57
|
await writeFile(CREDENTIALS_PATH, JSON.stringify({
|
|
@@ -60,6 +63,7 @@ export async function writeExperiencesCredentials(creds) {
|
|
|
60
63
|
...(generatePromptPath ? { generatePromptPath } : {}),
|
|
61
64
|
...(typeof autoFilter === 'boolean' ? { autoFilter } : {}),
|
|
62
65
|
...(typeof debug === 'boolean' ? { debug } : {}),
|
|
66
|
+
...(compositionMode === 'composite' || compositionMode === 'atomic' ? { compositionMode } : {}),
|
|
63
67
|
}, null, 2) + '\n', { mode: 0o600 });
|
|
64
68
|
}
|
|
65
69
|
export function experiencesCredentialsPath() {
|
|
@@ -96,6 +96,12 @@ export declare function runAgent(options: {
|
|
|
96
96
|
timeoutMs: number;
|
|
97
97
|
model?: string;
|
|
98
98
|
onOutput?: (chunk: string) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Deliver the prompt on stdin instead of as an argv positional. Required for
|
|
101
|
+
* large prompts (e.g. the composition resolver inlining candidate files),
|
|
102
|
+
* which overflow ARG_MAX when passed as an argument. Non-interactive only.
|
|
103
|
+
*/
|
|
104
|
+
promptViaStdin?: boolean;
|
|
99
105
|
}): Promise<AgentRunResult>;
|
|
100
106
|
export type AgentAuthStatus = 'ok' | 'unauthenticated' | 'not-found';
|
|
101
107
|
export declare function checkAgentAuth(agent: AgentName): Promise<AgentAuthStatus>;
|
|
@@ -225,25 +225,30 @@ export function resolveBinary(agent) {
|
|
|
225
225
|
return override.trim();
|
|
226
226
|
return AGENT_BINARIES[agent];
|
|
227
227
|
}
|
|
228
|
-
function buildArgs(agent, prompt, model) {
|
|
228
|
+
function buildArgs(agent, prompt, model, promptViaStdin = false) {
|
|
229
229
|
const m = model ?? DEFAULT_MODELS[agent];
|
|
230
|
+
// When the prompt is delivered on stdin, omit it from argv — a large prompt
|
|
231
|
+
// as a command-line argument overflows ARG_MAX (spawn E2BIG). All four CLIs
|
|
232
|
+
// read the prompt from stdin when it isn't passed positionally.
|
|
233
|
+
const promptArg = promptViaStdin ? [] : [prompt];
|
|
230
234
|
switch (agent) {
|
|
231
235
|
case 'claude':
|
|
232
|
-
return ['--print', '--model', m,
|
|
236
|
+
return ['--print', '--model', m, ...promptArg];
|
|
233
237
|
case 'codex':
|
|
234
238
|
// --dangerously-bypass-approvals-and-sandbox required for non-interactive use
|
|
235
|
-
return ['exec', '--model', m, '--dangerously-bypass-approvals-and-sandbox',
|
|
239
|
+
return ['exec', '--model', m, '--dangerously-bypass-approvals-and-sandbox', ...promptArg];
|
|
236
240
|
case 'opencode':
|
|
237
|
-
return ['run', '--model', m,
|
|
241
|
+
return ['run', '--model', m, ...promptArg];
|
|
238
242
|
case 'cursor':
|
|
239
243
|
// cursor-agent uses --print for non-interactive stdout output
|
|
240
|
-
return ['--print', '--model', m,
|
|
244
|
+
return ['--print', '--model', m, ...promptArg];
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
247
|
export async function runAgent(options) {
|
|
244
|
-
const { agent, prompt, interactive, timeoutMs, model, onOutput } = options;
|
|
248
|
+
const { agent, prompt, interactive, timeoutMs, model, onOutput, promptViaStdin } = options;
|
|
245
249
|
const binary = resolveBinary(agent);
|
|
246
|
-
const
|
|
250
|
+
const useStdin = !!promptViaStdin && !interactive;
|
|
251
|
+
const args = buildArgs(agent, prompt, model, useStdin);
|
|
247
252
|
const debug = getDebugLogger();
|
|
248
253
|
const startedAt = Date.now();
|
|
249
254
|
debug.event('agent', 'run.start', {
|
|
@@ -260,7 +265,18 @@ export async function runAgent(options) {
|
|
|
260
265
|
stdio: interactive ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
261
266
|
});
|
|
262
267
|
if (!interactive) {
|
|
263
|
-
child.stdin
|
|
268
|
+
if (useStdin && child.stdin) {
|
|
269
|
+
// Guard against EPIPE: the child may close stdin before we finish
|
|
270
|
+
// writing (fast exit, or it stops reading). Swallow the write error —
|
|
271
|
+
// the child's own exit code/stderr is the source of truth.
|
|
272
|
+
child.stdin.on('error', () => { });
|
|
273
|
+
child.stdin.write(prompt, () => {
|
|
274
|
+
child.stdin?.end();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
child.stdin?.end();
|
|
279
|
+
}
|
|
264
280
|
}
|
|
265
281
|
let stdout = '';
|
|
266
282
|
let stderr = '';
|