wuphf 0.71.9 → 0.71.11

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 CHANGED
@@ -166,4 +166,15 @@ To point the wrapper at a locally-built binary, set `WUPHF_BINARY`:
166
166
  WUPHF_BINARY=./wuphf npx wuphf --version
167
167
  ```
168
168
 
169
+ ## Auto-upgrade
170
+
171
+ `npm install -g` does not pull new versions on its own, so the wrapper
172
+ checks `registry.npmjs.org` once per 24h (cached at
173
+ `~/.wuphf/cache/latest-version.json`). If a newer release is available it
174
+ downloads the matching binary into `~/.wuphf/cache/binaries/` and runs it
175
+ instead — same SHA256 verification as `postinstall`. A one-line hint points
176
+ you at `npm install -g wuphf@latest` for a permanent upgrade.
177
+
178
+ Set `WUPHF_SKIP_VERSION_CHECK=1` to disable the check entirely.
179
+
169
180
  MIT licensed. Free, open source, self-hosted, your API keys.
package/bin/wuphf.js CHANGED
@@ -1,24 +1,92 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- // Thin shim that spawns the native wuphf binary. Downloads lazily on first
5
- // run if postinstall was skipped (common with `npm install --ignore-scripts`
6
- // and with some `npx` cache behaviors).
4
+ // Thin shim that spawns the native wuphf binary.
5
+ //
6
+ // Two responsibilities beyond a plain `spawn`:
7
+ //
8
+ // 1. Lazy download if postinstall was skipped (common with
9
+ // `npm install --ignore-scripts` and with some `npx` cache behaviors).
10
+ //
11
+ // 2. Self-heal when npm's published `latest` has moved past the installed
12
+ // version. `npm install -g` does NOT auto-upgrade, so a user who
13
+ // installed weeks ago runs their old binary forever without this
14
+ // check. We consult the npm registry (24h cache), and if a newer
15
+ // release exists, we transparently serve it from an out-of-tree
16
+ // version-keyed cache. The cached binary is verified against the
17
+ // release's checksums.txt via the same path postinstall uses — there
18
+ // is no path that runs an unverified binary.
19
+ //
20
+ // Escape hatches:
21
+ // WUPHF_BINARY=/path/to/wuphf — use a specific binary.
22
+ // WUPHF_SKIP_VERSION_CHECK=1 — never query npm, always run the
23
+ // locally-installed binary.
7
24
 
8
25
  const fs = require("node:fs");
9
26
  const path = require("node:path");
27
+ const os = require("node:os");
10
28
  const { spawn } = require("node:child_process");
11
- const { downloadBinary } = require("../scripts/download-binary");
29
+ const { downloadBinary, packageVersion } = require("../scripts/download-binary");
30
+ const { getLatestVersion, compareVersions } = require("../scripts/version-check");
12
31
 
13
- const binaryPath =
14
- process.env.WUPHF_BINARY || path.join(__dirname, "wuphf");
32
+ const installedBinary = path.join(__dirname, "wuphf");
33
+
34
+ function cachedBinaryPath(version) {
35
+ return path.join(
36
+ os.homedir(),
37
+ ".wuphf",
38
+ "cache",
39
+ "binaries",
40
+ `wuphf-${version}`,
41
+ );
42
+ }
43
+
44
+ async function resolveInstalledBinary() {
45
+ if (fs.existsSync(installedBinary)) return installedBinary;
46
+ return downloadBinary();
47
+ }
15
48
 
16
49
  async function ensureBinary() {
17
50
  if (process.env.WUPHF_BINARY && fs.existsSync(process.env.WUPHF_BINARY)) {
18
51
  return process.env.WUPHF_BINARY;
19
52
  }
20
- if (fs.existsSync(binaryPath)) return binaryPath;
21
- return downloadBinary();
53
+
54
+ const installed = await resolveInstalledBinary();
55
+ if (process.env.WUPHF_SKIP_VERSION_CHECK === "1") return installed;
56
+
57
+ const installedVersion = packageVersion();
58
+ const latestVersion = await getLatestVersion();
59
+ if (!latestVersion) return installed;
60
+ if (compareVersions(latestVersion, installedVersion) <= 0) return installed;
61
+
62
+ // npm has a newer release than what's installed. Serve the cached newer
63
+ // binary, downloading it once if absent. Integrity-verified via the same
64
+ // checksums.txt path as postinstall — a failure anywhere in that chain
65
+ // falls back to the installed binary rather than running something
66
+ // unverified or crashing the command.
67
+ const cachedPath = cachedBinaryPath(latestVersion);
68
+ if (!fs.existsSync(cachedPath)) {
69
+ try {
70
+ await downloadBinary({
71
+ version: latestVersion,
72
+ targetPath: cachedPath,
73
+ });
74
+ } catch (err) {
75
+ process.stderr.write(
76
+ `wuphf: self-heal download of v${latestVersion} failed: ${err.message}\n` +
77
+ `wuphf: running installed v${installedVersion}. ` +
78
+ `Run \`npm install -g wuphf@latest\` to upgrade.\n`,
79
+ );
80
+ return installed;
81
+ }
82
+ }
83
+
84
+ process.stderr.write(
85
+ `wuphf: serving cached v${latestVersion} (installed is v${installedVersion}). ` +
86
+ `Run \`npm install -g wuphf@latest\` to upgrade permanently, ` +
87
+ `or set WUPHF_SKIP_VERSION_CHECK=1 to disable this check.\n`,
88
+ );
89
+ return cachedPath;
22
90
  }
