copilot-hub 0.1.9 → 0.1.11
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 +18 -2
- package/apps/agent-engine/dist/index.js +359 -0
- package/apps/control-plane/README.md +4 -1
- package/apps/control-plane/dist/channels/hub-ops-commands.js +160 -2
- package/package.json +2 -1
- package/scripts/dist/cli.mjs +61 -0
- package/scripts/dist/daemon.mjs +1 -0
- package/scripts/dist/service.mjs +1 -0
- package/scripts/src/cli.mts +72 -0
- package/scripts/src/daemon.mts +1 -0
- package/scripts/src/service.mts +1 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Shared packages:
|
|
|
22
22
|
|
|
23
23
|
The same Telegram chat handles both operations and development:
|
|
24
24
|
|
|
25
|
-
- commands: `/help`, `/health`, `/bots`, `/create_agent`, `/cancel`
|
|
25
|
+
- commands: `/help`, `/health`, `/bots`, `/create_agent`, `/codex_status`, `/codex_login`, `/cancel`
|
|
26
26
|
- normal message: handled by the LLM assistant
|
|
27
27
|
|
|
28
28
|
## Workspace isolation
|
|
@@ -129,6 +129,8 @@ Hub commands:
|
|
|
129
129
|
- `/health`
|
|
130
130
|
- `/bots`
|
|
131
131
|
- `/create_agent`
|
|
132
|
+
- `/codex_status`
|
|
133
|
+
- `/codex_login`
|
|
132
134
|
- `/cancel`
|
|
133
135
|
|
|
134
136
|
### 3) Create runtime agent bot(s)
|
|
@@ -147,7 +149,14 @@ You need one Telegram bot token per runtime agent.
|
|
|
147
149
|
You can use `/bots` in the hub chat to manage policy, reset context, or delete an agent.
|
|
148
150
|
Default values are already applied, and actions start from that agent workspace folder.
|
|
149
151
|
|
|
150
|
-
### 4)
|
|
152
|
+
### 4) Switch Codex account from Telegram (optional)
|
|
153
|
+
|
|
154
|
+
- Run `/codex_status` to verify current Codex login state.
|
|
155
|
+
- Run `/codex_login` (or `/codex_switch`) to get a device code link, then complete sign-in from your phone.
|
|
156
|
+
- Optional: run `/codex_switch_key` if you want API-key based switch (`sk-...`).
|
|
157
|
+
- Running agents are restarted automatically after successful switch.
|
|
158
|
+
|
|
159
|
+
### 5) Token safety
|
|
151
160
|
|
|
152
161
|
- Never commit real bot tokens.
|
|
153
162
|
- If a token is leaked, regenerate it in `@BotFather` using `/revoke`.
|
|
@@ -170,12 +179,19 @@ npm run restart
|
|
|
170
179
|
npm run status
|
|
171
180
|
npm run logs
|
|
172
181
|
npm run configure
|
|
182
|
+
npm run update
|
|
173
183
|
npm run test
|
|
174
184
|
npm run lint
|
|
175
185
|
npm run format:check
|
|
176
186
|
npm run check:apps
|
|
177
187
|
```
|
|
178
188
|
|
|
189
|
+
Global update command:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
copilot-hub update
|
|
193
|
+
```
|
|
194
|
+
|
|
179
195
|
Service mode (optional, OS-native):
|
|
180
196
|
|
|
181
197
|
```bash
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import express from "express";
|
|
5
7
|
import { BotManager } from "@copilot-hub/core/bot-manager";
|
|
@@ -17,7 +19,9 @@ let botManager = null;
|
|
|
17
19
|
let instanceLock = null;
|
|
18
20
|
let controlPlane = null;
|
|
19
21
|
let secretStore = null;
|
|
22
|
+
let codexDeviceAuthSession = null;
|
|
20
23
|
const workerScriptPath = fileURLToPath(new URL("./agent-worker.js", import.meta.url));
|
|
24
|
+
const ANSI_ESCAPE_PATTERN = new RegExp(String.raw `\u001b\[[0-9;]*m`, "g");
|
|
21
25
|
await bootstrap();
|
|
22
26
|
async function bootstrap() {
|
|
23
27
|
try {
|
|
@@ -129,6 +133,75 @@ function buildApiApp({ botManager, controlPlane, registryFilePath }) {
|
|
|
129
133
|
secretStoreFile: config.secretStoreFilePath,
|
|
130
134
|
});
|
|
131
135
|
});
|
|
136
|
+
app.get("/api/system/codex/status", wrapAsync(async (req, res) => {
|
|
137
|
+
const status = readCodexLoginStatus();
|
|
138
|
+
const deviceAuth = getCodexDeviceAuthSnapshot();
|
|
139
|
+
res.json({
|
|
140
|
+
ok: true,
|
|
141
|
+
configured: status.configured,
|
|
142
|
+
codexBin: status.codexBin,
|
|
143
|
+
detail: status.detail,
|
|
144
|
+
deviceAuth,
|
|
145
|
+
});
|
|
146
|
+
}));
|
|
147
|
+
app.post("/api/system/codex/device_auth/start", wrapAsync(async (req, res) => {
|
|
148
|
+
const session = startCodexDeviceAuthSession();
|
|
149
|
+
const ready = await waitForDeviceCode(session, 12_000);
|
|
150
|
+
if (!ready) {
|
|
151
|
+
res.status(400).json({
|
|
152
|
+
error: session.error ||
|
|
153
|
+
"Could not initialize Codex device login flow. Retry '/codex_login' in a few seconds.",
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
res.json({
|
|
158
|
+
ok: true,
|
|
159
|
+
status: session.status,
|
|
160
|
+
loginUrl: session.loginUrl,
|
|
161
|
+
code: session.code,
|
|
162
|
+
});
|
|
163
|
+
}));
|
|
164
|
+
app.post("/api/system/codex/device_auth/cancel", wrapAsync(async (req, res) => {
|
|
165
|
+
const canceled = cancelCodexDeviceAuthSession();
|
|
166
|
+
res.json({
|
|
167
|
+
ok: true,
|
|
168
|
+
canceled,
|
|
169
|
+
});
|
|
170
|
+
}));
|
|
171
|
+
app.post("/api/system/codex/switch_api_key", wrapAsync(async (req, res) => {
|
|
172
|
+
cancelCodexDeviceAuthSession();
|
|
173
|
+
const apiKey = String(req.body?.apiKey ?? "").trim();
|
|
174
|
+
if (!looksLikeCodexApiKey(apiKey)) {
|
|
175
|
+
res.status(400).json({ error: "Field 'apiKey' is invalid." });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const login = runCodexCommand(["login", "--with-api-key"], {
|
|
179
|
+
inputText: `${apiKey}\n`,
|
|
180
|
+
});
|
|
181
|
+
if (!login.ok) {
|
|
182
|
+
const message = redactSecret(firstNonEmptyLine(login.errorMessage, login.stderr, login.stdout) ||
|
|
183
|
+
"Codex login failed. Check API key and retry.", apiKey);
|
|
184
|
+
res.status(400).json({ error: message });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const status = readCodexLoginStatus();
|
|
188
|
+
if (!status.configured) {
|
|
189
|
+
res.status(400).json({
|
|
190
|
+
error: status.detail || "Codex login verification failed after credential update. Retry once.",
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const restarted = await restartRunningBots();
|
|
195
|
+
res.json({
|
|
196
|
+
ok: true,
|
|
197
|
+
switched: true,
|
|
198
|
+
configured: true,
|
|
199
|
+
codexBin: status.codexBin,
|
|
200
|
+
detail: status.detail,
|
|
201
|
+
restartedBots: restarted.restartedBotIds,
|
|
202
|
+
restartFailures: restarted.failures,
|
|
203
|
+
});
|
|
204
|
+
}));
|
|
132
205
|
app.get("/api/extensions/contract", wrapAsync(async (req, res) => {
|
|
133
206
|
const contract = await controlPlane.runSystemAction(CONTROL_ACTIONS.EXTENSIONS_CONTRACT_GET, {});
|
|
134
207
|
res.json(contract);
|
|
@@ -350,3 +423,289 @@ function parseDeleteModeFromRequest(body) {
|
|
|
350
423
|
}
|
|
351
424
|
return "soft";
|
|
352
425
|
}
|
|
426
|
+
function startCodexDeviceAuthSession() {
|
|
427
|
+
if (codexDeviceAuthSession && isDeviceAuthActive(codexDeviceAuthSession.status)) {
|
|
428
|
+
return codexDeviceAuthSession;
|
|
429
|
+
}
|
|
430
|
+
const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
|
|
431
|
+
const session = {
|
|
432
|
+
id: createSessionId(),
|
|
433
|
+
status: "starting",
|
|
434
|
+
startedAt: new Date().toISOString(),
|
|
435
|
+
codexBin,
|
|
436
|
+
loginUrl: "",
|
|
437
|
+
code: "",
|
|
438
|
+
logLines: [],
|
|
439
|
+
error: "",
|
|
440
|
+
child: null,
|
|
441
|
+
restartedBots: [],
|
|
442
|
+
restartFailures: [],
|
|
443
|
+
};
|
|
444
|
+
const child = spawn(codexBin, ["login", "--device-auth"], {
|
|
445
|
+
cwd: config.kernelRootPath,
|
|
446
|
+
shell: false,
|
|
447
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
448
|
+
windowsHide: true,
|
|
449
|
+
env: process.env,
|
|
450
|
+
});
|
|
451
|
+
session.child = child;
|
|
452
|
+
codexDeviceAuthSession = session;
|
|
453
|
+
child.stdout.on("data", (chunk) => {
|
|
454
|
+
appendDeviceAuthOutput(session, chunk);
|
|
455
|
+
});
|
|
456
|
+
child.stderr.on("data", (chunk) => {
|
|
457
|
+
appendDeviceAuthOutput(session, chunk);
|
|
458
|
+
});
|
|
459
|
+
child.once("error", (error) => {
|
|
460
|
+
session.child = null;
|
|
461
|
+
session.status = "failed";
|
|
462
|
+
session.error = sanitizeError(error);
|
|
463
|
+
});
|
|
464
|
+
child.once("exit", (code) => {
|
|
465
|
+
session.child = null;
|
|
466
|
+
if (session.status === "canceled") {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (code === 0) {
|
|
470
|
+
session.status = "succeeded";
|
|
471
|
+
void restartRunningBots()
|
|
472
|
+
.then((restarted) => {
|
|
473
|
+
session.restartedBots = restarted.restartedBotIds;
|
|
474
|
+
session.restartFailures = restarted.failures;
|
|
475
|
+
})
|
|
476
|
+
.catch((error) => {
|
|
477
|
+
session.restartFailures = [{ botId: "*", error: sanitizeError(error) }];
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
session.status = "failed";
|
|
482
|
+
session.error =
|
|
483
|
+
firstNonEmptyLine(session.error, ...session.logLines.slice(-12)) ||
|
|
484
|
+
`Codex login exited with code ${String(code ?? "unknown")}.`;
|
|
485
|
+
});
|
|
486
|
+
return session;
|
|
487
|
+
}
|
|
488
|
+
function cancelCodexDeviceAuthSession() {
|
|
489
|
+
if (!codexDeviceAuthSession || !isDeviceAuthActive(codexDeviceAuthSession.status)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
codexDeviceAuthSession.status = "canceled";
|
|
493
|
+
codexDeviceAuthSession.error = "Canceled by user.";
|
|
494
|
+
try {
|
|
495
|
+
codexDeviceAuthSession.child?.kill("SIGTERM");
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// ignore
|
|
499
|
+
}
|
|
500
|
+
codexDeviceAuthSession.child = null;
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
function getCodexDeviceAuthSnapshot() {
|
|
504
|
+
const session = codexDeviceAuthSession;
|
|
505
|
+
if (!session) {
|
|
506
|
+
return { status: "idle" };
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
status: session.status,
|
|
510
|
+
startedAt: session.startedAt,
|
|
511
|
+
loginUrl: session.loginUrl || undefined,
|
|
512
|
+
code: session.code || undefined,
|
|
513
|
+
detail: session.error || undefined,
|
|
514
|
+
restartedBots: session.restartedBots,
|
|
515
|
+
restartFailures: session.restartFailures,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async function waitForDeviceCode(session, timeoutMs) {
|
|
519
|
+
const timeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 10_000;
|
|
520
|
+
const started = Date.now();
|
|
521
|
+
while (Date.now() - started < timeout) {
|
|
522
|
+
if (session.status === "failed" || session.status === "canceled") {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
if (session.loginUrl && session.code) {
|
|
526
|
+
if (session.status === "starting") {
|
|
527
|
+
session.status = "pending";
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
await sleep(150);
|
|
532
|
+
}
|
|
533
|
+
return Boolean(session.loginUrl && session.code);
|
|
534
|
+
}
|
|
535
|
+
function appendDeviceAuthOutput(session, chunk) {
|
|
536
|
+
const text = stripAnsi(String(chunk ?? ""));
|
|
537
|
+
if (!text.trim()) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const lines = text
|
|
541
|
+
.split(/\r?\n/)
|
|
542
|
+
.map((line) => line.trim())
|
|
543
|
+
.filter(Boolean);
|
|
544
|
+
if (lines.length === 0) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
session.logLines.push(...lines);
|
|
548
|
+
if (session.logLines.length > 120) {
|
|
549
|
+
session.logLines = session.logLines.slice(-120);
|
|
550
|
+
}
|
|
551
|
+
if (!session.loginUrl) {
|
|
552
|
+
const url = findDeviceLoginUrl(lines);
|
|
553
|
+
if (url) {
|
|
554
|
+
session.loginUrl = url;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!session.code) {
|
|
558
|
+
const code = findDeviceCode(lines);
|
|
559
|
+
if (code) {
|
|
560
|
+
session.code = code;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (session.loginUrl && session.code && session.status === "starting") {
|
|
564
|
+
session.status = "pending";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function findDeviceLoginUrl(lines) {
|
|
568
|
+
for (const line of lines) {
|
|
569
|
+
const urls = line.match(/https?:\/\/\S+/g);
|
|
570
|
+
if (!urls) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
for (const url of urls) {
|
|
574
|
+
if (url.includes("/codex/device")) {
|
|
575
|
+
return url;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
581
|
+
function findDeviceCode(lines) {
|
|
582
|
+
for (const line of lines) {
|
|
583
|
+
const match = line.match(/\b[A-Z0-9]{4}-[A-Z0-9]{5}\b/);
|
|
584
|
+
if (match?.[0]) {
|
|
585
|
+
return match[0];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
function stripAnsi(value) {
|
|
591
|
+
return String(value ?? "").replace(ANSI_ESCAPE_PATTERN, "");
|
|
592
|
+
}
|
|
593
|
+
function isDeviceAuthActive(status) {
|
|
594
|
+
const value = String(status ?? "")
|
|
595
|
+
.trim()
|
|
596
|
+
.toLowerCase();
|
|
597
|
+
return value === "starting" || value === "pending";
|
|
598
|
+
}
|
|
599
|
+
function createSessionId() {
|
|
600
|
+
return randomBytes(8).toString("hex");
|
|
601
|
+
}
|
|
602
|
+
function sleep(ms) {
|
|
603
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
604
|
+
}
|
|
605
|
+
function readCodexLoginStatus() {
|
|
606
|
+
const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
|
|
607
|
+
const status = runCodexCommand(["login", "status"]);
|
|
608
|
+
return {
|
|
609
|
+
configured: status.ok,
|
|
610
|
+
codexBin,
|
|
611
|
+
detail: firstNonEmptyLine(status.errorMessage, status.stderr, status.stdout),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function looksLikeCodexApiKey(value) {
|
|
615
|
+
const key = String(value ?? "").trim();
|
|
616
|
+
if (key.length < 20 || key.length > 4096) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
if (!/^[A-Za-z0-9._-]+$/.test(key)) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
return key.startsWith("sk-");
|
|
623
|
+
}
|
|
624
|
+
function runCodexCommand(args, { inputText = "" } = {}) {
|
|
625
|
+
const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
|
|
626
|
+
const result = spawnSync(codexBin, args, {
|
|
627
|
+
cwd: config.kernelRootPath,
|
|
628
|
+
shell: false,
|
|
629
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
630
|
+
windowsHide: true,
|
|
631
|
+
encoding: "utf8",
|
|
632
|
+
input: inputText,
|
|
633
|
+
env: process.env,
|
|
634
|
+
});
|
|
635
|
+
if (result.error) {
|
|
636
|
+
return {
|
|
637
|
+
ok: false,
|
|
638
|
+
status: 1,
|
|
639
|
+
stdout: "",
|
|
640
|
+
stderr: "",
|
|
641
|
+
errorMessage: formatCodexSpawnError(codexBin, result.error),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const status = Number.isInteger(result.status) ? result.status : 1;
|
|
645
|
+
return {
|
|
646
|
+
ok: status === 0,
|
|
647
|
+
status,
|
|
648
|
+
stdout: String(result.stdout ?? "").trim(),
|
|
649
|
+
stderr: String(result.stderr ?? "").trim(),
|
|
650
|
+
errorMessage: "",
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function formatCodexSpawnError(codexBin, error) {
|
|
654
|
+
const code = String(error?.code ?? "")
|
|
655
|
+
.trim()
|
|
656
|
+
.toUpperCase();
|
|
657
|
+
if (code === "ENOENT") {
|
|
658
|
+
return `Codex binary '${codexBin}' was not found.`;
|
|
659
|
+
}
|
|
660
|
+
if (code === "EPERM") {
|
|
661
|
+
return `Codex binary '${codexBin}' cannot be executed (EPERM).`;
|
|
662
|
+
}
|
|
663
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
664
|
+
return `Failed to execute '${codexBin}': ${firstNonEmptyLine(message)}`;
|
|
665
|
+
}
|
|
666
|
+
function firstNonEmptyLine(...values) {
|
|
667
|
+
for (const value of values) {
|
|
668
|
+
const line = String(value ?? "")
|
|
669
|
+
.split(/\r?\n/)
|
|
670
|
+
.map((entry) => entry.trim())
|
|
671
|
+
.find(Boolean);
|
|
672
|
+
if (line) {
|
|
673
|
+
return line;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return "";
|
|
677
|
+
}
|
|
678
|
+
function redactSecret(text, secret) {
|
|
679
|
+
const input = String(text ?? "");
|
|
680
|
+
const token = String(secret ?? "").trim();
|
|
681
|
+
if (!token) {
|
|
682
|
+
return input;
|
|
683
|
+
}
|
|
684
|
+
return input.split(token).join("[redacted]");
|
|
685
|
+
}
|
|
686
|
+
async function restartRunningBots() {
|
|
687
|
+
const statuses = await botManager.listBotsLive();
|
|
688
|
+
const runningBotIds = statuses
|
|
689
|
+
.filter((entry) => entry?.running === true)
|
|
690
|
+
.map((entry) => String(entry?.id ?? "").trim())
|
|
691
|
+
.filter(Boolean);
|
|
692
|
+
const restartedBotIds = [];
|
|
693
|
+
const failures = [];
|
|
694
|
+
for (const botId of runningBotIds) {
|
|
695
|
+
try {
|
|
696
|
+
await botManager.stopBot(botId);
|
|
697
|
+
await botManager.startBot(botId);
|
|
698
|
+
restartedBotIds.push(botId);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
failures.push({
|
|
702
|
+
botId,
|
|
703
|
+
error: sanitizeError(error),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
restartedBotIds,
|
|
709
|
+
failures,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
`control-plane` is the single Telegram hub for Copilot Hub.
|
|
4
4
|
|
|
5
5
|
It handles:
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
- simple operations commands (`/help`, `/health`, `/bots`, `/create_agent`, `/codex_status`, `/codex_login`, `/cancel`)
|
|
7
8
|
- LLM development requests through normal chat messages
|
|
8
9
|
|
|
9
10
|
## Setup
|
|
@@ -18,9 +19,11 @@ npm run start
|
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
Required env:
|
|
22
|
+
|
|
21
23
|
- `HUB_TELEGRAM_TOKEN` (or custom `HUB_TELEGRAM_TOKEN_ENV`)
|
|
22
24
|
|
|
23
25
|
Recommended env:
|
|
26
|
+
|
|
24
27
|
- `HUB_ENGINE_BASE_URL` (default: `http://127.0.0.1:8787`)
|
|
25
28
|
|
|
26
29
|
## Workspace and policy guards
|
|
@@ -7,6 +7,7 @@ const BOT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
7
7
|
const TELEGRAM_TOKEN_PATTERN = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
|
|
8
8
|
const menuSessions = new Map();
|
|
9
9
|
const createFlows = new Map();
|
|
10
|
+
const codexSwitchFlows = new Map();
|
|
10
11
|
const POLICY_PROFILES = {
|
|
11
12
|
safe: {
|
|
12
13
|
id: "safe",
|
|
@@ -75,6 +76,69 @@ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId }) {
|
|
|
75
76
|
}
|
|
76
77
|
return true;
|
|
77
78
|
}
|
|
79
|
+
if (command === "/codex_status") {
|
|
80
|
+
try {
|
|
81
|
+
const status = await apiGet("/api/system/codex/status");
|
|
82
|
+
const deviceAuth = status?.deviceAuth ?? {};
|
|
83
|
+
const deviceStatus = String(deviceAuth?.status ?? "idle");
|
|
84
|
+
await ctx.reply([
|
|
85
|
+
"Codex account:",
|
|
86
|
+
`configured: ${status?.configured ? "yes" : "no"}`,
|
|
87
|
+
`binary: ${String(status?.codexBin ?? "-")}`,
|
|
88
|
+
status?.detail ? `detail: ${String(status.detail)}` : "",
|
|
89
|
+
`deviceAuth: ${deviceStatus}`,
|
|
90
|
+
deviceAuth?.code ? `code: ${String(deviceAuth.code)}` : "",
|
|
91
|
+
deviceAuth?.loginUrl ? `link: ${String(deviceAuth.loginUrl)}` : "",
|
|
92
|
+
deviceAuth?.detail ? `deviceDetail: ${String(deviceAuth.detail)}` : "",
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
await ctx.reply(`Codex status failed: ${sanitizeError(error)}`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (command === "/codex_login" || command === "/codex_switch") {
|
|
103
|
+
try {
|
|
104
|
+
const response = await apiPost("/api/system/codex/device_auth/start", {});
|
|
105
|
+
const statusLabel = String(response?.status ?? "pending");
|
|
106
|
+
const loginUrl = String(response?.loginUrl ?? "").trim();
|
|
107
|
+
const code = String(response?.code ?? "").trim();
|
|
108
|
+
if (!loginUrl || !code) {
|
|
109
|
+
await ctx.reply("Codex login flow started, but code details were not ready yet. Retry /codex_login.");
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
await ctx.reply([
|
|
113
|
+
`Codex login flow: ${statusLabel}`,
|
|
114
|
+
`1) Open: ${loginUrl}`,
|
|
115
|
+
`2) Enter code: ${code}`,
|
|
116
|
+
"3) Finish sign-in on your phone, then run /codex_status",
|
|
117
|
+
"Use /cancel to abort this login flow.",
|
|
118
|
+
].join("\n"));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
await ctx.reply(`Codex login start failed: ${sanitizeError(error)}`);
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (command === "/codex_switch_key") {
|
|
126
|
+
if (createFlows.has(flowKey) || codexSwitchFlows.has(flowKey)) {
|
|
127
|
+
await ctx.reply("Another operation is active. Send /cancel first.");
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
codexSwitchFlows.set(flowKey, {
|
|
131
|
+
createdAt: Date.now(),
|
|
132
|
+
step: "api_key",
|
|
133
|
+
});
|
|
134
|
+
await ctx.reply([
|
|
135
|
+
"Codex account switch started.",
|
|
136
|
+
"Send your Codex API key now (example: sk-...).",
|
|
137
|
+
"Running agents will restart automatically after successful switch.",
|
|
138
|
+
"Use /cancel to stop.",
|
|
139
|
+
].join("\n"));
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
78
142
|
if (command === "/create_agent") {
|
|
79
143
|
const existing = createFlows.get(flowKey);
|
|
80
144
|
if (existing) {
|
|
@@ -97,8 +161,19 @@ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId }) {
|
|
|
97
161
|
return true;
|
|
98
162
|
}
|
|
99
163
|
if (command === "/cancel") {
|
|
100
|
-
const
|
|
101
|
-
|
|
164
|
+
const createDeleted = createFlows.delete(flowKey);
|
|
165
|
+
const switchDeleted = codexSwitchFlows.delete(flowKey);
|
|
166
|
+
let remoteCanceled = false;
|
|
167
|
+
try {
|
|
168
|
+
const canceled = await apiPost("/api/system/codex/device_auth/cancel", {});
|
|
169
|
+
remoteCanceled = canceled?.canceled === true;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
remoteCanceled = false;
|
|
173
|
+
}
|
|
174
|
+
await ctx.reply(createDeleted || switchDeleted || remoteCanceled
|
|
175
|
+
? "Current operation canceled."
|
|
176
|
+
: "No active operation.");
|
|
102
177
|
return true;
|
|
103
178
|
}
|
|
104
179
|
return false;
|
|
@@ -111,6 +186,13 @@ export async function maybeHandleHubOpsFollowUp({ ctx, runtime, channelId }) {
|
|
|
111
186
|
}
|
|
112
187
|
const chatId = getChatId(ctx);
|
|
113
188
|
const flowKey = buildFlowKey(runtime?.runtimeId, channelId, chatId);
|
|
189
|
+
const codexFlow = codexSwitchFlows.get(flowKey);
|
|
190
|
+
if (codexFlow) {
|
|
191
|
+
const handled = await handleCodexSwitchFlow({ ctx, flowKey, flow: codexFlow, text });
|
|
192
|
+
if (handled) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
114
196
|
const flow = createFlows.get(flowKey);
|
|
115
197
|
if (!flow) {
|
|
116
198
|
return false;
|
|
@@ -196,6 +278,46 @@ export async function maybeHandleHubOpsFollowUp({ ctx, runtime, channelId }) {
|
|
|
196
278
|
await ctx.reply("Flow reset. Use /create_agent to start again.");
|
|
197
279
|
return true;
|
|
198
280
|
}
|
|
281
|
+
async function handleCodexSwitchFlow({ ctx, flowKey, flow, text }) {
|
|
282
|
+
if (flow.step !== "api_key") {
|
|
283
|
+
codexSwitchFlows.delete(flowKey);
|
|
284
|
+
await ctx.reply("Flow reset. Use /codex_switch_key to start again.");
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
const apiKey = String(text ?? "").trim();
|
|
288
|
+
if (!looksLikeCodexApiKey(apiKey)) {
|
|
289
|
+
await ctx.reply("Invalid API key format. Send a key starting with 'sk-' or /cancel.");
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
await maybeDeleteIncomingMessage(ctx);
|
|
293
|
+
try {
|
|
294
|
+
const result = await apiPost("/api/system/codex/switch_api_key", {
|
|
295
|
+
apiKey,
|
|
296
|
+
});
|
|
297
|
+
codexSwitchFlows.delete(flowKey);
|
|
298
|
+
const restartedBots = Array.isArray(result?.restartedBots) ? result.restartedBots : [];
|
|
299
|
+
const restartFailures = Array.isArray(result?.restartFailures) ? result.restartFailures : [];
|
|
300
|
+
const lines = ["Codex account switched successfully."];
|
|
301
|
+
if (result?.detail) {
|
|
302
|
+
lines.push(`status: ${String(result.detail)}`);
|
|
303
|
+
}
|
|
304
|
+
if (restartedBots.length > 0) {
|
|
305
|
+
lines.push(`Agents restarted: ${restartedBots.join(", ")}`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
lines.push("No running agents needed restart.");
|
|
309
|
+
}
|
|
310
|
+
if (restartFailures.length > 0) {
|
|
311
|
+
lines.push(`Restart warnings: ${restartFailures.length}. Use /health then /bots to verify state.`);
|
|
312
|
+
}
|
|
313
|
+
await ctx.reply(lines.join("\n"));
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
await ctx.reply(`Codex switch failed:\n${sanitizeError(error)}\nRetry or /cancel.`);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
199
321
|
export async function maybeHandleHubOpsCallback({ ctx }) {
|
|
200
322
|
const rawData = String(ctx.callbackQuery?.data ?? "").trim();
|
|
201
323
|
if (!rawData.startsWith("hub:")) {
|
|
@@ -320,8 +442,15 @@ function buildHelpText(runtimeName) {
|
|
|
320
442
|
"/health",
|
|
321
443
|
"/bots",
|
|
322
444
|
"/create_agent",
|
|
445
|
+
"/codex_status",
|
|
446
|
+
"/codex_login",
|
|
447
|
+
"/codex_switch_key",
|
|
323
448
|
"/cancel",
|
|
324
449
|
"",
|
|
450
|
+
"Codex account:",
|
|
451
|
+
"/codex_login: switch account with device code flow",
|
|
452
|
+
"/codex_switch_key: switch account with API key",
|
|
453
|
+
"",
|
|
325
454
|
"Policy guide in /bots:",
|
|
326
455
|
"Safe: read-only + approval prompts",
|
|
327
456
|
"Standard: workspace write + approval prompts",
|
|
@@ -349,6 +478,12 @@ function cleanupState() {
|
|
|
349
478
|
createFlows.delete(key);
|
|
350
479
|
}
|
|
351
480
|
}
|
|
481
|
+
for (const [key, flow] of codexSwitchFlows.entries()) {
|
|
482
|
+
const createdAt = Number(flow?.createdAt ?? 0);
|
|
483
|
+
if (!Number.isFinite(createdAt) || now - createdAt > FLOW_TTL_MS) {
|
|
484
|
+
codexSwitchFlows.delete(key);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
352
487
|
}
|
|
353
488
|
async function renderBotsMenu(ctx, { editMessage = false, notice = "" } = {}) {
|
|
354
489
|
const bots = await fetchBots();
|
|
@@ -660,6 +795,29 @@ async function editMessageOrReply(ctx, text, options = {}) {
|
|
|
660
795
|
function getChatId(ctx) {
|
|
661
796
|
return String(ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id ?? "").trim();
|
|
662
797
|
}
|
|
798
|
+
function looksLikeCodexApiKey(value) {
|
|
799
|
+
const raw = String(value ?? "").trim();
|
|
800
|
+
if (!raw.startsWith("sk-")) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
if (raw.length < 20 || raw.length > 4096) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
return /^[A-Za-z0-9._-]+$/.test(raw);
|
|
807
|
+
}
|
|
808
|
+
async function maybeDeleteIncomingMessage(ctx) {
|
|
809
|
+
const chatId = ctx.chat?.id;
|
|
810
|
+
const messageId = ctx.message?.message_id;
|
|
811
|
+
if (!chatId || !messageId) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
await ctx.api.deleteMessage(chatId, messageId);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// Best effort only.
|
|
819
|
+
}
|
|
820
|
+
}
|
|
663
821
|
async function answerCallbackQuerySafe(ctx, text = "") {
|
|
664
822
|
try {
|
|
665
823
|
if (text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Copilot Hub CLI and runtime bundle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"status": "npm run build:scripts --silent && node scripts/dist/cli.mjs status",
|
|
58
58
|
"logs": "npm run build:scripts --silent && node scripts/dist/cli.mjs logs",
|
|
59
59
|
"configure": "npm run build:scripts --silent && node scripts/dist/cli.mjs configure",
|
|
60
|
+
"update": "npm run build:scripts --silent && node scripts/dist/cli.mjs update",
|
|
60
61
|
"test": "npm run test --workspaces --if-present",
|
|
61
62
|
"lint": "eslint .",
|
|
62
63
|
"format": "prettier --write \"{scripts,packages}/**/*.{js,mjs,mts,ts,json}\" \"apps/*/src/**/*.{js,ts}\" \"apps/*/package.json\" \"apps/*/test/**/*.{js,ts}\" \"apps/*/tsconfig*.json\" \"{README.md,CONTRIBUTING.md,ARCHITECTURE.md}\" \"eslint.config.mjs\" \"package.json\" \"tsconfig.base.json\"",
|
package/scripts/dist/cli.mjs
CHANGED
|
@@ -87,6 +87,11 @@ async function main() {
|
|
|
87
87
|
runNode(["scripts/dist/service.mjs", ...rawArgs.slice(1)]);
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
|
+
case "update":
|
|
91
|
+
case "upgrade": {
|
|
92
|
+
await runSelfUpdate();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
90
95
|
default: {
|
|
91
96
|
printUsage();
|
|
92
97
|
process.exit(1);
|
|
@@ -224,6 +229,51 @@ async function maybeOfferServiceInstall() {
|
|
|
224
229
|
}
|
|
225
230
|
writeServicePromptState("accepted");
|
|
226
231
|
}
|
|
232
|
+
async function runSelfUpdate() {
|
|
233
|
+
const serviceInstalled = isServiceAlreadyInstalled();
|
|
234
|
+
const runningBeforeUpdate = serviceInstalled ? isDaemonRunning() : hasRunningSupervisorWorkers();
|
|
235
|
+
if (serviceInstalled) {
|
|
236
|
+
const stopService = runNodeCapture(["scripts/dist/service.mjs", "stop"], "inherit");
|
|
237
|
+
if (!stopService.ok) {
|
|
238
|
+
console.log("Service stop reported an error. Continuing update attempt.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const stopLocal = runNodeCapture(["scripts/dist/supervisor.mjs", "down"], "inherit");
|
|
243
|
+
if (!stopLocal.ok) {
|
|
244
|
+
console.log("Local stop reported an error. Continuing update attempt.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const install = runNpm(["install", "-g", "copilot-hub@latest"], "inherit");
|
|
248
|
+
if (!install.ok) {
|
|
249
|
+
const detail = firstLine(install.errorMessage) || firstLine(install.stderr) || firstLine(install.stdout);
|
|
250
|
+
const normalizedDetail = detail.toLowerCase();
|
|
251
|
+
if (normalizedDetail.includes("ebusy") || normalizedDetail.includes("resource busy")) {
|
|
252
|
+
throw new Error([
|
|
253
|
+
"Update failed because files are locked by another process (EBUSY).",
|
|
254
|
+
"Close other terminals using copilot-hub, then retry 'copilot-hub update'.",
|
|
255
|
+
].join("\n"));
|
|
256
|
+
}
|
|
257
|
+
throw new Error(`Update failed: ${detail || "Unknown npm error."}`);
|
|
258
|
+
}
|
|
259
|
+
console.log("copilot-hub updated to latest.");
|
|
260
|
+
if (!runningBeforeUpdate) {
|
|
261
|
+
console.log("Services remain stopped. Run 'copilot-hub start' when ready.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (serviceInstalled) {
|
|
265
|
+
const startService = runNodeCapture(["scripts/dist/service.mjs", "start"], "inherit");
|
|
266
|
+
if (!startService.ok) {
|
|
267
|
+
console.log("Update completed, but service start failed. Run 'copilot-hub start' manually.");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const startLocal = runNodeCapture(["scripts/dist/supervisor.mjs", "up"], "inherit");
|
|
273
|
+
if (!startLocal.ok) {
|
|
274
|
+
console.log("Update completed, but local start failed. Run 'copilot-hub start' manually.");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
227
277
|
function isServiceSupportedOnCurrentPlatform() {
|
|
228
278
|
return (process.platform === "win32" || process.platform === "linux" || process.platform === "darwin");
|
|
229
279
|
}
|
|
@@ -238,6 +288,16 @@ function isServiceAlreadyInstalled() {
|
|
|
238
288
|
}
|
|
239
289
|
return status.ok;
|
|
240
290
|
}
|
|
291
|
+
function isDaemonRunning() {
|
|
292
|
+
const status = runNodeCapture(["scripts/dist/daemon.mjs", "status"], "pipe");
|
|
293
|
+
const output = String(status.combinedOutput ?? "").toLowerCase();
|
|
294
|
+
return output.includes("=== daemon ===") && output.includes("running: yes");
|
|
295
|
+
}
|
|
296
|
+
function hasRunningSupervisorWorkers() {
|
|
297
|
+
const status = runNodeCapture(["scripts/dist/supervisor.mjs", "status"], "pipe");
|
|
298
|
+
const output = String(status.combinedOutput ?? "").toLowerCase();
|
|
299
|
+
return output.includes("running: yes");
|
|
300
|
+
}
|
|
241
301
|
function readServicePromptState() {
|
|
242
302
|
if (!fs.existsSync(servicePromptStatePath)) {
|
|
243
303
|
return null;
|
|
@@ -587,6 +647,7 @@ function spawnNpm(args, options) {
|
|
|
587
647
|
function printUsage() {
|
|
588
648
|
console.log([
|
|
589
649
|
"Usage: node scripts/dist/cli.mjs <start|stop|restart|status|logs|configure|service|version|help>",
|
|
650
|
+
" node scripts/dist/cli.mjs <update|upgrade>",
|
|
590
651
|
"Service management:",
|
|
591
652
|
" node scripts/dist/cli.mjs service <install|uninstall|status|start|stop|help>",
|
|
592
653
|
].join("\n"));
|
package/scripts/dist/daemon.mjs
CHANGED
package/scripts/dist/service.mjs
CHANGED
package/scripts/src/cli.mts
CHANGED
|
@@ -94,6 +94,11 @@ async function main() {
|
|
|
94
94
|
runNode(["scripts/dist/service.mjs", ...rawArgs.slice(1)]);
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
+
case "update":
|
|
98
|
+
case "upgrade": {
|
|
99
|
+
await runSelfUpdate();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
97
102
|
default: {
|
|
98
103
|
printUsage();
|
|
99
104
|
process.exit(1);
|
|
@@ -258,6 +263,60 @@ async function maybeOfferServiceInstall() {
|
|
|
258
263
|
writeServicePromptState("accepted");
|
|
259
264
|
}
|
|
260
265
|
|
|
266
|
+
async function runSelfUpdate() {
|
|
267
|
+
const serviceInstalled = isServiceAlreadyInstalled();
|
|
268
|
+
const runningBeforeUpdate = serviceInstalled ? isDaemonRunning() : hasRunningSupervisorWorkers();
|
|
269
|
+
|
|
270
|
+
if (serviceInstalled) {
|
|
271
|
+
const stopService = runNodeCapture(["scripts/dist/service.mjs", "stop"], "inherit");
|
|
272
|
+
if (!stopService.ok) {
|
|
273
|
+
console.log("Service stop reported an error. Continuing update attempt.");
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
const stopLocal = runNodeCapture(["scripts/dist/supervisor.mjs", "down"], "inherit");
|
|
277
|
+
if (!stopLocal.ok) {
|
|
278
|
+
console.log("Local stop reported an error. Continuing update attempt.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const install = runNpm(["install", "-g", "copilot-hub@latest"], "inherit");
|
|
283
|
+
if (!install.ok) {
|
|
284
|
+
const detail =
|
|
285
|
+
firstLine(install.errorMessage) || firstLine(install.stderr) || firstLine(install.stdout);
|
|
286
|
+
const normalizedDetail = detail.toLowerCase();
|
|
287
|
+
if (normalizedDetail.includes("ebusy") || normalizedDetail.includes("resource busy")) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
[
|
|
290
|
+
"Update failed because files are locked by another process (EBUSY).",
|
|
291
|
+
"Close other terminals using copilot-hub, then retry 'copilot-hub update'.",
|
|
292
|
+
].join("\n"),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
throw new Error(`Update failed: ${detail || "Unknown npm error."}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log("copilot-hub updated to latest.");
|
|
299
|
+
|
|
300
|
+
if (!runningBeforeUpdate) {
|
|
301
|
+
console.log("Services remain stopped. Run 'copilot-hub start' when ready.");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (serviceInstalled) {
|
|
306
|
+
const startService = runNodeCapture(["scripts/dist/service.mjs", "start"], "inherit");
|
|
307
|
+
if (!startService.ok) {
|
|
308
|
+
console.log("Update completed, but service start failed. Run 'copilot-hub start' manually.");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const startLocal = runNodeCapture(["scripts/dist/supervisor.mjs", "up"], "inherit");
|
|
315
|
+
if (!startLocal.ok) {
|
|
316
|
+
console.log("Update completed, but local start failed. Run 'copilot-hub start' manually.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
261
320
|
function isServiceSupportedOnCurrentPlatform() {
|
|
262
321
|
return (
|
|
263
322
|
process.platform === "win32" || process.platform === "linux" || process.platform === "darwin"
|
|
@@ -276,6 +335,18 @@ function isServiceAlreadyInstalled() {
|
|
|
276
335
|
return status.ok;
|
|
277
336
|
}
|
|
278
337
|
|
|
338
|
+
function isDaemonRunning() {
|
|
339
|
+
const status = runNodeCapture(["scripts/dist/daemon.mjs", "status"], "pipe");
|
|
340
|
+
const output = String(status.combinedOutput ?? "").toLowerCase();
|
|
341
|
+
return output.includes("=== daemon ===") && output.includes("running: yes");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasRunningSupervisorWorkers() {
|
|
345
|
+
const status = runNodeCapture(["scripts/dist/supervisor.mjs", "status"], "pipe");
|
|
346
|
+
const output = String(status.combinedOutput ?? "").toLowerCase();
|
|
347
|
+
return output.includes("running: yes");
|
|
348
|
+
}
|
|
349
|
+
|
|
279
350
|
function readServicePromptState() {
|
|
280
351
|
if (!fs.existsSync(servicePromptStatePath)) {
|
|
281
352
|
return null;
|
|
@@ -693,6 +764,7 @@ function printUsage() {
|
|
|
693
764
|
console.log(
|
|
694
765
|
[
|
|
695
766
|
"Usage: node scripts/dist/cli.mjs <start|stop|restart|status|logs|configure|service|version|help>",
|
|
767
|
+
" node scripts/dist/cli.mjs <update|upgrade>",
|
|
696
768
|
"Service management:",
|
|
697
769
|
" node scripts/dist/cli.mjs service <install|uninstall|status|start|stop|help>",
|
|
698
770
|
].join("\n"),
|
package/scripts/src/daemon.mts
CHANGED