cartethyia 2.3.2 → 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-BprrKF3t.js → BannedPage-Dc8V-nxc.js} +1 -1
- package/dist/dashboard/assets/{CliToolDetailPage-CeeIxvCm.js → CliToolDetailPage-ifAh7wbK.js} +1 -1
- package/dist/dashboard/assets/{CliToolsPage-BQjMUpJJ.js → CliToolsPage-2A1c_dFb.js} +1 -1
- package/dist/dashboard/assets/{ConsoleLogPage-CAzWm-tU.js → ConsoleLogPage-FfxY8_yn.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-B3SKxD3y.js → CustomizationPage-CZ5H40Ha.js} +1 -1
- package/dist/dashboard/assets/{HealthEventsModal-DQNKf-5H.js → HealthEventsModal-CZKQak_s.js} +1 -1
- package/dist/dashboard/assets/{LoginPage-OXNbMIZC.js → LoginPage-sqFYY4uv.js} +1 -1
- package/dist/dashboard/assets/{OverviewPage-UKI1t9nU.js → OverviewPage-BOZJaauG.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-Bh_MZ03h.js → ProviderDetailPage-CNUCsNFg.js} +1 -1
- package/dist/dashboard/assets/{ProvidersPage-sP8nDPZN.js → ProvidersPage-CsXoemJ1.js} +1 -1
- package/dist/dashboard/assets/{ProxyPage-Ci1Ps9K3.js → ProxyPage-DvRt3aZP.js} +1 -1
- package/dist/dashboard/assets/{QuotaPage-A1jKKb0S.js → QuotaPage-DNYJWw7s.js} +1 -1
- package/dist/dashboard/assets/{SetupPage-Ce4ldWt7.js → SetupPage-Cz1DWkcr.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CFDyo2dT.js → StudioPage-CsKD9sy-.js} +1 -1
- package/dist/dashboard/assets/{UsagePage-BLCXYWis.js → UsagePage-_Zu6460J.js} +1 -1
- package/dist/dashboard/assets/{entry-B2Cv6U9r.js → entry-COuKgXuV.js} +3 -3
- package/dist/dashboard/assets/{index-C7EcMumQ.js → index-CF94-Gob.js} +2 -2
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli/cli.ts +60 -107
- 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
|
|
|
@@ -19,7 +26,7 @@ Usage:
|
|
|
19
26
|
cartethyia setup [--non-interactive]
|
|
20
27
|
cartethyia start
|
|
21
28
|
cartethyia menu
|
|
22
|
-
cartethyia
|
|
29
|
+
cartethyia stop stop a gateway left running in the background
|
|
23
30
|
cartethyia update
|
|
24
31
|
|
|
25
32
|
carte starts the gateway and opens a menu driven by the arrow keys and Enter:
|
|
@@ -48,6 +55,7 @@ async function loadUserConfig(): Promise<void> {
|
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
|
|
51
59
|
type TrayStartupSignal =
|
|
52
60
|
| { readonly status: "ready"; readonly url: string }
|
|
53
61
|
| { readonly status: "failed"; readonly message: string };
|
|
@@ -93,29 +101,6 @@ async function waitForTrayStartupSignal(
|
|
|
93
101
|
throw new Error("Cartethyia did not finish tray and gateway startup within 45 seconds");
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
function npmUpdate(): Promise<number> {
|
|
97
|
-
return new Promise((resolveExit) => {
|
|
98
|
-
const command = process.platform === "win32"
|
|
99
|
-
? "cmd.exe"
|
|
100
|
-
: "npm";
|
|
101
|
-
const args = process.platform === "win32"
|
|
102
|
-
? ["/d", "/s", "/c", "npm install --global cartethyia@latest"]
|
|
103
|
-
: ["install", "--global", "cartethyia@latest"];
|
|
104
|
-
let child;
|
|
105
|
-
try {
|
|
106
|
-
child = spawn(command, args, { cwd: tmpdir(), stdio: "inherit" });
|
|
107
|
-
} catch {
|
|
108
|
-
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
109
|
-
resolveExit(1);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
child.once("error", () => {
|
|
113
|
-
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
114
|
-
resolveExit(1);
|
|
115
|
-
});
|
|
116
|
-
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
104
|
|
|
120
105
|
function openUrl(url: string): Promise<void> {
|
|
121
106
|
const command = process.platform === "win32"
|
|
@@ -131,84 +116,6 @@ function openUrl(url: string): Promise<void> {
|
|
|
131
116
|
return child.exited.then(() => undefined);
|
|
132
117
|
}
|
|
133
118
|
|
|
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
|
-
|
|
188
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
189
|
-
return value !== null && typeof value === "object";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function readInstalledVersion(): Promise<string> {
|
|
193
|
-
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
194
|
-
if (!isRecord(metadata) || typeof metadata["version"] !== "string") {
|
|
195
|
-
throw new Error("Cartethyia package version is unavailable");
|
|
196
|
-
}
|
|
197
|
-
return metadata["version"];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function readLatestPublishedVersion(): Promise<string | null> {
|
|
201
|
-
try {
|
|
202
|
-
const response = await fetch("https://registry.npmjs.org/cartethyia/latest", {
|
|
203
|
-
signal: AbortSignal.timeout(4_000),
|
|
204
|
-
});
|
|
205
|
-
if (!response.ok) return null;
|
|
206
|
-
const metadata: unknown = await response.json();
|
|
207
|
-
return isRecord(metadata) && typeof metadata["version"] === "string" ? metadata["version"] : null;
|
|
208
|
-
} catch {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
119
|
|
|
213
120
|
function launchUpdatedTray(signalPath: string): ChildProcess {
|
|
214
121
|
const env = { ...process.env, CARTETHYIA_TRAY_STARTUP_SIGNAL: signalPath };
|
|
@@ -227,7 +134,11 @@ async function startGateway(port: number, drainToken: string): Promise<ReturnTyp
|
|
|
227
134
|
{
|
|
228
135
|
cwd: PACKAGE_ROOT,
|
|
229
136
|
env: { ...process.env, CARTETHYIA_DRAIN_TOKEN: drainToken },
|
|
230
|
-
|
|
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",
|
|
231
142
|
stdout: "inherit",
|
|
232
143
|
stderr: "inherit",
|
|
233
144
|
},
|
|
@@ -402,9 +313,13 @@ async function startMenu(): Promise<void> {
|
|
|
402
313
|
continue;
|
|
403
314
|
}
|
|
404
315
|
if (choice === 2) {
|
|
405
|
-
// The gateway is
|
|
406
|
-
//
|
|
407
|
-
|
|
316
|
+
// The gateway is a detached child, so returning here leaves it running
|
|
317
|
+
// while this process lets go of the terminal. Persist the drain token
|
|
318
|
+
// now, because `cartethyia stop` runs in a later shell and has no other
|
|
319
|
+
// way to reach this process.
|
|
320
|
+
writePersistedDrainToken(drainToken);
|
|
321
|
+
console.log(`Cartethyia keeps running at ${dashboardUrl}.`);
|
|
322
|
+
console.log("Stop it later with: cartethyia stop");
|
|
408
323
|
return;
|
|
409
324
|
}
|
|
410
325
|
if (choice === 3) {
|
|
@@ -412,9 +327,44 @@ async function startMenu(): Promise<void> {
|
|
|
412
327
|
console.log(code === 0 ? "Update finished." : "Update failed.");
|
|
413
328
|
}
|
|
414
329
|
}
|
|
330
|
+
clearPersistedDrainToken();
|
|
415
331
|
await shutDown();
|
|
416
332
|
}
|
|
417
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Stops a gateway left running by "Hide to background". That process is not a
|
|
336
|
+
* child of this one, so it cannot be signalled directly — the only handle is
|
|
337
|
+
* the loopback drain endpoint, which needs the drain token the start wrote.
|
|
338
|
+
*/
|
|
339
|
+
async function stopRunningGateway(): Promise<number> {
|
|
340
|
+
const port = resolvePort();
|
|
341
|
+
const token = readPersistedDrainToken();
|
|
342
|
+
if (token === null) {
|
|
343
|
+
console.error(
|
|
344
|
+
"No background Cartethyia gateway was found for this install. Start one with `carte`, or stop a foreground run with Ctrl-C.",
|
|
345
|
+
);
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const response = await fetch(`http://127.0.0.1:${port}/admin/drain`, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "x-drain-token": token },
|
|
352
|
+
signal: AbortSignal.timeout(3_000),
|
|
353
|
+
});
|
|
354
|
+
if (response.status !== 202) {
|
|
355
|
+
console.error(`The gateway refused the drain request (HTTP ${response.status}).`);
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
console.error("No gateway is listening on the configured port. It may already be stopped.");
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
clearPersistedDrainToken();
|
|
363
|
+
console.log("Cartethyia is draining and will stop once in-flight requests finish.");
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
418
368
|
async function startTray(): Promise<void> {
|
|
419
369
|
const port = resolvePort();
|
|
420
370
|
const drainToken = randomBytes(32).toString("hex");
|
|
@@ -660,7 +610,7 @@ async function startTray(): Promise<void> {
|
|
|
660
610
|
async function main(): Promise<void> {
|
|
661
611
|
assertBunVersion();
|
|
662
612
|
const [command] = process.argv.slice(2);
|
|
663
|
-
if (command === "setup" || command === "start" || command === "tray" || command === "menu") {
|
|
613
|
+
if (command === "setup" || command === "start" || command === "tray" || command === "menu" || command === "stop") {
|
|
664
614
|
await loadUserConfig();
|
|
665
615
|
}
|
|
666
616
|
|
|
@@ -679,6 +629,9 @@ async function main(): Promise<void> {
|
|
|
679
629
|
case "tray":
|
|
680
630
|
await startTray();
|
|
681
631
|
return;
|
|
632
|
+
case "stop":
|
|
633
|
+
process.exitCode = await stopRunningGateway();
|
|
634
|
+
return;
|
|
682
635
|
case "update": {
|
|
683
636
|
const code = await npmUpdate();
|
|
684
637
|
if (code !== 0) process.exitCode = code;
|
|
@@ -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
|
+
}
|