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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-hub",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Copilot Hub CLI and runtime bundle",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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, { reason: "fatal-configuration", exitCode: 1 });
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 evidence = [
441
+ const evidenceChunks = [
399
442
  String(ensureResult?.combinedOutput ?? ""),
400
443
  readLogTail(controlPlaneLogPath, 120),
401
444
  readLogTail(agentEngineLogPath, 120),
402
- ]
403
- .map((chunk) => String(chunk ?? "").trim())
404
- .filter(Boolean)
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 configure', set the token, then run 'copilot-hub start'.",
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
  }
@@ -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, { reason: "fatal-configuration", exitCode: 1 });
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 evidence = [
516
+ const evidenceChunks = [
470
517
  String(ensureResult?.combinedOutput ?? ""),
471
518
  readLogTail(controlPlaneLogPath, 120),
472
519
  readLogTail(agentEngineLogPath, 120),
473
- ]
474
- .map((chunk) => String(chunk ?? "").trim())
475
- .filter(Boolean)
476
- .join("\n")
477
- .toLowerCase();
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 configure', set the token, then run 'copilot-hub start'.",
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
  }