cartethyia 2.3.3 → 2.3.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-B8MQBL1Y.js → BannedPage-Dc8V-nxc.js} +1 -1
- package/dist/dashboard/assets/{CliToolDetailPage-DAAUVo7w.js → CliToolDetailPage-ifAh7wbK.js} +1 -1
- package/dist/dashboard/assets/{CliToolsPage--hLkg7LU.js → CliToolsPage-2A1c_dFb.js} +1 -1
- package/dist/dashboard/assets/{ConsoleLogPage-CRaToMW5.js → ConsoleLogPage-FfxY8_yn.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-CYiaepa1.js → CustomizationPage-CZ5H40Ha.js} +1 -1
- package/dist/dashboard/assets/{HealthEventsModal-CjsAm6Gm.js → HealthEventsModal-CZKQak_s.js} +1 -1
- package/dist/dashboard/assets/{LoginPage-DVL5oW3v.js → LoginPage-sqFYY4uv.js} +1 -1
- package/dist/dashboard/assets/{OverviewPage-DJ8WM59t.js → OverviewPage-BOZJaauG.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-CK0q-HYZ.js → ProviderDetailPage-CNUCsNFg.js} +1 -1
- package/dist/dashboard/assets/{ProvidersPage-BdVj-Vab.js → ProvidersPage-CsXoemJ1.js} +1 -1
- package/dist/dashboard/assets/{ProxyPage-CTohaici.js → ProxyPage-DvRt3aZP.js} +1 -1
- package/dist/dashboard/assets/{QuotaPage-CkHjJoim.js → QuotaPage-DNYJWw7s.js} +1 -1
- package/dist/dashboard/assets/{SetupPage-CRahC2bj.js → SetupPage-Cz1DWkcr.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CxDqECuN.js → StudioPage-CsKD9sy-.js} +1 -1
- package/dist/dashboard/assets/{UsagePage-TzfUYdQD.js → UsagePage-_Zu6460J.js} +1 -1
- package/dist/dashboard/assets/{entry-KOZCZ9VG.js → entry-COuKgXuV.js} +3 -3
- package/dist/dashboard/assets/{index-Bj96PK8B.js → index-CF94-Gob.js} +2 -2
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli/cli.ts +12 -149
- package/scripts/cli/menu.ts +53 -0
- package/scripts/cli/runtime-state.ts +62 -0
- package/scripts/cli/version.ts +66 -0
package/scripts/cli/cli.ts
CHANGED
|
@@ -8,6 +8,13 @@ import { isNewerRelease } from "../../src/console/updates/version-compare";
|
|
|
8
8
|
import { PACKAGE_ROOT, resolvePackagePath, resolveUserConfigDir } from "../../src/config/package-paths";
|
|
9
9
|
import { resolvePort } from "../../src/config/runtime";
|
|
10
10
|
import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../shared/env";
|
|
11
|
+
import { chooseFromMenu } from "./menu";
|
|
12
|
+
import {
|
|
13
|
+
clearPersistedDrainToken,
|
|
14
|
+
readPersistedDrainToken,
|
|
15
|
+
writePersistedDrainToken,
|
|
16
|
+
} from "./runtime-state";
|
|
17
|
+
import { npmUpdate, readInstalledVersion, readLatestPublishedVersion } from "./version";
|
|
11
18
|
|
|
12
19
|
process.env.CARTETHYIA_CONFIG_DIR ??= resolveUserConfigDir();
|
|
13
20
|
|
|
@@ -48,53 +55,6 @@ async function loadUserConfig(): Promise<void> {
|
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
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
58
|
|
|
99
59
|
type TrayStartupSignal =
|
|
100
60
|
| { readonly status: "ready"; readonly url: string }
|
|
@@ -141,29 +101,6 @@ async function waitForTrayStartupSignal(
|
|
|
141
101
|
throw new Error("Cartethyia did not finish tray and gateway startup within 45 seconds");
|
|
142
102
|
}
|
|
143
103
|
|
|
144
|
-
function npmUpdate(): Promise<number> {
|
|
145
|
-
return new Promise((resolveExit) => {
|
|
146
|
-
const command = process.platform === "win32"
|
|
147
|
-
? "cmd.exe"
|
|
148
|
-
: "npm";
|
|
149
|
-
const args = process.platform === "win32"
|
|
150
|
-
? ["/d", "/s", "/c", "npm install --global cartethyia@latest"]
|
|
151
|
-
: ["install", "--global", "cartethyia@latest"];
|
|
152
|
-
let child;
|
|
153
|
-
try {
|
|
154
|
-
child = spawn(command, args, { cwd: tmpdir(), stdio: "inherit" });
|
|
155
|
-
} catch {
|
|
156
|
-
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
157
|
-
resolveExit(1);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
child.once("error", () => {
|
|
161
|
-
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
162
|
-
resolveExit(1);
|
|
163
|
-
});
|
|
164
|
-
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
104
|
|
|
168
105
|
function openUrl(url: string): Promise<void> {
|
|
169
106
|
const command = process.platform === "win32"
|
|
@@ -179,84 +116,6 @@ function openUrl(url: string): Promise<void> {
|
|
|
179
116
|
return child.exited.then(() => undefined);
|
|
180
117
|
}
|
|
181
118
|
|
|
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
|
-
|
|
236
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
237
|
-
return value !== null && typeof value === "object";
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async function readInstalledVersion(): Promise<string> {
|
|
241
|
-
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
242
|
-
if (!isRecord(metadata) || typeof metadata["version"] !== "string") {
|
|
243
|
-
throw new Error("Cartethyia package version is unavailable");
|
|
244
|
-
}
|
|
245
|
-
return metadata["version"];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function readLatestPublishedVersion(): Promise<string | null> {
|
|
249
|
-
try {
|
|
250
|
-
const response = await fetch("https://registry.npmjs.org/cartethyia/latest", {
|
|
251
|
-
signal: AbortSignal.timeout(4_000),
|
|
252
|
-
});
|
|
253
|
-
if (!response.ok) return null;
|
|
254
|
-
const metadata: unknown = await response.json();
|
|
255
|
-
return isRecord(metadata) && typeof metadata["version"] === "string" ? metadata["version"] : null;
|
|
256
|
-
} catch {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
119
|
|
|
261
120
|
function launchUpdatedTray(signalPath: string): ChildProcess {
|
|
262
121
|
const env = { ...process.env, CARTETHYIA_TRAY_STARTUP_SIGNAL: signalPath };
|
|
@@ -275,7 +134,11 @@ async function startGateway(port: number, drainToken: string): Promise<ReturnTyp
|
|
|
275
134
|
{
|
|
276
135
|
cwd: PACKAGE_ROOT,
|
|
277
136
|
env: { ...process.env, CARTETHYIA_DRAIN_TOKEN: drainToken },
|
|
278
|
-
|
|
137
|
+
// "ignore", not "inherit": the gateway does not read stdin, and in menu
|
|
138
|
+
// mode the menu is the only stdin consumer. With "inherit" the gateway
|
|
139
|
+
// held the fd and swallowed every keypress after the first, so the menu
|
|
140
|
+
// rendered but never received Enter.
|
|
141
|
+
stdin: "ignore",
|
|
279
142
|
stdout: "inherit",
|
|
280
143
|
stderr: "inherit",
|
|
281
144
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single-select menu driven by the arrow keys and Enter, built on stdin raw
|
|
3
|
+
* mode so no menu dependency is added. It only reads keys and returns an
|
|
4
|
+
* index; the caller keeps owning the gateway process.
|
|
5
|
+
*
|
|
6
|
+
* Returns -1 when stdin is not a TTY (piped, CI, or a non-interactive run) so
|
|
7
|
+
* callers can fall back without blocking on input that will never arrive.
|
|
8
|
+
*/
|
|
9
|
+
export function chooseFromMenu(title: string, options: readonly string[]): Promise<number> {
|
|
10
|
+
const stdin = process.stdin;
|
|
11
|
+
if (!stdin.isTTY) return Promise.resolve(-1);
|
|
12
|
+
const { promise, resolve } = Promise.withResolvers<number>();
|
|
13
|
+
let index = 0;
|
|
14
|
+
|
|
15
|
+
const render = (): void => {
|
|
16
|
+
process.stdout.write(`\u001b[2J\u001b[H${title}\n\n`);
|
|
17
|
+
options.forEach((option, position) => {
|
|
18
|
+
const marker = position === index ? "\u2192" : " ";
|
|
19
|
+
process.stdout.write(` ${marker} ${option}\n`);
|
|
20
|
+
});
|
|
21
|
+
process.stdout.write("\n\u2191\u2193 move \u00b7 Enter choose \u00b7 Esc/q exit\n");
|
|
22
|
+
};
|
|
23
|
+
const cleanup = (choice: number): void => {
|
|
24
|
+
stdin.setRawMode(false);
|
|
25
|
+
stdin.off("keypress", onKey);
|
|
26
|
+
stdin.pause();
|
|
27
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
28
|
+
resolve(choice);
|
|
29
|
+
};
|
|
30
|
+
const onKey = (_chunk: unknown, key: { name?: string }): void => {
|
|
31
|
+
if (key.name === "up") {
|
|
32
|
+
index = (index - 1 + options.length) % options.length;
|
|
33
|
+
render();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (key.name === "down") {
|
|
37
|
+
index = (index + 1) % options.length;
|
|
38
|
+
render();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (key.name === "return") {
|
|
42
|
+
cleanup(index);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (key.name === "escape" || key.name === "q") cleanup(-1);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
stdin.setRawMode(true);
|
|
49
|
+
stdin.resume();
|
|
50
|
+
render();
|
|
51
|
+
stdin.on("keypress", onKey);
|
|
52
|
+
return promise;
|
|
53
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime state for a gateway left running by "Hide to background".
|
|
6
|
+
*
|
|
7
|
+
* Kept out of `.env` on purpose: the drain token authorises a shutdown, so it
|
|
8
|
+
* belongs in a machine-written file the operator never hand-edits and never
|
|
9
|
+
* commits. It is written when the menu detaches and removed when the gateway
|
|
10
|
+
* is drained, so a stale file never outlives the process it refers to.
|
|
11
|
+
*/
|
|
12
|
+
const RUNTIME_STATE_FILE = "runtime.json";
|
|
13
|
+
|
|
14
|
+
function statePath(): string | null {
|
|
15
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
16
|
+
return configDir ? join(configDir, RUNTIME_STATE_FILE) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Persists the drain token so a later `cartethyia stop` can reach the gateway. */
|
|
20
|
+
export function writePersistedDrainToken(token: string): void {
|
|
21
|
+
const path = statePath();
|
|
22
|
+
if (path === null) return;
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(path, JSON.stringify({ drainToken: token }), {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
mode: 0o600,
|
|
27
|
+
});
|
|
28
|
+
} catch {
|
|
29
|
+
// A read-only config directory costs only the `stop` command, not startup.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Returns the token for a background gateway, or null when there is none. */
|
|
34
|
+
export function readPersistedDrainToken(): string | null {
|
|
35
|
+
const path = statePath();
|
|
36
|
+
if (path === null || !existsSync(path)) return null;
|
|
37
|
+
try {
|
|
38
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
+
if (
|
|
40
|
+
parsed !== null &&
|
|
41
|
+
typeof parsed === "object" &&
|
|
42
|
+
"drainToken" in parsed &&
|
|
43
|
+
typeof parsed.drainToken === "string"
|
|
44
|
+
) {
|
|
45
|
+
return parsed.drainToken;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// A corrupt or stale state file reads as "no background gateway".
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Removes the token once the drain has been accepted. */
|
|
54
|
+
export function clearPersistedDrainToken(): void {
|
|
55
|
+
const path = statePath();
|
|
56
|
+
if (path === null) return;
|
|
57
|
+
try {
|
|
58
|
+
unlinkSync(path);
|
|
59
|
+
} catch {
|
|
60
|
+
// Best effort: the drain request already succeeded.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { resolvePackagePath } from "../../src/config/package-paths";
|
|
4
|
+
|
|
5
|
+
/** Reads the version from the installed package manifest. */
|
|
6
|
+
export async function readInstalledVersion(): Promise<string> {
|
|
7
|
+
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
8
|
+
if (
|
|
9
|
+
metadata === null ||
|
|
10
|
+
typeof metadata !== "object" ||
|
|
11
|
+
!("version" in metadata) ||
|
|
12
|
+
typeof metadata.version !== "string"
|
|
13
|
+
) {
|
|
14
|
+
throw new Error("Cartethyia package version is unavailable");
|
|
15
|
+
}
|
|
16
|
+
return metadata.version;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Asks the npm registry for the newest published version, or null on failure. */
|
|
20
|
+
export async function readLatestPublishedVersion(): Promise<string | null> {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch("https://registry.npmjs.org/cartethyia/latest", {
|
|
23
|
+
signal: AbortSignal.timeout(4_000),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) return null;
|
|
26
|
+
const metadata: unknown = await response.json();
|
|
27
|
+
if (
|
|
28
|
+
metadata !== null &&
|
|
29
|
+
typeof metadata === "object" &&
|
|
30
|
+
"version" in metadata &&
|
|
31
|
+
typeof metadata.version === "string"
|
|
32
|
+
) {
|
|
33
|
+
return metadata.version;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs `npm install --global cartethyia@latest`. On Windows npm is a shell
|
|
43
|
+
* script, so it has to go through cmd.exe rather than being spawned directly.
|
|
44
|
+
*/
|
|
45
|
+
export function npmUpdate(): Promise<number> {
|
|
46
|
+
return new Promise((resolveExit) => {
|
|
47
|
+
const command = process.platform === "win32" ? "cmd.exe" : "npm";
|
|
48
|
+
const args =
|
|
49
|
+
process.platform === "win32"
|
|
50
|
+
? ["/d", "/s", "/c", "npm install --global cartethyia@latest"]
|
|
51
|
+
: ["install", "--global", "cartethyia@latest"];
|
|
52
|
+
let child;
|
|
53
|
+
try {
|
|
54
|
+
child = spawn(command, args, { cwd: tmpdir(), stdio: "inherit" });
|
|
55
|
+
} catch {
|
|
56
|
+
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
57
|
+
resolveExit(1);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
child.once("error", () => {
|
|
61
|
+
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
62
|
+
resolveExit(1);
|
|
63
|
+
});
|
|
64
|
+
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
65
|
+
});
|
|
66
|
+
}
|