@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +33 -2
  3. package/dist/cli/cli-runner.d.ts +37 -0
  4. package/dist/cli/cli-runner.js +161 -0
  5. package/dist/cli-entry.js +6 -64
  6. package/dist/core/contracts/action-contract.d.ts +1 -3
  7. package/dist/core/contracts/tui-contract.d.ts +47 -0
  8. package/dist/core/contracts/tui-contract.js +1 -0
  9. package/dist/core/registry/action-registry.js +3 -1
  10. package/dist/dx/args/env.d.ts +6 -0
  11. package/dist/dx/args/env.js +15 -0
  12. package/dist/dx/args/index.d.ts +4 -0
  13. package/dist/dx/args/index.js +3 -0
  14. package/dist/dx/args/readers.d.ts +5 -0
  15. package/dist/dx/args/readers.js +36 -0
  16. package/dist/dx/args/types.d.ts +5 -0
  17. package/dist/dx/args/types.js +1 -0
  18. package/dist/dx/help/builder.d.ts +10 -0
  19. package/dist/dx/help/builder.js +11 -0
  20. package/dist/dx/help/index.d.ts +4 -0
  21. package/dist/dx/help/index.js +2 -0
  22. package/dist/dx/help/schema.d.ts +14 -0
  23. package/dist/dx/help/schema.js +33 -0
  24. package/dist/dx/runtime/action-result.d.ts +12 -0
  25. package/dist/dx/runtime/action-result.js +35 -0
  26. package/dist/dx/runtime/flow.d.ts +9 -0
  27. package/dist/dx/runtime/flow.js +19 -0
  28. package/dist/dx/runtime/index.d.ts +4 -0
  29. package/dist/dx/runtime/index.js +2 -0
  30. package/dist/dx/terminal/index.d.ts +3 -0
  31. package/dist/dx/terminal/index.js +3 -0
  32. package/dist/dx/terminal/intro-outro.d.ts +4 -0
  33. package/dist/dx/terminal/intro-outro.js +44 -0
  34. package/dist/dx/terminal/output.d.ts +19 -0
  35. package/dist/dx/terminal/output.js +59 -0
  36. package/dist/dx/tui/components.d.ts +6 -0
  37. package/dist/dx/tui/components.js +40 -0
  38. package/dist/dx/tui/flow.d.ts +2 -0
  39. package/dist/dx/tui/flow.js +14 -0
  40. package/dist/dx/tui/index.d.ts +16 -0
  41. package/dist/dx/tui/index.js +15 -0
  42. package/dist/dx/tui/no-result.d.ts +13 -0
  43. package/dist/dx/tui/no-result.js +18 -0
  44. package/dist/help/help-renderer.js +5 -1
  45. package/dist/help/hierarchy-resolver.js +1 -1
  46. package/dist/index.d.ts +16 -0
  47. package/dist/index.js +11 -0
  48. package/dist/runtime/dispatch.d.ts +2 -1
  49. package/dist/runtime/engine.d.ts +2 -1
  50. package/dist/runtime/engine.js +22 -1
  51. package/dist/runtime/mode-resolver.d.ts +3 -2
  52. package/dist/runtime/runtime-context.d.ts +8 -1
  53. package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
  54. package/dist/tui/component-adapters/autocomplete.js +34 -0
  55. package/dist/tui/component-adapters/cancel.d.ts +6 -0
  56. package/dist/tui/component-adapters/cancel.js +20 -0
  57. package/dist/tui/component-adapters/confirm.d.ts +2 -0
  58. package/dist/tui/component-adapters/confirm.js +13 -1
  59. package/dist/tui/component-adapters/multiselect.d.ts +4 -0
  60. package/dist/tui/component-adapters/multiselect.js +23 -3
  61. package/dist/tui/component-adapters/note.d.ts +7 -0
  62. package/dist/tui/component-adapters/note.js +23 -0
  63. package/dist/tui/component-adapters/password.d.ts +7 -0
  64. package/dist/tui/component-adapters/password.js +24 -0
  65. package/dist/tui/component-adapters/progress.d.ts +7 -0
  66. package/dist/tui/component-adapters/progress.js +44 -0
  67. package/dist/tui/component-adapters/readline-utils.d.ts +1 -0
  68. package/dist/tui/component-adapters/readline-utils.js +2 -0
  69. package/dist/tui/component-adapters/select.d.ts +2 -0
  70. package/dist/tui/component-adapters/select.js +25 -3
  71. package/dist/tui/component-adapters/spinner.d.ts +10 -0
  72. package/dist/tui/component-adapters/spinner.js +48 -0
  73. package/dist/tui/component-adapters/tasks.d.ts +9 -0
  74. package/dist/tui/component-adapters/tasks.js +31 -0
  75. package/dist/tui/component-adapters/text.d.ts +2 -0
  76. package/dist/tui/component-adapters/text.js +21 -4
  77. package/dist/tui/custom/custom-prompt.d.ts +16 -0
  78. package/dist/tui/custom/custom-prompt.js +72 -0
  79. package/dist/tui/custom/index.d.ts +2 -0
  80. package/dist/tui/custom/index.js +1 -0
  81. package/dist/tui/prompt-toolkit.d.ts +15 -0
  82. package/dist/tui/prompt-toolkit.js +17 -0
  83. package/docs/component-reference.md +474 -0
  84. package/docs/getting-started.md +242 -0
  85. package/docs/integration-examples.md +677 -0
  86. package/docs/module-authoring-guide.md +105 -1
  87. package/docs/snap-args.md +323 -0
  88. package/docs/snap-help.md +372 -0
  89. package/docs/snap-runtime.md +394 -0
  90. package/docs/snap-terminal.md +410 -0
  91. package/docs/snap-tui.md +529 -0
  92. package/package.json +15 -2
  93. package/.github/workflows/ci.yml +0 -26
  94. package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +0 -71
  95. package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +0 -76
  96. package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +0 -71
  97. package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +0 -69
  98. package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +0 -79
  99. package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +0 -75
  100. package/plans/260209-1547-hub-dual-runtime-framework/plan.md +0 -105
  101. package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +0 -27
  102. package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +0 -166
  103. package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +0 -87
  104. package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +0 -24
  105. package/src/cli/help-command.ts +0 -1
  106. package/src/cli-entry.ts +0 -83
  107. package/src/core/contracts/action-contract.ts +0 -30
  108. package/src/core/contracts/help-contract.ts +0 -20
  109. package/src/core/contracts/module-contract.ts +0 -7
  110. package/src/core/errors/framework-errors.ts +0 -26
  111. package/src/core/registry/action-registry.ts +0 -94
  112. package/src/help/help-command.ts +0 -32
  113. package/src/help/help-model.ts +0 -10
  114. package/src/help/help-renderer.ts +0 -21
  115. package/src/help/hierarchy-resolver.ts +0 -54
  116. package/src/index.ts +0 -10
  117. package/src/modules/sample-content/module.ts +0 -66
  118. package/src/modules/sample-system/module.ts +0 -74
  119. package/src/runtime/dispatch.ts +0 -64
  120. package/src/runtime/engine.ts +0 -59
  121. package/src/runtime/mode-resolver.ts +0 -18
  122. package/src/runtime/resume-store.ts +0 -53
  123. package/src/runtime/runtime-context.ts +0 -10
  124. package/src/runtime/state-machine.ts +0 -77
  125. package/src/tui/accessibility-footer.ts +0 -11
  126. package/src/tui/component-adapters/confirm.ts +0 -8
  127. package/src/tui/component-adapters/group.ts +0 -12
  128. package/src/tui/component-adapters/multiselect.ts +0 -22
  129. package/src/tui/component-adapters/select.ts +0 -18
  130. package/src/tui/component-adapters/text.ts +0 -13
  131. package/src/tui/interrupt-handlers.ts +0 -15
  132. package/tests/e2e/cli-smoke.e2e.test.ts +0 -19
  133. package/tests/integration/runtime-dispatch.integration.test.ts +0 -23
  134. package/tests/transcript/help.transcript.test.ts +0 -20
  135. package/tests/unit/state-machine.test.ts +0 -22
  136. package/tsconfig.build.json +0 -9
  137. package/tsconfig.json +0 -17
  138. package/vitest.config.ts +0 -8
