@levu/snap 0.1.0 → 0.1.1

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 (116) hide show
  1. package/README.md +7 -0
  2. package/dist/cli/cli-runner.d.ts +37 -0
  3. package/dist/cli/cli-runner.js +161 -0
  4. package/dist/cli-entry.js +6 -64
  5. package/dist/core/contracts/action-contract.d.ts +1 -3
  6. package/dist/core/contracts/tui-contract.d.ts +47 -0
  7. package/dist/core/contracts/tui-contract.js +1 -0
  8. package/dist/core/registry/action-registry.js +3 -1
  9. package/dist/dx/args/env.d.ts +6 -0
  10. package/dist/dx/args/env.js +15 -0
  11. package/dist/dx/args/index.d.ts +4 -0
  12. package/dist/dx/args/index.js +3 -0
  13. package/dist/dx/args/readers.d.ts +5 -0
  14. package/dist/dx/args/readers.js +36 -0
  15. package/dist/dx/args/types.d.ts +5 -0
  16. package/dist/dx/args/types.js +1 -0
  17. package/dist/dx/help/builder.d.ts +10 -0
  18. package/dist/dx/help/builder.js +11 -0
  19. package/dist/dx/help/index.d.ts +4 -0
  20. package/dist/dx/help/index.js +2 -0
  21. package/dist/dx/help/schema.d.ts +14 -0
  22. package/dist/dx/help/schema.js +33 -0
  23. package/dist/dx/runtime/action-result.d.ts +12 -0
  24. package/dist/dx/runtime/action-result.js +35 -0
  25. package/dist/dx/runtime/flow.d.ts +9 -0
  26. package/dist/dx/runtime/flow.js +19 -0
  27. package/dist/dx/runtime/index.d.ts +4 -0
  28. package/dist/dx/runtime/index.js +2 -0
  29. package/dist/dx/terminal/index.d.ts +2 -0
  30. package/dist/dx/terminal/index.js +1 -0
  31. package/dist/dx/terminal/output.d.ts +7 -0
  32. package/dist/dx/terminal/output.js +18 -0
  33. package/dist/dx/tui/components.d.ts +6 -0
  34. package/dist/dx/tui/components.js +40 -0
  35. package/dist/dx/tui/flow.d.ts +2 -0
  36. package/dist/dx/tui/flow.js +14 -0
  37. package/dist/dx/tui/index.d.ts +4 -0
  38. package/dist/dx/tui/index.js +3 -0
  39. package/dist/dx/tui/no-result.d.ts +13 -0
  40. package/dist/dx/tui/no-result.js +18 -0
  41. package/dist/help/help-renderer.js +5 -1
  42. package/dist/help/hierarchy-resolver.js +1 -1
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.js +9 -0
  45. package/dist/runtime/dispatch.d.ts +2 -1
  46. package/dist/runtime/engine.d.ts +2 -1
  47. package/dist/runtime/engine.js +22 -1
  48. package/dist/runtime/mode-resolver.d.ts +3 -2
  49. package/dist/runtime/runtime-context.d.ts +8 -1
  50. package/dist/tui/component-adapters/cancel.d.ts +6 -0
  51. package/dist/tui/component-adapters/cancel.js +20 -0
  52. package/dist/tui/component-adapters/confirm.d.ts +2 -0
  53. package/dist/tui/component-adapters/confirm.js +13 -1
  54. package/dist/tui/component-adapters/multiselect.d.ts +4 -0
  55. package/dist/tui/component-adapters/multiselect.js +23 -3
  56. package/dist/tui/component-adapters/readline-utils.d.ts +1 -0
  57. package/dist/tui/component-adapters/readline-utils.js +2 -0
  58. package/dist/tui/component-adapters/select.d.ts +2 -0
  59. package/dist/tui/component-adapters/select.js +25 -3
  60. package/dist/tui/component-adapters/text.d.ts +2 -0
  61. package/dist/tui/component-adapters/text.js +21 -4
  62. package/dist/tui/custom/custom-prompt.d.ts +16 -0
  63. package/dist/tui/custom/custom-prompt.js +72 -0
  64. package/dist/tui/custom/index.d.ts +2 -0
  65. package/dist/tui/custom/index.js +1 -0
  66. package/dist/tui/prompt-toolkit.d.ts +15 -0
  67. package/dist/tui/prompt-toolkit.js +17 -0
  68. package/package.json +13 -2
  69. package/.github/workflows/ci.yml +0 -26
  70. package/docs/help-contract-spec.md +0 -29
  71. package/docs/module-authoring-guide.md +0 -52
  72. package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +0 -71
  73. package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +0 -76
  74. package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +0 -71
  75. package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +0 -69
  76. package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +0 -79
  77. package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +0 -75
  78. package/plans/260209-1547-hub-dual-runtime-framework/plan.md +0 -105
  79. package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +0 -27
  80. package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +0 -166
  81. package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +0 -87
  82. package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +0 -24
  83. package/src/cli/help-command.ts +0 -1
  84. package/src/cli-entry.ts +0 -83
  85. package/src/core/contracts/action-contract.ts +0 -30
  86. package/src/core/contracts/help-contract.ts +0 -20
  87. package/src/core/contracts/module-contract.ts +0 -7
  88. package/src/core/errors/framework-errors.ts +0 -26
  89. package/src/core/registry/action-registry.ts +0 -94
  90. package/src/help/help-command.ts +0 -32
  91. package/src/help/help-model.ts +0 -10
  92. package/src/help/help-renderer.ts +0 -21
  93. package/src/help/hierarchy-resolver.ts +0 -54
  94. package/src/index.ts +0 -10
  95. package/src/modules/sample-content/module.ts +0 -66
  96. package/src/modules/sample-system/module.ts +0 -74
  97. package/src/runtime/dispatch.ts +0 -64
  98. package/src/runtime/engine.ts +0 -59
  99. package/src/runtime/mode-resolver.ts +0 -18
  100. package/src/runtime/resume-store.ts +0 -53
  101. package/src/runtime/runtime-context.ts +0 -10
  102. package/src/runtime/state-machine.ts +0 -77
  103. package/src/tui/accessibility-footer.ts +0 -11
  104. package/src/tui/component-adapters/confirm.ts +0 -8
  105. package/src/tui/component-adapters/group.ts +0 -12
  106. package/src/tui/component-adapters/multiselect.ts +0 -22
  107. package/src/tui/component-adapters/select.ts +0 -18
  108. package/src/tui/component-adapters/text.ts +0 -13
  109. package/src/tui/interrupt-handlers.ts +0 -15
  110. package/tests/e2e/cli-smoke.e2e.test.ts +0 -19
  111. package/tests/integration/runtime-dispatch.integration.test.ts +0 -23
  112. package/tests/transcript/help.transcript.test.ts +0 -20
  113. package/tests/unit/state-machine.test.ts +0 -22
  114. package/tsconfig.build.json +0 -9
  115. package/tsconfig.json +0 -17
  116. package/vitest.config.ts +0 -8
