@contentful/experience-design-system-cli 2.11.4-dev-build-d47ab65.0 → 2.12.1-dev-build-589b615.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 (142) 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 +259 -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 +814 -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/push-decision-gate-helpers.d.ts +8 -0
  62. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  63. package/dist/src/import/tui/push-progress.d.ts +23 -0
  64. package/dist/src/import/tui/push-progress.js +14 -0
  65. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  66. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  67. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  68. package/dist/src/import/tui/run-teaser.js +9 -0
  69. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  70. package/dist/src/import/tui/runLivePreview.js +73 -0
  71. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  72. package/dist/src/import/tui/runScopeGate.js +28 -0
  73. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  74. package/dist/src/import/tui/scope-gate-host.js +20 -0
  75. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  76. package/dist/src/import/tui/spawn-generate.js +53 -0
  77. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  78. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  79. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  80. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  81. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  82. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  83. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  84. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  85. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  86. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  87. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  88. package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
  89. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  90. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  91. package/dist/src/import/tui/useLivePreview.js +120 -0
  92. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  93. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  94. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  95. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  96. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  97. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  98. package/dist/src/lib/contentful-urls.d.ts +34 -0
  99. package/dist/src/lib/contentful-urls.js +41 -0
  100. package/dist/src/program.js +2 -0
  101. package/dist/src/runs/export-helpers.d.ts +30 -0
  102. package/dist/src/runs/export-helpers.js +59 -0
  103. package/dist/src/runs/fingerprint.d.ts +47 -0
  104. package/dist/src/runs/fingerprint.js +75 -0
  105. package/dist/src/runs/ls-command.d.ts +12 -0
  106. package/dist/src/runs/ls-command.js +137 -0
  107. package/dist/src/runs/modify-launcher.d.ts +24 -0
  108. package/dist/src/runs/modify-launcher.js +36 -0
  109. package/dist/src/runs/path-prompt.d.ts +8 -0
  110. package/dist/src/runs/path-prompt.js +72 -0
  111. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  112. package/dist/src/runs/push-creds-prompt.js +39 -0
  113. package/dist/src/runs/push-helpers.d.ts +29 -0
  114. package/dist/src/runs/push-helpers.js +69 -0
  115. package/dist/src/runs/replay-helpers.d.ts +67 -0
  116. package/dist/src/runs/replay-helpers.js +148 -0
  117. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  118. package/dist/src/runs/resolve-run-target.js +45 -0
  119. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  120. package/dist/src/runs/run-picker-mount.js +72 -0
  121. package/dist/src/runs/run-picker.d.ts +17 -0
  122. package/dist/src/runs/run-picker.js +145 -0
  123. package/dist/src/runs/save-conflict.d.ts +8 -0
  124. package/dist/src/runs/save-conflict.js +56 -0
  125. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  126. package/dist/src/runs/save-path-resolver.js +71 -0
  127. package/dist/src/runs/staleness.d.ts +38 -0
  128. package/dist/src/runs/staleness.js +140 -0
  129. package/dist/src/runs/store.d.ts +78 -0
  130. package/dist/src/runs/store.js +136 -0
  131. package/dist/src/session/cache-keys.d.ts +15 -0
  132. package/dist/src/session/cache-keys.js +38 -0
  133. package/dist/src/session/db.d.ts +89 -3
  134. package/dist/src/session/db.js +447 -104
  135. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  136. package/dist/src/setup/auto-filter-prompt.js +24 -0
  137. package/dist/src/setup/command.d.ts +7 -0
  138. package/dist/src/setup/command.js +58 -2
  139. package/dist/src/types.d.ts +6 -0
  140. package/package.json +2 -2
  141. package/skills/generate-components.md +54 -6
  142. 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,183 @@ 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
+ results.push({
171
+ componentKey: componentKey(component),
172
+ componentName: component.name,
173
+ decision: null,
174
+ failed: true,
175
+ });
176
+ continue;
177
+ }
178
+ const decision = call.tool === 'select_component' ? 'accepted' : 'rejected';
179
+ const finalColor = decision === 'accepted' ? c.green : c.red;
180
+ process.stderr.write(` ${pos} ${c.bold(component.name)} ${finalColor(decision)}${call.reason ? ` ${c.dim(call.reason)}` : ''}\n`);
181
+ emitProgress(item, decision, call.reason);
182
+ results.push({
139
183
  componentKey: componentKey(component),
140
184
  componentName: component.name,
141
- decision: null,
142
- failed: true,
143
- };
185
+ decision,
186
+ reason: call.reason,
187
+ failed: false,
188
+ });
144
189
  }
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
- };
190
+ return results;
155
191
  }
