@contentful/experience-design-system-cli 2.12.0 → 2.12.1-dev-build-bae15c7.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 (144) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +260 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +820 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/merge-ai-decisions.d.ts +5 -0
  62. package/dist/src/import/tui/merge-ai-decisions.js +22 -0
  63. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  64. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  65. package/dist/src/import/tui/push-progress.d.ts +23 -0
  66. package/dist/src/import/tui/push-progress.js +14 -0
  67. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  68. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  69. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  70. package/dist/src/import/tui/run-teaser.js +9 -0
  71. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  72. package/dist/src/import/tui/runLivePreview.js +73 -0
  73. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  74. package/dist/src/import/tui/runScopeGate.js +28 -0
  75. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  76. package/dist/src/import/tui/scope-gate-host.js +20 -0
  77. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  78. package/dist/src/import/tui/spawn-generate.js +53 -0
  79. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  80. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  81. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  82. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  83. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  84. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  85. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  86. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  87. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  88. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  89. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  90. package/dist/src/import/tui/steps/ScopeGateStep.js +250 -0
  91. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  92. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  93. package/dist/src/import/tui/useLivePreview.js +120 -0
  94. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  95. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  96. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  97. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  98. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  99. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  100. package/dist/src/lib/contentful-urls.d.ts +34 -0
  101. package/dist/src/lib/contentful-urls.js +41 -0
  102. package/dist/src/program.js +2 -0
  103. package/dist/src/runs/export-helpers.d.ts +30 -0
  104. package/dist/src/runs/export-helpers.js +59 -0
  105. package/dist/src/runs/fingerprint.d.ts +47 -0
  106. package/dist/src/runs/fingerprint.js +75 -0
  107. package/dist/src/runs/ls-command.d.ts +12 -0
  108. package/dist/src/runs/ls-command.js +137 -0
  109. package/dist/src/runs/modify-launcher.d.ts +24 -0
  110. package/dist/src/runs/modify-launcher.js +36 -0
  111. package/dist/src/runs/path-prompt.d.ts +8 -0
  112. package/dist/src/runs/path-prompt.js +72 -0
  113. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  114. package/dist/src/runs/push-creds-prompt.js +39 -0
  115. package/dist/src/runs/push-helpers.d.ts +29 -0
  116. package/dist/src/runs/push-helpers.js +69 -0
  117. package/dist/src/runs/replay-helpers.d.ts +67 -0
  118. package/dist/src/runs/replay-helpers.js +148 -0
  119. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  120. package/dist/src/runs/resolve-run-target.js +45 -0
  121. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  122. package/dist/src/runs/run-picker-mount.js +72 -0
  123. package/dist/src/runs/run-picker.d.ts +17 -0
  124. package/dist/src/runs/run-picker.js +145 -0
  125. package/dist/src/runs/save-conflict.d.ts +8 -0
  126. package/dist/src/runs/save-conflict.js +56 -0
  127. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  128. package/dist/src/runs/save-path-resolver.js +71 -0
  129. package/dist/src/runs/staleness.d.ts +38 -0
  130. package/dist/src/runs/staleness.js +140 -0
  131. package/dist/src/runs/store.d.ts +78 -0
  132. package/dist/src/runs/store.js +136 -0
  133. package/dist/src/session/cache-keys.d.ts +15 -0
  134. package/dist/src/session/cache-keys.js +38 -0
  135. package/dist/src/session/db.d.ts +89 -3
  136. package/dist/src/session/db.js +447 -104
  137. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  138. package/dist/src/setup/auto-filter-prompt.js +24 -0
  139. package/dist/src/setup/command.d.ts +7 -0
  140. package/dist/src/setup/command.js +58 -2
  141. package/dist/src/types.d.ts +6 -0
  142. package/package.json +2 -2
  143. package/skills/generate-components.md +54 -6
  144. package/skills/select-components.md +13 -4
@@ -1,16 +1,30 @@
1
- import { openPipelineDb, loadRawComponents, loadScannedFiles, createStep, updateStep } from '../../session/db.js';
1
+ import { openPipelineDb, loadRawComponents, loadScannedFiles, createStep, updateStep, computeComponentInputHash, lookupSelectCache, storeSelectCache, getCliCacheVersion, } from '../../session/db.js';
2
+ import { hashPromptForSkill } from '../../session/cache-keys.js';
2
3
  import { appendReviewEvent, getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, saveReviewState, } from '../select/persistence.js';
3
4
  import { loadReviewInput } from '../select/parser.js';
