copilot-hub 0.1.12 → 0.1.14
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/package.json +1 -1
- package/scripts/dist/daemon.mjs +114 -11
- package/scripts/src/daemon.mts +125 -13
package/package.json
CHANGED
package/scripts/dist/daemon.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -11,6 +12,7 @@ const runtimeDir = path.join(repoRoot, ".copilot-hub");
|
|
|
11
12
|
const pidsDir = path.join(runtimeDir, "pids");
|
|
12
13
|
const logsDir = path.join(repoRoot, "logs");
|
|
13
14
|
const daemonStatePath = path.join(pidsDir, "daemon.json");
|
|
15
|
+
const lastStartupErrorPath = path.join(runtimeDir, "last-startup-error.json");
|
|
14
16
|
const daemonLogPath = path.join(logsDir, "service-daemon.log");
|
|
15
17
|
const controlPlaneLogPath = path.join(logsDir, "control-plane.log");
|
|
16
18
|
const agentEngineLogPath = path.join(logsDir, "agent-engine.log");
|
|
@@ -106,6 +108,7 @@ async function runDaemonLoop() {
|
|
|
106
108
|
while (!state.stopping) {
|
|
107
109
|
const ensureResult = runSupervisor("ensure", { allowFailure: true });
|
|
108
110
|
if (ensureResult.ok) {
|
|
111
|
+
clearLastStartupError();
|
|
109
112
|
if (failureCount > 0) {
|
|
110
113
|
console.log("[daemon] workers recovered.");
|
|
111
114
|
}
|
|
@@ -115,10 +118,15 @@ async function runDaemonLoop() {
|
|
|
115
118
|
}
|
|
116
119
|
const fatal = detectFatalStartupError(ensureResult);
|
|
117
120
|
if (fatal) {
|
|
121
|
+
writeLastStartupError(fatal);
|
|
118
122
|
console.error(`[daemon] fatal startup error: ${fatal.reason}`);
|
|
119
123
|
console.error(`[daemon] action required: ${fatal.action}`);
|
|
120
124
|
state.stopping = true;
|
|
121
|
-
await shutdownDaemon(state, {
|
|
125
|
+
await shutdownDaemon(state, {
|
|
126
|
+
reason: "fatal-configuration",
|
|
127
|
+
exitCode: 1,
|
|
128
|
+
pauseBeforeExit: true,
|
|
129
|
+
});
|
|
122
130
|
return;
|
|
123
131
|
}
|
|
124
132
|
failureCount += 1;
|
|
@@ -158,6 +166,7 @@ function showDaemonStatus() {
|
|
|
158
166
|
console.log(`running: ${running ? "yes" : "no"}`);
|
|
159
167
|
console.log(`pid: ${running ? String(pid) : "-"}`);
|
|
160
168
|
console.log(`logFile: ${daemonLogPath}`);
|
|
169
|
+
printLastStartupError();
|
|
161
170
|
if (!fs.existsSync(supervisorScriptPath)) {
|
|
162
171
|
console.log("\n(worker status unavailable: supervisor script missing)");
|
|
163
172
|
return;
|
|
@@ -187,7 +196,7 @@ function setupSignalHandlers(state) {
|
|
|
187
196
|
void shutdownDaemon(state, { reason: "unhandled-rejection", exitCode: 1 });
|
|
188
197
|
});
|
|
189
198
|
}
|
|
190
|
-
async function shutdownDaemon(state, { reason, exitCode }) {
|
|
199
|
+
async function shutdownDaemon(state, { reason, exitCode, pauseBeforeExit = false }) {
|
|
191
200
|
if (state.shuttingDown) {
|
|
192
201
|
return;
|
|
193
202
|
}
|
|
@@ -195,6 +204,9 @@ async function shutdownDaemon(state, { reason, exitCode }) {
|
|
|
195
204
|
console.log(`[daemon] stopping (${reason})...`);
|
|
196
205
|
runSupervisor("down", { allowFailure: true });
|
|
197
206
|
removeDaemonState();
|
|
207
|
+
if (pauseBeforeExit) {
|
|
208
|
+
await maybePauseWindowBeforeExit();
|
|
209
|
+
}
|
|
198
210
|
process.exit(exitCode);
|
|
199
211
|
}
|
|
200
212
|
function ensureScripts() {
|
|
@@ -394,21 +406,50 @@ function getErrorMessage(error) {
|
|
|
394
406
|
}
|
|
395
407
|
return String(error ?? "Unknown error.");
|
|
396
408
|
}
|
|
409
|
+
async function maybePauseWindowBeforeExit() {
|
|
410
|
+
if (!shouldPauseBeforeExit()) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const rl = createInterface({
|
|
414
|
+
input: process.stdin,
|
|
415
|
+
output: process.stdout,
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
console.log("");
|
|
419
|
+
await rl.question("[daemon] Press Enter to close this window.");
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Ignore pause errors and exit anyway.
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
rl.close();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function shouldPauseBeforeExit() {
|
|
429
|
+
if (process.platform !== "win32") {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (!process.stdin || !process.stdout) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
397
440
|
function detectFatalStartupError(ensureResult) {
|
|
398
|
-
const
|
|
441
|
+
const evidenceChunks = [
|
|
399
442
|
String(ensureResult?.combinedOutput ?? ""),
|
|
400
443
|
readLogTail(controlPlaneLogPath, 120),
|
|
401
444
|
readLogTail(agentEngineLogPath, 120),
|
|
402
|
-
]
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
.join("\n")
|
|
406
|
-
.toLowerCase();
|
|
407
|
-
const missingHubToken = evidence.includes("hub telegram token is missing") && evidence.includes("hub_telegram_token");
|
|
445
|
+
].map((chunk) => String(chunk ?? "").trim());
|
|
446
|
+
const missingHubTokenLine = findLineContaining(evidenceChunks, (line) => line.includes("hub telegram token is missing") && line.includes("hub_telegram_token"));
|
|
447
|
+
const missingHubToken = Boolean(missingHubTokenLine);
|
|
408
448
|
if (missingHubToken) {
|
|
409
449
|
return {
|
|
410
|
-
reason: "Hub Telegram token is missing (HUB_TELEGRAM_TOKEN).",
|
|
411
|
-
action: "Run 'copilot-hub
|
|
450
|
+
reason: missingHubTokenLine || "Hub Telegram token is missing (HUB_TELEGRAM_TOKEN).",
|
|
451
|
+
action: "Run 'copilot-hub start' in a terminal (it will guide setup), then retry service.",
|
|
452
|
+
detectedAt: new Date().toISOString(),
|
|
412
453
|
};
|
|
413
454
|
}
|
|
414
455
|
return null;
|
|
@@ -426,6 +467,68 @@ function readLogTail(filePath, maxLines = 120) {
|
|
|
426
467
|
return "";
|
|
427
468
|
}
|
|
428
469
|
}
|
|
470
|
+
function findLineContaining(chunks, predicate) {
|
|
471
|
+
const lines = chunks
|
|
472
|
+
.flatMap((chunk) => String(chunk ?? "").split(/\r?\n/))
|
|
473
|
+
.map((line) => String(line ?? "").trim())
|
|
474
|
+
.filter(Boolean);
|
|
475
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
476
|
+
const line = lines[index];
|
|
477
|
+
if (predicate(line.toLowerCase())) {
|
|
478
|
+
return line;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return "";
|
|
482
|
+
}
|
|
483
|
+
function writeLastStartupError(value) {
|
|
484
|
+
try {
|
|
485
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
486
|
+
fs.writeFileSync(lastStartupErrorPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// Best effort only.
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function readLastStartupError() {
|
|
493
|
+
if (!fs.existsSync(lastStartupErrorPath)) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const raw = fs.readFileSync(lastStartupErrorPath, "utf8");
|
|
498
|
+
const parsed = JSON.parse(raw);
|
|
499
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function clearLastStartupError() {
|
|
506
|
+
if (!fs.existsSync(lastStartupErrorPath)) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
fs.rmSync(lastStartupErrorPath, { force: true });
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// Best effort only.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function printLastStartupError() {
|
|
517
|
+
const issue = readLastStartupError();
|
|
518
|
+
if (!issue) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
console.log("\n=== last startup error ===");
|
|
522
|
+
if (issue.detectedAt) {
|
|
523
|
+
console.log(`detectedAt: ${String(issue.detectedAt)}`);
|
|
524
|
+
}
|
|
525
|
+
if (issue.reason) {
|
|
526
|
+
console.log(`reason: ${String(issue.reason)}`);
|
|
527
|
+
}
|
|
528
|
+
if (issue.action) {
|
|
529
|
+
console.log(`action: ${String(issue.action)}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
429
532
|
function printUsage() {
|
|
430
533
|
console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
|
|
431
534
|
}
|
package/scripts/src/daemon.mts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -14,6 +15,7 @@ const pidsDir = path.join(runtimeDir, "pids");
|
|
|
14
15
|
const logsDir = path.join(repoRoot, "logs");
|
|
15
16
|
|
|
16
17
|
const daemonStatePath = path.join(pidsDir, "daemon.json");
|
|
18
|
+
const lastStartupErrorPath = path.join(runtimeDir, "last-startup-error.json");
|
|
17
19
|
const daemonLogPath = path.join(logsDir, "service-daemon.log");
|
|
18
20
|
const controlPlaneLogPath = path.join(logsDir, "control-plane.log");
|
|
19
21
|
const agentEngineLogPath = path.join(logsDir, "agent-engine.log");
|
|
@@ -125,6 +127,7 @@ async function runDaemonLoop() {
|
|
|
125
127
|
while (!state.stopping) {
|
|
126
128
|
const ensureResult = runSupervisor("ensure", { allowFailure: true });
|
|
127
129
|
if (ensureResult.ok) {
|
|
130
|
+
clearLastStartupError();
|
|
128
131
|
if (failureCount > 0) {
|
|
129
132
|
console.log("[daemon] workers recovered.");
|
|
130
133
|
}
|
|
@@ -135,10 +138,15 @@ async function runDaemonLoop() {
|
|
|
135
138
|
|
|
136
139
|
const fatal = detectFatalStartupError(ensureResult);
|
|
137
140
|
if (fatal) {
|
|
141
|
+
writeLastStartupError(fatal);
|
|
138
142
|
console.error(`[daemon] fatal startup error: ${fatal.reason}`);
|
|
139
143
|
console.error(`[daemon] action required: ${fatal.action}`);
|
|
140
144
|
state.stopping = true;
|
|
141
|
-
await shutdownDaemon(state, {
|
|
145
|
+
await shutdownDaemon(state, {
|
|
146
|
+
reason: "fatal-configuration",
|
|
147
|
+
exitCode: 1,
|
|
148
|
+
pauseBeforeExit: true,
|
|
149
|
+
});
|
|
142
150
|
return;
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -191,6 +199,7 @@ function showDaemonStatus() {
|
|
|
191
199
|
console.log(`running: ${running ? "yes" : "no"}`);
|
|
192
200
|
console.log(`pid: ${running ? String(pid) : "-"}`);
|
|
193
201
|
console.log(`logFile: ${daemonLogPath}`);
|
|
202
|
+
printLastStartupError();
|
|
194
203
|
|
|
195
204
|
if (!fs.existsSync(supervisorScriptPath)) {
|
|
196
205
|
console.log("\n(worker status unavailable: supervisor script missing)");
|
|
@@ -227,7 +236,7 @@ function setupSignalHandlers(state) {
|
|
|
227
236
|
});
|
|
228
237
|
}
|
|
229
238
|
|
|
230
|
-
async function shutdownDaemon(state, { reason, exitCode }) {
|
|
239
|
+
async function shutdownDaemon(state, { reason, exitCode, pauseBeforeExit = false }) {
|
|
231
240
|
if (state.shuttingDown) {
|
|
232
241
|
return;
|
|
233
242
|
}
|
|
@@ -236,6 +245,9 @@ async function shutdownDaemon(state, { reason, exitCode }) {
|
|
|
236
245
|
console.log(`[daemon] stopping (${reason})...`);
|
|
237
246
|
runSupervisor("down", { allowFailure: true });
|
|
238
247
|
removeDaemonState();
|
|
248
|
+
if (pauseBeforeExit) {
|
|
249
|
+
await maybePauseWindowBeforeExit();
|
|
250
|
+
}
|
|
239
251
|
|
|
240
252
|
process.exit(exitCode);
|
|
241
253
|
}
|
|
@@ -465,23 +477,58 @@ function getErrorMessage(error) {
|
|
|
465
477
|
return String(error ?? "Unknown error.");
|
|
466
478
|
}
|
|
467
479
|
|
|
480
|
+
async function maybePauseWindowBeforeExit() {
|
|
481
|
+
if (!shouldPauseBeforeExit()) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const rl = createInterface({
|
|
486
|
+
input: process.stdin,
|
|
487
|
+
output: process.stdout,
|
|
488
|
+
});
|
|
489
|
+
try {
|
|
490
|
+
console.log("");
|
|
491
|
+
await rl.question("[daemon] Press Enter to close this window.");
|
|
492
|
+
} catch {
|
|
493
|
+
// Ignore pause errors and exit anyway.
|
|
494
|
+
} finally {
|
|
495
|
+
rl.close();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function shouldPauseBeforeExit() {
|
|
500
|
+
if (process.platform !== "win32") {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!process.stdin || !process.stdout) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
468
515
|
function detectFatalStartupError(ensureResult) {
|
|
469
|
-
const
|
|
516
|
+
const evidenceChunks = [
|
|
470
517
|
String(ensureResult?.combinedOutput ?? ""),
|
|
471
518
|
readLogTail(controlPlaneLogPath, 120),
|
|
472
519
|
readLogTail(agentEngineLogPath, 120),
|
|
473
|
-
]
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
.
|
|
478
|
-
|
|
479
|
-
const missingHubToken =
|
|
480
|
-
evidence.includes("hub telegram token is missing") && evidence.includes("hub_telegram_token");
|
|
520
|
+
].map((chunk) => String(chunk ?? "").trim());
|
|
521
|
+
|
|
522
|
+
const missingHubTokenLine = findLineContaining(
|
|
523
|
+
evidenceChunks,
|
|
524
|
+
(line) => line.includes("hub telegram token is missing") && line.includes("hub_telegram_token"),
|
|
525
|
+
);
|
|
526
|
+
const missingHubToken = Boolean(missingHubTokenLine);
|
|
481
527
|
if (missingHubToken) {
|
|
482
528
|
return {
|
|
483
|
-
reason: "Hub Telegram token is missing (HUB_TELEGRAM_TOKEN).",
|
|
484
|
-
action: "Run 'copilot-hub
|
|
529
|
+
reason: missingHubTokenLine || "Hub Telegram token is missing (HUB_TELEGRAM_TOKEN).",
|
|
530
|
+
action: "Run 'copilot-hub start' in a terminal (it will guide setup), then retry service.",
|
|
531
|
+
detectedAt: new Date().toISOString(),
|
|
485
532
|
};
|
|
486
533
|
}
|
|
487
534
|
|
|
@@ -502,6 +549,71 @@ function readLogTail(filePath, maxLines = 120) {
|
|
|
502
549
|
}
|
|
503
550
|
}
|
|
504
551
|
|
|
552
|
+
function findLineContaining(chunks, predicate) {
|
|
553
|
+
const lines = chunks
|
|
554
|
+
.flatMap((chunk) => String(chunk ?? "").split(/\r?\n/))
|
|
555
|
+
.map((line) => String(line ?? "").trim())
|
|
556
|
+
.filter(Boolean);
|
|
557
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
558
|
+
const line = lines[index];
|
|
559
|
+
if (predicate(line.toLowerCase())) {
|
|
560
|
+
return line;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return "";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function writeLastStartupError(value) {
|
|
567
|
+
try {
|
|
568
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
569
|
+
fs.writeFileSync(lastStartupErrorPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
570
|
+
} catch {
|
|
571
|
+
// Best effort only.
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function readLastStartupError() {
|
|
576
|
+
if (!fs.existsSync(lastStartupErrorPath)) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
const raw = fs.readFileSync(lastStartupErrorPath, "utf8");
|
|
581
|
+
const parsed = JSON.parse(raw);
|
|
582
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function clearLastStartupError() {
|
|
589
|
+
if (!fs.existsSync(lastStartupErrorPath)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
fs.rmSync(lastStartupErrorPath, { force: true });
|
|
594
|
+
} catch {
|
|
595
|
+
// Best effort only.
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function printLastStartupError() {
|
|
600
|
+
const issue = readLastStartupError();
|
|
601
|
+
if (!issue) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
console.log("\n=== last startup error ===");
|
|
606
|
+
if (issue.detectedAt) {
|
|
607
|
+
console.log(`detectedAt: ${String(issue.detectedAt)}`);
|
|
608
|
+
}
|
|
609
|
+
if (issue.reason) {
|
|
610
|
+
console.log(`reason: ${String(issue.reason)}`);
|
|
611
|
+
}
|
|
612
|
+
if (issue.action) {
|
|
613
|
+
console.log(`action: ${String(issue.action)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
505
617
|
function printUsage() {
|
|
506
618
|
console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
|
|
507
619
|
}
|