tokmon 0.28.1 → 0.28.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/README.md +156 -234
- package/THIRD_PARTY_NOTICES.md +62 -0
- package/dist/{bootstrap-ink-L7QSJBMS.js → bootstrap-ink-WG4WG3R2.js} +1579 -1135
- package/dist/chunk-7CH6S7BR.js +447 -0
- package/dist/{chunk-CVKCVHS7.js → chunk-CFKUZCMB.js} +70 -310
- package/dist/chunk-HNWEJ6MS.js +1094 -0
- package/dist/chunk-JZSZEHVD.js +406 -0
- package/dist/{chunk-S33XIUAW.js → chunk-P57DVFNH.js} +277 -49
- package/dist/{chunk-4HU4EI5T.js → chunk-P5JN5DDX.js} +25 -46
- package/dist/chunk-TN6V7XSL.js +512 -0
- package/dist/chunk-USKOQIE3.js +278 -0
- package/dist/chunk-UYPDMVW5.js +21 -0
- package/dist/{cli-command-75LP4IDS.js → cli-command-MD3RJRP4.js} +249 -15
- package/dist/cli.js +6 -6
- package/dist/{config-HXFJTNLM.js → config-3DGZ47F7.js} +23 -3
- package/dist/daemon-EMOA36WB.js +377 -0
- package/dist/daemon-handle-JHE5MKMI.js +11 -0
- package/dist/server-EPSH52DR.js +12 -0
- package/dist/web/assets/{Area-CMXvOw33.js → Area-stRnp8Km.js} +1 -1
- package/dist/web/assets/analytics-moZZt9L_.js +2 -0
- package/dist/web/assets/breakdown-km9w7myU.js +4 -0
- package/dist/web/assets/button-DrX04Cc2.js +1 -0
- package/dist/web/assets/{chart-cZoLgpmG.js → chart-D0CtuL2k.js} +1 -1
- package/dist/web/assets/explore-B11uNJV1.js +22 -0
- package/dist/web/assets/index-RDhXsCNG.js +83 -0
- package/dist/web/assets/index-ZmOS8p9O.css +1 -0
- package/dist/web/assets/models-DYbZnY34.js +2 -0
- package/dist/web/assets/overview-CruICbfJ.js +2 -0
- package/dist/web/assets/panel-xckM3_GC.js +1 -0
- package/dist/web/assets/settings-sheet-G-BTSIF6.js +1 -0
- package/dist/web/assets/share-sheet-weIdd4DW.js +2 -0
- package/dist/web/assets/timeline-BHgniliZ.js +1 -0
- package/dist/web/index.html +30 -4
- package/package.json +13 -4
- package/site/THIRD_PARTY_NOTICES.md +83 -0
- package/site/public/fonts/OFL.txt +93 -0
- package/dist/chunk-3RTWFGGD.js +0 -275
- package/dist/chunk-FMP3P2WV.js +0 -299
- package/dist/chunk-HP5UZCXP.js +0 -436
- package/dist/chunk-SMPY52EV.js +0 -125
- package/dist/chunk-XDA5RJST.js +0 -193
- package/dist/daemon-CL4FJW26.js +0 -219
- package/dist/daemon-handle-QQLJE46Y.js +0 -10
- package/dist/server-EUO7CWYH.js +0 -11
- package/dist/web/assets/analytics-DqdZXOJL.js +0 -2
- package/dist/web/assets/breakdown-kOSSJ3mI.js +0 -4
- package/dist/web/assets/explore-vcsM8K9I.js +0 -22
- package/dist/web/assets/index-BErCXT7f.css +0 -1
- package/dist/web/assets/index-DH3dOnbg.js +0 -83
- package/dist/web/assets/models-B5ikm83_.js +0 -2
- package/dist/web/assets/overview-DD1kP8CP.js +0 -2
- package/dist/web/assets/panel-DTQBwExW.js +0 -1
- package/dist/web/assets/primitives-C36qJlA2.js +0 -1
- package/dist/web/assets/settings-sheet-_i2zTQjf.js +0 -1
- package/dist/web/assets/share-sheet-BLHmrFbm.js +0 -2
- package/dist/web/assets/timeline-B4uG-Emo.js +0 -1
- package/dist/web/assets/use-dialog-trap-bfaXEMz9.js +0 -1
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
send,
|
|
6
|
-
sendJson,
|
|
7
|
-
serveStatic
|
|
8
|
-
} from "./chunk-SMPY52EV.js";
|
|
3
|
+
resolveDaemonChannel
|
|
4
|
+
} from "./chunk-UYPDMVW5.js";
|
|
9
5
|
import {
|
|
10
6
|
buildAccounts,
|
|
11
7
|
colorHex,
|
|
8
|
+
deriveAccountIdentity,
|
|
9
|
+
deriveProviderHeadroom,
|
|
10
|
+
deriveQuotaViews,
|
|
12
11
|
fetchPeak,
|
|
13
12
|
namedHex
|
|
14
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-TN6V7XSL.js";
|
|
15
14
|
import {
|
|
16
15
|
PROVIDERS,
|
|
16
|
+
detectProviders,
|
|
17
|
+
resolveTimezone,
|
|
18
|
+
withTimeout
|
|
19
|
+
} from "./chunk-CFKUZCMB.js";
|
|
20
|
+
import {
|
|
21
|
+
ConfigPersistenceFailure,
|
|
22
|
+
ConfigUpdateConflictFailure,
|
|
23
|
+
RefreshFailure,
|
|
17
24
|
TOKMON_CAPABILITIES,
|
|
18
25
|
TOKMON_PROTOCOL_VERSION,
|
|
19
26
|
TOKMON_WS_METHODS,
|
|
20
27
|
TOKMON_WS_PATH,
|
|
21
|
-
TokmonRpcGroup
|
|
22
|
-
|
|
23
|
-
resolveTimezone,
|
|
24
|
-
withTimeout
|
|
25
|
-
} from "./chunk-CVKCVHS7.js";
|
|
28
|
+
TokmonRpcGroup
|
|
29
|
+
} from "./chunk-JZSZEHVD.js";
|
|
26
30
|
import {
|
|
27
31
|
cacheDir,
|
|
28
32
|
expandHome,
|
|
@@ -30,7 +34,7 @@ import {
|
|
|
30
34
|
normalizeConfig,
|
|
31
35
|
saveConfig,
|
|
32
36
|
snapshotCacheFile
|
|
33
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-HNWEJ6MS.js";
|
|
34
38
|
|
|
35
39
|
// src/web/server.ts
|
|
36
40
|
import { createServer } from "http";
|
|
@@ -66,9 +70,13 @@ async function fetchAccountBilling(account, tz) {
|
|
|
66
70
|
return p.fetchBilling(account, tz);
|
|
67
71
|
}
|
|
68
72
|
function assembleSnapshot(opts) {
|
|
73
|
+
const providerOrdinals = /* @__PURE__ */ new Map();
|
|
69
74
|
const accounts = opts.resolved.map((r) => {
|
|
70
75
|
const u = opts.usage.get(r.account.id);
|
|
71
76
|
const billing = opts.billing.get(r.account.id) ?? null;
|
|
77
|
+
const ordinal = (providerOrdinals.get(r.account.providerId) ?? 0) + 1;
|
|
78
|
+
providerOrdinals.set(r.account.providerId, ordinal);
|
|
79
|
+
const quotas = deriveQuotaViews(billing?.metrics ?? []);
|
|
72
80
|
return {
|
|
73
81
|
id: r.account.id,
|
|
74
82
|
providerId: r.account.providerId,
|
|
@@ -79,7 +87,22 @@ function assembleSnapshot(opts) {
|
|
|
79
87
|
hasBilling: r.hasBilling,
|
|
80
88
|
email: billing?.email ?? null,
|
|
81
89
|
displayName: billing?.displayName ?? null,
|
|
90
|
+
identity: deriveAccountIdentity({
|
|
91
|
+
name: r.account.name,
|
|
92
|
+
email: billing?.email,
|
|
93
|
+
displayName: billing?.displayName,
|
|
94
|
+
providerName: PROVIDERS[r.account.providerId].name,
|
|
95
|
+
ordinal,
|
|
96
|
+
privacyMode: opts.config.privacyMode
|
|
97
|
+
}),
|
|
98
|
+
quotas,
|
|
99
|
+
headroom: deriveProviderHeadroom([{
|
|
100
|
+
id: r.account.id,
|
|
101
|
+
lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
|
|
102
|
+
quotas
|
|
103
|
+
}], opts.config.tray.activeTimeoutMin, Date.now(), opts.config.tray.displayMetric),
|
|
82
104
|
plan: billing?.plan ?? null,
|
|
105
|
+
lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
|
|
83
106
|
dashboard: u?.dashboard ?? null,
|
|
84
107
|
table: u?.table ?? null,
|
|
85
108
|
billing,
|
|
@@ -91,6 +114,12 @@ function assembleSnapshot(opts) {
|
|
|
91
114
|
tableUpdatedAt: opts.tableUpdatedAt?.get(r.account.id) ?? null
|
|
92
115
|
};
|
|
93
116
|
});
|
|
117
|
+
const accountsByProvider = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const account of accounts) {
|
|
119
|
+
const group = accountsByProvider.get(account.providerId) ?? [];
|
|
120
|
+
group.push(account);
|
|
121
|
+
accountsByProvider.set(account.providerId, group);
|
|
122
|
+
}
|
|
94
123
|
const seen = /* @__PURE__ */ new Set();
|
|
95
124
|
const providers = [];
|
|
96
125
|
for (const r of opts.resolved) {
|
|
@@ -99,7 +128,17 @@ function assembleSnapshot(opts) {
|
|
|
99
128
|
providers.push({
|
|
100
129
|
id: r.account.providerId,
|
|
101
130
|
name: PROVIDERS[r.account.providerId].name,
|
|
102
|
-
color: namedHex(PROVIDERS[r.account.providerId].color)
|
|
131
|
+
color: namedHex(PROVIDERS[r.account.providerId].color),
|
|
132
|
+
headroom: deriveProviderHeadroom(
|
|
133
|
+
(accountsByProvider.get(r.account.providerId) ?? []).map((account) => ({
|
|
134
|
+
id: account.id,
|
|
135
|
+
lastActivityAt: account.lastActivityAt,
|
|
136
|
+
quotas: account.quotas ?? []
|
|
137
|
+
})),
|
|
138
|
+
opts.config.tray.activeTimeoutMin,
|
|
139
|
+
Date.now(),
|
|
140
|
+
opts.config.tray.displayMetric
|
|
141
|
+
)
|
|
103
142
|
});
|
|
104
143
|
}
|
|
105
144
|
return {
|
|
@@ -118,22 +157,139 @@ function tzFor(config) {
|
|
|
118
157
|
return resolveTimezone(config.timezone);
|
|
119
158
|
}
|
|
120
159
|
|
|
160
|
+
// src/web/static.ts
|
|
161
|
+
import { createReadStream, existsSync, readFileSync } from "fs";
|
|
162
|
+
import { stat } from "fs/promises";
|
|
163
|
+
import { fileURLToPath } from "url";
|
|
164
|
+
import { extname, join, normalize, sep } from "path";
|
|
165
|
+
var MIME = {
|
|
166
|
+
".html": "text/html; charset=utf-8",
|
|
167
|
+
".js": "text/javascript; charset=utf-8",
|
|
168
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
169
|
+
".css": "text/css; charset=utf-8",
|
|
170
|
+
".json": "application/json; charset=utf-8",
|
|
171
|
+
".svg": "image/svg+xml",
|
|
172
|
+
".png": "image/png",
|
|
173
|
+
".jpg": "image/jpeg",
|
|
174
|
+
".jpeg": "image/jpeg",
|
|
175
|
+
".gif": "image/gif",
|
|
176
|
+
".ico": "image/x-icon",
|
|
177
|
+
".woff": "font/woff",
|
|
178
|
+
".woff2": "font/woff2",
|
|
179
|
+
".ttf": "font/ttf",
|
|
180
|
+
".otf": "font/otf",
|
|
181
|
+
".map": "application/json; charset=utf-8",
|
|
182
|
+
".webmanifest": "application/manifest+json"
|
|
183
|
+
};
|
|
184
|
+
function findWebRoot() {
|
|
185
|
+
const candidates = ["./web/", "../web/", "../dist/web/", "../../dist/web/"];
|
|
186
|
+
for (const rel of candidates) {
|
|
187
|
+
try {
|
|
188
|
+
const dir = fileURLToPath(new URL(rel, import.meta.url));
|
|
189
|
+
if (existsSync(join(dir, "index.html"))) return dir.replace(/[\\/]+$/, "");
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function resolveStaticPath(webRoot, urlPath) {
|
|
196
|
+
let clean;
|
|
197
|
+
try {
|
|
198
|
+
clean = decodeURIComponent(urlPath.split("?")[0]);
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const rel = normalize(clean).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
|
|
203
|
+
const full = join(webRoot, rel);
|
|
204
|
+
if (full !== webRoot && !full.startsWith(webRoot + sep)) return null;
|
|
205
|
+
return full;
|
|
206
|
+
}
|
|
207
|
+
function send(res, status, type, body) {
|
|
208
|
+
res.writeHead(status, {
|
|
209
|
+
"Content-Type": type,
|
|
210
|
+
"Cache-Control": "no-store",
|
|
211
|
+
"Referrer-Policy": "no-referrer",
|
|
212
|
+
"X-Content-Type-Options": "nosniff"
|
|
213
|
+
});
|
|
214
|
+
res.end(body);
|
|
215
|
+
}
|
|
216
|
+
function sendJson(res, status, data) {
|
|
217
|
+
send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
|
|
218
|
+
}
|
|
219
|
+
function serveStatic(webRoot, urlPath, res) {
|
|
220
|
+
const path = urlPath.split("?")[0];
|
|
221
|
+
const filePath = resolveStaticPath(webRoot, path === "/" ? "/index.html" : path);
|
|
222
|
+
if (!filePath) {
|
|
223
|
+
send(res, 403, "text/plain", "forbidden");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
void stat(filePath).then((st) => {
|
|
227
|
+
if (st.isFile()) {
|
|
228
|
+
const type = MIME[extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
229
|
+
const immutable = filePath.includes(`${sep}assets${sep}`);
|
|
230
|
+
res.writeHead(200, {
|
|
231
|
+
"Content-Type": type,
|
|
232
|
+
"Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
|
|
233
|
+
"Referrer-Policy": "no-referrer",
|
|
234
|
+
"X-Content-Type-Options": "nosniff"
|
|
235
|
+
});
|
|
236
|
+
createReadStream(filePath).pipe(res);
|
|
237
|
+
} else {
|
|
238
|
+
throw new Error("not a file");
|
|
239
|
+
}
|
|
240
|
+
}).catch(() => {
|
|
241
|
+
if (extname(path)) {
|
|
242
|
+
send(res, 404, "text/plain", "not found");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const indexPath = join(webRoot, "index.html");
|
|
246
|
+
if (!existsSync(indexPath)) {
|
|
247
|
+
send(res, 404, "text/plain", "not found");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
res.writeHead(200, {
|
|
251
|
+
"Content-Type": MIME[".html"],
|
|
252
|
+
"Cache-Control": "no-cache",
|
|
253
|
+
"Referrer-Policy": "no-referrer",
|
|
254
|
+
"X-Content-Type-Options": "nosniff"
|
|
255
|
+
});
|
|
256
|
+
createReadStream(indexPath).on("error", () => {
|
|
257
|
+
try {
|
|
258
|
+
res.destroy();
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}).pipe(res);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function appVersion() {
|
|
265
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
266
|
+
try {
|
|
267
|
+
const p = fileURLToPath(new URL(rel, import.meta.url));
|
|
268
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
269
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return "";
|
|
274
|
+
}
|
|
275
|
+
|
|
121
276
|
// src/web/vite-dev.ts
|
|
122
|
-
import { existsSync } from "fs";
|
|
123
|
-
import { fileURLToPath, pathToFileURL } from "url";
|
|
277
|
+
import { existsSync as existsSync2 } from "fs";
|
|
278
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
124
279
|
import { createRequire } from "module";
|
|
125
|
-
import { join } from "path";
|
|
280
|
+
import { join as join2 } from "path";
|
|
126
281
|
function isDevMode() {
|
|
127
282
|
const forced = process.env.TOKMON_WEB_MODE;
|
|
128
283
|
if (forced === "dev") return true;
|
|
129
284
|
if (forced === "prod") return false;
|
|
285
|
+
if (process.env.NODE_TEST_CONTEXT) return false;
|
|
130
286
|
return import.meta.url.includes("/src/");
|
|
131
287
|
}
|
|
132
288
|
function findWebSource() {
|
|
133
289
|
for (const rel of ["../../web/", "../web/", "./web/"]) {
|
|
134
290
|
try {
|
|
135
|
-
const dir =
|
|
136
|
-
if (
|
|
291
|
+
const dir = fileURLToPath2(new URL(rel, import.meta.url));
|
|
292
|
+
if (existsSync2(join2(dir, "vite.config.ts")) && existsSync2(join2(dir, "index.html"))) {
|
|
137
293
|
return dir.replace(/[\\/]+$/, "");
|
|
138
294
|
}
|
|
139
295
|
} catch {
|
|
@@ -148,12 +304,12 @@ async function createViteDevServer(httpServer, log) {
|
|
|
148
304
|
return null;
|
|
149
305
|
}
|
|
150
306
|
try {
|
|
151
|
-
const req = createRequire(pathToFileURL(
|
|
307
|
+
const req = createRequire(pathToFileURL(join2(root, "package.json")).href);
|
|
152
308
|
const vitePath = req.resolve("vite");
|
|
153
309
|
const vite = await import(pathToFileURL(vitePath).href);
|
|
154
310
|
const dev = await vite.createServer({
|
|
155
311
|
root,
|
|
156
|
-
configFile:
|
|
312
|
+
configFile: join2(root, "vite.config.ts"),
|
|
157
313
|
server: { middlewareMode: true, hmr: { server: httpServer } },
|
|
158
314
|
appType: "spa",
|
|
159
315
|
clearScreen: false,
|
|
@@ -177,7 +333,7 @@ code{color:#e6b450}</style></head><body>
|
|
|
177
333
|
</body></html>`;
|
|
178
334
|
|
|
179
335
|
// src/web/data-engine.ts
|
|
180
|
-
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
336
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
181
337
|
|
|
182
338
|
// src/web/refresh-queue.ts
|
|
183
339
|
function deferred() {
|
|
@@ -331,15 +487,20 @@ function createDataEngine(opts) {
|
|
|
331
487
|
billingUpdatedAt,
|
|
332
488
|
tableUpdatedAt,
|
|
333
489
|
seeded,
|
|
334
|
-
peak
|
|
490
|
+
peak,
|
|
491
|
+
config: currentConfig
|
|
335
492
|
});
|
|
336
493
|
const hydrateFromCache = () => {
|
|
337
494
|
try {
|
|
338
|
-
const cached = JSON.parse(
|
|
495
|
+
const cached = JSON.parse(readFileSync2(snapshotCacheFile(), "utf-8"));
|
|
339
496
|
if (!cached || !Array.isArray(cached.accounts)) return;
|
|
340
497
|
for (const a of cached.accounts) {
|
|
341
498
|
if (a.dashboard || a.table) {
|
|
342
|
-
|
|
499
|
+
const dashboard = a.dashboard ? {
|
|
500
|
+
...a.dashboard,
|
|
501
|
+
lastActivityAt: typeof a.dashboard.lastActivityAt === "number" && Number.isSafeInteger(a.dashboard.lastActivityAt) && a.dashboard.lastActivityAt >= 0 ? a.dashboard.lastActivityAt : null
|
|
502
|
+
} : null;
|
|
503
|
+
usage.set(a.id, { dashboard, table: a.table });
|
|
343
504
|
if (a.dashboard) {
|
|
344
505
|
summaryState.set(a.id, "ready");
|
|
345
506
|
if (typeof a.summaryUpdatedAt === "number") summaryUpdatedAt.set(a.id, a.summaryUpdatedAt);
|
|
@@ -578,6 +739,7 @@ function createDataEngine(opts) {
|
|
|
578
739
|
broadcastConfig(config) {
|
|
579
740
|
if (stopped) return;
|
|
580
741
|
currentConfig = config;
|
|
742
|
+
rebuild();
|
|
581
743
|
for (const onConfig of configSubscribers) {
|
|
582
744
|
try {
|
|
583
745
|
onConfig(config);
|
|
@@ -665,23 +827,51 @@ async function resolveEngineConfig(config) {
|
|
|
665
827
|
billingIntervalMs: billingIntervalFor(config)
|
|
666
828
|
};
|
|
667
829
|
}
|
|
830
|
+
function configAffectsEngine(previous, next) {
|
|
831
|
+
return previous.interval !== next.interval || previous.billingInterval !== next.billingInterval || previous.timezone !== next.timezone || JSON.stringify(previous.accounts) !== JSON.stringify(next.accounts) || JSON.stringify(previous.disabledProviders) !== JSON.stringify(next.disabledProviders) || JSON.stringify(previous.accountDetection) !== JSON.stringify(next.accountDetection);
|
|
832
|
+
}
|
|
668
833
|
var updateQueues = /* @__PURE__ */ new WeakMap();
|
|
834
|
+
function hasOwn(value, key) {
|
|
835
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
836
|
+
}
|
|
837
|
+
function mergeCapabilityFields(incomingConfig, current) {
|
|
838
|
+
const incoming = incomingConfig;
|
|
839
|
+
const incomingTray = incoming.tray;
|
|
840
|
+
const incomingDesktop = incoming.desktop;
|
|
841
|
+
return {
|
|
842
|
+
...incoming,
|
|
843
|
+
appearance: hasOwn(incoming, "appearance") ? incoming.appearance : current.appearance,
|
|
844
|
+
accountDetection: hasOwn(incoming, "accountDetection") ? incoming.accountDetection : current.accountDetection,
|
|
845
|
+
tray: hasOwn(incoming, "tray") && incomingTray ? {
|
|
846
|
+
...current.tray,
|
|
847
|
+
...incomingTray,
|
|
848
|
+
pinnedProviders: hasOwn(incomingTray, "pinnedProviders") ? incomingTray.pinnedProviders : current.tray.pinnedProviders
|
|
849
|
+
} : current.tray,
|
|
850
|
+
desktop: hasOwn(incoming, "desktop") && incomingDesktop ? {
|
|
851
|
+
...current.desktop,
|
|
852
|
+
...incomingDesktop,
|
|
853
|
+
expandedProviders: hasOwn(incomingDesktop, "expandedProviders") ? incomingDesktop.expandedProviders : current.desktop.expandedProviders,
|
|
854
|
+
graphRangeDays: hasOwn(incomingDesktop, "graphRangeDays") ? incomingDesktop.graphRangeDays : current.desktop.graphRangeDays
|
|
855
|
+
} : current.desktop
|
|
856
|
+
};
|
|
857
|
+
}
|
|
669
858
|
async function applyConfigUpdateUnlocked(engine, state, input) {
|
|
670
859
|
if (input.expectedRevision !== state.config.revision) {
|
|
671
860
|
throw new ConfigConflictError(toConfigState(state.config));
|
|
672
861
|
}
|
|
673
862
|
const normalized = normalizeConfig({
|
|
674
|
-
...input.config,
|
|
863
|
+
...mergeCapabilityFields(input.config, state.config),
|
|
675
864
|
revision: state.config.revision + 1
|
|
676
865
|
});
|
|
677
|
-
const
|
|
866
|
+
const reconfigureEngine = configAffectsEngine(state.config, normalized);
|
|
867
|
+
const engineConfig = reconfigureEngine ? await resolveEngineConfig(normalized) : null;
|
|
678
868
|
try {
|
|
679
869
|
await saveConfig(normalized);
|
|
680
870
|
} catch (error) {
|
|
681
871
|
throw new ConfigPersistenceError(error);
|
|
682
872
|
}
|
|
683
873
|
state.config = normalized;
|
|
684
|
-
engine.setConfig(engineConfig);
|
|
874
|
+
if (engineConfig) engine.setConfig(engineConfig);
|
|
685
875
|
engine.broadcastConfig(normalized);
|
|
686
876
|
return toConfigState(normalized);
|
|
687
877
|
}
|
|
@@ -703,11 +893,11 @@ import { Effect, Exit, Layer, Queue, Scope, Stream } from "effect";
|
|
|
703
893
|
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
704
894
|
|
|
705
895
|
// src/web/fs.ts
|
|
706
|
-
import { readdir, stat, realpath } from "fs/promises";
|
|
896
|
+
import { readdir, stat as stat2, realpath } from "fs/promises";
|
|
707
897
|
import { homedir } from "os";
|
|
708
|
-
import { join as
|
|
898
|
+
import { join as join3, resolve as resolvePath, isAbsolute, sep as sep2 } from "path";
|
|
709
899
|
function isContained(root, target) {
|
|
710
|
-
return target === root || target.startsWith(root +
|
|
900
|
+
return target === root || target.startsWith(root + sep2);
|
|
711
901
|
}
|
|
712
902
|
function parentFor(root, abs) {
|
|
713
903
|
const parentResolved = resolvePath(abs, "..");
|
|
@@ -727,7 +917,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
727
917
|
const abs = isContained(root, real) ? real : root;
|
|
728
918
|
let st;
|
|
729
919
|
try {
|
|
730
|
-
st = await
|
|
920
|
+
st = await stat2(abs);
|
|
731
921
|
} catch {
|
|
732
922
|
return { path: abs, parent: parentFor(root, abs), entries: [] };
|
|
733
923
|
}
|
|
@@ -742,7 +932,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
742
932
|
for (const d of dirents) {
|
|
743
933
|
if (d.name.startsWith(".")) continue;
|
|
744
934
|
let dir = d.isDirectory();
|
|
745
|
-
const full =
|
|
935
|
+
const full = join3(abs, d.name);
|
|
746
936
|
if (d.isSymbolicLink()) {
|
|
747
937
|
let real2;
|
|
748
938
|
try {
|
|
@@ -752,7 +942,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
752
942
|
}
|
|
753
943
|
if (!isContained(root, real2)) continue;
|
|
754
944
|
try {
|
|
755
|
-
dir = (await
|
|
945
|
+
dir = (await stat2(full)).isDirectory();
|
|
756
946
|
} catch {
|
|
757
947
|
continue;
|
|
758
948
|
}
|
|
@@ -847,27 +1037,45 @@ function configStream(engine) {
|
|
|
847
1037
|
yield* Scope.addFinalizer(scope, Effect.sync(unsubscribe));
|
|
848
1038
|
}), { bufferSize: 16, strategy: "sliding" });
|
|
849
1039
|
}
|
|
850
|
-
function
|
|
1040
|
+
function configUpdateFailureEffect(error) {
|
|
851
1041
|
if (error instanceof ConfigConflictError) {
|
|
852
|
-
return { kind: "conflict", state: error.state };
|
|
1042
|
+
return Effect.fail(new ConfigUpdateConflictFailure({ kind: "conflict", state: error.state }));
|
|
853
1043
|
}
|
|
854
1044
|
if (error instanceof ConfigPersistenceError) {
|
|
855
|
-
return { kind: "persistence", message: error.message };
|
|
1045
|
+
return Effect.fail(new ConfigPersistenceFailure({ kind: "persistence", message: error.message }));
|
|
856
1046
|
}
|
|
857
|
-
return
|
|
1047
|
+
return Effect.die(error);
|
|
1048
|
+
}
|
|
1049
|
+
function configUpdateEffect(engine, state, input) {
|
|
1050
|
+
return Effect.tryPromise({
|
|
1051
|
+
try: () => applyConfigUpdate(engine, state, input),
|
|
1052
|
+
catch: (error) => error
|
|
1053
|
+
}).pipe(Effect.matchEffect({
|
|
1054
|
+
onFailure: configUpdateFailureEffect,
|
|
1055
|
+
onSuccess: Effect.succeed
|
|
1056
|
+
}));
|
|
1057
|
+
}
|
|
1058
|
+
function refreshFailureEffect(error) {
|
|
1059
|
+
return error instanceof AggregateError ? Effect.fail(new RefreshFailure({ kind: "refresh", message: error.message })) : Effect.die(error);
|
|
1060
|
+
}
|
|
1061
|
+
function refreshEffect(engine, scope) {
|
|
1062
|
+
return Effect.tryPromise({
|
|
1063
|
+
try: () => engine.refresh(scope),
|
|
1064
|
+
catch: (error) => error
|
|
1065
|
+
}).pipe(Effect.matchEffect({
|
|
1066
|
+
onFailure: refreshFailureEffect,
|
|
1067
|
+
onSuccess: Effect.succeed
|
|
1068
|
+
}));
|
|
858
1069
|
}
|
|
859
1070
|
async function mountWsRpc(server, deps) {
|
|
860
1071
|
const scope = await Effect.runPromise(Scope.make());
|
|
861
1072
|
const wss = new NodeWS.WebSocketServer({ noServer: true });
|
|
862
1073
|
const handlersLayer = TokmonRpcGroup.toLayer(
|
|
863
1074
|
TokmonRpcGroup.of({
|
|
864
|
-
[TOKMON_WS_METHODS.getConfig]: () => Effect.
|
|
865
|
-
[TOKMON_WS_METHODS.setConfig]: (config) =>
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}),
|
|
869
|
-
[TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => Effect.promise(() => deps.engine.refresh(scope2)),
|
|
870
|
-
[TOKMON_WS_METHODS.browseFs]: ({ path }) => Effect.tryPromise(() => listHomeDirectory(path)),
|
|
1075
|
+
[TOKMON_WS_METHODS.getConfig]: () => Effect.promise(() => Promise.resolve(deps.state.config ?? loadConfig()).then(toConfigState)),
|
|
1076
|
+
[TOKMON_WS_METHODS.setConfig]: (config) => configUpdateEffect(deps.engine, deps.state, config),
|
|
1077
|
+
[TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => refreshEffect(deps.engine, scope2),
|
|
1078
|
+
[TOKMON_WS_METHODS.browseFs]: ({ path }) => Effect.promise(() => listHomeDirectory(path)),
|
|
871
1079
|
[TOKMON_WS_METHODS.snapshot]: () => snapshotStream(deps.engine),
|
|
872
1080
|
[TOKMON_WS_METHODS.config]: () => configStream(deps.engine).pipe(Stream.map(toConfigState))
|
|
873
1081
|
})
|
|
@@ -957,7 +1165,7 @@ function guardSameOrigin(req, res) {
|
|
|
957
1165
|
}
|
|
958
1166
|
return true;
|
|
959
1167
|
}
|
|
960
|
-
function createRouter(engine, state, vite, webRoot, wsToken, version) {
|
|
1168
|
+
function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVersion, capabilities, ownerKind, channel) {
|
|
961
1169
|
return (req, res) => {
|
|
962
1170
|
const url = req.url || "/";
|
|
963
1171
|
const path = url.split("?")[0];
|
|
@@ -973,6 +1181,10 @@ function createRouter(engine, state, vite, webRoot, wsToken, version) {
|
|
|
973
1181
|
ok: true,
|
|
974
1182
|
ready: engine.snapshot() !== null,
|
|
975
1183
|
version,
|
|
1184
|
+
protocolVersion,
|
|
1185
|
+
capabilities,
|
|
1186
|
+
ownerKind,
|
|
1187
|
+
channel,
|
|
976
1188
|
// Discovery requires this proof; public health checks retain their useful 200 response.
|
|
977
1189
|
owner: tokenMatches(header2(req, "x-tokmon-token"), wsToken)
|
|
978
1190
|
});
|
|
@@ -1003,7 +1215,11 @@ function createRouter(engine, state, vite, webRoot, wsToken, version) {
|
|
|
1003
1215
|
async function startWebServer(opts) {
|
|
1004
1216
|
const state = { config: opts.config };
|
|
1005
1217
|
const tz = tzFor(state.config);
|
|
1006
|
-
const version = appVersion();
|
|
1218
|
+
const version = opts.version ?? appVersion();
|
|
1219
|
+
const protocolVersion = opts.protocolVersion ?? TOKMON_PROTOCOL_VERSION;
|
|
1220
|
+
const capabilities = opts.capabilities ?? TOKMON_CAPABILITIES;
|
|
1221
|
+
const ownerKind = opts.ownerKind ?? "cli";
|
|
1222
|
+
const channel = resolveDaemonChannel(opts.channel);
|
|
1007
1223
|
const summaryIntervalMs = summaryIntervalFor(state.config);
|
|
1008
1224
|
const billingIntervalMs = billingIntervalFor(state.config);
|
|
1009
1225
|
const wsToken = opts.wsToken ?? randomBytes(32).toString("base64url");
|
|
@@ -1017,10 +1233,21 @@ async function startWebServer(opts) {
|
|
|
1017
1233
|
let closeWsRpc = null;
|
|
1018
1234
|
try {
|
|
1019
1235
|
if (isDevMode()) vite = await createViteDevServer(server, log);
|
|
1020
|
-
const webRoot = vite ? null : findWebRoot();
|
|
1236
|
+
const webRoot = vite ? null : opts.webRoot ?? findWebRoot();
|
|
1021
1237
|
if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
|
|
1022
1238
|
engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
|
|
1023
|
-
server.addListener("request", createRouter(
|
|
1239
|
+
server.addListener("request", createRouter(
|
|
1240
|
+
engine,
|
|
1241
|
+
state,
|
|
1242
|
+
vite,
|
|
1243
|
+
webRoot,
|
|
1244
|
+
wsToken,
|
|
1245
|
+
version,
|
|
1246
|
+
protocolVersion,
|
|
1247
|
+
capabilities,
|
|
1248
|
+
ownerKind,
|
|
1249
|
+
channel
|
|
1250
|
+
));
|
|
1024
1251
|
closeWsRpc = await mountWsRpc(server, { engine, state });
|
|
1025
1252
|
const bindHost = state.config.allowNetworkAccess ? NETWORK_HOST : LOOPBACK_HOST;
|
|
1026
1253
|
const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT, bindHost);
|
|
@@ -1116,5 +1343,6 @@ function listenWithFallback(server, startPort, host) {
|
|
|
1116
1343
|
}
|
|
1117
1344
|
|
|
1118
1345
|
export {
|
|
1346
|
+
appVersion,
|
|
1119
1347
|
startWebServer
|
|
1120
1348
|
};
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
isAlive,
|
|
4
|
-
probeHealth,
|
|
5
3
|
readLock,
|
|
6
|
-
|
|
4
|
+
retireIncompatibleCliOwner,
|
|
7
5
|
verifyLock
|
|
8
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-USKOQIE3.js";
|
|
9
7
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
daemonChannelFromWire,
|
|
9
|
+
resolveDaemonChannel
|
|
10
|
+
} from "./chunk-UYPDMVW5.js";
|
|
11
|
+
import {
|
|
12
|
+
TOKMON_PROTOCOL_VERSION
|
|
13
|
+
} from "./chunk-JZSZEHVD.js";
|
|
12
14
|
|
|
13
15
|
// src/client/daemon-handle.ts
|
|
14
16
|
import { spawn } from "child_process";
|
|
15
17
|
import { extname } from "path";
|
|
16
18
|
var HANDSHAKE_TIMEOUT_MS = process.platform === "win32" ? 15e3 : 1e4;
|
|
17
|
-
var UPGRADE_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
18
19
|
function runtimeExecArgv(entry, override) {
|
|
19
20
|
if (override) return override;
|
|
20
21
|
const ext = extname(entry).toLowerCase();
|
|
@@ -37,7 +38,7 @@ function runtimeExecArgv(entry, override) {
|
|
|
37
38
|
function parseHandshake(line) {
|
|
38
39
|
try {
|
|
39
40
|
const value = JSON.parse(line);
|
|
40
|
-
return value?.ready === 1 && typeof value.url === "string" && typeof value.wsToken === "string" && typeof value.version === "string" ? value : null;
|
|
41
|
+
return value?.ready === 1 && typeof value.url === "string" && typeof value.wsToken === "string" && typeof value.version === "string" && typeof value.protocolVersion === "number" && Array.isArray(value.capabilities) && value.capabilities.every((capability) => typeof capability === "string") && (value.ownerKind === "cli" || value.ownerKind === "desktop") && daemonChannelFromWire(value.channel) !== null ? { ...value, channel: daemonChannelFromWire(value.channel) } : null;
|
|
41
42
|
} catch {
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
@@ -46,52 +47,30 @@ function connected(url) {
|
|
|
46
47
|
return { kind: "spawned", baseUrl: url, stop: () => {
|
|
47
48
|
} };
|
|
48
49
|
}
|
|
49
|
-
async function attach(opts,
|
|
50
|
-
const lock = await verifyLock(readLock(opts),
|
|
50
|
+
async function attach(opts, protocolVersion) {
|
|
51
|
+
const lock = await verifyLock(readLock(opts), protocolVersion);
|
|
51
52
|
return lock ? connected(lock.url) : null;
|
|
52
53
|
}
|
|
53
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
-
async function retireIncompatibleDaemon(opts, version, timeoutMs) {
|
|
55
|
-
const lock = readLock(opts);
|
|
56
|
-
if (!lock || lock.state !== "ready" || lock.version === version || lock.pid === process.pid || !isAlive(lock.pid)) return;
|
|
57
|
-
const authenticated = await probeHealth(
|
|
58
|
-
lock.url,
|
|
59
|
-
lock.wsToken,
|
|
60
|
-
lock.version,
|
|
61
|
-
Math.min(1e3, timeoutMs)
|
|
62
|
-
);
|
|
63
|
-
if (!authenticated) return;
|
|
64
|
-
try {
|
|
65
|
-
process.kill(lock.pid, "SIGTERM");
|
|
66
|
-
} catch {
|
|
67
|
-
if (!isAlive(lock.pid)) reclaimDeadLock(opts);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const deadline = Date.now() + Math.min(timeoutMs, UPGRADE_SHUTDOWN_TIMEOUT_MS);
|
|
71
|
-
while (Date.now() < deadline) {
|
|
72
|
-
const current = readLock(opts);
|
|
73
|
-
if (!current || current.ownerId !== lock.ownerId) return;
|
|
74
|
-
if (!isAlive(lock.pid)) {
|
|
75
|
-
reclaimDeadLock(opts);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
await delay(50);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
54
|
async function attachOrSpawn(opts = {}) {
|
|
82
|
-
const
|
|
55
|
+
const protocolVersion = TOKMON_PROTOCOL_VERSION;
|
|
83
56
|
const timeoutMs = opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS;
|
|
84
|
-
const
|
|
57
|
+
const baseEnv = opts.env ?? process.env;
|
|
58
|
+
const channel = resolveDaemonChannel(opts.channel, baseEnv);
|
|
59
|
+
const lockOpts = { cachePath: opts.cachePath, channel };
|
|
60
|
+
const existing = await attach(lockOpts, protocolVersion);
|
|
85
61
|
if (existing) return existing;
|
|
86
|
-
await
|
|
87
|
-
const upgraded = await attach(
|
|
62
|
+
await retireIncompatibleCliOwner(lockOpts, protocolVersion, timeoutMs);
|
|
63
|
+
const upgraded = await attach(lockOpts, protocolVersion);
|
|
88
64
|
if (upgraded) return upgraded;
|
|
89
65
|
const entry = opts.entry ?? process.argv[1];
|
|
90
66
|
if (!entry) return degraded();
|
|
91
67
|
const execPath = opts.execPath ?? process.execPath;
|
|
92
68
|
const args = ["__daemon", "--port", "0", "--no-open"];
|
|
93
|
-
const
|
|
94
|
-
|
|
69
|
+
const env = {
|
|
70
|
+
...baseEnv,
|
|
71
|
+
TOKMON_CHANNEL: channel,
|
|
72
|
+
...opts.cachePath ? { TOKMON_DAEMON_CACHE_DIR: opts.cachePath } : {}
|
|
73
|
+
};
|
|
95
74
|
return new Promise((resolve) => {
|
|
96
75
|
let child;
|
|
97
76
|
try {
|
|
@@ -117,7 +96,7 @@ async function attachOrSpawn(opts = {}) {
|
|
|
117
96
|
resolve(handle);
|
|
118
97
|
};
|
|
119
98
|
const tryAttach = (final = false) => {
|
|
120
|
-
void attach(
|
|
99
|
+
void attach(lockOpts, protocolVersion).then((found) => {
|
|
121
100
|
if (found) {
|
|
122
101
|
finish(found);
|
|
123
102
|
return;
|
|
@@ -143,7 +122,7 @@ async function attachOrSpawn(opts = {}) {
|
|
|
143
122
|
const line = stdout.slice(0, newline).trim();
|
|
144
123
|
stdout = stdout.slice(newline + 1);
|
|
145
124
|
const handshake = parseHandshake(line);
|
|
146
|
-
if (!handshake || handshake.
|
|
125
|
+
if (!handshake || handshake.protocolVersion !== protocolVersion || handshake.channel !== channel) continue;
|
|
147
126
|
tryAttach();
|
|
148
127
|
return;
|
|
149
128
|
}
|