@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
package/README.md CHANGED
@@ -8,10 +8,17 @@ It runs one action contract in 2 modes:
8
8
 
9
9
  It also enforces deterministic, text-only help so both **humans** and **AI agents** can discover commands reliably.
10
10
 
11
+ For module/tool authors, Snap also exposes optional DX helper groups:
12
+ - `SnapArgs` (typed argv readers/parsers)
13
+ - `SnapHelp` (arg-schema driven help + commandline contracts)
14
+ - `SnapRuntime` (standardized action result helpers)
15
+ - `SnapTui` (typed flow/component definitions, including custom components)
16
+
11
17
  ## What this framework does
12
18
 
13
19
  - Enforces action triad at registration: `tui + commandline + help`
14
20
  - Uses one runtime engine for TUI and CLI paths
21
+ - Uses Clack-powered prompt adapters for interactive TUI (`select`, `text`, `confirm`, `multiselect`)
15
22
  - Supports workflow transitions: `next`, `back`, `jump`, `exit`
16
23
  - Supports resume checkpoints for interrupted flows
17
24
  - Produces stable help output hierarchy:
@@ -0,0 +1,37 @@
1
+ import type { ActionRegistry } from '../core/registry/action-registry.js';
2
+ import type { CliArgs } from '../dx/args/index.js';
3
+ export interface ParsedCliInput {
4
+ wantsHelp: boolean;
5
+ positional: string[];
6
+ args: CliArgs;
7
+ }
8
+ export interface RunMultiModuleCliInput {
9
+ registry: ActionRegistry;
10
+ argv: string[];
11
+ isTTY?: boolean;
12
+ }
13
+ export interface RunSingleModuleCliInput {
14
+ registry: ActionRegistry;
15
+ argv: string[];
16
+ moduleId: string;
17
+ defaultActionId?: string;
18
+ helpDefaultTarget?: 'module' | 'action';
19
+ isTTY?: boolean;
20
+ }
21
+ export interface SubmoduleRoute {
22
+ moduleId: string;
23
+ defaultActionId?: string;
24
+ helpDefaultTarget?: 'module' | 'action';
25
+ aliases?: string[];
26
+ }
27
+ export interface RunSubmoduleCliInput {
28
+ registry: ActionRegistry;
29
+ argv: string[];
30
+ submodules: SubmoduleRoute[];
31
+ defaultSubmoduleId?: string;
32
+ isTTY?: boolean;
33
+ }
34
+ export declare const parseCliInput: (argv: string[]) => ParsedCliInput;
35
+ export declare const runMultiModuleCli: (input: RunMultiModuleCliInput) => Promise<number>;
36
+ export declare const runSingleModuleCli: (input: RunSingleModuleCliInput) => Promise<number>;
37
+ export declare const runSubmoduleCli: (input: RunSubmoduleCliInput) => Promise<number>;
@@ -0,0 +1,161 @@
1
+ import { createTerminalOutput } from '../dx/terminal/index.js';
2
+ import { dispatchAction } from '../runtime/dispatch.js';
3
+ import { runHelpCommand } from './help-command.js';
4
+ const writeEnvelope = (envelope) => {
5
+ const terminal = createTerminalOutput();
6
+ if (envelope.data !== undefined)
7
+ terminal.line(String(envelope.data));
8
+ if (envelope.errorMessage)
9
+ terminal.error(envelope.errorMessage);
10
+ return envelope.exitCode;
11
+ };
12
+ const runSingleFromParsed = async (input) => {
13
+ const helpDefaultTarget = input.helpDefaultTarget ?? 'module';
14
+ const explicitActionId = input.parsed.positional[0] === input.moduleId
15
+ ? input.parsed.positional[1]
16
+ : input.parsed.positional[0];
17
+ if (input.parsed.wantsHelp) {
18
+ const helpActionId = explicitActionId ?? (helpDefaultTarget === 'action' ? input.defaultActionId : undefined);
19
+ return writeEnvelope(runHelpCommand({
20
+ registry: input.registry,
21
+ moduleId: input.moduleId,
22
+ actionId: helpActionId
23
+ }));
24
+ }
25
+ const actionId = explicitActionId ?? input.defaultActionId;
26
+ if (!actionId) {
27
+ return writeEnvelope(runHelpCommand({
28
+ registry: input.registry,
29
+ moduleId: input.moduleId
30
+ }));
31
+ }
32
+ const result = await dispatchAction({
33
+ registry: input.registry,
34
+ moduleId: input.moduleId,
35
+ actionId,
36
+ args: input.parsed.args,
37
+ isTTY: input.isTTY
38
+ });
39
+ return writeEnvelope(result);
40
+ };
41
+ const findSubmoduleRoute = (routes, token) => {
42
+ if (!token)
43
+ return undefined;
44
+ return routes.find((route) => route.moduleId === token || (route.aliases !== undefined && route.aliases.includes(token)));
45
+ };
46
+ export const parseCliInput = (argv) => {
47
+ const positional = [];
48
+ const args = {};
49
+ let wantsHelp = false;
50
+ for (let index = 0; index < argv.length; index += 1) {
51
+ const token = argv[index];
52
+ if (token === '-h' || token === '--help') {
53
+ wantsHelp = true;
54
+ continue;
55
+ }
56
+ if (token === '--') {
57
+ positional.push(...argv.slice(index + 1));
58
+ break;
59
+ }
60
+ if (token.startsWith('--')) {
61
+ const body = token.slice(2);
62
+ const separatorIndex = body.indexOf('=');
63
+ if (separatorIndex >= 0) {
64
+ const key = body.slice(0, separatorIndex);
65
+ const value = body.slice(separatorIndex + 1);
66
+ if (key.length > 0)
67
+ args[key] = value;
68
+ continue;
69
+ }
70
+ if (body.length === 0)
71
+ continue;
72
+ const next = argv[index + 1];
73
+ if (next !== undefined && !next.startsWith('-')) {
74
+ args[body] = next;
75
+ index += 1;
76
+ }
77
+ else {
78
+ args[body] = true;
79
+ }
80
+ continue;
81
+ }
82
+ positional.push(token);
83
+ }
84
+ return { wantsHelp, positional, args };
85
+ };
86
+ export const runMultiModuleCli = async (input) => {
87
+ const parsed = parseCliInput(input.argv);
88
+ const moduleId = parsed.positional[0];
89
+ const actionId = parsed.positional[1];
90
+ const isTTY = input.isTTY ?? Boolean(process.stdout.isTTY);
91
+ if (parsed.wantsHelp || !moduleId) {
92
+ return writeEnvelope(runHelpCommand({
93
+ registry: input.registry,
94
+ moduleId,
95
+ actionId
96
+ }));
97
+ }
98
+ if (!actionId) {
99
+ return writeEnvelope(runHelpCommand({
100
+ registry: input.registry,
101
+ moduleId
102
+ }));
103
+ }
104
+ const result = await dispatchAction({
105
+ registry: input.registry,
106
+ moduleId,
107
+ actionId,
108
+ args: parsed.args,
109
+ isTTY
110
+ });
111
+ return writeEnvelope(result);
112
+ };
113
+ export const runSingleModuleCli = async (input) => {
114
+ const parsed = parseCliInput(input.argv);
115
+ const isTTY = input.isTTY ?? Boolean(process.stdout.isTTY);
116
+ return runSingleFromParsed({
117
+ registry: input.registry,
118
+ parsed,
119
+ moduleId: input.moduleId,
120
+ defaultActionId: input.defaultActionId,
121
+ helpDefaultTarget: input.helpDefaultTarget,
122
+ isTTY
123
+ });
124
+ };
125
+ export const runSubmoduleCli = async (input) => {
126
+ const parsed = parseCliInput(input.argv);
127
+ const isTTY = input.isTTY ?? Boolean(process.stdout.isTTY);
128
+ const firstToken = parsed.positional[0];
129
+ const matchedRoute = findSubmoduleRoute(input.submodules, firstToken);
130
+ if (matchedRoute) {
131
+ return runSingleFromParsed({
132
+ registry: input.registry,
133
+ parsed: {
134
+ ...parsed,
135
+ positional: parsed.positional.slice(1)
136
+ },
137
+ moduleId: matchedRoute.moduleId,
138
+ defaultActionId: matchedRoute.defaultActionId,
139
+ helpDefaultTarget: matchedRoute.helpDefaultTarget,
140
+ isTTY
141
+ });
142
+ }
143
+ if (!parsed.wantsHelp && parsed.positional.length === 0 && input.defaultSubmoduleId) {
144
+ const defaultRoute = findSubmoduleRoute(input.submodules, input.defaultSubmoduleId);
145
+ if (defaultRoute) {
146
+ return runSingleFromParsed({
147
+ registry: input.registry,
148
+ parsed,
149
+ moduleId: defaultRoute.moduleId,
150
+ defaultActionId: defaultRoute.defaultActionId,
151
+ helpDefaultTarget: defaultRoute.helpDefaultTarget,
152
+ isTTY
153
+ });
154
+ }
155
+ }
156
+ return runMultiModuleCli({
157
+ registry: input.registry,
158
+ argv: input.argv,
159
+ isTTY
160
+ });
161
+ };
package/dist/cli-entry.js CHANGED
@@ -2,79 +2,21 @@ import { fileURLToPath } from 'node:url';
2
2
  import { createRegistry } from './index.js';
