@the-open-engine/zeroshot 6.31.2 → 6.32.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/README.md +66 -98
- package/cli/index.js +251 -252
- package/cli/lib/setup-provider-readiness.js +86 -0
- package/cli/lib/setup-scanner-worker.js +120 -0
- package/cli/lib/setup-scanner.js +185 -0
- package/cli/lib/setup-wizard-input.js +146 -0
- package/cli/lib/setup-wizard-model.js +205 -0
- package/cli/lib/setup-wizard-plan-view.js +157 -0
- package/cli/lib/setup-wizard-scan-view.js +144 -0
- package/cli/lib/setup-wizard-terminal.js +237 -0
- package/cli/lib/setup-wizard-view.js +180 -0
- package/cli/lib/setup-wizard.js +281 -0
- package/cli/message-formatters-normal.js +14 -18
- package/cli/message-formatters-watch.js +53 -141
- package/lib/completion.js +102 -153
- package/lib/settings.js +10 -2
- package/lib/setup-apply.js +62 -55
- package/lib/setup-plan.js +32 -52
- package/lib/start-cluster.js +65 -25
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -3
- package/scripts/postinstall.js +54 -0
- package/src/preflight.js +27 -1
- package/src/status-footer.js +19 -12
- package/task-lib/commands/list.js +90 -78
- package/task-lib/commands/status.js +97 -40
- package/task-lib/effective-status.js +52 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const {
|
|
3
|
+
CANCEL_KEYS,
|
|
4
|
+
beginTerminal,
|
|
5
|
+
createKeyReader,
|
|
6
|
+
createSelectionState,
|
|
7
|
+
parseKeys,
|
|
8
|
+
reduceSelection,
|
|
9
|
+
} = require('./setup-wizard-input');
|
|
10
|
+
|
|
11
|
+
const WIZARD_WIDTH = 74;
|
|
12
|
+
const WIZARD_SPINNER = Object.freeze(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']);
|
|
13
|
+
const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
14
|
+
|
|
15
|
+
function forcedColorLevel(env) {
|
|
16
|
+
if (!Object.prototype.hasOwnProperty.call(env, 'FORCE_COLOR')) return null;
|
|
17
|
+
const forced = String(env.FORCE_COLOR).trim();
|
|
18
|
+
if (forced === '' || forced === 'true') return 1;
|
|
19
|
+
const parsed = Number(forced);
|
|
20
|
+
return Number.isInteger(parsed) ? Math.max(0, Math.min(3, parsed)) : 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function colorLevel(stdout, env) {
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(env, 'NO_COLOR') || env.TERM === 'dumb') return 0;
|
|
25
|
+
const forced = forcedColorLevel(env);
|
|
26
|
+
if (forced !== null) return forced;
|
|
27
|
+
if (!stdout.isTTY) return 0;
|
|
28
|
+
const depth = typeof stdout.getColorDepth === 'function' ? stdout.getColorDepth(env) : 8;
|
|
29
|
+
if (depth >= 24) return 3;
|
|
30
|
+
if (depth >= 8) return 2;
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function accentStyle(colors, level) {
|
|
35
|
+
if (level >= 3) return colors.hex('#c2240c');
|
|
36
|
+
if (level >= 2) return colors.ansi256(130);
|
|
37
|
+
return colors.red;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createWizardTheme(stdout, env = process.env) {
|
|
41
|
+
const level = colorLevel(stdout, env);
|
|
42
|
+
const colors = new chalk.Instance({ level });
|
|
43
|
+
const accent = accentStyle(colors, level);
|
|
44
|
+
return {
|
|
45
|
+
color: level > 0,
|
|
46
|
+
accent,
|
|
47
|
+
bold: colors.bold,
|
|
48
|
+
dim: colors.dim,
|
|
49
|
+
success: colors.green,
|
|
50
|
+
warning: colors.yellow,
|
|
51
|
+
danger: colors.red,
|
|
52
|
+
plain: (text) => String(text),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stripAnsi(text) {
|
|
57
|
+
return String(text).replace(ANSI_PATTERN, '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function displayWidth(text) {
|
|
61
|
+
return [...stripAnsi(text)].length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fit(text, maxWidth) {
|
|
65
|
+
if (maxWidth <= 0) return '';
|
|
66
|
+
if (displayWidth(text) <= maxWidth) return text;
|
|
67
|
+
if (maxWidth === 1) return '…';
|
|
68
|
+
const plain = stripAnsi(text);
|
|
69
|
+
return `${[...plain].slice(0, maxWidth - 1).join('')}…`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function terminalWidth(stdout) {
|
|
73
|
+
const columns = Number.isInteger(stdout.columns) ? stdout.columns : WIZARD_WIDTH;
|
|
74
|
+
return Math.max(12, Math.min(WIZARD_WIDTH, columns - 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function padEndPlain(text, width) {
|
|
78
|
+
return `${text}${' '.repeat(Math.max(0, width - displayWidth(text)))}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stepGlyph(state) {
|
|
82
|
+
if (state === 'done') return '◆';
|
|
83
|
+
if (state === 'active') return '*';
|
|
84
|
+
if (state === 'failed') return '!';
|
|
85
|
+
return '◇';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stepGlyphStyle(theme, state) {
|
|
89
|
+
if (state === 'failed') return theme.danger;
|
|
90
|
+
if (state === 'active') return theme.accent;
|
|
91
|
+
return theme.plain;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function stepHead(theme, state, title, { meta = '', width = WIZARD_WIDTH } = {}) {
|
|
95
|
+
const glyph = stepGlyph(state);
|
|
96
|
+
const glyphStyle = stepGlyphStyle(theme, state);
|
|
97
|
+
const titleStyle = state === 'active' ? theme.bold : theme.plain;
|
|
98
|
+
const left = `${glyph} ${title}`;
|
|
99
|
+
const right = fit(meta, Math.max(0, width - displayWidth(left) - 3));
|
|
100
|
+
const dots = Math.max(2, width - displayWidth(left) - displayWidth(right) - (right ? 2 : 1));
|
|
101
|
+
return ` ${glyphStyle(glyph)} ${titleStyle(title)} ${theme.dim('·'.repeat(dots))}${right ? ` ${right}` : ''}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function gutter(theme, text = '') {
|
|
105
|
+
return ` ${theme.dim('|')}${text ? ` ${text}` : ''}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function line(stdout, text = '') {
|
|
109
|
+
stdout.write(`${text}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class LiveRegion {
|
|
113
|
+
constructor(stdout) {
|
|
114
|
+
this.stdout = stdout;
|
|
115
|
+
this.lineCount = 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_erase() {
|
|
119
|
+
if (this.lineCount === 0) return;
|
|
120
|
+
this.stdout.write('\r');
|
|
121
|
+
if (this.lineCount > 1) this.stdout.write(`\x1b[${this.lineCount - 1}A`);
|
|
122
|
+
for (let index = 0; index < this.lineCount; index += 1) {
|
|
123
|
+
this.stdout.write('\x1b[2K');
|
|
124
|
+
if (index < this.lineCount - 1) this.stdout.write('\n');
|
|
125
|
+
}
|
|
126
|
+
if (this.lineCount > 1) this.stdout.write(`\x1b[${this.lineCount - 1}A`);
|
|
127
|
+
this.stdout.write('\r');
|
|
128
|
+
this.lineCount = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
paint(lines) {
|
|
132
|
+
this._erase();
|
|
133
|
+
const normalized = lines.map((item) => String(item));
|
|
134
|
+
if (normalized.length === 0) return;
|
|
135
|
+
this.stdout.write(normalized.join('\n'));
|
|
136
|
+
this.lineCount = normalized.length;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clear() {
|
|
140
|
+
this._erase();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
commit(lines) {
|
|
144
|
+
this.paint(lines);
|
|
145
|
+
if (this.lineCount > 0) this.stdout.write('\n');
|
|
146
|
+
this.lineCount = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function choiceRadio(theme, choice, selected) {
|
|
151
|
+
if (choice.disabled) return theme.dim('○');
|
|
152
|
+
return selected ? theme.accent('◉') : '○';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function choiceLabel(theme, choice, selected) {
|
|
156
|
+
if (choice.disabled) return theme.dim(choice.label);
|
|
157
|
+
return selected ? theme.bold(choice.label) : choice.label;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function defaultChoiceFrame({ theme, title, meta, choices, state, stdout, orientation }) {
|
|
161
|
+
const width = terminalWidth(stdout);
|
|
162
|
+
const rows = [stepHead(theme, 'active', title, { meta, width }), gutter(theme)];
|
|
163
|
+
if (orientation === 'horizontal') {
|
|
164
|
+
const actions = choices
|
|
165
|
+
.map((choice, index) => {
|
|
166
|
+
const selected = index === state.selected;
|
|
167
|
+
const label = selected ? theme.bold(choice.label) : theme.dim(choice.label);
|
|
168
|
+
return `${selected ? '▸' : ' '} ${label}`;
|
|
169
|
+
})
|
|
170
|
+
.join(' ');
|
|
171
|
+
rows.push(gutter(theme, fit(actions, width - 3)));
|
|
172
|
+
rows.push(gutter(theme));
|
|
173
|
+
rows.push(gutter(theme, theme.dim('←→ choose · ↵ confirm · esc cancel')));
|
|
174
|
+
return rows;
|
|
175
|
+
}
|
|
176
|
+
choices.forEach((choice, index) => {
|
|
177
|
+
const selected = index === state.selected;
|
|
178
|
+
const marker = selected ? theme.accent('▸') : ' ';
|
|
179
|
+
const radio = choiceRadio(theme, choice, selected);
|
|
180
|
+
const label = choiceLabel(theme, choice, selected);
|
|
181
|
+
rows.push(gutter(theme, fit(`${marker} ${radio} ${label}`, width - 3)));
|
|
182
|
+
});
|
|
183
|
+
rows.push(gutter(theme));
|
|
184
|
+
rows.push(gutter(theme, theme.dim('↑↓ move · ↵ confirm · esc cancel')));
|
|
185
|
+
return rows;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function selectChoice({
|
|
189
|
+
stdout,
|
|
190
|
+
reader,
|
|
191
|
+
live = new LiveRegion(stdout),
|
|
192
|
+
theme = createWizardTheme(stdout),
|
|
193
|
+
title,
|
|
194
|
+
meta = '',
|
|
195
|
+
choices,
|
|
196
|
+
initial = 0,
|
|
197
|
+
orientation = 'vertical',
|
|
198
|
+
renderFrame = defaultChoiceFrame,
|
|
199
|
+
}) {
|
|
200
|
+
let state = createSelectionState(choices, initial);
|
|
201
|
+
if (state.selected < 0) return null;
|
|
202
|
+
live.paint(renderFrame({ theme, title, meta, choices, state, stdout, orientation }));
|
|
203
|
+
while (state.status === 'active') {
|
|
204
|
+
const key = await reader.read();
|
|
205
|
+
const next = reduceSelection(state, key, choices, orientation);
|
|
206
|
+
if (key === 'resize' || next !== state) {
|
|
207
|
+
state = next;
|
|
208
|
+
if (state.status === 'active') {
|
|
209
|
+
live.paint(renderFrame({ theme, title, meta, choices, state, stdout, orientation }));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
live.clear();
|
|
214
|
+
return state.status === 'confirmed' ? state.value : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
CANCEL_KEYS,
|
|
219
|
+
LiveRegion,
|
|
220
|
+
WIZARD_SPINNER,
|
|
221
|
+
WIZARD_WIDTH,
|
|
222
|
+
beginTerminal,
|
|
223
|
+
createKeyReader,
|
|
224
|
+
createSelectionState,
|
|
225
|
+
createWizardTheme,
|
|
226
|
+
displayWidth,
|
|
227
|
+
fit,
|
|
228
|
+
gutter,
|
|
229
|
+
line,
|
|
230
|
+
padEndPlain,
|
|
231
|
+
parseKeys,
|
|
232
|
+
reduceSelection,
|
|
233
|
+
selectChoice,
|
|
234
|
+
stepHead,
|
|
235
|
+
stripAnsi,
|
|
236
|
+
terminalWidth,
|
|
237
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const {
|
|
2
|
+
LiveRegion,
|
|
3
|
+
createWizardTheme,
|
|
4
|
+
fit,
|
|
5
|
+
gutter,
|
|
6
|
+
line,
|
|
7
|
+
selectChoice,
|
|
8
|
+
stepHead,
|
|
9
|
+
terminalWidth,
|
|
10
|
+
} = require('./setup-wizard-terminal');
|
|
11
|
+
const {
|
|
12
|
+
createPlanState,
|
|
13
|
+
planFrame,
|
|
14
|
+
reducePlanState,
|
|
15
|
+
renderApplyFrame,
|
|
16
|
+
selectPlan,
|
|
17
|
+
} = require('./setup-wizard-plan-view');
|
|
18
|
+
const { ScanPresenter, formatSeconds, probeDetail } = require('./setup-wizard-scan-view');
|
|
19
|
+
|
|
20
|
+
function readinessStyle(theme, status) {
|
|
21
|
+
if (status === 'ready') return theme.success;
|
|
22
|
+
if (status === 'login-required') return theme.warning;
|
|
23
|
+
if (status === 'incompatible') return theme.danger;
|
|
24
|
+
return theme.dim;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function providerRadio(theme, choice, selected) {
|
|
28
|
+
if (choice.disabled) return theme.dim('○');
|
|
29
|
+
return selected ? theme.accent('◉') : '○';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function providerChoiceFrame({ theme, title, meta, choices, state, stdout }) {
|
|
33
|
+
const width = terminalWidth(stdout);
|
|
34
|
+
const rows = [stepHead(theme, 'active', title, { meta, width }), gutter(theme)];
|
|
35
|
+
choices.forEach((choice, index) => {
|
|
36
|
+
const selected = index === state.selected;
|
|
37
|
+
const cursor = selected ? theme.accent('▸') : ' ';
|
|
38
|
+
const radio = providerRadio(theme, choice, selected);
|
|
39
|
+
const style = readinessStyle(theme, choice.status);
|
|
40
|
+
const name = selected && !choice.disabled ? theme.bold(choice.label) : style(choice.label);
|
|
41
|
+
rows.push(gutter(theme, fit(`${cursor} ${radio} ${name} ${style(choice.status)}`, width - 3)));
|
|
42
|
+
if (selected && choice.detail) {
|
|
43
|
+
rows.push(gutter(theme, fit(theme.dim(` ${choice.detail}`), width - 3)));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
rows.push(gutter(theme));
|
|
47
|
+
rows.push(gutter(theme, theme.dim('↑↓ move · ↵ confirm · esc cancel')));
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class WizardRenderer {
|
|
52
|
+
constructor({ stdout, env = process.env, clock = globalThis, motion } = {}) {
|
|
53
|
+
this.stdout = stdout;
|
|
54
|
+
this.theme = createWizardTheme(stdout, env);
|
|
55
|
+
this.live = new LiveRegion(stdout);
|
|
56
|
+
this.clock = clock;
|
|
57
|
+
this.motion = motion ?? (env.CI !== 'true' && env.TERM !== 'dumb');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
intro() {
|
|
61
|
+
line(this.stdout, this.theme.bold(this.theme.accent('zeroshot')));
|
|
62
|
+
line(
|
|
63
|
+
this.stdout,
|
|
64
|
+
this.theme.dim('Independent execution. Verified changes. · read-only until Apply')
|
|
65
|
+
);
|
|
66
|
+
line(this.stdout);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scanPresenter() {
|
|
70
|
+
return new ScanPresenter({
|
|
71
|
+
stdout: this.stdout,
|
|
72
|
+
theme: this.theme,
|
|
73
|
+
live: this.live,
|
|
74
|
+
motion: this.motion,
|
|
75
|
+
clock: this.clock,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async choose({ title, meta, choices, initial, reader, provider = false }) {
|
|
80
|
+
const value = await selectChoice({
|
|
81
|
+
stdout: this.stdout,
|
|
82
|
+
reader,
|
|
83
|
+
live: this.live,
|
|
84
|
+
theme: this.theme,
|
|
85
|
+
title,
|
|
86
|
+
meta,
|
|
87
|
+
choices,
|
|
88
|
+
initial,
|
|
89
|
+
renderFrame: provider ? providerChoiceFrame : undefined,
|
|
90
|
+
});
|
|
91
|
+
if (value === null) return null;
|
|
92
|
+
const choice = choices.find((item) => item.value === value);
|
|
93
|
+
this.live.commit([
|
|
94
|
+
stepHead(this.theme, 'done', title, {
|
|
95
|
+
meta: choice.label,
|
|
96
|
+
width: terminalWidth(this.stdout),
|
|
97
|
+
}),
|
|
98
|
+
gutter(this.theme, choice.detail || choice.label),
|
|
99
|
+
gutter(this.theme),
|
|
100
|
+
]);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
plan(reader, buildModel) {
|
|
105
|
+
return selectPlan({
|
|
106
|
+
stdout: this.stdout,
|
|
107
|
+
reader,
|
|
108
|
+
live: this.live,
|
|
109
|
+
theme: this.theme,
|
|
110
|
+
buildModel,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
applyStarted() {
|
|
115
|
+
this.live.paint(
|
|
116
|
+
renderApplyFrame({ stdout: this.stdout, theme: this.theme, results: [], verified: false })
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
applyReceipts(results) {
|
|
121
|
+
for (let count = 1; count <= results.length; count += 1) {
|
|
122
|
+
this.live.paint(
|
|
123
|
+
renderApplyFrame({
|
|
124
|
+
stdout: this.stdout,
|
|
125
|
+
theme: this.theme,
|
|
126
|
+
results: results.slice(0, count),
|
|
127
|
+
verified: false,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
applyVerified(results) {
|
|
134
|
+
this.live.commit(
|
|
135
|
+
renderApplyFrame({ stdout: this.stdout, theme: this.theme, results, verified: true })
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
failed(title, error, results = []) {
|
|
140
|
+
this.live.commit(
|
|
141
|
+
renderApplyFrame({ stdout: this.stdout, theme: this.theme, results, failed: true })
|
|
142
|
+
);
|
|
143
|
+
line(this.stdout, `${this.theme.danger('!')} ${title}`);
|
|
144
|
+
line(this.stdout, ` ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fatal(title, error) {
|
|
148
|
+
this.live.clear();
|
|
149
|
+
line(this.stdout, `${this.theme.danger('!')} ${title}`);
|
|
150
|
+
line(this.stdout, ` ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ready(provider, isolation) {
|
|
154
|
+
const width = terminalWidth(this.stdout);
|
|
155
|
+
this.live.commit([
|
|
156
|
+
stepHead(this.theme, 'done', 'Ready', { meta: `${provider} · ${isolation}`, width }),
|
|
157
|
+
gutter(this.theme, this.theme.bold('zeroshot run "Describe the change"')),
|
|
158
|
+
gutter(this.theme, 'undo: zeroshot setup undo'),
|
|
159
|
+
gutter(this.theme),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
cancelled() {
|
|
164
|
+
this.live.clear();
|
|
165
|
+
line(this.stdout, this.theme.dim('Setup cancelled. Nothing was written.'));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
ScanPresenter,
|
|
171
|
+
WizardRenderer,
|
|
172
|
+
createPlanState,
|
|
173
|
+
formatSeconds,
|
|
174
|
+
planFrame,
|
|
175
|
+
probeDetail,
|
|
176
|
+
providerChoiceFrame,
|
|
177
|
+
reducePlanState,
|
|
178
|
+
renderApplyFrame,
|
|
179
|
+
selectPlan,
|
|
180
|
+
};
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
const { isDeepStrictEqual } = require('util');
|
|
2
|
+
|
|
3
|
+
const { applyDecisionValues } = require('../../lib/setup-apply');
|
|
4
|
+
const { buildSetupPlan, getNestedValue, resolveDecisionPath } = require('../../lib/setup-plan');
|
|
5
|
+
const {
|
|
6
|
+
getSettingsFile,
|
|
7
|
+
loadSettings,
|
|
8
|
+
mutateSettings,
|
|
9
|
+
settingsFileExists,
|
|
10
|
+
} = require('../../lib/settings');
|
|
11
|
+
const { readRepoSettings } = require('../../lib/repo-settings');
|
|
12
|
+
const { runPreflight } = require('../../src/preflight');
|
|
13
|
+
const { providerChoices } = require('./setup-provider-readiness');
|
|
14
|
+
const { scanSetupEnvironment } = require('./setup-scanner');
|
|
15
|
+
const {
|
|
16
|
+
buildWizardDecisions,
|
|
17
|
+
buildWizardPlanModel,
|
|
18
|
+
collectSetupScan,
|
|
19
|
+
isolationChoices,
|
|
20
|
+
preferredIndex,
|
|
21
|
+
} = require('./setup-wizard-model');
|
|
22
|
+
const {
|
|
23
|
+
beginTerminal,
|
|
24
|
+
createKeyReader,
|
|
25
|
+
line,
|
|
26
|
+
parseKeys,
|
|
27
|
+
selectChoice,
|
|
28
|
+
stripAnsi,
|
|
29
|
+
} = require('./setup-wizard-terminal');
|
|
30
|
+
const { WizardRenderer } = require('./setup-wizard-view');
|
|
31
|
+
|
|
32
|
+
function defaultDeps() {
|
|
33
|
+
return {
|
|
34
|
+
applyDecisionValues,
|
|
35
|
+
buildSetupPlan,
|
|
36
|
+
getSettingsFile,
|
|
37
|
+
loadSettings,
|
|
38
|
+
mutateSettings,
|
|
39
|
+
readRepoSettings,
|
|
40
|
+
runPreflight,
|
|
41
|
+
scanSetupEnvironment,
|
|
42
|
+
settingsFileExists,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function verifyPersistedDecisions(decisions, persisted) {
|
|
47
|
+
for (const [decisionId, expected] of Object.entries(decisions)) {
|
|
48
|
+
const target = resolveDecisionPath(decisionId);
|
|
49
|
+
if (!target || target.scope !== 'global') continue;
|
|
50
|
+
const actual = getNestedValue(persisted, target.path);
|
|
51
|
+
if (decisionId.startsWith('providerLevel.')) {
|
|
52
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
53
|
+
if (actual?.[key] !== value) {
|
|
54
|
+
throw new Error(`Persisted ${target.path}.${key} did not match the approved plan`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else if (!isDeepStrictEqual(actual, expected)) {
|
|
58
|
+
throw new Error(`Persisted ${target.path} did not match the approved plan`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function preflightFailure(result) {
|
|
64
|
+
const message = result.errors
|
|
65
|
+
.map((error) => stripAnsi(error).trim())
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join('\n');
|
|
68
|
+
return new Error(message || 'Setup preflight failed');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function verifyAppliedSetup({ resolved, decisions, provider, isolation, cwd }) {
|
|
72
|
+
const persisted = resolved.loadSettings();
|
|
73
|
+
verifyPersistedDecisions(decisions, persisted);
|
|
74
|
+
const preflight = await resolved.runPreflight({
|
|
75
|
+
cwd,
|
|
76
|
+
settings: persisted,
|
|
77
|
+
provider,
|
|
78
|
+
requireDocker: isolation === 'docker',
|
|
79
|
+
requireGit: isolation === 'worktree',
|
|
80
|
+
quiet: true,
|
|
81
|
+
});
|
|
82
|
+
if (!preflight.valid) throw preflightFailure(preflight);
|
|
83
|
+
return { persisted, preflight };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderBlockedProviders(renderer, choices) {
|
|
87
|
+
const theme = renderer.theme;
|
|
88
|
+
line(renderer.stdout, theme.danger('No provider is ready for the selected isolation.'));
|
|
89
|
+
for (const choice of choices) {
|
|
90
|
+
line(
|
|
91
|
+
renderer.stdout,
|
|
92
|
+
` ${choice.label}: ${choice.status}${choice.detail ? ` · ${choice.detail}` : ''}`
|
|
93
|
+
);
|
|
94
|
+
const action =
|
|
95
|
+
choice.status === 'unavailable' ? choice.installInstructions : choice.authInstructions;
|
|
96
|
+
if (action) line(renderer.stdout, ` ${action.split('\n')[0]}`);
|
|
97
|
+
}
|
|
98
|
+
line(renderer.stdout, 'Run `zeroshot setup` again after resolving one provider.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cancelledResult(renderer, plan) {
|
|
102
|
+
renderer.cancelled();
|
|
103
|
+
return { status: 'cancelled', applied: false, exitCode: 130, plan };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasAvailableProvider(probes) {
|
|
107
|
+
return Object.entries(probes).some(
|
|
108
|
+
([id, probe]) => id.startsWith('provider:') && probe.available
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function blockedProviderResult(renderer, request) {
|
|
113
|
+
renderBlockedProviders(renderer, providerChoices(request));
|
|
114
|
+
return { status: 'no-provider', applied: false, exitCode: 1, plan: request.plan };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function chooseWizardConfiguration({ renderer, reader, plan, probes, settings }) {
|
|
118
|
+
const isolations = isolationChoices(plan);
|
|
119
|
+
if (!hasAvailableProvider(probes)) {
|
|
120
|
+
const preview = isolations.find((choice) => !choice.disabled)?.value || 'none';
|
|
121
|
+
return {
|
|
122
|
+
result: blockedProviderResult(renderer, { plan, probes, isolation: preview, settings }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const isolation = await renderer.choose({
|
|
126
|
+
title: 'Isolation',
|
|
127
|
+
meta: 'execution context',
|
|
128
|
+
choices: isolations,
|
|
129
|
+
initial: preferredIndex(isolations, plan.recommended.defaultIsolation),
|
|
130
|
+
reader,
|
|
131
|
+
});
|
|
132
|
+
if (!isolation) return { result: cancelledResult(renderer, plan) };
|
|
133
|
+
const providers = providerChoices({ plan, probes, isolation, settings });
|
|
134
|
+
if (!providers.some((choice) => !choice.disabled)) {
|
|
135
|
+
return {
|
|
136
|
+
result: blockedProviderResult(renderer, { plan, probes, isolation, settings }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const provider = await renderer.choose({
|
|
140
|
+
title: 'Provider',
|
|
141
|
+
meta: `${isolation}-compatible`,
|
|
142
|
+
choices: providers,
|
|
143
|
+
initial: preferredIndex(providers, plan.recommended.defaultProvider),
|
|
144
|
+
reader,
|
|
145
|
+
provider: true,
|
|
146
|
+
});
|
|
147
|
+
return provider ? { provider, isolation } : { result: cancelledResult(renderer, plan) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function applyApprovedPlan({
|
|
151
|
+
renderer,
|
|
152
|
+
resolved,
|
|
153
|
+
plan,
|
|
154
|
+
provider,
|
|
155
|
+
isolation,
|
|
156
|
+
enabledGroups,
|
|
157
|
+
cwd,
|
|
158
|
+
}) {
|
|
159
|
+
renderer.applyStarted();
|
|
160
|
+
const decisions = buildWizardDecisions(plan, provider, isolation, enabledGroups);
|
|
161
|
+
let receipts = [];
|
|
162
|
+
try {
|
|
163
|
+
receipts = resolved.applyDecisionValues({ decisions, cwd });
|
|
164
|
+
renderer.applyReceipts(receipts);
|
|
165
|
+
const verification = await verifyAppliedSetup({
|
|
166
|
+
resolved,
|
|
167
|
+
decisions,
|
|
168
|
+
provider,
|
|
169
|
+
isolation,
|
|
170
|
+
cwd,
|
|
171
|
+
});
|
|
172
|
+
resolved.mutateSettings((current) => {
|
|
173
|
+
current.setupVersion = 1;
|
|
174
|
+
});
|
|
175
|
+
renderer.applyVerified(receipts);
|
|
176
|
+
renderer.ready(provider, isolation);
|
|
177
|
+
return {
|
|
178
|
+
status: 'applied',
|
|
179
|
+
applied: true,
|
|
180
|
+
exitCode: 0,
|
|
181
|
+
decisions,
|
|
182
|
+
results: receipts,
|
|
183
|
+
verification,
|
|
184
|
+
plan,
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
renderer.failed('Setup was not completed.', error, receipts);
|
|
188
|
+
return {
|
|
189
|
+
status: 'failed',
|
|
190
|
+
applied: receipts.some((receipt) => receipt.applied),
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
error,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function runSetupWizard({
|
|
198
|
+
cwd = process.cwd(),
|
|
199
|
+
stdin = process.stdin,
|
|
200
|
+
stdout = process.stdout,
|
|
201
|
+
env = process.env,
|
|
202
|
+
deps = {},
|
|
203
|
+
} = {}) {
|
|
204
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
205
|
+
line(stdout, 'Interactive setup requires a TTY.');
|
|
206
|
+
line(stdout, 'Use `zeroshot setup plan` and `zeroshot setup apply --decisions <file>`.');
|
|
207
|
+
return { status: 'non-interactive', applied: false, exitCode: 1 };
|
|
208
|
+
}
|
|
209
|
+
const resolved = { ...defaultDeps(), ...deps };
|
|
210
|
+
const settings = resolved.loadSettings();
|
|
211
|
+
settings.__meta = { fileExists: resolved.settingsFileExists() };
|
|
212
|
+
const { settings: repoSettings } = resolved.readRepoSettings(cwd);
|
|
213
|
+
const restoreTerminal = beginTerminal(stdin, stdout);
|
|
214
|
+
const reader = createKeyReader(stdin, stdout);
|
|
215
|
+
const renderer = new WizardRenderer({
|
|
216
|
+
stdout,
|
|
217
|
+
env,
|
|
218
|
+
clock: resolved.clock,
|
|
219
|
+
motion: resolved.motion,
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
renderer.intro();
|
|
223
|
+
const scanPresenter = renderer.scanPresenter();
|
|
224
|
+
const scan = await collectSetupScan({
|
|
225
|
+
cwd,
|
|
226
|
+
settings,
|
|
227
|
+
repoSettings,
|
|
228
|
+
env,
|
|
229
|
+
resolved,
|
|
230
|
+
deps,
|
|
231
|
+
onProgress: (event) => scanPresenter.handle(event),
|
|
232
|
+
});
|
|
233
|
+
scanPresenter.commit(scan);
|
|
234
|
+
const selection = await chooseWizardConfiguration({
|
|
235
|
+
renderer,
|
|
236
|
+
reader,
|
|
237
|
+
plan: scan.plan,
|
|
238
|
+
probes: scan.probes,
|
|
239
|
+
settings,
|
|
240
|
+
});
|
|
241
|
+
if (selection.result) return selection.result;
|
|
242
|
+
const settingsFile = resolved.getSettingsFile();
|
|
243
|
+
const buildModel = (enabledGroups) =>
|
|
244
|
+
buildWizardPlanModel({
|
|
245
|
+
plan: scan.plan,
|
|
246
|
+
settings,
|
|
247
|
+
settingsFile,
|
|
248
|
+
provider: selection.provider,
|
|
249
|
+
isolation: selection.isolation,
|
|
250
|
+
enabledGroups,
|
|
251
|
+
});
|
|
252
|
+
const approval = await renderer.plan(reader, buildModel);
|
|
253
|
+
if (approval.action !== 'apply') return cancelledResult(renderer, scan.plan);
|
|
254
|
+
return applyApprovedPlan({
|
|
255
|
+
renderer,
|
|
256
|
+
resolved,
|
|
257
|
+
plan: scan.plan,
|
|
258
|
+
provider: selection.provider,
|
|
259
|
+
isolation: selection.isolation,
|
|
260
|
+
enabledGroups: approval.enabled,
|
|
261
|
+
cwd,
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
renderer.fatal('Setup scan failed.', error);
|
|
265
|
+
return { status: 'failed', applied: false, exitCode: 1, error };
|
|
266
|
+
} finally {
|
|
267
|
+
reader.close();
|
|
268
|
+
restoreTerminal();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
runSetupWizard,
|
|
274
|
+
buildWizardDecisions,
|
|
275
|
+
buildWizardPlanModel,
|
|
276
|
+
isolationChoices,
|
|
277
|
+
parseKeys,
|
|
278
|
+
preferredIndex,
|
|
279
|
+
selectChoice,
|
|
280
|
+
verifyPersistedDecisions,
|
|
281
|
+
};
|