package/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2025-02-24
9
+
10
+ ### Added
11
+ - **New Components**: Added `spinner` component for loading states during long-running operations
12
+ - **New Components**: Added `password` component for secure text input
13
+ - **New Terminal Features**: Added structured logging with `log.info()`, `log.success()`, `log.warn()`, `log.error()` utilities
14
+ - **Documentation**: Comprehensive Getting Started guide with installation and quick start examples
15
+ - **Documentation**: SnapArgs documentation - type-safe argument reading helpers
16
+ - **Documentation**: SnapHelp documentation - schema-driven help generation
17
+ - **Documentation**: SnapRuntime documentation - standardized action result helpers
18
+ - **Documentation**: SnapTui documentation - typed TUI flow definitions and components
19
+ - **Documentation**: SnapTerminal documentation - terminal output and logging helpers
20
+ - **Documentation**: Integration examples demonstrating common patterns
21
+ - **Examples**: Full example project in `example/` directory
22
+ - **Tests**: Added comprehensive test coverage for new components and documentation examples
23
+
24
+ ### Changed
25
+ - Enhanced `TerminalOutput` interface with `info()`, `success()`, and `warn()` methods
26
+ - Updated `.gitignore` with comprehensive Node.js package patterns
27
+ - Removed `.idea/` folder from git tracking
28
+
29
+ ### Fixed
30
+ - Fixed issue where users couldn't discover available components (now fully documented)
31
+
32
+ ## [0.1.1] - 2025-02-10
33
+
34
+ ### Added
35
+ - Initial release of Snap framework
36
+ - Contract-first TypeScript framework for terminal workflows
37
+ - Core contracts: ActionContract, ModuleContract, HelpContract, TuiContract
38
+ - CLI runners: multi-module, single-module, and submodule support
39
+ - DX helpers: SnapArgs, SnapHelp, SnapRuntime, SnapTui, SnapTerminal
40
+ - TUI components: text, confirm, select, multiselect, cancel, group
41
+ - Help system with deterministic, text-only output
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:
@@ -45,5 +52,29 @@ npm run dev -- system node-info
45
52
 
