@smithers-orchestrator/cli 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +55 -0
  3. package/src/AgentAvailability.ts +13 -0
  4. package/src/AgentAvailabilityStatus.ts +5 -0
  5. package/src/AggregateNodeDetailParams.ts +5 -0
  6. package/src/AskOptions.ts +12 -0
  7. package/src/ChatAttemptMeta.ts +7 -0
  8. package/src/ChatAttemptRow.ts +12 -0
  9. package/src/ChatOutputEvent.ts +6 -0
  10. package/src/DiffBundleLike.ts +6 -0
  11. package/src/DiscoveredWorkflow.ts +9 -0
  12. package/src/EnrichedNodeDetail.ts +60 -0
  13. package/src/EventCategory.ts +18 -0
  14. package/src/FindDbWaitOptions.ts +4 -0
  15. package/src/FormatEventLineOptions.ts +4 -0
  16. package/src/HijackCandidate.ts +11 -0
  17. package/src/HijackLaunchSpec.ts +6 -0
  18. package/src/InitWorkflowPackOptions.ts +4 -0
  19. package/src/InitWorkflowPackResult.ts +6 -0
  20. package/src/NativeHijackEngine.ts +8 -0
  21. package/src/NodeDetailAttempt.ts +22 -0
  22. package/src/NodeDetailTokenUsage.ts +11 -0
  23. package/src/NodeDetailToolCall.ts +12 -0
  24. package/src/ParsedNodeOutputEvent.ts +9 -0
  25. package/src/RenderNodeDetailOptions.ts +4 -0
  26. package/src/RunAutoResumeSkipReason.ts +4 -0
  27. package/src/RunDiffCommandInput.ts +13 -0
  28. package/src/RunDiffCommandResult.ts +3 -0
  29. package/src/RunOutputCommandInput.ts +12 -0
  30. package/src/RunOutputCommandResult.ts +3 -0
  31. package/src/RunRewindCommandInput.ts +14 -0
  32. package/src/RunRewindCommandResult.ts +3 -0
  33. package/src/RunTreeCommandInput.ts +14 -0
  34. package/src/RunTreeCommandResult.ts +3 -0
  35. package/src/SmithersEventType.ts +3 -0
  36. package/src/SupervisorOptions.ts +33 -0
  37. package/src/SupervisorPollSummary.ts +6 -0
  38. package/src/TreeRenderOptions.ts +5 -0
  39. package/src/WatchLoopOptions.ts +9 -0
  40. package/src/WatchLoopResult.ts +8 -0
  41. package/src/WatchRenderContext.ts +4 -0
  42. package/src/WhyBlocker.ts +17 -0
  43. package/src/WhyBlockerKind.ts +9 -0
  44. package/src/WhyDiagnosis.ts +10 -0
  45. package/src/WorkflowCta.ts +4 -0
  46. package/src/WorkflowSourceType.ts +1 -0
  47. package/src/agent-detection.js +257 -0
  48. package/src/ask.js +491 -0
  49. package/src/chat.js +226 -0
  50. package/src/diff.js +221 -0
  51. package/src/event-categories.js +141 -0
  52. package/src/find-db.js +93 -0
  53. package/src/format.js +272 -0
  54. package/src/hijack-session.js +207 -0
  55. package/src/hijack.js +226 -0
  56. package/src/index.d.ts +1 -0
  57. package/src/index.js +4868 -0
  58. package/src/mcp/SemanticMcpServerOptions.ts +4 -0
  59. package/src/mcp/SemanticToolCallResult.ts +14 -0
  60. package/src/mcp/SemanticToolContext.ts +6 -0
  61. package/src/mcp/SemanticToolDefinition.ts +13 -0
  62. package/src/mcp/SemanticToolError.ts +6 -0
  63. package/src/mcp/semantic-server.js +41 -0
  64. package/src/mcp/semantic-tools.js +1242 -0
  65. package/src/node-detail.js +682 -0
  66. package/src/output.js +111 -0
  67. package/src/resume-detached.js +37 -0
  68. package/src/rewind.js +88 -0
  69. package/src/scheduler.js +112 -0
  70. package/src/smithersRuntime.js +63 -0
  71. package/src/supervisor.js +418 -0
  72. package/src/tree.js +307 -0
  73. package/src/tui/app.jsx +139 -0
  74. package/src/tui/app.tsx +5 -0
  75. package/src/tui/components/AskModal.jsx +109 -0
  76. package/src/tui/components/AskModal.tsx +3 -0
  77. package/src/tui/components/AttentionPane.jsx +112 -0
  78. package/src/tui/components/AttentionPane.tsx +6 -0
  79. package/src/tui/components/ChatPane.jsx +57 -0
  80. package/src/tui/components/ChatPane.tsx +7 -0
  81. package/src/tui/components/CronList.jsx +87 -0
  82. package/src/tui/components/CronList.tsx +5 -0
  83. package/src/tui/components/DetailsPane.jsx +96 -0
  84. package/src/tui/components/DetailsPane.tsx +7 -0
  85. package/src/tui/components/FramesPane.jsx +147 -0
  86. package/src/tui/components/FramesPane.tsx +8 -0
  87. package/src/tui/components/LogsPane.jsx +46 -0
  88. package/src/tui/components/LogsPane.tsx +6 -0
  89. package/src/tui/components/MetricsPane.jsx +108 -0
  90. package/src/tui/components/MetricsPane.tsx +5 -0
  91. package/src/tui/components/NodeDetailView.jsx +284 -0
  92. package/src/tui/components/NodeDetailView.tsx +7 -0
  93. package/src/tui/components/NodeInspector.jsx +51 -0
  94. package/src/tui/components/NodeInspector.tsx +7 -0
  95. package/src/tui/components/RunDetailView.jsx +190 -0
  96. package/src/tui/components/RunDetailView.tsx +7 -0
  97. package/src/tui/components/RunsList.jsx +184 -0
  98. package/src/tui/components/RunsList.tsx +7 -0
  99. package/src/tui/components/SqliteBrowser.jsx +131 -0
  100. package/src/tui/components/SqliteBrowser.tsx +5 -0
  101. package/src/tui/components/WorkflowLauncher.jsx +63 -0
  102. package/src/tui/components/WorkflowLauncher.tsx +3 -0
  103. package/src/util/CliErrorMapping.ts +7 -0
  104. package/src/util/CliExitCode.ts +10 -0
  105. package/src/util/errorMessage.js +212 -0
  106. package/src/util/exitCodes.js +18 -0
  107. package/src/watch.js +128 -0
  108. package/src/why-diagnosis.js +1000 -0
  109. package/src/workflow-pack.js +2151 -0
  110. package/src/workflows.js +122 -0
