@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.
Files changed (168) hide show
  1. package/.github/workflows/ci.yml +26 -0
  2. package/README.md +49 -0
  3. package/dist/cli/help-command.d.ts +1 -0
  4. package/dist/cli/help-command.js +1 -0
  5. package/dist/cli-entry.d.ts +1 -0
  6. package/dist/cli-entry.js +80 -0
  7. package/dist/core/contracts/action-contract.d.ts +25 -0
  8. package/dist/core/contracts/action-contract.js +1 -0
  9. package/dist/core/contracts/help-contract.d.ts +18 -0
  10. package/dist/core/contracts/help-contract.js +1 -0
  11. package/dist/core/contracts/module-contract.d.ts +6 -0
  12. package/dist/core/contracts/module-contract.js +1 -0
  13. package/dist/core/errors/framework-errors.d.ts +19 -0
  14. package/dist/core/errors/framework-errors.js +26 -0
  15. package/dist/core/registry/action-registry.d.ts +16 -0
  16. package/dist/core/registry/action-registry.js +52 -0
  17. package/dist/help/help-command.d.ts +8 -0
  18. package/dist/help/help-command.js +21 -0
  19. package/dist/help/help-model.d.ts +9 -0
  20. package/dist/help/help-model.js +1 -0
  21. package/dist/help/help-renderer.d.ts +2 -0
  22. package/dist/help/help-renderer.js +16 -0
  23. package/dist/help/hierarchy-resolver.d.ts +3 -0
  24. package/dist/help/hierarchy-resolver.js +43 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +8 -0
  27. package/dist/modules/sample-content/module.d.ts +3 -0
  28. package/dist/modules/sample-content/module.js +61 -0
  29. package/dist/modules/sample-system/module.d.ts +3 -0
  30. package/dist/modules/sample-system/module.js +71 -0
  31. package/dist/runtime/dispatch.d.ts +11 -0
  32. package/dist/runtime/dispatch.js +47 -0
  33. package/dist/runtime/engine.d.ts +13 -0
  34. package/dist/runtime/engine.js +44 -0
  35. package/dist/runtime/mode-resolver.d.ts +8 -0
  36. package/dist/runtime/mode-resolver.js +8 -0
  37. package/dist/runtime/resume-store.d.ts +13 -0
  38. package/dist/runtime/resume-store.js +45 -0
  39. package/dist/runtime/runtime-context.d.ts +9 -0
  40. package/dist/runtime/runtime-context.js +1 -0
  41. package/dist/runtime/state-machine.d.ts +32 -0
  42. package/dist/runtime/state-machine.js +50 -0
  43. package/dist/src/cli/help-command.d.ts +1 -0
  44. package/dist/src/cli/help-command.js +1 -0
  45. package/dist/src/cli-entry.d.ts +1 -0
  46. package/dist/src/cli-entry.js +80 -0
  47. package/dist/src/core/contracts/action-contract.d.ts +25 -0
  48. package/dist/src/core/contracts/action-contract.js +1 -0
  49. package/dist/src/core/contracts/help-contract.d.ts +18 -0
  50. package/dist/src/core/contracts/help-contract.js +1 -0
  51. package/dist/src/core/contracts/module-contract.d.ts +6 -0
  52. package/dist/src/core/contracts/module-contract.js +1 -0
  53. package/dist/src/core/errors/framework-errors.d.ts +19 -0
  54. package/dist/src/core/errors/framework-errors.js +26 -0
  55. package/dist/src/core/registry/action-registry.d.ts +16 -0
  56. package/dist/src/core/registry/action-registry.js +52 -0
  57. package/dist/src/help/help-command.d.ts +8 -0
  58. package/dist/src/help/help-command.js +21 -0
  59. package/dist/src/help/help-model.d.ts +9 -0
  60. package/dist/src/help/help-model.js +1 -0
  61. package/dist/src/help/help-renderer.d.ts +2 -0
  62. package/dist/src/help/help-renderer.js +16 -0
  63. package/dist/src/help/hierarchy-resolver.d.ts +3 -0
  64. package/dist/src/help/hierarchy-resolver.js +43 -0
  65. package/dist/src/index.d.ts +3 -0
  66. package/dist/src/index.js +8 -0
  67. package/dist/src/modules/sample-content/module.d.ts +3 -0
  68. package/dist/src/modules/sample-content/module.js +61 -0
  69. package/dist/src/modules/sample-system/module.d.ts +3 -0
  70. package/dist/src/modules/sample-system/module.js +71 -0
  71. package/dist/src/runtime/dispatch.d.ts +11 -0
  72. package/dist/src/runtime/dispatch.js +47 -0
  73. package/dist/src/runtime/engine.d.ts +13 -0
  74. package/dist/src/runtime/engine.js +44 -0
  75. package/dist/src/runtime/mode-resolver.d.ts +8 -0
  76. package/dist/src/runtime/mode-resolver.js +8 -0
  77. package/dist/src/runtime/resume-store.d.ts +13 -0
  78. package/dist/src/runtime/resume-store.js +45 -0
  79. package/dist/src/runtime/runtime-context.d.ts +9 -0
  80. package/dist/src/runtime/runtime-context.js +1 -0
  81. package/dist/src/runtime/state-machine.d.ts +32 -0
  82. package/dist/src/runtime/state-machine.js +50 -0
  83. package/dist/src/tui/accessibility-footer.d.ts +7 -0
  84. package/dist/src/tui/accessibility-footer.js +4 -0
  85. package/dist/src/tui/component-adapters/confirm.d.ts +5 -0
  86. package/dist/src/tui/component-adapters/confirm.js +3 -0
  87. package/dist/src/tui/component-adapters/group.d.ts +5 -0
  88. package/dist/src/tui/component-adapters/group.js +7 -0
  89. package/dist/src/tui/component-adapters/multiselect.d.ts +10 -0
  90. package/dist/src/tui/component-adapters/multiselect.js +9 -0
  91. package/dist/src/tui/component-adapters/select.d.ts +10 -0
  92. package/dist/src/tui/component-adapters/select.js +7 -0
  93. package/dist/src/tui/component-adapters/text.d.ts +6 -0
  94. package/dist/src/tui/component-adapters/text.js +7 -0
  95. package/dist/src/tui/interrupt-handlers.d.ts +7 -0
  96. package/dist/src/tui/interrupt-handlers.js +7 -0
  97. package/dist/tests/e2e/cli-smoke.e2e.test.d.ts +1 -0
  98. package/dist/tests/e2e/cli-smoke.e2e.test.js +16 -0
  99. package/dist/tests/integration/runtime-dispatch.integration.test.d.ts +1 -0
  100. package/dist/tests/integration/runtime-dispatch.integration.test.js +20 -0
  101. package/dist/tests/transcript/help.transcript.test.d.ts +1 -0
  102. package/dist/tests/transcript/help.transcript.test.js +18 -0
  103. package/dist/tests/unit/state-machine.test.d.ts +1 -0
  104. package/dist/tests/unit/state-machine.test.js +20 -0
  105. package/dist/tui/accessibility-footer.d.ts +7 -0
  106. package/dist/tui/accessibility-footer.js +4 -0
  107. package/dist/tui/component-adapters/confirm.d.ts +5 -0
  108. package/dist/tui/component-adapters/confirm.js +3 -0
  109. package/dist/tui/component-adapters/group.d.ts +5 -0
  110. package/dist/tui/component-adapters/group.js +7 -0
  111. package/dist/tui/component-adapters/multiselect.d.ts +10 -0
  112. package/dist/tui/component-adapters/multiselect.js +9 -0
  113. package/dist/tui/component-adapters/select.d.ts +10 -0
  114. package/dist/tui/component-adapters/select.js +7 -0
  115. package/dist/tui/component-adapters/text.d.ts +6 -0
  116. package/dist/tui/component-adapters/text.js +7 -0
  117. package/dist/tui/interrupt-handlers.d.ts +7 -0
  118. package/dist/tui/interrupt-handlers.js +7 -0
  119. package/dist/vitest.config.d.ts +2 -0
  120. package/dist/vitest.config.js +7 -0
  121. package/docs/help-contract-spec.md +29 -0
  122. package/docs/module-authoring-guide.md +52 -0
  123. package/package.json +20 -0
  124. package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +71 -0
  125. package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +76 -0
  126. package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +71 -0
  127. package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +69 -0
  128. package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +79 -0
  129. package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +75 -0
  130. package/plans/260209-1547-hub-dual-runtime-framework/plan.md +105 -0
  131. package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +27 -0
  132. package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +166 -0
  133. package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +87 -0
  134. package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +24 -0
  135. package/src/cli/help-command.ts +1 -0
  136. package/src/cli-entry.ts +83 -0
  137. package/src/core/contracts/action-contract.ts +30 -0
  138. package/src/core/contracts/help-contract.ts +20 -0
  139. package/src/core/contracts/module-contract.ts +7 -0
  140. package/src/core/errors/framework-errors.ts +26 -0
  141. package/src/core/registry/action-registry.ts +94 -0
  142. package/src/help/help-command.ts +32 -0
  143. package/src/help/help-model.ts +10 -0
  144. package/src/help/help-renderer.ts +21 -0
  145. package/src/help/hierarchy-resolver.ts +54 -0
  146. package/src/index.ts +10 -0
  147. package/src/modules/sample-content/module.ts +66 -0
  148. package/src/modules/sample-system/module.ts +74 -0
  149. package/src/runtime/dispatch.ts +64 -0
  150. package/src/runtime/engine.ts +59 -0
  151. package/src/runtime/mode-resolver.ts +18 -0
  152. package/src/runtime/resume-store.ts +53 -0
  153. package/src/runtime/runtime-context.ts +10 -0
  154. package/src/runtime/state-machine.ts +77 -0
  155. package/src/tui/accessibility-footer.ts +11 -0
  156. package/src/tui/component-adapters/confirm.ts +8 -0
  157. package/src/tui/component-adapters/group.ts +12 -0
  158. package/src/tui/component-adapters/multiselect.ts +22 -0
  159. package/src/tui/component-adapters/select.ts +18 -0
  160. package/src/tui/component-adapters/text.ts +13 -0
  161. package/src/tui/interrupt-handlers.ts +15 -0
  162. package/tests/e2e/cli-smoke.e2e.test.ts +19 -0
  163. package/tests/integration/runtime-dispatch.integration.test.ts +23 -0
  164. package/tests/transcript/help.transcript.test.ts +20 -0
  165. package/tests/unit/state-machine.test.ts +22 -0
  166. package/tsconfig.build.json +9 -0
  167. package/tsconfig.json +17 -0
  168. 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,6 @@
