@logicseek/hello-cc 0.1.1 → 0.1.3
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 +164 -0
- package/README.md +31 -6
- package/README.zh-CN.md +31 -7
- package/assets/logo.svg +84 -0
- package/bin/hcc.mjs +2388 -290
- package/docs/README.md +24 -0
- package/docs/README.zh-CN.md +24 -0
- package/docs/commands.md +135 -0
- package/docs/commands.zh-CN.md +126 -0
- package/docs/design.md +12 -4
- package/docs/guide.md +83 -71
- package/docs/guide.zh-CN.md +74 -68
- package/docs/implementation.md +21 -1
- package/package.json +7 -1
- package/scripts/github-release.mjs +190 -0
- package/scripts/regression.mjs +852 -31
- package/scripts/release-notes.mjs +112 -0
package/docs/guide.zh-CN.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# hello-cc 用户指南
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
这份文档说明安装、启动和日常使用流程。紧凑命令清单见
|
|
4
|
+
[命令参考](commands.zh-CN.md)。
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## 安装、更新和卸载
|
|
6
7
|
|
|
7
8
|
全局安装 hello-cc:
|
|
8
9
|
|
|
@@ -13,10 +14,29 @@ npm install -g @logicseek/hello-cc
|
|
|
13
14
|
更新已有的全局安装:
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
|
|
17
|
+
hcc update
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
`hcc`
|
|
20
|
+
`hcc update` 会执行 `npm install -g @logicseek/hello-cc@latest`。使用
|
|
21
|
+
`hcc update --help` 可以查看更新选项。`hcc task update` 更新的是协作任务状态,不是安装包版本。
|
|
22
|
+
|
|
23
|
+
移除本机 hooks 和 shims:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
hcc uninstall
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
同时移除当前项目的 `.hello-cc` 数据和指导块:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
hcc uninstall --purge --yes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
移除全局 npm 包:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm uninstall -g @logicseek/hello-cc
|
|
39
|
+
```
|
|
20
40
|
|
|
21
41
|
## 默认入口
|
|
22
42
|
|
|
@@ -47,8 +67,8 @@ project: /path/to/project
|
|
|
47
67
|
database: /path/to/project/.hello-cc/mesh.db
|
|
48
68
|
runtime: /path/to/project/.hello-cc/runtime.json
|
|
49
69
|
log: /path/to/project/.hello-cc/web.log
|
|
50
|
-
open: http://<machine-ip>:8787/?project=%2Fpath%2Fto%2Fproject
|
|
51
|
-
local: http://127.0.0.1:8787/?project=%2Fpath%2Fto%2Fproject
|
|
70
|
+
open: http://<machine-ip>:8787/?token=<token>&project=%2Fpath%2Fto%2Fproject
|
|
71
|
+
local: http://127.0.0.1:8787/?token=<token>&project=%2Fpath%2Fto%2Fproject
|
|
52
72
|
shims: installed claude, codex
|
|
53
73
|
PATH updated in ~/.bashrc; open a new terminal or source it
|
|
54
74
|
stop: hcc down
|
|
@@ -123,6 +143,8 @@ Claude/Codex/普通 shell
|
|
|
123
143
|
|
|
124
144
|
- 任务是项目事实。所有 peer 默认都能看到未 `done` 且未 `abandoned` 的任务。
|
|
125
145
|
- 消息是带收件人的邮箱消息。`hcc msg inbox` 默认读取当前 peer 的未读消息,`hcc msg ack` 记录每个 peer 的已读状态。
|
|
146
|
+
- 消息回复有 thread。回复某条消息时使用 `hcc msg reply --id N --body "..."`,
|
|
147
|
+
查看完整线程时使用 `hcc msg thread --id N`。
|
|
126
148
|
- `ask` 和 `broadcast` 会写入持久消息;加 `--inject` 时,还会把内容实时注入到 Web runtime 已接管的终端。
|
|
127
149
|
- 锁使用 SQLite 事务和 TTL,是协作式冲突避免,不是文件系统强制沙箱。
|
|
128
150
|
|
|
@@ -137,7 +159,8 @@ Claude/Codex/普通 shell
|
|
|
137
159
|
- 未完成任务
|
|
138
160
|
- 当前 peer 的未读消息
|
|
139
161
|
- 活跃锁
|
|
140
|
-
-
|
|
162
|
+
- 来自 `hcc state` 的机器可读下一步动作
|
|
163
|
+
- 建议执行的 `hcc status`、`hcc state`、`hcc peers`、`hcc task list`、`hcc msg inbox`、`hcc lock list`
|
|
141
164
|
|
|
142
165
|
因此,当你在已接入的 Claude/Codex 里问“其他会话在做什么”,模型应该基于 hello-cc 注入的最新状态回答,而不是回答“我无法知道其他会话”。
|
|
143
166
|
|
|
@@ -158,31 +181,41 @@ claude -p 'Do you see hello-cc open tasks?' --debug hooks --debug-file /tmp/hcc-
|
|
|
158
181
|
|
|
159
182
|
debug 日志里应该出现 `Hook UserPromptSubmit ... provided additionalContext`,并包含 `hello-cc coordination` 块。
|
|
160
183
|
|
|
184
|
+
`hcc state --peer <peer> [--resource PATH]` 会读取同一份协作状态,但不会执行
|
|
185
|
+
确认消息、认领任务、获取锁、发送消息、创建 handoff 或标记完成这类协作动作。
|
|
186
|
+
它返回现有项目数组、统一 `timeline`,以及 `automation` 对象。关键机器字段是
|
|
187
|
+
`automation.next_action.argv`:agent 可以显式执行这条 hcc 命令,让协作流程留下
|
|
188
|
+
可审计记录,而不是只依赖 prompt 里的自然语言规则。`automation.current_task`
|
|
189
|
+
会携带当前 peer 的活动任务,让恢复后的会话继续同一个任务,直到交接、完成或阻塞。
|
|
190
|
+
|
|
161
191
|
## Web 控制台
|
|
162
192
|
|
|
163
|
-
|
|
193
|
+
默认开启本机和内网访问,并启用 token 认证:
|
|
164
194
|
|
|
165
195
|
```bash
|
|
166
196
|
hcc web
|
|
167
197
|
```
|
|
168
198
|
|
|
169
|
-
|
|
199
|
+
裸 `hcc web` 会保存自动生成的 token,并在重启后复用。需要替换保存值时可显式设置:
|
|
170
200
|
|
|
171
201
|
```bash
|
|
172
|
-
HCC_WEB_TOKEN='choose-a-long-token' hcc web --
|
|
202
|
+
HCC_WEB_TOKEN='choose-a-long-token' hcc web --port 8787
|
|
173
203
|
```
|
|
174
204
|
|
|
175
205
|
同一内网机器打开:
|
|
176
206
|
|
|
177
207
|
```text
|
|
178
|
-
http://<machine-ip>:8787
|
|
208
|
+
http://<machine-ip>:8787/?token=<token>
|
|
179
209
|
```
|
|
180
210
|
|
|
211
|
+
使用 `hcc web --local` 可只监听 `127.0.0.1`。只有在可信本地/测试环境才使用
|
|
212
|
+
`hcc web --no-token`。
|
|
213
|
+
|
|
181
214
|
Web 控制台能做这些事:
|
|
182
215
|
|
|
183
216
|
- 在一个端口里切换多个已注册项目 root
|
|
184
217
|
- 直接在浏览器里注册新的项目路径
|
|
185
|
-
-
|
|
218
|
+
- 查看项目状态:下一步动作、timeline、peers、tasks、locks
|
|
186
219
|
- 操作本地终端:启动 Claude/Codex/shell、发送键盘输入、查看输出、停止接管
|
|
187
220
|
|
|
188
221
|
Web 操作的是 hello-cc 管理的本机 tmux 终端:
|
|
@@ -193,63 +226,12 @@ Web 操作的是 hello-cc 管理的本机 tmux 终端:
|
|
|
193
226
|
- 通过 `hcc peer attach` 接入的已有 tmux pane
|
|
194
227
|
|
|
195
228
|
已经打开的普通 raw 终端不能被无条件捕获或控制,除非它原本就在 tmux、screen 或 hello-cc shim 之下。它仍然可以通过 hooks 和 `hcc` 命令参与任务、消息和锁。
|
|
229
|
+
对于这类 coordination-only peer,Web 会通过 Timeline 展示协作轨迹,而不是显示终端流。
|
|
196
230
|
|
|
197
|
-
##
|
|
231
|
+
## 命令参考
|
|
198
232
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
hcc web [--host HOST] [--port N] [--token TEXT] [--local] [--no-discover] [--no-guidance]
|
|
202
|
-
hcc down
|
|
203
|
-
|
|
204
|
-
# 本地协调,无 Web
|
|
205
|
-
hcc up [--no-discover] [--no-guidance]
|
|
206
|
-
|
|
207
|
-
# 对等节点和状态
|
|
208
|
-
hcc peers
|
|
209
|
-
hcc status [--peer ID]
|
|
210
|
-
hcc scan [--register]
|
|
211
|
-
hcc prompt --peer ID [--kind codex|claude|shell|other] [--role ROLE]
|
|
212
|
-
hcc join --peer ID [--kind codex|claude|shell|other] [--role ROLE]
|
|
213
|
-
hcc env --peer ID
|
|
214
|
-
hcc heartbeat [--peer ID] [--renew-locks --ttl 900]
|
|
215
|
-
hcc run --peer ID --kind codex|claude|shell --role ROLE -- COMMAND [ARGS...]
|
|
216
|
-
|
|
217
|
-
# Web 可控终端
|
|
218
|
-
hcc peer list
|
|
219
|
-
hcc peer start PEER [--kind K] [--role R] [--cwd DIR] [--restart-env] -- COMMAND [ARGS...]
|
|
220
|
-
hcc peer start PEER --kind codex --resume SESSION_ID [--restart-env]
|
|
221
|
-
hcc peer start PEER --kind codex --last
|
|
222
|
-
hcc peer start PEER --kind claude --resume SESSION_ID [--restart-env]
|
|
223
|
-
hcc peer start PEER --kind claude --continue
|
|
224
|
-
hcc peer attach PEER [--pane PANE] [--kind K] [--role R] [--cwd DIR]
|
|
225
|
-
hcc peer stop PEER
|
|
226
|
-
hcc inject PEER TEXT [--no-enter]
|
|
227
|
-
|
|
228
|
-
# 消息
|
|
229
|
-
hcc msg send [--from ID] [--to ID|all] --body TEXT [--task N] [--kind note|task|handoff]
|
|
230
|
-
hcc msg inbox [--peer ID] [--wait SEC] [--all] [--limit N]
|
|
231
|
-
hcc msg ack [--peer ID] --id N
|
|
232
|
-
hcc ask PEER MESSAGE [--from ID] [--task N] [--inject]
|
|
233
|
-
hcc broadcast MESSAGE [--from ID] [--task N] [--inject]
|
|
234
|
-
|
|
235
|
-
# 任务
|
|
236
|
-
hcc task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
|
|
237
|
-
hcc task list [--status S] [--peer ID] [--all]
|
|
238
|
-
hcc task claim [--peer ID] --id N
|
|
239
|
-
hcc task next [--peer ID]
|
|
240
|
-
hcc task update [--peer ID] --id N --status running|review|blocked|done|abandoned [--summary TEXT] [--body TEXT] [--to ID]
|
|
241
|
-
hcc task done [--peer ID] --id N --summary TEXT
|
|
242
|
-
|
|
243
|
-
# 锁和交接
|
|
244
|
-
hcc lock acquire [--peer ID] --resource PATH [--task N] [--ttl SEC] [--reason TEXT]
|
|
245
|
-
hcc lock renew [--peer ID] --resource PATH [--ttl SEC]
|
|
246
|
-
hcc lock release [--peer ID] --resource PATH [--force]
|
|
247
|
-
hcc lock list [--all]
|
|
248
|
-
hcc handoff create [--from ID] --summary TEXT [--task N] [--to ID] [--changed-files JSON_OR_CSV] [--tests TEXT] [--risks TEXT]
|
|
249
|
-
hcc handoff list [--task N] [--limit N]
|
|
250
|
-
hcc event tail [--limit N]
|
|
251
|
-
hcc gc [--older-than DAYS] [--yes]
|
|
252
|
-
```
|
|
233
|
+
使用 `hcc --help` 查看命令列表,使用 `hcc <command> --help` 查看单个命令。
|
|
234
|
+
维护中的命令摘要见 [命令参考](commands.zh-CN.md)。
|
|
253
235
|
|
|
254
236
|
## 稳定 peer 身份
|
|
255
237
|
|
|
@@ -284,10 +266,34 @@ hcc task next
|
|
|
284
266
|
hcc lock acquire --resource vllm/router --ttl 900 --reason "实现路由诊断"
|
|
285
267
|
```
|
|
286
268
|
|
|
269
|
+
`task next` 会先返回当前 peer 已经认领、运行、审查或阻塞中的任务。只有明确
|
|
270
|
+
要再接一个 pending 任务时,才使用 `hcc task next --force`。
|
|
271
|
+
|
|
272
|
+
当工作确实可以并行时,用显式团队把父任务拆成子任务:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
hcc team plan --from-task 12 \
|
|
276
|
+
--item "docs:更新用户指南" \
|
|
277
|
+
--item "tests:增加回归测试" \
|
|
278
|
+
--workers codex:1,claude:1
|
|
279
|
+
|
|
280
|
+
hcc team start --from-task 12 \
|
|
281
|
+
--item "docs:更新用户指南" \
|
|
282
|
+
--item "tests:增加回归测试" \
|
|
283
|
+
--workers codex:1,claude:1
|
|
284
|
+
|
|
285
|
+
hcc team status --task 12
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`team plan` 是只读命令。`team start` 会创建子任务和分配消息,但不会隐藏地启动
|
|
289
|
+
模型进程。每个 agent 仍然通过普通任务、锁、交接和完成命令处理自己的子任务。
|
|
290
|
+
|
|
287
291
|
发消息或实时注入:
|
|
288
292
|
|
|
289
293
|
```bash
|
|
290
294
|
hcc msg send --to claude-a --body "实现已准备好供审查。"
|
|
295
|
+
hcc msg reply --id 12 --body "已审查,未发现阻塞问题。"
|
|
296
|
+
hcc msg thread --id 12
|
|
291
297
|
hcc ask claude-a "请审查当前实现。" --inject
|
|
292
298
|
hcc broadcast "暂停编辑 vllm/router,等待任务完成。" --inject
|
|
293
299
|
```
|
|
@@ -311,7 +317,7 @@ hcc lock release --resource vllm/router
|
|
|
311
317
|
| `HCC_PEER` | 默认 peer 身份 |
|
|
312
318
|
| `HCC_ROOT` | 覆盖项目根目录 |
|
|
313
319
|
| `HCC_DB` | 覆盖数据库路径 |
|
|
314
|
-
| `HCC_WEB_TOKEN` |
|
|
320
|
+
| `HCC_WEB_TOKEN` | 替换并保存固定 Web 访问令牌 |
|
|
315
321
|
| `HCC_RUNTIME_URL` | 直接指定 runtime URL |
|
|
316
322
|
| `HCC_NO_AUTO_INSTALL_TMUX=1` | 禁用 tmux 自动安装,主要用于测试 |
|
|
317
323
|
|
package/docs/implementation.md
CHANGED
|
@@ -119,7 +119,9 @@ Messages:
|
|
|
119
119
|
```text
|
|
120
120
|
from peer
|
|
121
121
|
to peer | all | tag:<tag> | task:<task-id>
|
|
122
|
-
kind note | ask | handoff | system
|
|
122
|
+
kind note | ask | reply | handoff | system
|
|
123
|
+
reply_to optional source message id
|
|
124
|
+
thread_id root message id for threaded collaboration history
|
|
123
125
|
delivery store-only | inject-terminal | both
|
|
124
126
|
```
|
|
125
127
|
|
|
@@ -128,11 +130,29 @@ Tasks:
|
|
|
128
130
|
```text
|
|
129
131
|
pending -> claimed -> running -> review/block/done|abandoned
|
|
130
132
|
owner is dynamic; there is no fixed dispatcher
|
|
133
|
+
parent_id/team_role record explicit team child tasks
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
Default task reads are lifecycle-based: all peers see tasks until they are
|
|
134
137
|
`done` or `abandoned`. Read/ack state exists only for messages.
|
|
135
138
|
|
|
139
|
+
`hcc team plan` is read-only. `hcc team start` creates child tasks under the
|
|
140
|
+
parent and assignment messages; it does not spawn hidden agents or bypass the
|
|
141
|
+
normal claim/lock/handoff protocol.
|
|
142
|
+
|
|
143
|
+
State:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
hcc state / /api/state
|
|
147
|
+
timeline derived from messages, handoffs, tasks, locks, and selected events
|
|
148
|
+
automation.next_action.argv suggests the next explicit hcc command
|
|
149
|
+
automation.current_task preserves sticky task context across prompts
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
State inspection does not perform coordination actions such as acking messages,
|
|
153
|
+
claiming tasks, acquiring locks, sending messages, creating handoffs, or marking
|
|
154
|
+
tasks done. Opening a DB may still run schema/version maintenance.
|
|
155
|
+
|
|
136
156
|
Locks:
|
|
137
157
|
|
|
138
158
|
```text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logicseek/hello-cc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local control plane for coordinating Claude Code, Codex, and other coding CLI sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
"lib/",
|
|
13
13
|
"scripts/",
|
|
14
|
+
"assets/",
|
|
14
15
|
"README.md",
|
|
15
16
|
"README.zh-CN.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
16
18
|
"LICENSE",
|
|
17
19
|
"docs/"
|
|
18
20
|
],
|
|
@@ -20,6 +22,10 @@
|
|
|
20
22
|
"node": ">=24.0.0"
|
|
21
23
|
},
|
|
22
24
|
"scripts": {
|
|
25
|
+
"release:notes": "node ./scripts/release-notes.mjs",
|
|
26
|
+
"release:check": "node ./scripts/release-notes.mjs --check",
|
|
27
|
+
"release:github": "node ./scripts/github-release.mjs",
|
|
28
|
+
"release:github:dry-run": "node ./scripts/github-release.mjs --dry-run",
|
|
23
29
|
"smoke": "node ./bin/hcc.mjs --help",
|
|
24
30
|
"test": "node ./scripts/regression.mjs",
|
|
25
31
|
"test:regression": "node ./scripts/regression.mjs"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(import.meta.dirname, '..');
|
|
8
|
+
const githubApi = 'https://api.github.com';
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.log(`github-release
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
node scripts/github-release.mjs [--version VERSION] [--repo OWNER/REPO] [--dry-run]
|
|
15
|
+
|
|
16
|
+
Creates or updates the GitHub Release for vVERSION using the matching
|
|
17
|
+
CHANGELOG.md section as the release body.
|
|
18
|
+
|
|
19
|
+
Authentication:
|
|
20
|
+
Set GH_TOKEN or GITHUB_TOKEN to a token with repo release permissions.
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(file) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const opts = { dryRun: false, help: false, repo: null, version: null };
|
|
30
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === '--help' || arg === '-h') {
|
|
33
|
+
opts.help = true;
|
|
34
|
+
} else if (arg === '--dry-run') {
|
|
35
|
+
opts.dryRun = true;
|
|
36
|
+
} else if (arg === '--repo') {
|
|
37
|
+
opts.repo = argv[++i];
|
|
38
|
+
if (opts.repo === undefined) throw new Error('Missing value for --repo');
|
|
39
|
+
} else if (arg.startsWith('--repo=')) {
|
|
40
|
+
opts.repo = arg.slice('--repo='.length);
|
|
41
|
+
} else if (arg === '--version') {
|
|
42
|
+
opts.version = argv[++i];
|
|
43
|
+
if (opts.version === undefined) throw new Error('Missing value for --version');
|
|
44
|
+
} else if (arg.startsWith('--version=')) {
|
|
45
|
+
opts.version = arg.slice('--version='.length);
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return opts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeVersion(version) {
|
|
54
|
+
return String(version || '').replace(/^v/, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function releaseSection(markdown, version) {
|
|
58
|
+
const lines = markdown.split(/\r?\n/);
|
|
59
|
+
const heading = `## ${version}`;
|
|
60
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
61
|
+
if (start < 0) return null;
|
|
62
|
+
|
|
63
|
+
let end = lines.length;
|
|
64
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
65
|
+
if (/^##\s+/.test(lines[i])) {
|
|
66
|
+
end = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(start, end).join('\n').trimEnd();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateSection(section, version) {
|
|
74
|
+
const problems = [];
|
|
75
|
+
if (!section) {
|
|
76
|
+
problems.push(`CHANGELOG.md is missing section ## ${version}`);
|
|
77
|
+
return problems;
|
|
78
|
+
}
|
|
79
|
+
if (!section.includes('### Summary')) problems.push(`## ${version} is missing ### Summary`);
|
|
80
|
+
if (!section.includes('### Highlights')) problems.push(`## ${version} is missing ### Highlights`);
|
|
81
|
+
if (section.includes('TODO') || section.includes('TBD')) problems.push(`## ${version} still contains TODO/TBD text`);
|
|
82
|
+
const bulletCount = (section.match(/^- /gm) || []).length;
|
|
83
|
+
if (bulletCount < 2) problems.push(`## ${version} should include at least two release-note bullets`);
|
|
84
|
+
return problems;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function repoFromPackage(pkg) {
|
|
88
|
+
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
89
|
+
if (!repo) return null;
|
|
90
|
+
const match = repo.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
|
|
91
|
+
return match ? match[1] : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function envToken() {
|
|
95
|
+
return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function githubRequest(method, route, token, body = null) {
|
|
99
|
+
const response = await fetch(`${githubApi}${route}`, {
|
|
100
|
+
method,
|
|
101
|
+
headers: {
|
|
102
|
+
Accept: 'application/vnd.github+json',
|
|
103
|
+
Authorization: `Bearer ${token}`,
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
'User-Agent': 'hello-cc-release-script'
|
|
106
|
+
},
|
|
107
|
+
body: body ? JSON.stringify(body) : undefined
|
|
108
|
+
});
|
|
109
|
+
const text = await response.text();
|
|
110
|
+
const data = text ? JSON.parse(text) : null;
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const detail = data?.message ? `: ${data.message}` : '';
|
|
113
|
+
const error = new Error(`GitHub API ${method} ${route} failed with ${response.status}${detail}`);
|
|
114
|
+
error.status = response.status;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function main() {
|
|
121
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
122
|
+
if (opts.help) {
|
|
123
|
+
usage();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pkg = readJson(path.join(repoRoot, 'package.json'));
|
|
128
|
+
const version = normalizeVersion(opts.version || pkg.version);
|
|
129
|
+
const tag = `v${version}`;
|
|
130
|
+
const repo = opts.repo || process.env.GITHUB_REPOSITORY || repoFromPackage(pkg);
|
|
131
|
+
if (!repo) throw new Error('Missing repository; pass --repo OWNER/REPO');
|
|
132
|
+
|
|
133
|
+
const changelog = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
|
134
|
+
const body = releaseSection(changelog, version);
|
|
135
|
+
const problems = validateSection(body, version);
|
|
136
|
+
if (problems.length) {
|
|
137
|
+
console.error(problems.join('\n'));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const payload = {
|
|
143
|
+
tag_name: tag,
|
|
144
|
+
name: tag,
|
|
145
|
+
body,
|
|
146
|
+
draft: false,
|
|
147
|
+
prerelease: false
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (opts.dryRun) {
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
ok: true,
|
|
153
|
+
dry_run: true,
|
|
154
|
+
repo,
|
|
155
|
+
tag,
|
|
156
|
+
name: payload.name,
|
|
157
|
+
body_length: body.length,
|
|
158
|
+
body_preview: body.split(/\r?\n/).slice(0, 8).join('\n')
|
|
159
|
+
}, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const token = envToken();
|
|
164
|
+
if (!token) throw new Error('Missing GH_TOKEN or GITHUB_TOKEN');
|
|
165
|
+
|
|
166
|
+
let existing = null;
|
|
167
|
+
try {
|
|
168
|
+
existing = await githubRequest('GET', `/repos/${repo}/releases/tags/${tag}`, token);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.status !== 404) throw err;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = existing
|
|
174
|
+
? await githubRequest('PATCH', `/repos/${repo}/releases/${existing.id}`, token, payload)
|
|
175
|
+
: await githubRequest('POST', `/repos/${repo}/releases`, token, payload);
|
|
176
|
+
|
|
177
|
+
console.log(JSON.stringify({
|
|
178
|
+
ok: true,
|
|
179
|
+
action: existing ? 'updated' : 'created',
|
|
180
|
+
repo,
|
|
181
|
+
tag,
|
|
182
|
+
url: result.html_url,
|
|
183
|
+
body_length: body.length
|
|
184
|
+
}, null, 2));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch((err) => {
|
|
188
|
+
console.error(err.message || String(err));
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
});
|