@usagefleet/cli 1.2.91 → 1.2.93
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 +1 -1
- package/dist/update.js +103 -9
- package/package.json +1 -1
package/dist/release.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by .github/workflows/release.yml.
|
|
2
|
-
export const RELEASE_VERSION = "1.2.
|
|
2
|
+
export const RELEASE_VERSION = "1.2.93";
|
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
|
-
|
|
110
|
-
const
|
|
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
|
-
?
|
|
114
|
-
:
|
|
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.
|
|
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;
|