@levu/snap 0.1.1 → 0.3.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 (39) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +27 -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 +6 -0
  12. package/dist/index.js +3 -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/multiline-text.d.ts +13 -0
  16. package/dist/tui/component-adapters/multiline-text.js +166 -0
  17. package/dist/tui/component-adapters/note.d.ts +7 -0
  18. package/dist/tui/component-adapters/note.js +23 -0
  19. package/dist/tui/component-adapters/password.d.ts +7 -0
  20. package/dist/tui/component-adapters/password.js +24 -0
  21. package/dist/tui/component-adapters/progress.d.ts +7 -0
  22. package/dist/tui/component-adapters/progress.js +44 -0
  23. package/dist/tui/component-adapters/spinner.d.ts +21 -0
  24. package/dist/tui/component-adapters/spinner.js +89 -0
  25. package/dist/tui/component-adapters/tasks.d.ts +9 -0
  26. package/dist/tui/component-adapters/tasks.js +31 -0
  27. package/dist/tui/component-adapters/text.d.ts +4 -0
  28. package/dist/tui/component-adapters/text.js +18 -0
  29. package/docs/component-reference.md +557 -0
  30. package/docs/getting-started.md +242 -0
  31. package/docs/help-contract-spec.md +29 -0
  32. package/docs/integration-examples.md +677 -0
  33. package/docs/module-authoring-guide.md +156 -0
  34. package/docs/snap-args.md +323 -0
  35. package/docs/snap-help.md +372 -0
  36. package/docs/snap-runtime.md +394 -0
  37. package/docs/snap-terminal.md +410 -0
  38. package/docs/snap-tui.md +529 -0
  39. 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
@@ -19,6 +19,7 @@ For module/tool authors, Snap also exposes optional DX helper groups:
19
19
  - Enforces action triad at registration: `tui + commandline + help`
20
20
  - Uses one runtime engine for TUI and CLI paths
21
21
  - Uses Clack-powered prompt adapters for interactive TUI (`select`, `text`, `confirm`, `multiselect`)
22
+ - Text prompts support clipboard paste and multiline input for pasting multiple lines
22
23
  - Supports workflow transitions: `next`, `back`, `jump`, `exit`
23
24
  - Supports resume checkpoints for interrupted flows
24
25
  - Produces stable help output hierarchy:
@@ -52,5 +53,29 @@ npm run dev -- system node-info
52
53
 
53
54
  ## For module authors
54
55
 
