omkit 0.8.2 → 0.8.4
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/dist/cli/ui/app.js +43 -3
- package/dist/client/channel.d.ts +5 -0
- package/dist/client/channel.js +9 -2
- package/dist/client/runner.js +33 -0
- package/package.json +1 -1
package/dist/cli/ui/app.js
CHANGED
|
@@ -5,6 +5,17 @@ import { RunModel } from "../../output/RunModel.js";
|
|
|
5
5
|
import { OmList } from "./views/OmList.js";
|
|
6
6
|
import { RunView } from "./views/RunView.js";
|
|
7
7
|
import { Spinner } from "./Spinner.js";
|
|
8
|
+
/**
|
|
9
|
+
* How often the run screen redraws while the log streams in.
|
|
10
|
+
*
|
|
11
|
+
* An Ink frame costs ~25ms at minimum and grows with the milestone tail, so redrawing per log
|
|
12
|
+
* entry caps the app at roughly 40 entries/second — far under what a build, a test suite, or a
|
|
13
|
+
* dev server emits. The app then falls permanently behind: `settled` is the *last* message on
|
|
14
|
+
* the channel, so it only arrives once every backlogged entry has been drawn, and the run looks
|
|
15
|
+
* hung long after the child has finished. Folding into the model is cheap; drawing is not, so
|
|
16
|
+
* the two run at different rates.
|
|
17
|
+
*/
|
|
18
|
+
const REDRAW_MS = 80;
|
|
8
19
|
/** The interactive app: discover → list/search → run → live milestones + prompts. */
|
|
9
20
|
export function App({ client, onExit, oms: initialOms, }) {
|
|
10
21
|
const { exit } = useApp();
|
|
@@ -21,6 +32,16 @@ export function App({ client, onExit, oms: initialOms, }) {
|
|
|
21
32
|
const sessionRef = useRef(null);
|
|
22
33
|
const verdictRef = useRef(undefined);
|
|
23
34
|
const tearingRef = useRef(false);
|
|
35
|
+
const redrawTimer = useRef(undefined);
|
|
36
|
+
const stopRedraw = () => {
|
|
37
|
+
if (redrawTimer.current === undefined)
|
|
38
|
+
return;
|
|
39
|
+
clearInterval(redrawTimer.current);
|
|
40
|
+
redrawTimer.current = undefined;
|
|
41
|
+
};
|
|
42
|
+
// The redraw timer holds the event loop open, so it must not outlive the app — an unmount
|
|
43
|
+
// for any reason other than the run settling would otherwise keep the CLI from exiting.
|
|
44
|
+
useEffect(() => stopRedraw, []);
|
|
24
45
|
useEffect(() => {
|
|
25
46
|
if (initialOms)
|
|
26
47
|
return; // provided by the caller's pre-flight discovery
|
|
@@ -37,17 +58,34 @@ export function App({ client, onExit, oms: initialOms, }) {
|
|
|
37
58
|
const model = new RunModel();
|
|
38
59
|
const s = client.run(om.file);
|
|
39
60
|
sessionRef.current = s;
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
// Every entry lands in the model immediately; the screen catches up on its own cadence.
|
|
62
|
+
// `dirty` keeps an idle run from redrawing over nothing — see REDRAW_MS for why the two
|
|
63
|
+
// are separated at all.
|
|
64
|
+
let dirty = false;
|
|
65
|
+
const redraw = () => {
|
|
66
|
+
if (!dirty)
|
|
67
|
+
return;
|
|
68
|
+
dirty = false;
|
|
42
69
|
setLines(model.milestones());
|
|
43
70
|
setStatus(model.summary());
|
|
71
|
+
};
|
|
72
|
+
redrawTimer.current = setInterval(redraw, REDRAW_MS);
|
|
73
|
+
s.on("log", (entry) => {
|
|
74
|
+
model.apply(entry);
|
|
75
|
+
dirty = true;
|
|
76
|
+
});
|
|
77
|
+
// A prompt blocks the child until it's answered, so it must not wait for the next tick —
|
|
78
|
+
// and the milestones that led to it are the context the question is read in.
|
|
79
|
+
s.on("prompt", (req) => {
|
|
80
|
+
redraw();
|
|
81
|
+
setPrompt(req);
|
|
44
82
|
});
|
|
45
|
-
s.on("prompt", (req) => setPrompt(req));
|
|
46
83
|
// The child gave up on the prompt (timed out or torn down). Withdraw its box so a stale,
|
|
47
84
|
// unanswerable prompt doesn't linger — but only if it's still the one on screen, so a late
|
|
48
85
|
// withdrawal can't clobber a fresh prompt.
|
|
49
86
|
s.on("promptDone", (id) => setPrompt((p) => (p?.id === id ? undefined : p)));
|
|
50
87
|
s.on("settled", (v) => {
|
|
88
|
+
redraw(); // the run's closing milestones, before its verdict
|
|
51
89
|
setVerdict(v);
|
|
52
90
|
setPrompt(undefined);
|
|
53
91
|
});
|
|
@@ -57,6 +95,8 @@ export function App({ client, onExit, oms: initialOms, }) {
|
|
|
57
95
|
// so it can print the durable summary block, and unmount. Without this a self-completing run
|
|
58
96
|
// would show its verdict line but never exit or render the summary.
|
|
59
97
|
void s.result.then((v) => {
|
|
98
|
+
stopRedraw();
|
|
99
|
+
redraw(); // last pass: whatever arrived since the final tick
|
|
60
100
|
verdictRef.current = v;
|
|
61
101
|
setVerdict(v);
|
|
62
102
|
onExit?.(v);
|
package/dist/client/channel.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export interface Transport {
|
|
|
5
5
|
send(message: SupervisorMessage): void;
|
|
6
6
|
onMessage(handler: (message: ChildMessage) => void): void;
|
|
7
7
|
onClose(handler: (code: number | null) => void): void;
|
|
8
|
+
/**
|
|
9
|
+
* Whatever the child printed outside the channel, for a close that never reached `settled`.
|
|
10
|
+
* Read only on that path — a run that reports for itself needs no second account.
|
|
11
|
+
*/
|
|
12
|
+
diagnostics?(): string[];
|
|
8
13
|
}
|
|
9
14
|
/**
|
|
10
15
|
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt`/`prompt-done` child
|
package/dist/client/channel.js
CHANGED
|
@@ -35,8 +35,15 @@ export function createChannel(transport) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
transport.onClose(() => {
|
|
38
|
-
if (
|
|
39
|
-
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
// The child died before it could report: there is no run folder and no summary of its own.
|
|
41
|
+
// What it printed on its way out — a module that failed to load, a crash during import — is
|
|
42
|
+
// the only account of why, so it stands in as the summary. Without it the frontend shows a
|
|
43
|
+
// bare "✗ failed" and the reason is lost with the process.
|
|
44
|
+
const output = transport.diagnostics?.() ?? [];
|
|
45
|
+
const summary = output.length ? ["the run ended before it could report:", ...output] : [];
|
|
46
|
+
resolveResult({ ok: false, folder: "", summary });
|
|
40
47
|
});
|
|
41
48
|
return {
|
|
42
49
|
on(event, handler) {
|
package/dist/client/runner.js
CHANGED
|
@@ -18,6 +18,30 @@ export function inspectArgs(inspect) {
|
|
|
18
18
|
return [];
|
|
19
19
|
return [`--inspect=${inspect.port}`];
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* How much of a child's raw output to keep. Enough for a module-resolution failure or a stack
|
|
23
|
+
* trace, small enough that a run which prints megabytes still costs nothing to supervise.
|
|
24
|
+
*/
|
|
25
|
+
const TAIL_BYTES = 8 * 1024;
|
|
26
|
+
/**
|
|
27
|
+
* Drain a child's raw streams while keeping the last {@link TAIL_BYTES} of them. Attaching a
|
|
28
|
+
* `data` handler is what puts each stream in flowing mode — that, not the tail, is what stops
|
|
29
|
+
* the child wedging on a full pipe. Returns a reader for the kept lines, oldest first; stdout
|
|
30
|
+
* and stderr share one buffer so a message split across them stays in order.
|
|
31
|
+
*/
|
|
32
|
+
function keepTail(...streams) {
|
|
33
|
+
let text = "";
|
|
34
|
+
for (const stream of streams) {
|
|
35
|
+
stream?.setEncoding("utf8");
|
|
36
|
+
stream?.on("data", (chunk) => {
|
|
37
|
+
text = (text + chunk).slice(-TAIL_BYTES);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return () => text
|
|
41
|
+
.split("\n")
|
|
42
|
+
.map((line) => line.replace(/\r$/, "").trimEnd())
|
|
43
|
+
.filter((line) => line !== "");
|
|
44
|
+
}
|
|
21
45
|
/**
|
|
22
46
|
* Fork `omFile` as a supervised child: the `tsx` loader runs the TypeScript directly,
|
|
23
47
|
* `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
|
|
@@ -38,10 +62,19 @@ export function runOm(omFile, opts = {}) {
|
|
|
38
62
|
env: { ...process.env, OMKIT_SUPERVISED: "1", ...opts.env },
|
|
39
63
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
40
64
|
});
|
|
65
|
+
// Piped, not read: nothing in the supervisor consumes these streams, so once the OS pipe
|
|
66
|
+
// buffer fills (~64 KB) the child blocks on its next write — permanently, since the only
|
|
67
|
+
// thing the supervisor ever reads is the IPC channel. `patch-console` swaps the console
|
|
68
|
+
// methods but not `process.stdout.write`, so any direct write in an om (or in a library it
|
|
69
|
+
// uses) lands here and wedges the run before it can settle. Draining is what keeps the child
|
|
70
|
+
// moving; the tail is kept because a child that dies before the channel opens has nowhere
|
|
71
|
+
// else to say why (see the Transport's `diagnostics`).
|
|
72
|
+
const tail = keepTail(child.stdout, child.stderr);
|
|
41
73
|
const transport = {
|
|
42
74
|
send: (message) => void child.send(message),
|
|
43
75
|
onMessage: (handler) => void child.on("message", (m) => handler(m)),
|
|
44
76
|
onClose: (handler) => void child.on("close", (code) => handler(code)),
|
|
77
|
+
diagnostics: tail,
|
|
45
78
|
};
|
|
46
79
|
const session = createChannel(transport);
|
|
47
80
|
// When the run settles, the child has already written its logs and reaped its process tree —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|