@levu/snap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +26 -0
- package/README.md +49 -0
- package/dist/cli/help-command.d.ts +1 -0
- package/dist/cli/help-command.js +1 -0
- package/dist/cli-entry.d.ts +1 -0
- package/dist/cli-entry.js +80 -0
- package/dist/core/contracts/action-contract.d.ts +25 -0
- package/dist/core/contracts/action-contract.js +1 -0
- package/dist/core/contracts/help-contract.d.ts +18 -0
- package/dist/core/contracts/help-contract.js +1 -0
- package/dist/core/contracts/module-contract.d.ts +6 -0
- package/dist/core/contracts/module-contract.js +1 -0
- package/dist/core/errors/framework-errors.d.ts +19 -0
- package/dist/core/errors/framework-errors.js +26 -0
- package/dist/core/registry/action-registry.d.ts +16 -0
- package/dist/core/registry/action-registry.js +52 -0
- package/dist/help/help-command.d.ts +8 -0
- package/dist/help/help-command.js +21 -0
- package/dist/help/help-model.d.ts +9 -0
- package/dist/help/help-model.js +1 -0
- package/dist/help/help-renderer.d.ts +2 -0
- package/dist/help/help-renderer.js +16 -0
- package/dist/help/hierarchy-resolver.d.ts +3 -0
- package/dist/help/hierarchy-resolver.js +43 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/modules/sample-content/module.d.ts +3 -0
- package/dist/modules/sample-content/module.js +61 -0
- package/dist/modules/sample-system/module.d.ts +3 -0
- package/dist/modules/sample-system/module.js +71 -0
- package/dist/runtime/dispatch.d.ts +11 -0
- package/dist/runtime/dispatch.js +47 -0
- package/dist/runtime/engine.d.ts +13 -0
- package/dist/runtime/engine.js +44 -0
- package/dist/runtime/mode-resolver.d.ts +8 -0
- package/dist/runtime/mode-resolver.js +8 -0
- package/dist/runtime/resume-store.d.ts +13 -0
- package/dist/runtime/resume-store.js +45 -0
- package/dist/runtime/runtime-context.d.ts +9 -0
- package/dist/runtime/runtime-context.js +1 -0
- package/dist/runtime/state-machine.d.ts +32 -0
- package/dist/runtime/state-machine.js +50 -0
- package/dist/src/cli/help-command.d.ts +1 -0
- package/dist/src/cli/help-command.js +1 -0
- package/dist/src/cli-entry.d.ts +1 -0
- package/dist/src/cli-entry.js +80 -0
- package/dist/src/core/contracts/action-contract.d.ts +25 -0
- package/dist/src/core/contracts/action-contract.js +1 -0
- package/dist/src/core/contracts/help-contract.d.ts +18 -0
- package/dist/src/core/contracts/help-contract.js +1 -0
- package/dist/src/core/contracts/module-contract.d.ts +6 -0
- package/dist/src/core/contracts/module-contract.js +1 -0
- package/dist/src/core/errors/framework-errors.d.ts +19 -0
- package/dist/src/core/errors/framework-errors.js +26 -0
- package/dist/src/core/registry/action-registry.d.ts +16 -0
- package/dist/src/core/registry/action-registry.js +52 -0
- package/dist/src/help/help-command.d.ts +8 -0
- package/dist/src/help/help-command.js +21 -0
- package/dist/src/help/help-model.d.ts +9 -0
- package/dist/src/help/help-model.js +1 -0
- package/dist/src/help/help-renderer.d.ts +2 -0
- package/dist/src/help/help-renderer.js +16 -0
- package/dist/src/help/hierarchy-resolver.d.ts +3 -0
- package/dist/src/help/hierarchy-resolver.js +43 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +8 -0
- package/dist/src/modules/sample-content/module.d.ts +3 -0
- package/dist/src/modules/sample-content/module.js +61 -0
- package/dist/src/modules/sample-system/module.d.ts +3 -0
- package/dist/src/modules/sample-system/module.js +71 -0
- package/dist/src/runtime/dispatch.d.ts +11 -0
- package/dist/src/runtime/dispatch.js +47 -0
- package/dist/src/runtime/engine.d.ts +13 -0
- package/dist/src/runtime/engine.js +44 -0
- package/dist/src/runtime/mode-resolver.d.ts +8 -0
- package/dist/src/runtime/mode-resolver.js +8 -0
- package/dist/src/runtime/resume-store.d.ts +13 -0
- package/dist/src/runtime/resume-store.js +45 -0
- package/dist/src/runtime/runtime-context.d.ts +9 -0
- package/dist/src/runtime/runtime-context.js +1 -0
- package/dist/src/runtime/state-machine.d.ts +32 -0
- package/dist/src/runtime/state-machine.js +50 -0
- package/dist/src/tui/accessibility-footer.d.ts +7 -0
- package/dist/src/tui/accessibility-footer.js +4 -0
- package/dist/src/tui/component-adapters/confirm.d.ts +5 -0
- package/dist/src/tui/component-adapters/confirm.js +3 -0
- package/dist/src/tui/component-adapters/group.d.ts +5 -0
- package/dist/src/tui/component-adapters/group.js +7 -0
- package/dist/src/tui/component-adapters/multiselect.d.ts +10 -0
- package/dist/src/tui/component-adapters/multiselect.js +9 -0
- package/dist/src/tui/component-adapters/select.d.ts +10 -0
- package/dist/src/tui/component-adapters/select.js +7 -0
- package/dist/src/tui/component-adapters/text.d.ts +6 -0
- package/dist/src/tui/component-adapters/text.js +7 -0
- package/dist/src/tui/interrupt-handlers.d.ts +7 -0
- package/dist/src/tui/interrupt-handlers.js +7 -0
- package/dist/tests/e2e/cli-smoke.e2e.test.d.ts +1 -0
- package/dist/tests/e2e/cli-smoke.e2e.test.js +16 -0
- package/dist/tests/integration/runtime-dispatch.integration.test.d.ts +1 -0
- package/dist/tests/integration/runtime-dispatch.integration.test.js +20 -0
- package/dist/tests/transcript/help.transcript.test.d.ts +1 -0
- package/dist/tests/transcript/help.transcript.test.js +18 -0
- package/dist/tests/unit/state-machine.test.d.ts +1 -0
- package/dist/tests/unit/state-machine.test.js +20 -0
- package/dist/tui/accessibility-footer.d.ts +7 -0
- package/dist/tui/accessibility-footer.js +4 -0
- package/dist/tui/component-adapters/confirm.d.ts +5 -0
- package/dist/tui/component-adapters/confirm.js +3 -0
- package/dist/tui/component-adapters/group.d.ts +5 -0
- package/dist/tui/component-adapters/group.js +7 -0
- package/dist/tui/component-adapters/multiselect.d.ts +10 -0
- package/dist/tui/component-adapters/multiselect.js +9 -0
- package/dist/tui/component-adapters/select.d.ts +10 -0
- package/dist/tui/component-adapters/select.js +7 -0
- package/dist/tui/component-adapters/text.d.ts +6 -0
- package/dist/tui/component-adapters/text.js +7 -0
- package/dist/tui/interrupt-handlers.d.ts +7 -0
- package/dist/tui/interrupt-handlers.js +7 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +7 -0
- package/docs/help-contract-spec.md +29 -0
- package/docs/module-authoring-guide.md +52 -0
- package/package.json +20 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-01-foundation-and-contracts.md +71 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-02-runtime-and-state-machine.md +76 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-03-tui-components-and-policies.md +71 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-04-help-system-and-ai-readability.md +69 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-05-testing-and-quality-gates.md +79 -0
- package/plans/260209-1547-hub-dual-runtime-framework/phase-06-sample-modules-and-adoption.md +75 -0
- package/plans/260209-1547-hub-dual-runtime-framework/plan.md +105 -0
- package/plans/260209-1547-hub-dual-runtime-framework/reports/planner-report.md +27 -0
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-01-report.md +166 -0
- package/plans/260209-1547-hub-dual-runtime-framework/research/researcher-02-report.md +87 -0
- package/plans/260209-1547-hub-dual-runtime-framework/scout/scout-01-report.md +24 -0
- package/src/cli/help-command.ts +1 -0
- package/src/cli-entry.ts +83 -0
- package/src/core/contracts/action-contract.ts +30 -0
- package/src/core/contracts/help-contract.ts +20 -0
- package/src/core/contracts/module-contract.ts +7 -0
- package/src/core/errors/framework-errors.ts +26 -0
- package/src/core/registry/action-registry.ts +94 -0
- package/src/help/help-command.ts +32 -0
- package/src/help/help-model.ts +10 -0
- package/src/help/help-renderer.ts +21 -0
- package/src/help/hierarchy-resolver.ts +54 -0
- package/src/index.ts +10 -0
- package/src/modules/sample-content/module.ts +66 -0
- package/src/modules/sample-system/module.ts +74 -0
- package/src/runtime/dispatch.ts +64 -0
- package/src/runtime/engine.ts +59 -0
- package/src/runtime/mode-resolver.ts +18 -0
- package/src/runtime/resume-store.ts +53 -0
- package/src/runtime/runtime-context.ts +10 -0
- package/src/runtime/state-machine.ts +77 -0
- package/src/tui/accessibility-footer.ts +11 -0
- package/src/tui/component-adapters/confirm.ts +8 -0
- package/src/tui/component-adapters/group.ts +12 -0
- package/src/tui/component-adapters/multiselect.ts +22 -0
- package/src/tui/component-adapters/select.ts +18 -0
- package/src/tui/component-adapters/text.ts +13 -0
- package/src/tui/interrupt-handlers.ts +15 -0
- package/tests/e2e/cli-smoke.e2e.test.ts +19 -0
- package/tests/integration/runtime-dispatch.integration.test.ts +23 -0
- package/tests/transcript/help.transcript.test.ts +20 -0
- package/tests/unit/state-machine.test.ts +22 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface HelpArgumentSpec {
|
|
2
|
+
name: string;
|
|
3
|
+
required: boolean;
|
|
4
|
+
description: string;
|
|
5
|
+
example?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HelpUseCaseSpec {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
command: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HelpContract {
|
|
15
|
+
summary: string;
|
|
16
|
+
args: HelpArgumentSpec[];
|
|
17
|
+
examples: string[];
|
|
18
|
+
useCases: HelpUseCaseSpec[];
|
|
19
|
+
keybindings: string[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export enum ExitCode {
|
|
2
|
+
SUCCESS = 0,
|
|
3
|
+
VALIDATION_ERROR = 2,
|
|
4
|
+
INTERRUPTED = 130,
|
|
5
|
+
INTERNAL_ERROR = 1
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum FrameworkErrorCode {
|
|
9
|
+
TRIAD_INCOMPLETE = 'TRIAD_INCOMPLETE',
|
|
10
|
+
DUPLICATE_ACTION = 'DUPLICATE_ACTION',
|
|
11
|
+
DUPLICATE_MODULE = 'DUPLICATE_MODULE',
|
|
12
|
+
MODULE_NOT_FOUND = 'MODULE_NOT_FOUND',
|
|
13
|
+
ACTION_NOT_FOUND = 'ACTION_NOT_FOUND',
|
|
14
|
+
INVALID_TRANSITION = 'INVALID_TRANSITION'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class FrameworkError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
public readonly code: FrameworkErrorCode,
|
|
20
|
+
public readonly exitCode: ExitCode,
|
|
21
|
+
message: string
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'FrameworkError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ActionContract } from '../contracts/action-contract.js';
|
|
2
|
+
import type { ModuleContract } from '../contracts/module-contract.js';
|
|
3
|
+
import {
|
|
4
|
+
ExitCode,
|
|
5
|
+
FrameworkError,
|
|
6
|
+
FrameworkErrorCode
|
|
7
|
+
} from '../errors/framework-errors.js';
|
|
8
|
+
|
|
9
|
+
export interface ActionRef {
|
|
10
|
+
moduleId: string;
|
|
11
|
+
action: ActionContract;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ActionRegistry {
|
|
15
|
+
private readonly modules = new Map<string, ModuleContract>();
|
|
16
|
+
private readonly actions = new Map<string, ActionRef>();
|
|
17
|
+
|
|
18
|
+
registerModule(moduleContract: ModuleContract): void {
|
|
19
|
+
if (this.modules.has(moduleContract.moduleId)) {
|
|
20
|
+
throw new FrameworkError(
|
|
21
|
+
FrameworkErrorCode.DUPLICATE_MODULE,
|
|
22
|
+
ExitCode.VALIDATION_ERROR,
|
|
23
|
+
`Duplicate module registration: ${moduleContract.moduleId}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.modules.set(moduleContract.moduleId, moduleContract);
|
|
28
|
+
|
|
29
|
+
for (const action of moduleContract.actions) {
|
|
30
|
+
this.assertTriad(moduleContract.moduleId, action);
|
|
31
|
+
const key = this.actionKey(moduleContract.moduleId, action.actionId);
|
|
32
|
+
if (this.actions.has(key)) {
|
|
33
|
+
throw new FrameworkError(
|
|
34
|
+
FrameworkErrorCode.DUPLICATE_ACTION,
|
|
35
|
+
ExitCode.VALIDATION_ERROR,
|
|
36
|
+
`Duplicate action registration: ${key}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
this.actions.set(key, { moduleId: moduleContract.moduleId, action });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
listModules(): ModuleContract[] {
|
|
44
|
+
return [...this.modules.values()].sort((a, b) => a.moduleId.localeCompare(b.moduleId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getModule(moduleId: string): ModuleContract {
|
|
48
|
+
const moduleContract = this.modules.get(moduleId);
|
|
49
|
+
if (!moduleContract) {
|
|
50
|
+
throw new FrameworkError(
|
|
51
|
+
FrameworkErrorCode.MODULE_NOT_FOUND,
|
|
52
|
+
ExitCode.VALIDATION_ERROR,
|
|
53
|
+
`Module not found: ${moduleId}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return moduleContract;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getAction(moduleId: string, actionId: string): ActionContract {
|
|
60
|
+
const key = this.actionKey(moduleId, actionId);
|
|
61
|
+
const actionRef = this.actions.get(key);
|
|
62
|
+
if (!actionRef) {
|
|
63
|
+
throw new FrameworkError(
|
|
64
|
+
FrameworkErrorCode.ACTION_NOT_FOUND,
|
|
65
|
+
ExitCode.VALIDATION_ERROR,
|
|
66
|
+
`Action not found: ${key}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return actionRef.action;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private actionKey(moduleId: string, actionId: string): string {
|
|
73
|
+
return `${moduleId}.${actionId}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private assertTriad(moduleId: string, action: ActionContract): void {
|
|
77
|
+
const hasTui = Array.isArray(action.tui?.steps) && action.tui.steps.length > 0;
|
|
78
|
+
const hasCommandline = Array.isArray(action.commandline?.requiredArgs);
|
|
79
|
+
const hasHelp =
|
|
80
|
+
typeof action.help?.summary === 'string' &&
|
|
81
|
+
Array.isArray(action.help?.args) &&
|
|
82
|
+
Array.isArray(action.help?.examples) &&
|
|
83
|
+
Array.isArray(action.help?.useCases) &&
|
|
84
|
+
Array.isArray(action.help?.keybindings);
|
|
85
|
+
|
|
86
|
+
if (!hasTui || !hasCommandline || !hasHelp) {
|
|
87
|
+
throw new FrameworkError(
|
|
88
|
+
FrameworkErrorCode.TRIAD_INCOMPLETE,
|
|
89
|
+
ExitCode.VALIDATION_ERROR,
|
|
90
|
+
`Triad incomplete for action ${moduleId}.${action.actionId}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ExitCode } from '../core/errors/framework-errors.js';
|
|
2
|
+
import type { ActionResultEnvelope } from '../core/contracts/action-contract.js';
|
|
3
|
+
import type { ActionRegistry } from '../core/registry/action-registry.js';
|
|
4
|
+
import { resolveHelpHierarchy } from './hierarchy-resolver.js';
|
|
5
|
+
import { renderHelp } from './help-renderer.js';
|
|
6
|
+
|
|
7
|
+
export interface HelpCommandInput {
|
|
8
|
+
registry: ActionRegistry;
|
|
9
|
+
moduleId?: string;
|
|
10
|
+
actionId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const runHelpCommand = (input: HelpCommandInput): ActionResultEnvelope<string> => {
|
|
14
|
+
const modules = input.registry.listModules();
|
|
15
|
+
const views = resolveHelpHierarchy(modules, input.moduleId, input.actionId);
|
|
16
|
+
|
|
17
|
+
if (views.length === 0) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
mode: 'commandline',
|
|
21
|
+
exitCode: ExitCode.VALIDATION_ERROR,
|
|
22
|
+
errorMessage: 'No help target found'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
mode: 'commandline',
|
|
29
|
+
exitCode: ExitCode.SUCCESS,
|
|
30
|
+
data: renderHelp(views)
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ActionHelpView, HelpSection } from './help-model.js';
|
|
2
|
+
|
|
3
|
+
const renderSection = (section: HelpSection): string => {
|
|
4
|
+
const header = `## ${section.title}`;
|
|
5
|
+
const lines =
|
|
6
|
+
section.lines.length > 0 ? section.lines.map((line) => `- ${line}`).join('\n') : '- (none)';
|
|
7
|
+
return `${header}\n${lines}`;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const renderHelp = (views: ActionHelpView[]): string =>
|
|
11
|
+
views
|
|
12
|
+
.map((view) => {
|
|
13
|
+
const head = [
|
|
14
|
+
'# HELP',
|
|
15
|
+
`MODULE: ${view.moduleId}`,
|
|
16
|
+
`ACTION: ${view.actionId ?? '*'}`
|
|
17
|
+
].join('\n');
|
|
18
|
+
const body = view.sections.map(renderSection).join('\n\n');
|
|
19
|
+
return `${head}\n\n${body}`;
|
|
20
|
+
})
|
|
21
|
+
.join('\n\n---\n\n');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ActionContract } from '../core/contracts/action-contract.js';
|
|
2
|
+
import type { ModuleContract } from '../core/contracts/module-contract.js';
|
|
3
|
+
import type { ActionHelpView } from './help-model.js';
|
|
4
|
+
|
|
5
|
+
export const resolveHelpHierarchy = (
|
|
6
|
+
modules: ModuleContract[],
|
|
7
|
+
moduleId?: string,
|
|
8
|
+
actionId?: string
|
|
9
|
+
): ActionHelpView[] => {
|
|
10
|
+
const scopedModules = moduleId ? modules.filter((m) => m.moduleId === moduleId) : modules;
|
|
11
|
+
|
|
12
|
+
return scopedModules.flatMap((moduleContract) => {
|
|
13
|
+
if (!actionId) {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
moduleId: moduleContract.moduleId,
|
|
17
|
+
sections: [
|
|
18
|
+
{
|
|
19
|
+
title: 'MODULE',
|
|
20
|
+
lines: [moduleContract.description]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'ACTIONS',
|
|
24
|
+
lines: moduleContract.actions.map((action) => `${action.actionId} - ${action.description}`)
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const action = moduleContract.actions.find((item) => item.actionId === actionId);
|
|
32
|
+
if (!action) return [];
|
|
33
|
+
|
|
34
|
+
return [toActionHelp(moduleContract.moduleId, action)];
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const toActionHelp = (moduleId: string, action: ActionContract): ActionHelpView => ({
|
|
39
|
+
moduleId,
|
|
40
|
+
actionId: action.actionId,
|
|
41
|
+
sections: [
|
|
42
|
+
{ title: 'SUMMARY', lines: [action.help.summary] },
|
|
43
|
+
{
|
|
44
|
+
title: 'ARGS',
|
|
45
|
+
lines: action.help.args.map((arg) => `${arg.required ? '*' : '-'} ${arg.name}: ${arg.description}`)
|
|
46
|
+
},
|
|
47
|
+
{ title: 'EXAMPLES', lines: action.help.examples },
|
|
48
|
+
{
|
|
49
|
+
title: 'USE-CASES',
|
|
50
|
+
lines: action.help.useCases.map((useCase) => `${useCase.name}: ${useCase.command}`)
|
|
51
|
+
},
|
|
52
|
+
{ title: 'KEYBINDINGS', lines: action.help.keybindings }
|
|
53
|
+
]
|
|
54
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ActionRegistry } from './core/registry/action-registry.js';
|
|
2
|
+
import type { ModuleContract } from './core/contracts/module-contract.js';
|
|
3
|
+
|
|
4
|
+
export const createRegistry = (modules: ModuleContract[]): ActionRegistry => {
|
|
5
|
+
const registry = new ActionRegistry();
|
|
6
|
+
for (const moduleContract of modules) {
|
|
7
|
+
registry.registerModule(moduleContract);
|
|
8
|
+
}
|
|
9
|
+
return registry;
|
|
10
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ModuleContract } from '../../core/contracts/module-contract.js';
|
|
2
|
+
import { ExitCode } from '../../core/errors/framework-errors.js';
|
|
3
|
+
|
|
4
|
+
const toSlug = (value: string): string =>
|
|
5
|
+
value
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
9
|
+
.replace(/\s+/g, '-')
|
|
10
|
+
.replace(/-+/g, '-');
|
|
11
|
+
|
|
12
|
+
const sampleContentModule: ModuleContract = {
|
|
13
|
+
moduleId: 'content',
|
|
14
|
+
description: 'Content-processing sample actions for framework adoption.',
|
|
15
|
+
actions: [
|
|
16
|
+
{
|
|
17
|
+
actionId: 'slugify',
|
|
18
|
+
description: 'Convert text into URL-safe slug.',
|
|
19
|
+
tui: { steps: ['collect-text', 'preview-slug'] },
|
|
20
|
+
commandline: { requiredArgs: ['text'] },
|
|
21
|
+
help: {
|
|
22
|
+
summary: 'Convert input text into deterministic lowercase slug.',
|
|
23
|
+
args: [{ name: 'text', required: true, description: 'Source text to transform.', example: '--text="Hello World"' }],
|
|
24
|
+
examples: ['hub content slugify --text="Hello World"'],
|
|
25
|
+
useCases: [{ name: 'blog url', description: 'Generate post slug', command: 'hub content slugify --text="my post"' }],
|
|
26
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
27
|
+
},
|
|
28
|
+
run: async (context) => {
|
|
29
|
+
const text = typeof context.args.text === 'string' && context.args.text.length > 0
|
|
30
|
+
? context.args.text
|
|
31
|
+
: 'interactive text';
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
mode: context.mode,
|
|
35
|
+
exitCode: ExitCode.SUCCESS,
|
|
36
|
+
data: toSlug(text)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
actionId: 'word-count',
|
|
42
|
+
description: 'Count words from input text.',
|
|
43
|
+
tui: { steps: ['collect-text', 'show-count'] },
|
|
44
|
+
commandline: { requiredArgs: ['text'] },
|
|
45
|
+
help: {
|
|
46
|
+
summary: 'Count whitespace-separated words in deterministic way.',
|
|
47
|
+
args: [{ name: 'text', required: true, description: 'Input text for counting.', example: '--text="one two"' }],
|
|
48
|
+
examples: ['hub content word-count --text="one two three"'],
|
|
49
|
+
useCases: [{ name: 'draft checks', description: 'Validate article length quickly', command: 'hub content word-count --text="draft body"' }],
|
|
50
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
51
|
+
},
|
|
52
|
+
run: async (context) => {
|
|
53
|
+
const text = typeof context.args.text === 'string' ? context.args.text.trim() : '';
|
|
54
|
+
const words = text.length === 0 ? 0 : text.split(/\s+/).length;
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
mode: context.mode,
|
|
58
|
+
exitCode: ExitCode.SUCCESS,
|
|
59
|
+
data: String(words)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default sampleContentModule;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ModuleContract } from '../../core/contracts/module-contract.js';
|
|
2
|
+
import { ExitCode } from '../../core/errors/framework-errors.js';
|
|
3
|
+
|
|
4
|
+
const sampleSystemModule: ModuleContract = {
|
|
5
|
+
moduleId: 'system',
|
|
6
|
+
description: 'System utility sample actions for framework adoption.',
|
|
7
|
+
actions: [
|
|
8
|
+
{
|
|
9
|
+
actionId: 'env-check',
|
|
10
|
+
description: 'Read environment and report platform details.',
|
|
11
|
+
tui: { steps: ['select-keys', 'review-output'] },
|
|
12
|
+
commandline: { requiredArgs: ['key'] },
|
|
13
|
+
help: {
|
|
14
|
+
summary: 'Check environment variable and runtime platform.',
|
|
15
|
+
args: [
|
|
16
|
+
{
|
|
17
|
+
name: 'key',
|
|
18
|
+
required: true,
|
|
19
|
+
description: 'Environment key to inspect.',
|
|
20
|
+
example: '--key=HOME'
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
examples: ['hub system env-check --key=HOME'],
|
|
24
|
+
useCases: [
|
|
25
|
+
{
|
|
26
|
+
name: 'debug env',
|
|
27
|
+
description: 'Inspect required runtime variable',
|
|
28
|
+
command: 'hub system env-check --key=NODE_ENV'
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
32
|
+
},
|
|
33
|
+
run: async (context) => {
|
|
34
|
+
const key = typeof context.args.key === 'string' ? context.args.key : '';
|
|
35
|
+
const value = key ? process.env[key] ?? '' : '';
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
mode: context.mode,
|
|
39
|
+
exitCode: ExitCode.SUCCESS,
|
|
40
|
+
data: `${key}=${value}`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
actionId: 'node-info',
|
|
46
|
+
description: 'Display Node and OS info.',
|
|
47
|
+
tui: { steps: ['collect-options', 'show-runtime'] },
|
|
48
|
+
commandline: { requiredArgs: [] },
|
|
49
|
+
help: {
|
|
50
|
+
summary: 'Print deterministic Node runtime and platform info.',
|
|
51
|
+
args: [],
|
|
52
|
+
examples: ['hub system node-info'],
|
|
53
|
+
useCases: [
|
|
54
|
+
{
|
|
55
|
+
name: 'verify runtime',
|
|
56
|
+
description: 'Check active node version and OS',
|
|
57
|
+
command: 'hub system node-info'
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
keybindings: ['Enter confirm', 'Esc cancel']
|
|
61
|
+
},
|
|
62
|
+
run: async (context) => {
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
mode: context.mode,
|
|
66
|
+
exitCode: ExitCode.SUCCESS,
|
|
67
|
+
data: `node=${process.version};platform=${process.platform}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default sampleSystemModule;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ExitCode, FrameworkError } from '../core/errors/framework-errors.js';
|
|
2
|
+
import type { ActionResultEnvelope } from '../core/contracts/action-contract.js';
|
|
3
|
+
import type { ActionRegistry } from '../core/registry/action-registry.js';
|
|
4
|
+
import { executeAction } from './engine.js';
|
|
5
|
+
import { resolveRuntimeMode } from './mode-resolver.js';
|
|
6
|
+
import { FileResumeStore } from './resume-store.js';
|
|
7
|
+
|
|
8
|
+
export interface DispatchInput {
|
|
9
|
+
registry: ActionRegistry;
|
|
10
|
+
moduleId: string;
|
|
11
|
+
actionId: string;
|
|
12
|
+
args: Record<string, string | boolean>;
|
|
13
|
+
isTTY: boolean;
|
|
14
|
+
resumeFilePath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const dispatchAction = async (input: DispatchInput): Promise<ActionResultEnvelope> => {
|
|
18
|
+
try {
|
|
19
|
+
const action = input.registry.getAction(input.moduleId, input.actionId);
|
|
20
|
+
const mode = resolveRuntimeMode({
|
|
21
|
+
isTTY: input.isTTY,
|
|
22
|
+
providedArgs: input.args,
|
|
23
|
+
commandline: action.commandline
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const missingRequired = action.commandline.requiredArgs.filter(
|
|
27
|
+
(arg) => input.args[arg] === undefined || input.args[arg] === ''
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (mode === 'commandline' && missingRequired.length > 0) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
mode,
|
|
34
|
+
exitCode: ExitCode.VALIDATION_ERROR,
|
|
35
|
+
errorMessage: `Missing required args: ${missingRequired.join(', ')}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resumeStore = input.resumeFilePath ? new FileResumeStore(input.resumeFilePath) : undefined;
|
|
40
|
+
|
|
41
|
+
return executeAction({
|
|
42
|
+
moduleId: input.moduleId,
|
|
43
|
+
action,
|
|
44
|
+
mode,
|
|
45
|
+
args: input.args,
|
|
46
|
+
resumeStore
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof FrameworkError) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
mode: 'commandline',
|
|
53
|
+
exitCode: error.exitCode,
|
|
54
|
+
errorMessage: error.message
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
mode: 'commandline',
|
|
60
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
61
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown dispatch error'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ExitCode } from '../core/errors/framework-errors.js';
|
|
2
|
+
import type { ActionContract, ActionResultEnvelope, RuntimeMode } from '../core/contracts/action-contract.js';
|
|
3
|
+
import type { RuntimeContext } from './runtime-context.js';
|
|
4
|
+
import { ResumeStore } from './resume-store.js';
|
|
5
|
+
import { StateMachine, type WorkflowNode } from './state-machine.js';
|
|
6
|
+
|
|
7
|
+
export interface EngineInput {
|
|
8
|
+
moduleId: string;
|
|
9
|
+
action: ActionContract;
|
|
10
|
+
mode: RuntimeMode;
|
|
11
|
+
args: Record<string, string | boolean>;
|
|
12
|
+
workflowId?: string;
|
|
13
|
+
workflowNodes?: WorkflowNode[];
|
|
14
|
+
resumeStore?: ResumeStore;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const executeAction = async (input: EngineInput): Promise<ActionResultEnvelope> => {
|
|
18
|
+
const nodes = input.workflowNodes ?? input.action.tui.steps.map((step) => ({ id: step, label: step }));
|
|
19
|
+
const workflowId = input.workflowId ?? `${input.moduleId}.${input.action.actionId}`;
|
|
20
|
+
|
|
21
|
+
let initialNodeId: string | undefined;
|
|
22
|
+
if (input.resumeStore) {
|
|
23
|
+
const checkpoint = await input.resumeStore.load();
|
|
24
|
+
if (checkpoint?.workflowId === workflowId) {
|
|
25
|
+
initialNodeId = checkpoint.nodeId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const stateMachine = new StateMachine(workflowId, nodes, initialNodeId);
|
|
30
|
+
const context: RuntimeContext = {
|
|
31
|
+
moduleId: input.moduleId,
|
|
32
|
+
actionId: input.action.actionId,
|
|
33
|
+
mode: input.mode,
|
|
34
|
+
args: input.args,
|
|
35
|
+
stateMachine
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = await input.action.run(context);
|
|
40
|
+
if (input.resumeStore) {
|
|
41
|
+
if (stateMachine.snapshot().exited || result.ok) {
|
|
42
|
+
await input.resumeStore.clear();
|
|
43
|
+
} else {
|
|
44
|
+
await input.resumeStore.save(stateMachine.checkpoint());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { ...result, mode: input.mode, exitCode: result.exitCode ?? ExitCode.SUCCESS };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (input.resumeStore) {
|
|
50
|
+
await input.resumeStore.save(stateMachine.checkpoint());
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
mode: input.mode,
|
|
55
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
56
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandlineContract, RuntimeMode } from '../core/contracts/action-contract.js';
|
|
2
|
+
|
|
3
|
+
export interface RuntimeResolutionInput {
|
|
4
|
+
isTTY: boolean;
|
|
5
|
+
providedArgs: Record<string, string | boolean>;
|
|
6
|
+
commandline: CommandlineContract;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const hasRequiredArgs = (
|
|
10
|
+
requiredArgs: string[],
|
|
11
|
+
providedArgs: Record<string, string | boolean>
|
|
12
|
+
): boolean => requiredArgs.every((arg) => providedArgs[arg] !== undefined && providedArgs[arg] !== '');
|
|
13
|
+
|
|
14
|
+
export const resolveRuntimeMode = (input: RuntimeResolutionInput): RuntimeMode => {
|
|
15
|
+
if (!input.isTTY) return 'commandline';
|
|
16
|
+
if (hasRequiredArgs(input.commandline.requiredArgs, input.providedArgs)) return 'commandline';
|
|
17
|
+
return 'tui';
|
|
18
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { WorkflowCheckpoint } from './state-machine.js';
|
|
4
|
+
|
|
5
|
+
export interface ResumeStore {
|
|
6
|
+
load: () => Promise<WorkflowCheckpoint | undefined>;
|
|
7
|
+
save: (checkpoint: WorkflowCheckpoint) => Promise<void>;
|
|
8
|
+
clear: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class FileResumeStore implements ResumeStore {
|
|
12
|
+
constructor(private readonly filePath: string) {}
|
|
13
|
+
|
|
14
|
+
async load(): Promise<WorkflowCheckpoint | undefined> {
|
|
15
|
+
let raw: string;
|
|
16
|
+
try {
|
|
17
|
+
raw = await fs.readFile(this.filePath, 'utf8');
|
|
18
|
+
} catch (error: unknown) {
|
|
19
|
+
if (isIgnorableReadError(error)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(raw) as WorkflowCheckpoint;
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async save(checkpoint: WorkflowCheckpoint): Promise<void> {
|
|
33
|
+
await fs.mkdir(dirname(this.filePath), { recursive: true });
|
|
34
|
+
await fs.writeFile(this.filePath, JSON.stringify(checkpoint), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async clear(): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
await fs.unlink(this.filePath);
|
|
40
|
+
} catch (error: unknown) {
|
|
41
|
+
if (isIgnorableReadError(error)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isIgnorableReadError = (error: unknown): boolean =>
|
|
50
|
+
typeof error === 'object' &&
|
|
51
|
+
error !== null &&
|
|
52
|
+
'code' in error &&
|
|
53
|
+
((error as { code?: string }).code === 'ENOENT');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RuntimeMode } from '../core/contracts/action-contract.js';
|
|
2
|
+
import type { StateMachine } from './state-machine.js';
|
|
3
|
+
|
|
4
|
+
export interface RuntimeContext {
|
|
5
|
+
moduleId: string;
|
|
6
|
+
actionId: string;
|
|
7
|
+
mode: RuntimeMode;
|
|
8
|
+
args: Record<string, string | boolean>;
|
|
9
|
+
stateMachine?: StateMachine;
|
|
10
|
+
}
|