cartethyia 2.3.1 → 2.3.3
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-BZQBIhxF.js → BannedPage-B8MQBL1Y.js} +1 -1
- package/dist/dashboard/assets/{CliToolDetailPage-ChtDDIFA.js → CliToolDetailPage-DAAUVo7w.js} +1 -1
- package/dist/dashboard/assets/{CliToolsPage-CfmolGV7.js → CliToolsPage--hLkg7LU.js} +1 -1
- package/dist/dashboard/assets/{ConsoleLogPage-ZCwKyxxl.js → ConsoleLogPage-CRaToMW5.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-WJejmVl_.js → CustomizationPage-CYiaepa1.js} +1 -1
- package/dist/dashboard/assets/{HealthEventsModal-URh6wOhb.js → HealthEventsModal-CjsAm6Gm.js} +1 -1
- package/dist/dashboard/assets/{LoginPage-C5ib2ujx.js → LoginPage-DVL5oW3v.js} +1 -1
- package/dist/dashboard/assets/{OverviewPage-BwOb4p9C.js → OverviewPage-DJ8WM59t.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-w4o_xJlP.js → ProviderDetailPage-CK0q-HYZ.js} +1 -1
- package/dist/dashboard/assets/{ProvidersPage-6_WFzAGN.js → ProvidersPage-BdVj-Vab.js} +1 -1
- package/dist/dashboard/assets/{ProxyPage-DaGQsvht.js → ProxyPage-CTohaici.js} +1 -1
- package/dist/dashboard/assets/{QuotaPage-BPPN7W5b.js → QuotaPage-CkHjJoim.js} +1 -1
- package/dist/dashboard/assets/{SetupPage-CDqTXPe7.js → SetupPage-CRahC2bj.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CiGmv9Ju.js → StudioPage-CxDqECuN.js} +1 -1
- package/dist/dashboard/assets/{UsagePage-CxrGngrm.js → UsagePage-TzfUYdQD.js} +1 -1
- package/dist/dashboard/assets/{entry-C3M9aRLH.js → entry-KOZCZ9VG.js} +3 -3
- package/dist/dashboard/assets/{index-BS-k3kIz.js → index-Bj96PK8B.js} +2 -2
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli/carte-launcher.cjs +5 -1
- package/scripts/cli/cli-launcher.cjs +32 -11
- package/scripts/cli/cli.ts +223 -5
|
@@ -21,17 +21,34 @@ const startupSignalPath = trayMode
|
|
|
21
21
|
? path.join(os.tmpdir(), `cartethyia-tray-${process.pid}-${randomUUID()}.json`)
|
|
22
22
|
: null;
|
|
23
23
|
let spawnError;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
// On Windows, `cmd /c start "" /b` gives bun its own console group so the
|
|
25
|
+
// launching terminal can close without signalling the gateway. Elsewhere
|
|
26
|
+
// `detached: true` is enough. Either way the child is unref'd below so the
|
|
27
|
+
// launcher can exit without taking the gateway down with it.
|
|
28
|
+
const useWindowsDetach = process.platform === "win32" && trayMode;
|
|
29
|
+
const child = useWindowsDetach
|
|
30
|
+
? spawn("cmd", ["/c", "start", "", "/b", "bun", "run", cliPath, ...process.argv.slice(2)], {
|
|
31
|
+
cwd: packageRoot,
|
|
32
|
+
detached: true,
|
|
33
|
+
stdio: "ignore",
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
env: {
|
|
36
|
+
...process.env,
|
|
37
|
+
CARTETHYIA_CONFIG_DIR: configDir,
|
|
38
|
+
CARTETHYIA_TRAY_STARTUP_SIGNAL: startupSignalPath,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
: spawn("bun", ["run", cliPath, ...process.argv.slice(2)], {
|
|
42
|
+
cwd: packageRoot,
|
|
43
|
+
detached: trayMode,
|
|
44
|
+
stdio: trayMode ? "ignore" : "inherit",
|
|
45
|
+
windowsHide: trayMode,
|
|
46
|
+
env: {
|
|
47
|
+
...process.env,
|
|
48
|
+
CARTETHYIA_CONFIG_DIR: configDir,
|
|
49
|
+
...(trayMode ? { CARTETHYIA_TRAY_STARTUP_SIGNAL: startupSignalPath } : {}),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
35
52
|
|
|
36
53
|
child.once("error", (error) => {
|
|
37
54
|
spawnError = error;
|
|
@@ -61,6 +78,10 @@ if (trayMode) {
|
|
|
61
78
|
return;
|
|
62
79
|
}
|
|
63
80
|
console.log(`Cartethyia is ready at ${url}`);
|
|
81
|
+
// The gateway child is already detached and unref'd; this Node process is
|
|
82
|
+
// the only thing still holding the launching terminal open. Exit once the
|
|
83
|
+
// URL has been shown so the shell prompt comes back.
|
|
84
|
+
setTimeout(() => process.exit(0), 1_500);
|
|
64
85
|
};
|
|
65
86
|
const poll = () => {
|
|
66
87
|
if (settled) return;
|
package/scripts/cli/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import SysTray from "systray2";
|
|
7
7
|
import { isNewerRelease } from "../../src/console/updates/version-compare";
|
|
8
8
|
import { PACKAGE_ROOT, resolvePackagePath, resolveUserConfigDir } from "../../src/config/package-paths";
|
|
9
|
+
import { resolvePort } from "../../src/config/runtime";
|
|
9
10
|
import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../shared/env";
|
|
10
11
|
|
|
11
12
|
process.env.CARTETHYIA_CONFIG_DIR ??= resolveUserConfigDir();
|
|
@@ -13,13 +14,18 @@ process.env.CARTETHYIA_CONFIG_DIR ??= resolveUserConfigDir();
|
|
|
13
14
|
const HELP = `Cartethyia ${MINIMUM_BUN_VERSION}+ (Bun is required)
|
|
14
15
|
|
|
15
16
|
Usage:
|
|
16
|
-
carte
|
|
17
|
+
carte start, then show an arrow-key menu (no GUI needed)
|
|
18
|
+
carte --tray start with a system-tray icon instead
|
|
17
19
|
cartethyia setup [--non-interactive]
|
|
18
20
|
cartethyia start
|
|
19
|
-
cartethyia
|
|
21
|
+
cartethyia menu
|
|
22
|
+
cartethyia stop stop a gateway left running in the background
|
|
20
23
|
cartethyia update
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
carte starts the gateway and opens a menu driven by the arrow keys and Enter:
|
|
26
|
+
open the console, open settings, hide to the background, check for updates, or
|
|
27
|
+
exit. It needs no desktop environment, so it works over SSH and headless.
|
|
28
|
+
carte --tray uses a system-tray icon instead, which needs a graphical session.
|
|
23
29
|
Setup explains Lite (embedded SQLite) and Full (external PostgreSQL), then saves
|
|
24
30
|
configuration under the current user's Cartethyia config directory.
|
|
25
31
|
`;
|
|
@@ -42,6 +48,54 @@ async function loadUserConfig(): Promise<void> {
|
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Runtime state for a gateway left running by "Hide to background".
|
|
53
|
+
*
|
|
54
|
+
* Kept out of `.env` on purpose: the drain token authorises a shutdown, so it
|
|
55
|
+
* belongs in a machine-written file the operator never hand-edits and never
|
|
56
|
+
* commits. It is written when the menu detaches and removed when the gateway
|
|
57
|
+
* is drained, so a stale file never outlives the process it refers to.
|
|
58
|
+
*/
|
|
59
|
+
const RUNTIME_STATE_FILE = "runtime.json";
|
|
60
|
+
|
|
61
|
+
function writePersistedDrainToken(token: string): void {
|
|
62
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
63
|
+
if (!configDir) return;
|
|
64
|
+
try {
|
|
65
|
+
writeFileSync(join(configDir, RUNTIME_STATE_FILE), JSON.stringify({ drainToken: token }), {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
mode: 0o600,
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
// A read-only config directory costs only the `stop` command, not startup.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readPersistedDrainToken(): string | null {
|
|
75
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
76
|
+
if (!configDir) return null;
|
|
77
|
+
const statePath = join(configDir, RUNTIME_STATE_FILE);
|
|
78
|
+
if (!existsSync(statePath)) return null;
|
|
79
|
+
try {
|
|
80
|
+
const parsed: unknown = JSON.parse(readFileSync(statePath, "utf8"));
|
|
81
|
+
if (isRecord(parsed) && typeof parsed["drainToken"] === "string") return parsed["drainToken"];
|
|
82
|
+
} catch {
|
|
83
|
+
// A corrupt or stale state file reads as "no background gateway".
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearPersistedDrainToken(): void {
|
|
89
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
90
|
+
if (!configDir) return;
|
|
91
|
+
try {
|
|
92
|
+
unlinkSync(join(configDir, RUNTIME_STATE_FILE));
|
|
93
|
+
} catch {
|
|
94
|
+
// Best effort: the drain request already succeeded.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
45
99
|
type TrayStartupSignal =
|
|
46
100
|
| { readonly status: "ready"; readonly url: string }
|
|
47
101
|
| { readonly status: "failed"; readonly message: string };
|
|
@@ -125,6 +179,60 @@ function openUrl(url: string): Promise<void> {
|
|
|
125
179
|
return child.exited.then(() => undefined);
|
|
126
180
|
}
|
|
127
181
|
|
|
182
|
+
/**
|
|
183
|
+
* A single-select menu driven by the arrow keys and Enter, built on stdin raw
|
|
184
|
+
* mode so no menu dependency is added. It only reads keys and returns an
|
|
185
|
+
* index; the caller keeps owning the gateway process.
|
|
186
|
+
*
|
|
187
|
+
* Returns -1 when stdin is not a TTY (piped, CI, or a non-interactive run) so
|
|
188
|
+
* callers can fall back without blocking on input that will never arrive.
|
|
189
|
+
*/
|
|
190
|
+
function chooseFromMenu(title: string, options: readonly string[]): Promise<number> {
|
|
191
|
+
const stdin = process.stdin;
|
|
192
|
+
if (!stdin.isTTY) return Promise.resolve(-1);
|
|
193
|
+
const { promise, resolve } = Promise.withResolvers<number>();
|
|
194
|
+
let index = 0;
|
|
195
|
+
|
|
196
|
+
const render = (): void => {
|
|
197
|
+
process.stdout.write(`\u001b[2J\u001b[H${title}\n\n`);
|
|
198
|
+
options.forEach((option, position) => {
|
|
199
|
+
const marker = position === index ? "\u2192" : " ";
|
|
200
|
+
process.stdout.write(` ${marker} ${option}\n`);
|
|
201
|
+
});
|
|
202
|
+
process.stdout.write("\n\u2191\u2193 move \u00b7 Enter choose \u00b7 Esc/q exit\n");
|
|
203
|
+
};
|
|
204
|
+
const cleanup = (choice: number): void => {
|
|
205
|
+
stdin.setRawMode(false);
|
|
206
|
+
stdin.off("keypress", onKey);
|
|
207
|
+
stdin.pause();
|
|
208
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
209
|
+
resolve(choice);
|
|
210
|
+
};
|
|
211
|
+
const onKey = (_chunk: unknown, key: { name?: string }): void => {
|
|
212
|
+
if (key.name === "up") {
|
|
213
|
+
index = (index - 1 + options.length) % options.length;
|
|
214
|
+
render();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (key.name === "down") {
|
|
218
|
+
index = (index + 1) % options.length;
|
|
219
|
+
render();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (key.name === "return") {
|
|
223
|
+
cleanup(index);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (key.name === "escape" || key.name === "q") cleanup(-1);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
stdin.setRawMode(true);
|
|
230
|
+
stdin.resume();
|
|
231
|
+
render();
|
|
232
|
+
stdin.on("keypress", onKey);
|
|
233
|
+
return promise;
|
|
234
|
+
}
|
|
235
|
+
|
|
128
236
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
129
237
|
return value !== null && typeof value === "object";
|
|
130
238
|
}
|
|
@@ -289,8 +397,112 @@ async function stopTray(tray: SysTray, ready: boolean, exited: boolean): Promise
|
|
|
289
397
|
if (!tray.killed) tray.process.kill();
|
|
290
398
|
}
|
|
291
399
|
|
|
400
|
+
/**
|
|
401
|
+
* The default `carte` experience: start the gateway, then offer an arrow-key
|
|
402
|
+
* menu. This needs no desktop environment, so it works over SSH, on a headless
|
|
403
|
+
* box, and on Windows where the tray icon lands in the hidden-icons overflow
|
|
404
|
+
* and is never seen. `carte --tray` opts into the icon instead.
|
|
405
|
+
*/
|
|
406
|
+
async function startMenu(): Promise<void> {
|
|
407
|
+
const port = resolvePort();
|
|
408
|
+
const drainToken = randomBytes(32).toString("hex");
|
|
409
|
+
const dashboardUrl = `http://127.0.0.1:${port}/console`;
|
|
410
|
+
const settingsUrl = `${dashboardUrl}/settings`;
|
|
411
|
+
const currentVersion = await readInstalledVersion();
|
|
412
|
+
|
|
413
|
+
const gateway = await startGateway(port, drainToken);
|
|
414
|
+
let stopping = false;
|
|
415
|
+
const shutDown = async (): Promise<void> => {
|
|
416
|
+
if (stopping) return;
|
|
417
|
+
stopping = true;
|
|
418
|
+
await stopGateway(gateway, port, drainToken);
|
|
419
|
+
};
|
|
420
|
+
process.once("SIGINT", () => void shutDown());
|
|
421
|
+
process.once("SIGTERM", () => void shutDown());
|
|
422
|
+
|
|
423
|
+
const latest = await readLatestPublishedVersion();
|
|
424
|
+
const updateLabel =
|
|
425
|
+
latest !== null && isNewerRelease(currentVersion, latest)
|
|
426
|
+
? `Update to ${latest}`
|
|
427
|
+
: "Check for updates";
|
|
428
|
+
|
|
429
|
+
const options = [
|
|
430
|
+
`Open Console \u00b7 ${dashboardUrl}`,
|
|
431
|
+
"Settings \u00b7 backup & restore, reset password",
|
|
432
|
+
"Hide to background and keep running",
|
|
433
|
+
updateLabel,
|
|
434
|
+
"Exit",
|
|
435
|
+
] as const;
|
|
436
|
+
|
|
437
|
+
console.log(`Cartethyia ${currentVersion} is running at ${dashboardUrl}`);
|
|
438
|
+
for (;;) {
|
|
439
|
+
const choice = await chooseFromMenu(
|
|
440
|
+
`Cartethyia ${currentVersion} \u2014 running at ${dashboardUrl}`,
|
|
441
|
+
options,
|
|
442
|
+
);
|
|
443
|
+
if (choice === -1 || choice === 4) break;
|
|
444
|
+
if (choice === 0) {
|
|
445
|
+
await openUrl(dashboardUrl);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (choice === 1) {
|
|
449
|
+
await openUrl(settingsUrl);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (choice === 2) {
|
|
453
|
+
// The gateway is a detached child, so returning here leaves it running
|
|
454
|
+
// while this process lets go of the terminal. Persist the drain token
|
|
455
|
+
// now, because `cartethyia stop` runs in a later shell and has no other
|
|
456
|
+
// way to reach this process.
|
|
457
|
+
writePersistedDrainToken(drainToken);
|
|
458
|
+
console.log(`Cartethyia keeps running at ${dashboardUrl}.`);
|
|
459
|
+
console.log("Stop it later with: cartethyia stop");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (choice === 3) {
|
|
463
|
+
const code = await npmUpdate();
|
|
464
|
+
console.log(code === 0 ? "Update finished." : "Update failed.");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
clearPersistedDrainToken();
|
|
468
|
+
await shutDown();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Stops a gateway left running by "Hide to background". That process is not a
|
|
473
|
+
* child of this one, so it cannot be signalled directly — the only handle is
|
|
474
|
+
* the loopback drain endpoint, which needs the drain token the start wrote.
|
|
475
|
+
*/
|
|
476
|
+
async function stopRunningGateway(): Promise<number> {
|
|
477
|
+
const port = resolvePort();
|
|
478
|
+
const token = readPersistedDrainToken();
|
|
479
|
+
if (token === null) {
|
|
480
|
+
console.error(
|
|
481
|
+
"No background Cartethyia gateway was found for this install. Start one with `carte`, or stop a foreground run with Ctrl-C.",
|
|
482
|
+
);
|
|
483
|
+
return 1;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const response = await fetch(`http://127.0.0.1:${port}/admin/drain`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers: { "x-drain-token": token },
|
|
489
|
+
signal: AbortSignal.timeout(3_000),
|
|
490
|
+
});
|
|
491
|
+
if (response.status !== 202) {
|
|
492
|
+
console.error(`The gateway refused the drain request (HTTP ${response.status}).`);
|
|
493
|
+
return 1;
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
console.error("No gateway is listening on the configured port. It may already be stopped.");
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
clearPersistedDrainToken();
|
|
500
|
+
console.log("Cartethyia is draining and will stop once in-flight requests finish.");
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
292
505
|
async function startTray(): Promise<void> {
|
|
293
|
-
const { resolvePort } = await import("../../src/config/runtime");
|
|
294
506
|
const port = resolvePort();
|
|
295
507
|
const drainToken = randomBytes(32).toString("hex");
|
|
296
508
|
const currentVersion = await readInstalledVersion();
|
|
@@ -535,7 +747,7 @@ async function startTray(): Promise<void> {
|
|
|
535
747
|
async function main(): Promise<void> {
|
|
536
748
|
assertBunVersion();
|
|
537
749
|
const [command] = process.argv.slice(2);
|
|
538
|
-
if (command === "setup" || command === "start" || command === "tray") {
|
|
750
|
+
if (command === "setup" || command === "start" || command === "tray" || command === "menu" || command === "stop") {
|
|
539
751
|
await loadUserConfig();
|
|
540
752
|
}
|
|
541
753
|
|
|
@@ -548,9 +760,15 @@ async function main(): Promise<void> {
|
|
|
548
760
|
case "start":
|
|
549
761
|
await import("../../src/main");
|
|
550
762
|
return;
|
|
763
|
+
case "menu":
|
|
764
|
+
await startMenu();
|
|
765
|
+
return;
|
|
551
766
|
case "tray":
|
|
552
767
|
await startTray();
|
|
553
768
|
return;
|
|
769
|
+
case "stop":
|
|
770
|
+
process.exitCode = await stopRunningGateway();
|
|
771
|
+
return;
|
|
554
772
|
case "update": {
|
|
555
773
|
const code = await npmUpdate();
|
|
556
774
|
if (code !== 0) process.exitCode = code;
|