whoburnedmore 0.8.7 → 0.9.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/README.md +6 -2
- package/dist/index.js +615 -64
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npx whoburnedmore --local
|
|
|
62
62
|
| `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
|
|
63
63
|
| `npx whoburnedmore login` | Sign in to claim a public handle + join the leaderboard |
|
|
64
64
|
| `npx whoburnedmore logout` | Forget the local token (your data is untouched) |
|
|
65
|
-
| `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (
|
|
65
|
+
| `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (every 15 min) |
|
|
66
66
|
| `npx whoburnedmore uninstall-sync` | Remove the background sync |
|
|
67
67
|
|
|
68
68
|
## Supported tools
|
|
@@ -88,10 +88,14 @@ It uses a device flow — a code appears in your terminal, you approve it in the
|
|
|
88
88
|
Want your dashboard to stay fresh without re-running by hand?
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
npx whoburnedmore install-sync # background sync
|
|
91
|
+
npx whoburnedmore install-sync # background sync every 15 min (launchd / cron / scheduled task)
|
|
92
92
|
npx whoburnedmore uninstall-sync # remove it
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
The installed background job runs the latest published `whoburnedmore` package on
|
|
96
|
+
each sync tick, so future CLI fixes are picked up automatically after the next
|
|
97
|
+
15-minute refresh.
|
|
98
|
+
|
|
95
99
|
## Links
|
|
96
100
|
|
|
97
101
|
- 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,29 @@ import pc2 from "picocolors";
|
|
|
16
16
|
|
|
17
17
|
// src/args.ts
|
|
18
18
|
function parseBoard(args) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
return parseValueFlag(args, "--board");
|
|
20
|
+
}
|
|
21
|
+
function parseOrg(args) {
|
|
22
|
+
return parseValueFlag(args, "--org");
|
|
23
|
+
}
|
|
24
|
+
function parsePass(args) {
|
|
25
|
+
return parseValueFlag(args, "--pass") ?? parseValueFlag(args, "--code");
|
|
26
|
+
}
|
|
27
|
+
function parseInstallToken(args) {
|
|
28
|
+
return parseValueFlag(args, "--token");
|
|
29
|
+
}
|
|
30
|
+
function applyScope(payload, flags) {
|
|
31
|
+
if (flags.board) payload.board = flags.board;
|
|
32
|
+
if (flags.org) payload.org = flags.org;
|
|
33
|
+
if (flags.org && flags.orgCode)
|
|
34
|
+
payload.orgCode = flags.orgCode;
|
|
35
|
+
return payload;
|
|
36
|
+
}
|
|
37
|
+
function parseValueFlag(args, name) {
|
|
38
|
+
const prefix = `${name}=`;
|
|
39
|
+
const eq = args.find((a) => a.startsWith(prefix));
|
|
40
|
+
if (eq) return eq.slice(prefix.length).trim() || void 0;
|
|
41
|
+
const i = args.indexOf(name);
|
|
22
42
|
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
23
43
|
return args[i + 1].trim() || void 0;
|
|
24
44
|
}
|
|
@@ -31,7 +51,16 @@ function resolveCommand(args) {
|
|
|
31
51
|
if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
|
|
32
52
|
return "version";
|
|
33
53
|
}
|
|
34
|
-
|
|
54
|
+
const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token", "--pass", "--code"]);
|
|
55
|
+
for (let i = 0; i < args.length; i++) {
|
|
56
|
+
const a = args[i];
|
|
57
|
+
if (a.startsWith("-")) {
|
|
58
|
+
if (valueFlags.has(a)) i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
return a;
|
|
62
|
+
}
|
|
63
|
+
return "run";
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
// src/api.ts
|
|
@@ -41,6 +70,20 @@ function apiBase() {
|
|
|
41
70
|
function webBase() {
|
|
42
71
|
return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
|
|
43
72
|
}
|
|
73
|
+
function isTrustedWebUrl(url) {
|
|
74
|
+
let u;
|
|
75
|
+
let base;
|
|
76
|
+
try {
|
|
77
|
+
u = new URL(url);
|
|
78
|
+
base = new URL(webBase());
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.protocol === base.protocol && u.host === base.host;
|
|
83
|
+
}
|
|
84
|
+
function isOpenableUrl(url) {
|
|
85
|
+
return /^(https?|file):\/\//.test(url);
|
|
86
|
+
}
|
|
44
87
|
async function readJson(res) {
|
|
45
88
|
const text = await res.text();
|
|
46
89
|
if (!text) return {};
|
|
@@ -83,6 +126,11 @@ function claimUrl(dashboardUrl, anonKey) {
|
|
|
83
126
|
function boardClaimUrl(boardUrl, slug, anonKey) {
|
|
84
127
|
return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
|
|
85
128
|
}
|
|
129
|
+
function resolveOpenTarget(result, anonKey) {
|
|
130
|
+
const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.dashboardUrl;
|
|
131
|
+
const target = result.orgBoardUrl ? boardClaimUrl(result.orgBoardUrl, result.slug, anonKey) : result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
|
|
132
|
+
return { baseUrl, target };
|
|
133
|
+
}
|
|
86
134
|
async function anonVisibility(anonKey, listed) {
|
|
87
135
|
const { status, body } = await post(
|
|
88
136
|
"/v1/anon/visibility",
|
|
@@ -103,6 +151,15 @@ async function anonRemove(anonKey) {
|
|
|
103
151
|
throw new Error(b.error ?? `failed (HTTP ${res.status})`);
|
|
104
152
|
}
|
|
105
153
|
}
|
|
154
|
+
async function redeemServerInstall(token, anonKey) {
|
|
155
|
+
const { status, body } = await post("/v1/server-install/redeem", { token, anonKey });
|
|
156
|
+
if (status !== 200) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
body.error ?? `server install failed (HTTP ${status})`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return body;
|
|
162
|
+
}
|
|
106
163
|
|
|
107
164
|
// src/autosync.ts
|
|
108
165
|
import { spawnSync } from "node:child_process";
|
|
@@ -116,8 +173,7 @@ import {
|
|
|
116
173
|
writeFileSync as writeFileSync2
|
|
117
174
|
} from "node:fs";
|
|
118
175
|
import { homedir as homedir2, platform } from "node:os";
|
|
119
|
-
import { join as join2 } from "node:path";
|
|
120
|
-
import { fileURLToPath } from "node:url";
|
|
176
|
+
import { dirname, join as join2, win32 } from "node:path";
|
|
121
177
|
|
|
122
178
|
// src/config.ts
|
|
123
179
|
import { randomBytes } from "node:crypto";
|
|
@@ -131,6 +187,8 @@ import {
|
|
|
131
187
|
import { homedir } from "node:os";
|
|
132
188
|
import { join } from "node:path";
|
|
133
189
|
function defaultConfigDir() {
|
|
190
|
+
const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
|
|
191
|
+
if (override) return override;
|
|
134
192
|
return join(homedir(), ".config", "whoburnedmore");
|
|
135
193
|
}
|
|
136
194
|
function loadConfig(dir = defaultConfigDir()) {
|
|
@@ -142,6 +200,9 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
142
200
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
143
201
|
if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
|
|
144
202
|
config.lastSyncAt = parsed.lastSyncAt;
|
|
203
|
+
if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
|
|
204
|
+
config.launchNotificationDeliveredAt = parsed.launchNotificationDeliveredAt;
|
|
205
|
+
}
|
|
145
206
|
return Object.keys(config).length > 0 ? config : null;
|
|
146
207
|
} catch {
|
|
147
208
|
return null;
|
|
@@ -167,19 +228,33 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
|
|
|
167
228
|
const config = loadConfig(dir) ?? {};
|
|
168
229
|
saveConfig(dir, { ...config, lastSyncAt: when });
|
|
169
230
|
}
|
|
231
|
+
function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
|
|
232
|
+
const config = loadConfig(dir) ?? {};
|
|
233
|
+
saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
|
|
234
|
+
}
|
|
170
235
|
|
|
171
236
|
// src/autosync.ts
|
|
172
|
-
var
|
|
237
|
+
var SYNC_INTERVAL_MINUTES = 15;
|
|
238
|
+
function syncIntervalLabel(mins = SYNC_INTERVAL_MINUTES) {
|
|
239
|
+
return mins % 60 === 0 ? `${mins / 60}h` : `${mins}m`;
|
|
240
|
+
}
|
|
173
241
|
var LABEL = "com.whoburnedmore.sync";
|
|
174
242
|
var STABLE_NODE_CANDIDATES = [
|
|
175
243
|
"/opt/homebrew/bin/node",
|
|
176
244
|
"/usr/local/bin/node",
|
|
177
245
|
"/usr/bin/node"
|
|
178
246
|
];
|
|
247
|
+
var STABLE_NPM_CANDIDATES = [
|
|
248
|
+
"/opt/homebrew/bin/npm",
|
|
249
|
+
"/usr/local/bin/npm",
|
|
250
|
+
"/usr/bin/npm"
|
|
251
|
+
];
|
|
252
|
+
var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
|
|
179
253
|
function syncLogPath() {
|
|
180
254
|
return join2(defaultConfigDir(), "sync.log");
|
|
181
255
|
}
|
|
182
|
-
function buildLaunchdPlist(
|
|
256
|
+
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
|
|
257
|
+
const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
183
258
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
184
259
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
185
260
|
<plist version="1.0">
|
|
@@ -188,12 +263,10 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
188
263
|
<string>${LABEL}</string>
|
|
189
264
|
<key>ProgramArguments</key>
|
|
190
265
|
<array>
|
|
191
|
-
|
|
192
|
-
<string>${scriptPath}</string>
|
|
193
|
-
<string>sync</string>
|
|
266
|
+
${programArguments}
|
|
194
267
|
</array>
|
|
195
268
|
<key>StartInterval</key>
|
|
196
|
-
<integer>${
|
|
269
|
+
<integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
|
|
197
270
|
<!-- Run once right after login/reboot so a machine that was off (or asleep)
|
|
198
271
|
through a scheduled tick catches up immediately, then keeps to the
|
|
199
272
|
interval. Submits are idempotent server-side, so an extra run is safe. -->
|
|
@@ -203,9 +276,9 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
203
276
|
<key>ProcessType</key>
|
|
204
277
|
<string>Background</string>
|
|
205
278
|
<key>StandardOutPath</key>
|
|
206
|
-
<string>${logPath}</string>
|
|
279
|
+
<string>${xmlEscape(logPath)}</string>
|
|
207
280
|
<key>StandardErrorPath</key>
|
|
208
|
-
<string>${logPath}</string>
|
|
281
|
+
<string>${xmlEscape(logPath)}</string>
|
|
209
282
|
</dict>
|
|
210
283
|
</plist>
|
|
211
284
|
`;
|
|
@@ -213,9 +286,6 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
213
286
|
function launchAgentPath() {
|
|
214
287
|
return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
215
288
|
}
|
|
216
|
-
function cliScriptPath() {
|
|
217
|
-
return fileURLToPath(new URL("./index.js", import.meta.url));
|
|
218
|
-
}
|
|
219
289
|
function isUsableNode(p) {
|
|
220
290
|
if (!existsSync2(p)) return false;
|
|
221
291
|
const res = spawnSync(p, ["-v"], { encoding: "utf8" });
|
|
@@ -232,8 +302,51 @@ function resolveNodePath(opts) {
|
|
|
232
302
|
}
|
|
233
303
|
return execPath;
|
|
234
304
|
}
|
|
305
|
+
function isUsableNpm(p) {
|
|
306
|
+
if (!existsSync2(p)) return false;
|
|
307
|
+
const res = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
308
|
+
return res.status === 0;
|
|
309
|
+
}
|
|
310
|
+
function resolveNpmPath(opts) {
|
|
311
|
+
const candidates = opts?.candidates ?? STABLE_NPM_CANDIDATES;
|
|
312
|
+
const check = opts?.check ?? isUsableNpm;
|
|
313
|
+
const execPath = opts?.execPath ?? process.execPath;
|
|
314
|
+
const os = opts?.platform ?? platform();
|
|
315
|
+
for (const c of candidates) {
|
|
316
|
+
if (check(c)) return c;
|
|
317
|
+
}
|
|
318
|
+
const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : join2(dirname(execPath), "npm");
|
|
319
|
+
if (check(sibling)) return sibling;
|
|
320
|
+
return os === "win32" ? "npm.cmd" : "npm";
|
|
321
|
+
}
|
|
322
|
+
function syncCommandArgs(npmPath = resolveNpmPath()) {
|
|
323
|
+
return [
|
|
324
|
+
npmPath,
|
|
325
|
+
"exec",
|
|
326
|
+
"--yes",
|
|
327
|
+
"--ignore-scripts",
|
|
328
|
+
"--package",
|
|
329
|
+
LATEST_PACKAGE_SPEC,
|
|
330
|
+
"--",
|
|
331
|
+
"whoburnedmore",
|
|
332
|
+
"sync"
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
function xmlEscape(value) {
|
|
336
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """).replaceAll("'", "'").replaceAll(">", ">");
|
|
337
|
+
}
|
|
338
|
+
function shellQuote(value) {
|
|
339
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
340
|
+
}
|
|
341
|
+
function windowsQuote(value) {
|
|
342
|
+
const escaped = value.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, (slashes) => `${slashes}${slashes}`);
|
|
343
|
+
return `"${escaped}"`;
|
|
344
|
+
}
|
|
345
|
+
function windowsCommandLine(args) {
|
|
346
|
+
return args.map(windowsQuote).join(" ");
|
|
347
|
+
}
|
|
235
348
|
function expectedDarwinPlist() {
|
|
236
|
-
return buildLaunchdPlist(
|
|
349
|
+
return buildLaunchdPlist(syncCommandArgs());
|
|
237
350
|
}
|
|
238
351
|
function plistDrift(installed, expected) {
|
|
239
352
|
if (installed === null) return "absent";
|
|
@@ -251,40 +364,125 @@ function installAutoSync() {
|
|
|
251
364
|
writeFileSync2(plistPath, expectedDarwinPlist());
|
|
252
365
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
253
366
|
spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
|
|
254
|
-
return `launchd agent installed (${plistPath}), syncing every ${
|
|
367
|
+
return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
|
|
255
368
|
}
|
|
256
369
|
if (os === "linux") {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const res = spawnSync("crontab", ["-"], { input: next });
|
|
265
|
-
if (res.status !== 0) throw new Error("could not install crontab entry");
|
|
266
|
-
return `cron entry installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
|
|
370
|
+
const viaCron = tryInstallCron();
|
|
371
|
+
if (viaCron) return viaCron;
|
|
372
|
+
const viaSystemd = tryInstallSystemd();
|
|
373
|
+
if (viaSystemd) return viaSystemd;
|
|
374
|
+
throw new Error(
|
|
375
|
+
"could not install background sync: no usable crontab or systemd user timer. Run `whoburnedmore daemon` under your process manager (systemd service, Docker CMD, pm2 or nohup) to keep syncing."
|
|
376
|
+
);
|
|
267
377
|
}
|
|
268
378
|
if (os === "win32") {
|
|
269
379
|
const res = spawnSync("schtasks", [
|
|
270
380
|
"/Create",
|
|
271
381
|
"/F",
|
|
272
382
|
"/SC",
|
|
273
|
-
"
|
|
383
|
+
"MINUTE",
|
|
274
384
|
"/MO",
|
|
275
|
-
String(
|
|
385
|
+
String(SYNC_INTERVAL_MINUTES),
|
|
276
386
|
"/TN",
|
|
277
387
|
"whoburnedmore-sync",
|
|
278
388
|
"/TR",
|
|
279
|
-
|
|
389
|
+
windowsCommandLine(syncCommandArgs())
|
|
280
390
|
]);
|
|
281
391
|
if (res.status !== 0) throw new Error("could not create scheduled task");
|
|
282
|
-
return `scheduled task installed, syncing every ${
|
|
392
|
+
return `scheduled task installed, syncing every ${syncIntervalLabel()}`;
|
|
283
393
|
}
|
|
284
394
|
throw new Error(`auto-sync is not supported on ${os}`);
|
|
285
395
|
}
|
|
286
|
-
function
|
|
287
|
-
return `0 */${
|
|
396
|
+
function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
|
|
397
|
+
return mins % 60 === 0 ? `0 */${mins / 60} * * *` : `*/${mins} * * * *`;
|
|
398
|
+
}
|
|
399
|
+
function expectedLinuxCronLine(opts) {
|
|
400
|
+
const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
|
|
401
|
+
return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
|
|
402
|
+
}
|
|
403
|
+
var SYSTEMD_UNIT = "whoburnedmore-sync";
|
|
404
|
+
function systemdUserDir() {
|
|
405
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || join2(homedir2(), ".config");
|
|
406
|
+
return join2(base, "systemd", "user");
|
|
407
|
+
}
|
|
408
|
+
function systemdServicePath() {
|
|
409
|
+
return join2(systemdUserDir(), `${SYSTEMD_UNIT}.service`);
|
|
410
|
+
}
|
|
411
|
+
function systemdTimerPath() {
|
|
412
|
+
return join2(systemdUserDir(), `${SYSTEMD_UNIT}.timer`);
|
|
413
|
+
}
|
|
414
|
+
function systemdQuote(value) {
|
|
415
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
416
|
+
}
|
|
417
|
+
function buildSystemdService(commandArgs = syncCommandArgs()) {
|
|
418
|
+
const execStart = commandArgs.map(systemdQuote).join(" ");
|
|
419
|
+
return `[Unit]
|
|
420
|
+
Description=whoburnedmore background token-usage sync
|
|
421
|
+
|
|
422
|
+
[Service]
|
|
423
|
+
Type=oneshot
|
|
424
|
+
ExecStart=${execStart}
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
function buildSystemdTimer(mins = SYNC_INTERVAL_MINUTES) {
|
|
428
|
+
return `[Unit]
|
|
429
|
+
Description=whoburnedmore background token-usage sync timer
|
|
430
|
+
|
|
431
|
+
[Timer]
|
|
432
|
+
OnBootSec=1min
|
|
433
|
+
OnUnitActiveSec=${mins}min
|
|
434
|
+
Persistent=true
|
|
435
|
+
|
|
436
|
+
[Install]
|
|
437
|
+
WantedBy=timers.target
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
function binaryExists(cmd, probeArgs = ["--version"]) {
|
|
441
|
+
const res = spawnSync(cmd, probeArgs, { stdio: "ignore" });
|
|
442
|
+
return !res.error;
|
|
443
|
+
}
|
|
444
|
+
function linuxSyncMechanism() {
|
|
445
|
+
const cron = spawnSync("crontab", ["-l"], { encoding: "utf8" });
|
|
446
|
+
if (!cron.error && cron.status === 0 && cron.stdout.includes("whoburnedmore")) {
|
|
447
|
+
return "cron";
|
|
448
|
+
}
|
|
449
|
+
if (existsSync2(systemdTimerPath())) return "systemd";
|
|
450
|
+
return "none";
|
|
451
|
+
}
|
|
452
|
+
function tryInstallCron() {
|
|
453
|
+
if (!binaryExists("crontab", ["-l"])) return null;
|
|
454
|
+
const line = expectedLinuxCronLine();
|
|
455
|
+
const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
|
|
456
|
+
const existing = current.status === 0 ? current.stdout : "";
|
|
457
|
+
const kept = existing.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
|
|
458
|
+
const next = `${kept.trimEnd()}
|
|
459
|
+
${line}
|
|
460
|
+
`.replace(/^\n+/, "");
|
|
461
|
+
const res = spawnSync("crontab", ["-"], { input: next });
|
|
462
|
+
if (res.status !== 0) return null;
|
|
463
|
+
return `cron entry installed, syncing every ${syncIntervalLabel()}`;
|
|
464
|
+
}
|
|
465
|
+
function tryInstallSystemd() {
|
|
466
|
+
if (!binaryExists("systemctl", ["--user", "--version"])) return null;
|
|
467
|
+
try {
|
|
468
|
+
mkdirSync2(systemdUserDir(), { recursive: true });
|
|
469
|
+
writeFileSync2(systemdServicePath(), buildSystemdService());
|
|
470
|
+
writeFileSync2(systemdTimerPath(), buildSystemdTimer());
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
475
|
+
const res = spawnSync(
|
|
476
|
+
"systemctl",
|
|
477
|
+
["--user", "enable", "--now", `${SYSTEMD_UNIT}.timer`],
|
|
478
|
+
{ stdio: "ignore" }
|
|
479
|
+
);
|
|
480
|
+
if (res.status !== 0) {
|
|
481
|
+
rmSync(systemdServicePath(), { force: true });
|
|
482
|
+
rmSync(systemdTimerPath(), { force: true });
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
return `systemd user timer installed, syncing every ${syncIntervalLabel()} (run \`loginctl enable-linger\` to keep syncing while logged out)`;
|
|
288
486
|
}
|
|
289
487
|
function uninstallAutoSync() {
|
|
290
488
|
const os = platform();
|
|
@@ -297,12 +495,25 @@ function uninstallAutoSync() {
|
|
|
297
495
|
return "launchd agent removed";
|
|
298
496
|
}
|
|
299
497
|
if (os === "linux") {
|
|
498
|
+
let removed = false;
|
|
300
499
|
const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
|
|
301
|
-
if (current.status === 0 && current.stdout.includes("whoburnedmore")) {
|
|
500
|
+
if (!current.error && current.status === 0 && current.stdout.includes("whoburnedmore")) {
|
|
302
501
|
const next = current.stdout.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
|
|
303
502
|
spawnSync("crontab", ["-"], { input: next });
|
|
503
|
+
removed = true;
|
|
304
504
|
}
|
|
305
|
-
|
|
505
|
+
if (existsSync2(systemdTimerPath())) {
|
|
506
|
+
spawnSync(
|
|
507
|
+
"systemctl",
|
|
508
|
+
["--user", "disable", "--now", `${SYSTEMD_UNIT}.timer`],
|
|
509
|
+
{ stdio: "ignore" }
|
|
510
|
+
);
|
|
511
|
+
rmSync(systemdServicePath(), { force: true });
|
|
512
|
+
rmSync(systemdTimerPath(), { force: true });
|
|
513
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
514
|
+
removed = true;
|
|
515
|
+
}
|
|
516
|
+
return removed ? "background sync removed" : "nothing to remove";
|
|
306
517
|
}
|
|
307
518
|
if (os === "win32") {
|
|
308
519
|
spawnSync("schtasks", ["/Delete", "/F", "/TN", "whoburnedmore-sync"]);
|
|
@@ -313,8 +524,7 @@ function uninstallAutoSync() {
|
|
|
313
524
|
function autoSyncInstalled() {
|
|
314
525
|
if (platform() === "darwin") return existsSync2(launchAgentPath());
|
|
315
526
|
if (platform() === "linux") {
|
|
316
|
-
|
|
317
|
-
return current.status === 0 && current.stdout.includes("whoburnedmore");
|
|
527
|
+
return linuxSyncMechanism() !== "none";
|
|
318
528
|
}
|
|
319
529
|
if (platform() === "win32") {
|
|
320
530
|
const res = spawnSync("schtasks", ["/Query", "/TN", "whoburnedmore-sync"], {
|
|
@@ -324,22 +534,40 @@ function autoSyncInstalled() {
|
|
|
324
534
|
}
|
|
325
535
|
return false;
|
|
326
536
|
}
|
|
537
|
+
function readInstalledSystemd() {
|
|
538
|
+
try {
|
|
539
|
+
return `${readFileSync2(systemdServicePath(), "utf8")}
|
|
540
|
+
${readFileSync2(systemdTimerPath(), "utf8")}`;
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function expectedSystemd() {
|
|
546
|
+
return `${buildSystemdService()}
|
|
547
|
+
${buildSystemdTimer()}`;
|
|
548
|
+
}
|
|
327
549
|
function readInstalledAgent() {
|
|
328
550
|
if (platform() === "darwin") {
|
|
329
551
|
const p = launchAgentPath();
|
|
330
552
|
return existsSync2(p) ? readFileSync2(p, "utf8") : null;
|
|
331
553
|
}
|
|
332
554
|
if (platform() === "linux") {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
555
|
+
const mech = linuxSyncMechanism();
|
|
556
|
+
if (mech === "cron") {
|
|
557
|
+
const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
|
|
558
|
+
if (current.error || current.status !== 0) return null;
|
|
559
|
+
return current.stdout.split("\n").find((l) => l.includes("whoburnedmore")) ?? null;
|
|
560
|
+
}
|
|
561
|
+
if (mech === "systemd") return readInstalledSystemd();
|
|
562
|
+
return null;
|
|
337
563
|
}
|
|
338
564
|
return null;
|
|
339
565
|
}
|
|
340
566
|
function expectedAgent() {
|
|
341
567
|
if (platform() === "darwin") return expectedDarwinPlist();
|
|
342
|
-
if (platform() === "linux")
|
|
568
|
+
if (platform() === "linux") {
|
|
569
|
+
return linuxSyncMechanism() === "systemd" ? expectedSystemd() : expectedLinuxCronLine();
|
|
570
|
+
}
|
|
343
571
|
return null;
|
|
344
572
|
}
|
|
345
573
|
function autoSyncDrift() {
|
|
@@ -363,6 +591,41 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
363
591
|
return false;
|
|
364
592
|
}
|
|
365
593
|
}
|
|
594
|
+
function notifyLaunchLive(opts) {
|
|
595
|
+
const os = opts?.platform ?? platform();
|
|
596
|
+
const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
|
|
597
|
+
const title = "whoburnedmore is live";
|
|
598
|
+
const message = "Your dashboard is ready. Go to whoburnedmore.com";
|
|
599
|
+
if (os === "darwin") {
|
|
600
|
+
return run2("osascript", [
|
|
601
|
+
"-e",
|
|
602
|
+
`display notification "${message}" with title "${title}"`
|
|
603
|
+
]).status === 0;
|
|
604
|
+
}
|
|
605
|
+
if (os === "linux") {
|
|
606
|
+
return run2("notify-send", [title, message]).status === 0;
|
|
607
|
+
}
|
|
608
|
+
if (os === "win32") {
|
|
609
|
+
const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
610
|
+
const script = [
|
|
611
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
612
|
+
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
613
|
+
"$n.Icon = [System.Drawing.SystemIcons]::Application",
|
|
614
|
+
`$n.BalloonTipTitle = ${psQuote(title)}`,
|
|
615
|
+
`$n.BalloonTipText = ${psQuote(message)}`,
|
|
616
|
+
"$n.Visible = $true",
|
|
617
|
+
"$n.ShowBalloonTip(10000)",
|
|
618
|
+
"Start-Sleep -Seconds 2",
|
|
619
|
+
"$n.Dispose()"
|
|
620
|
+
].join("; ");
|
|
621
|
+
return run2("powershell", [
|
|
622
|
+
"-NoProfile",
|
|
623
|
+
"-Command",
|
|
624
|
+
script
|
|
625
|
+
]).status === 0;
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
366
629
|
function autoSyncLoaded() {
|
|
367
630
|
if (platform() === "darwin") {
|
|
368
631
|
const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
|
|
@@ -389,10 +652,28 @@ function printBanner() {
|
|
|
389
652
|
console.log();
|
|
390
653
|
}
|
|
391
654
|
|
|
655
|
+
// src/daemon.ts
|
|
656
|
+
async function daemonLoop(deps) {
|
|
657
|
+
let cycles = 0;
|
|
658
|
+
while (!deps.isStopped()) {
|
|
659
|
+
cycles++;
|
|
660
|
+
try {
|
|
661
|
+
await deps.runOnce();
|
|
662
|
+
deps.log("synced");
|
|
663
|
+
} catch (err) {
|
|
664
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
665
|
+
deps.log(`sync failed: ${message} \u2014 retrying next cycle`);
|
|
666
|
+
}
|
|
667
|
+
if (deps.isStopped()) break;
|
|
668
|
+
await deps.wait(deps.intervalMs);
|
|
669
|
+
}
|
|
670
|
+
return cycles;
|
|
671
|
+
}
|
|
672
|
+
|
|
392
673
|
// src/collect.ts
|
|
393
674
|
import { execFile } from "node:child_process";
|
|
394
675
|
import { createRequire as createRequire3 } from "node:module";
|
|
395
|
-
import { dirname as
|
|
676
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
396
677
|
import { promisify } from "node:util";
|
|
397
678
|
|
|
398
679
|
// src/attribution.ts
|
|
@@ -751,7 +1032,7 @@ import { join as join5 } from "node:path";
|
|
|
751
1032
|
// src/tokscale.ts
|
|
752
1033
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
753
1034
|
import { createRequire } from "node:module";
|
|
754
|
-
import { dirname, join as join4 } from "node:path";
|
|
1035
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
755
1036
|
var LOOKBACK_DAYS = 30;
|
|
756
1037
|
function num(n) {
|
|
757
1038
|
const v = Math.round(Number(n));
|
|
@@ -795,7 +1076,7 @@ function resolveTokscaleBin() {
|
|
|
795
1076
|
const pkg = require3("tokscale/package.json");
|
|
796
1077
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
|
|
797
1078
|
if (!rel) return null;
|
|
798
|
-
const binPath = join4(
|
|
1079
|
+
const binPath = join4(dirname2(pkgPath), rel);
|
|
799
1080
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
800
1081
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
801
1082
|
}
|
|
@@ -1128,7 +1409,7 @@ function resolveCcusageBin() {
|
|
|
1128
1409
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
1129
1410
|
const pkg = require3("ccusage/package.json");
|
|
1130
1411
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
1131
|
-
const binPath = join6(
|
|
1412
|
+
const binPath = join6(dirname3(pkgPath), rel);
|
|
1132
1413
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
1133
1414
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
1134
1415
|
}
|
|
@@ -1152,6 +1433,13 @@ function dedupeDaily(entries) {
|
|
|
1152
1433
|
}
|
|
1153
1434
|
return [...byKey.values()];
|
|
1154
1435
|
}
|
|
1436
|
+
function entryTokens(e) {
|
|
1437
|
+
return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
|
|
1438
|
+
}
|
|
1439
|
+
function capByTokens(rows, max, tokens) {
|
|
1440
|
+
if (rows.length <= max) return rows;
|
|
1441
|
+
return [...rows].sort((a, b) => tokens(b) - tokens(a)).slice(0, max);
|
|
1442
|
+
}
|
|
1155
1443
|
function dedupeSessions(sessions) {
|
|
1156
1444
|
const byId = /* @__PURE__ */ new Map();
|
|
1157
1445
|
const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
|
|
@@ -1263,9 +1551,14 @@ async function collectAll(onProgress) {
|
|
|
1263
1551
|
};
|
|
1264
1552
|
});
|
|
1265
1553
|
return {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1554
|
+
// Cap each array to the server's accepted maximum (the shared SubmitPayload
|
|
1555
|
+
// schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
|
|
1556
|
+
// highest-token rows. Without this, a power user with >10000 distinct sessions
|
|
1557
|
+
// would have their ENTIRE submit rejected with a 400 instead of a capped one.
|
|
1558
|
+
// tools/skills/projects are already bounded upstream (attribution caps).
|
|
1559
|
+
entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
|
|
1560
|
+
sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
|
|
1561
|
+
blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
|
|
1269
1562
|
toolsFound,
|
|
1270
1563
|
tools,
|
|
1271
1564
|
skills,
|
|
@@ -1296,8 +1589,8 @@ function buildStatusReport(s) {
|
|
|
1296
1589
|
" \u21B3 config is out of date \u2014 it will self-repair on your next run"
|
|
1297
1590
|
);
|
|
1298
1591
|
}
|
|
1299
|
-
lines.push(` \u2022 Interval: every ${s.
|
|
1300
|
-
const staleAfterMs = s.
|
|
1592
|
+
lines.push(` \u2022 Interval: every ${syncIntervalLabel(s.intervalMinutes)}`);
|
|
1593
|
+
const staleAfterMs = s.intervalMinutes * 2 * 60 * 1e3;
|
|
1301
1594
|
if (s.lastSyncAt === null) {
|
|
1302
1595
|
lines.push(" \u2022 Last sync: never recorded");
|
|
1303
1596
|
lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
|
|
@@ -1306,7 +1599,7 @@ function buildStatusReport(s) {
|
|
|
1306
1599
|
lines.push(` \u2022 Last sync: ${ago(age)}`);
|
|
1307
1600
|
if (age > staleAfterMs) {
|
|
1308
1601
|
lines.push(
|
|
1309
|
-
` \u26A0 STALE: last sync was over ${s.
|
|
1602
|
+
` \u26A0 STALE: last sync was over ${syncIntervalLabel(s.intervalMinutes * 2)} ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
|
|
1310
1603
|
);
|
|
1311
1604
|
} else {
|
|
1312
1605
|
lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
|
|
@@ -1328,7 +1621,7 @@ function agentStatusReport(now = Date.now()) {
|
|
|
1328
1621
|
installed: autoSyncInstalled(),
|
|
1329
1622
|
loaded: autoSyncLoaded(),
|
|
1330
1623
|
drift: autoSyncDrift(),
|
|
1331
|
-
|
|
1624
|
+
intervalMinutes: SYNC_INTERVAL_MINUTES,
|
|
1332
1625
|
lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
|
|
1333
1626
|
now,
|
|
1334
1627
|
nodePath,
|
|
@@ -5378,6 +5671,48 @@ var coerce = {
|
|
|
5378
5671
|
};
|
|
5379
5672
|
var NEVER = INVALID;
|
|
5380
5673
|
|
|
5674
|
+
// ../shared/dist/tenant.js
|
|
5675
|
+
var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
5676
|
+
"www",
|
|
5677
|
+
"api",
|
|
5678
|
+
"app",
|
|
5679
|
+
"admin",
|
|
5680
|
+
"mail",
|
|
5681
|
+
"email",
|
|
5682
|
+
"cdn",
|
|
5683
|
+
"static",
|
|
5684
|
+
"assets",
|
|
5685
|
+
"blog",
|
|
5686
|
+
"docs",
|
|
5687
|
+
"status",
|
|
5688
|
+
"help",
|
|
5689
|
+
"support",
|
|
5690
|
+
"ingest",
|
|
5691
|
+
"vercel",
|
|
5692
|
+
"preview",
|
|
5693
|
+
"staging",
|
|
5694
|
+
"dev",
|
|
5695
|
+
"test"
|
|
5696
|
+
]);
|
|
5697
|
+
|
|
5698
|
+
// ../shared/dist/launch-gate.js
|
|
5699
|
+
var LaunchAccessMode = external_exports.enum(["full", "invited", "waitlisted"]);
|
|
5700
|
+
var LaunchStatusResponse = external_exports.object({
|
|
5701
|
+
mode: LaunchAccessMode,
|
|
5702
|
+
launchAt: external_exports.string().datetime(),
|
|
5703
|
+
now: external_exports.string().datetime(),
|
|
5704
|
+
remainingSeconds: external_exports.number().int().nonnegative(),
|
|
5705
|
+
live: external_exports.boolean()
|
|
5706
|
+
});
|
|
5707
|
+
var LaunchRedeemRequest = external_exports.object({
|
|
5708
|
+
code: external_exports.string().trim().min(2).max(64)
|
|
5709
|
+
});
|
|
5710
|
+
var LaunchRedeemResponse = external_exports.object({
|
|
5711
|
+
ok: external_exports.literal(true),
|
|
5712
|
+
mode: LaunchAccessMode,
|
|
5713
|
+
expiresAt: external_exports.string().datetime().nullable()
|
|
5714
|
+
});
|
|
5715
|
+
|
|
5381
5716
|
// ../shared/dist/index.js
|
|
5382
5717
|
var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
|
|
5383
5718
|
var tokenCount = external_exports.number().int().nonnegative();
|
|
@@ -5493,7 +5828,16 @@ var SubmitPayload = external_exports.object({
|
|
|
5493
5828
|
*/
|
|
5494
5829
|
attributionComplete: external_exports.boolean().optional(),
|
|
5495
5830
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
5496
|
-
board: external_exports.string().min(1).max(32).optional()
|
|
5831
|
+
board: external_exports.string().min(1).max(32).optional(),
|
|
5832
|
+
/** Optional organization slug (from `--org=<slug>`): auto-join this org on submit. */
|
|
5833
|
+
org: external_exports.string().min(2).max(32).optional(),
|
|
5834
|
+
/**
|
|
5835
|
+
* Optional org join password (from `--pass=<code>` / `--code=<code>`): required
|
|
5836
|
+
* to attach a CLI run to an `org`. Back-compat: omittable — a run with no org
|
|
5837
|
+
* never needs it, and a wrong/missing code only skips the org attach (the
|
|
5838
|
+
* personal submit still succeeds).
|
|
5839
|
+
*/
|
|
5840
|
+
orgCode: external_exports.string().min(1).max(64).optional()
|
|
5497
5841
|
});
|
|
5498
5842
|
var AnonSubmitPayload = SubmitPayload.extend({
|
|
5499
5843
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
@@ -5504,6 +5848,96 @@ function entryTotalTokens(e) {
|
|
|
5504
5848
|
}
|
|
5505
5849
|
var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
|
|
5506
5850
|
var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
|
|
5851
|
+
var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
|
|
5852
|
+
var MemberRole = external_exports.enum(["owner", "admin", "member"]);
|
|
5853
|
+
var OrgBoardVisibility = external_exports.enum(["public", "members"]);
|
|
5854
|
+
var HexColor = external_exports.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "must be a hex color like #f97316");
|
|
5855
|
+
var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
5856
|
+
...RESERVED_SUBDOMAINS,
|
|
5857
|
+
"o",
|
|
5858
|
+
"d",
|
|
5859
|
+
"u",
|
|
5860
|
+
"boards",
|
|
5861
|
+
"board",
|
|
5862
|
+
"claim",
|
|
5863
|
+
"signin",
|
|
5864
|
+
"signout",
|
|
5865
|
+
"login",
|
|
5866
|
+
"logout",
|
|
5867
|
+
"dashboard",
|
|
5868
|
+
"install",
|
|
5869
|
+
"for-teams",
|
|
5870
|
+
"teams",
|
|
5871
|
+
"about",
|
|
5872
|
+
"contact",
|
|
5873
|
+
"trust",
|
|
5874
|
+
"cli",
|
|
5875
|
+
"feedback",
|
|
5876
|
+
"guides",
|
|
5877
|
+
"guide",
|
|
5878
|
+
"join",
|
|
5879
|
+
"leaderboard",
|
|
5880
|
+
"settings",
|
|
5881
|
+
"account",
|
|
5882
|
+
"me",
|
|
5883
|
+
"new",
|
|
5884
|
+
"robots",
|
|
5885
|
+
"sitemap"
|
|
5886
|
+
]);
|
|
5887
|
+
var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
5888
|
+
function isValidSlug(slug) {
|
|
5889
|
+
return typeof slug === "string" && slug.length >= 2 && slug.length <= 32 && SLUG_RE.test(slug) && // Reject consecutive hyphens — the regex above permits "a--b", which makes
|
|
5890
|
+
// for confusing subdomains and is never a deliberate slug.
|
|
5891
|
+
!slug.includes("--") && !RESERVED_SLUGS.has(slug);
|
|
5892
|
+
}
|
|
5893
|
+
var OrgSlug = external_exports.string().min(2).max(32).refine(isValidSlug, "invalid or reserved slug");
|
|
5894
|
+
var OrgWindow = external_exports.object({
|
|
5895
|
+
startDate: DateString.nullable().optional(),
|
|
5896
|
+
endDate: DateString.nullable().optional()
|
|
5897
|
+
}).refine((w) => !(w.startDate && w.endDate) || w.endDate >= w.startDate, { message: "endDate must be on or after startDate", path: ["endDate"] });
|
|
5898
|
+
var OrgJoinPolicy = external_exports.object({
|
|
5899
|
+
allowCodeJoin: external_exports.boolean().default(true),
|
|
5900
|
+
allowDomainJoin: external_exports.boolean().default(false),
|
|
5901
|
+
/** Verified email domains that auto-join, e.g. ["acme.com"]. */
|
|
5902
|
+
emailDomains: external_exports.array(external_exports.string().min(3).max(253).toLowerCase()).max(20).default([])
|
|
5903
|
+
});
|
|
5904
|
+
var OrgApplicationInput = external_exports.object({
|
|
5905
|
+
type: OrgType,
|
|
5906
|
+
orgName: external_exports.string().min(1).max(120),
|
|
5907
|
+
desiredSlug: external_exports.string().min(2).max(32).optional(),
|
|
5908
|
+
contactName: external_exports.string().min(1).max(120),
|
|
5909
|
+
contactEmail: external_exports.string().email().max(254),
|
|
5910
|
+
website: external_exports.string().url().max(300).optional(),
|
|
5911
|
+
/** Rough headcount / attendee estimate, free text. */
|
|
5912
|
+
size: external_exports.string().max(60).optional(),
|
|
5913
|
+
message: external_exports.string().max(2e3).optional()
|
|
5914
|
+
});
|
|
5915
|
+
var OrgProvisionInput = external_exports.object({
|
|
5916
|
+
/** When provisioning straight from an application. */
|
|
5917
|
+
applicationId: external_exports.string().min(1).max(64).optional(),
|
|
5918
|
+
slug: OrgSlug,
|
|
5919
|
+
name: external_exports.string().min(1).max(120),
|
|
5920
|
+
type: OrgType,
|
|
5921
|
+
/** Handle (or email) of the user who becomes Owner. */
|
|
5922
|
+
ownerHandle: external_exports.string().min(1).max(120).optional(),
|
|
5923
|
+
ownerEmail: external_exports.string().email().max(254).optional(),
|
|
5924
|
+
description: external_exports.string().max(2e3).optional(),
|
|
5925
|
+
boardVisibility: OrgBoardVisibility.optional(),
|
|
5926
|
+
window: OrgWindow.optional()
|
|
5927
|
+
});
|
|
5928
|
+
var OrgSettingsInput = external_exports.object({
|
|
5929
|
+
name: external_exports.string().min(1).max(120).optional(),
|
|
5930
|
+
description: external_exports.string().max(2e3).nullable().optional(),
|
|
5931
|
+
accentColor: HexColor.optional(),
|
|
5932
|
+
logoUrl: external_exports.string().url().max(500).nullable().optional(),
|
|
5933
|
+
boardVisibility: OrgBoardVisibility.optional(),
|
|
5934
|
+
window: OrgWindow.optional(),
|
|
5935
|
+
joinPolicy: OrgJoinPolicy.partial().optional()
|
|
5936
|
+
});
|
|
5937
|
+
var OrgJoinInput = external_exports.object({
|
|
5938
|
+
/** Required only when joining via a code; domain/admin joins omit it. */
|
|
5939
|
+
code: external_exports.string().min(1).max(64).optional()
|
|
5940
|
+
});
|
|
5507
5941
|
|
|
5508
5942
|
// src/output.ts
|
|
5509
5943
|
function formatTokens(n) {
|
|
@@ -5815,6 +6249,7 @@ function startProgress() {
|
|
|
5815
6249
|
};
|
|
5816
6250
|
}
|
|
5817
6251
|
function openBrowser(url) {
|
|
6252
|
+
if (!isOpenableUrl(url)) return;
|
|
5818
6253
|
const os = platform3();
|
|
5819
6254
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5820
6255
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
@@ -5880,7 +6315,7 @@ async function run(flags) {
|
|
|
5880
6315
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
5881
6316
|
if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
|
|
5882
6317
|
payload.attributionComplete = true;
|
|
5883
|
-
|
|
6318
|
+
applyScope(payload, flags);
|
|
5884
6319
|
if (flags.dryRun) {
|
|
5885
6320
|
console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
5886
6321
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -5909,13 +6344,30 @@ async function run(flags) {
|
|
|
5909
6344
|
recordSync();
|
|
5910
6345
|
} catch {
|
|
5911
6346
|
}
|
|
5912
|
-
|
|
6347
|
+
try {
|
|
6348
|
+
const config = loadConfig();
|
|
6349
|
+
if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
|
|
6350
|
+
if (notifyLaunchLive()) {
|
|
6351
|
+
recordLaunchNotificationDelivered();
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
} catch {
|
|
6355
|
+
}
|
|
6356
|
+
const { baseUrl, target } = resolveOpenTarget(result, anonKey);
|
|
6357
|
+
const trusted = isTrustedWebUrl(baseUrl);
|
|
5913
6358
|
if (!flags.quiet) {
|
|
5914
6359
|
console.log(
|
|
5915
6360
|
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
5916
6361
|
);
|
|
5917
|
-
|
|
5918
|
-
|
|
6362
|
+
if (trusted) {
|
|
6363
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
6364
|
+
openBrowser(target);
|
|
6365
|
+
} else {
|
|
6366
|
+
console.log(
|
|
6367
|
+
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
6368
|
+
);
|
|
6369
|
+
console.log(` ${baseUrl}`);
|
|
6370
|
+
}
|
|
5919
6371
|
}
|
|
5920
6372
|
const lines = submitNextStepLines(result);
|
|
5921
6373
|
for (const line of lines) {
|
|
@@ -5933,10 +6385,89 @@ async function run(flags) {
|
|
|
5933
6385
|
if (!flags.quiet) {
|
|
5934
6386
|
console.log();
|
|
5935
6387
|
console.log(
|
|
5936
|
-
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every
|
|
6388
|
+
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
5937
6389
|
);
|
|
5938
6390
|
}
|
|
5939
6391
|
}
|
|
6392
|
+
async function linkServerInstall(token) {
|
|
6393
|
+
if (!token) {
|
|
6394
|
+
throw new Error("missing install token \u2014 use `npx whoburnedmore link --token=<token>`");
|
|
6395
|
+
}
|
|
6396
|
+
const anonKey = ensureAnonKey();
|
|
6397
|
+
const linked = await redeemServerInstall(token, anonKey);
|
|
6398
|
+
console.log(
|
|
6399
|
+
linked.alreadyLinked ? ` This machine is already linked to @${linked.handle}.` : ` Linked this machine to @${linked.handle}.`
|
|
6400
|
+
);
|
|
6401
|
+
if (linked.mergedDays > 0) {
|
|
6402
|
+
console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
|
|
6403
|
+
}
|
|
6404
|
+
await run({
|
|
6405
|
+
dryRun: false,
|
|
6406
|
+
noSubmit: false,
|
|
6407
|
+
local: false,
|
|
6408
|
+
quiet: true
|
|
6409
|
+
});
|
|
6410
|
+
try {
|
|
6411
|
+
const action = reconcileAutoSync();
|
|
6412
|
+
if (action === "installed") {
|
|
6413
|
+
console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
|
|
6414
|
+
} else if (action === "reinstalled") {
|
|
6415
|
+
console.log(pc2.dim(" Background sync repaired; this machine will refresh every 15 min."));
|
|
6416
|
+
} else {
|
|
6417
|
+
console.log(pc2.dim(" Background sync is already configured."));
|
|
6418
|
+
}
|
|
6419
|
+
} catch {
|
|
6420
|
+
console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
|
|
6421
|
+
}
|
|
6422
|
+
console.log(` Profile: ${linked.profileUrl}`);
|
|
6423
|
+
}
|
|
6424
|
+
function waitOrAbort(ms, signal) {
|
|
6425
|
+
if (signal.aborted) return Promise.resolve();
|
|
6426
|
+
return new Promise((resolve) => {
|
|
6427
|
+
const onAbort = () => {
|
|
6428
|
+
clearTimeout(timer);
|
|
6429
|
+
resolve();
|
|
6430
|
+
};
|
|
6431
|
+
const timer = setTimeout(() => {
|
|
6432
|
+
signal.removeEventListener("abort", onAbort);
|
|
6433
|
+
resolve();
|
|
6434
|
+
}, ms);
|
|
6435
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6436
|
+
});
|
|
6437
|
+
}
|
|
6438
|
+
async function runDaemon() {
|
|
6439
|
+
ensureAnonKey();
|
|
6440
|
+
const controller = new AbortController();
|
|
6441
|
+
const onSignal = () => controller.abort();
|
|
6442
|
+
process.on("SIGINT", onSignal);
|
|
6443
|
+
process.on("SIGTERM", onSignal);
|
|
6444
|
+
console.log(pc2.bold(" whoburnedmore daemon"));
|
|
6445
|
+
console.log(
|
|
6446
|
+
pc2.dim(
|
|
6447
|
+
` Syncing in the foreground every ${syncIntervalLabel()} \u2014 run this under systemd, a Docker CMD, pm2 or nohup to keep a server on the leaderboard. Ctrl-C to stop.`
|
|
6448
|
+
)
|
|
6449
|
+
);
|
|
6450
|
+
if (!process.env.WHOBURNEDMORE_CONFIG_DIR) {
|
|
6451
|
+
console.log(
|
|
6452
|
+
pc2.dim(
|
|
6453
|
+
" Tip: set WHOBURNEDMORE_CONFIG_DIR to a persistent path so this machine's identity survives container/VM restarts."
|
|
6454
|
+
)
|
|
6455
|
+
);
|
|
6456
|
+
}
|
|
6457
|
+
console.log();
|
|
6458
|
+
const cycles = await daemonLoop({
|
|
6459
|
+
intervalMs: SYNC_INTERVAL_MINUTES * 6e4,
|
|
6460
|
+
isStopped: () => controller.signal.aborted,
|
|
6461
|
+
log: (line) => console.log(` ${pc2.dim((/* @__PURE__ */ new Date()).toISOString())} ${line}`),
|
|
6462
|
+
wait: (ms) => waitOrAbort(ms, controller.signal),
|
|
6463
|
+
runOnce: async () => {
|
|
6464
|
+
rotateLogIfLarge();
|
|
6465
|
+
await run({ dryRun: false, noSubmit: false, local: false, quiet: true });
|
|
6466
|
+
}
|
|
6467
|
+
});
|
|
6468
|
+
console.log();
|
|
6469
|
+
console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
|
|
6470
|
+
}
|
|
5940
6471
|
async function main() {
|
|
5941
6472
|
const major = Number(process.versions.node.split(".")[0]);
|
|
5942
6473
|
if (major < 20) {
|
|
@@ -5945,13 +6476,16 @@ async function main() {
|
|
|
5945
6476
|
return;
|
|
5946
6477
|
}
|
|
5947
6478
|
const args = process.argv.slice(2);
|
|
5948
|
-
const
|
|
6479
|
+
const baseCommand = resolveCommand(args);
|
|
6480
|
+
const command = args.includes("--watch") && (baseCommand === "run" || baseCommand === "sync") ? "daemon" : baseCommand;
|
|
5949
6481
|
const flags = {
|
|
5950
6482
|
dryRun: args.includes("--dry-run"),
|
|
5951
6483
|
noSubmit: args.includes("--no-submit"),
|
|
5952
6484
|
local: args.includes("--local"),
|
|
5953
6485
|
quiet: command === "sync",
|
|
5954
|
-
board: parseBoard(args)
|
|
6486
|
+
board: parseBoard(args),
|
|
6487
|
+
org: parseOrg(args),
|
|
6488
|
+
orgCode: parsePass(args)
|
|
5955
6489
|
};
|
|
5956
6490
|
switch (command) {
|
|
5957
6491
|
case "run":
|
|
@@ -5963,6 +6497,12 @@ async function main() {
|
|
|
5963
6497
|
await run({ ...flags, noSubmit: false, dryRun: false, local: false });
|
|
5964
6498
|
break;
|
|
5965
6499
|
}
|
|
6500
|
+
case "link":
|
|
6501
|
+
await linkServerInstall(parseInstallToken(args));
|
|
6502
|
+
break;
|
|
6503
|
+
case "daemon":
|
|
6504
|
+
await runDaemon();
|
|
6505
|
+
break;
|
|
5966
6506
|
case "status":
|
|
5967
6507
|
case "doctor": {
|
|
5968
6508
|
for (const line of agentStatusReport()) console.log(line);
|
|
@@ -6016,9 +6556,12 @@ function printHelp() {
|
|
|
6016
6556
|
${pc2.bold("usage")}
|
|
6017
6557
|
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
6018
6558
|
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
6559
|
+
npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
|
|
6019
6560
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
6020
6561
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
6021
6562
|
npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
|
|
6563
|
+
npx whoburnedmore link --token=TOKEN link this server/VM to your signed-in account
|
|
6564
|
+
npx whoburnedmore daemon keep syncing in the foreground (VMs/containers with no cron)
|
|
6022
6565
|
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
6023
6566
|
npx whoburnedmore public put it back on the leaderboard
|
|
6024
6567
|
npx whoburnedmore remove delete your dashboard and its data
|
|
@@ -6027,12 +6570,20 @@ function printHelp() {
|
|
|
6027
6570
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
6028
6571
|
|
|
6029
6572
|
Background sync is on by default: after your first run, your page refreshes
|
|
6030
|
-
automatically every
|
|
6573
|
+
automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
6031
6574
|
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
6032
6575
|
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
6033
6576
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
6034
6577
|
your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
|
|
6035
6578
|
your machine at all.
|
|
6579
|
+
|
|
6580
|
+
${pc2.bold("servers & VMs")}
|
|
6581
|
+
Generate a one-time \`link\` command from your profile on whoburnedmore.com and
|
|
6582
|
+
run it inside the VM to bind that machine to your account. On a persistent VM
|
|
6583
|
+
background sync uses cron or a systemd user timer automatically; in a container
|
|
6584
|
+
or any host without a scheduler, run \`whoburnedmore daemon\` under your process
|
|
6585
|
+
manager instead. Set WHOBURNEDMORE_CONFIG_DIR to a persistent path so the
|
|
6586
|
+
machine identity survives restarts. See docs/SERVER-VM-SETUP.md.
|
|
6036
6587
|
`);
|
|
6037
6588
|
}
|
|
6038
6589
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoburnedmore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Find out who burned more — submit your AI coding-agent token usage to the public leaderboard at whoburnedmore.com",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:ccusage --external:picocolors --external:tokscale",
|
|
14
14
|
"test": "vitest run",
|
|
15
15
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
16
|
-
"
|
|
16
|
+
"smoke:package": "node scripts/smoke-package.mjs",
|
|
17
|
+
"prepublishOnly": "pnpm run lint && pnpm run test && pnpm run build && pnpm run smoke:package",
|
|
17
18
|
"release:patch": "npm version patch && npm publish",
|
|
18
19
|
"release:minor": "npm version minor && npm publish",
|
|
19
20
|
"release:major": "npm version major && npm publish"
|