@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.
- package/dist/package.json +14 -10
- package/dist/src/index.js +34937 -9
- package/dist/src/lib/cli-path.d.ts +4 -0
- package/dist/src/runs/export-helpers.d.ts +1 -12
- package/dist/src/runs/push-helpers.d.ts +1 -6
- package/package.json +12 -7
- package/skills/generate-components.md +427 -0
- package/skills/generate-tokens.md +194 -0
- package/skills/select-components.md +224 -0
- package/dist/src/analytics/apply.js +0 -30
- package/dist/src/analytics/client.js +0 -81
- package/dist/src/analytics/constants.js +0 -4
- package/dist/src/analytics/env.js +0 -14
- package/dist/src/analytics/exit.js +0 -24
- package/dist/src/analytics/index.js +0 -9
- package/dist/src/analytics/normalize.js +0 -18
- package/dist/src/analytics/os.js +0 -13
- package/dist/src/analytics/session.js +0 -15
- package/dist/src/analytics/tracker.js +0 -126
- package/dist/src/analytics/types.js +0 -1
- package/dist/src/analyze/build-analyze-view-rows.js +0 -63
- package/dist/src/analyze/command.js +0 -599
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +0 -22
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +0 -44
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +0 -31
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +0 -88
- package/dist/src/analyze/composition/agent-parser/sandbox.js +0 -187
- package/dist/src/analyze/composition/apply-mapping.js +0 -67
- package/dist/src/analyze/composition/candidate-critic-agent.js +0 -39
- package/dist/src/analyze/composition/candidate-critic.js +0 -63
- package/dist/src/analyze/composition/candidate-files.js +0 -131
- package/dist/src/analyze/composition/composition-cache-key.js +0 -19
- package/dist/src/analyze/composition/interchange-schema.js +0 -61
- package/dist/src/analyze/composition/manifest-doc-evidence.js +0 -136
- package/dist/src/analyze/composition/merge-edges.js +0 -70
- package/dist/src/analyze/composition/parse-map-edges.js +0 -55
- package/dist/src/analyze/composition/resolve-mapping-cli.js +0 -29
- package/dist/src/analyze/composition/resolve-mapping.js +0 -111
- package/dist/src/analyze/cycle-detection.js +0 -234
- package/dist/src/analyze/cycle-view.js +0 -28
- package/dist/src/analyze/fuzzy-search.js +0 -55
- package/dist/src/analyze/issue-inheritance.js +0 -108
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.js +0 -170
- package/dist/src/analyze/search-neighborhood.js +0 -56
- package/dist/src/analyze/select/command.js +0 -484
- package/dist/src/analyze/select/index.js +0 -5
- package/dist/src/analyze/select/parser.js +0 -53
- package/dist/src/analyze/select/persistence.js +0 -80
- package/dist/src/analyze/select/preview-annotations.js +0 -62
- package/dist/src/analyze/select/stdout.js +0 -3
- package/dist/src/analyze/select/tui/App.js +0 -483
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +0 -48
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +0 -125
- package/dist/src/analyze/select/tui/components/FieldEditor.js +0 -1131
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +0 -36
- package/dist/src/analyze/select/tui/components/GotoBanner.js +0 -30
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +0 -517
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +0 -17
- package/dist/src/analyze/select/tui/components/JsonEditor.js +0 -131
- package/dist/src/analyze/select/tui/components/JsonPanel.js +0 -66
- package/dist/src/analyze/select/tui/components/LineagePanel.js +0 -22
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +0 -29
- package/dist/src/analyze/select/tui/components/QuitDialog.js +0 -14
- package/dist/src/analyze/select/tui/components/RationalePanel.js +0 -80
- package/dist/src/analyze/select/tui/components/Sidebar.js +0 -92
- package/dist/src/analyze/select/tui/components/SourcePanel.js +0 -52
- package/dist/src/analyze/select/tui/components/StatusBar.js +0 -7
- package/dist/src/analyze/select/tui/components/TopBar.js +0 -10
- package/dist/src/analyze/select/tui/components/removed-components-text.js +0 -11
- package/dist/src/analyze/select/tui/hooks/scroll-offset.js +0 -26
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +0 -66
- package/dist/src/analyze/select/tui/hooks/useKeymap.js +0 -67
- package/dist/src/analyze/select/tui/hooks/useSession.js +0 -52
- package/dist/src/analyze/select/tui/hooks/useUndo.js +0 -26
- package/dist/src/analyze/select/tui/theme.js +0 -28
- package/dist/src/analyze/select/types.js +0 -39
- package/dist/src/analyze/select-agent/command.js +0 -558
- package/dist/src/analyze/select-agent/context-builder.js +0 -197
- package/dist/src/analyze/select-agent/show-rationale.js +0 -92
- package/dist/src/analyze/selection-cascade.js +0 -19
- package/dist/src/analyze/slot-graph.js +0 -17
- package/dist/src/analyze/tui/AnalyzeView.js +0 -64
- package/dist/src/apply/api-client.js +0 -440
- package/dist/src/apply/command.js +0 -845
- package/dist/src/apply/manifest.js +0 -1
- package/dist/src/apply/preview-utils.js +0 -26
- package/dist/src/apply/tui/SelectView.js +0 -34
- package/dist/src/apply/tui/ServerApplyView.js +0 -49
- package/dist/src/apply/tui/ServerPreviewView.js +0 -23
- package/dist/src/credentials-store.js +0 -74
- package/dist/src/generate/command.js +0 -597
- package/dist/src/generate/edit/command.js +0 -127
- package/dist/src/generate/tui/GenerateView.js +0 -10
- package/dist/src/host-utils.js +0 -26
- package/dist/src/import/agent-model-resolve.js +0 -35
- package/dist/src/import/auto-filter-resolve.js +0 -16
- package/dist/src/import/command.js +0 -380
- package/dist/src/import/cycle-auto-reject.js +0 -14
- package/dist/src/import/orchestrator.js +0 -603
- package/dist/src/import/path-utils.js +0 -30
- package/dist/src/import/picker-dispatch.js +0 -44
- package/dist/src/import/print-prompt.js +0 -31
- package/dist/src/import/strip-allowed-components.js +0 -22
- package/dist/src/import/tui/CustomPromptBanner.js +0 -8
- package/dist/src/import/tui/WizardApp.js +0 -1670
- package/dist/src/import/tui/ai-flag.js +0 -6
- package/dist/src/import/tui/auto-filter-error.js +0 -29
- package/dist/src/import/tui/autocomplete.js +0 -32
- package/dist/src/import/tui/components/AutoFilterBanner.js +0 -19
- package/dist/src/import/tui/components/CounterStrip.js +0 -12
- package/dist/src/import/tui/components/LegendEntry.js +0 -6
- package/dist/src/import/tui/cycle-panel-scroll.js +0 -33
- package/dist/src/import/tui/final-review-host.js +0 -25
- package/dist/src/import/tui/group-collapse.js +0 -11
- package/dist/src/import/tui/history.js +0 -53
- package/dist/src/import/tui/hooks/useLineage.js +0 -50
- package/dist/src/import/tui/hooks/useOverlayPanel.js +0 -27
- package/dist/src/import/tui/lineage-layout.js +0 -82
- package/dist/src/import/tui/merge-ai-decisions.js +0 -22
- package/dist/src/import/tui/push-decision-gate-helpers.js +0 -8
- package/dist/src/import/tui/push-progress.js +0 -1
- package/dist/src/import/tui/run-print-files-helpers.js +0 -8
- package/dist/src/import/tui/run-teaser.js +0 -9
- package/dist/src/import/tui/runLivePreview.js +0 -82
- package/dist/src/import/tui/runScopeGate.js +0 -28
- package/dist/src/import/tui/scope-gate-columns.js +0 -94
- package/dist/src/import/tui/scope-gate-host.js +0 -28
- package/dist/src/import/tui/sidebar-width.js +0 -3
- package/dist/src/import/tui/spawn-generate.js +0 -53
- package/dist/src/import/tui/step-filters.js +0 -42
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +0 -726
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +0 -210
- package/dist/src/import/tui/steps/CredentialsStep.js +0 -108
- package/dist/src/import/tui/steps/DoneStep.js +0 -24
- package/dist/src/import/tui/steps/ErrorStep.js +0 -16
- package/dist/src/import/tui/steps/GateStep.js +0 -22
- package/dist/src/import/tui/steps/GenerateReviewStep.js +0 -1618
- package/dist/src/import/tui/steps/PathValidationStep.js +0 -152
- package/dist/src/import/tui/steps/PreviewStep.js +0 -37
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +0 -23
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +0 -73
- package/dist/src/import/tui/steps/PushingStep.js +0 -24
- package/dist/src/import/tui/steps/RunningStep.js +0 -21
- package/dist/src/import/tui/steps/ScopeGateStep.js +0 -907
- package/dist/src/import/tui/steps/TokenInputStep.js +0 -88
- package/dist/src/import/tui/steps/WelcomeStep.js +0 -34
- package/dist/src/import/tui/steps/WizardPreviewStep.js +0 -169
- package/dist/src/import/tui/steps/auto-reject-decision.js +0 -9
- package/dist/src/import/tui/steps/breaking-change-format.js +0 -24
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +0 -175
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.js +0 -122
- package/dist/src/import/tui/wizard-422-helpers.js +0 -60
- package/dist/src/import/tui/wizard-generate-progress.js +0 -25
- package/dist/src/import/tui/wizard-save-flow.js +0 -20
- package/dist/src/import/tui/wizard-state-transitions.js +0 -32
- package/dist/src/lib/agent-model-options.js +0 -15
- package/dist/src/lib/command-options.js +0 -30
- package/dist/src/lib/composition-mode.js +0 -39
- package/dist/src/lib/contentful-urls.js +0 -27
- package/dist/src/lib/debug-logger.js +0 -229
- package/dist/src/lib/debug-preamble.js +0 -33
- package/dist/src/lib/error-parser.js +0 -269
- package/dist/src/lib/prompt-overrides.js +0 -69
- package/dist/src/lib/terminal-capabilities.js +0 -51
- package/dist/src/lib/user-agent.js +0 -29
- package/dist/src/output/format.js +0 -110
- package/dist/src/print/command.js +0 -221
- package/dist/src/print/validate/tui/ValidateView.js +0 -37
- package/dist/src/print/validate/validators/cdf-validator.js +0 -104
- package/dist/src/print/validate/validators/dtcg-validator.js +0 -110
- package/dist/src/print/validate/validators/format-errors.js +0 -18
- package/dist/src/program.js +0 -105
- package/dist/src/runs/export-helpers.js +0 -59
- package/dist/src/runs/fingerprint.js +0 -75
- package/dist/src/runs/ls-command.js +0 -137
- package/dist/src/runs/modify-launcher.js +0 -42
- package/dist/src/runs/path-prompt.js +0 -72
- package/dist/src/runs/push-creds-prompt.js +0 -39
- package/dist/src/runs/push-helpers.js +0 -72
- package/dist/src/runs/push-launcher.js +0 -77
- package/dist/src/runs/replay-helpers.js +0 -164
- package/dist/src/runs/resolve-run-target.js +0 -45
- package/dist/src/runs/run-picker-mount.js +0 -72
- package/dist/src/runs/run-picker.js +0 -129
- package/dist/src/runs/save-conflict.js +0 -56
- package/dist/src/runs/save-path-resolver.js +0 -75
- package/dist/src/runs/staleness.js +0 -140
- package/dist/src/runs/store.js +0 -136
- package/dist/src/session/cache-keys.js +0 -44
- package/dist/src/session/command.js +0 -261
- package/dist/src/session/db.js +0 -1538
- package/dist/src/session/migration.js +0 -117
- package/dist/src/session/session-id.js +0 -212
- package/dist/src/session/stats.js +0 -89
- package/dist/src/setup/analytics-prompt.js +0 -24
- package/dist/src/setup/auto-filter-prompt.js +0 -24
- package/dist/src/setup/command.js +0 -966
- package/dist/src/setup/composition-mode-prompt.js +0 -37
- package/dist/src/setup/debug-mode-prompt.js +0 -22
- 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
|
-
}
|