@shuyhere/bb-agent 0.0.9 → 0.0.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/CHANGELOG.md +27 -0
- package/README.md +51 -37
- package/package.json +1 -1
- package/scripts/postinstall.js +444 -78
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to BB-Agent will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.11] - 2026-04-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- startup update notices in the fullscreen transcript are now highlighted so available updates stand out clearly during startup
|
|
13
|
+
- read-tool line ranges in fullscreen tool activity now highlight the requested span, so values like `2148-2267/5006` stand out while the model is using tools
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
|
|
17
|
+
- npm install now caches verified native binaries by version/platform and reuses them on reinstall instead of re-downloading every time
|
|
18
|
+
- npm install now shows more frequent download progress with transfer rate information to make slow installs easier to understand
|
|
19
|
+
- npm install now avoids unnecessary re-verification on cache hits, making repeat installs faster
|
|
20
|
+
|
|
21
|
+
## [0.0.10] - 2026-04-07
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- npm install now uses a longer timeout, retries release-binary downloads, and reports real download errors instead of incorrectly saying no matching prebuilt binary exists
|
|
26
|
+
- npm install now shows progress logs during native binary download and verification so first-time installs on macOS/Linux are less confusing
|
|
27
|
+
- fullscreen `/login` provider-family status now correctly shows OpenAI OAuth state after ChatGPT login instead of incorrectly showing the API key path as not authenticated
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- README install docs now lead with `npm install -g @shuyhere/bb-agent`, move terminal/font guidance into Troubleshooting, and clearly separate npm install from building from source for development
|
|
32
|
+
|
|
8
33
|
## [0.0.9] - 2026-04-07
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -46,5 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
46
71
|
|
|
47
72
|
- latest published package includes the post-0.0.7 startup, auth, model-default, and update-notice improvements
|
|
48
73
|
|
|
74
|
+
[0.0.11]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.11
|
|
75
|
+
[0.0.10]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.10
|
|
49
76
|
[0.0.9]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.9
|
|
50
77
|
[0.0.8]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.8
|
package/README.md
CHANGED
|
@@ -2,44 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
BB means Bridge Baby in Death Stranding. I named this project that way because while building it, I was also enjoying Death Stranding and loved the idea of connecting everyone together.
|
|
5
|
+
> BB means Bridge Baby in Death Stranding. I named this project that way because while building it, I was also enjoying Death Stranding and loved the idea of connecting everyone together.
|
|
6
6
|
|
|
7
7
|
A Rust-native AI coding agent for the terminal — featuring a fullscreen TUI, multi-provider support, tool use, session persistence, branching, extensions, and skills.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- JetBrains Mono
|
|
16
|
-
- SF Mono / Menlo
|
|
17
|
-
- Fira Code
|
|
18
|
-
- Cascadia Mono
|
|
19
|
-
- Nerd Font variants of the above
|
|
20
|
-
|
|
21
|
-
If some symbols look broken, missing, or too narrow in your terminal:
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @shuyhere/bb-agent
|
|
13
|
+
```
|
|
22
14
|
|
|
23
|
-
1.
|
|
24
|
-
2. make sure your terminal uses UTF-8
|
|
25
|
-
3. enable BB-Agent compatibility mode
|
|
15
|
+
### 1. Install with npm
|
|
26
16
|
|
|
27
|
-
|
|
17
|
+
npm install downloads a small wrapper package first, then fetches the matching native BB-Agent binary from the GitHub release for your platform.
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
What to expect:
|
|
20
|
+
- first install can take a bit because npm downloads and verifies the native binary
|
|
21
|
+
- the installer now prints progress and retry information while downloading
|
|
22
|
+
- after install, run `bb`
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
Current GitHub release binaries are published for:
|
|
25
|
+
- Linux x86_64
|
|
26
|
+
- macOS x86_64
|
|
27
|
+
- macOS arm64 (Apple Silicon)
|
|
28
|
+
- Windows x86_64
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
"compatibility_mode": true
|
|
38
|
-
}
|
|
39
|
-
```
|
|
30
|
+
If no matching prebuilt binary is available, or the download fails, npm install will print source-build instructions instead.
|
|
40
31
|
|
|
32
|
+
### 2. Build from source for development
|
|
41
33
|
|
|
42
|
-
|
|
34
|
+
Use this if you want to develop BB-Agent itself, work on the Rust codebase, or install directly without the npm downloader.
|
|
43
35
|
|
|
44
36
|
Requires [Rust](https://rustup.rs). Install Rust first if you don't have it:
|
|
45
37
|
|
|
@@ -48,7 +40,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
|
48
40
|
source ~/.cargo/env
|
|
49
41
|
```
|
|
50
42
|
|
|
51
|
-
Then build and install BB-Agent:
|
|
43
|
+
Then build and install BB-Agent from source:
|
|
52
44
|
|
|
53
45
|
```bash
|
|
54
46
|
git clone https://github.com/shuyhere/bb-agent.git
|
|
@@ -58,16 +50,6 @@ cargo install --path crates/cli
|
|
|
58
50
|
|
|
59
51
|
This compiles the `bb` binary and installs it to `~/.cargo/bin/bb` (which Rust adds to your PATH).
|
|
60
52
|
|
|
61
|
-
### npm (Linux/macOS/Windows — downloads matching prebuilt binary when available)
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npm install -g @shuyhere/bb-agent
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
> If no matching prebuilt binary is available for your platform, npm install will print source-build instructions instead. After install, run `bb` to start.
|
|
68
|
-
>
|
|
69
|
-
> Current GitHub release binaries are published for Linux x86_64, macOS x86_64/arm64, and Windows x86_64.
|
|
70
|
-
|
|
71
53
|
## Getting Started
|
|
72
54
|
|
|
73
55
|
### 1. Start the TUI
|
|
@@ -205,6 +187,38 @@ BB-Agent uses layered configuration:
|
|
|
205
187
|
| `bb-tui` | Terminal UI components and fullscreen experience |
|
|
206
188
|
| `bb-cli` | The `bb` command-line application |
|
|
207
189
|
|
|
190
|
+
## Troubleshooting
|
|
191
|
+
|
|
192
|
+
### Terminal & Font Compatibility
|
|
193
|
+
|
|
194
|
+
BB-Agent uses Unicode glyphs and ANSI color in the fullscreen TUI. For the best visual experience, use a modern terminal and a Unicode-capable monospace font such as:
|
|
195
|
+
|
|
196
|
+
- JetBrains Mono
|
|
197
|
+
- SF Mono / Menlo
|
|
198
|
+
- Fira Code
|
|
199
|
+
- Cascadia Mono
|
|
200
|
+
- Nerd Font variants of the above
|
|
201
|
+
|
|
202
|
+
If some symbols look broken, missing, or too narrow in your terminal:
|
|
203
|
+
|
|
204
|
+
1. switch to a Unicode-capable monospace font
|
|
205
|
+
2. make sure your terminal uses UTF-8
|
|
206
|
+
3. enable BB-Agent compatibility mode
|
|
207
|
+
|
|
208
|
+
Compatibility mode uses safer ASCII-style fallback glyphs for spinner/status/tool markers:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
BB_TUI_COMPAT=1 bb
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or set this in `~/.bb-agent/settings.json`:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"compatibility_mode": true
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
208
222
|
## Documentation
|
|
209
223
|
|
|
210
224
|
- [Configuration Reference](docs/configuration.md) — settings.json, AGENTS.md, templates
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
const https = require("https");
|
|
10
|
+
const http = require("http");
|
|
10
11
|
|
|
11
12
|
const packageJson = require("../package.json");
|
|
12
13
|
const BINARY_RELEASE_TAG = `v${packageJson.version}`;
|
|
13
14
|
const REPO = "shuyhere/bb-agent";
|
|
14
|
-
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
15
15
|
const NATIVE_DIR = path.join(__dirname, "..", "native");
|
|
16
|
-
const DOWNLOAD_TIMEOUT_MS =
|
|
16
|
+
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
17
|
+
const DOWNLOAD_PROGRESS_INTERVAL_MS = 1_000;
|
|
18
|
+
const MAX_REDIRECTS = 8;
|
|
19
|
+
const MAX_DOWNLOAD_ATTEMPTS = 3;
|
|
17
20
|
|
|
18
21
|
function isWindows() {
|
|
19
22
|
return os.platform() === "win32";
|
|
@@ -44,119 +47,482 @@ function getTarget() {
|
|
|
44
47
|
return `${a}-${p}`;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
function
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
+
function assetNameForTarget(target) {
|
|
51
|
+
return isWindows() ? `bb-${target}.exe` : `bb-${target}`;
|
|
52
|
+
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
function logLine(message = "") {
|
|
55
|
+
try {
|
|
56
|
+
process.stderr.write(`${message}\n`);
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
}
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (res.statusCode !== 200) {
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
62
|
-
}
|
|
63
|
-
const file = fs.createWriteStream(dest);
|
|
64
|
-
res.pipe(file);
|
|
65
|
-
file.on("finish", () => { clearTimeout(timer); file.close(); resolve(); });
|
|
66
|
-
file.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
67
|
-
});
|
|
68
|
-
req.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
69
|
-
req.on("timeout", () => { req.destroy(); clearTimeout(timer); reject(new Error("Request timed out")); });
|
|
70
|
-
};
|
|
71
|
-
follow(url);
|
|
72
|
-
});
|
|
60
|
+
function makeDownloadError(kind, message, statusCode) {
|
|
61
|
+
const err = new Error(message);
|
|
62
|
+
err.kind = kind;
|
|
63
|
+
if (statusCode) err.statusCode = statusCode;
|
|
64
|
+
return err;
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
function
|
|
76
|
-
|
|
67
|
+
function formatBytes(bytes) {
|
|
68
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
69
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
70
|
+
let value = bytes;
|
|
71
|
+
let unit = 0;
|
|
72
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
73
|
+
value /= 1024;
|
|
74
|
+
unit += 1;
|
|
75
|
+
}
|
|
76
|
+
return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatRate(bytesPerSecond) {
|
|
80
|
+
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s";
|
|
81
|
+
return `${formatBytes(bytesPerSecond)}/s`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ensureParentDir(filePath) {
|
|
85
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function removeIfExists(filePath) {
|
|
89
|
+
try {
|
|
90
|
+
fs.unlinkSync(filePath);
|
|
91
|
+
} catch (_) {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function binaryVersion(binaryPath) {
|
|
95
|
+
try {
|
|
96
|
+
const out = execFileSync(binaryPath, ["--version"], {
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
timeout: 2500,
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
});
|
|
101
|
+
return (out || "").trim();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function binaryMatchesCurrentVersion(binaryPath) {
|
|
108
|
+
const version = binaryVersion(binaryPath);
|
|
109
|
+
if (!version) return false;
|
|
110
|
+
return version.includes(packageJson.version);
|
|
77
111
|
}
|
|
78
112
|
|
|
79
113
|
function hasBundledNativeBinary() {
|
|
80
114
|
const dest = nativeBinaryPath();
|
|
81
115
|
if (!fs.existsSync(dest)) return false;
|
|
116
|
+
if (!binaryMatchesCurrentVersion(dest)) return false;
|
|
82
117
|
try {
|
|
83
|
-
|
|
118
|
+
fs.accessSync(dest, fs.constants.X_OK);
|
|
84
119
|
return true;
|
|
85
120
|
} catch {
|
|
86
121
|
return false;
|
|
87
122
|
}
|
|
88
123
|
}
|
|
89
124
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
function cacheRootDir() {
|
|
126
|
+
if (process.env.BB_INSTALL_CACHE_DIR && process.env.BB_INSTALL_CACHE_DIR.trim()) {
|
|
127
|
+
return process.env.BB_INSTALL_CACHE_DIR;
|
|
128
|
+
}
|
|
93
129
|
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
const home = os.homedir();
|
|
131
|
+
if (isWindows()) {
|
|
132
|
+
return path.join(
|
|
133
|
+
process.env.LOCALAPPDATA || process.env.APPDATA || path.join(home, "AppData", "Local"),
|
|
134
|
+
"bb-agent"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (os.platform() === "darwin") {
|
|
138
|
+
return path.join(home, "Library", "Caches", "bb-agent");
|
|
139
|
+
}
|
|
140
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(home, ".cache"), "bb-agent");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cacheBinaryPath(target) {
|
|
144
|
+
return path.join(cacheRootDir(), "prebuilt", packageJson.version, assetNameForTarget(target));
|
|
145
|
+
}
|
|
96
146
|
|
|
147
|
+
function cacheMetadataPath(target) {
|
|
148
|
+
return `${cacheBinaryPath(target)}.json`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function loadCacheMetadata(target) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(fs.readFileSync(cacheMetadataPath(target), "utf8"));
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function storeCacheMetadata(target, binaryPath) {
|
|
97
160
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
161
|
+
const stat = fs.statSync(binaryPath);
|
|
162
|
+
ensureParentDir(cacheMetadataPath(target));
|
|
163
|
+
fs.writeFileSync(
|
|
164
|
+
cacheMetadataPath(target),
|
|
165
|
+
JSON.stringify(
|
|
166
|
+
{
|
|
167
|
+
version: packageJson.version,
|
|
168
|
+
target,
|
|
169
|
+
assetName: assetNameForTarget(target),
|
|
170
|
+
binaryName: nativeBinaryName(),
|
|
171
|
+
size: stat.size,
|
|
172
|
+
verifiedAt: new Date().toISOString(),
|
|
173
|
+
},
|
|
174
|
+
null,
|
|
175
|
+
2
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function copyBinary(src, dest) {
|
|
182
|
+
ensureParentDir(dest);
|
|
183
|
+
fs.copyFileSync(src, dest);
|
|
184
|
+
if (!isWindows()) {
|
|
100
185
|
fs.chmodSync(dest, 0o755);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function installFromVerifiedCache(target) {
|
|
190
|
+
const cached = cacheBinaryPath(target);
|
|
191
|
+
const meta = loadCacheMetadata(target);
|
|
192
|
+
if (!fs.existsSync(cached) || !meta) return false;
|
|
193
|
+
if (meta.version !== packageJson.version || meta.target !== target) return false;
|
|
194
|
+
|
|
195
|
+
let stat;
|
|
196
|
+
try {
|
|
197
|
+
stat = fs.statSync(cached);
|
|
198
|
+
} catch (_) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (!stat.isFile() || stat.size <= 0) return false;
|
|
202
|
+
if (meta.size && stat.size !== meta.size) return false;
|
|
203
|
+
|
|
204
|
+
logLine(`Using cached BB-Agent binary for ${target} (${formatBytes(stat.size)}).`);
|
|
205
|
+
copyBinary(cached, nativeBinaryPath());
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function refreshCacheFromExistingBinary(target, sourcePath) {
|
|
210
|
+
if (!binaryMatchesCurrentVersion(sourcePath)) return false;
|
|
211
|
+
const cached = cacheBinaryPath(target);
|
|
212
|
+
copyBinary(sourcePath, cached);
|
|
213
|
+
storeCacheMetadata(target, cached);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function maybeRepairCache(target) {
|
|
218
|
+
const cached = cacheBinaryPath(target);
|
|
219
|
+
if (!fs.existsSync(cached)) return false;
|
|
101
220
|
|
|
102
|
-
|
|
221
|
+
const meta = loadCacheMetadata(target);
|
|
222
|
+
if (meta && meta.version === packageJson.version && meta.target === target && meta.size) {
|
|
103
223
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
224
|
+
const stat = fs.statSync(cached);
|
|
225
|
+
if (stat.isFile() && stat.size === meta.size) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
} catch (_) {
|
|
108
229
|
return false;
|
|
109
230
|
}
|
|
231
|
+
}
|
|
110
232
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try { fs.unlinkSync(dest); } catch {}
|
|
233
|
+
logLine(`Checking cached BB-Agent binary for ${target}...`);
|
|
234
|
+
if (!binaryMatchesCurrentVersion(cached)) {
|
|
235
|
+
removeIfExists(cached);
|
|
236
|
+
removeIfExists(cacheMetadataPath(target));
|
|
116
237
|
return false;
|
|
117
238
|
}
|
|
239
|
+
|
|
240
|
+
storeCacheMetadata(target, cached);
|
|
241
|
+
logLine("Verified cached BB-Agent binary for reuse.");
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function requestBinary(url, dest, redirects = 0) {
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
if (redirects > MAX_REDIRECTS) {
|
|
248
|
+
reject(makeDownloadError("redirect", "Too many redirects"));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const client = url.startsWith("https:") ? https : http;
|
|
253
|
+
const req = client.get(
|
|
254
|
+
url,
|
|
255
|
+
{
|
|
256
|
+
headers: {
|
|
257
|
+
"User-Agent": `${packageJson.name}/${packageJson.version} (postinstall)`,
|
|
258
|
+
Accept: "application/octet-stream,application/octet-stream; q=0.9,*/*;q=0.1",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
(res) => {
|
|
262
|
+
const status = res.statusCode || 0;
|
|
263
|
+
|
|
264
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
265
|
+
res.resume();
|
|
266
|
+
requestBinary(res.headers.location, dest, redirects + 1)
|
|
267
|
+
.then(resolve)
|
|
268
|
+
.catch(reject);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (status === 404) {
|
|
273
|
+
res.resume();
|
|
274
|
+
reject(makeDownloadError("not-found", `HTTP 404 for ${url}`, 404));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (status !== 200) {
|
|
279
|
+
res.resume();
|
|
280
|
+
reject(makeDownloadError("http", `HTTP ${status} for ${url}`, status));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const totalBytes = Number(res.headers["content-length"] || 0);
|
|
285
|
+
const startedAt = Date.now();
|
|
286
|
+
let downloadedBytes = 0;
|
|
287
|
+
let lastLoggedAt = 0;
|
|
288
|
+
|
|
289
|
+
if (totalBytes > 0) {
|
|
290
|
+
logLine(`Release asset size: ${formatBytes(totalBytes)}.`);
|
|
291
|
+
} else {
|
|
292
|
+
logLine("Release asset size: unknown (streaming download).");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
ensureParentDir(dest);
|
|
296
|
+
const file = fs.createWriteStream(dest);
|
|
297
|
+
let settled = false;
|
|
298
|
+
|
|
299
|
+
const finish = (fn, value) => {
|
|
300
|
+
if (settled) return;
|
|
301
|
+
settled = true;
|
|
302
|
+
clearTimeout(timeout);
|
|
303
|
+
fn(value);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
res.on("data", (chunk) => {
|
|
307
|
+
downloadedBytes += chunk.length;
|
|
308
|
+
const now = Date.now();
|
|
309
|
+
if (now - lastLoggedAt >= DOWNLOAD_PROGRESS_INTERVAL_MS) {
|
|
310
|
+
lastLoggedAt = now;
|
|
311
|
+
const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001);
|
|
312
|
+
const rate = downloadedBytes / elapsedSeconds;
|
|
313
|
+
if (totalBytes > 0) {
|
|
314
|
+
const percent = Math.min(100, Math.round((downloadedBytes / totalBytes) * 100));
|
|
315
|
+
logLine(
|
|
316
|
+
`Download progress: ${percent}% (${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}, ${formatRate(rate)})`
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
logLine(`Downloaded ${formatBytes(downloadedBytes)} so far (${formatRate(rate)})...`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
file.on("finish", () => {
|
|
325
|
+
file.close((closeErr) => {
|
|
326
|
+
if (closeErr) {
|
|
327
|
+
finish(reject, makeDownloadError("write", closeErr.message));
|
|
328
|
+
} else {
|
|
329
|
+
const elapsedSeconds = Math.max((Date.now() - startedAt) / 1000, 0.001);
|
|
330
|
+
const rate = downloadedBytes / elapsedSeconds;
|
|
331
|
+
if (totalBytes > 0) {
|
|
332
|
+
logLine(
|
|
333
|
+
`Download complete: ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} in ${elapsedSeconds.toFixed(1)}s (${formatRate(rate)}).`
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
logLine(
|
|
337
|
+
`Download complete: ${formatBytes(downloadedBytes)} in ${elapsedSeconds.toFixed(1)}s (${formatRate(rate)}).`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
finish(resolve);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
file.on("error", (err) => {
|
|
346
|
+
try { file.close(() => {}); } catch (_) {}
|
|
347
|
+
removeIfExists(dest);
|
|
348
|
+
finish(reject, makeDownloadError("write", err.message));
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
res.on("error", (err) => {
|
|
352
|
+
try { file.close(() => {}); } catch (_) {}
|
|
353
|
+
removeIfExists(dest);
|
|
354
|
+
finish(reject, makeDownloadError("network", err.message));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
res.pipe(file);
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const timeout = setTimeout(() => {
|
|
362
|
+
req.destroy(makeDownloadError("timeout", `Download timed out after ${DOWNLOAD_TIMEOUT_MS}ms`));
|
|
363
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
364
|
+
|
|
365
|
+
req.on("error", (err) => {
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
reject(makeDownloadError(err.kind || "network", err.message));
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function verifyBinary(binaryPath) {
|
|
373
|
+
logLine("Verifying downloaded binary...");
|
|
374
|
+
const version = binaryVersion(binaryPath);
|
|
375
|
+
if (!version) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
message: "binary verification failed",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (!version.includes(packageJson.version)) {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
message: `expected version ${packageJson.version}, got '${version}'`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return { ok: true, version };
|
|
118
388
|
}
|
|
119
389
|
|
|
390
|
+
async function tryDownloadPrebuilt(target) {
|
|
391
|
+
const assetName = assetNameForTarget(target);
|
|
392
|
+
const url = `https://github.com/${REPO}/releases/download/${BINARY_RELEASE_TAG}/${assetName}`;
|
|
393
|
+
|
|
394
|
+
fs.mkdirSync(NATIVE_DIR, { recursive: true });
|
|
395
|
+
const dest = nativeBinaryPath();
|
|
396
|
+
const tmpDest = `${dest}.tmp`;
|
|
397
|
+
|
|
398
|
+
if (installFromVerifiedCache(target)) {
|
|
399
|
+
logLine("✓ BB-Agent binary installed successfully from cache.");
|
|
400
|
+
return { ok: true, source: "cache" };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (maybeRepairCache(target) && installFromVerifiedCache(target)) {
|
|
404
|
+
logLine("✓ BB-Agent binary installed successfully from cache.");
|
|
405
|
+
return { ok: true, source: "cache" };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let lastError = null;
|
|
409
|
+
for (let attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt += 1) {
|
|
410
|
+
try {
|
|
411
|
+
logLine(
|
|
412
|
+
`Downloading BB-Agent ${BINARY_RELEASE_TAG} for ${target} (attempt ${attempt}/${MAX_DOWNLOAD_ATTEMPTS})...`
|
|
413
|
+
);
|
|
414
|
+
logLine("This may take a little while on first install because npm downloads the native binary from the GitHub release.");
|
|
415
|
+
removeIfExists(tmpDest);
|
|
416
|
+
await requestBinary(url, tmpDest, 0);
|
|
417
|
+
if (!isWindows()) {
|
|
418
|
+
fs.chmodSync(tmpDest, 0o755);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const verified = verifyBinary(tmpDest);
|
|
422
|
+
if (!verified.ok) {
|
|
423
|
+
removeIfExists(tmpDest);
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
kind: "verify",
|
|
427
|
+
message: `Downloaded binary could not run: ${verified.message}`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fs.renameSync(tmpDest, dest);
|
|
432
|
+
refreshCacheFromExistingBinary(target, dest);
|
|
433
|
+
logLine("Cached verified BB-Agent binary for future installs.");
|
|
434
|
+
logLine("✓ BB-Agent binary installed successfully.");
|
|
435
|
+
return { ok: true, source: "download" };
|
|
436
|
+
} catch (err) {
|
|
437
|
+
lastError = err;
|
|
438
|
+
removeIfExists(tmpDest);
|
|
439
|
+
if (err.kind === "not-found") {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
kind: "not-found",
|
|
443
|
+
message: `No release asset named ${assetName} was found for ${BINARY_RELEASE_TAG}.`,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (attempt < MAX_DOWNLOAD_ATTEMPTS) {
|
|
447
|
+
logLine(`Download failed (${err.message}). Retrying...`);
|
|
448
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
kind: (lastError && lastError.kind) || "download",
|
|
456
|
+
message: (lastError && lastError.message) || "unknown download failure",
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function printFallbackHelp(platform, reason) {
|
|
461
|
+
logLine("");
|
|
462
|
+
if (reason && reason.kind === "not-found") {
|
|
463
|
+
logLine(`BB-Agent ${packageJson.version}: matching prebuilt binary is not published for ${platform}.`);
|
|
464
|
+
} else if (reason) {
|
|
465
|
+
logLine(`BB-Agent ${packageJson.version}: failed to download the prebuilt binary for ${platform}.`);
|
|
466
|
+
logLine(`Reason: ${reason.message}`);
|
|
467
|
+
} else {
|
|
468
|
+
logLine(`BB-Agent ${packageJson.version}: matching prebuilt binary not available yet for ${platform}.`);
|
|
469
|
+
}
|
|
470
|
+
logLine("");
|
|
471
|
+
logLine("╔══════════════════════════════════════════════════════════════╗");
|
|
472
|
+
logLine(
|
|
473
|
+
"║ BB-Agent: npm could not install native binary for " +
|
|
474
|
+
platform.padEnd(16) +
|
|
475
|
+
" ║"
|
|
476
|
+
);
|
|
477
|
+
logLine("║ ║");
|
|
478
|
+
logLine("║ Install Rust (if needed): ║");
|
|
479
|
+
logLine("║ https://rustup.rs ║");
|
|
480
|
+
logLine("║ Then install with rustup for your platform ║");
|
|
481
|
+
logLine("║ ║");
|
|
482
|
+
logLine("║ Then build BB-Agent: ║");
|
|
483
|
+
logLine("║ git clone https://github.com/shuyhere/bb-agent.git ║");
|
|
484
|
+
logLine("║ cd bb-agent && cargo install --path crates/cli ║");
|
|
485
|
+
logLine("║ ║");
|
|
486
|
+
logLine("║ Then run: bb ║");
|
|
487
|
+
logLine("╚══════════════════════════════════════════════════════════════╝");
|
|
488
|
+
logLine("");
|
|
489
|
+
}
|
|
120
490
|
|
|
121
491
|
async function main() {
|
|
122
492
|
if (process.env.BB_SKIP_POSTINSTALL) {
|
|
123
493
|
return;
|
|
124
494
|
}
|
|
125
495
|
|
|
126
|
-
if (hasBundledNativeBinary())
|
|
496
|
+
if (hasBundledNativeBinary()) {
|
|
497
|
+
logLine(`BB-Agent ${packageJson.version} native binary already present; skipping download.`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
127
500
|
|
|
128
501
|
const target = getTarget();
|
|
502
|
+
const platform = `${os.platform()}-${os.arch()}`;
|
|
129
503
|
|
|
130
|
-
// Try prebuilt binary
|
|
131
504
|
if (target) {
|
|
132
|
-
const
|
|
133
|
-
if (ok)
|
|
505
|
+
const result = await tryDownloadPrebuilt(target);
|
|
506
|
+
if (result.ok) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
printFallbackHelp(platform, result);
|
|
510
|
+
return;
|
|
134
511
|
}
|
|
135
512
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.log("║ ║");
|
|
153
|
-
console.log("║ Then run: bb ║");
|
|
154
|
-
console.log("╚══════════════════════════════════════════════════════════════╝");
|
|
155
|
-
console.log("");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
main().catch((err) => {
|
|
159
|
-
// Never fail npm install — just print instructions
|
|
160
|
-
console.error("BB-Agent postinstall notice:", err.message);
|
|
161
|
-
console.log("Install manually: git clone https://github.com/shuyhere/bb-agent.git && cd bb-agent && cargo install --path crates/cli");
|
|
162
|
-
});
|
|
513
|
+
printFallbackHelp(platform, {
|
|
514
|
+
kind: "unsupported-platform",
|
|
515
|
+
message: `Unsupported target mapping for ${platform}`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
main()
|
|
520
|
+
.catch((err) => {
|
|
521
|
+
logLine(`BB-Agent postinstall notice: ${err && err.message ? err.message : String(err)}`);
|
|
522
|
+
logLine(
|
|
523
|
+
"Install manually: git clone https://github.com/shuyhere/bb-agent.git && cd bb-agent && cargo install --path crates/cli"
|
|
524
|
+
);
|
|
525
|
+
})
|
|
526
|
+
.finally(() => {
|
|
527
|
+
process.exitCode = 0;
|
|
528
|
+
});
|