@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,47 @@
1
+ import { ExitCode, FrameworkError } from '../core/errors/framework-errors.js';
2
+ import { executeAction } from './engine.js';
3
+ import { resolveRuntimeMode } from './mode-resolver.js';
4
+ import { FileResumeStore } from './resume-store.js';
5
+ export const dispatchAction = async (input) => {
6
+ try {
7
+ const action = input.registry.getAction(input.moduleId, input.actionId);
8
+ const mode = resolveRuntimeMode({
9
+ isTTY: input.isTTY,
10
+ providedArgs: input.args,
11
+ commandline: action.commandline
12
+ });
13
+ const missingRequired = action.commandline.requiredArgs.filter((arg) => input.args[arg] === undefined || input.args[arg] === '');
14
+ if (mode === 'commandline' && missingRequired.length > 0) {
15
+ return {
16
+ ok: false,
17
+ mode,
18
+ exitCode: ExitCode.VALIDATION_ERROR,
19
+ errorMessage: `Missing required args: ${missingRequired.join(', ')}`
20
+ };
21
+ }
22
+ const resumeStore = input.resumeFilePath ? new FileResumeStore(input.resumeFilePath) : undefined;
23
+ return executeAction({
24
+ moduleId: input.moduleId,
25
+ action,
26
+ mode,
27
+ args: input.args,
28
+ resumeStore
29
+ });
30
+ }
31
+ catch (error) {
32
+ if (error instanceof FrameworkError) {
33
+ return {
34
+ ok: false,
35
+ mode: 'commandline',
36
+ exitCode: error.exitCode,
37
+ errorMessage: error.message
38
+ };
39
+ }
40
+ return {
41
+ ok: false,
42
+ mode: 'commandline',
43
+ exitCode: ExitCode.INTERNAL_ERROR,
44
+ errorMessage: error instanceof Error ? error.message : 'Unknown dispatch error'
45
+ };
46
+ }
47
+ };
@@ -0,0 +1,13 @@
1
+ import type { ActionContract, ActionResultEnvelope, RuntimeMode } from '../core/contracts/action-contract.js';
2
+ import { ResumeStore } from './resume-store.js';
3
+ import { type WorkflowNode } from './state-machine.js';
4
+ export interface EngineInput {
5
+ moduleId: string;
6
+ action: ActionContract;
7
+ mode: RuntimeMode;
8
+ args: Record<string, string | boolean>;
9
+ workflowId?: string;
10
+ workflowNodes?: WorkflowNode[];
11
+ resumeStore?: ResumeStore;
12
+ }
13
+ export declare const executeAction: (input: EngineInput) => Promise<ActionResultEnvelope>;
@@ -0,0 +1,44 @@
1
+ import { ExitCode } from '../core/errors/framework-errors.js';
2
+ import { StateMachine } from './state-machine.js';
3
+ export const executeAction = async (input) => {
4
+ const nodes = input.workflowNodes ?? input.action.tui.steps.map((step) => ({ id: step, label: step }));
5
+ const workflowId = input.workflowId ?? `${input.moduleId}.${input.action.actionId}`;
6
+ let initialNodeId;
7
+ if (input.resumeStore) {
8
+ const checkpoint = await input.resumeStore.load();
9
+ if (checkpoint?.workflowId === workflowId) {
10
+ initialNodeId = checkpoint.nodeId;
11
+ }
12
+ }
13
+ const stateMachine = new StateMachine(workflowId, nodes, initialNodeId);
14
+ const context = {
15
+ moduleId: input.moduleId,
16
+ actionId: input.action.actionId,
17
+ mode: input.mode,
18
+ args: input.args,
19
+ stateMachine
20
+ };
21
+ try {
22
+ const result = await input.action.run(context);
23
+ if (input.resumeStore) {
24
+ if (stateMachine.snapshot().exited || result.ok) {
25
+ await input.resumeStore.clear();
26
+ }
27
+ else {
28
+ await input.resumeStore.save(stateMachine.checkpoint());
29
+ }
30
+ }
31
+ return { ...result, mode: input.mode, exitCode: result.exitCode ?? ExitCode.SUCCESS };
32
+ }
33
+ catch (error) {
34
+ if (input.resumeStore) {
35
+ await input.resumeStore.save(stateMachine.checkpoint());
36
+ }
37
+ return {
38
+ ok: false,
39
+ mode: input.mode,
40
+ exitCode: ExitCode.INTERNAL_ERROR,
41
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
42
+ };
43
+ }
44
+ };
@@ -0,0 +1,8 @@
1
+ import type { CommandlineContract, RuntimeMode } from '../core/contracts/action-contract.js';
2
+ export interface RuntimeResolutionInput {
3
+ isTTY: boolean;
4
+ providedArgs: Record<string, string | boolean>;
5
+ commandline: CommandlineContract;
6
+ }
7
+ export declare const hasRequiredArgs: (requiredArgs: string[], providedArgs: Record<string, string | boolean>) => boolean;
8
+ export declare const resolveRuntimeMode: (input: RuntimeResolutionInput) => RuntimeMode;
@@ -0,0 +1,8 @@
1
+ export const hasRequiredArgs = (requiredArgs, providedArgs) => requiredArgs.every((arg) => providedArgs[arg] !== undefined && providedArgs[arg] !== '');
2
+ export const resolveRuntimeMode = (input) => {
3
+ if (!input.isTTY)
4
+ return 'commandline';
5
+ if (hasRequiredArgs(input.commandline.requiredArgs, input.providedArgs))
6
+ return 'commandline';
7
+ return 'tui';
8
+ };
@@ -0,0 +1,13 @@
1
+ import type { WorkflowCheckpoint } from './state-machine.js';
2
+ export interface ResumeStore {
3
+ load: () => Promise<WorkflowCheckpoint | undefined>;
4
+ save: (checkpoint: WorkflowCheckpoint) => Promise<void>;
5
+ clear: () => Promise<void>;
6
+ }
7
+ export declare class FileResumeStore implements ResumeStore {
8
+ private readonly filePath;
9
+ constructor(filePath: string);
10
+ load(): Promise<WorkflowCheckpoint | undefined>;
11
+ save(checkpoint: WorkflowCheckpoint): Promise<void>;
12
+ clear(): Promise<void>;
13
+ }
@@ -0,0 +1,45 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ export class FileResumeStore {
4
+ filePath;
5
+ constructor(filePath) {
6
+ this.filePath = filePath;
7
+ }
8
+ async load() {
9
+ let raw;
10
+ try {
11
+ raw = await fs.readFile(this.filePath, 'utf8');
12
+ }
13
+ catch (error) {
14
+ if (isIgnorableReadError(error)) {
15
+ return undefined;
16
+ }
17
+ throw error;
18
+ }
19
+ try {
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ async save(checkpoint) {
27
+ await fs.mkdir(dirname(this.filePath), { recursive: true });
28
+ await fs.writeFile(this.filePath, JSON.stringify(checkpoint), 'utf8');
29
+ }
30
+ async clear() {
31
+ try {
32
+ await fs.unlink(this.filePath);
33
+ }
34
+ catch (error) {
35
+ if (isIgnorableReadError(error)) {
36
+ return;
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+ const isIgnorableReadError = (error) => typeof error === 'object' &&
43
+ error !== null &&
44
+ 'code' in error &&
45
+ (error.code === 'ENOENT');
@@ -0,0 +1,9 @@
1
+ import type { RuntimeMode } from '../core/contracts/action-contract.js';
2
+ import type { StateMachine } from './state-machine.js';
3
+ export interface RuntimeContext {
4
+ moduleId: string;
5
+ actionId: string;
6
+ mode: RuntimeMode;
7
+ args: Record<string, string | boolean>;
8
+ stateMachine?: StateMachine;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ export type StateTransitionType = 'next' | 'back' | 'jump' | 'exit';
2
+ export interface WorkflowNode {
3
+ id: string;
4
+ label: string;
5
+ }
6
+ export interface WorkflowCheckpoint {
7
+ workflowId: string;
8
+ nodeId: string;
9
+ cursor: number;
10
+ timestamp: string;
11
+ }
12
+ export interface StateTransition {
13
+ type: StateTransitionType;
14
+ targetNodeId?: string;
15
+ }
16
+ export interface StateMachineSnapshot {
17
+ workflowId: string;
18
+ nodes: WorkflowNode[];
19
+ cursor: number;
20
+ exited: boolean;
21
+ }
22
+ export declare class StateMachine {
23
+ private readonly workflowId;
24
+ private readonly nodes;
25
+ private cursor;
26
+ private exited;
27
+ constructor(workflowId: string, nodes: WorkflowNode[], initialNodeId?: string);
28
+ transition(transition: StateTransition): StateMachineSnapshot;
29
+ currentNode(): WorkflowNode;
30
+ checkpoint(): WorkflowCheckpoint;
31
+ snapshot(): StateMachineSnapshot;
32
+ }
@@ -0,0 +1,50 @@
1
+ export class StateMachine {
2
+ workflowId;
3
+ nodes;
4
+ cursor = 0;
5
+ exited = false;
6
+ constructor(workflowId, nodes, initialNodeId) {
7
+ this.workflowId = workflowId;
8
+ this.nodes = nodes;
9
+ if (initialNodeId) {
10
+ const index = nodes.findIndex((node) => node.id === initialNodeId);
11
+ if (index >= 0)
12
+ this.cursor = index;
13
+ }
14
+ }
15
+ transition(transition) {
16
+ if (this.exited)
17
+ return this.snapshot();
18
+ if (transition.type === 'next')
19
+ this.cursor = Math.min(this.cursor + 1, this.nodes.length - 1);
20
+ if (transition.type === 'back')
21
+ this.cursor = Math.max(this.cursor - 1, 0);
22
+ if (transition.type === 'jump' && transition.targetNodeId) {
23
+ const index = this.nodes.findIndex((node) => node.id === transition.targetNodeId);
24
+ if (index >= 0)
25
+ this.cursor = index;
26
+ }
27
+ if (transition.type === 'exit')
28
+ this.exited = true;
29
+ return this.snapshot();
30
+ }
31
+ currentNode() {
32
+ return this.nodes[this.cursor];
33
+ }
34
+ checkpoint() {
35
+ return {
36
+ workflowId: this.workflowId,
37
+ nodeId: this.currentNode().id,
38
+ cursor: this.cursor,
39
+ timestamp: new Date().toISOString()
40
+ };
41
+ }
42
+ snapshot() {
43
+ return {
44
+ workflowId: this.workflowId,
45
+ nodes: this.nodes,
46
+ cursor: this.cursor,
47
+ exited: this.exited
48
+ };
49
+ }
50
+ }
@@ -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[];