@onebrain-ai/cli 3.0.0 → 3.1.0
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 +10 -0
- package/bin/onebrain.js +8 -1
- package/package.json +2 -2
- package/postinstall.js +286 -26
package/README.md
CHANGED
|
@@ -53,6 +53,16 @@ The shim still runs but exits with `command not found` (127) until the binary is
|
|
|
53
53
|
|
|
54
54
|
This is the first v3 release on npm. v2.x was the TypeScript/Bun implementation and is now deprecated — use this v3 package going forward. See the [v3.0.0 CHANGELOG](https://github.com/onebrain-ai/onebrain-cli/blob/main/CHANGELOG.md) for the full migration story.
|
|
55
55
|
|
|
56
|
+
## Releasing
|
|
57
|
+
|
|
58
|
+
Source for this package lives at `npm-wrapper/` in the [`onebrain-ai/onebrain-cli`](https://github.com/onebrain-ai/onebrain-cli) repository. Publishes happen automatically from the `npm-publish` job in `.github/workflows/release.yml` whenever a stable `vMAJOR.MINOR.PATCH` tag is pushed:
|
|
59
|
+
|
|
60
|
+
1. The job uses npm Trusted Publishers (OIDC `id-token: write`) — there is no long-lived `NPM_TOKEN` secret to rotate.
|
|
61
|
+
2. `npm version "$VERSION" --no-git-tag-version --allow-same-version` rewrites `package.json` to match the git tag, so the wrapper version always equals the binary release version.
|
|
62
|
+
3. `npm publish --access public --provenance` ships the package with a Sigstore attestation linking it to the exact workflow run and commit.
|
|
63
|
+
|
|
64
|
+
Tags containing `-` (e.g. `v3.0.1-rc.1`) are treated as prereleases and skip the npm publish step. Do not publish this package manually from a local clone — the trusted-publisher policy only honors publishes that originate from this workflow.
|
|
65
|
+
|
|
56
66
|
## License
|
|
57
67
|
|
|
58
68
|
[AGPL-3.0-only](LICENSE) — matches the upstream CLI binary. If you make a modified version available to users over a network (AGPL §13 — SaaS, internal APIs, any networked interaction), you must release your modifications under the same license. For commercial licensing inquiries, contact [hello@onebrain.run](mailto:hello@onebrain.run).
|
package/bin/onebrain.js
CHANGED
|
@@ -20,7 +20,7 @@ const binaryPath = path.join(__dirname, binaryName);
|
|
|
20
20
|
|
|
21
21
|
if (!fs.existsSync(binaryPath)) {
|
|
22
22
|
console.error(`[@onebrain-ai/cli] Binary not found at ${binaryPath}`);
|
|
23
|
-
console.error('Re-run `npm install -g @onebrain-ai/cli` to download it,');
|
|
23
|
+
console.error('Re-run `npm install` (or `npm install -g @onebrain-ai/cli`) to download it,');
|
|
24
24
|
console.error('or visit https://github.com/onebrain-ai/onebrain-cli/releases/latest');
|
|
25
25
|
process.exit(127);
|
|
26
26
|
}
|
|
@@ -30,4 +30,11 @@ if (child.error) {
|
|
|
30
30
|
console.error('[@onebrain-ai/cli] Failed to exec:', child.error.message);
|
|
31
31
|
process.exit(1);
|
|
32
32
|
}
|
|
33
|
+
// Signal-terminated child: re-raise on this process so the parent shell sees
|
|
34
|
+
// the canonical `128 + signum` exit (Ctrl-C → SIGINT → 130, SIGTERM → 143).
|
|
35
|
+
// Without this, CI tooling can't distinguish a user interrupt from a real
|
|
36
|
+
// error — both would collapse to exit 1.
|
|
37
|
+
if (child.signal) {
|
|
38
|
+
process.kill(process.pid, child.signal);
|
|
39
|
+
}
|
|
33
40
|
process.exit(child.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebrain-ai/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Local-first Rust CLI for OneBrain — personal AI OS for Obsidian. Downloads the matching platform binary from GitHub Releases on install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"onebrain",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"postinstall": "node postinstall.js"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
38
|
+
"node": ">=20"
|
|
39
39
|
},
|
|
40
40
|
"os": [
|
|
41
41
|
"darwin",
|
package/postinstall.js
CHANGED
|
@@ -2,21 +2,32 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @onebrain-ai/cli postinstall — downloads the matching platform binary
|
|
4
4
|
* from the OneBrain CLI GitHub Release tagged v${pkg.version}, extracts it
|
|
5
|
-
* to ./bin/, and chmods the result.
|
|
5
|
+
* to ./bin/, verifies SHA256, smoke-runs the binary, and chmods the result.
|
|
6
6
|
*
|
|
7
7
|
* No npm-side caching of binaries — every install pulls fresh from GitHub
|
|
8
8
|
* (matches the rustup / esbuild / swc pattern). For a faster install path,
|
|
9
9
|
* see the optionalDependencies-per-platform layout we may switch to in
|
|
10
10
|
* v3.0.x.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Knobs:
|
|
13
|
+
* ONEBRAIN_CLI_SKIP_POSTINSTALL=1 — skip the download entirely (for CI
|
|
14
|
+
* environments that supply their own
|
|
15
|
+
* binary).
|
|
16
|
+
* ONEBRAIN_CLI_LIBC=glibc|musl — override Linux libc auto-detection.
|
|
17
|
+
* ONEBRAIN_CLI_ARM=v6|v7 — override 32-bit ARM version detect.
|
|
18
|
+
* ONEBRAIN_CLI_DEBUG=1 — verbose logging (cleanup, libc probe).
|
|
19
|
+
*
|
|
20
|
+
* Telemetry: every fetch sends `User-Agent: onebrain-cli-postinstall/$VERSION`
|
|
21
|
+
* to GitHub. This is the only telemetry the postinstall emits; opt out by
|
|
22
|
+
* either setting ONEBRAIN_CLI_SKIP_POSTINSTALL=1 or downloading the binary
|
|
23
|
+
* manually from the GitHub Release.
|
|
14
24
|
*/
|
|
15
25
|
'use strict';
|
|
16
26
|
|
|
17
27
|
const fs = require('node:fs');
|
|
18
28
|
const path = require('node:path');
|
|
19
29
|
const https = require('node:https');
|
|
30
|
+
const crypto = require('node:crypto');
|
|
20
31
|
const { execFileSync } = require('node:child_process');
|
|
21
32
|
|
|
22
33
|
if (process.env.ONEBRAIN_CLI_SKIP_POSTINSTALL) {
|
|
@@ -26,7 +37,11 @@ if (process.env.ONEBRAIN_CLI_SKIP_POSTINSTALL) {
|
|
|
26
37
|
|
|
27
38
|
const pkg = require('./package.json');
|
|
28
39
|
const VERSION = pkg.version;
|
|
40
|
+
const DEBUG = !!process.env.ONEBRAIN_CLI_DEBUG;
|
|
29
41
|
|
|
42
|
+
// The fixed-target host map — Linux + arm32 + musl rows live in resolveTriple
|
|
43
|
+
// because they need runtime detection. This table is the "no detection needed"
|
|
44
|
+
// fast path.
|
|
30
45
|
const TRIPLE_MAP = {
|
|
31
46
|
'darwin-arm64': 'aarch64-apple-darwin',
|
|
32
47
|
'darwin-x64': 'x86_64-apple-darwin',
|
|
@@ -36,15 +51,7 @@ const TRIPLE_MAP = {
|
|
|
36
51
|
'win32-x64': 'x86_64-pc-windows-msvc',
|
|
37
52
|
};
|
|
38
53
|
|
|
39
|
-
const
|
|
40
|
-
const triple = TRIPLE_MAP[key];
|
|
41
|
-
if (!triple) {
|
|
42
|
-
console.error(`[@onebrain-ai/cli] Unsupported platform: ${key}`);
|
|
43
|
-
console.error('Supported: ' + Object.keys(TRIPLE_MAP).join(', '));
|
|
44
|
-
console.error('Manual download: https://github.com/onebrain-ai/onebrain-cli/releases/latest');
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
54
|
+
const triple = resolveTriple();
|
|
48
55
|
const isWin = process.platform === 'win32';
|
|
49
56
|
const archiveExt = isWin ? 'zip' : 'tar.gz';
|
|
50
57
|
const archiveName = `onebrain-${triple}.${archiveExt}`;
|
|
@@ -58,17 +65,36 @@ const archivePath = path.join(root, archiveName);
|
|
|
58
65
|
console.log(`[@onebrain-ai/cli] Downloading v${VERSION} for ${triple} ...`);
|
|
59
66
|
console.log(` ${url}`);
|
|
60
67
|
|
|
61
|
-
downloadFile(url, archivePath).then(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
downloadFile(url, archivePath).then(async () => {
|
|
69
|
+
try {
|
|
70
|
+
// Supply-chain integrity: verify the downloaded archive against the
|
|
71
|
+
// `.sha256` published alongside it in the same GitHub Release. The
|
|
72
|
+
// release workflow generates both files in the same job, so a mismatch
|
|
73
|
+
// indicates either archive corruption or tampering between GitHub's
|
|
74
|
+
// S3 CDN and this host. This closes the gap between the OIDC-attested
|
|
75
|
+
// wrapper publish and the binary the postinstall actually executes.
|
|
76
|
+
console.log('[@onebrain-ai/cli] Verifying SHA256 ...');
|
|
77
|
+
const sha256Body = await downloadText(`${url}.sha256`);
|
|
78
|
+
const expectedHash = sha256Body.trim().split(/\s+/)[0].toLowerCase();
|
|
79
|
+
const actualHash = crypto.createHash('sha256')
|
|
80
|
+
.update(fs.readFileSync(archivePath))
|
|
81
|
+
.digest('hex');
|
|
82
|
+
if (!expectedHash || expectedHash !== actualHash) {
|
|
83
|
+
throw new Error(`SHA256 mismatch — expected ${expectedHash || '<empty>'}, got ${actualHash}. Archive may be corrupt or tampered.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('[@onebrain-ai/cli] Extracting ...');
|
|
87
|
+
extractArchive(archivePath, binDir);
|
|
88
|
+
} finally {
|
|
89
|
+
// Always remove the archive — failed verification or extraction
|
|
90
|
+
// shouldn't leave half-staged files lingering in node_modules/.
|
|
91
|
+
if (fs.existsSync(archivePath)) {
|
|
92
|
+
try { fs.unlinkSync(archivePath); }
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (DEBUG) console.warn(`[@onebrain-ai/cli] archive cleanup failed: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
|
-
fs.unlinkSync(archivePath);
|
|
72
98
|
|
|
73
99
|
const binaryName = isWin ? 'onebrain.exe' : 'onebrain';
|
|
74
100
|
const binaryPath = path.join(binDir, binaryName);
|
|
@@ -79,24 +105,219 @@ downloadFile(url, archivePath).then(() => {
|
|
|
79
105
|
if (!isWin) {
|
|
80
106
|
fs.chmodSync(binaryPath, 0o755);
|
|
81
107
|
}
|
|
82
|
-
|
|
108
|
+
|
|
109
|
+
// Smoke-run the binary so a wrong-libc / wrong-arch download fails at
|
|
110
|
+
// install time with an actionable error instead of segfaulting at first
|
|
111
|
+
// user invocation. ~30-80 ms cost, catches every silent install bug from
|
|
112
|
+
// the detector layer above.
|
|
113
|
+
try {
|
|
114
|
+
const out = execFileSync(binaryPath, ['--version'], {
|
|
115
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
116
|
+
timeout: 10_000,
|
|
117
|
+
}).toString().trim();
|
|
118
|
+
console.log(`[@onebrain-ai/cli] Installed ${out} → ${binaryPath}`);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`[@onebrain-ai/cli] Binary installed but failed to run: ${err.message}`);
|
|
121
|
+
console.error('This usually means the wrong libc/arch variant was selected for your host.');
|
|
122
|
+
console.error('Override with one of:');
|
|
123
|
+
if (process.platform === 'linux' && process.arch === 'arm') {
|
|
124
|
+
console.error(' ONEBRAIN_CLI_ARM=v6 npm install @onebrain-ai/cli # for older ARM (Pi 1 / Pi Zero)');
|
|
125
|
+
console.error(' ONEBRAIN_CLI_ARM=v7 npm install @onebrain-ai/cli # for 32-bit Pi 2/3/4');
|
|
126
|
+
} else if (process.platform === 'linux') {
|
|
127
|
+
console.error(' ONEBRAIN_CLI_LIBC=musl npm install @onebrain-ai/cli');
|
|
128
|
+
console.error(' ONEBRAIN_CLI_LIBC=glibc npm install @onebrain-ai/cli');
|
|
129
|
+
}
|
|
130
|
+
console.error('Set ONEBRAIN_CLI_DEBUG=1 to see which detector path fired.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
83
133
|
}).catch((err) => {
|
|
84
134
|
console.error('[@onebrain-ai/cli] Install failed:', err.message);
|
|
135
|
+
console.error('Possible causes: package.json version mismatches a published release, release was yanked,');
|
|
136
|
+
console.error('CDN propagation lag, or this platform variant is not yet built.');
|
|
85
137
|
console.error('Manual download: https://github.com/onebrain-ai/onebrain-cli/releases/latest');
|
|
86
138
|
process.exit(1);
|
|
87
139
|
});
|
|
88
140
|
|
|
89
|
-
|
|
141
|
+
// resolveTriple picks the Rust target triple for the current host. On Linux
|
|
142
|
+
// the choice between glibc/musl and ARMv6/ARMv7 is dynamic — Alpine and
|
|
143
|
+
// other musl-based distros need the musl-linked binary or they'll fail at
|
|
144
|
+
// runtime when the dynamic loader can't resolve glibc symbols. Raspberry Pi
|
|
145
|
+
// devices span ARMv6 (Pi 1, Pi Zero) through ARMv8 (Pi 5) and the wrong
|
|
146
|
+
// binary segfaults with an illegal-instruction trap.
|
|
147
|
+
function resolveTriple() {
|
|
148
|
+
if (process.platform === 'linux') {
|
|
149
|
+
if (process.arch === 'arm') {
|
|
150
|
+
return resolveArmTriple();
|
|
151
|
+
}
|
|
152
|
+
const libc = resolveLinuxLibc();
|
|
153
|
+
if (libc === 'musl') {
|
|
154
|
+
if (process.arch === 'x64') return 'x86_64-unknown-linux-musl';
|
|
155
|
+
console.error(`[@onebrain-ai/cli] Unsupported platform: linux-${process.arch}-musl`);
|
|
156
|
+
console.error('Only x86_64 musl is published. Build from source or use a glibc-based distro:');
|
|
157
|
+
console.error(' https://github.com/onebrain-ai/onebrain-cli');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const key = `${process.platform}-${process.arch}`;
|
|
163
|
+
const triple = TRIPLE_MAP[key];
|
|
164
|
+
if (!triple) {
|
|
165
|
+
console.error(`[@onebrain-ai/cli] Unsupported platform: ${key}`);
|
|
166
|
+
console.error('Supported: ' + Object.keys(TRIPLE_MAP).join(', '));
|
|
167
|
+
console.error('Manual download: https://github.com/onebrain-ai/onebrain-cli/releases/latest');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
return triple;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// resolveLinuxLibc returns 'musl' or 'glibc'. Override via ONEBRAIN_CLI_LIBC.
|
|
174
|
+
// Detection layers (positive-verification — never silently fall through):
|
|
175
|
+
// 1. Env override — always wins.
|
|
176
|
+
// 2. /etc/alpine-release — definitive Alpine signal.
|
|
177
|
+
// 3. process.report.header.glibcVersionRuntime — empty string on musl,
|
|
178
|
+
// a version like "2.39" on glibc (stable in Node 14+).
|
|
179
|
+
// On a fully unknown Node build (process.report missing, no /etc/alpine-release)
|
|
180
|
+
// we WARN and default to 'glibc' — the user can override via env if the guess
|
|
181
|
+
// is wrong instead of the postinstall installing the wrong binary silently.
|
|
182
|
+
function resolveLinuxLibc() {
|
|
183
|
+
const override = process.env.ONEBRAIN_CLI_LIBC;
|
|
184
|
+
if (override === 'musl' || override === 'glibc') {
|
|
185
|
+
if (DEBUG) console.log(`[@onebrain-ai/cli] libc override: ${override}`);
|
|
186
|
+
return override;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
if (fs.existsSync('/etc/alpine-release')) return 'musl';
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (DEBUG) console.warn(`[@onebrain-ai/cli] libc probe (alpine-release): ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
if (typeof process.report?.getReport === 'function') {
|
|
197
|
+
const header = process.report.getReport()?.header;
|
|
198
|
+
if (header && typeof header.glibcVersionRuntime === 'string') {
|
|
199
|
+
return header.glibcVersionRuntime === '' ? 'musl' : 'glibc';
|
|
200
|
+
}
|
|
201
|
+
// Report exists but the field shape is unfamiliar — likely a future
|
|
202
|
+
// Node version restructured it. Don't guess; warn and let the smoke
|
|
203
|
+
// test catch a wrong choice.
|
|
204
|
+
console.warn('[@onebrain-ai/cli] libc detector: process.report shape unrecognised, defaulting to glibc.');
|
|
205
|
+
console.warn(' Override with ONEBRAIN_CLI_LIBC=musl if this is an Alpine/musl host.');
|
|
206
|
+
return 'glibc';
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (DEBUG) console.warn(`[@onebrain-ai/cli] libc probe (process.report): ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.warn('[@onebrain-ai/cli] libc detector: no probe succeeded, defaulting to glibc.');
|
|
213
|
+
console.warn(' Override with ONEBRAIN_CLI_LIBC=musl if this is an Alpine/musl host.');
|
|
214
|
+
return 'glibc';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// resolveArmTriple picks between armv6 (Pi 1, Pi Zero) and armv7
|
|
218
|
+
// (Pi 2/3/4 in 32-bit OS, Pi Zero 2 W in 32-bit OS) for Linux 32-bit ARM.
|
|
219
|
+
// Override via ONEBRAIN_CLI_ARM=v6 or v7.
|
|
220
|
+
//
|
|
221
|
+
// Conservative default = ARMv6 because an ARMv6 binary runs on ARMv7 hosts
|
|
222
|
+
// but an ARMv7 binary crashes with SIGILL on ARMv6 hosts. We sacrifice some
|
|
223
|
+
// performance on Pi 4 32-bit for correctness on Pi Zero.
|
|
224
|
+
function resolveArmTriple() {
|
|
225
|
+
const override = process.env.ONEBRAIN_CLI_ARM;
|
|
226
|
+
if (override === 'v7') return 'armv7-unknown-linux-gnueabihf';
|
|
227
|
+
if (override === 'v6') return 'arm-unknown-linux-gnueabihf';
|
|
228
|
+
|
|
229
|
+
// process.config.variables.arm_version is set at Node build time:
|
|
230
|
+
// '6' on Node's armv6l build, '7' on armv7l, missing on builds without
|
|
231
|
+
// ARM-specific flags.
|
|
232
|
+
const armVersion = process.config?.variables?.arm_version;
|
|
233
|
+
if (armVersion === '7') return 'armv7-unknown-linux-gnueabihf';
|
|
234
|
+
if (armVersion === '6') return 'arm-unknown-linux-gnueabihf';
|
|
235
|
+
|
|
236
|
+
// Fall back to /proc/cpuinfo. CHECK ORDER MATTERS: the kernel reports
|
|
237
|
+
// `CPU architecture: 7` even on Pi 1 / Pi Zero (ARM1176 cores) because the
|
|
238
|
+
// chip supports VMSAv7 (virtual memory arch) despite the instruction-set
|
|
239
|
+
// being ARMv6. So the `CPU architecture` line cannot be trusted on its own.
|
|
240
|
+
// The `model name` line ("ARMv6-compatible processor" on Pi Zero) is the
|
|
241
|
+
// reliable signal, and we check it FIRST.
|
|
242
|
+
try {
|
|
243
|
+
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
|
|
244
|
+
if (/ARMv6|ARM1176|ARM11\b/i.test(cpuinfo)) {
|
|
245
|
+
return 'arm-unknown-linux-gnueabihf';
|
|
246
|
+
}
|
|
247
|
+
if (/ARMv7|Cortex-A[789]|Cortex-A1[5-7]/i.test(cpuinfo) || /CPU architecture:\s*7/m.test(cpuinfo)) {
|
|
248
|
+
return 'armv7-unknown-linux-gnueabihf';
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (DEBUG) console.warn(`[@onebrain-ai/cli] arm probe (cpuinfo): ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.warn('[@onebrain-ai/cli] ARM version detection inconclusive — defaulting to ARMv6 (compatible everywhere, slower on Pi 4).');
|
|
255
|
+
console.warn(' Override with ONEBRAIN_CLI_ARM=v7 to force the ARMv7 binary.');
|
|
256
|
+
return 'arm-unknown-linux-gnueabihf';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// extractArchive unpacks .tar.gz on Unix and .zip on Windows. The Windows
|
|
260
|
+
// path tries bsdtar first (ships on Windows 10 1803+) and falls back to
|
|
261
|
+
// PowerShell Expand-Archive for older hosts. PowerShell paths use
|
|
262
|
+
// -LiteralPath to avoid wildcard expansion on '[' or ']' and are escaped
|
|
263
|
+
// for single-quote literals — '' is PowerShell's only single-quote escape
|
|
264
|
+
// inside a '...'-string, so `escapePsSingleQuoted` closes off injection.
|
|
265
|
+
function extractArchive(archive, destDir) {
|
|
266
|
+
if (!isWin) {
|
|
267
|
+
execFileSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
execFileSync('tar', ['-xf', archive, '-C', destDir], { stdio: 'inherit' });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
// ANY tar failure on Windows falls back to PowerShell — the archive
|
|
275
|
+
// already passed SHA256, so a tar error means either tar.exe is missing
|
|
276
|
+
// (pre-1803 Windows 10, ENOENT) or PATH points at GNU tar from MSYS2 /
|
|
277
|
+
// Git-for-Windows / Cygwin (which doesn't grok .zip and exits non-zero).
|
|
278
|
+
// Expand-Archive ships with PowerShell 5.1 on every Windows 10+ host.
|
|
279
|
+
const reason = err.code || err.message;
|
|
280
|
+
console.log(`[@onebrain-ai/cli] tar failed (${reason}), falling back to PowerShell Expand-Archive ...`);
|
|
281
|
+
const escapedArchive = escapePsSingleQuoted(archive);
|
|
282
|
+
const escapedDest = escapePsSingleQuoted(destDir);
|
|
283
|
+
const command = `$ErrorActionPreference='Stop'; Expand-Archive -LiteralPath '${escapedArchive}' -DestinationPath '${escapedDest}' -Force`;
|
|
284
|
+
execFileSync(
|
|
285
|
+
'powershell.exe',
|
|
286
|
+
['-NoProfile', '-NonInteractive', '-Command', command],
|
|
287
|
+
{ stdio: 'inherit' }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function escapePsSingleQuoted(s) {
|
|
293
|
+
return s.replace(/'/g, "''");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// downloadFile streams a binary to disk. Follows GitHub's redirect chain
|
|
297
|
+
// (S3 CDN) and retries on 404 with exponential backoff — after `npm publish`
|
|
298
|
+
// the wrapper can be live before the release CDN has fanned out, so a fresh
|
|
299
|
+
// `npm install` can race the binary. Three retries at 2s/4s/8s = 14s total
|
|
300
|
+
// covers the worst observed lag. The same 404 also fires for permanent
|
|
301
|
+
// failures (wrong version, yanked release, missing platform variant) — the
|
|
302
|
+
// final reject path elaborates on the possible causes.
|
|
303
|
+
function downloadFile(url, dest, redirects = 5, retries = 3, backoffMs = 2000) {
|
|
90
304
|
return new Promise((resolve, reject) => {
|
|
91
305
|
https.get(url, { headers: { 'User-Agent': `onebrain-cli-postinstall/${VERSION}` } }, (res) => {
|
|
92
|
-
// GitHub Release downloads always redirect to S3 — follow.
|
|
93
306
|
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
94
307
|
if (redirects <= 0) {
|
|
95
308
|
reject(new Error('Too many redirects'));
|
|
96
309
|
return;
|
|
97
310
|
}
|
|
98
311
|
res.resume();
|
|
99
|
-
downloadFile(res.headers.location, dest, redirects - 1).then(resolve, reject);
|
|
312
|
+
downloadFile(res.headers.location, dest, redirects - 1, retries, backoffMs).then(resolve, reject);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (res.statusCode === 404 && retries > 0) {
|
|
316
|
+
res.resume();
|
|
317
|
+
console.log(`[@onebrain-ai/cli] binary not yet on CDN, retrying in ${backoffMs}ms ...`);
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
downloadFile(url, dest, redirects, retries - 1, backoffMs * 2).then(resolve, reject);
|
|
320
|
+
}, backoffMs);
|
|
100
321
|
return;
|
|
101
322
|
}
|
|
102
323
|
if (res.statusCode !== 200) {
|
|
@@ -110,3 +331,42 @@ function downloadFile(url, dest, redirects = 5) {
|
|
|
110
331
|
}).on('error', reject);
|
|
111
332
|
});
|
|
112
333
|
}
|
|
334
|
+
|
|
335
|
+
// downloadText follows the same redirect + retry chain as downloadFile but
|
|
336
|
+
// buffers the body into a UTF-8 string. Used for small metadata files
|
|
337
|
+
// (currently the `.sha256` checksum next to each release archive). Don't
|
|
338
|
+
// use it for binaries — the body lives in memory for the duration of the
|
|
339
|
+
// request. Inherits the same 404 retry policy since the .sha256 is
|
|
340
|
+
// published in the same Release as the archive and CDN-lags together.
|
|
341
|
+
function downloadText(url, redirects = 5, retries = 3, backoffMs = 2000) {
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
https.get(url, { headers: { 'User-Agent': `onebrain-cli-postinstall/${VERSION}` } }, (res) => {
|
|
344
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
345
|
+
if (redirects <= 0) {
|
|
346
|
+
reject(new Error('Too many redirects'));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
res.resume();
|
|
350
|
+
downloadText(res.headers.location, redirects - 1, retries, backoffMs).then(resolve, reject);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (res.statusCode === 404 && retries > 0) {
|
|
354
|
+
res.resume();
|
|
355
|
+
console.log(`[@onebrain-ai/cli] sha256 not yet on CDN, retrying in ${backoffMs}ms ...`);
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
downloadText(url, redirects, retries - 1, backoffMs * 2).then(resolve, reject);
|
|
358
|
+
}, backoffMs);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (res.statusCode !== 200) {
|
|
362
|
+
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
let body = '';
|
|
366
|
+
res.setEncoding('utf8');
|
|
367
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
368
|
+
res.on('end', () => resolve(body));
|
|
369
|
+
res.on('error', reject);
|
|
370
|
+
}).on('error', reject);
|
|
371
|
+
});
|
|
372
|
+
}
|