@levu/snap 0.1.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/.github/workflows/ci.yml +26 -0
- package/README.md +49 -0
- package/dist/cli/help-command.d.ts +1 -0
- package/dist/cli/help-command.js +1 -0
- package/dist/cli-entry.d.ts +1 -0
- package/dist/cli-entry.js +80 -0
- package/dist/core/contracts/action-contract.d.ts +25 -0
- package/dist/core/contracts/action-contract.js +1 -0
- package/dist/core/contracts/help-contract.d.ts +18 -0
- package/dist/core/contracts/help-contract.js +1 -0
- package/dist/core/contracts/module-contract.d.ts +6 -0
- package/dist/core/contracts/module-contract.js +1 -0
- package/dist/core/errors/framework-errors.d.ts +19 -0
- package/dist/core/errors/framework-errors.js +26 -0
- package/dist/core/registry/action-registry.d.ts +16 -0
- package/dist/core/registry/action-registry.js +52 -0
- package/dist/help/help-command.d.ts +8 -0
- package/dist/help/help-command.js +21 -0
- package/dist/help/help-model.d.ts +9 -0
- package/dist/help/help-model.js +1 -0
- package/dist/help/help-renderer.d.ts +2 -0
- package/dist/help/help-renderer.js +16 -0
- package/dist/help/hierarchy-resolver.d.ts +3 -0
- package/dist/help/hierarchy-resolver.js +43 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/modules/sample-content/module.d.ts +3 -0
- package/dist/modules/sample-content/module.js +61 -0
- package/dist/modules/sample-system/module.d.ts +3 -0
- package/dist/modules/sample-system/module.js +71 -0
- package/dist/runtime/dispatch.d.ts +11 -0
- package/dist/runtime/dispatch.js +47 -0
- package/dist/runtime/engine.d.ts +13 -0
- package/dist/runtime/engine.js +44 -0
- package/dist/runtime/mode-resolver.d.ts +8 -0
- package/dist/runtime/mode-resolver.js +8 -0
- package/dist/runtime/resume-store.d.ts +13 -0
- package/dist/runtime/resume-store.js +45 -0
- package/dist/runtime/runtime-context.d.ts +9 -0
- package/dist/runtime/runtime-context.js +1 -0
- package/dist/runtime/state-machine.d.ts +32 -0
- package/dist/runtime/state-machine.js +50 -0
- package/dist/src/cli/help-command.d.ts +1 -0
- package/dist/src/cli/help-command.js +1 -0
- package/dist/src/cli-entry.d.ts +1 -0
- package/dist/src/cli-entry.js +80 -0
- package/dist/src/core/contracts/action-contract.d.ts +25 -0
- package/dist/src/core/contracts/action-contract.js +1 -0
- package/dist/src/core/contracts/help-contract.d.ts +18 -0
- package/dist/src/core/contracts/help-contract.js +1 -0
- package/dist/src/core/contracts/module-contract.d.ts +6 -0
- package/dist/src/core/contracts/module-contract.js +1 -0
- package/dist/src/core/errors/framework-errors.d.ts +19 -0
- package/dist/src/core/errors/framework-errors.js +26 -0
- package/dist/src/core/registry/action-registry.d.ts +16 -0
- package/dist/src/core/registry/action-registry.js +52 -0
- package/dist/src/help/help-command.d.ts +8 -0
- package/dist/src/help/help-command.js +21 -0
- package/dist/src/help/help-model.d.ts +9 -0
- package/dist/src/help/help-model.js +1 -0
- package/dist/src/help/help-renderer.d.ts +2 -0
- package/dist/src/help/help-renderer.js +16 -0
- package/dist/src/help/hierarchy-resolver.d.ts +3 -0
- package/dist/src/help/hierarchy-resolver.js +43 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +8 -0
- package/dist/src/modules/sample-content/module.d.ts +3 -0
- package/dist/src/modules/sample-content/module.js +61 -0
- package/dist/src/modules/sample-system/module.d.ts +3 -0
- package/dist/src/modules/sample-system/module.js +71 -0
- package/dist/src/runtime/dispatch.d.ts +11 -0
- package/dist/src/runtime/dispatch.js +47 -0
- package/dist/src/runtime/engine.d.ts +13 -0
- package/dist/src/runtime/engine.js +44 -0
- package/dist/src/runtime/mode-resolver.d.ts +8 -0
- package/dist/src/runtime/mode-resolver.js +8 -0
- package/dist/src/runtime/resume-store.d.ts +13 -0
- package/dist/src/runtime/resume-store.js +45 -0
- package/dist/src/runtime/runtime-context.d.ts +9 -0
- package/dist/src/runtime/runtime-context.js +1 -0
- package/dist/src/runtime/state-machine.d.ts +32 -0
- package/dist/src/runtime/state-machine.js +50 -0
- package/dist/src/tui/accessibility-footer.d.ts +7 -0
- package/dist/src/tui/accessibility-footer.js +4 -0
- package/dist/src/tui/component-adapters/confirm.d.ts +5 -0
- package/dist/src/tui/component-adapters/confirm.js +3 -0
- package/dist/src/tui/component-adapters/group.d.ts +5 -0
- package/dist/src/tui/component-adapters/group.js +7 -0
- package/dist/src/tui/component-adapters/multiselect.d.ts +10 -0
- package/dist/src/tui/component-adapters/multiselect.js +9 -0
- package/dist/src/tui/component-adapters/select.d.ts +10 -0
- package/dist/src/tui/component-adapters/select.js +7 -0
- package/dist/src/tui/component-adapters/text.d.ts +6 -0
- package/dist/src/tui/component-adapters/text.js +7 -0
- package/dist/src/tui/interrupt-handlers.d.ts +7 -0
- package/dist/src/tui/interrupt-handlers.js +7 -0
- package/dist/tests/e2e/cli-smoke.e2e.test.d.ts +1 -0
- package/dist/tests/e2e/cli-smoke.e2e.test.js +16 -0
- package/dist/tests/integration/runtime-dispatch.integration.test.d.ts +1 -0
- package/dist/tests/integration/runtime-dispatch.integration.test.js +20 -0
- package/dist/tests/transcript/help.transcript.test.d.ts +1 -0
- package/dist/tests/transcript/help.transcript.test.js +18 -0
- package/dist/tests/unit/state-machine.test.d.ts +1 -0
- package/dist/tests/unit/state-machine.test.js +20 -0
- package/dist/tui/accessibility-footer.d.ts +7 -0
- package/dist/tui/accessibility-footer.js +4 -0
- package/dist/tui/component-adapters/confirm.d.ts +5 -0
- package/dist/tui/component-adapters/confirm.js +3 -0
- package/dist/tui/component-adapters/group.d.ts +5 -0
- package/dist/tui/component-adapters/group.js +7 -0
- package/dist/tui/component-adapters/multiselect.d.ts +10 -0
- package/dist/tui/component-adapters/multiselect.js +9 -0
- package/dist/tui/component-adapters/select.d.ts +10 -0
- package/dist/tui/component-adapters/select.js +7 -0
- package/dist/tui/component-adapters/text.d.ts +6 -0
- package/dist/tui/component-adapters/text.js +7 -0
- package/dist/tui/interrupt-handlers.d.ts +7 -0
- package/dist/tui/interrupt-handlers.js +7 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +7 -0
- package/docs/help-contract-spec.md +29 -0
- package/docs/module-authoring-guide.md +52 -0
- package/package.json +20 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +71 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +76 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +71 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +69 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +79 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +75 -0
- package/plans/260209-1547-hub-dual-runtime-framework/plan.md +105 -0
- package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +27 -0
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +166 -0
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +87 -0
- package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +24 -0
- package/src/cli/help-command.ts +1 -0
- package/src/cli-entry.ts +83 -0
- package/src/core/contracts/action-contract.ts +30 -0
- package/src/core/contracts/help-contract.ts +20 -0
- package/src/core/contracts/module-contract.ts +7 -0
- package/src/core/errors/framework-errors.ts +26 -0
- package/src/core/registry/action-registry.ts +94 -0
- package/src/help/help-command.ts +32 -0
- package/src/help/help-model.ts +10 -0
- package/src/help/help-renderer.ts +21 -0
- package/src/help/hierarchy-resolver.ts +54 -0
- package/src/index.ts +10 -0
- package/src/modules/sample-content/module.ts +66 -0
- package/src/modules/sample-system/module.ts +74 -0
- package/src/runtime/dispatch.ts +64 -0
- package/src/runtime/engine.ts +59 -0
- package/src/runtime/mode-resolver.ts +18 -0
- package/src/runtime/resume-store.ts +53 -0
- package/src/runtime/runtime-context.ts +10 -0
- package/src/runtime/state-machine.ts +77 -0
- package/src/tui/accessibility-footer.ts +11 -0
- package/src/tui/component-adapters/confirm.ts +8 -0
- package/src/tui/component-adapters/group.ts +12 -0
- package/src/tui/component-adapters/multiselect.ts +22 -0
- package/src/tui/component-adapters/select.ts +18 -0
- package/src/tui/component-adapters/text.ts +13 -0
- package/src/tui/interrupt-handlers.ts +15 -0
- package/tests/e2e/cli-smoke.e2e.test.ts +19 -0
- package/tests/integration/runtime-dispatch.integration.test.ts +23 -0
- package/tests/transcript/help.transcript.test.ts +20 -0
- package/tests/unit/state-machine.test.ts +22 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ['**']
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: test-${{ matrix.os }}-node${{ matrix.node }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
|
+
node: [20, 22]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node }}
|
|
22
|
+
cache: npm
|
|
23
|
+
- run: npm ci
|
|
24
|
+
- run: npm run typecheck
|
|
25
|
+
- run: npm run build
|
|
26
|
+
- run: npm test
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Snap
|
|
2
|
+
|
|
3
|
+
Snap is a contract-first TypeScript framework for terminal workflows.
|
|
4
|
+
|
|
5
|
+
It runs one action contract in 2 modes:
|
|
6
|
+
- **TUI-first** (default for interactive terminals)
|
|
7
|
+
- **Auto CLI** (non-interactive when required args are already provided)
|
|
8
|
+
|
|
9
|
+
It also enforces deterministic, text-only help so both **humans** and **AI agents** can discover commands reliably.
|
|
10
|
+
|
|
11
|
+
## What this framework does
|
|
12
|
+
|
|
13
|
+
- Enforces action triad at registration: `tui + commandline + help`
|
|
14
|
+
- Uses one runtime engine for TUI and CLI paths
|
|
15
|
+
- Supports workflow transitions: `next`, `back`, `jump`, `exit`
|
|
16
|
+
- Supports resume checkpoints for interrupted flows
|
|
17
|
+
- Produces stable help output hierarchy:
|
|
18
|
+
- `snap -h`
|
|
19
|
+
- `snap -h <module>`
|
|
20
|
+
- `snap -h <module> <action>`
|
|
21
|
+
|
|
22
|
+
## Repository
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone git@github.com:khanglvm/snap.git
|
|
26
|
+
cd snap
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npm run typecheck
|
|
34
|
+
npm run build
|
|
35
|
+
npm test
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run dev -- -h
|
|
42
|
+
npm run dev -- content slugify --text="Hello World"
|
|
43
|
+
npm run dev -- system node-info
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## For module authors
|
|
47
|
+
|
|
48
|
+
- `docs/module-authoring-guide.md`
|
|
49
|
+
- `docs/help-contract-spec.md`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runHelpCommand, type HelpCommandInput } from '../help/help-command.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runHelpCommand } from '../help/help-command.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const runCli: (argv: string[], isTTY?: boolean) => Promise<number>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { createRegistry } from './index.js';
|
|
3
|
+
import sampleContentModule from './modules/sample-content/module.js';
|
|
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
|
+
};
|
|
34
|
+
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;
|
|
69
|
+
};
|
|
70
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
71
|
+
if (isMain) {
|
|
72
|
+
runCli(process.argv.slice(2))
|
|
73
|
+
.then((code) => {
|
|
74
|
+
process.exitCode = code;
|
|
75
|
+
})
|
|
76
|
+
.catch((error) => {
|
|
77
|
+
process.stderr.write(`${error instanceof Error ? error.message : 'Unknown CLI error'}\n`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HelpContract } from './help-contract.js';
|
|
2
|
+
import type { RuntimeContext } from '../../runtime/runtime-context.js';
|
|
3
|
+
export type RuntimeMode = 'tui' | 'commandline';
|
|
4
|
+
export interface CommandlineContract {
|
|
5
|
+
requiredArgs: string[];
|
|
6
|
+
optionalArgs?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface TuiContract {
|
|
9
|
+
steps: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface ActionResultEnvelope<T = unknown> {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
mode: RuntimeMode;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
data?: T;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ActionContract {
|
|
19
|
+
actionId: string;
|
|
20
|
+
description: string;
|
|
21
|
+
tui: TuiContract;
|
|
22
|
+
commandline: CommandlineContract;
|
|
23
|
+
help: HelpContract;
|
|
24
|
+
run: (context: RuntimeContext) => Promise<ActionResultEnvelope>;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface HelpArgumentSpec {
|
|
2
|
+
name: string;
|
|
3
|
+
required: boolean;
|
|
4
|
+
description: string;
|
|
5
|
+
example?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HelpUseCaseSpec {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
command: string;
|
|
11
|
+
}
|
|
12
|
+
export interface HelpContract {
|
|
13
|
+
summary: string;
|
|
14
|
+
args: HelpArgumentSpec[];
|
|
15
|
+
examples: string[];
|
|
16
|
+
useCases: HelpUseCaseSpec[];
|
|
17
|
+
keybindings: string[];
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare enum ExitCode {
|
|
2
|
+
SUCCESS = 0,
|
|
3
|
+
VALIDATION_ERROR = 2,
|
|
4
|
+
INTERRUPTED = 130,
|
|
5
|
+
INTERNAL_ERROR = 1
|
|
6
|
+
}
|
|
7
|
+
export declare enum FrameworkErrorCode {
|
|
8
|
+
TRIAD_INCOMPLETE = "TRIAD_INCOMPLETE",
|
|
9
|
+
DUPLICATE_ACTION = "DUPLICATE_ACTION",
|
|
10
|
+
DUPLICATE_MODULE = "DUPLICATE_MODULE",
|
|
11
|
+
MODULE_NOT_FOUND = "MODULE_NOT_FOUND",
|
|
12
|
+
ACTION_NOT_FOUND = "ACTION_NOT_FOUND",
|
|
13
|
+
INVALID_TRANSITION = "INVALID_TRANSITION"
|
|
14
|
+
}
|
|
15
|
+
export declare class FrameworkError extends Error {
|
|
16
|
+
readonly code: FrameworkErrorCode;
|
|
17
|
+
readonly exitCode: ExitCode;
|
|
18
|
+
constructor(code: FrameworkErrorCode, exitCode: ExitCode, message: string);
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export var ExitCode;
|
|
2
|
+
(function (ExitCode) {
|
|
3
|
+
ExitCode[ExitCode["SUCCESS"] = 0] = "SUCCESS";
|
|
4
|
+
ExitCode[ExitCode["VALIDATION_ERROR"] = 2] = "VALIDATION_ERROR";
|
|
5
|
+
ExitCode[ExitCode["INTERRUPTED"] = 130] = "INTERRUPTED";
|
|
6
|
+
ExitCode[ExitCode["INTERNAL_ERROR"] = 1] = "INTERNAL_ERROR";
|
|
7
|
+
})(ExitCode || (ExitCode = {}));
|
|
8
|
+
export var FrameworkErrorCode;
|
|
9
|
+
(function (FrameworkErrorCode) {
|
|
10
|
+
FrameworkErrorCode["TRIAD_INCOMPLETE"] = "TRIAD_INCOMPLETE";
|
|
11
|
+
FrameworkErrorCode["DUPLICATE_ACTION"] = "DUPLICATE_ACTION";
|
|
12
|
+
FrameworkErrorCode["DUPLICATE_MODULE"] = "DUPLICATE_MODULE";
|
|
13
|
+
FrameworkErrorCode["MODULE_NOT_FOUND"] = "MODULE_NOT_FOUND";
|
|
14
|
+
FrameworkErrorCode["ACTION_NOT_FOUND"] = "ACTION_NOT_FOUND";
|
|
15
|
+
FrameworkErrorCode["INVALID_TRANSITION"] = "INVALID_TRANSITION";
|
|
16
|
+
})(FrameworkErrorCode || (FrameworkErrorCode = {}));
|
|
17
|
+
export class FrameworkError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
exitCode;
|
|
20
|
+
constructor(code, exitCode, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.exitCode = exitCode;
|
|
24
|
+
this.name = 'FrameworkError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ActionContract } from '../contracts/action-contract.js';
|
|
2
|
+
import type { ModuleContract } from '../contracts/module-contract.js';
|
|
3
|
+
export interface ActionRef {
|
|
4
|
+
moduleId: string;
|
|
5
|
+
action: ActionContract;
|
|
6
|
+
}
|
|
7
|
+
export declare class ActionRegistry {
|
|
8
|
+
private readonly modules;
|
|
9
|
+
private readonly actions;
|
|
10
|
+
registerModule(moduleContract: ModuleContract): void;
|
|
11
|
+
listModules(): ModuleContract[];
|
|
12
|
+
getModule(moduleId: string): ModuleContract;
|
|
13
|
+
getAction(moduleId: string, actionId: string): ActionContract;
|
|
14
|
+
private actionKey;
|
|
15
|
+
private assertTriad;
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ExitCode, FrameworkError, FrameworkErrorCode } from '../errors/framework-errors.js';
|
|
2
|
+
export class ActionRegistry {
|
|
3
|
+
modules = new Map();
|
|
4
|
+
actions = new Map();
|
|
5
|
+
registerModule(moduleContract) {
|
|
6
|
+
if (this.modules.has(moduleContract.moduleId)) {
|
|
7
|
+
throw new FrameworkError(FrameworkErrorCode.DUPLICATE_MODULE, ExitCode.VALIDATION_ERROR, `Duplicate module registration: ${moduleContract.moduleId}`);
|
|
8
|
+
}
|
|
9
|
+
this.modules.set(moduleContract.moduleId, moduleContract);
|
|
10
|
+
for (const action of moduleContract.actions) {
|
|
11
|
+
this.assertTriad(moduleContract.moduleId, action);
|
|
12
|
+
const key = this.actionKey(moduleContract.moduleId, action.actionId);
|
|
13
|
+
if (this.actions.has(key)) {
|
|
14
|
+
throw new FrameworkError(FrameworkErrorCode.DUPLICATE_ACTION, ExitCode.VALIDATION_ERROR, `Duplicate action registration: ${key}`);
|
|
15
|
+
}
|
|
16
|
+
this.actions.set(key, { moduleId: moduleContract.moduleId, action });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
listModules() {
|
|
20
|
+
return [...this.modules.values()].sort((a, b) => a.moduleId.localeCompare(b.moduleId));
|
|
21
|
+
}
|
|
22
|
+
getModule(moduleId) {
|
|
23
|
+
const moduleContract = this.modules.get(moduleId);
|
|
24
|
+
if (!moduleContract) {
|
|
25
|
+
throw new FrameworkError(FrameworkErrorCode.MODULE_NOT_FOUND, ExitCode.VALIDATION_ERROR, `Module not found: ${moduleId}`);
|
|
26
|
+
}
|
|
27
|
+
return moduleContract;
|
|
28
|
+
}
|
|
29
|
+
getAction(moduleId, actionId) {
|
|
30
|
+
const key = this.actionKey(moduleId, actionId);
|
|
31
|
+
const actionRef = this.actions.get(key);
|
|
32
|
+
if (!actionRef) {
|
|
33
|
+
throw new FrameworkError(FrameworkErrorCode.ACTION_NOT_FOUND, ExitCode.VALIDATION_ERROR, `Action not found: ${key}`);
|
|
34
|
+
}
|
|
35
|
+
return actionRef.action;
|
|
36
|
+
}
|
|
37
|
+
actionKey(moduleId, actionId) {
|
|
38
|
+
return `${moduleId}.${actionId}`;
|
|
39
|
+
}
|
|
40
|
+
assertTriad(moduleId, action) {
|
|
41
|
+
const hasTui = Array.isArray(action.tui?.steps) && action.tui.steps.length > 0;
|
|
42
|
+
const hasCommandline = Array.isArray(action.commandline?.requiredArgs);
|
|
43
|
+
const hasHelp = typeof action.help?.summary === 'string' &&
|
|
44
|
+
Array.isArray(action.help?.args) &&
|
|
45
|
+
Array.isArray(action.help?.examples) &&
|
|
46
|
+
Array.isArray(action.help?.useCases) &&
|
|
47
|
+
Array.isArray(action.help?.keybindings);
|
|
48
|
+
if (!hasTui || !hasCommandline || !hasHelp) {
|
|
49
|
+
throw new FrameworkError(FrameworkErrorCode.TRIAD_INCOMPLETE, ExitCode.VALIDATION_ERROR, `Triad incomplete for action ${moduleId}.${action.actionId}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ActionResultEnvelope } from '../core/contracts/action-contract.js';
|
|
2
|
+
import type { ActionRegistry } from '../core/registry/action-registry.js';
|
|
3
|
+
export interface HelpCommandInput {
|
|
4
|
+
registry: ActionRegistry;
|
|
5
|
+
moduleId?: string;
|
|
6
|
+
actionId?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const runHelpCommand: (input: HelpCommandInput) => ActionResultEnvelope<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ExitCode } from '../core/errors/framework-errors.js';
|
|
2
|
+
import { resolveHelpHierarchy } from './hierarchy-resolver.js';
|
|
3
|
+
import { renderHelp } from './help-renderer.js';
|
|
4
|
+
export const runHelpCommand = (input) => {
|
|
5
|
+
const modules = input.registry.listModules();
|
|
6
|
+
const views = resolveHelpHierarchy(modules, input.moduleId, input.actionId);
|
|
7
|
+
if (views.length === 0) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
mode: 'commandline',
|
|
11
|
+
exitCode: ExitCode.VALIDATION_ERROR,
|
|
12
|
+
errorMessage: 'No help target found'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
mode: 'commandline',
|
|
18
|
+
exitCode: ExitCode.SUCCESS,
|
|
19
|
+
data: renderHelp(views)
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const renderSection = (section) => {
|
|
2
|
+
const header = `## ${section.title}`;
|
|
3
|
+
const lines = section.lines.length > 0 ? section.lines.map((line) => `- ${line}`).join('\n') : '- (none)';
|
|
4
|
+
return `${header}\n${lines}`;
|
|
5
|
+
};
|
|
6
|
+
export const renderHelp = (views) => views
|
|
7
|
+
.map((view) => {
|
|
8
|
+
const head = [
|
|
9
|
+
'# HELP',
|
|
10
|
+
`MODULE: ${view.moduleId}`,
|
|
11
|
+
`ACTION: ${view.actionId ?? '*'}`
|
|
12
|
+
].join('\n');
|
|
13
|
+
const body = view.sections.map(renderSection).join('\n\n');
|
|
14
|
+
return `${head}\n\n${body}`;
|
|
15
|
+
})
|
|
16
|
+
.join('\n\n---\n\n');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const resolveHelpHierarchy = (modules, moduleId, actionId) => {
|
|
2
|
+
const scopedModules = moduleId ? modules.filter((m) => m.moduleId === moduleId) : modules;
|
|
3
|
+
return scopedModules.flatMap((moduleContract) => {
|
|
4
|
+
if (!actionId) {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
moduleId: moduleContract.moduleId,
|
|
8
|
+
sections: [
|
|
9
|
+
{
|
|
10
|
+
title: 'MODULE',
|
|
11
|
+
lines: [moduleContract.description]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: 'ACTIONS',
|
|
15
|
+
lines: moduleContract.actions.map((action) => `${action.actionId} - ${action.description}`)
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
const action = moduleContract.actions.find((item) => item.actionId === actionId);
|
|
22
|
+
if (!action)
|
|
23
|
+
return [];
|
|
24
|
+
return [toActionHelp(moduleContract.moduleId, action)];
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const toActionHelp = (moduleId, action) => ({
|
|
28
|
+
moduleId,
|
|
29
|
+
actionId: action.actionId,
|
|
30
|
+
sections: [
|
|
31
|
+
{ title: 'SUMMARY', lines: [action.help.summary] },
|
|
32
|
+
{
|
|
33
|
+
title: 'ARGS',
|
|
34
|
+
lines: action.help.args.map((arg) => `${arg.required ? '*' : '-'} ${arg.name}: ${arg.description}`)
|
|
35
|
+
},
|
|
36
|
+
{ title: 'EXAMPLES', lines: action.help.examples },
|
|
37
|
+
{
|
|
38
|
+
title: 'USE-CASES',
|
|
39
|
+
lines: action.help.useCases.map((useCase) => `${useCase.name}: ${useCase.command}`)
|
|
40
|
+
},
|
|
41
|
+
{ title: 'KEYBINDINGS', lines: action.help.keybindings }
|
|
42
|
+
]
|
|
43
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ActionRegistry } from './core/registry/action-registry.js';
|
|
2
|
+
export const createRegistry = (modules) => {
|
|
3
|
+
const registry = new ActionRegistry();
|
|
4
|
+
for (const moduleContract of modules) {
|
|
5
|
+
registry.registerModule(moduleContract);
|
|
6
|
+
}
|
|
7
|
+
return registry;
|
|
8
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ExitCode } from '../../core/errors/framework-errors.js';
|
|
2
|
+
const toSlug = (value) => value
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.trim()
|
|
5
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
6
|
+
.replace(/\s+/g, '-')
|
|
7
|
+
.replace(/-+/g, '-');
|
|
8
|
+
const sampleContentModule = {
|
|
9
|
+
moduleId: 'content',
|
|
10
|
+
description: 'Content-processing sample actions for framework adoption.',
|
|
11
|
+
actions: [
|
|
12
|
+
{
|
|
13
|
+
actionId: 'slugify',
|
|
14
|
+
description: 'Convert text into URL-safe slug.',
|
|
15
|
+
tui: { steps: ['collect-text', 'preview-slug'] },
|
|
16
|
+
commandline: { requiredArgs: ['text'] },
|
|
17
|
+
help: {
|
|
18
|
+
summary: 'Convert input text into deterministic lowercase slug.',
|
|
19
|
+
args: [{ name: 'text', required: true, description: 'Source text to transform.', example: '--text="Hello World"' }],
|
|
20
|
+
examples: ['hub content slugify --text="Hello World"'],
|
|
21
|
+
useCases: [{ name: 'blog url', description: 'Generate post slug', command: 'hub content slugify --text="my post"' }],
|
|
22
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
23
|
+
},
|
|
24
|
+
run: async (context) => {
|
|
25
|
+
const text = typeof context.args.text === 'string' && context.args.text.length > 0
|
|
26
|
+
? context.args.text
|
|
27
|
+
: 'interactive text';
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
mode: context.mode,
|
|
31
|
+
exitCode: ExitCode.SUCCESS,
|
|
32
|
+
data: toSlug(text)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
actionId: 'word-count',
|
|
38
|
+
description: 'Count words from input text.',
|
|
39
|
+
tui: { steps: ['collect-text', 'show-count'] },
|
|
40
|
+
commandline: { requiredArgs: ['text'] },
|
|
41
|
+
help: {
|
|
42
|
+
summary: 'Count whitespace-separated words in deterministic way.',
|
|
43
|
+
args: [{ name: 'text', required: true, description: 'Input text for counting.', example: '--text="one two"' }],
|
|
44
|
+
examples: ['hub content word-count --text="one two three"'],
|
|
45
|
+
useCases: [{ name: 'draft checks', description: 'Validate article length quickly', command: 'hub content word-count --text="draft body"' }],
|
|
46
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
47
|
+
},
|
|
48
|
+
run: async (context) => {
|
|
49
|
+
const text = typeof context.args.text === 'string' ? context.args.text.trim() : '';
|
|
50
|
+
const words = text.length === 0 ? 0 : text.split(/\s+/).length;
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
mode: context.mode,
|
|
54
|
+
exitCode: ExitCode.SUCCESS,
|
|
55
|
+
data: String(words)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
export default sampleContentModule;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ExitCode } from '../../core/errors/framework-errors.js';
|
|
2
|
+
const sampleSystemModule = {
|
|
3
|
+
moduleId: 'system',
|
|
4
|
+
description: 'System utility sample actions for framework adoption.',
|
|
5
|
+
actions: [
|
|
6
|
+
{
|
|
7
|
+
actionId: 'env-check',
|
|
8
|
+
description: 'Read environment and report platform details.',
|
|
9
|
+
tui: { steps: ['select-keys', 'review-output'] },
|
|
10
|
+
commandline: { requiredArgs: ['key'] },
|
|
11
|
+
help: {
|
|
12
|
+
summary: 'Check environment variable and runtime platform.',
|
|
13
|
+
args: [
|
|
14
|
+
{
|
|
15
|
+
name: 'key',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'Environment key to inspect.',
|
|
18
|
+
example: '--key=HOME'
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
examples: ['hub system env-check --key=HOME'],
|
|
22
|
+
useCases: [
|
|
23
|
+
{
|
|
24
|
+
name: 'debug env',
|
|
25
|
+
description: 'Inspect required runtime variable',
|
|
26
|
+
command: 'hub system env-check --key=NODE_ENV'
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
30
|
+
},
|
|
31
|
+
run: async (context) => {
|
|
32
|
+
const key = typeof context.args.key === 'string' ? context.args.key : '';
|
|
33
|
+
const value = key ? process.env[key] ?? '' : '';
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
mode: context.mode,
|
|
37
|
+
exitCode: ExitCode.SUCCESS,
|
|
38
|
+
data: `${key}=${value}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
actionId: 'node-info',
|
|
44
|
+
description: 'Display Node and OS info.',
|
|
45
|
+
tui: { steps: ['collect-options', 'show-runtime'] },
|
|
46
|
+
commandline: { requiredArgs: [] },
|
|
47
|
+
help: {
|
|
48
|
+
summary: 'Print deterministic Node runtime and platform info.',
|
|
49
|
+
args: [],
|
|
50
|
+
examples: ['hub system node-info'],
|
|
51
|
+
useCases: [
|
|
52
|
+
{
|
|
53
|
+
name: 'verify runtime',
|
|
54
|
+
description: 'Check active node version and OS',
|
|
55
|
+
command: 'hub system node-info'
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
59
|
+
},
|
|
60
|
+
run: async (context) => {
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
mode: context.mode,
|
|
64
|
+
exitCode: ExitCode.SUCCESS,
|
|
65
|
+
data: `node=${process.version};platform=${process.platform}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
export default sampleSystemModule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ActionResultEnvelope } from '../core/contracts/action-contract.js';
|
|
2
|
+
import type { ActionRegistry } from '../core/registry/action-registry.js';
|
|
3
|
+
export interface DispatchInput {
|
|
4
|
+
registry: ActionRegistry;
|
|
5
|
+
moduleId: string;
|
|
6
|
+
actionId: string;
|
|
7
|
+
args: Record<string, string | boolean>;
|
|
8
|
+
isTTY: boolean;
|
|
9
|
+
resumeFilePath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const dispatchAction: (input: DispatchInput) => Promise<ActionResultEnvelope>;
|