omkit 0.0.2 → 0.0.3
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/actions/command.d.ts +28 -10
- package/dist/actions/command.js +70 -19
- package/dist/core/process.d.ts +13 -0
- package/dist/core/process.js +35 -7
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AnyActionInstance } from "../core/action.ts";
|
|
1
2
|
/** Tunes an action built with {@link command}. */
|
|
2
3
|
export interface CommandOptions {
|
|
3
4
|
/**
|
|
@@ -9,18 +10,35 @@ export interface CommandOptions {
|
|
|
9
10
|
label?: string;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* Runs a fixed
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* source); `cmd` runs verbatim in the resolved `cwd`.
|
|
13
|
+
* Runs a fixed command as an action **instance** — built in a single call, the
|
|
14
|
+
* same shape as every other bundled action (`healthcheck`, `prompt`, …), rather
|
|
15
|
+
* than a factory you instantiate separately. `name` identifies it across the run
|
|
16
|
+
* (the `launch <name>` line and the child's log source).
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* )
|
|
18
|
+
* Two forms:
|
|
19
|
+
*
|
|
20
|
+
* - **Shell form** — `command(name, cmd, opts)`. The string runs verbatim
|
|
21
|
+
* through a shell (PowerShell on Windows), so pipes, globs, and `&&` work:
|
|
22
|
+
*
|
|
23
|
+
* command("TSKB:dev", "npm run dev:explorer", { cwd: pkg })
|
|
24
|
+
*
|
|
25
|
+
* The direct child is the *shell*, and the real program is a grandchild. On
|
|
26
|
+
* Windows teardown reaps the tree only while that shell is alive — so a GUI
|
|
27
|
+
* app that ignores console Ctrl-C (Electron) can be **orphaned and survive**.
|
|
28
|
+
*
|
|
29
|
+
* - **Direct form** — `command(name, file, args, opts)`. Spawns `file` with
|
|
30
|
+
* `args` **without a shell**, so the action's direct child *is* the program.
|
|
31
|
+
* On teardown it's force-reaped by its own PID (`taskkill /F /T`), so it dies
|
|
32
|
+
* even when a shell would have orphaned it. Use this for long-lived apps you
|
|
33
|
+
* launch and must be able to kill on Ctrl-C:
|
|
34
|
+
*
|
|
35
|
+
* command("platform", electronExe, [".", "--inspect"], { cwd: app })
|
|
36
|
+
*
|
|
37
|
+
* `file` must be a real executable (an actual `electron.exe`), not a `.cmd`
|
|
38
|
+
* shim — shims need a shell to run.
|
|
22
39
|
*
|
|
23
40
|
* For anything beyond a static command — interpolated args, emitted events, a
|
|
24
41
|
* daemon loop — drop down to `action(...).run(...)` directly.
|
|
25
42
|
*/
|
|
26
|
-
export declare
|
|
43
|
+
export declare function command(name: string, cmd: string, options?: CommandOptions): AnyActionInstance;
|
|
44
|
+
export declare function command(name: string, file: string, args: string[], options?: CommandOptions): AnyActionInstance;
|
package/dist/actions/command.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { action } from "../core/action.js";
|
|
4
|
+
import { killTree } from "../core/process.js";
|
|
5
|
+
import { captureSnapshot } from "../core/output.js";
|
|
3
6
|
/**
|
|
4
7
|
* Build the literal part of a zx tagged template from a command string, so it
|
|
5
8
|
* runs verbatim. zx only quotes interpolated args, not the template's `pieces` —
|
|
@@ -8,25 +11,73 @@ import { action } from "../core/action.js";
|
|
|
8
11
|
* verbatim is exactly what we want.
|
|
9
12
|
*/
|
|
10
13
|
const verbatim = (cmd) => Object.assign([cmd], { raw: [cmd] });
|
|
14
|
+
export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
|
|
15
|
+
const direct = Array.isArray(argsOrOptions);
|
|
16
|
+
const options = (direct ? maybeOptions : argsOrOptions) ?? {};
|
|
17
|
+
if (direct) {
|
|
18
|
+
return action(name).run((ctx) => spawnDirect(ctx.logs, ctx.signal, name, cmdOrFile, argsOrOptions, options))();
|
|
19
|
+
}
|
|
20
|
+
return action(name).run(({ proc }) => {
|
|
21
|
+
// path.resolve keeps an absolute cwd as-is and resolves a relative one
|
|
22
|
+
// against process.cwd() — exactly the "absolute from where node runs" rule.
|
|
23
|
+
const cwd = path.resolve(options.cwd ?? ".");
|
|
24
|
+
// proc sources the LogsCollector from async context and streams output into it.
|
|
25
|
+
return proc(options.label ?? name, { cwd })(verbatim(cmdOrFile));
|
|
26
|
+
})(); // construct the instance here, so callers get one in a single call
|
|
27
|
+
}
|
|
11
28
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* run(
|
|
19
|
-
* command("TSKB:dev", "npm run dev:explorer", { cwd: pkg }),
|
|
20
|
-
* command("TSKB:test", "npm test", { cwd: repo }),
|
|
21
|
-
* );
|
|
22
|
-
*
|
|
23
|
-
* For anything beyond a static command — interpolated args, emitted events, a
|
|
24
|
-
* daemon loop — drop down to `action(...).run(...)` directly.
|
|
29
|
+
* The direct (shell-less) launcher behind {@link command}'s four-arg form.
|
|
30
|
+
* Spawns `file`/`args` with no shell — so the returned child *is* the program,
|
|
31
|
+
* killable by its own PID on teardown — streams its stdout/stderr into the run's
|
|
32
|
+
* log, and resolves when it exits (0 = ok; a non-zero exit is a genuine failure,
|
|
33
|
+
* unless the run is tearing down, where the abort-kill is the reason).
|
|
25
34
|
*/
|
|
26
|
-
|
|
27
|
-
// path.resolve keeps an absolute cwd as-is and resolves a relative one
|
|
28
|
-
// against process.cwd() — exactly the "absolute from where node runs" rule.
|
|
35
|
+
const spawnDirect = (logs, signal, name, file, args, options) => {
|
|
29
36
|
const cwd = path.resolve(options.cwd ?? ".");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const source = options.label ?? name;
|
|
38
|
+
// windowsHide keeps a stray console window from flashing for the child.
|
|
39
|
+
const child = spawn(file, args, { cwd, windowsHide: true });
|
|
40
|
+
// Mirror proc's launch line: a filterable milestone on the timeline, with the
|
|
41
|
+
// structured args snapshotted and linked (see process.ts for the rationale).
|
|
42
|
+
const snap = captureSnapshot(`proc-${name}-start`, { cmd: file, args, cwd });
|
|
43
|
+
logs.append({
|
|
44
|
+
source: name,
|
|
45
|
+
level: "event",
|
|
46
|
+
message: `${name} · start · ${file} ${args.join(" ")} · in ${cwd} · → ${snap.rel}`,
|
|
47
|
+
});
|
|
48
|
+
if (child.stdout)
|
|
49
|
+
logs.attach(child.stdout, source, "info");
|
|
50
|
+
if (child.stderr)
|
|
51
|
+
logs.attach(child.stderr, source, "error");
|
|
52
|
+
// Force-reap the child's tree on teardown, by its own PID. Guarded so a child
|
|
53
|
+
// that has already exited isn't re-killed (its PID may have been reused).
|
|
54
|
+
let finished = false;
|
|
55
|
+
const onAbort = () => {
|
|
56
|
+
if (finished || child.pid == null)
|
|
57
|
+
return;
|
|
58
|
+
killTree(child.pid);
|
|
59
|
+
};
|
|
60
|
+
const done = () => {
|
|
61
|
+
finished = true;
|
|
62
|
+
signal.removeEventListener("abort", onAbort);
|
|
63
|
+
};
|
|
64
|
+
if (signal.aborted)
|
|
65
|
+
killTree(child.pid ?? -1);
|
|
66
|
+
else
|
|
67
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
child.on("error", (error) => {
|
|
70
|
+
done();
|
|
71
|
+
reject(error);
|
|
72
|
+
});
|
|
73
|
+
child.on("exit", (code, sig) => {
|
|
74
|
+
done();
|
|
75
|
+
// During teardown the abort-kill is why it exited — a clean shutdown, not a
|
|
76
|
+
// fault; let it settle successfully so it isn't recorded as a failure.
|
|
77
|
+
if (signal.aborted || code === 0)
|
|
78
|
+
resolve();
|
|
79
|
+
else
|
|
80
|
+
reject(new Error(`${name} exited with ${code ?? sig}`));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
package/dist/core/process.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { type Options, type ProcessPromise } from "zx";
|
|
2
2
|
import type { Logger } from "./log-collector/LogsCollector.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Force-kill a process **and its whole tree, synchronously**.
|
|
5
|
+
*
|
|
6
|
+
* Windows has no process groups: a child outlives its parent, and a Ctrl-C sent
|
|
7
|
+
* to the console group can make an intermediate shell exit and *orphan* the real
|
|
8
|
+
* app (e.g. electron.exe), which then survives teardown. So the tree must be
|
|
9
|
+
* reaped explicitly by PID — `taskkill /F /T` does it, and running it
|
|
10
|
+
* **synchronously** guarantees it completes before the run's own process exits
|
|
11
|
+
* (a fire-and-forget kill can lose that race). On POSIX, zx's `kill` already
|
|
12
|
+
* does a process-group tree kill. Best-effort: an already-dead/missing PID is
|
|
13
|
+
* ignored.
|
|
14
|
+
*/
|
|
15
|
+
export declare const killTree: (pid: number) => void;
|
|
3
16
|
/**
|
|
4
17
|
* Spawns a command (tagged-template, exactly like zx's `$`) and streams the
|
|
5
18
|
* child's stdout/stderr into a log (stdout as `info`, stderr as `error`) under
|
package/dist/core/process.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import os from "node:os";
|
|
2
3
|
import { $, kill, usePowerShell } from "zx";
|
|
3
4
|
import { captureSnapshot } from "./output.js";
|
|
@@ -5,6 +6,31 @@ import { captureSnapshot } from "./output.js";
|
|
|
5
6
|
// Use Windows PowerShell there so zx spawns and quotes for the right shell.
|
|
6
7
|
if (os.platform() === "win32")
|
|
7
8
|
usePowerShell();
|
|
9
|
+
/**
|
|
10
|
+
* Force-kill a process **and its whole tree, synchronously**.
|
|
11
|
+
*
|
|
12
|
+
* Windows has no process groups: a child outlives its parent, and a Ctrl-C sent
|
|
13
|
+
* to the console group can make an intermediate shell exit and *orphan* the real
|
|
14
|
+
* app (e.g. electron.exe), which then survives teardown. So the tree must be
|
|
15
|
+
* reaped explicitly by PID — `taskkill /F /T` does it, and running it
|
|
16
|
+
* **synchronously** guarantees it completes before the run's own process exits
|
|
17
|
+
* (a fire-and-forget kill can lose that race). On POSIX, zx's `kill` already
|
|
18
|
+
* does a process-group tree kill. Best-effort: an already-dead/missing PID is
|
|
19
|
+
* ignored.
|
|
20
|
+
*/
|
|
21
|
+
export const killTree = (pid) => {
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
try {
|
|
24
|
+
execFileSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// already gone, access denied, or taskkill missing — nothing more to do
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
void kill(pid).catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
8
34
|
/**
|
|
9
35
|
* Builds a {@link Proc} bound to a specific log sink and abort signal. The action
|
|
10
36
|
* pipeline injects one per action as `ctx.proc`, wired to that action's logger
|
|
@@ -46,19 +72,21 @@ export const createProc = (logs, signal) => (name, opts) => {
|
|
|
46
72
|
});
|
|
47
73
|
logs.attach(child.stdout, name, "info");
|
|
48
74
|
logs.attach(child.stderr, name, "error");
|
|
49
|
-
// Kill the child's entire process tree on teardown
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
75
|
+
// Kill the child's entire process tree on teardown via {@link killTree} —
|
|
76
|
+
// a synchronous, force tree kill by PID (taskkill /F /T on Windows,
|
|
77
|
+
// process-group kill on Unix). Synchronous so it finishes before the run's
|
|
78
|
+
// process exits. NOTE: with zx the direct child is a *shell wrapper*
|
|
79
|
+
// (PowerShell on Windows), so this only reaps grandchildren while that
|
|
80
|
+
// wrapper is still alive; a GUI app that must survive its shell being
|
|
81
|
+
// Ctrl-C'd should be launched shell-less via `command(name, file, args)`,
|
|
82
|
+
// whose direct child is the app itself.
|
|
55
83
|
let finished = false;
|
|
56
84
|
const onAbort = () => {
|
|
57
85
|
if (finished)
|
|
58
86
|
return;
|
|
59
87
|
const pid = child.pid;
|
|
60
88
|
if (pid != null)
|
|
61
|
-
|
|
89
|
+
killTree(pid);
|
|
62
90
|
};
|
|
63
91
|
const markFinished = () => {
|
|
64
92
|
finished = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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",
|