@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,966 +0,0 @@
1
- import { execFile, spawn } from 'node:child_process';
2
- import { appendFile, readFile, access } from 'node:fs/promises';
3
- import { join, dirname } from 'node:path';
4
- import { homedir } from 'node:os';
5
- import { fileURLToPath } from 'node:url';
6
- import { createInterface } from 'node:readline';
7
- import { promisify } from 'node:util';
8
- import { readExperiencesCredentials, writeExperiencesCredentials, experiencesCredentialsPath, } from '../credentials-store.js';
9
- import { promptAutoFilterPreference } from './auto-filter-prompt.js';
10
- import { promptDebugModePreference } from './debug-mode-prompt.js';
11
- import { promptAnalyticsPreference } from './analytics-prompt.js';
12
- import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
13
- const execFileAsync = promisify(execFile);
14
- const REQUIRED_NODE_MAJOR = 24;
15
- // ── Output helpers ────────────────────────────────────────────────────────────
16
- function ok(msg) {
17
- process.stdout.write(` \x1b[32m✓\x1b[0m ${msg}\n`);
18
- }
19
- function fail(msg) {
20
- process.stdout.write(` \x1b[31m✗\x1b[0m ${msg}\n`);
21
- }
22
- function warn(msg) {
23
- process.stdout.write(` \x1b[33m⚠\x1b[0m ${msg}\n`);
24
- }
25
- function info(msg) {
26
- process.stdout.write(` ${msg}\n`);
27
- }
28
- function section(title, tag) {
29
- const tagStr = tag ? (tag === '[required]' ? ` \x1b[31m[required]\x1b[0m` : ` \x1b[2m[optional]\x1b[0m`) : '';
30
- process.stdout.write(`\n\x1b[1m${title}\x1b[0m${tagStr}\n`);
31
- }
32
- function dim(msg) {
33
- process.stdout.write(`\x1b[2m${msg}\x1b[0m\n`);
34
- }
35
- // ── Prompt helpers ────────────────────────────────────────────────────────────
36
- function isInteractivePromptSession() {
37
- return !!(process.stdin.isTTY && process.stdout.isTTY);
38
- }
39
- function prompt(question) {
40
- if (!isInteractivePromptSession()) {
41
- return Promise.resolve('');
42
- }
43
- const rl = createInterface({ input: process.stdin, output: process.stdout });
44
- return new Promise((resolve) => {
45
- rl.question(question, (answer) => {
46
- rl.close();
47
- resolve(answer.trim());
48
- });
49
- });
50
- }
51
- function promptSecret(question) {
52
- if (!isInteractivePromptSession()) {
53
- return Promise.resolve('');
54
- }
55
- // Use readline for all prompts — mixing raw-mode stdin listeners with
56
- // readline createInterface causes readline to buffer+unshift unconsumed
57
- // input back onto the stream, which the raw listener then re-reads,
58
- // doubling the typed value. Using readline throughout avoids this entirely.
59
- return new Promise((resolve) => {
60
- const rl = createInterface({
61
- input: process.stdin,
62
- output: process.stdout,
63
- terminal: process.stdin.isTTY,
64
- });
65
- let value = '';
66
- process.stdout.write(question);
67
- let origWrite = null;
68
- if (process.stdin.isTTY) {
69
- // Intercept the readline output write so we can replace echoed chars with *
70
- origWrite = rl.output.write.bind(rl.output);
71
- rl.output.write = (s) => {
72
- // Allow newline through; suppress everything else (the echoed characters)
73
- if (s === '\r\n' || s === '\n' || s === '\r')
74
- origWrite(s);
75
- };
76
- }
77
- rl.on('line', (line) => {
78
- value = line;
79
- rl.close();
80
- });
81
- rl.once('close', () => {
82
- // Restore stdout.write before resolving — the interceptor patches rl.output.write
83
- // which is process.stdout.write, so without restoring it all subsequent output is swallowed.
84
- if (origWrite) {
85
- rl.output.write = origWrite;
86
- }
87
- // In TTY mode readline already emitted \n when Enter was pressed; only add one in non-TTY.
88
- if (!process.stdin.isTTY)
89
- process.stdout.write('\n');
90
- // rl.close() pauses stdin; resume it so subsequent prompt() calls work.
91
- process.stdin.resume();
92
- resolve(value);
93
- });
94
- });
95
- }
96
- async function confirm(question, defaultYes = true) {
97
- if (!isInteractivePromptSession()) {
98
- return false;
99
- }
100
- const hint = defaultYes ? '[Y/n]' : '[y/N]';
101
- const answer = await prompt(` ${question} ${hint} `);
102
- if (!answer)
103
- return defaultYes;
104
- return answer.toLowerCase().startsWith('y');
105
- }
106
- // ── Shell helpers ─────────────────────────────────────────────────────────────
107
- async function binaryExists(name) {
108
- try {
109
- await execFileAsync('which', [name]);
110
- return true;
111
- }
112
- catch {
113
- return false;
114
- }
115
- }
116
- function runSpawn(cmd, args, opts = {}) {
117
- return new Promise((resolve) => {
118
- const child = spawn(cmd, args, {
119
- cwd: opts.cwd,
120
- env: opts.env ?? process.env,
121
- stdio: ['ignore', 'pipe', 'pipe'],
122
- });
123
- let settled = false;
124
- let stdout = '';
125
- let stderr = '';
126
- child.on('error', (err) => {
127
- if (!settled) {
128
- settled = true;
129
- resolve({ exitCode: 1, stdout: '', stderr: err.message });
130
- }
131
- });
132
- child.stdout.on('data', (d) => {
133
- stdout += String(d);
134
- });
135
- child.stderr.on('data', (d) => {
136
- stderr += String(d);
137
- });
138
- child.on('exit', (code) => {
139
- if (!settled) {
140
- settled = true;
141
- resolve({ exitCode: code ?? 1, stdout, stderr });
142
- }
143
- });
144
- });
145
- }
146
- // ── Shell profile detection ───────────────────────────────────────────────────
147
- async function detectShellProfile() {
148
- const shell = process.env['SHELL'] ?? '';
149
- const home = homedir();
150
- if (shell.includes('zsh')) {
151
- return join(home, '.zshrc');
152
- }
153
- if (shell.includes('bash')) {
154
- // Prefer .bash_profile on macOS (login shell), .bashrc on Linux
155
- const bashProfile = join(home, '.bash_profile');
156
- const exists = await access(bashProfile)
157
- .then(() => true)
158
- .catch(() => false);
159
- return exists ? bashProfile : join(home, '.bashrc');
160
- }
161
- if (shell.includes('fish')) {
162
- return join(home, '.config', 'fish', 'config.fish');
163
- }
164
- return join(home, '.profile');
165
- }
166
- async function profileContains(profilePath, str) {
167
- try {
168
- const content = await readFile(profilePath, 'utf8');
169
- return content.includes(str);
170
- }
171
- catch {
172
- return false;
173
- }
174
- }
175
- async function appendToProfile(profilePath, lines) {
176
- await appendFile(profilePath, `\n${lines}\n`, 'utf8');
177
- }
178
- // ── Step 1: Node.js ───────────────────────────────────────────────────────────
179
- async function setupNode() {
180
- section('Step 1: Node.js', '[required]');
181
- const current = process.versions.node;
182
- const major = parseInt(current.split('.')[0], 10);
183
- if (major >= REQUIRED_NODE_MAJOR) {
184
- ok(`Node.js v${current} — already good`);
185
- return true;
186
- }
187
- fail(`Node.js v${current} — need v${REQUIRED_NODE_MAJOR}+`);
188
- info('');
189
- const hasNvm = (await binaryExists('nvm')) ||
190
- (await access(join(homedir(), '.nvm', 'nvm.sh'))
191
- .then(() => true)
192
- .catch(() => false));
193
- const hasFnm = await binaryExists('fnm');
194
- if (hasNvm) {
195
- info(`nvm detected. Will run: nvm install ${REQUIRED_NODE_MAJOR} && nvm use ${REQUIRED_NODE_MAJOR}`);
196
- const go = await confirm(`Install and switch to Node ${REQUIRED_NODE_MAJOR} via nvm?`);
197
- if (!go) {
198
- warn(`Skipped. Re-run experiences setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
199
- return false;
200
- }
201
- // nvm is a shell function so we source it and run in a subshell
202
- const nvmScript = join(homedir(), '.nvm', 'nvm.sh');
203
- const result = await runSpawn('bash', [
204
- '-c',
205
- `source "${nvmScript}" && nvm install ${REQUIRED_NODE_MAJOR} && nvm alias default ${REQUIRED_NODE_MAJOR}`,
206
- ]);
207
- if (result.exitCode !== 0) {
208
- fail('nvm install failed');
209
- info(result.stderr.trim().split('\n').slice(0, 5).join('\n'));
210
- info(`Run manually: nvm install ${REQUIRED_NODE_MAJOR} && nvm use ${REQUIRED_NODE_MAJOR}`);
211
- return false;
212
- }
213
- ok(`Node ${REQUIRED_NODE_MAJOR} installed via nvm. Re-run experiences setup in a fresh shell to pick it up.`);
214
- return false; // Need fresh shell to get the new node on PATH
215
- }
216
- if (hasFnm) {
217
- info(`fnm detected. Will run: fnm install ${REQUIRED_NODE_MAJOR} && fnm use ${REQUIRED_NODE_MAJOR}`);
218
- const go = await confirm(`Install and switch to Node ${REQUIRED_NODE_MAJOR} via fnm?`);
219
- if (!go) {
220
- warn(`Skipped. Re-run experiences setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
221
- return false;
222
- }
223
- const result = await runSpawn('fnm', ['install', String(REQUIRED_NODE_MAJOR)]);
224
- if (result.exitCode !== 0) {
225
- fail('fnm install failed');
226
- info(`Run manually: fnm install ${REQUIRED_NODE_MAJOR} && fnm use ${REQUIRED_NODE_MAJOR}`);
227
- return false;
228
- }
229
- const useResult = await runSpawn('fnm', ['use', String(REQUIRED_NODE_MAJOR)]);
230
- if (useResult.exitCode !== 0) {
231
- warn(`fnm use ${REQUIRED_NODE_MAJOR} failed — node installed but not activated`);
232
- info(`Run manually: fnm use ${REQUIRED_NODE_MAJOR} && fnm default ${REQUIRED_NODE_MAJOR}`);
233
- }
234
- else {
235
- const defaultResult = await runSpawn('fnm', ['default', String(REQUIRED_NODE_MAJOR)]);
236
- if (defaultResult.exitCode !== 0) {
237
- warn(`fnm default ${REQUIRED_NODE_MAJOR} failed — version won't persist across new shells`);
238
- info(`Run manually: fnm default ${REQUIRED_NODE_MAJOR}`);
239
- }
240
- }
241
- ok(`Node ${REQUIRED_NODE_MAJOR} installed via fnm. Re-run experiences setup in a fresh shell.`);
242
- return false;
243
- }
244
- // No version manager found — offer to install nvm
245
- info('No Node version manager detected (nvm or fnm).');
246
- const installNvm = await confirm('Install nvm now? (recommended)');
247
- if (installNvm) {
248
- info('Running nvm install script...');
249
- const result = await runSpawn('bash', [
250
- '-c',
251
- 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash',
252
- ]);
253
- if (result.exitCode !== 0) {
254
- fail('nvm install failed');
255
- info('Install manually: https://github.com/nvm-sh/nvm#installing-and-updating');
256
- return false;
257
- }
258
- ok('nvm installed. Open a new shell, then re-run experiences setup.');
259
- return false;
260
- }
261
- info(`Install Node ${REQUIRED_NODE_MAJOR} manually from https://nodejs.org`);
262
- return false;
263
- }
264
- // ── Step 2: pnpm ─────────────────────────────────────────────────────────────
265
- async function setupPnpm() {
266
- section('Step 2: pnpm', '[required]');
267
- if (await binaryExists('pnpm')) {
268
- const v = await runSpawn('pnpm', ['--version']);
269
- ok(`pnpm v${v.stdout.trim()} — already installed`);
270
- return true;
271
- }
272
- fail('pnpm not found');
273
- info('');
274
- const hasCorecpack = await binaryExists('corepack');
275
- if (hasCorecpack) {
276
- info('Will run: corepack enable && corepack prepare pnpm@latest --activate');
277
- const go = await confirm('Install pnpm via corepack?');
278
- if (go) {
279
- const r1 = await runSpawn('corepack', ['enable']);
280
- const r2 = r1.exitCode === 0 ? await runSpawn('corepack', ['prepare', 'pnpm@latest', '--activate']) : r1;
281
- if (r2.exitCode !== 0) {
282
- fail('corepack install failed');
283
- info('Try: npm install -g pnpm');
284
- return false;
285
- }
286
- ok('pnpm installed via corepack');
287
- return true;
288
- }
289
- }
290
- info('Will run: npm install -g pnpm');
291
- const go = await confirm('Install pnpm via npm?');
292
- if (!go) {
293
- warn('Skipped. Install pnpm manually: npm install -g pnpm');
294
- return false;
295
- }
296
- const result = await runSpawn('npm', ['install', '-g', 'pnpm']);
297
- if (result.exitCode !== 0) {
298
- fail('npm install -g pnpm failed');
299
- info(result.stderr.trim().split('\n').slice(0, 5).join('\n'));
300
- return false;
301
- }
302
- ok('pnpm installed');
303
- return true;
304
- }
305
- // ── Step 3: install + build ───────────────────────────────────────────────────
306
- async function setupBuild(repoRoot) {
307
- section('Step 3: Install dependencies & build', '[required]');
308
- info('Running pnpm install...');
309
- const installResult = await runSpawn('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot });
310
- if (installResult.exitCode !== 0) {
311
- fail('pnpm install failed');
312
- const errLines = installResult.stderr.trim().split('\n').slice(0, 8);
313
- for (const line of errLines)
314
- info(line);
315
- info('');
316
- info('Try: pnpm install (without --frozen-lockfile) to update the lockfile');
317
- return false;
318
- }
319
- ok('Dependencies installed');
320
- info('Building CLI...');
321
- const buildResult = await runSpawn('pnpm', ['--filter', '@contentful/experience-design-system-cli', 'run', 'build'], {
322
- cwd: repoRoot,
323
- });
324
- if (buildResult.exitCode !== 0) {
325
- fail('Build failed');
326
- const errLines = buildResult.stderr.trim().split('\n').slice(0, 10);
327
- for (const line of errLines)
328
- info(line);
329
- return false;
330
- }
331
- ok('CLI built successfully');
332
- return true;
333
- }
334
- // ── Step 4: agent CLI ─────────────────────────────────────────────────────────
335
- const AGENT_DEFS = [
336
- { name: 'Claude Code', binary: 'claude', installHint: 'npm install -g @anthropic-ai/claude-code && claude login' },
337
- { name: 'OpenAI Codex', binary: 'codex', installHint: 'npm install -g @openai/codex (requires OPENAI_API_KEY)' },
338
- { name: 'OpenCode', binary: 'opencode', installHint: 'npm install -g opencode-ai && opencode auth' },
339
- ];
340
- function pick(items, defaultIdx = 0) {
341
- items.forEach((item, i) => {
342
- const num = `\x1b[1m[${i + 1}]\x1b[0m`;
343
- const desc = item.description ? ` \x1b[2m${item.description}\x1b[0m` : '';
344
- const def = i === defaultIdx ? ` \x1b[2m(default)\x1b[0m` : '';
345
- process.stdout.write(` ${num} ${item.label}${desc}${def}\n`);
346
- });
347
- process.stdout.write(` \x1b[2m[s] Skip\x1b[0m\n`);
348
- }
349
- async function promptCodexModel() {
350
- if (process.env['OPENAI_API_KEY'])
351
- return undefined; // API key users get the default (gpt-4.1-nano)
352
- info('');
353
- process.stdout.write(` \x1b[33m⚠\x1b[0m No OPENAI_API_KEY — using ChatGPT account authentication.\n`);
354
- info(' Tip: run \x1b[1mcodex\x1b[0m then type \x1b[1m/model\x1b[0m to browse all available models.');
355
- info('');
356
- info(' Choose a model:');
357
- info('');
358
- pick([
359
- { label: 'gpt-5.4-mini', description: 'fast, lower cost' },
360
- { label: 'gpt-5.5', description: 'most capable' },
361
- { label: 'gpt-5.4' },
362
- ]);
363
- info('');
364
- const choice = await prompt(' \x1b[2m›\x1b[0m Your choice [1]: ');
365
- if (choice === '1' || choice === '')
366
- return 'gpt-5.4-mini';
367
- if (choice === '2')
368
- return 'gpt-5.5';
369
- if (choice === '3')
370
- return 'gpt-5.4';
371
- return undefined;
372
- }
373
- async function setupAgent() {
374
- section('Step 4: Coding agent (claude, codex, or opencode)', '[required]');
375
- info('experiences import uses a coding agent to generate component definitions.');
376
- info('');
377
- const found = (await Promise.all(AGENT_DEFS.map(async (a) => ((await binaryExists(a.binary)) ? a : null)))).filter((a) => a !== null);
378
- if (found.length === 1) {
379
- ok(`${found[0].name} (${found[0].binary}) found`);
380
- const agentModel = found[0].binary === 'codex' ? await promptCodexModel() : undefined;
381
- return { agent: found[0].binary, agentModel };
382
- }
383
- if (found.length > 1) {
384
- info('Multiple coding agents found. Choose one to use as the default:');
385
- info('');
386
- pick(found.map((a) => ({ label: `${a.name}`, description: a.binary })));
387
- info('');
388
- const choice = await prompt(' \x1b[2m›\x1b[0m Your choice [1]: ');
389
- if (choice.toLowerCase() === 's') {
390
- warn('Skipped. Install a coding agent before running experiences import.');
391
- return { agent: undefined, agentModel: undefined };
392
- }
393
- const parsed = choice === '' ? 1 : parseInt(choice, 10);
394
- const idx = Number.isNaN(parsed) || parsed < 1 || parsed > found.length ? 0 : parsed - 1;
395
- const selected = found[idx];
396
- ok(`${selected.name} \x1b[2m(${selected.binary})\x1b[0m selected`);
397
- const agentModel = selected.binary === 'codex' ? await promptCodexModel() : undefined;
398
- return { agent: selected.binary, agentModel };
399
- }
400
- warn('No coding agent found on PATH');
401
- info('');
402
- info('Choose one to install:');
403
- info('');
404
- pick([
405
- { label: 'Claude Code', description: 'npm install -g @anthropic-ai/claude-code' },
406
- { label: 'OpenAI Codex', description: 'npm install -g @openai/codex' },
407
- { label: 'OpenCode', description: 'npm install -g opencode-ai' },
408
- ]);
409
- info('');
410
- const choice = await prompt(' Your choice: ');
411
- if (choice === '1' || choice === '') {
412
- const r = await runSpawn('npm', ['install', '-g', '@anthropic-ai/claude-code']);
413
- if (r.exitCode !== 0) {
414
- fail('Install failed');
415
- info(r.stderr.trim().split('\n').slice(0, 5).join('\n'));
416
- return { agent: undefined, agentModel: undefined };
417
- }
418
- if (!(await binaryExists('claude'))) {
419
- fail('claude binary not found on PATH after install — check your npm global bin directory');
420
- return { agent: undefined, agentModel: undefined };
421
- }
422
- ok('Claude Code installed');
423
- info('');
424
- info('Next: run `claude login` to authenticate (browser OAuth).');
425
- info('Or set ANTHROPIC_API_KEY in your shell profile.');
426
- return { agent: 'claude', agentModel: undefined };
427
- }
428
- if (choice === '2') {
429
- const r = await runSpawn('npm', ['install', '-g', '@openai/codex']);
430
- if (r.exitCode !== 0) {
431
- fail('Install failed');
432
- return { agent: undefined, agentModel: undefined };
433
- }
434
- if (!(await binaryExists('codex'))) {
435
- fail('codex binary not found on PATH after install — check your npm global bin directory');
436
- return { agent: undefined, agentModel: undefined };
437
- }
438
- ok('OpenAI Codex installed');
439
- const agentModel = await promptCodexModel();
440
- return { agent: 'codex', agentModel };
441
- }
442
- if (choice === '3') {
443
- const r = await runSpawn('npm', ['install', '-g', 'opencode-ai']);
444
- if (r.exitCode !== 0) {
445
- fail('Install failed');
446
- return { agent: undefined, agentModel: undefined };
447
- }
448
- if (!(await binaryExists('opencode'))) {
449
- fail('opencode binary not found on PATH after install — check your npm global bin directory');
450
- return { agent: undefined, agentModel: undefined };
451
- }
452
- ok('OpenCode installed');
453
- info('Run `opencode auth` to configure your provider.');
454
- return { agent: 'opencode', agentModel: undefined };
455
- }
456
- warn('Skipped. Install a coding agent before running experiences import.');
457
- return { agent: undefined, agentModel: undefined };
458
- }
459
- // ── Step 5: Contentful credentials ───────────────────────────────────────────
460
- async function setupContentfulCredentials() {
461
- section('Step 5: Contentful credentials', '[optional]');
462
- info(`Saved to ${experiencesCredentialsPath()} — loaded automatically by experiences import.`);
463
- info('');
464
- // INTEG-4410: as of the precedence flip, disk wins over env — but env vars
465
- // still act as a fallback when disk is empty, so operators who have any
466
- // CONTENTFUL_* / EDS_HOST env set should know the ambient value would take
467
- // effect if they skip this step. Warn regardless of the choice.
468
- const envShadowing = [
469
- process.env['CONTENTFUL_SPACE_ID'] ? 'CONTENTFUL_SPACE_ID' : null,
470
- process.env['CONTENTFUL_ENVIRONMENT_ID'] ? 'CONTENTFUL_ENVIRONMENT_ID' : null,
471
- process.env['CONTENTFUL_MANAGEMENT_TOKEN'] ? 'CONTENTFUL_MANAGEMENT_TOKEN' : null,
472
- process.env['EDS_HOST'] ? 'EDS_HOST' : null,
473
- ].filter((v) => !!v);
474
- if (envShadowing.length > 0) {
475
- warn(`Env vars set: ${envShadowing.join(', ')}. Values saved here take precedence; env vars only apply where disk is empty.`);
476
- info('');
477
- }
478
- const stored = await readExperiencesCredentials();
479
- const currentSpace = stored.spaceId;
480
- const currentEnv = stored.environmentId;
481
- const currentToken = stored.cmaToken;
482
- const storedHost = stored.host;
483
- const currentHost = storedHost ?? DEFAULT_CONFIGURED_HOST;
484
- const hasAny = !!(currentSpace || currentEnv || currentToken);
485
- if (hasAny) {
486
- info('Current values:');
487
- if (currentSpace) {
488
- ok(`Space ID ${currentSpace}`);
489
- }
490
- else {
491
- warn('Space ID (not set)');
492
- }
493
- if (currentEnv) {
494
- ok(`Environment ID ${currentEnv}`);
495
- }
496
- else {
497
- warn('Environment ID (not set)');
498
- }
499
- if (currentToken) {
500
- ok(`CMA Token ${'•'.repeat(Math.min(currentToken.length, 8))}...`);
501
- }
502
- else {
503
- warn('CMA Token (not set)');
504
- }
505
- ok(`API Host ${currentHost}`);
506
- info('');
507
- }
508
- const allSet = !!(currentSpace && currentEnv && currentToken);
509
- const doUpdate = await confirm(hasAny ? 'Update credentials?' : 'Configure Contentful credentials?', !allSet);
510
- if (!doUpdate) {
511
- if (allSet) {
512
- ok('Credentials already configured — no changes made');
513
- }
514
- else {
515
- warn('Skipped. experiences import will prompt for credentials interactively.');
516
- }
517
- return true;
518
- }
519
- info('');
520
- info('Get your CMA token: Contentful web app → Settings → API keys → Content management tokens');
521
- info('');
522
- const spaceIdInput = await prompt(` Space ID${currentSpace ? ` [${currentSpace}]` : ''}: `);
523
- const spaceId = spaceIdInput || currentSpace;
524
- const envIdInput = await prompt(` Environment ID [${currentEnv || 'master'}]: `);
525
- const environmentId = envIdInput || currentEnv || 'master';
526
- const tokenInput = await promptSecret(` CMA token${currentToken ? ' [press Enter to keep existing]' : ' (paste here)'}: `);
527
- const cmaToken = tokenInput || currentToken;
528
- if (!cmaToken || !spaceId) {
529
- warn('Space ID and CMA token are required. Skipped.');
530
- return false;
531
- }
532
- const hostInput = await prompt(` API host [${currentHost}]: `);
533
- const host = toConfiguredHost(hostInput) ?? storedHost;
534
- const existing = await readExperiencesCredentials();
535
- await writeExperiencesCredentials({ ...existing, spaceId, environmentId, cmaToken, ...(host ? { host } : {}) });
536
- ok(`Credentials saved to ${experiencesCredentialsPath()}`);
537
- ok(`API host set to ${host ?? DEFAULT_CONFIGURED_HOST}`);
538
- info('Run experiences import — credentials will be pre-filled automatically.');
539
- return true;
540
- }
541
- /**
542
- * Prompt the operator for a custom skill prompt path. Returns the resolved
543
- * trimmed value, or `undefined` to leave the current value unchanged, or `null`
544
- * to clear it. `ask` is injectable so tests can stub stdin.
545
- */
546
- export async function promptCustomSkillPath(kind, current, ask = prompt) {
547
- const flagName = kind === 'select' ? '--select-prompt-path' : '--generate-prompt-path';
548
- void flagName;
549
- const label = kind === 'select' ? 'select (analyze select-agent)' : 'generate (generate components)';
550
- const currentLabel = current ? ` [${current}]` : ' [none]';
551
- const answer = await ask(` Custom ${label} prompt path${currentLabel} (empty=keep, "-"=clear): `);
552
- const trimmed = answer.trim();
553
- if (trimmed === '')
554
- return undefined;
555
- if (trimmed === '-')
556
- return null;
557
- return trimmed;
558
- }
559
- // ── Step 6: Optional quality-of-life ─────────────────────────────────────────
560
- async function setupQoL(profilePath) {
561
- section('Step 6: Optional extras', '[optional]');
562
- info('These are not required for experiences import but improve the experience.');
563
- info('');
564
- // 6a: AI auto-filter default
565
- const existingCreds = await readExperiencesCredentials();
566
- info('AI auto-filter — runs an agent pass before the manual scope-gate to prefilter components.');
567
- info('Operators who prefer to review every component can default this OFF and override per run with --auto-filter.');
568
- const autoFilter = await promptAutoFilterPreference((q) => prompt(q), existingCreds.autoFilter);
569
- if (autoFilter !== (existingCreds.autoFilter ?? true)) {
570
- await writeExperiencesCredentials({ ...existingCreds, autoFilter });
571
- ok(`AI auto-filter default set to ${autoFilter ? 'ON' : 'OFF'}`);
572
- }
573
- else {
574
- dim(' unchanged');
575
- }
576
- info('');
577
- // 6b: EDS_EXTRACT_CONCURRENCY
578
- const hasConcurrency = await profileContains(profilePath, 'EDS_EXTRACT_CONCURRENCY');
579
- if (!hasConcurrency) {
580
- info('EDS_EXTRACT_CONCURRENCY — controls how many components are analyzed in parallel.');
581
- info('Default is 4. Set higher (e.g. 8) on fast machines to speed up large codebases.');
582
- const setConcurrency = await confirm('Add EDS_EXTRACT_CONCURRENCY=8 to your profile?', false);
583
- if (setConcurrency) {
584
- await appendToProfile(profilePath, '# experiences performance\nexport EDS_EXTRACT_CONCURRENCY=8');
585
- ok(`EDS_EXTRACT_CONCURRENCY=8 written to ${profilePath}`);
586
- }
587
- else {
588
- dim(' skipped');
589
- }
590
- }
591
- else {
592
- ok('EDS_EXTRACT_CONCURRENCY — already set');
593
- }
594
- // 6c (Feature 8): custom skill prompt paths
595
- info('');
596
- info('Custom skill prompt paths — point select-agent and/or generate components at your own .md prompts.');
597
- info('When set, the bundled invariants (utility-wrapper rejection, description rules) do NOT apply.');
598
- const offerCustomPrompts = await confirm('Configure custom skill prompt paths?', false);
599
- if (offerCustomPrompts) {
600
- const stored = await readExperiencesCredentials();
601
- const selectAnswer = await promptCustomSkillPath('select', stored.selectPromptPath);
602
- const generateAnswer = await promptCustomSkillPath('generate', stored.generatePromptPath);
603
- const updated = { ...stored };
604
- if (selectAnswer === null)
605
- delete updated.selectPromptPath;
606
- else if (selectAnswer !== undefined)
607
- updated.selectPromptPath = selectAnswer;
608
- if (generateAnswer === null)
609
- delete updated.generatePromptPath;
610
- else if (generateAnswer !== undefined)
611
- updated.generatePromptPath = generateAnswer;
612
- await writeExperiencesCredentials(updated);
613
- ok(`Custom prompt paths saved to ${experiencesCredentialsPath()}`);
614
- }
615
- else {
616
- dim(' skipped');
617
- }
618
- // 6d: Debug-mode default
619
- info('');
620
- info('Debug logging — writes a JSONL trace of every command decision (agent calls, tool calls,');
621
- info('apply actions, filter decisions, etc.) to ~/.contentful/experience-design-system-cli/debug/.');
622
- info('Useful for developers debugging the CLI; OFF by default because traces are verbose.');
623
- const debugCreds = await readExperiencesCredentials();
624
- const debugChoice = await promptDebugModePreference((q) => prompt(q), debugCreds.debug);
625
- if (debugChoice !== (debugCreds.debug ?? false)) {
626
- await writeExperiencesCredentials({ ...debugCreds, debug: debugChoice });
627
- ok(`Debug logging default set to ${debugChoice ? 'ON' : 'OFF'}`);
628
- }
629
- else {
630
- dim(' unchanged');
631
- }
632
- info('');
633
- // 6d.1: Analytics opt-out
634
- info('');
635
- info('Anonymous usage analytics — helps us see which commands are used and where imports');
636
- info('succeed or fail. Never includes source code, file paths, credentials, or authored content.');
637
- info('Disabling here persists the opt-out; it will not silently re-enable later. See README > Usage data.');
638
- const analyticsCreds = await readExperiencesCredentials();
639
- const analyticsDisabled = await promptAnalyticsPreference((q) => prompt(q), analyticsCreds.analyticsDisabled);
640
- if (analyticsDisabled !== (analyticsCreds.analyticsDisabled ?? false)) {
641
- await writeExperiencesCredentials({ ...analyticsCreds, analyticsDisabled });
642
- ok(`Analytics ${analyticsDisabled ? 'disabled' : 'enabled'}`);
643
- }
644
- else {
645
- dim(' unchanged');
646
- }
647
- info('');
648
- // 6e: NO_COLOR
649
- info('');
650
- info('NO_COLOR — set to 1 to disable ANSI color output (useful in CI or plain terminals).');
651
- const setNoColor = await confirm('Add NO_COLOR=1 (disable colors) to your profile?', false);
652
- if (setNoColor) {
653
- const hasNoColor = await profileContains(profilePath, 'NO_COLOR');
654
- if (!hasNoColor) {
655
- await appendToProfile(profilePath, 'export NO_COLOR=1');
656
- ok(`NO_COLOR=1 written to ${profilePath}`);
657
- }
658
- else {
659
- warn('NO_COLOR already present in profile — skipping');
660
- }
661
- }
662
- else {
663
- dim(' skipped');
664
- }
665
- }
666
- // ── Doctor checks ─────────────────────────────────────────────────────────────
667
- async function checkNode() {
668
- section('Checking Node.js version');
669
- const current = process.versions.node;
670
- const major = parseInt(current.split('.')[0], 10);
671
- if (major < REQUIRED_NODE_MAJOR) {
672
- fail(`Node.js v${current} — need v${REQUIRED_NODE_MAJOR}+`);
673
- info('');
674
- info('How to fix:');
675
- if (await binaryExists('nvm')) {
676
- info(` nvm install ${REQUIRED_NODE_MAJOR}`);
677
- info(` nvm use ${REQUIRED_NODE_MAJOR}`);
678
- info(` nvm alias default ${REQUIRED_NODE_MAJOR} # make it permanent`);
679
- }
680
- else if (await binaryExists('fnm')) {
681
- info(` fnm install ${REQUIRED_NODE_MAJOR}`);
682
- info(` fnm use ${REQUIRED_NODE_MAJOR}`);
683
- }
684
- else {
685
- info(` Download Node v${REQUIRED_NODE_MAJOR} from https://nodejs.org`);
686
- }
687
- return false;
688
- }
689
- ok(`Node.js v${current}`);
690
- return true;
691
- }
692
- async function checkPnpm(pkgRoot) {
693
- section('Checking pnpm');
694
- if (!(await binaryExists('pnpm'))) {
695
- fail('pnpm not found');
696
- info('How to fix:');
697
- info(' npm install -g pnpm');
698
- info(' # or: corepack enable pnpm');
699
- return false;
700
- }
701
- const versionResult = await runSpawn('pnpm', ['--version']);
702
- if (versionResult.exitCode !== 0) {
703
- fail('pnpm found but not working');
704
- info('Try reinstalling: npm install -g pnpm --force');
705
- return false;
706
- }
707
- ok(`pnpm v${versionResult.stdout.trim()}`);
708
- const pingResult = await runSpawn('pnpm', ['exec', 'node', '--version'], { cwd: pkgRoot });
709
- if (pingResult.exitCode !== 0) {
710
- fail('pnpm cannot execute in repo root');
711
- info('The pnpm global store may be mismatched with your current Node version.');
712
- info('How to fix:');
713
- info(' npm install -g pnpm --force');
714
- return false;
715
- }
716
- return true;
717
- }
718
- async function checkDependencies(pkgRoot) {
719
- section('Checking dependencies (pnpm install)');
720
- const nodeModulesExists = await access(join(pkgRoot, 'node_modules'))
721
- .then(() => true)
722
- .catch(() => false);
723
- if (!nodeModulesExists) {
724
- info('node_modules not found — running pnpm install...');
725
- }
726
- else {
727
- info('Running pnpm install to ensure dependencies are up to date...');
728
- }
729
- const repoRoot = join(pkgRoot, '..', '..');
730
- const result = await runSpawn('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot });
731
- if (result.exitCode !== 0) {
732
- fail('pnpm install failed');
733
- info('');
734
- const errLines = result.stderr.trim().split('\n').slice(0, 10);
735
- for (const line of errLines)
736
- info(line);
737
- info('');
738
- info('How to fix:');
739
- info(' • Try: pnpm install (without --frozen-lockfile) to update the lockfile');
740
- info(' • Check that your Node version matches: cat .nvmrc');
741
- return false;
742
- }
743
- ok('Dependencies installed');
744
- return true;
745
- }
746
- async function checkBuild(pkgRoot) {
747
- section('Building CLI');
748
- info('Running pnpm build...');
749
- const repoRoot = join(pkgRoot, '..', '..');
750
- const result = await runSpawn('pnpm', ['--filter', '@contentful/experience-design-system-cli', 'run', 'build'], {
751
- cwd: repoRoot,
752
- });
753
- if (result.exitCode !== 0) {
754
- fail('Build failed');
755
- info('');
756
- const errLines = result.stderr.trim().split('\n').slice(0, 15);
757
- for (const line of errLines)
758
- info(line);
759
- info('');
760
- info('How to fix:');
761
- info(' • Check for TypeScript errors: pnpm typecheck');
762
- info(' • If the error is in a generated file under dist/, try: pnpm clean && pnpm build');
763
- return false;
764
- }
765
- ok('Build succeeded');
766
- return true;
767
- }
768
- async function checkAgent() {
769
- section('Checking coding agent');
770
- const agents = AGENT_DEFS;
771
- const creds = await readExperiencesCredentials();
772
- const savedAgent = creds.agent;
773
- const savedModel = creds.agentModel;
774
- if (savedAgent) {
775
- const found = await binaryExists(savedAgent);
776
- if (found) {
777
- const modelStr = savedModel ? ` — model: ${savedModel}` : '';
778
- ok(`${savedAgent}${modelStr} (saved preference)`);
779
- return true;
780
- }
781
- else {
782
- warn(`Saved agent '${savedAgent}' not found on PATH`);
783
- info(`Re-run experiences setup to reconfigure.`);
784
- return false;
785
- }
786
- }
787
- for (const agent of agents) {
788
- if (await binaryExists(agent.binary)) {
789
- ok(`${agent.name} (${agent.binary}) found`);
790
- info('Tip: run experiences setup to save a default agent and model.');
791
- return true;
792
- }
793
- }
794
- warn('No coding agent found on PATH');
795
- info('The coding agent is required for the generate steps in experiences import.');
796
- info('Install one of:');
797
- info(' • Claude Code: npm install -g @anthropic-ai/claude-code');
798
- info(' • OpenAI Codex: npm install -g @openai/codex');
799
- info(' • OpenCode: npm install -g opencode-ai');
800
- return false;
801
- }
802
- // ── Commands ──────────────────────────────────────────────────────────────────
803
- export function registerSetupCommand(program) {
804
- program
805
- .command('doctor')
806
- .description('Check prerequisites so experiences import runs without errors')
807
- .option('--skip-build', 'Skip the pnpm install + build step (useful if already built)')
808
- .option('--skip-agent', 'Skip the coding agent check')
809
- .action(async (opts) => {
810
- process.stderr.write('\x1b[1mexperiences doctor\x1b[0m — checking your environment\n');
811
- const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
812
- const results = [];
813
- const nodeOk = await checkNode();
814
- results.push({ name: 'Node.js version', ok: nodeOk, required: true });
815
- if (nodeOk) {
816
- const pnpmOk = await checkPnpm(pkgRoot);
817
- results.push({ name: 'pnpm', ok: pnpmOk, required: true });
818
- if (!opts.skipBuild) {
819
- if (pnpmOk) {
820
- const depsOk = await checkDependencies(pkgRoot);
821
- results.push({ name: 'dependencies', ok: depsOk, required: true });
822
- if (depsOk) {
823
- const buildOk = await checkBuild(pkgRoot);
824
- results.push({ name: 'build', ok: buildOk, required: true });
825
- }
826
- }
827
- }
828
- else {
829
- info('\nSkipping install + build (--skip-build)');
830
- }
831
- }
832
- if (!opts.skipAgent) {
833
- const agentOk = await checkAgent();
834
- results.push({ name: 'coding agent', ok: agentOk, required: false });
835
- }
836
- section('Summary');
837
- const failed = results.filter((r) => !r.ok);
838
- const requiredFailed = failed.filter((r) => r.required);
839
- for (const r of results) {
840
- if (r.ok) {
841
- ok(r.name);
842
- }
843
- else if (r.required) {
844
- fail(`${r.name} — required`);
845
- }
846
- else {
847
- warn(`${r.name} — optional`);
848
- }
849
- }
850
- if (requiredFailed.length === 0 && failed.length === 0) {
851
- process.stderr.write('\n\x1b[32m\x1b[1m✓ All checks passed. You are ready to run: experiences import\x1b[0m\n\n');
852
- process.exit(0);
853
- }
854
- else if (requiredFailed.length === 0) {
855
- process.stderr.write('\n\x1b[33m\x1b[1m⚠ Required checks passed, but optional checks failed.\x1b[0m\n');
856
- process.stderr.write(' You can run \x1b[1mexperiences import\x1b[0m but the generate steps may fail without a coding agent.\n\n');
857
- process.exit(0);
858
- }
859
- else {
860
- process.stderr.write(`\n\x1b[31m\x1b[1m✗ ${requiredFailed.length} required check${requiredFailed.length === 1 ? '' : 's'} failed.\x1b[0m\n`);
861
- process.stderr.write(' Fix the issues above, then re-run \x1b[1mexperiences doctor\x1b[0m.\n\n');
862
- process.exit(1);
863
- }
864
- });
865
- program
866
- .command('setup')
867
- .description('Interactive setup wizard: installs prerequisites and configures credentials for experiences import')
868
- .option('--skip-build', 'Skip the pnpm install + build step')
869
- .option('--skip-agent', 'Skip the coding agent check')
870
- .option('--skip-credentials', 'Skip the Contentful credentials step')
871
- .option('--skip-optional', 'Skip optional quality-of-life extras')
872
- .action(async (opts) => {
873
- process.stdout.write('\n\x1b[1mexperiences setup\x1b[0m — interactive setup wizard\n');
874
- process.stdout.write('Sets up everything you need to run \x1b[1mexperiences import\x1b[0m.\n');
875
- const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
876
- const repoRoot = join(pkgRoot, '..', '..');
877
- const profilePath = await detectShellProfile();
878
- const results = [];
879
- // Step 1: Node
880
- const nodeOk = await setupNode();
881
- results.push({ name: 'Node.js 24+', passed: nodeOk, required: true });
882
- if (!nodeOk) {
883
- process.stdout.write('\n\x1b[33mNode.js setup requires a shell restart. Re-run experiences setup afterwards.\x1b[0m\n\n');
884
- process.exit(0);
885
- }
886
- // Step 2: pnpm
887
- const pnpmOk = await setupPnpm();
888
- results.push({ name: 'pnpm', passed: pnpmOk, required: true });
889
- // Step 3: install + build
890
- if (!opts.skipBuild && pnpmOk) {
891
- const buildOk = await setupBuild(repoRoot);
892
- results.push({ name: 'install & build', passed: buildOk, required: true });
893
- }
894
- else if (opts.skipBuild) {
895
- info('\nSkipping install + build (--skip-build)');
896
- }
897
- // Step 4: agent
898
- if (!opts.skipAgent) {
899
- const { agent, agentModel } = await setupAgent();
900
- if (agent) {
901
- const stored = await readExperiencesCredentials();
902
- const { agentModel: _staleModel, ...storedWithoutModel } = stored;
903
- await writeExperiencesCredentials({ ...storedWithoutModel, agent, ...(agentModel ? { agentModel } : {}) });
904
- }
905
- results.push({ name: 'coding agent', passed: !!agent, required: true });
906
- }
907
- else {
908
- info('\nSkipping agent check (--skip-agent)');
909
- results.push({ name: 'coding agent', passed: true, required: false });
910
- }
911
- // Step 5: credentials
912
- if (!opts.skipCredentials) {
913
- const credsOk = await setupContentfulCredentials();
914
- results.push({ name: 'Contentful credentials', passed: credsOk, required: false });
915
- }
916
- else {
917
- info('\nSkipping credentials (--skip-credentials)');
918
- }
919
- // Step 6: optional QoL
920
- if (!opts.skipOptional) {
921
- await setupQoL(profilePath);
922
- }
923
- // ── Summary ────────────────────────────────────────────────────────────
924
- section('Summary');
925
- const requiredFailed = results.filter((r) => r.required && !r.passed);
926
- const optionalFailed = results.filter((r) => !r.required && !r.passed);
927
- for (const r of results) {
928
- if (r.passed) {
929
- ok(`${r.name}`);
930
- }
931
- else if (r.required) {
932
- fail(`${r.name} — required`);
933
- }
934
- else {
935
- warn(`${r.name} — optional`);
936
- }
937
- }
938
- process.stdout.write('\n');
939
- if (requiredFailed.length === 0) {
940
- process.stdout.write('\x1b[32m\x1b[1m✓ Setup complete. You can now run: experiences import\x1b[0m\n');
941
- if (optionalFailed.length > 0) {
942
- process.stdout.write(" (Some optional steps were skipped — that's fine.)\n");
943
- }
944
- }
945
- else {
946
- process.stdout.write(`\x1b[33m\x1b[1m⚠ ${requiredFailed.length} required step${requiredFailed.length === 1 ? '' : 's'} incomplete.\x1b[0m\n`);
947
- process.stdout.write(' Complete the steps above, then re-run \x1b[1mexperiences setup\x1b[0m.\n');
948
- }
949
- // ── Offer experiences doctor ───────────────────────────────────────────────────
950
- process.stdout.write('\n');
951
- const runDoctor = process.stdout.isTTY &&
952
- (await confirm('Run experiences doctor now to verify your environment?', requiredFailed.length === 0));
953
- if (runDoctor) {
954
- process.stdout.write('\n');
955
- const cliBin = process.argv[1] ?? fileURLToPath(import.meta.url);
956
- const doctorResult = await runSpawn(process.execPath, [cliBin, 'doctor'], {
957
- env: process.env,
958
- });
959
- process.stdout.write(doctorResult.stdout);
960
- process.stderr.write(doctorResult.stderr);
961
- process.exit(doctorResult.exitCode);
962
- }
963
- process.stdout.write('\nRun \x1b[1mexperiences doctor\x1b[0m at any time to re-check your environment.\n\n');
964
- process.exit(requiredFailed.length === 0 ? 0 : 1);
965
- });
966
- }