46
53
  ## For module authors
47
54
 
48
- - `docs/module-authoring-guide.md`
49
- - `docs/help-contract-spec.md`
55
+ ### Documentation
56
+
57
+ - **[Getting Started](./docs/getting-started.md)** - Quick start guide
58
+ - **[Module Authoring Guide](./docs/module-authoring-guide.md)** - Core concepts
59
+ - **[Help Contract Spec](./docs/help-contract-spec.md)** - Help format specification
60
+
61
+ ### DX Helper References
62
+
63
+ - **[SnapArgs](./docs/snap-args.md)** - Type-safe argument reading
64
+ - **[SnapHelp](./docs/snap-help.md)** - Schema-driven help generation
65
+ - **[SnapRuntime](./docs/snap-runtime.md)** - Standardized action results
66
+ - **[SnapTui](./docs/snap-tui.md)** - Typed TUI flow definitions
67
+ - **[SnapTerminal](./docs/snap-terminal.md)** - Terminal output helpers
68
+
69
+ ### Additional Resources
70
+
71
+ - **[Integration Examples](./docs/integration-examples.md)** - Common patterns
72
+ - **[Component Reference](./docs/component-reference.md)** - All components and gaps
73
+
74
+ ### Examples
75
+
76
+ See [`examples/`](./examples/) for working code examples:
77
+ - `basic-module.ts` - Minimal module structure
78
+ - `advanced-flow.ts` - Multi-step workflows
79
+ - `dx-helpers.ts` - All DX helpers in action
80
+ - `custom-prompt.ts` - Custom prompts with validation
@@ -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>>;