3
3
  import sampleContentModule from './modules/sample-content/module.js';
4
4
  import sampleSystemModule from './modules/sample-system/module.js';
5
- import { dispatchAction } from './runtime/dispatch.js';
6
- import { runHelpCommand } from './cli/help-command.js';
7
- const parseCliArgs = (argv) => {
8
- const args = {};
9
- const positional = [];
10
- for (const token of argv) {
11
- if (token.startsWith('--')) {
12
- const body = token.slice(2);
13
- const separatorIndex = body.indexOf('=');
14
- if (separatorIndex === -1) {
15
- if (body.length > 0)
16
- args[body] = true;
17
- }
18
- else {
19
- const key = body.slice(0, separatorIndex);
20
- const value = body.slice(separatorIndex + 1);
21
- if (key.length > 0)
22
- args[key] = value;
23
- }
24
- continue;
25
- }
26
- positional.push(token);
27
- }
28
- return {
29
- moduleId: positional[0],
30
- actionId: positional[1],
31
- args
32
- };
33
- };
5
+ import { runMultiModuleCli } from './cli/cli-runner.js';
6
+ import { createTerminalOutput } from './dx/terminal/index.js';
34
7
  const registry = createRegistry([sampleContentModule, sampleSystemModule]);
35
- export const runCli = async (argv, isTTY = Boolean(process.stdout.isTTY)) => {
36
- const wantsHelp = argv.includes('-h') || argv.includes('--help');
37
- const filtered = argv.filter((item) => item !== '-h' && item !== '--help');
38
- const parsed = parseCliArgs(filtered);
39
- if (wantsHelp || !parsed.moduleId) {
40
- const helpResult = runHelpCommand({
41
- registry,
42
- moduleId: parsed.moduleId,
43
- actionId: parsed.actionId
44
- });
45
- if (helpResult.data)
46
- process.stdout.write(`${helpResult.data}\n`);
47
- if (helpResult.errorMessage)
48
- process.stderr.write(`${helpResult.errorMessage}\n`);
49
- return helpResult.exitCode;
50
- }
51
- if (!parsed.actionId) {
52
- const helpResult = runHelpCommand({ registry, moduleId: parsed.moduleId });
53
- if (helpResult.data)
54
- process.stdout.write(`${helpResult.data}\n`);
55
- return helpResult.exitCode;
56
- }
57
- const result = await dispatchAction({
58
- registry,
59
- moduleId: parsed.moduleId,
60
- actionId: parsed.actionId,
61
- args: parsed.args,
62
- isTTY
63
- });
64
- if (result.data !== undefined)
65
- process.stdout.write(`${String(result.data)}\n`);
66
- if (result.errorMessage)
67
- process.stderr.write(`${result.errorMessage}\n`);
68
- return result.exitCode;
8
+ export const runCli = async (argv, isTTY) => {
9
+ return runMultiModuleCli({ registry, argv, isTTY });
69
10
  };
70
11
  const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
71
12
  if (isMain) {
13
+ const terminal = createTerminalOutput();
72
14
  runCli(process.argv.slice(2))
73
15
  .then((code) => {
74
16
  process.exitCode = code;
75
17
  })
76
18
  .catch((error) => {
77
- process.stderr.write(`${error instanceof Error ? error.message : 'Unknown CLI error'}\n`);
19
+ terminal.error(error instanceof Error ? error.message : 'Unknown CLI error');
78
20
  process.exitCode = 1;
79
21
  });
80
22
  }
@@ -1,13 +1,11 @@
1
1
  import type { HelpContract } from './help-contract.js';
2
2
  import type { RuntimeContext } from '../../runtime/runtime-context.js';
3
+ import type { TuiContract } from './tui-contract.js';
3
4
  export type RuntimeMode = 'tui' | 'commandline';
4
5
  export interface CommandlineContract {
5
6
  requiredArgs: string[];
6
7
  optionalArgs?: string[];
7
8
  }
8
- export interface TuiContract {
9
- steps: string[];
10
- }
11
9
  export interface ActionResultEnvelope<T = unknown> {
12
10
  ok: boolean;
13
11
  mode: RuntimeMode;
@@ -0,0 +1,47 @@
1
+ export type TuiComponentType = 'text' | 'confirm' | 'select' | 'multiselect' | 'group' | 'custom';
2
+ export interface TuiOptionContract {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ export interface TuiStandardComponentContract {
8
+ componentId: string;
9
+ type: Exclude<TuiComponentType, 'custom'>;
10
+ label: string;
11
+ arg?: string;
12
+ required?: boolean;
13
+ placeholder?: string;
14
+ options?: TuiOptionContract[];
15
+ defaultValue?: string | boolean | string[];
16
+ }
17
+ export interface TuiCustomComponentContract<TConfig extends Record<string, unknown> = Record<string, unknown>> {
18
+ componentId: string;
19
+ type: 'custom';
20
+ label: string;
21
+ arg?: string;
22
+ required?: boolean;
23
+ renderer: string;
24
+ config?: TConfig;
25
+ defaultValue?: string | boolean | string[];
26
+ }
27
+ export type TuiComponentContract = TuiStandardComponentContract | TuiCustomComponentContract;
28
+ export type TuiTransitionType = 'next' | 'back' | 'jump' | 'exit';
29
+ export interface TuiStepTransitionContract {
30
+ on: TuiTransitionType;
31
+ targetStepId?: string;
32
+ }
33
+ export interface TuiStepContract {
34
+ stepId: string;
35
+ title: string;
36
+ description?: string;
37
+ components?: TuiComponentContract[];
38
+ transitions?: TuiStepTransitionContract[];
39
+ }
40
+ export interface TuiFlowContract {
41
+ entryStepId?: string;
42
+ steps: TuiStepContract[];
43
+ }
44
+ export interface TuiContract {
45
+ steps?: string[];
46
+ flow?: TuiFlowContract;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -38,7 +38,9 @@ export class ActionRegistry {
38
38
  return `${moduleId}.${actionId}`;
39
39
  }
40
40
  assertTriad(moduleId, action) {
41
- const hasTui = Array.isArray(action.tui?.steps) && action.tui.steps.length > 0;
41
+ const hasLegacySteps = Array.isArray(action.tui?.steps) && action.tui.steps.length > 0;
42
+ const hasFlowSteps = Array.isArray(action.tui?.flow?.steps) && action.tui.flow.steps.length > 0;
43
+ const hasTui = hasLegacySteps || hasFlowSteps;
42
44
  const hasCommandline = Array.isArray(action.commandline?.requiredArgs);
43
45
  const hasHelp = typeof action.help?.summary === 'string' &&
44
46
  Array.isArray(action.help?.args) &&
@@ -0,0 +1,6 @@
1
+ import type { CliArgs } from './types.js';
2
+ export interface CollectEnvArgsInput {
3
+ args: CliArgs;
4
+ reservedKeys?: Iterable<string>;
5
+ }
6
+ export declare const collectUpperSnakeCaseEnvArgs: (input: CollectEnvArgsInput) => Record<string, string>;
@@ -0,0 +1,15 @@
1
+ import { isUpperSnakeCaseKey } from './types.js';
2
+ export const collectUpperSnakeCaseEnvArgs = (input) => {
3
+ const reserved = new Set(input.reservedKeys ?? []);
4
+ const env = {};
5
+ for (const [key, value] of Object.entries(input.args)) {
6
+ if (reserved.has(key))
7
+ continue;
8
+ if (!isUpperSnakeCaseKey(key))
9
+ continue;
10
+ if (typeof value !== 'string' || value.length === 0)
11
+ continue;
12
+ env[key] = value;
13
+ }
14
+ return env;
15
+ };
@@ -0,0 +1,4 @@
1
+ export type { CliArgs, UpperSnakeCaseKey } from './types.js';
2
+ export { isUpperSnakeCaseKey } from './types.js';
3
+ export { readStringArg, readRequiredStringArg, parseBooleanLike, readBooleanArg } from './readers.js';
4
+ export { collectUpperSnakeCaseEnvArgs } from './env.js';
@@ -0,0 +1,3 @@
1
+ export { isUpperSnakeCaseKey } from './types.js';
2
+ export { readStringArg, readRequiredStringArg, parseBooleanLike, readBooleanArg } from './readers.js';
3
+ export { collectUpperSnakeCaseEnvArgs } from './env.js';
@@ -0,0 +1,5 @@
1
+ import type { CliArgs } from './types.js';
2
+ export declare const readStringArg: (args: CliArgs, ...keys: string[]) => string | undefined;
3
+ export declare const readRequiredStringArg: (args: CliArgs, key: string, message?: string) => string;
4
+ export declare const parseBooleanLike: (value: string | boolean | undefined) => boolean | undefined;
5
+ export declare const readBooleanArg: (args: CliArgs, ...keys: string[]) => boolean | undefined;
@@ -0,0 +1,36 @@
1
+ export const readStringArg = (args, ...keys) => {
2
+ for (const key of keys) {
3
+ const value = args[key];
4
+ if (typeof value === 'string' && value.trim().length > 0) {
5
+ return value.trim();
6
+ }
7
+ }
8
+ return undefined;
9
+ };
10
+ export const readRequiredStringArg = (args, key, message) => {
11
+ const value = readStringArg(args, key);
12
+ if (!value) {
13
+ throw new Error(message ?? `Missing required arg: ${key}`);
14
+ }
15
+ return value;
16
+ };
17
+ export const parseBooleanLike = (value) => {
18
+ if (value === undefined)
19
+ return undefined;
20
+ if (typeof value === 'boolean')
21
+ return value;
22
+ const normalized = value.trim().toLowerCase();
23
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized))
24
+ return true;
25
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized))
26
+ return false;
27
+ throw new Error(`Invalid boolean value "${value}".`);
28
+ };
29
+ export const readBooleanArg = (args, ...keys) => {
30
+ for (const key of keys) {
31
+ if (args[key] === undefined)
32
+ continue;
33
+ return parseBooleanLike(args[key]);
34
+ }
35
+ return undefined;
36
+ };
@@ -0,0 +1,5 @@
1
+ export type CliArgs = Record<string, string | boolean>;
2
+ export type UpperSnakeCaseKey = string & {
3
+ readonly __upperSnakeCaseKey: unique symbol;
4
+ };
5
+ export declare const isUpperSnakeCaseKey: (value: string) => value is UpperSnakeCaseKey;
@@ -0,0 +1 @@
1
+ export const isUpperSnakeCaseKey = (value) => /^[A-Z_][A-Z0-9_]*$/.test(value);
@@ -0,0 +1,10 @@
1
+ import type { HelpContract, HelpUseCaseSpec } from '../../core/contracts/help-contract.js';
2
+ import type { ArgSchemaMap } from './schema.js';
3
+ export interface HelpBuilderInput<Key extends string> {
4
+ summary: string;
5
+ argSchema: ArgSchemaMap<Key>;
6
+ examples?: string[];
7
+ useCases?: HelpUseCaseSpec[];
8
+ keybindings?: string[];
9
+ }
10
+ export declare const buildHelpFromArgSchema: <Key extends string>(input: HelpBuilderInput<Key>) => HelpContract;
@@ -0,0 +1,11 @@
1
+ import { helpArgsFromArgSchema } from './schema.js';
2
+ const DEFAULT_KEYBINDINGS = ['Enter confirm', 'Esc cancel'];
3
+ export const buildHelpFromArgSchema = (input) => {
4
+ return {
5
+ summary: input.summary,
6
+ args: helpArgsFromArgSchema(input.argSchema),
7
+ examples: input.examples ?? [],
8
+ useCases: input.useCases ?? [],
9
+ keybindings: input.keybindings ?? DEFAULT_KEYBINDINGS
10
+ };
11
+ };
@@ -0,0 +1,4 @@
1
+ export type { ArgSchema, ArgSchemaMap } from './schema.js';
2
+ export { defineArgSchema, commandlineFromArgSchema, helpArgsFromArgSchema } from './schema.js';
3
+ export type { HelpBuilderInput } from './builder.js';
4
+ export { buildHelpFromArgSchema } from './builder.js';
@@ -0,0 +1,2 @@
1
+ export { defineArgSchema, commandlineFromArgSchema, helpArgsFromArgSchema } from './schema.js';
2
+ export { buildHelpFromArgSchema } from './builder.js';
@@ -0,0 +1,14 @@
1
+ import type { CommandlineContract } from '../../core/contracts/action-contract.js';
2
+ import type { HelpArgumentSpec } from '../../core/contracts/help-contract.js';
3
+ export interface ArgSchema {
4
+ description: string;
5
+ example?: string;
6
+ required?: boolean;
7
+ includeInCommandline?: boolean;
8
+ includeInHelp?: boolean;
9
+ helpName?: string;
10
+ }
11
+ export type ArgSchemaMap<Key extends string = string> = Record<Key, ArgSchema>;
12
+ export declare const defineArgSchema: <Key extends string>(schema: ArgSchemaMap<Key>) => ArgSchemaMap<Key>;
13
+ export declare const commandlineFromArgSchema: <Key extends string>(schema: ArgSchemaMap<Key>) => CommandlineContract;
14
+ export declare const helpArgsFromArgSchema: <Key extends string>(schema: ArgSchemaMap<Key>) => HelpArgumentSpec[];
@@ -0,0 +1,33 @@
1
+ export const defineArgSchema = (schema) => schema;
2
+ export const commandlineFromArgSchema = (schema) => {
3
+ const requiredArgs = [];
4
+ const optionalArgs = [];
5
+ for (const [name, spec] of Object.entries(schema)) {
6
+ if (spec.includeInCommandline === false)
7
+ continue;
8
+ if (spec.required) {
9
+ requiredArgs.push(name);
10
+ }
11
+ else {
12
+ optionalArgs.push(name);
13
+ }
14
+ }
15
+ return {
16
+ requiredArgs,
17
+ optionalArgs
18
+ };
19
+ };
20
+ export const helpArgsFromArgSchema = (schema) => {
21
+ const results = [];
22
+ for (const [name, spec] of Object.entries(schema)) {
23
+ if (spec.includeInHelp === false)
24
+ continue;
25
+ results.push({
26
+ name: spec.helpName ?? name,
27
+ required: Boolean(spec.required),
28
+ description: spec.description,
29
+ example: spec.example
30
+ });
31
+ }
32
+ return results;
33
+ };
@@ -0,0 +1,12 @@
1
+ import type { ActionResultEnvelope } from '../../core/contracts/action-contract.js';
2
+ import { ExitCode } from '../../core/errors/framework-errors.js';
3
+ import type { RuntimeContext } from '../../runtime/runtime-context.js';
4
+ export declare const toSuccessResult: <T>(context: RuntimeContext, data: T, exitCode?: ExitCode) => ActionResultEnvelope<T>;
5
+ export declare const toErrorResult: <T = unknown>(context: RuntimeContext, error: unknown, fallbackMessage: string, exitCode?: ExitCode) => ActionResultEnvelope<T>;
6
+ export interface RunActionSafelyInput<T> {
7
+ context: RuntimeContext;
8
+ execute: () => Promise<T>;
9
+ fallbackErrorMessage: string;
10
+ onSuccess?: (result: T) => T;
11
+ }
12
+ export declare const runActionSafely: <T>(input: RunActionSafelyInput<T>) => Promise<ActionResultEnvelope<T>>;
@@ -0,0 +1,35 @@
1
+ import { ExitCode } from '../../core/errors/framework-errors.js';
2
+ import { isPromptCancelledError } from '../../tui/component-adapters/cancel.js';
3
+ export const toSuccessResult = (context, data, exitCode = ExitCode.SUCCESS) => ({
4
+ ok: true,
5
+ mode: context.mode,
6
+ exitCode,
7
+ data
8
+ });
9
+ export const toErrorResult = (context, error, fallbackMessage, exitCode = ExitCode.VALIDATION_ERROR) => {
10
+ if (isPromptCancelledError(error)) {
11
+ return {
12
+ ok: false,
13
+ mode: context.mode,
14
+ exitCode: ExitCode.INTERRUPTED,
15
+ errorMessage: undefined
16
+ };
17
+ }
18
+ return {
19
+ ok: false,
20
+ mode: context.mode,
21
+ exitCode,
22
+ errorMessage: error instanceof Error ? error.message : fallbackMessage
23
+ };
24
+ };
25
+ export const runActionSafely = async (input) => {
26
+ try {
27
+ const result = await input.execute();
28
+ input.context.flow.exit();
29
+ const finalResult = input.onSuccess ? input.onSuccess(result) : result;
30
+ return toSuccessResult(input.context, finalResult);
31
+ }
32
+ catch (error) {
33
+ return toErrorResult(input.context, error, input.fallbackErrorMessage);
34
+ }
35
+ };
@@ -0,0 +1,9 @@
1
+ import type { StateMachine } from '../../runtime/state-machine.js';
2
+ export interface FlowController {
3
+ next(): void;
4
+ back(): void;
5
+ jump(stepId: string): void;
6
+ exit(): void;
7
+ currentStepId(): string | undefined;
8
+ }
9
+ export declare const createFlowController: (stateMachine?: StateMachine) => FlowController;
@@ -0,0 +1,19 @@
1
+ export const createFlowController = (stateMachine) => {
2
+ return {
3
+ next() {
4
+ stateMachine?.transition({ type: 'next' });
5
+ },
6
+ back() {
7
+ stateMachine?.transition({ type: 'back' });
8
+ },
9
+ jump(stepId) {
10
+ stateMachine?.transition({ type: 'jump', targetNodeId: stepId });
11
+ },
12
+ exit() {
13
+ stateMachine?.transition({ type: 'exit' });
14
+ },
15
+ currentStepId() {
16
+ return stateMachine?.currentNode().id;
17
+ }
18
+ };
19
+ };
@@ -0,0 +1,4 @@
1
+ export type { FlowController } from './flow.js';
2
+ export { createFlowController } from './flow.js';
3
+ export { toSuccessResult, toErrorResult, runActionSafely } from './action-result.js';
4
+ export type { RunActionSafelyInput } from './action-result.js';
@@ -0,0 +1,2 @@
1
+ export { createFlowController } from './flow.js';
2
+ export { toSuccessResult, toErrorResult, runActionSafely } from './action-result.js';
@@ -0,0 +1,2 @@
1
+ export type { TerminalOutput } from './output.js';
2
+ export { createTerminalOutput } from './output.js';
@@ -0,0 +1 @@
1
+ export { createTerminalOutput } from './output.js';
@@ -0,0 +1,7 @@
1
+ import type { Writable } from 'node:stream';
2
+ export interface TerminalOutput {
3
+ line(message: string): void;
4
+ lines(messages: readonly string[]): void;
5
+ error(message: string): void;
6
+ }
7
+ export declare const createTerminalOutput: (stdout?: Writable, stderr?: Writable) => TerminalOutput;