usagemax 0.3.7 → 0.3.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 +16 -7
- package/package.json +2 -2
- package/src/cli.js +86 -27
- package/src/sources.js +1 -1
- package/src/updates.js +37 -0
package/README.md
CHANGED
|
@@ -40,10 +40,12 @@ bunx usagemax sync --dry-run --explain
|
|
|
40
40
|
bunx usagemax sync
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The CLI checks npm's `latest` dist-tag at most twice per day
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
The CLI checks npm's `latest` dist-tag at most twice per day. Run
|
|
44
|
+
`usagemax update` to update a global installation through the package manager
|
|
45
|
+
that launched it (Bun or npm). Add `--check` for a read-only check, or
|
|
46
|
+
`--json` for automation. A command suffix such as `usagemax update sync` hands
|
|
47
|
+
that command to the newest release without typing `@latest`. Set
|
|
48
|
+
`USAGEMAX_AUTO_UPDATE=1` for an explicit automatic handoff. Use `--no-update-check` or set
|
|
47
49
|
`USAGEMAX_DISABLE_UPDATE_CHECK=1` in offline environments. Interactive
|
|
48
50
|
terminals show a small stderr progress line; JSON,
|
|
49
51
|
quiet, CI, and scheduled runs remain machine-readable and quiet.
|
|
@@ -53,7 +55,7 @@ logs. This example only inspects local source coverage:
|
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
57
|
set +x
|
|
56
|
-
bunx usagemax doctor --deep --json | jq '{complete, sources:
|
|
58
|
+
bunx usagemax doctor --deep --json | jq '{complete: .inventoryComplete, sources: .detectedSources}'
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
The JSON shape is intended for local automation; unsupported or unavailable
|
|
@@ -81,6 +83,9 @@ usagemax token status Diagnose a key piped on stdin
|
|
|
81
83
|
usagemax service install Opt into periodic OS checkpoints
|
|
82
84
|
usagemax service status|run|uninstall
|
|
83
85
|
usagemax unlink [--revoke] Remove local credentials
|
|
86
|
+
usagemax update Update the global CLI through Bun or npm
|
|
87
|
+
usagemax update --check Check without installing
|
|
88
|
+
usagemax update sync Run sync through the newest CLI release
|
|
84
89
|
```
|
|
85
90
|
|
|
86
91
|
Useful options:
|
|
@@ -92,11 +97,15 @@ bunx usagemax sync --restart # restart an expired saved upload
|
|
|
92
97
|
bunx usagemax status --remote --json # remote check; secret is never printed
|
|
93
98
|
bunx usagemax doctor --deep --json # parse and audit retained history
|
|
94
99
|
bunx usagemax link UMX-… --no-sync # link without uploading yet
|
|
100
|
+
bunx usagemax --version --json # print CLI and ccusage versions
|
|
101
|
+
bunx usagemax update --json # update and return machine-readable status
|
|
102
|
+
bunx usagemax update --check --json # check without installing
|
|
103
|
+
bunx usagemax unlink --json # safe automation result; no secret output
|
|
95
104
|
```
|
|
96
105
|
|
|
97
106
|
## Sources and coverage
|
|
98
107
|
|
|
99
|
-
UsageMax pins [ccusage v20.0.
|
|
108
|
+
UsageMax pins [ccusage v20.0.23](https://github.com/ccusage/ccusage/releases/tag/v20.0.23)
|
|
100
109
|
and supports its 16 adapters: Amp, Claude Code, Codebuff, Codex, GitHub Copilot
|
|
101
110
|
CLI, Factory Droid, Gemini CLI, Goose, Grok Build, Hermes, Kilo Code, Kimi CLI,
|
|
102
111
|
OpenClaw, OpenCode, Pi, and Qwen Code. Named Pi-format stores are discovered as
|
|
@@ -132,7 +141,7 @@ printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
|
|
|
132
141
|
--json
|
|
133
142
|
```
|
|
134
143
|
|
|
135
|
-
The `token status` command is included in CLI `0.3.
|
|
144
|
+
The `token status` command is included in CLI `0.3.9`. If a fresh environment
|
|
136
145
|
still has an older npm tag, run `usagemax update token status` or
|
|
137
146
|
`node packages/cli/src/cli.js token status` from this repository until the new
|
|
138
147
|
package is published.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usagemax",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Link local coding-agent usage to your UsageMax profile",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"usagemax",
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"prepublishOnly": "npm test"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"ccusage": "20.0.
|
|
60
|
+
"ccusage": "20.0.23"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { intervalMinutes, manageService, runScheduledSync } from "./service.js";
|
|
|
18
18
|
import { collectorStatusView, requestCollectorStatus, requestSnapshot } from "./transport.js";
|
|
19
19
|
import { resumeUpload, restartExpiredUpload, withConfigLock } from "./resume.js";
|
|
20
20
|
import { CCUSAGE_VERSION, ccusageEnvironment, ccusageHome, discoverProviderArchives, SOURCE_INVENTORY_VERSION, sourceInventory, SUPPORTED_SOURCES } from "./sources.js";
|
|
21
|
-
import { checkForUpdate, runLatest } from "./updates.js";
|
|
21
|
+
import { checkForUpdate, packageManagerFor, runLatest, updateGlobal } from "./updates.js";
|
|
22
22
|
|
|
23
23
|
// Make the short-lived collector recognizable in Activity Monitor and `ps`.
|
|
24
24
|
// Windows may still display the underlying node.exe image name in Task Manager.
|
|
@@ -26,7 +26,7 @@ process.title = "UsageMax";
|
|
|
26
26
|
|
|
27
27
|
const require = createRequire(import.meta.url);
|
|
28
28
|
const executeFile = promisify(execFile);
|
|
29
|
-
const VERSION = "0.3.
|
|
29
|
+
const VERSION = "0.3.9";
|
|
30
30
|
const PUBLIC_API_ORIGIN = "https://usagemax.com/api";
|
|
31
31
|
const DEFAULT_LINK_ENDPOINT = `${PUBLIC_API_ORIGIN}/v1/devices/link`;
|
|
32
32
|
const DEFAULT_STATUS_ENDPOINT = `${PUBLIC_API_ORIGIN}/v1/devices/status`;
|
|
@@ -121,6 +121,7 @@ function help() {
|
|
|
121
121
|
process.stdout.write(" usagemax sync [--full] [--archives] [--restart] [--dry-run] [--explain] [--json]\n");
|
|
122
122
|
process.stdout.write(" Reconcile once; --archives performs one-time recovery\n");
|
|
123
123
|
process.stdout.write(" [--quiet|--no-progress] [--check-updates|--no-update-check] Control progress and the cached release check\n");
|
|
124
|
+
process.stdout.write(" [--json] Emit one JSON object per link/sync phase\n");
|
|
124
125
|
process.stdout.write(" usagemax status Show local link status\n");
|
|
125
126
|
process.stdout.write(" --remote [--json] Verify the stored collector credential without printing it\n");
|
|
126
127
|
process.stdout.write(" [--quiet|--no-progress] Disable interactive progress output\n");
|
|
@@ -131,9 +132,12 @@ function help() {
|
|
|
131
132
|
process.stdout.write(" usagemax service status|run|uninstall\n");
|
|
132
133
|
process.stdout.write(" usagemax doctor [--deep] [--json] [--quiet|--no-progress]\n");
|
|
133
134
|
process.stdout.write(" Check source coverage; --deep parses full history\n");
|
|
134
|
-
process.stdout.write(" usagemax update
|
|
135
|
+
process.stdout.write(" usagemax update Update the global CLI through Bun or npm\n");
|
|
136
|
+
process.stdout.write(" [--check] [--json] Check only or emit machine-readable status\n");
|
|
137
|
+
process.stdout.write(" usagemax update <command args> Run a command through the latest CLI\n");
|
|
135
138
|
process.stdout.write(" usagemax report [...args] Run a local ccusage report\n");
|
|
136
139
|
process.stdout.write(" usagemax unlink [--revoke] Remove locally; --revoke also disables uploads\n");
|
|
140
|
+
process.stdout.write(" [--json] Emit machine-readable unlink status\n");
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
function ccusageCliPath() {
|
|
@@ -182,6 +186,7 @@ function warnVersion(body) {
|
|
|
182
186
|
async function link(args) {
|
|
183
187
|
const code = normalizeLinkCode(args[0]);
|
|
184
188
|
if (!code) throw new Error("Paste the one-use UMX link code shown at usagemax.com/account.");
|
|
189
|
+
const json = args.includes("--json");
|
|
185
190
|
const configuredEndpoint = process.env.USAGEMAX_LINK_ENDPOINT || DEFAULT_LINK_ENDPOINT;
|
|
186
191
|
const endpoint = validHttpsUrl(configuredEndpoint, { allowLocalhost: true });
|
|
187
192
|
if (!endpoint) throw new Error("USAGEMAX_LINK_ENDPOINT must use HTTPS, except for localhost development.");
|
|
@@ -223,13 +228,17 @@ async function link(args) {
|
|
|
223
228
|
};
|
|
224
229
|
await writeConfig(config);
|
|
225
230
|
warnVersion(body);
|
|
226
|
-
|
|
231
|
+
if (json) {
|
|
232
|
+
process.stdout.write(`${JSON.stringify({ linked: true, version: VERSION, deviceName: savedName, profileHandle: config.profileHandle || null, profileUrl: config.profileUrl, deviceIdConfigured: true, sync: args.includes("--no-sync") ? "skipped" : "started" })}\n`);
|
|
233
|
+
} else {
|
|
234
|
+
process.stdout.write(`Linked ${savedName} to ${config.profileHandle ? `@${config.profileHandle}` : "UsageMax"}.\n`);
|
|
235
|
+
}
|
|
227
236
|
if (args.includes("--no-sync")) {
|
|
228
|
-
process.stdout.write("No usage was uploaded. Run `bunx usagemax sync --full` when you are ready.\n");
|
|
237
|
+
if (!json) process.stdout.write("No usage was uploaded. Run `bunx usagemax sync --full` when you are ready.\n");
|
|
229
238
|
return;
|
|
230
239
|
}
|
|
231
|
-
process.stdout.write("Running the first one-shot sync…\n");
|
|
232
|
-
await sync(["--full"], config);
|
|
240
|
+
if (!json) process.stdout.write("Running the first one-shot sync…\n");
|
|
241
|
+
await sync(["--full", ...(json ? ["--json"] : [])], config);
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
async function sync(args, suppliedConfig) {
|
|
@@ -254,6 +263,7 @@ async function sync(args, suppliedConfig) {
|
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
async function syncPrepared(args, suppliedConfig, recovery, progress = createProgress({ noProgress: true })) {
|
|
266
|
+
const startedAt = Date.now();
|
|
257
267
|
const config = suppliedConfig || await readConfig();
|
|
258
268
|
if (!config) throw new Error("This computer is not linked. Open https://usagemax.com/account and create a link code.");
|
|
259
269
|
config.deviceId = await stableInstallationId(configDirectory(), config.deviceId);
|
|
@@ -269,7 +279,7 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
|
|
|
269
279
|
if (config.pendingSync) {
|
|
270
280
|
progress.update("Resuming the saved upload checkpoint…");
|
|
271
281
|
if (dryRun) {
|
|
272
|
-
const result = { ...config.pendingSync.result, dryRun: true, pendingRunId: config.pendingSync.runId };
|
|
282
|
+
const result = { ...config.pendingSync.result, dryRun: true, pendingRunId: config.pendingSync.runId, cliVersion: VERSION, ccusageVersion: CCUSAGE_VERSION, durationMs: Date.now() - startedAt };
|
|
273
283
|
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `Dry run: saved run ${config.pendingSync.runId} awaits resume; no upload.\n`);
|
|
274
284
|
progress.succeed("Dry run complete; saved upload remains untouched.");
|
|
275
285
|
return result;
|
|
@@ -280,9 +290,10 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
|
|
|
280
290
|
warn: warnVersion,
|
|
281
291
|
onProgress: ({ index, total, operation, acknowledged }) => progress.update(`${acknowledged ? "Uploaded" : "Uploading"} ${index}/${total} · ${operation}`),
|
|
282
292
|
});
|
|
293
|
+
const summary = { ...result, cliVersion: VERSION, ccusageVersion: CCUSAGE_VERSION, durationMs: Date.now() - startedAt };
|
|
283
294
|
progress.succeed("Resumed and completed the saved sync.");
|
|
284
|
-
process.stdout.write(json ? `${JSON.stringify(
|
|
285
|
-
return
|
|
295
|
+
process.stdout.write(json ? `${JSON.stringify(summary)}\n` : "Resumed and completed the saved sync. Run sync again to scan newer local changes.\n");
|
|
296
|
+
return summary;
|
|
286
297
|
}
|
|
287
298
|
const inventory = await sourceInventory({ env: recovery.env, home: ccusageHome(recovery.env) });
|
|
288
299
|
progress.update(`Found ${inventory.sources.length} source${inventory.sources.length === 1 ? "" : "s"} and ${inventory.files}${inventory.truncated ? "+" : ""} local data file${inventory.files === 1 ? "" : "s"}.`);
|
|
@@ -292,9 +303,12 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
|
|
|
292
303
|
today, now: Date.now(), inventoryVersion: SOURCE_INVENTORY_VERSION, requestedFull, requestedArchives,
|
|
293
304
|
});
|
|
294
305
|
if (skip) {
|
|
295
|
-
const result = { accepted: 0, changedRows: 0, sessions: 0, sources: inventory.sources, corrections: 0, scanned: false, full: false, coverage: config.lastCoverage || "partial" };
|
|
306
|
+
const result = { accepted: 0, changedRows: 0, sessions: 0, sources: inventory.sources, corrections: 0, scanned: false, full: false, coverage: config.lastCoverage || "partial", skipReason: "inventory_unchanged", cliVersion: VERSION, ccusageVersion: CCUSAGE_VERSION, durationMs: Date.now() - startedAt };
|
|
296
307
|
if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
297
|
-
else
|
|
308
|
+
else {
|
|
309
|
+
process.stdout.write("Already up to date. Local usage files have not changed; no logs were parsed or uploaded.\n");
|
|
310
|
+
if (explain) process.stdout.write(`Skipped because the source inventory fingerprint is unchanged; coverage remains ${result.coverage}.\n`);
|
|
311
|
+
}
|
|
298
312
|
progress.succeed("No local changes; upload skipped.");
|
|
299
313
|
return;
|
|
300
314
|
}
|
|
@@ -334,6 +348,8 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
|
|
|
334
348
|
coverage: authoritative ? "complete" : "partial",
|
|
335
349
|
coverageReason: "Parser does not certify complete source/day coverage; decreases and deletions are protected.",
|
|
336
350
|
range: { from: days[0], to: days.at(-1) },
|
|
351
|
+
cliVersion: VERSION,
|
|
352
|
+
ccusageVersion: CCUSAGE_VERSION,
|
|
337
353
|
};
|
|
338
354
|
progress.update(`Prepared ${partitions.length} usage chunk${partitions.length === 1 ? "" : "s"} and ${sessions.length} session identifier${sessions.length === 1 ? "" : "s"}.`);
|
|
339
355
|
if (requestedArchives) {
|
|
@@ -341,6 +357,7 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
|
|
|
341
357
|
result.unsupportedArchives = recovery.unsupported;
|
|
342
358
|
}
|
|
343
359
|
if (dryRun) {
|
|
360
|
+
result.durationMs = Date.now() - startedAt;
|
|
344
361
|
if (json) process.stdout.write(`${JSON.stringify({ ...result, dryRun: true })}\n`);
|
|
345
362
|
else {
|
|
346
363
|
process.stdout.write(`Dry run: ${partitions.length} partition(s), ${result.changedRows} changed row(s), ${sessions.length} private session identifiers, no upload.\n`);
|
|
@@ -389,6 +406,7 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
|
|
|
389
406
|
warn: warnVersion,
|
|
390
407
|
onProgress: ({ index, total, operation, acknowledged }) => progress.update(`${acknowledged ? "Uploaded" : "Uploading"} ${index}/${total} · ${operation}`),
|
|
391
408
|
});
|
|
409
|
+
result.durationMs = Date.now() - startedAt;
|
|
392
410
|
progress.succeed(`Sync complete · ${partitions.length} chunk${partitions.length === 1 ? "" : "s"}, ${sessions.length} session identifier${sessions.length === 1 ? "" : "s"}.`);
|
|
393
411
|
if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
394
412
|
else {
|
|
@@ -431,7 +449,7 @@ async function status(args = []) {
|
|
|
431
449
|
const progress = createProgress({ json, quiet: args.includes("--quiet"), noProgress: args.includes("--no-progress") });
|
|
432
450
|
if (!config) {
|
|
433
451
|
if (json) {
|
|
434
|
-
process.stdout.write(`${JSON.stringify({ linked: false, remote: remote ? { status: "not_linked" } : undefined })}\n`);
|
|
452
|
+
process.stdout.write(`${JSON.stringify({ linked: false, version: VERSION, ccusageVersion: CCUSAGE_VERSION, configPath: configPath(), remote: remote ? { status: "not_linked" } : undefined })}\n`);
|
|
435
453
|
return;
|
|
436
454
|
}
|
|
437
455
|
process.stdout.write("Not linked. Open https://usagemax.com/account to connect this computer.\n");
|
|
@@ -444,12 +462,18 @@ async function status(args = []) {
|
|
|
444
462
|
}
|
|
445
463
|
const local = {
|
|
446
464
|
linked: true,
|
|
465
|
+
version: VERSION,
|
|
466
|
+
ccusageVersion: CCUSAGE_VERSION,
|
|
467
|
+
configPath: configPath(),
|
|
447
468
|
deviceName: config.deviceName || deviceLabel(),
|
|
448
469
|
profileHandle: config.profileHandle || null,
|
|
449
470
|
deviceIdConfigured: Boolean(config.deviceId),
|
|
450
471
|
lastSyncAt: config.lastSyncAt || null,
|
|
451
472
|
lastFullSyncAt: config.lastFullSyncAt || null,
|
|
452
473
|
pendingSync: config.pendingSync?.runId || null,
|
|
474
|
+
coverage: config.lastCoverage || null,
|
|
475
|
+
knownSources: Array.isArray(config.knownSources) ? config.knownSources : [],
|
|
476
|
+
snapshotRows: Object.keys(config.snapshots || {}).length,
|
|
453
477
|
profileUrl: config.profileUrl || "https://usagemax.com/account",
|
|
454
478
|
};
|
|
455
479
|
let remoteView;
|
|
@@ -473,8 +497,11 @@ async function status(args = []) {
|
|
|
473
497
|
return;
|
|
474
498
|
}
|
|
475
499
|
process.stdout.write(`Linked: ${config.deviceName || deviceLabel()}${config.profileHandle ? ` → @${config.profileHandle}` : ""}\n`);
|
|
500
|
+
process.stdout.write(`CLI: ${VERSION} · ccusage ${CCUSAGE_VERSION}\n`);
|
|
476
501
|
process.stdout.write(`Last sync: ${config.lastSyncAt || "never"}\n`);
|
|
477
502
|
process.stdout.write(`Last full reconciliation: ${config.lastFullSyncAt || "never"}\n`);
|
|
503
|
+
process.stdout.write(`Coverage: ${config.lastCoverage || "unknown"} · ${Object.keys(config.snapshots || {}).length} checkpoint rows\n`);
|
|
504
|
+
if (Array.isArray(config.knownSources) && config.knownSources.length) process.stdout.write(`Known sources: ${config.knownSources.join(", ")}\n`);
|
|
478
505
|
if (config.pendingSync) process.stdout.write(`Pending sync: ${config.pendingSync.runId}; rerun sync to resume\n`);
|
|
479
506
|
process.stdout.write(`Profile: ${config.profileUrl || "https://usagemax.com/account"}\n`);
|
|
480
507
|
if (remote) printRemoteStatus(remoteView);
|
|
@@ -538,6 +565,9 @@ async function doctor(args = []) {
|
|
|
538
565
|
const archives = await discoverProviderArchives({ env, home: ccusageHome(env) });
|
|
539
566
|
const homes = String(env.USAGEMAX_DISCOVERED_HOMES || ccusageHome(env)).split(",").filter(Boolean);
|
|
540
567
|
const result = {
|
|
568
|
+
cliVersion: VERSION,
|
|
569
|
+
ccusageVersion: CCUSAGE_VERSION,
|
|
570
|
+
configPath: configPath(),
|
|
541
571
|
linked: Boolean(config),
|
|
542
572
|
homes,
|
|
543
573
|
detectedSources: inventory.sources,
|
|
@@ -592,23 +622,46 @@ async function report(args) {
|
|
|
592
622
|
}
|
|
593
623
|
|
|
594
624
|
async function update(args = []) {
|
|
625
|
+
const json = args.includes("--json");
|
|
626
|
+
const checkOnly = args.includes("--check") || args.includes("--no-install");
|
|
627
|
+
const commandArgs = args.filter((arg) => !["--check", "--no-install", "--json"].includes(arg));
|
|
628
|
+
const handoffArgs = commandArgs.length ? [...commandArgs, ...(json ? ["--json"] : [])] : [];
|
|
595
629
|
const check = await checkForUpdate(configDirectory(), VERSION, { force: true });
|
|
596
630
|
if (!check.latest) {
|
|
597
|
-
|
|
631
|
+
const result = { version: VERSION, latest: null, newer: false, status: "unavailable" };
|
|
632
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${VERSION} · update check unavailable.\n`);
|
|
598
633
|
return;
|
|
599
634
|
}
|
|
600
635
|
if (!check.newer) {
|
|
601
|
-
|
|
636
|
+
const result = { version: VERSION, latest: check.latest, newer: false, status: "current" };
|
|
637
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${VERSION} is current.\n`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (handoffArgs.length) {
|
|
641
|
+
const result = { version: VERSION, latest: check.latest, newer: true, status: "available", command: `usagemax update ${commandArgs.join(" ")}` };
|
|
642
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${check.latest} is available (current ${VERSION}).\n`);
|
|
643
|
+
if (!json) process.stdout.write(`Launching UsageMax ${check.latest}…\n`);
|
|
644
|
+
return runLatest(handoffArgs);
|
|
645
|
+
}
|
|
646
|
+
const manager = packageManagerFor({ cliPath: process.argv[1], runtime: process.versions, env: process.env });
|
|
647
|
+
const installCommand = manager === "bun" ? "bun add --global usagemax@latest" : "npm install --global usagemax@latest";
|
|
648
|
+
const available = { version: VERSION, latest: check.latest, newer: true, status: "available", manager, installCommand };
|
|
649
|
+
if (checkOnly) {
|
|
650
|
+
process.stdout.write(json ? `${JSON.stringify(available)}\n` : `UsageMax CLI ${check.latest} is available (current ${VERSION}). Run \`${installCommand}\`.\n`);
|
|
602
651
|
return;
|
|
603
652
|
}
|
|
604
|
-
process.stdout.write(`UsageMax
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
653
|
+
if (!json) process.stdout.write(`Updating UsageMax ${VERSION} → ${check.latest} via ${manager}…\n`);
|
|
654
|
+
try {
|
|
655
|
+
await updateGlobal({ latest: check.latest, manager, quiet: json });
|
|
656
|
+
} catch (error) {
|
|
657
|
+
const result = { ...available, status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
658
|
+
if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
659
|
+
else process.stderr.write(`UsageMax update failed: ${result.error}\n`);
|
|
660
|
+
process.exitCode = 1;
|
|
608
661
|
return;
|
|
609
662
|
}
|
|
610
|
-
|
|
611
|
-
|
|
663
|
+
const result = { ...available, status: "updated", installedVersion: check.latest };
|
|
664
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI is now ${check.latest}.\n`);
|
|
612
665
|
}
|
|
613
666
|
|
|
614
667
|
async function maybeUpdate(command, args) {
|
|
@@ -629,11 +682,13 @@ async function maybeUpdate(command, args) {
|
|
|
629
682
|
async function removeLink(args = []) {
|
|
630
683
|
const path = configPath();
|
|
631
684
|
const config = await readConfig();
|
|
685
|
+
const json = args.includes("--json");
|
|
632
686
|
if (!config) {
|
|
633
|
-
process.stdout.write("This computer is not linked.\n");
|
|
687
|
+
process.stdout.write(json ? `${JSON.stringify({ unlinked: false, reason: "not_linked" })}\n` : "This computer is not linked.\n");
|
|
634
688
|
return;
|
|
635
689
|
}
|
|
636
|
-
|
|
690
|
+
const revoked = args.includes("--revoke");
|
|
691
|
+
if (revoked) {
|
|
637
692
|
const endpoint = validHttpsUrl(config.revokeUrl, { allowLocalhost: true });
|
|
638
693
|
if (!endpoint) throw new Error("This collector does not have a valid revoke endpoint. Revoke it at https://usagemax.com/account.");
|
|
639
694
|
const response = await fetch(endpoint, {
|
|
@@ -647,16 +702,20 @@ async function removeLink(args = []) {
|
|
|
647
702
|
if (!response.ok) throw new Error("UsageMax could not revoke this collector. It remains linked locally.");
|
|
648
703
|
}
|
|
649
704
|
await unlink(path);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
705
|
+
if (json) {
|
|
706
|
+
process.stdout.write(`${JSON.stringify({ unlinked: true, revoked, retainedInstallationIdentity: true })}\n`);
|
|
707
|
+
} else {
|
|
708
|
+
process.stdout.write(revoked
|
|
709
|
+
? "Revoked this collector and removed its local key. Existing usage totals were retained.\n"
|
|
710
|
+
: "Removed the local UsageMax collector key. This computer's private installation identity was retained so relinking cannot duplicate its usage. Revoke the collector in your account if this computer is no longer trusted.\n");
|
|
711
|
+
}
|
|
653
712
|
}
|
|
654
713
|
|
|
655
714
|
async function main() {
|
|
656
715
|
const args = process.argv.slice(2);
|
|
657
716
|
const command = args[0] || "sync";
|
|
658
717
|
if (["--help", "-h", "help"].includes(command)) return help();
|
|
659
|
-
if (["--version", "-v"].includes(command)) return process.stdout.write(`${VERSION}\n`);
|
|
718
|
+
if (["--version", "-v"].includes(command)) return process.stdout.write(args.includes("--json") ? `${JSON.stringify({ version: VERSION, ccusageVersion: CCUSAGE_VERSION })}\n` : `${VERSION}\n`);
|
|
660
719
|
if (command === "update") return update(args.slice(1));
|
|
661
720
|
if (await maybeUpdate(command, args)) return;
|
|
662
721
|
if (command === "service") {
|
package/src/sources.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
export const CCUSAGE_VERSION = "20.0.
|
|
6
|
+
export const CCUSAGE_VERSION = "20.0.23";
|
|
7
7
|
export const SOURCE_INVENTORY_VERSION = 3;
|
|
8
8
|
export const SUPPORTED_SOURCES = [
|
|
9
9
|
"amp",
|
package/src/updates.js
CHANGED
|
@@ -83,6 +83,43 @@ function runner() {
|
|
|
83
83
|
return { command: "bunx", prefix: ["--bun"] };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Pick the package manager that owns the current invocation. Bun's global and
|
|
88
|
+
* ephemeral package paths are explicit; npm remains the safe default for a
|
|
89
|
+
* normal Node installation. The override is useful for managed environments
|
|
90
|
+
* where the executable is wrapped by a fleet-specific package manager.
|
|
91
|
+
*/
|
|
92
|
+
export function packageManagerFor({ cliPath = process.argv[1], runtime = process.versions, env = process.env } = {}) {
|
|
93
|
+
const override = String(env.USAGEMAX_PACKAGE_MANAGER || "").trim().toLowerCase();
|
|
94
|
+
if (override === "bun" || override === "npm") return override;
|
|
95
|
+
const normalizedPath = String(cliPath || "").replaceAll("\\", "/");
|
|
96
|
+
if (normalizedPath.includes("/.bun/install/") || runtime?.bun) return "bun";
|
|
97
|
+
if (env.npm_execpath || String(env.npm_config_user_agent || "").startsWith("npm/")) return "npm";
|
|
98
|
+
return "npm";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Install the current public release into the user's global package scope. */
|
|
102
|
+
export async function updateGlobal({ latest = "latest", manager = packageManagerFor(), spawnImpl = spawn, env = process.env, quiet = false, platform = process.platform } = {}) {
|
|
103
|
+
if (manager !== "bun" && manager !== "npm") throw new Error(`Unsupported package manager: ${manager}`);
|
|
104
|
+
if (!/^\d+\.\d+\.\d+$/.test(String(latest)) && latest !== "latest") throw new Error("Invalid UsageMax release version.");
|
|
105
|
+
const command = manager === "bun" ? (platform === "win32" ? "bun.exe" : "bun") : (platform === "win32" ? "npm.cmd" : "npm");
|
|
106
|
+
const args = manager === "bun"
|
|
107
|
+
? ["add", "--global", `usagemax@${latest}`]
|
|
108
|
+
: ["install", "--global", `usagemax@${latest}`];
|
|
109
|
+
const child = spawnImpl(command, args, {
|
|
110
|
+
stdio: quiet ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
111
|
+
env: { ...env, NPM_CONFIG_UPDATE_NOTIFIER: "false" },
|
|
112
|
+
});
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
child.once("error", reject);
|
|
115
|
+
child.once("exit", (status) => {
|
|
116
|
+
const code = status ?? 1;
|
|
117
|
+
if (code === 0) resolve({ manager, command, args });
|
|
118
|
+
else reject(Object.assign(new Error(`${command} ${args.join(" ")} exited with code ${code}.`), { code: "USAGEMAX_UPDATE_FAILED", exitCode: code }));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
/** Re-run a command through the current npm dist-tag without requiring @latest. */
|
|
87
124
|
export async function runLatest(args, { spawnImpl = spawn } = {}) {
|
|
88
125
|
const selected = runner();
|