@onebrain-ai/cli 2.3.3 → 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.
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @onebrain-ai/cli shim — execs the platform-native onebrain binary that
4
+ * `postinstall.js` extracted into this directory. Uniform across darwin,
5
+ * linux, and win32 so npm consumers get a single `onebrain` command name.
6
+ *
7
+ * Exit code is propagated from the child; if the binary is missing (e.g.
8
+ * postinstall was skipped via ONEBRAIN_CLI_SKIP_POSTINSTALL or failed),
9
+ * we print a helpful pointer and exit 127 (sysexits "command not found").
10
+ */
11
+ 'use strict';
12
+
13
+ const path = require('node:path');
14
+ const fs = require('node:fs');
15
+ const { spawnSync } = require('node:child_process');
16
+
17
+ const isWin = process.platform === 'win32';
18
+ const binaryName = isWin ? 'onebrain.exe' : 'onebrain';
19
+ const binaryPath = path.join(__dirname, binaryName);
20
+
21
+ if (!fs.existsSync(binaryPath)) {
22
+ console.error(`[@onebrain-ai/cli] Binary not found at ${binaryPath}`);
23
+ console.error('Re-run `npm install` (or `npm install -g @onebrain-ai/cli`) to download it,');
24
+ console.error('or visit https://github.com/onebrain-ai/onebrain-cli/releases/latest');
25
+ process.exit(127);
26
+ }
27
+
28
+ const child = spawnSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
29
+ if (child.error) {
30
+ console.error('[@onebrain-ai/cli] Failed to exec:', child.error.message);
31
+ process.exit(1);
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
+ }
40
+ process.exit(child.status ?? 1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@onebrain-ai/cli",
3
- "version": "2.3.3",
4
- "description": "CLI for OneBrain — personal AI OS for Obsidian with persistent memory, 24+ skills, and Claude Code integration",
3
+ "version": "3.1.0",
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",
7
7
  "obsidian",
@@ -9,41 +9,41 @@
9
9
  "cli",
10
10
  "memory",
11
11
  "knowledge-management",
12
- "claude",
12
+ "claude-code",
13
13
  "agent",
14
14
  "pkm",
15
- "productivity",
16
- "vault"
15
+ "rust"
17
16
  ],
18
17
  "homepage": "https://onebrain.run",
18
+ "bugs": "https://github.com/onebrain-ai/onebrain-cli/issues",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "git+https://github.com/onebrain-ai/onebrain.git"
21
+ "url": "git+https://github.com/onebrain-ai/onebrain-cli.git"
22
22
  },
23
- "bugs": "https://github.com/onebrain-ai/onebrain/issues",
24
- "license": "MIT",
25
- "type": "module",
23
+ "license": "AGPL-3.0-only",
24
+ "author": "OneBrain Contributors",
26
25
  "bin": {
27
- "onebrain": "dist/onebrain"
26
+ "onebrain": "bin/onebrain.js"
28
27
  },