4
5
  import { buildPrompt } from '../../generate/prompt-builder.js';
6
+ import { formatCustomPromptBanner } from '../../generate/command.js';
5
7
  import { parseSelectToolCallLines, runAgent } from '../../generate/agent-runner.js';
8
+ import { access } from 'node:fs/promises';
6
9
  import { readExperiencesCredentials } from '../../credentials-store.js';
7
10
  import { OutputFormatter, c } from '../../output/format.js';
8
11
  import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
12
+ import { runShowRationale } from './show-rationale.js';
9
13
  import { isAbsolute, resolve } from 'node:path';
10
14
  import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
11
15
  const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
12
16
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
13
- const DEFAULT_CONCURRENCY = 5;
17
+ export const DEFAULT_CONCURRENCY = 10;
18
+ export const DEFAULT_BATCH_SIZE = 5;
19
+ function resolveBatchSize() {
20
+ const raw = process.env.EDS_SELECT_BATCH_SIZE;
21
+ if (!raw)
22
+ return DEFAULT_BATCH_SIZE;
23
+ const n = Number(raw);
24
+ if (!Number.isFinite(n) || n < 1)
25
+ return DEFAULT_BATCH_SIZE;
26
+ return Math.floor(n);
27
+ }
14
28
  function resolveSessionId(sessionFlag) {
15
29
  if (sessionFlag)
16
30
  return sessionFlag;
@@ -83,15 +97,14 @@ function resolveProjectRoot(sessionId, projectRootFlag) {
83
97
  db.close();
84
98
  }
85
99
  }
86
- async function selectOneComponent(agent, model, candidate, index, total, verbose) {
87
- const { component } = candidate;
100
+ async function selectBatch(agent, model, batch, total, verbose, skillPathOverride) {
88
101
  const prompt = await buildPrompt({
89
102
  skill: 'select',
90
103
  mode: 'autonomous',
91
- rawComponentsInline: JSON.stringify([buildComponentData(candidate)], null, 2),
104
+ rawComponentsInline: JSON.stringify(batch.map((b) => buildComponentData(b.candidate)), null, 2),
92
105
  outDir: process.cwd(),
106
+ skillPathOverride,
93
107
  });
94
- const pos = c.dim(`[${index + 1}/${total}]`);
95
108
  let outputBuf = '';
96
109
  const formatter = new OutputFormatter(verbose, (s) => {
97
110
  outputBuf += s;
@@ -106,67 +119,184 @@ async function selectOneComponent(agent, model, candidate, index, total, verbose
106
119
  });
107
120
  formatter.flush();
108
121
  if (verbose) {
109
- process.stderr.write(` ${pos} ${c.bold(component.name)}\n${outputBuf}`);
122
+ const names = batch.map((b) => b.candidate.component.name).join(', ');
123
+ process.stderr.write(` ${c.bold(`batch: ${names}`)}\n${outputBuf}`);
110
124
  }
125
+ // Feature 3: emit one progress= line per component (in input order) regardless
126
+ // of batch outcome. The wizard's runAutoFilter parser depends on this contract.
127
+ const emitProgress = (item, decision, reason) => {
128
+ const reasonEncoded = reason ? encodeURIComponent(reason) : '';
129
+ process.stderr.write(`progress=select-agent:${item.index + 1}/${total}:${decision}:${item.candidate.component.name}:${reasonEncoded}\n`);
130
+ };
131
+ // Whole-batch failure: timeout or non-zero exit. Mark every item failed and
132
+ // do NOT emit progress lines (no decision was made).
111
133
  if (result.timedOut) {
112
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('timed out')}\n`);
113
- return {
114
- componentKey: componentKey(component),
115
- componentName: component.name,
134
+ for (const item of batch) {
135
+ const pos = c.dim(`[${item.index + 1}/${total}]`);
136
+ process.stderr.write(` ${pos} ${c.bold(item.candidate.component.name)} ${c.yellow('timed out')}\n`);
137
+ }
138
+ return batch.map((item) => ({
139
+ componentKey: componentKey(item.candidate.component),
140
+ componentName: item.candidate.component.name,
116
141
  decision: null,
117
142
  failed: true,
118
- };
143
+ }));
119
144
  }
120
145
  if (result.exitCode !== 0) {
121
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.red(`agent exited with code ${result.exitCode}`)}\n`);
122
- return {
123
- componentKey: componentKey(component),
124
- componentName: component.name,
146
+ for (const item of batch) {
147
+ const pos = c.dim(`[${item.index + 1}/${total}]`);
148
+ process.stderr.write(` ${pos} ${c.bold(item.candidate.component.name)} ${c.red(`agent exited with code ${result.exitCode}`)}\n`);
149
+ }
150
+ return batch.map((item) => ({
151
+ componentKey: componentKey(item.candidate.component),
152
+ componentName: item.candidate.component.name,
125
153
  decision: null,
126
154
  failed: true,
127
- };
155
+ }));
128
156
  }
129
157
  const { calls, warnings } = parseSelectToolCallLines(result.stdout);
130
158
  if (warnings.length > 0) {
131
159
  for (const warning of warnings) {
132
- process.stderr.write(` ${c.yellow('⚠')} ${component.name}: ${warning}\n`);
160
+ process.stderr.write(` ${c.yellow('⚠')} batch: ${warning}\n`);
133
161
  }
134
162
  }
135
- const call = calls.find((toolCall) => toolCall.name === component.name) ?? calls[0];
136
- if (!call) {
137
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('no tool call')}\n`);
138
- return {
163
+ const results = [];
164
+ for (const item of batch) {
165
+ const { component } = item.candidate;
166
+ const pos = c.dim(`[${item.index + 1}/${total}]`);
167
+ const call = calls.find((toolCall) => toolCall.name === component.name);
168
+ if (!call) {
169
+ process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('no tool call')}\n`);
170
+ emitProgress(item, 'failed', 'no-tool-call-from-agent');
171
+ results.push({
172
+ componentKey: componentKey(component),
173
+ componentName: component.name,
174
+ decision: null,
175
+ failed: true,
176
+ });
177
+ continue;
178
+ }
179
+ const decision = call.tool === 'select_component' ? 'accepted' : 'rejected';
180
+ const finalColor = decision === 'accepted' ? c.green : c.red;
181
+ process.stderr.write(` ${pos} ${c.bold(component.name)} ${finalColor(decision)}${call.reason ? ` ${c.dim(call.reason)}` : ''}\n`);
182
+ emitProgress(item, decision, call.reason);
183
+ results.push({
139
184
  componentKey: componentKey(component),
140
185
  componentName: component.name,
141
- decision: null,
142
- failed: true,
143
- };
186
+ decision,
187
+ reason: call.reason,
188
+ failed: false,
189
+ });
144
190
  }
