@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,599 +0,0 @@
1
- import { createElement } from 'react';
2
- import { render } from 'ink';
3
- import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
4
- import { isAbsolute, join, relative, resolve } from 'node:path';
5
- import { addAgentModelOptions } from '../lib/agent-model-options.js';
6
- import { extractComponents, preClassifyComponent, isNonAuthorableComponent, computeExtractionScore, deriveNeedsReview, describeReviewReasons, inspectComponentSource, validateExtractedComponents, } from '@contentful/experience-design-system-extraction';
7
- import { AnalyzeView } from './tui/AnalyzeView.js';
8
- import { registerAnalyzeEditCommand } from './select/command.js';
9
- import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
10
- import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, storeSlotCycles, getCliCacheVersion, lookupCompositionCache, storeCompositionCache, } from '../session/db.js';
11
- import { findSlotCycles, suggestCycleBreakEdge } from './cycle-detection.js';
12
- import { resolveCompositionMode } from '../lib/composition-mode.js';
13
- import { resolveMapping } from './composition/resolve-mapping.js';
14
- import { loadUserMap, resolveCompositionSources } from './composition/resolve-mapping-cli.js';
15
- import { selectCandidateFiles, capCandidatesToPromptBudget } from './composition/candidate-files.js';
16
- import { critiqueCandidates } from './composition/candidate-critic.js';
17
- import { buildDirCriticPrompt, parseDirCriticReply } from './composition/candidate-critic-agent.js';
18
- import { buildCompositionInputHash } from './composition/composition-cache-key.js';
19
- import { collectManifestDocEdges } from './composition/manifest-doc-evidence.js';
20
- import { runParserInSandbox } from './composition/agent-parser/sandbox.js';
21
- import { resolveViaAgentParser } from './composition/agent-parser/resolve-via-parser.js';
22
- import { parsePromptOverrides, resolvePromptOverride } from '../lib/prompt-overrides.js';
23
- import { DEFAULT_AGENT_NAME, isAgentName, runAgent, } from '@contentful/experience-design-system-generation';
24
- import { readExperiencesCredentials } from '../credentials-store.js';
25
- import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
26
- import { getInteractiveTerminalSupport } from '../lib/terminal-capabilities.js';
27
- import { bindAnalyticsSessionId, emitSessionStarted, enrichCommandResult, exitWithAnalytics, isPipelineAnalyticsChild, } from '../analytics/index.js';
28
- import { getDebugLogger } from '../lib/debug-logger.js';
29
- const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
30
- /**
31
- * `.json`/`.md` are scanned too (Figma `manifest.json`, `AGENTS.md`-style
32
- * docs, and other design/composition-adjacent files we don't yet have a name
33
- * for) — gated by a denylist rather than an allowlist, so coverage isn't
34
- * capped at a couple of exact filenames. Their content is never inlined into
35
- * an LLM prompt by virtue of being scanned here; that's a separate gate (see
36
- * `selectCandidateFiles` in candidate-files.ts) which still only admits files
37
- * matching its own name/content-marker heuristics. Deterministic parsing
38
- * (manifest-doc-evidence.ts) reads this full set directly, with no LLM
39
- * involved, which is the actual prompt-injection safeguard for that signal.
40
- */
41
- const DENYLIST_GATED_EXTENSIONS = new Set(['.json', '.md']);
42
- const DENYLISTED_EXACT_FILE_NAMES = new Set([
43
- 'package.json',
44
- 'package-lock.json',
45
- 'npm-shrinkwrap.json',
46
- 'nx.json',
47
- 'project.json',
48
- 'turbo.json',
49
- 'lerna.json',
50
- 'jsconfig.json',
51
- ]);
52
- /** Config-file families that vary by suffix (`tsconfig.build.json`, `.eslintrc.cjs.json`, ...) plus common repo docs. */
53
- const DENYLISTED_FILE_NAME_PATTERNS = [
54
- /^tsconfig(\..+)?\.json$/,
55
- /^\.?eslintrc(\..+)?\.json$/,
56
- /^\.?prettierrc(\..+)?\.json$/,
57
- /^(readme|changelog|contributing|code_of_conduct|license|security)(\..+)?\.md$/i,
58
- ];
59
- function isDenylistedNoiseFile(name) {
60
- return DENYLISTED_EXACT_FILE_NAMES.has(name) || DENYLISTED_FILE_NAME_PATTERNS.some((pattern) => pattern.test(name));
61
- }
62
- const IGNORED_DIRECTORY_NAMES = new Set([
63
- '.changeset',
64
- '.git',
65
- '.github',
66
- '.idea',
67
- '.next',
68
- '.nuxt',
69
- '.vscode',
70
- 'build',
71
- 'coverage',
72
- 'demo',
73
- 'demos',
74
- 'dist',
75
- 'example',
76
- 'examples',
77
- 'node_modules',
78
- 'out',
79
- 'storybook-static',
80
- ]);
81
- const IGNORED_FILE_SUFFIXES = new Set([
82
- '.stories.ts',
83
- '.stories.tsx',
84
- '.stories.js',
85
- '.stories.jsx',
86
- '.story.ts',
87
- '.story.tsx',
88
- '.story.js',
89
- '.story.jsx',
90
- '.spec.ts',
91
- '.spec.tsx',
92
- '.test.ts',
93
- '.test.tsx',
94
- ]);
95
- function pluralize(count, singular, plural = `${singular}s`) {
96
- return `${count} ${count === 1 ? singular : plural}`;
97
- }
98
- function resolveFromProjectRoot(projectRoot, inputPath) {
99
- return isAbsolute(inputPath) ? inputPath : resolve(projectRoot, inputPath);
100
- }
101
- function wrapperConfidenceToIssueCount(confidence) {
102
- if (confidence >= 4)
103
- return 2;
104
- if (confidence === 3)
105
- return 1;
106
- return 0;
107
- }
108
- async function pathExists(path) {
109
- return Boolean(await stat(path).catch(() => null));
110
- }
111
- export async function collectSourceFiles(directory, onProgress) {
112
- const files = [];
113
- async function visit(currentDirectory) {
114
- const entries = await readdir(currentDirectory, { withFileTypes: true });
115
- const subdirs = [];
116
- for (const entry of entries) {
117
- const fullPath = join(currentDirectory, entry.name);
118
- if (entry.isDirectory()) {
119
- if (!IGNORED_DIRECTORY_NAMES.has(entry.name)) {
120
- subdirs.push(fullPath);
121
- }
122
- continue;
123
- }
124
- if (!entry.isFile()) {
125
- continue;
126
- }
127
- const extension = entry.name.slice(entry.name.lastIndexOf('.'));
128
- const isCodeFile = SCANNED_FILE_EXTENSIONS.has(extension) && !entry.name.endsWith('.d.ts');
129
- const isNoiseGatedFile = DENYLIST_GATED_EXTENSIONS.has(extension) && !isDenylistedNoiseFile(entry.name);
130
- if (!isCodeFile && !isNoiseGatedFile) {
131
- continue;
132
- }
133
- if ([...IGNORED_FILE_SUFFIXES].some((suffix) => entry.name.endsWith(suffix))) {
134
- continue;
135
- }
136
- files.push(fullPath);
137
- onProgress?.(files.length);
138
- }
139
- await Promise.all(subdirs.map((subdir) => visit(subdir)));
140
- }
141
- await visit(directory);
142
- return files.sort();
143
- }
144
- /** Read the persisted default composition mode; missing config is fine. */
145
- async function safeReadCompositionMode() {
146
- try {
147
- return (await readExperiencesCredentials()).compositionMode;
148
- }
149
- catch {
150
- return undefined;
151
- }
152
- }
153
- /**
154
- * Build a { version, groups } interchange skeleton (spec T1) from the resolved
155
- * components' slot allowedComponents — reflecting BOTH typed-slot edges the
156
- * extractor found and anything the mapping resolver added.
157
- */
158
- export function componentsToInterchangeMap(components) {
159
- const groups = {};
160
- for (const c of components) {
161
- const children = new Set();
162
- for (const slot of c.slots) {
163
- for (const child of slot.allowedComponents ?? [])
164
- children.add(child);
165
- }
166
- if (children.size > 0)
167
- groups[c.name] = [...children].sort();
168
- }
169
- const sorted = {};
170
- for (const parent of Object.keys(groups).sort())
171
- sorted[parent] = groups[parent];
172
- return { version: 1, groups: sorted };
173
- }
174
- /** Resolve which coding-agent runs mapping resolution: `--agent` flag > env > default. */
175
- function resolveCompositionAgentName(flagValue) {
176
- if (flagValue && isAgentName(flagValue))
177
- return flagValue;
178
- const env = process.env['EDS_COMPOSITION_AGENT'];
179
- if (env && isAgentName(env))
180
- return env;
181
- return DEFAULT_AGENT_NAME;
182
- }
183
- /**
184
- * Read the source files of the extracted components plus nearby mapping/meta
185
- * files, so the candidate pre-filter (T3) can pick the relevant ones. Reads
186
- * each unique `sourcePath` once; missing files are skipped.
187
- */
188
- async function readCandidateFiles(components, extraFiles = []) {
189
- const paths = new Set(extraFiles);
190
- for (const c of components) {
191
- if (c.sourcePath)
192
- paths.add(c.sourcePath);
193
- }
194
- const out = [];
195
- await Promise.all([...paths].map(async (p) => {
196
- try {
197
- const content = await readFile(p, 'utf8');
198
- out.push({ path: p, content });
199
- }
200
- catch {
201
- void 0;
202
- }
203
- }));
204
- return out;
205
- }
206
- export function registerAnalyzeCommand(program) {
207
- const analyze = program
208
- .command('analyze')
209
- .description('Extract component definitions from a project, or correct analysis output');
210
- const extractCmd = analyze
211
- .command('extract')
212
- .description('Extract component definitions from a project')
213
- .requiredOption('--project <path>', 'Path to the project root')
214
- .option('--dir <path>', 'Path to the component source directory relative to the project root')
215
- .option('--resolve-unreachable <mode>', "Retry pass for unresolved Svelte Props types: 'auto' (default), 'always', or 'never'", 'auto')
216
- .option('--composite', 'Resolve embedded-component composition (opt in; default is atomic)')
217
- .option('--atomic', 'Skip composition resolution — flat components only (default)')
218
- .option('--composition-map <path>', 'Consume a hand-authored parent→children interchange map (implies --composite)')
219
- .option('--composition-agent', 'Opt into agentic mapping resolution when deterministic sources find no groups (implies --composite)')
220
- .option('--composition-refresh', 'Force the mapping agent to run even where deterministic sources answered (implies --composite)')
221
- .option('--generate-map <path>', 'Write a skeleton interchange map from resolved composition (implies --composite)')
222
- .option('--prompt <stage=value>', 'Override a stage prompt (repeatable). value is a file path or literal text, e.g. --prompt composition=./p.md', (v, acc) => [...acc, v], []);
223
- addAgentModelOptions(extractCmd, {
224
- includeModel: false,
225
- agentDescription: 'Coding agent for composition mapping resolution (claude|codex|opencode|cursor)',
226
- })
227
- .option('--composition-agent-mode <mode>', "Agent resolution mode: 'parser' (agent writes a sandboxed parser — deterministic, default) or 'edges' (agent lists edges directly)", 'parser')
228
- .action(async (opts) => {
229
- const resolveUnreachable = (() => {
230
- const v = opts.resolveUnreachable ?? 'auto';
231
- if (v !== 'auto' && v !== 'always' && v !== 'never') {
232
- process.stderr.write(`Error: --resolve-unreachable must be one of 'auto', 'always', 'never' (got '${v}')\n`);
233
- process.exit(1);
234
- }
235
- return v;
236
- })();
237
- const projectRoot = resolve(opts.project);
238
- const outDir = join(projectRoot, '.contentful');
239
- let sourceDirectory;
240
- if (opts.dir !== undefined) {
241
- sourceDirectory = resolveFromProjectRoot(projectRoot, opts.dir);
242
- if (!(await pathExists(sourceDirectory))) {
243
- process.stderr.write(`Error: source directory does not exist: ${sourceDirectory}\n`);
244
- process.exit(1);
245
- }
246
- }
247
- else {
248
- const srcPath = resolveFromProjectRoot(projectRoot, 'src');
249
- sourceDirectory = (await pathExists(srcPath)) ? srcPath : projectRoot;
250
- }
251
- const sourceFiles = await collectSourceFiles(sourceDirectory, (count) => {
252
- if (!process.stdout.isTTY) {
253
- process.stderr.write(`progress=scan:${count}\n`);
254
- }
255
- });
256
- const extraction = await extractComponents(sourceFiles, ({ filesProcessed, componentsFound }) => {
257
- if (!process.stdout.isTTY) {
258
- process.stderr.write(`progress=extract:${filesProcessed}/${sourceFiles.length}:${componentsFound}\n`);
259
- }
260
- }, { resolveUnreachable, projectRoot });
261
- await mkdir(outDir, { recursive: true });
262
- const db = openPipelineDb();
263
- const { sessionId } = getOrCreateSession(db, undefined, undefined, {
264
- command: 'analyze extract',
265
- inputPath: projectRoot,
266
- outDir,
267
- });
268
- await bindAnalyticsSessionId(sessionId);
269
- if (!isPipelineAnalyticsChild()) {
270
- await emitSessionStarted('analyze_extract');
271
- }
272
- const stepId = createStep(db, sessionId, 'analyze extract', {
273
- project: projectRoot,
274
- });
275
- const classifiedComponents = extraction.components.map(preClassifyComponent);
276
- const inspectedComponents = await Promise.all(classifiedComponents.map(async (component) => ({
277
- component,
278
- inspection: await inspectComponentSource(component),
279
- })));
280
- const filteredComponents = [];
281
- const filterWarnings = [];
282
- for (const { component, inspection } of inspectedComponents) {
283
- const verdict = isNonAuthorableComponent(component);
284
- const keepDespiteZeroSurface = verdict.skip && verdict.reason === 'component has no props and no slots' && inspection.keepDespiteZeroSurface;
285
- const retainedForReview = verdict.skip && !keepDespiteZeroSurface;
286
- if (retainedForReview) {
287
- filterWarnings.push(`${component.name}: requires operator review (${verdict.reason})`);
288
- }
289
- if (keepDespiteZeroSurface) {
290
- filterWarnings.push(`${component.name}: retained despite 0 props/slots because the source renders visible or compositional UI`);
291
- }
292
- if (inspection.reviewReasons.length > 0) {
293
- const reviewNotes = describeReviewReasons(inspection.reviewReasons)
294
- .filter((note) => note !== 'high-confidence data-fetch wrapper')
295
- .join('; ');
296
- if (reviewNotes) {
297
- filterWarnings.push(`${component.name}: ${reviewNotes}`);
298
- }
299
- }
300
- // Preserve any extractor-level review reasons (e.g. `props-type-unresolved`
301
- // from the Svelte parser) by merging them into the post-processing recompute.
302
- // Without this, recomputing here clobbers the per-extractor signal.
303
- const extractorReasons = component.reviewReasons ?? [];
304
- const nonAuthorableReason = retainedForReview ? [`non-authorable:${verdict.reason}`] : [];
305
- const { confidence, reasons } = computeExtractionScore(component, {
306
- additionalIssueCount: wrapperConfidenceToIssueCount(inspection.wrapperConfidence) +
307
- extractorReasons.length +
308
- nonAuthorableReason.length,
309
- additionalReasons: [...extractorReasons, ...inspection.reviewReasons, ...nonAuthorableReason],
310
- });
311
- filteredComponents.push({
312
- ...component,
313
- extractionConfidence: confidence,
314
- reviewReasons: reasons,
315
- needsReview: deriveNeedsReview(confidence) ||
316
- inspection.wrapperConfidence >= 4 ||
317
- inspection.keepDespiteZeroSurface ||
318
- retainedForReview ||
319
- // An extractor-level type-resolution failure is a strong signal regardless
320
- // of the otherwise-derived confidence threshold; force review.
321
- extractorReasons.includes('props-type-unresolved') ||
322
- (component.needsReview ?? false),
323
- });
324
- }
325
- let validatedComponents = validateExtractedComponents(filteredComponents);
326
- // Composition mapping resolution (spec U2). Only in composite mode and
327
- // only when a source is provided (user map / agent opt-in).
328
- // Atomic (default) never resolves — it would only be stripped later.
329
- const compositionMode = resolveCompositionMode(opts, (await safeReadCompositionMode()) ?? undefined);
330
- if (compositionMode === 'composite') {
331
- const sources = resolveCompositionSources(opts);
332
- const { overrides: promptOverrides, errors: promptErrors } = parsePromptOverrides(opts.prompt ?? []);
333
- for (const err of promptErrors) {
334
- process.stderr.write(`Error: ${err}\n`);
335
- process.exit(1);
336
- }
337
- let compositionPrompt;
338
- const compositionOverride = promptOverrides.get('composition');
339
- if (compositionOverride) {
340
- try {
341
- compositionPrompt = await resolvePromptOverride(compositionOverride);
342
- }
343
- catch (e) {
344
- process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
345
- process.exit(1);
346
- }
347
- }
348
- let userMap;
349
- if (opts.compositionMap) {
350
- const loaded = await loadUserMap(opts.compositionMap);
351
- if (!loaded.ok) {
352
- process.stderr.write(`Error: ${loaded.error}\n`);
353
- process.exit(1);
354
- }
355
- userMap = loaded.map;
356
- }
357
- const hasSource = !!userMap || sources.useAgent || sources.forceAgent;
358
- if (hasSource || opts.generateMap) {
359
- // Composition progress mirrors the scan/extract progress convention:
360
- // emit `progress=composition:<phase>` on stderr so the wizard can
361
- // render a second progress line during the (potentially slow, agent-
362
- // backed) resolution instead of appearing frozen.
363
- const emitCompositionProgress = (phase) => {
364
- if (!process.stdout.isTTY)
365
- process.stderr.write(`progress=composition:${phase}\n`);
366
- };
367
- emitCompositionProgress('resolving');
368
- const allFiles = await readCandidateFiles(validatedComponents, sourceFiles);
369
- // Decouple the two file sets (design: candidate-heuristic fragility):
370
- // - `promptFiles`: a bounded candidate SAMPLE inlined into the agent
371
- // prompt so it sees the convention without ingesting the whole repo.
372
- // - `allFiles`: EVERY scanned file, handed to the authored parser at
373
- // runtime. The parser is deterministic code — give it everything so
374
- // a candidate-filter miss can never starve it of a definition file.
375
- const selectedCandidates = selectCandidateFiles(allFiles).map((c) => ({ path: c.path, content: c.content }));
376
- // Cap the inlined set so a large design system can't overflow the
377
- // agent's context window and fail resolution outright (see the budget
378
- // constant). The runtime parser still sees EVERY file.
379
- const capped = capCandidatesToPromptBudget(selectedCandidates);
380
- let promptFiles = capped.kept;
381
- if (capped.dropped.length > 0) {
382
- process.stderr.write(`Warning: composition — ${capped.dropped.length} candidate file(s) omitted from the agent prompt to fit the context budget; resolution runs on the ${promptFiles.length} highest-value files.\n`);
383
- }
384
- const runtimeFiles = allFiles.map((c) => ({ path: c.path, content: c.content }));
385
- const resolverAgent = resolveCompositionAgentName(opts.agent);
386
- const parserMode = (opts.compositionAgentMode ?? 'parser') !== 'edges';
387
- const componentNameSet = new Set(validatedComponents.map((c) => c.name));
388
- const cacheVersion = await getCliCacheVersion();
389
- // Tracks the exit code of the most recent spawnAgent call so the
390
- // caching sites can refuse to persist a failed run (a non-zero exit —
391
- // e.g. a context-window overflow — otherwise poisons the cache and
392
- // replays the error on every subsequent run).
393
- let lastAgentExitCode = 0;
394
- const spawnAgent = async (prompt) => {
395
- const res = await runAgent({
396
- agent: resolverAgent,
397
- prompt,
398
- timeoutMs: 120_000,
399
- promptViaStdin: true,
400
- onDebugEvent: (name, payload) => getDebugLogger().event('agent', name, payload),
401
- });
402
- lastAgentExitCode = res.exitCode;
403
- if (res.exitCode !== 0 && res.stderr.trim()) {
404
- process.stderr.write(`Warning: composition — agent exited ${res.exitCode}: ${res.stderr.trim()}\n`);
405
- }
406
- return res.stdout;
407
- };
408
- // Completeness critic: let the agent flag composition-relevant dirs
409
- // the keyword filter missed (by path/name alone — cheap). It can only
410
- // ADD to the prompt sample. Runs ONLY on a genuine authoring pass (see
411
- // the cache-miss branch below) so a parser cache hit pays nothing.
412
- const runCandidateCritic = async () => {
413
- const critic = await critiqueCandidates(runtimeFiles, promptFiles, async (dirs) => parseDirCriticReply(await spawnAgent(buildDirCriticPrompt(dirs)), dirs));
414
- if (critic.addedDirs.length > 0) {
415
- promptFiles = critic.files;
416
- if (process.env['EDS_DEBUG']) {
417
- process.stderr.write(`[composition-debug] critic added dirs: ${critic.addedDirs.join(', ')}\n`);
418
- }
419
- }
420
- };
421
- // Agent-authored parser path (default): the agent writes a sandboxed
422
- // (ctx) => Edge[] parser we run deterministically, cached by parser
423
- // SOURCE. Falls back to direct edge-emission if authoring fails.
424
- let parserEdges;
425
- if (sources.useAgent && parserMode) {
426
- const parserCacheKey = buildCompositionInputHash({
427
- files: runtimeFiles,
428
- agent: resolverAgent,
429
- kind: 'parser',
430
- });
431
- const cachedSource = opts.compositionRefresh
432
- ? null
433
- : lookupCompositionCache(db, parserCacheKey, cacheVersion);
434
- if (cachedSource !== null) {
435
- emitCompositionProgress('cache-hit');
436
- const ran = await runParserInSandbox(cachedSource, {
437
- files: runtimeFiles,
438
- componentNames: [...componentNameSet],
439
- });
440
- if (!ran.error) {
441
- parserEdges = ran.edges.filter((e) => e.parent !== e.child && componentNameSet.has(e.parent) && componentNameSet.has(e.child));
442
- }
443
- }
444
- if (parserEdges === undefined) {
445
- await runCandidateCritic();
446
- const pr = await resolveViaAgentParser({
447
- files: promptFiles,
448
- runtimeFiles,
449
- componentNames: componentNameSet,
450
- runAgentFn: ({ prompt }) => spawnAgent(prompt),
451
- ...(compositionPrompt ? { instructionOverride: compositionPrompt } : {}),
452
- onPhase: (phase) => emitCompositionProgress(phase),
453
- // The candidate filter selected these files because they carry
454
- // composition markers, so a clean 0-edge parse is suspicious —
455
- // let the resolver spend its one repair round on it.
456
- retryOnEmpty: promptFiles.length > 0,
457
- });
458
- for (const w of pr.warnings)
459
- process.stderr.write(`Warning: composition — ${w}\n`);
460
- if (!pr.usedFallback) {
461
- parserEdges = pr.edges;
462
- if (pr.parserSource && lastAgentExitCode === 0) {
463
- storeCompositionCache(db, parserCacheKey, cacheVersion, pr.parserSource);
464
- }
465
- }
466
- else {
467
- process.stderr.write('Warning: composition — parser mode failed; falling back to edge emission\n');
468
- }
469
- }
470
- }
471
- if (process.env['EDS_DEBUG'] && parserEdges) {
472
- process.stderr.write(`[composition-debug] prompt files: ${promptFiles.length}; runtime files: ${runtimeFiles.length}; componentNames: ${componentNameSet.size}; parser edges: ${parserEdges.length}\n`);
473
- for (const e of parserEdges)
474
- process.stderr.write(`[composition-debug] edge ${e.parent} -> ${e.child}\n`);
475
- }
476
- // Manifest (Figma `manifest.json`)/doc (`AGENTS.md`) evidence — rank
477
- // 4/5, deterministic (no LLM), runs over the FULL file set
478
- // regardless of composition mode/agent settings since it's cheap
479
- // and code/design-adjacent rather than agent-derived.
480
- const manifestDocEdges = collectManifestDocEdges(runtimeFiles, validatedComponents, componentNameSet);
481
- const extraEdges = [...(parserEdges ?? []), ...manifestDocEdges];
482
- // Edge-emission cache (used for both explicit edges-mode and the
483
- // parser-mode fallback). Keyed on prompt files + agent identity — the
484
- // agent emits edges directly from what it reads in the prompt.
485
- const agentCacheKey = buildCompositionInputHash({
486
- files: promptFiles,
487
- agent: resolverAgent,
488
- kind: 'edges',
489
- });
490
- const useEdgeEmission = sources.useAgent && parserEdges === undefined;
491
- const result = await resolveMapping({
492
- components: validatedComponents,
493
- ...(userMap ? { userMap } : {}),
494
- ...(extraEdges.length > 0 ? { extraEdges } : {}),
495
- useAgent: useEdgeEmission,
496
- forceAgent: sources.forceAgent && useEdgeEmission,
497
- files: promptFiles,
498
- ...(compositionPrompt ? { promptOverride: compositionPrompt } : {}),
499
- runAgentFn: async ({ prompt }) => {
500
- if (!opts.compositionRefresh) {
501
- const cached = lookupCompositionCache(db, agentCacheKey, cacheVersion);
502
- if (cached !== null) {
503
- emitCompositionProgress('cache-hit');
504
- return cached;
505
- }
506
- }
507
- emitCompositionProgress(`agent:${resolverAgent}`);
508
- const stdout = await spawnAgent(prompt);
509
- if (lastAgentExitCode === 0) {
510
- storeCompositionCache(db, agentCacheKey, cacheVersion, stdout);
511
- }
512
- return stdout;
513
- },
514
- });
515
- emitCompositionProgress('done');
516
- for (const w of result.warnings)
517
- process.stderr.write(`Warning: composition — ${w}\n`);
518
- for (const c of result.conflicts) {
519
- process.stderr.write(`Warning: composition conflict on ${c.parent}→${c.child}: kept ${c.winner}, dropped ${c.loser}\n`);
520
- }
521
- validatedComponents = result.components;
522
- if (opts.generateMap) {
523
- // Reflect the FULL resolved composition — typed-slot edges already
524
- // on the extracted components PLUS anything the resolver added — not
525
- // just the resolver's own contributed edges.
526
- const skeleton = componentsToInterchangeMap(validatedComponents);
527
- await writeFile(opts.generateMap, JSON.stringify(skeleton, null, 2) + '\n');
528
- process.stderr.write(`Wrote composition map skeleton to ${opts.generateMap}\n`);
529
- }
530
- }
531
- }
532
- storeRawComponents(db, sessionId, validatedComponents);
533
- const cycleInput = validatedComponents.map((c) => ({
534
- name: c.name,
535
- slots: c.slots.map((s) => ({ name: s.name, allowedComponents: s.allowedComponents })),
536
- }));
537
- const cycles = findSlotCycles(cycleInput);
538
- const withBreaks = cycles.map((cycle) => ({
539
- ...cycle,
540
- suggestedBreak: suggestCycleBreakEdge(cycle, cycles),
541
- }));
542
- storeSlotCycles(db, sessionId, withBreaks);
543
- storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
544
- updateStep(db, stepId, 'complete', { sessionId });
545
- enrichCommandResult({ extracted_component_count: validatedComponents.length });
546
- db.close();
547
- const allWarnings = [...extraction.warnings, ...filterWarnings];
548
- const { rows: componentRows, totalErrors } = buildAnalyzeViewRows(filteredComponents, validatedComponents, allWarnings);
549
- // Split warnings: per-component (those whose prefix matches a surviving component name)
550
- // are rendered under that component in the TUI; global ones (retry summaries,
551
- // non-authorable skips, anything else) are rendered at the top of the warnings panel
552
- // so they don't disappear into the count. `partitionGlobalWarnings` shares its
553
- // matching rule with `buildAnalyzeViewRows` to keep the two halves symmetric.
554
- const globalWarnings = partitionGlobalWarnings(allWarnings, componentRows.map((r) => r.name));
555
- const analyzeResult = {
556
- sourceDirectory,
557
- sessionId,
558
- fileCount: sourceFiles.length,
559
- components: componentRows,
560
- totalWarnings: allWarnings.length,
561
- totalErrors,
562
- globalWarnings,
563
- };
564
- if (getInteractiveTerminalSupport().supported) {
565
- const { waitUntilExit } = render(createElement(AnalyzeView, {
566
- result: analyzeResult,
567
- onExit: () => void exitWithAnalytics(0),
568
- }));
569
- await waitUntilExit();
570
- }
571
- else {
572
- const sessionLine = `session=${sessionId}\n`;
573
- process.stdout.write(sessionLine);
574
- const summaryLines = [
575
- `Scanned ${pluralize(sourceFiles.length, 'source file')} in ${sourceDirectory}`,
576
- `Extracted ${pluralize(extraction.components.length, 'component')}`,
577
- ];
578
- if (totalErrors > 0) {
579
- summaryLines.push(`Errors (${totalErrors}):`);
580
- for (const c of componentRows) {
581
- for (const e of c.errors) {
582
- summaryLines.push(`- ${c.name}: ${e}`);
583
- }
584
- }
585
- }
586
- if (allWarnings.length > 0) {
587
- summaryLines.push(`Warnings (${allWarnings.length}):`);
588
- summaryLines.push(...allWarnings.map((w) => `- ${w}`));
589
- }
590
- else if (totalErrors === 0) {
591
- summaryLines.push('Warnings: none');
592
- }
593
- process.stderr.write(summaryLines.join('\n') + '\n');
594
- await exitWithAnalytics(0);
595
- }
596
- });
597
- registerAnalyzeEditCommand(analyze);
598
- registerAnalyzeSelectAgentCommand(analyze);
599
- }
@@ -1,22 +0,0 @@
1
- import { loadPrompt } from './load-prompt.js';
2
- /**
3
- * Build the prompt that asks the agent to WRITE a pure parser function (spec:
4
- * agent-authored-parser, Phase 2) rather than list edges. The static
5
- * instruction + contract live in `prompts/composition-parser.md`; this
6
- * appends the dynamic candidate files + component names. The authored parser
7
- * runs in the sandbox (sandbox.ts), so the contract forbids all I/O.
8
- */
9
- const PROMPT_FILE = 'composition-parser.md';
10
- export function buildAuthorPrompt(files, componentNames, instructionOverride) {
11
- const fileBlocks = files.map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');
12
- const instruction = instructionOverride?.trim() ? instructionOverride.trim() : loadPrompt(PROMPT_FILE).trim();
13
- return [
14
- instruction,
15
- '',
16
- 'Component names (use ONLY these):',
17
- componentNames.join(', '),
18
- '',
19
- 'Candidate files:',
20
- fileBlocks,
21
- ].join('\n');
22
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Pull the parser function source out of the agent's stdout (spec:
3
- * agent-authored-parser, Phase 2). Agents typically wrap code in a fenced
4
- * block, but often emit a non-parser block first (a JSON sample, prose). We
5
- * return the first block that is actually SHAPED like the contract — a
6
- * default-exported function taking one param — so a stray sample never reaches
7
- * the sandbox. Lenient by design: no parser-shaped source → null → caller
8
- * falls back.
9
- */
10
- const FENCE = /```(?:[a-zA-Z]+)?\n([\s\S]*?)```/g;
11
- /**
12
- * Cheap structural check that source matches the parser contract:
13
- * `export default` of a function (declaration or arrow) that takes at least
14
- * one parameter. Not a full parse — just enough to reject JSON samples, prose,
15
- * and no-arg functions before we spend a sandbox spawn on them.
16
- */
17
- export function looksLikeParser(source) {
18
- const s = source.trim();
19
- if (!/export\s+default\b/.test(s))
20
- return false;
21
- // export default function [name] (param...) { ... }
22
- const fnDecl = /export\s+default\s+(?:async\s+)?function\b[^(]*\(\s*[A-Za-z_$][\w$]*/;
23
- // export default (param...) => ... OR export default param => ...
24
- const arrow = /export\s+default\s+(?:async\s+)?(?:\(\s*[A-Za-z_$][\w$]*|[A-Za-z_$][\w$]*\s*=>)/;
25
- return fnDecl.test(s) || arrow.test(s);
26
- }
27
- export function extractParserSource(stdout) {
28
- FENCE.lastIndex = 0;
29
- let m;
30
- while ((m = FENCE.exec(stdout)) !== null) {
31
- const candidate = m[1].trim();
32
- if (candidate !== '' && looksLikeParser(candidate))
33
- return candidate;
34
- }
35
- // No parser-shaped fenced block — accept bare source only if it, too, is
36
- // shaped like the contract.
37
- const idx = stdout.indexOf('export default');
38
- if (idx !== -1) {
39
- const bare = stdout.slice(idx).trim();
40
- if (looksLikeParser(bare))
41
- return bare;
42
- }
43
- return null;
44
- }