@@ -0,0 +1,18 @@
1
+ const writeLine = (stream, message) => {
2
+ stream.write(`${message}\n`);
3
+ };
4
+ export const createTerminalOutput = (stdout = process.stdout, stderr = process.stderr) => {
5
+ return {
6
+ line(message) {
7
+ writeLine(stdout, message);
8
+ },
9
+ lines(messages) {
10
+ for (const message of messages) {
11
+ writeLine(stdout, message);
12
+ }
13
+ },
14
+ error(message) {
15
+ writeLine(stderr, message);
16
+ }
17
+ };
18
+ };
@@ -0,0 +1,6 @@
1
+ import type { TuiComponentContract, TuiCustomComponentContract, TuiOptionContract, TuiStepContract } from '../../core/contracts/tui-contract.js';
2
+ export declare const defineTuiOptions: <TOption extends TuiOptionContract>(options: readonly TOption[]) => TOption[];
3
+ export declare const defineTuiComponent: <TComponent extends TuiComponentContract>(component: TComponent) => TComponent;
4
+ export declare const defineCustomTuiComponent: <TConfig extends Record<string, unknown> = Record<string, unknown>>(component: Omit<TuiCustomComponentContract<TConfig>, "type">) => TuiCustomComponentContract<TConfig>;
5
+ export declare const defineTuiStep: <TStep extends TuiStepContract>(step: TStep) => TStep;
6
+ export declare const isCustomTuiComponent: (component: TuiComponentContract) => component is TuiCustomComponentContract;
@@ -0,0 +1,40 @@
1
+ export const defineTuiOptions = (options) => {
2
+ if (options.length === 0) {
3
+ throw new Error('TUI select options cannot be empty.');
4
+ }
5
+ return [...options];
6
+ };
7
+ export const defineTuiComponent = (component) => {
8
+ if (!component.componentId.trim()) {
9
+ throw new Error('TUI componentId cannot be empty.');
10
+ }
11
+ if (!component.label.trim()) {
12
+ throw new Error(`TUI component label cannot be empty: ${component.componentId}`);
13
+ }
14
+ if (component.type === 'select' || component.type === 'multiselect') {
15
+ if (!component.options || component.options.length === 0) {
16
+ throw new Error(`TUI component options required for ${component.componentId}.`);
17
+ }
18
+ }
19
+ if (component.type === 'custom' && !component.renderer.trim()) {
20
+ throw new Error(`Custom TUI component renderer is required: ${component.componentId}.`);
21
+ }
22
+ return component;
23
+ };
24
+ export const defineCustomTuiComponent = (component) => defineTuiComponent({
25
+ ...component,
26
+ type: 'custom'
27
+ });
28
+ export const defineTuiStep = (step) => {
29
+ if (!step.stepId.trim()) {
30
+ throw new Error('TUI stepId cannot be empty.');
31
+ }
32
+ if (!step.title.trim()) {
33
+ throw new Error(`TUI step title cannot be empty: ${step.stepId}`);
34
+ }
35
+ if (step.components) {
36
+ step.components.forEach((component) => defineTuiComponent(component));
37
+ }
38
+ return step;
39
+ };
40
+ export const isCustomTuiComponent = (component) => component.type === 'custom';
@@ -0,0 +1,2 @@
1
+ import type { TuiFlowContract } from '../../core/contracts/tui-contract.js';
2
+ export declare const defineTuiFlow: <TFlow extends TuiFlowContract>(flow: TFlow) => TFlow;
@@ -0,0 +1,14 @@
1
+ import { defineTuiStep } from './components.js';
2
+ export const defineTuiFlow = (flow) => {
3
+ if (!flow.steps || flow.steps.length === 0) {
4
+ throw new Error('TUI flow must include at least one step.');
5
+ }
6
+ const steps = flow.steps.map((step) => defineTuiStep(step));
7
+ if (flow.entryStepId && !steps.some((step) => step.stepId === flow.entryStepId)) {
8
+ throw new Error(`TUI flow entry step does not exist: ${flow.entryStepId}`);
9
+ }
10
+ return {
11
+ ...flow,
12
+ steps
13
+ };
14
+ };
@@ -0,0 +1,4 @@
1
+ export { defineTuiOptions, defineTuiComponent, defineCustomTuiComponent, defineTuiStep, isCustomTuiComponent } from './components.js';
2
+ export { defineTuiFlow } from './flow.js';
3
+ export { backToPreviousOnNoResult, formatNoResultMessage } from './no-result.js';
4
+ export type { NoResultBackContext, NoResultBackInput } from './no-result.js';
@@ -0,0 +1,3 @@
1
+ export { defineTuiOptions, defineTuiComponent, defineCustomTuiComponent, defineTuiStep, isCustomTuiComponent } from './components.js';
2
+ export { defineTuiFlow } from './flow.js';
3
+ export { backToPreviousOnNoResult, formatNoResultMessage } from './no-result.js';
@@ -0,0 +1,13 @@
1
+ import type { FlowController } from '../runtime/index.js';
2
+ import type { TerminalOutput } from '../terminal/index.js';
3
+ export interface NoResultBackContext {
4
+ flow: Pick<FlowController, 'back'>;
5
+ terminal: Pick<TerminalOutput, 'line'>;
6
+ }
7
+ export interface NoResultBackInput {
8
+ context: NoResultBackContext;
9
+ entityName?: string;
10
+ message?: string;
11
+ }
12
+ export declare const formatNoResultMessage: (entityName?: string) => string;
13
+ export declare const backToPreviousOnNoResult: (input: NoResultBackInput) => void;
@@ -0,0 +1,18 @@
1
+ const DEFAULT_NO_RESULT_MESSAGE = 'No results found.';
2
+ const normalizeEntityName = (entityName) => {
3
+ if (entityName === undefined)
4
+ return undefined;
5
+ const normalized = entityName.trim();
6
+ return normalized.length > 0 ? normalized : undefined;
7
+ };
8
+ export const formatNoResultMessage = (entityName) => {
9
+ const normalizedEntityName = normalizeEntityName(entityName);
10
+ if (!normalizedEntityName)
11
+ return DEFAULT_NO_RESULT_MESSAGE;
12
+ return `No ${normalizedEntityName} found.`;
13
+ };
14
+ export const backToPreviousOnNoResult = (input) => {
15
+ const message = input.message?.trim().length ? input.message.trim() : formatNoResultMessage(input.entityName);
16
+ input.context.terminal.line(message);
17
+ input.context.flow.back();
18
+ };
@@ -1,6 +1,10 @@
1
+ const renderLine = (line) => line
2
+ .split('\n')
3
+ .map((part, index) => (index === 0 ? `- ${part}` : ` ${part}`))
4
+ .join('\n');
1
5
  const renderSection = (section) => {
2
6
  const header = `## ${section.title}`;
3
- const lines = section.lines.length > 0 ? section.lines.map((line) => `- ${line}`).join('\n') : '- (none)';
7
+ const lines = section.lines.length > 0 ? section.lines.map(renderLine).join('\n') : '- (none)';
4
8
  return `${header}\n${lines}`;
5
9
  };