55
- - `docs/module-authoring-guide.md`
56
- - `docs/help-contract-spec.md`
56
+ ### Documentation
57
+
58
+ - **[Getting Started](./docs/getting-started.md)** - Quick start guide
59
+ - **[Module Authoring Guide](./docs/module-authoring-guide.md)** - Core concepts
60
+ - **[Help Contract Spec](./docs/help-contract-spec.md)** - Help format specification
61
+
62
+ ### DX Helper References
63
+
64
+ - **[SnapArgs](./docs/snap-args.md)** - Type-safe argument reading
65
+ - **[SnapHelp](./docs/snap-help.md)** - Schema-driven help generation
66
+ - **[SnapRuntime](./docs/snap-runtime.md)** - Standardized action results
67
+ - **[SnapTui](./docs/snap-tui.md)** - Typed TUI flow definitions
68
+ - **[SnapTerminal](./docs/snap-terminal.md)** - Terminal output helpers
69
+
70
+ ### Additional Resources
71
+
72
+ - **[Integration Examples](./docs/integration-examples.md)** - Common patterns
73
+ - **[Component Reference](./docs/component-reference.md)** - All components and gaps
74
+
75
+ ### Examples
76
+
77
+ See [`examples/`](./examples/) for working code examples:
78
+ - `basic-module.ts` - Minimal module structure
79
+ - `advanced-flow.ts` - Multi-step workflows
80
+ - `dx-helpers.ts` - All DX helpers in action
81
+ - `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,10 @@ 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';
19
+ export { createMultilineTextPrompt } from './tui/component-adapters/multiline-text.js';
20
+ export type { MultilineTextOptions } from './tui/component-adapters/multiline-text.js';
15
21
  export declare const createRegistry: (modules: ModuleContract[]) => ActionRegistry;
package/dist/index.js CHANGED
@@ -8,6 +8,9 @@ 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';
13
+ export { createMultilineTextPrompt } from './tui/component-adapters/multiline-text.js';
11
14
  export const createRegistry = (modules) => {
12
15
  const registry = new ActionRegistry();
13
16
  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,13 @@
1
+ import { Readable } from 'node:stream';
2
+ import { Writable } from 'node:stream';
3
+ export interface MultilineTextOptions {
4
+ message: string;
5
+ initialValue?: string;
6
+ placeholder?: string;
7
+ validate?: (value: string | undefined) => string | Error | undefined;
8
+ allowPaste?: boolean;
9
+ input?: Readable;
10
+ output?: Writable;
11
+ signal?: AbortSignal;
12
+ }
13
+ export declare const createMultilineTextPrompt: () => (opts: MultilineTextOptions) => Promise<string | symbol>;
@@ -0,0 +1,166 @@
1
+ import { createInterface } from 'node:readline';
2
+ import * as pc from 'picocolors';
3
+ export const createMultilineTextPrompt = () => {
4
+ return async (opts) => {
5
+ const { message, initialValue = '', placeholder = '', validate, allowPaste = false, input = process.stdin, output = process.stdout, signal, } = opts;
6
+ // Use standard text prompt for single line paste
7
+ if (!allowPaste) {
8
+ const { text: textPrompt } = await import('@clack/prompts');
9
+ return textPrompt({
10
+ message,
11
+ initialValue,
12
+ placeholder,
13
+ validate,
14
+ input,
15
+ output,
16
+ signal,
17
+ });
18
+ }
19
+ // For multiline paste support, use a custom readline-based approach
20
+ return new Promise((resolve, reject) => {
21
+ const rl = createInterface({
22
+ input,
23
+ output,
24
+ terminal: true,
25
+ });
26
+ let value = initialValue;
27
+ let cancelled = false;
28
+ const cleanup = () => {
29
+ rl.close();
30
+ };
31
+ const submit = (val) => {
32
+ cleanup();
33
+ resolve(val);
34
+ };
35
+ const doCancel = () => {
36
+ cancelled = true;
37
+ cleanup();
38
+ const { isCancel: cancelSymbol } = require('@clack/prompts');
39
+ resolve(cancelSymbol);
40
+ };
41
+ // Show instructions
42
+ output.write(`\n${pc.cyan('○')} ${pc.bold(message)}\n`);
43
+ if (allowPaste) {
44
+ output.write(pc.dim(` Paste support: Ctrl+V to paste (macOS/Linux: Cmd+Shift+V)\n`));
45
+ }
46
+ output.write(pc.dim(` Press Enter twice or Alt+Enter to submit\n`));
47
+ const lines = value.split('\n');
48
+ let currentLine = lines.length > 0 ? lines.pop() : '';
49
+ const showPrompt = () => {
50
+ output.write(`\n${pc.dim('> ')}${currentLine}`);
51
+ };
52
+ showPrompt();
53
+ // Handle paste from clipboard
54
+ const handlePaste = async () => {
55
+ try {
56
+ const { execSync } = await import('node:child_process');
57
+ const platform = process.platform;
58
+ if (platform === 'darwin') {
59
+ return execSync('pbpaste', { encoding: 'utf-8' });
60
+ }
61
+ else if (platform === 'win32') {
62
+ return execSync('powershell -command "Get-Clipboard"', { encoding: 'utf-8', shell: true }).trim();
63
+ }
64
+ else if (platform === 'linux') {
65
+ try {
66
+ return execSync('xclip -selection clipboard -o', {
67
+ encoding: 'utf-8',
68
+ });
69
+ }
70
+ catch {
71
+ try {
72
+ return execSync('xsel --clipboard --output', {
73
+ encoding: 'utf-8',
74
+ });
75
+ }
76
+ catch {
77
+ return '';
78
+ }
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ // Silent fail if clipboard is unavailable
84
+ }
85
+ return '';
86
+ };
87
+ let lastEnterTime = 0;
88
+ const DOUBLE_ENTER_TIMEOUT = 500; // ms
89
+ rl.on('line', (line) => {
90
+ if (cancelled)
91
+ return;
92
+ const now = Date.now();
93
+ // Check for double Enter to submit
94
+ if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
95
+ submit(lines.join('\n') + currentLine);
96
+ return;
97
+ }
98
+ lastEnterTime = now;
99
+ if (line.trim() === '') {
100
+ // Empty line - add to lines
101
+ if (currentLine !== '') {
102
+ lines.push(currentLine);
103
+ currentLine = '';
104
+ }
105
+ }
106
+ else {
107
+ // Non-empty line
108
+ if (currentLine !== '') {
109
+ lines.push(currentLine);
110
+ }
111
+ currentLine = line;
112
+ }
113
+ showPrompt();
114
+ });
115
+ // Handle SIGINT (Ctrl+C)
116
+ rl.on('SIGINT', () => {
117
+ doCancel();
118
+ });
119
+ // Handle signal
120
+ if (signal) {
121
+ signal.addEventListener('abort', () => {
122
+ doCancel();
123
+ });
124
+ }
125
+ // Handle paste via keyboard shortcut
126
+ if (allowPaste && input.setRawMode) {
127
+ input.setRawMode(true);
128
+ input.resume();
129
+ input.on('keypress', async (str, key) => {
130
+ if (cancelled)
131
+ return;
132
+ // Detect Ctrl+V or Cmd+V for paste
133
+ if ((key.ctrl && key.name === 'v') || (key.meta && key.name === 'v')) {
134
+ const pasted = await handlePaste();
135
+ if (pasted) {
136
+ // Clear current line and show pasted content
137
+ output.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
138
+ const pastedLines = pasted.split('\n');
139
+ if (pastedLines.length > 1) {
140
+ // Multiline paste
141
+ lines.push(...pastedLines.slice(0, -1));
142
+ currentLine = pastedLines[pastedLines.length - 1];
143
+ }
144
+ else {
145
+ // Single line paste
146
+ currentLine += pasted;
147
+ }
148
+ output.write(`${pc.dim('> ')}${currentLine}`);
149
+ }
150
+ }
151
+ else if (key.alt && key.name === 'enter') {
152
+ // Alt+Enter to submit
153
+ submit(lines.join('\n') + currentLine);
154
+ }
155
+ else if (key.name === 'escape') {
156
+ doCancel();
157
+ }
158
+ });
159
+ }
160
+ // Handle non-interactive terminal
161
+ if (!input.isTTY) {
162
+ submit(initialValue);
163
+ }
164
+ });
165
+ };
166
+ };
@@ -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,21 @@
1
+ export interface SpinnerOptions {
2
+ message?: string;
3
+ indicator?: 'dots' | 'timer';
4
+ onCancel?: () => void;
5
+ cancelMessage?: string;
6
+ errorMessage?: string;
7
+ frames?: string[];
8
+ delay?: number;
9
+ styleFrame?: (frame: string) => string;
10
+ }
11
+ export interface Spinner {
12
+ start(message?: string): void;
13
+ stop(message?: string): void;
14
+ cancel(message?: string): void;
15
+ error(message?: string): void;
16
+ message(message: string): void;
17
+ clear(): void;
18
+ readonly isCancelled: boolean;
19
+ }
20
+ export declare const createSpinner: (options?: SpinnerOptions) => Spinner;
21
+ export declare const spinner: (options?: SpinnerOptions) => Spinner;