@jspg-ai/coding-bb 0.0.3-beta.8 → 0.0.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.
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +0 -15
- package/cbb/dev-standards/rules/cbb-workspace-discipline.md +38 -0
- package/cbb/lib/external/index.js +228 -0
- package/cbb/lib/install/agents-nav.js +92 -0
- package/cbb/lib/install/claude-code.js +8 -1
- package/cbb/lib/install/command-skill.js +105 -0
- package/cbb/lib/install/init.js +399 -129
- package/cbb/lib/install/minimax.js +60 -0
- package/cbb/lib/install/rule-rewrite.js +1 -1
- package/cbb/lib/install/setup-decision.js +92 -0
- package/cbb/lib/install/tool-selection.js +23 -0
- package/cbb/lib/install/trae.js +18 -8
- package/cbb/lib/install/workbuddy.js +36 -0
- package/cbb/lib/install/workspaces.js +15 -61
- package/cbb/lib/utils/check-update.js +13 -5
- package/cbb/lib/utils/githook.js +230 -0
- package/cbb/lib/utils/gitignore.js +17 -4
- package/cbb/lib/utils/plugin-hook.js +92 -0
- package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/_shared/scripts/check-unarchived.js +77 -0
- package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/_shared/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/_shared/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/_shared/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
- package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +194 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +133 -81
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +77 -48
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -136
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +44 -9
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +15 -4
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +136 -123
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +11 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +37 -9
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +212 -56
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/check-unarchived.js +77 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
- package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
- package/config/openspec/schemas/spec-driven/schema.yaml +20 -0
- package/config/upstream-mirrors.json +5 -0
- package/external/frontend-design/.version +6 -0
- package/external/frontend-design/LICENSE.txt +177 -0
- package/external/frontend-design/SKILL.md +71 -0
- package/openspec/.version +2 -2
- package/openspec/commands/archive.md +15 -8
- package/openspec/commands/bulk-archive.md +13 -6
- package/openspec/commands/continue.md +0 -1
- package/openspec/commands/explore.md +1 -1
- package/openspec/commands/ff.md +1 -1
- package/openspec/commands/onboard.md +9 -4
- package/openspec/commands/update.md +11 -5
- package/openspec/commands/verify.md +57 -26
- package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
- package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
- package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
- package/openspec/skills/openspec-explore/SKILL.md +2 -2
- package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
- package/openspec/skills/openspec-new-change/SKILL.md +1 -1
- package/openspec/skills/openspec-onboard/SKILL.md +10 -5
- package/openspec/skills/openspec-propose/SKILL.md +1 -1
- package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
- package/openspec/skills/openspec-update-change/SKILL.md +12 -6
- package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
- package/package.json +3 -2
- package/superpowers/.version +3 -3
- package/superpowers/skills/writing-plans/SKILL.md +32 -20
- package/cbb/lib/install/codebuddy.js +0 -30
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
- package/config/workspace-agents.sample.md +0 -41
- package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 收尾 / 交付合并门禁 —— 直合并通道 + 合并判定(ADR 0007 / 0008)
|
|
5
|
+
*
|
|
6
|
+
* 把需求分支合并进目标分支并推送远端:
|
|
7
|
+
* - 供 cbb-worktree-close / cbb-local-mode 的收尾门禁调用(默认目标 = 主干)
|
|
8
|
+
* - 供 cbb-worktree-push Step 7 的"直接合并"出口调用(--target 可指定测试分支)
|
|
9
|
+
* 默认交付走 MR(人工在浏览器合入);本脚本是用户在门禁处**显式确认**后的
|
|
10
|
+
* "由 agent 直接完成合并"通道。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* node merge-branches.js <ws_root> <branch> [--apps a,b] [--target <分支>] [--allow-checkout] [--dry-run]
|
|
14
|
+
* node merge-branches.js <ws_root> <branch> [--apps a,b] [--target <分支>] --check # 仅判定,不合并
|
|
15
|
+
*
|
|
16
|
+
* 目标分支:
|
|
17
|
+
* 缺省 / --target 等于该仓库探测主干 → 主干快路径(本地主干检出上 merge --no-ff → push)
|
|
18
|
+
* --target 指定非主干分支 → 临时 worktree(基于 origin/<target>,不动用户检出)
|
|
19
|
+
*
|
|
20
|
+
* 模式:
|
|
21
|
+
* --check 逐仓库判定是否已合并到目标(主干含 release-* 中转 / squash / cherry-pick 兜底,
|
|
22
|
+
* 算法与 cbb-worktree-close 的 safety-check.js 同源);不修改任何状态
|
|
23
|
+
* 默认 两阶段(ADR 0007):
|
|
24
|
+
* 1. 预检:逐仓库校验(目标可解析 / 检出干净 / 分支存在)
|
|
25
|
+
* 任一不过 → 退出 2,不执行任何合并
|
|
26
|
+
* 2. 执行:merge --no-ff → push;执行中失败 → 停止并报告已完成清单(不回滚;重跑幂等)
|
|
27
|
+
* --dry-run 只做预检,不执行
|
|
28
|
+
*
|
|
29
|
+
* 退出码:0 全部成功(含 skipped / merged)/ 1 参数错误 / 2 预检或执行失败(含 need-mr / unmerged)
|
|
30
|
+
* 末行:MERGE_RESULT [...] 或 MERGE_CHECK_RESULT [...]
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const { execSync } = require('child_process');
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const os = require('os');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
|
|
38
|
+
function run(cmd, cwd) {
|
|
39
|
+
try {
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
out: execSync(cmd, {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
timeout: 120000,
|
|
47
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
48
|
+
}).trim(),
|
|
49
|
+
};
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function detectMain(repoPath) {
|
|
56
|
+
const remote = run('git remote', repoPath);
|
|
57
|
+
if (!remote.ok || !remote.out) return '';
|
|
58
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
59
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
60
|
+
if (symbolic.ok && symbolic.out) return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 合并判定(与 cbb-worktree-close/scripts/safety-check.js 同源) ──
|
|
65
|
+
// 0=merged, 1=unmerged, 2=to-ref-missing
|
|
66
|
+
function mergedTo(repoPath, from, to) {
|
|
67
|
+
if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
|
|
68
|
+
if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
|
|
69
|
+
if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
|
|
70
|
+
// 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
|
|
71
|
+
const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
|
|
72
|
+
if (!cherry.ok) return 1;
|
|
73
|
+
if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mergedToMain(repoPath, branchName, main) {
|
|
78
|
+
if (mergedTo(repoPath, branchName, main) === 0) return 'main';
|
|
79
|
+
if (mergedTo(repoPath, branchName, 'origin/' + main) === 0) return 'origin/' + main;
|
|
80
|
+
const releaseList = run('git for-each-ref --format="%(refname:short)" "refs/heads/release-*" "refs/remotes/origin/release-*"', repoPath);
|
|
81
|
+
if (releaseList.ok && releaseList.out) {
|
|
82
|
+
const releases = releaseList.out.split(/\r?\n/).filter(Boolean).sort();
|
|
83
|
+
for (const rel of releases) {
|
|
84
|
+
if (mergedTo(repoPath, branchName, rel) === 0) {
|
|
85
|
+
if (mergedTo(repoPath, rel, main) === 0) return 'main-via-' + rel;
|
|
86
|
+
if (mergedTo(repoPath, rel, 'origin/' + main) === 0) return 'origin/' + main + '-via-' + rel;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 目标为具体分支时的已合并判定(含 origin 镜像,不做 release 中转) */
|
|
94
|
+
function mergedToTarget(repoPath, branchName, target) {
|
|
95
|
+
if (mergedTo(repoPath, branchName, target) === 0) return target;
|
|
96
|
+
if (mergedTo(repoPath, branchName, 'origin/' + target) === 0) return 'origin/' + target;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function classifyPushError(stderr) {
|
|
101
|
+
const s = (stderr || '').toLowerCase();
|
|
102
|
+
if (s.includes('protected branch') || s.includes('deny updating')) return 'branch-protected';
|
|
103
|
+
if (s.includes('non-fast-forward') || s.includes('fetch first') || s.includes('rejected')) return 'non-fast-forward';
|
|
104
|
+
if (s.includes('permission denied (publickey)')) return 'ssh-auth-failed';
|
|
105
|
+
if (s.includes('authentication failed')) return 'https-auth-failed';
|
|
106
|
+
if (s.includes('could not resolve host') || s.includes('connection refused') || s.includes('hung up')) return 'network-unreachable';
|
|
107
|
+
return 'unknown';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadApps(wsRoot) {
|
|
111
|
+
try {
|
|
112
|
+
const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
|
|
113
|
+
return Array.isArray(config.apps) ? config.apps : [];
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 参与仓库:workspace(根 = 主干检出)+ 各 app(.codespace/<app> = 主干检出) */
|
|
120
|
+
function buildRepos(wsRoot, appFilter) {
|
|
121
|
+
const repos = [{ name: 'workspace', repo: wsRoot, work: wsRoot }];
|
|
122
|
+
for (const app of loadApps(wsRoot)) {
|
|
123
|
+
if (appFilter && !appFilter.has(app.name)) continue;
|
|
124
|
+
const dir = path.join(wsRoot, '.codespace', app.name);
|
|
125
|
+
repos.push({ name: app.name, repo: dir, work: dir });
|
|
126
|
+
}
|
|
127
|
+
return repos;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasRef(repoPath, ref) {
|
|
131
|
+
return run('git rev-parse --verify --quiet "' + ref + '"', repoPath).ok;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 清理临时 worktree(失败仅警告,不阻断) */
|
|
135
|
+
function cleanupTempWorktree(repoPath, tmpDir) {
|
|
136
|
+
if (!tmpDir) return;
|
|
137
|
+
const removed = run('git worktree remove --force "' + tmpDir + '"', repoPath);
|
|
138
|
+
if (!removed.ok && fs.existsSync(tmpDir)) {
|
|
139
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { /* Windows 锁:留给用户 */ }
|
|
140
|
+
}
|
|
141
|
+
run('git worktree prune', repoPath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function main() {
|
|
145
|
+
const args = process.argv.slice(2);
|
|
146
|
+
const wsRoot = args[0];
|
|
147
|
+
const branch = args[1];
|
|
148
|
+
|
|
149
|
+
if (!wsRoot || !branch || branch.indexOf('--') === 0) {
|
|
150
|
+
console.error('Usage: node merge-branches.js <ws_root> <branch> [--apps a,b] [--target b] [--allow-checkout] [--dry-run] [--check]');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const appsIdx = args.indexOf('--apps');
|
|
155
|
+
const appFilter = appsIdx >= 0
|
|
156
|
+
? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
|
|
157
|
+
: null;
|
|
158
|
+
const targetIdx = args.indexOf('--target');
|
|
159
|
+
const targetArg = targetIdx >= 0 ? (args[targetIdx + 1] || '').trim() : '';
|
|
160
|
+
if (targetIdx >= 0 && !targetArg) {
|
|
161
|
+
console.error('--target 需要提供分支名');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
const allowCheckout = args.includes('--allow-checkout');
|
|
165
|
+
const dryRun = args.includes('--dry-run');
|
|
166
|
+
const checkOnly = args.includes('--check');
|
|
167
|
+
|
|
168
|
+
const repos = buildRepos(wsRoot, appFilter);
|
|
169
|
+
|
|
170
|
+
// ── --check:仅判定每仓库是否已合并到目标 ─────────────────
|
|
171
|
+
if (checkOnly) {
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const rp of repos) {
|
|
174
|
+
const rec = { name: rp.name, status: 'unmerged', merged: null, main: null, target: null };
|
|
175
|
+
if (!fs.existsSync(path.join(rp.repo, '.git'))) {
|
|
176
|
+
rec.status = 'no-repo';
|
|
177
|
+
results.push(rec);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
run('git fetch --quiet origin', rp.repo);
|
|
181
|
+
const main = detectMain(rp.repo);
|
|
182
|
+
if (!main) {
|
|
183
|
+
rec.status = 'failed';
|
|
184
|
+
results.push(rec);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
rec.main = main;
|
|
188
|
+
const target = targetArg || main;
|
|
189
|
+
rec.target = target;
|
|
190
|
+
if (!hasRef(rp.repo, 'refs/heads/' + branch)) {
|
|
191
|
+
rec.status = 'no-branch';
|
|
192
|
+
results.push(rec);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const merged = (!targetArg || target === main)
|
|
196
|
+
? mergedToMain(rp.repo, branch, main)
|
|
197
|
+
: mergedToTarget(rp.repo, branch, target);
|
|
198
|
+
rec.merged = merged;
|
|
199
|
+
rec.status = merged ? 'merged' : 'unmerged';
|
|
200
|
+
results.push(rec);
|
|
201
|
+
}
|
|
202
|
+
results.forEach(r => console.log(r.status.toUpperCase() + ' ' + r.name + (r.merged ? ' → ' + r.merged : '') + (r.target ? ' [target: ' + r.target + ']' : '')));
|
|
203
|
+
const ok = results.every(r => r.status !== 'unmerged' && r.status !== 'failed');
|
|
204
|
+
console.log('---');
|
|
205
|
+
console.log('MERGE_CHECK_RESULT', JSON.stringify({ branch, target: targetArg || null, results }));
|
|
206
|
+
process.exit(ok ? 0 : 2);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── 阶段 1:预检 ──────────────────────────────────────────
|
|
210
|
+
const prechecks = [];
|
|
211
|
+
for (const rp of repos) {
|
|
212
|
+
const rec = { name: rp.name, status: 'ok', main: null, target: null, temp: false, reason: null };
|
|
213
|
+
if (!fs.existsSync(path.join(rp.repo, '.git'))) {
|
|
214
|
+
rec.status = 'skipped';
|
|
215
|
+
rec.reason = 'no-repo';
|
|
216
|
+
prechecks.push(rec);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const fetch = run('git fetch --quiet origin', rp.repo);
|
|
220
|
+
if (!fetch.ok) {
|
|
221
|
+
rec.status = 'failed';
|
|
222
|
+
rec.reason = 'fetch-failed';
|
|
223
|
+
prechecks.push(rec);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const main = detectMain(rp.repo);
|
|
227
|
+
if (!main) {
|
|
228
|
+
rec.status = 'failed';
|
|
229
|
+
rec.reason = 'cannot-detect-main';
|
|
230
|
+
prechecks.push(rec);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
rec.main = main;
|
|
234
|
+
const target = targetArg || main;
|
|
235
|
+
rec.target = target;
|
|
236
|
+
|
|
237
|
+
if (!hasRef(rp.repo, 'refs/heads/' + branch)) {
|
|
238
|
+
rec.status = 'skipped';
|
|
239
|
+
rec.reason = 'no-branch';
|
|
240
|
+
prechecks.push(rec);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const targetLocal = hasRef(rp.repo, 'refs/heads/' + target);
|
|
244
|
+
const targetRemote = hasRef(rp.repo, 'refs/remotes/origin/' + target);
|
|
245
|
+
if (!targetLocal && !targetRemote) {
|
|
246
|
+
rec.status = 'failed';
|
|
247
|
+
rec.reason = 'target-not-found';
|
|
248
|
+
prechecks.push(rec);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (target === main) {
|
|
253
|
+
// 主干路径(快路径,与 ADR 0007 收尾门禁一致)
|
|
254
|
+
if (mergedToMain(rp.repo, branch, main)) {
|
|
255
|
+
rec.status = 'skipped';
|
|
256
|
+
rec.reason = 'already-merged';
|
|
257
|
+
prechecks.push(rec);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const status = run('git status --porcelain', rp.work);
|
|
261
|
+
if (!status.ok || status.out) {
|
|
262
|
+
rec.status = 'failed';
|
|
263
|
+
rec.reason = 'dirty-worktree';
|
|
264
|
+
prechecks.push(rec);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const current = run('git branch --show-current', rp.work);
|
|
268
|
+
if (current.out && current.out !== main && !allowCheckout) {
|
|
269
|
+
rec.status = 'failed';
|
|
270
|
+
rec.reason = 'not-on-main';
|
|
271
|
+
prechecks.push(rec);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const ahead = run('git rev-list --count "origin/' + main + '..' + main + '"', rp.repo);
|
|
275
|
+
if (ahead.ok && ahead.out && parseInt(ahead.out, 10) > 0) {
|
|
276
|
+
rec.status = 'failed';
|
|
277
|
+
rec.reason = 'main-ahead-of-origin';
|
|
278
|
+
prechecks.push(rec);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
// 指定分支路径:已合并则跳过;当前检出即目标且干净 → 就地;否则临时 worktree
|
|
283
|
+
if (mergedToTarget(rp.repo, branch, target)) {
|
|
284
|
+
rec.status = 'skipped';
|
|
285
|
+
rec.reason = 'already-merged';
|
|
286
|
+
prechecks.push(rec);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const current = run('git branch --show-current', rp.work);
|
|
290
|
+
if (current.out === target) {
|
|
291
|
+
const status = run('git status --porcelain', rp.work);
|
|
292
|
+
if (!status.ok || status.out) {
|
|
293
|
+
rec.status = 'failed';
|
|
294
|
+
rec.reason = 'dirty-worktree';
|
|
295
|
+
prechecks.push(rec);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
rec.temp = true; // 执行阶段创建临时 worktree
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
prechecks.push(rec);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const hasPrecheckFailure = prechecks.some(p => p.status === 'failed');
|
|
306
|
+
prechecks.forEach(p => {
|
|
307
|
+
if (p.status === 'failed') console.log('FAIL ' + p.name + ': ' + p.reason);
|
|
308
|
+
else if (p.status === 'skipped') console.log('SKIP ' + p.name + ': ' + p.reason);
|
|
309
|
+
else console.log('OK ' + p.name + ' → 合并至 ' + p.target + (p.temp ? '(临时 worktree)' : ''));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (hasPrecheckFailure) {
|
|
313
|
+
console.log('---');
|
|
314
|
+
console.log('MERGE_RESULT', JSON.stringify({ phase: 'precheck', results: prechecks }));
|
|
315
|
+
process.exit(2);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (dryRun) {
|
|
319
|
+
console.log('---');
|
|
320
|
+
console.log('MERGE_RESULT', JSON.stringify({ phase: 'precheck', dry_run: true, results: prechecks }));
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── 阶段 2:执行(停止式,不回滚) ────────────────────────
|
|
325
|
+
const results = prechecks.map(p => ({ ...p, status: p.status, detail: null }));
|
|
326
|
+
let stopped = false;
|
|
327
|
+
|
|
328
|
+
for (let i = 0; i < prechecks.length; i += 1) {
|
|
329
|
+
const p = prechecks[i];
|
|
330
|
+
const rp = repos[i];
|
|
331
|
+
if (p.status !== 'ok') continue;
|
|
332
|
+
if (stopped) {
|
|
333
|
+
results[i].status = 'not-attempted';
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const target = p.target;
|
|
338
|
+
const mergeMsg = "Merge branch '" + branch + "'";
|
|
339
|
+
|
|
340
|
+
if (target === p.main) {
|
|
341
|
+
// ── 主干路径(快路径) ──
|
|
342
|
+
const current = run('git branch --show-current', rp.work);
|
|
343
|
+
if (current.out !== target) {
|
|
344
|
+
const co = run('git checkout --quiet "' + target + '"', rp.work);
|
|
345
|
+
if (!co.ok) {
|
|
346
|
+
results[i].status = 'failed';
|
|
347
|
+
results[i].detail = 'checkout-failed: ' + co.err.split(/\r?\n/)[0];
|
|
348
|
+
stopped = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const pull = run('git pull --ff-only --quiet origin "' + target + '"', rp.repo);
|
|
353
|
+
if (!pull.ok) {
|
|
354
|
+
results[i].status = 'failed';
|
|
355
|
+
results[i].detail = 'pull-failed: ' + pull.err.split(/\r?\n/)[0];
|
|
356
|
+
stopped = true;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const merge = run('git merge --no-ff -m "' + mergeMsg + '" "' + branch + '"', rp.repo);
|
|
360
|
+
if (!merge.ok) {
|
|
361
|
+
run('git merge --abort', rp.repo);
|
|
362
|
+
results[i].status = 'failed';
|
|
363
|
+
results[i].detail = 'merge-failed: ' + merge.err.split(/\r?\n/)[0];
|
|
364
|
+
stopped = true;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
let push = run('git push origin "' + target + '"', rp.repo);
|
|
368
|
+
if (!push.ok) {
|
|
369
|
+
const code = classifyPushError(push.err);
|
|
370
|
+
if (code === 'non-fast-forward') {
|
|
371
|
+
const retryPull = run('git pull --ff-only --quiet origin "' + target + '"', rp.repo);
|
|
372
|
+
push = retryPull.ok ? run('git push origin "' + target + '"', rp.repo) : retryPull;
|
|
373
|
+
}
|
|
374
|
+
if (!push.ok) {
|
|
375
|
+
const code2 = classifyPushError(push.err);
|
|
376
|
+
if (code2 === 'branch-protected') {
|
|
377
|
+
results[i].status = 'need-mr';
|
|
378
|
+
results[i].detail = 'branch-protected(目标分支受保护,请改用 MR 出口)';
|
|
379
|
+
} else {
|
|
380
|
+
results[i].status = 'failed';
|
|
381
|
+
results[i].detail = 'push-failed: ' + (push.err || '').split(/\r?\n/)[0];
|
|
382
|
+
}
|
|
383
|
+
stopped = true;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
results[i].status = 'merged';
|
|
388
|
+
results[i].detail = 'merged+pushed origin/' + target;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── 指定分支路径(当前检出即目标 → 就地;否则临时 worktree) ──
|
|
393
|
+
let workDir = rp.work;
|
|
394
|
+
let tmpDir = null;
|
|
395
|
+
const current = run('git branch --show-current', rp.work);
|
|
396
|
+
if (current.out !== target) {
|
|
397
|
+
tmpDir = path.join(os.tmpdir(), 'cbb-merge-' + process.pid + '-' + i);
|
|
398
|
+
const base = hasRef(rp.repo, 'refs/remotes/origin/' + target) ? 'origin/' + target : target;
|
|
399
|
+
const add = run('git worktree add --detach "' + tmpDir + '" "' + base + '"', rp.repo);
|
|
400
|
+
if (!add.ok) {
|
|
401
|
+
results[i].status = 'failed';
|
|
402
|
+
results[i].detail = 'worktree-add-failed: ' + add.err.split(/\r?\n/)[0];
|
|
403
|
+
stopped = true;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
workDir = tmpDir;
|
|
407
|
+
} else {
|
|
408
|
+
const pull = run('git pull --ff-only --quiet origin "' + target + '"', rp.repo);
|
|
409
|
+
if (!pull.ok) {
|
|
410
|
+
results[i].status = 'failed';
|
|
411
|
+
results[i].detail = 'pull-failed: ' + pull.err.split(/\r?\n/)[0];
|
|
412
|
+
stopped = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const merge = run('git merge --no-ff -m "' + mergeMsg + '" "' + branch + '"', workDir);
|
|
418
|
+
if (!merge.ok) {
|
|
419
|
+
run('git merge --abort', workDir);
|
|
420
|
+
cleanupTempWorktree(rp.repo, tmpDir);
|
|
421
|
+
results[i].status = 'failed';
|
|
422
|
+
results[i].detail = 'merge-failed: ' + merge.err.split(/\r?\n/)[0];
|
|
423
|
+
stopped = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let push = run('git push origin "HEAD:refs/heads/' + target + '"', workDir);
|
|
428
|
+
if (!push.ok) {
|
|
429
|
+
const code = classifyPushError(push.err);
|
|
430
|
+
if (code === 'non-fast-forward') {
|
|
431
|
+
// 远端已前进:在合并工作区 ff 同步后重试一次
|
|
432
|
+
const retry = run('git fetch --quiet origin "' + target + '" && git reset --hard FETCH_HEAD', workDir);
|
|
433
|
+
if (retry.ok) {
|
|
434
|
+
const rem = run('git merge --no-ff -m "' + mergeMsg + '" "' + branch + '"', workDir);
|
|
435
|
+
if (rem.ok) push = run('git push origin "HEAD:refs/heads/' + target + '"', workDir);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!push.ok) {
|
|
439
|
+
const code2 = classifyPushError(push.err);
|
|
440
|
+
cleanupTempWorktree(rp.repo, tmpDir);
|
|
441
|
+
if (code2 === 'branch-protected') {
|
|
442
|
+
results[i].status = 'need-mr';
|
|
443
|
+
results[i].detail = 'branch-protected(目标分支受保护,请改用 MR 出口)';
|
|
444
|
+
} else {
|
|
445
|
+
results[i].status = 'failed';
|
|
446
|
+
results[i].detail = 'push-failed: ' + (push.err || '').split(/\r?\n/)[0];
|
|
447
|
+
}
|
|
448
|
+
stopped = true;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
cleanupTempWorktree(rp.repo, tmpDir);
|
|
453
|
+
results[i].status = 'merged';
|
|
454
|
+
results[i].detail = 'merged+pushed origin/' + target + (tmpDir ? '(临时 worktree)' : '');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
results.forEach(r => {
|
|
458
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : ''));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const ok = results.every(r => r.status === 'merged' || r.status === 'skipped');
|
|
462
|
+
console.log('---');
|
|
463
|
+
console.log('MERGE_RESULT', JSON.stringify({ phase: 'execute', target: targetArg || null, results }));
|
|
464
|
+
process.exit(ok ? 0 : 2);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (require.main === module) main();
|
|
468
|
+
|
|
469
|
+
module.exports = { run, detectMain, mergedTo, mergedToMain, mergedToTarget, classifyPushError, loadApps, buildRepos };
|
|
@@ -7,6 +7,8 @@ require('./silence-popup');
|
|
|
7
7
|
* force: "true" 启用 --force,"false" 不启用
|
|
8
8
|
*
|
|
9
9
|
* 行为:
|
|
10
|
+
* 0. 物理删除前先显式拆除 AI 配置 junction(源文件零接触,防 Windows
|
|
11
|
+
* 删除例程遗留/穿透——实测 git worktree remove 遇 junction 会静默残留)
|
|
10
12
|
* 1. 先移除各应用 worktree
|
|
11
13
|
* 2. 再移除 workspace worktree
|
|
12
14
|
* 3. 三层降级:git worktree remove → prune+rm → 仅 prune
|
|
@@ -32,6 +34,8 @@ if (!wsRoot || !branch || !forceStr) {
|
|
|
32
34
|
|
|
33
35
|
const force = forceStr === 'true';
|
|
34
36
|
const wtDir = path.join(wsRoot, '.worktrees', 'worktree-' + branch);
|
|
37
|
+
// AI 配置 junction 拆除脚本(close 自包含副本;源真 _shared/scripts/junction-ai-config.js)
|
|
38
|
+
const junctionScript = path.join(__dirname, 'junction-ai-config.js');
|
|
35
39
|
|
|
36
40
|
function run(cmd, cwd) {
|
|
37
41
|
try {
|
|
@@ -42,11 +46,14 @@ function run(cmd, cwd) {
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function removePhysicalDir(dir) {
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
// 统一 Node fs.rmSync:跨平台;对 reparse point(junction/symlink)只删链接不穿透目标。
|
|
50
|
+
// 不用 rmdir /s /q / rm -rf——两者对 junction 的行为因实现/版本而异(实测 git 会静默残留目录树)
|
|
51
|
+
try {
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
return { ok: true, out: '' };
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return { ok: false, err: e.message };
|
|
48
56
|
}
|
|
49
|
-
return run('rm -rf "' + dir + '"');
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
const lockedDirs = [];
|
|
@@ -58,14 +65,33 @@ function removeWt(repoPath, wtPath, name) {
|
|
|
58
65
|
return;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
// 第 0 层:显式拆除 AI 配置 junction(只解除链接、源文件零接触;
|
|
69
|
+
// 实测 Windows 删除例程遇 junction 会静默失败、残留整个目录树)
|
|
70
|
+
const jr = run('node "' + junctionScript + '" "' + wsRoot + '" "' + wtPath + '" --remove');
|
|
71
|
+
if (jr.ok) {
|
|
72
|
+
const m = (jr.out || '').match(/JUNCTION_REMOVE_RESULT (.+)$/m);
|
|
73
|
+
if (m) {
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(m[1]);
|
|
76
|
+
if (data.removed && data.removed.length) console.log('🔗 ' + name + ' AI 配置 junction 已拆除:' + data.removed.join(', '));
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
console.log('⚠️ ' + name + ' junction 拆除失败(继续移除):' + (jr.err || ''));
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
const forceFlag = force ? ' --force' : '';
|
|
62
84
|
// 第 1 层:git worktree remove
|
|
63
85
|
const r1 = run('git worktree remove' + forceFlag + ' "' + wtPath + '"', repoPath);
|
|
64
|
-
if (r1.ok) {
|
|
86
|
+
if (r1.ok && !fs.existsSync(wtPath)) {
|
|
65
87
|
console.log('✅ ' + name + ' worktree 已移除');
|
|
66
88
|
results.push({ name, status: 'removed' });
|
|
67
89
|
return;
|
|
68
90
|
}
|
|
91
|
+
if (r1.ok && fs.existsSync(wtPath)) {
|
|
92
|
+
// git 报成功但物理目录残留(Windows + junction/系统差异的静默失败)→ 物理兜底
|
|
93
|
+
console.log('⚠️ ' + name + ':git 元数据已移除但物理目录残留,执行物理清理');
|
|
94
|
+
}
|
|
69
95
|
|
|
70
96
|
// 第 2 层:prune + 物理目录清理(解耦)
|
|
71
97
|
run('git worktree prune', repoPath);
|
|
@@ -78,19 +104,28 @@ function removeWt(repoPath, wtPath, name) {
|
|
|
78
104
|
|
|
79
105
|
// 第 3 层:目录被锁
|
|
80
106
|
lockedDirs.push(wtPath);
|
|
81
|
-
console.log('⏳ ' + name + '
|
|
107
|
+
console.log('⏳ ' + name + ' 元数据已清理,物理目录被锁定(init 不会自动删除目录,需手动清理)');
|
|
82
108
|
results.push({ name, status: 'locked' });
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
// 1. 应用 worktrees
|
|
86
|
-
const configPath = path.join(wsRoot, '
|
|
112
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
87
113
|
let config = {};
|
|
88
114
|
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) {}
|
|
89
115
|
|
|
90
116
|
const apps = config.apps || [];
|
|
117
|
+
/** 解析 app worktree 物理路径:新布局 .codespace/<app> 优先,兼容 ≤beta.23 旧平铺 <app> */
|
|
118
|
+
function resolveAppWt(appName) {
|
|
119
|
+
const nested = path.join(wtDir, '.codespace', appName);
|
|
120
|
+
if (fs.existsSync(path.join(nested, '.git'))) return nested;
|
|
121
|
+
const legacy = path.join(wtDir, appName);
|
|
122
|
+
if (fs.existsSync(path.join(legacy, '.git'))) return legacy;
|
|
123
|
+
return nested;
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
console.log('=== 移除应用 Worktrees ===');
|
|
92
127
|
for (const app of apps) {
|
|
93
|
-
const appWt =
|
|
128
|
+
const appWt = resolveAppWt(app.name);
|
|
94
129
|
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
95
130
|
removeWt(appPath, appWt, app.name);
|
|
96
131
|
}
|
|
@@ -110,7 +145,7 @@ if (lockedDirs.length > 0) {
|
|
|
110
145
|
console.log('');
|
|
111
146
|
console.log('⏳ 以下目录被外部进程占用(IDE 等),git 元数据已清理,物理目录仍保留:');
|
|
112
147
|
lockedDirs.forEach(d => console.log(' - ' + d));
|
|
113
|
-
console.log(' → 请关闭占用这些目录的程序(如 IDE
|
|
148
|
+
console.log(' → 请关闭占用这些目录的程序(如 IDE)后手动删除(init 不再自动清理物理目录)');
|
|
114
149
|
}
|
|
115
150
|
|
|
116
151
|
console.log('---');
|
|
@@ -38,8 +38,10 @@ function mergedTo(repoPath, from, to) {
|
|
|
38
38
|
if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
|
|
39
39
|
if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
|
|
40
40
|
if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
|
|
42
|
+
const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
|
|
43
|
+
if (!cherry.ok) return 1;
|
|
44
|
+
if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
|
|
43
45
|
return 0;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -152,19 +154,28 @@ function checkRepo(name, repoPath, wtPath) {
|
|
|
152
154
|
return { name, wt: wtPath, fetch_ok: fetchOk, dirty_files: dirtyFiles, merged, main, remote_exists: remoteExists, remote: remoteName, unpushed_count: unpushedCount };
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
const configPath = path.join(wsRoot, '
|
|
157
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
156
158
|
let config = {};
|
|
157
159
|
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) {}
|
|
158
160
|
|
|
159
161
|
const apps = config.apps || [];
|
|
160
162
|
const wtDir = path.join(wsRoot, '.worktrees', 'worktree-' + branch);
|
|
161
163
|
|
|
164
|
+
/** 解析 app worktree 物理路径:新布局 .codespace/<app> 优先,兼容 ≤beta.23 旧平铺 <app> */
|
|
165
|
+
function resolveAppWt(appName) {
|
|
166
|
+
const nested = path.join(wtDir, '.codespace', appName);
|
|
167
|
+
if (fs.existsSync(path.join(nested, '.git'))) return nested;
|
|
168
|
+
const legacy = path.join(wtDir, appName);
|
|
169
|
+
if (fs.existsSync(path.join(legacy, '.git'))) return legacy;
|
|
170
|
+
return nested;
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
const results = [];
|
|
163
174
|
|
|
164
175
|
results.push(checkRepo('workspace', wsRoot, wtDir));
|
|
165
176
|
|
|
166
177
|
for (const app of apps) {
|
|
167
|
-
const appWt =
|
|
178
|
+
const appWt = resolveAppWt(app.name);
|
|
168
179
|
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
169
180
|
results.push(checkRepo(app.name, appPath, appWt));
|
|
170
181
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// koffi is resolved from cbb global install (no local node_modules needed).
|
|
5
5
|
//
|
|
6
6
|
// 唯一真源:cbb/worktrees/_shared/scripts/silence-popup.js
|
|
7
|
-
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init /
|
|
7
|
+
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / close / push / local-mode)。
|
|
8
8
|
// skill 内同目录引用:脚本第一行 `require('./silence-popup')`。
|
|
9
9
|
if (process.platform === 'win32') {
|
|
10
10
|
try {
|