cartethyia 2.4.2 → 2.4.4
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/dist/dashboard/assets/{BannedPage-KprmzKo8.js → BannedPage-TDue2X0p.js} +1 -1
- package/dist/dashboard/assets/{CliToolDetailPage-BL_oNmmg.js → CliToolDetailPage-qaHXuiI5.js} +1 -1
- package/dist/dashboard/assets/{CliToolsPage-XjDkuNCh.js → CliToolsPage-BFuQBKez.js} +1 -1
- package/dist/dashboard/assets/{ConsoleLogPage-COtZ6SqU.js → ConsoleLogPage-vkZBxMVo.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-0K0ySe8K.js → CustomizationPage-BhglcqyR.js} +1 -1
- package/dist/dashboard/assets/{HealthEventsModal-O05igcJY.js → HealthEventsModal-DDkHfwpV.js} +1 -1
- package/dist/dashboard/assets/{LoginPage-a_voqncN.js → LoginPage-CeqROJrT.js} +1 -1
- package/dist/dashboard/assets/{OverviewPage-hl2ua1qC.js → OverviewPage-CRXSAITY.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-CjpQIFMI.js → ProviderDetailPage-cNAD_6uN.js} +1 -1
- package/dist/dashboard/assets/{ProvidersPage-xsrxYHsk.js → ProvidersPage-Cv5tdxlx.js} +1 -1
- package/dist/dashboard/assets/{ProxyPage-Bm8Eu21x.js → ProxyPage-CWvMfzjj.js} +1 -1
- package/dist/dashboard/assets/{QuotaPage-BKU9VtlD.js → QuotaPage-IYQOOHR4.js} +1 -1
- package/dist/dashboard/assets/{SetupPage-CdjhQfnC.js → SetupPage-BPDNo3Ek.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-DMGEaI3G.js → StudioPage-BE5DJONZ.js} +1 -1
- package/dist/dashboard/assets/{UsagePage-B6D-eJDG.js → UsagePage-B6LA4ewF.js} +1 -1
- package/dist/dashboard/assets/{entry-B92UGcyr.js → entry-DTiWhIY-.js} +3 -3
- package/dist/dashboard/assets/{index-BGSJSZeV.js → index-BA_ni9Ix.js} +2 -2
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli/cli.ts +108 -270
- package/scripts/cli/tray.ps1 +98 -0
- package/scripts/cli/tray.ts +213 -0
package/scripts/cli/cli.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
1
|
+
import { existsSync, mkdirSync, openSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
6
|
-
import SysTray from "systray2";
|
|
7
5
|
import { isNewerRelease } from "../../src/console/updates/version-compare";
|
|
8
6
|
import { PACKAGE_ROOT, resolvePackagePath, resolveUserConfigDir } from "../../src/config/package-paths";
|
|
9
7
|
import { resolvePort } from "../../src/config/runtime";
|
|
10
8
|
import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../shared/env";
|
|
11
9
|
import { chooseFromMenu, confirmByTyping, confirmStep } from "./menu";
|
|
12
10
|
import { ensureDrainToken, readPersistedDrainToken } from "./runtime-state";
|
|
11
|
+
import { createTray } from "./tray";
|
|
13
12
|
import { npmUpdate, readInstalledVersion, readLatestPublishedVersion } from "./version";
|
|
14
13
|
import { renderWordmark } from "./wordmark";
|
|
15
14
|
|
|
@@ -69,36 +68,6 @@ function reportTrayStartup(signal: TrayStartupSignal): void {
|
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
async function waitForTrayStartupSignal(
|
|
73
|
-
signalPath: string,
|
|
74
|
-
child: ChildProcess,
|
|
75
|
-
timeoutMs: number,
|
|
76
|
-
): Promise<string> {
|
|
77
|
-
let spawnError: Error | undefined;
|
|
78
|
-
child.once("error", (error) => {
|
|
79
|
-
spawnError = error;
|
|
80
|
-
});
|
|
81
|
-
const deadline = Date.now() + timeoutMs;
|
|
82
|
-
while (Date.now() < deadline) {
|
|
83
|
-
if (existsSync(signalPath)) {
|
|
84
|
-
try {
|
|
85
|
-
const parsed: unknown = JSON.parse(readFileSync(signalPath, "utf8"));
|
|
86
|
-
if (parsed !== null && typeof parsed === "object" && "status" in parsed) {
|
|
87
|
-
if (parsed.status === "ready" && "url" in parsed && typeof parsed.url === "string") return parsed.url;
|
|
88
|
-
if (parsed.status === "failed" && "message" in parsed && typeof parsed.message === "string") {
|
|
89
|
-
throw new Error(parsed.message);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (spawnError) throw spawnError;
|
|
97
|
-
if (child.exitCode !== null) throw new Error(`updated Cartethyia exited during startup (exit code ${child.exitCode})`);
|
|
98
|
-
await Bun.sleep(150);
|
|
99
|
-
}
|
|
100
|
-
throw new Error("Cartethyia did not finish tray and gateway startup within 45 seconds");
|
|
101
|
-
}
|
|
102
71
|
|
|
103
72
|
|
|
104
73
|
function openUrl(url: string): Promise<void> {
|
|
@@ -116,16 +85,6 @@ function openUrl(url: string): Promise<void> {
|
|
|
116
85
|
}
|
|
117
86
|
|
|
118
87
|
|
|
119
|
-
function launchUpdatedTray(signalPath: string): ChildProcess {
|
|
120
|
-
const env = { ...process.env, CARTETHYIA_TRAY_STARTUP_SIGNAL: signalPath };
|
|
121
|
-
const child = spawn(
|
|
122
|
-
process.execPath,
|
|
123
|
-
["run", resolvePackagePath("scripts", "cli", "cli.ts"), "tray"],
|
|
124
|
-
{ cwd: tmpdir(), detached: true, stdio: "ignore", windowsHide: true, env },
|
|
125
|
-
);
|
|
126
|
-
child.unref();
|
|
127
|
-
return child;
|
|
128
|
-
}
|
|
129
88
|
|
|
130
89
|
async function startGateway(
|
|
131
90
|
port: number,
|
|
@@ -259,42 +218,7 @@ async function stopGateway(
|
|
|
259
218
|
}
|
|
260
219
|
}
|
|
261
220
|
|
|
262
|
-
async function waitForTray(tray: SysTray): Promise<void> {
|
|
263
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
264
|
-
try {
|
|
265
|
-
await Promise.race([
|
|
266
|
-
tray.ready(),
|
|
267
|
-
new Promise<never>((_, reject) => {
|
|
268
|
-
timer = setTimeout(() => reject(new Error("System tray startup timed out")), 15_000);
|
|
269
|
-
}),
|
|
270
|
-
]);
|
|
271
|
-
} finally {
|
|
272
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function stopTray(tray: SysTray, ready: boolean, exited: boolean): Promise<void> {
|
|
277
|
-
if (exited) return;
|
|
278
|
-
if (!ready) {
|
|
279
|
-
tray.process.kill();
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
221
|
|
|
283
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
284
|
-
try {
|
|
285
|
-
await Promise.race([
|
|
286
|
-
tray.kill(false),
|
|
287
|
-
new Promise<void>((resolveExit) => {
|
|
288
|
-
timer = setTimeout(() => resolveExit(), 5_000);
|
|
289
|
-
}),
|
|
290
|
-
]);
|
|
291
|
-
} catch {
|
|
292
|
-
// Fall through to a direct child termination below.
|
|
293
|
-
} finally {
|
|
294
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
295
|
-
}
|
|
296
|
-
if (!tray.killed) tray.process.kill();
|
|
297
|
-
}
|
|
298
222
|
|
|
299
223
|
/**
|
|
300
224
|
* The default `carte` experience: start the gateway, then offer an arrow-key
|
|
@@ -432,8 +356,22 @@ async function startMenu(): Promise<void> {
|
|
|
432
356
|
async () => openUrl(repositoryUrl),
|
|
433
357
|
async () => {
|
|
434
358
|
if (upToDate) return;
|
|
359
|
+
// npm cannot replace the package directory while a process is running
|
|
360
|
+
// from it: on Windows the rename fails with EBUSY and npm exits 14.
|
|
361
|
+
// The gateway has to be stopped before the install, not after.
|
|
362
|
+
console.log("Stopping the gateway so npm can replace its files…");
|
|
363
|
+
await shutDown();
|
|
435
364
|
const code = await npmUpdate();
|
|
436
|
-
|
|
365
|
+
if (code !== 0) {
|
|
366
|
+
console.log(`Update failed (npm exited ${code}). Restarting the current version.`);
|
|
367
|
+
const restored = await startGateway(port, drainToken);
|
|
368
|
+
restored.unref();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const restarted = await startGateway(port, drainToken);
|
|
372
|
+
restarted.unref();
|
|
373
|
+
const installed = await readInstalledVersion();
|
|
374
|
+
console.log(`Cartethyia updated and restarted on ${installed} at ${dashboardUrl}`);
|
|
437
375
|
latestVersion = await readLatestPublishedVersion();
|
|
438
376
|
},
|
|
439
377
|
async () => {
|
|
@@ -620,243 +558,142 @@ async function stopRunningGateway(): Promise<number> {
|
|
|
620
558
|
}
|
|
621
559
|
|
|
622
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Runs the gateway with a tray icon. The tray is platform-specific (`tray.ts`):
|
|
563
|
+
* on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
|
|
564
|
+
* an empty console tab open for the tray's whole lifetime, and that window is
|
|
565
|
+
* owned by Windows Terminal — not by this process, so it cannot be found and
|
|
566
|
+
* hidden from inside.
|
|
567
|
+
*/
|
|
623
568
|
async function startTray(): Promise<void> {
|
|
624
569
|
const port = resolvePort();
|
|
625
570
|
const drainToken = ensureDrainToken();
|
|
626
|
-
const currentVersion = await readInstalledVersion();
|
|
627
571
|
const dashboardUrl = `http://127.0.0.1:${port}/console`;
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
|
|
631
|
-
);
|
|
632
|
-
const repository = {
|
|
633
|
-
title: "Repository: risunCode/cartethyia",
|
|
634
|
-
tooltip: "Open the Cartethyia GitHub repository",
|
|
635
|
-
enabled: true,
|
|
636
|
-
};
|
|
637
|
-
const update = {
|
|
638
|
-
title: "Checking for updates…",
|
|
639
|
-
tooltip: "Update Cartethyia and restart the tray process",
|
|
640
|
-
enabled: false,
|
|
641
|
-
};
|
|
642
|
-
const openConsole = {
|
|
643
|
-
title: `Open Console · ${dashboardUrl}`,
|
|
644
|
-
tooltip: `Open the local Console at ${dashboardUrl}`,
|
|
645
|
-
enabled: true,
|
|
646
|
-
};
|
|
647
|
-
const resetPassword = {
|
|
648
|
-
title: "Reset console password",
|
|
649
|
-
tooltip: "Delete the console account so /console asks for a new one",
|
|
650
|
-
enabled: true,
|
|
651
|
-
};
|
|
652
|
-
const backupRestore = {
|
|
653
|
-
title: "Backup",
|
|
654
|
-
tooltip: "Write a gzipped config backup to your download directory",
|
|
655
|
-
enabled: true,
|
|
656
|
-
};
|
|
657
|
-
const exit = {
|
|
658
|
-
title: "Exit",
|
|
659
|
-
tooltip: "Stop Cartethyia gracefully and close the tray",
|
|
660
|
-
enabled: true,
|
|
661
|
-
};
|
|
662
|
-
const tray = new SysTray({
|
|
663
|
-
menu: {
|
|
664
|
-
icon,
|
|
665
|
-
title: "Cartethyia",
|
|
666
|
-
tooltip: "Cartethyia AI gateway",
|
|
667
|
-
items: [
|
|
668
|
-
repository,
|
|
669
|
-
update,
|
|
670
|
-
SysTray.separator,
|
|
671
|
-
openConsole,
|
|
672
|
-
resetPassword,
|
|
673
|
-
backupRestore,
|
|
674
|
-
SysTray.separator,
|
|
675
|
-
exit,
|
|
676
|
-
],
|
|
677
|
-
},
|
|
678
|
-
copyDir: true,
|
|
679
|
-
});
|
|
572
|
+
const repositoryUrl = "https://github.com/risunCode/cartethyia";
|
|
573
|
+
const currentVersion = await readInstalledVersion();
|
|
680
574
|
|
|
681
|
-
let gateway:
|
|
682
|
-
let trayReady = false;
|
|
683
|
-
let trayExited = false;
|
|
575
|
+
let gateway: Bun.Subprocess | null = null;
|
|
684
576
|
let closing = false;
|
|
685
577
|
let busy = false;
|
|
686
578
|
let resetArmed = false;
|
|
687
|
-
let latestVersion: string | null = null;
|
|
688
579
|
let closePromise: Promise<void> | undefined;
|
|
689
580
|
|
|
581
|
+
const tray = createTray({
|
|
582
|
+
iconPath: resolvePackagePath(
|
|
583
|
+
"tray-assets",
|
|
584
|
+
process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
|
|
585
|
+
),
|
|
586
|
+
tooltip: `Cartethyia ${currentVersion}`,
|
|
587
|
+
items: [
|
|
588
|
+
{ kind: "action", id: "repository", title: "Visit repository" },
|
|
589
|
+
{ kind: "action", id: "update", title: `Cartethyia ${currentVersion} \u00b7 checking\u2026`, enabled: false },
|
|
590
|
+
{ kind: "separator" },
|
|
591
|
+
{ kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
|
|
592
|
+
{ kind: "action", id: "reset", title: "Reset console password" },
|
|
593
|
+
{ kind: "action", id: "backup", title: "Backup" },
|
|
594
|
+
{ kind: "separator" },
|
|
595
|
+
{ kind: "action", id: "exit", title: "Exit" },
|
|
596
|
+
],
|
|
597
|
+
onSelect: (id: string) => void handleSelect(id),
|
|
598
|
+
onExit: () => void close(1),
|
|
599
|
+
});
|
|
600
|
+
|
|
690
601
|
const close = (exitCode = 0): Promise<void> => {
|
|
691
602
|
if (closePromise) return closePromise;
|
|
692
603
|
closing = true;
|
|
693
604
|
closePromise = (async () => {
|
|
694
|
-
const current:
|
|
605
|
+
const current: Bun.Subprocess | null = gateway;
|
|
695
606
|
gateway = null;
|
|
696
607
|
if (current) await stopGateway(current, port, drainToken);
|
|
697
|
-
await
|
|
608
|
+
await tray.close();
|
|
698
609
|
process.exitCode = exitCode;
|
|
699
610
|
})();
|
|
700
611
|
return closePromise;
|
|
701
612
|
};
|
|
702
613
|
|
|
703
614
|
const refreshVersion = async (): Promise<void> => {
|
|
704
|
-
const
|
|
615
|
+
const fetched = await readLatestPublishedVersion();
|
|
705
616
|
if (closing) return;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
update.enabled = true;
|
|
710
|
-
} else {
|
|
711
|
-
if (isNewerRelease(currentVersion, fetchedVersion)) {
|
|
712
|
-
update.title = `Cartethyia ${currentVersion} · update to ${fetchedVersion}`;
|
|
713
|
-
update.enabled = true;
|
|
714
|
-
} else {
|
|
715
|
-
update.title = `Cartethyia ${currentVersion} · up to date`;
|
|
716
|
-
update.enabled = false;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
try {
|
|
720
|
-
await tray.sendAction({ type: "update-item", item: update });
|
|
721
|
-
} catch {
|
|
722
|
-
// The tray may be closing while the background version check completes.
|
|
617
|
+
if (fetched === null) {
|
|
618
|
+
tray.update("update", `Cartethyia ${currentVersion} \u00b7 check for updates`, true);
|
|
619
|
+
return;
|
|
723
620
|
}
|
|
621
|
+
const newer = isNewerRelease(currentVersion, fetched);
|
|
622
|
+
tray.update(
|
|
623
|
+
"update",
|
|
624
|
+
newer
|
|
625
|
+
? `Cartethyia ${currentVersion} \u00b7 update to ${fetched}`
|
|
626
|
+
: `Cartethyia ${currentVersion} \u00b7 up to date`,
|
|
627
|
+
newer,
|
|
628
|
+
);
|
|
724
629
|
};
|
|
725
630
|
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
resetArmed = true;
|
|
743
|
-
console.log(
|
|
744
|
-
"Click \"Reset console password\" again to delete the console account. This removes every operator login; providers, API keys, and routing are kept.",
|
|
745
|
-
);
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
resetArmed = false;
|
|
749
|
-
const removed = await resetConsoleAccount(port, drainToken);
|
|
750
|
-
if (removed === 0) {
|
|
751
|
-
console.log(`Console account removed. Open ${dashboardUrl} to create a new one.`);
|
|
752
|
-
await openUrl(dashboardUrl);
|
|
753
|
-
}
|
|
631
|
+
const runUpdate = async (): Promise<void> => {
|
|
632
|
+
if (busy) return;
|
|
633
|
+
busy = true;
|
|
634
|
+
// npm cannot replace the package directory while a process is running from
|
|
635
|
+
// it: on Windows the rename fails with EBUSY and npm exits 14. Stop first.
|
|
636
|
+
tray.update("update", "Stopping to update\u2026", false);
|
|
637
|
+
const current = gateway;
|
|
638
|
+
gateway = null;
|
|
639
|
+
if (current) await stopGateway(current, port, drainToken);
|
|
640
|
+
|
|
641
|
+
tray.update("update", "Updating Cartethyia\u2026", false);
|
|
642
|
+
const code = await npmUpdate();
|
|
643
|
+
if (code !== 0) {
|
|
644
|
+
gateway = await startGateway(port, drainToken);
|
|
645
|
+
busy = false;
|
|
646
|
+
tray.update("update", `Update failed (npm ${code}) \u2014 retry`, true);
|
|
754
647
|
return;
|
|
755
648
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
649
|
+
gateway = await startGateway(port, drainToken);
|
|
650
|
+
busy = false;
|
|
651
|
+
const installed = await readInstalledVersion();
|
|
652
|
+
tray.update("update", `Cartethyia ${installed} \u00b7 up to date`, false);
|
|
653
|
+
console.log(`Cartethyia updated and restarted on ${installed} at ${dashboardUrl}`);
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
async function handleSelect(id: string): Promise<void> {
|
|
657
|
+
if (closing) return;
|
|
658
|
+
if (id === "exit") return void (await close());
|
|
659
|
+
if (id === "console") return void (await openUrl(dashboardUrl));
|
|
660
|
+
if (id === "repository") return void (await openUrl(repositoryUrl));
|
|
661
|
+
if (id === "backup") {
|
|
759
662
|
const result = await exportBackup(port, drainToken);
|
|
760
|
-
if (result !== null)
|
|
663
|
+
if (result !== null) {
|
|
761
664
|
console.log(
|
|
762
665
|
`Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
|
|
763
666
|
);
|
|
667
|
+
}
|
|
764
668
|
return;
|
|
765
669
|
}
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
if (title !== update.title || !update.enabled || busy) return;
|
|
775
|
-
if (latestVersion === null || !isNewerRelease(currentVersion, latestVersion)) {
|
|
776
|
-
await refreshVersion();
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
busy = true;
|
|
781
|
-
update.title = "Updating Cartethyia…";
|
|
782
|
-
update.enabled = false;
|
|
783
|
-
try {
|
|
784
|
-
await tray.sendAction({ type: "update-item", item: update });
|
|
785
|
-
const current = gateway;
|
|
786
|
-
if (!current) throw new Error("Cartethyia gateway is not running");
|
|
787
|
-
gateway = null;
|
|
788
|
-
await stopGateway(current, port, drainToken);
|
|
789
|
-
if (closing) return;
|
|
790
|
-
|
|
791
|
-
console.log("Updating Cartethyia through npm...");
|
|
792
|
-
const code = await npmUpdate();
|
|
793
|
-
if (closing) return;
|
|
794
|
-
if (code !== 0) {
|
|
795
|
-
console.error(`npm update failed with exit code ${code}; restarting the current installation.`);
|
|
796
|
-
gateway = await startGateway(port, drainToken);
|
|
797
|
-
await refreshVersion();
|
|
670
|
+
if (id === "reset") {
|
|
671
|
+
// A tray click cannot ask for the typed confirmation the menu uses, so
|
|
672
|
+
// the first click arms the action and the second one runs it.
|
|
673
|
+
if (!resetArmed) {
|
|
674
|
+
resetArmed = true;
|
|
675
|
+
tray.update("reset", "Reset console password \u00b7 click again to delete", true);
|
|
798
676
|
return;
|
|
799
677
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const updatedUrl = await waitForTrayStartupSignal(signalPath, updatedProcess, 60_000);
|
|
805
|
-
console.log(`Cartethyia updated to ${await readInstalledVersion()} and restarted at ${updatedUrl}`);
|
|
806
|
-
await close();
|
|
807
|
-
} finally {
|
|
808
|
-
if (existsSync(signalPath)) unlinkSync(signalPath);
|
|
809
|
-
}
|
|
810
|
-
} catch (error) {
|
|
811
|
-
const reason = error instanceof Error ? error.message : "unknown error";
|
|
812
|
-
console.error(`Cartethyia update or restart failed: ${reason}`);
|
|
813
|
-
if (!closing && gateway === null) {
|
|
814
|
-
try {
|
|
815
|
-
gateway = await startGateway(port, drainToken);
|
|
816
|
-
} catch {
|
|
817
|
-
await close(1);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
if (!closing) {
|
|
821
|
-
update.title = "Update failed — retry";
|
|
822
|
-
update.enabled = true;
|
|
823
|
-
try {
|
|
824
|
-
await tray.sendAction({ type: "update-item", item: update });
|
|
825
|
-
} catch {
|
|
826
|
-
// The tray may have failed while the update was being recovered.
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
} finally {
|
|
830
|
-
busy = false;
|
|
678
|
+
resetArmed = false;
|
|
679
|
+
tray.update("reset", "Reset console password", true);
|
|
680
|
+
await resetConsoleAccount(port, drainToken);
|
|
681
|
+
return;
|
|
831
682
|
}
|
|
832
|
-
|
|
683
|
+
if (id === "update") await runUpdate();
|
|
684
|
+
}
|
|
833
685
|
|
|
834
|
-
await tray.onClick((event) => {
|
|
835
|
-
void handleClick(event.item.title).catch(() => {
|
|
836
|
-
console.error("The selected system-tray action failed.");
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
tray.onExit(() => {
|
|
840
|
-
trayExited = true;
|
|
841
|
-
if (!closing) void close(1);
|
|
842
|
-
});
|
|
843
|
-
tray.onError(() => {
|
|
844
|
-
console.error("The system-tray process failed; stopping Cartethyia.");
|
|
845
|
-
if (!closing) void close(1);
|
|
846
|
-
});
|
|
847
686
|
process.once("SIGINT", () => void close());
|
|
848
687
|
process.once("SIGTERM", () => void close());
|
|
849
688
|
|
|
850
689
|
try {
|
|
851
|
-
await
|
|
852
|
-
trayReady = true;
|
|
690
|
+
await tray.ready();
|
|
853
691
|
gateway = await startGateway(port, drainToken);
|
|
854
|
-
reportTrayStartup({ status: "ready", url: dashboardUrl });
|
|
855
692
|
console.log(`Cartethyia is running at ${dashboardUrl}`);
|
|
856
693
|
void refreshVersion();
|
|
857
694
|
|
|
858
695
|
while (!closing) {
|
|
859
|
-
const current:
|
|
696
|
+
const current: Bun.Subprocess | null = gateway;
|
|
860
697
|
if (!current) {
|
|
861
698
|
await Bun.sleep(100);
|
|
862
699
|
continue;
|
|
@@ -873,6 +710,7 @@ async function startTray(): Promise<void> {
|
|
|
873
710
|
}
|
|
874
711
|
}
|
|
875
712
|
|
|
713
|
+
|
|
876
714
|
async function main(): Promise<void> {
|
|
877
715
|
assertBunVersion();
|
|
878
716
|
const [command] = process.argv.slice(2);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Cartethyia tray icon for Windows using NotifyIcon.
|
|
2
|
+
#
|
|
3
|
+
# Why PowerShell instead of a Node tray library: `systray2` and friends bundle a
|
|
4
|
+
# helper binary and still leave the spawning process attached to a console, so
|
|
5
|
+
# Windows Terminal shows an empty tab for the tray's whole lifetime. A
|
|
6
|
+
# PowerShell NotifyIcon owns no console of its own when started with
|
|
7
|
+
# -WindowStyle Hidden, and the protocol here is plain JSON lines on
|
|
8
|
+
# stdin/stdout, so the parent stays in charge.
|
|
9
|
+
#
|
|
10
|
+
# Protocol: the parent writes one JSON object per line.
|
|
11
|
+
# {"action":"add-item","index":0,"title":"...","enabled":true}
|
|
12
|
+
# {"action":"update-item","index":0,"title":"...","enabled":true}
|
|
13
|
+
# {"action":"set-tooltip","text":"..."}
|
|
14
|
+
# {"action":"kill"}
|
|
15
|
+
# This script writes one JSON object per line:
|
|
16
|
+
# {"type":"started"} {"type":"ready"} {"type":"click","index":0}
|
|
17
|
+
param([string]$IconPath, [string]$Tooltip)
|
|
18
|
+
|
|
19
|
+
$ErrorActionPreference = "Stop"
|
|
20
|
+
|
|
21
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
22
|
+
Add-Type -AssemblyName System.Drawing
|
|
23
|
+
|
|
24
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
25
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
26
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
27
|
+
|
|
28
|
+
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
29
|
+
[System.Windows.Forms.Application]::SetCompatibleTextRenderingDefault($false)
|
|
30
|
+
|
|
31
|
+
$script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
|
|
32
|
+
$script:notifyIcon.Icon = New-Object System.Drawing.Icon($IconPath)
|
|
33
|
+
$script:notifyIcon.Text = $Tooltip
|
|
34
|
+
$script:notifyIcon.Visible = $true
|
|
35
|
+
|
|
36
|
+
$script:menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
37
|
+
$script:notifyIcon.ContextMenuStrip = $script:menu
|
|
38
|
+
$script:items = @()
|
|
39
|
+
|
|
40
|
+
function Write-Event($obj) {
|
|
41
|
+
$json = $obj | ConvertTo-Json -Compress
|
|
42
|
+
[Console]::Out.WriteLine($json)
|
|
43
|
+
[Console]::Out.Flush()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Add-MenuItem($index, $title, $enabled) {
|
|
47
|
+
$item = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
48
|
+
$item.Text = $title
|
|
49
|
+
$item.Enabled = $enabled
|
|
50
|
+
$idx = $index
|
|
51
|
+
$item.Add_Click({ Write-Event @{ type = "click"; index = $idx } }.GetNewClosure())
|
|
52
|
+
$script:menu.Items.Add($item) | Out-Null
|
|
53
|
+
$script:items += $item
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Update-MenuItem($index, $title, $enabled) {
|
|
57
|
+
if ($index -lt $script:items.Count) {
|
|
58
|
+
$script:items[$index].Text = $title
|
|
59
|
+
$script:items[$index].Enabled = $enabled
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Set-Tooltip($text) {
|
|
64
|
+
# NotifyIcon.Text is capped at 63 characters.
|
|
65
|
+
if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
|
|
66
|
+
$script:notifyIcon.Text = $text
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Stdin is polled from the UI thread: a background thread reading Console.In
|
|
70
|
+
# races the message loop and deadlocks when the menu is open.
|
|
71
|
+
$script:timer = New-Object System.Windows.Forms.Timer
|
|
72
|
+
$script:timer.Interval = 100
|
|
73
|
+
$script:timer.Add_Tick({
|
|
74
|
+
try {
|
|
75
|
+
while ([Console]::In.Peek() -ne -1) {
|
|
76
|
+
$line = [Console]::In.ReadLine()
|
|
77
|
+
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
78
|
+
$cmd = $line | ConvertFrom-Json
|
|
79
|
+
switch ($cmd.action) {
|
|
80
|
+
"add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
81
|
+
"update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
82
|
+
"set-tooltip" { Set-Tooltip $cmd.text }
|
|
83
|
+
"ready" { Write-Event @{ type = "ready" } }
|
|
84
|
+
"kill" {
|
|
85
|
+
$script:notifyIcon.Visible = $false
|
|
86
|
+
$script:notifyIcon.Dispose()
|
|
87
|
+
[System.Windows.Forms.Application]::Exit()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
Write-Event @{ type = "error"; message = $_.Exception.Message }
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
$script:timer.Start()
|
|
96
|
+
|
|
97
|
+
Write-Event @{ type = "started" }
|
|
98
|
+
[System.Windows.Forms.Application]::Run()
|