@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 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`
@@ -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
- const result = await input.action.run(context);
44
- if (input.resumeStore) {
45
- if (stateMachine.snapshot().exited || result.ok) {
46
- await input.resumeStore.clear();
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
- else {
49
- await input.resumeStore.save(stateMachine.checkpoint());
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 declare const createPromptToolkit: () => PromptToolkit;
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
- export const createPromptToolkit = () => {
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: runTextPrompt,
11
- confirm: runConfirmPrompt,
12
- select: runSelectPrompt,
13
- multiselect: runMultiSelectPrompt,
14
- group: runGroupPrompt,
15
- custom: customRunner.run
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.11",
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",