@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
|
@@ -2,6 +2,7 @@ import { resolve, join } from 'node:path';
|
|
|
2
2
|
import { runPipeline } from './orchestrator.js';
|
|
3
3
|
import { resolveAutoFilter } from './auto-filter-resolve.js';
|
|
4
4
|
import { resolveAgent, resolveModel } from './agent-model-resolve.js';
|
|
5
|
+
import { resolveCompositionMode } from '../lib/composition-mode.js';
|
|
5
6
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
6
7
|
import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
|
|
7
8
|
import { replayRun, modifyRun } from '../runs/replay-helpers.js';
|
|
@@ -28,7 +29,7 @@ export function registerImportCommand(program) {
|
|
|
28
29
|
.option('--skip-analyze', 'Skip the analyze step (uses most recent extract session)')
|
|
29
30
|
.option('--skip-generate', 'Skip the generate step (uses most recent generate session)')
|
|
30
31
|
.option('--print', 'Write components.json to --out after generation')
|
|
31
|
-
.option('--skip-apply', 'Skip pushing to Contentful
|
|
32
|
+
.option('--skip-apply', '(deprecated alias for --no-push) Skip pushing to Contentful')
|
|
32
33
|
.option('--no-cache', 'Re-run all steps even if output already exists')
|
|
33
34
|
.option('--yes', 'Skip interactive confirmation in apply push')
|
|
34
35
|
.option('--verbose', 'Show full agent output and all entity progress')
|
|
@@ -38,10 +39,19 @@ export function registerImportCommand(program) {
|
|
|
38
39
|
.option('--dry-run', "(deprecated, will change semantics in a future release) Print generate components prompt without invoking the agent. Use --print-prompt for the same behaviour explicitly, or '--dry-run --no-push' for the upcoming manifest-preview semantics.")
|
|
39
40
|
.option('--print-prompt', 'Print the generate components prompt without invoking the agent. Replaces the legacy --dry-run prompt-print behaviour on this command.')
|
|
40
41
|
.option('--auto-accept-scope', 'Accept all extracted components without prompting (for scripted/non-TTY callers)')
|
|
42
|
+
.option('--composite', 'Import embedded-component hierarchy (opt in; default is atomic)')
|
|
43
|
+
.option('--atomic', 'Import flat components with no embedded-component hierarchy (default)')
|
|
44
|
+
.option('--composition-map <path>', 'Consume a hand-authored parent→children interchange map (implies --composite)')
|
|
45
|
+
.option('--composition-agent', 'Opt into agentic mapping resolution when deterministic sources find no groups (implies --composite)')
|
|
46
|
+
.option('--composition-refresh', 'Bypass the composition cache and re-resolve from scratch, forcing the agent to run (implies --composite)')
|
|
47
|
+
.option('--composition-agent-mode <mode>', "Agent mode: 'parser' (agent writes a sandboxed parser, default) or 'edges' (agent lists edges)")
|
|
48
|
+
.option('--generate-map <path>', 'Also write a composition-map skeleton from resolved edges during extract (implies --composite)')
|
|
49
|
+
.option('--prompt <stage=value>', 'Override a stage prompt (repeatable). value is a file path or literal text, e.g. --prompt composition=./p.md', (v, acc) => [...acc, v], [])
|
|
50
|
+
.option('--auto-reject-cycles', 'Automatically reject components involved in slot cycles and retry')
|
|
41
51
|
.option('--auto-filter', 'Force the AI auto-filter ON (overrides the credentials.json autoFilter preference)')
|
|
42
52
|
.option('--no-auto-filter', 'Skip the automatic AI pre-filter; jump straight to manual scope-gate (overrides the credentials.json autoFilter preference; no-op when paired with --auto-accept-scope)')
|
|
43
53
|
.option('--no-live-preview', 'Skip the automatic preview re-run after each FieldEditor save (no-op when paired with --auto-accept-scope)')
|
|
44
|
-
.option('--no-push', '
|
|
54
|
+
.option('--no-push', 'Import without pushing to Contentful. Interactive: runs the full wizard (extract → scope-gate → generate → final-review) and stops before push. Non-interactive (piped/CI): runs headless through generate. No credentials needed either way.')
|
|
45
55
|
.option('--no-save', 'Push without writing components.json / tokens.json to disk (default: save AND push)')
|
|
46
56
|
.option('--out-dir <path>', 'Save components.json / tokens.json to this directory; bypasses the inline save-path prompt')
|
|
47
57
|
.option('--on-conflict <mode>', "How to handle existing components.json / tokens.json at the save path: 'overwrite' replaces files, 'skip' writes to a timestamped subdirectory, 'fail' exits non-zero. Skips the wizard's interactive conflict gate when set.", (value) => {
|
|
@@ -59,9 +69,31 @@ export function registerImportCommand(program) {
|
|
|
59
69
|
.option('--save-as-new', 'Only valid with --modify: always save to a new path (prompts for one)')
|
|
60
70
|
.option('--force', 'Bypass staleness checks when paired with --push-from-run or --modify.')
|
|
61
71
|
.action(async (opts) => {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
72
|
+
// --modify and --push-from-run resume a recorded session; the composition
|
|
73
|
+
// mode comes from that run's record, so composition flags on the command
|
|
74
|
+
// line don't apply. Warn and clear them rather than let them mislead.
|
|
75
|
+
if (opts.modify !== undefined || opts.pushFromRun !== undefined) {
|
|
76
|
+
const passedCompositionFlags = [
|
|
77
|
+
opts.composite ? '--composite' : null,
|
|
78
|
+
opts.atomic ? '--atomic' : null,
|
|
79
|
+
opts.compositionMap ? '--composition-map' : null,
|
|
80
|
+
opts.compositionAgent ? '--composition-agent' : null,
|
|
81
|
+
opts.compositionAgentMode ? '--composition-agent-mode' : null,
|
|
82
|
+
opts.compositionRefresh ? '--composition-refresh' : null,
|
|
83
|
+
opts.generateMap ? '--generate-map' : null,
|
|
84
|
+
].filter((f) => f !== null);
|
|
85
|
+
if (passedCompositionFlags.length > 0) {
|
|
86
|
+
const entry = opts.modify !== undefined ? '--modify' : '--push-from-run';
|
|
87
|
+
process.stderr.write(`Note: ${passedCompositionFlags.join(', ')} ignored with ${entry} — composition mode comes from the recorded run.\n`);
|
|
88
|
+
opts.composite = undefined;
|
|
89
|
+
opts.atomic = undefined;
|
|
90
|
+
opts.compositionMap = undefined;
|
|
91
|
+
opts.compositionAgent = undefined;
|
|
92
|
+
opts.compositionAgentMode = undefined;
|
|
93
|
+
opts.compositionRefresh = undefined;
|
|
94
|
+
opts.generateMap = undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
65
97
|
if (opts.pushFromRun !== undefined) {
|
|
66
98
|
if (opts.modify !== undefined) {
|
|
67
99
|
process.stderr.write('Error: --push-from-run and --modify are mutually exclusive. --push-from-run pushes the recorded session; --modify re-opens the wizard for edits.\n');
|
|
@@ -106,7 +138,6 @@ export function registerImportCommand(program) {
|
|
|
106
138
|
return;
|
|
107
139
|
}
|
|
108
140
|
}
|
|
109
|
-
// ── --modify handling ──────────────────────────────────────────────
|
|
110
141
|
if (opts.modify !== undefined) {
|
|
111
142
|
if (opts.project !== '.') {
|
|
112
143
|
process.stderr.write('Error: --modify and --project are mutually exclusive. The project path is read from the recorded run.\n');
|
|
@@ -154,10 +185,6 @@ export function registerImportCommand(program) {
|
|
|
154
185
|
process.exit(1);
|
|
155
186
|
return;
|
|
156
187
|
}
|
|
157
|
-
// ── --raw-tokens validation ─────────────────────────────────────────
|
|
158
|
-
// The raw-tokens path is a source file the wizard hands to
|
|
159
|
-
// `generate tokens --raw-tokens <path>`. Validate at parse time so
|
|
160
|
-
// operators get an immediate error before the wizard renders.
|
|
161
188
|
if (opts.rawTokens !== undefined) {
|
|
162
189
|
if (opts.tokens !== undefined) {
|
|
163
190
|
process.stderr.write('Error: --raw-tokens and --tokens are mutually exclusive: --raw-tokens is the source file to classify, --tokens is a pre-classified DTCG sidecar.\n');
|
|
@@ -182,9 +209,16 @@ export function registerImportCommand(program) {
|
|
|
182
209
|
process.stderr.write(promptFlags.deprecationNotice);
|
|
183
210
|
}
|
|
184
211
|
const dryRunForward = promptFlags.forwardDryRun;
|
|
212
|
+
// "Don't push" is one user intent (--no-push); --skip-apply is a
|
|
213
|
+
// deprecated alias. Interactive runs (TTY) take the wizard and stop
|
|
214
|
+
// before push; non-interactive runs take the headless pipeline and stop
|
|
215
|
+
// after generate. Either way no credentials are required.
|
|
216
|
+
const noPushRequested = opts.push === false || opts.skipApply === true;
|
|
185
217
|
const isHeadless = opts.skipAnalyze ||
|
|
186
218
|
opts.skipGenerate ||
|
|
187
|
-
|
|
219
|
+
// A "don't push" request on a non-TTY is a headless intent (the wizard
|
|
220
|
+
// needs a TTY); in a TTY it stays interactive and is NOT headless.
|
|
221
|
+
(noPushRequested && !process.stdout.isTTY) ||
|
|
188
222
|
!!opts.spaceId ||
|
|
189
223
|
!!opts.environmentId ||
|
|
190
224
|
!!opts.cmaToken ||
|
|
@@ -193,7 +227,7 @@ export function registerImportCommand(program) {
|
|
|
193
227
|
false;
|
|
194
228
|
const autoAcceptScope = opts.autoAcceptScope ?? false;
|
|
195
229
|
if (!process.stdout.isTTY && !isHeadless && !autoAcceptScope) {
|
|
196
|
-
process.stderr.write('Error: experiences import is interactive. Pass --auto-accept-scope, or use a headless mode by providing credentials (--space-id, --environment-id, --cma-token) or one of --
|
|
230
|
+
process.stderr.write('Error: experiences import is interactive. Pass --auto-accept-scope, or use a headless mode by providing credentials (--space-id, --environment-id, --cma-token) or one of --no-push, --skip-analyze, --skip-generate, --yes, --dry-run, --print-prompt.\n');
|
|
197
231
|
process.exit(1);
|
|
198
232
|
return;
|
|
199
233
|
}
|
|
@@ -202,15 +236,9 @@ export function registerImportCommand(program) {
|
|
|
202
236
|
const { createElement } = await import('react');
|
|
203
237
|
const { WizardApp } = await import('./tui/WizardApp.js');
|
|
204
238
|
const creds = await readExperiencesCredentials();
|
|
205
|
-
// Parity-audit Q4: resolve --agent / --model overrides against the
|
|
206
|
-
// stored credentials.json so both flags are functional for the
|
|
207
|
-
// wizard path. Flag wins, then stored value, then default.
|
|
208
239
|
const resolvedAgent = resolveAgent(opts.agent, creds.agent);
|
|
209
240
|
const resolvedModel = resolveModel(opts.model, creds.agentModel);
|
|
210
|
-
|
|
211
|
-
// When runs.json has prior entries and the operator didn't ask for
|
|
212
|
-
// a specific entry point, open the wizard with the run picker. The
|
|
213
|
-
// helper enforces every gate (TTY, conflicting flags, file state).
|
|
241
|
+
const resolvedCompositionMode = resolveCompositionMode(opts, creds.compositionMode);
|
|
214
242
|
const pickerDecision = await shouldShowRunPicker({
|
|
215
243
|
flags: {
|
|
216
244
|
...(opts.pushFromRun !== undefined ? { pushFromRun: opts.pushFromRun } : {}),
|
|
@@ -222,10 +250,6 @@ export function registerImportCommand(program) {
|
|
|
222
250
|
isTTY: !!process.stdin.isTTY,
|
|
223
251
|
});
|
|
224
252
|
let pickerSelection = null;
|
|
225
|
-
// Ink `unmount` handle captured after `render(...)` below so the
|
|
226
|
-
// picker callback can tear down the wizard cleanly. Prior to this
|
|
227
|
-
// fix the callback did `setImmediate(() => process.exit(0))`, which
|
|
228
|
-
// killed the process before `dispatchPickerSelection` could run.
|
|
229
253
|
let unmountInk = null;
|
|
230
254
|
const pickerProps = {};
|
|
231
255
|
if (pickerDecision.shouldShow) {
|
|
@@ -245,10 +269,18 @@ export function registerImportCommand(program) {
|
|
|
245
269
|
initialProjectPath: opts.project !== '.' ? resolve(opts.project) : undefined,
|
|
246
270
|
host: opts.host,
|
|
247
271
|
autoAcceptScope,
|
|
272
|
+
autoRejectCycles: opts.autoRejectCycles ?? false,
|
|
273
|
+
compositionMode: resolvedCompositionMode,
|
|
274
|
+
...(opts.compositionMap ? { compositionMap: opts.compositionMap } : {}),
|
|
275
|
+
...(opts.compositionAgent ? { compositionAgent: true } : {}),
|
|
276
|
+
...(opts.compositionAgentMode ? { compositionAgentMode: opts.compositionAgentMode } : {}),
|
|
277
|
+
...(opts.compositionRefresh ? { compositionRefresh: true } : {}),
|
|
278
|
+
...(opts.generateMap ? { generateMap: opts.generateMap } : {}),
|
|
279
|
+
...(opts.prompt && opts.prompt.length > 0 ? { promptOverrides: opts.prompt } : {}),
|
|
248
280
|
noCache: opts.cache === false,
|
|
249
281
|
autoFilter: resolveAutoFilter({ autoFilter: opts.autoFilter }, creds.autoFilter),
|
|
250
282
|
livePreview: opts.livePreview !== false,
|
|
251
|
-
noPush:
|
|
283
|
+
noPush: noPushRequested,
|
|
252
284
|
noSave: opts.save === false,
|
|
253
285
|
...(opts.outDir ? { outDirOverride: resolve(opts.outDir) } : {}),
|
|
254
286
|
...(opts.onConflict ? { onConflictMode: opts.onConflict } : {}),
|
|
@@ -259,10 +291,6 @@ export function registerImportCommand(program) {
|
|
|
259
291
|
}));
|
|
260
292
|
unmountInk = unmount;
|
|
261
293
|
await waitUntilExit();
|
|
262
|
-
// ── Picker dispatch ─────────────────────────────────────────────
|
|
263
|
-
// If the operator picked a run, route into replayRun / modifyRun.
|
|
264
|
-
// dispatchPickerSelection lives in ./picker-dispatch.ts so this
|
|
265
|
-
// decision is testable without an Ink runtime.
|
|
266
294
|
if (pickerSelection) {
|
|
267
295
|
await dispatchPickerSelection(pickerSelection, {
|
|
268
296
|
...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
|
|
@@ -278,21 +306,23 @@ export function registerImportCommand(program) {
|
|
|
278
306
|
}
|
|
279
307
|
return;
|
|
280
308
|
}
|
|
281
|
-
|
|
309
|
+
// --no-push (and its deprecated alias --skip-apply) both mean "stop
|
|
310
|
+
// before the push" on the headless path — so neither requires credentials.
|
|
311
|
+
const skipApply = noPushRequested;
|
|
282
312
|
const spaceId = opts.spaceId ?? process.env['CONTENTFUL_SPACE_ID'];
|
|
283
313
|
const environmentId = opts.environmentId ?? process.env['CONTENTFUL_ENVIRONMENT_ID'];
|
|
284
314
|
const cmaToken = opts.cmaToken ?? process.env['CONTENTFUL_MANAGEMENT_TOKEN'];
|
|
285
315
|
if (!skipApply && (!spaceId || !environmentId || !cmaToken)) {
|
|
286
|
-
process.stderr.write('Error: --space-id (or CONTENTFUL_SPACE_ID), --environment-id (or CONTENTFUL_ENVIRONMENT_ID), and --cma-token (or CONTENTFUL_MANAGEMENT_TOKEN) are required unless --
|
|
316
|
+
process.stderr.write('Error: --space-id (or CONTENTFUL_SPACE_ID), --environment-id (or CONTENTFUL_ENVIRONMENT_ID), and --cma-token (or CONTENTFUL_MANAGEMENT_TOKEN) are required unless --no-push is set.\n');
|
|
287
317
|
process.exit(1);
|
|
288
318
|
return;
|
|
289
319
|
}
|
|
290
320
|
const projectRoot = resolve(opts.project);
|
|
291
321
|
const outDir = opts.out ? resolve(opts.out) : join(projectRoot, '.contentful');
|
|
292
|
-
// Parity-audit Q4: also honor stored agent/model in headless mode.
|
|
293
322
|
const headlessCreds = await readExperiencesCredentials();
|
|
294
323
|
const headlessAgent = resolveAgent(opts.agent, headlessCreds.agent);
|
|
295
324
|
const headlessModel = resolveModel(opts.model, headlessCreds.agentModel);
|
|
325
|
+
const headlessCompositionMode = resolveCompositionMode(opts, headlessCreds.compositionMode);
|
|
296
326
|
const result = await runPipeline({
|
|
297
327
|
project: projectRoot,
|
|
298
328
|
out: outDir,
|
|
@@ -317,6 +347,14 @@ export function registerImportCommand(program) {
|
|
|
317
347
|
host: opts.host,
|
|
318
348
|
dryRun: dryRunForward,
|
|
319
349
|
selectPromptPath: opts.selectPromptPath,
|
|
350
|
+
autoRejectCycles: opts.autoRejectCycles ?? false,
|
|
351
|
+
compositionMode: headlessCompositionMode,
|
|
352
|
+
...(opts.compositionMap ? { compositionMap: opts.compositionMap } : {}),
|
|
353
|
+
...(opts.compositionAgent ? { compositionAgent: true } : {}),
|
|
354
|
+
...(opts.compositionAgentMode ? { compositionAgentMode: opts.compositionAgentMode } : {}),
|
|
355
|
+
...(opts.compositionRefresh ? { compositionRefresh: true } : {}),
|
|
356
|
+
...(opts.generateMap ? { generateMap: opts.generateMap } : {}),
|
|
357
|
+
...(opts.prompt && opts.prompt.length > 0 ? { promptOverrides: opts.prompt } : {}),
|
|
320
358
|
}, (line) => process.stderr.write(line + '\n'));
|
|
321
359
|
const hasFailed = result.steps.some((s) => s.status === 'failed');
|
|
322
360
|
if (!process.stdout.isTTY) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { findAllAncestors } from '../analyze/lineage.js';
|
|
2
|
+
export function computeCycleAutoRejectTargets(slotCycles, graph) {
|
|
3
|
+
const targets = new Set();
|
|
4
|
+
const participants = new Set();
|
|
5
|
+
for (const cyc of slotCycles)
|
|
6
|
+
for (const p of cyc.path)
|
|
7
|
+
participants.add(p);
|
|
8
|
+
for (const p of participants) {
|
|
9
|
+
targets.add(p);
|
|
10
|
+
for (const anc of findAllAncestors(p, graph))
|
|
11
|
+
targets.add(anc);
|
|
12
|
+
}
|
|
13
|
+
return targets;
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CompositionMode } from '../lib/composition-mode.js';
|
|
1
2
|
export interface PipelineOptions {
|
|
2
3
|
project: string;
|
|
3
4
|
out: string;
|
|
@@ -23,6 +24,15 @@ export interface PipelineOptions {
|
|
|
23
24
|
deselect?: string[];
|
|
24
25
|
/** Forwarded to the spawned `analyze select-agent` subprocess. */
|
|
25
26
|
selectPromptPath?: string;
|
|
27
|
+
/** When true, auto-reject cycle participants and retry push instead of surfacing an error. */
|
|
28
|
+
autoRejectCycles?: boolean;
|
|
29
|
+
compositionMode?: CompositionMode;
|
|
30
|
+
compositionMap?: string;
|
|
31
|
+
compositionAgent?: boolean;
|
|
32
|
+
compositionAgentMode?: string;
|
|
33
|
+
compositionRefresh?: boolean;
|
|
34
|
+
generateMap?: string;
|
|
35
|
+
promptOverrides?: string[];
|
|
26
36
|
}
|
|
27
37
|
export interface StepResult {
|
|
28
38
|
step: string;
|
|
@@ -36,19 +46,22 @@ export interface PipelineResult {
|
|
|
36
46
|
session: string;
|
|
37
47
|
project: string;
|
|
38
48
|
steps: StepResult[];
|
|
49
|
+
cycleError?: {
|
|
50
|
+
report: string[];
|
|
51
|
+
};
|
|
39
52
|
}
|
|
53
|
+
export declare const SLOT_CYCLE_MARKER = "manifest:components/slot-cycles";
|
|
54
|
+
export declare function isSlotCycleError(result: {
|
|
55
|
+
exitCode: number;
|
|
56
|
+
stderr: string;
|
|
57
|
+
}): boolean;
|
|
58
|
+
export declare function extractCycleReport(stderr: string): string[];
|
|
59
|
+
export declare function parseCycleComponentNames(report: string[]): string[];
|
|
40
60
|
export declare function isPreviewValidationError(result: {
|
|
41
61
|
exitCode: number;
|
|
42
62
|
stderr: string;
|
|
43
63
|
}): boolean;
|
|
44
64
|
export declare function parseOffendingComponentNames(output: string): string[];
|
|
45
|
-
/**
|
|
46
|
-
* Build the apply-push StepResult record. Centralizes the success/failure
|
|
47
|
-
* shape so excludedByValidationRetry is recorded consistently in both
|
|
48
|
-
* branches — previously the total-failure branch dropped it, leaving users
|
|
49
|
-
* with a failed pipeline and no audit trail of what was auto-excluded
|
|
50
|
-
* before the retry loop gave up.
|
|
51
|
-
*/
|
|
52
65
|
export declare function buildPushStepResult(args: {
|
|
53
66
|
created: number;
|
|
54
67
|
updated: number;
|
|
@@ -2,7 +2,8 @@ import { mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
|
-
import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, } from '../session/db.js';
|
|
5
|
+
import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, loadCDFComponents, } from '../session/db.js';
|
|
6
|
+
import { detectSlotCycles, formatSlotCycleReport } from '../apply/command.js';
|
|
6
7
|
import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
|
|
7
8
|
import { buildPostPushUrl } from '../lib/contentful-urls.js';
|
|
8
9
|
import { getDebugLogger, debugEnvForSubprocess } from '../lib/debug-logger.js';
|
|
@@ -43,27 +44,34 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
|
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
const MAX_VALIDATION_RETRIES = Number(process.env['EDS_MAX_VALIDATION_RETRIES'] ?? 2);
|
|
47
|
+
export const SLOT_CYCLE_MARKER = 'manifest:components/slot-cycles';
|
|
48
|
+
export function isSlotCycleError(result) {
|
|
49
|
+
return result.exitCode !== 0 && result.stderr.includes(SLOT_CYCLE_MARKER);
|
|
50
|
+
}
|
|
51
|
+
export function extractCycleReport(stderr) {
|
|
52
|
+
return stderr.split('\n').filter((line) => line.trim());
|
|
53
|
+
}
|
|
54
|
+
export function parseCycleComponentNames(report) {
|
|
55
|
+
const names = [];
|
|
56
|
+
for (const line of report) {
|
|
57
|
+
const m = line.match(/Fix: remove '([^']+)' from /);
|
|
58
|
+
if (m)
|
|
59
|
+
names.push(m[1]);
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(names)];
|
|
62
|
+
}
|
|
46
63
|
export function isPreviewValidationError(result) {
|
|
47
64
|
return (result.exitCode !== 0 &&
|
|
48
65
|
result.stderr.includes(`${PREVIEW_ERROR_PREFIX} 422`) &&
|
|
49
66
|
result.stderr.includes(VALIDATION_FAILED_CODE));
|
|
50
67
|
}
|
|
51
68
|
export function parseOffendingComponentNames(output) {
|
|
52
|
-
// The 422 body is appended to the ApiError message by the constructor and
|
|
53
|
-
// written to stderr by die(). Extract the JSON portion by finding the first '{'.
|
|
54
69
|
const jsonStart = output.indexOf('{');
|
|
55
70
|
if (jsonStart === -1)
|
|
56
71
|
return [];
|
|
57
72
|
const errors = parsePreviewValidationErrors(output.slice(jsonStart));
|
|
58
73
|
return [...new Set(errors.map((e) => e.componentName))];
|
|
59
74
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Build the apply-push StepResult record. Centralizes the success/failure
|
|
62
|
-
* shape so excludedByValidationRetry is recorded consistently in both
|
|
63
|
-
* branches — previously the total-failure branch dropped it, leaving users
|
|
64
|
-
* with a failed pipeline and no audit trail of what was auto-excluded
|
|
65
|
-
* before the retry loop gave up.
|
|
66
|
-
*/
|
|
67
75
|
export function buildPushStepResult(args) {
|
|
68
76
|
const { created, updated, failed, durationMs, stderr, excludedByRetry, totalFailure } = args;
|
|
69
77
|
const excludedDetail = excludedByRetry.length > 0 ? { excludedByValidationRetry: excludedByRetry } : {};
|
|
@@ -106,13 +114,11 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
106
114
|
await mkdir(outDir, { recursive: true });
|
|
107
115
|
const steps = [];
|
|
108
116
|
let stepNum = 0;
|
|
109
|
-
// print components is an optional step; adjust total accordingly
|
|
110
117
|
const totalSteps = 4 + (opts.print ? 1 : 0);
|
|
111
118
|
function stepLabel(name) {
|
|
112
119
|
stepNum++;
|
|
113
120
|
return ` Step ${stepNum}/${totalSteps} ${name} `;
|
|
114
121
|
}
|
|
115
|
-
// ── Step 1: analyze extract ──────────────────────────────────────────────
|
|
116
122
|
const analyzeLabel = stepLabel('Statically analyzing project');
|
|
117
123
|
const analyzeSkipped = !opts.noCache && opts.skipAnalyze;
|
|
118
124
|
let extractSessionId = null;
|
|
@@ -123,7 +129,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
123
129
|
status: 'skipped',
|
|
124
130
|
reason: '--skip-analyze',
|
|
125
131
|
});
|
|
126
|
-
// Try to find a prior extract session to hand to downstream steps
|
|
127
132
|
const prior = findLatestSessionForCommand(db, 'analyze extract');
|
|
128
133
|
extractSessionId = prior ?? null;
|
|
129
134
|
}
|
|
@@ -133,6 +138,23 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
133
138
|
});
|
|
134
139
|
const t0 = Date.now();
|
|
135
140
|
const analyzeArgs = ['analyze', 'extract', '--project', projectRoot];
|
|
141
|
+
if (opts.compositionMode === 'composite') {
|
|
142
|
+
analyzeArgs.push('--composite');
|
|
143
|
+
if (opts.compositionMap)
|
|
144
|
+
analyzeArgs.push('--composition-map', opts.compositionMap);
|
|
145
|
+
if (opts.compositionAgent)
|
|
146
|
+
analyzeArgs.push('--composition-agent');
|
|
147
|
+
if (opts.compositionAgentMode)
|
|
148
|
+
analyzeArgs.push('--composition-agent-mode', opts.compositionAgentMode);
|
|
149
|
+
if (opts.compositionRefresh)
|
|
150
|
+
analyzeArgs.push('--composition-refresh');
|
|
151
|
+
if (opts.generateMap)
|
|
152
|
+
analyzeArgs.push('--generate-map', opts.generateMap);
|
|
153
|
+
for (const p of opts.promptOverrides ?? [])
|
|
154
|
+
analyzeArgs.push('--prompt', p);
|
|
155
|
+
if (opts.agent)
|
|
156
|
+
analyzeArgs.push('--agent', opts.agent);
|
|
157
|
+
}
|
|
136
158
|
const r = await runStep(analyzeArgs, cliPath);
|
|
137
159
|
const durationMs = Date.now() - t0;
|
|
138
160
|
if (r.exitCode !== 0) {
|
|
@@ -149,7 +171,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
149
171
|
db.close();
|
|
150
172
|
return { session: sessionId, project: projectRoot, steps };
|
|
151
173
|
}
|
|
152
|
-
// Read session ID from DB — avoids dependence on subprocess stdout format.
|
|
153
174
|
extractSessionId = findLatestSessionForCommand(db, 'analyze extract') ?? null;
|
|
154
175
|
const componentMatch = /Extracted (\d+) component/.exec(r.stderr);
|
|
155
176
|
const componentCount = componentMatch ? Number(componentMatch[1]) : 0;
|
|
@@ -164,7 +185,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
164
185
|
detail: { components: componentCount },
|
|
165
186
|
});
|
|
166
187
|
}
|
|
167
|
-
// ── Step 2: analyze select ───────────────────────────────────────────────
|
|
168
188
|
const editLabel = stepLabel('Filtering components');
|
|
169
189
|
if (analyzeSkipped) {
|
|
170
190
|
progressWriter(`${editLabel}– skipped (--skip-analyze)`);
|
|
@@ -179,7 +199,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
179
199
|
extractSession: extractSessionId,
|
|
180
200
|
});
|
|
181
201
|
const t0Edit = Date.now();
|
|
182
|
-
// Use agentic select when an agent is available and no manual select/deselect patterns are given.
|
|
183
202
|
const useAgentSelect = !opts.selectAll && (!opts.select || opts.select.length === 0) && (!opts.deselect || opts.deselect.length === 0);
|
|
184
203
|
let editArgs;
|
|
185
204
|
if (useAgentSelect) {
|
|
@@ -202,7 +221,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
202
221
|
else if (opts.deselect && opts.deselect.length > 0) {
|
|
203
222
|
for (const p of opts.deselect)
|
|
204
223
|
editArgs.push('--deselect', p);
|
|
205
|
-
editArgs.push('--select-all');
|
|
224
|
+
editArgs.push('--select-all');
|
|
206
225
|
}
|
|
207
226
|
else {
|
|
208
227
|
editArgs.push('--select-all');
|
|
@@ -245,7 +264,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
245
264
|
reason: 'no extract session',
|
|
246
265
|
});
|
|
247
266
|
}
|
|
248
|
-
// ── Step 3: generate components ──────────────────────────────────────────
|
|
249
267
|
if (opts.skipGenerate) {
|
|
250
268
|
const generateLabel = stepLabel('Categorizing component props');
|
|
251
269
|
progressWriter(`${generateLabel}– skipped (--skip-generate)`);
|
|
@@ -301,7 +319,29 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
301
319
|
progressWriter(`${generateLabel}✓ components stored locally (${(durationMs / 1000).toFixed(1)}s)`);
|
|
302
320
|
steps.push({ step: 'generate components', status: 'complete', durationMs });
|
|
303
321
|
}
|
|
304
|
-
//
|
|
322
|
+
// Block save or push when the accepted set still contains a slot cycle.
|
|
323
|
+
// Skipped under --auto-reject-cycles, which delegates to the push-path
|
|
324
|
+
// retry loop below (reject cycle members, then retry) instead of blocking.
|
|
325
|
+
if (extractSessionId && !opts.autoRejectCycles) {
|
|
326
|
+
const acceptedCycles = (() => {
|
|
327
|
+
try {
|
|
328
|
+
return detectSlotCycles(loadCDFComponents(db, extractSessionId));
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
})();
|
|
334
|
+
if (acceptedCycles.length > 0) {
|
|
335
|
+
const report = formatSlotCycleReport(acceptedCycles);
|
|
336
|
+
process.stderr.write(report.join('\n') + '\n');
|
|
337
|
+
progressWriter('Slot cycle detected. The accepted components contain circular slot references — refusing to save or push.');
|
|
338
|
+
for (const line of report)
|
|
339
|
+
progressWriter(line);
|
|
340
|
+
steps.push({ step: 'cycle gate', status: 'failed', error: report.join('\n') });
|
|
341
|
+
db.close();
|
|
342
|
+
return { session: sessionId, project: projectRoot, steps, cycleError: { report } };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
305
345
|
if (opts.print) {
|
|
306
346
|
const printLabel = stepLabel('Writing components.json');
|
|
307
347
|
const printArgs = ['print', 'components', '--out', componentsPath];
|
|
@@ -331,7 +371,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
331
371
|
progressWriter(`${printLabel}✓ components.json written (${(durationMs / 1000).toFixed(1)}s)`);
|
|
332
372
|
steps.push({ step: 'print components', status: 'complete', durationMs });
|
|
333
373
|
}
|
|
334
|
-
// ── Step 4/5: apply push ─────────────────────────────────────────────────
|
|
335
374
|
const applyLabelText = opts.spaceId && opts.environmentId
|
|
336
375
|
? `Applying changes to Space: ${opts.spaceId} Environment: ${opts.environmentId}`
|
|
337
376
|
: 'Applying changes to Contentful';
|
|
@@ -361,10 +400,6 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
361
400
|
'--cma-token',
|
|
362
401
|
opts.cmaToken,
|
|
363
402
|
];
|
|
364
|
-
// Components are stored under the extract session ID (generate command uses resolveSessionId
|
|
365
|
-
// which returns the passed --session value, i.e. the extract session). Pass that directly so
|
|
366
|
-
// apply push reads from the DB without needing a components.json file.
|
|
367
|
-
// Fall back to --components so the step fails with a clear error rather than a generic one.
|
|
368
403
|
if (extractSessionId) {
|
|
369
404
|
pushArgs.push('--session', extractSessionId);
|
|
370
405
|
}
|
|
@@ -379,28 +414,20 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
379
414
|
pushArgs.push('--host', opts.host);
|
|
380
415
|
if (opts.verbose)
|
|
381
416
|
pushArgs.push('--verbose');
|
|
382
|
-
pushArgs.push('--yes');
|
|
417
|
+
pushArgs.push('--yes');
|
|
383
418
|
const pushStepId = createStep(db, sessionId, 'apply push', {
|
|
384
419
|
components: componentsPath,
|
|
385
420
|
});
|
|
386
421
|
const t0 = Date.now();
|
|
387
422
|
let r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
388
|
-
// Bounded retry loop: on a preview-phase 422 with a parseable ValidationFailed
|
|
389
|
-
// body, exclude the offending components and re-run apply push.
|
|
390
423
|
const excludedByRetry = [];
|
|
391
424
|
let validationRetryCount = 0;
|
|
392
425
|
while (validationRetryCount < MAX_VALIDATION_RETRIES && isPreviewValidationError(r) && extractSessionId) {
|
|
393
426
|
const offenders = parseOffendingComponentNames(r.stderr + r.stdout);
|
|
394
427
|
if (offenders.length === 0)
|
|
395
|
-
break;
|
|
428
|
+
break;
|
|
396
429
|
process.stderr.write(`[retry ${validationRetryCount + 1}/${MAX_VALIDATION_RETRIES}] Preview validation failed — excluding ${offenders.join(', ')} and retrying\n`);
|
|
397
430
|
excludedByRetry.push(...offenders);
|
|
398
|
-
// No --select-all here: that would route through runNonInteractive's
|
|
399
|
-
// rebuild path, which DELETEs all rows and re-inserts with default
|
|
400
|
-
// status='extracted' — wiping the post-`generate components` state
|
|
401
|
-
// (status='generated' + raw_props.cdf_type) the next apply push reads.
|
|
402
|
-
// The bare --exclude-components form takes the rejectComponentsByName
|
|
403
|
-
// early-return: pure UPDATE, no rebuild.
|
|
404
431
|
const rejectArgs = [
|
|
405
432
|
'analyze',
|
|
406
433
|
'select',
|
|
@@ -411,7 +438,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
411
438
|
];
|
|
412
439
|
const rejectResult = await runStep(rejectArgs, cliPath);
|
|
413
440
|
if (rejectResult.exitCode !== 0)
|
|
414
|
-
break;
|
|
441
|
+
break;
|
|
415
442
|
r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
416
443
|
validationRetryCount++;
|
|
417
444
|
}
|
|
@@ -433,6 +460,83 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
433
460
|
? (pushResult.componentTypes?.failed ?? 0) + (pushResult.designTokens?.failed ?? 0)
|
|
434
461
|
: Number(/(\d+) failed/.exec(r.stdout + r.stderr)?.[1] ?? 0);
|
|
435
462
|
const totalPushed = created + updated + failed;
|
|
463
|
+
if (isSlotCycleError(r)) {
|
|
464
|
+
const report = extractCycleReport(r.stderr);
|
|
465
|
+
if (opts.autoRejectCycles && extractSessionId) {
|
|
466
|
+
const cycleNames = parseCycleComponentNames(report);
|
|
467
|
+
if (cycleNames.length > 0) {
|
|
468
|
+
process.stderr.write(`[cycle-retry] Slot cycle detected — excluding ${cycleNames.join(', ')} and retrying\n`);
|
|
469
|
+
const rejectArgs = [
|
|
470
|
+
'analyze',
|
|
471
|
+
'select',
|
|
472
|
+
'--session',
|
|
473
|
+
extractSessionId,
|
|
474
|
+
'--exclude-components',
|
|
475
|
+
cycleNames.join(','),
|
|
476
|
+
];
|
|
477
|
+
const rejectResult = await runStep(rejectArgs, cliPath);
|
|
478
|
+
if (rejectResult.exitCode === 0) {
|
|
479
|
+
const retryT0 = Date.now();
|
|
480
|
+
const retryR = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
481
|
+
const retryDurationMs = Date.now() - t0 + (Date.now() - retryT0);
|
|
482
|
+
if (isSlotCycleError(retryR)) {
|
|
483
|
+
const retryReport = extractCycleReport(retryR.stderr);
|
|
484
|
+
updateStep(db, pushStepId, 'failed', {}, retryR.stderr);
|
|
485
|
+
progressWriter(`${pushLabel}✗ failed (${(retryDurationMs / 1000).toFixed(1)}s)`);
|
|
486
|
+
progressWriter('Slot cycle detected after auto-reject retry. The manifest still contains circular slot references.');
|
|
487
|
+
for (const line of retryReport)
|
|
488
|
+
progressWriter(line);
|
|
489
|
+
steps.push(buildPushStepResult({
|
|
490
|
+
created,
|
|
491
|
+
updated,
|
|
492
|
+
failed,
|
|
493
|
+
durationMs: retryDurationMs,
|
|
494
|
+
stderr: retryR.stderr,
|
|
495
|
+
excludedByRetry: [...excludedByRetry, ...cycleNames],
|
|
496
|
+
totalFailure: true,
|
|
497
|
+
}));
|
|
498
|
+
db.close();
|
|
499
|
+
return { session: sessionId, project: projectRoot, steps, cycleError: { report: retryReport } };
|
|
500
|
+
}
|
|
501
|
+
if (retryR.exitCode !== 0) {
|
|
502
|
+
updateStep(db, pushStepId, 'failed', {}, retryR.stderr);
|
|
503
|
+
progressWriter(`${pushLabel}✗ failed (${(retryDurationMs / 1000).toFixed(1)}s)`);
|
|
504
|
+
steps.push(buildPushStepResult({
|
|
505
|
+
created,
|
|
506
|
+
updated,
|
|
507
|
+
failed,
|
|
508
|
+
durationMs: retryDurationMs,
|
|
509
|
+
stderr: retryR.stderr,
|
|
510
|
+
excludedByRetry: [...excludedByRetry, ...cycleNames],
|
|
511
|
+
totalFailure: true,
|
|
512
|
+
}));
|
|
513
|
+
db.close();
|
|
514
|
+
return { session: sessionId, project: projectRoot, steps };
|
|
515
|
+
}
|
|
516
|
+
excludedByRetry.push(...cycleNames);
|
|
517
|
+
r = retryR;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (isSlotCycleError(r)) {
|
|
522
|
+
updateStep(db, pushStepId, 'failed', {}, r.stderr);
|
|
523
|
+
progressWriter(`${pushLabel}✗ failed (${(durationMs / 1000).toFixed(1)}s)`);
|
|
524
|
+
progressWriter('Slot cycle detected. The manifest contains circular slot references that would block the push.');
|
|
525
|
+
for (const line of report)
|
|
526
|
+
progressWriter(line);
|
|
527
|
+
steps.push(buildPushStepResult({
|
|
528
|
+
created,
|
|
529
|
+
updated,
|
|
530
|
+
failed,
|
|
531
|
+
durationMs,
|
|
532
|
+
stderr: r.stderr,
|
|
533
|
+
excludedByRetry,
|
|
534
|
+
totalFailure: true,
|
|
535
|
+
}));
|
|
536
|
+
db.close();
|
|
537
|
+
return { session: sessionId, project: projectRoot, steps, cycleError: { report } };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
436
540
|
if (r.exitCode !== 0 && (totalPushed === 0 || failed === totalPushed)) {
|
|
437
541
|
// Total failure — nothing was pushed
|
|
438
542
|
updateStep(db, pushStepId, 'failed', {}, r.stderr);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CDFComponentEntry } from '@contentful/experience-design-system-types';
|
|
2
|
+
/**
|
|
3
|
+
* Atomic-mode normalization (spec T8, option a): keep every slot's structure
|
|
4
|
+
* but drop its `$allowedComponents` composition constraint. Applied over the
|
|
5
|
+
* loaded component array regardless of source (`--session` or `--components`),
|
|
6
|
+
* so the atomic bypass cannot leak a live composition link into the pushed
|
|
7
|
+
* manifest. Starving `$allowedComponents` at this single point also makes
|
|
8
|
+
* slot-cycle detection structurally return zero (see T12).
|
|
9
|
+
*
|
|
10
|
+
* Returns a new array with cloned entries; inputs are not mutated.
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripAllowedComponents(components: Array<{
|
|
13
|
+
key: string;
|
|
14
|
+
entry: CDFComponentEntry;
|
|
15
|
+
}>): Array<{
|
|
16
|
+
key: string;
|
|
17
|
+
entry: CDFComponentEntry;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic-mode normalization (spec T8, option a): keep every slot's structure
|
|
3
|
+
* but drop its `$allowedComponents` composition constraint. Applied over the
|
|
4
|
+
* loaded component array regardless of source (`--session` or `--components`),
|
|
5
|
+
* so the atomic bypass cannot leak a live composition link into the pushed
|
|
6
|
+
* manifest. Starving `$allowedComponents` at this single point also makes
|
|
7
|
+
* slot-cycle detection structurally return zero (see T12).
|
|
8
|
+
*
|
|
9
|
+
* Returns a new array with cloned entries; inputs are not mutated.
|
|
10
|
+
*/
|
|
11
|
+
export function stripAllowedComponents(components) {
|
|
12
|
+
return components.map(({ key, entry }) => {
|
|
13
|
+
if (!entry.$slots)
|
|
14
|
+
return { key, entry };
|
|
15
|
+
const $slots = {};
|
|
16
|
+
for (const [slotName, slotDef] of Object.entries(entry.$slots)) {
|
|
17
|
+
const { $allowedComponents: _dropped, ...rest } = slotDef;
|
|
18
|
+
$slots[slotName] = rest;
|
|
19
|
+
}
|
|
20
|
+
return { key, entry: { ...entry, $slots } };
|
|
21
|
+
});
|
|
22
|
+
}
|