@levu/snap 0.1.0 → 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 +33 -2
- package/dist/cli/cli-runner.d.ts +37 -0
- package/dist/cli/cli-runner.js +161 -0
- package/dist/cli-entry.js +6 -64
- package/dist/core/contracts/action-contract.d.ts +1 -3
- package/dist/core/contracts/tui-contract.d.ts +47 -0
- package/dist/core/contracts/tui-contract.js +1 -0
- package/dist/core/registry/action-registry.js +3 -1
- package/dist/dx/args/env.d.ts +6 -0
- package/dist/dx/args/env.js +15 -0
- package/dist/dx/args/index.d.ts +4 -0
- package/dist/dx/args/index.js +3 -0
- package/dist/dx/args/readers.d.ts +5 -0
- package/dist/dx/args/readers.js +36 -0
- package/dist/dx/args/types.d.ts +5 -0
- package/dist/dx/args/types.js +1 -0
- package/dist/dx/help/builder.d.ts +10 -0
- package/dist/dx/help/builder.js +11 -0
- package/dist/dx/help/index.d.ts +4 -0
- package/dist/dx/help/index.js +2 -0
- package/dist/dx/help/schema.d.ts +14 -0
- package/dist/dx/help/schema.js +33 -0
- package/dist/dx/runtime/action-result.d.ts +12 -0
- package/dist/dx/runtime/action-result.js +35 -0
- package/dist/dx/runtime/flow.d.ts +9 -0
- package/dist/dx/runtime/flow.js +19 -0
- package/dist/dx/runtime/index.d.ts +4 -0
- package/dist/dx/runtime/index.js +2 -0
- package/dist/dx/terminal/index.d.ts +3 -0
- package/dist/dx/terminal/index.js +3 -0
- 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 +19 -0
- package/dist/dx/terminal/output.js +59 -0
- package/dist/dx/tui/components.d.ts +6 -0
- package/dist/dx/tui/components.js +40 -0
- package/dist/dx/tui/flow.d.ts +2 -0
- package/dist/dx/tui/flow.js +14 -0
- package/dist/dx/tui/index.d.ts +16 -0
- package/dist/dx/tui/index.js +15 -0
- package/dist/dx/tui/no-result.d.ts +13 -0
- package/dist/dx/tui/no-result.js +18 -0
- package/dist/help/help-renderer.js +5 -1
- package/dist/help/hierarchy-resolver.js +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +11 -0
- package/dist/runtime/dispatch.d.ts +2 -1
- package/dist/runtime/engine.d.ts +2 -1
- package/dist/runtime/engine.js +22 -1
- package/dist/runtime/mode-resolver.d.ts +3 -2
- package/dist/runtime/runtime-context.d.ts +8 -1
- package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
- package/dist/tui/component-adapters/autocomplete.js +34 -0
- package/dist/tui/component-adapters/cancel.d.ts +6 -0
- package/dist/tui/component-adapters/cancel.js +20 -0
- package/dist/tui/component-adapters/confirm.d.ts +2 -0
- package/dist/tui/component-adapters/confirm.js +13 -1
- package/dist/tui/component-adapters/multiselect.d.ts +4 -0
- package/dist/tui/component-adapters/multiselect.js +23 -3
- 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/readline-utils.d.ts +1 -0
- package/dist/tui/component-adapters/readline-utils.js +2 -0
- package/dist/tui/component-adapters/select.d.ts +2 -0
- package/dist/tui/component-adapters/select.js +25 -3
- 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/dist/tui/component-adapters/text.d.ts +2 -0
- package/dist/tui/component-adapters/text.js +21 -4
- package/dist/tui/custom/custom-prompt.d.ts +16 -0
- package/dist/tui/custom/custom-prompt.js +72 -0
- package/dist/tui/custom/index.d.ts +2 -0
- package/dist/tui/custom/index.js +1 -0
- package/dist/tui/prompt-toolkit.d.ts +15 -0
- package/dist/tui/prompt-toolkit.js +17 -0
- package/docs/component-reference.md +474 -0
- package/docs/getting-started.md +242 -0
- package/docs/integration-examples.md +677 -0
- package/docs/module-authoring-guide.md +105 -1
- 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 +15 -2
- package/.github/workflows/ci.yml +0 -26
- package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +0 -71
- package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +0 -76
- package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +0 -71
- package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +0 -69
- package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +0 -79
- package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +0 -75
- package/plans/260209-1547-hub-dual-runtime-framework/plan.md +0 -105
- package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +0 -27
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +0 -166
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +0 -87
- package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +0 -24
- package/src/cli/help-command.ts +0 -1
- package/src/cli-entry.ts +0 -83
- package/src/core/contracts/action-contract.ts +0 -30
- package/src/core/contracts/help-contract.ts +0 -20
- package/src/core/contracts/module-contract.ts +0 -7
- package/src/core/errors/framework-errors.ts +0 -26
- package/src/core/registry/action-registry.ts +0 -94
- package/src/help/help-command.ts +0 -32
- package/src/help/help-model.ts +0 -10
- package/src/help/help-renderer.ts +0 -21
- package/src/help/hierarchy-resolver.ts +0 -54
- package/src/index.ts +0 -10
- package/src/modules/sample-content/module.ts +0 -66
- package/src/modules/sample-system/module.ts +0 -74
- package/src/runtime/dispatch.ts +0 -64
- package/src/runtime/engine.ts +0 -59
- package/src/runtime/mode-resolver.ts +0 -18
- package/src/runtime/resume-store.ts +0 -53
- package/src/runtime/runtime-context.ts +0 -10
- package/src/runtime/state-machine.ts +0 -77
- package/src/tui/accessibility-footer.ts +0 -11
- package/src/tui/component-adapters/confirm.ts +0 -8
- package/src/tui/component-adapters/group.ts +0 -12
- package/src/tui/component-adapters/multiselect.ts +0 -22
- package/src/tui/component-adapters/select.ts +0 -18
- package/src/tui/component-adapters/text.ts +0 -13
- package/src/tui/interrupt-handlers.ts +0 -15
- package/tests/e2e/cli-smoke.e2e.test.ts +0 -19
- package/tests/integration/runtime-dispatch.integration.test.ts +0 -23
- package/tests/transcript/help.transcript.test.ts +0 -20
- package/tests/unit/state-machine.test.ts +0 -22
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -8
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
import { multiselect } from '@clack/prompts';
|
|
2
|
+
import { isInteractiveTerminal } from './readline-utils.js';
|
|
3
|
+
import { unwrapClackResult } from './cancel.js';
|
|
1
4
|
export const runMultiSelectPrompt = async (input) => {
|
|
2
5
|
if (input.options.length === 0) {
|
|
3
6
|
throw new Error(`No options available for multiselect prompt: ${input.message}`);
|
|
4
7
|
}
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
const defaultValues = input.initialValues && input.initialValues.length > 0
|
|
9
|
+
? input.initialValues.filter((value) => input.options.some((option) => option.value === value))
|
|
10
|
+
: [input.options[0].value];
|
|
11
|
+
if (!isInteractiveTerminal()) {
|
|
12
|
+
return defaultValues;
|
|
7
13
|
}
|
|
8
|
-
|
|
14
|
+
const message = input.showInstructions === false
|
|
15
|
+
? input.message
|
|
16
|
+
: `${input.message} (space to toggle, a to select all)`;
|
|
17
|
+
const selected = await multiselect({
|
|
18
|
+
message,
|
|
19
|
+
options: input.options.map((option) => ({
|
|
20
|
+
value: option.value,
|
|
21
|
+
label: option.label,
|
|
22
|
+
hint: option.hint,
|
|
23
|
+
disabled: option.disabled
|
|
24
|
+
})),
|
|
25
|
+
initialValues: defaultValues,
|
|
26
|
+
required: input.required ?? false
|
|
27
|
+
});
|
|
28
|
+
return unwrapClackResult(selected);
|
|
9
29
|
};
|
|
@@ -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 @@
|
|
|
1
|
+
export declare const isInteractiveTerminal: () => boolean;
|
|
@@ -1,7 +1,29 @@
|
|
|
1
|
+
import { select } from '@clack/prompts';
|
|
2
|
+
import { isInteractiveTerminal } from './readline-utils.js';
|
|
3
|
+
import { unwrapClackResult } from './cancel.js';
|
|
1
4
|
export const runSelectPrompt = async (input) => {
|
|
2
|
-
|
|
3
|
-
if (!selected) {
|
|
5
|
+
if (input.options.length === 0) {
|
|
4
6
|
throw new Error(`No options available for select prompt: ${input.message}`);
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
const initialValue = input.initialValue && input.options.some((option) => option.value === input.initialValue)
|
|
9
|
+
? input.initialValue
|
|
10
|
+
: undefined;
|
|
11
|
+
const defaultValue = initialValue ?? input.options[0]?.value;
|
|
12
|
+
if (!defaultValue) {
|
|
13
|
+
throw new Error(`No options available for select prompt: ${input.message}`);
|
|
14
|
+
}
|
|
15
|
+
if (!isInteractiveTerminal()) {
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
const selection = await select({
|
|
19
|
+
message: input.message,
|
|
20
|
+
options: input.options.map((option) => ({
|
|
21
|
+
value: option.value,
|
|
22
|
+
label: option.label,
|
|
23
|
+
hint: option.hint,
|
|
24
|
+
disabled: option.disabled
|
|
25
|
+
})),
|
|
26
|
+
initialValue: defaultValue
|
|
27
|
+
});
|
|
28
|
+
return unwrapClackResult(selection);
|
|
7
29
|
};
|
|
@@ -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
|
+
};
|
|
@@ -2,5 +2,7 @@ export interface TextPromptInput {
|
|
|
2
2
|
message: string;
|
|
3
3
|
initialValue?: string;
|
|
4
4
|
required?: boolean;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
validate?: (value: string) => string | Error | undefined;
|
|
5
7
|
}
|
|
6
8
|
export declare const runTextPrompt: (input: TextPromptInput) => Promise<string>;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { text } from '@clack/prompts';
|
|
2
|
+
import { isInteractiveTerminal } from './readline-utils.js';
|
|
3
|
+
import { unwrapClackResult } from './cancel.js';
|
|
1
4
|
export const runTextPrompt = async (input) => {
|
|
2
|
-
const
|
|
3
|
-
if (
|
|
4
|
-
|
|
5
|
+
const fallbackValue = input.initialValue ?? '';
|
|
6
|
+
if (!isInteractiveTerminal()) {
|
|
7
|
+
if (input.required && fallbackValue.trim().length === 0) {
|
|
8
|
+
throw new Error(`Required text value missing: ${input.message}`);
|
|
9
|
+
}
|
|
10
|
+
return fallbackValue;
|
|
5
11
|
}
|
|
6
|
-
|
|
12
|
+
const value = await text({
|
|
13
|
+
message: input.message,
|
|
14
|
+
initialValue: input.initialValue,
|
|
15
|
+
placeholder: input.placeholder,
|
|
16
|
+
validate: (raw) => {
|
|
17
|
+
if (input.required && (!raw || raw.trim().length === 0)) {
|
|
18
|
+
return `Required text value missing: ${input.message}`;
|
|
19
|
+
}
|
|
20
|
+
return input.validate?.(raw ?? '');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return unwrapClackResult(value);
|
|
7
24
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface CustomPromptInput<TValue = string> {
|
|
2
|
+
message: string;
|
|
3
|
+
defaultValue?: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
parse?: (raw: string) => TValue;
|
|
7
|
+
validate?: (value: TValue) => string | undefined;
|
|
8
|
+
onValue?: (value: TValue) => void;
|
|
9
|
+
onSubmit?: (value: TValue) => void;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface CustomPromptRunner {
|
|
13
|
+
run<TValue = string>(input: CustomPromptInput<TValue>): Promise<TValue>;
|
|
14
|
+
}
|
|
15
|
+
export declare const runCustomPrompt: <TValue = string>(input: CustomPromptInput<TValue>) => Promise<TValue>;
|
|
16
|
+
export declare const createCustomPromptRunner: () => CustomPromptRunner;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { text } from '@clack/prompts';
|
|
2
|
+
import { isInteractiveTerminal } from '../component-adapters/readline-utils.js';
|
|
3
|
+
import { unwrapClackResult } from '../component-adapters/cancel.js';
|
|
4
|
+
const toAbortError = () => {
|
|
5
|
+
const error = new Error('Prompt aborted.');
|
|
6
|
+
error.name = 'AbortError';
|
|
7
|
+
return error;
|
|
8
|
+
};
|
|
9
|
+
const assertNotAborted = (signal) => {
|
|
10
|
+
if (signal?.aborted) {
|
|
11
|
+
throw toAbortError();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const defaultParse = (raw) => raw;
|
|
15
|
+
export const runCustomPrompt = async (input) => {
|
|
16
|
+
const parse = input.parse ?? (defaultParse);
|
|
17
|
+
const fallback = input.defaultValue ?? '';
|
|
18
|
+
const validateValue = (value) => {
|
|
19
|
+
const message = input.validate?.(value);
|
|
20
|
+
if (message)
|
|
21
|
+
throw new Error(message);
|
|
22
|
+
};
|
|
23
|
+
const parseAndValidate = (raw) => {
|
|
24
|
+
const parsed = parse(raw);
|
|
25
|
+
validateValue(parsed);
|
|
26
|
+
input.onValue?.(parsed);
|
|
27
|
+
return parsed;
|
|
28
|
+
};
|
|
29
|
+
assertNotAborted(input.signal);
|
|
30
|
+
if (!isInteractiveTerminal()) {
|
|
31
|
+
if (input.required && fallback.trim().length === 0) {
|
|
32
|
+
throw new Error(`Required value missing: ${input.message}`);
|
|
33
|
+
}
|
|
34
|
+
const value = parseAndValidate(fallback);
|
|
35
|
+
input.onSubmit?.(value);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const onAbort = () => {
|
|
39
|
+
input.onCancel?.();
|
|
40
|
+
};
|
|
41
|
+
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
42
|
+
try {
|
|
43
|
+
const rawValue = await text({
|
|
44
|
+
message: input.message,
|
|
45
|
+
initialValue: input.defaultValue,
|
|
46
|
+
validate: (value) => {
|
|
47
|
+
if ((!value || value.length === 0) && input.required && fallback.trim().length === 0) {
|
|
48
|
+
return `Required value missing: ${input.message}`;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
parseAndValidate(value && value.length > 0 ? value : fallback);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return error instanceof Error ? error.message : 'Invalid value';
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
signal: input.signal
|
|
59
|
+
});
|
|
60
|
+
assertNotAborted(input.signal);
|
|
61
|
+
const resolved = unwrapClackResult(rawValue);
|
|
62
|
+
const value = parseAndValidate(resolved.length > 0 ? resolved : fallback);
|
|
63
|
+
input.onSubmit?.(value);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
input.signal?.removeEventListener('abort', onAbort);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
export const createCustomPromptRunner = () => ({
|
|
71
|
+
run: runCustomPrompt
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runCustomPrompt, createCustomPromptRunner } from './custom-prompt.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConfirmPromptInput } from './component-adapters/confirm.js';
|
|
2
|
+
import type { GroupStep } from './component-adapters/group.js';
|
|
3
|
+
import type { MultiSelectPromptInput } from './component-adapters/multiselect.js';
|
|
4
|
+
import type { SelectPromptInput } from './component-adapters/select.js';
|
|
5
|
+
import type { TextPromptInput } from './component-adapters/text.js';
|
|
6
|
+
import { type CustomPromptInput } from './custom/custom-prompt.js';
|
|
7
|
+
export interface PromptToolkit {
|
|
8
|
+
text(input: TextPromptInput): Promise<string>;
|
|
9
|
+
confirm(input: ConfirmPromptInput): Promise<boolean>;
|
|
10
|
+
select(input: SelectPromptInput): Promise<string>;
|
|
11
|
+
multiselect(input: MultiSelectPromptInput): Promise<string[]>;
|
|
12
|
+
group<T = unknown>(steps: GroupStep<T>[]): Promise<Record<string, T>>;
|
|
13
|
+
custom<T>(input: CustomPromptInput<T>): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
export declare const createPromptToolkit: () => PromptToolkit;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runConfirmPrompt } from './component-adapters/confirm.js';
|
|
2
|
+
import { runGroupPrompt } from './component-adapters/group.js';
|
|
3
|
+
import { runMultiSelectPrompt } from './component-adapters/multiselect.js';
|
|
4
|
+
import { runSelectPrompt } from './component-adapters/select.js';
|
|
5
|
+
import { runTextPrompt } from './component-adapters/text.js';
|
|
6
|
+
import { createCustomPromptRunner } from './custom/custom-prompt.js';
|
|
7
|
+
export const createPromptToolkit = () => {
|
|
8
|
+
const customRunner = createCustomPromptRunner();
|
|
9
|
+
return {
|
|
10
|
+
text: runTextPrompt,
|
|
11
|
+
confirm: runConfirmPrompt,
|
|
12
|
+
select: runSelectPrompt,
|
|
13
|
+
multiselect: runMultiSelectPrompt,
|
|
14
|
+
group: runGroupPrompt,
|
|
15
|
+
custom: customRunner.run
|
|
16
|
+
};
|
|
17
|
+
};
|