cartethyia 2.4.1 → 2.4.3
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-hGt6sYfC.js → BannedPage-CFWM4WEi.js} +1 -1
- package/dist/dashboard/assets/{CliToolDetailPage-C-v5GoF-.js → CliToolDetailPage-DSGFhzfL.js} +1 -1
- package/dist/dashboard/assets/{CliToolsPage-Da2XCM9A.js → CliToolsPage-tsIxtVpX.js} +1 -1
- package/dist/dashboard/assets/{ConsoleLogPage-xT-LP8xZ.js → ConsoleLogPage-BY7Y_0z8.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-D0PQsW3f.js → CustomizationPage-DlyXzyIf.js} +1 -1
- package/dist/dashboard/assets/{HealthEventsModal-DTiQ4ulO.js → HealthEventsModal-KG3K2C5M.js} +1 -1
- package/dist/dashboard/assets/{LoginPage-jMx71m_m.js → LoginPage-B4T7XQcx.js} +1 -1
- package/dist/dashboard/assets/{OverviewPage-BQpanEmj.js → OverviewPage-BeVW5HiF.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-DMh9bfu0.js → ProviderDetailPage-D8CTrTMh.js} +1 -1
- package/dist/dashboard/assets/{ProvidersPage-Bfg0qSLE.js → ProvidersPage-DSSnMPdU.js} +1 -1
- package/dist/dashboard/assets/{ProxyPage-CSSN-6vg.js → ProxyPage-DjXFNVqh.js} +1 -1
- package/dist/dashboard/assets/{QuotaPage-BPksahOs.js → QuotaPage-ZRiPzRxI.js} +1 -1
- package/dist/dashboard/assets/{SetupPage-DxKaH8LS.js → SetupPage-4v0f70wB.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-B9WEa99R.js → StudioPage-DQ3V_uhU.js} +1 -1
- package/dist/dashboard/assets/{UsagePage-BOSJhVq0.js → UsagePage-DA2RUnXI.js} +1 -1
- package/dist/dashboard/assets/{entry-C1Dn2nQu.js → entry-8jVcj7OO.js} +3 -3
- package/dist/dashboard/assets/{index-TZNbg9dv.js → index-gJaMvav9.js} +2 -2
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli/cli.ts +97 -305
- package/scripts/cli/tray.ps1 +98 -0
- package/scripts/cli/tray.ts +213 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import SysTray from "systray2";
|
|
4
|
+
import { resolvePackagePath } from "../../src/config/package-paths";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One tray row. `action` rows report their id on click; separators are drawn
|
|
8
|
+
* but never fire.
|
|
9
|
+
*/
|
|
10
|
+
export type TrayItem =
|
|
11
|
+
| { readonly kind: "action"; readonly id: string; readonly title: string; readonly enabled?: boolean }
|
|
12
|
+
| { readonly kind: "separator" };
|
|
13
|
+
|
|
14
|
+
export interface TrayOptions {
|
|
15
|
+
readonly iconPath: string;
|
|
16
|
+
readonly tooltip: string;
|
|
17
|
+
readonly items: readonly TrayItem[];
|
|
18
|
+
readonly onSelect: (id: string) => void;
|
|
19
|
+
readonly onExit: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Tray {
|
|
23
|
+
/** Resolves once the icon is on screen and clicks can arrive. */
|
|
24
|
+
ready(): Promise<void>;
|
|
25
|
+
/** Replaces the title and enabled state of an existing row. */
|
|
26
|
+
update(id: string, title: string, enabled?: boolean): void;
|
|
27
|
+
setTooltip(text: string): void;
|
|
28
|
+
/** Removes the icon. Idempotent. */
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Windows tray: a PowerShell NotifyIcon driven over JSON lines.
|
|
34
|
+
*
|
|
35
|
+
* `systray2` on Windows leaves the spawning process attached to a console, so
|
|
36
|
+
* Windows Terminal keeps an empty tab open for the tray's whole lifetime — the
|
|
37
|
+
* window cannot be hidden from inside the process because it is owned by the
|
|
38
|
+
* terminal, and `MainWindowHandle` reads 0 so it cannot be found and closed
|
|
39
|
+
* either. A PowerShell NotifyIcon started with `-WindowStyle Hidden` owns no
|
|
40
|
+
* console at all, which is the only arrangement that shows an icon and no
|
|
41
|
+
* window.
|
|
42
|
+
*/
|
|
43
|
+
class WindowsTray implements Tray {
|
|
44
|
+
readonly #process: ChildProcess;
|
|
45
|
+
readonly #onSelect: (id: string) => void;
|
|
46
|
+
readonly #onExit: () => void;
|
|
47
|
+
readonly #index = new Map<string, number>();
|
|
48
|
+
#ready: Promise<void>;
|
|
49
|
+
#closed = false;
|
|
50
|
+
|
|
51
|
+
constructor(options: TrayOptions) {
|
|
52
|
+
this.#onSelect = options.onSelect;
|
|
53
|
+
this.#onExit = options.onExit;
|
|
54
|
+
|
|
55
|
+
this.#process = spawn(
|
|
56
|
+
"powershell.exe",
|
|
57
|
+
[
|
|
58
|
+
"-NoProfile",
|
|
59
|
+
"-NonInteractive",
|
|
60
|
+
"-ExecutionPolicy",
|
|
61
|
+
"Bypass",
|
|
62
|
+
"-WindowStyle",
|
|
63
|
+
"Hidden",
|
|
64
|
+
"-InputFormat",
|
|
65
|
+
"Text",
|
|
66
|
+
"-OutputFormat",
|
|
67
|
+
"Text",
|
|
68
|
+
"-File",
|
|
69
|
+
resolvePackagePath("scripts", "cli", "tray.ps1"),
|
|
70
|
+
"-IconPath",
|
|
71
|
+
options.iconPath,
|
|
72
|
+
"-Tooltip",
|
|
73
|
+
options.tooltip,
|
|
74
|
+
],
|
|
75
|
+
{ windowsHide: true, stdio: ["pipe", "pipe", "pipe"] },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
79
|
+
this.#ready = promise;
|
|
80
|
+
|
|
81
|
+
const lines = createInterface({ input: this.#process.stdout! });
|
|
82
|
+
lines.on("line", (line) => {
|
|
83
|
+
let event: unknown;
|
|
84
|
+
try {
|
|
85
|
+
event = JSON.parse(line);
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (event === null || typeof event !== "object" || !("type" in event)) return;
|
|
90
|
+
if (event.type === "ready") {
|
|
91
|
+
resolve();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (event.type === "click" && "index" in event && typeof event.index === "number") {
|
|
95
|
+
for (const [id, index] of this.#index) {
|
|
96
|
+
if (index === event.index) {
|
|
97
|
+
this.#onSelect(id);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.#process.once("error", reject);
|
|
105
|
+
this.#process.once("exit", () => {
|
|
106
|
+
if (!this.#closed) this.#onExit();
|
|
107
|
+
reject(new Error("the tray process exited before it was ready"));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Rows are added after "started": the script creates its NotifyIcon first
|
|
111
|
+
// and would drop commands that arrive before it is listening.
|
|
112
|
+
let index = 0;
|
|
113
|
+
for (const item of options.items) {
|
|
114
|
+
if (item.kind === "separator") {
|
|
115
|
+
this.#send({ action: "add-item", index, title: "-", enabled: false });
|
|
116
|
+
index += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
this.#index.set(item.id, index);
|
|
120
|
+
this.#send({ action: "add-item", index, title: item.title, enabled: item.enabled ?? true });
|
|
121
|
+
index += 1;
|
|
122
|
+
}
|
|
123
|
+
this.#send({ action: "ready" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#send(command: Record<string, unknown>): void {
|
|
127
|
+
if (this.#closed || !this.#process.stdin?.writable) return;
|
|
128
|
+
this.#process.stdin.write(`${JSON.stringify(command)}\n`, "utf8");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ready(): Promise<void> {
|
|
132
|
+
return this.#ready;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
update(id: string, title: string, enabled = true): void {
|
|
136
|
+
const index = this.#index.get(id);
|
|
137
|
+
if (index === undefined) return;
|
|
138
|
+
this.#send({ action: "update-item", index, title, enabled });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setTooltip(text: string): void {
|
|
142
|
+
this.#send({ action: "set-tooltip", text });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async close(): Promise<void> {
|
|
146
|
+
if (this.#closed) return;
|
|
147
|
+
this.#closed = true;
|
|
148
|
+
this.#send({ action: "kill" });
|
|
149
|
+
await new Promise<void>((resolve) => {
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
this.#process.kill();
|
|
152
|
+
resolve();
|
|
153
|
+
}, 1_500);
|
|
154
|
+
this.#process.once("exit", () => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* macOS and Linux tray: `systray2` runs its own helper, and neither platform
|
|
164
|
+
* attaches a console window to it, so the bundled tray is used as-is.
|
|
165
|
+
*/
|
|
166
|
+
class UnixTray implements Tray {
|
|
167
|
+
readonly #tray: SysTray;
|
|
168
|
+
|
|
169
|
+
constructor(options: TrayOptions) {
|
|
170
|
+
this.#tray = new SysTray({
|
|
171
|
+
menu: {
|
|
172
|
+
icon: options.iconPath,
|
|
173
|
+
title: "Cartethyia",
|
|
174
|
+
tooltip: options.tooltip,
|
|
175
|
+
items: options.items.map((item) =>
|
|
176
|
+
item.kind === "separator"
|
|
177
|
+
? SysTray.separator
|
|
178
|
+
: { title: item.title, tooltip: item.title, enabled: item.enabled ?? true },
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
copyDir: true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ready(): Promise<void> {
|
|
186
|
+
return this.#tray.ready().then(() => undefined);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
update(id: string, title: string, enabled = true): void {
|
|
190
|
+
void id;
|
|
191
|
+
void this.#tray.sendAction({ type: "update-item", item: { title, tooltip: title, enabled } });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setTooltip(text: string): void {
|
|
195
|
+
// systray2 has no tooltip-only action; the tooltip travels with the title,
|
|
196
|
+
// which `update()` already sends.
|
|
197
|
+
void text;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async close(): Promise<void> {
|
|
201
|
+
if (this.#tray.killed) return;
|
|
202
|
+
try {
|
|
203
|
+
this.#tray.kill();
|
|
204
|
+
} catch {
|
|
205
|
+
// The helper may already be gone.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Creates the tray for this platform. */
|
|
211
|
+
export function createTray(options: TrayOptions): Tray {
|
|
212
|
+
return process.platform === "win32" ? new WindowsTray(options) : new UnixTray(options);
|
|
213
|
+
}
|