29
- "files": ["dist/onebrain", "dist/postinstall.js"],
28
+ "files": [
29
+ "bin/",
30
+ "postinstall.js",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
30
34
  "scripts": {
31
- "build": "bun build src/index.ts --outfile dist/onebrain --target bun",
32
- "build:postinstall": "bun build src/scripts/postinstall.ts --outfile dist/postinstall.js --target node",
33
- "postinstall": "node dist/postinstall.js",
34
- "test": "bun test --pass-with-no-tests src/",
35
- "typecheck": "tsc --noEmit"
35
+ "postinstall": "node postinstall.js"
36
36
  },
37
- "dependencies": {
38
- "@clack/prompts": "^0.9",
39
- "commander": "^12",
40
- "picocolors": "^1",
41
- "yaml": "^2"
37
+ "engines": {
38
+ "node": ">=20"
42
39
  },
43
- "devDependencies": {
44
- "@biomejs/biome": "^1.9",
45
- "@types/bun": "latest",
46
- "@types/node": "^20",
47
- "typescript": "^5.7"
48
- }
40
+ "os": [
41
+ "darwin",
42
+ "linux",
43
+ "win32"
44
+ ],
45
+ "cpu": [
46
+ "x64",
47
+ "arm64"
48
+ ]
49
49
  }
package/postinstall.js ADDED
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @onebrain-ai/cli postinstall — downloads the matching platform binary
4
+ * from the OneBrain CLI GitHub Release tagged v${pkg.version}, extracts it
5
+ * to ./bin/, verifies SHA256, smoke-runs the binary, and chmods the result.
6
+ *
7
+ * No npm-side caching of binaries — every install pulls fresh from GitHub
8
+ * (matches the rustup / esbuild / swc pattern). For a faster install path,
9
+ * see the optionalDependencies-per-platform layout we may switch to in
10
+ * v3.0.x.
11
+ *
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.
24
+ */
25
+ 'use strict';
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+ const https = require('node:https');
30
+ const crypto = require('node:crypto');
31
+ const { execFileSync } = require('node:child_process');
32
+
33
+ if (process.env.ONEBRAIN_CLI_SKIP_POSTINSTALL) {
34
+ console.log('[@onebrain-ai/cli] ONEBRAIN_CLI_SKIP_POSTINSTALL set — skipping binary download.');
35
+ process.exit(0);
36
+ }
37
+
38
+ const pkg = require('./package.json');
39
+ const VERSION = pkg.version;
40
+ const DEBUG = !!process.env.ONEBRAIN_CLI_DEBUG;
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.
45
+ const TRIPLE_MAP = {
46
+ 'darwin-arm64': 'aarch64-apple-darwin',
47
+ 'darwin-x64': 'x86_64-apple-darwin',
48
+ 'linux-arm64': 'aarch64-unknown-linux-gnu',
49
+ 'linux-x64': 'x86_64-unknown-linux-gnu',
50
+ 'win32-arm64': 'aarch64-pc-windows-msvc',
51
+ 'win32-x64': 'x86_64-pc-windows-msvc',
52
+ };
53
+
54
+ const triple = resolveTriple();
55
+ const isWin = process.platform === 'win32';
56
+ const archiveExt = isWin ? 'zip' : 'tar.gz';
57
+ const archiveName = `onebrain-${triple}.${archiveExt}`;
58
+ const url = `https://github.com/onebrain-ai/onebrain-cli/releases/download/v${VERSION}/${archiveName}`;
59
+
60
+ const root = __dirname;
61
+ const binDir = path.join(root, 'bin');
62
+ fs.mkdirSync(binDir, { recursive: true });
63
+ const archivePath = path.join(root, archiveName);
64
+
65
+ console.log(`[@onebrain-ai/cli] Downloading v${VERSION} for ${triple} ...`);
66
+ console.log(` ${url}`);
67
+
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
+ }
97
+ }
98
+
99
+ const binaryName = isWin ? 'onebrain.exe' : 'onebrain';
100
+ const binaryPath = path.join(binDir, binaryName);
101
+ if (!fs.existsSync(binaryPath)) {
102
+ console.error(`[@onebrain-ai/cli] Extraction succeeded but ${binaryName} not found at ${binDir}`);
103
+ process.exit(1);
104
+ }
105
+ if (!isWin) {
106
+ fs.chmodSync(binaryPath, 0o755);
107
+ }
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
+ }
133
+ }).catch((err) => {
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.');
137
+ console.error('Manual download: https://github.com/onebrain-ai/onebrain-cli/releases/latest');
138
+ process.exit(1);
139
+ });
140
+
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) {
304
+ return new Promise((resolve, reject) => {
305
+ https.get(url, { headers: { 'User-Agent': `onebrain-cli-postinstall/${VERSION}` } }, (res) => {
306
+ if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
307
+ if (redirects <= 0) {
308
+ reject(new Error('Too many redirects'));
309
+ return;
310
+ }
311
+ res.resume();
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);
321
+ return;
322
+ }
323
+ if (res.statusCode !== 200) {
324
+ reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
325
+ return;
326
+ }
327
+ const file = fs.createWriteStream(dest);
328
+ res.pipe(file);
329
+ file.on('finish', () => file.close(() => resolve()));
330
+ file.on('error', reject);
331
+ }).on('error', reject);
332
+ });
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
+ }