cartethyia 2.3.0 → 2.3.2

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 (23) hide show
  1. package/README.md +25 -0
  2. package/dist/dashboard/assets/{BannedPage-ClPA4nQv.js → BannedPage-BprrKF3t.js} +1 -1
  3. package/dist/dashboard/assets/{CliToolDetailPage-Bevb0yY1.js → CliToolDetailPage-CeeIxvCm.js} +1 -1
  4. package/dist/dashboard/assets/{CliToolsPage-DsryPzyc.js → CliToolsPage-BQjMUpJJ.js} +1 -1
  5. package/dist/dashboard/assets/{ConsoleLogPage-CTzqdm6k.js → ConsoleLogPage-CAzWm-tU.js} +1 -1
  6. package/dist/dashboard/assets/{CustomizationPage-CMxyDZej.js → CustomizationPage-B3SKxD3y.js} +1 -1
  7. package/dist/dashboard/assets/{HealthEventsModal-CxkQ4JaG.js → HealthEventsModal-DQNKf-5H.js} +1 -1
  8. package/dist/dashboard/assets/{LoginPage-CL2GR7wU.js → LoginPage-OXNbMIZC.js} +1 -1
  9. package/dist/dashboard/assets/{OverviewPage-b0Ggk2qb.js → OverviewPage-UKI1t9nU.js} +1 -1
  10. package/dist/dashboard/assets/{ProviderDetailPage-Y1Q4UPbO.js → ProviderDetailPage-Bh_MZ03h.js} +1 -1
  11. package/dist/dashboard/assets/{ProvidersPage-C-GI5Qrf.js → ProvidersPage-sP8nDPZN.js} +1 -1
  12. package/dist/dashboard/assets/{ProxyPage-d7vbm79h.js → ProxyPage-Ci1Ps9K3.js} +1 -1
  13. package/dist/dashboard/assets/{QuotaPage-BmLGXSiw.js → QuotaPage-A1jKKb0S.js} +1 -1
  14. package/dist/dashboard/assets/{SetupPage-BZ2NsM8f.js → SetupPage-Ce4ldWt7.js} +1 -1
  15. package/dist/dashboard/assets/{StudioPage-txsvSOwb.js → StudioPage-CFDyo2dT.js} +1 -1
  16. package/dist/dashboard/assets/{UsagePage-D8C_HZBY.js → UsagePage-BLCXYWis.js} +1 -1
  17. package/dist/dashboard/assets/{entry-DcX5sMzh.js → entry-B2Cv6U9r.js} +3 -3
  18. package/dist/dashboard/assets/{index-By44uTGZ.js → index-C7EcMumQ.js} +2 -2
  19. package/dist/dashboard/index.html +1 -1
  20. package/package.json +2 -2
  21. package/scripts/cli/carte-launcher.cjs +5 -1
  22. package/scripts/cli/cli-launcher.cjs +32 -11
  23. package/scripts/cli/cli.ts +132 -4
