@jspg-ai/coding-bb 0.0.5-beta.3 → 0.0.5-beta.4

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.
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -21,7 +21,8 @@ require('./silence-popup');
21
21
  * - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
22
22
  * - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
23
23
  * 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
24
- * (本地主干与 origin 分叉 → FAIL pull-failed,交用户决策;远端无主干 → 注记跳过)
24
+ * (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
25
+ * pull-failed,交用户决策;远端无主干 → 注记跳过)
25
26
  * 6. 已在主干 → 跳过切回,仍追平(幂等)
26
27
  *
27
28
  * 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
@@ -160,12 +161,18 @@ function main() {
160
161
  }
161
162
 
162
163
  if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
164
+ const before = run('git rev-parse --quiet HEAD', rp.repo);
163
165
  const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
164
166
  if (!pull.ok) {
165
167
  rec.status = 'failed';
166
168
  rec.reason = 'pull-failed';
167
169
  rec.detail = (pull.err || '').split(/\r?\n/)[0];
168
170
  failed = true;
171
+ } else {
172
+ const after = run('git rev-parse --quiet HEAD', rp.repo);
173
+ rec.detail = before.ok && after.ok && before.out !== after.out
174
+ ? 'pulled origin/' + main
175
+ : 'up-to-date';
169
176
  }
170
177
  } else if (!rec.reason) {
171
178
  rec.reason = 'no-remote-trunk';
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cbb-local-mode
3
- description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃后回主干 + 删分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
3
+ description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃;交付完成回主干 + 删分支,废弃保留本地分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
4
4
  ---
5
5
 
6
6
  # Local Mode(本地模式)
@@ -103,9 +103,36 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<
103
103
  ```
104
104
 
105
105
  `need-mr`(远端主干受保护)→ 改用提 MR;`failed` → 输出明细、修复后重跑
106
- - **确认废弃** → `deprecate-branches.js`(远程重命名 `<需求名>-deprecated`;本地分支保留原名)
106
+ - **确认废弃** → 三步:检查未推送提交 → 二次确认 → 执行。**废弃后仍进 5.4 步骤 1 回主干,但跳过步骤 2(本地分支保留原名,不调用 `delete-branches.js`)**。
107
107
 
108
- **5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master):
108
+ 1. 检查未推送提交(废弃重命名只覆盖远端已推送的部分,本地 ahead 提交不会进 `<需求名>-deprecated`):
109
+
110
+ ```bash
111
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
112
+ ```
113
+
114
+ **解读末行 `DEPRECATE_CHECK_RESULT`**:每仓库 `unpushed`(本地领先远端的提交数;`all` = 远端无该分支、提交全部未推送;`0` = 已全部推送)。存在 `unpushed > 0` / `all` 时用 `AskUserQuestion`:
115
+
116
+ ```
117
+ ⚠️ 以下仓库存在未推送提交(将只保留在本地分支,不进 <需求名>-deprecated):
118
+ - workspace: 3 个 commit
119
+
120
+ 请选择处理方式:
121
+ [先推送](推荐)调 cbb-worktree-push 推送全部提交后回到本步
122
+ [继续] 不推送直接废弃(这些提交只存在于被保留的本地分支)
123
+ [中止] 终止流程,我先手动处理
124
+ ```
125
+
126
+ 2. 二次确认(远程重命名不可逆,`AskUserQuestion` 固定选项):确认废弃 / 中止
127
+ 3. 执行:
128
+
129
+ ```bash
130
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>"
131
+ ```
132
+
133
+ `failed` → 报告并终止;完成后进 5.4(**仅步骤 1**)
134
+
135
+ **5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master。**废弃出口仅执行步骤 1**——分支保留、不删):
109
136
 
110
137
  1. 各仓库切回主干并追平远端(替代原裸 git 三连;不自动 stash,失败即报告):
111
138
 
@@ -114,20 +141,20 @@ node "<skill_dir>/scripts/return-to-trunk.js" "<ws_root>" --apps "<app1,app2>"
114
141
  ```
115
142
 
116
143
  **解读末行 `TRUNK_RESULT`**:
117
- - 退出码 0 → 全部已回主干并追平
144
+ - 退出码 0 → 全部已回主干并追平(detail 注记 `pulled origin/<主干>` = 本次有追平;`up-to-date` = 本就最新)
118
145
  - 某仓库 `dirty-worktree` → 检出上有未提交改动(回归 5.1 前置,先处理再重跑);**不自动 stash**
119
146
  - 某仓库 `pull-failed`(本地主干与 origin 分叉)→ 输出详情,交用户决策
120
147
  - 某仓库 `cannot-detect-main` → 检查该仓库远端与主干分支(`fetch-failed` 同理)
121
148
  - 幂等:已在主干的仓库仅追平远端,重复执行无害
122
149
 
123
- 2. 本地 + 远程需求分支删除(本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
150
+ 2. 本地 + 远程需求分支删除(**仅交付完成出口执行**——MR 已合入 / 直合并成功;**废弃出口跳过本步**,本地分支保留原名。本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
124
151
 
125
152
  ```bash
126
153
  node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
127
154
  ```
128
155
 
129
156
  - openspec change 归档在合并前执行(产物在分支上随交付合入)
130
- - 全部仓库回主干且分支删除后,向用户报告"本地模式已收尾,空间回到干净态"
157
+ - 全部仓库回主干(交付完成出口含分支删除)后,向用户报告"本地模式已收尾,空间回到干净态"
131
158
 
132
159
  ## 升级 worktree(中途要并行 / 要隔离)
133
160
 
@@ -146,7 +173,7 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
146
173
  | commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
147
174
  | 误在 main 检出上写了文件 | `git checkout -b <需求名>` 后改动随分支走;已 staged 则 unstage 后再切 |
148
175
  | 提交推送 | 说"提交并推送" → cbb-worktree-push(本地布局自动发现) |
149
- | 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃) |
176
+ | 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃(仅回主干不删分支)) |
150
177
  | 要并行新需求 | 转 worktree 模式(init 分支从 origin/main 建,不占本地检出);本地需求继续占用根 |
151
178
  | 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回主干腾检出(return-to-trunk.js) |
152
179
 
@@ -174,6 +201,11 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
174
201
  - **问题:** 回主干后未追平远端,本地主干陈旧,下次 `start-local` 从旧基线建分支
175
202
  - **修复:** 收尾固定两步脚本:`return-to-trunk.js`(切回主干 + `pull --ff-only` 追平)→ `delete-branches.js` 删分支
176
203
 
204
+ ### 废弃出口误跑删分支
205
+
206
+ - **问题:** 选"确认废弃"后照 5.4 全跑,`delete-branches.js` 对未合并的废弃分支 `-D` 强删——远端 `<需求名>-deprecated` 只含已推送部分,本地领先提交只剩 reflog 可捞
207
+ - **修复:** 废弃出口仅执行 5.4 步骤 1(回主干 + 追平);本地分支保留原名,**不调用** `delete-branches.js`
208
+
177
209
  ### 中途插单静默混入
178
210
 
179
211
  - **问题:** 用户插的"顺手改一下"与本需求改动混在同一 commit,评审困难
@@ -200,5 +232,6 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
200
232
  - 切分支统一走 `start-local.js`(基线 = `<remote>/<main>`,不取 HEAD)
201
233
  - 收尾先过门禁判定(`merge-branches.js --check`);未合并必须三选一后收尾
202
234
  - 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4 `return-to-trunk.js` 切回主干)
