@levu/snap 0.3.11 → 0.3.13
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 +17 -0
- package/README.md +1 -0
- package/dist/runtime/engine.js +45 -8
- package/dist/tui/component-adapters/cancel.d.ts +5 -0
- package/dist/tui/component-adapters/cancel.js +13 -0
- package/dist/tui/prompt-toolkit.d.ts +17 -1
- package/dist/tui/prompt-toolkit.js +34 -7
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.13] - 2026-03-05
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- TUI prompt cancellation (`Esc`) now defaults to step-back behavior when the workflow has a previous step; when already at the first/root step, cancel still exits.
|
|
12
|
+
- Runtime engine now maps root prompt-cancel to exit code `130` (`INTERRUPTED`) instead of internal error.
|
|
13
|
+
- Added prompt lifecycle hooks in `createPromptToolkit` to support cancel/retry navigation handling.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Added regression tests for prompt retry/cancel lifecycle handling and runtime retry/interrupted behavior.
|
|
17
|
+
|
|
18
|
+
## [0.3.12] - 2026-03-04
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Added missing runtime dependency `picocolors` to prevent `ERR_MODULE_NOT_FOUND` when consuming password/multiline adapters from published packages.
|
|
22
|
+
- Improved password prompt stability with a raw-mode terminal path to reduce cursor/glyph jitter in interactive input.
|
|
23
|
+
- Added password adapter regression tests and updated component reference keyboard behavior notes.
|
|
24
|
+
|
|
8
25
|
## [0.3.11] - 2026-02-28
|
|
9
26
|
|
|
10
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ For module/tool authors, Snap also exposes optional DX helper groups:
|
|
|
21
21
|
- Uses Clack-powered prompt adapters for interactive TUI (`select`, `text`, `confirm`, `multiselect`)
|
|
22
22
|
- Text prompts support clipboard paste and multiline input for pasting multiple lines
|
|
23
23
|
- Supports workflow transitions: `next`, `back`, `jump`, `exit`
|
|
24
|
+
- In TUI mode, pressing `Esc` backs to the previous workflow step by default; pressing `Esc` on the root step exits.
|
|
24
25
|
- Supports resume checkpoints for interrupted flows
|
|
25
26
|
- Produces stable help output hierarchy:
|
|
26
27
|
- `snap -h`
|
package/dist/runtime/engine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ExitCode } from '../core/errors/framework-errors.js';
|
|
2
2
|
import { createFlowController } from '../dx/runtime/index.js';
|
|
3
3
|
import { createTerminalOutput } from '../dx/terminal/index.js';
|
|
4
|
+
import { isPromptCancelledError, isPromptRetryError } from '../tui/component-adapters/cancel.js';
|
|
4
5
|
import { createPromptToolkit } from '../tui/prompt-toolkit.js';
|
|
5
6
|
import { StateMachine } from './state-machine.js';
|
|
6
7
|
const resolveWorkflowNodes = (action) => {
|
|
@@ -36,20 +37,56 @@ export const executeAction = async (input) => {
|
|
|
36
37
|
args: input.args,
|
|
37
38
|
flow: createFlowController(stateMachine),
|
|
38
39
|
terminal: createTerminalOutput(),
|
|
39
|
-
prompts: createPromptToolkit(
|
|
40
|
+
prompts: createPromptToolkit({
|
|
41
|
+
onPromptResolved: () => {
|
|
42
|
+
if (input.mode !== 'tui')
|
|
43
|
+
return;
|
|
44
|
+
stateMachine.transition({ type: 'next' });
|
|
45
|
+
},
|
|
46
|
+
onPromptCancelled: () => {
|
|
47
|
+
if (input.mode !== 'tui')
|
|
48
|
+
return 'cancel';
|
|
49
|
+
if (stateMachine.snapshot().cursor <= 0)
|
|
50
|
+
return 'cancel';
|
|
51
|
+
stateMachine.transition({ type: 'back' });
|
|
52
|
+
return 'retry';
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
40
55
|
stateMachine
|
|
41
56
|
};
|
|
42
57
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await input.
|
|
58
|
+
while (true) {
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = await input.action.run(context);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (isPromptRetryError(error)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (isPromptCancelledError(error)) {
|
|
68
|
+
if (input.resumeStore) {
|
|
69
|
+
await input.resumeStore.clear();
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
mode: input.mode,
|
|
74
|
+
exitCode: ExitCode.INTERRUPTED,
|
|
75
|
+
errorMessage: undefined
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
47
79
|
}
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
if (input.resumeStore) {
|
|
81
|
+
if (stateMachine.snapshot().exited || result.ok) {
|
|
82
|
+
await input.resumeStore.clear();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await input.resumeStore.save(stateMachine.checkpoint());
|
|
86
|
+
}
|
|
50
87
|
}
|
|
88
|
+
return { ...result, mode: input.mode, exitCode: result.exitCode ?? ExitCode.SUCCESS };
|
|
51
89
|
}
|
|
52
|
-
return { ...result, mode: input.mode, exitCode: result.exitCode ?? ExitCode.SUCCESS };
|
|
53
90
|
}
|
|
54
91
|
catch (error) {
|
|
55
92
|
if (input.resumeStore) {
|
|
@@ -2,5 +2,10 @@ export declare class PromptCancelledError extends Error {
|
|
|
2
2
|
readonly isPromptCancelled = true;
|
|
3
3
|
constructor(message?: string);
|
|
4
4
|
}
|
|
5
|
+
export declare class PromptRetryError extends Error {
|
|
6
|
+
readonly isPromptRetry = true;
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
5
9
|
export declare const isPromptCancelledError: (error: unknown) => error is PromptCancelledError;
|
|
10
|
+
export declare const isPromptRetryError: (error: unknown) => error is PromptRetryError;
|
|
6
11
|
export declare const unwrapClackResult: <T>(value: T | symbol, cancelMessage?: string) => T;
|
|
@@ -6,12 +6,25 @@ export class PromptCancelledError extends Error {
|
|
|
6
6
|
this.name = 'PromptCancelledError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
export class PromptRetryError extends Error {
|
|
10
|
+
isPromptRetry = true;
|
|
11
|
+
constructor(message = 'Retry prompt workflow.') {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'PromptRetryError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
9
16
|
export const isPromptCancelledError = (error) => {
|
|
10
17
|
return error instanceof PromptCancelledError || (typeof error === 'object' &&
|
|
11
18
|
error !== null &&
|
|
12
19
|
'isPromptCancelled' in error &&
|
|
13
20
|
error.isPromptCancelled === true);
|
|
14
21
|
};
|
|
22
|
+
export const isPromptRetryError = (error) => {
|
|
23
|
+
return error instanceof PromptRetryError || (typeof error === 'object' &&
|
|
24
|
+
error !== null &&
|
|
25
|
+
'isPromptRetry' in error &&
|
|
26
|
+
error.isPromptRetry === true);
|
|
27
|
+
};
|
|
15
28
|
export const unwrapClackResult = (value, cancelMessage = 'Cancelled by user.') => {
|
|
16
29
|
if (isCancel(value)) {
|
|
17
30
|
throw new PromptCancelledError(cancelMessage);
|
|
@@ -3,6 +3,7 @@ import type { GroupStep } from './component-adapters/group.js';
|
|
|
3
3
|
import type { MultiSelectPromptInput } from './component-adapters/multiselect.js';
|
|
4
4
|
import type { SelectPromptInput } from './component-adapters/select.js';
|
|
5
5
|
import type { TextPromptInput } from './component-adapters/text.js';
|
|
6
|
+
import { PromptCancelledError } from './component-adapters/cancel.js';
|
|
6
7
|
import { type CustomPromptInput } from './custom/custom-prompt.js';
|
|
7
8
|
export interface PromptToolkit {
|
|
8
9
|
text(input: TextPromptInput): Promise<string>;
|
|
@@ -12,4 +13,19 @@ export interface PromptToolkit {
|
|
|
12
13
|
group<T = unknown>(steps: GroupStep<T>[]): Promise<Record<string, T>>;
|
|
13
14
|
custom<T>(input: CustomPromptInput<T>): Promise<T>;
|
|
14
15
|
}
|
|
15
|
-
export
|
|
16
|
+
export type PromptCancelDecision = 'cancel' | 'retry';
|
|
17
|
+
interface PromptToolkitAdapters {
|
|
18
|
+
text: (input: TextPromptInput) => Promise<string>;
|
|
19
|
+
confirm: (input: ConfirmPromptInput) => Promise<boolean>;
|
|
20
|
+
select: (input: SelectPromptInput) => Promise<string>;
|
|
21
|
+
multiselect: (input: MultiSelectPromptInput) => Promise<string[]>;
|
|
22
|
+
group: <T = unknown>(steps: GroupStep<T>[]) => Promise<Record<string, T>>;
|
|
23
|
+
custom: <T>(input: CustomPromptInput<T>) => Promise<T>;
|
|
24
|
+
}
|
|
25
|
+
export interface PromptToolkitOptions {
|
|
26
|
+
onPromptResolved?: () => void | Promise<void>;
|
|
27
|
+
onPromptCancelled?: (error: PromptCancelledError) => PromptCancelDecision | void | Promise<PromptCancelDecision | void>;
|
|
28
|
+
adapters?: Partial<PromptToolkitAdapters>;
|
|
29
|
+
}
|
|
30
|
+
export declare const createPromptToolkit: (options?: PromptToolkitOptions) => PromptToolkit;
|
|
31
|
+
export {};
|
|
@@ -3,15 +3,42 @@ import { runGroupPrompt } from './component-adapters/group.js';
|
|
|
3
3
|
import { runMultiSelectPrompt } from './component-adapters/multiselect.js';
|
|
4
4
|
import { runSelectPrompt } from './component-adapters/select.js';
|
|
5
5
|
import { runTextPrompt } from './component-adapters/text.js';
|
|
6
|
+
import { isPromptCancelledError, PromptRetryError } from './component-adapters/cancel.js';
|
|
6
7
|
import { createCustomPromptRunner } from './custom/custom-prompt.js';
|
|
7
|
-
|
|
8
|
+
const withPromptLifecycle = (runner, options) => {
|
|
9
|
+
return async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
const value = await runner(input);
|
|
12
|
+
await options.onPromptResolved?.();
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (isPromptCancelledError(error)) {
|
|
17
|
+
const decision = await options.onPromptCancelled?.(error);
|
|
18
|
+
if (decision === 'retry') {
|
|
19
|
+
throw new PromptRetryError();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export const createPromptToolkit = (options = {}) => {
|
|
8
27
|
const customRunner = createCustomPromptRunner();
|
|
28
|
+
const adapters = {
|
|
29
|
+
text: options.adapters?.text ?? runTextPrompt,
|
|
30
|
+
confirm: options.adapters?.confirm ?? runConfirmPrompt,
|
|
31
|
+
select: options.adapters?.select ?? runSelectPrompt,
|
|
32
|
+
multiselect: options.adapters?.multiselect ?? runMultiSelectPrompt,
|
|
33
|
+
group: options.adapters?.group ?? runGroupPrompt,
|
|
34
|
+
custom: options.adapters?.custom ?? customRunner.run
|
|
35
|
+
};
|
|
9
36
|
return {
|
|
10
|
-
text:
|
|
11
|
-
confirm:
|
|
12
|
-
select:
|
|
13
|
-
multiselect:
|
|
14
|
-
group:
|
|
15
|
-
custom:
|
|
37
|
+
text: withPromptLifecycle(adapters.text, options),
|
|
38
|
+
confirm: withPromptLifecycle(adapters.confirm, options),
|
|
39
|
+
select: withPromptLifecycle(adapters.select, options),
|
|
40
|
+
multiselect: withPromptLifecycle(adapters.multiselect, options),
|
|
41
|
+
group: withPromptLifecycle(adapters.group, options),
|
|
42
|
+
custom: withPromptLifecycle(adapters.custom, options)
|
|
16
43
|
};
|
|
17
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levu/snap",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"snap": "./dist/cli-entry.js"
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"dev": "tsx src/cli-entry.ts"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@clack/prompts": "^1.0.0"
|
|
15
|
+
"@clack/prompts": "^1.0.0",
|
|
16
|
+
"picocolors": "^1.1.1"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@types/node": "^22.10.2",
|