@contentful/experience-design-system-cli 2.26.1-dev-build-a852362.0 → 2.26.1-dev-build-f03f014.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 (204) hide show
  1. package/dist/package.json +14 -10
  2. package/dist/src/index.js +34937 -9
  3. package/dist/src/lib/cli-path.d.ts +4 -0
  4. package/dist/src/runs/export-helpers.d.ts +1 -12
  5. package/dist/src/runs/push-helpers.d.ts +1 -6
  6. package/package.json +12 -7
  7. package/skills/generate-components.md +427 -0
  8. package/skills/generate-tokens.md +194 -0
  9. package/skills/select-components.md +224 -0
  10. package/dist/src/analytics/apply.js +0 -30
  11. package/dist/src/analytics/client.js +0 -81
  12. package/dist/src/analytics/constants.js +0 -4
  13. package/dist/src/analytics/env.js +0 -14
  14. package/dist/src/analytics/exit.js +0 -24
  15. package/dist/src/analytics/index.js +0 -9
  16. package/dist/src/analytics/normalize.js +0 -18
  17. package/dist/src/analytics/os.js +0 -13
  18. package/dist/src/analytics/session.js +0 -15
  19. package/dist/src/analytics/tracker.js +0 -126
  20. package/dist/src/analytics/types.js +0 -1
  21. package/dist/src/analyze/build-analyze-view-rows.js +0 -63
  22. package/dist/src/analyze/command.js +0 -599
  23. package/dist/src/analyze/composite-closure.js +0 -0
  24. package/dist/src/analyze/composition/agent-parser/author-prompt.js +0 -22
  25. package/dist/src/analyze/composition/agent-parser/extract-parser.js +0 -44
  26. package/dist/src/analyze/composition/agent-parser/load-prompt.js +0 -31
  27. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +0 -88
  28. package/dist/src/analyze/composition/agent-parser/sandbox.js +0 -187
  29. package/dist/src/analyze/composition/apply-mapping.js +0 -67
  30. package/dist/src/analyze/composition/candidate-critic-agent.js +0 -39
  31. package/dist/src/analyze/composition/candidate-critic.js +0 -63
  32. package/dist/src/analyze/composition/candidate-files.js +0 -131
  33. package/dist/src/analyze/composition/composition-cache-key.js +0 -19
  34. package/dist/src/analyze/composition/interchange-schema.js +0 -61
  35. package/dist/src/analyze/composition/manifest-doc-evidence.js +0 -136
  36. package/dist/src/analyze/composition/merge-edges.js +0 -70
  37. package/dist/src/analyze/composition/parse-map-edges.js +0 -55
  38. package/dist/src/analyze/composition/resolve-mapping-cli.js +0 -29
  39. package/dist/src/analyze/composition/resolve-mapping.js +0 -111
  40. package/dist/src/analyze/cycle-detection.js +0 -234
  41. package/dist/src/analyze/cycle-view.js +0 -28
  42. package/dist/src/analyze/fuzzy-search.js +0 -55
  43. package/dist/src/analyze/issue-inheritance.js +0 -108
  44. package/dist/src/analyze/lineage.js +0 -0
  45. package/dist/src/analyze/scope-gate-cascade.js +0 -170
  46. package/dist/src/analyze/search-neighborhood.js +0 -56
  47. package/dist/src/analyze/select/command.js +0 -484
  48. package/dist/src/analyze/select/index.js +0 -5
  49. package/dist/src/analyze/select/parser.js +0 -53
  50. package/dist/src/analyze/select/persistence.js +0 -80
  51. package/dist/src/analyze/select/preview-annotations.js +0 -62
  52. package/dist/src/analyze/select/stdout.js +0 -3
  53. package/dist/src/analyze/select/tui/App.js +0 -483
  54. package/dist/src/analyze/select/tui/components/ComponentDetail.js +0 -48
  55. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +0 -125
  56. package/dist/src/analyze/select/tui/components/FieldEditor.js +0 -1131
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +0 -36
  58. package/dist/src/analyze/select/tui/components/GotoBanner.js +0 -30
  59. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +0 -517
  60. package/dist/src/analyze/select/tui/components/HelpOverlay.js +0 -17
  61. package/dist/src/analyze/select/tui/components/JsonEditor.js +0 -131
  62. package/dist/src/analyze/select/tui/components/JsonPanel.js +0 -66
  63. package/dist/src/analyze/select/tui/components/LineagePanel.js +0 -22
  64. package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +0 -29
  65. package/dist/src/analyze/select/tui/components/QuitDialog.js +0 -14
  66. package/dist/src/analyze/select/tui/components/RationalePanel.js +0 -80
  67. package/dist/src/analyze/select/tui/components/Sidebar.js +0 -92
  68. package/dist/src/analyze/select/tui/components/SourcePanel.js +0 -52
  69. package/dist/src/analyze/select/tui/components/StatusBar.js +0 -7
  70. package/dist/src/analyze/select/tui/components/TopBar.js +0 -10
  71. package/dist/src/analyze/select/tui/components/removed-components-text.js +0 -11
  72. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +0 -26
  73. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +0 -66
  74. package/dist/src/analyze/select/tui/hooks/useKeymap.js +0 -67
  75. package/dist/src/analyze/select/tui/hooks/useSession.js +0 -52
  76. package/dist/src/analyze/select/tui/hooks/useUndo.js +0 -26
  77. package/dist/src/analyze/select/tui/theme.js +0 -28
  78. package/dist/src/analyze/select/types.js +0 -39
  79. package/dist/src/analyze/select-agent/command.js +0 -558
  80. package/dist/src/analyze/select-agent/context-builder.js +0 -197
  81. package/dist/src/analyze/select-agent/show-rationale.js +0 -92
  82. package/dist/src/analyze/selection-cascade.js +0 -19
  83. package/dist/src/analyze/slot-graph.js +0 -17
  84. package/dist/src/analyze/tui/AnalyzeView.js +0 -64
  85. package/dist/src/apply/api-client.js +0 -440
  86. package/dist/src/apply/command.js +0 -845
  87. package/dist/src/apply/manifest.js +0 -1
  88. package/dist/src/apply/preview-utils.js +0 -26
  89. package/dist/src/apply/tui/SelectView.js +0 -34
  90. package/dist/src/apply/tui/ServerApplyView.js +0 -49
  91. package/dist/src/apply/tui/ServerPreviewView.js +0 -23
  92. package/dist/src/credentials-store.js +0 -74
  93. package/dist/src/generate/command.js +0 -597
  94. package/dist/src/generate/edit/command.js +0 -127
  95. package/dist/src/generate/tui/GenerateView.js +0 -10
  96. package/dist/src/host-utils.js +0 -26
  97. package/dist/src/import/agent-model-resolve.js +0 -35
  98. package/dist/src/import/auto-filter-resolve.js +0 -16
  99. package/dist/src/import/command.js +0 -380
  100. package/dist/src/import/cycle-auto-reject.js +0 -14
  101. package/dist/src/import/orchestrator.js +0 -603
  102. package/dist/src/import/path-utils.js +0 -30
  103. package/dist/src/import/picker-dispatch.js +0 -44
  104. package/dist/src/import/print-prompt.js +0 -31
  105. package/dist/src/import/strip-allowed-components.js +0 -22
  106. package/dist/src/import/tui/CustomPromptBanner.js +0 -8
  107. package/dist/src/import/tui/WizardApp.js +0 -1670
  108. package/dist/src/import/tui/ai-flag.js +0 -6
  109. package/dist/src/import/tui/auto-filter-error.js +0 -29
  110. package/dist/src/import/tui/autocomplete.js +0 -32
  111. package/dist/src/import/tui/components/AutoFilterBanner.js +0 -19
  112. package/dist/src/import/tui/components/CounterStrip.js +0 -12
  113. package/dist/src/import/tui/components/LegendEntry.js +0 -6
  114. package/dist/src/import/tui/cycle-panel-scroll.js +0 -33
  115. package/dist/src/import/tui/final-review-host.js +0 -25
  116. package/dist/src/import/tui/group-collapse.js +0 -11
  117. package/dist/src/import/tui/history.js +0 -53
  118. package/dist/src/import/tui/hooks/useLineage.js +0 -50
  119. package/dist/src/import/tui/hooks/useOverlayPanel.js +0 -27
  120. package/dist/src/import/tui/lineage-layout.js +0 -82
  121. package/dist/src/import/tui/merge-ai-decisions.js +0 -22
  122. package/dist/src/import/tui/push-decision-gate-helpers.js +0 -8
  123. package/dist/src/import/tui/push-progress.js +0 -1
  124. package/dist/src/import/tui/run-print-files-helpers.js +0 -8
  125. package/dist/src/import/tui/run-teaser.js +0 -9
  126. package/dist/src/import/tui/runLivePreview.js +0 -82
  127. package/dist/src/import/tui/runScopeGate.js +0 -28
  128. package/dist/src/import/tui/scope-gate-columns.js +0 -94
  129. package/dist/src/import/tui/scope-gate-host.js +0 -28
  130. package/dist/src/import/tui/sidebar-width.js +0 -3
  131. package/dist/src/import/tui/spawn-generate.js +0 -53
  132. package/dist/src/import/tui/step-filters.js +0 -42
  133. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +0 -726
  134. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +0 -210
  135. package/dist/src/import/tui/steps/CredentialsStep.js +0 -108
  136. package/dist/src/import/tui/steps/DoneStep.js +0 -24
  137. package/dist/src/import/tui/steps/ErrorStep.js +0 -16
  138. package/dist/src/import/tui/steps/GateStep.js +0 -22
  139. package/dist/src/import/tui/steps/GenerateReviewStep.js +0 -1618
  140. package/dist/src/import/tui/steps/PathValidationStep.js +0 -152
  141. package/dist/src/import/tui/steps/PreviewStep.js +0 -37
  142. package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +0 -23
  143. package/dist/src/import/tui/steps/PushDecisionGateStep.js +0 -73
  144. package/dist/src/import/tui/steps/PushingStep.js +0 -24
  145. package/dist/src/import/tui/steps/RunningStep.js +0 -21
  146. package/dist/src/import/tui/steps/ScopeGateStep.js +0 -907
  147. package/dist/src/import/tui/steps/TokenInputStep.js +0 -88
  148. package/dist/src/import/tui/steps/WelcomeStep.js +0 -34
  149. package/dist/src/import/tui/steps/WizardPreviewStep.js +0 -169
  150. package/dist/src/import/tui/steps/auto-reject-decision.js +0 -9
  151. package/dist/src/import/tui/steps/breaking-change-format.js +0 -24
  152. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  153. package/dist/src/import/tui/steps/preview-diff.js +0 -175
  154. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  155. package/dist/src/import/tui/useLivePreview.js +0 -122
  156. package/dist/src/import/tui/wizard-422-helpers.js +0 -60
  157. package/dist/src/import/tui/wizard-generate-progress.js +0 -25
  158. package/dist/src/import/tui/wizard-save-flow.js +0 -20
  159. package/dist/src/import/tui/wizard-state-transitions.js +0 -32
  160. package/dist/src/lib/agent-model-options.js +0 -15
  161. package/dist/src/lib/command-options.js +0 -30
  162. package/dist/src/lib/composition-mode.js +0 -39
  163. package/dist/src/lib/contentful-urls.js +0 -27
  164. package/dist/src/lib/debug-logger.js +0 -229
  165. package/dist/src/lib/debug-preamble.js +0 -33
  166. package/dist/src/lib/error-parser.js +0 -269
  167. package/dist/src/lib/prompt-overrides.js +0 -69
  168. package/dist/src/lib/terminal-capabilities.js +0 -51
  169. package/dist/src/lib/user-agent.js +0 -29
  170. package/dist/src/output/format.js +0 -110
  171. package/dist/src/print/command.js +0 -221
  172. package/dist/src/print/validate/tui/ValidateView.js +0 -37
  173. package/dist/src/print/validate/validators/cdf-validator.js +0 -104
  174. package/dist/src/print/validate/validators/dtcg-validator.js +0 -110
  175. package/dist/src/print/validate/validators/format-errors.js +0 -18
  176. package/dist/src/program.js +0 -105
  177. package/dist/src/runs/export-helpers.js +0 -59
  178. package/dist/src/runs/fingerprint.js +0 -75
  179. package/dist/src/runs/ls-command.js +0 -137
  180. package/dist/src/runs/modify-launcher.js +0 -42
  181. package/dist/src/runs/path-prompt.js +0 -72
  182. package/dist/src/runs/push-creds-prompt.js +0 -39
  183. package/dist/src/runs/push-helpers.js +0 -72
  184. package/dist/src/runs/push-launcher.js +0 -77
  185. package/dist/src/runs/replay-helpers.js +0 -164
  186. package/dist/src/runs/resolve-run-target.js +0 -45
  187. package/dist/src/runs/run-picker-mount.js +0 -72
  188. package/dist/src/runs/run-picker.js +0 -129
  189. package/dist/src/runs/save-conflict.js +0 -56
  190. package/dist/src/runs/save-path-resolver.js +0 -75
  191. package/dist/src/runs/staleness.js +0 -140
  192. package/dist/src/runs/store.js +0 -136
  193. package/dist/src/session/cache-keys.js +0 -44
  194. package/dist/src/session/command.js +0 -261
  195. package/dist/src/session/db.js +0 -1538
  196. package/dist/src/session/migration.js +0 -117
  197. package/dist/src/session/session-id.js +0 -212
  198. package/dist/src/session/stats.js +0 -89
  199. package/dist/src/setup/analytics-prompt.js +0 -24
  200. package/dist/src/setup/auto-filter-prompt.js +0 -24
  201. package/dist/src/setup/command.js +0 -966
  202. package/dist/src/setup/composition-mode-prompt.js +0 -37
  203. package/dist/src/setup/debug-mode-prompt.js +0 -22
  204. package/dist/src/types.js +0 -1