203
- - 收尾脚本化(`return-to-trunk.js` 回主干 + `delete-branches.js` 删分支)完成后报告干净态
235
+ - 废弃出口:先 `deprecate-branches.js --check` 查未推送提交(有 ahead 须用户决策)→ 二次确认后执行;废弃后**仅回主干**(5.4 步骤 1),不删本地分支
236
+ - 收尾脚本化(`return-to-trunk.js` 回主干 + 交付完成出口 `delete-branches.js` 删分支)完成后报告干净态
204
237
  - 所有写操作前的分支确认(当前检出 = 需求分支)
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -21,7 +21,8 @@ require('./silence-popup');
21
21
  * - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
22
22
  * - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
23
23
  * 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
24
- * (本地主干与 origin 分叉 → FAIL pull-failed,交用户决策;远端无主干 → 注记跳过)
24
+ * (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
25
+ * pull-failed,交用户决策;远端无主干 → 注记跳过)
25
26
  * 6. 已在主干 → 跳过切回,仍追平(幂等)
26
27
  *
27
28
  * 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
@@ -160,12 +161,18 @@ function main() {
160
161
  }
161
162
 
162
163
  if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
164
+ const before = run('git rev-parse --quiet HEAD', rp.repo);
163
165
  const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
164
166
  if (!pull.ok) {
165
167
  rec.status = 'failed';
166
168
  rec.reason = 'pull-failed';
167
169
  rec.detail = (pull.err || '').split(/\r?\n/)[0];
168
170
  failed = true;
171
+ } else {
172
+ const after = run('git rev-parse --quiet HEAD', rp.repo);
173
+ rec.detail = before.ok && after.ok && before.out !== after.out
174
+ ? 'pulled origin/' + main
175
+ : 'up-to-date';
169
176
  }
170
177
  } else if (!rec.reason) {
171
178
  rec.reason = 'no-remote-trunk';
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspg-ai/coding-bb",
3
- "version": "0.0.5-beta.3",
3
+ "version": "0.0.5-beta.4",
4
4
  "description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
5
5
  "main": "cbb/lib/install/init.js",
6
6
  "bin": {