gsdd-cli 0.18.5 → 0.19.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/LICENSE +21 -21
- package/README.md +609 -608
- package/agents/DISTILLATION.md +421 -421
- package/agents/README.md +62 -62
- package/agents/approach-explorer.md +361 -361
- package/agents/debugger.md +82 -82
- package/agents/executor.md +394 -394
- package/agents/integration-checker.md +318 -318
- package/agents/mapper.md +103 -103
- package/agents/planner.md +313 -313
- package/agents/researcher.md +84 -84
- package/agents/roadmapper.md +296 -296
- package/agents/synthesizer.md +236 -236
- package/agents/verifier.md +337 -337
- package/bin/adapters/agents.mjs +34 -34
- package/bin/adapters/claude.mjs +191 -191
- package/bin/adapters/codex.mjs +85 -85
- package/bin/adapters/index.mjs +20 -20
- package/bin/adapters/opencode.mjs +278 -278
- package/bin/gsdd.mjs +123 -116
- package/bin/lib/cli-utils.mjs +28 -28
- package/bin/lib/evidence-contract.mjs +112 -112
- package/bin/lib/file-ops.mjs +186 -144
- package/bin/lib/health-truth.mjs +181 -178
- package/bin/lib/health.mjs +246 -226
- package/bin/lib/init-flow.mjs +247 -231
- package/bin/lib/init-prompts.mjs +248 -247
- package/bin/lib/init-runtime.mjs +191 -190
- package/bin/lib/init.mjs +17 -17
- package/bin/lib/lifecycle-preflight.mjs +347 -325
- package/bin/lib/lifecycle-state.mjs +351 -267
- package/bin/lib/manifest.mjs +116 -114
- package/bin/lib/models.mjs +411 -411
- package/bin/lib/phase.mjs +360 -358
- package/bin/lib/plan-constants.mjs +30 -30
- package/bin/lib/provenance.mjs +109 -106
- package/bin/lib/rendering.mjs +115 -83
- package/bin/lib/runtime-freshness.mjs +214 -214
- package/bin/lib/templates.mjs +225 -224
- package/bin/lib/workspace-root.mjs +2 -1
- package/distilled/DESIGN.md +2333 -2323
- package/distilled/EVIDENCE-INDEX.md +394 -392
- package/distilled/README.md +196 -193
- package/distilled/SKILL.md +86 -85
- package/distilled/templates/agents.block.md +21 -21
- package/distilled/templates/agents.md +6 -6
- package/distilled/templates/approach.md +232 -232
- package/distilled/templates/auth-matrix.md +78 -78
- package/distilled/templates/brownfield-change/CHANGE.md +99 -0
- package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
- package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
- package/distilled/templates/codebase/architecture.md +110 -110
- package/distilled/templates/codebase/concerns.md +95 -95
- package/distilled/templates/codebase/conventions.md +193 -193
- package/distilled/templates/codebase/stack.md +96 -96
- package/distilled/templates/delegates/approach-explorer.md +25 -25
- package/distilled/templates/delegates/mapper-arch.md +26 -26
- package/distilled/templates/delegates/mapper-concerns.md +27 -27
- package/distilled/templates/delegates/mapper-quality.md +28 -28
- package/distilled/templates/delegates/mapper-tech.md +25 -25
- package/distilled/templates/delegates/plan-checker.md +68 -68
- package/distilled/templates/delegates/researcher-architecture.md +30 -30
- package/distilled/templates/delegates/researcher-features.md +30 -30
- package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
- package/distilled/templates/delegates/researcher-stack.md +30 -30
- package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
- package/distilled/templates/research/architecture.md +57 -57
- package/distilled/templates/research/features.md +23 -23
- package/distilled/templates/research/pitfalls.md +46 -46
- package/distilled/templates/research/stack.md +45 -45
- package/distilled/templates/research/summary.md +67 -67
- package/distilled/templates/roadmap.md +74 -62
- package/distilled/templates/spec.md +110 -110
- package/distilled/workflows/audit-milestone.md +275 -271
- package/distilled/workflows/complete-milestone.md +336 -332
- package/distilled/workflows/execute.md +454 -449
- package/distilled/workflows/map-codebase.md +253 -253
- package/distilled/workflows/new-milestone.md +242 -238
- package/distilled/workflows/new-project.md +398 -398
- package/distilled/workflows/pause.md +160 -156
- package/distilled/workflows/plan-milestone-gaps.md +183 -183
- package/distilled/workflows/plan.md +451 -447
- package/distilled/workflows/progress.md +227 -223
- package/distilled/workflows/quick.md +351 -347
- package/distilled/workflows/resume.md +220 -212
- package/distilled/workflows/verify-work.md +260 -260
- package/distilled/workflows/verify.md +431 -429
- package/docs/BROWNFIELD-PROOF.md +95 -95
- package/docs/RUNTIME-SUPPORT.md +80 -69
- package/docs/USER-GUIDE.md +394 -386
- package/docs/VERIFICATION-DISCIPLINE.md +59 -59
- package/docs/claude/context-monitor.md +98 -98
- package/docs/proof/consumer-node-cli/README.md +37 -37
- package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
- package/docs/proof/consumer-node-cli/SPEC.md +17 -17
- package/docs/proof/consumer-node-cli/brief.md +9 -9
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
- package/package.json +62 -61
package/bin/lib/init-prompts.mjs
CHANGED
|
@@ -1,247 +1,248 @@
|
|
|
1
|
-
import * as readline from 'readline';
|
|
2
|
-
import { DEFAULT_GIT_PROTOCOL, resolveCost, resolveRigor } from './models.mjs';
|
|
3
|
-
import { buildRuntimeChoices, INIT_VERSION, resolveWizardAdapterTargets } from './init-runtime.mjs';
|
|
4
|
-
|
|
5
|
-
const ANSI = {
|
|
6
|
-
reset: '\x1b[0m',
|
|
7
|
-
dim: '\x1b[2m',
|
|
8
|
-
bold: '\x1b[1m',
|
|
9
|
-
cyan: '\x1b[36m',
|
|
10
|
-
green: '\x1b[32m',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function createInitPromptApi({ input = process.stdin, output = process.stdout } = {}) {
|
|
14
|
-
return {
|
|
15
|
-
async runInitWizard({ cwd, adapters }) {
|
|
16
|
-
output.write(`${ANSI.bold}${ANSI.cyan}GSDD Install Wizard${ANSI.reset}\n`);
|
|
17
|
-
output.write(`${ANSI.dim}Portable skills are always installed. Select the runtimes you want GSDD to feel native in.${ANSI.reset}\n\n`);
|
|
18
|
-
|
|
19
|
-
const runtimeChoices = buildRuntimeChoices(adapters);
|
|
20
|
-
const selectedRuntimes = await promptMultiSelect({
|
|
21
|
-
input,
|
|
22
|
-
output,
|
|
23
|
-
title: 'Step 1 of 3 - Select runtimes',
|
|
24
|
-
hint: 'Space toggles, Enter confirms.',
|
|
25
|
-
choices: runtimeChoices,
|
|
26
|
-
});
|
|
27
|
-
const installGovernance = await promptConfirm({
|
|
28
|
-
input,
|
|
29
|
-
output,
|
|
30
|
-
title: 'Step 2 of 3 - Repo-wide AGENTS.md governance',
|
|
31
|
-
prompt: 'Install repo-wide AGENTS.md rules?',
|
|
32
|
-
defaultValue: false,
|
|
33
|
-
details: [
|
|
34
|
-
'Why care: consistent behavioral discipline across sessions and mixed-runtime teams.',
|
|
35
|
-
'Why skip it: it writes to the repo root, and your selected runtimes already discover .agents/skills/ natively.',
|
|
36
|
-
],
|
|
37
|
-
});
|
|
38
|
-
const config = await promptForConfig(cwd, { input, output });
|
|
39
|
-
return {
|
|
40
|
-
selectedRuntimes,
|
|
41
|
-
adapterTargets: resolveWizardAdapterTargets(selectedRuntimes, installGovernance),
|
|
42
|
-
config,
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
promptForConfig(cwd) {
|
|
46
|
-
return promptForConfig(cwd, { input, output });
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function promptForConfig(cwd, { input = process.stdin, output = process.stdout } = {}) {
|
|
52
|
-
output.write(`\n${ANSI.bold}Step 3 of 3 - Configure planning defaults${ANSI.reset}\n`);
|
|
53
|
-
|
|
54
|
-
const rigor = await promptSingleSelect({
|
|
55
|
-
input,
|
|
56
|
-
output,
|
|
57
|
-
title: 'Rigor',
|
|
58
|
-
choices: [
|
|
59
|
-
{ value: 'quick', label: 'quick', description: 'Faster setup with lighter planning rigor' },
|
|
60
|
-
{ value: 'balanced', label: 'balanced', description: 'Recommended default for most projects' },
|
|
61
|
-
{ value: 'thorough', label: 'thorough', description: 'Maximum research and plan review rigor' },
|
|
62
|
-
],
|
|
63
|
-
defaultIndex: 1,
|
|
64
|
-
});
|
|
65
|
-
const cost = await promptSingleSelect({
|
|
66
|
-
input,
|
|
67
|
-
output,
|
|
68
|
-
title: 'Cost',
|
|
69
|
-
choices: [
|
|
70
|
-
{ value: 'budget', label: 'budget', description: 'Lower-cost model profile and no parallelization' },
|
|
71
|
-
{ value: 'balanced', label: 'balanced', description: 'Recommended cost/quality tradeoff' },
|
|
72
|
-
{ value: 'quality', label: 'quality', description: 'Maximum quality with parallel work enabled' },
|
|
73
|
-
],
|
|
74
|
-
defaultIndex: 1,
|
|
75
|
-
});
|
|
76
|
-
const commitDocs = await promptSingleSelect({
|
|
77
|
-
input,
|
|
78
|
-
output,
|
|
79
|
-
title: 'Planning docs in git',
|
|
80
|
-
choices: [
|
|
81
|
-
{ value: true, label: 'yes', description: 'Track .planning/ in git.' },
|
|
82
|
-
{ value: false, label: 'no', description: 'Keep .planning/ local only.' },
|
|
83
|
-
],
|
|
84
|
-
defaultIndex: 0,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const rigorConfig = resolveRigor(rigor);
|
|
88
|
-
const costConfig = resolveCost(cost);
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
...rigorConfig,
|
|
92
|
-
...costConfig,
|
|
93
|
-
commitDocs,
|
|
94
|
-
gitProtocol: { ...DEFAULT_GIT_PROTOCOL },
|
|
95
|
-
initVersion: INIT_VERSION,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function yesNoChoices(prompt, defaultYes) {
|
|
100
|
-
return [
|
|
101
|
-
{ value: true, label: 'yes', description: `${prompt} Yes.` },
|
|
102
|
-
{ value: false, label: 'no', description: `${prompt} No.` },
|
|
103
|
-
].sort((a, b) => {
|
|
104
|
-
if (defaultYes) return a.value === true ? -1 : 1;
|
|
105
|
-
return a.value === false ? -1 : 1;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export async function promptConfirm({ input, output, title, prompt, defaultValue, details = [] }) {
|
|
110
|
-
if (typeof input.resume === 'function') input.resume();
|
|
111
|
-
output.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
|
|
112
|
-
for (const detail of details) {
|
|
113
|
-
output.write(` ${ANSI.dim}${detail}${ANSI.reset}\n`);
|
|
114
|
-
}
|
|
115
|
-
const rl = readline.createInterface({ input, output });
|
|
116
|
-
const suffix = defaultValue ? '[Y/n]' : '[y/N]';
|
|
117
|
-
try {
|
|
118
|
-
const answer = await new Promise((resolve) => rl.question(` ${prompt} ${suffix}: `, resolve));
|
|
119
|
-
const normalized = answer.trim().toLowerCase();
|
|
120
|
-
if (!normalized) return defaultValue;
|
|
121
|
-
return normalized === 'y' || normalized === 'yes';
|
|
122
|
-
} finally {
|
|
123
|
-
rl.close();
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function promptSingleSelect({ input, output, title, choices, defaultIndex = 0 }) {
|
|
128
|
-
const selected = await promptChoiceList({
|
|
129
|
-
input,
|
|
130
|
-
output,
|
|
131
|
-
title,
|
|
132
|
-
choices: choices.map((choice, index) => ({
|
|
133
|
-
...choice,
|
|
134
|
-
selected: index === defaultIndex,
|
|
135
|
-
detected: false,
|
|
136
|
-
})),
|
|
137
|
-
multi: false,
|
|
138
|
-
});
|
|
139
|
-
return selected[0];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function promptMultiSelect({ input, output, title, hint, choices }) {
|
|
143
|
-
return promptChoiceList({ input, output, title, hint, choices, multi: true });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function promptChoiceList({ input, output, title, hint = 'Use arrows to move. Enter confirms.', choices, multi }) {
|
|
147
|
-
if (typeof input.resume === 'function') input.resume();
|
|
148
|
-
readline.emitKeypressEvents(input);
|
|
149
|
-
const previousRawMode = typeof input.isRaw === 'boolean' ? input.isRaw : false;
|
|
150
|
-
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
151
|
-
|
|
152
|
-
let cursor = Math.max(0, choices.findIndex((choice) => choice.selected));
|
|
153
|
-
let renderedLines = 0;
|
|
154
|
-
|
|
155
|
-
const selectCursor = () => {
|
|
156
|
-
if (multi) return;
|
|
157
|
-
for (const choice of choices) choice.selected = false;
|
|
158
|
-
choices[cursor].selected = true;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const measureLines = (text) => {
|
|
162
|
-
const columns = Math.max(20, Number(output.columns) || 80);
|
|
163
|
-
const visible = stripAnsi(text);
|
|
164
|
-
return visible.split('\n').reduce((total, line) => {
|
|
165
|
-
const width = Math.max(1, line.length);
|
|
166
|
-
return total + Math.ceil(width / columns);
|
|
167
|
-
}, 0);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const render = () => {
|
|
171
|
-
if (renderedLines > 0) {
|
|
172
|
-
readline.moveCursor(output, 0, -renderedLines);
|
|
173
|
-
}
|
|
174
|
-
readline.cursorTo(output, 0);
|
|
175
|
-
readline.clearScreenDown(output);
|
|
176
|
-
const lines = [`${ANSI.bold}${title}${ANSI.reset}`];
|
|
177
|
-
if (hint) lines.push(`${ANSI.dim}${hint}${ANSI.reset}`);
|
|
178
|
-
lines.push('');
|
|
179
|
-
for (let i = 0; i < choices.length; i++) {
|
|
180
|
-
const choice = choices[i];
|
|
181
|
-
const pointer = i === cursor ? `${ANSI.green}>${ANSI.reset}` : ' ';
|
|
182
|
-
const mark = choice.selected ? `${ANSI.green}[x]${ANSI.reset}` : '[ ]';
|
|
183
|
-
const detected = choice.detected ? ` ${ANSI.dim}(detected)${ANSI.reset}` : '';
|
|
184
|
-
lines.push(`${pointer} ${mark} ${choice.label}${detected}`);
|
|
185
|
-
lines.push(` ${ANSI.dim}${choice.description}${ANSI.reset}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
renderedLines = 0;
|
|
189
|
-
for (const line of lines) {
|
|
190
|
-
output.write(`${line}\n`);
|
|
191
|
-
renderedLines += measureLines(line);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
render();
|
|
196
|
-
|
|
197
|
-
const values = await new Promise((resolve, reject) => {
|
|
198
|
-
const cleanup = () => {
|
|
199
|
-
input.off('keypress', onKeypress);
|
|
200
|
-
if (typeof input.setRawMode === 'function') input.setRawMode(previousRawMode);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
choices
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
import { DEFAULT_GIT_PROTOCOL, resolveCost, resolveRigor } from './models.mjs';
|
|
3
|
+
import { buildRuntimeChoices, INIT_VERSION, resolveWizardAdapterTargets } from './init-runtime.mjs';
|
|
4
|
+
|
|
5
|
+
const ANSI = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createInitPromptApi({ input = process.stdin, output = process.stdout } = {}) {
|
|
14
|
+
return {
|
|
15
|
+
async runInitWizard({ cwd, adapters }) {
|
|
16
|
+
output.write(`${ANSI.bold}${ANSI.cyan}GSDD Install Wizard${ANSI.reset}\n`);
|
|
17
|
+
output.write(`${ANSI.dim}Portable skills are always installed. Select the runtimes you want GSDD to feel native in.${ANSI.reset}\n\n`);
|
|
18
|
+
|
|
19
|
+
const runtimeChoices = buildRuntimeChoices(adapters);
|
|
20
|
+
const selectedRuntimes = await promptMultiSelect({
|
|
21
|
+
input,
|
|
22
|
+
output,
|
|
23
|
+
title: 'Step 1 of 3 - Select runtimes',
|
|
24
|
+
hint: 'Space toggles, Enter confirms.',
|
|
25
|
+
choices: runtimeChoices,
|
|
26
|
+
});
|
|
27
|
+
const installGovernance = await promptConfirm({
|
|
28
|
+
input,
|
|
29
|
+
output,
|
|
30
|
+
title: 'Step 2 of 3 - Repo-wide AGENTS.md governance',
|
|
31
|
+
prompt: 'Install repo-wide AGENTS.md rules?',
|
|
32
|
+
defaultValue: false,
|
|
33
|
+
details: [
|
|
34
|
+
'Why care: consistent behavioral discipline across sessions and mixed-runtime teams.',
|
|
35
|
+
'Why skip it: it writes to the repo root, and your selected runtimes already discover .agents/skills/ natively.',
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
const config = await promptForConfig(cwd, { input, output });
|
|
39
|
+
return {
|
|
40
|
+
selectedRuntimes,
|
|
41
|
+
adapterTargets: resolveWizardAdapterTargets(selectedRuntimes, installGovernance),
|
|
42
|
+
config,
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
promptForConfig(cwd) {
|
|
46
|
+
return promptForConfig(cwd, { input, output });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function promptForConfig(cwd, { input = process.stdin, output = process.stdout } = {}) {
|
|
52
|
+
output.write(`\n${ANSI.bold}Step 3 of 3 - Configure planning defaults${ANSI.reset}\n`);
|
|
53
|
+
|
|
54
|
+
const rigor = await promptSingleSelect({
|
|
55
|
+
input,
|
|
56
|
+
output,
|
|
57
|
+
title: 'Rigor',
|
|
58
|
+
choices: [
|
|
59
|
+
{ value: 'quick', label: 'quick', description: 'Faster setup with lighter planning rigor' },
|
|
60
|
+
{ value: 'balanced', label: 'balanced', description: 'Recommended default for most projects' },
|
|
61
|
+
{ value: 'thorough', label: 'thorough', description: 'Maximum research and plan review rigor' },
|
|
62
|
+
],
|
|
63
|
+
defaultIndex: 1,
|
|
64
|
+
});
|
|
65
|
+
const cost = await promptSingleSelect({
|
|
66
|
+
input,
|
|
67
|
+
output,
|
|
68
|
+
title: 'Cost',
|
|
69
|
+
choices: [
|
|
70
|
+
{ value: 'budget', label: 'budget', description: 'Lower-cost model profile and no parallelization' },
|
|
71
|
+
{ value: 'balanced', label: 'balanced', description: 'Recommended cost/quality tradeoff' },
|
|
72
|
+
{ value: 'quality', label: 'quality', description: 'Maximum quality with parallel work enabled' },
|
|
73
|
+
],
|
|
74
|
+
defaultIndex: 1,
|
|
75
|
+
});
|
|
76
|
+
const commitDocs = await promptSingleSelect({
|
|
77
|
+
input,
|
|
78
|
+
output,
|
|
79
|
+
title: 'Planning docs in git',
|
|
80
|
+
choices: [
|
|
81
|
+
{ value: true, label: 'yes', description: 'Track .planning/ in git.' },
|
|
82
|
+
{ value: false, label: 'no', description: 'Keep .planning/ local only.' },
|
|
83
|
+
],
|
|
84
|
+
defaultIndex: 0,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const rigorConfig = resolveRigor(rigor);
|
|
88
|
+
const costConfig = resolveCost(cost);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...rigorConfig,
|
|
92
|
+
...costConfig,
|
|
93
|
+
commitDocs,
|
|
94
|
+
gitProtocol: { ...DEFAULT_GIT_PROTOCOL },
|
|
95
|
+
initVersion: INIT_VERSION,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function yesNoChoices(prompt, defaultYes) {
|
|
100
|
+
return [
|
|
101
|
+
{ value: true, label: 'yes', description: `${prompt} Yes.` },
|
|
102
|
+
{ value: false, label: 'no', description: `${prompt} No.` },
|
|
103
|
+
].sort((a, b) => {
|
|
104
|
+
if (defaultYes) return a.value === true ? -1 : 1;
|
|
105
|
+
return a.value === false ? -1 : 1;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function promptConfirm({ input, output, title, prompt, defaultValue, details = [] }) {
|
|
110
|
+
if (typeof input.resume === 'function') input.resume();
|
|
111
|
+
output.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
|
|
112
|
+
for (const detail of details) {
|
|
113
|
+
output.write(` ${ANSI.dim}${detail}${ANSI.reset}\n`);
|
|
114
|
+
}
|
|
115
|
+
const rl = readline.createInterface({ input, output });
|
|
116
|
+
const suffix = defaultValue ? '[Y/n]' : '[y/N]';
|
|
117
|
+
try {
|
|
118
|
+
const answer = await new Promise((resolve) => rl.question(` ${prompt} ${suffix}: `, resolve));
|
|
119
|
+
const normalized = answer.trim().toLowerCase();
|
|
120
|
+
if (!normalized) return defaultValue;
|
|
121
|
+
return normalized === 'y' || normalized === 'yes';
|
|
122
|
+
} finally {
|
|
123
|
+
rl.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function promptSingleSelect({ input, output, title, choices, defaultIndex = 0 }) {
|
|
128
|
+
const selected = await promptChoiceList({
|
|
129
|
+
input,
|
|
130
|
+
output,
|
|
131
|
+
title,
|
|
132
|
+
choices: choices.map((choice, index) => ({
|
|
133
|
+
...choice,
|
|
134
|
+
selected: index === defaultIndex,
|
|
135
|
+
detected: false,
|
|
136
|
+
})),
|
|
137
|
+
multi: false,
|
|
138
|
+
});
|
|
139
|
+
return selected[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function promptMultiSelect({ input, output, title, hint, choices }) {
|
|
143
|
+
return promptChoiceList({ input, output, title, hint, choices, multi: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function promptChoiceList({ input, output, title, hint = 'Use arrows to move. Enter confirms.', choices, multi }) {
|
|
147
|
+
if (typeof input.resume === 'function') input.resume();
|
|
148
|
+
readline.emitKeypressEvents(input);
|
|
149
|
+
const previousRawMode = typeof input.isRaw === 'boolean' ? input.isRaw : false;
|
|
150
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
151
|
+
|
|
152
|
+
let cursor = Math.max(0, choices.findIndex((choice) => choice.selected));
|
|
153
|
+
let renderedLines = 0;
|
|
154
|
+
|
|
155
|
+
const selectCursor = () => {
|
|
156
|
+
if (multi) return;
|
|
157
|
+
for (const choice of choices) choice.selected = false;
|
|
158
|
+
choices[cursor].selected = true;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const measureLines = (text) => {
|
|
162
|
+
const columns = Math.max(20, Number(output.columns) || 80);
|
|
163
|
+
const visible = stripAnsi(text);
|
|
164
|
+
return visible.split('\n').reduce((total, line) => {
|
|
165
|
+
const width = Math.max(1, line.length);
|
|
166
|
+
return total + Math.ceil(width / columns);
|
|
167
|
+
}, 0);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const render = () => {
|
|
171
|
+
if (renderedLines > 0) {
|
|
172
|
+
readline.moveCursor(output, 0, -renderedLines);
|
|
173
|
+
}
|
|
174
|
+
readline.cursorTo(output, 0);
|
|
175
|
+
readline.clearScreenDown(output);
|
|
176
|
+
const lines = [`${ANSI.bold}${title}${ANSI.reset}`];
|
|
177
|
+
if (hint) lines.push(`${ANSI.dim}${hint}${ANSI.reset}`);
|
|
178
|
+
lines.push('');
|
|
179
|
+
for (let i = 0; i < choices.length; i++) {
|
|
180
|
+
const choice = choices[i];
|
|
181
|
+
const pointer = i === cursor ? `${ANSI.green}>${ANSI.reset}` : ' ';
|
|
182
|
+
const mark = choice.selected ? `${ANSI.green}[x]${ANSI.reset}` : '[ ]';
|
|
183
|
+
const detected = choice.detected ? ` ${ANSI.dim}(detected)${ANSI.reset}` : '';
|
|
184
|
+
lines.push(`${pointer} ${mark} ${choice.label}${detected}`);
|
|
185
|
+
lines.push(` ${ANSI.dim}${choice.description}${ANSI.reset}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
renderedLines = 0;
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
output.write(`${line}\n`);
|
|
191
|
+
renderedLines += measureLines(line);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
render();
|
|
196
|
+
|
|
197
|
+
const values = await new Promise((resolve, reject) => {
|
|
198
|
+
const cleanup = () => {
|
|
199
|
+
input.off('keypress', onKeypress);
|
|
200
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(previousRawMode);
|
|
201
|
+
if (typeof input.pause === 'function') input.pause();
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const onKeypress = (_, key = {}) => {
|
|
205
|
+
if (key.ctrl && key.name === 'c') {
|
|
206
|
+
cleanup();
|
|
207
|
+
reject(new Error('Prompt cancelled by user'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (key.name === 'up') {
|
|
211
|
+
cursor = cursor === 0 ? choices.length - 1 : cursor - 1;
|
|
212
|
+
selectCursor();
|
|
213
|
+
render();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (key.name === 'down') {
|
|
217
|
+
cursor = cursor === choices.length - 1 ? 0 : cursor + 1;
|
|
218
|
+
selectCursor();
|
|
219
|
+
render();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (key.name === 'space') {
|
|
223
|
+
if (multi) {
|
|
224
|
+
choices[cursor].selected = !choices[cursor].selected;
|
|
225
|
+
} else {
|
|
226
|
+
for (const choice of choices) choice.selected = false;
|
|
227
|
+
choices[cursor].selected = true;
|
|
228
|
+
}
|
|
229
|
+
render();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (key.name === 'return') {
|
|
233
|
+
selectCursor();
|
|
234
|
+
cleanup();
|
|
235
|
+
resolve(choices.filter((choice) => choice.selected).map((choice) => choice.value ?? choice.id));
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
input.on('keypress', onKeypress);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
output.write('\n');
|
|
243
|
+
return values;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function stripAnsi(text) {
|
|
247
|
+
return String(text).replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
248
|
+
}
|