@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.
- package/CHANGELOG.md +41 -0
- package/README.md +26 -2
- package/dist/dx/terminal/index.d.ts +2 -1
- package/dist/dx/terminal/index.js +3 -1
- package/dist/dx/terminal/intro-outro.d.ts +4 -0
- package/dist/dx/terminal/intro-outro.js +44 -0
- package/dist/dx/terminal/output.d.ts +13 -1
- package/dist/dx/terminal/output.js +43 -2
- package/dist/dx/tui/index.d.ts +12 -0
- package/dist/dx/tui/index.js +12 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
- package/dist/tui/component-adapters/autocomplete.js +34 -0
- package/dist/tui/component-adapters/note.d.ts +7 -0
- package/dist/tui/component-adapters/note.js +23 -0
- package/dist/tui/component-adapters/password.d.ts +7 -0
- package/dist/tui/component-adapters/password.js +24 -0
- package/dist/tui/component-adapters/progress.d.ts +7 -0
- package/dist/tui/component-adapters/progress.js +44 -0
- package/dist/tui/component-adapters/spinner.d.ts +10 -0
- package/dist/tui/component-adapters/spinner.js +48 -0
- package/dist/tui/component-adapters/tasks.d.ts +9 -0
- package/dist/tui/component-adapters/tasks.js +31 -0
- package/docs/component-reference.md +474 -0
- package/docs/getting-started.md +242 -0
- package/docs/help-contract-spec.md +29 -0
- package/docs/integration-examples.md +677 -0
- package/docs/module-authoring-guide.md +156 -0
- package/docs/snap-args.md +323 -0
- package/docs/snap-help.md +372 -0
- package/docs/snap-runtime.md +394 -0
- package/docs/snap-terminal.md +410 -0
- package/docs/snap-tui.md +529 -0
- 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
|
-
|
|
56
|
-
|
|
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
|
|
@@ -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
|
|
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
|
+
};
|
package/dist/dx/tui/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/dx/tui/index.js
CHANGED
|
@@ -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,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,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,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
|
+
};
|