@openbuff/cli 0.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 ADDED
@@ -0,0 +1,107 @@
1
+ # The most powerful coding agent
2
+
3
+ Openbuff is a CLI tool that writes code for you.
4
+
5
+ 1. Run `openbuff` from your project directory
6
+ 2. Tell it what to do
7
+ 3. It will read and write to files and run commands to produce the code you want
8
+
9
+ Note: Openbuff will run commands in your terminal as it deems necessary to fulfill your request.
10
+
11
+ ## Installation
12
+
13
+ To install Openbuff, run:
14
+
15
+ ```bash
16
+ npm install -g openbuff
17
+ ```
18
+
19
+ (Use `sudo` if you get a permission error.)
20
+
21
+ ## Usage
22
+
23
+ After installation, you can start Openbuff by running:
24
+
25
+ ```bash
26
+ openbuff [project-directory]
27
+ ```
28
+
29
+ If no project directory is specified, Openbuff will use the current directory.
30
+
31
+ Once running, simply chat with Openbuff to say what coding task you want done.
32
+
33
+ ## Features
34
+
35
+ - Understands your whole codebase
36
+ - Creates and edits multiple files based on your request
37
+ - Can run your tests or type checker or linter; can install packages
38
+ - It's powerful: ask Openbuff to keep working until it reaches a condition and it will.
39
+
40
+ Our users regularly use Openbuff to implement new features, write unit tests, refactor code, write scripts, or give advice.
41
+
42
+ ## Knowledge Files
43
+
44
+ To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you.
45
+
46
+ Openbuff can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually!
47
+
48
+ Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update!
49
+
50
+ ## Tips
51
+
52
+ 1. Type '/help' or just '/' to see available commands.
53
+ 2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses.
54
+ 3. Type `undo` or `redo` to revert or reapply file changes from the conversation.
55
+ 4. Press `Esc` or `Ctrl+C` while Openbuff is generating a response to stop it.
56
+
57
+ ## Troubleshooting
58
+
59
+ ### Permission Errors
60
+
61
+ If you are getting permission errors during installation, try using sudo:
62
+
63
+ ```
64
+ sudo npm install -g openbuff
65
+ ```
66
+
67
+ If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download).
68
+
69
+ ### Corporate Proxy / Firewall
70
+
71
+ If you see `Failed to download openbuff: Request timeout` or `Failed to determine latest version`, you may be behind a corporate proxy or firewall.
72
+
73
+ Openbuff respects standard proxy environment variables. Set `HTTPS_PROXY` to route traffic through your proxy:
74
+
75
+ **Linux / macOS (bash/zsh):**
76
+ ```bash
77
+ export HTTPS_PROXY=http://your-proxy-server:port
78
+ openbuff
79
+ ```
80
+
81
+ **Windows (PowerShell):**
82
+ ```powershell
83
+ $env:HTTPS_PROXY = "http://your-proxy-server:port"
84
+ openbuff
85
+ ```
86
+
87
+ **Windows (CMD):**
88
+ ```cmd
89
+ set HTTPS_PROXY=http://your-proxy-server:port
90
+ openbuff
91
+ ```
92
+
93
+ To make it permanent, add the `export` or `set` line to your shell profile (e.g. `~/.bashrc`, `~/.zshrc`, or Windows System Environment Variables).
94
+
95
+ **Supported environment variables:**
96
+
97
+ | Variable | Purpose |
98
+ |---|---|
99
+ | `HTTPS_PROXY` / `https_proxy` | Proxy for HTTPS requests (recommended) |
100
+ | `HTTP_PROXY` / `http_proxy` | Fallback proxy for HTTP requests |
101
+ | `NO_PROXY` / `no_proxy` | Comma-separated list of hostnames to bypass the proxy (port suffixes are ignored) |
102
+
103
+ Both `http://` and `https://` proxy URLs are supported. Proxy authentication is supported via URL credentials (e.g. `http://user:password@proxy:port`).
104
+
105
+ ## Feedback
106
+
107
+ We value your input! Please open a GitHub issue with your feedback. Thank you for using Openbuff!
package/http.js ADDED
@@ -0,0 +1,176 @@
1
+ const http = require('http')
2
+ const https = require('https')
3
+ const tls = require('tls')
4
+
5
+ function createReleaseHttpClient({
6
+ env = process.env,
7
+ userAgent,
8
+ requestTimeout,
9
+ httpModule = http,
10
+ httpsModule = https,
11
+ tlsModule = tls,
12
+ }) {
13
+ function getProxyUrl() {
14
+ return (
15
+ env.HTTPS_PROXY ||
16
+ env.https_proxy ||
17
+ env.HTTP_PROXY ||
18
+ env.http_proxy ||
19
+ null
20
+ )
21
+ }
22
+
23
+ function shouldBypassProxy(hostname) {
24
+ const noProxy = env.NO_PROXY || env.no_proxy || ''
25
+ if (!noProxy) return false
26
+
27
+ const domains = noProxy
28
+ .split(',')
29
+ .map((domain) => domain.trim().toLowerCase().replace(/:\d+$/, ''))
30
+ const host = hostname.toLowerCase()
31
+
32
+ return domains.some((domain) => {
33
+ if (domain === '*') return true
34
+ if (domain.startsWith('.')) {
35
+ return host.endsWith(domain) || host === domain.slice(1)
36
+ }
37
+ return host === domain || host.endsWith(`.${domain}`)
38
+ })
39
+ }
40
+
41
+ function connectThroughProxy(proxyUrl, targetHost, targetPort) {
42
+ return new Promise((resolve, reject) => {
43
+ const proxy = new URL(proxyUrl)
44
+ const isHttpsProxy = proxy.protocol === 'https:'
45
+ const connectOptions = {
46
+ hostname: proxy.hostname,
47
+ port: proxy.port || (isHttpsProxy ? 443 : 80),
48
+ method: 'CONNECT',
49
+ path: `${targetHost}:${targetPort}`,
50
+ headers: {
51
+ Host: `${targetHost}:${targetPort}`,
52
+ },
53
+ }
54
+
55
+ if (proxy.username || proxy.password) {
56
+ const auth = Buffer.from(
57
+ `${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(
58
+ proxy.password || '',
59
+ )}`,
60
+ ).toString('base64')
61
+ connectOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
62
+ }
63
+
64
+ const transport = isHttpsProxy ? httpsModule : httpModule
65
+ const req = transport.request(connectOptions)
66
+
67
+ req.on('connect', (res, socket) => {
68
+ if (res.statusCode === 200) {
69
+ resolve(socket)
70
+ return
71
+ }
72
+
73
+ socket.destroy()
74
+ reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`))
75
+ })
76
+
77
+ req.on('error', (error) => {
78
+ reject(new Error(`Proxy connection failed: ${error.message}`))
79
+ })
80
+
81
+ req.setTimeout(requestTimeout, () => {
82
+ req.destroy()
83
+ reject(new Error('Proxy connection timeout.'))
84
+ })
85
+
86
+ req.end()
87
+ })
88
+ }
89
+
90
+ async function buildRequestOptions(url, options = {}) {
91
+ const parsedUrl = new URL(url)
92
+ const reqOptions = {
93
+ hostname: parsedUrl.hostname,
94
+ port: parsedUrl.port || 443,
95
+ path: parsedUrl.pathname + parsedUrl.search,
96
+ headers: {
97
+ 'User-Agent': userAgent,
98
+ ...options.headers,
99
+ },
100
+ }
101
+
102
+ const proxyUrl = getProxyUrl()
103
+ if (!proxyUrl || shouldBypassProxy(parsedUrl.hostname)) {
104
+ return reqOptions
105
+ }
106
+
107
+ const tunnelSocket = await connectThroughProxy(
108
+ proxyUrl,
109
+ parsedUrl.hostname,
110
+ parsedUrl.port || 443,
111
+ )
112
+
113
+ class TunnelAgent extends httpsModule.Agent {
114
+ createConnection(_options, callback) {
115
+ const secureSocket = tlsModule.connect({
116
+ socket: tunnelSocket,
117
+ servername: parsedUrl.hostname,
118
+ })
119
+
120
+ if (typeof callback === 'function') {
121
+ if (typeof secureSocket.once === 'function') {
122
+ let settled = false
123
+ const finish = (error) => {
124
+ if (settled) return
125
+ settled = true
126
+ callback(error || null, error ? undefined : secureSocket)
127
+ }
128
+
129
+ secureSocket.once('secureConnect', () => finish(null))
130
+ secureSocket.once('error', (error) => finish(error))
131
+ } else {
132
+ callback(null, secureSocket)
133
+ }
134
+ }
135
+
136
+ return secureSocket
137
+ }
138
+ }
139
+
140
+ reqOptions.agent = new TunnelAgent({ keepAlive: false })
141
+ return reqOptions
142
+ }
143
+
144
+ async function httpGet(url, options = {}) {
145
+ const reqOptions = await buildRequestOptions(url, options)
146
+
147
+ return new Promise((resolve, reject) => {
148
+ const req = httpsModule.get(reqOptions, (res) => {
149
+ if (res.statusCode === 301 || res.statusCode === 302) {
150
+ res.resume()
151
+ httpGet(new URL(res.headers.location, url).href, options)
152
+ .then(resolve)
153
+ .catch(reject)
154
+ return
155
+ }
156
+
157
+ resolve(res)
158
+ })
159
+
160
+ req.on('error', reject)
161
+ req.setTimeout(options.timeout || requestTimeout, () => {
162
+ req.destroy()
163
+ reject(new Error('Request timeout.'))
164
+ })
165
+ })
166
+ }
167
+
168
+ return {
169
+ getProxyUrl,
170
+ httpGet,
171
+ }
172
+ }
173
+
174
+ module.exports = {
175
+ createReleaseHttpClient,
176
+ }
package/index.js ADDED
@@ -0,0 +1,603 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process')
4
+ const fs = require('fs')
5
+ const http = require('http')
6
+ const https = require('https')
7
+ const os = require('os')
8
+ const path = require('path')
9
+ const zlib = require('zlib')
10
+
11
+ const tar = require('tar')
12
+ const { createReleaseHttpClient } = require('./http')
13
+
14
+ // npm package name — used for registry version checks (auto-update).
15
+ // Distinct from binaryName below: the npm package is scoped (@openbuff/cli)
16
+ // but the compiled binary and command users type is still `openbuff`.
17
+ const npmPackageName = '@openbuff/cli'
18
+ const binaryName = 'openbuff'
19
+
20
+ /**
21
+ * Terminal escape sequences to reset terminal state after the child process exits.
22
+ * When the binary is SIGKILL'd, it can't clean up its own terminal state.
23
+ * The wrapper (this process) survives and must reset these modes.
24
+ *
25
+ * Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts
26
+ */
27
+ const TERMINAL_RESET_SEQUENCES =
28
+ '\x1b[?1049l' + // Exit alternate screen buffer
29
+ '\x1b[?1000l' + // Disable X10 mouse mode
30
+ '\x1b[?1002l' + // Disable button event mouse mode
31
+ '\x1b[?1003l' + // Disable any-event mouse mode (all motion)
32
+ '\x1b[?1006l' + // Disable SGR extended mouse mode
33
+ '\x1b[?1004l' + // Disable focus reporting
34
+ '\x1b[?2004l' + // Disable bracketed paste mode
35
+ '\x1b[?25h' // Show cursor
36
+
37
+ function resetTerminal() {
38
+ try {
39
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
40
+ process.stdin.setRawMode(false)
41
+ }
42
+ } catch {
43
+ // stdin may be closed
44
+ }
45
+ try {
46
+ if (process.stdout.isTTY) {
47
+ process.stdout.write(TERMINAL_RESET_SEQUENCES)
48
+ }
49
+ } catch {
50
+ // stdout may be closed
51
+ }
52
+ }
53
+
54
+ function createConfig(binName) {
55
+ const homeDir = os.homedir()
56
+ const configDir = path.join(homeDir, '.config', 'openbuff')
57
+ const resolvedBinaryName =
58
+ process.platform === 'win32' ? `${binName}.exe` : binName
59
+
60
+ return {
61
+ homeDir,
62
+ configDir,
63
+ binaryName: resolvedBinaryName,
64
+ binaryPath: path.join(configDir, resolvedBinaryName),
65
+ metadataPath: path.join(configDir, 'openbuff-metadata.json'),
66
+ tempDownloadDir: path.join(configDir, '.download-temp'),
67
+ userAgent: `${binName}-cli`,
68
+ requestTimeout: 20000,
69
+ }
70
+ }
71
+
72
+ const CONFIG = createConfig(binaryName)
73
+ const { getProxyUrl, httpGet } = createReleaseHttpClient({
74
+ env: process.env,
75
+ userAgent: CONFIG.userAgent,
76
+ requestTimeout: CONFIG.requestTimeout,
77
+ })
78
+
79
+ function getPostHogConfig() {
80
+ const apiKey =
81
+ process.env.CODEBUFF_POSTHOG_API_KEY ||
82
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY
83
+ const host =
84
+ process.env.CODEBUFF_POSTHOG_HOST ||
85
+ process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
86
+
87
+ if (!apiKey || !host) {
88
+ return null
89
+ }
90
+
91
+ return { apiKey, host }
92
+ }
93
+
94
+ /**
95
+ * Track update failure event to PostHog.
96
+ * Fire-and-forget - errors are silently ignored.
97
+ */
98
+ function trackUpdateFailed(errorMessage, version, context = {}) {
99
+ try {
100
+ const posthogConfig = getPostHogConfig()
101
+ if (!posthogConfig) {
102
+ return
103
+ }
104
+
105
+ const payload = JSON.stringify({
106
+ api_key: posthogConfig.apiKey,
107
+ event: 'cli.update_openbuff_failed',
108
+ properties: {
109
+ distinct_id: `anonymous-${CONFIG.homeDir}`,
110
+ error: errorMessage,
111
+ version: version || 'unknown',
112
+ platform: process.platform,
113
+ arch: process.arch,
114
+ ...context,
115
+ },
116
+ timestamp: new Date().toISOString(),
117
+ })
118
+
119
+ const parsedUrl = new URL(`${posthogConfig.host}/capture/`)
120
+ const isHttps = parsedUrl.protocol === 'https:'
121
+ const options = {
122
+ hostname: parsedUrl.hostname,
123
+ port: parsedUrl.port || (isHttps ? 443 : 80),
124
+ path: parsedUrl.pathname + parsedUrl.search,
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ 'Content-Length': Buffer.byteLength(payload),
129
+ },
130
+ }
131
+
132
+ const transport = isHttps ? https : http
133
+ const req = transport.request(options)
134
+ req.on('error', () => {}) // Silently ignore errors
135
+ req.write(payload)
136
+ req.end()
137
+ } catch (e) {
138
+ // Silently ignore any tracking errors
139
+ }
140
+ }
141
+
142
+ // Binary tarball asset filenames on the GitHub Release. The binary is named
143
+ // `openbuff` (see binaryName above) regardless of the scoped npm package name.
144
+ const PLATFORM_TARGETS = {
145
+ 'linux-x64': `${binaryName}-linux-x64.tar.gz`,
146
+ 'linux-arm64': `${binaryName}-linux-arm64.tar.gz`,
147
+ 'darwin-x64': `${binaryName}-darwin-x64.tar.gz`,
148
+ 'darwin-arm64': `${binaryName}-darwin-arm64.tar.gz`,
149
+ 'win32-x64': `${binaryName}-win32-x64.tar.gz`,
150
+ }
151
+
152
+ const term = {
153
+ clearLine: () => {
154
+ if (process.stderr.isTTY) {
155
+ process.stderr.write('\r\x1b[K')
156
+ }
157
+ },
158
+ write: (text) => {
159
+ term.clearLine()
160
+ process.stderr.write(text)
161
+ },
162
+ writeLine: (text) => {
163
+ term.clearLine()
164
+ process.stderr.write(text + '\n')
165
+ },
166
+ }
167
+
168
+ async function getLatestVersion() {
169
+ try {
170
+ const res = await httpGet(
171
+ `https://registry.npmjs.org/${npmPackageName}/latest`,
172
+ )
173
+
174
+ if (res.statusCode !== 200) return null
175
+
176
+ const body = await streamToString(res)
177
+ const packageData = JSON.parse(body)
178
+
179
+ return packageData.version || null
180
+ } catch (error) {
181
+ return null
182
+ }
183
+ }
184
+
185
+ function streamToString(stream) {
186
+ return new Promise((resolve, reject) => {
187
+ let data = ''
188
+ stream.on('data', (chunk) => (data += chunk))
189
+ stream.on('end', () => resolve(data))
190
+ stream.on('error', reject)
191
+ })
192
+ }
193
+
194
+ function getCurrentVersion() {
195
+ try {
196
+ if (!fs.existsSync(CONFIG.metadataPath)) {
197
+ return null
198
+ }
199
+ const metadata = JSON.parse(fs.readFileSync(CONFIG.metadataPath, 'utf8'))
200
+ // Also verify the binary still exists
201
+ if (!fs.existsSync(CONFIG.binaryPath)) {
202
+ return null
203
+ }
204
+ return metadata.version || null
205
+ } catch (error) {
206
+ return null
207
+ }
208
+ }
209
+
210
+ function compareVersions(v1, v2) {
211
+ if (!v1 || !v2) return 0
212
+
213
+ // Always update if the current version is not a valid semver
214
+ // e.g. 1.0.420-beta.1
215
+ if (!v1.match(/^\d+(\.\d+)*$/)) {
216
+ return -1
217
+ }
218
+
219
+ const parseVersion = (version) => {
220
+ const parts = version.split('-')
221
+ const mainParts = parts[0].split('.').map(Number)
222
+ const prereleaseParts = parts[1] ? parts[1].split('.') : []
223
+ return { main: mainParts, prerelease: prereleaseParts }
224
+ }
225
+
226
+ const p1 = parseVersion(v1)
227
+ const p2 = parseVersion(v2)
228
+
229
+ for (let i = 0; i < Math.max(p1.main.length, p2.main.length); i++) {
230
+ const n1 = p1.main[i] || 0
231
+ const n2 = p2.main[i] || 0
232
+
233
+ if (n1 < n2) return -1
234
+ if (n1 > n2) return 1
235
+ }
236
+
237
+ if (p1.prerelease.length === 0 && p2.prerelease.length === 0) {
238
+ return 0
239
+ } else if (p1.prerelease.length === 0) {
240
+ return 1
241
+ } else if (p2.prerelease.length === 0) {
242
+ return -1
243
+ } else {
244
+ for (
245
+ let i = 0;
246
+ i < Math.max(p1.prerelease.length, p2.prerelease.length);
247
+ i++
248
+ ) {
249
+ const pr1 = p1.prerelease[i] || ''
250
+ const pr2 = p2.prerelease[i] || ''
251
+
252
+ const isNum1 = !isNaN(parseInt(pr1))
253
+ const isNum2 = !isNaN(parseInt(pr2))
254
+
255
+ if (isNum1 && isNum2) {
256
+ const num1 = parseInt(pr1)
257
+ const num2 = parseInt(pr2)
258
+ if (num1 < num2) return -1
259
+ if (num1 > num2) return 1
260
+ } else if (isNum1 && !isNum2) {
261
+ return 1
262
+ } else if (!isNum1 && isNum2) {
263
+ return -1
264
+ } else if (pr1 < pr2) {
265
+ return -1
266
+ } else if (pr1 > pr2) {
267
+ return 1
268
+ }
269
+ }
270
+ return 0
271
+ }
272
+ }
273
+
274
+ function formatBytes(bytes) {
275
+ if (bytes === 0) return '0 B'
276
+ const k = 1024
277
+ const sizes = ['B', 'KB', 'MB', 'GB']
278
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
279
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
280
+ }
281
+
282
+ function createProgressBar(percentage, width = 30) {
283
+ const filled = Math.round((width * percentage) / 100)
284
+ const empty = width - filled
285
+ return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
286
+ }
287
+
288
+ async function downloadBinary(version) {
289
+ const platformKey = `${process.platform}-${process.arch}`
290
+ const fileName = PLATFORM_TARGETS[platformKey]
291
+
292
+ if (!fileName) {
293
+ const error = new Error(`Unsupported platform: ${process.platform} ${process.arch}`)
294
+ trackUpdateFailed(error.message, version, { stage: 'platform_check' })
295
+ throw error
296
+ }
297
+
298
+ // Binaries are hosted as GitHub Release assets on the public repo.
299
+ // Public repo → unauthenticated downloads; GitHub 302-redirects to a CDN,
300
+ // and the http.js client follows redirects. OPENBUFF_DOWNLOAD_BASE may
301
+ // override the base (e.g. for staging mirrors).
302
+ const downloadUrl = `${
303
+ process.env.OPENBUFF_DOWNLOAD_BASE ||
304
+ 'https://github.com/AnzoBenjamin/openbuff/releases/download'
305
+ }/v${version}/${fileName}`
306
+
307
+ // Ensure config directory exists
308
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
309
+
310
+ // Clean up any previous temp download directory
311
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
312
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
313
+ }
314
+ fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
315
+
316
+ term.write('Downloading...')
317
+
318
+ const res = await httpGet(downloadUrl)
319
+
320
+ if (res.statusCode !== 200) {
321
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
322
+ const error = new Error(`Download failed: HTTP ${res.statusCode}`)
323
+ trackUpdateFailed(error.message, version, { stage: 'http_download', statusCode: res.statusCode })
324
+ throw error
325
+ }
326
+
327
+ const totalSize = parseInt(res.headers['content-length'] || '0', 10)
328
+ let downloadedSize = 0
329
+ let lastProgressTime = Date.now()
330
+
331
+ res.on('data', (chunk) => {
332
+ downloadedSize += chunk.length
333
+ const now = Date.now()
334
+ if (now - lastProgressTime >= 100 || downloadedSize === totalSize) {
335
+ lastProgressTime = now
336
+ if (totalSize > 0) {
337
+ const pct = Math.round((downloadedSize / totalSize) * 100)
338
+ term.write(
339
+ `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(
340
+ totalSize,
341
+ )}`,
342
+ )
343
+ } else {
344
+ term.write(`Downloading... ${formatBytes(downloadedSize)}`)
345
+ }
346
+ }
347
+ })
348
+
349
+ // Extract to temp directory
350
+ await new Promise((resolve, reject) => {
351
+ res
352
+ .pipe(zlib.createGunzip())
353
+ .pipe(tar.x({ cwd: CONFIG.tempDownloadDir }))
354
+ .on('finish', resolve)
355
+ .on('error', reject)
356
+ })
357
+
358
+ const tempBinaryPath = path.join(CONFIG.tempDownloadDir, CONFIG.binaryName)
359
+
360
+ // Verify the binary was extracted
361
+ if (!fs.existsSync(tempBinaryPath)) {
362
+ const files = fs.readdirSync(CONFIG.tempDownloadDir)
363
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
364
+ const error = new Error(
365
+ `Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
366
+ )
367
+ trackUpdateFailed(error.message, version, { stage: 'extraction' })
368
+ throw error
369
+ }
370
+
371
+ // Set executable permissions
372
+ if (process.platform !== 'win32') {
373
+ fs.chmodSync(tempBinaryPath, 0o755)
374
+ }
375
+
376
+ // Move binary to final location
377
+ try {
378
+ if (fs.existsSync(CONFIG.binaryPath)) {
379
+ try {
380
+ fs.unlinkSync(CONFIG.binaryPath)
381
+ } catch (err) {
382
+ // Fallback: try renaming the locked/undeletable binary (Windows)
383
+ const backupPath = CONFIG.binaryPath + `.old.${Date.now()}`
384
+ try {
385
+ fs.renameSync(CONFIG.binaryPath, backupPath)
386
+ } catch (renameErr) {
387
+ throw new Error(
388
+ `Failed to replace existing binary. ` +
389
+ `unlink error: ${err.code || err.message}, ` +
390
+ `rename error: ${renameErr.code || renameErr.message}`,
391
+ )
392
+ }
393
+ }
394
+ }
395
+ fs.renameSync(tempBinaryPath, CONFIG.binaryPath)
396
+
397
+ // Move tree-sitter.wasm next to the binary if the tarball included
398
+ // it. The CLI binary loads this at startup; embedding it inside the
399
+ // binary itself was unreliable on Windows (bun --compile asset
400
+ // bundling silently dropped or unbound it across several attempts),
401
+ // so we ship it as a sibling file instead. Older artifacts that
402
+ // pre-date this change won't have the wasm and will still install —
403
+ // they'll just hit the same crash they had before, which is fine.
404
+ const tempWasmPath = path.join(CONFIG.tempDownloadDir, 'tree-sitter.wasm')
405
+ if (fs.existsSync(tempWasmPath)) {
406
+ const targetWasmPath = path.join(
407
+ path.dirname(CONFIG.binaryPath),
408
+ 'tree-sitter.wasm',
409
+ )
410
+ try {
411
+ if (fs.existsSync(targetWasmPath)) fs.unlinkSync(targetWasmPath)
412
+ } catch {
413
+ // best effort; rename below will surface the real error if it matters
414
+ }
415
+ fs.renameSync(tempWasmPath, targetWasmPath)
416
+ }
417
+
418
+ // Save version metadata for fast version checking
419
+ fs.writeFileSync(
420
+ CONFIG.metadataPath,
421
+ JSON.stringify({ version }, null, 2),
422
+ )
423
+ } finally {
424
+ // Clean up temp directory even if rename fails
425
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
426
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
427
+ }
428
+ }
429
+
430
+ term.clearLine()
431
+ console.log('Download complete! Starting Openbuff...')
432
+ }
433
+
434
+ async function ensureBinaryExists() {
435
+ const currentVersion = getCurrentVersion()
436
+ if (currentVersion !== null) {
437
+ return
438
+ }
439
+
440
+ const version = await getLatestVersion()
441
+ if (!version) {
442
+ console.error('❌ Failed to determine latest version')
443
+ console.error('Please check your internet connection and try again')
444
+ if (!getProxyUrl()) {
445
+ console.error(
446
+ 'If you are behind a proxy, set the HTTPS_PROXY environment variable',
447
+ )
448
+ }
449
+ process.exit(1)
450
+ }
451
+
452
+ try {
453
+ await downloadBinary(version)
454
+ } catch (error) {
455
+ term.clearLine()
456
+ console.error('❌ Failed to download openbuff:', error.message)
457
+ console.error('Please check your internet connection and try again')
458
+ if (!getProxyUrl()) {
459
+ console.error(
460
+ 'If you are behind a proxy, set the HTTPS_PROXY environment variable',
461
+ )
462
+ }
463
+ process.exit(1)
464
+ }
465
+ }
466
+
467
+ async function checkForUpdates(runningProcess, exitListener) {
468
+ try {
469
+ const currentVersion = getCurrentVersion()
470
+
471
+ const latestVersion = await getLatestVersion()
472
+ if (!latestVersion) return
473
+
474
+ if (
475
+ // Download new version if current version is unknown or outdated.
476
+ currentVersion === null ||
477
+ compareVersions(currentVersion, latestVersion) < 0
478
+ ) {
479
+ term.clearLine()
480
+
481
+ runningProcess.removeListener('exit', exitListener)
482
+
483
+ await new Promise((resolve) => {
484
+ let exited = false
485
+ runningProcess.once('exit', () => {
486
+ exited = true
487
+ resolve()
488
+ })
489
+ runningProcess.kill('SIGTERM')
490
+ setTimeout(() => {
491
+ if (!exited) {
492
+ runningProcess.kill('SIGKILL')
493
+ // Safety: resolve after giving SIGKILL time to take effect
494
+ setTimeout(() => resolve(), 1000)
495
+ }
496
+ }, 5000)
497
+ })
498
+
499
+ resetTerminal()
500
+ console.log(`Update available: ${currentVersion} → ${latestVersion}`)
501
+
502
+ await downloadBinary(latestVersion)
503
+
504
+ const newChild = spawn(CONFIG.binaryPath, process.argv.slice(2), {
505
+ stdio: 'inherit',
506
+ detached: false,
507
+ })
508
+
509
+ newChild.on('exit', (code, signal) => {
510
+ resetTerminal()
511
+ printCrashDiagnostics(code, signal)
512
+ process.exit(signal ? 1 : (code || 0))
513
+ })
514
+
515
+ newChild.on('error', (err) => {
516
+ console.error('Failed to start openbuff:', err.message)
517
+ process.exit(1)
518
+ })
519
+
520
+ return new Promise(() => {})
521
+ }
522
+ } catch (error) {
523
+ // Ignore update failures
524
+ }
525
+ }
526
+
527
+ function printCrashDiagnostics(code, signal) {
528
+ // Windows NTSTATUS codes (unsigned DWORD)
529
+ const unsignedCode = code != null && code < 0 ? (code >>> 0) : code
530
+ const isIllegalInstruction =
531
+ signal === 'SIGILL' ||
532
+ (process.platform === 'win32' && unsignedCode === 0xC000001D)
533
+ const isAccessViolation =
534
+ signal === 'SIGSEGV' ||
535
+ (process.platform === 'win32' && unsignedCode === 0xC0000005)
536
+ const isBusError = signal === 'SIGBUS'
537
+ const isAbort =
538
+ signal === 'SIGABRT' ||
539
+ (process.platform === 'win32' && unsignedCode === 0xC0000409)
540
+
541
+ if (!isIllegalInstruction && !isAccessViolation && !isBusError && !isAbort) return
542
+
543
+ const exitInfo = signal ? `signal ${signal}` : `code ${code}`
544
+ console.error('')
545
+ console.error(`❌ ${binaryName} exited immediately (${exitInfo})`)
546
+ console.error('')
547
+
548
+ if (isIllegalInstruction) {
549
+ console.error('Your CPU may not support the required instruction set (AVX2).')
550
+ console.error('This typically affects CPUs from before 2013.')
551
+ console.error('Unfortunately, this binary is not compatible with your system.')
552
+ console.error('')
553
+ } else if (isAccessViolation) {
554
+ console.error('The binary crashed with an access violation.')
555
+ console.error('')
556
+ } else if (isBusError) {
557
+ console.error('The binary crashed with a bus error.')
558
+ console.error('This may indicate a platform compatibility issue.')
559
+ console.error('')
560
+ } else if (isAbort) {
561
+ console.error('The binary crashed with an abort signal.')
562
+ console.error('')
563
+ }
564
+
565
+ console.error('System info:')
566
+ console.error(` Platform: ${process.platform} ${process.arch}`)
567
+ console.error(` Node: ${process.version}`)
568
+ console.error(` Binary: ${CONFIG.binaryPath}`)
569
+ console.error('')
570
+ console.error('Please report this issue at:')
571
+ console.error(' https://github.com/AnzoBenjamin/openbuff/issues')
572
+ console.error('')
573
+ }
574
+
575
+ async function main() {
576
+ await ensureBinaryExists()
577
+
578
+ const child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
579
+ stdio: 'inherit',
580
+ })
581
+
582
+ const exitListener = (code, signal) => {
583
+ resetTerminal()
584
+ printCrashDiagnostics(code, signal)
585
+ process.exit(signal ? 1 : (code || 0))
586
+ }
587
+
588
+ child.on('exit', exitListener)
589
+
590
+ child.on('error', (err) => {
591
+ console.error('Failed to start openbuff:', err.message)
592
+ process.exit(1)
593
+ })
594
+
595
+ setTimeout(() => {
596
+ checkForUpdates(child, exitListener)
597
+ }, 100)
598
+ }
599
+
600
+ main().catch((error) => {
601
+ console.error('❌ Unexpected error:', error.message)
602
+ process.exit(1)
603
+ })
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@openbuff/cli",
3
+ "version": "0.1.0",
4
+ "description": "AI coding agent",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "openbuff": "index.js",
8
+ "cb": "index.js"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node postinstall.js",
12
+ "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const configDir = path.join(os.homedir(), '.config', 'openbuff'); for (const name of (process.platform === 'win32' ? ['openbuff.exe'] : ['openbuff'])) { try { fs.unlinkSync(path.join(configDir, name)) } catch (e) { /* ignore if file doesn't exist */ } }\""
13
+ },
14
+ "files": [
15
+ "index.js",
16
+ "http.js",
17
+ "postinstall.js",
18
+ "README.md"
19
+ ],
20
+ "os": [
21
+ "darwin",
22
+ "linux",
23
+ "win32"
24
+ ],
25
+ "cpu": [
26
+ "x64",
27
+ "arm64"
28
+ ],
29
+ "engines": {
30
+ "node": ">=16"
31
+ },
32
+ "dependencies": {
33
+ "tar": "^7.0.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/AnzoBenjamin/openbuff.git"
38
+ },
39
+ "homepage": "https://github.com/AnzoBenjamin/openbuff",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
package/postinstall.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ // Clean up managed binaries so the wrapper downloads a fresh copy.
8
+ const configDir = path.join(os.homedir(), '.config', 'openbuff');
9
+ const binaryNames = process.platform === 'win32'
10
+ ? ['openbuff.exe']
11
+ : ['openbuff'];
12
+
13
+ for (const binaryName of binaryNames) {
14
+ try {
15
+ fs.unlinkSync(path.join(configDir, binaryName));
16
+ } catch (e) {
17
+ /* ignore if file doesn't exist */
18
+ }
19
+ }
20
+
21
+ // Print welcome message
22
+ console.log('\n');
23
+ console.log('🎉 Welcome to Openbuff!');
24
+ console.log('\n');
25
+ console.log('To get started:');
26
+ console.log(' 1. cd to your project directory');
27
+ console.log(' 2. Run: openbuff');
28
+ console.log('\n');
29
+ console.log('Example:');
30
+ console.log(' $ cd ~/my-project');
31
+ console.log(' $ openbuff');
32
+ console.log('\n');
33
+ console.log('For more information, visit: https://github.com/AnzoBenjamin/openbuff');
34
+ console.log('\n');