@@ -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
- const child = spawn("bun", ["run", cliPath, ...process.argv.slice(2)], {
25
- cwd: packageRoot,
26
- detached: trayMode,
27
- stdio: trayMode ? "ignore" : "inherit",
28
- windowsHide: trayMode,
29
- env: {
30
- ...process.env,
31
- CARTETHYIA_CONFIG_DIR: configDir,
32
- ...(trayMode ? { CARTETHYIA_TRAY_STARTUP_SIGNAL: startupSignalPath } : {}),
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;
@@ -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
21
+ cartethyia menu
19
22
  cartethyia tray
20
23
  cartethyia update
21
24
 
22
- The short carte command starts Cartethyia in the system tray.
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
  `;
@@ -125,6 +131,60 @@ function openUrl(url: string): Promise<void> {
125
131
  return child.exited.then(() => undefined);
126
132
  }
127
133
 
134
+ /**
135
+ * A single-select menu driven by the arrow keys and Enter, built on stdin raw
136
+ * mode so no menu dependency is added. It only reads keys and returns an
137
+ * index; the caller keeps owning the gateway process.
138
+ *
139
+ * Returns -1 when stdin is not a TTY (piped, CI, or a non-interactive run) so
140
+ * callers can fall back without blocking on input that will never arrive.
141
+ */
142
+ function chooseFromMenu(title: string, options: readonly string[]): Promise<number> {
143
+ const stdin = process.stdin;
144
+ if (!stdin.isTTY) return Promise.resolve(-1);
145
+ const { promise, resolve } = Promise.withResolvers<number>();
146
+ let index = 0;
147
+
148
+ const render = (): void => {
149
+ process.stdout.write(`\u001b[2J\u001b[H${title}\n\n`);
150
+ options.forEach((option, position) => {
151
+ const marker = position === index ? "\u2192" : " ";
152
+ process.stdout.write(` ${marker} ${option}\n`);
153
+ });
154
+ process.stdout.write("\n\u2191\u2193 move \u00b7 Enter choose \u00b7 Esc/q exit\n");
155
+ };
156
+ const cleanup = (choice: number): void => {
157
+ stdin.setRawMode(false);
158
+ stdin.off("keypress", onKey);
159
+ stdin.pause();
160
+ process.stdout.write("\u001b[2J\u001b[H");
161
+ resolve(choice);
162
+ };
163
+ const onKey = (_chunk: unknown, key: { name?: string }): void => {
164
+ if (key.name === "up") {
165
+ index = (index - 1 + options.length) % options.length;
166
+ render();
167
+ return;
168
+ }
169
+ if (key.name === "down") {
170
+ index = (index + 1) % options.length;
171
+ render();
172
+ return;
173
+ }
174
+ if (key.name === "return") {
175
+ cleanup(index);
176
+ return;
177
+ }
178
+ if (key.name === "escape" || key.name === "q") cleanup(-1);
179
+ };
180
+
181
+ stdin.setRawMode(true);
182
+ stdin.resume();
183
+ render();
184
+ stdin.on("keypress", onKey);
185
+ return promise;
186
+ }
187
+
128
188
  function isRecord(value: unknown): value is Record<string, unknown> {
129
189
  return value !== null && typeof value === "object";
130
190
  }
@@ -289,8 +349,73 @@ async function stopTray(tray: SysTray, ready: boolean, exited: boolean): Promise
289
349
  if (!tray.killed) tray.process.kill();
290
350
  }
291
351
 
352
+ /**
353
+ * The default `carte` experience: start the gateway, then offer an arrow-key
354
+ * menu. This needs no desktop environment, so it works over SSH, on a headless
355
+ * box, and on Windows where the tray icon lands in the hidden-icons overflow
356
+ * and is never seen. `carte --tray` opts into the icon instead.
357
+ */
358
+ async function startMenu(): Promise<void> {
359
+ const port = resolvePort();
360
+ const drainToken = randomBytes(32).toString("hex");
361
+ const dashboardUrl = `http://127.0.0.1:${port}/console`;
362
+ const settingsUrl = `${dashboardUrl}/settings`;
363
+ const currentVersion = await readInstalledVersion();
364
+
365
+ const gateway = await startGateway(port, drainToken);
366
+ let stopping = false;
367
+ const shutDown = async (): Promise<void> => {
368
+ if (stopping) return;
369
+ stopping = true;
370
+ await stopGateway(gateway, port, drainToken);
371
+ };
372
+ process.once("SIGINT", () => void shutDown());
373
+ process.once("SIGTERM", () => void shutDown());
374
+
375
+ const latest = await readLatestPublishedVersion();
376
+ const updateLabel =
377
+ latest !== null && isNewerRelease(currentVersion, latest)
378
+ ? `Update to ${latest}`
379
+ : "Check for updates";
380
+
381
+ const options = [
382
+ `Open Console \u00b7 ${dashboardUrl}`,
383
+ "Settings \u00b7 backup & restore, reset password",
384
+ "Hide to background and keep running",
385
+ updateLabel,
386
+ "Exit",
387
+ ] as const;
388
+
389
+ console.log(`Cartethyia ${currentVersion} is running at ${dashboardUrl}`);
390
+ for (;;) {
391
+ const choice = await chooseFromMenu(
392
+ `Cartethyia ${currentVersion} \u2014 running at ${dashboardUrl}`,
393
+ options,
394
+ );
395
+ if (choice === -1 || choice === 4) break;
396
+ if (choice === 0) {
397
+ await openUrl(dashboardUrl);
398
+ continue;
399
+ }
400
+ if (choice === 1) {
401
+ await openUrl(settingsUrl);
402
+ continue;
403
+ }
404
+ 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`);
408
+ return;
409
+ }
410
+ if (choice === 3) {
411
+ const code = await npmUpdate();
412
+ console.log(code === 0 ? "Update finished." : "Update failed.");
413
+ }
414
+ }
415
+ await shutDown();
416
+ }
417
+
292
418
  async function startTray(): Promise<void> {
293
- const { resolvePort } = await import("../../src/config/runtime");
294
419
  const port = resolvePort();
295
420
  const drainToken = randomBytes(32).toString("hex");
296
421
  const currentVersion = await readInstalledVersion();
@@ -535,7 +660,7 @@ async function startTray(): Promise<void> {
535
660
  async function main(): Promise<void> {
536
661
  assertBunVersion();
537
662
  const [command] = process.argv.slice(2);
538
- if (command === "setup" || command === "start" || command === "tray") {
663
+ if (command === "setup" || command === "start" || command === "tray" || command === "menu") {
539
664
  await loadUserConfig();
540
665
  }
541
666
 
@@ -548,6 +673,9 @@ async function main(): Promise<void> {
548
673
  case "start":
549
674
  await import("../../src/main");
550
675
  return;
676
+ case "menu":
677
+ await startMenu();
678
+ return;
551
679
  case "tray":
552
680
  await startTray();
553
681
  return;