@@ -1,558 +0,0 @@
1
- import { addAgentModelOptions } from '../../lib/agent-model-options.js';
2
- import { openPipelineDb, loadRawComponents, loadScannedFiles, createStep, updateStep, computeComponentInputHash, lookupSelectCache, storeSelectCache, getCliCacheVersion, } from '../../session/db.js';
3
- import { hashPromptForSkill } from '../../session/cache-keys.js';
4
- import { appendReviewEvent, getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, saveReviewState, } from '../select/persistence.js';
5
- import { loadReviewInput } from '../select/parser.js';
6
- import { AGENT_NAMES, buildPrompt, createLocalCliAgentInvoker, formatCustomPromptBanner, isAgentName, parseSelectToolCallLines, } from '@contentful/experience-design-system-generation';
7
- import { access } from 'node:fs/promises';
8
- import { readExperiencesCredentials } from '../../credentials-store.js';
9
- import { OutputFormatter, c } from '../../output/format.js';
10
- import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
11
- import { runShowRationale } from './show-rationale.js';
12
- import { isAbsolute, resolve } from 'node:path';
13
- import { getDebugLogger } from '../../lib/debug-logger.js';
14
- import { bindAnalyticsSessionId, enrichCommandResult, exitWithAnalytics } from '../../analytics/index.js';
15
- import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
16
- const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
17
- export const DEFAULT_CONCURRENCY = 10;
18
- export const DEFAULT_BATCH_SIZE = 5;
19
- const invoker = createLocalCliAgentInvoker({
20
- onDebugEvent: (name, payload) => getDebugLogger().event('agent', name, payload),
21
- });
22
- function resolveBatchSize() {
23
- const raw = process.env.EDS_SELECT_BATCH_SIZE;
24
- if (!raw)
25
- return DEFAULT_BATCH_SIZE;
26
- const n = Number(raw);
27
- if (!Number.isFinite(n) || n < 1)
28
- return DEFAULT_BATCH_SIZE;
29
- return Math.floor(n);
30
- }
31
- async function resolveSessionId(sessionFlag) {
32
- if (sessionFlag)
33
- return sessionFlag;
34
- const db = openPipelineDb();
35
- try {
36
- const row = db
37
- .prepare(`SELECT s.id FROM sessions s
38
- JOIN steps st ON st.session_id = s.id
39
- WHERE st.command = 'analyze extract'
40
- AND st.status = 'complete'
41
- ORDER BY st.started_at DESC
42
- LIMIT 1`)
43
- .get();
44
- if (!row) {
45
- process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
46
- return await exitWithAnalytics(1);
47
- }
48
- return row.id;
49
- }
50
- finally {
51
- db.close();
52
- }
53
- }
54
- function componentKey(component) {
55
- return `${component.name}::${component.source}`;
56
- }
57
- function buildComponentData(candidate) {
58
- const { component, selectionContext } = candidate;
59
- const payload = {
60
- name: component.name,
61
- source: component.source,
62
- framework: component.framework,
63
- propCount: component.props.length,
64
- slotCount: component.slots.length,
65
- propNames: component.props.slice(0, 8).map((p) => p.name),
66
- props: component.props,
67
- slots: component.slots,
68
- extractionConfidence: component.extractionConfidence ?? null,
69
- needsReview: component.needsReview ?? false,
70
- };
71
- if (component.reviewReasons && component.reviewReasons.length > 0) {
72
- payload.reviewReasons = component.reviewReasons;
73
- }
74
- if (selectionContext) {
75
- payload.selectionContext = selectionContext;
76
- }
77
- return payload;
78
- }
79
- function resolveProjectRoot(sessionId, projectRootFlag) {
80
- if (projectRootFlag)
81
- return resolve(projectRootFlag);
82
- const db = openPipelineDb();
83
- try {
84
- const row = db
85
- .prepare(`SELECT inputs FROM steps
86
- WHERE session_id = ?
87
- AND command = 'analyze extract'
88
- ORDER BY started_at DESC
89
- LIMIT 1`)
90
- .get(sessionId);
91
- if (!row?.inputs)
92
- return null;
93
- const parsed = JSON.parse(row.inputs);
94
- return parsed.project ? resolve(parsed.project) : null;
95
- }
96
- catch {
97
- return null;
98
- }
99
- finally {
100
- db.close();
101
- }
102
- }
103
- async function selectBatch(agent, model, batch, total, verbose, skillPathOverride) {
104
- const prompt = await buildPrompt({
105
- skill: 'select',
106
- mode: 'autonomous',
107
- rawComponentsInline: JSON.stringify(batch.map((b) => buildComponentData(b.candidate)), null, 2),
108
- outDir: process.cwd(),
109
- skillPathOverride,
110
- });
111
- let outputBuf = '';
112
- const formatter = new OutputFormatter(verbose, (s) => {
113
- outputBuf += s;
114
- });
115
- const result = await invoker.invoke({
116
- agent,
117
- model,
118
- prompt,
119
- timeoutMs: DEFAULT_TIMEOUT_MS,
120
- onOutput: (chunk) => formatter.push(chunk),
121
- });
122
- formatter.flush();
123
- if (verbose) {
124
- const names = batch.map((b) => b.candidate.component.name).join(', ');
125
- process.stderr.write(` ${c.bold(`batch: ${names}`)}\n${outputBuf}`);
126
- }
127
- // Feature 3: emit one progress= line per component (in input order) regardless
128
- // of batch outcome. The wizard's runAutoFilter parser depends on this contract.
129
- const emitProgress = (item, decision, reason) => {
130
- const reasonEncoded = reason ? encodeURIComponent(reason) : '';
131
- process.stderr.write(`progress=select-agent:${item.index + 1}/${total}:${decision}:${item.candidate.component.name}:${reasonEncoded}\n`);
132
- };
133
- // Whole-batch failure: timeout or non-zero exit. Mark every item failed and
134
- // do NOT emit progress lines (no decision was made).
135
- if (result.timedOut) {
136
- for (const item of batch) {
137
- const pos = c.dim(`[${item.index + 1}/${total}]`);
138
- process.stderr.write(` ${pos} ${c.bold(item.candidate.component.name)} ${c.yellow('timed out')}\n`);
139
- }
140
- return batch.map((item) => ({
141
- componentKey: componentKey(item.candidate.component),
142
- componentName: item.candidate.component.name,
143
- decision: null,
144
- failed: true,
145
- }));
146
- }
147
- if (result.exitCode !== 0) {
148
- for (const item of batch) {
149
- const pos = c.dim(`[${item.index + 1}/${total}]`);
150
- process.stderr.write(` ${pos} ${c.bold(item.candidate.component.name)} ${c.red(`agent exited with code ${result.exitCode}`)}\n`);
151
- }
152
- return batch.map((item) => ({
153
- componentKey: componentKey(item.candidate.component),
154
- componentName: item.candidate.component.name,
155
- decision: null,
156
- failed: true,
157
- }));
158
- }
159
- const { calls, warnings } = parseSelectToolCallLines(result.stdout);
160
- if (warnings.length > 0) {
161
- for (const warning of warnings) {
162
- process.stderr.write(` ${c.yellow('⚠')} batch: ${warning}\n`);
163
- }
164
- }
165
- const results = [];
166
- for (const item of batch) {
167
- const { component } = item.candidate;
168
- const pos = c.dim(`[${item.index + 1}/${total}]`);
169
- const call = calls.find((toolCall) => toolCall.name === component.name);
170
- if (!call) {
171
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('no tool call')}\n`);
172
- emitProgress(item, 'failed', 'no-tool-call-from-agent');
173
- results.push({
174
- componentKey: componentKey(component),
175
- componentName: component.name,
176
- decision: null,
177
- failed: true,
178
- });
179
- continue;
180
- }
181
- const decision = call.tool === 'select_component' ? 'accepted' : 'rejected';
182
- const finalColor = decision === 'accepted' ? c.green : c.red;
183
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${finalColor(decision)}${call.reason ? ` ${c.dim(call.reason)}` : ''}\n`);
184
- emitProgress(item, decision, call.reason);
185
- results.push({
186
- componentKey: componentKey(component),
187
- componentName: component.name,
188
- decision,
189
- reason: call.reason,
190
- failed: false,
191
- });
192
- }
193
- return results;
194
- }
195
- async function selectAllComponents(agent, model, components, verbose, skillPathOverride, cacheConfig = { noCache: true }) {
196
- const concurrency = Number(process.env.EDS_GENERATE_CONCURRENCY ?? DEFAULT_CONCURRENCY);
197
- const batchSize = resolveBatchSize();
198
- const total = components.length;
199
- // ── Fine-grained select cache: replay decisions for components whose
200
- // (component_hash, prompt_hash, cli_version) triple already has a row. Only
201
- // uncached components are forwarded to the LLM batches.
202
- const cachedResults = new Map();
203
- let promptHash = '';
204
- let cliVersion = '';
205
- if (!cacheConfig.noCache) {
206
- try {
207
- promptHash = await hashPromptForSkill('select', agent, model, skillPathOverride);
208
- cliVersion = await getCliCacheVersion();
209
- const db = openPipelineDb(cacheConfig.dbPath);
210
- try {
211
- for (let i = 0; i < components.length; i++) {
212
- const candidate = components[i];
213
- const compHash = computeComponentInputHash(candidate.component);
214
- const hit = lookupSelectCache(db, compHash, promptHash, cliVersion);
215
- if (hit) {
216
- const finalColor = hit.decision === 'accepted' ? c.green : c.red;
217
- process.stderr.write(` ${c.dim(`[${i + 1}/${total}]`)} ${c.bold(candidate.component.name)} ${finalColor(hit.decision)} ${c.dim('(cached)')}` +
218
- (hit.reason ? ` ${c.dim(hit.reason)}` : '') +
219
- '\n');
220
- // Mirror the progress= line that selectBatch emits for live LLM
221
- // calls so the wizard's parser sees a uniform stream.
222
- const reasonEncoded = hit.reason ? encodeURIComponent(hit.reason) : '';
223
- process.stderr.write(`progress=select-agent:${i + 1}/${total}:${hit.decision}:${candidate.component.name}:${reasonEncoded}\n`);
224
- cachedResults.set(i, {
225
- componentKey: componentKey(candidate.component),
226
- componentName: candidate.component.name,
227
- decision: hit.decision,
228
- reason: hit.reason ?? undefined,
229
- failed: false,
230
- });
231
- }
232
- }
233
- }
234
- finally {
235
- db.close();
236
- }
237
- }
238
- catch {
239
- // Cache prefill is best-effort. On any error, fall through and run the
240
- // full LLM batch path.
241
- }
242
- }
243
- // Build the work-set of components we still need to ask the LLM about.
244
- const uncached = [];
245
- const uncachedIndices = [];
246
- for (let i = 0; i < components.length; i++) {
247
- if (!cachedResults.has(i)) {
248
- uncached.push(components[i]);
249
- uncachedIndices.push(i);
250
- }
251
- }
252
- // Chunk into batches of `batchSize`, preserving original indices for the
253
- // per-component progress=select-agent: lines.
254
- const batches = [];
255
- for (let i = 0; i < uncached.length; i += batchSize) {
256
- const slice = uncached.slice(i, i + batchSize);
257
- batches.push(slice.map((candidate, j) => ({ candidate, index: uncachedIndices[i + j] })));
258
- }
259
- process.stderr.write(`Validating ${c.bold(String(uncached.length))} component${uncached.length === 1 ? '' : 's'}` +
260
- (cachedResults.size > 0 ? c.dim(` (${cachedResults.size} cached)`) : '') +
261
- c.dim(` (concurrency: ${concurrency}, batch: ${batchSize}, batches: ${batches.length})`) +
262
- '\n');
263
- const results = new Array(total);
264
- for (const [i, r] of cachedResults)
265
- results[i] = r;
266
- let nextBatch = 0;
267
- async function worker() {
268
- while (nextBatch < batches.length) {
269
- const b = nextBatch++;
270
- const batch = batches[b];
271
- const batchResults = await selectBatch(agent, model, batch, total, verbose, skillPathOverride);
272
- for (let k = 0; k < batch.length; k++) {
273
- results[batch[k].index] = batchResults[k];
274
- }
275
- }
276
- }
277
- await Promise.all(Array.from({ length: Math.min(concurrency, batches.length) }, worker));
278
- // Persist fresh decisions (skip cached + failed ones).
279
- if (!cacheConfig.noCache && promptHash) {
280
- try {
281
- const db = openPipelineDb(cacheConfig.dbPath);
282
- try {
283
- for (let i = 0; i < results.length; i++) {
284
- if (cachedResults.has(i))
285
- continue;
286
- const r = results[i];
287
- if (!r || r.failed || !r.decision)
288
- continue;
289
- const candidate = components[i];
290
- const compHash = computeComponentInputHash(candidate.component);
291
- storeSelectCache(db, compHash, promptHash, cliVersion, r.decision, r.reason ?? null);
292
- }
293
- }
294
- finally {
295
- db.close();
296
- }
297
- }
298
- catch {
299
- // Best-effort.
300
- }
301
- }
302
- return results;
303
- }
304
- export function registerAnalyzeSelectAgentCommand(program) {
305
- const cmd = program
306
- .command('select-agent')
307
- .description('Use an AI agent to select components for Contentful Experience Orchestration')
308
- .option('--session <id>', 'Session ID from analyze extract (defaults to most recent)')
309
- .option('--project-root <path>', 'Project root for resolving component source files');
310
- addAgentModelOptions(cmd)
311
- .option('--verbose', 'Show full agent output including reasoning text')
312
- .option('--dry-run', 'Print the prompt for the first component without invoking the agent')
313
- .option('--exclude-invalid', 'Auto-reject components with validation errors instead of failing loud (LLM cannot fix structural issues)')
314
- .option('--select-prompt-path <path>', 'Path to a custom .md skill prompt for select-agent (bypasses bundled prompt invariants)')
315
- .option('--no-select-cache', 'Skip the per-component select cache and re-LLM every component')
316
- .option('--no-cache', 'Skip ALL fine-grained caches (extract, select, generate)')
317
- .option('--show-rationale', 'Read-only: print the AI rejection rationale persisted by a prior select-agent run and exit. ' +
318
- 'No LLM call. Combine with --json for machine-readable output.')
319
- .option('--json', 'When used with --show-rationale, emit a JSON array instead of a human-readable table')
320
- .action(async (opts) => {
321
- // --show-rationale is a read-only branch that short-circuits the normal
322
- // select-agent flow. It re-reads the rationale columns persisted by a
323
- // prior run (raw_components.status + reject_reason) and prints them.
324
- // No LLM call, no agent resolution, no validation gates.
325
- if (opts.showRationale) {
326
- try {
327
- runShowRationale({ session: opts.session, json: opts.json });
328
- return;
329
- }
330
- catch (err) {
331
- const message = err instanceof Error ? err.message : String(err);
332
- process.stderr.write(`Error: ${message}\n`);
333
- await exitWithAnalytics(1);
334
- return;
335
- }
336
- }
337
- const savedCreds = await readExperiencesCredentials();
338
- const agentName = opts.agent ?? savedCreds.agent;
339
- const model = opts.model ?? savedCreds.agentModel;
340
- if (!agentName || !isAgentName(agentName)) {
341
- process.stderr.write(`Error: no agent configured. Pass --agent <name> or run experiences setup. Accepted values: ${AGENT_NAMES.join(', ')}\n`);
342
- await exitWithAnalytics(1);
343
- return;
344
- }
345
- const agent = agentName;
346
- // Feature 8: validate + announce custom prompt path before any heavy
347
- // work. Flag wins over saved credentials.
348
- const selectPromptPath = opts.selectPromptPath ?? savedCreds.selectPromptPath;
349
- if (selectPromptPath) {
350
- const resolvedPath = resolve(selectPromptPath);
351
- const exists = await access(resolvedPath)
352
- .then(() => true)
353
- .catch(() => false);
354
- if (!exists) {
355
- process.stderr.write(`Error: custom prompt path not found: ${resolvedPath}\n`);
356
- await exitWithAnalytics(1);
357
- return;
358
- }
359
- if (!selectPromptPath.toLowerCase().endsWith('.md')) {
360
- process.stderr.write(`WARNING: custom prompt path does not end in .md (${selectPromptPath}) — proceeding anyway.\n`);
361
- }
362
- process.stderr.write(formatCustomPromptBanner('select', resolvedPath));
363
- }
364
- const sessionId = await resolveSessionId(opts.session);
365
- await bindAnalyticsSessionId(sessionId);
366
- const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
367
- const db = openPipelineDb();
368
- let rawComponents;
369
- let scannedFiles = [];
370
- try {
371
- rawComponents = loadRawComponents(db, sessionId);
372
- scannedFiles = loadScannedFiles(db, sessionId);
373
- }
374
- finally {
375
- db.close();
376
- }
377
- if (rawComponents.length === 0) {
378
- process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
379
- await exitWithAnalytics(1);
380
- return;
381
- }
382
- // Re-run validation (not persisted to DB, so always recompute).
383
- const validatedComponents = validateExtractedComponents(rawComponents);
384
- // Fail-loud gate: if any component has validation errors, refuse to
385
- // proceed unless --exclude-invalid is set. The LLM cannot fix
386
- // structural issues (empty names, slot/prop collisions, etc.) — silent
387
- // exclusion in this CI-style command would drop components without the
388
- // caller noticing.
389
- const invalidComponents = validatedComponents.filter(shouldExcludeDueToValidation);
390
- if (invalidComponents.length > 0 && !opts.excludeInvalid) {
391
- const lines = [
392
- `Error: ${invalidComponents.length} component(s) failed validation; refusing select-agent without --exclude-invalid:`,
393
- ];
394
- for (const comp of invalidComponents) {
395
- const codes = (comp.validationIssues ?? [])
396
- .filter((i) => i.severity === 'error')
397
- .map((i) => i.code)
398
- .join(', ');
399
- lines.push(` ✗ ${comp.name} ${codes}`);
400
- }
401
- lines.push('');
402
- lines.push('Re-run with --exclude-invalid to auto-reject these components, or fix them in source first.');
403
- process.stderr.write(lines.join('\n') + '\n');
404
- await exitWithAnalytics(1);
405
- return;
406
- }
407
- const componentsForAgent = validatedComponents.filter((comp) => !shouldExcludeDueToValidation(comp));
408
- if (invalidComponents.length > 0) {
409
- process.stderr.write(c.yellow(formatExclusionWarning(invalidComponents)));
410
- }
411
- if (selectionRoot && scannedFiles.length > 0) {
412
- scannedFiles = scannedFiles.map((f) => (isAbsolute(f) ? f : resolve(selectionRoot, f)));
413
- }
414
- if (selectionRoot && scannedFiles.length === 0 && rawComponents.length > 0) {
415
- process.stderr.write('warn: session has no scanned-files index (likely extracted on an older CLI version). ' +
416
- 'Re-run `analyze extract` to enable data-fetch wrapper detection during selection.\n');
417
- }
418
- let selectionCandidates = componentsForAgent.map((component) => ({ component }));
419
- if (selectionRoot && scannedFiles.length > 0) {
420
- const repoIndex = await buildRepoContextIndex(selectionRoot, scannedFiles).catch(() => null);
421
- if (repoIndex) {
422
- selectionCandidates = componentsForAgent.map((component) => ({
423
- component,
424
- selectionContext: buildSelectionContext(repoIndex, component),
425
- }));
426
- }
427
- }
428
- if (opts.dryRun) {
429
- if (selectionCandidates.length === 0) {
430
- process.stderr.write('No valid components to preview — all components were excluded due to validation errors.\n');
431
- await exitWithAnalytics(0);
432
- return;
433
- }
434
- const first = selectionCandidates[0];
435
- const prompt = await buildPrompt({
436
- skill: 'select',
437
- mode: 'autonomous',
438
- rawComponentsInline: JSON.stringify([buildComponentData(first)], null, 2),
439
- outDir: process.cwd(),
440
- skillPathOverride: selectPromptPath ? resolve(selectPromptPath) : undefined,
441
- });
442
- process.stdout.write(prompt + '\n');
443
- await exitWithAnalytics(0);
444
- return;
445
- }
446
- // --no-cache is the global kill-switch; --no-select-cache is the stage-
447
- // specific opt-out. Both Commander negation flags arrive as `false` when
448
- // the user passed --no-*; default (undefined) means cache stays on.
449
- const noCache = opts.cache === false || opts.selectCache === false || process.env.EDS_NO_CACHE === '1';
450
- const selectResults = await selectAllComponents(agent, model, selectionCandidates, opts.verbose ?? false, selectPromptPath ? resolve(selectPromptPath) : undefined, { noCache });
451
- // Build decision map from results. Seed auto-rejections from validation exclusions first.
452
- const decisions = new Map();
453
- // Track per-component reasons (for raw_components.reject_reason persistence).
454
- // Validation auto-rejections get a synthesized "validation error: <codes>" reason
455
- // so the scope-gate UI can distinguish wizard- vs LLM-driven exclusions.
456
- const reasons = new Map();
457
- for (const comp of invalidComponents) {
458
- decisions.set(componentKey(comp), 'rejected');
459
- const codes = (comp.validationIssues ?? [])
460
- .filter((i) => i.severity === 'error')
461
- .map((i) => i.code)
462
- .join(', ');
463
- reasons.set(componentKey(comp), `validation error: ${codes}`);
464
- }
465
- for (const r of selectResults) {
466
- decisions.set(r.componentKey, r.decision);
467
- if (r.decision === 'rejected' && r.reason) {
468
- reasons.set(r.componentKey, r.reason);
469
- }
470
- else if (r.decision === 'accepted') {
471
- // Defensive: clear any previously-stored reason on re-runs that flip a
472
- // rejected component to accepted.
473
- reasons.set(r.componentKey, null);
474
- }
475
- }
476
- // Feature 3: persist decisions to raw_components (status + reject_reason)
477
- // so the wizard can render LLM exclusions in the scope-gate without re-reading
478
- // the snapshot file. We open a fresh DB handle here (the earlier one was closed
479
- // after loading raw components above) and reuse it for the step-recording below.
480
- const persistDb = openPipelineDb();
481
- try {
482
- const updateStmt = persistDb.prepare(`UPDATE raw_components
483
- SET status = ?, reject_reason = ?
484
- WHERE session_id = ? AND component_id = ?`);
485
- for (const comp of validatedComponents) {
486
- const key = componentKey(comp);
487
- const decision = decisions.get(key);
488
- if (!decision)
489
- continue;
490
- if (!comp.component_id)
491
- continue;
492
- const reason = reasons.get(key) ?? null;
493
- updateStmt.run(decision, decision === 'accepted' ? null : reason, sessionId, comp.component_id);
494
- }
495
- }
496
- finally {
497
- persistDb.close();
498
- }
499
- const artifactsRoot = getRefineArtifactsRoot();
500
- let snapshot;
501
- try {
502
- snapshot = await loadReviewInput(validatedComponents, {
503
- reviewRoot: selectionRoot ?? undefined,
504
- });
505
- snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
506
- }
507
- catch (error) {
508
- process.stderr.write(`Error: unable to initialize select session.\n${error instanceof Error ? error.message : String(error)}\n`);
509
- await exitWithAnalytics(1);
510
- return;
511
- }
512
- const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
513
- const updated = {
514
- ...snapshot,
515
- components: snapshot.components.map((comp) => {
516
- const key = componentKey(comp.originalProposal);
517
- const decision = decisions.get(key);
518
- if (!decision)
519
- return comp;
520
- return { ...comp, status: decision };
521
- }),
522
- };
523
- await saveReviewState(paths.statePath, updated);
524
- await Promise.all(updated.components
525
- .filter((component) => component.status === 'accepted' || component.status === 'rejected')
526
- .map((component) => appendReviewEvent(paths.eventsPath, {
527
- type: 'select_agent_decision',
528
- payload: {
529
- component: component.name,
530
- source: component.originalProposal.source,
531
- status: component.status,
532
- },
533
- })));
534
- const accepted = updated.components.filter((comp) => comp.status === 'accepted');
535
- const rejected = updated.components.filter((comp) => comp.status === 'rejected');
536
- const unresolved = updated.components.filter((comp) => comp.status === 'needs-review');
537
- const failed = selectResults.filter((r) => r.failed);
538
- if (failed.length > 0) {
539
- process.stderr.write(c.red(`Failed (${failed.length}/${selectResults.length}):`) + '\n');
540
- for (const f of failed) {
541
- process.stderr.write(` ${c.red('✗')} ${f.componentName}\n`);
542
- }
543
- }
544
- const stepDb = openPipelineDb();
545
- const stepId = createStep(stepDb, sessionId, 'analyze select', {
546
- sessionId,
547
- });
548
- try {
549
- const status = failed.length === selectResults.length ? 'failed' : 'complete';
550
- updateStep(stepDb, stepId, status, { sessionId });
551
- }
552
- finally {
553
- stepDb.close();
554
- }
555
- enrichCommandResult({ accepted_component_count: accepted.length });
556
- process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length} Needs review: ${unresolved.length}\n`);
557
- });
558
- }