@usagefleet/cli 1.2.91 → 1.2.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/release.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by .github/workflows/release.yml.
2
- export const RELEASE_VERSION = "1.2.91";
2
+ export const RELEASE_VERSION = "1.2.92";
package/dist/update.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { basename, delimiter, dirname, join } from 'node:path';
4
4
  import { RELEASE_VERSION } from './release.js';
5
+ import { tilde } from './ui.js';
5
6
  /** The published package: one artifact for every OS, installed with
6
7
  * `npm i -g @usagefleet/cli`. */
7
8
  const PACKAGE = '@usagefleet/cli';
@@ -19,18 +20,100 @@ function npmCommand() {
19
20
  const sibling = join(dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
20
21
  return existsSync(sibling) ? sibling : 'npm';
21
22
  }
23
+ /** Every global install is a little project directory, and the lockfile at its
24
+ * root names the manager that owns it. npm is the one that leaves none behind,
25
+ * so "no lockfile" is what makes it the default rather than a guess. */
26
+ const MANAGERS = [
27
+ { lockfile: 'bun.lock', bin: 'bun', install: ['add', '--global'] },
28
+ { lockfile: 'bun.lockb', bin: 'bun', install: ['add', '--global'] },
29
+ { lockfile: 'pnpm-lock.yaml', bin: 'pnpm', install: ['add', '--global'] },
30
+ { lockfile: 'yarn.lock', bin: 'yarn', install: ['global', 'add'] },
31
+ ];
32
+ /** The directory a global install was unpacked into: the parent of the
33
+ * `node_modules` holding the running code. null outside a node_modules, which
34
+ * is a dev checkout or a bundle — neither is ours to upgrade. */
35
+ function installRoot(script) {
36
+ try {
37
+ const pkg = dirname(dirname(realpathSync(script))); // <root>/node_modules/@usagefleet/cli
38
+ const modules = dirname(dirname(pkg));
39
+ return basename(modules) === 'node_modules' ? dirname(modules) : null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /** The version sitting on disk behind the running script — its own package.json,
46
+ * read fresh rather than from the baked-in RELEASE_VERSION, so it reports what
47
+ * an install just wrote instead of what this process started as. null when it
48
+ * can't be resolved, which means "don't judge". */
49
+ function installedVersion(script) {
50
+ const root = installRoot(script);
51
+ if (!root) {
52
+ return null;
53
+ }
54
+ try {
55
+ const manifest = join(root, 'node_modules', PACKAGE, 'package.json');
56
+ return JSON.parse(readFileSync(manifest, 'utf-8')).version ?? null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /** A manager binary sitting next to its own global root (`~/.bun/bin/bun`,
63
+ * `~/.local/share/pnpm/pnpm`) beats hoping the bare name resolves: launchd and
64
+ * systemd hand the collector a minimal PATH that has none of them on it. */
65
+ function managerBinary(root, bin) {
66
+ const exe = process.platform === 'win32' ? `${bin}.cmd` : bin;
67
+ for (let dir = root; dir !== dirname(dir); dir = dirname(dir)) {
68
+ const found = [join(dir, exe), join(dir, 'bin', exe)].find(candidate => existsSync(candidate));
69
+ if (found) {
70
+ return found;
71
+ }
72
+ }
73
+ return bin;
74
+ }
75
+ /** The upgrade command for the install this collector actually runs from.
76
+ *
77
+ * It matters because `npm install --global` only ever writes to npm's own
78
+ * prefix: told to upgrade a collector that bun or pnpm put on the machine, npm
79
+ * installs a second copy somewhere else and exits 0. The service keeps running
80
+ * the untouched original, so the collector re-runs a "successful" upgrade every
81
+ * six hours and never moves off its version. */
82
+ function updateCommand(script, version) {
83
+ const spec = `${PACKAGE}@${version}`;
84
+ const root = installRoot(script);
85
+ const manager = root ? MANAGERS.find(m => existsSync(join(root, m.lockfile))) : undefined;
86
+ return manager && root
87
+ ? { cmd: managerBinary(root, manager.bin), args: [...manager.install, spec] }
88
+ : { cmd: npmCommand(), args: ['install', '--global', spec] };
89
+ }
22
90
  /** An `npm install` that never returns would hang the watch loop forever, since
23
91
  * the update check is awaited inline before the next tick is scheduled: the
24
92
  * daemon would still be "running" while collecting nothing, with an empty log.
25
93
  * Generous enough for a slow registry on a cold cache. */
26
94
  const RUN_TIMEOUT_MS = 10 * 60_000;
95
+ /** The environment with our own node on PATH.
96
+ *
97
+ * Not a nicety: launchd hands a service PATH=/usr/bin:/bin:/usr/sbin:/sbin and
98
+ * npm is a script starting `#!/usr/bin/env node`, so unless node happens to sit
99
+ * in /usr/bin — it doesn't for Homebrew, nvm, fnm, volta or asdf — the update
100
+ * dies with exit 127 on every tick, forever, and the device sits on the version
101
+ * it was installed at. */
102
+ function pathWithNode() {
103
+ const env = { ...process.env };
104
+ // Windows enumerates it as `Path`; writing a second `PATH` key would leave the
105
+ // child with two, and which one wins is anyone's guess.
106
+ const key = Object.keys(env).find(name => name.toUpperCase() === 'PATH') ?? 'PATH';
107
+ env[key] = [dirname(process.execPath), env[key]].filter(Boolean).join(delimiter);
108
+ return env;
109
+ }
27
110
  /** Exit code of a finished child, or null when it could not be started, was
28
111
  * killed for exceeding RUN_TIMEOUT_MS, or died on a signal. */
29
112
  function run(cmd, args) {
30
113
  return new Promise(resolve => {
31
114
  // shell on Windows: node refuses to spawn a .cmd directly since the 2024
32
115
  // argument-injection fix. Every argument here is a literal or VERSION-checked.
33
- const child = spawn(cmd, args, { shell: process.platform === 'win32', stdio: 'ignore' });
116
+ const child = spawn(cmd, args, { shell: process.platform === 'win32', stdio: 'ignore', env: pathWithNode() });
34
117
  const timer = setTimeout(() => child.kill(), RUN_TIMEOUT_MS);
35
118
  // Node emits 'close' after 'error' for a failed spawn, so both handlers can
36
119
  // run; `settled` makes the first one win. oxlint's promise rule sees the two
@@ -106,17 +189,28 @@ export async function checkForUpdate(log, force = false) {
106
189
  }
107
190
  return null;
108
191
  }
109
- log('ok', `${RELEASE_VERSION} ${latest} · installing ${PACKAGE}…`);
110
- const code = await run(npmCommand(), ['install', '--global', `${PACKAGE}@${latest}`]);
192
+ const { cmd, args } = updateCommand(self, latest);
193
+ const manager = basename(cmd).replace(/\.cmd$/, '');
194
+ log('ok', `${RELEASE_VERSION} → ${latest} · installing ${PACKAGE} with ${manager}…`);
195
+ const code = await run(cmd, args);
111
196
  if (code !== 0) {
112
197
  log('warn', code === null
113
- ? 'npm not available · reinstall with `npm i -g @usagefleet/cli`'
114
- : `npm install failed (exit ${code}) · if the global prefix needs root, run it yourself`);
198
+ ? `${manager} not available · reinstall with \`${manager} ${args.join(' ')}\``
199
+ : `${manager} install failed (exit ${code}) · if the global prefix needs root, run it yourself`);
200
+ return null;
201
+ }
202
+ // A package manager exits 0 for an install it wrote somewhere this collector
203
+ // does not run from, so "success" is not the same as the new version being
204
+ // live. Restarting on that would bounce the service back onto the very same
205
+ // old code, every six hours, forever.
206
+ const onDisk = installedVersion(self);
207
+ if (onDisk && onDisk !== latest) {
208
+ log('warn', `still ${onDisk} at ${tilde(self)} · ${manager} updated a different install · reinstall it there`);
115
209
  return null;
116
210
  }
117
211
  // Detached: `login` rewrites the service definition and restarts it, which
118
- // kills this process tree. npm replaced the file behind `self`, so this is
119
- // already the new version.
212
+ // kills this process tree. The install replaced the file behind `self`, so
213
+ // this is already the new version.
120
214
  spawn(process.execPath, [self, 'login'], { detached: true, stdio: 'ignore' }).unref();
121
215
  log('ok', `installed ${latest} · restarting service`);
122
216
  return latest;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagefleet/cli",
3
- "version": "1.2.91",
3
+ "version": "1.2.92",
4
4
  "description": "Tails Claude Code, Claude Desktop, and pi agent JSONL logs and reports token usage to a UsageFleet server.",
5
5
  "keywords": [
6
6
  "claude",