23
91
 
24
92
  function run(resolvedPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuphf",
3
- "version": "0.71.9",
3
+ "version": "0.71.11",
4
4
  "description": "Slack for AI employees with a shared brain. A collaborative office where AI employees run your work 24x7.",
5
5
  "bin": {
6
6
  "wuphf": "bin/wuphf.js"
@@ -9,6 +9,7 @@
9
9
  "bin/wuphf.js",
10
10
  "scripts/download-binary.js",
11
11
  "scripts/postinstall.js",
12
+ "scripts/version-check.js",
12
13
  "README.md"
13
14
  ],
14
15
  "scripts": {
@@ -162,12 +162,21 @@ async function verifyArchive({ version, archivePath, archiveBasename, silent })
162
162
  }
163
163
  }
164
164
 
165
- async function downloadBinary({ silent = false } = {}) {
166
- const version = packageVersion();
167
- const archiveBasename = archiveName(version);
168
- const url = releaseAssetUrl(version, archiveBasename);
169
- const binDir = path.join(__dirname, "..", "bin");
170
- const binaryPath = path.join(binDir, "wuphf");
165
+ // Options:
166
+ // silent — suppress progress output on stderr.
167
+ // version — download a specific tagged release instead of the one
168
+ // recorded in package.json. Used by bin/wuphf.js to fetch a
169
+ // newer release into an out-of-tree cache when npm's latest
170
+ // has moved past the installed version.
171
+ // targetPath — where to place the extracted binary. Defaults to
172
+ // bin/wuphf inside this package. The out-of-tree cache uses
173
+ // a version-keyed path so multiple versions can coexist.
174
+ async function downloadBinary({ silent = false, version, targetPath } = {}) {
175
+ const resolvedVersion = version ?? packageVersion();
176
+ const archiveBasename = archiveName(resolvedVersion);
177
+ const url = releaseAssetUrl(resolvedVersion, archiveBasename);
178
+ const binaryPath = targetPath ?? path.join(__dirname, "..", "bin", "wuphf");
179
+ const binDir = path.dirname(binaryPath);
171
180
 
172
181
  await fsp.mkdir(binDir, { recursive: true });
173
182
 
@@ -181,7 +190,12 @@ async function downloadBinary({ silent = false } = {}) {
181
190
  await fetchToFile(url, archivePath);
182
191
 
183
192
  // Integrity check BEFORE we extract or execute anything.
184
- await verifyArchive({ version, archivePath, archiveBasename, silent });
193
+ await verifyArchive({
194
+ version: resolvedVersion,
195
+ archivePath,
196
+ archiveBasename,
197
+ silent,
198
+ });
185
199
 
186
200
  // Extract using system tar (available on darwin + linux).
187
201
  execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], {
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ // Queries npm for the published `latest` wuphf version, with a 24h on-disk
4
+ // cache so we don't hammer the registry on every CLI invocation.
5
+ //
6
+ // Why this exists: `npm install -g wuphf` does not auto-upgrade. A user who
7
+ // installed weeks ago runs their old binary forever unless they manually
8
+ // re-install. The shim in bin/wuphf.js uses this to transparently serve the
9
+ // *latest* release from a verified cache while pointing the user at a real
10
+ // fix (`npm install -g wuphf@latest`). See bin/wuphf.js for how the result
11
+ // feeds into ensureBinary().
12
+ //
13
+ // Contract:
14
+ // - getLatestVersion() returns a semver string or null. null means
15
+ // "couldn't check" and the caller MUST fall back to the installed
16
+ // version. Network errors, malformed responses, and fetch timeouts all
17
+ // resolve to null rather than throwing — this path runs before every
18
+ // command and must never break invocation.
19
+ // - compareVersions(a, b) implements major.minor.patch ordering with a
20
+ // lexicographic tiebreaker on pre-release suffixes (SemVer: a release
21
+ // sorts above its pre-releases, matching `0.68.8` > `0.68.8-rc.1`).
22
+
23
+ const fs = require("node:fs");
24
+ const fsp = require("node:fs/promises");
25
+ const path = require("node:path");
26
+ const os = require("node:os");
27
+
28
+ const REGISTRY_URL = "https://registry.npmjs.org/wuphf/latest";
29
+ // Generous enough to survive a cold TLS handshake on a slow network but
30
+ // short enough to not stall the CLI noticeably. Only runs once per 24h
31
+ // per user because the result is cached on disk.
32
+ const FETCH_TIMEOUT_MS = 3000;
33
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
34
+
35
+ function cacheDir() {
36
+ // Sits under ~/.wuphf so HOME-override dev environments (see
37
+ // docs/LOCAL-DEV-PROD-ISOLATION.md) get a separate cache from prod.
38
+ return path.join(os.homedir(), ".wuphf", "cache");
39
+ }
40
+
41
+ function latestVersionCachePath() {
42
+ return path.join(cacheDir(), "latest-version.json");
43
+ }
44
+
45
+ async function readCache() {
46
+ try {
47
+ const raw = await fsp.readFile(latestVersionCachePath(), "utf8");
48
+ const data = JSON.parse(raw);
49
+ if (typeof data.version !== "string" || typeof data.checkedAt !== "number") {
50
+ return null;
51
+ }
52
+ const age = Date.now() - data.checkedAt;
53
+ if (age < 0 || age > CACHE_TTL_MS) return null;
54
+ return data.version;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ async function writeCache(version) {
61
+ try {
62
+ await fsp.mkdir(cacheDir(), { recursive: true });
63
+ const target = latestVersionCachePath();
64
+ const tmp = `${target}.tmp`;
65
+ await fsp.writeFile(tmp, JSON.stringify({ version, checkedAt: Date.now() }));
66
+ await fsp.rename(tmp, target);
67
+ } catch {
68
+ // Cache write is best-effort. A read-only home, full disk, or permission
69
+ // error should not block the user's command.
70
+ }
71
+ }
72
+
73
+ async function fetchLatestFromRegistry() {
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
76
+ try {
77
+ const res = await fetch(REGISTRY_URL, {
78
+ signal: controller.signal,
79
+ headers: { Accept: "application/json" },
80
+ redirect: "follow",
81
+ });
82
+ if (!res.ok) return null;
83
+ const data = await res.json();
84
+ return typeof data.version === "string" ? data.version : null;
85
+ } catch {
86
+ return null;
87
+ } finally {
88
+ clearTimeout(timer);
89
+ }
90
+ }
91
+
92
+ async function getLatestVersion() {
93
+ const cached = await readCache();
94
+ if (cached) return cached;
95
+ const latest = await fetchLatestFromRegistry();
96
+ if (latest) await writeCache(latest);
97
+ return latest;
98
+ }
99
+
100
+ function compareVersions(a, b) {
101
+ const [aCore, aPre = ""] = a.split("-");
102
+ const [bCore, bPre = ""] = b.split("-");
103
+ const aParts = aCore.split(".").map((x) => Number.parseInt(x, 10) || 0);
104
+ const bParts = bCore.split(".").map((x) => Number.parseInt(x, 10) || 0);
105
+ for (let i = 0; i < 3; i += 1) {
106
+ const ap = aParts[i] ?? 0;
107
+ const bp = bParts[i] ?? 0;
108
+ if (ap > bp) return 1;
109
+ if (ap < bp) return -1;
110
+ }
111
+ if (aPre === bPre) return 0;
112
+ // SemVer: a release sorts above its own pre-releases.
113
+ if (!aPre) return 1;
114
+ if (!bPre) return -1;
115
+ return aPre < bPre ? -1 : 1;
116
+ }
117
+
118
+ module.exports = {
119
+ getLatestVersion,
120
+ compareVersions,
121
+ cacheDir,
122
+ latestVersionCachePath,
123
+ // Exported for tests.
124
+ fetchLatestFromRegistry,
125
+ readCache,
126
+ writeCache,
127
+ };