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.
Files changed (59) hide show
  1. package/README.md +162 -0
  2. package/package.json +37 -0
  3. package/src/core-skills/module-help.csv +5 -0
  4. package/src/core-skills/module.yaml +33 -0
  5. package/src/core-skills/stellar-brainstorming/SKILL.md +6 -0
  6. package/src/core-skills/stellar-brainstorming/steps/step-01-session-setup.md +67 -0
  7. package/src/core-skills/stellar-brainstorming/steps/step-02a-user-selected.md +20 -0
  8. package/src/core-skills/stellar-brainstorming/steps/step-02b-ai-recommended.md +29 -0
  9. package/src/core-skills/stellar-brainstorming/steps/step-03-technique-execution.md +69 -0
  10. package/src/core-skills/stellar-brainstorming/steps/step-04-idea-organization.md +64 -0
  11. package/src/core-skills/stellar-brainstorming/workflow.md +50 -0
  12. package/src/core-skills/stellar-help/SKILL.md +71 -0
  13. package/src/core-skills/stellar-party-mode/SKILL.md +109 -0
  14. package/src/scripts/resolve_config.py +170 -0
  15. package/src/scripts/resolve_customization.py +209 -0
  16. package/src/stellar-skills/1-analysis/stellar-agent-analyst/SKILL.md +71 -0
  17. package/src/stellar-skills/1-analysis/stellar-agent-analyst/customize.toml +41 -0
  18. package/src/stellar-skills/1-analysis/stellar-analytics/SKILL.md +239 -0
  19. package/src/stellar-skills/1-analysis/stellar-domain-research/SKILL.md +82 -0
  20. package/src/stellar-skills/1-analysis/stellar-market-research/SKILL.md +90 -0
  21. package/src/stellar-skills/2-planning/stellar-agent-pm/SKILL.md +57 -0
  22. package/src/stellar-skills/2-planning/stellar-agent-pm/customize.toml +36 -0
  23. package/src/stellar-skills/2-planning/stellar-epics-stories/SKILL.md +106 -0
  24. package/src/stellar-skills/2-planning/stellar-prd/SKILL.md +115 -0
  25. package/src/stellar-skills/2-planning/stellar-project-brief/SKILL.md +83 -0
  26. package/src/stellar-skills/3-architecture/stellar-agent-architect/SKILL.md +53 -0
  27. package/src/stellar-skills/3-architecture/stellar-agent-architect/customize.toml +31 -0
  28. package/src/stellar-skills/3-architecture/stellar-architecture-doc/SKILL.md +162 -0
  29. package/src/stellar-skills/4-implementation/stellar-agent-developer/SKILL.md +54 -0
  30. package/src/stellar-skills/4-implementation/stellar-agent-developer/customize.toml +56 -0
  31. package/src/stellar-skills/4-implementation/stellar-agent-devops/SKILL.md +54 -0
  32. package/src/stellar-skills/4-implementation/stellar-agent-devops/customize.toml +36 -0
  33. package/src/stellar-skills/4-implementation/stellar-agent-frontend/SKILL.md +54 -0
  34. package/src/stellar-skills/4-implementation/stellar-agent-frontend/customize.toml +52 -0
  35. package/src/stellar-skills/4-implementation/stellar-agent-qa/SKILL.md +54 -0
  36. package/src/stellar-skills/4-implementation/stellar-agent-qa/customize.toml +31 -0
  37. package/src/stellar-skills/4-implementation/stellar-create-asset/SKILL.md +145 -0
  38. package/src/stellar-skills/4-implementation/stellar-create-transaction/SKILL.md +134 -0
  39. package/src/stellar-skills/4-implementation/stellar-deploy-contract/SKILL.md +124 -0
  40. package/src/stellar-skills/4-implementation/stellar-freighter-integration/SKILL.md +193 -0
  41. package/src/stellar-skills/4-implementation/stellar-horizon-integration/SKILL.md +198 -0
  42. package/src/stellar-skills/4-implementation/stellar-init-contract/SKILL.md +102 -0
  43. package/src/stellar-skills/4-implementation/stellar-liquidity-pool/SKILL.md +156 -0
  44. package/src/stellar-skills/4-implementation/stellar-nextjs-setup/SKILL.md +198 -0
  45. package/src/stellar-skills/4-implementation/stellar-nextjs-soroban/SKILL.md +228 -0
  46. package/src/stellar-skills/4-implementation/stellar-nextjs-wallet/SKILL.md +276 -0
  47. package/src/stellar-skills/4-implementation/stellar-sep10-auth/SKILL.md +252 -0
  48. package/src/stellar-skills/4-implementation/stellar-setup-environment/SKILL.md +163 -0
  49. package/src/stellar-skills/4-implementation/stellar-setup-trustline/SKILL.md +107 -0
  50. package/src/stellar-skills/4-implementation/stellar-test-contract/SKILL.md +146 -0
  51. package/src/stellar-skills/4-implementation/stellar-write-contract/SKILL.md +140 -0
  52. package/src/stellar-skills/module-help.csv +24 -0
  53. package/src/stellar-skills/module.yaml +103 -0
  54. package/tools/installer/cli-utils.js +39 -0
  55. package/tools/installer/commands/init.js +335 -0
  56. package/tools/installer/fs-native.js +116 -0
  57. package/tools/installer/prompts.js +852 -0
  58. package/tools/installer/stellar-cli.js +80 -0
  59. 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
+ };