6
10
  export const renderHelp = (views) => views
@@ -31,7 +31,7 @@ const toActionHelp = (moduleId, action) => ({
31
31
  { title: 'SUMMARY', lines: [action.help.summary] },
32
32
  {
33
33
  title: 'ARGS',
34
- lines: action.help.args.map((arg) => `${arg.required ? '*' : '-'} ${arg.name}: ${arg.description}`)
34
+ lines: action.help.args.map((arg) => `${arg.name} (${arg.required ? 'required' : 'optional'}): ${arg.description}`)
35
35
  },
36
36
  { title: 'EXAMPLES', lines: action.help.examples },
37
37
  {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,15 @@
1
1
  import { ActionRegistry } from './core/registry/action-registry.js';
2
2
  import type { ModuleContract } from './core/contracts/module-contract.js';
3
+ export { parseCliInput, runMultiModuleCli, runSingleModuleCli } from './cli/cli-runner.js';
4
+ export type { SubmoduleRoute } from './cli/cli-runner.js';
5
+ export { runSubmoduleCli } from './cli/cli-runner.js';
6
+ export * as SnapArgs from './dx/args/index.js';
7
+ export * as SnapHelp from './dx/help/index.js';
8
+ export * as SnapRuntime from './dx/runtime/index.js';
9
+ export * as SnapTerminal from './dx/terminal/index.js';
10
+ export * as SnapTui from './dx/tui/index.js';
11
+ export { createPromptToolkit } from './tui/prompt-toolkit.js';
12
+ export type { PromptToolkit } from './tui/prompt-toolkit.js';
13
+ export { runCustomPrompt, createCustomPromptRunner } from './tui/custom/index.js';
14
+ export type { CustomPromptInput, CustomPromptRunner } from './tui/custom/index.js';
3
15
  export declare const createRegistry: (modules: ModuleContract[]) => ActionRegistry;
package/dist/index.js CHANGED
@@ -1,4 +1,13 @@
1
1
  import { ActionRegistry } from './core/registry/action-registry.js';
2
+ export { parseCliInput, runMultiModuleCli, runSingleModuleCli } from './cli/cli-runner.js';
3
+ export { runSubmoduleCli } from './cli/cli-runner.js';
4
+ export * as SnapArgs from './dx/args/index.js';
5
+ export * as SnapHelp from './dx/help/index.js';
6
+ export * as SnapRuntime from './dx/runtime/index.js';
7
+ export * as SnapTerminal from './dx/terminal/index.js';
8
+ export * as SnapTui from './dx/tui/index.js';
9
+ export { createPromptToolkit } from './tui/prompt-toolkit.js';
10
+ export { runCustomPrompt, createCustomPromptRunner } from './tui/custom/index.js';
2
11
  export const createRegistry = (modules) => {
3
12
  const registry = new ActionRegistry();
4
13
  for (const moduleContract of modules) {
@@ -1,10 +1,11 @@
1
1
  import type { ActionResultEnvelope } from '../core/contracts/action-contract.js';
2
2
  import type { ActionRegistry } from '../core/registry/action-registry.js';
3
+ import type { CliArgs } from '../dx/args/index.js';
3
4
  export interface DispatchInput {
4
5
  registry: ActionRegistry;
5
6
  moduleId: string;
6
7
  actionId: string;
7
- args: Record<string, string | boolean>;
8
+ args: CliArgs;
8
9
  isTTY: boolean;
9
10
  resumeFilePath?: string;
10
11
  }
@@ -1,11 +1,12 @@
1
1
  import type { ActionContract, ActionResultEnvelope, RuntimeMode } from '../core/contracts/action-contract.js';
2
+ import type { CliArgs } from '../dx/args/index.js';
2
3
  import { ResumeStore } from './resume-store.js';
3
4
  import { type WorkflowNode } from './state-machine.js';
4
5
  export interface EngineInput {
5
6
  moduleId: string;
6
7
  action: ActionContract;
7
8
  mode: RuntimeMode;
8
- args: Record<string, string | boolean>;
9
+ args: CliArgs;
9
10
  workflowId?: string;
10
11
  workflowNodes?: WorkflowNode[];
11
12
  resumeStore?: ResumeStore;
@@ -1,7 +1,22 @@
1
1
  import { ExitCode } from '../core/errors/framework-errors.js';
2
+ import { createFlowController } from '../dx/runtime/index.js';
3
+ import { createTerminalOutput } from '../dx/terminal/index.js';
4
+ import { createPromptToolkit } from '../tui/prompt-toolkit.js';
2
5
  import { StateMachine } from './state-machine.js';
6
+ const resolveWorkflowNodes = (action) => {
7
+ if (action.tui.flow?.steps && action.tui.flow.steps.length > 0) {
8
+ return action.tui.flow.steps.map((step) => ({
9
+ id: step.stepId,
10
+ label: step.title
11
+ }));
12
+ }
13
+ if (action.tui.steps && action.tui.steps.length > 0) {
14
+ return action.tui.steps.map((step) => ({ id: step, label: step }));
15
+ }
16
+ return [{ id: 'default', label: 'default' }];
17
+ };
3
18
  export const executeAction = async (input) => {
4
- const nodes = input.workflowNodes ?? input.action.tui.steps.map((step) => ({ id: step, label: step }));
19
+ const nodes = input.workflowNodes ?? resolveWorkflowNodes(input.action);
5
20
  const workflowId = input.workflowId ?? `${input.moduleId}.${input.action.actionId}`;
6
21
  let initialNodeId;
7
22
  if (input.resumeStore) {
@@ -10,12 +25,18 @@ export const executeAction = async (input) => {
10
25
  initialNodeId = checkpoint.nodeId;
11
26
  }
12
27
  }
28
+ if (!initialNodeId && input.action.tui.flow?.entryStepId) {
29
+ initialNodeId = input.action.tui.flow.entryStepId;
30
+ }
13
31
  const stateMachine = new StateMachine(workflowId, nodes, initialNodeId);
14
32
  const context = {
15
33
  moduleId: input.moduleId,
16
34
  actionId: input.action.actionId,
17
35
  mode: input.mode,
18
36
  args: input.args,
37
+ flow: createFlowController(stateMachine),
38
+ terminal: createTerminalOutput(),
39
+ prompts: createPromptToolkit(),
19
40
  stateMachine
20
41
  };
21
42
  try {
@@ -1,8 +1,9 @@
1
1
  import type { CommandlineContract, RuntimeMode } from '../core/contracts/action-contract.js';
2
+ import type { CliArgs } from '../dx/args/index.js';
2
3
  export interface RuntimeResolutionInput {
3
4
  isTTY: boolean;
4
- providedArgs: Record<string, string | boolean>;
5
+ providedArgs: CliArgs;
5
6
  commandline: CommandlineContract;
6
7
  }
7
- export declare const hasRequiredArgs: (requiredArgs: string[], providedArgs: Record<string, string | boolean>) => boolean;
8
+ export declare const hasRequiredArgs: (requiredArgs: string[], providedArgs: CliArgs) => boolean;
8
9
  export declare const resolveRuntimeMode: (input: RuntimeResolutionInput) => RuntimeMode;
@@ -1,9 +1,16 @@
1
1
  import type { RuntimeMode } from '../core/contracts/action-contract.js';
2
+ import type { CliArgs } from '../dx/args/index.js';
3
+ import type { FlowController } from '../dx/runtime/index.js';
4
+ import type { TerminalOutput } from '../dx/terminal/index.js';
2
5
  import type { StateMachine } from './state-machine.js';
6
+ import type { PromptToolkit } from '../tui/prompt-toolkit.js';
3
7
  export interface RuntimeContext {
4
8
  moduleId: string;
5
9
  actionId: string;
6
10
  mode: RuntimeMode;
7
- args: Record<string, string | boolean>;
11
+ args: CliArgs;
12
+ flow: FlowController;
13
+ terminal: TerminalOutput;
14
+ prompts: PromptToolkit;
8
15
  stateMachine?: StateMachine;
9
16
  }
@@ -0,0 +1,6 @@
1
+ export declare class PromptCancelledError extends Error {
2
+ readonly isPromptCancelled = true;
3
+ constructor(message?: string);
4
+ }
5
+ export declare const isPromptCancelledError: (error: unknown) => error is PromptCancelledError;
6
+ export declare const unwrapClackResult: <T>(value: T | symbol, cancelMessage?: string) => T;
@@ -0,0 +1,20 @@
1
+ import { isCancel } from '@clack/prompts';
2
+ export class PromptCancelledError extends Error {
3
+ isPromptCancelled = true;
4
+ constructor(message = 'Cancelled by user.') {
5
+ super(message);
6
+ this.name = 'PromptCancelledError';
7
+ }
8
+ }
9
+ export const isPromptCancelledError = (error) => {
10
+ return error instanceof PromptCancelledError || (typeof error === 'object' &&
11
+ error !== null &&
12
+ 'isPromptCancelled' in error &&
13
+ error.isPromptCancelled === true);
14
+ };
15
+ export const unwrapClackResult = (value, cancelMessage = 'Cancelled by user.') => {
16
+ if (isCancel(value)) {
17
+ throw new PromptCancelledError(cancelMessage);
18
+ }
19
+ return value;
20
+ };
@@ -1,5 +1,7 @@
1
1
  export interface ConfirmPromptInput {
2
2
  message: string;
3
3
  initialValue?: boolean;
4
+ active?: string;
5
+ inactive?: string;
4
6
  }
5
7
  export declare const runConfirmPrompt: (input: ConfirmPromptInput) => Promise<boolean>;
@@ -1,3 +1,15 @@
1
+ import { confirm } from '@clack/prompts';
2
+ import { isInteractiveTerminal } from './readline-utils.js';
3
+ import { unwrapClackResult } from './cancel.js';
1
4
  export const runConfirmPrompt = async (input) => {
2
- return input.initialValue ?? true;
5
+ const fallback = input.initialValue ?? true;
6
+ if (!isInteractiveTerminal())
7
+ return fallback;
8
+ const value = await confirm({
9
+ message: input.message,
10
+ initialValue: fallback,
11
+ active: input.active,
12
+ inactive: input.inactive
13
+ });
14
+ return unwrapClackResult(value);
3
15
  };
@@ -1,10 +1,14 @@
1
1
  export interface MultiSelectOption {
2
2
  value: string;
3
3
  label: string;
4
+ hint?: string;
5
+ disabled?: boolean;
4
6
  }
5
7
  export interface MultiSelectPromptInput {
6
8
  message: string;
7
9
  options: MultiSelectOption[];
8
10
  initialValues?: string[];
11
+ required?: boolean;
12
+ showInstructions?: boolean;
9
13
  }
10
14
  export declare const runMultiSelectPrompt: (input: MultiSelectPromptInput) => Promise<string[]>;
@@ -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
- if (input.initialValues && input.initialValues.length > 0) {
6
- return input.initialValues.filter((value) => input.options.some((option) => option.value === value));
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
- return [input.options[0].value];
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 @@
1
+ export declare const isInteractiveTerminal: () => boolean;
@@ -0,0 +1,2 @@
1
+ import { stdin as input, stdout as output } from 'node:process';
2
+ export const isInteractiveTerminal = () => Boolean(input.isTTY && output.isTTY);
@@ -1,6 +1,8 @@
1
1
  export interface SelectOption {
2
2
  value: string;
3
3
  label: string;
4
+ hint?: string;
5
+ disabled?: boolean;
4
6
  }
5
7
  export interface SelectPromptInput {
6
8
  message: string;
@@ -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
- const selected = input.initialValue ?? input.options[0]?.value;
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
- return selected;
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
  };
@@ -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 value = input.initialValue ?? '';
3
- if (input.required && value.trim().length === 0) {
4
- throw new Error(`Required text value missing: ${input.message}`);
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
- return value;
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,2 @@
1
+ export type { CustomPromptInput, CustomPromptRunner } from './custom-prompt.js';
2
+ export { runCustomPrompt, createCustomPromptRunner } from './custom-prompt.js';
@@ -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
+ };