wuphf 0.71.8 → 0.71.10
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 +11 -0
- package/bin/wuphf.js +76 -8
- package/package.json +2 -1
- package/scripts/download-binary.js +21 -7
- package/scripts/version-check.js +127 -0
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.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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.
|
|
3
|
+
"version": "0.71.10",
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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({
|
|
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
|
+
};
|