1
+ import type { ActionContract } from './action-contract.js';
2
+ export interface ModuleContract {
3
+ moduleId: string;
4
+ description: string;
5
+ actions: ActionContract[];
6
+ }
@@ -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,9 @@
1
+ export interface HelpSection {
2
+ title: 'MODULE' | 'ACTIONS' | 'SUMMARY' | 'ARGS' | 'EXAMPLES' | 'USE-CASES' | 'KEYBINDINGS';
3
+ lines: string[];
4
+ }
5
+ export interface ActionHelpView {
6
+ moduleId: string;
7
+ actionId?: string;
8
+ sections: HelpSection[];
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ActionHelpView } from './help-model.js';
2
+ export declare const renderHelp: (views: ActionHelpView[]) => string;
@@ -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,3 @@
1
+ import type { ModuleContract } from '../core/contracts/module-contract.js';
2
+ import type { ActionHelpView } from './help-model.js';
3
+ export declare const resolveHelpHierarchy: (modules: ModuleContract[], moduleId?: string, actionId?: string) => ActionHelpView[];
@@ -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
+ });
@@ -0,0 +1,3 @@
1
+ import { ActionRegistry } from './core/registry/action-registry.js';
2
+ import type { ModuleContract } from './core/contracts/module-contract.js';
3
+ export declare const createRegistry: (modules: ModuleContract[]) => ActionRegistry;
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,3 @@
1
+ import type { ModuleContract } from '../../core/contracts/module-contract.js';
2
+ declare const sampleContentModule: ModuleContract;
3
+ export default sampleContentModule;
@@ -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,3 @@
1
+ import type { ModuleContract } from '../../core/contracts/module-contract.js';
2
+ declare const sampleSystemModule: ModuleContract;
3
+ export default sampleSystemModule;
@@ -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>;