@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,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;
@@ -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>;
@@ -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,7 @@
1
+ export interface AccessibilityFooterInput {
2
+ moduleId: string;
3
+ actionId: string;
4
+ nodeId: string;
5
+ keybindings: string[];
6
+ }
7
+ export declare const renderAccessibilityFooter: (input: AccessibilityFooterInput) => string;
@@ -0,0 +1,4 @@
1
+ export const renderAccessibilityFooter = (input) => {
2
+ const keys = input.keybindings.join(' | ');
3
+ return `[ctx module=${input.moduleId} action=${input.actionId} node=${input.nodeId}] [keys ${keys}]`;
4
+ };
@@ -0,0 +1,5 @@
1
+ export interface ConfirmPromptInput {
2
+ message: string;
3
+ initialValue?: boolean;
4
+ }
5
+ export declare const runConfirmPrompt: (input: ConfirmPromptInput) => Promise<boolean>;
@@ -0,0 +1,3 @@
1
+ export const runConfirmPrompt = async (input) => {
2
+ return input.initialValue ?? true;
3
+ };
@@ -0,0 +1,5 @@
1
+ export interface GroupStep<T = unknown> {
2
+ key: string;
3
+ run: () => Promise<T>;
4
+ }
5
+ export declare const runGroupPrompt: <T = unknown>(steps: GroupStep<T>[]) => Promise<Record<string, T>>;
@@ -0,0 +1,7 @@
1
+ export const runGroupPrompt = async (steps) => {
2
+ const result = {};
3
+ for (const step of steps) {
4
+ result[step.key] = await step.run();
5
+ }
6
+ return result;
7
+ };
@@ -0,0 +1,10 @@
1
+ export interface MultiSelectOption {
2
+ value: string;
3
+ label: string;
4
+ }
5
+ export interface MultiSelectPromptInput {
6
+ message: string;
7
+ options: MultiSelectOption[];
8
+ initialValues?: string[];
9
+ }
10
+ export declare const runMultiSelectPrompt: (input: MultiSelectPromptInput) => Promise<string[]>;
@@ -0,0 +1,9 @@
1
+ export const runMultiSelectPrompt = async (input) => {
2
+ if (input.options.length === 0) {
3
+ throw new Error(`No options available for multiselect prompt: ${input.message}`);
4
+ }
5
+ if (input.initialValues && input.initialValues.length > 0) {
6
+ return input.initialValues.filter((value) => input.options.some((option) => option.value === value));
7
+ }
8
+ return [input.options[0].value];
9
+ };
@@ -0,0 +1,10 @@
1
+ export interface SelectOption {
2
+ value: string;
3
+ label: string;
4
+ }
5
+ export interface SelectPromptInput {
6
+ message: string;
7
+ options: SelectOption[];
8
+ initialValue?: string;
9
+ }
10
+ export declare const runSelectPrompt: (input: SelectPromptInput) => Promise<string>;
@@ -0,0 +1,7 @@
1
+ export const runSelectPrompt = async (input) => {
2
+ const selected = input.initialValue ?? input.options[0]?.value;
3
+ if (!selected) {
4
+ throw new Error(`No options available for select prompt: ${input.message}`);
5
+ }
6
+ return selected;
7
+ };
@@ -0,0 +1,6 @@
1
+ export interface TextPromptInput {
2
+ message: string;
3
+ initialValue?: string;
4
+ required?: boolean;
5
+ }
6
+ export declare const runTextPrompt: (input: TextPromptInput) => Promise<string>;
@@ -0,0 +1,7 @@
1
+ export const runTextPrompt = async (input) => {
2
+ const value = input.initialValue ?? '';
3
+ if (input.required && value.trim().length === 0) {
4
+ throw new Error(`Required text value missing: ${input.message}`);
5
+ }
6
+ return value;
7
+ };
@@ -0,0 +1,7 @@
1
+ import { ExitCode } from '../core/errors/framework-errors.js';
2
+ export type InterruptSignal = 'SIGINT' | 'ESC' | 'TIMEOUT';
3
+ export interface InterruptResult {
4
+ exitCode: ExitCode;
5
+ reason: string;
6
+ }
7
+ export declare const handleInterrupt: (signal: InterruptSignal) => InterruptResult;
@@ -0,0 +1,7 @@
1
+ import { ExitCode } from '../core/errors/framework-errors.js';
2
+ export const handleInterrupt = (signal) => {
3
+ if (signal === 'TIMEOUT') {
4
+ return { exitCode: ExitCode.VALIDATION_ERROR, reason: 'Workflow timed out' };
5
+ }
6
+ return { exitCode: ExitCode.INTERRUPTED, reason: `Interrupted by ${signal}` };
7
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { resolve } from 'node:path';
4
+ describe('e2e cli smoke', () => {
5
+ it('prints help output from cli entry', () => {
6
+ const projectRoot = resolve(__dirname, '../..');
7
+ const tsxCli = resolve(projectRoot, 'node_modules/tsx/dist/cli.mjs');
8
+ const cliEntry = resolve(projectRoot, 'src/cli-entry.ts');
9
+ const result = spawnSync(process.execPath, [tsxCli, cliEntry, '-h'], {
10
+ cwd: projectRoot,
11
+ encoding: 'utf8'
12
+ });
13
+ expect(result.status).toBe(0);
14
+ expect(result.stdout).toContain('# HELP');
15
+ });
16
+ });