@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.
Files changed (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. 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
- // --- Output helpers ---
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.map((bc) => `${bc.propertyId}: ${bc.reason}`).join(', ');
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, prompt];
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', prompt];
239
+ return ['exec', '--model', m, '--dangerously-bypass-approvals-and-sandbox', ...promptArg];
236
240
  case 'opencode':
237
- return ['run', '--model', m, prompt];
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, prompt];
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 args = buildArgs(agent, prompt, model);
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?.end();
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 = '';