@omnicross/cli-launcher 0.1.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.
- package/LICENSE +21 -0
- package/NOTICE +38 -0
- package/README.md +21 -0
- package/dist/index.cjs +865 -0
- package/dist/index.d.cts +468 -0
- package/dist/index.d.ts +468 -0
- package/dist/index.js +851 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
+
}) : x)(function(x) {
|
|
8
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
+
});
|
|
11
|
+
var __esm = (fn, res) => function __init() {
|
|
12
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
|
+
};
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
|
|
28
|
+
// src/pty-adapter.ts
|
|
29
|
+
var pty_adapter_exports = {};
|
|
30
|
+
__export(pty_adapter_exports, {
|
|
31
|
+
buildPtyEnv: () => buildPtyEnv,
|
|
32
|
+
createPtyAdapter: () => createPtyAdapter
|
|
33
|
+
});
|
|
34
|
+
function buildPtyEnv(overlay) {
|
|
35
|
+
return overlay ? { ...process.env, ...overlay } : { ...process.env };
|
|
36
|
+
}
|
|
37
|
+
function createPtyAdapter(input) {
|
|
38
|
+
const nodePty = __require("node-pty");
|
|
39
|
+
const shell = input.shell ?? (process.platform === "win32" ? "powershell.exe" : "/bin/bash");
|
|
40
|
+
const pty = nodePty.spawn(shell, input.args ?? [], {
|
|
41
|
+
name: "xterm-256color",
|
|
42
|
+
cols: input.cols ?? 120,
|
|
43
|
+
rows: input.rows ?? 30,
|
|
44
|
+
cwd: input.cwd,
|
|
45
|
+
env: buildPtyEnv(input.env)
|
|
46
|
+
});
|
|
47
|
+
const stdoutListeners = [];
|
|
48
|
+
const stderrListeners = [];
|
|
49
|
+
const dataDisposable = pty.onData((data) => {
|
|
50
|
+
for (const cb of stdoutListeners) cb(data);
|
|
51
|
+
});
|
|
52
|
+
let exitResolve = null;
|
|
53
|
+
const exitPromise = new Promise((resolve) => {
|
|
54
|
+
exitResolve = resolve;
|
|
55
|
+
});
|
|
56
|
+
const exitDisposable = pty.onExit(({ exitCode, signal }) => {
|
|
57
|
+
exitResolve?.({ code: exitCode, signal: signal != null ? String(signal) : null });
|
|
58
|
+
});
|
|
59
|
+
let disposed = false;
|
|
60
|
+
return {
|
|
61
|
+
pid: pty.pid,
|
|
62
|
+
onStdout(cb) {
|
|
63
|
+
stdoutListeners.push(cb);
|
|
64
|
+
},
|
|
65
|
+
onStderr(cb) {
|
|
66
|
+
stderrListeners.push(cb);
|
|
67
|
+
},
|
|
68
|
+
stdin: {
|
|
69
|
+
write(data) {
|
|
70
|
+
if (!disposed) pty.write(data);
|
|
71
|
+
},
|
|
72
|
+
end() {
|
|
73
|
+
if (!disposed) pty.write("");
|
|
74
|
+
},
|
|
75
|
+
destroy() {
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
write(data) {
|
|
79
|
+
if (!disposed) pty.write(data);
|
|
80
|
+
},
|
|
81
|
+
resize(cols, rows) {
|
|
82
|
+
if (!disposed) pty.resize(cols, rows);
|
|
83
|
+
},
|
|
84
|
+
wait() {
|
|
85
|
+
return exitPromise;
|
|
86
|
+
},
|
|
87
|
+
kill(signal) {
|
|
88
|
+
if (disposed) return;
|
|
89
|
+
try {
|
|
90
|
+
pty.kill(signal);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
dispose() {
|
|
95
|
+
if (disposed) return;
|
|
96
|
+
disposed = true;
|
|
97
|
+
dataDisposable.dispose();
|
|
98
|
+
exitDisposable.dispose();
|
|
99
|
+
try {
|
|
100
|
+
pty.kill();
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
var init_pty_adapter = __esm({
|
|
107
|
+
"src/pty-adapter.ts"() {
|
|
108
|
+
"use strict";
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// src/supervisor.ts
|
|
113
|
+
import { randomUUID } from "crypto";
|
|
114
|
+
import { serializeError } from "@omnicross/core/serializeError";
|
|
115
|
+
|
|
116
|
+
// src/child-adapter.ts
|
|
117
|
+
import { spawn as cpSpawn2 } from "child_process";
|
|
118
|
+
|
|
119
|
+
// src/kill-tree.ts
|
|
120
|
+
import { spawn as cpSpawn } from "child_process";
|
|
121
|
+
var TAG = "[ProcessSupervisor]";
|
|
122
|
+
var MIN_GRACE_MS = 0;
|
|
123
|
+
var MAX_GRACE_MS = 6e4;
|
|
124
|
+
var DEFAULT_GRACE_MS = 3e3;
|
|
125
|
+
var IS_WIN = process.platform === "win32";
|
|
126
|
+
function isProcessAlive(pid) {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(pid, 0);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function killProcessTree(pid, opts) {
|
|
135
|
+
const graceMs = Math.max(MIN_GRACE_MS, Math.min(MAX_GRACE_MS, opts?.graceMs ?? DEFAULT_GRACE_MS));
|
|
136
|
+
if (!isProcessAlive(pid)) return;
|
|
137
|
+
if (IS_WIN) {
|
|
138
|
+
killTreeWindows(pid, graceMs);
|
|
139
|
+
} else {
|
|
140
|
+
killTreeUnix(pid, graceMs);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function killTreeWindows(pid, graceMs) {
|
|
144
|
+
try {
|
|
145
|
+
cpSpawn("taskkill", ["/T", "/PID", String(pid)], {
|
|
146
|
+
detached: true,
|
|
147
|
+
windowsHide: true,
|
|
148
|
+
stdio: "ignore"
|
|
149
|
+
}).unref();
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.warn(TAG, `taskkill graceful failed for pid=${pid}:`, err);
|
|
152
|
+
}
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
if (!isProcessAlive(pid)) return;
|
|
155
|
+
try {
|
|
156
|
+
cpSpawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
157
|
+
detached: true,
|
|
158
|
+
windowsHide: true,
|
|
159
|
+
stdio: "ignore"
|
|
160
|
+
}).unref();
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.warn(TAG, `taskkill forced failed for pid=${pid}:`, err);
|
|
163
|
+
}
|
|
164
|
+
}, graceMs);
|
|
165
|
+
timer.unref();
|
|
166
|
+
}
|
|
167
|
+
function killTreeUnix(pid, graceMs) {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(-pid, "SIGTERM");
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err.code !== "ESRCH") {
|
|
172
|
+
console.warn(TAG, `SIGTERM failed for pgid=${pid}:`, err);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(-pid, "SIGKILL");
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err.code !== "ESRCH") {
|
|
181
|
+
console.warn(TAG, `SIGKILL failed for pgid=${pid}:`, err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, graceMs);
|
|
185
|
+
timer.unref();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/child-adapter.ts
|
|
189
|
+
var TAG2 = "[ProcessSupervisor]";
|
|
190
|
+
var IS_WIN2 = process.platform === "win32";
|
|
191
|
+
function createChildAdapter(input) {
|
|
192
|
+
const [command, ...args] = input.argv;
|
|
193
|
+
if (!command) {
|
|
194
|
+
throw new Error(`${TAG2} argv must have at least one element (the command)`);
|
|
195
|
+
}
|
|
196
|
+
const opts = {
|
|
197
|
+
cwd: input.cwd,
|
|
198
|
+
env: input.env ? { ...process.env, ...input.env } : void 0,
|
|
199
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
200
|
+
windowsHide: IS_WIN2,
|
|
201
|
+
windowsVerbatimArguments: input.windowsVerbatimArguments,
|
|
202
|
+
detached: !IS_WIN2
|
|
203
|
+
};
|
|
204
|
+
const child = cpSpawn2(command, args, opts);
|
|
205
|
+
const stdinMode = input.stdinMode ?? (input.input ? "pipe-closed" : "pipe-open");
|
|
206
|
+
if (input.input && child.stdin) {
|
|
207
|
+
child.stdin.write(input.input, () => {
|
|
208
|
+
if (stdinMode === "pipe-closed") {
|
|
209
|
+
child.stdin?.end();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
} else if (stdinMode === "pipe-closed" && child.stdin) {
|
|
213
|
+
child.stdin.end();
|
|
214
|
+
}
|
|
215
|
+
let disposed = false;
|
|
216
|
+
return {
|
|
217
|
+
pid: child.pid,
|
|
218
|
+
onStdout(cb) {
|
|
219
|
+
child.stdout?.setEncoding("utf8");
|
|
220
|
+
child.stdout?.on("data", cb);
|
|
221
|
+
},
|
|
222
|
+
onStderr(cb) {
|
|
223
|
+
child.stderr?.setEncoding("utf8");
|
|
224
|
+
child.stderr?.on("data", cb);
|
|
225
|
+
},
|
|
226
|
+
wait() {
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
child.on("error", (err) => {
|
|
229
|
+
console.error(TAG2, `spawn error for "${command}":`, err.message);
|
|
230
|
+
resolve({ code: -1, signal: null });
|
|
231
|
+
});
|
|
232
|
+
child.on("close", (code, signal) => {
|
|
233
|
+
resolve({ code, signal: signal ?? null });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
kill(signal) {
|
|
238
|
+
if (disposed || !child.pid) return;
|
|
239
|
+
if (signal === "SIGKILL") {
|
|
240
|
+
killProcessTree(child.pid);
|
|
241
|
+
} else {
|
|
242
|
+
try {
|
|
243
|
+
child.kill(signal ?? "SIGTERM");
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.warn(TAG2, `child.kill failed for pid=${child.pid}:`, err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
stdin: child.stdin,
|
|
250
|
+
dispose() {
|
|
251
|
+
if (disposed) return;
|
|
252
|
+
disposed = true;
|
|
253
|
+
child.stdout?.removeAllListeners();
|
|
254
|
+
child.stderr?.removeAllListeners();
|
|
255
|
+
child.removeAllListeners();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/run-registry.ts
|
|
261
|
+
var MAX_EXITED = 500;
|
|
262
|
+
function createRunRegistry() {
|
|
263
|
+
const records = /* @__PURE__ */ new Map();
|
|
264
|
+
function shallowCopy(r) {
|
|
265
|
+
return { ...r };
|
|
266
|
+
}
|
|
267
|
+
function pruneExited() {
|
|
268
|
+
const exited = [];
|
|
269
|
+
for (const r of records.values()) {
|
|
270
|
+
if (r.state === "exited") exited.push(r);
|
|
271
|
+
}
|
|
272
|
+
if (exited.length <= MAX_EXITED) return;
|
|
273
|
+
exited.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
|
|
274
|
+
const toRemove = exited.length - MAX_EXITED;
|
|
275
|
+
for (let i = 0; i < toRemove; i++) {
|
|
276
|
+
records.delete(exited[i].runId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
add(record) {
|
|
281
|
+
records.set(record.runId, { ...record });
|
|
282
|
+
},
|
|
283
|
+
get(runId) {
|
|
284
|
+
const r = records.get(runId);
|
|
285
|
+
return r ? shallowCopy(r) : void 0;
|
|
286
|
+
},
|
|
287
|
+
list() {
|
|
288
|
+
return Array.from(records.values()).map(shallowCopy);
|
|
289
|
+
},
|
|
290
|
+
listByScope(scopeKey) {
|
|
291
|
+
const result = [];
|
|
292
|
+
for (const r of records.values()) {
|
|
293
|
+
if (r.scopeKey === scopeKey) result.push(shallowCopy(r));
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
},
|
|
297
|
+
updateState(runId, state, patch) {
|
|
298
|
+
const r = records.get(runId);
|
|
299
|
+
if (!r) return;
|
|
300
|
+
r.state = state;
|
|
301
|
+
r.updatedAtMs = Date.now();
|
|
302
|
+
if (patch) {
|
|
303
|
+
if (patch.pid !== void 0) r.pid = patch.pid;
|
|
304
|
+
if (patch.terminationReason !== void 0) r.terminationReason = patch.terminationReason;
|
|
305
|
+
if (patch.exitCode !== void 0) r.exitCode = patch.exitCode;
|
|
306
|
+
if (patch.exitSignal !== void 0) r.exitSignal = patch.exitSignal;
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
touchOutput(runId) {
|
|
310
|
+
const r = records.get(runId);
|
|
311
|
+
if (!r) return;
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
r.lastOutputAtMs = now;
|
|
314
|
+
r.updatedAtMs = now;
|
|
315
|
+
},
|
|
316
|
+
finalize(runId, reason, exitCode, exitSignal) {
|
|
317
|
+
const r = records.get(runId);
|
|
318
|
+
if (!r) return;
|
|
319
|
+
r.state = "exited";
|
|
320
|
+
if (!r.terminationReason) {
|
|
321
|
+
r.terminationReason = reason;
|
|
322
|
+
}
|
|
323
|
+
if (exitCode !== void 0) r.exitCode = exitCode;
|
|
324
|
+
if (exitSignal !== void 0) r.exitSignal = exitSignal;
|
|
325
|
+
r.updatedAtMs = Date.now();
|
|
326
|
+
pruneExited();
|
|
327
|
+
},
|
|
328
|
+
delete(runId) {
|
|
329
|
+
records.delete(runId);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/supervisor.ts
|
|
335
|
+
var TAG3 = "[ProcessSupervisor]";
|
|
336
|
+
function createProcessSupervisor() {
|
|
337
|
+
const registry = createRunRegistry();
|
|
338
|
+
const active = /* @__PURE__ */ new Map();
|
|
339
|
+
function spawn(input) {
|
|
340
|
+
const runId = input.runId ?? randomUUID();
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const captureOutput = input.captureOutput ?? true;
|
|
343
|
+
if (input.replaceExistingScope && input.scopeKey) {
|
|
344
|
+
cancelScope(input.scopeKey, "manual-cancel");
|
|
345
|
+
}
|
|
346
|
+
const record = {
|
|
347
|
+
runId,
|
|
348
|
+
sessionId: input.sessionId,
|
|
349
|
+
backendId: input.backendId,
|
|
350
|
+
scopeKey: input.scopeKey,
|
|
351
|
+
startedAtMs: now,
|
|
352
|
+
lastOutputAtMs: now,
|
|
353
|
+
createdAtMs: now,
|
|
354
|
+
updatedAtMs: now,
|
|
355
|
+
state: "starting"
|
|
356
|
+
};
|
|
357
|
+
registry.add(record);
|
|
358
|
+
let stdout = "";
|
|
359
|
+
let stderr = "";
|
|
360
|
+
let forcedReason = null;
|
|
361
|
+
let settled = false;
|
|
362
|
+
let overallTimer = null;
|
|
363
|
+
let noOutputTimer = null;
|
|
364
|
+
let adapter;
|
|
365
|
+
try {
|
|
366
|
+
if (input.mode === "pty") {
|
|
367
|
+
const { createPtyAdapter: createPtyAdapter2 } = (init_pty_adapter(), __toCommonJS(pty_adapter_exports));
|
|
368
|
+
adapter = createPtyAdapter2(input);
|
|
369
|
+
} else {
|
|
370
|
+
adapter = createChildAdapter(input);
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
registry.finalize(runId, "spawn-error", -1, null);
|
|
374
|
+
const exit = {
|
|
375
|
+
reason: "spawn-error",
|
|
376
|
+
exitCode: -1,
|
|
377
|
+
exitSignal: null,
|
|
378
|
+
durationMs: 0,
|
|
379
|
+
stdout: "",
|
|
380
|
+
stderr: serializeError(err),
|
|
381
|
+
timedOut: false,
|
|
382
|
+
noOutputTimedOut: false
|
|
383
|
+
};
|
|
384
|
+
return {
|
|
385
|
+
runId,
|
|
386
|
+
startedAtMs: now,
|
|
387
|
+
wait: () => Promise.resolve(exit),
|
|
388
|
+
cancel: () => {
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
registry.updateState(runId, "running", { pid: adapter.pid });
|
|
393
|
+
function cancelRun(reason) {
|
|
394
|
+
if (settled) return;
|
|
395
|
+
forcedReason = reason ?? "manual-cancel";
|
|
396
|
+
registry.updateState(runId, "exiting", { terminationReason: forcedReason });
|
|
397
|
+
adapter.kill("SIGKILL");
|
|
398
|
+
}
|
|
399
|
+
active.set(runId, { cancel: cancelRun, adapter, scopeKey: input.scopeKey });
|
|
400
|
+
function clearTimers() {
|
|
401
|
+
if (overallTimer) {
|
|
402
|
+
clearTimeout(overallTimer);
|
|
403
|
+
overallTimer = null;
|
|
404
|
+
}
|
|
405
|
+
if (noOutputTimer) {
|
|
406
|
+
clearTimeout(noOutputTimer);
|
|
407
|
+
noOutputTimer = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (input.timeoutMs && input.timeoutMs > 0) {
|
|
411
|
+
overallTimer = setTimeout(() => {
|
|
412
|
+
if (!settled) {
|
|
413
|
+
console.warn(TAG3, `overall timeout (${input.timeoutMs}ms) for run=${runId}`);
|
|
414
|
+
cancelRun("overall-timeout");
|
|
415
|
+
}
|
|
416
|
+
}, input.timeoutMs);
|
|
417
|
+
overallTimer.unref();
|
|
418
|
+
}
|
|
419
|
+
function resetNoOutputTimer() {
|
|
420
|
+
if (noOutputTimer) clearTimeout(noOutputTimer);
|
|
421
|
+
if (!input.noOutputTimeoutMs || input.noOutputTimeoutMs <= 0 || settled) return;
|
|
422
|
+
noOutputTimer = setTimeout(() => {
|
|
423
|
+
if (!settled) {
|
|
424
|
+
console.warn(TAG3, `no-output timeout (${input.noOutputTimeoutMs}ms) for run=${runId}`);
|
|
425
|
+
cancelRun("no-output-timeout");
|
|
426
|
+
}
|
|
427
|
+
}, input.noOutputTimeoutMs);
|
|
428
|
+
noOutputTimer.unref();
|
|
429
|
+
}
|
|
430
|
+
resetNoOutputTimer();
|
|
431
|
+
adapter.onStdout((chunk) => {
|
|
432
|
+
if (captureOutput) stdout += chunk;
|
|
433
|
+
registry.touchOutput(runId);
|
|
434
|
+
resetNoOutputTimer();
|
|
435
|
+
input.onStdout?.(chunk);
|
|
436
|
+
});
|
|
437
|
+
adapter.onStderr((chunk) => {
|
|
438
|
+
if (captureOutput) stderr += chunk;
|
|
439
|
+
registry.touchOutput(runId);
|
|
440
|
+
resetNoOutputTimer();
|
|
441
|
+
input.onStderr?.(chunk);
|
|
442
|
+
});
|
|
443
|
+
let stdinHandle;
|
|
444
|
+
if (adapter.stdin) {
|
|
445
|
+
stdinHandle = {
|
|
446
|
+
write(data) {
|
|
447
|
+
adapter.stdin?.write(data);
|
|
448
|
+
},
|
|
449
|
+
end() {
|
|
450
|
+
adapter.stdin?.end();
|
|
451
|
+
},
|
|
452
|
+
destroy() {
|
|
453
|
+
adapter.stdin?.destroy();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const waitPromise = adapter.wait().then(({ code, signal }) => {
|
|
458
|
+
settled = true;
|
|
459
|
+
clearTimers();
|
|
460
|
+
const reason = forcedReason ?? (code === -1 ? "spawn-error" : signal ? "signal" : "exit");
|
|
461
|
+
registry.finalize(runId, reason, code, signal);
|
|
462
|
+
active.delete(runId);
|
|
463
|
+
adapter.dispose();
|
|
464
|
+
return {
|
|
465
|
+
reason,
|
|
466
|
+
exitCode: code,
|
|
467
|
+
exitSignal: signal,
|
|
468
|
+
durationMs: Date.now() - now,
|
|
469
|
+
stdout,
|
|
470
|
+
stderr,
|
|
471
|
+
timedOut: reason === "overall-timeout",
|
|
472
|
+
noOutputTimedOut: reason === "no-output-timeout"
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
let cachedWait = null;
|
|
476
|
+
return {
|
|
477
|
+
runId,
|
|
478
|
+
pid: adapter.pid,
|
|
479
|
+
startedAtMs: now,
|
|
480
|
+
stdin: stdinHandle,
|
|
481
|
+
wait() {
|
|
482
|
+
if (!cachedWait) cachedWait = waitPromise;
|
|
483
|
+
return cachedWait;
|
|
484
|
+
},
|
|
485
|
+
cancel: cancelRun
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function cancel(runId, reason) {
|
|
489
|
+
const entry = active.get(runId);
|
|
490
|
+
if (!entry) return;
|
|
491
|
+
entry.cancel(reason);
|
|
492
|
+
}
|
|
493
|
+
function cancelScope(scopeKey, reason) {
|
|
494
|
+
for (const [runId, entry] of active) {
|
|
495
|
+
if (entry.scopeKey === scopeKey) {
|
|
496
|
+
entry.cancel(reason);
|
|
497
|
+
console.warn(TAG3, `cancelled run=${runId} in scope="${scopeKey}"`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function getRecord(runId) {
|
|
502
|
+
return registry.get(runId);
|
|
503
|
+
}
|
|
504
|
+
function resizePty(runId, cols, rows) {
|
|
505
|
+
const entry = active.get(runId);
|
|
506
|
+
if (!entry) return false;
|
|
507
|
+
if ("resize" in entry.adapter && typeof entry.adapter.resize === "function") {
|
|
508
|
+
entry.adapter.resize(cols, rows);
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
return { spawn, cancel, cancelScope, getRecord, resizePty };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/proxy-env/claude-proxy-env.ts
|
|
517
|
+
import { resolveProviderEndpoint } from "@omnicross/core/completion";
|
|
518
|
+
import { getProviderProxy } from "@omnicross/core/provider-proxy";
|
|
519
|
+
var CLAUDE_PROXY_API_KEY_SENTINEL = "omnicross-proxy";
|
|
520
|
+
async function buildClaudeCliLaunchConfig(inputs) {
|
|
521
|
+
const provider = await inputs.llmConfig.getProvider(inputs.providerId);
|
|
522
|
+
if (!provider) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`claude proxy: provider not found: ${inputs.providerId}. Add a provider row to the daemon config (omnicross providers add \u2026).`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const { apiKey } = resolveProviderEndpoint(provider);
|
|
528
|
+
if (!resolveApiKey(apiKey)) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`claude proxy: provider "${inputs.providerId}" has no valid API key. Set apiKey (or a $ENV_VAR reference) on the provider row.`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const route = {
|
|
534
|
+
sessionId: inputs.sessionId ?? null,
|
|
535
|
+
targetProviderFormat: provider.apiFormat === "anthropic" ? "anthropic" : "transform",
|
|
536
|
+
model: inputs.model,
|
|
537
|
+
ingressFormat: "anthropic-messages",
|
|
538
|
+
authMode: "byo",
|
|
539
|
+
providerId: inputs.providerId
|
|
540
|
+
};
|
|
541
|
+
const proxy = getProviderProxy();
|
|
542
|
+
const token = proxy.addRoute(route);
|
|
543
|
+
const baseUrl = proxy.getBaseUrl();
|
|
544
|
+
console.info("[buildClaudeCliLaunchConfig] claude route ready", {
|
|
545
|
+
sessionId: inputs.sessionId,
|
|
546
|
+
baseUrl,
|
|
547
|
+
providerId: inputs.providerId,
|
|
548
|
+
model: inputs.model
|
|
549
|
+
});
|
|
550
|
+
const env = {
|
|
551
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
552
|
+
ANTHROPIC_AUTH_TOKEN: token,
|
|
553
|
+
ANTHROPIC_API_KEY: CLAUDE_PROXY_API_KEY_SENTINEL,
|
|
554
|
+
ANTHROPIC_MODEL: inputs.model
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
env,
|
|
558
|
+
baseUrl,
|
|
559
|
+
onSessionEnd: () => {
|
|
560
|
+
try {
|
|
561
|
+
proxy.removeRoute(token);
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function resolveApiKey(apiKey) {
|
|
568
|
+
if (!apiKey) return "";
|
|
569
|
+
if (apiKey.startsWith("$")) {
|
|
570
|
+
return process.env[apiKey.slice(1)] || "";
|
|
571
|
+
}
|
|
572
|
+
return apiKey;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/proxy-env/cli-proxy-env.ts
|
|
576
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
577
|
+
import { tmpdir } from "os";
|
|
578
|
+
import { join } from "path";
|
|
579
|
+
import { resolveProviderEndpoint as resolveProviderEndpoint2 } from "@omnicross/core/completion";
|
|
580
|
+
import { getProviderProxy as getProviderProxy2 } from "@omnicross/core/provider-proxy";
|
|
581
|
+
var CHAT_PROXY_BASE_PATH = "/v1";
|
|
582
|
+
var OPENCODE_PROXY_PROVIDER_ID = "omnicross";
|
|
583
|
+
var OPENCODE_PROXY_TOKEN_ENV = "OMNICROSS_OPENCODE_TOKEN";
|
|
584
|
+
async function buildChatCliLaunchConfig(inputs) {
|
|
585
|
+
const provider = await inputs.llmConfig.getProvider(inputs.providerId);
|
|
586
|
+
if (!provider) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`${inputs.backendId} proxy: provider not found: ${inputs.providerId}. Add an OpenAI-compatible provider in Settings \u2192 LLM Providers.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const { apiKey } = resolveProviderEndpoint2(provider);
|
|
592
|
+
if (!resolveApiKey2(apiKey)) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`${inputs.backendId} proxy: provider "${inputs.providerId}" has no valid API key. Add an API key in Settings \u2192 LLM Providers.`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const route = {
|
|
598
|
+
sessionId: inputs.sessionId ?? null,
|
|
599
|
+
targetProviderFormat: "transform",
|
|
600
|
+
model: inputs.model,
|
|
601
|
+
ingressFormat: "openai-chat",
|
|
602
|
+
authMode: "byo",
|
|
603
|
+
providerId: inputs.providerId
|
|
604
|
+
};
|
|
605
|
+
const proxy = getProviderProxy2();
|
|
606
|
+
const token = proxy.addRoute(route);
|
|
607
|
+
const baseUrl = proxy.getBaseUrl();
|
|
608
|
+
const chatBase = `${baseUrl}${CHAT_PROXY_BASE_PATH}`;
|
|
609
|
+
const cleanups = [() => proxy.removeRoute(token)];
|
|
610
|
+
const env = buildBackendEnv(inputs.backendId, {
|
|
611
|
+
chatBase,
|
|
612
|
+
baseUrl,
|
|
613
|
+
token,
|
|
614
|
+
model: inputs.model,
|
|
615
|
+
cleanups
|
|
616
|
+
});
|
|
617
|
+
console.log("[buildChatCliLaunchConfig] chat-CLI route ready", {
|
|
618
|
+
backendId: inputs.backendId,
|
|
619
|
+
sessionId: inputs.sessionId,
|
|
620
|
+
baseUrl,
|
|
621
|
+
providerId: inputs.providerId,
|
|
622
|
+
model: inputs.model
|
|
623
|
+
});
|
|
624
|
+
return {
|
|
625
|
+
env,
|
|
626
|
+
baseUrl,
|
|
627
|
+
onSessionEnd: () => {
|
|
628
|
+
for (const c of cleanups) {
|
|
629
|
+
try {
|
|
630
|
+
c();
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function buildBackendEnv(backendId, ctx) {
|
|
638
|
+
switch (backendId) {
|
|
639
|
+
case "qwen":
|
|
640
|
+
return {
|
|
641
|
+
OPENAI_BASE_URL: ctx.chatBase,
|
|
642
|
+
OPENAI_API_KEY: ctx.token,
|
|
643
|
+
OPENAI_MODEL: ctx.model
|
|
644
|
+
};
|
|
645
|
+
case "copilot":
|
|
646
|
+
return {
|
|
647
|
+
COPILOT_PROVIDER_BASE_URL: ctx.chatBase,
|
|
648
|
+
COPILOT_PROVIDER_TYPE: "openai",
|
|
649
|
+
COPILOT_PROVIDER_API_KEY: ctx.token,
|
|
650
|
+
COPILOT_MODEL: ctx.model
|
|
651
|
+
};
|
|
652
|
+
case "opencode":
|
|
653
|
+
return buildOpencodeEnv(ctx);
|
|
654
|
+
default: {
|
|
655
|
+
const _exhaustive = backendId;
|
|
656
|
+
throw new Error(`Unsupported chat CLI backend: ${String(_exhaustive)}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function buildOpencodeEnv(ctx) {
|
|
661
|
+
const id = OPENCODE_PROXY_PROVIDER_ID;
|
|
662
|
+
const config = {
|
|
663
|
+
$schema: "https://opencode.ai/config.json",
|
|
664
|
+
provider: {
|
|
665
|
+
[id]: {
|
|
666
|
+
npm: "@ai-sdk/openai-compatible",
|
|
667
|
+
name: "Omnicross Proxy",
|
|
668
|
+
options: {
|
|
669
|
+
baseURL: ctx.chatBase,
|
|
670
|
+
apiKey: `{env:${OPENCODE_PROXY_TOKEN_ENV}}`
|
|
671
|
+
},
|
|
672
|
+
models: {
|
|
673
|
+
[ctx.model]: {}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const dir = mkdtempSync(join(tmpdir(), "omnicross-opencode-"));
|
|
679
|
+
const configPath = join(dir, "opencode.json");
|
|
680
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
681
|
+
ctx.cleanups.push(() => rmSync(dir, { recursive: true, force: true }));
|
|
682
|
+
return {
|
|
683
|
+
OPENCODE_CONFIG: configPath,
|
|
684
|
+
[OPENCODE_PROXY_TOKEN_ENV]: ctx.token
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async function buildGeminiCliLaunchConfig(inputs) {
|
|
688
|
+
const provider = await inputs.llmConfig.getProvider(inputs.providerId);
|
|
689
|
+
if (!provider) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`gemini-cli proxy: provider not found: ${inputs.providerId}. Add a Gemini-compatible provider in Settings \u2192 LLM Providers.`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const { apiKey } = resolveProviderEndpoint2(provider);
|
|
695
|
+
if (!resolveApiKey2(apiKey)) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`gemini-cli proxy: provider "${inputs.providerId}" has no valid API key. Add an API key in Settings \u2192 LLM Providers.`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const route = {
|
|
701
|
+
sessionId: inputs.sessionId ?? null,
|
|
702
|
+
targetProviderFormat: "transform",
|
|
703
|
+
model: inputs.model,
|
|
704
|
+
ingressFormat: "gemini-generatecontent",
|
|
705
|
+
authMode: "byo",
|
|
706
|
+
providerId: inputs.providerId
|
|
707
|
+
};
|
|
708
|
+
const proxy = getProviderProxy2();
|
|
709
|
+
const token = proxy.addRoute(route);
|
|
710
|
+
const baseUrl = proxy.getBaseUrl();
|
|
711
|
+
console.log("[buildGeminiCliLaunchConfig] gemini-CLI route ready (forced API-key)", {
|
|
712
|
+
sessionId: inputs.sessionId,
|
|
713
|
+
baseUrl,
|
|
714
|
+
providerId: inputs.providerId,
|
|
715
|
+
model: inputs.model
|
|
716
|
+
});
|
|
717
|
+
const env = {
|
|
718
|
+
GOOGLE_GEMINI_BASE_URL: baseUrl,
|
|
719
|
+
GEMINI_API_KEY: token,
|
|
720
|
+
GEMINI_MODEL: inputs.model,
|
|
721
|
+
// Force API-key mode off the OAuth/Code-Assist (non-interceptable) path.
|
|
722
|
+
GOOGLE_GENAI_USE_GCA: "false"
|
|
723
|
+
};
|
|
724
|
+
return {
|
|
725
|
+
env,
|
|
726
|
+
baseUrl,
|
|
727
|
+
onSessionEnd: () => {
|
|
728
|
+
try {
|
|
729
|
+
proxy.removeRoute(token);
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function resolveApiKey2(apiKey) {
|
|
736
|
+
if (!apiKey) return "";
|
|
737
|
+
if (apiKey.startsWith("$")) {
|
|
738
|
+
return process.env[apiKey.slice(1)] || "";
|
|
739
|
+
}
|
|
740
|
+
return apiKey;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/proxy-env/codex-proxy-env.ts
|
|
744
|
+
import { resolveProviderEndpoint as resolveProviderEndpoint3 } from "@omnicross/core/completion";
|
|
745
|
+
import { getProviderProxy as getProviderProxy3 } from "@omnicross/core/provider-proxy";
|
|
746
|
+
var CODEX_PROXY_PROVIDER_NAME = "omnicross";
|
|
747
|
+
var CODEX_PROXY_BASE_PATH = "/openai";
|
|
748
|
+
function buildCodexConfigOverrides(baseUrl) {
|
|
749
|
+
const name = CODEX_PROXY_PROVIDER_NAME;
|
|
750
|
+
const providerBaseUrl = `${baseUrl}${CODEX_PROXY_BASE_PATH}`;
|
|
751
|
+
return [
|
|
752
|
+
"-c",
|
|
753
|
+
`model_provider="${name}"`,
|
|
754
|
+
"-c",
|
|
755
|
+
`model_providers.${name}.name="${name}"`,
|
|
756
|
+
"-c",
|
|
757
|
+
`model_providers.${name}.base_url="${providerBaseUrl}"`,
|
|
758
|
+
"-c",
|
|
759
|
+
`model_providers.${name}.wire_api="responses"`,
|
|
760
|
+
// requires_openai_auth is a boolean — UNQUOTED so the TOML override is typed
|
|
761
|
+
// as a bool, not the string "true".
|
|
762
|
+
"-c",
|
|
763
|
+
`model_providers.${name}.requires_openai_auth=true`,
|
|
764
|
+
// disable_response_storage: the CLI sends full context each turn (no
|
|
765
|
+
// server-side response store) — required because the proxy upstream is
|
|
766
|
+
// stateless w.r.t. Codex's response-id store (design Q3 contract).
|
|
767
|
+
"-c",
|
|
768
|
+
"disable_response_storage=true"
|
|
769
|
+
];
|
|
770
|
+
}
|
|
771
|
+
async function buildCodexLaunchConfig(inputs) {
|
|
772
|
+
const authMode = inputs.authMode ?? "byo";
|
|
773
|
+
if (authMode === "byo") {
|
|
774
|
+
const provider = await inputs.llmConfig.getProvider(inputs.providerId);
|
|
775
|
+
if (!provider) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`Codex proxy: provider not found: ${inputs.providerId}. Add an OpenAI-compatible provider in Settings \u2192 LLM Providers.`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const { apiKey } = resolveProviderEndpoint3(provider);
|
|
781
|
+
const resolved = resolveApiKey3(apiKey);
|
|
782
|
+
if (!resolved) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Codex proxy: provider "${inputs.providerId}" has no valid API key. Add an API key in Settings \u2192 LLM Providers.`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
} else if (!inputs.subscriptionProfile) {
|
|
788
|
+
throw new Error("Codex proxy: subscription mode requires a codex subscription profile.");
|
|
789
|
+
}
|
|
790
|
+
const route = {
|
|
791
|
+
sessionId: inputs.sessionId ?? null,
|
|
792
|
+
targetProviderFormat: "openai-responses",
|
|
793
|
+
model: inputs.model,
|
|
794
|
+
ingressFormat: "openai-responses",
|
|
795
|
+
authMode,
|
|
796
|
+
providerId: inputs.providerId,
|
|
797
|
+
subscriptionProfile: inputs.subscriptionProfile ?? null
|
|
798
|
+
};
|
|
799
|
+
const proxy = getProviderProxy3();
|
|
800
|
+
const token = proxy.addRoute(route);
|
|
801
|
+
const baseUrl = proxy.getBaseUrl();
|
|
802
|
+
console.log("[buildCodexLaunchConfig] Codex route ready", {
|
|
803
|
+
sessionId: inputs.sessionId,
|
|
804
|
+
baseUrl,
|
|
805
|
+
providerId: inputs.providerId,
|
|
806
|
+
model: inputs.model,
|
|
807
|
+
authMode
|
|
808
|
+
});
|
|
809
|
+
return {
|
|
810
|
+
env: {
|
|
811
|
+
// Satisfy the CLI's `requires_openai_auth = true` precondition. The CLI
|
|
812
|
+
// forwards this as the Responses-API key; the resident proxy uses it as
|
|
813
|
+
// the route TOKEN (looked up, then discarded) and re-authenticates
|
|
814
|
+
// upstream from the route's own provider/subscription credential.
|
|
815
|
+
OPENAI_API_KEY: token
|
|
816
|
+
},
|
|
817
|
+
extraArgs: buildCodexConfigOverrides(baseUrl),
|
|
818
|
+
baseUrl,
|
|
819
|
+
onSessionEnd: () => {
|
|
820
|
+
proxy.removeRoute(token);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function resolveApiKey3(apiKey) {
|
|
825
|
+
if (!apiKey) return "";
|
|
826
|
+
if (apiKey.startsWith("$")) {
|
|
827
|
+
return process.env[apiKey.slice(1)] || "";
|
|
828
|
+
}
|
|
829
|
+
return apiKey;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/index.ts
|
|
833
|
+
var instance = null;
|
|
834
|
+
function getProcessSupervisor() {
|
|
835
|
+
if (!instance) instance = createProcessSupervisor();
|
|
836
|
+
return instance;
|
|
837
|
+
}
|
|
838
|
+
export {
|
|
839
|
+
CHAT_PROXY_BASE_PATH,
|
|
840
|
+
CLAUDE_PROXY_API_KEY_SENTINEL,
|
|
841
|
+
CODEX_PROXY_BASE_PATH,
|
|
842
|
+
CODEX_PROXY_PROVIDER_NAME,
|
|
843
|
+
OPENCODE_PROXY_PROVIDER_ID,
|
|
844
|
+
OPENCODE_PROXY_TOKEN_ENV,
|
|
845
|
+
buildChatCliLaunchConfig,
|
|
846
|
+
buildClaudeCliLaunchConfig,
|
|
847
|
+
buildCodexConfigOverrides,
|
|
848
|
+
buildCodexLaunchConfig,
|
|
849
|
+
buildGeminiCliLaunchConfig,
|
|
850
|
+
getProcessSupervisor
|
|
851
|
+
};
|