@workflow-manager/runner 0.6.0 → 0.8.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/README.md +5 -1
- package/dist/cliRunRenderer.js +18 -56
- package/dist/engine.js +201 -68
- package/dist/generated/bundledSkills.d.ts +5 -0
- package/dist/generated/bundledSkills.js +22 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +466 -87
- package/dist/manPage.d.ts +1 -1
- package/dist/manPage.js +47 -9
- package/dist/parser.js +10 -1
- package/dist/runFormat.d.ts +11 -0
- package/dist/runFormat.js +56 -0
- package/dist/runnerApi.js +14 -0
- package/dist/sessionFile.d.ts +11 -0
- package/dist/sessionFile.js +61 -0
- package/dist/tui/ansi.d.ts +23 -0
- package/dist/tui/ansi.js +102 -0
- package/dist/tui/layout.d.ts +18 -0
- package/dist/tui/layout.js +245 -0
- package/dist/tui/logStore.d.ts +22 -0
- package/dist/tui/logStore.js +78 -0
- package/dist/tui/screen.d.ts +35 -0
- package/dist/tui/screen.js +99 -0
- package/dist/tui/theme.d.ts +13 -0
- package/dist/tui/theme.js +55 -0
- package/dist/tui/tuiRunRenderer.d.ts +83 -0
- package/dist/tui/tuiRunRenderer.js +316 -0
- package/dist/types.d.ts +15 -8
- package/package.json +4 -2
- package/skills/workflow-author/README.md +18 -0
- package/skills/workflow-author/SKILL.md +306 -0
package/dist/manPage.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MAN_PAGE_SOURCE = ".TH WFM 1 \"April 2026\" \"@workflow-manager/runner\" \"User Commands\"\n.SH NAME\nwfm \\- run markdown or json workflows from the CLI\n.SH SYNOPSIS\n.B wfm\n.I command\n[options]\n.SH DESCRIPTION\nwfm parses a workflow definition file, validates it, and executes\nit with deterministic in-memory orchestration.\n\nWorkflow files can be Markdown with YAML frontmatter or JSON.\n.SH COMMANDS\n.TP\n.B doctor [workflow.md|workflow.json] [--json]\nInspect host adapter setup, LLM access keys, and current adapter implementation status. When a workflow path is provided, also validate schema and runtime requirements without executing steps.\n.TP\n.B skill list\nList the agent skills bundled with the npm package.\n.TP\n.B skill install [name ...] [--agent claude|opencode] [--global] [--dir path] [--all] [--force]\nInstall bundled agent skills into an agent skill directory. Defaults to the workflow-manager-cli skill and the project-level Claude Code directory (./.claude/skills). Existing skills are not overwritten unless --force is passed.\n.TP\n.B scaffold [path] [--format markdown|json]\nCreate a starter workflow file. Format defaults to markdown unless the output\npath ends in .json.\n.TP\n.B validate <workflow.md|workflow.json>\nValidate workflow structure and report schema errors.\n.TP\n.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json]\nRun the workflow with live CLI progress and optional JSON output.\n.TP\n.B approve [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]\nApprove the current waiting runner step through the local attach API.\n.TP\n.B resume [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]\nAlias for approve, intended for external resume flows.\n.TP\n.B cancel [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]\nCancel the current waiting runner step through the local attach API.\n.TP\n.B auth <login|whoami|logout> [--token value]\nManage remote registry authentication for CLI publish and pull flows.\n.TP\n.B publish <workflow.md|workflow.json> [--slug slug] [--title text] [--description text] [--visibility public|private] [--version label] [--tag a,b] [--draft]\nPublish a validated local workflow to the remote registry.\n.TP\n.B pull <owner/slug> [--version label] [--output path]\nDownload a remote workflow and write it to a local file.\n.TP\n.B search [query]\nSearch public workflows from the remote registry.\n.TP\n.B remote info <owner/slug>\nShow metadata and source information for a remote workflow.\n.TP\n.B man\nOpen this man page.\n.TP\n.B --version, -v, version\nPrint the installed wfm version and exit.\n.SH RUN OPTIONS\n.TP\n.B --input <path>\nJSON file merged into global workflow input state.\n.TP\n.B --objective <text>\nOverride the default run objective.\n.TP\n.B --confirm <stepA,stepB:human,...>\nProvide explicit confirmations for steps that require validation.\n.TP\n.B --auto-confirm-all\nBypass confirmation gating for all steps.\n.TP\n.B --port <number>\nBind the local attach API to a specific port. If omitted, the OS assigns a free port on 127.0.0.1.\n.TP\n.B --verbose\nStream per-step agent output and execution updates to stderr while the workflow runs.\n.TP\n.B --json\nPrint the final run result as JSON on stdout while keeping live progress on stderr.\n.TP\nHuman approval steps in an interactive terminal show an inline review prompt so they can be approved or cancelled without a separate HTTP client.\n.TP\n.B --url <value>\nRunner attach API base URL for approve, resume, or
|
|
1
|
+
export declare const MAN_PAGE_SOURCE = ".TH WFM 1 \"April 2026\" \"@workflow-manager/runner\" \"User Commands\"\n.SH NAME\nwfm \\- run markdown or json workflows from the CLI\n.SH SYNOPSIS\n.B wfm\n.I command\n[options]\n.SH DESCRIPTION\nwfm parses a workflow definition file, validates it, and executes\nit with deterministic in-memory orchestration.\n\nWorkflow files can be Markdown with YAML frontmatter or JSON.\n.SH COMMANDS\n.TP\n.B doctor [workflow.md|workflow.json] [--json]\nInspect host adapter setup, LLM access keys, and current adapter implementation status. When a workflow path is provided, also validate schema and runtime requirements without executing steps.\n.TP\n.B skill list\nList the agent skills bundled with the npm package.\n.TP\n.B skill install [name ...] [--agent claude|opencode] [--global] [--dir path] [--all] [--force]\nInstall bundled agent skills into an agent skill directory. Defaults to the workflow-manager-cli skill and the project-level Claude Code directory (./.claude/skills). Existing skills are not overwritten unless --force is passed.\n.TP\n.B scaffold [path] [--format markdown|json] [--template default|agent-validated]\nCreate a starter workflow file. Format defaults to markdown unless the output\npath ends in .json. Template defaults to default (the general multi-step example);\nagent-validated scaffolds a compact pipeline demonstrating first-class agent\nvalidation (validation.mode: agent with a validator agent and criteria).\n.TP\n.B validate <workflow.md|workflow.json>\nValidate workflow structure and report schema errors.\n.TP\n.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--session-file path] [--verbose] [--json] [--ui]\nRun the workflow with live CLI progress and optional JSON output.\n.TP\n.B approve [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]\nApprove the current waiting runner step through the local attach API.\n.TP\n.B resume [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]\nAlias for approve, intended for external resume flows.\n.TP\n.B cancel [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]\nCancel the current waiting runner step through the local attach API.\n.TP\n.B status [--url value] [--token value] [--session-file path] [--run-id value] [--step key]\nPrint the current run snapshot (or one step detail with --step) as JSON on stdout.\n.TP\n.B logs [--url value] [--token value] [--session-file path] [--run-id value] [--step key] [--limit number] [--cursor value]\nPrint buffered agent stdout/stderr chunks as JSON on stdout.\n.TP\n.B events [--url value] [--token value] [--session-file path] [--run-id value] [--since sequence] [--include-logs]\nPrint run events as JSON on stdout in a single poll (no streaming). Log events are excluded unless --include-logs is passed.\n.TP\n.B auth <login|whoami|logout> [--token value]\nManage remote registry authentication for CLI publish and pull flows.\n.TP\n.B publish <workflow.md|workflow.json> [--slug slug] [--title text] [--description text] [--visibility public|private] [--version label] [--tag a,b] [--draft]\nPublish a validated local workflow to the remote registry.\n.TP\n.B pull <owner/slug> [--version label] [--output path]\nDownload a remote workflow and write it to a local file.\n.TP\n.B search [query]\nSearch public workflows from the remote registry.\n.TP\n.B remote info <owner/slug>\nShow metadata and source information for a remote workflow.\n.TP\n.B man\nOpen this man page.\n.TP\n.B --version, -v, version\nPrint the installed wfm version and exit.\n.SH RUN OPTIONS\n.TP\n.B --input <path>\nJSON file merged into global workflow input state.\n.TP\n.B --objective <text>\nOverride the default run objective.\n.TP\n.B --confirm <stepA,stepB:human,...>\nProvide explicit confirmations for steps that require validation.\n.TP\n.B --auto-confirm-all\nBypass confirmation gating for all steps.\n.TP\n.B --port <number>\nBind the local attach API to a specific port. If omitted, the OS assigns a free port on 127.0.0.1.\n.TP\n.B --session-file <path>\nWrite attach connection details (base URL, bearer token, run id, pid, timestamps) to a JSON file with mode 0600 when the run starts, and rewrite it with endedAt and the final status when the run finishes. Attach commands (approve, resume, cancel, status, logs, events) accept the same flag to read those details back.\n.TP\n.B --verbose\nStream per-step agent output and execution updates to stderr while the workflow runs.\n.TP\n.B --json\nPrint the final run result as JSON on stdout while keeping live progress on stderr.\n.TP\n.B --ui\nFull-screen terminal UI (requires a TTY; falls back to standard output).\n.TP\nHuman approval steps in an interactive terminal show an inline review prompt so they can be approved or cancelled without a separate HTTP client.\n.TP\n.B --url <value>\nRunner attach API base URL for approve, resume, cancel, status, logs, or events commands.\n.TP\n.B --token <value>\nRunner attach API bearer token for approve, resume, cancel, status, logs, or events commands.\n.TP\n.B --run-id <value>\nRunner id to control. If omitted, the CLI reads it from /session.\n.TP\n.B --step <value>\nOptional step key when controlling a specific waiting step, or when scoping status and logs output.\n.TP\n.B --limit <number>\nMaximum number of log chunks returned by the logs command. Defaults to 200.\n.TP\n.B --cursor <value>\nPagination cursor for the logs command, taken from a previous nextCursor value.\n.TP\n.B --since <sequence>\nOnly return events with a sequence greater than this value in the events command.\n.TP\n.B --include-logs\nInclude agent.stdout and agent.stderr events in the events command output.\n.TP\n.B --actor <value>\nActor name recorded in approval audit events.\n.TP\n.B --note <text>\nOptional approval or cancellation note recorded in the event payload.\n.SH EXAMPLES\n.TP\nValidate markdown workflow:\n.B wfm validate ./example-workflow.md\n.TP\nValidate json workflow:\n.B wfm validate ./example-workflow.json\n.TP\nScaffold json workflow file:\n.B wfm scaffold ./new-workflow.json --format json\n.TP\nScaffold an agent-validated pipeline example:\n.B wfm scaffold ./agent-validated.md --template agent-validated\n.TP\nAuthenticate with a CLI token:\n.B wfm auth login --token wm_exampletoken\n.TP\nPublish a workflow:\n.B wfm publish ./example-workflow.json --visibility public --tag example,automation\n.TP\nPull a remote workflow:\n.B wfm pull alice/remote-bunny --output ./remote-bunny.json\n.TP\nSearch the remote registry:\n.B wfm search bunny\n.TP\nRun with explicit confirmations:\n.B wfm run ./example-workflow.json --confirm discover:human,qa_gate:human\n.TP\nRun with a session file for attach clients:\n.B wfm run ./example-workflow.json --session-file ./run-session.json\n.TP\nObserve and control the run through the session file:\n.B wfm status --session-file ./run-session.json && wfm approve --session-file ./run-session.json --step qa_gate\n.TP\nInspect host setup:\n.B wfm doctor\n.TP\nCheck a workflow before running it:\n.B wfm doctor ./example-workflow.json\n.TP\nInstall the bundled CLI skill for Claude Code:\n.B wfm skill install\n.TP\nInstall every bundled skill globally:\n.B wfm skill install --all --global\n.SH FILES\n.TP\n.B man/wfm.1\nThe manual page source shipped with this repository.\n.SH EXIT STATUS\n.TP\n.B 0\nSuccessful command execution.\n.TP\n.B 1\nValidation or runtime error.\n.TP\n.B 2\nRun completed in non-success terminal status.\n";
|
package/dist/manPage.js
CHANGED
|
@@ -21,25 +21,36 @@ List the agent skills bundled with the npm package.
|
|
|
21
21
|
.B skill install [name ...] [--agent claude|opencode] [--global] [--dir path] [--all] [--force]
|
|
22
22
|
Install bundled agent skills into an agent skill directory. Defaults to the workflow-manager-cli skill and the project-level Claude Code directory (./.claude/skills). Existing skills are not overwritten unless --force is passed.
|
|
23
23
|
.TP
|
|
24
|
-
.B scaffold [path] [--format markdown|json]
|
|
24
|
+
.B scaffold [path] [--format markdown|json] [--template default|agent-validated]
|
|
25
25
|
Create a starter workflow file. Format defaults to markdown unless the output
|
|
26
|
-
path ends in .json.
|
|
26
|
+
path ends in .json. Template defaults to default (the general multi-step example);
|
|
27
|
+
agent-validated scaffolds a compact pipeline demonstrating first-class agent
|
|
28
|
+
validation (validation.mode: agent with a validator agent and criteria).
|
|
27
29
|
.TP
|
|
28
30
|
.B validate <workflow.md|workflow.json>
|
|
29
31
|
Validate workflow structure and report schema errors.
|
|
30
32
|
.TP
|
|
31
|
-
.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json]
|
|
33
|
+
.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--session-file path] [--verbose] [--json] [--ui]
|
|
32
34
|
Run the workflow with live CLI progress and optional JSON output.
|
|
33
35
|
.TP
|
|
34
|
-
.B approve [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
36
|
+
.B approve [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]
|
|
35
37
|
Approve the current waiting runner step through the local attach API.
|
|
36
38
|
.TP
|
|
37
|
-
.B resume [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
39
|
+
.B resume [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]
|
|
38
40
|
Alias for approve, intended for external resume flows.
|
|
39
41
|
.TP
|
|
40
|
-
.B cancel [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
42
|
+
.B cancel [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]
|
|
41
43
|
Cancel the current waiting runner step through the local attach API.
|
|
42
44
|
.TP
|
|
45
|
+
.B status [--url value] [--token value] [--session-file path] [--run-id value] [--step key]
|
|
46
|
+
Print the current run snapshot (or one step detail with --step) as JSON on stdout.
|
|
47
|
+
.TP
|
|
48
|
+
.B logs [--url value] [--token value] [--session-file path] [--run-id value] [--step key] [--limit number] [--cursor value]
|
|
49
|
+
Print buffered agent stdout/stderr chunks as JSON on stdout.
|
|
50
|
+
.TP
|
|
51
|
+
.B events [--url value] [--token value] [--session-file path] [--run-id value] [--since sequence] [--include-logs]
|
|
52
|
+
Print run events as JSON on stdout in a single poll (no streaming). Log events are excluded unless --include-logs is passed.
|
|
53
|
+
.TP
|
|
43
54
|
.B auth <login|whoami|logout> [--token value]
|
|
44
55
|
Manage remote registry authentication for CLI publish and pull flows.
|
|
45
56
|
.TP
|
|
@@ -77,25 +88,43 @@ Bypass confirmation gating for all steps.
|
|
|
77
88
|
.B --port <number>
|
|
78
89
|
Bind the local attach API to a specific port. If omitted, the OS assigns a free port on 127.0.0.1.
|
|
79
90
|
.TP
|
|
91
|
+
.B --session-file <path>
|
|
92
|
+
Write attach connection details (base URL, bearer token, run id, pid, timestamps) to a JSON file with mode 0600 when the run starts, and rewrite it with endedAt and the final status when the run finishes. Attach commands (approve, resume, cancel, status, logs, events) accept the same flag to read those details back.
|
|
93
|
+
.TP
|
|
80
94
|
.B --verbose
|
|
81
95
|
Stream per-step agent output and execution updates to stderr while the workflow runs.
|
|
82
96
|
.TP
|
|
83
97
|
.B --json
|
|
84
98
|
Print the final run result as JSON on stdout while keeping live progress on stderr.
|
|
85
99
|
.TP
|
|
100
|
+
.B --ui
|
|
101
|
+
Full-screen terminal UI (requires a TTY; falls back to standard output).
|
|
102
|
+
.TP
|
|
86
103
|
Human approval steps in an interactive terminal show an inline review prompt so they can be approved or cancelled without a separate HTTP client.
|
|
87
104
|
.TP
|
|
88
105
|
.B --url <value>
|
|
89
|
-
Runner attach API base URL for approve, resume, or
|
|
106
|
+
Runner attach API base URL for approve, resume, cancel, status, logs, or events commands.
|
|
90
107
|
.TP
|
|
91
108
|
.B --token <value>
|
|
92
|
-
Runner attach API bearer token for approve, resume, or
|
|
109
|
+
Runner attach API bearer token for approve, resume, cancel, status, logs, or events commands.
|
|
93
110
|
.TP
|
|
94
111
|
.B --run-id <value>
|
|
95
112
|
Runner id to control. If omitted, the CLI reads it from /session.
|
|
96
113
|
.TP
|
|
97
114
|
.B --step <value>
|
|
98
|
-
Optional step key when controlling a specific waiting step.
|
|
115
|
+
Optional step key when controlling a specific waiting step, or when scoping status and logs output.
|
|
116
|
+
.TP
|
|
117
|
+
.B --limit <number>
|
|
118
|
+
Maximum number of log chunks returned by the logs command. Defaults to 200.
|
|
119
|
+
.TP
|
|
120
|
+
.B --cursor <value>
|
|
121
|
+
Pagination cursor for the logs command, taken from a previous nextCursor value.
|
|
122
|
+
.TP
|
|
123
|
+
.B --since <sequence>
|
|
124
|
+
Only return events with a sequence greater than this value in the events command.
|
|
125
|
+
.TP
|
|
126
|
+
.B --include-logs
|
|
127
|
+
Include agent.stdout and agent.stderr events in the events command output.
|
|
99
128
|
.TP
|
|
100
129
|
.B --actor <value>
|
|
101
130
|
Actor name recorded in approval audit events.
|
|
@@ -113,6 +142,9 @@ Validate json workflow:
|
|
|
113
142
|
Scaffold json workflow file:
|
|
114
143
|
.B wfm scaffold ./new-workflow.json --format json
|
|
115
144
|
.TP
|
|
145
|
+
Scaffold an agent-validated pipeline example:
|
|
146
|
+
.B wfm scaffold ./agent-validated.md --template agent-validated
|
|
147
|
+
.TP
|
|
116
148
|
Authenticate with a CLI token:
|
|
117
149
|
.B wfm auth login --token wm_exampletoken
|
|
118
150
|
.TP
|
|
@@ -128,6 +160,12 @@ Search the remote registry:
|
|
|
128
160
|
Run with explicit confirmations:
|
|
129
161
|
.B wfm run ./example-workflow.json --confirm discover:human,qa_gate:human
|
|
130
162
|
.TP
|
|
163
|
+
Run with a session file for attach clients:
|
|
164
|
+
.B wfm run ./example-workflow.json --session-file ./run-session.json
|
|
165
|
+
.TP
|
|
166
|
+
Observe and control the run through the session file:
|
|
167
|
+
.B wfm status --session-file ./run-session.json && wfm approve --session-file ./run-session.json --step qa_gate
|
|
168
|
+
.TP
|
|
131
169
|
Inspect host setup:
|
|
132
170
|
.B wfm doctor
|
|
133
171
|
.TP
|
package/dist/parser.js
CHANGED
|
@@ -185,9 +185,18 @@ export function validateWorkflow(def) {
|
|
|
185
185
|
errors.push(`Unsupported adapter for ${step.key}: ${adapter}`);
|
|
186
186
|
}
|
|
187
187
|
const mode = step.validation?.mode ?? step.approvalSpec?.validation?.mode;
|
|
188
|
-
if (mode && !["none", "human", "external"].includes(mode)) {
|
|
188
|
+
if (mode && !["none", "human", "external", "agent"].includes(mode)) {
|
|
189
189
|
errors.push(`Invalid validation mode for ${step.key}: ${mode}`);
|
|
190
190
|
}
|
|
191
|
+
if (step.approvalSpec?.validation?.mode === "agent") {
|
|
192
|
+
errors.push(`Approval step ${step.key} cannot use agent validation`);
|
|
193
|
+
}
|
|
194
|
+
if (step.validation?.mode === "agent" && step.validation.agent?.adapterKey) {
|
|
195
|
+
const validatorAdapter = step.validation.agent.adapterKey;
|
|
196
|
+
if (!SUPPORTED_ADAPTERS.includes(validatorAdapter)) {
|
|
197
|
+
errors.push(`Unsupported validator adapter for ${step.key}: ${validatorAdapter}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
191
200
|
}
|
|
192
201
|
const cycle = findDependencyCycle(def);
|
|
193
202
|
if (cycle) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StepDefinition, StepRunStatus, WorkflowRunStatus } from "./types.js";
|
|
2
|
+
export declare function isTerminalStatus(status: WorkflowRunStatus): boolean;
|
|
3
|
+
export declare function formatCount(value: number, singular: string, plural: string): string;
|
|
4
|
+
export declare function formatDurationMs(value: number): string;
|
|
5
|
+
export declare function elapsedFrom(iso: string | null, now?: number): string;
|
|
6
|
+
export declare function splitLines(value: string): {
|
|
7
|
+
lines: string[];
|
|
8
|
+
remainder: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function describeAgentStatus(status: StepRunStatus): string;
|
|
11
|
+
export declare function stepLabel(step: StepDefinition): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function isTerminalStatus(status) {
|
|
2
|
+
return status === "succeeded" || status === "failed" || status === "cancelled";
|
|
3
|
+
}
|
|
4
|
+
export function formatCount(value, singular, plural) {
|
|
5
|
+
return `${value} ${value === 1 ? singular : plural}`;
|
|
6
|
+
}
|
|
7
|
+
export function formatDurationMs(value) {
|
|
8
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
9
|
+
return "0s";
|
|
10
|
+
}
|
|
11
|
+
if (value < 1000) {
|
|
12
|
+
return `${Math.max(0, Math.round(value))}ms`;
|
|
13
|
+
}
|
|
14
|
+
const seconds = value / 1000;
|
|
15
|
+
if (seconds < 60) {
|
|
16
|
+
return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
|
|
17
|
+
}
|
|
18
|
+
const minutes = Math.floor(seconds / 60);
|
|
19
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
20
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
21
|
+
}
|
|
22
|
+
export function elapsedFrom(iso, now = Date.now()) {
|
|
23
|
+
if (!iso) {
|
|
24
|
+
return "0s";
|
|
25
|
+
}
|
|
26
|
+
const startedAt = Date.parse(iso);
|
|
27
|
+
if (Number.isNaN(startedAt)) {
|
|
28
|
+
return "0s";
|
|
29
|
+
}
|
|
30
|
+
return formatDurationMs(now - startedAt);
|
|
31
|
+
}
|
|
32
|
+
export function splitLines(value) {
|
|
33
|
+
const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
34
|
+
const parts = normalized.split("\n");
|
|
35
|
+
const remainder = parts.pop() ?? "";
|
|
36
|
+
return { lines: parts, remainder };
|
|
37
|
+
}
|
|
38
|
+
export function describeAgentStatus(status) {
|
|
39
|
+
switch (status) {
|
|
40
|
+
case "running":
|
|
41
|
+
return "started";
|
|
42
|
+
case "succeeded":
|
|
43
|
+
return "succeeded";
|
|
44
|
+
case "failed":
|
|
45
|
+
return "failed";
|
|
46
|
+
case "waiting_for_approval":
|
|
47
|
+
return "waiting for approval";
|
|
48
|
+
case "cancelled":
|
|
49
|
+
return "cancelled";
|
|
50
|
+
default:
|
|
51
|
+
return status.replace(/_/g, " ");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function stepLabel(step) {
|
|
55
|
+
return step.title ?? step.objective ?? step.key;
|
|
56
|
+
}
|
package/dist/runnerApi.js
CHANGED
|
@@ -136,6 +136,20 @@ export async function startRunnerApiServer(store, requestedPort) {
|
|
|
136
136
|
});
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
|
+
const eventsListMatch = pathname.match(/^\/runs\/([^/]+)\/events\/list$/);
|
|
140
|
+
if (method === "GET" && eventsListMatch) {
|
|
141
|
+
const runId = decodeURIComponent(eventsListMatch[1] ?? "");
|
|
142
|
+
if (!store.isKnownRun(runId)) {
|
|
143
|
+
errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const sinceSequence = Number.parseInt(url.searchParams.get("sinceSequence") ?? "0", 10) || undefined;
|
|
147
|
+
const includeLogs = parseBoolean(url.searchParams.get("includeLogs"), true);
|
|
148
|
+
const items = store.events(sinceSequence, includeLogs);
|
|
149
|
+
const nextSequence = items.reduce((max, event) => Math.max(max, event.sequence), sinceSequence ?? 0);
|
|
150
|
+
jsonResponse(res, 200, { items, nextSequence });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
139
153
|
const eventsMatch = pathname.match(/^\/runs\/([^/]+)\/events$/);
|
|
140
154
|
if (method === "GET" && eventsMatch) {
|
|
141
155
|
const runId = decodeURIComponent(eventsMatch[1] ?? "");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RunnerSessionFile {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
attachToken: string;
|
|
4
|
+
runId: string;
|
|
5
|
+
pid: number;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
endedAt?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function writeSessionFile(filePath: string, session: RunnerSessionFile): void;
|
|
11
|
+
export declare function readSessionFile(filePath: string): RunnerSessionFile | string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function writeSessionFile(filePath, session) {
|
|
4
|
+
const resolvedPath = path.resolve(filePath);
|
|
5
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
6
|
+
fs.writeFileSync(resolvedPath, `${JSON.stringify(session, null, 2)}\n`, {
|
|
7
|
+
encoding: "utf-8",
|
|
8
|
+
mode: 0o600,
|
|
9
|
+
});
|
|
10
|
+
// writeFileSync only applies the mode on creation; enforce it on rewrites too.
|
|
11
|
+
fs.chmodSync(resolvedPath, 0o600);
|
|
12
|
+
}
|
|
13
|
+
export function readSessionFile(filePath) {
|
|
14
|
+
const resolvedPath = path.resolve(filePath);
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = fs.readFileSync(resolvedPath, "utf-8");
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return `Could not read session file ${resolvedPath}: ${error.message}`;
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return `Session file ${resolvedPath} is not valid JSON`;
|
|
28
|
+
}
|
|
29
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30
|
+
return `Session file ${resolvedPath} must contain a JSON object`;
|
|
31
|
+
}
|
|
32
|
+
const record = parsed;
|
|
33
|
+
for (const field of ["baseUrl", "attachToken", "runId", "startedAt"]) {
|
|
34
|
+
if (typeof record[field] !== "string" || record[field] === "") {
|
|
35
|
+
return `Session file ${resolvedPath} is missing required field: ${field}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof record.pid !== "number" || !Number.isInteger(record.pid)) {
|
|
39
|
+
return `Session file ${resolvedPath} is missing required field: pid`;
|
|
40
|
+
}
|
|
41
|
+
if (record.endedAt !== undefined && typeof record.endedAt !== "string") {
|
|
42
|
+
return `Session file ${resolvedPath} has an invalid endedAt field`;
|
|
43
|
+
}
|
|
44
|
+
if (record.status !== undefined && typeof record.status !== "string") {
|
|
45
|
+
return `Session file ${resolvedPath} has an invalid status field`;
|
|
46
|
+
}
|
|
47
|
+
const session = {
|
|
48
|
+
baseUrl: record.baseUrl,
|
|
49
|
+
attachToken: record.attachToken,
|
|
50
|
+
runId: record.runId,
|
|
51
|
+
pid: record.pid,
|
|
52
|
+
startedAt: record.startedAt,
|
|
53
|
+
};
|
|
54
|
+
if (typeof record.endedAt === "string") {
|
|
55
|
+
session.endedAt = record.endedAt;
|
|
56
|
+
}
|
|
57
|
+
if (typeof record.status === "string") {
|
|
58
|
+
session.status = record.status;
|
|
59
|
+
}
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const ALT_SCREEN_ENTER = "\u001B[?1049h";
|
|
2
|
+
export declare const ALT_SCREEN_LEAVE = "\u001B[?1049l";
|
|
3
|
+
export declare const CURSOR_HIDE = "\u001B[?25l";
|
|
4
|
+
export declare const CURSOR_SHOW = "\u001B[?25h";
|
|
5
|
+
export declare const CURSOR_HOME = "\u001B[H";
|
|
6
|
+
export declare const CLEAR_SCREEN = "\u001B[2J";
|
|
7
|
+
export declare const CLEAR_LINE_END = "\u001B[K";
|
|
8
|
+
export declare function stripAnsi(text: string): string;
|
|
9
|
+
/** ANSI-aware column count (per code unit; see module comment). */
|
|
10
|
+
export declare function visibleWidth(text: string): number;
|
|
11
|
+
/**
|
|
12
|
+
* Truncates `text` to at most `width` visible columns, appending `ellipsis`
|
|
13
|
+
* when truncation happens. ANSI escape sequences are copied through intact
|
|
14
|
+
* (never split) and do not count toward the width. If any escape sequence
|
|
15
|
+
* was emitted before the cut, a style reset is appended so styling does not
|
|
16
|
+
* bleed past the truncated text.
|
|
17
|
+
*
|
|
18
|
+
* Invariant: visibleWidth(result) <= width. When `width` is smaller than the
|
|
19
|
+
* ellipsis itself, a slice of the ellipsis is returned to keep the invariant.
|
|
20
|
+
*/
|
|
21
|
+
export declare function truncateVisible(text: string, width: number, ellipsis?: string): string;
|
|
22
|
+
/** Right-pads `text` with spaces up to `width` visible columns (no-op when already wider). */
|
|
23
|
+
export declare function padVisible(text: string, width: number): string;
|
package/dist/tui/ansi.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Escape-code constants and ANSI-aware string math for the TUI renderer.
|
|
2
|
+
// Pure string helpers only: no I/O, no terminal capability detection.
|
|
3
|
+
//
|
|
4
|
+
// Width is counted per UTF-16 code unit after stripping ANSI CSI sequences.
|
|
5
|
+
// Grapheme clusters and East Asian wide characters are intentionally not
|
|
6
|
+
// handled (no grapheme lib); this is acceptable for the status/log text the
|
|
7
|
+
// TUI renders and keeps these helpers dependency-free.
|
|
8
|
+
export const ALT_SCREEN_ENTER = "\x1b[?1049h";
|
|
9
|
+
export const ALT_SCREEN_LEAVE = "\x1b[?1049l";
|
|
10
|
+
export const CURSOR_HIDE = "\x1b[?25l";
|
|
11
|
+
export const CURSOR_SHOW = "\x1b[?25h";
|
|
12
|
+
export const CURSOR_HOME = "\x1b[H";
|
|
13
|
+
export const CLEAR_SCREEN = "\x1b[2J";
|
|
14
|
+
export const CLEAR_LINE_END = "\x1b[K";
|
|
15
|
+
const ESC = "\x1b";
|
|
16
|
+
const STYLE_RESET = "\x1b[0m";
|
|
17
|
+
/**
|
|
18
|
+
* Returns the length of the ANSI CSI escape sequence starting at `start`,
|
|
19
|
+
* or 0 when `start` does not begin a well-formed CSI sequence.
|
|
20
|
+
*
|
|
21
|
+
* CSI grammar: ESC `[` then parameter/intermediate bytes (0x20-0x3f)
|
|
22
|
+
* terminated by a final byte (0x40-0x7e).
|
|
23
|
+
*/
|
|
24
|
+
function escapeSequenceLength(text, start) {
|
|
25
|
+
if (text[start] !== ESC || text[start + 1] !== "[") {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
let index = start + 2;
|
|
29
|
+
while (index < text.length) {
|
|
30
|
+
const code = text.charCodeAt(index);
|
|
31
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
32
|
+
return index - start + 1;
|
|
33
|
+
}
|
|
34
|
+
if (code < 0x20 || code > 0x3f) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
export function stripAnsi(text) {
|
|
42
|
+
let out = "";
|
|
43
|
+
let index = 0;
|
|
44
|
+
while (index < text.length) {
|
|
45
|
+
const skip = escapeSequenceLength(text, index);
|
|
46
|
+
if (skip > 0) {
|
|
47
|
+
index += skip;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
out += text[index];
|
|
51
|
+
index += 1;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** ANSI-aware column count (per code unit; see module comment). */
|
|
56
|
+
export function visibleWidth(text) {
|
|
57
|
+
return stripAnsi(text).length;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Truncates `text` to at most `width` visible columns, appending `ellipsis`
|
|
61
|
+
* when truncation happens. ANSI escape sequences are copied through intact
|
|
62
|
+
* (never split) and do not count toward the width. If any escape sequence
|
|
63
|
+
* was emitted before the cut, a style reset is appended so styling does not
|
|
64
|
+
* bleed past the truncated text.
|
|
65
|
+
*
|
|
66
|
+
* Invariant: visibleWidth(result) <= width. When `width` is smaller than the
|
|
67
|
+
* ellipsis itself, a slice of the ellipsis is returned to keep the invariant.
|
|
68
|
+
*/
|
|
69
|
+
export function truncateVisible(text, width, ellipsis = "…") {
|
|
70
|
+
if (visibleWidth(text) <= width) {
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
if (width <= 0) {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
if (width < ellipsis.length) {
|
|
77
|
+
return ellipsis.slice(0, width);
|
|
78
|
+
}
|
|
79
|
+
const budget = width - ellipsis.length;
|
|
80
|
+
let out = "";
|
|
81
|
+
let visible = 0;
|
|
82
|
+
let sawEscape = false;
|
|
83
|
+
let index = 0;
|
|
84
|
+
while (index < text.length && visible < budget) {
|
|
85
|
+
const skip = escapeSequenceLength(text, index);
|
|
86
|
+
if (skip > 0) {
|
|
87
|
+
out += text.slice(index, index + skip);
|
|
88
|
+
sawEscape = true;
|
|
89
|
+
index += skip;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
out += text[index];
|
|
93
|
+
visible += 1;
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
return out + ellipsis + (sawEscape ? STYLE_RESET : "");
|
|
97
|
+
}
|
|
98
|
+
/** Right-pads `text` with spaces up to `width` visible columns (no-op when already wider). */
|
|
99
|
+
export function padVisible(text, width) {
|
|
100
|
+
const missing = width - visibleWidth(text);
|
|
101
|
+
return missing > 0 ? text + " ".repeat(missing) : text;
|
|
102
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RunSnapshot, StepDetailSnapshot } from "../types.js";
|
|
2
|
+
import type { TuiLogStore } from "./logStore.js";
|
|
3
|
+
import type { Theme } from "./theme.js";
|
|
4
|
+
export interface TuiFrameState {
|
|
5
|
+
snapshot: RunSnapshot | null;
|
|
6
|
+
stepDetails: Map<string, StepDetailSnapshot>;
|
|
7
|
+
logs: TuiLogStore;
|
|
8
|
+
selectedStepKey: string | null;
|
|
9
|
+
follow: boolean;
|
|
10
|
+
attachUrl: string;
|
|
11
|
+
attachToken: string;
|
|
12
|
+
statusMessage?: string | null;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
now: number;
|
|
16
|
+
theme: Theme;
|
|
17
|
+
}
|
|
18
|
+
export declare function renderFrame(state: TuiFrameState): string[];
|