@@ -0,0 +1,184 @@
1
+ // @ts-nocheck
2
+ import React, { useEffect, useState } from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ import { formatAge } from "../../format.js";
5
+ import { basename } from "node:path";
6
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
7
+
8
+ /**
9
+ * @param {{ adapter: SmithersDb; focused: boolean; onChange: (runId: string) => void; onSubmit: (runId: string) => void; }} value
10
+ */
11
+ export function RunsList({ adapter, focused, onChange, onSubmit, }) {
12
+ const [runs, setRuns] = useState([]);
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ const [filterMode, setFilterMode] = useState("all");
15
+ const [selectedRunStats, setSelectedRunStats] = useState(null);
16
+ useEffect(() => {
17
+ let mounted = true;
18
+ async function pollRuns() {
19
+ if (!mounted)
20
+ return;
21
+ try {
22
+ const fetchedRuns = await adapter.listRuns(20, filterMode === "pending" ? "waiting-approval" : undefined);
23
+ if (mounted) {
24
+ setRuns(fetchedRuns);
25
+ if (fetchedRuns.length > 0 && selectedIndex === 0) {
26
+ // Auto select the first run initially if nothing was selected
27
+ // But we don't want to call onSelect constantly, so we'll just let the list render.
28
+ }
29
+ }
30
+ }
31
+ catch (err) { }
32
+ if (mounted)
33
+ setTimeout(pollRuns, 1000);
34
+ }
35
+ pollRuns();
36
+ return () => {
37
+ mounted = false;
38
+ };
39
+ }, [adapter, selectedIndex, filterMode]);
40
+ useEffect(() => {
41
+ if (runs.length > 0) {
42
+ onChange(runs[selectedIndex]?.runId ?? runs[0].runId);
43
+ }
44
+ }, [runs, selectedIndex, onChange]);
45
+ const selectedRun = runs[selectedIndex];
46
+ useEffect(() => {
47
+ let mounted = true;
48
+ if (!selectedRun) {
49
+ setSelectedRunStats(null);
50
+ return;
51
+ }
52
+ async function fetchStats() {
53
+ try {
54
+ const events = await adapter.listEvents(selectedRun.runId, -1, 10000);
55
+ const nodes = await adapter.listNodes(selectedRun.runId);
56
+ let tIn = 0, tOut = 0, tCache = 0;
57
+ for (const e of events) {
58
+ if (e.type === "TokenUsageReported") {
59
+ try {
60
+ const p = JSON.parse(e.payloadJson);
61
+ tIn += (p.inputTokens ?? 0);
62
+ tOut += (p.outputTokens ?? 0);
63
+ tCache += (p.cacheReadTokens ?? 0);
64
+ }
65
+ catch { }
66
+ }
67
+ }
68
+ const sMs = selectedRun.startedAtMs || selectedRun.createdAtMs;
69
+ const durationMs = selectedRun.finishedAtMs && sMs
70
+ ? selectedRun.finishedAtMs - sMs
71
+ : sMs ? Date.now() - sMs : 0;
72
+ if (mounted) {
73
+ setSelectedRunStats({
74
+ tokensIn: tIn,
75
+ tokensOut: tOut,
76
+ tokensCache: tCache,
77
+ durationMs,
78
+ nodeCount: nodes.length,
79
+ });
80
+ }
81
+ }
82
+ catch { }
83
+ }
84
+ fetchStats();
85
+ return () => { mounted = false; };
86
+ }, [adapter, selectedRun?.runId]);
87
+ useKeyboard((key) => {
88
+ if (!focused)
89
+ return;
90
+ if (key.name === "p" || key.name === "P") {
91
+ setFilterMode((m) => m === "all" ? "pending" : "all");
92
+ setSelectedIndex(0);
93
+ return;
94
+ }
95
+ if (runs[selectedIndex]) {
96
+ const runId = runs[selectedIndex].runId;
97
+ const status = runs[selectedIndex].status;
98
+ if (key.name === "enter" || key.name === "return") {
99
+ onSubmit(runId);
100
+ return;
101
+ }
102
+ if (key.name === "y" && status === "waiting-approval") {
103
+ Bun.spawn(["bun", "run", "src/index.js", "approve", runId], { stdout: "ignore", stderr: "ignore" }).unref();
104
+ return;
105
+ }
106
+ if (key.name === "d" && status === "waiting-approval") {
107
+ Bun.spawn(["bun", "run", "src/index.js", "deny", runId], { stdout: "ignore", stderr: "ignore" }).unref();
108
+ return;
109
+ }
110
+ if (key.name === "c" && (status === "running" || status === "waiting-timer")) {
111
+ Bun.spawn(["bun", "run", "src/index.js", "cancel", runId], { stdout: "ignore", stderr: "ignore" }).unref();
112
+ return;
113
+ }
114
+ if (key.name === "r" && (status === "failed" || status === "cancelled")) {
115
+ const path = runs[selectedIndex].workflowPath;
116
+ if (path) {
117
+ Bun.spawn(["bun", "run", "src/index.js", "up", path, "--resume", "--runId", runId, "-d"], { stdout: "ignore", stderr: "ignore" }).unref();
118
+ }
119
+ return;
120
+ }
121
+ }
122
+ if (key.name === "k") {
123
+ Bun.spawn(["bun", "run", "src/index.js", "down"], { stdout: "ignore", stderr: "ignore" }).unref();
124
+ return;
125
+ }
126
+ });
127
+ const options = runs.map((run) => {
128
+ const workflowName = run.workflowName ?? (run.workflowPath ? basename(run.workflowPath) : "—");
129
+ const started = run.startedAtMs ? formatAge(run.startedAtMs) : run.createdAtMs ? formatAge(run.createdAtMs) : "—";
130
+ return {
131
+ name: `${workflowName} (${run.status})`,
132
+ description: `${run.runId.slice(-6)} - ${started}`,
133
+ value: run.runId,
134
+ };
135
+ });
136
+ if (runs.length === 0) {
137
+ return <text style={{ margin: 1 }}>No runs found.</text>;
138
+ }
139
+ let truncatedOutput = selectedRun?.errorJson;
140
+ if (!truncatedOutput) {
141
+ if (selectedRun?.status === "finished") {
142
+ truncatedOutput = "Run completed successfully without generic failure traces.\nPress [Enter] to inspect individual task payloads.";
143
+ }
144
+ else if (selectedRun?.status === "failed") {
145
+ truncatedOutput = "Workflow failed, but no global error stack trace was captured. Press [Enter] to inspect which individual node crashed.";
146
+ }
147
+ else {
148
+ truncatedOutput = "Workflow is still active or pending execution...";
149
+ }
150
+ }
151
+ if (truncatedOutput.length > 500) {
152
+ const head = truncatedOutput.substring(0, 200);
153
+ const tail = truncatedOutput.substring(truncatedOutput.length - 200);
154
+ truncatedOutput = `${head}\n\n... [ TRUNCATED ${truncatedOutput.length - 400} CHARACTERS ] ...\n\n${tail}`;
155
+ }
156
+ return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
157
+ {/* Left List */}
158
+ <box style={{ width: 40, height: "100%", borderRight: true, borderColor: "#34d399", flexDirection: "column" }}>
159
+ <select style={{ width: "100%", height: "100%" }} options={options} focused={focused} onChange={(index) => {
160
+ setSelectedIndex(index);
161
+ }} textColor="#E2E8F0" selectedTextColor="#34d399" selectedBackgroundColor="#1f2937" descriptionColor="#9ca3af" selectedDescriptionColor="#a7f3d0" showScrollIndicator wrapSelection/>
162
+ </box>
163
+
164
+ {/* Right Preview */}
165
+ <box style={{ flexGrow: 1, height: "100%", flexDirection: "column", paddingLeft: 1 }}>
166
+ <text style={{ color: "yellow" }}>State: {selectedRun?.status}</text>
167
+ {selectedRunStats && (<text style={{ color: "#a855f7", marginTop: 1 }}>
168
+ ⏱️ {(selectedRunStats.durationMs / 1000).toFixed(1)}s | 🧩 {selectedRunStats.nodeCount} Tasks | 🪙 {selectedRunStats.tokensIn} IN, {selectedRunStats.tokensOut} OUT, {selectedRunStats.tokensCache} CACHE
169
+ </text>)}
170
+ <text style={{ color: "gray", marginTop: 1 }}>--- Run Status / Error Summary ---</text>
171
+ <scrollbox style={{ flexGrow: 1, width: "100%", marginTop: 1 }}>
172
+ <text>{truncatedOutput}</text>
173
+ </scrollbox>
174
+
175
+ {/* Action Footer */}
176
+ <box style={{ width: "100%", height: 3, borderTop: true, borderColor: "gray", flexDirection: "column" }}>
177
+ <text style={{ color: "#93c5fd" }}>{filterMode === "all" ? "[P] Filter: Show Pending Approvals Only" : "[P] Filter: Show All Workflows"}</text>
178
+ {selectedRun?.status === "waiting-approval" && <text style={{ color: "green" }}>[Y] Approve | [D] Deny Human Task</text>}
179
+ {(selectedRun?.status === "running" || selectedRun?.status === "waiting-timer") && <text style={{ color: "red" }}>[C] Safely Cancel/Halt Workflow | [K] Kill All Active Workflows</text>}
180
+ {(selectedRun?.status === "failed" || selectedRun?.status === "cancelled") && <text style={{ color: "yellow" }}>[R] Resume from latest checkpoint</text>}
181
+ </box>
182
+ </box>
183
+ </box>);
184
+ }
@@ -0,0 +1,7 @@
1
+ import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
+ export declare function RunsList({ adapter, focused, onChange, onSubmit, }: {
3
+ adapter: SmithersDb;
4
+ focused: boolean;
5
+ onChange: (runId: string) => void;
6
+ onSubmit: (runId: string) => void;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,131 @@
1
+ // @ts-nocheck
2
+ import React, { useEffect, useState } from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
5
+
6
+ /**
7
+ * @param {{ adapter: SmithersDb; onBack: () => void; }} value
8
+ */
9
+ export function SqliteBrowser({ adapter, onBack, }) {
10
+ const [tables, setTables] = useState([]);
11
+ const [selectedTableIdx, setSelectedTableIdx] = useState(0);
12
+ const [query, setQuery] = useState("");
13
+ const [results, setResults] = useState([]);
14
+ const [error, setError] = useState(null);
15
+ const [focusedPane, setFocusedPane] = useState("tables");
16
+ // Fetch tables on mount
17
+ useEffect(() => {
18
+ let mounted = true;
19
+ adapter.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
20
+ .then((res) => {
21
+ if (mounted) {
22
+ const names = res.map((r) => r.name).sort();
23
+ setTables(names);
24
+ if (names.length > 0) {
25
+ setQuery(`SELECT * FROM ${names[0]} LIMIT 50;`);
26
+ }
27
+ }
28
+ })
29
+ .catch((err) => {
30
+ if (mounted)
31
+ setError(err.message);
32
+ });
33
+ return () => { mounted = false; };
34
+ }, [adapter]);
35
+ // Execute query whenever it changes
36
+ useEffect(() => {
37
+ let mounted = true;
38
+ if (!query.trim()) {
39
+ setResults([]);
40
+ return;
41
+ }
42
+ adapter.rawQuery(query)
43
+ .then((res) => {
44
+ if (mounted) {
45
+ setResults(res);
46
+ setError(null);
47
+ }
48
+ })
49
+ .catch((err) => {
50
+ if (mounted) {
51
+ setError(err.message);
52
+ setResults([]);
53
+ }
54
+ });
55
+ return () => { mounted = false; };
56
+ }, [adapter, query]);
57
+ useKeyboard((key) => {
58
+ if (key.name === "escape") {
59
+ if (focusedPane === "query") {
60
+ setFocusedPane("tables");
61
+ }
62
+ else {
63
+ onBack();
64
+ }
65
+ return;
66
+ }
67
+ if (key.name === "tab") {
68
+ setFocusedPane((p) => p === "tables" ? "query" : p === "query" ? "results" : "tables");
69
+ return;
70
+ }
71
+ if (focusedPane === "tables" && tables.length > 0) {
72
+ if (key.name === "down" || key.name === "j") {
73
+ const next = Math.min(tables.length - 1, selectedTableIdx + 1);
74
+ setSelectedTableIdx(next);
75
+ setQuery(`SELECT * FROM ${tables[next]} LIMIT 50;`);
76
+ }
77
+ if (key.name === "up" || key.name === "k") {
78
+ const prev = Math.max(0, selectedTableIdx - 1);
79
+ setSelectedTableIdx(prev);
80
+ setQuery(`SELECT * FROM ${tables[prev]} LIMIT 50;`);
81
+ }
82
+ }
83
+ if (focusedPane === "query") {
84
+ if (key.name === "backspace") {
85
+ setQuery((q) => q.slice(0, -1));
86
+ return;
87
+ }
88
+ if (key.name === "enter" || key.name === "return") {
89
+ setFocusedPane("results");
90
+ return;
91
+ }
92
+ if (!key.ctrl && !key.meta && key.sequence && key.sequence.length === 1 && key.name !== "up" && key.name !== "down" && key.name !== "left" && key.name !== "right") {
93
+ setQuery((q) => q + key.sequence);
94
+ }
95
+ }
96
+ });
97
+ const tableOptions = tables.map((t) => ({
98
+ name: t,
99
+ value: t,
100
+ }));
101
+ const resultText = error
102
+ ? `[!] Query Error:\n${error}`
103
+ : results.length === 0
104
+ ? "No results."
105
+ : JSON.stringify(results, null, 2);
106
+ return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
107
+ {/* Left Pane: Tables List */}
108
+ <box style={{ width: 30, height: "100%", borderRight: true, borderColor: focusedPane === "tables" ? "#34d399" : "gray", flexDirection: "column" }}>
109
+ <text style={{ color: focusedPane === "tables" ? "white" : "gray", marginBottom: 1 }}> Tables </text>
110
+ <select style={{ width: "100%", flexGrow: 1 }} options={tableOptions} focused={focusedPane === "tables"} onChange={(idx) => {
111
+ setSelectedTableIdx(idx);
112
+ setQuery(`SELECT * FROM ${tables[idx]} LIMIT 50;`);
113
+ }} textColor="#E2E8F0" selectedTextColor="#34d399" selectedBackgroundColor="#1f2937"/>
114
+ </box>
115
+
116
+ {/* Right Pane: Query & Results */}
117
+ <box style={{ flexGrow: 1, height: "100%", flexDirection: "column", paddingLeft: 1 }}>
118
+ <box style={{ width: "100%", height: 3, borderBottom: true, borderColor: focusedPane === "query" ? "#34d399" : "gray", flexDirection: "column" }}>
119
+ <text style={{ color: "yellow" }}> SQL Query: {focusedPane === "query" ? "(Press Enter to run)" : ""} </text>
120
+ {focusedPane === "query" ? (<text style={{ color: "white", marginTop: 1 }}>{"> "}{query}█</text>) : (<text style={{ color: "white", marginTop: 1 }}>{query}</text>)}
121
+ </box>
122
+
123
+ <box style={{ flexGrow: 1, width: "100%", flexDirection: "column", marginTop: 1 }}>
124
+ <text style={{ color: "gray", marginBottom: 1 }}> Results ({results.length} rows) </text>
125
+ <scrollbox style={{ flexGrow: 1, width: "100%" }}>
126
+ <text style={{ color: error ? "red" : "#a7f3d0" }}>{resultText}</text>
127
+ </scrollbox>
128
+ </box>
129
+ </box>
130
+ </box>);
131
+ }
@@ -0,0 +1,5 @@
1
+ import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
+ export declare function SqliteBrowser({ adapter, onBack, }: {
3
+ adapter: SmithersDb;
4
+ onBack: () => void;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,63 @@
1
+ // @ts-nocheck
2
+ import React, { useEffect, useState } from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ import { readdirSync } from "node:fs";
5
+ /**
6
+ * @param {{ onClose: () => void }} value
7
+ */
8
+ export function WorkflowLauncher({ onClose }) {
9
+ const [examples, setExamples] = useState([]);
10
+ const [selectedIndex, setSelectedIndex] = useState(0);
11
+ useEffect(() => {
12
+ try {
13
+ const files = readdirSync("examples").filter((f) => /\.(?:jsx?|tsx?)$/.test(f)).filter(f => !f.startsWith("_"));
14
+ setExamples(files);
15
+ }
16
+ catch {
17
+ setExamples([]);
18
+ }
19
+ }, []);
20
+ useKeyboard((key) => {
21
+ if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
22
+ onClose();
23
+ return;
24
+ }
25
+ if (key.name === "down" || key.name === "j") {
26
+ setSelectedIndex((s) => Math.min(s + 1, Math.max(0, examples.length - 1)));
27
+ }
28
+ if (key.name === "up" || key.name === "k") {
29
+ setSelectedIndex((s) => Math.max(s - 1, 0));
30
+ }
31
+ if (key.name === "enter" || key.name === "return") {
32
+ if (examples[selectedIndex]) {
33
+ // spawn the workflow
34
+ const file = examples[selectedIndex];
35
+ try {
36
+ const proc = Bun.spawn(["bun", "run", "src/index.js", "up", `examples/${file}`, "-d"], {
37
+ stdin: "ignore",
38
+ stdout: "ignore",
39
+ stderr: "ignore",
40
+ });
41
+ proc.unref(); // allow the parent process (TUI) to exit independently
42
+ }
43
+ catch { }
44
+ onClose();
45
+ }
46
+ }
47
+ });
48
+ return (<box style={{
49
+ flexGrow: 1,
50
+ width: "100%",
51
+ height: "100%",
52
+ border: true,
53
+ borderColor: "yellow",
54
+ flexDirection: "column",
55
+ }} title="Launch Workflow [Esc to Close, Enter to Run]">
56
+ <text style={{ margin: 1, color: "gray" }}>Select an example to run in the background:</text>
57
+ {examples.length === 0 ? (<text style={{ margin: 1 }}>No examples found in ./examples</text>) : (<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 2 }}>
58
+ {examples.map((ex, i) => (<text key={ex} style={{ color: selectedIndex === i ? "green" : "white" }}>
59
+ {selectedIndex === i ? "▶ " : " "}{ex}
60
+ </text>))}
61
+ </scrollbox>)}
62
+ </box>);
63
+ }
@@ -0,0 +1,3 @@
1
+ export declare function WorkflowLauncher({ onClose }: {
2
+ onClose: () => void;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import type { CliExitCode } from "./CliExitCode.ts";
2
+
3
+ export type CliErrorMapping = {
4
+ message: string;
5
+ hint: string;
6
+ exitCode: CliExitCode;
7
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Uniform CLI exit codes for the devtools live-run commands.
3
+ *
4
+ * - 0 ok
5
+ * - 1 user error (bad flags, missing id, declined confirmation)
6
+ * - 2 server error (transport, backend, unexpected condition)
7
+ * - 3 declined (user aborted at an interactive prompt)
8
+ * - 130 sigint (ctrl-c during a watch/stream)
9
+ */
10
+ export type CliExitCode = 0 | 1 | 2 | 3 | 130;
@@ -0,0 +1,212 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./CliErrorMapping.ts").CliErrorMapping} CliErrorMapping */
3
+ // @smithers-type-exports-end
4
+
5
+ import {
6
+ EXIT_USER_ERROR,
7
+ EXIT_SERVER_ERROR,
8
+ } from "./exitCodes.js";
9
+
10
+ /**
11
+ * Exhaustive map of every typed error code the four devtools RPCs may
12
+ * return. Each entry yields a user-friendly message plus an actionable
13
+ * hint and the uniform exit code to surface.
14
+ *
15
+ * Codes come from:
16
+ * - getDevToolsSnapshot / streamDevTools (ticket 0010, 0011)
17
+ * - getNodeDiff (ticket 0012)
18
+ * - getNodeOutput (ticket 0012)
19
+ * - jumpToFrame (ticket 0013)
20
+ *
21
+ * Plus two transport-level codes used by the CLI itself when it cannot
22
+ * reach the server or the auth token is missing.
23
+ *
24
+ * @type {Readonly<Record<string, CliErrorMapping>>}
25
+ */
26
+ export const CLI_ERROR_MESSAGES = Object.freeze({
27
+ // ----- Input validation (every Invalid* → exit 1) -----
28
+ InvalidRunId: {
29
+ message: "The run id is not in the expected shape.",
30
+ hint: "Run ids must match /^[a-z0-9_-]{1,64}$/. Check for typos or pick a run from `smithers ps`.",
31
+ exitCode: EXIT_USER_ERROR,
32
+ },
33
+ InvalidNodeId: {
34
+ message: "The node id is not in the expected shape.",
35
+ hint: "Node ids must match /^[a-zA-Z0-9:_-]{1,128}$/. Copy the id from `smithers tree` or `smithers node`.",
36
+ exitCode: EXIT_USER_ERROR,
37
+ },
38
+ InvalidIteration: {
39
+ message: "The iteration number is invalid.",
40
+ hint: "Iteration must be a non-negative 32-bit integer. Omit --iteration to use the latest.",
41
+ exitCode: EXIT_USER_ERROR,
42
+ },
43
+ InvalidFrameNo: {
44
+ message: "The frame number is invalid.",
45
+ hint: "Frame numbers must be non-negative integers. Omit --frame to use the latest frame.",
46
+ exitCode: EXIT_USER_ERROR,
47
+ },
48
+ InvalidDelta: {
49
+ message: "The server produced a delta the client cannot apply.",
50
+ hint: "This usually self-corrects; retry the command. If it persists, file a bug with the run id.",
51
+ exitCode: EXIT_SERVER_ERROR,
52
+ },
53
+
54
+ // ----- Lookups -----
55
+ RunNotFound: {
56
+ message: "No run with that id exists in the local database.",
57
+ hint: "Use `smithers ps` to list runs.",
58
+ exitCode: EXIT_USER_ERROR,
59
+ },
60
+ NodeNotFound: {
61
+ message: "That node does not exist in this run.",
62
+ hint: "Use `smithers tree <runId>` to see the available nodes.",
63
+ exitCode: EXIT_USER_ERROR,
64
+ },
65
+ IterationNotFound: {
66
+ message: "That iteration of the node does not exist.",
67
+ hint: "Omit --iteration to use the latest iteration, or pick one from `smithers node`.",
68
+ exitCode: EXIT_USER_ERROR,
69
+ },
70
+ AttemptNotFound: {
71
+ message: "That node has no attempts yet.",
72
+ hint: "Wait for the task to start, or rerun the workflow.",
73
+ exitCode: EXIT_USER_ERROR,
74
+ },
75
+ AttemptNotFinished: {
76
+ message: "The latest attempt is still running.",
77
+ hint: "Wait for the task to finish before asking for a diff, or jump to a frame before it started.",
78
+ exitCode: EXIT_USER_ERROR,
79
+ },
80
+ FrameOutOfRange: {
81
+ message: "That frame number is outside the range recorded for this run.",
82
+ hint: "Use `smithers tree <runId>` (without --frame) to see the latest frameNo.",
83
+ exitCode: EXIT_USER_ERROR,
84
+ },
85
+ SeqOutOfRange: {
86
+ message: "The requested sequence number is outside the live stream window.",
87
+ hint: "Reconnect without --from-seq to rebase from a fresh snapshot.",
88
+ exitCode: EXIT_USER_ERROR,
89
+ },
90
+
91
+ // ----- Stream lifecycle -----
92
+ BackpressureDisconnect: {
93
+ message: "The server disconnected the stream because the client fell behind.",
94
+ hint: "Re-run with a slower consumer (e.g. pipe to `less -R`) or drop --watch.",
95
+ exitCode: EXIT_SERVER_ERROR,
96
+ },
97
+
98
+ // ----- Auth -----
99
+ Unauthorized: {
100
+ message: "The request was rejected because credentials are missing or expired.",
101
+ hint: "Run `smithers login` and try again.",
102
+ exitCode: EXIT_SERVER_ERROR,
103
+ },
104
+
105
+ // ----- Rewind -----
106
+ ConfirmationRequired: {
107
+ message: "The server requires explicit confirmation for this rewind.",
108
+ hint: "Rerun the command with --yes to confirm.",
109
+ exitCode: EXIT_USER_ERROR,
110
+ },
111
+ Busy: {
112
+ message: "Another rewind is already in progress for this run.",
113
+ hint: "Wait for the current rewind to finish, then retry.",
114
+ exitCode: EXIT_SERVER_ERROR,
115
+ },
116
+ UnsupportedSandbox: {
117
+ message: "This run uses a sandbox type that cannot be rewound.",
118
+ hint: "Only jj-backed runs are supported. Start the run under jj to rewind it.",
119
+ exitCode: EXIT_SERVER_ERROR,
120
+ },
121
+ VcsError: {
122
+ message: "The version control operation failed.",
123
+ hint: "Inspect the workspace for a dirty working copy or missing commits and retry.",
124
+ exitCode: EXIT_SERVER_ERROR,
125
+ },
126
+ RewindFailed: {
127
+ message: "The rewind did not complete successfully.",
128
+ hint: "Check `smithers why <runId>` for details and retry after addressing the cause.",
129
+ exitCode: EXIT_SERVER_ERROR,
130
+ },
131
+ RateLimited: {
132
+ message: "Too many rewind attempts in a short window.",
133
+ hint: "Wait a minute and try again, or lower the rewind frequency.",
134
+ exitCode: EXIT_SERVER_ERROR,
135
+ },
136
+ WorkingTreeDirty: {
137
+ message: "The working tree has uncommitted changes that block the diff.",
138
+ hint: "Commit or stash the changes (or run the command on a clean checkout) and retry.",
139
+ exitCode: EXIT_SERVER_ERROR,
140
+ },
141
+
142
+ // ----- Diff / output payload -----
143
+ DiffTooLarge: {
144
+ message: "The diff exceeds the payload budget and cannot be sent in full.",
145
+ hint: "Rerun with --stat for a summary only.",
146
+ exitCode: EXIT_SERVER_ERROR,
147
+ },
148
+ NodeHasNoOutput: {
149
+ message: "This node does not produce an output row.",
150
+ hint: "Only tasks with a registered output table expose --pretty output.",
151
+ exitCode: EXIT_USER_ERROR,
152
+ },
153
+ SchemaConversionError: {
154
+ message: "The server could not derive a schema for this output row.",
155
+ hint: "Use --json to print the raw row without schema ordering.",
156
+ exitCode: EXIT_SERVER_ERROR,
157
+ },
158
+ MalformedOutputRow: {
159
+ message: "The stored output row is not valid JSON.",
160
+ hint: "Inspect the row with `smithers node` and file a bug if it reproduces.",
161
+ exitCode: EXIT_SERVER_ERROR,
162
+ },
163
+ PayloadTooLarge: {
164
+ message: "The output row exceeds the payload budget.",
165
+ hint: "Use `--json | jq` to slice the row into smaller pieces, or inspect it via `smithers node`.",
166
+ exitCode: EXIT_SERVER_ERROR,
167
+ },
168
+
169
+ // ----- Transport / infra (not produced by route functions, but the
170
+ // boundary tests in the ticket reference them).
171
+ ServerUnreachable: {
172
+ message: "The smithers gateway is not reachable.",
173
+ hint: "Check SMITHERS_HOST and verify the gateway is running.",
174
+ exitCode: EXIT_SERVER_ERROR,
175
+ },
176
+ AuthExpired: {
177
+ message: "The authentication session has expired.",
178
+ hint: "Run `smithers login` to refresh credentials.",
179
+ exitCode: EXIT_SERVER_ERROR,
180
+ },
181
+ });
182
+
183
+ /**
184
+ * @param {string | undefined | null} code
185
+ * @param {string} [rawMessage]
186
+ * @returns {CliErrorMapping}
187
+ */
188
+ export function getCliErrorMapping(code, rawMessage) {
189
+ if (code && Object.prototype.hasOwnProperty.call(CLI_ERROR_MESSAGES, code)) {
190
+ return CLI_ERROR_MESSAGES[code];
191
+ }
192
+ return {
193
+ message: rawMessage && rawMessage.length > 0
194
+ ? rawMessage
195
+ : (code ? `Unexpected error: ${code}` : "Unexpected error."),
196
+ hint: "If this persists, file a bug with the run id and the command that was run.",
197
+ exitCode: EXIT_SERVER_ERROR,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * @param {string | undefined | null} code
203
+ * @param {string} [rawMessage]
204
+ * @returns {string}
205
+ */
206
+ export function formatCliErrorForStderr(code, rawMessage) {
207
+ const mapping = getCliErrorMapping(code, rawMessage);
208
+ const heading = code
209
+ ? `error: ${code}: ${mapping.message}`
210
+ : `error: ${mapping.message}`;
211
+ return `${heading}\n hint: ${mapping.hint}`;
212
+ }
@@ -0,0 +1,18 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./CliExitCode.ts").CliExitCode} CliExitCode */
3
+ // @smithers-type-exports-end
4
+
5
+ /** Uniform CLI exit codes for the devtools live-run commands. */
6
+ export const EXIT_OK = 0;
7
+ export const EXIT_USER_ERROR = 1;
8
+ export const EXIT_SERVER_ERROR = 2;
9
+ export const EXIT_DECLINED = 3;
10
+ export const EXIT_SIGINT = 130;
11
+
12
+ export const CLI_EXIT_CODES = Object.freeze({
13
+ ok: EXIT_OK,
14
+ userError: EXIT_USER_ERROR,
15
+ serverError: EXIT_SERVER_ERROR,
16
+ declined: EXIT_DECLINED,
17
+ sigint: EXIT_SIGINT,
18
+ });