156
- async function selectAllComponents(agent, model, components, verbose) {
192
+ async function selectAllComponents(agent, model, components, verbose, skillPathOverride, cacheConfig = { noCache: true }) {
157
193
  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})`) +
194
+ const batchSize = resolveBatchSize();
195
+ const total = components.length;
196
+ // ── Fine-grained select cache: replay decisions for components whose
197
+ // (component_hash, prompt_hash, cli_version) triple already has a row. Only
198
+ // uncached components are forwarded to the LLM batches.
199
+ const cachedResults = new Map();
200
+ let promptHash = '';
201
+ let cliVersion = '';
202
+ if (!cacheConfig.noCache) {
203
+ try {
204
+ promptHash = await hashPromptForSkill('select', skillPathOverride);
205
+ cliVersion = await getCliCacheVersion();
206
+ const db = openPipelineDb(cacheConfig.dbPath);
207
+ try {
208
+ for (let i = 0; i < components.length; i++) {
209
+ const candidate = components[i];
210
+ const compHash = computeComponentInputHash(candidate.component);
211
+ const hit = lookupSelectCache(db, compHash, promptHash, cliVersion);
212
+ if (hit) {
213
+ const finalColor = hit.decision === 'accepted' ? c.green : c.red;
214
+ process.stderr.write(` ${c.dim(`[${i + 1}/${total}]`)} ${c.bold(candidate.component.name)} ${finalColor(hit.decision)} ${c.dim('(cached)')}` +
215
+ (hit.reason ? ` ${c.dim(hit.reason)}` : '') +
216
+ '\n');
217
+ // Mirror the progress= line that selectBatch emits for live LLM
218
+ // calls so the wizard's parser sees a uniform stream.
219
+ const reasonEncoded = hit.reason ? encodeURIComponent(hit.reason) : '';
220
+ process.stderr.write(`progress=select-agent:${i + 1}/${total}:${hit.decision}:${candidate.component.name}:${reasonEncoded}\n`);
221
+ cachedResults.set(i, {
222
+ componentKey: componentKey(candidate.component),
223
+ componentName: candidate.component.name,
224
+ decision: hit.decision,
225
+ reason: hit.reason ?? undefined,
226
+ failed: false,
227
+ });
228
+ }
229
+ }
230
+ }
231
+ finally {
232
+ db.close();
233
+ }
234
+ }
235
+ catch {
236
+ // Cache prefill is best-effort. On any error, fall through and run the
237
+ // full LLM batch path.
238
+ }
239
+ }
240
+ // Build the work-set of components we still need to ask the LLM about.
241
+ const uncached = [];
242
+ const uncachedIndices = [];
243
+ for (let i = 0; i < components.length; i++) {
244
+ if (!cachedResults.has(i)) {
245
+ uncached.push(components[i]);
246
+ uncachedIndices.push(i);
247
+ }
248
+ }
249
+ // Chunk into batches of `batchSize`, preserving original indices for the
250
+ // per-component progress=select-agent: lines.
251
+ const batches = [];
252
+ for (let i = 0; i < uncached.length; i += batchSize) {
253
+ const slice = uncached.slice(i, i + batchSize);
254
+ batches.push(slice.map((candidate, j) => ({ candidate, index: uncachedIndices[i + j] })));
255
+ }
256
+ process.stderr.write(`Validating ${c.bold(String(uncached.length))} component${uncached.length === 1 ? '' : 's'}` +
257
+ (cachedResults.size > 0 ? c.dim(` (${cachedResults.size} cached)`) : '') +
258
+ c.dim(` (concurrency: ${concurrency}, batch: ${batchSize}, batches: ${batches.length})`) +
160
259
  '\n');
161
- const results = new Array(components.length);
162
- let next = 0;
260
+ const results = new Array(total);
261
+ for (const [i, r] of cachedResults)
262
+ results[i] = r;
263
+ let nextBatch = 0;
163
264
  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);
265
+ while (nextBatch < batches.length) {
266
+ const b = nextBatch++;
267
+ const batch = batches[b];
268
+ const batchResults = await selectBatch(agent, model, batch, total, verbose, skillPathOverride);
269
+ for (let k = 0; k < batch.length; k++) {
270
+ results[batch[k].index] = batchResults[k];
271
+ }
272
+ }
273
+ }
274
+ await Promise.all(Array.from({ length: Math.min(concurrency, batches.length) }, worker));
275
+ // Persist fresh decisions (skip cached + failed ones).
276
+ if (!cacheConfig.noCache && promptHash) {
277
+ try {
278
+ const db = openPipelineDb(cacheConfig.dbPath);
279
+ try {
280
+ for (let i = 0; i < results.length; i++) {
281
+ if (cachedResults.has(i))
282
+ continue;
283
+ const r = results[i];
284
+ if (!r || r.failed || !r.decision)
285
+ continue;
286
+ const candidate = components[i];
287
+ const compHash = computeComponentInputHash(candidate.component);
288
+ storeSelectCache(db, compHash, promptHash, cliVersion, r.decision, r.reason ?? null);
289
+ }
290
+ }
291
+ finally {
292
+ db.close();
293
+ }
294
+ }
295
+ catch {
296
+ // Best-effort.
167
297
  }
168
298
  }
169
- await Promise.all(Array.from({ length: Math.min(concurrency, components.length) }, worker));
170
299
  return results;
171
300
  }
172
301
  export function registerAnalyzeSelectAgentCommand(program) {
@@ -180,7 +309,29 @@ export function registerAnalyzeSelectAgentCommand(program) {
180
309
  .option('--verbose', 'Show full agent output including reasoning text')
181
310
  .option('--dry-run', 'Print the prompt for the first component without invoking the agent')
182
311
  .option('--exclude-invalid', 'Auto-reject components with validation errors instead of failing loud (LLM cannot fix structural issues)')
312
+ .option('--select-prompt-path <path>', 'Path to a custom .md skill prompt for select-agent (bypasses bundled prompt invariants)')
313
+ .option('--no-select-cache', 'Skip the per-component select cache and re-LLM every component')
314
+ .option('--no-cache', 'Skip ALL fine-grained caches (extract, select, generate)')
315
+ .option('--show-rationale', 'Read-only: print the AI rejection rationale persisted by a prior select-agent run and exit. ' +
316
+ 'No LLM call. Combine with --json for machine-readable output.')
317
+ .option('--json', 'When used with --show-rationale, emit a JSON array instead of a human-readable table')
183
318
  .action(async (opts) => {
319
+ // --show-rationale is a read-only branch that short-circuits the normal
320
+ // select-agent flow. It re-reads the rationale columns persisted by a
321
+ // prior run (raw_components.status + reject_reason) and prints them.
322
+ // No LLM call, no agent resolution, no validation gates.
323
+ if (opts.showRationale) {
324
+ try {
325
+ runShowRationale({ session: opts.session, json: opts.json });
326
+ return;
327
+ }
328
+ catch (err) {
329
+ const message = err instanceof Error ? err.message : String(err);
330
+ process.stderr.write(`Error: ${message}\n`);
331
+ process.exit(1);
332
+ return;
333
+ }
334
+ }
184
335
  const savedCreds = await readExperiencesCredentials();
185
336
  const agentName = opts.agent ?? savedCreds.agent;
186
337
  const model = opts.model ?? savedCreds.agentModel;
@@ -190,6 +341,24 @@ export function registerAnalyzeSelectAgentCommand(program) {
190
341
  return;
191
342
  }
192
343
  const agent = agentName;
344
+ // Feature 8: validate + announce custom prompt path before any heavy
345
+ // work. Flag wins over saved credentials.
346
+ const selectPromptPath = opts.selectPromptPath ?? savedCreds.selectPromptPath;
347
+ if (selectPromptPath) {
348
+ const resolvedPath = resolve(selectPromptPath);
349
+ const exists = await access(resolvedPath)
350
+ .then(() => true)
351
+ .catch(() => false);
352
+ if (!exists) {
353
+ process.stderr.write(`Error: custom prompt path not found: ${resolvedPath}\n`);
354
+ process.exit(1);
355
+ return;
356
+ }
357
+ if (!selectPromptPath.toLowerCase().endsWith('.md')) {
358
+ process.stderr.write(`WARNING: custom prompt path does not end in .md (${selectPromptPath}) — proceeding anyway.\n`);
359
+ }
360
+ process.stderr.write(formatCustomPromptBanner('select', resolvedPath));
361
+ }
193
362
  const sessionId = resolveSessionId(opts.session);
194
363
  const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
195
364
  const db = openPipelineDb();
@@ -265,19 +434,64 @@ export function registerAnalyzeSelectAgentCommand(program) {
265
434
  mode: 'autonomous',
266
435
  rawComponentsInline: JSON.stringify([buildComponentData(first)], null, 2),
267
436
  outDir: process.cwd(),
437
+ skillPathOverride: selectPromptPath ? resolve(selectPromptPath) : undefined,
268
438
  });
269
439
  process.stdout.write(prompt + '\n');
270
440
  process.exit(0);
271
441
  return;
272
442
  }
273
- const selectResults = await selectAllComponents(agent, model, selectionCandidates, opts.verbose ?? false);
443
+ // --no-cache is the global kill-switch; --no-select-cache is the stage-
444
+ // specific opt-out. Both Commander negation flags arrive as `false` when
445
+ // the user passed --no-*; default (undefined) means cache stays on.
446
+ const noCache = opts.cache === false || opts.selectCache === false || process.env.EDS_NO_CACHE === '1';
447
+ const selectResults = await selectAllComponents(agent, model, selectionCandidates, opts.verbose ?? false, selectPromptPath ? resolve(selectPromptPath) : undefined, { noCache });
274
448
  // Build decision map from results. Seed auto-rejections from validation exclusions first.
275
449
  const decisions = new Map();
450
+ // Track per-component reasons (for raw_components.reject_reason persistence).
451
+ // Validation auto-rejections get a synthesized "validation error: <codes>" reason
452
+ // so the scope-gate UI can distinguish wizard- vs LLM-driven exclusions.
453
+ const reasons = new Map();
276
454
  for (const comp of invalidComponents) {
277
455
  decisions.set(componentKey(comp), 'rejected');
456
+ const codes = (comp.validationIssues ?? [])
457
+ .filter((i) => i.severity === 'error')
458
+ .map((i) => i.code)
459
+ .join(', ');
460
+ reasons.set(componentKey(comp), `validation error: ${codes}`);
278
461
  }
279
462
  for (const r of selectResults) {
280
463
  decisions.set(r.componentKey, r.decision);
464
+ if (r.decision === 'rejected' && r.reason) {
465
+ reasons.set(r.componentKey, r.reason);
466
+ }
467
+ else if (r.decision === 'accepted') {
468
+ // Defensive: clear any previously-stored reason on re-runs that flip a
469
+ // rejected component to accepted.
470
+ reasons.set(r.componentKey, null);
471
+ }
472
+ }
473
+ // Feature 3: persist decisions to raw_components (status + reject_reason)
474
+ // so the wizard can render LLM exclusions in the scope-gate without re-reading
475
+ // the snapshot file. We open a fresh DB handle here (the earlier one was closed
476
+ // after loading raw components above) and reuse it for the step-recording below.
477
+ const persistDb = openPipelineDb();
478
+ try {
479
+ const updateStmt = persistDb.prepare(`UPDATE raw_components
480
+ SET status = ?, reject_reason = ?
481
+ WHERE session_id = ? AND component_id = ?`);
482
+ for (const comp of validatedComponents) {
483
+ const key = componentKey(comp);
484
+ const decision = decisions.get(key);
485
+ if (!decision)
486
+ continue;
487
+ if (!comp.component_id)
488
+ continue;
489
+ const reason = reasons.get(key) ?? null;
490
+ updateStmt.run(decision, decision === 'accepted' ? null : reason, sessionId, comp.component_id);
491
+ }
492
+ }
493
+ finally {
494
+ persistDb.close();
281
495
  }
282
496
  const artifactsRoot = getRefineArtifactsRoot();
283
497
  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>;