@workflow-manager/runner 0.6.0 → 0.7.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 +1 -1
- package/dist/cliRunRenderer.js +1 -56
- package/dist/index.js +57 -32
- package/dist/manPage.d.ts +1 -1
- package/dist/manPage.js +4 -1
- package/dist/runFormat.d.ts +11 -0
- package/dist/runFormat.js +56 -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 +305 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Use `wfm doctor` to inspect host adapter setup and `wfm doctor <workflow>` to va
|
|
|
78
78
|
|
|
79
79
|
During `wfm run`, the CLI starts a local attach API on `127.0.0.1`. Use `--port <n>` to bind a fixed port or omit it to let the OS choose one. The CLI prints the attach base URL and bearer token before execution starts.
|
|
80
80
|
|
|
81
|
-
By default, `wfm run` now prints live workflow progress to stderr with the current step, elapsed workflow time, and remaining step count. Pass `--verbose` to stream per-step agent output chunks and execution status updates while the workflow is running. When a human approval is required in an interactive terminal, the CLI now prints an approval summary and prompts for approve or cancel inline.
|
|
81
|
+
By default, `wfm run` now prints live workflow progress to stderr with the current step, elapsed workflow time, and remaining step count. Pass `--verbose` to stream per-step agent output chunks and execution status updates while the workflow is running. When a human approval is required in an interactive terminal, the CLI now prints an approval summary and prompts for approve or cancel inline. Pass `--ui` for a full-screen terminal UI with a live step list and per-step activity pane instead (falls back to standard output when stdin/stdout aren't an interactive terminal); see `doc/guide/terminal-ui.md`.
|
|
82
82
|
|
|
83
83
|
Runner API endpoints include:
|
|
84
84
|
|
package/dist/cliRunRenderer.js
CHANGED
|
@@ -1,56 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return status === "succeeded" || status === "failed" || status === "cancelled";
|
|
3
|
-
}
|
|
4
|
-
function formatCount(value, singular, plural) {
|
|
5
|
-
return `${value} ${value === 1 ? singular : plural}`;
|
|
6
|
-
}
|
|
7
|
-
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
|
-
function elapsedFrom(iso) {
|
|
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(Date.now() - startedAt);
|
|
31
|
-
}
|
|
32
|
-
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
|
-
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
|
-
}
|
|
1
|
+
import { describeAgentStatus, elapsedFrom, formatCount, isTerminalStatus, splitLines, stepLabel, } from "./runFormat.js";
|
|
54
2
|
export class CliRunRenderer {
|
|
55
3
|
verbose;
|
|
56
4
|
stream;
|
|
@@ -281,6 +229,3 @@ export class CliRunRenderer {
|
|
|
281
229
|
this.stream.write(`${line}\n`);
|
|
282
230
|
}
|
|
283
231
|
}
|
|
284
|
-
function stepLabel(step) {
|
|
285
|
-
return step.title ?? step.objective ?? step.key;
|
|
286
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import matter from "gray-matter";
|
|
9
9
|
import { CliRunRenderer } from "./cliRunRenderer.js";
|
|
10
|
+
import { TuiRunRenderer } from "./tui/tuiRunRenderer.js";
|
|
10
11
|
import { startRunnerApiServer } from "./runnerApi.js";
|
|
11
12
|
import { RunnerSessionStore } from "./runnerSession.js";
|
|
12
13
|
import { parseWorkflowFile, validateWorkflow } from "./parser.js";
|
|
@@ -55,7 +56,7 @@ function usage() {
|
|
|
55
56
|
row("scaffold [path]", "Write a starter workflow file"),
|
|
56
57
|
row("validate <file>", "Check a workflow for errors"),
|
|
57
58
|
row("doctor [file]", "Check host setup; with a file, preflight it"),
|
|
58
|
-
row("run <file>", "Run a workflow with live progress"),
|
|
59
|
+
row("run <file>", "Run a workflow with live progress (--ui for full-screen)"),
|
|
59
60
|
"",
|
|
60
61
|
"Control a running workflow",
|
|
61
62
|
row("approve", "Approve a step waiting for review"),
|
|
@@ -688,6 +689,7 @@ async function cmdRun(filePath) {
|
|
|
688
689
|
let runnerServer;
|
|
689
690
|
let sessionStore;
|
|
690
691
|
let liveRenderer;
|
|
692
|
+
let tuiRenderer;
|
|
691
693
|
try {
|
|
692
694
|
workflow = parseWorkflowFile(resolvedPath);
|
|
693
695
|
const errors = validateWorkflow(workflow);
|
|
@@ -729,57 +731,78 @@ async function cmdRun(filePath) {
|
|
|
729
731
|
console.error("--port must be an integer between 0 and 65535");
|
|
730
732
|
return 1;
|
|
731
733
|
}
|
|
734
|
+
const wantUi = hasFlag("--ui");
|
|
735
|
+
const useTui = wantUi && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
736
|
+
if (wantUi && !useTui) {
|
|
737
|
+
process.stderr.write("⚠ --ui requires an interactive terminal; falling back to standard output\n");
|
|
738
|
+
}
|
|
732
739
|
sessionStore = new RunnerSessionStore({
|
|
733
740
|
runId,
|
|
734
741
|
workflow,
|
|
735
742
|
objective: objective ?? workflow.title,
|
|
736
743
|
objectives: workflow.objectives ?? [],
|
|
737
744
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
745
|
+
if (!useTui) {
|
|
746
|
+
liveRenderer = new CliRunRenderer({
|
|
747
|
+
workflow,
|
|
748
|
+
verbose: hasFlag("--verbose"),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
742
751
|
runnerServer = await startRunnerApiServer(sessionStore, requestedPort);
|
|
743
752
|
const session = sessionStore.sessionInfo();
|
|
744
|
-
|
|
753
|
+
if (!useTui) {
|
|
754
|
+
process.stderr.write(`Attach API: ${session.baseUrl} (token ${session.attachToken})\n`);
|
|
755
|
+
}
|
|
756
|
+
if (useTui) {
|
|
757
|
+
tuiRenderer = new TuiRunRenderer({
|
|
758
|
+
workflow,
|
|
759
|
+
session: sessionStore,
|
|
760
|
+
attachUrl: session.baseUrl,
|
|
761
|
+
attachToken: session.attachToken,
|
|
762
|
+
});
|
|
763
|
+
tuiRenderer.start();
|
|
764
|
+
}
|
|
745
765
|
const result = await runWorkflow(workflow, {
|
|
746
766
|
runId,
|
|
747
767
|
objective,
|
|
748
768
|
input,
|
|
749
769
|
confirmations,
|
|
750
770
|
autoConfirmAll: hasFlag("--auto-confirm-all"),
|
|
751
|
-
interactive: process.stdin.isTTY,
|
|
771
|
+
interactive: useTui ? false : process.stdin.isTTY,
|
|
752
772
|
workflowFilePath: resolvedPath,
|
|
753
|
-
approvalPrompt:
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
773
|
+
approvalPrompt: useTui
|
|
774
|
+
? undefined
|
|
775
|
+
: async (request) => {
|
|
776
|
+
liveRenderer?.pauseHeartbeat();
|
|
777
|
+
try {
|
|
778
|
+
const decision = await promptForApprovalDecision(request.stepKey, request.reason, request.validation ?? "external", request.preview ?? null, "cli", request.signal);
|
|
779
|
+
if (!decision) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
const metadata = {
|
|
783
|
+
actor: decision.actor,
|
|
784
|
+
note: decision.note,
|
|
785
|
+
source: decision.source,
|
|
786
|
+
};
|
|
787
|
+
const outcome = decision.decision === "cancelled"
|
|
788
|
+
? sessionStore?.cancel(request.stepKey, metadata)
|
|
789
|
+
: request.validation === "external"
|
|
790
|
+
? sessionStore?.resume(request.stepKey, metadata)
|
|
791
|
+
: sessionStore?.approve(request.stepKey, metadata);
|
|
792
|
+
if (outcome && !outcome.ok) {
|
|
793
|
+
process.stderr.write(`Could not apply terminal decision for ${request.stepKey}: ${outcome.reason ?? "unknown error"}\n`);
|
|
794
|
+
}
|
|
758
795
|
return null;
|
|
759
796
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
note: decision.note,
|
|
763
|
-
source: decision.source,
|
|
764
|
-
};
|
|
765
|
-
const outcome = decision.decision === "cancelled"
|
|
766
|
-
? sessionStore?.cancel(request.stepKey, metadata)
|
|
767
|
-
: request.validation === "external"
|
|
768
|
-
? sessionStore?.resume(request.stepKey, metadata)
|
|
769
|
-
: sessionStore?.approve(request.stepKey, metadata);
|
|
770
|
-
if (outcome && !outcome.ok) {
|
|
771
|
-
process.stderr.write(`Could not apply terminal decision for ${request.stepKey}: ${outcome.reason ?? "unknown error"}\n`);
|
|
797
|
+
finally {
|
|
798
|
+
liveRenderer?.resumeHeartbeat();
|
|
772
799
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
finally {
|
|
776
|
-
liveRenderer?.resumeHeartbeat();
|
|
777
|
-
}
|
|
778
|
-
},
|
|
779
|
-
observer: combineRunObservers(sessionStore, liveRenderer),
|
|
800
|
+
},
|
|
801
|
+
observer: combineRunObservers(sessionStore, useTui ? tuiRenderer : liveRenderer),
|
|
780
802
|
controller: sessionStore,
|
|
781
803
|
});
|
|
782
|
-
|
|
804
|
+
tuiRenderer?.stop();
|
|
805
|
+
liveRenderer?.close();
|
|
783
806
|
if (hasFlag("--json")) {
|
|
784
807
|
console.log(JSON.stringify({ session: sessionStore.sessionInfo(), ...result }, null, 2));
|
|
785
808
|
}
|
|
@@ -807,6 +830,7 @@ async function cmdRun(filePath) {
|
|
|
807
830
|
return result.status === "succeeded" ? 0 : 2;
|
|
808
831
|
}
|
|
809
832
|
catch (err) {
|
|
833
|
+
tuiRenderer?.stop();
|
|
810
834
|
liveRenderer?.close();
|
|
811
835
|
console.error(`Run error: ${err.message}`);
|
|
812
836
|
if (workflow) {
|
|
@@ -820,6 +844,7 @@ async function cmdRun(filePath) {
|
|
|
820
844
|
return 1;
|
|
821
845
|
}
|
|
822
846
|
finally {
|
|
847
|
+
tuiRenderer?.stop();
|
|
823
848
|
liveRenderer?.close();
|
|
824
849
|
await runnerServer?.close().catch(() => undefined);
|
|
825
850
|
}
|
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 cancel commands.\n.TP\n.B --token <value>\nRunner attach API bearer token for approve, resume, or cancel 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.\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\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\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";
|
|
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] [--ui]\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\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, or cancel commands.\n.TP\n.B --token <value>\nRunner attach API bearer token for approve, resume, or cancel 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.\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\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\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
|
@@ -28,7 +28,7 @@ path ends in .json.
|
|
|
28
28
|
.B validate <workflow.md|workflow.json>
|
|
29
29
|
Validate workflow structure and report schema errors.
|
|
30
30
|
.TP
|
|
31
|
-
.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json]
|
|
31
|
+
.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json] [--ui]
|
|
32
32
|
Run the workflow with live CLI progress and optional JSON output.
|
|
33
33
|
.TP
|
|
34
34
|
.B approve [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
@@ -83,6 +83,9 @@ Stream per-step agent output and execution updates to stderr while the workflow
|
|
|
83
83
|
.B --json
|
|
84
84
|
Print the final run result as JSON on stdout while keeping live progress on stderr.
|
|
85
85
|
.TP
|
|
86
|
+
.B --ui
|
|
87
|
+
Full-screen terminal UI (requires a TTY; falls back to standard output).
|
|
88
|
+
.TP
|
|
86
89
|
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
90
|
.TP
|
|
88
91
|
.B --url <value>
|
|
@@ -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
|
+
}
|
|
@@ -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[];
|