@onebrain-ai/cli 3.0.0 → 3.1.1

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
@@ -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.0.0",
3
+ "version": "3.1.1",
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": ">=18"
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
- * Bypass: set ONEBRAIN_CLI_SKIP_POSTINSTALL=1 to skip the download (useful
13
- * for CI environments that supply their own binary).
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 key = `${process.platform}-${process.arch}`;
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
- console.log('[@onebrain-ai/cli] Extracting ...');
63
- // `tar` ships on macOS, Linux, and Windows 10+ (bsdtar). Spawn it via
64
- // execFileSync argv form so the path is never shell-interpolated.
65
- if (isWin) {
66
- // Windows zip bsdtar auto-detects format.
67
- execFileSync('tar', ['-xf', archivePath, '-C', binDir], { stdio: 'inherit' });
68
- } else {
69
- execFileSync('tar', ['-xzf', archivePath, '-C', binDir], { stdio: 'inherit' });
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
- console.log(`[@onebrain-ai/cli] Installed onebrain v${VERSION} → ${binaryPath}`);
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
- function downloadFile(url, dest, redirects = 5) {
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
+ }