cartethyia 2.3.4 → 2.4.1
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-Dc8V-nxc.js → BannedPage-hGt6sYfC.js} +1 -1
- package/dist/dashboard/assets/{CliToolDetailPage-ifAh7wbK.js → CliToolDetailPage-C-v5GoF-.js} +1 -1
- package/dist/dashboard/assets/{CliToolsPage-2A1c_dFb.js → CliToolsPage-Da2XCM9A.js} +1 -1
- package/dist/dashboard/assets/{ConsoleLogPage-FfxY8_yn.js → ConsoleLogPage-xT-LP8xZ.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-CZ5H40Ha.js → CustomizationPage-D0PQsW3f.js} +1 -1
- package/dist/dashboard/assets/{HealthEventsModal-CZKQak_s.js → HealthEventsModal-DTiQ4ulO.js} +1 -1
- package/dist/dashboard/assets/{LoginPage-sqFYY4uv.js → LoginPage-jMx71m_m.js} +1 -1
- package/dist/dashboard/assets/{OverviewPage-BOZJaauG.js → OverviewPage-BQpanEmj.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-CNUCsNFg.js → ProviderDetailPage-DMh9bfu0.js} +1 -1
- package/dist/dashboard/assets/{ProvidersPage-CsXoemJ1.js → ProvidersPage-Bfg0qSLE.js} +1 -1
- package/dist/dashboard/assets/{ProxyPage-DvRt3aZP.js → ProxyPage-CSSN-6vg.js} +1 -1
- package/dist/dashboard/assets/{QuotaPage-DNYJWw7s.js → QuotaPage-BPksahOs.js} +1 -1
- package/dist/dashboard/assets/{SetupPage-Cz1DWkcr.js → SetupPage-DxKaH8LS.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CsKD9sy-.js → StudioPage-B9WEa99R.js} +1 -1
- package/dist/dashboard/assets/{UsagePage-_Zu6460J.js → UsagePage-BOSJhVq0.js} +1 -1
- package/dist/dashboard/assets/{entry-COuKgXuV.js → entry-C1Dn2nQu.js} +3 -3
- package/dist/dashboard/assets/{index-CF94-Gob.js → index-TZNbg9dv.js} +2 -2
- package/dist/dashboard/index.html +1 -1
- package/package.json +2 -1
- package/scripts/cli/cli-launcher.cjs +12 -1
- package/scripts/cli/cli.ts +948 -657
- package/scripts/cli/menu.ts +107 -53
- package/scripts/cli/runtime-state.ts +48 -32
- package/scripts/cli/setup.ts +448 -443
- package/scripts/cli/wordmark.ts +58 -0
- package/src/boot/app.ts +39 -0
- package/src/console/auth/account-reset.ts +67 -0
- package/src/console/backup/export-endpoint.ts +111 -0
package/scripts/cli/menu.ts
CHANGED
|
@@ -1,53 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
import Enquirer from "enquirer";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One row in a menu: an action the caller can run, or a rule between groups.
|
|
5
|
+
*
|
|
6
|
+
* Rules are rendered by `enquirer`'s own separator role and are never
|
|
7
|
+
* selectable, so the caller's action list stays a plain array indexed by the
|
|
8
|
+
* rows the operator can actually pick.
|
|
9
|
+
*/
|
|
10
|
+
export type MenuRow =
|
|
11
|
+
| { readonly kind: "action"; readonly label: string; readonly hint?: string }
|
|
12
|
+
| { readonly kind: "rule" };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single-select menu driven by the arrow keys and Enter.
|
|
16
|
+
*
|
|
17
|
+
* Built on `enquirer` rather than a hand-rolled raw-mode listener: a raw
|
|
18
|
+
* `process.stdin.on("keypress")` handler never fires unless something first
|
|
19
|
+
* calls `readline.emitKeypressEvents`, and the menu rendered as inert text for
|
|
20
|
+
* exactly that reason. `enquirer` owns the keypress plumbing, including Ctrl-C.
|
|
21
|
+
*
|
|
22
|
+
* `header` is printed once here and never handed to `enquirer`: enquirer
|
|
23
|
+
* redraws its whole frame on every keypress, so a header inside the prompt
|
|
24
|
+
* repeated once per keystroke and stacked the wordmark down the terminal.
|
|
25
|
+
*
|
|
26
|
+
* Returns -1 when stdin is not a TTY (piped, CI, or a non-interactive run) or
|
|
27
|
+
* when the operator cancels, so the caller can drain and exit either way.
|
|
28
|
+
*/
|
|
29
|
+
export async function chooseFromMenu(
|
|
30
|
+
header: string | readonly string[],
|
|
31
|
+
rows: readonly MenuRow[],
|
|
32
|
+
): Promise<number> {
|
|
33
|
+
if (!process.stdin.isTTY) return -1;
|
|
34
|
+
|
|
35
|
+
const headerLines = typeof header === "string" ? [header] : header;
|
|
36
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
37
|
+
for (const line of headerLines) process.stdout.write(`${line}\n`);
|
|
38
|
+
process.stdout.write("\n");
|
|
39
|
+
|
|
40
|
+
// Enquirer indexes the rows it renders, rules included, so the action index
|
|
41
|
+
// has to be recovered from the label rather than from the position.
|
|
42
|
+
const labels = rows.flatMap((row) => (row.kind === "action" ? [row.label] : []));
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const answer = await Enquirer.prompt<{ choice: string }>({
|
|
47
|
+
type: "select",
|
|
48
|
+
name: "choice",
|
|
49
|
+
message: "Select an action",
|
|
50
|
+
choices: rows.map((row) =>
|
|
51
|
+
row.kind === "rule"
|
|
52
|
+
? { name: "rule", message: "", role: "separator" }
|
|
53
|
+
: {
|
|
54
|
+
name: row.label,
|
|
55
|
+
message: row.label,
|
|
56
|
+
...(row.hint === undefined ? {} : { hint: row.hint }),
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
initial: 0,
|
|
60
|
+
});
|
|
61
|
+
return labels.indexOf(answer.choice);
|
|
62
|
+
} catch {
|
|
63
|
+
// Enquirer rejects on Esc and on its own Ctrl-C handling. Return -1 so the
|
|
64
|
+
// caller drains the gateway rather than leaving a detached process behind.
|
|
65
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Asks the operator to type an exact word before a destructive action runs.
|
|
72
|
+
*
|
|
73
|
+
* Enter alone must not be enough: the reset deletes every console credential,
|
|
74
|
+
* so the operator types the word rather than confirming a highlight. Returns
|
|
75
|
+
* false on cancel, on a mismatch, or when stdin is not a TTY.
|
|
76
|
+
*/
|
|
77
|
+
export async function confirmByTyping(word: string, message: string): Promise<boolean> {
|
|
78
|
+
if (!process.stdin.isTTY) return false;
|
|
79
|
+
try {
|
|
80
|
+
const answer = await Enquirer.prompt<{ confirmation: string }>({
|
|
81
|
+
type: "input",
|
|
82
|
+
name: "confirmation",
|
|
83
|
+
message: `${message} (type ${word})`,
|
|
84
|
+
validate: (value: string) =>
|
|
85
|
+
value.trim() === word ? true : `type ${word} exactly to confirm, or Ctrl-C to cancel`,
|
|
86
|
+
});
|
|
87
|
+
return answer.confirmation.trim() === word;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A yes/no gate for the step before the typed confirmation. */
|
|
94
|
+
export async function confirmStep(message: string): Promise<boolean> {
|
|
95
|
+
if (!process.stdin.isTTY) return false;
|
|
96
|
+
try {
|
|
97
|
+
const answer = await Enquirer.prompt<{ proceed: boolean }>({
|
|
98
|
+
type: "confirm",
|
|
99
|
+
name: "proceed",
|
|
100
|
+
message,
|
|
101
|
+
initial: false,
|
|
102
|
+
});
|
|
103
|
+
return answer.proceed;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,37 +1,31 @@
|
|
|
1
|
-
import { existsSync, readFileSync,
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* The install's drain token, persisted in the config directory.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Why it persists instead of being generated per run: `cartethyia stop` runs in
|
|
8
|
+
* a different process, possibly minutes later, and the token is the only way it
|
|
9
|
+
* can authenticate to a gateway it did not start. A token regenerated on each
|
|
10
|
+
* start could never match the gateway that is actually running, which is why
|
|
11
|
+
* `stop` reported "no gateway found" or a 401 on a live gateway.
|
|
12
|
+
*
|
|
13
|
+
* It is kept out of `.env` on purpose: it authorises a shutdown, so it belongs
|
|
14
|
+
* in a machine-written file the operator never hand-edits and never commits.
|
|
15
|
+
* Mode 0600 where the platform honours it.
|
|
11
16
|
*/
|
|
12
17
|
const RUNTIME_STATE_FILE = "runtime.json";
|
|
13
18
|
|
|
19
|
+
interface RuntimeState {
|
|
20
|
+
readonly drainToken: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
function statePath(): string | null {
|
|
15
24
|
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
16
25
|
return configDir ? join(configDir, RUNTIME_STATE_FILE) : null;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
|
|
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 {
|
|
28
|
+
function readState(): RuntimeState | null {
|
|
35
29
|
const path = statePath();
|
|
36
30
|
if (path === null || !existsSync(path)) return null;
|
|
37
31
|
try {
|
|
@@ -40,23 +34,45 @@ export function readPersistedDrainToken(): string | null {
|
|
|
40
34
|
parsed !== null &&
|
|
41
35
|
typeof parsed === "object" &&
|
|
42
36
|
"drainToken" in parsed &&
|
|
43
|
-
typeof parsed.drainToken === "string"
|
|
37
|
+
typeof parsed.drainToken === "string" &&
|
|
38
|
+
parsed.drainToken.length > 0
|
|
44
39
|
) {
|
|
45
|
-
return parsed.drainToken;
|
|
40
|
+
return { drainToken: parsed.drainToken };
|
|
46
41
|
}
|
|
47
42
|
} catch {
|
|
48
|
-
// A corrupt or stale state file reads as "no
|
|
43
|
+
// A corrupt or stale state file reads as "no token".
|
|
49
44
|
}
|
|
50
45
|
return null;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Returns the install's drain token, creating and persisting one on first use.
|
|
50
|
+
*
|
|
51
|
+
* Every start calls this, so a gateway started from the menu, from `tray`, or
|
|
52
|
+
* from `cartethyia start` all authenticate with the same token and `stop`
|
|
53
|
+
* always has the one that works.
|
|
54
|
+
*/
|
|
55
|
+
export function ensureDrainToken(): string {
|
|
56
|
+
const existing = readState();
|
|
57
|
+
if (existing !== null) return existing.drainToken;
|
|
58
|
+
|
|
59
|
+
const token = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "");
|
|
55
60
|
const path = statePath();
|
|
56
|
-
if (path
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (path !== null) {
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(path, JSON.stringify({ drainToken: token }), {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
// A read-only config directory still yields a usable in-process token;
|
|
69
|
+
// only `stop` from another process is lost.
|
|
70
|
+
}
|
|
61
71
|
}
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The persisted token, or null when this install has never started. */
|
|
76
|
+
export function readPersistedDrainToken(): string | null {
|
|
77
|
+
return readState()?.drainToken ?? null;
|
|
62
78
|
}
|