onepipeline-cli 0.95.2 → 0.95.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/README.md +7 -1
- package/bin/onepipeline.js +131 -12
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -8,7 +8,13 @@ onepipeline --help
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
The binary ships inside a per-platform package that npm selects by `os`/`cpu`, so
|
|
11
|
-
there is no compile step and no Rust toolchain to install.
|
|
11
|
+
there is no compile step and no Rust toolchain to install.
|
|
12
|
+
|
|
13
|
+
On Linux and macOS the `onepipeline` command *becomes* the binary's process
|
|
14
|
+
(`process.execve`), so it needs Node.js 22.15+ or 23.11+ and refuses an older
|
|
15
|
+
one. On Windows, which has no exec, it runs the binary under a small keeper
|
|
16
|
+
process that passes its exit status and console interrupts through and never
|
|
17
|
+
leaves it running once the command has ended. The same binary is on
|
|
12
18
|
[PyPI](https://pypi.org/project/onepipeline-cli/) (`pip install
|
|
13
19
|
onepipeline-cli`) and [crates.io](https://crates.io/crates/onepipeline)
|
|
14
20
|
(`cargo install onepipeline`).
|
package/bin/onepipeline.js
CHANGED
|
@@ -7,15 +7,30 @@
|
|
|
7
7
|
// compile, no download at install time. The platform-specific binary ships
|
|
8
8
|
// inside a per-platform package (`onepipeline-cli-<platform>-<arch>`) declared
|
|
9
9
|
// in this package's `optionalDependencies`; npm installs only the one whose
|
|
10
|
-
// `os`/`cpu` match the host, and this shim resolves it and
|
|
11
|
-
// caller's argv.
|
|
10
|
+
// `os`/`cpu` match the host, and this shim resolves it and starts it with the
|
|
11
|
+
// caller's argv, stdin, stdout and stderr.
|
|
12
|
+
//
|
|
13
|
+
// How it starts it is the point of this file. On Linux and macOS it *replaces
|
|
14
|
+
// itself* with the binary (`process.execve`), so the process the caller started
|
|
15
|
+
// is the engine: the PID the caller holds is the engine's, a signal sent to it
|
|
16
|
+
// reaches the engine, and a process the engine starts sees the engine as its
|
|
17
|
+
// parent — the process tree a PyPI or cargo install gives. A caller that marks
|
|
18
|
+
// itself by the PID that execs the engine depends on exactly that, so a Node.js
|
|
19
|
+
// without `process.execve` (before 22.15 / 23.11) is refused there rather than
|
|
20
|
+
// given a child process in the engine's place.
|
|
21
|
+
//
|
|
22
|
+
// Windows has no exec. There the engine runs under a keeper process this one
|
|
23
|
+
// starts (`supervise` below): this process exits with the engine's status, the
|
|
24
|
+
// Ctrl+C and Ctrl+Break a console delivers reach the engine as they are, and the
|
|
25
|
+
// engine is ended rather than orphaned when this process is ended — abruptly too.
|
|
12
26
|
//
|
|
13
27
|
// This file is committed source; the version and the optionalDependency versions
|
|
14
28
|
// are stamped from Cargo.toml at publish time by scripts/npm-build.mjs, which
|
|
15
29
|
// also generates the per-platform packages from the release binaries.
|
|
16
30
|
|
|
31
|
+
const fs = require("node:fs");
|
|
17
32
|
const path = require("node:path");
|
|
18
|
-
const {
|
|
33
|
+
const { spawn } = require("node:child_process");
|
|
19
34
|
|
|
20
35
|
// process.platform-process.arch -> the platform package that carries the binary.
|
|
21
36
|
// The keys mirror the targets release/release.py builds, the TARGETS table in
|
|
@@ -82,16 +97,120 @@ function binaryPath() {
|
|
|
82
97
|
}
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
|
|
100
|
+
// The interrupts a Windows console delivers to every process attached to it,
|
|
101
|
+
// the engine included. The engine already has each one, as itself, so this
|
|
102
|
+
// process and the keeper hold on through them and report what the engine does.
|
|
103
|
+
const CONSOLE_EVENTS = ["SIGINT", "SIGBREAK"];
|
|
104
|
+
|
|
105
|
+
// The keeper, a Node process this one starts with an IPC channel to it. It
|
|
106
|
+
// starts the engine as its own child, so it knows the engine the moment the
|
|
107
|
+
// engine exists, and tells this process the engine's status. The channel breaks
|
|
108
|
+
// however this process ends — TerminateProcess included, where no handler of
|
|
109
|
+
// this process runs — and the keeper then ends the engine: there is no moment
|
|
110
|
+
// at which the engine runs and nothing that would end it knows it, however
|
|
111
|
+
// early in its start this process is ended. A message the keeper cannot
|
|
112
|
+
// deliver is the same break, seen first: this process ended before the keeper
|
|
113
|
+
// ran, and the failed send ends the engine rather than the keeper alone.
|
|
114
|
+
const KEEPER = `
|
|
115
|
+
const { spawn } = require("node:child_process");
|
|
116
|
+
const [binary, ...args] = JSON.parse(process.argv[1]);
|
|
117
|
+
for (const signal of ${JSON.stringify(CONSOLE_EVENTS)}) process.on(signal, () => {});
|
|
118
|
+
const engine = spawn(binary, args, { stdio: "inherit" });
|
|
119
|
+
const end = () => {
|
|
120
|
+
engine.kill("SIGKILL");
|
|
121
|
+
process.exit(0);
|
|
122
|
+
};
|
|
123
|
+
process.on("disconnect", end);
|
|
124
|
+
const send = (message, then = () => {}) =>
|
|
125
|
+
process.send(message, (error) => (error ? end() : then()));
|
|
126
|
+
if (engine.pid !== undefined) send({ engine: engine.pid });
|
|
127
|
+
engine.on("error", (error) => send({ error: error.message }, () => process.exit(0)));
|
|
128
|
+
engine.on("exit", (code) => send({ status: code ?? 1 }, () => process.exit(0)));
|
|
129
|
+
`;
|
|
86
130
|
|
|
87
|
-
|
|
88
|
-
|
|
131
|
+
function supervise(binary, args) {
|
|
132
|
+
for (const signal of CONSOLE_EVENTS) process.on(signal, () => {});
|
|
133
|
+
const keeper = spawn(process.execPath, ["-e", KEEPER, JSON.stringify([binary, ...args])], {
|
|
134
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
135
|
+
});
|
|
136
|
+
// Without a keeper nothing would end the engine if this process were ended,
|
|
137
|
+
// so no engine is started: the command refuses instead.
|
|
138
|
+
keeper.on("error", (error) =>
|
|
139
|
+
fail(
|
|
140
|
+
`could not start the process that keeps the onepipeline binary from outliving ` +
|
|
141
|
+
`this command, so the binary was not started: ${error.message}`,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
let engine = null;
|
|
145
|
+
const endEngine = () => {
|
|
146
|
+
if (engine !== null) {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(engine, "SIGKILL");
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
// The keeper's messages cross a process boundary, so each is read as exactly
|
|
153
|
+
// one of its three envelopes; anything else ends the engine and the command.
|
|
154
|
+
const isCount = (value) => Number.isInteger(value) && value >= 0;
|
|
155
|
+
// A Windows exit code is 32 bits; process.exit would wrap a larger one, 2^32 to 0.
|
|
156
|
+
const isStatus = (value) => isCount(value) && value <= 0xffffffff;
|
|
157
|
+
keeper.on("message", (message) => {
|
|
158
|
+
const keys = message !== null && typeof message === "object" ? Object.keys(message) : [];
|
|
159
|
+
const [key] = keys;
|
|
160
|
+
if (keys.length === 1 && key === "engine" && isCount(message.engine) && message.engine > 0) {
|
|
161
|
+
engine = message.engine;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (keys.length === 1 && key === "error" && typeof message.error === "string") {
|
|
165
|
+
fail(`failed to launch the onepipeline binary: ${message.error}`);
|
|
166
|
+
}
|
|
167
|
+
// The engine's exit code verbatim — the contract assigns meaning to 1 and 2,
|
|
168
|
+
// and a caller reads them.
|
|
169
|
+
if (keys.length === 1 && key === "status" && isStatus(message.status)) {
|
|
170
|
+
process.exit(message.status);
|
|
171
|
+
}
|
|
172
|
+
endEngine();
|
|
173
|
+
fail(
|
|
174
|
+
`the process keeping the onepipeline binary sent an unreadable message: ` +
|
|
175
|
+
JSON.stringify(message),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
// A keeper gone without the engine's status has left the engine with
|
|
179
|
+
// nothing to end it, so this process ends it before saying so.
|
|
180
|
+
keeper.on("disconnect", () => {
|
|
181
|
+
endEngine();
|
|
182
|
+
fail("the process keeping the onepipeline binary ended without reporting its status");
|
|
183
|
+
});
|
|
89
184
|
}
|
|
90
185
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
186
|
+
const binary = binaryPath();
|
|
187
|
+
const args = process.argv.slice(2);
|
|
188
|
+
|
|
189
|
+
if (process.platform === "win32") {
|
|
190
|
+
supervise(binary, args);
|
|
191
|
+
} else {
|
|
192
|
+
if (typeof process.execve !== "function") {
|
|
193
|
+
fail(
|
|
194
|
+
`Node.js ${process.version} cannot run onepipeline from npm on ${process.platform}: ` +
|
|
195
|
+
"the command has to become the onepipeline binary's own process, which needs " +
|
|
196
|
+
`process.execve (Node.js ${require("../package.json").engines.node}). Upgrade Node.js, or ` +
|
|
197
|
+
`${OTHER_INSTALLS.charAt(0).toLowerCase()}${OTHER_INSTALLS.slice(1)}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
// Node ignores SIGXFSZ, and an ignored signal survives exec while a handled
|
|
201
|
+
// one returns to its default: handle it so the engine, and every program it
|
|
202
|
+
// starts, begins with the disposition a shell would give it. (Node ignores
|
|
203
|
+
// SIGPIPE too, but the engine's runtime ignores that itself and restores it
|
|
204
|
+
// for what it starts.)
|
|
205
|
+
process.on("SIGXFSZ", () => {});
|
|
206
|
+
// On Node.js 22 an execve that fails aborts this process rather than
|
|
207
|
+
// throwing, so the binary is checked first: a missing or non-executable one
|
|
208
|
+
// is refused here with the same message a throwing execve would get.
|
|
209
|
+
try {
|
|
210
|
+
if (!fs.statSync(binary).isFile()) throw new Error(`${binary} is not a file`);
|
|
211
|
+
fs.accessSync(binary, fs.constants.X_OK);
|
|
212
|
+
process.execve(binary, [binary, ...args], process.env);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
fail(`failed to launch the onepipeline binary: ${error.message}`);
|
|
215
|
+
}
|
|
96
216
|
}
|
|
97
|
-
process.exit(result.status === null ? 1 : result.status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onepipeline-cli",
|
|
3
|
-
"version": "0.95.
|
|
3
|
+
"version": "0.95.4",
|
|
4
4
|
"description": "Execute a task DAG over oneagentgraph and onevcs, merging their event streams into one.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"orchestration",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"bin/onepipeline.js"
|
|
25
25
|
],
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": "^22.15.0 || >=23.11.0"
|
|
28
28
|
},
|
|
29
29
|
"optionalDependencies": {
|
|
30
|
-
"onepipeline-cli-linux-x64": "0.95.
|
|
31
|
-
"onepipeline-cli-linux-arm64": "0.95.
|
|
32
|
-
"onepipeline-cli-darwin-arm64": "0.95.
|
|
33
|
-
"onepipeline-cli-win32-x64": "0.95.
|
|
30
|
+
"onepipeline-cli-linux-x64": "0.95.4",
|
|
31
|
+
"onepipeline-cli-linux-arm64": "0.95.4",
|
|
32
|
+
"onepipeline-cli-darwin-arm64": "0.95.4",
|
|
33
|
+
"onepipeline-cli-win32-x64": "0.95.4"
|
|
34
34
|
}
|
|
35
35
|
}
|