whoburnedmore 0.8.5 → 0.8.9
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 +391 -45
- 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,24 @@ 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 parseInstallToken(args) {
|
|
25
|
+
return parseValueFlag(args, "--token");
|
|
26
|
+
}
|
|
27
|
+
function applyScope(payload, flags) {
|
|
28
|
+
if (flags.board) payload.board = flags.board;
|
|
29
|
+
if (flags.org) payload.org = flags.org;
|
|
30
|
+
return payload;
|
|
31
|
+
}
|
|
32
|
+
function parseValueFlag(args, name) {
|
|
33
|
+
const prefix = `${name}=`;
|
|
34
|
+
const eq = args.find((a) => a.startsWith(prefix));
|
|
35
|
+
if (eq) return eq.slice(prefix.length).trim() || void 0;
|
|
36
|
+
const i = args.indexOf(name);
|
|
22
37
|
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
23
38
|
return args[i + 1].trim() || void 0;
|
|
24
39
|
}
|
|
@@ -31,7 +46,16 @@ function resolveCommand(args) {
|
|
|
31
46
|
if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
|
|
32
47
|
return "version";
|
|
33
48
|
}
|
|
34
|
-
|
|
49
|
+
const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token"]);
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
const a = args[i];
|
|
52
|
+
if (a.startsWith("-")) {
|
|
53
|
+
if (valueFlags.has(a)) i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
return a;
|
|
57
|
+
}
|
|
58
|
+
return "run";
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
// src/api.ts
|
|
@@ -41,6 +65,20 @@ function apiBase() {
|
|
|
41
65
|
function webBase() {
|
|
42
66
|
return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
|
|
43
67
|
}
|
|
68
|
+
function isTrustedWebUrl(url) {
|
|
69
|
+
let u;
|
|
70
|
+
let base;
|
|
71
|
+
try {
|
|
72
|
+
u = new URL(url);
|
|
73
|
+
base = new URL(webBase());
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.protocol === base.protocol && u.host === base.host;
|
|
78
|
+
}
|
|
79
|
+
function isOpenableUrl(url) {
|
|
80
|
+
return /^(https?|file):\/\//.test(url);
|
|
81
|
+
}
|
|
44
82
|
async function readJson(res) {
|
|
45
83
|
const text = await res.text();
|
|
46
84
|
if (!text) return {};
|
|
@@ -80,6 +118,9 @@ async function anonSubmit(anonKey, payload) {
|
|
|
80
118
|
function claimUrl(dashboardUrl, anonKey) {
|
|
81
119
|
return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
|
|
82
120
|
}
|
|
121
|
+
function boardClaimUrl(boardUrl, slug, anonKey) {
|
|
122
|
+
return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
|
|
123
|
+
}
|
|
83
124
|
async function anonVisibility(anonKey, listed) {
|
|
84
125
|
const { status, body } = await post(
|
|
85
126
|
"/v1/anon/visibility",
|
|
@@ -100,6 +141,15 @@ async function anonRemove(anonKey) {
|
|
|
100
141
|
throw new Error(b.error ?? `failed (HTTP ${res.status})`);
|
|
101
142
|
}
|
|
102
143
|
}
|
|
144
|
+
async function redeemServerInstall(token, anonKey) {
|
|
145
|
+
const { status, body } = await post("/v1/server-install/redeem", { token, anonKey });
|
|
146
|
+
if (status !== 200) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
body.error ?? `server install failed (HTTP ${status})`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return body;
|
|
152
|
+
}
|
|
103
153
|
|
|
104
154
|
// src/autosync.ts
|
|
105
155
|
import { spawnSync } from "node:child_process";
|
|
@@ -113,8 +163,7 @@ import {
|
|
|
113
163
|
writeFileSync as writeFileSync2
|
|
114
164
|
} from "node:fs";
|
|
115
165
|
import { homedir as homedir2, platform } from "node:os";
|
|
116
|
-
import { join as join2 } from "node:path";
|
|
117
|
-
import { fileURLToPath } from "node:url";
|
|
166
|
+
import { dirname, join as join2, win32 } from "node:path";
|
|
118
167
|
|
|
119
168
|
// src/config.ts
|
|
120
169
|
import { randomBytes } from "node:crypto";
|
|
@@ -128,6 +177,8 @@ import {
|
|
|
128
177
|
import { homedir } from "node:os";
|
|
129
178
|
import { join } from "node:path";
|
|
130
179
|
function defaultConfigDir() {
|
|
180
|
+
const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
|
|
181
|
+
if (override) return override;
|
|
131
182
|
return join(homedir(), ".config", "whoburnedmore");
|
|
132
183
|
}
|
|
133
184
|
function loadConfig(dir = defaultConfigDir()) {
|
|
@@ -139,6 +190,9 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
139
190
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
140
191
|
if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
|
|
141
192
|
config.lastSyncAt = parsed.lastSyncAt;
|
|
193
|
+
if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
|
|
194
|
+
config.launchNotificationDeliveredAt = parsed.launchNotificationDeliveredAt;
|
|
195
|
+
}
|
|
142
196
|
return Object.keys(config).length > 0 ? config : null;
|
|
143
197
|
} catch {
|
|
144
198
|
return null;
|
|
@@ -164,19 +218,33 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
|
|
|
164
218
|
const config = loadConfig(dir) ?? {};
|
|
165
219
|
saveConfig(dir, { ...config, lastSyncAt: when });
|
|
166
220
|
}
|
|
221
|
+
function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
|
|
222
|
+
const config = loadConfig(dir) ?? {};
|
|
223
|
+
saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
|
|
224
|
+
}
|
|
167
225
|
|
|
168
226
|
// src/autosync.ts
|
|
169
|
-
var
|
|
227
|
+
var SYNC_INTERVAL_MINUTES = 15;
|
|
228
|
+
function syncIntervalLabel(mins = SYNC_INTERVAL_MINUTES) {
|
|
229
|
+
return mins % 60 === 0 ? `${mins / 60}h` : `${mins}m`;
|
|
230
|
+
}
|
|
170
231
|
var LABEL = "com.whoburnedmore.sync";
|
|
171
232
|
var STABLE_NODE_CANDIDATES = [
|
|
172
233
|
"/opt/homebrew/bin/node",
|
|
173
234
|
"/usr/local/bin/node",
|
|
174
235
|
"/usr/bin/node"
|
|
175
236
|
];
|
|
237
|
+
var STABLE_NPM_CANDIDATES = [
|
|
238
|
+
"/opt/homebrew/bin/npm",
|
|
239
|
+
"/usr/local/bin/npm",
|
|
240
|
+
"/usr/bin/npm"
|
|
241
|
+
];
|
|
242
|
+
var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
|
|
176
243
|
function syncLogPath() {
|
|
177
244
|
return join2(defaultConfigDir(), "sync.log");
|
|
178
245
|
}
|
|
179
|
-
function buildLaunchdPlist(
|
|
246
|
+
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
|
|
247
|
+
const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
180
248
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
181
249
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
182
250
|
<plist version="1.0">
|
|
@@ -185,12 +253,10 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
185
253
|
<string>${LABEL}</string>
|
|
186
254
|
<key>ProgramArguments</key>
|
|
187
255
|
<array>
|
|
188
|
-
|
|
189
|
-
<string>${scriptPath}</string>
|
|
190
|
-
<string>sync</string>
|
|
256
|
+
${programArguments}
|
|
191
257
|
</array>
|
|
192
258
|
<key>StartInterval</key>
|
|
193
|
-
<integer>${
|
|
259
|
+
<integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
|
|
194
260
|
<!-- Run once right after login/reboot so a machine that was off (or asleep)
|
|
195
261
|
through a scheduled tick catches up immediately, then keeps to the
|
|
196
262
|
interval. Submits are idempotent server-side, so an extra run is safe. -->
|
|
@@ -200,9 +266,9 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
200
266
|
<key>ProcessType</key>
|
|
201
267
|
<string>Background</string>
|
|
202
268
|
<key>StandardOutPath</key>
|
|
203
|
-
<string>${logPath}</string>
|
|
269
|
+
<string>${xmlEscape(logPath)}</string>
|
|
204
270
|
<key>StandardErrorPath</key>
|
|
205
|
-
<string>${logPath}</string>
|
|
271
|
+
<string>${xmlEscape(logPath)}</string>
|
|
206
272
|
</dict>
|
|
207
273
|
</plist>
|
|
208
274
|
`;
|
|
@@ -210,9 +276,6 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
210
276
|
function launchAgentPath() {
|
|
211
277
|
return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
212
278
|
}
|
|
213
|
-
function cliScriptPath() {
|
|
214
|
-
return fileURLToPath(new URL("./index.js", import.meta.url));
|
|
215
|
-
}
|
|
216
279
|
function isUsableNode(p) {
|
|
217
280
|
if (!existsSync2(p)) return false;
|
|
218
281
|
const res = spawnSync(p, ["-v"], { encoding: "utf8" });
|
|
@@ -229,8 +292,51 @@ function resolveNodePath(opts) {
|
|
|
229
292
|
}
|
|
230
293
|
return execPath;
|
|
231
294
|
}
|
|
295
|
+
function isUsableNpm(p) {
|
|
296
|
+
if (!existsSync2(p)) return false;
|
|
297
|
+
const res = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
298
|
+
return res.status === 0;
|
|
299
|
+
}
|
|
300
|
+
function resolveNpmPath(opts) {
|
|
301
|
+
const candidates = opts?.candidates ?? STABLE_NPM_CANDIDATES;
|
|
302
|
+
const check = opts?.check ?? isUsableNpm;
|
|
303
|
+
const execPath = opts?.execPath ?? process.execPath;
|
|
304
|
+
const os = opts?.platform ?? platform();
|
|
305
|
+
for (const c of candidates) {
|
|
306
|
+
if (check(c)) return c;
|
|
307
|
+
}
|
|
308
|
+
const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : join2(dirname(execPath), "npm");
|
|
309
|
+
if (check(sibling)) return sibling;
|
|
310
|
+
return os === "win32" ? "npm.cmd" : "npm";
|
|
311
|
+
}
|
|
312
|
+
function syncCommandArgs(npmPath = resolveNpmPath()) {
|
|
313
|
+
return [
|
|
314
|
+
npmPath,
|
|
315
|
+
"exec",
|
|
316
|
+
"--yes",
|
|
317
|
+
"--ignore-scripts",
|
|
318
|
+
"--package",
|
|
319
|
+
LATEST_PACKAGE_SPEC,
|
|
320
|
+
"--",
|
|
321
|
+
"whoburnedmore",
|
|
322
|
+
"sync"
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
function xmlEscape(value) {
|
|
326
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """).replaceAll("'", "'").replaceAll(">", ">");
|
|
327
|
+
}
|
|
328
|
+
function shellQuote(value) {
|
|
329
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
330
|
+
}
|
|
331
|
+
function windowsQuote(value) {
|
|
332
|
+
const escaped = value.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, (slashes) => `${slashes}${slashes}`);
|
|
333
|
+
return `"${escaped}"`;
|
|
334
|
+
}
|
|
335
|
+
function windowsCommandLine(args) {
|
|
336
|
+
return args.map(windowsQuote).join(" ");
|
|
337
|
+
}
|
|
232
338
|
function expectedDarwinPlist() {
|
|
233
|
-
return buildLaunchdPlist(
|
|
339
|
+
return buildLaunchdPlist(syncCommandArgs());
|
|
234
340
|
}
|
|
235
341
|
function plistDrift(installed, expected) {
|
|
236
342
|
if (installed === null) return "absent";
|
|
@@ -248,7 +354,7 @@ function installAutoSync() {
|
|
|
248
354
|
writeFileSync2(plistPath, expectedDarwinPlist());
|
|
249
355
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
250
356
|
spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
|
|
251
|
-
return `launchd agent installed (${plistPath}), syncing every ${
|
|
357
|
+
return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
|
|
252
358
|
}
|
|
253
359
|
if (os === "linux") {
|
|
254
360
|
const line = expectedLinuxCronLine();
|
|
@@ -260,28 +366,32 @@ ${line}
|
|
|
260
366
|
`.replace(/^\n+/, "");
|
|
261
367
|
const res = spawnSync("crontab", ["-"], { input: next });
|
|
262
368
|
if (res.status !== 0) throw new Error("could not install crontab entry");
|
|
263
|
-
return `cron entry installed, syncing every ${
|
|
369
|
+
return `cron entry installed, syncing every ${syncIntervalLabel()}`;
|
|
264
370
|
}
|
|
265
371
|
if (os === "win32") {
|
|
266
372
|
const res = spawnSync("schtasks", [
|
|
267
373
|
"/Create",
|
|
268
374
|
"/F",
|
|
269
375
|
"/SC",
|
|
270
|
-
"
|
|
376
|
+
"MINUTE",
|
|
271
377
|
"/MO",
|
|
272
|
-
String(
|
|
378
|
+
String(SYNC_INTERVAL_MINUTES),
|
|
273
379
|
"/TN",
|
|
274
380
|
"whoburnedmore-sync",
|
|
275
381
|
"/TR",
|
|
276
|
-
|
|
382
|
+
windowsCommandLine(syncCommandArgs())
|
|
277
383
|
]);
|
|
278
384
|
if (res.status !== 0) throw new Error("could not create scheduled task");
|
|
279
|
-
return `scheduled task installed, syncing every ${
|
|
385
|
+
return `scheduled task installed, syncing every ${syncIntervalLabel()}`;
|
|
280
386
|
}
|
|
281
387
|
throw new Error(`auto-sync is not supported on ${os}`);
|
|
282
388
|
}
|
|
283
|
-
function
|
|
284
|
-
return `0 */${
|
|
389
|
+
function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
|
|
390
|
+
return mins % 60 === 0 ? `0 */${mins / 60} * * *` : `*/${mins} * * * *`;
|
|
391
|
+
}
|
|
392
|
+
function expectedLinuxCronLine(opts) {
|
|
393
|
+
const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
|
|
394
|
+
return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
|
|
285
395
|
}
|
|
286
396
|
function uninstallAutoSync() {
|
|
287
397
|
const os = platform();
|
|
@@ -360,6 +470,41 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
360
470
|
return false;
|
|
361
471
|
}
|
|
362
472
|
}
|
|
473
|
+
function notifyLaunchLive(opts) {
|
|
474
|
+
const os = opts?.platform ?? platform();
|
|
475
|
+
const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
|
|
476
|
+
const title = "whoburnedmore is live";
|
|
477
|
+
const message = "Your dashboard is ready. Go to whoburnedmore.com";
|
|
478
|
+
if (os === "darwin") {
|
|
479
|
+
return run2("osascript", [
|
|
480
|
+
"-e",
|
|
481
|
+
`display notification "${message}" with title "${title}"`
|
|
482
|
+
]).status === 0;
|
|
483
|
+
}
|
|
484
|
+
if (os === "linux") {
|
|
485
|
+
return run2("notify-send", [title, message]).status === 0;
|
|
486
|
+
}
|
|
487
|
+
if (os === "win32") {
|
|
488
|
+
const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
489
|
+
const script = [
|
|
490
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
491
|
+
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
492
|
+
"$n.Icon = [System.Drawing.SystemIcons]::Application",
|
|
493
|
+
`$n.BalloonTipTitle = ${psQuote(title)}`,
|
|
494
|
+
`$n.BalloonTipText = ${psQuote(message)}`,
|
|
495
|
+
"$n.Visible = $true",
|
|
496
|
+
"$n.ShowBalloonTip(10000)",
|
|
497
|
+
"Start-Sleep -Seconds 2",
|
|
498
|
+
"$n.Dispose()"
|
|
499
|
+
].join("; ");
|
|
500
|
+
return run2("powershell", [
|
|
501
|
+
"-NoProfile",
|
|
502
|
+
"-Command",
|
|
503
|
+
script
|
|
504
|
+
]).status === 0;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
363
508
|
function autoSyncLoaded() {
|
|
364
509
|
if (platform() === "darwin") {
|
|
365
510
|
const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
|
|
@@ -389,7 +534,7 @@ function printBanner() {
|
|
|
389
534
|
// src/collect.ts
|
|
390
535
|
import { execFile } from "node:child_process";
|
|
391
536
|
import { createRequire as createRequire3 } from "node:module";
|
|
392
|
-
import { dirname as
|
|
537
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
393
538
|
import { promisify } from "node:util";
|
|
394
539
|
|
|
395
540
|
// src/attribution.ts
|
|
@@ -748,7 +893,7 @@ import { join as join5 } from "node:path";
|
|
|
748
893
|
// src/tokscale.ts
|
|
749
894
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
750
895
|
import { createRequire } from "node:module";
|
|
751
|
-
import { dirname, join as join4 } from "node:path";
|
|
896
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
752
897
|
var LOOKBACK_DAYS = 30;
|
|
753
898
|
function num(n) {
|
|
754
899
|
const v = Math.round(Number(n));
|
|
@@ -792,7 +937,7 @@ function resolveTokscaleBin() {
|
|
|
792
937
|
const pkg = require3("tokscale/package.json");
|
|
793
938
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
|
|
794
939
|
if (!rel) return null;
|
|
795
|
-
const binPath = join4(
|
|
940
|
+
const binPath = join4(dirname2(pkgPath), rel);
|
|
796
941
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
797
942
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
798
943
|
}
|
|
@@ -1125,7 +1270,7 @@ function resolveCcusageBin() {
|
|
|
1125
1270
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
1126
1271
|
const pkg = require3("ccusage/package.json");
|
|
1127
1272
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
1128
|
-
const binPath = join6(
|
|
1273
|
+
const binPath = join6(dirname3(pkgPath), rel);
|
|
1129
1274
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
1130
1275
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
1131
1276
|
}
|
|
@@ -1149,6 +1294,13 @@ function dedupeDaily(entries) {
|
|
|
1149
1294
|
}
|
|
1150
1295
|
return [...byKey.values()];
|
|
1151
1296
|
}
|
|
1297
|
+
function entryTokens(e) {
|
|
1298
|
+
return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
|
|
1299
|
+
}
|
|
1300
|
+
function capByTokens(rows, max, tokens) {
|
|
1301
|
+
if (rows.length <= max) return rows;
|
|
1302
|
+
return [...rows].sort((a, b) => tokens(b) - tokens(a)).slice(0, max);
|
|
1303
|
+
}
|
|
1152
1304
|
function dedupeSessions(sessions) {
|
|
1153
1305
|
const byId = /* @__PURE__ */ new Map();
|
|
1154
1306
|
const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
|
|
@@ -1260,9 +1412,14 @@ async function collectAll(onProgress) {
|
|
|
1260
1412
|
};
|
|
1261
1413
|
});
|
|
1262
1414
|
return {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1415
|
+
// Cap each array to the server's accepted maximum (the shared SubmitPayload
|
|
1416
|
+
// schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
|
|
1417
|
+
// highest-token rows. Without this, a power user with >10000 distinct sessions
|
|
1418
|
+
// would have their ENTIRE submit rejected with a 400 instead of a capped one.
|
|
1419
|
+
// tools/skills/projects are already bounded upstream (attribution caps).
|
|
1420
|
+
entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
|
|
1421
|
+
sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
|
|
1422
|
+
blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
|
|
1266
1423
|
toolsFound,
|
|
1267
1424
|
tools,
|
|
1268
1425
|
skills,
|
|
@@ -1293,8 +1450,8 @@ function buildStatusReport(s) {
|
|
|
1293
1450
|
" \u21B3 config is out of date \u2014 it will self-repair on your next run"
|
|
1294
1451
|
);
|
|
1295
1452
|
}
|
|
1296
|
-
lines.push(` \u2022 Interval: every ${s.
|
|
1297
|
-
const staleAfterMs = s.
|
|
1453
|
+
lines.push(` \u2022 Interval: every ${syncIntervalLabel(s.intervalMinutes)}`);
|
|
1454
|
+
const staleAfterMs = s.intervalMinutes * 2 * 60 * 1e3;
|
|
1298
1455
|
if (s.lastSyncAt === null) {
|
|
1299
1456
|
lines.push(" \u2022 Last sync: never recorded");
|
|
1300
1457
|
lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
|
|
@@ -1303,7 +1460,7 @@ function buildStatusReport(s) {
|
|
|
1303
1460
|
lines.push(` \u2022 Last sync: ${ago(age)}`);
|
|
1304
1461
|
if (age > staleAfterMs) {
|
|
1305
1462
|
lines.push(
|
|
1306
|
-
` \u26A0 STALE: last sync was over ${s.
|
|
1463
|
+
` \u26A0 STALE: last sync was over ${syncIntervalLabel(s.intervalMinutes * 2)} ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
|
|
1307
1464
|
);
|
|
1308
1465
|
} else {
|
|
1309
1466
|
lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
|
|
@@ -1325,7 +1482,7 @@ function agentStatusReport(now = Date.now()) {
|
|
|
1325
1482
|
installed: autoSyncInstalled(),
|
|
1326
1483
|
loaded: autoSyncLoaded(),
|
|
1327
1484
|
drift: autoSyncDrift(),
|
|
1328
|
-
|
|
1485
|
+
intervalMinutes: SYNC_INTERVAL_MINUTES,
|
|
1329
1486
|
lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
|
|
1330
1487
|
now,
|
|
1331
1488
|
nodePath,
|
|
@@ -5375,6 +5532,48 @@ var coerce = {
|
|
|
5375
5532
|
};
|
|
5376
5533
|
var NEVER = INVALID;
|
|
5377
5534
|
|
|
5535
|
+
// ../shared/dist/tenant.js
|
|
5536
|
+
var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
5537
|
+
"www",
|
|
5538
|
+
"api",
|
|
5539
|
+
"app",
|
|
5540
|
+
"admin",
|
|
5541
|
+
"mail",
|
|
5542
|
+
"email",
|
|
5543
|
+
"cdn",
|
|
5544
|
+
"static",
|
|
5545
|
+
"assets",
|
|
5546
|
+
"blog",
|
|
5547
|
+
"docs",
|
|
5548
|
+
"status",
|
|
5549
|
+
"help",
|
|
5550
|
+
"support",
|
|
5551
|
+
"ingest",
|
|
5552
|
+
"vercel",
|
|
5553
|
+
"preview",
|
|
5554
|
+
"staging",
|
|
5555
|
+
"dev",
|
|
5556
|
+
"test"
|
|
5557
|
+
]);
|
|
5558
|
+
|
|
5559
|
+
// ../shared/dist/launch-gate.js
|
|
5560
|
+
var LaunchAccessMode = external_exports.enum(["full", "invited", "waitlisted"]);
|
|
5561
|
+
var LaunchStatusResponse = external_exports.object({
|
|
5562
|
+
mode: LaunchAccessMode,
|
|
5563
|
+
launchAt: external_exports.string().datetime(),
|
|
5564
|
+
now: external_exports.string().datetime(),
|
|
5565
|
+
remainingSeconds: external_exports.number().int().nonnegative(),
|
|
5566
|
+
live: external_exports.boolean()
|
|
5567
|
+
});
|
|
5568
|
+
var LaunchRedeemRequest = external_exports.object({
|
|
5569
|
+
code: external_exports.string().trim().min(2).max(64)
|
|
5570
|
+
});
|
|
5571
|
+
var LaunchRedeemResponse = external_exports.object({
|
|
5572
|
+
ok: external_exports.literal(true),
|
|
5573
|
+
mode: LaunchAccessMode,
|
|
5574
|
+
expiresAt: external_exports.string().datetime().nullable()
|
|
5575
|
+
});
|
|
5576
|
+
|
|
5378
5577
|
// ../shared/dist/index.js
|
|
5379
5578
|
var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
|
|
5380
5579
|
var tokenCount = external_exports.number().int().nonnegative();
|
|
@@ -5490,7 +5689,9 @@ var SubmitPayload = external_exports.object({
|
|
|
5490
5689
|
*/
|
|
5491
5690
|
attributionComplete: external_exports.boolean().optional(),
|
|
5492
5691
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
5493
|
-
board: external_exports.string().min(1).max(32).optional()
|
|
5692
|
+
board: external_exports.string().min(1).max(32).optional(),
|
|
5693
|
+
/** Optional organization slug (from `--org=<slug>`): auto-join this org on submit. */
|
|
5694
|
+
org: external_exports.string().min(2).max(32).optional()
|
|
5494
5695
|
});
|
|
5495
5696
|
var AnonSubmitPayload = SubmitPayload.extend({
|
|
5496
5697
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
@@ -5501,6 +5702,94 @@ function entryTotalTokens(e) {
|
|
|
5501
5702
|
}
|
|
5502
5703
|
var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
|
|
5503
5704
|
var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
|
|
5705
|
+
var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
|
|
5706
|
+
var MemberRole = external_exports.enum(["owner", "admin", "member"]);
|
|
5707
|
+
var OrgBoardVisibility = external_exports.enum(["public", "members"]);
|
|
5708
|
+
var HexColor = external_exports.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "must be a hex color like #f97316");
|
|
5709
|
+
var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
5710
|
+
...RESERVED_SUBDOMAINS,
|
|
5711
|
+
"o",
|
|
5712
|
+
"d",
|
|
5713
|
+
"u",
|
|
5714
|
+
"boards",
|
|
5715
|
+
"board",
|
|
5716
|
+
"claim",
|
|
5717
|
+
"signin",
|
|
5718
|
+
"signout",
|
|
5719
|
+
"login",
|
|
5720
|
+
"logout",
|
|
5721
|
+
"dashboard",
|
|
5722
|
+
"install",
|
|
5723
|
+
"for-teams",
|
|
5724
|
+
"teams",
|
|
5725
|
+
"about",
|
|
5726
|
+
"contact",
|
|
5727
|
+
"trust",
|
|
5728
|
+
"cli",
|
|
5729
|
+
"feedback",
|
|
5730
|
+
"guides",
|
|
5731
|
+
"guide",
|
|
5732
|
+
"join",
|
|
5733
|
+
"leaderboard",
|
|
5734
|
+
"settings",
|
|
5735
|
+
"account",
|
|
5736
|
+
"me",
|
|
5737
|
+
"new",
|
|
5738
|
+
"robots",
|
|
5739
|
+
"sitemap"
|
|
5740
|
+
]);
|
|
5741
|
+
var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
5742
|
+
function isValidSlug(slug) {
|
|
5743
|
+
return typeof slug === "string" && slug.length >= 2 && slug.length <= 32 && SLUG_RE.test(slug) && !RESERVED_SLUGS.has(slug);
|
|
5744
|
+
}
|
|
5745
|
+
var OrgSlug = external_exports.string().min(2).max(32).refine(isValidSlug, "invalid or reserved slug");
|
|
5746
|
+
var OrgWindow = external_exports.object({
|
|
5747
|
+
startDate: DateString.nullable().optional(),
|
|
5748
|
+
endDate: DateString.nullable().optional()
|
|
5749
|
+
});
|
|
5750
|
+
var OrgJoinPolicy = external_exports.object({
|
|
5751
|
+
allowCodeJoin: external_exports.boolean().default(true),
|
|
5752
|
+
allowDomainJoin: external_exports.boolean().default(false),
|
|
5753
|
+
/** Verified email domains that auto-join, e.g. ["acme.com"]. */
|
|
5754
|
+
emailDomains: external_exports.array(external_exports.string().min(3).max(253).toLowerCase()).max(20).default([])
|
|
5755
|
+
});
|
|
5756
|
+
var OrgApplicationInput = external_exports.object({
|
|
5757
|
+
type: OrgType,
|
|
5758
|
+
orgName: external_exports.string().min(1).max(120),
|
|
5759
|
+
desiredSlug: external_exports.string().min(2).max(32).optional(),
|
|
5760
|
+
contactName: external_exports.string().min(1).max(120),
|
|
5761
|
+
contactEmail: external_exports.string().email().max(254),
|
|
5762
|
+
website: external_exports.string().url().max(300).optional(),
|
|
5763
|
+
/** Rough headcount / attendee estimate, free text. */
|
|
5764
|
+
size: external_exports.string().max(60).optional(),
|
|
5765
|
+
message: external_exports.string().max(2e3).optional()
|
|
5766
|
+
});
|
|
5767
|
+
var OrgProvisionInput = external_exports.object({
|
|
5768
|
+
/** When provisioning straight from an application. */
|
|
5769
|
+
applicationId: external_exports.string().min(1).max(64).optional(),
|
|
5770
|
+
slug: OrgSlug,
|
|
5771
|
+
name: external_exports.string().min(1).max(120),
|
|
5772
|
+
type: OrgType,
|
|
5773
|
+
/** Handle (or email) of the user who becomes Owner. */
|
|
5774
|
+
ownerHandle: external_exports.string().min(1).max(120).optional(),
|
|
5775
|
+
ownerEmail: external_exports.string().email().max(254).optional(),
|
|
5776
|
+
description: external_exports.string().max(2e3).optional(),
|
|
5777
|
+
boardVisibility: OrgBoardVisibility.optional(),
|
|
5778
|
+
window: OrgWindow.optional()
|
|
5779
|
+
});
|
|
5780
|
+
var OrgSettingsInput = external_exports.object({
|
|
5781
|
+
name: external_exports.string().min(1).max(120).optional(),
|
|
5782
|
+
description: external_exports.string().max(2e3).nullable().optional(),
|
|
5783
|
+
accentColor: HexColor.optional(),
|
|
5784
|
+
logoUrl: external_exports.string().url().max(500).nullable().optional(),
|
|
5785
|
+
boardVisibility: OrgBoardVisibility.optional(),
|
|
5786
|
+
window: OrgWindow.optional(),
|
|
5787
|
+
joinPolicy: OrgJoinPolicy.partial().optional()
|
|
5788
|
+
});
|
|
5789
|
+
var OrgJoinInput = external_exports.object({
|
|
5790
|
+
/** Required only when joining via a code; domain/admin joins omit it. */
|
|
5791
|
+
code: external_exports.string().min(1).max(64).optional()
|
|
5792
|
+
});
|
|
5504
5793
|
|
|
5505
5794
|
// src/output.ts
|
|
5506
5795
|
function formatTokens(n) {
|
|
@@ -5812,6 +6101,7 @@ function startProgress() {
|
|
|
5812
6101
|
};
|
|
5813
6102
|
}
|
|
5814
6103
|
function openBrowser(url) {
|
|
6104
|
+
if (!isOpenableUrl(url)) return;
|
|
5815
6105
|
const os = platform3();
|
|
5816
6106
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5817
6107
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
@@ -5877,7 +6167,7 @@ async function run(flags) {
|
|
|
5877
6167
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
5878
6168
|
if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
|
|
5879
6169
|
payload.attributionComplete = true;
|
|
5880
|
-
|
|
6170
|
+
applyScope(payload, flags);
|
|
5881
6171
|
if (flags.dryRun) {
|
|
5882
6172
|
console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
5883
6173
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -5906,13 +6196,31 @@ async function run(flags) {
|
|
|
5906
6196
|
recordSync();
|
|
5907
6197
|
} catch {
|
|
5908
6198
|
}
|
|
5909
|
-
|
|
6199
|
+
try {
|
|
6200
|
+
const config = loadConfig();
|
|
6201
|
+
if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
|
|
6202
|
+
if (notifyLaunchLive()) {
|
|
6203
|
+
recordLaunchNotificationDelivered();
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
} catch {
|
|
6207
|
+
}
|
|
6208
|
+
const baseUrl = result.boardUrl ?? result.dashboardUrl;
|
|
6209
|
+
const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
|
|
6210
|
+
const trusted = isTrustedWebUrl(baseUrl);
|
|
5910
6211
|
if (!flags.quiet) {
|
|
5911
6212
|
console.log(
|
|
5912
6213
|
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
5913
6214
|
);
|
|
5914
|
-
|
|
5915
|
-
|
|
6215
|
+
if (trusted) {
|
|
6216
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
6217
|
+
openBrowser(target);
|
|
6218
|
+
} else {
|
|
6219
|
+
console.log(
|
|
6220
|
+
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
6221
|
+
);
|
|
6222
|
+
console.log(` ${baseUrl}`);
|
|
6223
|
+
}
|
|
5916
6224
|
}
|
|
5917
6225
|
const lines = submitNextStepLines(result);
|
|
5918
6226
|
for (const line of lines) {
|
|
@@ -5930,10 +6238,42 @@ async function run(flags) {
|
|
|
5930
6238
|
if (!flags.quiet) {
|
|
5931
6239
|
console.log();
|
|
5932
6240
|
console.log(
|
|
5933
|
-
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every
|
|
6241
|
+
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.")
|
|
5934
6242
|
);
|
|
5935
6243
|
}
|
|
5936
6244
|
}
|
|
6245
|
+
async function linkServerInstall(token) {
|
|
6246
|
+
if (!token) {
|
|
6247
|
+
throw new Error("missing install token \u2014 use `npx whoburnedmore link --token=<token>`");
|
|
6248
|
+
}
|
|
6249
|
+
const anonKey = ensureAnonKey();
|
|
6250
|
+
const linked = await redeemServerInstall(token, anonKey);
|
|
6251
|
+
console.log(
|
|
6252
|
+
linked.alreadyLinked ? ` This machine is already linked to @${linked.handle}.` : ` Linked this machine to @${linked.handle}.`
|
|
6253
|
+
);
|
|
6254
|
+
if (linked.mergedDays > 0) {
|
|
6255
|
+
console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
|
|
6256
|
+
}
|
|
6257
|
+
await run({
|
|
6258
|
+
dryRun: false,
|
|
6259
|
+
noSubmit: false,
|
|
6260
|
+
local: false,
|
|
6261
|
+
quiet: true
|
|
6262
|
+
});
|
|
6263
|
+
try {
|
|
6264
|
+
const action = reconcileAutoSync();
|
|
6265
|
+
if (action === "installed") {
|
|
6266
|
+
console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
|
|
6267
|
+
} else if (action === "reinstalled") {
|
|
6268
|
+
console.log(pc2.dim(" Background sync repaired; this machine will refresh every 15 min."));
|
|
6269
|
+
} else {
|
|
6270
|
+
console.log(pc2.dim(" Background sync is already configured."));
|
|
6271
|
+
}
|
|
6272
|
+
} catch {
|
|
6273
|
+
console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
|
|
6274
|
+
}
|
|
6275
|
+
console.log(` Profile: ${linked.profileUrl}`);
|
|
6276
|
+
}
|
|
5937
6277
|
async function main() {
|
|
5938
6278
|
const major = Number(process.versions.node.split(".")[0]);
|
|
5939
6279
|
if (major < 20) {
|
|
@@ -5948,7 +6288,8 @@ async function main() {
|
|
|
5948
6288
|
noSubmit: args.includes("--no-submit"),
|
|
5949
6289
|
local: args.includes("--local"),
|
|
5950
6290
|
quiet: command === "sync",
|
|
5951
|
-
board: parseBoard(args)
|
|
6291
|
+
board: parseBoard(args),
|
|
6292
|
+
org: parseOrg(args)
|
|
5952
6293
|
};
|
|
5953
6294
|
switch (command) {
|
|
5954
6295
|
case "run":
|
|
@@ -5960,6 +6301,9 @@ async function main() {
|
|
|
5960
6301
|
await run({ ...flags, noSubmit: false, dryRun: false, local: false });
|
|
5961
6302
|
break;
|
|
5962
6303
|
}
|
|
6304
|
+
case "link":
|
|
6305
|
+
await linkServerInstall(parseInstallToken(args));
|
|
6306
|
+
break;
|
|
5963
6307
|
case "status":
|
|
5964
6308
|
case "doctor": {
|
|
5965
6309
|
for (const line of agentStatusReport()) console.log(line);
|
|
@@ -6013,9 +6357,11 @@ function printHelp() {
|
|
|
6013
6357
|
${pc2.bold("usage")}
|
|
6014
6358
|
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
6015
6359
|
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
6360
|
+
npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
|
|
6016
6361
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
6017
6362
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
6018
6363
|
npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
|
|
6364
|
+
npx whoburnedmore link --token=TOKEN link this server to your signed-in account
|
|
6019
6365
|
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
6020
6366
|
npx whoburnedmore public put it back on the leaderboard
|
|
6021
6367
|
npx whoburnedmore remove delete your dashboard and its data
|
|
@@ -6024,7 +6370,7 @@ function printHelp() {
|
|
|
6024
6370
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
6025
6371
|
|
|
6026
6372
|
Background sync is on by default: after your first run, your page refreshes
|
|
6027
|
-
automatically every
|
|
6373
|
+
automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
6028
6374
|
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
6029
6375
|
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
6030
6376
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoburnedmore",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
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"
|