@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.
- package/README.md +7 -0
- 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 +2 -0
- package/dist/dx/terminal/index.js +1 -0
- package/dist/dx/terminal/output.d.ts +7 -0
- package/dist/dx/terminal/output.js +18 -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 +4 -0
- package/dist/dx/tui/index.js +3 -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 +12 -0
- package/dist/index.js +9 -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/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/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/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/package.json +13 -2
- package/.github/workflows/ci.yml +0 -26
- package/docs/help-contract-spec.md +0 -29
- package/docs/module-authoring-guide.md +0 -52
- 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/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 {
|
|
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>>;
|
|
@@ -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 @@
|
|
|
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;
|