gsdd-cli 0.18.4 → 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.
Files changed (100) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +625 -607
  3. package/agents/DISTILLATION.md +421 -421
  4. package/agents/README.md +62 -62
  5. package/agents/approach-explorer.md +361 -361
  6. package/agents/debugger.md +82 -82
  7. package/agents/executor.md +394 -394
  8. package/agents/integration-checker.md +318 -318
  9. package/agents/mapper.md +103 -103
  10. package/agents/planner.md +313 -313
  11. package/agents/researcher.md +84 -84
  12. package/agents/roadmapper.md +296 -296
  13. package/agents/synthesizer.md +236 -236
  14. package/agents/verifier.md +337 -337
  15. package/bin/adapters/agents.mjs +34 -34
  16. package/bin/adapters/claude.mjs +191 -191
  17. package/bin/adapters/codex.mjs +85 -85
  18. package/bin/adapters/index.mjs +20 -20
  19. package/bin/adapters/opencode.mjs +278 -278
  20. package/bin/gsdd.mjs +123 -116
  21. package/bin/lib/cli-utils.mjs +28 -28
  22. package/bin/lib/evidence-contract.mjs +112 -112
  23. package/bin/lib/file-ops.mjs +209 -161
  24. package/bin/lib/health-truth.mjs +181 -178
  25. package/bin/lib/health.mjs +265 -235
  26. package/bin/lib/init-flow.mjs +252 -236
  27. package/bin/lib/init-prompts.mjs +248 -247
  28. package/bin/lib/init-runtime.mjs +225 -212
  29. package/bin/lib/init.mjs +17 -17
  30. package/bin/lib/lifecycle-preflight.mjs +360 -333
  31. package/bin/lib/lifecycle-state.mjs +351 -267
  32. package/bin/lib/manifest.mjs +116 -114
  33. package/bin/lib/models.mjs +411 -411
  34. package/bin/lib/phase.mjs +397 -378
  35. package/bin/lib/plan-constants.mjs +30 -30
  36. package/bin/lib/provenance.mjs +109 -106
  37. package/bin/lib/rendering.mjs +178 -130
  38. package/bin/lib/runtime-freshness.mjs +221 -219
  39. package/bin/lib/templates.mjs +225 -224
  40. package/bin/lib/workspace-root.mjs +132 -0
  41. package/distilled/DESIGN.md +2347 -2327
  42. package/distilled/EVIDENCE-INDEX.md +397 -394
  43. package/distilled/README.md +196 -193
  44. package/distilled/SKILL.md +86 -85
  45. package/distilled/templates/agents.block.md +21 -21
  46. package/distilled/templates/agents.md +6 -6
  47. package/distilled/templates/approach.md +232 -232
  48. package/distilled/templates/auth-matrix.md +78 -78
  49. package/distilled/templates/brownfield-change/CHANGE.md +99 -0
  50. package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
  51. package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
  52. package/distilled/templates/codebase/architecture.md +110 -110
  53. package/distilled/templates/codebase/concerns.md +95 -95
  54. package/distilled/templates/codebase/conventions.md +193 -193
  55. package/distilled/templates/codebase/stack.md +96 -96
  56. package/distilled/templates/delegates/approach-explorer.md +25 -25
  57. package/distilled/templates/delegates/mapper-arch.md +26 -26
  58. package/distilled/templates/delegates/mapper-concerns.md +27 -27
  59. package/distilled/templates/delegates/mapper-quality.md +28 -28
  60. package/distilled/templates/delegates/mapper-tech.md +25 -25
  61. package/distilled/templates/delegates/plan-checker.md +68 -68
  62. package/distilled/templates/delegates/researcher-architecture.md +30 -30
  63. package/distilled/templates/delegates/researcher-features.md +30 -30
  64. package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
  65. package/distilled/templates/delegates/researcher-stack.md +30 -30
  66. package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
  67. package/distilled/templates/research/architecture.md +57 -57
  68. package/distilled/templates/research/features.md +23 -23
  69. package/distilled/templates/research/pitfalls.md +46 -46
  70. package/distilled/templates/research/stack.md +45 -45
  71. package/distilled/templates/research/summary.md +67 -67
  72. package/distilled/templates/roadmap.md +74 -62
  73. package/distilled/templates/spec.md +110 -110
  74. package/distilled/workflows/audit-milestone.md +275 -271
  75. package/distilled/workflows/complete-milestone.md +336 -332
  76. package/distilled/workflows/execute.md +454 -449
  77. package/distilled/workflows/map-codebase.md +253 -253
  78. package/distilled/workflows/new-milestone.md +242 -238
  79. package/distilled/workflows/new-project.md +398 -398
  80. package/distilled/workflows/pause.md +160 -156
  81. package/distilled/workflows/plan-milestone-gaps.md +183 -183
  82. package/distilled/workflows/plan.md +451 -447
  83. package/distilled/workflows/progress.md +227 -223
  84. package/distilled/workflows/quick.md +351 -347
  85. package/distilled/workflows/resume.md +220 -212
  86. package/distilled/workflows/verify-work.md +260 -260
  87. package/distilled/workflows/verify.md +431 -429
  88. package/docs/BROWNFIELD-PROOF.md +95 -95
  89. package/docs/RUNTIME-SUPPORT.md +93 -75
  90. package/docs/USER-GUIDE.md +440 -399
  91. package/docs/VERIFICATION-DISCIPLINE.md +59 -59
  92. package/docs/claude/context-monitor.md +98 -98
  93. package/docs/proof/consumer-node-cli/README.md +37 -37
  94. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
  95. package/docs/proof/consumer-node-cli/SPEC.md +17 -17
  96. package/docs/proof/consumer-node-cli/brief.md +9 -9
  97. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
  98. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
  99. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
  100. package/package.json +62 -61
@@ -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
- const onKeypress = (_, key = {}) => {
204
- if (key.ctrl && key.name === 'c') {
205
- cleanup();
206
- reject(new Error('Prompt cancelled by user'));
207
- return;
208
- }
209
- if (key.name === 'up') {
210
- cursor = cursor === 0 ? choices.length - 1 : cursor - 1;
211
- selectCursor();
212
- render();
213
- return;
214
- }
215
- if (key.name === 'down') {
216
- cursor = cursor === choices.length - 1 ? 0 : cursor + 1;
217
- selectCursor();
218
- render();
219
- return;
220
- }
221
- if (key.name === 'space') {
222
- if (multi) {
223
- choices[cursor].selected = !choices[cursor].selected;
224
- } else {
225
- for (const choice of choices) choice.selected = false;
226
- choices[cursor].selected = true;
227
- }
228
- render();
229
- return;
230
- }
231
- if (key.name === 'return') {
232
- selectCursor();
233
- cleanup();
234
- resolve(choices.filter((choice) => choice.selected).map((choice) => choice.value ?? choice.id));
235
- }
236
- };
237
-
238
- input.on('keypress', onKeypress);
239
- });
240
-
241
- output.write('\n');
242
- return values;
243
- }
244
-
245
- export function stripAnsi(text) {
246
- return String(text).replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
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
+ }