@levu/snap 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +33 -2
- package/dist/cli/cli-runner.d.ts +37 -0
- package/dist/cli/cli-runner.js +161 -0
- package/dist/cli-entry.js +6 -64
- package/dist/core/contracts/action-contract.d.ts +1 -3
- package/dist/core/contracts/tui-contract.d.ts +47 -0
- package/dist/core/contracts/tui-contract.js +1 -0
- package/dist/core/registry/action-registry.js +3 -1
- package/dist/dx/args/env.d.ts +6 -0
- package/dist/dx/args/env.js +15 -0
- package/dist/dx/args/index.d.ts +4 -0
- package/dist/dx/args/index.js +3 -0
- package/dist/dx/args/readers.d.ts +5 -0
- package/dist/dx/args/readers.js +36 -0
- package/dist/dx/args/types.d.ts +5 -0
- package/dist/dx/args/types.js +1 -0
- package/dist/dx/help/builder.d.ts +10 -0
- package/dist/dx/help/builder.js +11 -0
- package/dist/dx/help/index.d.ts +4 -0
- package/dist/dx/help/index.js +2 -0
- package/dist/dx/help/schema.d.ts +14 -0
- package/dist/dx/help/schema.js +33 -0
- package/dist/dx/runtime/action-result.d.ts +12 -0
- package/dist/dx/runtime/action-result.js +35 -0
- package/dist/dx/runtime/flow.d.ts +9 -0
- package/dist/dx/runtime/flow.js +19 -0
- package/dist/dx/runtime/index.d.ts +4 -0
- package/dist/dx/runtime/index.js +2 -0
- package/dist/dx/terminal/index.d.ts +3 -0
- package/dist/dx/terminal/index.js +3 -0
- package/dist/dx/terminal/intro-outro.d.ts +4 -0
- package/dist/dx/terminal/intro-outro.js +44 -0
- package/dist/dx/terminal/output.d.ts +19 -0
- package/dist/dx/terminal/output.js +59 -0
- package/dist/dx/tui/components.d.ts +6 -0
- package/dist/dx/tui/components.js +40 -0
- package/dist/dx/tui/flow.d.ts +2 -0
- package/dist/dx/tui/flow.js +14 -0
- package/dist/dx/tui/index.d.ts +16 -0
- package/dist/dx/tui/index.js +15 -0
- package/dist/dx/tui/no-result.d.ts +13 -0
- package/dist/dx/tui/no-result.js +18 -0
- package/dist/help/help-renderer.js +5 -1
- package/dist/help/hierarchy-resolver.js +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +11 -0
- package/dist/runtime/dispatch.d.ts +2 -1
- package/dist/runtime/engine.d.ts +2 -1
- package/dist/runtime/engine.js +22 -1
- package/dist/runtime/mode-resolver.d.ts +3 -2
- package/dist/runtime/runtime-context.d.ts +8 -1
- package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
- package/dist/tui/component-adapters/autocomplete.js +34 -0
- package/dist/tui/component-adapters/cancel.d.ts +6 -0
- package/dist/tui/component-adapters/cancel.js +20 -0
- package/dist/tui/component-adapters/confirm.d.ts +2 -0
- package/dist/tui/component-adapters/confirm.js +13 -1
- package/dist/tui/component-adapters/multiselect.d.ts +4 -0
- package/dist/tui/component-adapters/multiselect.js +23 -3
- package/dist/tui/component-adapters/note.d.ts +7 -0
- package/dist/tui/component-adapters/note.js +23 -0
- package/dist/tui/component-adapters/password.d.ts +7 -0
- package/dist/tui/component-adapters/password.js +24 -0
- package/dist/tui/component-adapters/progress.d.ts +7 -0
- package/dist/tui/component-adapters/progress.js +44 -0
- package/dist/tui/component-adapters/readline-utils.d.ts +1 -0
- package/dist/tui/component-adapters/readline-utils.js +2 -0
- package/dist/tui/component-adapters/select.d.ts +2 -0
- package/dist/tui/component-adapters/select.js +25 -3
- package/dist/tui/component-adapters/spinner.d.ts +10 -0
- package/dist/tui/component-adapters/spinner.js +48 -0
- package/dist/tui/component-adapters/tasks.d.ts +9 -0
- package/dist/tui/component-adapters/tasks.js +31 -0
- package/dist/tui/component-adapters/text.d.ts +2 -0
- package/dist/tui/component-adapters/text.js +21 -4
- package/dist/tui/custom/custom-prompt.d.ts +16 -0
- package/dist/tui/custom/custom-prompt.js +72 -0
- package/dist/tui/custom/index.d.ts +2 -0
- package/dist/tui/custom/index.js +1 -0
- package/dist/tui/prompt-toolkit.d.ts +15 -0
- package/dist/tui/prompt-toolkit.js +17 -0
- package/docs/component-reference.md +474 -0
- package/docs/getting-started.md +242 -0
- package/docs/integration-examples.md +677 -0
- package/docs/module-authoring-guide.md +105 -1
- package/docs/snap-args.md +323 -0
- package/docs/snap-help.md +372 -0
- package/docs/snap-runtime.md +394 -0
- package/docs/snap-terminal.md +410 -0
- package/docs/snap-tui.md +529 -0
- package/package.json +15 -2
- package/.github/workflows/ci.yml +0 -26
- package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +0 -71
- package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +0 -76
- package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +0 -71
- package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +0 -69
- package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +0 -79
- package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +0 -75
- package/plans/260209-1547-hub-dual-runtime-framework/plan.md +0 -105
- package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +0 -27
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +0 -166
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +0 -87
- package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +0 -24
- package/src/cli/help-command.ts +0 -1
- package/src/cli-entry.ts +0 -83
- package/src/core/contracts/action-contract.ts +0 -30
- package/src/core/contracts/help-contract.ts +0 -20
- package/src/core/contracts/module-contract.ts +0 -7
- package/src/core/errors/framework-errors.ts +0 -26
- package/src/core/registry/action-registry.ts +0 -94
- package/src/help/help-command.ts +0 -32
- package/src/help/help-model.ts +0 -10
- package/src/help/help-renderer.ts +0 -21
- package/src/help/hierarchy-resolver.ts +0 -54
- package/src/index.ts +0 -10
- package/src/modules/sample-content/module.ts +0 -66
- package/src/modules/sample-system/module.ts +0 -74
- package/src/runtime/dispatch.ts +0 -64
- package/src/runtime/engine.ts +0 -59
- package/src/runtime/mode-resolver.ts +0 -18
- package/src/runtime/resume-store.ts +0 -53
- package/src/runtime/runtime-context.ts +0 -10
- package/src/runtime/state-machine.ts +0 -77
- package/src/tui/accessibility-footer.ts +0 -11
- package/src/tui/component-adapters/confirm.ts +0 -8
- package/src/tui/component-adapters/group.ts +0 -12
- package/src/tui/component-adapters/multiselect.ts +0 -22
- package/src/tui/component-adapters/select.ts +0 -18
- package/src/tui/component-adapters/text.ts +0 -13
- package/src/tui/interrupt-handlers.ts +0 -15
- package/tests/e2e/cli-smoke.e2e.test.ts +0 -19
- package/tests/integration/runtime-dispatch.integration.test.ts +0 -23
- package/tests/transcript/help.transcript.test.ts +0 -20
- package/tests/unit/state-machine.test.ts +0 -22
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -8
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
|
-
|
|
49
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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,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,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 @@
|
|
|
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,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>>;
|