145
- const decision = call.tool === 'select_component' ? 'accepted' : 'rejected';
146
- const finalColor = decision === 'accepted' ? c.green : c.red;
147
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${finalColor(decision)}${call.reason ? ` ${c.dim(call.reason)}` : ''}\n`);
148
- return {
149
- componentKey: componentKey(component),
150
- componentName: component.name,
151
- decision,
152
- reason: call.reason,
153
- failed: false,
154
- };
191
+ return results;
155
192
  }
156
- async function selectAllComponents(agent, model, components, verbose) {
193
+ async function selectAllComponents(agent, model, components, verbose, skillPathOverride, cacheConfig = { noCache: true }) {
157
194
  const concurrency = Number(process.env.EDS_GENERATE_CONCURRENCY ?? DEFAULT_CONCURRENCY);
158
- process.stderr.write(`Validating ${c.bold(String(components.length))} component${components.length === 1 ? '' : 's'}` +
159
- c.dim(` (concurrency: ${concurrency})`) +
195
+ const batchSize = resolveBatchSize();
196
+ const total = components.length;
197
+ // ── Fine-grained select cache: replay decisions for components whose
198
+ // (component_hash, prompt_hash, cli_version) triple already has a row. Only
199
+ // uncached components are forwarded to the LLM batches.
200
+ const cachedResults = new Map();
201
+ let promptHash = '';
202
+ let cliVersion = '';
203
+ if (!cacheConfig.noCache) {
204
+ try {
205
+ promptHash = await hashPromptForSkill('select', skillPathOverride);
206
+ cliVersion = await getCliCacheVersion();
207
+ const db = openPipelineDb(cacheConfig.dbPath);
208
+ try {
209
+ for (let i = 0; i < components.length; i++) {
210
+ const candidate = components[i];
211
+ const compHash = computeComponentInputHash(candidate.component);
212
+ const hit = lookupSelectCache(db, compHash, promptHash, cliVersion);
213
+ if (hit) {
214
+ const finalColor = hit.decision === 'accepted' ? c.green : c.red;
215
+ process.stderr.write(` ${c.dim(`[${i + 1}/${total}]`)} ${c.bold(candidate.component.name)} ${finalColor(hit.decision)} ${c.dim('(cached)')}` +
216
+ (hit.reason ? ` ${c.dim(hit.reason)}` : '') +
217
+ '\n');
218
+ // Mirror the progress= line that selectBatch emits for live LLM
219
+ // calls so the wizard's parser sees a uniform stream.
220
+ const reasonEncoded = hit.reason ? encodeURIComponent(hit.reason) : '';
221
+ process.stderr.write(`progress=select-agent:${i + 1}/${total}:${hit.decision}:${candidate.component.name}:${reasonEncoded}\n`);
222
+ cachedResults.set(i, {
223
+ componentKey: componentKey(candidate.component),
224
+ componentName: candidate.component.name,
225
+ decision: hit.decision,
226
+ reason: hit.reason ?? undefined,
227
+ failed: false,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ finally {
233
+ db.close();
234
+ }
235
+ }
236
+ catch {
237
+ // Cache prefill is best-effort. On any error, fall through and run the
238
+ // full LLM batch path.
239
+ }
240
+ }
241
+ // Build the work-set of components we still need to ask the LLM about.
242
+ const uncached = [];
243
+ const uncachedIndices = [];
244
+ for (let i = 0; i < components.length; i++) {
245
+ if (!cachedResults.has(i)) {
246
+ uncached.push(components[i]);
247
+ uncachedIndices.push(i);
248
+ }
249
+ }
250
+ // Chunk into batches of `batchSize`, preserving original indices for the
251
+ // per-component progress=select-agent: lines.
252
+ const batches = [];
253
+ for (let i = 0; i < uncached.length; i += batchSize) {
254
+ const slice = uncached.slice(i, i + batchSize);
255
+ batches.push(slice.map((candidate, j) => ({ candidate, index: uncachedIndices[i + j] })));
256
+ }
257
+ process.stderr.write(`Validating ${c.bold(String(uncached.length))} component${uncached.length === 1 ? '' : 's'}` +
258
+ (cachedResults.size > 0 ? c.dim(` (${cachedResults.size} cached)`) : '') +
259
+ c.dim(` (concurrency: ${concurrency}, batch: ${batchSize}, batches: ${batches.length})`) +
160
260
  '\n');
161
- const results = new Array(components.length);
162
- let next = 0;
261
+ const results = new Array(total);
262
+ for (const [i, r] of cachedResults)
263
+ results[i] = r;
264
+ let nextBatch = 0;
163
265
  async function worker() {
164
- while (next < components.length) {
165
- const i = next++;
166
- results[i] = await selectOneComponent(agent, model, components[i], i, components.length, verbose);
266
+ while (nextBatch < batches.length) {
267
+ const b = nextBatch++;
268
+ const batch = batches[b];
269
+ const batchResults = await selectBatch(agent, model, batch, total, verbose, skillPathOverride);
270
+ for (let k = 0; k < batch.length; k++) {
271
+ results[batch[k].index] = batchResults[k];
272
+ }
273
+ }
274
+ }
275
+ await Promise.all(Array.from({ length: Math.min(concurrency, batches.length) }, worker));
276
+ // Persist fresh decisions (skip cached + failed ones).
277
+ if (!cacheConfig.noCache && promptHash) {
278
+ try {
279
+ const db = openPipelineDb(cacheConfig.dbPath);
280
+ try {
281
+ for (let i = 0; i < results.length; i++) {
282
+ if (cachedResults.has(i))
283
+ continue;
284
+ const r = results[i];
285
+ if (!r || r.failed || !r.decision)
286
+ continue;
287
+ const candidate = components[i];
288
+ const compHash = computeComponentInputHash(candidate.component);
289
+ storeSelectCache(db, compHash, promptHash, cliVersion, r.decision, r.reason ?? null);
290
+ }
291
+ }
292
+ finally {
293
+ db.close();
294
+ }
295
+ }
296
+ catch {
297
+ // Best-effort.
167
298
  }
168
299
  }
169
- await Promise.all(Array.from({ length: Math.min(concurrency, components.length) }, worker));
170
300
  return results;
171
301
  }
172
302
  export function registerAnalyzeSelectAgentCommand(program) {
@@ -180,7 +310,29 @@ export function registerAnalyzeSelectAgentCommand(program) {
180
310
  .option('--verbose', 'Show full agent output including reasoning text')
181
311
  .option('--dry-run', 'Print the prompt for the first component without invoking the agent')
182
312
  .option('--exclude-invalid', 'Auto-reject components with validation errors instead of failing loud (LLM cannot fix structural issues)')
313
+ .option('--select-prompt-path <path>', 'Path to a custom .md skill prompt for select-agent (bypasses bundled prompt invariants)')
314
+ .option('--no-select-cache', 'Skip the per-component select cache and re-LLM every component')
315
+ .option('--no-cache', 'Skip ALL fine-grained caches (extract, select, generate)')
316
+ .option('--show-rationale', 'Read-only: print the AI rejection rationale persisted by a prior select-agent run and exit. ' +
317
+ 'No LLM call. Combine with --json for machine-readable output.')
318
+ .option('--json', 'When used with --show-rationale, emit a JSON array instead of a human-readable table')
183
319
  .action(async (opts) => {
320
+ // --show-rationale is a read-only branch that short-circuits the normal
321
+ // select-agent flow. It re-reads the rationale columns persisted by a
322
+ // prior run (raw_components.status + reject_reason) and prints them.
323
+ // No LLM call, no agent resolution, no validation gates.
324
+ if (opts.showRationale) {
325
+ try {
326
+ runShowRationale({ session: opts.session, json: opts.json });
327
+ return;
328
+ }
329
+ catch (err) {
330
+ const message = err instanceof Error ? err.message : String(err);
331
+ process.stderr.write(`Error: ${message}\n`);
332
+ process.exit(1);
333
+ return;
334
+ }
335
+ }
184
336
  const savedCreds = await readExperiencesCredentials();
185
337
  const agentName = opts.agent ?? savedCreds.agent;
186
338
  const model = opts.model ?? savedCreds.agentModel;
@@ -190,6 +342,24 @@ export function registerAnalyzeSelectAgentCommand(program) {
190
342
  return;
191
343
  }
192
344
  const agent = agentName;
345
+ // Feature 8: validate + announce custom prompt path before any heavy
346
+ // work. Flag wins over saved credentials.
347
+ const selectPromptPath = opts.selectPromptPath ?? savedCreds.selectPromptPath;
348
+ if (selectPromptPath) {
349
+ const resolvedPath = resolve(selectPromptPath);
350
+ const exists = await access(resolvedPath)
351
+ .then(() => true)
352
+ .catch(() => false);
353
+ if (!exists) {
354
+ process.stderr.write(`Error: custom prompt path not found: ${resolvedPath}\n`);
355
+ process.exit(1);
356
+ return;
357
+ }
358
+ if (!selectPromptPath.toLowerCase().endsWith('.md')) {
359
+ process.stderr.write(`WARNING: custom prompt path does not end in .md (${selectPromptPath}) — proceeding anyway.\n`);
360
+ }
361
+ process.stderr.write(formatCustomPromptBanner('select', resolvedPath));
362
+ }
193
363
  const sessionId = resolveSessionId(opts.session);
194
364
  const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
195
365
  const db = openPipelineDb();
@@ -265,19 +435,64 @@ export function registerAnalyzeSelectAgentCommand(program) {
265
435
  mode: 'autonomous',
266
436
  rawComponentsInline: JSON.stringify([buildComponentData(first)], null, 2),
267
437
  outDir: process.cwd(),
438
+ skillPathOverride: selectPromptPath ? resolve(selectPromptPath) : undefined,
268
439
  });
269
440
  process.stdout.write(prompt + '\n');
270
441
  process.exit(0);
271
442
  return;
272
443
  }
273
- const selectResults = await selectAllComponents(agent, model, selectionCandidates, opts.verbose ?? false);
444
+ // --no-cache is the global kill-switch; --no-select-cache is the stage-
445
+ // specific opt-out. Both Commander negation flags arrive as `false` when
446
+ // the user passed --no-*; default (undefined) means cache stays on.
447
+ const noCache = opts.cache === false || opts.selectCache === false || process.env.EDS_NO_CACHE === '1';
448
+ const selectResults = await selectAllComponents(agent, model, selectionCandidates, opts.verbose ?? false, selectPromptPath ? resolve(selectPromptPath) : undefined, { noCache });
274
449
  // Build decision map from results. Seed auto-rejections from validation exclusions first.
275
450
  const decisions = new Map();
451
+ // Track per-component reasons (for raw_components.reject_reason persistence).
452
+ // Validation auto-rejections get a synthesized "validation error: <codes>" reason
453
+ // so the scope-gate UI can distinguish wizard- vs LLM-driven exclusions.
454
+ const reasons = new Map();
276
455
  for (const comp of invalidComponents) {
277
456
  decisions.set(componentKey(comp), 'rejected');
457
+ const codes = (comp.validationIssues ?? [])
458
+ .filter((i) => i.severity === 'error')
459
+ .map((i) => i.code)
460
+ .join(', ');
461
+ reasons.set(componentKey(comp), `validation error: ${codes}`);
278
462
  }
279
463
  for (const r of selectResults) {
280
464
  decisions.set(r.componentKey, r.decision);
465
+ if (r.decision === 'rejected' && r.reason) {
466
+ reasons.set(r.componentKey, r.reason);
467
+ }
468
+ else if (r.decision === 'accepted') {
469
+ // Defensive: clear any previously-stored reason on re-runs that flip a
470
+ // rejected component to accepted.
471
+ reasons.set(r.componentKey, null);
472
+ }
473
+ }
474
+ // Feature 3: persist decisions to raw_components (status + reject_reason)
475
+ // so the wizard can render LLM exclusions in the scope-gate without re-reading
476
+ // the snapshot file. We open a fresh DB handle here (the earlier one was closed
477
+ // after loading raw components above) and reuse it for the step-recording below.
478
+ const persistDb = openPipelineDb();
479
+ try {
480
+ const updateStmt = persistDb.prepare(`UPDATE raw_components
481
+ SET status = ?, reject_reason = ?
482
+ WHERE session_id = ? AND component_id = ?`);
483
+ for (const comp of validatedComponents) {
484
+ const key = componentKey(comp);
485
+ const decision = decisions.get(key);
486
+ if (!decision)
487
+ continue;
488
+ if (!comp.component_id)
489
+ continue;
490
+ const reason = reasons.get(key) ?? null;
491
+ updateStmt.run(decision, decision === 'accepted' ? null : reason, sessionId, comp.component_id);
492
+ }
493
+ }
494
+ finally {
495
+ persistDb.close();
281
496
  }
282
497
  const artifactsRoot = getRefineArtifactsRoot();
283
498
  let snapshot;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Read-only rationale view for the `analyze select-agent --show-rationale` flag.
3
+ *
4
+ * Reads the rationale data already persisted by a previous `analyze select-agent`
5
+ * run (component-level decision + reject reason on raw_components; per-prop
6
+ * rationale on raw_props — both populated by the writer path in command.ts and
7
+ * the rationale columns added in the schema migration for PR #52). No LLM
8
+ * invocation, no schema changes.
9
+ *
10
+ * Schema-version assertion: if the rationale columns are absent (older DB),
11
+ * throw with a clear message pointing at the remediation step.
12
+ */
13
+ export interface RationaleRow {
14
+ name: string;
15
+ decision: 'accepted' | 'rejected' | 'needs-review' | 'extracted';
16
+ reason: string | null;
17
+ }
18
+ /**
19
+ * Resolves which session to read from. If `sessionFlag` is provided, asserts
20
+ * the session exists and has at least one raw component; otherwise falls back
21
+ * to the most recent completed `analyze select` step.
22
+ */
23
+ export declare function resolveRationaleSession(sessionFlag: string | undefined, dbPath?: string): string;
24
+ export declare function loadRationaleRows(sessionId: string, dbPath?: string): RationaleRow[];
25
+ export declare function formatRationaleTable(rows: RationaleRow[]): string;
26
+ export declare function formatRationaleJson(rows: RationaleRow[]): string;
27
+ /**
28
+ * Top-level dispatch for `analyze select-agent --show-rationale`. Pure I/O
29
+ * boundary: reads DB, writes formatted output to stdout, throws on error.
30
+ */
31
+ export declare function runShowRationale(opts: {
32
+ session?: string;
33
+ json?: boolean;
34
+ dbPath?: string;
35
+ }): void;
@@ -0,0 +1,92 @@
1
+ import { openPipelineDb, findLatestSessionForCommand } from '../../session/db.js';
2
+ const REQUIRED_COLUMNS = ['status', 'reject_reason'];
3
+ function assertSchema(dbPath) {
4
+ const db = openPipelineDb(dbPath);
5
+ try {
6
+ const cols = db.prepare('PRAGMA table_info(raw_components)').all();
7
+ const have = new Set(cols.map((c) => c.name));
8
+ const missing = REQUIRED_COLUMNS.filter((c) => !have.has(c));
9
+ if (missing.length > 0) {
10
+ throw new Error(`Rationale data missing — run a fresh 'experiences import' on this session first. ` +
11
+ `(raw_components is missing column(s): ${missing.join(', ')})`);
12
+ }
13
+ }
14
+ finally {
15
+ db.close();
16
+ }
17
+ }
18
+ /**
19
+ * Resolves which session to read from. If `sessionFlag` is provided, asserts
20
+ * the session exists and has at least one raw component; otherwise falls back
21
+ * to the most recent completed `analyze select` step.
22
+ */
23
+ export function resolveRationaleSession(sessionFlag, dbPath) {
24
+ const db = openPipelineDb(dbPath);
25
+ try {
26
+ if (sessionFlag) {
27
+ const row = db.prepare('SELECT 1 AS ok FROM raw_components WHERE session_id = ? LIMIT 1').get(sessionFlag);
28
+ if (!row) {
29
+ throw new Error(`Session not found or has no components: '${sessionFlag}'. ` +
30
+ `Run 'analyze extract' + 'analyze select-agent' on this session first.`);
31
+ }
32
+ return sessionFlag;
33
+ }
34
+ const latest = findLatestSessionForCommand(db, 'analyze select');
35
+ if (!latest) {
36
+ throw new Error(`No completed 'analyze select' session found. Run 'analyze select-agent' first, or pass --session <id>.`);
37
+ }
38
+ return latest;
39
+ }
40
+ finally {
41
+ db.close();
42
+ }
43
+ }
44
+ export function loadRationaleRows(sessionId, dbPath) {
45
+ const db = openPipelineDb(dbPath);
46
+ try {
47
+ const rows = db
48
+ .prepare(`SELECT name, status, reject_reason FROM raw_components
49
+ WHERE session_id = ?
50
+ ORDER BY name`)
51
+ .all(sessionId);
52
+ return rows.map((r) => ({
53
+ name: r.name,
54
+ decision: r.status ?? 'extracted',
55
+ reason: r.reject_reason,
56
+ }));
57
+ }
58
+ finally {
59
+ db.close();
60
+ }
61
+ }
62
+ export function formatRationaleTable(rows) {
63
+ if (rows.length === 0) {
64
+ return 'No rationale rows found for this session.\n';
65
+ }
66
+ const header = { name: 'Component', decision: 'Decision', reason: 'Reason' };
67
+ const all = [header, ...rows.map((r) => ({ name: r.name, decision: r.decision, reason: r.reason ?? '' }))];
68
+ const nameWidth = Math.max(...all.map((r) => r.name.length));
69
+ const decisionWidth = Math.max(...all.map((r) => r.decision.length));
70
+ const lines = [];
71
+ const fmt = (n, d, r) => `${n.padEnd(nameWidth)} ${d.padEnd(decisionWidth)} ${r}`;
72
+ lines.push(fmt(header.name, header.decision, header.reason));
73
+ lines.push(fmt('-'.repeat(nameWidth), '-'.repeat(decisionWidth), '-'.repeat(Math.max(6, header.reason.length))));
74
+ for (const row of rows) {
75
+ lines.push(fmt(row.name, row.decision, row.reason ?? ''));
76
+ }
77
+ return lines.join('\n') + '\n';
78
+ }
79
+ export function formatRationaleJson(rows) {
80
+ return (JSON.stringify(rows.map((r) => ({ name: r.name, decision: r.decision, reason: r.reason })), null, 2) + '\n');
81
+ }
82
+ /**
83
+ * Top-level dispatch for `analyze select-agent --show-rationale`. Pure I/O
84
+ * boundary: reads DB, writes formatted output to stdout, throws on error.
85
+ */
86
+ export function runShowRationale(opts) {
87
+ assertSchema(opts.dbPath);
88
+ const sessionId = resolveRationaleSession(opts.session, opts.dbPath);
89
+ const rows = loadRationaleRows(sessionId, opts.dbPath);
90
+ const output = opts.json ? formatRationaleJson(rows) : formatRationaleTable(rows);
91
+ process.stdout.write(output);
92
+ }
@@ -7,6 +7,7 @@ import { ApiError, ImportApiClient } from './api-client.js';
7
7
  import { openPipelineDb, loadCDFComponents } from '../session/db.js';
8
8
  import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
9
9
  import { SelectView, makeSelectKey } from './tui/SelectView.js';
10
+ import { buildPostPushUrl } from '../lib/contentful-urls.js';
10
11
  function die(message) {
11
12
  process.stderr.write(`${message}\n`);
12
13
  process.exit(1);
@@ -214,7 +215,7 @@ function buildPreviewOutput(preview, spaceId, environmentId) {
214
215
  },
215
216
  };
216
217
  }
217
- function buildApplyOutput(operation, spaceId, environmentId) {
218
+ function buildApplyOutput(operation, spaceId, environmentId, host) {
218
219
  const items = operation.items ?? [];
219
220
  const componentItems = items.filter((i) => i.entityType === 'ComponentType');
220
221
  const tokenItems = items.filter((i) => i.entityType === 'DesignToken');
@@ -233,6 +234,7 @@ function buildApplyOutput(operation, spaceId, environmentId) {
233
234
  summary: operation.summary,
234
235
  componentTypes: countByAction(componentItems),
235
236
  designTokens: countByAction(tokenItems),
237
+ viewUrl: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId }),
236
238
  failures: items
237
239
  .filter((item) => item.status === 'failed')
238
240
  .map((item) => ({
@@ -529,7 +531,7 @@ export function registerApplyCommand(program) {
529
531
  die(`Error: ${e.message}`);
530
532
  throw e;
531
533
  }
532
- const summary = buildApplyOutput(operation, spaceId, environmentId);
534
+ const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
533
535
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
534
536
  process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
535
537
  return;
@@ -583,6 +585,7 @@ export function registerApplyCommand(program) {
583
585
  operation,
584
586
  spaceId,
585
587
  environmentId,
588
+ host: opts.host,
586
589
  }));
587
590
  resolvePromise();
588
591
  };
@@ -696,7 +699,7 @@ export function registerApplyCommand(program) {
696
699
  die(`Error: ${e.message}`);
697
700
  throw e;
698
701
  }
699
- const summary = buildApplyOutput(operation, spaceId, environmentId);
702
+ const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
700
703
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
701
704
  process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
702
705
  return;
@@ -762,6 +765,7 @@ export function registerApplyCommand(program) {
762
765
  operation,
763
766
  spaceId,
764
767
  environmentId,
768
+ host: opts.host,
765
769
  }));
766
770
  resolvePromise();
767
771
  };
@@ -27,6 +27,8 @@ interface ServerApplyDoneProps {
27
27
  operation: ApplyOperationResponse;
28
28
  spaceId: string;
29
29
  environmentId: string;
30
+ /** Configured API host (e.g. `api.contentful.com`). Used to derive the post-push webapp URL. */
31
+ host?: string;
30
32
  }
31
- export declare function ServerApplyDone({ operation, spaceId, environmentId }: ServerApplyDoneProps): React.ReactElement;
33
+ export declare function ServerApplyDone({ operation, spaceId, environmentId, host }: ServerApplyDoneProps): React.ReactElement;
32
34
  export {};
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { ServerPreviewView } from './ServerPreviewView.js';
4
+ import { buildPostPushUrl } from '../../lib/contentful-urls.js';
4
5
  export function ServerPreviewConfirm({ preview, spaceId, environmentId, breakingWithImpact, onConfirm, onCancel, }) {
5
6
  useInput((input, key) => {
6
7
  if (key.return)
@@ -24,14 +25,14 @@ export function ServerApplyProgress({ spaceId, environmentId, status, operationI
24
25
  });
25
26
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), status === 'applying' && _jsx(Text, { children: " Submitting apply request..." }), status === 'polling' && _jsxs(Text, { children: [" Operation ", operationId, " in progress..."] }), status === 'error' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [" \u2717 Error: ", error] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }))] }));
26
27
  }
27
- export function ServerApplyDone({ operation, spaceId, environmentId }) {
28
+ export function ServerApplyDone({ operation, spaceId, environmentId, host }) {
28
29
  useInput((input, key) => {
29
30
  if (key.escape || input === 'q') {
30
31
  process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
31
32
  }
32
33
  });
33
34
  const failures = (operation.items ?? []).filter((item) => item.status === 'failed');
34
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
35
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), operation.sys.status === 'succeeded' && operation.summary.succeeded > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " View your design system:" }), _jsxs(Text, { color: "cyan", children: [" ", buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
35
36
  }
36
37
  function formatItemError(error) {
37
38
  if (!error)
@@ -5,6 +5,11 @@ export type ExperiencesCredentials = {
5
5
  host?: string;
6
6
  agent?: string;
7
7
  agentModel?: string;
8
+ /** Feature 8: persisted custom prompt path for `analyze select-agent`. */
9
+ selectPromptPath?: string;
10
+ /** Feature 8: persisted custom prompt path for `generate components`. */
11
+ generatePromptPath?: string;
12
+ autoFilter?: boolean;
8
13
  };
9
14
  export declare function readExperiencesCredentials(): Promise<ExperiencesCredentials>;
10
15
  export declare function writeExperiencesCredentials(creds: ExperiencesCredentials): Promise<void>;