omkit 0.1.3 → 0.1.5

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.
@@ -1,5 +1,10 @@
1
1
  /** Tunes an action built with {@link command}. */
2
2
  export interface CommandOptions {
3
+ /**
4
+ * Launch `cmdOrFile` directly with these arguments instead of executing it
5
+ * through a shell. Omit to run the command as a shell command.
6
+ */
7
+ args?: readonly string[];
3
8
  /** Directory to run in; relative paths resolve against `process.cwd()`. Default ".". */
4
9
  cwd?: string;
5
10
  /** Full environment for the child (handed as-is, not merged). Omit to inherit. */
@@ -7,21 +12,12 @@ export interface CommandOptions {
7
12
  /** Let this command inherit the orchestrator's debugger (default false). */
8
13
  inheritDebugger?: boolean;
9
14
  }
10
- declare function build(name: string, cmdOrFile: string, argsOrOptions: readonly string[] | CommandOptions, maybeOptions: CommandOptions): import("../index.ts").ActionInstance<import("zx").ProcessOutput, import("../core/types.ts").NoEvents, void>;
11
15
  /**
12
- * Runs a shell command as an action instance. Two forms:
16
+ * Runs a command as an action instance.
13
17
  *
14
- * - `command(name, cmd, options?)` — a full command **string**, run verbatim through the
15
- * shell, so pipes, globs and `&&` work (quote anything yourself).
16
- * - `command(name, file, args, options?)` — an executable **plus an args array**; each arg
17
- * is quoted individually, so spaces and `=`/paths in args are safe without hand-quoting.
18
+ * By default the command is executed through the shell.
18
19
  *
19
- * Output streams into the run's log under `name`; a non-zero exit fails the action;
20
- * teardown kills the child tree. For interpolated events or a daemon loop, drop to
21
- * `action(...).run(...)`.
20
+ * If `options.args` is provided, `cmdOrFile` is treated as an executable and
21
+ * launched directly without a shell.
22
22
  */
23
- export declare const command: {
24
- (name: string, cmd: string, options?: CommandOptions): ReturnType<typeof build>;
25
- (name: string, file: string, args: readonly string[], options?: CommandOptions): ReturnType<typeof build>;
26
- };
27
- export {};
23
+ export declare function command(name: string, cmdOrFile: string, options?: CommandOptions): import("../index.ts").ActionInstance<import("zx").ProcessOutput, import("../core/types.ts").NoEvents, void> | import("../index.ts").ActionInstance<void, import("../core/types.ts").NoEvents, void>;
@@ -1,38 +1,79 @@
1
+ import { spawn } from "node:child_process";
1
2
  import path from "node:path";
2
3
  import { action } from "../core/action.js";
4
+ import { debuggerFreeEnv, killTree, streamLines } from "../system/proc.js";
3
5
  /**
4
- * Build a zx tagged template that runs `cmd` verbatim — zx only quotes interpolated
5
- * args, not the template's `pieces`, so the whole (developer-supplied) command in
6
- * `pieces` runs exactly as written (pipes, globs, `&&`).
6
+ * Build a zx tagged template that runs `cmd` verbatim.
7
7
  */
8
8
  const verbatim = (cmd) => Object.assign([cmd], { raw: [cmd] });
9
- function build(name, cmdOrFile, argsOrOptions, maybeOptions) {
10
- const hasArgs = Array.isArray(argsOrOptions);
11
- const args = hasArgs ? argsOrOptions : undefined;
12
- const options = (hasArgs ? maybeOptions : argsOrOptions) ?? {};
13
- return action(name).run((ctx) => {
14
- const cwd = path.resolve(options.cwd ?? ".");
15
- const proc = ctx.proc(name, {
16
- cwd,
17
- ...(options.env ? { env: options.env } : {}),
18
- ...(options.inheritDebugger ? { inheritDebugger: true } : {}),
19
- });
20
- // `file, args` form: zx quotes the file and every arg individually (arrays spread), so
21
- // paths/values with spaces are safe and nothing is re-parsed by the shell. String form:
22
- // run the whole command verbatim.
23
- return args ? proc `${cmdOrFile} ${args}` : proc(verbatim(cmdOrFile));
24
- })();
25
- }
26
9
  /**
27
- * Runs a shell command as an action instance. Two forms:
10
+ * Runs a command as an action instance.
28
11
  *
29
- * - `command(name, cmd, options?)` — a full command **string**, run verbatim through the
30
- * shell, so pipes, globs and `&&` work (quote anything yourself).
31
- * - `command(name, file, args, options?)` — an executable **plus an args array**; each arg
32
- * is quoted individually, so spaces and `=`/paths in args are safe without hand-quoting.
12
+ * By default the command is executed through the shell.
33
13
  *
34
- * Output streams into the run's log under `name`; a non-zero exit fails the action;
35
- * teardown kills the child tree. For interpolated events or a daemon loop, drop to
36
- * `action(...).run(...)`.
14
+ * If `options.args` is provided, `cmdOrFile` is treated as an executable and
15
+ * launched directly without a shell.
37
16
  */
38
- export const command = (name, cmdOrFile, argsOrOptions = {}, maybeOptions = {}) => build(name, cmdOrFile, argsOrOptions, maybeOptions);
17
+ export function command(name, cmdOrFile, options = {}) {
18
+ const cwd = path.resolve(options.cwd ?? ".");
19
+ // Shell command.
20
+ if (options.args === undefined) {
21
+ return action(name).run((ctx) => ctx.proc(name, {
22
+ cwd,
23
+ ...(options.env && { env: options.env }),
24
+ ...(options.inheritDebugger && {
25
+ inheritDebugger: true,
26
+ }),
27
+ })(verbatim(cmdOrFile)))();
28
+ }
29
+ // Direct executable.
30
+ return action(name).run(async (ctx) => {
31
+ const inherit = options.inheritDebugger ?? Boolean(process.env.OMKIT_INHERIT_DEBUGGER);
32
+ const env = inherit
33
+ ? (options.env ?? process.env)
34
+ : debuggerFreeEnv(options.env ?? process.env);
35
+ const args = options.args ?? [];
36
+ const child = spawn(cmdOrFile, args, {
37
+ cwd,
38
+ env,
39
+ detached: process.platform !== "win32",
40
+ windowsHide: true,
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+ console.log(`${name} · start · '${cmdOrFile}' ${args.join(" ")} · in ${cwd}`);
44
+ void streamLines(child.stdout, console.info);
45
+ void streamLines(child.stderr, console.error);
46
+ let finished = false;
47
+ const onAbort = () => {
48
+ if (!finished && child.pid !== undefined) {
49
+ killTree(child.pid);
50
+ }
51
+ };
52
+ if (ctx.signal.aborted) {
53
+ onAbort();
54
+ }
55
+ else {
56
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
57
+ }
58
+ try {
59
+ await new Promise((resolve, reject) => {
60
+ child.once("error", (err) => {
61
+ reject(new Error(`${name}: ${err.message}`, { cause: err }));
62
+ });
63
+ child.once("exit", (code, signal) => {
64
+ finished = true;
65
+ if (ctx.signal.aborted || code === 0) {
66
+ resolve();
67
+ }
68
+ else {
69
+ reject(Object.assign(new Error(signal ? `${name} terminated by ${signal}` : `${name} exited with code ${code}`), { exitCode: code ?? undefined }));
70
+ }
71
+ });
72
+ });
73
+ }
74
+ finally {
75
+ finished = true;
76
+ ctx.signal.removeEventListener("abort", onAbort);
77
+ }
78
+ })();
79
+ }
@@ -49,3 +49,5 @@ export declare const procRegistry: ProcRegistry;
49
49
  * actions just describe the command — output streams in, the child dies on teardown.
50
50
  */
51
51
  export declare const createProc: (sink: ProcSink, signal: AbortSignal) => Proc;
52
+ /** Split a byte/string stream into lines, calling `onLine` for each non-empty one. */
53
+ export declare function streamLines(stream: AsyncIterable<unknown>, onLine: (line: string) => void): Promise<void>;
@@ -130,7 +130,7 @@ export const createProc = (sink, signal) => (name, opts) => {
130
130
  };
131
131
  };
132
132
  /** Split a byte/string stream into lines, calling `onLine` for each non-empty one. */
133
- async function streamLines(stream, onLine) {
133
+ export async function streamLines(stream, onLine) {
134
134
  let buffer = "";
135
135
  for await (const chunk of stream) {
136
136
  buffer += String(chunk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",