stellar-agent 0.1.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 +162 -0
- package/package.json +37 -0
- package/src/core-skills/module-help.csv +5 -0
- package/src/core-skills/module.yaml +33 -0
- package/src/core-skills/stellar-brainstorming/SKILL.md +6 -0
- package/src/core-skills/stellar-brainstorming/steps/step-01-session-setup.md +67 -0
- package/src/core-skills/stellar-brainstorming/steps/step-02a-user-selected.md +20 -0
- package/src/core-skills/stellar-brainstorming/steps/step-02b-ai-recommended.md +29 -0
- package/src/core-skills/stellar-brainstorming/steps/step-03-technique-execution.md +69 -0
- package/src/core-skills/stellar-brainstorming/steps/step-04-idea-organization.md +64 -0
- package/src/core-skills/stellar-brainstorming/workflow.md +50 -0
- package/src/core-skills/stellar-help/SKILL.md +71 -0
- package/src/core-skills/stellar-party-mode/SKILL.md +109 -0
- package/src/scripts/resolve_config.py +170 -0
- package/src/scripts/resolve_customization.py +209 -0
- package/src/stellar-skills/1-analysis/stellar-agent-analyst/SKILL.md +71 -0
- package/src/stellar-skills/1-analysis/stellar-agent-analyst/customize.toml +41 -0
- package/src/stellar-skills/1-analysis/stellar-analytics/SKILL.md +239 -0
- package/src/stellar-skills/1-analysis/stellar-domain-research/SKILL.md +82 -0
- package/src/stellar-skills/1-analysis/stellar-market-research/SKILL.md +90 -0
- package/src/stellar-skills/2-planning/stellar-agent-pm/SKILL.md +57 -0
- package/src/stellar-skills/2-planning/stellar-agent-pm/customize.toml +36 -0
- package/src/stellar-skills/2-planning/stellar-epics-stories/SKILL.md +106 -0
- package/src/stellar-skills/2-planning/stellar-prd/SKILL.md +115 -0
- package/src/stellar-skills/2-planning/stellar-project-brief/SKILL.md +83 -0
- package/src/stellar-skills/3-architecture/stellar-agent-architect/SKILL.md +53 -0
- package/src/stellar-skills/3-architecture/stellar-agent-architect/customize.toml +31 -0
- package/src/stellar-skills/3-architecture/stellar-architecture-doc/SKILL.md +162 -0
- package/src/stellar-skills/4-implementation/stellar-agent-developer/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-developer/customize.toml +56 -0
- package/src/stellar-skills/4-implementation/stellar-agent-devops/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-devops/customize.toml +36 -0
- package/src/stellar-skills/4-implementation/stellar-agent-frontend/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-frontend/customize.toml +52 -0
- package/src/stellar-skills/4-implementation/stellar-agent-qa/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-qa/customize.toml +31 -0
- package/src/stellar-skills/4-implementation/stellar-create-asset/SKILL.md +145 -0
- package/src/stellar-skills/4-implementation/stellar-create-transaction/SKILL.md +134 -0
- package/src/stellar-skills/4-implementation/stellar-deploy-contract/SKILL.md +124 -0
- package/src/stellar-skills/4-implementation/stellar-freighter-integration/SKILL.md +193 -0
- package/src/stellar-skills/4-implementation/stellar-horizon-integration/SKILL.md +198 -0
- package/src/stellar-skills/4-implementation/stellar-init-contract/SKILL.md +102 -0
- package/src/stellar-skills/4-implementation/stellar-liquidity-pool/SKILL.md +156 -0
- package/src/stellar-skills/4-implementation/stellar-nextjs-setup/SKILL.md +198 -0
- package/src/stellar-skills/4-implementation/stellar-nextjs-soroban/SKILL.md +228 -0
- package/src/stellar-skills/4-implementation/stellar-nextjs-wallet/SKILL.md +276 -0
- package/src/stellar-skills/4-implementation/stellar-sep10-auth/SKILL.md +252 -0
- package/src/stellar-skills/4-implementation/stellar-setup-environment/SKILL.md +163 -0
- package/src/stellar-skills/4-implementation/stellar-setup-trustline/SKILL.md +107 -0
- package/src/stellar-skills/4-implementation/stellar-test-contract/SKILL.md +146 -0
- package/src/stellar-skills/4-implementation/stellar-write-contract/SKILL.md +140 -0
- package/src/stellar-skills/module-help.csv +24 -0
- package/src/stellar-skills/module.yaml +103 -0
- package/tools/installer/cli-utils.js +39 -0
- package/tools/installer/commands/init.js +335 -0
- package/tools/installer/fs-native.js +116 -0
- package/tools/installer/prompts.js +852 -0
- package/tools/installer/stellar-cli.js +80 -0
- package/tools/installer/yaml-format.js +245 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clack/prompts wrapper for BMAD CLI
|
|
3
|
+
*
|
|
4
|
+
* This module provides a unified interface for CLI prompts using @clack/prompts.
|
|
5
|
+
* It replaces Inquirer.js to fix Windows arrow key navigation issues (libuv #852).
|
|
6
|
+
*
|
|
7
|
+
* @module prompts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let _clack = null;
|
|
11
|
+
let _clackCore = null;
|
|
12
|
+
let _picocolors = null;
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lazy-load @clack/prompts (ESM module)
|
|
19
|
+
* @returns {Promise<Object>} The clack prompts module
|
|
20
|
+
*/
|
|
21
|
+
async function getClack() {
|
|
22
|
+
if (!_clack) {
|
|
23
|
+
_clack = await import('@clack/prompts');
|
|
24
|
+
}
|
|
25
|
+
return _clack;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lazy-load @clack/core (ESM module)
|
|
30
|
+
* @returns {Promise<Object>} The clack core module
|
|
31
|
+
*/
|
|
32
|
+
async function getClackCore() {
|
|
33
|
+
if (!_clackCore) {
|
|
34
|
+
_clackCore = await import('@clack/core');
|
|
35
|
+
}
|
|
36
|
+
return _clackCore;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lazy-load picocolors
|
|
41
|
+
* @returns {Promise<Object>} The picocolors module
|
|
42
|
+
*/
|
|
43
|
+
async function getPicocolors() {
|
|
44
|
+
if (!_picocolors) {
|
|
45
|
+
_picocolors = (await import('picocolors')).default;
|
|
46
|
+
}
|
|
47
|
+
return _picocolors;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle user cancellation gracefully
|
|
52
|
+
* @param {any} value - The value to check
|
|
53
|
+
* @param {string} [message='Operation cancelled'] - Message to display
|
|
54
|
+
* @returns {boolean} True if cancelled
|
|
55
|
+
*/
|
|
56
|
+
async function handleCancel(value, message = 'Operation cancelled') {
|
|
57
|
+
const clack = await getClack();
|
|
58
|
+
if (clack.isCancel(value)) {
|
|
59
|
+
clack.cancel(message);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Display intro message
|
|
67
|
+
* @param {string} message - The intro message
|
|
68
|
+
*/
|
|
69
|
+
async function intro(message) {
|
|
70
|
+
const clack = await getClack();
|
|
71
|
+
clack.intro(message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Display outro message
|
|
76
|
+
* @param {string} message - The outro message
|
|
77
|
+
*/
|
|
78
|
+
async function outro(message) {
|
|
79
|
+
const clack = await getClack();
|
|
80
|
+
clack.outro(message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Display a note/info box
|
|
85
|
+
* @param {string} message - The note content
|
|
86
|
+
* @param {string} [title] - Optional title
|
|
87
|
+
*/
|
|
88
|
+
async function note(message, title) {
|
|
89
|
+
const clack = await getClack();
|
|
90
|
+
clack.note(message, title);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Display a spinner for async operations
|
|
95
|
+
* Wraps @clack/prompts spinner with isSpinning state tracking
|
|
96
|
+
* @returns {Object} Spinner controller with start, stop, message, error, cancel, clear, isSpinning
|
|
97
|
+
*/
|
|
98
|
+
async function spinner() {
|
|
99
|
+
const clack = await getClack();
|
|
100
|
+
const s = clack.spinner();
|
|
101
|
+
let spinning = false;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
start: (msg) => {
|
|
105
|
+
if (spinning) {
|
|
106
|
+
s.message(msg);
|
|
107
|
+
} else {
|
|
108
|
+
spinning = true;
|
|
109
|
+
s.start(msg);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
stop: (msg) => {
|
|
113
|
+
if (spinning) {
|
|
114
|
+
spinning = false;
|
|
115
|
+
s.stop(msg);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
message: (msg) => {
|
|
119
|
+
if (spinning) s.message(msg);
|
|
120
|
+
},
|
|
121
|
+
error: (msg) => {
|
|
122
|
+
spinning = false;
|
|
123
|
+
s.error(msg);
|
|
124
|
+
},
|
|
125
|
+
cancel: (msg) => {
|
|
126
|
+
spinning = false;
|
|
127
|
+
s.cancel(msg);
|
|
128
|
+
},
|
|
129
|
+
clear: () => {
|
|
130
|
+
spinning = false;
|
|
131
|
+
s.clear();
|
|
132
|
+
},
|
|
133
|
+
get isSpinning() {
|
|
134
|
+
return spinning;
|
|
135
|
+
},
|
|
136
|
+
get isCancelled() {
|
|
137
|
+
return s.isCancelled;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Single-select prompt (replaces Inquirer 'list' type)
|
|
144
|
+
* @param {Object} options - Prompt options
|
|
145
|
+
* @param {string} options.message - The question to ask
|
|
146
|
+
* @param {Array} options.choices - Array of choices [{name, value, hint?}]
|
|
147
|
+
* @param {any} [options.default] - Default selected value
|
|
148
|
+
* @returns {Promise<any>} Selected value
|
|
149
|
+
*/
|
|
150
|
+
async function select(options) {
|
|
151
|
+
const clack = await getClack();
|
|
152
|
+
|
|
153
|
+
// Convert Inquirer-style choices to clack format
|
|
154
|
+
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
|
155
|
+
const clackOptions = options.choices
|
|
156
|
+
.filter((c) => c.type !== 'separator') // Skip separators for now
|
|
157
|
+
.map((choice) => {
|
|
158
|
+
if (typeof choice === 'string' || typeof choice === 'number') {
|
|
159
|
+
return { value: choice, label: String(choice) };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
value: choice.value === undefined ? choice.name : choice.value,
|
|
163
|
+
label: choice.name || choice.label || String(choice.value),
|
|
164
|
+
hint: choice.hint || choice.description,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Find initial value
|
|
169
|
+
let initialValue;
|
|
170
|
+
if (options.default !== undefined) {
|
|
171
|
+
initialValue = options.default;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = await clack.select({
|
|
175
|
+
message: options.message,
|
|
176
|
+
options: clackOptions,
|
|
177
|
+
initialValue,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await handleCancel(result);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Multi-select prompt (replaces Inquirer 'checkbox' type)
|
|
186
|
+
* @param {Object} options - Prompt options
|
|
187
|
+
* @param {string} options.message - The question to ask
|
|
188
|
+
* @param {Array} options.choices - Array of choices [{name, value, checked?, hint?}]
|
|
189
|
+
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
|
190
|
+
* @returns {Promise<Array>} Array of selected values
|
|
191
|
+
*/
|
|
192
|
+
async function multiselect(options) {
|
|
193
|
+
const clack = await getClack();
|
|
194
|
+
|
|
195
|
+
// Support both clack-native (options) and Inquirer-style (choices) APIs
|
|
196
|
+
let clackOptions;
|
|
197
|
+
let initialValues;
|
|
198
|
+
|
|
199
|
+
if (options.options) {
|
|
200
|
+
// Native clack format: options with label/value
|
|
201
|
+
clackOptions = options.options;
|
|
202
|
+
initialValues = options.initialValues || [];
|
|
203
|
+
} else {
|
|
204
|
+
// Convert Inquirer-style choices to clack format
|
|
205
|
+
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
|
206
|
+
clackOptions = options.choices
|
|
207
|
+
.filter((c) => c.type !== 'separator') // Skip separators
|
|
208
|
+
.map((choice) => {
|
|
209
|
+
if (typeof choice === 'string' || typeof choice === 'number') {
|
|
210
|
+
return { value: choice, label: String(choice) };
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
value: choice.value === undefined ? choice.name : choice.value,
|
|
214
|
+
label: choice.name || choice.label || String(choice.value),
|
|
215
|
+
hint: choice.hint || choice.description,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Find initial values (pre-checked items)
|
|
220
|
+
initialValues = options.choices
|
|
221
|
+
.filter((c) => c.checked && c.type !== 'separator')
|
|
222
|
+
.map((c) => (c.value === undefined ? c.name : c.value));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = await clack.multiselect({
|
|
226
|
+
message: options.message,
|
|
227
|
+
options: clackOptions,
|
|
228
|
+
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
229
|
+
required: options.required || false,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await handleCancel(result);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Default filter function for autocomplete - case-insensitive label matching
|
|
238
|
+
* @param {string} search - Search string
|
|
239
|
+
* @param {Object} option - Option object with label
|
|
240
|
+
* @returns {boolean} Whether the option matches
|
|
241
|
+
*/
|
|
242
|
+
function defaultAutocompleteFilter(search, option) {
|
|
243
|
+
const label = option.label ?? String(option.value ?? '');
|
|
244
|
+
return label.toLowerCase().includes(search.toLowerCase());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Autocomplete multi-select prompt with type-ahead filtering
|
|
249
|
+
* Custom implementation that always shows "Space/Tab:" in the hint
|
|
250
|
+
* @param {Object} options - Prompt options
|
|
251
|
+
* @param {string} options.message - The question to ask
|
|
252
|
+
* @param {Array} options.options - Array of choices [{label, value, hint?}]
|
|
253
|
+
* @param {string} [options.placeholder] - Placeholder text for search input
|
|
254
|
+
* @param {Array} [options.initialValues] - Array of initially selected values
|
|
255
|
+
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
|
256
|
+
* @param {number} [options.maxItems=5] - Maximum visible items in scrollable list
|
|
257
|
+
* @param {Function} [options.filter] - Custom filter function (search, option) => boolean
|
|
258
|
+
* @param {Array} [options.lockedValues] - Values that are always selected and cannot be toggled off
|
|
259
|
+
* @returns {Promise<Array>} Array of selected values
|
|
260
|
+
*/
|
|
261
|
+
async function autocompleteMultiselect(options) {
|
|
262
|
+
const core = await getClackCore();
|
|
263
|
+
const clack = await getClack();
|
|
264
|
+
const color = await getPicocolors();
|
|
265
|
+
|
|
266
|
+
const filterFn = options.filter ?? defaultAutocompleteFilter;
|
|
267
|
+
const lockedSet = new Set(options.lockedValues || []);
|
|
268
|
+
|
|
269
|
+
const prompt = new core.AutocompletePrompt({
|
|
270
|
+
options: options.options,
|
|
271
|
+
multiple: true,
|
|
272
|
+
filter: filterFn,
|
|
273
|
+
validate: () => {
|
|
274
|
+
if (options.required && prompt.selectedValues.length === 0) {
|
|
275
|
+
return 'Please select at least one item';
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])],
|
|
279
|
+
render() {
|
|
280
|
+
const barColor = this.state === 'error' ? color.yellow : color.cyan;
|
|
281
|
+
const bar = barColor(clack.S_BAR);
|
|
282
|
+
const barEnd = barColor(clack.S_BAR_END);
|
|
283
|
+
|
|
284
|
+
const title = `${color.gray(clack.S_BAR)}\n${clack.symbol(this.state)} ${options.message}\n`;
|
|
285
|
+
|
|
286
|
+
const userInput = this.userInput;
|
|
287
|
+
const placeholder = options.placeholder || 'Type to search...';
|
|
288
|
+
const hasPlaceholder = userInput === '' && placeholder !== undefined;
|
|
289
|
+
|
|
290
|
+
// Show placeholder or user input with cursor
|
|
291
|
+
const searchDisplay =
|
|
292
|
+
this.isNavigating || hasPlaceholder ? color.dim(hasPlaceholder ? placeholder : userInput) : this.userInputWithCursor;
|
|
293
|
+
|
|
294
|
+
const allOptions = this.options;
|
|
295
|
+
const matchCount =
|
|
296
|
+
this.filteredOptions.length === allOptions.length
|
|
297
|
+
? ''
|
|
298
|
+
: color.dim(` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`);
|
|
299
|
+
|
|
300
|
+
// Render option with checkbox
|
|
301
|
+
const renderOption = (opt, isHighlighted) => {
|
|
302
|
+
const isSelected = this.selectedValues.includes(opt.value);
|
|
303
|
+
const isLocked = lockedSet.has(opt.value);
|
|
304
|
+
const label = opt.label ?? String(opt.value ?? '');
|
|
305
|
+
const hintText = opt.hint && isHighlighted ? color.dim(` (${opt.hint})`) : '';
|
|
306
|
+
|
|
307
|
+
let checkbox;
|
|
308
|
+
if (isLocked) {
|
|
309
|
+
checkbox = color.green(clack.S_CHECKBOX_SELECTED);
|
|
310
|
+
const lockHint = color.dim(' (always installed)');
|
|
311
|
+
return isHighlighted ? `${checkbox} ${label}${lockHint}` : `${checkbox} ${color.dim(label)}${lockHint}`;
|
|
312
|
+
}
|
|
313
|
+
checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
|
|
314
|
+
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
switch (this.state) {
|
|
318
|
+
case 'submit': {
|
|
319
|
+
return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case 'cancel': {
|
|
323
|
+
return `${title}${color.gray(clack.S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
default: {
|
|
327
|
+
// Always show "SPACE:" regardless of isNavigating state
|
|
328
|
+
const hints = [`${color.dim('↑/↓')} to navigate`, `${color.dim('TAB/SPACE:')} select`, `${color.dim('ENTER:')} confirm`];
|
|
329
|
+
|
|
330
|
+
const noMatchesLine = this.filteredOptions.length === 0 && userInput ? [`${bar} ${color.yellow('No matches found')}`] : [];
|
|
331
|
+
|
|
332
|
+
const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : [];
|
|
333
|
+
|
|
334
|
+
const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine];
|
|
335
|
+
|
|
336
|
+
const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`];
|
|
337
|
+
|
|
338
|
+
const optionLines = clack.limitOptions({
|
|
339
|
+
cursor: this.cursor,
|
|
340
|
+
options: this.filteredOptions,
|
|
341
|
+
style: renderOption,
|
|
342
|
+
maxItems: options.maxItems || 5,
|
|
343
|
+
output: options.output,
|
|
344
|
+
rowPadding: headerLines.length + footerLines.length,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return [...headerLines, ...optionLines.map((line) => `${bar} ${line}`), ...footerLines].join('\n');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Prevent locked values from being toggled off
|
|
354
|
+
if (lockedSet.size > 0) {
|
|
355
|
+
const originalToggle = prompt.toggleSelected.bind(prompt);
|
|
356
|
+
prompt.toggleSelected = function (value) {
|
|
357
|
+
// If locked and already selected, skip the toggle (would deselect)
|
|
358
|
+
if (lockedSet.has(value) && this.selectedValues.includes(value)) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
originalToggle(value);
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// === FIX: Make SPACE always act as selection key (not search input) ===
|
|
366
|
+
// Override _isActionKey to treat SPACE like TAB - always an action key
|
|
367
|
+
// This prevents SPACE from being added to the search input
|
|
368
|
+
const originalIsActionKey = prompt._isActionKey.bind(prompt);
|
|
369
|
+
prompt._isActionKey = function (char, key) {
|
|
370
|
+
if (key && key.name === 'space') {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
return originalIsActionKey(char, key);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true)
|
|
377
|
+
prompt.on('key', (char, key) => {
|
|
378
|
+
if (key && key.name === 'space' && !prompt.isNavigating) {
|
|
379
|
+
const focused = prompt.filteredOptions[prompt.cursor];
|
|
380
|
+
if (focused) prompt.toggleSelected(focused.value);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// === END FIX ===
|
|
384
|
+
|
|
385
|
+
const result = await prompt.prompt();
|
|
386
|
+
await handleCancel(result);
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Confirm prompt (replaces Inquirer 'confirm' type)
|
|
392
|
+
* @param {Object} options - Prompt options
|
|
393
|
+
* @param {string} options.message - The question to ask
|
|
394
|
+
* @param {boolean} [options.default=true] - Default value
|
|
395
|
+
* @returns {Promise<boolean>} User's answer
|
|
396
|
+
*/
|
|
397
|
+
async function confirm(options) {
|
|
398
|
+
const clack = await getClack();
|
|
399
|
+
|
|
400
|
+
const result = await clack.confirm({
|
|
401
|
+
message: options.message,
|
|
402
|
+
initialValue: options.default === undefined ? true : options.default,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await handleCancel(result);
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Text input prompt with Tab-to-fill-placeholder support (replaces Inquirer 'input' type)
|
|
411
|
+
*
|
|
412
|
+
* This custom implementation restores the Tab-to-fill-placeholder behavior that was
|
|
413
|
+
* intentionally removed in @clack/prompts v1.0.0 (placeholder became purely visual).
|
|
414
|
+
* Uses @clack/core's TextPrompt primitive with custom key handling.
|
|
415
|
+
*
|
|
416
|
+
* @param {Object} options - Prompt options
|
|
417
|
+
* @param {string} options.message - The question to ask
|
|
418
|
+
* @param {string} [options.default] - Default value
|
|
419
|
+
* @param {string} [options.placeholder] - Placeholder text (defaults to options.default if not provided)
|
|
420
|
+
* @param {Function} [options.validate] - Validation function
|
|
421
|
+
* @returns {Promise<string>} User's input
|
|
422
|
+
*/
|
|
423
|
+
async function text(options) {
|
|
424
|
+
const core = await getClackCore();
|
|
425
|
+
const color = await getPicocolors();
|
|
426
|
+
|
|
427
|
+
// Use default as placeholder if placeholder not explicitly provided
|
|
428
|
+
// This shows the default value as grayed-out hint text
|
|
429
|
+
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
|
430
|
+
const defaultValue = options.default;
|
|
431
|
+
|
|
432
|
+
const prompt = new core.TextPrompt({
|
|
433
|
+
defaultValue,
|
|
434
|
+
validate: options.validate,
|
|
435
|
+
render() {
|
|
436
|
+
const title = `${color.gray('◆')} ${options.message}`;
|
|
437
|
+
|
|
438
|
+
// Show placeholder as dim text when input is empty
|
|
439
|
+
let valueDisplay;
|
|
440
|
+
if (this.state === 'error') {
|
|
441
|
+
valueDisplay = color.yellow(this.userInputWithCursor);
|
|
442
|
+
} else if (this.userInput) {
|
|
443
|
+
valueDisplay = this.userInputWithCursor;
|
|
444
|
+
} else if (placeholder) {
|
|
445
|
+
// Show placeholder with cursor indicator when empty
|
|
446
|
+
valueDisplay = `${color.inverse(color.hidden('_'))}${color.dim(placeholder)}`;
|
|
447
|
+
} else {
|
|
448
|
+
valueDisplay = color.inverse(color.hidden('_'));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const bar = color.gray('│');
|
|
452
|
+
|
|
453
|
+
// Handle different states
|
|
454
|
+
if (this.state === 'submit') {
|
|
455
|
+
return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || defaultValue || '')}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (this.state === 'cancel') {
|
|
459
|
+
return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(this.userInput || ''))}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (this.state === 'error') {
|
|
463
|
+
return `${color.yellow('▲')} ${options.message}\n${bar} ${valueDisplay}\n${color.yellow('│')} ${color.yellow(this.error)}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return `${title}\n${bar} ${valueDisplay}\n${bar}`;
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Add Tab key handler to fill placeholder into input
|
|
471
|
+
prompt.on('key', (char) => {
|
|
472
|
+
if (char === '\t' && placeholder && !prompt.userInput) {
|
|
473
|
+
// Use _setUserInput with write=true to populate the readline and update internal state
|
|
474
|
+
prompt._setUserInput(placeholder, true);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const result = await prompt.prompt();
|
|
479
|
+
await handleCancel(result);
|
|
480
|
+
|
|
481
|
+
// TextPrompt's finalize handler already applies defaultValue for empty input
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Password input prompt (replaces Inquirer 'password' type)
|
|
487
|
+
* @param {Object} options - Prompt options
|
|
488
|
+
* @param {string} options.message - The question to ask
|
|
489
|
+
* @param {Function} [options.validate] - Validation function
|
|
490
|
+
* @returns {Promise<string>} User's input
|
|
491
|
+
*/
|
|
492
|
+
async function password(options) {
|
|
493
|
+
const clack = await getClack();
|
|
494
|
+
|
|
495
|
+
const result = await clack.password({
|
|
496
|
+
message: options.message,
|
|
497
|
+
validate: options.validate,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await handleCancel(result);
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Run tasks with spinner feedback
|
|
506
|
+
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
|
507
|
+
* @returns {Promise<void>}
|
|
508
|
+
*/
|
|
509
|
+
async function tasks(taskList) {
|
|
510
|
+
const clack = await getClack();
|
|
511
|
+
await clack.tasks(taskList);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Log messages with styling
|
|
516
|
+
*/
|
|
517
|
+
const log = {
|
|
518
|
+
async info(message) {
|
|
519
|
+
const clack = await getClack();
|
|
520
|
+
clack.log.info(message);
|
|
521
|
+
},
|
|
522
|
+
async success(message) {
|
|
523
|
+
const clack = await getClack();
|
|
524
|
+
clack.log.success(message);
|
|
525
|
+
},
|
|
526
|
+
async warn(message) {
|
|
527
|
+
const clack = await getClack();
|
|
528
|
+
clack.log.warn(message);
|
|
529
|
+
},
|
|
530
|
+
async error(message) {
|
|
531
|
+
const clack = await getClack();
|
|
532
|
+
clack.log.error(message);
|
|
533
|
+
},
|
|
534
|
+
async message(message) {
|
|
535
|
+
const clack = await getClack();
|
|
536
|
+
clack.log.message(message);
|
|
537
|
+
},
|
|
538
|
+
async step(message) {
|
|
539
|
+
const clack = await getClack();
|
|
540
|
+
clack.log.step(message);
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Display cancellation message
|
|
546
|
+
* @param {string} [message='Operation cancelled'] - The cancellation message
|
|
547
|
+
*/
|
|
548
|
+
async function cancel(message = 'Operation cancelled') {
|
|
549
|
+
const clack = await getClack();
|
|
550
|
+
clack.cancel(message);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Display content in a styled box
|
|
555
|
+
* @param {string} content - The box content
|
|
556
|
+
* @param {string} [title] - Optional title
|
|
557
|
+
* @param {Object} [options] - Box options (contentAlign, titleAlign, width, rounded, formatBorder, etc.)
|
|
558
|
+
*/
|
|
559
|
+
async function box(content, title, options) {
|
|
560
|
+
const clack = await getClack();
|
|
561
|
+
clack.box(content, title, options);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Autocomplete single-select prompt with type-ahead filtering
|
|
566
|
+
* @param {Object} options - Autocomplete options
|
|
567
|
+
* @param {string} options.message - The prompt message
|
|
568
|
+
* @param {Array} options.options - Array of choices [{value, label, hint?}]
|
|
569
|
+
* @param {string} [options.placeholder] - Placeholder text
|
|
570
|
+
* @param {number} [options.maxItems] - Maximum visible items
|
|
571
|
+
* @param {Function} [options.filter] - Custom filter function
|
|
572
|
+
* @returns {Promise<any>} Selected value
|
|
573
|
+
*/
|
|
574
|
+
async function autocomplete(options) {
|
|
575
|
+
const clack = await getClack();
|
|
576
|
+
const result = await clack.autocomplete(options);
|
|
577
|
+
await handleCancel(result);
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function hasPathSeparator(value) {
|
|
582
|
+
return value.endsWith('/') || value.endsWith('\\');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function expandHome(input) {
|
|
586
|
+
if (!input) return input;
|
|
587
|
+
if (input === '~') return os.homedir();
|
|
588
|
+
if (input.startsWith('~/') || input.startsWith('~\\')) {
|
|
589
|
+
return path.join(os.homedir(), input.slice(2));
|
|
590
|
+
}
|
|
591
|
+
return input;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function toDirectoryOption(value, label = value, synthetic = false) {
|
|
595
|
+
return { value, label, synthetic };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function isExistingDirectory(value) {
|
|
599
|
+
try {
|
|
600
|
+
return fs.existsSync(value) && fs.statSync(value).isDirectory();
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function listDirectoryOptions(input, options) {
|
|
607
|
+
const cwd = options.cwd || process.cwd();
|
|
608
|
+
const rawInput = input.trim();
|
|
609
|
+
const expandedInput = expandHome(rawInput);
|
|
610
|
+
const trailingSep = hasPathSeparator(rawInput) || hasPathSeparator(expandedInput);
|
|
611
|
+
const resolvedInput = expandedInput ? path.resolve(cwd, expandedInput) : cwd;
|
|
612
|
+
const browseDir = expandedInput && !trailingSep && !isExistingDirectory(resolvedInput) ? path.dirname(resolvedInput) : resolvedInput;
|
|
613
|
+
const prefix = expandedInput && browseDir !== resolvedInput ? path.basename(resolvedInput).toLowerCase() : '';
|
|
614
|
+
const results = [];
|
|
615
|
+
|
|
616
|
+
if (!trailingSep && isExistingDirectory(resolvedInput)) {
|
|
617
|
+
results.push(toDirectoryOption(resolvedInput, `. (use this directory)`));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (isExistingDirectory(browseDir)) {
|
|
621
|
+
try {
|
|
622
|
+
for (const entry of fs.readdirSync(browseDir, { withFileTypes: true })) {
|
|
623
|
+
if (!entry.isDirectory()) continue;
|
|
624
|
+
if (prefix && !entry.name.toLowerCase().startsWith(prefix)) continue;
|
|
625
|
+
const fullPath = path.join(browseDir, entry.name);
|
|
626
|
+
if (!results.some((option) => option.value === fullPath)) {
|
|
627
|
+
results.push(toDirectoryOption(fullPath));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
// Skip unreadable directories; validation still reports path issues.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const validation = options.validate?.(rawInput);
|
|
636
|
+
const hasMatchingOption = results.some((option) => option.value === resolvedInput);
|
|
637
|
+
if (expandedInput && !validation && !hasMatchingOption) {
|
|
638
|
+
results.unshift(toDirectoryOption(resolvedInput, `Create/use: ${resolvedInput}`, true));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return results;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Directory prompt with autocomplete candidates and create-directory support.
|
|
646
|
+
* Uses @clack/core directly so typed paths that do not exist yet can still be
|
|
647
|
+
* submitted when validation allows creating them.
|
|
648
|
+
* @param {Object} options - Prompt options
|
|
649
|
+
* @param {string} options.message - Prompt message
|
|
650
|
+
* @param {string} [options.default] - Default directory
|
|
651
|
+
* @param {string} [options.placeholder] - Placeholder text
|
|
652
|
+
* @param {Function} [options.validate] - Sync validation function
|
|
653
|
+
* @returns {Promise<string>} Selected or typed directory path
|
|
654
|
+
*/
|
|
655
|
+
async function directory(options) {
|
|
656
|
+
const core = await getClackCore();
|
|
657
|
+
const color = await getPicocolors();
|
|
658
|
+
const tabCompletion = {
|
|
659
|
+
prefix: '',
|
|
660
|
+
index: -1,
|
|
661
|
+
options: [],
|
|
662
|
+
lastValue: '',
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
let prompt;
|
|
666
|
+
prompt = new core.AutocompletePrompt({
|
|
667
|
+
initialValue: options.default,
|
|
668
|
+
options: () => listDirectoryOptions(prompt?.userInput || '', options),
|
|
669
|
+
filter: () => true,
|
|
670
|
+
validate: (value) => options.validate?.(value ?? prompt.userInput),
|
|
671
|
+
render() {
|
|
672
|
+
const title = `${color.gray('◆')} ${options.message}`;
|
|
673
|
+
const bar = color.gray('│');
|
|
674
|
+
const barEnd = color.gray('└');
|
|
675
|
+
const userInput = this.userInput;
|
|
676
|
+
const placeholder = options.placeholder || options.default;
|
|
677
|
+
const inputDisplay = userInput ? this.userInputWithCursor : `${color.inverse(color.hidden('_'))}${color.dim(placeholder || '')}`;
|
|
678
|
+
const errorLine = this.state === 'error' ? [`${color.yellow('│')} ${color.yellow(this.error)}`] : [];
|
|
679
|
+
|
|
680
|
+
switch (this.state) {
|
|
681
|
+
case 'submit': {
|
|
682
|
+
return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || '')}`;
|
|
683
|
+
}
|
|
684
|
+
case 'cancel': {
|
|
685
|
+
return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(userInput || ''))}`;
|
|
686
|
+
}
|
|
687
|
+
default: {
|
|
688
|
+
return [title, `${bar} ${inputDisplay}`, ...errorLine, barEnd].join('\n');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const hasSetUserInput = typeof prompt._setUserInput === 'function';
|
|
695
|
+
const hasClearUserInput = typeof prompt._clearUserInput === 'function';
|
|
696
|
+
|
|
697
|
+
prompt.on('key', (_, key) => {
|
|
698
|
+
if (key?.name !== 'tab') return;
|
|
699
|
+
if (!hasSetUserInput) return; // @clack/core API surface changed — skip Tab silently.
|
|
700
|
+
const currentInput = prompt.userInput;
|
|
701
|
+
const isContinuingCycle = tabCompletion.lastValue && currentInput === tabCompletion.lastValue;
|
|
702
|
+
const completionOptions = isContinuingCycle ? tabCompletion.options : prompt.filteredOptions.filter((option) => !option.synthetic);
|
|
703
|
+
if (completionOptions.length === 0) return;
|
|
704
|
+
|
|
705
|
+
if (isContinuingCycle) {
|
|
706
|
+
tabCompletion.index = (tabCompletion.index + 1) % completionOptions.length;
|
|
707
|
+
} else {
|
|
708
|
+
tabCompletion.prefix = currentInput;
|
|
709
|
+
tabCompletion.options = completionOptions;
|
|
710
|
+
tabCompletion.index = 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const focusedOption = completionOptions[tabCompletion.index];
|
|
714
|
+
if (!focusedOption) return;
|
|
715
|
+
const completedValue = focusedOption.value;
|
|
716
|
+
tabCompletion.lastValue = completedValue;
|
|
717
|
+
if (hasClearUserInput) prompt._clearUserInput();
|
|
718
|
+
prompt._setUserInput(completedValue, true);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const result = await prompt.prompt();
|
|
722
|
+
await handleCancel(result);
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Get the color utility (picocolors instance from @clack/prompts)
|
|
728
|
+
* @returns {Promise<Object>} The color utility (picocolors)
|
|
729
|
+
*/
|
|
730
|
+
async function getColor() {
|
|
731
|
+
return await getPicocolors();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Execute an array of Inquirer-style questions using @clack/prompts
|
|
736
|
+
* This provides compatibility with dynamic question arrays
|
|
737
|
+
* @param {Array} questions - Array of Inquirer-style question objects
|
|
738
|
+
* @returns {Promise<Object>} Object with answers keyed by question name
|
|
739
|
+
*/
|
|
740
|
+
async function prompt(questions) {
|
|
741
|
+
const answers = {};
|
|
742
|
+
|
|
743
|
+
for (const question of questions) {
|
|
744
|
+
const { type, name, message, choices, default: defaultValue, validate, when } = question;
|
|
745
|
+
|
|
746
|
+
// Handle conditional questions via 'when' property
|
|
747
|
+
if (when !== undefined) {
|
|
748
|
+
const shouldAsk = typeof when === 'function' ? await when(answers) : when;
|
|
749
|
+
if (!shouldAsk) continue;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
let answer;
|
|
753
|
+
|
|
754
|
+
switch (type) {
|
|
755
|
+
case 'input': {
|
|
756
|
+
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
|
757
|
+
answer = await text({
|
|
758
|
+
message,
|
|
759
|
+
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
760
|
+
validate: validate
|
|
761
|
+
? (val) => {
|
|
762
|
+
const result = validate(val, answers);
|
|
763
|
+
if (result instanceof Promise) {
|
|
764
|
+
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
|
765
|
+
}
|
|
766
|
+
return result === true ? undefined : result;
|
|
767
|
+
}
|
|
768
|
+
: undefined,
|
|
769
|
+
});
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
case 'confirm': {
|
|
774
|
+
answer = await confirm({
|
|
775
|
+
message,
|
|
776
|
+
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
777
|
+
});
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
case 'list': {
|
|
782
|
+
answer = await select({
|
|
783
|
+
message,
|
|
784
|
+
choices: choices || [],
|
|
785
|
+
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
786
|
+
});
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
case 'checkbox': {
|
|
791
|
+
answer = await multiselect({
|
|
792
|
+
message,
|
|
793
|
+
choices: choices || [],
|
|
794
|
+
required: false,
|
|
795
|
+
});
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
case 'password': {
|
|
800
|
+
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
|
801
|
+
answer = await password({
|
|
802
|
+
message,
|
|
803
|
+
validate: validate
|
|
804
|
+
? (val) => {
|
|
805
|
+
const result = validate(val, answers);
|
|
806
|
+
if (result instanceof Promise) {
|
|
807
|
+
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
|
808
|
+
}
|
|
809
|
+
return result === true ? undefined : result;
|
|
810
|
+
}
|
|
811
|
+
: undefined,
|
|
812
|
+
});
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
default: {
|
|
817
|
+
// Default to text input for unknown types
|
|
818
|
+
answer = await text({
|
|
819
|
+
message,
|
|
820
|
+
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
answers[name] = answer;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return answers;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
module.exports = {
|
|
832
|
+
getClack,
|
|
833
|
+
getColor,
|
|
834
|
+
handleCancel,
|
|
835
|
+
intro,
|
|
836
|
+
outro,
|
|
837
|
+
cancel,
|
|
838
|
+
note,
|
|
839
|
+
box,
|
|
840
|
+
spinner,
|
|
841
|
+
select,
|
|
842
|
+
multiselect,
|
|
843
|
+
autocompleteMultiselect,
|
|
844
|
+
autocomplete,
|
|
845
|
+
directory,
|
|
846
|
+
confirm,
|
|
847
|
+
text,
|
|
848
|
+
password,
|
|
849
|
+
tasks,
|
|
850
|
+
log,
|
|
851
|
+
prompt,
|
|
852
|
+
};
|