@levu/snap 0.1.1 → 0.2.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 (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +26 -2
  3. package/dist/dx/terminal/index.d.ts +2 -1
  4. package/dist/dx/terminal/index.js +3 -1
  5. package/dist/dx/terminal/intro-outro.d.ts +4 -0
  6. package/dist/dx/terminal/intro-outro.js +44 -0
  7. package/dist/dx/terminal/output.d.ts +13 -1
  8. package/dist/dx/terminal/output.js +43 -2
  9. package/dist/dx/tui/index.d.ts +12 -0
  10. package/dist/dx/tui/index.js +12 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +2 -0
  13. package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
  14. package/dist/tui/component-adapters/autocomplete.js +34 -0
  15. package/dist/tui/component-adapters/note.d.ts +7 -0
  16. package/dist/tui/component-adapters/note.js +23 -0
  17. package/dist/tui/component-adapters/password.d.ts +7 -0
  18. package/dist/tui/component-adapters/password.js +24 -0
  19. package/dist/tui/component-adapters/progress.d.ts +7 -0
  20. package/dist/tui/component-adapters/progress.js +44 -0
  21. package/dist/tui/component-adapters/spinner.d.ts +10 -0
  22. package/dist/tui/component-adapters/spinner.js +48 -0
  23. package/dist/tui/component-adapters/tasks.d.ts +9 -0
  24. package/dist/tui/component-adapters/tasks.js +31 -0
  25. package/docs/component-reference.md +474 -0
  26. package/docs/getting-started.md +242 -0
  27. package/docs/help-contract-spec.md +29 -0
  28. package/docs/integration-examples.md +677 -0
  29. package/docs/module-authoring-guide.md +156 -0
  30. package/docs/snap-args.md +323 -0
  31. package/docs/snap-help.md +372 -0
  32. package/docs/snap-runtime.md +394 -0
  33. package/docs/snap-terminal.md +410 -0
  34. package/docs/snap-tui.md +529 -0
  35. package/package.json +4 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2025-02-24
9
+
10
+ ### Added
11
+ - **New Components**: Added `spinner` component for loading states during long-running operations
12
+ - **New Components**: Added `password` component for secure text input
13
+ - **New Terminal Features**: Added structured logging with `log.info()`, `log.success()`, `log.warn()`, `log.error()` utilities
14
+ - **Documentation**: Comprehensive Getting Started guide with installation and quick start examples
15
+ - **Documentation**: SnapArgs documentation - type-safe argument reading helpers
16
+ - **Documentation**: SnapHelp documentation - schema-driven help generation
17
+ - **Documentation**: SnapRuntime documentation - standardized action result helpers
18
+ - **Documentation**: SnapTui documentation - typed TUI flow definitions and components
19
+ - **Documentation**: SnapTerminal documentation - terminal output and logging helpers
20
+ - **Documentation**: Integration examples demonstrating common patterns
21
+ - **Examples**: Full example project in `example/` directory
22
+ - **Tests**: Added comprehensive test coverage for new components and documentation examples
23
+
24
+ ### Changed
25
+ - Enhanced `TerminalOutput` interface with `info()`, `success()`, and `warn()` methods
26
+ - Updated `.gitignore` with comprehensive Node.js package patterns
27
+ - Removed `.idea/` folder from git tracking
28
+
29
+ ### Fixed
30
+ - Fixed issue where users couldn't discover available components (now fully documented)
31
+
32
+ ## [0.1.1] - 2025-02-10
33
+
34
+ ### Added
35
+ - Initial release of Snap framework
36
+ - Contract-first TypeScript framework for terminal workflows
37
+ - Core contracts: ActionContract, ModuleContract, HelpContract, TuiContract
38
+ - CLI runners: multi-module, single-module, and submodule support
39
+ - DX helpers: SnapArgs, SnapHelp, SnapRuntime, SnapTui, SnapTerminal
40
+ - TUI components: text, confirm, select, multiselect, cancel, group
41
+ - Help system with deterministic, text-only output
package/README.md CHANGED
@@ -52,5 +52,29 @@ npm run dev -- system node-info
52
52
 
53
53
  ## For module authors
54
54
 
55
- - `docs/module-authoring-guide.md`
56
- - `docs/help-contract-spec.md`
55
+ ### Documentation
56
+
57
+ - **[Getting Started](./docs/getting-started.md)** - Quick start guide
58
+ - **[Module Authoring Guide](./docs/module-authoring-guide.md)** - Core concepts
59
+ - **[Help Contract Spec](./docs/help-contract-spec.md)** - Help format specification
60
+
61
+ ### DX Helper References
62
+
63
+ - **[SnapArgs](./docs/snap-args.md)** - Type-safe argument reading
64
+ - **[SnapHelp](./docs/snap-help.md)** - Schema-driven help generation
65
+ - **[SnapRuntime](./docs/snap-runtime.md)** - Standardized action results
66
+ - **[SnapTui](./docs/snap-tui.md)** - Typed TUI flow definitions
67
+ - **[SnapTerminal](./docs/snap-terminal.md)** - Terminal output helpers
68
+
69
+ ### Additional Resources
70
+
71
+ - **[Integration Examples](./docs/integration-examples.md)** - Common patterns
72
+ - **[Component Reference](./docs/component-reference.md)** - All components and gaps
73
+
74
+ ### Examples
75
+
76
+ See [`examples/`](./examples/) for working code examples:
77
+ - `basic-module.ts` - Minimal module structure
78
+ - `advanced-flow.ts` - Multi-step workflows
79
+ - `dx-helpers.ts` - All DX helpers in action
80
+ - `custom-prompt.ts` - Custom prompts with validation
@@ -1,2 +1,3 @@
1
1
  export type { TerminalOutput } from './output.js';
2
- export { createTerminalOutput } from './output.js';
2
+ export { createTerminalOutput, log } from './output.js';
3
+ export { intro, outro, cancel } from './intro-outro.js';
@@ -1 +1,3 @@
1
- export { createTerminalOutput } from './output.js';
1
+ export { createTerminalOutput, log } from './output.js';
2
+ // Intro/outro/cancel utilities
3
+ export { intro, outro, cancel } from './intro-outro.js';
@@ -0,0 +1,4 @@
1
+ import type { Writable } from 'node:stream';
2
+ export declare const intro: (message: string, stdout?: Writable) => void;
3
+ export declare const outro: (message: string, stdout?: Writable) => void;
4
+ export declare const cancel: (message: string, stdout?: Writable) => void;
@@ -0,0 +1,44 @@
1
+ const boxChars = {
2
+ topLeft: '┌',
3
+ topRight: '┐',
4
+ bottomLeft: '└',
5
+ bottomRight: '┘',
6
+ horizontal: '─',
7
+ vertical: '│',
8
+ left: ' '
9
+ };
10
+ const renderBox = (message, leftChar, rightChar, stream) => {
11
+ const padding = 1;
12
+ const lines = message.split('\n');
13
+ const maxLength = Math.max(...lines.map((line) => line.length));
14
+ stream.write(`${leftChar}${boxChars.horizontal}${' '.repeat(maxLength + padding * 2)}${boxChars.horizontal}${rightChar}\n`);
15
+ for (const line of lines) {
16
+ stream.write(`${boxChars.vertical} ${line.padEnd(maxLength + padding)} ${boxChars.vertical}\n`);
17
+ }
18
+ stream.write(`${leftChar === '┌' ? boxChars.bottomLeft : leftChar}${boxChars.horizontal}${' '.repeat(maxLength + padding * 2)}${boxChars.horizontal}${rightChar === '┐' ? boxChars.topRight : rightChar}\n`);
19
+ };
20
+ export const intro = (message, stdout = process.stdout) => {
21
+ if (!message) {
22
+ stdout.write('\n');
23
+ return;
24
+ }
25
+ renderBox(message, boxChars.topLeft, boxChars.topRight, stdout);
26
+ };
27
+ export const outro = (message, stdout = process.stdout) => {
28
+ if (!message) {
29
+ stdout.write('\n');
30
+ return;
31
+ }
32
+ stdout.write('\n');
33
+ renderBox(message, boxChars.bottomLeft, boxChars.bottomRight, stdout);
34
+ stdout.write('\n');
35
+ };
36
+ export const cancel = (message, stdout = process.stdout) => {
37
+ if (!message) {
38
+ stdout.write('\n');
39
+ return;
40
+ }
41
+ stdout.write('\n');
42
+ renderBox(message, boxChars.bottomLeft, boxChars.bottomRight, stdout);
43
+ stdout.write('\n');
44
+ };
@@ -3,5 +3,17 @@ export interface TerminalOutput {
3
3
  line(message: string): void;
4
4
  lines(messages: readonly string[]): void;
5
5
  error(message: string): void;
6
+ info(message: string): void;
7
+ success(message: string): void;
8
+ warn(message: string): void;
6
9
  }
7
- export declare const createTerminalOutput: (stdout?: Writable, stderr?: Writable) => TerminalOutput;
10
+ export interface LogOptions {
11
+ prefix?: string;
12
+ }
13
+ export declare const createTerminalOutput: (stdout?: Writable, stderr?: Writable, options?: LogOptions) => TerminalOutput;
14
+ export declare const log: {
15
+ info(message: string): void;
16
+ success(message: string): void;
17
+ warn(message: string): void;
18
+ error(message: string): void;
19
+ };
@@ -1,7 +1,20 @@
1
+ const symbols = {
2
+ info: 'ℹ',
3
+ success: '✔',
4
+ warn: '⚠',
5
+ error: '✖'
6
+ };
1
7
  const writeLine = (stream, message) => {
2
8
  stream.write(`${message}\n`);
3
9
  };
4
- export const createTerminalOutput = (stdout = process.stdout, stderr = process.stderr) => {
10
+ export const createTerminalOutput = (stdout = process.stdout, stderr = process.stderr, options = {}) => {
11
+ const formatLog = (symbol, message, color) => {
12
+ const prefix = options.prefix ?? '';
13
+ if (color) {
14
+ return `${prefix}${color}${symbol} \x1b[0m${message}`;
15
+ }
16
+ return `${prefix}${symbol} ${message}`;
17
+ };
5
18
  return {
6
19
  line(message) {
7
20
  writeLine(stdout, message);
@@ -12,7 +25,35 @@ export const createTerminalOutput = (stdout = process.stdout, stderr = process.s
12
25
  }
13
26
  },
14
27
  error(message) {
15
- writeLine(stderr, message);
28
+ writeLine(stderr, formatLog(symbols.error, message, '\x1b[31m')); // Red
29
+ },
30
+ info(message) {
31
+ writeLine(stdout, formatLog(symbols.info, message, '\x1b[34m')); // Blue
32
+ },
33
+ success(message) {
34
+ writeLine(stdout, formatLog(symbols.success, message, '\x1b[32m')); // Green
35
+ },
36
+ warn(message) {
37
+ writeLine(stdout, formatLog(symbols.warn, message, '\x1b[33m')); // Yellow
16
38
  }
17
39
  };
18
40
  };
41
+ // Convenience log functions using default streams
42
+ export const log = {
43
+ info(message) {
44
+ const terminal = createTerminalOutput();
45
+ terminal.info(message);
46
+ },
47
+ success(message) {
48
+ const terminal = createTerminalOutput();
49
+ terminal.success(message);
50
+ },
51
+ warn(message) {
52
+ const terminal = createTerminalOutput();
53
+ terminal.warn(message);
54
+ },
55
+ error(message) {
56
+ const terminal = createTerminalOutput();
57
+ terminal.error(message);
58
+ }
59
+ };
@@ -2,3 +2,15 @@ export { defineTuiOptions, defineTuiComponent, defineCustomTuiComponent, defineT
2
2
  export { defineTuiFlow } from './flow.js';
3
3
  export { backToPreviousOnNoResult, formatNoResultMessage } from './no-result.js';
4
4
  export type { NoResultBackContext, NoResultBackInput } from './no-result.js';
5
+ export { createSpinner, spinner } from '../../tui/component-adapters/spinner.js';
6
+ export type { Spinner, SpinnerOptions } from '../../tui/component-adapters/spinner.js';
7
+ export { runPasswordPrompt } from '../../tui/component-adapters/password.js';
8
+ export type { PasswordPromptInput } from '../../tui/component-adapters/password.js';
9
+ export { createProgress, progress } from '../../tui/component-adapters/progress.js';
10
+ export type { Progress } from '../../tui/component-adapters/progress.js';
11
+ export { tasks } from '../../tui/component-adapters/tasks.js';
12
+ export type { Task, TasksOptions } from '../../tui/component-adapters/tasks.js';
13
+ export { note } from '../../tui/component-adapters/note.js';
14
+ export type { NoteInput } from '../../tui/component-adapters/note.js';
15
+ export { runAutocompletePrompt } from '../../tui/component-adapters/autocomplete.js';
16
+ export type { AutocompleteInput, AutocompleteOption } from '../../tui/component-adapters/autocomplete.js';
@@ -1,3 +1,15 @@
1
1
  export { defineTuiOptions, defineTuiComponent, defineCustomTuiComponent, defineTuiStep, isCustomTuiComponent } from './components.js';
2
2
  export { defineTuiFlow } from './flow.js';
3
3
  export { backToPreviousOnNoResult, formatNoResultMessage } from './no-result.js';
4
+ // Spinner component for loading states
5
+ export { createSpinner, spinner } from '../../tui/component-adapters/spinner.js';
6
+ // Password component for secure input
7
+ export { runPasswordPrompt } from '../../tui/component-adapters/password.js';
8
+ // Progress component for quantified operations
9
+ export { createProgress, progress } from '../../tui/component-adapters/progress.js';
10
+ // Tasks component for sequential async operations
11
+ export { tasks } from '../../tui/component-adapters/tasks.js';
12
+ // Note component for decorative message boxes
13
+ export { note } from '../../tui/component-adapters/note.js';
14
+ // Autocomplete component for searchable selections
15
+ export { runAutocompletePrompt } from '../../tui/component-adapters/autocomplete.js';
package/dist/index.d.ts CHANGED
@@ -12,4 +12,8 @@ export { createPromptToolkit } from './tui/prompt-toolkit.js';
12
12
  export type { PromptToolkit } from './tui/prompt-toolkit.js';
13
13
  export { runCustomPrompt, createCustomPromptRunner } from './tui/custom/index.js';
14
14
  export type { CustomPromptInput, CustomPromptRunner } from './tui/custom/index.js';
15
+ export { createSpinner, spinner } from './tui/component-adapters/spinner.js';
16
+ export type { Spinner, SpinnerOptions } from './tui/component-adapters/spinner.js';
17
+ export { runPasswordPrompt } from './tui/component-adapters/password.js';
18
+ export type { PasswordPromptInput } from './tui/component-adapters/password.js';
15
19
  export declare const createRegistry: (modules: ModuleContract[]) => ActionRegistry;
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ export * as SnapTerminal from './dx/terminal/index.js';
8
8
  export * as SnapTui from './dx/tui/index.js';
9
9
  export { createPromptToolkit } from './tui/prompt-toolkit.js';
10
10
  export { runCustomPrompt, createCustomPromptRunner } from './tui/custom/index.js';
11
+ export { createSpinner, spinner } from './tui/component-adapters/spinner.js';
12
+ export { runPasswordPrompt } from './tui/component-adapters/password.js';
11
13
  export const createRegistry = (modules) => {
12
14
  const registry = new ActionRegistry();
13
15
  for (const moduleContract of modules) {
@@ -0,0 +1,15 @@
1
+ export interface AutocompleteOption {
2
+ value: string;
3
+ label: string;
4
+ hint?: string;
5
+ }
6
+ export interface AutocompleteInput {
7
+ message: string;
8
+ options: AutocompleteOption[];
9
+ placeholder?: string;
10
+ initialValue?: string;
11
+ maxItems?: number;
12
+ required?: boolean;
13
+ validate?: (value: string) => string | Error | undefined;
14
+ }
15
+ export declare const runAutocompletePrompt: (input: AutocompleteInput) => Promise<string | undefined>;
@@ -0,0 +1,34 @@
1
+ import { autocomplete as clackAutocomplete } from '@clack/prompts';
2
+ import { isInteractiveTerminal } from './readline-utils.js';
3
+ import { unwrapClackResult } from './cancel.js';
4
+ export const runAutocompletePrompt = async (input) => {
5
+ if (!isInteractiveTerminal()) {
6
+ // Non-interactive: return initial value or first option
7
+ return input.initialValue ?? input.options[0]?.value;
8
+ }
9
+ const value = await clackAutocomplete({
10
+ message: input.message,
11
+ options: input.options,
12
+ placeholder: input.placeholder,
13
+ initialValue: input.initialValue,
14
+ maxItems: input.maxItems,
15
+ validate: (raw) => {
16
+ // Handle string | string[] | undefined from clack
17
+ const valueToValidate = typeof raw === 'string' ? raw : Array.isArray(raw) ? raw[0] ?? '' : '';
18
+ if (input.required && !valueToValidate) {
19
+ return 'This field is required';
20
+ }
21
+ if (input.validate) {
22
+ const validationResult = input.validate(valueToValidate);
23
+ if (typeof validationResult === 'string') {
24
+ return validationResult;
25
+ }
26
+ if (validationResult instanceof Error) {
27
+ return validationResult.message;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ });
33
+ return unwrapClackResult(value);
34
+ };
@@ -0,0 +1,7 @@
1
+ import type { Writable } from 'node:stream';
2
+ export interface NoteInput {
3
+ message: string;
4
+ title?: string;
5
+ format?: (line: string) => string;
6
+ }
7
+ export declare const note: (input: NoteInput, stdout?: Writable) => void;
@@ -0,0 +1,23 @@
1
+ const boxChars = {
2
+ topLeft: '╮',
3
+ topRight: '╯',
4
+ bottomLeft: '╰',
5
+ bottomRight: '╯',
6
+ horizontal: '─',
7
+ vertical: '│'
8
+ };
9
+ export const note = (input, stdout = process.stdout) => {
10
+ const { message, title = '', format } = input;
11
+ const lines = message.split('\n');
12
+ const formattedLines = format ? lines.map(format) : lines;
13
+ const maxLength = Math.max(...formattedLines.map((line) => line.length), title.length);
14
+ const padding = 2;
15
+ // Top line
16
+ stdout.write(`${boxChars.vertical} ${title.padEnd(maxLength + padding)} ${boxChars.topLeft}${boxChars.horizontal.repeat(maxLength + padding * 2)}${boxChars.horizontal}\n`);
17
+ // Message lines
18
+ for (const line of formattedLines) {
19
+ stdout.write(`${boxChars.vertical} ${' '.repeat(maxLength + padding)} ${boxChars.vertical} ${line.padEnd(maxLength + padding)} ${boxChars.vertical}\n`);
20
+ }
21
+ // Bottom line
22
+ stdout.write(`${boxChars.horizontal}${boxChars.horizontal.repeat(maxLength + padding * 2)}${boxChars.horizontal}${boxChars.horizontal.repeat(maxLength + padding * 2)}${boxChars.vertical}\n`);
23
+ };
@@ -0,0 +1,7 @@
1
+ export interface PasswordPromptInput {
2
+ message: string;
3
+ required?: boolean;
4
+ validate?: (value: string) => string | Error | undefined;
5
+ mask?: string;
6
+ }
7
+ export declare const runPasswordPrompt: (input: PasswordPromptInput) => Promise<string>;
@@ -0,0 +1,24 @@
1
+ import { password as clackPassword } from '@clack/prompts';
2
+ import { isInteractiveTerminal } from './readline-utils.js';
3
+ import { unwrapClackResult } from './cancel.js';
4
+ export const runPasswordPrompt = async (input) => {
5
+ if (!isInteractiveTerminal()) {
6
+ // For non-interactive terminals, read from stdin in a secure way
7
+ // or return empty/throw error
8
+ if (input.required) {
9
+ throw new Error(`Password required: ${input.message}`);
10
+ }
11
+ return '';
12
+ }
13
+ const value = await clackPassword({
14
+ message: input.message,
15
+ mask: input.mask ?? '•',
16
+ validate: (raw) => {
17
+ if (input.required && (!raw || raw.trim().length === 0)) {
18
+ return `Password is required`;
19
+ }
20
+ return input.validate?.(raw ?? '');
21
+ }
22
+ });
23
+ return unwrapClackResult(value);
24
+ };
@@ -0,0 +1,7 @@
1
+ export interface Progress {
2
+ start(message: string): void;
3
+ message(message: string): void;
4
+ stop(message?: string): void;
5
+ }
6
+ export declare const createProgress: () => Progress;
7
+ export declare const progress: () => Progress;
@@ -0,0 +1,44 @@
1
+ import { isInteractiveTerminal } from './readline-utils.js';
2
+ export const createProgress = () => {
3
+ // Non-interactive fallback
4
+ if (!isInteractiveTerminal()) {
5
+ let currentMessage = '';
6
+ return {
7
+ start(message) {
8
+ currentMessage = message;
9
+ process.stdout.write(`${message}...\n`);
10
+ },
11
+ message(newMessage) {
12
+ currentMessage = newMessage;
13
+ process.stdout.write(`${newMessage}\n`);
14
+ },
15
+ stop(finalMessage) {
16
+ if (finalMessage) {
17
+ process.stdout.write(`${finalMessage}\n`);
18
+ }
19
+ }
20
+ };
21
+ }
22
+ // Interactive progress using @clack/prompts
23
+ let progressInstance = null;
24
+ return {
25
+ start(message) {
26
+ // Lazy load progress
27
+ import('@clack/prompts').then(({ progress }) => {
28
+ progressInstance = progress();
29
+ progressInstance.start(message);
30
+ });
31
+ },
32
+ message(newMessage) {
33
+ if (progressInstance) {
34
+ progressInstance.message(newMessage);
35
+ }
36
+ },
37
+ stop(finalMessage) {
38
+ if (progressInstance) {
39
+ progressInstance.stop(finalMessage);
40
+ }
41
+ }
42
+ };
43
+ };
44
+ export const progress = createProgress;
@@ -0,0 +1,10 @@
1
+ export interface SpinnerOptions {
2
+ message?: string;
3
+ }
4
+ export interface Spinner {
5
+ start(message?: string): void;
6
+ stop(message?: string): void;
7
+ message(message: string): void;
8
+ }
9
+ export declare const createSpinner: (options?: SpinnerOptions) => Spinner;
10
+ export declare const spinner: (options?: SpinnerOptions) => Spinner;
@@ -0,0 +1,48 @@
1
+ import { spinner as clackSpinner } from '@clack/prompts';
2
+ import { isInteractiveTerminal } from './readline-utils.js';
3
+ export const createSpinner = (options = {}) => {
4
+ // Non-interactive fallback
5
+ if (!isInteractiveTerminal()) {
6
+ let currentMessage = options.message ?? '';
7
+ return {
8
+ start(message) {
9
+ currentMessage = message ?? currentMessage;
10
+ if (currentMessage) {
11
+ process.stdout.write(`${currentMessage}...\n`);
12
+ }
13
+ },
14
+ stop(message) {
15
+ if (message) {
16
+ process.stdout.write(`${message}\n`);
17
+ }
18
+ },
19
+ message(newMessage) {
20
+ currentMessage = newMessage;
21
+ }
22
+ };
23
+ }
24
+ // Interactive spinner using @clack/prompts
25
+ const internalSpinner = clackSpinner();
26
+ return {
27
+ start(message) {
28
+ if (message) {
29
+ internalSpinner.start(message);
30
+ }
31
+ else if (options.message) {
32
+ internalSpinner.start(options.message);
33
+ }
34
+ },
35
+ stop(message) {
36
+ if (message) {
37
+ internalSpinner.stop(message);
38
+ }
39
+ else {
40
+ internalSpinner.stop();
41
+ }
42
+ },
43
+ message(newMessage) {
44
+ internalSpinner.message(newMessage);
45
+ }
46
+ };
47
+ };
48
+ export const spinner = createSpinner;
@@ -0,0 +1,9 @@
1
+ export interface Task {
2
+ title: string;
3
+ task: (message: (msg: string) => void) => Promise<string>;
4
+ }
5
+ export interface TasksOptions {
6
+ /** Called when a task is cancelled */
7
+ onCancel?: (results: Record<string, string>) => void;
8
+ }
9
+ export declare const tasks: (taskList: Task[], options?: TasksOptions) => Promise<Record<string, string>>;
@@ -0,0 +1,31 @@
1
+ import { isInteractiveTerminal } from './readline-utils.js';
2
+ export const tasks = async (taskList, options = {}) => {
3
+ const results = {};
4
+ if (!isInteractiveTerminal()) {
5
+ // Non-interactive: just run tasks without spinner
6
+ for (const taskItem of taskList) {
7
+ try {
8
+ const result = await taskItem.task(() => { });
9
+ results[taskItem.title] = result;
10
+ process.stdout.write(`${taskItem.title}: ${result}\n`);
11
+ }
12
+ catch (error) {
13
+ results[taskItem.title] = error instanceof Error ? error.message : String(error);
14
+ }
15
+ }
16
+ return results;
17
+ }
18
+ // Import clack dynamically for interactive mode
19
+ const { tasks: clackTasks } = await import('@clack/prompts');
20
+ // Wrap tasks to collect results
21
+ const wrappedTasks = taskList.map((task) => ({
22
+ ...task,
23
+ task: async (message) => {
24
+ const result = await task.task(message);
25
+ results[task.title] = result;
26
+ return result;
27
+ }
28
+ }));
29
+ await clackTasks(wrappedTasks, options);
30
+ return results;
31
+ };