codebuff-cli 1.0.16 → 1.0.18
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/LICENSE +202 -0
- package/README.md +202 -58
- package/README.zh-CN.md +251 -0
- package/cli/README.md +84 -0
- package/cli/bin/codebuff.cjs +193 -0
- package/cli/scripts/download-binary.cjs +171 -0
- package/package.json +78 -31
- package/http.js +0 -176
- package/index.js +0 -592
- package/postinstall.js +0 -34
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Codebuff & Freebuff
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
**[Codebuff](https://codebuff.com)** 是一款开源的 AI 编程助手,能根据自然语言指令直接修改你的代码库。**[Freebuff](https://www.npmjs.com/package/freebuff)** 是它的免费、广告支持版本——无需订阅、无需积分、零配置。
|
|
6
|
+
|
|
7
|
+
与那种"一个模型干所有事"的工具不同,Codebuff 会协调多个专业化的智能体(agent)协同工作,理解你的项目并做出精准的改动。
|
|
8
|
+
|
|
9
|
+
<div align="center">
|
|
10
|
+
<img src="./assets/codebuff-vs-claude-code.png" alt="Codebuff vs Claude Code" width="400">
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
在我们的[评测](evals/README.md)中,Codebuff 在 175+ 个真实开源仓库的编码任务上以 61% 对 53% 的成绩领先 Claude Code。
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 工作原理
|
|
17
|
+
|
|
18
|
+
当你让 Codebuff "给我的 API 加上身份验证"时,它可能会调用:
|
|
19
|
+
|
|
20
|
+
1. **File Picker Agent** —— 扫描代码库、理解架构、找出相关文件
|
|
21
|
+
2. **Planner Agent** —— 规划哪些文件需要改、按什么顺序改
|
|
22
|
+
3. **Editor Agent** —— 执行精确的修改
|
|
23
|
+
4. **Reviewer Agent** —— 校验改动是否正确
|
|
24
|
+
|
|
25
|
+
<div align="center">
|
|
26
|
+
<img src="./assets/multi-agents.png" alt="Codebuff Multi-Agents" width="250">
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
相比单模型工具,这种多智能体方案能带来更准的上下文理解、更精确的修改,以及更少的错误。
|
|
30
|
+
|
|
31
|
+
## CLI:装好就能写代码
|
|
32
|
+
|
|
33
|
+
安装:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g codebuff
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
运行:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd your-project
|
|
43
|
+
codebuff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
然后直接告诉 Codebuff 你想做什么,剩下的它自己搞定:
|
|
47
|
+
|
|
48
|
+
- "修掉用户注册里的 SQL 注入漏洞"
|
|
49
|
+
- "给所有 API 端点加上限流"
|
|
50
|
+
- "重构数据库连接代码,提升性能"
|
|
51
|
+
|
|
52
|
+
Codebuff 会找到对应的文件,跨多个文件做改动,并跑测试确认没有破坏现有功能。
|
|
53
|
+
|
|
54
|
+
## 创建自定义智能体
|
|
55
|
+
|
|
56
|
+
要开始构建自己的智能体,先启动 Codebuff 然后执行 `/init`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
codebuff
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
进入 CLI 后:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
/init
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
这会生成:
|
|
69
|
+
```
|
|
70
|
+
knowledge.md # Codebuff 用的项目上下文
|
|
71
|
+
.agents/
|
|
72
|
+
└── types/ # TypeScript 类型定义
|
|
73
|
+
├── agent-definition.ts
|
|
74
|
+
├── tools.ts
|
|
75
|
+
└── util-types.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
通过编写智能体定义文件,你可以最大程度地控制智能体的行为。
|
|
79
|
+
|
|
80
|
+
通过指定工具、可派生的子智能体和提示词来实现自己的工作流。我们还提供了 TypeScript 生成器,方便你以更程序化的方式控制流程。
|
|
81
|
+
|
|
82
|
+
下面是一个 `git-committer` 智能体的例子,它会基于当前的 git 状态生成提交。注意它先跑 `git diff` 和 `git log` 分析改动,然后再把决策权交给 LLM,让它撰写有意义的 commit message 并完成实际提交。
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
export default {
|
|
86
|
+
id: 'git-committer',
|
|
87
|
+
displayName: 'Git Committer',
|
|
88
|
+
model: 'openai/gpt-5-nano',
|
|
89
|
+
toolNames: ['read_files', 'run_terminal_command', 'end_turn'],
|
|
90
|
+
|
|
91
|
+
instructionsPrompt:
|
|
92
|
+
'You create meaningful git commits by analyzing changes, reading relevant files for context, and crafting clear commit messages that explain the "why" behind changes.',
|
|
93
|
+
|
|
94
|
+
async *handleSteps() {
|
|
95
|
+
// 分析改动
|
|
96
|
+
yield { tool: 'run_terminal_command', command: 'git diff' }
|
|
97
|
+
yield { tool: 'run_terminal_command', command: 'git log --oneline -5' }
|
|
98
|
+
|
|
99
|
+
// 暂存文件,并用合适的 message 生成提交
|
|
100
|
+
yield 'STEP_ALL'
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## SDK:在生产环境里跑智能体
|
|
106
|
+
|
|
107
|
+
安装 [SDK 包](https://www.npmjs.com/package/@codebuff/sdk)——注意这跟 CLI 用的 codebuff 包是两个不同的包。
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install @codebuff/sdk
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
引入 client,开始跑智能体:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { CodebuffClient } from '@codebuff/sdk'
|
|
117
|
+
|
|
118
|
+
// 1. 初始化 client
|
|
119
|
+
const client = new CodebuffClient({
|
|
120
|
+
apiKey: 'your-api-key',
|
|
121
|
+
cwd: '/path/to/your/project',
|
|
122
|
+
onError: (error) => console.error('Codebuff error:', error.message),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 2. 跑一个编码任务……
|
|
126
|
+
const result = await client.run({
|
|
127
|
+
agent: 'base', // Codebuff 默认的基础编码智能体
|
|
128
|
+
prompt: 'Add error handling to all API endpoints',
|
|
129
|
+
handleEvent: (event) => {
|
|
130
|
+
console.log('Progress', event)
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// 3. 也可以跑自定义智能体!
|
|
135
|
+
const myCustomAgent: AgentDefinition = {
|
|
136
|
+
id: 'greeter',
|
|
137
|
+
displayName: 'Greeter',
|
|
138
|
+
model: 'openai/gpt-5.1',
|
|
139
|
+
instructionsPrompt: 'Say hello!',
|
|
140
|
+
}
|
|
141
|
+
await client.run({
|
|
142
|
+
agent: 'greeter',
|
|
143
|
+
agentDefinitions: [myCustomAgent],
|
|
144
|
+
prompt: 'My name is Bob.',
|
|
145
|
+
customToolDefinitions: [], // 也可以加自定义工具!
|
|
146
|
+
handleEvent: (event) => {
|
|
147
|
+
console.log('Progress', event)
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
更多 SDK 用法请看[这里](https://www.npmjs.com/package/@codebuff/sdk)。
|
|
153
|
+
|
|
154
|
+
## Freebuff:免费的编程智能体
|
|
155
|
+
|
|
156
|
+
不想订阅?**[Freebuff](https://www.npmjs.com/package/freebuff)** 是 Codebuff 的免费版本——无需订阅、无需积分、零配置,装上就能用。
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm install -g freebuff
|
|
160
|
+
cd your-project
|
|
161
|
+
freebuff
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Freebuff 由广告支持,使用经过优化、兼顾速度与质量的模型。内置网页检索、浏览器使用等能力。详情见 [Freebuff README](./freebuff/README.md)。
|
|
165
|
+
|
|
166
|
+
## 为什么选 Codebuff
|
|
167
|
+
|
|
168
|
+
**自定义工作流**:用 TypeScript 生成器把 AI 生成和程序化控制混着用。智能体可以派生子智能体、按条件分支、跑多步流程。
|
|
169
|
+
|
|
170
|
+
**OpenRouter 上的任何模型**:Claude Code 把你锁死在 Anthropic 的模型上,Codebuff 不一样——它支持 [OpenRouter](https://openrouter.ai/models) 上的所有模型,从 Claude、GPT 到 Qwen、DeepSeek 这类专用模型都行。可以按任务切换模型,也能随时用上最新发布的模型,不必等平台跟进。
|
|
171
|
+
|
|
172
|
+
**复用已发布的智能体**:把社区[已发布的智能体](https://www.codebuff.com/store)拼起来用,少走弯路。Codebuff 智能体就是新一代的 MCP!
|
|
173
|
+
|
|
174
|
+
**SDK**:把 Codebuff 嵌进你自己的应用里。可以创建自定义工具、对接 CI/CD,或把编码能力内嵌进你的产品。
|
|
175
|
+
|
|
176
|
+
## 进阶用法
|
|
177
|
+
|
|
178
|
+
### 自定义智能体工作流
|
|
179
|
+
|
|
180
|
+
用 `/init` 命令创建带专门工作流的智能体:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
codebuff
|
|
184
|
+
/init
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
这会在 `.agents/` 下生成一套可自定义的智能体结构。
|
|
188
|
+
|
|
189
|
+
## 参与贡献
|
|
190
|
+
|
|
191
|
+
我们 ❤️ 来自社区的贡献——无论是修 bug、调整智能体、还是改进文档。
|
|
192
|
+
|
|
193
|
+
**想参与?** 看一眼[贡献指南](./CONTRIBUTING.md) 就能上手。
|
|
194
|
+
|
|
195
|
+
### 运行测试
|
|
196
|
+
|
|
197
|
+
跑测试套件:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cd cli
|
|
201
|
+
bun test
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**交互式端到端测试**需要 tmux:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# macOS
|
|
208
|
+
brew install tmux
|
|
209
|
+
|
|
210
|
+
# Ubuntu/Debian
|
|
211
|
+
sudo apt-get install tmux
|
|
212
|
+
|
|
213
|
+
# Windows(通过 WSL)
|
|
214
|
+
wsl --install
|
|
215
|
+
sudo apt-get install tmux
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
更完整的测试文档见 [cli/src/__tests__/README.md](cli/src/__tests__/README.md)。
|
|
219
|
+
|
|
220
|
+
可以帮忙的方向:
|
|
221
|
+
|
|
222
|
+
- 🐛 **修 bug** 或新增功能
|
|
223
|
+
- 🤖 **打造专用智能体**并发布到 Agent Store
|
|
224
|
+
- 📚 **完善文档**或撰写教程
|
|
225
|
+
- 💡 **分享想法**:在 [GitHub Issues](https://github.com/CodebuffAI/codebuff/issues) 留言
|
|
226
|
+
|
|
227
|
+
## 开始使用
|
|
228
|
+
|
|
229
|
+
### 安装
|
|
230
|
+
|
|
231
|
+
**CLI**:`npm install -g codebuff`
|
|
232
|
+
|
|
233
|
+
**SDK**:`npm install @codebuff/sdk`
|
|
234
|
+
|
|
235
|
+
**Freebuff(免费版)**:`npm install -g freebuff`
|
|
236
|
+
|
|
237
|
+
### 资源
|
|
238
|
+
|
|
239
|
+
**文档**:[codebuff.com/docs](https://codebuff.com/docs)
|
|
240
|
+
|
|
241
|
+
**社区**:[Discord](https://codebuff.com/discord)
|
|
242
|
+
|
|
243
|
+
**Issue 与想法**:[GitHub Issues](https://github.com/CodebuffAI/codebuff/issues)
|
|
244
|
+
|
|
245
|
+
**贡献指南**:[CONTRIBUTING.md](./CONTRIBUTING.md) ——想贡献从这里开始!
|
|
246
|
+
|
|
247
|
+
**支持**:[support@codebuff.com](mailto:support@codebuff.com)
|
|
248
|
+
|
|
249
|
+
## Star 历史
|
|
250
|
+
|
|
251
|
+
[](https://www.star-history.com/#CodebuffAI/codebuff&Date)
|
package/cli/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @codebuff/cli
|
|
2
|
+
|
|
3
|
+
A Terminal User Interface (TUI) package built with OpenTUI and React.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Development
|
|
12
|
+
|
|
13
|
+
Run the TUI in development mode:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Testing
|
|
20
|
+
|
|
21
|
+
Run the test suite:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Interactive E2E Testing
|
|
28
|
+
|
|
29
|
+
For testing interactive CLI features, install tmux:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# macOS
|
|
33
|
+
brew install tmux
|
|
34
|
+
|
|
35
|
+
# Ubuntu/Debian
|
|
36
|
+
sudo apt-get install tmux
|
|
37
|
+
|
|
38
|
+
# Windows (via WSL)
|
|
39
|
+
wsl --install
|
|
40
|
+
sudo apt-get install tmux
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then run the proof-of-concept:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bun run test:tmux-poc
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Note:** When sending input to the CLI via tmux, you must use bracketed paste mode. Standard `send-keys` drops characters.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# ❌ Broken: tmux send-keys -t session "hello"
|
|
53
|
+
# ✅ Works: tmux send-keys -t session $'\e[200~hello\e[201~'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
See [tmux.knowledge.md](tmux.knowledge.md) for comprehensive tmux documentation and [src/__tests__/README.md](src/__tests__/README.md) for testing documentation.
|
|
57
|
+
|
|
58
|
+
## Build
|
|
59
|
+
|
|
60
|
+
Build the package:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun run build
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Run
|
|
67
|
+
|
|
68
|
+
Run the built TUI:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bun run start
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or use the binary directly:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codebuff-tui
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Features
|
|
81
|
+
|
|
82
|
+
- Built with OpenTUI for modern terminal interfaces
|
|
83
|
+
- Uses React for declarative component-based UI
|
|
84
|
+
- TypeScript support out of the box
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
|
|
9
|
+
const BINARY_NAME = 'codebuff';
|
|
10
|
+
const REPO = 'Marcus-Mok-GH/codebuff-cli';
|
|
11
|
+
|
|
12
|
+
const moduleBinary = path.join(__dirname, process.platform === 'win32' ? `${BINARY_NAME}.exe` : BINARY_NAME);
|
|
13
|
+
const localDir = path.join(os.homedir(), '.codebuff', 'bin');
|
|
14
|
+
const localBinary = path.join(localDir, process.platform === 'win32' ? `${BINARY_NAME}.exe` : BINARY_NAME);
|
|
15
|
+
const VERSION_FILE = path.join(localDir, '.version');
|
|
16
|
+
|
|
17
|
+
function resolveBinaryPath() {
|
|
18
|
+
if (fs.existsSync(localBinary)) return localBinary;
|
|
19
|
+
if (fs.existsSync(moduleBinary)) return moduleBinary;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getVersion() {
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.join(__dirname, '..', '..', 'package.json'),
|
|
26
|
+
path.join(__dirname, '..', 'package.json'),
|
|
27
|
+
];
|
|
28
|
+
for (const p of candidates) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
31
|
+
if (pkg.version) return pkg.version;
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getCachedVersion() {
|
|
38
|
+
try {
|
|
39
|
+
return fs.readFileSync(VERSION_FILE, 'utf8').trim();
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeVersionFile(version) {
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(VERSION_FILE, version, 'utf8');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Ignore write errors - version file is best-effort
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function download(url, dest) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const client = url.startsWith('https:') ? https : http;
|
|
56
|
+
const file = fs.createWriteStream(dest);
|
|
57
|
+
|
|
58
|
+
const req = client.get(
|
|
59
|
+
url,
|
|
60
|
+
{ headers: { 'User-Agent': 'codebuff-cli-installer' } },
|
|
61
|
+
(res) => {
|
|
62
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
63
|
+
file.close();
|
|
64
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
65
|
+
download(new URL(res.headers.location, url).href, dest)
|
|
66
|
+
.then(resolve)
|
|
67
|
+
.catch(reject);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (res.statusCode !== 200) {
|
|
72
|
+
file.close();
|
|
73
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
74
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
res.pipe(file);
|
|
79
|
+
file.on('finish', () => {
|
|
80
|
+
file.close();
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
req.on('error', (err) => {
|
|
87
|
+
file.close();
|
|
88
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
89
|
+
reject(err);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
req.setTimeout(30000, () => {
|
|
93
|
+
req.destroy();
|
|
94
|
+
file.close();
|
|
95
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
96
|
+
reject(new Error('Request timeout'));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function tryDownload(url, dest) {
|
|
102
|
+
try {
|
|
103
|
+
await download(url, dest);
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function downloadBinary(destPath) {
|
|
111
|
+
const version = getVersion();
|
|
112
|
+
if (!version) {
|
|
113
|
+
throw new Error('Could not determine version from package.json');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const baseUrl = `https://github.com/${REPO}/releases/download/v${version}`;
|
|
117
|
+
const platformName = `${BINARY_NAME}-${process.platform}-${process.arch}${process.platform === 'win32' ? '.exe' : ''}`;
|
|
118
|
+
const platformUrl = `${baseUrl}/${platformName}`;
|
|
119
|
+
const genericUrl = `${baseUrl}/${BINARY_NAME}`;
|
|
120
|
+
|
|
121
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
122
|
+
|
|
123
|
+
if (await tryDownload(platformUrl, destPath)) {
|
|
124
|
+
console.log(`Downloaded platform-specific binary: ${platformName}`);
|
|
125
|
+
} else if (await tryDownload(genericUrl, destPath)) {
|
|
126
|
+
console.log(`Downloaded generic binary: ${BINARY_NAME}`);
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Failed to download binary from:\n ${platformUrl}\n ${genericUrl}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.platform !== 'win32') {
|
|
134
|
+
fs.chmodSync(destPath, 0o755);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function runBinary(binaryPath) {
|
|
139
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
140
|
+
stdio: 'inherit',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
child.on('exit', (code, signal) => {
|
|
144
|
+
process.exit(signal ? 1 : code || 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.on('error', (err) => {
|
|
148
|
+
console.error('Failed to start codebuff:', err.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
const pkgVersion = getVersion();
|
|
155
|
+
const cachedVersion = getCachedVersion();
|
|
156
|
+
|
|
157
|
+
// Check if local cache is stale
|
|
158
|
+
if (fs.existsSync(localBinary) && pkgVersion && cachedVersion !== pkgVersion) {
|
|
159
|
+
console.log(`Binary cache outdated (${cachedVersion || 'none'} → ${pkgVersion}). Re-downloading...`);
|
|
160
|
+
try { fs.unlinkSync(localBinary); } catch {}
|
|
161
|
+
try { fs.unlinkSync(VERSION_FILE); } catch {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let binaryPath = resolveBinaryPath();
|
|
165
|
+
|
|
166
|
+
if (binaryPath) {
|
|
167
|
+
runBinary(binaryPath);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log('Codebuff binary not found. Downloading...');
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await downloadBinary(localBinary);
|
|
175
|
+
if (pkgVersion) writeVersionFile(pkgVersion);
|
|
176
|
+
binaryPath = localBinary;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('Failed to download codebuff:', err.message);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(binaryPath)) {
|
|
183
|
+
console.error('Binary still not found after download.');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
runBinary(binaryPath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch((err) => {
|
|
191
|
+
console.error('Error:', err.message);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
|
|
7
|
+
const REPO = 'Marcus-Mok-GH/codebuff-cli';
|
|
8
|
+
const BINARY_NAME = 'codebuff';
|
|
9
|
+
const MAX_RETRIES = 3;
|
|
10
|
+
const REQUEST_TIMEOUT_MS = 120000;
|
|
11
|
+
const MAX_REDIRECTS = 10;
|
|
12
|
+
|
|
13
|
+
function getVersion() {
|
|
14
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
17
|
+
if (pkg.version) return pkg.version;
|
|
18
|
+
} catch {}
|
|
19
|
+
throw new Error('Could not determine version from ' + pkgPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getPlatform() {
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
const arch = process.arch;
|
|
25
|
+
const mappings = {
|
|
26
|
+
darwin: 'darwin',
|
|
27
|
+
linux: 'linux',
|
|
28
|
+
win32: 'win32',
|
|
29
|
+
};
|
|
30
|
+
const osName = mappings[platform] || platform;
|
|
31
|
+
return osName + '-' + arch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getBinaryPath() {
|
|
35
|
+
const binDir = path.join(__dirname, '..', 'bin');
|
|
36
|
+
const name = process.platform === 'win32' ? BINARY_NAME + '.exe' : BINARY_NAME;
|
|
37
|
+
return path.join(binDir, name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cleanup(dest) {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(dest)) {
|
|
43
|
+
fs.unlinkSync(dest);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore cleanup errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function download(url, dest, redirectCount = 0) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
53
|
+
reject(new Error('Too many redirects (max ' + MAX_REDIRECTS + ') while downloading from ' + url));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const client = url.startsWith('https:') ? https : http;
|
|
58
|
+
const file = fs.createWriteStream(dest);
|
|
59
|
+
|
|
60
|
+
const req = client.get(
|
|
61
|
+
url,
|
|
62
|
+
{ headers: { 'User-Agent': 'codebuff-cli-installer' } },
|
|
63
|
+
(res) => {
|
|
64
|
+
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
|
|
65
|
+
file.close();
|
|
66
|
+
cleanup(dest);
|
|
67
|
+
download(new URL(res.headers.location, url).href, dest, redirectCount + 1)
|
|
68
|
+
.then(resolve)
|
|
69
|
+
.catch(reject);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (res.statusCode !== 200) {
|
|
74
|
+
file.close();
|
|
75
|
+
cleanup(dest);
|
|
76
|
+
reject(new Error('Download failed: HTTP ' + res.statusCode + ' from ' + url));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
res.pipe(file);
|
|
81
|
+
file.on('finish', () => {
|
|
82
|
+
file.close();
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
req.on('error', (err) => {
|
|
89
|
+
file.close();
|
|
90
|
+
cleanup(dest);
|
|
91
|
+
reject(new Error('Network error downloading from ' + url + ': ' + err.message));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
95
|
+
req.destroy();
|
|
96
|
+
file.close();
|
|
97
|
+
cleanup(dest);
|
|
98
|
+
reject(new Error('Download timeout (' + REQUEST_TIMEOUT_MS + 'ms) for ' + url));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function downloadWithRetry(url, dest) {
|
|
104
|
+
let lastError;
|
|
105
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
console.log('Downloading binary (attempt ' + attempt + '/' + MAX_RETRIES + ')...');
|
|
108
|
+
await download(url, dest);
|
|
109
|
+
return true;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
lastError = err;
|
|
112
|
+
console.error('Attempt ' + attempt + ' failed: ' + err.message);
|
|
113
|
+
if (attempt < MAX_RETRIES) {
|
|
114
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
115
|
+
console.log('Retrying in ' + delay + 'ms...');
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.error('Failed to download binary after ' + MAX_RETRIES + ' attempts.');
|
|
121
|
+
console.error('Last error: ' + lastError.message);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main() {
|
|
126
|
+
const version = getVersion();
|
|
127
|
+
const platform = getPlatform();
|
|
128
|
+
const binaryPath = getBinaryPath();
|
|
129
|
+
|
|
130
|
+
const url = 'https://github.com/' + REPO + '/releases/download/v' + version + '/codebuff-' + platform;
|
|
131
|
+
|
|
132
|
+
console.log('Platform: ' + platform);
|
|
133
|
+
console.log('Version: ' + version);
|
|
134
|
+
console.log('Binary path: ' + binaryPath);
|
|
135
|
+
console.log('Download URL: ' + url);
|
|
136
|
+
|
|
137
|
+
fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(binaryPath)) {
|
|
140
|
+
console.log('Binary already exists. Skipping download.');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (await downloadWithRetry(url, binaryPath)) {
|
|
145
|
+
console.log('Download complete.');
|
|
146
|
+
const versionFile = path.join(path.dirname(binaryPath), '.version');
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(versionFile, version, 'utf8');
|
|
149
|
+
console.log('Version file written.');
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.warn('Warning: could not write version file:', err.message);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
console.error('\nUnable to download the codebuff binary.');
|
|
155
|
+
console.error('You can try installing manually from:');
|
|
156
|
+
console.error(' ' + url);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (process.platform !== 'win32') {
|
|
161
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
162
|
+
console.log('Made binary executable.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log('Binary installed at: ' + binaryPath);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((err) => {
|
|
169
|
+
console.error('Fatal error:', err.message);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|