copilot-hub 0.1.6 → 0.1.8
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 +29 -18
- package/apps/control-plane/.env.example +2 -3
- package/apps/control-plane/README.md +0 -1
- package/apps/control-plane/dist/channels/telegram-channel.js +126 -2
- package/apps/control-plane/dist/copilot-hub.js +2 -20
- package/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/core/dist/agent-supervisor.js +38 -3
- package/packages/core/dist/agent-supervisor.js.map +1 -1
- package/packages/core/dist/bot-runtime.d.ts +1 -0
- package/packages/core/dist/bot-runtime.js +8 -0
- package/packages/core/dist/bot-runtime.js.map +1 -1
- package/packages/core/dist/bridge-service.d.ts +1 -0
- package/packages/core/dist/bridge-service.js +6 -0
- package/packages/core/dist/bridge-service.js.map +1 -1
- package/packages/core/dist/codex-app-client.d.ts +1 -0
- package/packages/core/dist/codex-app-client.js +172 -0
- package/packages/core/dist/codex-app-client.js.map +1 -1
- package/packages/core/dist/codex-provider.d.ts +1 -0
- package/packages/core/dist/codex-provider.js +9 -0
- package/packages/core/dist/codex-provider.js.map +1 -1
- package/packages/core/dist/telegram-channel.js +126 -2
- package/packages/core/dist/telegram-channel.js.map +1 -1
- package/scripts/dist/cli.mjs +126 -6
- package/scripts/dist/daemon.mjs +388 -0
- package/scripts/dist/service.mjs +520 -0
- package/scripts/dist/supervisor.mjs +22 -3
- package/scripts/src/cli.mts +148 -6
- package/scripts/src/daemon.mts +458 -0
- package/scripts/src/service.mts +635 -0
- package/scripts/src/supervisor.mts +25 -3
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const runtimeDir = path.join(repoRoot, ".copilot-hub");
|
|
11
|
+
const pidsDir = path.join(runtimeDir, "pids");
|
|
12
|
+
const logsDir = path.join(repoRoot, "logs");
|
|
13
|
+
const daemonStatePath = path.join(pidsDir, "daemon.json");
|
|
14
|
+
const daemonLogPath = path.join(logsDir, "service-daemon.log");
|
|
15
|
+
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
16
|
+
const supervisorScriptPath = path.join(repoRoot, "scripts", "dist", "supervisor.mjs");
|
|
17
|
+
const nodeBin = process.execPath;
|
|
18
|
+
const BASE_CHECK_MS = 5000;
|
|
19
|
+
const MAX_BACKOFF_MS = 60000;
|
|
20
|
+
const action = String(process.argv[2] ?? "status")
|
|
21
|
+
.trim()
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
try {
|
|
24
|
+
await main();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error(getErrorMessage(error));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
async function main() {
|
|
31
|
+
switch (action) {
|
|
32
|
+
case "start":
|
|
33
|
+
await startDaemonProcess();
|
|
34
|
+
return;
|
|
35
|
+
case "run":
|
|
36
|
+
await runDaemonLoop();
|
|
37
|
+
return;
|
|
38
|
+
case "stop":
|
|
39
|
+
await stopDaemonProcess();
|
|
40
|
+
return;
|
|
41
|
+
case "status":
|
|
42
|
+
showDaemonStatus();
|
|
43
|
+
return;
|
|
44
|
+
case "help":
|
|
45
|
+
printUsage();
|
|
46
|
+
return;
|
|
47
|
+
default:
|
|
48
|
+
printUsage();
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function startDaemonProcess() {
|
|
53
|
+
ensureScripts();
|
|
54
|
+
ensureRuntimeDirs();
|
|
55
|
+
const existingPid = getRunningDaemonPid();
|
|
56
|
+
if (existingPid > 0) {
|
|
57
|
+
console.log(`[daemon] already running (pid ${existingPid})`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
removeDaemonState();
|
|
61
|
+
const logFd = fs.openSync(daemonLogPath, "a");
|
|
62
|
+
let child;
|
|
63
|
+
try {
|
|
64
|
+
child = spawn(nodeBin, [daemonScriptPath, "run"], {
|
|
65
|
+
cwd: repoRoot,
|
|
66
|
+
detached: true,
|
|
67
|
+
stdio: ["ignore", logFd, logFd],
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
shell: false,
|
|
70
|
+
env: process.env,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
fs.closeSync(logFd);
|
|
75
|
+
}
|
|
76
|
+
const pid = normalizePid(child?.pid);
|
|
77
|
+
if (pid <= 0) {
|
|
78
|
+
throw new Error("Failed to spawn daemon process.");
|
|
79
|
+
}
|
|
80
|
+
child.unref();
|
|
81
|
+
const ready = await waitForExit(pid, 250, false);
|
|
82
|
+
if (ready) {
|
|
83
|
+
throw new Error(`Daemon process exited immediately (pid ${pid}). Check logs: ${daemonLogPath}`);
|
|
84
|
+
}
|
|
85
|
+
console.log(`[daemon] started (pid ${pid})`);
|
|
86
|
+
}
|
|
87
|
+
async function runDaemonLoop() {
|
|
88
|
+
ensureScripts();
|
|
89
|
+
ensureRuntimeDirs();
|
|
90
|
+
const existingPid = getRunningDaemonPid();
|
|
91
|
+
if (existingPid > 0 && existingPid !== process.pid) {
|
|
92
|
+
console.log(`[daemon] already running (pid ${existingPid})`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
writeDaemonState({
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
startedAt: new Date().toISOString(),
|
|
98
|
+
command: `${nodeBin} ${daemonScriptPath} run`,
|
|
99
|
+
});
|
|
100
|
+
const state = { stopping: false, shuttingDown: false };
|
|
101
|
+
setupSignalHandlers(state);
|
|
102
|
+
console.log(`[daemon] running (pid ${process.pid})`);
|
|
103
|
+
let failureCount = 0;
|
|
104
|
+
while (!state.stopping) {
|
|
105
|
+
const ensureResult = runSupervisor("ensure", { allowFailure: true });
|
|
106
|
+
if (ensureResult.ok) {
|
|
107
|
+
if (failureCount > 0) {
|
|
108
|
+
console.log("[daemon] workers recovered.");
|
|
109
|
+
}
|
|
110
|
+
failureCount = 0;
|
|
111
|
+
await sleepInterruptible(BASE_CHECK_MS, () => state.stopping);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
failureCount += 1;
|
|
115
|
+
const delay = computeBackoffDelay(failureCount);
|
|
116
|
+
const reason = firstLine(ensureResult.combinedOutput) ||
|
|
117
|
+
`supervisor ensure exited with code ${String(ensureResult.status ?? "unknown")}`;
|
|
118
|
+
console.error(`[daemon] worker health check failed: ${reason}. Retrying in ${Math.ceil(delay / 1000)}s.`);
|
|
119
|
+
await sleepInterruptible(delay, () => state.stopping);
|
|
120
|
+
}
|
|
121
|
+
await shutdownDaemon(state, { reason: "stop-request", exitCode: 0 });
|
|
122
|
+
}
|
|
123
|
+
async function stopDaemonProcess() {
|
|
124
|
+
ensureRuntimeDirs();
|
|
125
|
+
const pid = getRunningDaemonPid();
|
|
126
|
+
if (pid <= 0) {
|
|
127
|
+
removeDaemonState();
|
|
128
|
+
runSupervisor("down", { allowFailure: true });
|
|
129
|
+
console.log("[daemon] not running.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await terminateProcess(pid);
|
|
133
|
+
if (isProcessRunning(pid)) {
|
|
134
|
+
throw new Error(`Daemon did not stop cleanly (pid ${pid}).`);
|
|
135
|
+
}
|
|
136
|
+
removeDaemonState();
|
|
137
|
+
runSupervisor("down", { allowFailure: true });
|
|
138
|
+
console.log("[daemon] stopped.");
|
|
139
|
+
}
|
|
140
|
+
function showDaemonStatus() {
|
|
141
|
+
ensureRuntimeDirs();
|
|
142
|
+
const pid = getRunningDaemonPid();
|
|
143
|
+
const running = pid > 0;
|
|
144
|
+
if (!running) {
|
|
145
|
+
removeDaemonState();
|
|
146
|
+
}
|
|
147
|
+
console.log("\n=== daemon ===");
|
|
148
|
+
console.log(`running: ${running ? "yes" : "no"}`);
|
|
149
|
+
console.log(`pid: ${running ? String(pid) : "-"}`);
|
|
150
|
+
console.log(`logFile: ${daemonLogPath}`);
|
|
151
|
+
if (!fs.existsSync(supervisorScriptPath)) {
|
|
152
|
+
console.log("\n(worker status unavailable: supervisor script missing)");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log("\n=== workers ===");
|
|
156
|
+
runSupervisor("status", { allowFailure: true, stdio: "inherit" });
|
|
157
|
+
}
|
|
158
|
+
function setupSignalHandlers(state) {
|
|
159
|
+
const requestStop = () => {
|
|
160
|
+
state.stopping = true;
|
|
161
|
+
};
|
|
162
|
+
process.on("SIGINT", requestStop);
|
|
163
|
+
process.on("SIGTERM", requestStop);
|
|
164
|
+
process.on("SIGHUP", requestStop);
|
|
165
|
+
process.on("uncaughtException", (error) => {
|
|
166
|
+
if (!state.shuttingDown) {
|
|
167
|
+
console.error(`[daemon] uncaught exception: ${getErrorMessage(error)}`);
|
|
168
|
+
}
|
|
169
|
+
state.stopping = true;
|
|
170
|
+
void shutdownDaemon(state, { reason: "uncaught-exception", exitCode: 1 });
|
|
171
|
+
});
|
|
172
|
+
process.on("unhandledRejection", (reason) => {
|
|
173
|
+
if (!state.shuttingDown) {
|
|
174
|
+
console.error(`[daemon] unhandled rejection: ${getErrorMessage(reason)}`);
|
|
175
|
+
}
|
|
176
|
+
state.stopping = true;
|
|
177
|
+
void shutdownDaemon(state, { reason: "unhandled-rejection", exitCode: 1 });
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function shutdownDaemon(state, { reason, exitCode }) {
|
|
181
|
+
if (state.shuttingDown) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
state.shuttingDown = true;
|
|
185
|
+
console.log(`[daemon] stopping (${reason})...`);
|
|
186
|
+
runSupervisor("down", { allowFailure: true });
|
|
187
|
+
removeDaemonState();
|
|
188
|
+
process.exit(exitCode);
|
|
189
|
+
}
|
|
190
|
+
function ensureScripts() {
|
|
191
|
+
if (!fs.existsSync(supervisorScriptPath)) {
|
|
192
|
+
throw new Error([
|
|
193
|
+
"Supervisor script is missing.",
|
|
194
|
+
"Run 'npm run build:scripts' (or reinstall package) and retry.",
|
|
195
|
+
].join("\n"));
|
|
196
|
+
}
|
|
197
|
+
if (!fs.existsSync(daemonScriptPath)) {
|
|
198
|
+
throw new Error([
|
|
199
|
+
"Daemon script is missing.",
|
|
200
|
+
"Run 'npm run build:scripts' (or reinstall package) and retry.",
|
|
201
|
+
].join("\n"));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function ensureRuntimeDirs() {
|
|
205
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
206
|
+
fs.mkdirSync(pidsDir, { recursive: true });
|
|
207
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
function getRunningDaemonPid() {
|
|
210
|
+
const state = readDaemonState();
|
|
211
|
+
const pid = normalizePid(state?.pid);
|
|
212
|
+
if (pid <= 0) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
return isProcessRunning(pid) ? pid : 0;
|
|
216
|
+
}
|
|
217
|
+
function readDaemonState() {
|
|
218
|
+
if (!fs.existsSync(daemonStatePath)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(fs.readFileSync(daemonStatePath, "utf8"));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function writeDaemonState(value) {
|
|
229
|
+
fs.mkdirSync(path.dirname(daemonStatePath), { recursive: true });
|
|
230
|
+
fs.writeFileSync(daemonStatePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
231
|
+
}
|
|
232
|
+
function removeDaemonState() {
|
|
233
|
+
if (!fs.existsSync(daemonStatePath)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
fs.rmSync(daemonStatePath, { force: true });
|
|
237
|
+
}
|
|
238
|
+
function normalizePid(value) {
|
|
239
|
+
const pid = Number.parseInt(String(value ?? ""), 10);
|
|
240
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
return pid;
|
|
244
|
+
}
|
|
245
|
+
function isProcessRunning(pid) {
|
|
246
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
process.kill(pid, 0);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EPERM") {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function terminateProcess(pid) {
|
|
261
|
+
if (process.platform === "win32") {
|
|
262
|
+
await killTreeWindows(pid);
|
|
263
|
+
if (!(await waitForExit(pid, 7000))) {
|
|
264
|
+
await killTreeWindows(pid);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
sendSignal(pid, "SIGTERM");
|
|
269
|
+
if (await waitForExit(pid, 7000)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
sendSignal(pid, "SIGKILL");
|
|
273
|
+
await waitForExit(pid, 2000);
|
|
274
|
+
}
|
|
275
|
+
function killTreeWindows(pid) {
|
|
276
|
+
return new Promise((resolve) => {
|
|
277
|
+
const child = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
278
|
+
stdio: "ignore",
|
|
279
|
+
shell: false,
|
|
280
|
+
windowsHide: true,
|
|
281
|
+
});
|
|
282
|
+
child.once("error", () => resolve());
|
|
283
|
+
child.once("exit", () => resolve());
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function sendSignal(pid, signal) {
|
|
287
|
+
try {
|
|
288
|
+
process.kill(-pid, signal);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// continue
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
process.kill(pid, signal);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// ignore
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function waitForExit(pid, timeoutMs, expectExit = true) {
|
|
302
|
+
const deadline = Date.now() + timeoutMs;
|
|
303
|
+
while (Date.now() < deadline) {
|
|
304
|
+
const running = isProcessRunning(pid);
|
|
305
|
+
if (expectExit && !running) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
if (!expectExit && running) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
await sleep(100);
|
|
312
|
+
}
|
|
313
|
+
const stillRunning = isProcessRunning(pid);
|
|
314
|
+
return expectExit ? !stillRunning : stillRunning === false;
|
|
315
|
+
}
|
|
316
|
+
function runSupervisor(actionValue, { allowFailure = false, stdio = "pipe" } = {}) {
|
|
317
|
+
return runChecked(nodeBin, [supervisorScriptPath, String(actionValue ?? "").trim()], {
|
|
318
|
+
allowFailure,
|
|
319
|
+
stdio,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function runChecked(command, args, { stdio = "pipe", allowFailure = false } = {}) {
|
|
323
|
+
const spawnStdio = stdio;
|
|
324
|
+
const result = spawnSync(command, args, {
|
|
325
|
+
cwd: repoRoot,
|
|
326
|
+
shell: false,
|
|
327
|
+
stdio: spawnStdio,
|
|
328
|
+
encoding: "utf8",
|
|
329
|
+
env: process.env,
|
|
330
|
+
});
|
|
331
|
+
const stdout = String(result.stdout ?? "").trim();
|
|
332
|
+
const stderr = String(result.stderr ?? "").trim();
|
|
333
|
+
const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
334
|
+
const spawnErrorCode = String(result.error?.code ?? "")
|
|
335
|
+
.trim()
|
|
336
|
+
.toUpperCase();
|
|
337
|
+
const ok = !result.error && result.status === 0;
|
|
338
|
+
if (!ok && !allowFailure) {
|
|
339
|
+
const errorMessage = result.error && spawnErrorCode
|
|
340
|
+
? `${command} failed (${spawnErrorCode}).`
|
|
341
|
+
: combinedOutput || `${command} exited with code ${String(result.status ?? "unknown")}.`;
|
|
342
|
+
throw new Error(errorMessage);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
ok,
|
|
346
|
+
status: result.status,
|
|
347
|
+
stdout,
|
|
348
|
+
stderr,
|
|
349
|
+
combinedOutput,
|
|
350
|
+
spawnErrorCode,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function computeBackoffDelay(failureCount) {
|
|
354
|
+
const power = Math.max(0, failureCount - 1);
|
|
355
|
+
const calculated = BASE_CHECK_MS * 2 ** power;
|
|
356
|
+
return Math.min(calculated, MAX_BACKOFF_MS);
|
|
357
|
+
}
|
|
358
|
+
async function sleepInterruptible(ms, shouldStop) {
|
|
359
|
+
const deadline = Date.now() + ms;
|
|
360
|
+
while (Date.now() < deadline) {
|
|
361
|
+
if (shouldStop()) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await sleep(Math.min(250, Math.max(1, deadline - Date.now())));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function sleep(ms) {
|
|
368
|
+
return new Promise((resolve) => {
|
|
369
|
+
setTimeout(resolve, ms);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function firstLine(value) {
|
|
373
|
+
const text = String(value ?? "").trim();
|
|
374
|
+
if (!text) {
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
const [line] = text.split(/\r?\n/, 1);
|
|
378
|
+
return String(line ?? "").trim();
|
|
379
|
+
}
|
|
380
|
+
function getErrorMessage(error) {
|
|
381
|
+
if (error instanceof Error && error.message) {
|
|
382
|
+
return error.message;
|
|
383
|
+
}
|
|
384
|
+
return String(error ?? "Unknown error.");
|
|
385
|
+
}
|
|
386
|
+
function printUsage() {
|
|
387
|
+
console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
|
|
388
|
+
}
|