cartethyia 2.3.2 → 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.
Files changed (20) hide show
  1. package/dist/dashboard/assets/{BannedPage-BprrKF3t.js → BannedPage-B8MQBL1Y.js} +1 -1
  2. package/dist/dashboard/assets/{CliToolDetailPage-CeeIxvCm.js → CliToolDetailPage-DAAUVo7w.js} +1 -1
  3. package/dist/dashboard/assets/{CliToolsPage-BQjMUpJJ.js → CliToolsPage--hLkg7LU.js} +1 -1
  4. package/dist/dashboard/assets/{ConsoleLogPage-CAzWm-tU.js → ConsoleLogPage-CRaToMW5.js} +1 -1
  5. package/dist/dashboard/assets/{CustomizationPage-B3SKxD3y.js → CustomizationPage-CYiaepa1.js} +1 -1
  6. package/dist/dashboard/assets/{HealthEventsModal-DQNKf-5H.js → HealthEventsModal-CjsAm6Gm.js} +1 -1
  7. package/dist/dashboard/assets/{LoginPage-OXNbMIZC.js → LoginPage-DVL5oW3v.js} +1 -1
  8. package/dist/dashboard/assets/{OverviewPage-UKI1t9nU.js → OverviewPage-DJ8WM59t.js} +1 -1
  9. package/dist/dashboard/assets/{ProviderDetailPage-Bh_MZ03h.js → ProviderDetailPage-CK0q-HYZ.js} +1 -1
  10. package/dist/dashboard/assets/{ProvidersPage-sP8nDPZN.js → ProvidersPage-BdVj-Vab.js} +1 -1
  11. package/dist/dashboard/assets/{ProxyPage-Ci1Ps9K3.js → ProxyPage-CTohaici.js} +1 -1
  12. package/dist/dashboard/assets/{QuotaPage-A1jKKb0S.js → QuotaPage-CkHjJoim.js} +1 -1
  13. package/dist/dashboard/assets/{SetupPage-Ce4ldWt7.js → SetupPage-CRahC2bj.js} +1 -1
  14. package/dist/dashboard/assets/{StudioPage-CFDyo2dT.js → StudioPage-CxDqECuN.js} +1 -1
  15. package/dist/dashboard/assets/{UsagePage-BLCXYWis.js → UsagePage-TzfUYdQD.js} +1 -1
  16. package/dist/dashboard/assets/{entry-B2Cv6U9r.js → entry-KOZCZ9VG.js} +3 -3
  17. package/dist/dashboard/assets/{index-C7EcMumQ.js → index-Bj96PK8B.js} +2 -2
  18. package/dist/dashboard/index.html +1 -1
  19. package/package.json +1 -1
  20. package/scripts/cli/cli.ts +95 -5
@@ -19,7 +19,7 @@ Usage:
19
19
  cartethyia setup [--non-interactive]
20
20
  cartethyia start
21
21
  cartethyia menu
22
- cartethyia tray
22
+ cartethyia stop stop a gateway left running in the background
23
23
  cartethyia update
24
24
 
25
25
  carte starts the gateway and opens a menu driven by the arrow keys and Enter:
@@ -48,6 +48,54 @@ async function loadUserConfig(): Promise<void> {
48
48
  }
49
49
  }
50
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
+
51
99
  type TrayStartupSignal =
52
100
  | { readonly status: "ready"; readonly url: string }
53
101
  | { readonly status: "failed"; readonly message: string };
@@ -402,9 +450,13 @@ async function startMenu(): Promise<void> {
402
450
  continue;
403
451
  }
404
452
  if (choice === 2) {
405
- // The gateway is already a detached child, so returning here leaves it
406
- // running while this process lets go of the terminal.
407
- console.log(`Cartethyia keeps running at ${dashboardUrl}. Stop it with: cartethyia stop`);
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");
408
460
  return;
409
461
  }
410
462
  if (choice === 3) {
@@ -412,9 +464,44 @@ async function startMenu(): Promise<void> {
412
464
  console.log(code === 0 ? "Update finished." : "Update failed.");
413
465
  }
414
466
  }
467
+ clearPersistedDrainToken();
415
468
  await shutDown();
416
469
  }
417
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
+
418
505
  async function startTray(): Promise<void> {
419
506
  const port = resolvePort();
420
507
  const drainToken = randomBytes(32).toString("hex");
@@ -660,7 +747,7 @@ async function startTray(): Promise<void> {
660
747
  async function main(): Promise<void> {
661
748
  assertBunVersion();
662
749
  const [command] = process.argv.slice(2);
663
- if (command === "setup" || command === "start" || command === "tray" || command === "menu") {
750
+ if (command === "setup" || command === "start" || command === "tray" || command === "menu" || command === "stop") {
664
751
  await loadUserConfig();
665
752
  }
666
753
 
@@ -679,6 +766,9 @@ async function main(): Promise<void> {
679
766
  case "tray":
680
767
  await startTray();
681
768
  return;
769
+ case "stop":
770
+ process.exitCode = await stopRunningGateway();
771
+ return;
682
772
  case "update": {
683
773
  const code = await npmUpdate();
684
774
  if (code !== 0) process.exitCode = code;