cartethyia 2.5.8 → 2.5.10
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/.env.example +7 -42
- package/README.md +11 -10
- package/dist/dashboard/assets/{BannedPage-BKAI54IA.js → BannedPage-Cd30Z0b9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CkGvxrHu.js → CliToolDetailPage-wf5u-ww6.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Cj8MYpeY.js → CliToolsPage-DykPvuck.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BDXBEbWT.js → CombosPage-DwvSD3vs.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-49KH4z3l.js → ConsoleLogPage-CIr7KDex.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-EHjBmNBZ.js → CustomizationPage-82X0DDdH.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-ByUTtj7j.js → HealthEventsModal-BYPucTTn.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CYSCKT8S.js → LoginPage-CS5mPuVF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-C0UCUsVJ.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-Bu3iqBMm.js → OverviewPage-Dk_ew3YU.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderDetailPage-FHdgmgjU.js → ProviderDetailPage-CAiRn2Q0.js} +1 -1
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-CCvYS9CF.js → ProvidersPage-vdja5ZPK.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-HOl9cjxl.js → ProxyPage-BB4CNm4-.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-MUGzIpTK.js → QuotaPage-FEc4iNyq.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CTdF5TCa.js → SettingsPage-Dlfi5L4N.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-Bra7rkKx.js → SetupPage-FadUOl9S.js} +1 -1
- package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-BRscd-vl.js → SortableList-Cfy6_Khv.js} +1 -1
- package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BeevnFtM.js → StudioPage-CM92fgJf.js} +1 -1
- package/dist/dashboard/assets/StudioPage-CM92fgJf.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BWik9fpu.js → UsagePage-BT4MyQeT.js} +1 -1
- package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
- package/dist/dashboard/assets/{app-BIt446yE.js → app-B7pVRhJ1.js} +1 -1
- package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-B7pVRhJ1.js.gz} +0 -0
- package/dist/dashboard/assets/{badge-CR72dasT.js → badge-Bvyr7D0u.js} +1 -1
- package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-DIvKAZih.js} +1 -1
- package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
- package/dist/dashboard/assets/{card-DS7CQEsO.js → card-D5_gYIS_.js} +1 -1
- package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-B6vDBtbm.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-CpjrgdwJ.js} +1 -1
- package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BWmt3b5y.js} +1 -1
- package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B9Xvtnfj.js → entry-B-gvimrg.js} +1 -1
- package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-C_oakV3z.js} +1 -1
- package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BqZ5vrM7.js → entry-Dj0cHfEc.js} +3 -3
- package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-RRHHWGxh.js} +1 -1
- package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
- package/dist/dashboard/assets/{index-9ARd3FIs.js → index-Bhey_rg_.js} +2 -2
- package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C2_EQ5Uf.js} +1 -1
- package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
- package/dist/dashboard/assets/{input-V1umFiBk.js → input-BMRlQ450.js} +1 -1
- package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-XR5xP8ew.js} +1 -1
- package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-Cw07dioc.js} +1 -1
- package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-DrdA9oSM.js} +1 -1
- package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
- package/dist/dashboard/assets/{select-D_BBhBEF.js → select-CxVVB__N.js} +1 -1
- package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
- package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-uPOwM9Hs.js} +1 -1
- package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
- package/dist/dashboard/assets/{state-mPDRlQkl.js → state-D1Yz79iL.js} +1 -1
- package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-sevsRrRo.js} +1 -1
- package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +40 -40
- package/package.json +1 -3
- package/scripts/cli/admin.ts +3 -0
- package/scripts/cli/console-menu.ts +299 -275
- package/scripts/cli/doctor.ts +1 -1
- package/scripts/cli/gateway.ts +1 -2
- package/scripts/{health.ts → cli/health.ts} +4 -4
- package/scripts/cli/main.ts +3 -0
- package/scripts/cli/prompt.ts +137 -107
- package/scripts/cli/start.ts +3 -0
- package/scripts/cli/tray.ts +167 -208
- package/src/config/package-paths.ts +24 -0
- package/src/console/backup/nine-router.ts +43 -9
- package/src/console/remote-support/service.ts +270 -114
- package/src/console/routing/pools/store.ts +1 -1
- package/src/console/updates/repository-updates.ts +40 -39
- package/src/observability/event-day-index.ts +1 -2
- package/src/protocol/registry.ts +2 -0
- package/src/providers/default-registry.ts +4 -0
- package/src/providers/discovery/openrouter-free-roster.ts +0 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/capabilities.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +2 -1
- package/src/providers/integrations/github/github.ts +3 -1
- package/src/providers/integrations/opencode/provider.ts +2 -3
- package/src/providers/integrations/trae/trae-headers.ts +17 -48
- package/src/providers/integrations/trae/trae.ts +22 -24
- package/src/providers/operations/provider-credential-service.ts +29 -3
- package/src/providers/provider-probe.ts +3 -0
- package/dist/dashboard/assets/BannedPage-BKAI54IA.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CkGvxrHu.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Cj8MYpeY.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BDXBEbWT.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-49KH4z3l.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-EHjBmNBZ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-ByUTtj7j.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CYSCKT8S.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Bu3iqBMm.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-FHdgmgjU.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CCvYS9CF.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-HOl9cjxl.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-MUGzIpTK.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CTdF5TCa.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Bra7rkKx.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-BRscd-vl.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BeevnFtM.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BWik9fpu.js.gz +0 -0
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-BqZ5vrM7.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/index-9ARd3FIs.js.gz +0 -0
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/select-D_BBhBEF.js.gz +0 -0
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/scripts/build/aot.ts +0 -143
- package/scripts/build/build.ts +0 -24
- package/scripts/build/compile.ts +0 -88
- package/scripts/build/restart.ts +0 -120
- package/scripts/build/start.ts +0 -25
package/scripts/cli/prompt.ts
CHANGED
|
@@ -1,107 +1,137 @@
|
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
/**
|
|
30
|
+
* Options for `chooseFromMenu`. `initialIndex` is an **action** index, not a
|
|
31
|
+
* row index — the caller numbers from the actions it exposes, which is also
|
|
32
|
+
* what the function returns. Rules (separators) are not indexed.
|
|
33
|
+
*/
|
|
34
|
+
export interface MenuOptions {
|
|
35
|
+
readonly initialIndex?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function chooseFromMenu(
|
|
39
|
+
header: string | readonly string[],
|
|
40
|
+
rows: readonly MenuRow[],
|
|
41
|
+
options: MenuOptions = {},
|
|
42
|
+
): Promise<number> {
|
|
43
|
+
if (!process.stdin.isTTY) return -1;
|
|
44
|
+
|
|
45
|
+
const headerLines = typeof header === "string" ? [header] : header;
|
|
46
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
47
|
+
for (const line of headerLines) process.stdout.write(`${line}\n`);
|
|
48
|
+
process.stdout.write("\n");
|
|
49
|
+
|
|
50
|
+
// Enquirer indexes the rows it renders, rules included, so the action index
|
|
51
|
+
// has to be recovered from the label rather than from the position.
|
|
52
|
+
const labels = rows.flatMap((row) => (row.kind === "action" ? [row.label] : []));
|
|
53
|
+
// Translate the action-index the caller passed into the row-index enquirer
|
|
54
|
+
// wants (which counts rules). An out-of-range or unspecified initial index
|
|
55
|
+
// falls back to the first action.
|
|
56
|
+
const clampedActionIndex = (() => {
|
|
57
|
+
if (options.initialIndex === undefined) return 0;
|
|
58
|
+
if (options.initialIndex < 0) return 0;
|
|
59
|
+
if (options.initialIndex >= labels.length) return labels.length - 1;
|
|
60
|
+
return options.initialIndex;
|
|
61
|
+
})();
|
|
62
|
+
let seenActions = 0;
|
|
63
|
+
let rowInitial = 0;
|
|
64
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
65
|
+
const row = rows[i];
|
|
66
|
+
if (row === undefined || row.kind !== "action") continue;
|
|
67
|
+
if (seenActions === clampedActionIndex) {
|
|
68
|
+
rowInitial = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
seenActions += 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const answer = await Enquirer.prompt<{ choice: string }>({
|
|
77
|
+
type: "select",
|
|
78
|
+
name: "choice",
|
|
79
|
+
message: "Select an action",
|
|
80
|
+
choices: rows.map((row) =>
|
|
81
|
+
row.kind === "rule"
|
|
82
|
+
? { name: "rule", message: "", role: "separator" }
|
|
83
|
+
: {
|
|
84
|
+
name: row.label,
|
|
85
|
+
message: row.label,
|
|
86
|
+
...(row.hint === undefined ? {} : { hint: row.hint }),
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
initial: rowInitial,
|
|
90
|
+
});
|
|
91
|
+
return labels.indexOf(answer.choice);
|
|
92
|
+
} catch {
|
|
93
|
+
// Enquirer rejects on Esc and on its own Ctrl-C handling. Return -1 so the
|
|
94
|
+
// caller drains the gateway rather than leaving a detached process behind.
|
|
95
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Asks the operator to type an exact word before a destructive action runs.
|
|
102
|
+
*
|
|
103
|
+
* Enter alone must not be enough: the reset deletes every console credential,
|
|
104
|
+
* so the operator types the word rather than confirming a highlight. Returns
|
|
105
|
+
* false on cancel, on a mismatch, or when stdin is not a TTY.
|
|
106
|
+
*/
|
|
107
|
+
export async function confirmByTyping(word: string, message: string): Promise<boolean> {
|
|
108
|
+
if (!process.stdin.isTTY) return false;
|
|
109
|
+
try {
|
|
110
|
+
const answer = await Enquirer.prompt<{ confirmation: string }>({
|
|
111
|
+
type: "input",
|
|
112
|
+
name: "confirmation",
|
|
113
|
+
message: `${message} (type ${word})`,
|
|
114
|
+
validate: (value: string) =>
|
|
115
|
+
value.trim() === word ? true : `type ${word} exactly to confirm, or Ctrl-C to cancel`,
|
|
116
|
+
});
|
|
117
|
+
return answer.confirmation.trim() === word;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A yes/no gate for the step before the typed confirmation. */
|
|
124
|
+
export async function confirmStep(message: string): Promise<boolean> {
|
|
125
|
+
if (!process.stdin.isTTY) return false;
|
|
126
|
+
try {
|
|
127
|
+
const answer = await Enquirer.prompt<{ proceed: boolean }>({
|
|
128
|
+
type: "confirm",
|
|
129
|
+
name: "proceed",
|
|
130
|
+
message,
|
|
131
|
+
initial: false,
|
|
132
|
+
});
|
|
133
|
+
return answer.proceed;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
package/scripts/cli/start.ts
CHANGED
|
@@ -26,6 +26,9 @@ export async function startForeground(): Promise<void> {
|
|
|
26
26
|
const blocker = await inspectPortHolder(port, token);
|
|
27
27
|
if (blocker !== null) throw new Error(blocker);
|
|
28
28
|
writeInstanceLock(port, process.pid, token);
|
|
29
|
+
// dynamic-import: `src/main` boots the gateway as a side effect of module
|
|
30
|
+
// evaluation. Importing it statically would start the server before the
|
|
31
|
+
// port probe + instance-lock sequence above has a chance to refuse the run.
|
|
29
32
|
await import("../../src/main");
|
|
30
33
|
}
|
|
31
34
|
|
package/scripts/cli/tray.ts
CHANGED
|
@@ -1,208 +1,167 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{ kind: "action", id: "
|
|
68
|
-
{ kind: "separator" },
|
|
69
|
-
{ kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
|
|
70
|
-
{ kind: "action", id: "reset", title: "Reset console password" },
|
|
71
|
-
{ kind: "action", id: "backup", title: "Backup" },
|
|
72
|
-
{ kind: "separator" },
|
|
73
|
-
{ kind: "action", id: "exit", title: "Exit" },
|
|
74
|
-
],
|
|
75
|
-
onSelect: (id: string) => void handleSelect(id),
|
|
76
|
-
onExit: () => void close(1),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const close = (exitCode = 0): Promise<void> => {
|
|
80
|
-
if (closePromise) return closePromise;
|
|
81
|
-
closing = true;
|
|
82
|
-
closePromise = (async () => {
|
|
83
|
-
const current: Bun.Subprocess | null = gateway;
|
|
84
|
-
gateway = null;
|
|
85
|
-
if (current) await stopGateway(current, port, drainToken);
|
|
86
|
-
await tray.close();
|
|
87
|
-
process.exitCode = exitCode;
|
|
88
|
-
})();
|
|
89
|
-
return closePromise;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
await
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// terminal that launched it, so closing that terminal took the gateway
|
|
169
|
-
// down too and "Hide to tray" left nothing serving. `stopGateway` drains
|
|
170
|
-
// over HTTP before it ever reaches for `kill()`, so detaching costs the
|
|
171
|
-
// exit path nothing: the tray can still stop the gateway on Exit.
|
|
172
|
-
gateway = await startGateway(port, drainToken, { detached: true });
|
|
173
|
-
// The launcher that spawned this tray is blocked on a signal file: it
|
|
174
|
-
// cannot read our stdio, because the tray is detached. Without this write
|
|
175
|
-
// it waits out its 60 s deadline and reports a failure for a tray that is
|
|
176
|
-
// running fine.
|
|
177
|
-
reportTrayStartup({ status: "ready", url: dashboardUrl });
|
|
178
|
-
console.log(`Cartethyia is running at ${dashboardUrl}`);
|
|
179
|
-
void refreshVersion();
|
|
180
|
-
|
|
181
|
-
let misses = 0;
|
|
182
|
-
// A detached child's `exited` never settles on Windows, so the old
|
|
183
|
-
// `await current.exited` would have parked here forever and the tray would
|
|
184
|
-
// never notice the gateway going away. Poll its health endpoint instead:
|
|
185
|
-
// that answers for a process we no longer own, which is the whole point of
|
|
186
|
-
// detaching it. The probe is the same one startup used, so a gateway that
|
|
187
|
-
// is up reads as up regardless of who spawned it.
|
|
188
|
-
while (!closing) {
|
|
189
|
-
await Bun.sleep(TRAY_HEALTH_POLL_MS);
|
|
190
|
-
if (closing || gateway === null) continue;
|
|
191
|
-
const alive = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
192
|
-
signal: AbortSignal.timeout(2_000),
|
|
193
|
-
}).then((response) => response.ok).catch(() => false);
|
|
194
|
-
if (alive) {
|
|
195
|
-
misses = 0;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
misses += 1;
|
|
199
|
-
if (misses < TRAY_HEALTH_MISSES_BEFORE_CLOSE) continue;
|
|
200
|
-
gateway = null;
|
|
201
|
-
console.error("Cartethyia stopped answering; the tray is closing.");
|
|
202
|
-
await close(1);
|
|
203
|
-
}
|
|
204
|
-
} catch (error) {
|
|
205
|
-
await close(1);
|
|
206
|
-
throw error;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
1
|
+
import { resolvePackagePath } from "../../src/config/package-paths";
|
|
2
|
+
import { resolvePort } from "../../src/config/runtime";
|
|
3
|
+
import { ensureDrainToken } from "./instance";
|
|
4
|
+
import { startGateway } from "./gateway";
|
|
5
|
+
import { stopGateway } from "./gateway";
|
|
6
|
+
import { readInstalledVersion } from "./version";
|
|
7
|
+
import { reportTrayStartup } from "./startup-signal";
|
|
8
|
+
import { createTray } from "./tray/index";
|
|
9
|
+
import { openUrl } from "./background";
|
|
10
|
+
import { exportBackup, resetConsoleAccount } from "./admin";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runs the gateway with a tray icon. The tray is platform-specific (`tray/`):
|
|
14
|
+
* on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
|
|
15
|
+
* an empty console tab open for the tray's whole lifetime, and that window is
|
|
16
|
+
* owned by Windows Terminal — not by this process, so it cannot be found and
|
|
17
|
+
* hidden from inside.
|
|
18
|
+
*
|
|
19
|
+
* The gateway it starts is detached, which is what makes the health poll below
|
|
20
|
+
* necessary — see {@link TRAY_HEALTH_POLL_MS}.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How often the tray asks whether the gateway is still answering.
|
|
25
|
+
*
|
|
26
|
+
* 10 s: often enough that an operator who stops the gateway by hand sees the
|
|
27
|
+
* icon go away, and far below the cost of a request. A detached gateway can no
|
|
28
|
+
* longer be waited on, so this poll is the only signal left.
|
|
29
|
+
*/
|
|
30
|
+
const TRAY_HEALTH_POLL_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Consecutive misses to forgive before a dead gateway closes the tray.
|
|
34
|
+
*
|
|
35
|
+
* One refusal is not a stopped gateway — a probe can lose a race with startup
|
|
36
|
+
* or a slow request — and closing the icon on one would make startup feel
|
|
37
|
+
* random.
|
|
38
|
+
*/
|
|
39
|
+
const TRAY_HEALTH_MISSES_BEFORE_CLOSE = 3;
|
|
40
|
+
|
|
41
|
+
export async function startTray(): Promise<void> {
|
|
42
|
+
const port = resolvePort();
|
|
43
|
+
const drainToken = ensureDrainToken();
|
|
44
|
+
const dashboardUrl = `http://127.0.0.1:${port}/console`;
|
|
45
|
+
const releasesUrl = "https://github.com/risunCodex/Cartethyia/releases";
|
|
46
|
+
const currentVersion = await readInstalledVersion();
|
|
47
|
+
|
|
48
|
+
let gateway: Bun.Subprocess | null = null;
|
|
49
|
+
let closing = false;
|
|
50
|
+
let resetArmed = false;
|
|
51
|
+
let closePromise: Promise<void> | undefined;
|
|
52
|
+
|
|
53
|
+
const tray = createTray({
|
|
54
|
+
iconPath: resolvePackagePath(
|
|
55
|
+
"scripts",
|
|
56
|
+
"cli",
|
|
57
|
+
"tray",
|
|
58
|
+
"assets",
|
|
59
|
+
process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
|
|
60
|
+
),
|
|
61
|
+
tooltip: `Cartethyia ${currentVersion}`,
|
|
62
|
+
items: [
|
|
63
|
+
// Version row is informational; clicking it opens the releases page so
|
|
64
|
+
// the operator can decide for themselves. No background check: the tray
|
|
65
|
+
// used to poll the registry on open and the result was a cue the user
|
|
66
|
+
// did not ask for.
|
|
67
|
+
{ kind: "action", id: "version", title: `Cartethyia ${currentVersion}`, enabled: true },
|
|
68
|
+
{ kind: "separator" },
|
|
69
|
+
{ kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
|
|
70
|
+
{ kind: "action", id: "reset", title: "Reset console password" },
|
|
71
|
+
{ kind: "action", id: "backup", title: "Backup" },
|
|
72
|
+
{ kind: "separator" },
|
|
73
|
+
{ kind: "action", id: "exit", title: "Exit" },
|
|
74
|
+
],
|
|
75
|
+
onSelect: (id: string) => void handleSelect(id),
|
|
76
|
+
onExit: () => void close(1),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const close = (exitCode = 0): Promise<void> => {
|
|
80
|
+
if (closePromise) return closePromise;
|
|
81
|
+
closing = true;
|
|
82
|
+
closePromise = (async () => {
|
|
83
|
+
const current: Bun.Subprocess | null = gateway;
|
|
84
|
+
gateway = null;
|
|
85
|
+
if (current) await stopGateway(current, port, drainToken);
|
|
86
|
+
await tray.close();
|
|
87
|
+
process.exitCode = exitCode;
|
|
88
|
+
})();
|
|
89
|
+
return closePromise;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
async function handleSelect(id: string): Promise<void> {
|
|
93
|
+
if (closing) return;
|
|
94
|
+
if (id === "exit") return void (await close());
|
|
95
|
+
if (id === "console") return void (await openUrl(dashboardUrl));
|
|
96
|
+
if (id === "version") return void (await openUrl(releasesUrl));
|
|
97
|
+
if (id === "backup") {
|
|
98
|
+
const result = await exportBackup(port, drainToken);
|
|
99
|
+
if (result !== null) {
|
|
100
|
+
console.log(
|
|
101
|
+
`Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (id === "reset") {
|
|
107
|
+
// A tray click cannot ask for the typed confirmation the menu uses, so
|
|
108
|
+
// the first click arms the action and the second one runs it.
|
|
109
|
+
if (!resetArmed) {
|
|
110
|
+
resetArmed = true;
|
|
111
|
+
tray.update("reset", "Reset console password \u00b7 click again to delete", true);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
resetArmed = false;
|
|
115
|
+
tray.update("reset", "Reset console password", true);
|
|
116
|
+
await resetConsoleAccount(port, drainToken);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
process.once("SIGINT", () => void close());
|
|
122
|
+
process.once("SIGTERM", () => void close());
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await tray.ready();
|
|
126
|
+
// Detached, unlike every other caller: the gateway must outlive this
|
|
127
|
+
// process. Attached, it dies with the tray — and the tray dies with the
|
|
128
|
+
// terminal that launched it, so closing that terminal took the gateway
|
|
129
|
+
// down too and "Hide to tray" left nothing serving. `stopGateway` drains
|
|
130
|
+
// over HTTP before it ever reaches for `kill()`, so detaching costs the
|
|
131
|
+
// exit path nothing: the tray can still stop the gateway on Exit.
|
|
132
|
+
gateway = await startGateway(port, drainToken, { detached: true });
|
|
133
|
+
// The launcher that spawned this tray is blocked on a signal file: it
|
|
134
|
+
// cannot read our stdio, because the tray is detached. Without this write
|
|
135
|
+
// it waits out its 60 s deadline and reports a failure for a tray that is
|
|
136
|
+
// running fine.
|
|
137
|
+
reportTrayStartup({ status: "ready", url: dashboardUrl });
|
|
138
|
+
console.log(`Cartethyia is running at ${dashboardUrl}`);
|
|
139
|
+
|
|
140
|
+
let misses = 0;
|
|
141
|
+
// A detached child's `exited` never settles on Windows, so the old
|
|
142
|
+
// `await current.exited` would have parked here forever and the tray would
|
|
143
|
+
// never notice the gateway going away. Poll its health endpoint instead:
|
|
144
|
+
// that answers for a process we no longer own, which is the whole point of
|
|
145
|
+
// detaching it. The probe is the same one startup used, so a gateway that
|
|
146
|
+
// is up reads as up regardless of who spawned it.
|
|
147
|
+
while (!closing) {
|
|
148
|
+
await Bun.sleep(TRAY_HEALTH_POLL_MS);
|
|
149
|
+
if (closing || gateway === null) continue;
|
|
150
|
+
const alive = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
151
|
+
signal: AbortSignal.timeout(2_000),
|
|
152
|
+
}).then((response) => response.ok).catch(() => false);
|
|
153
|
+
if (alive) {
|
|
154
|
+
misses = 0;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
misses += 1;
|
|
158
|
+
if (misses < TRAY_HEALTH_MISSES_BEFORE_CLOSE) continue;
|
|
159
|
+
gateway = null;
|
|
160
|
+
console.error("Cartethyia stopped answering; the tray is closing.");
|
|
161
|
+
await close(1);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
await close(1);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|