@jspg-ai/coding-bb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +104 -0
- package/cbb/dev-standards/rules/cbb-coding-rule.md +50 -0
- package/cbb/dev-standards/rules/cbb-priority.md +56 -0
- package/cbb/dev-standards/skills/architecture-specs/SKILL.md +129 -0
- package/cbb/dev-standards/skills/coding-specs/SKILL.md +376 -0
- package/cbb/dev-standards/skills/coding-specs/references/concurrency.md +53 -0
- package/cbb/dev-standards/skills/coding-specs/references/config-center.md +16 -0
- package/cbb/dev-standards/skills/coding-specs/references/distributed.md +42 -0
- package/cbb/dev-standards/skills/coding-specs/references/es-coding.md +60 -0
- package/cbb/dev-standards/skills/coding-specs/references/scheduled-task.md +25 -0
- package/cbb/dev-standards/skills/coding-specs/references/security.md +11 -0
- package/cbb/dev-standards/skills/coding-specs/references/unit-testing.md +43 -0
- package/cbb/dev-standards/skills/es-design-specs/SKILL.md +104 -0
- package/cbb/dev-standards/skills/mysql-design-specs/SKILL.md +89 -0
- package/cbb/lib/install/claude-code.js +20 -0
- package/cbb/lib/install/cleanup.js +64 -0
- package/cbb/lib/install/init.js +1875 -0
- package/cbb/lib/install/qoder.js +20 -0
- package/cbb/lib/install/workspaces.js +232 -0
- package/cbb/lib/openspec/index.js +554 -0
- package/cbb/lib/superpowers/index.js +265 -0
- package/cbb/lib/utils/check-update.js +147 -0
- package/cbb/lib/utils/checkbox.js +383 -0
- package/cbb/lib/utils/gitignore.js +69 -0
- package/cbb/lib/utils/output.js +64 -0
- package/cbb/lib/utils/settings.js +119 -0
- package/cbb/lib/utils/version.js +135 -0
- package/cbb/lib/wiki/api-client.js +358 -0
- package/cbb/lib/wiki/cli.js +427 -0
- package/cbb/lib/wiki/convert.js +464 -0
- package/cbb/lib/wiki/index.js +218 -0
- package/cbb/lib/wiki/mermaid-guard.js +103 -0
- package/cbb/lib/wiki/split.js +131 -0
- package/cbb/tools/cbb-decompile-jar/SKILL.md +220 -0
- package/cbb/tools/cbb-decompile-jar/scripts/decompile.py +863 -0
- package/cbb/tools/cbb-design-to-wiki/SKILL.md +180 -0
- package/cbb/tools/cbb-mvn-guide/SKILL.md +281 -0
- package/cbb/tools/cbb-mvn-guide/scripts/mvn_jdk_manager.py +378 -0
- package/cbb/tools/cbb-mvn-guide/scripts/mvn_pom_jdk_reader.py +197 -0
- package/cbb/tools/cbb-plantuml-authoring/SKILL.md +448 -0
- package/cbb/tools/cbb-plantuml-authoring/references/drawing-templates.md +479 -0
- package/cbb/tools/cbb-wiki-ops/SKILL.md +134 -0
- package/cbb/tools/cbb-wiki-ops/scripts/check-auth.js +44 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/_shared/scripts/push-core.js +136 -0
- package/cbb/worktrees/_shared/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/commands/close.md +64 -0
- package/cbb/worktrees/commands/extend.md +65 -0
- package/cbb/worktrees/commands/init.md +55 -0
- package/cbb/worktrees/commands/push.md +42 -0
- package/cbb/worktrees/skills/openspec-close-worktree/SKILL.md +449 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-env.js +76 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-unarchived.js +48 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/delete-branches.js +136 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/discover-apps.js +76 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/remove-worktrees.js +124 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/safety-check.js +174 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/SKILL.md +390 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-env.js +95 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-repos.js +98 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js +135 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/install-ai.js +150 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/list-available-apps.js +88 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/push-core.js +136 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/sync-repos.js +93 -0
- package/cbb/worktrees/skills/openspec-init-worktree/SKILL.md +539 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/auto-open.js +136 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env-deep.js +87 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env.js +90 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-repos.js +171 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-branches.js +103 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-worktrees.js +154 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/generate-app-options.js +93 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/install-ai.js +177 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/parse-config.js +67 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-branches.js +101 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-core.js +136 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/sync-repos.js +95 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/update-gitignore.js +134 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/SKILL.md +307 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/commit-worktrees.js +269 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-core.js +136 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-worktrees.js +159 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/silence-popup.js +23 -0
- package/config/cbb.yaml +6 -0
- package/config/config.sample.json +16 -0
- package/config/openspec/config.yaml +28 -0
- package/config/openspec/schemas/spec-driven/schema.yaml +247 -0
- package/config/openspec/schemas/spec-driven/templates/design.md +475 -0
- package/config/openspec/schemas/spec-driven/templates/proposal.md +58 -0
- package/config/openspec/schemas/spec-driven/templates/spec.md +8 -0
- package/config/openspec/schemas/spec-driven/templates/tasks.md +9 -0
- package/config/workspaces.json +15 -0
- package/openspec/.version +6 -0
- package/openspec/commands/apply.md +182 -0
- package/openspec/commands/archive.md +223 -0
- package/openspec/commands/bulk-archive.md +334 -0
- package/openspec/commands/continue.md +112 -0
- package/openspec/commands/explore.md +206 -0
- package/openspec/commands/ff.md +111 -0
- package/openspec/commands/new.md +70 -0
- package/openspec/commands/onboard.md +555 -0
- package/openspec/commands/propose.md +157 -0
- package/openspec/commands/sync.md +256 -0
- package/openspec/commands/update.md +85 -0
- package/openspec/commands/verify.md +169 -0
- package/openspec/skills/openspec-apply-change/SKILL.md +187 -0
- package/openspec/skills/openspec-archive-change/SKILL.md +181 -0
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +338 -0
- package/openspec/skills/openspec-continue-change/SKILL.md +117 -0
- package/openspec/skills/openspec-explore/SKILL.md +342 -0
- package/openspec/skills/openspec-ff-change/SKILL.md +116 -0
- package/openspec/skills/openspec-new-change/SKILL.md +76 -0
- package/openspec/skills/openspec-onboard/SKILL.md +560 -0
- package/openspec/skills/openspec-propose/SKILL.md +162 -0
- package/openspec/skills/openspec-sync-specs/SKILL.md +261 -0
- package/openspec/skills/openspec-update-change/SKILL.md +90 -0
- package/openspec/skills/openspec-verify-change/SKILL.md +174 -0
- package/package.json +46 -0
- package/superpowers/.version +6 -0
- package/superpowers/skills/brainstorming/SKILL.md +250 -0
- package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
- package/superpowers/skills/brainstorming/scripts/helper.js +167 -0
- package/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
- package/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
- package/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/superpowers/skills/brainstorming/visual-companion.md +299 -0
- package/superpowers/skills/dispatching-parallel-agents/SKILL.md +167 -0
- package/superpowers/skills/executing-plans/SKILL.md +64 -0
- package/superpowers/skills/finishing-a-development-branch/SKILL.md +225 -0
- package/superpowers/skills/receiving-code-review/SKILL.md +205 -0
- package/superpowers/skills/requesting-code-review/SKILL.md +95 -0
- package/superpowers/skills/requesting-code-review/code-reviewer.md +181 -0
- package/superpowers/skills/subagent-driven-development/SKILL.md +568 -0
- package/superpowers/skills/subagent-driven-development/implementer-prompt.md +154 -0
- package/superpowers/skills/subagent-driven-development/re-review-prompt.md +115 -0
- package/superpowers/skills/subagent-driven-development/scripts/review-package +46 -0
- package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +40 -0
- package/superpowers/skills/subagent-driven-development/scripts/task-brief +41 -0
- package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
- package/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/superpowers/skills/systematic-debugging/SKILL.md +283 -0
- package/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/superpowers/skills/systematic-debugging/find-polluter.sh +72 -0
- package/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/superpowers/skills/systematic-debugging/test-academic.md +14 -0
- package/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/superpowers/skills/test-driven-development/SKILL.md +320 -0
- package/superpowers/skills/test-driven-development/writing-good-tests.md +198 -0
- package/superpowers/skills/using-git-worktrees/SKILL.md +167 -0
- package/superpowers/skills/using-superpowers/SKILL.md +63 -0
- package/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
- package/superpowers/skills/using-superpowers/references/codex-tools.md +108 -0
- package/superpowers/skills/using-superpowers/references/gemini-tools.md +63 -0
- package/superpowers/skills/using-superpowers/references/hermes-tools.md +56 -0
- package/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
- package/superpowers/skills/verification-before-completion/SKILL.md +120 -0
- package/superpowers/skills/writing-plans/SKILL.md +171 -0
- package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/superpowers/skills/writing-skills/SKILL.md +679 -0
- package/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
- package/superpowers/skills/writing-skills/render-graphs.js +169 -0
- package/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* openspec-extend-worktree 分支创建 + 嵌套 worktree
|
|
5
|
+
*/
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const { pushSingle } = require('./push-core');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const wsRoot = process.argv[2];
|
|
12
|
+
const branch = process.argv[3];
|
|
13
|
+
const appsNames = process.argv[4];
|
|
14
|
+
|
|
15
|
+
if (!wsRoot || !branch || !appsNames) {
|
|
16
|
+
console.error('Usage: node create-branches-and-worktrees.js <ws_root> <branch> <app1,app2,...>');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadSelectedApps() {
|
|
21
|
+
const configPath = path.join(wsRoot, 'CONFIG.json');
|
|
22
|
+
if (!fs.existsSync(configPath)) return null;
|
|
23
|
+
let config;
|
|
24
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) { return null; }
|
|
25
|
+
const allApps = Array.isArray(config.apps) ? config.apps : [];
|
|
26
|
+
const namesSet = new Set(appsNames.split(',').map(s => s.trim()).filter(Boolean));
|
|
27
|
+
return allApps.filter(a => namesSet.has(a.name));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apps = loadSelectedApps();
|
|
31
|
+
if (!apps || apps.length === 0) {
|
|
32
|
+
console.error('Apps not found in CONFIG.json: ' + appsNames);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tryExec(cmd, cwd) {
|
|
37
|
+
try {
|
|
38
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectMainBranch(cwd) {
|
|
45
|
+
const remote = tryExec('git remote', cwd);
|
|
46
|
+
if (!remote.ok || !remote.out) return 'main';
|
|
47
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
48
|
+
const symbolic = tryExec('git symbolic-ref "refs/remotes/' + r + '/HEAD"', cwd);
|
|
49
|
+
if (symbolic.ok && symbolic.out) {
|
|
50
|
+
return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
51
|
+
}
|
|
52
|
+
return 'main';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const results = [];
|
|
56
|
+
|
|
57
|
+
function processApp(app) {
|
|
58
|
+
const appPath = path.join(wsRoot, '.code', app.name);
|
|
59
|
+
const nestedWt = path.join(wsRoot, '.worktrees', 'worktree-' + branch, app.name);
|
|
60
|
+
|
|
61
|
+
// 1: branch check
|
|
62
|
+
const branchCheck = tryExec('git branch --list "' + branch + '"', appPath);
|
|
63
|
+
let branchCreated = false;
|
|
64
|
+
if (branchCheck.ok && branchCheck.out) {
|
|
65
|
+
console.log('Skip ' + app.name + ': branch exists, reuse');
|
|
66
|
+
} else {
|
|
67
|
+
const remote = tryExec('git remote', appPath);
|
|
68
|
+
if (!remote.ok || !remote.out) {
|
|
69
|
+
console.error('FAIL ' + app.name + ': no remote');
|
|
70
|
+
results.push({ name: app.name, status: 'failed', step: 'branch', error: 'no remote' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
74
|
+
const main = detectMainBranch(appPath);
|
|
75
|
+
const create = tryExec('git branch "' + branch + '" "' + r + '/' + main + '"', appPath);
|
|
76
|
+
if (create.ok) {
|
|
77
|
+
console.log('OK ' + app.name + ': branch ' + branch + ' created (from ' + r + '/' + main + ')');
|
|
78
|
+
branchCreated = true;
|
|
79
|
+
} else {
|
|
80
|
+
console.error('FAIL ' + app.name + ': branch create failed');
|
|
81
|
+
results.push({ name: app.name, status: 'failed', step: 'branch', error: create.err });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2: nested worktree check
|
|
87
|
+
if (fs.existsSync(nestedWt) && fs.existsSync(path.join(nestedWt, '.git'))) {
|
|
88
|
+
console.log('Skip ' + app.name + ': nested worktree exists, reuse');
|
|
89
|
+
results.push({ name: app.name, status: 'reused' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fs.mkdirSync(path.dirname(nestedWt), { recursive: true });
|
|
94
|
+
const wtCreate = tryExec('git worktree add "' + nestedWt + '" "' + branch + '"', appPath);
|
|
95
|
+
if (wtCreate.ok) {
|
|
96
|
+
console.log('OK ' + app.name + ': nested worktree created');
|
|
97
|
+
results.push({ name: app.name, status: branchCreated ? 'created' : 'branch-reused-wt-created' });
|
|
98
|
+
} else {
|
|
99
|
+
console.error('FAIL ' + app.name + ': worktree create failed');
|
|
100
|
+
results.push({ name: app.name, status: 'failed', step: 'worktree', error: wtCreate.err });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const app of apps) {
|
|
105
|
+
processApp(app);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3: 推送本次新建的分支并建立 upstream(仅 status='created',避免对 init-worktree 已推过的复用分支重复 push)
|
|
109
|
+
const newlyCreated = results.filter(r => r.status === 'created');
|
|
110
|
+
if (newlyCreated.length > 0) {
|
|
111
|
+
console.log('=== 推送新分支建立 upstream ===');
|
|
112
|
+
for (const r of newlyCreated) {
|
|
113
|
+
const app = apps.find(a => a.name === r.name);
|
|
114
|
+
if (!app) continue;
|
|
115
|
+
const appPath = path.join(wsRoot, '.code', app.name);
|
|
116
|
+
const pushResult = pushSingle(appPath, app.name, branch);
|
|
117
|
+
if (pushResult.status === 'pushed') {
|
|
118
|
+
console.log('OK ' + app.name + ': pushed ' + pushResult.upstream);
|
|
119
|
+
} else if (pushResult.status === 'remote-exists') {
|
|
120
|
+
console.log('WARN ' + app.name + ': remote branch already exists (需要用户决策)');
|
|
121
|
+
} else {
|
|
122
|
+
console.error('FAIL ' + app.name + ': push failed - ' + (pushResult.message || pushResult.error));
|
|
123
|
+
}
|
|
124
|
+
// 覆盖原 result,附加 push 状态
|
|
125
|
+
const idx = results.findIndex(x => x.name === r.name);
|
|
126
|
+
if (idx >= 0) {
|
|
127
|
+
results[idx] = Object.assign({}, results[idx], { push_status: pushResult.status, push_error: pushResult.error, push_upstream: pushResult.upstream });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const hasFailed = results.some(r => r.status === 'failed' || r.push_status === 'failed' || r.push_status === 'remote-exists');
|
|
133
|
+
console.log('---');
|
|
134
|
+
console.log('BRANCH_WT_RESULT', JSON.stringify(results));
|
|
135
|
+
process.exit(hasFailed ? 2 : 0);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require('./silence-popup');
|
|
2
|
+
/**
|
|
3
|
+
* openspec-shared 目标需求名识别
|
|
4
|
+
*
|
|
5
|
+
* 唯一真源:cbb/worktrees/_shared/scripts/find-target-worktree.js
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(extend / close / push)。
|
|
7
|
+
*
|
|
8
|
+
* 用法:node find-target-worktree.js <ws_root>
|
|
9
|
+
*
|
|
10
|
+
* 优先级:
|
|
11
|
+
* 1. cwd 在 .worktrees/worktree-xxx/ 内 → 自动从路径反推
|
|
12
|
+
* 2. .worktrees/ 下只有一个 worktree → 自动选用
|
|
13
|
+
* 3. 多个 worktree → 按 mtime 倒序,第一个为推荐
|
|
14
|
+
*
|
|
15
|
+
* 输出:
|
|
16
|
+
* 末行输出需求名(用于 AI agent 解析)
|
|
17
|
+
* 多个时按 [1] xxx ⭐ / [2] yyy 列出
|
|
18
|
+
*
|
|
19
|
+
* 退出码:
|
|
20
|
+
* 0 - 唯一识别(自动或唯一)
|
|
21
|
+
* 1 - 多个 worktree,需用户输入
|
|
22
|
+
* 2 - 无 worktree
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const wsRoot = process.argv[2];
|
|
30
|
+
|
|
31
|
+
if (!wsRoot) {
|
|
32
|
+
console.error('❌ 用法:node find-target-worktree.js <ws_root>');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 优先级 1:cwd 反推
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
const cwdMatch = cwd.match(/\.worktrees[\\\/]worktree-([^\\\/]+)/);
|
|
39
|
+
if (cwdMatch) {
|
|
40
|
+
const branch = cwdMatch[1];
|
|
41
|
+
console.log('✅ 从 cwd 反推需求名:' + branch);
|
|
42
|
+
console.log('---');
|
|
43
|
+
console.log(branch);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const worktreesDir = path.join(wsRoot, '.worktrees');
|
|
48
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
49
|
+
console.error('❌ .worktrees 目录不存在:' + worktreesDir);
|
|
50
|
+
console.error(' 请先执行 openspec-init-worktree 创建 worktree');
|
|
51
|
+
console.log('---');
|
|
52
|
+
console.log('NO_WORKTREE');
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 列出所有 worktree-xxx 目录
|
|
57
|
+
const entries = fs.readdirSync(worktreesDir, { withFileTypes: true })
|
|
58
|
+
.filter(e => e.isDirectory() && e.name.startsWith('worktree-'))
|
|
59
|
+
.map(e => ({
|
|
60
|
+
name: e.name.substring('worktree-'.length),
|
|
61
|
+
fullName: e.name,
|
|
62
|
+
path: path.join(worktreesDir, e.name),
|
|
63
|
+
mtime: fs.statSync(path.join(worktreesDir, e.name)).mtime.getTime(),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
console.error('❌ 未找到任何 worktree');
|
|
68
|
+
console.error(' 请先执行 openspec-init-worktree 创建 worktree');
|
|
69
|
+
console.log('---');
|
|
70
|
+
console.log('NO_WORKTREE');
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 优先级 2:唯一
|
|
75
|
+
if (entries.length === 1) {
|
|
76
|
+
console.log('✅ 自动选用唯一 worktree:' + entries[0].name);
|
|
77
|
+
console.log('---');
|
|
78
|
+
console.log(entries[0].name);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 优先级 3:多个 → 按 mtime 倒序
|
|
83
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
84
|
+
|
|
85
|
+
console.log('⚠️ 检测到 ' + entries.length + ' 个 worktree(按最近修改排序):');
|
|
86
|
+
entries.forEach((e, i) => {
|
|
87
|
+
if (i === 0) {
|
|
88
|
+
console.log(' [' + (i + 1) + '] ' + e.name + ' ⭐ (推荐:最近修改)');
|
|
89
|
+
} else {
|
|
90
|
+
console.log(' [' + (i + 1) + '] ' + e.name);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
console.log('请在对话中输入序号(1/2/3)或完整分支名:');
|
|
94
|
+
console.log('---');
|
|
95
|
+
console.log('MULTIPLE_WORKTREEES');
|
|
96
|
+
process.exit(1);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require('./silence-popup');
|
|
2
|
+
/**
|
|
3
|
+
* openspec-shared 工作空间根目录定位
|
|
4
|
+
*
|
|
5
|
+
* 唯一真源:cbb/worktrees/_shared/scripts/find-workspace-root.js
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / extend / close / push)。
|
|
7
|
+
*
|
|
8
|
+
* 优先:git worktree list 第一行(主仓库根目录,不受 worktree 内 CONFIG.json 副本干扰)
|
|
9
|
+
* 兜底:当前目录含 CONFIG.json
|
|
10
|
+
*
|
|
11
|
+
* 退出码:
|
|
12
|
+
* 0 - 定位成功,输出 WS_ROOT 绝对路径
|
|
13
|
+
* 1 - 未找到工作空间根目录
|
|
14
|
+
*
|
|
15
|
+
* 输出格式:
|
|
16
|
+
* 第一行:人类可读报告
|
|
17
|
+
* 最后一行:纯路径(便于 AI agent 解析后用于 cd)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
function tryExec(cmd) {
|
|
25
|
+
try {
|
|
26
|
+
return { ok: true, out: execSync(cmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return { ok: false };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let wsRoot = '';
|
|
33
|
+
|
|
34
|
+
// 优先:git worktree list 第一行
|
|
35
|
+
const worktreeList = tryExec('git worktree list');
|
|
36
|
+
if (worktreeList.ok) {
|
|
37
|
+
const firstLine = worktreeList.out.split('\n')[0];
|
|
38
|
+
if (firstLine) {
|
|
39
|
+
// 格式:<path> <sha> [<branch>]
|
|
40
|
+
const candidate = firstLine.split(/\s+/)[0];
|
|
41
|
+
if (candidate && fs.existsSync(candidate)) {
|
|
42
|
+
wsRoot = candidate;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 兜底:当前目录含 CONFIG.json
|
|
48
|
+
if (!wsRoot) {
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
if (fs.existsSync(path.join(cwd, 'CONFIG.json'))) {
|
|
51
|
+
wsRoot = cwd;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 校验
|
|
56
|
+
if (!wsRoot || !fs.existsSync(path.join(wsRoot, 'CONFIG.json'))) {
|
|
57
|
+
console.error('❌ 未找到工作空间根目录');
|
|
58
|
+
console.error(' 原因:git worktree list 失败且当前目录无 CONFIG.json');
|
|
59
|
+
console.error(' 请在业务工作空间根目录(由 cbb setup 初始化、包含 CONFIG.json)下执行本技能');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('✅ 工作空间根目录:' + wsRoot);
|
|
64
|
+
console.log('---');
|
|
65
|
+
// 最后一行输出纯路径,AI agent 可直接 cd 使用
|
|
66
|
+
console.log(wsRoot);
|
|
67
|
+
process.exit(0);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* openspec-extend-worktree 确保 workspace worktree 已装 AI 配置(必要步骤)
|
|
5
|
+
*
|
|
6
|
+
* 用法:node install-ai.js <ws_root> <branch>
|
|
7
|
+
*
|
|
8
|
+
* 行为:
|
|
9
|
+
* - 仅对 workspace worktree 跑 cbb init --yes(开发约束不在 Java 应用 worktree 中)
|
|
10
|
+
* - 用 .cbb/.managed-by-cbb 判重,已 init 过的 workspace 跳过
|
|
11
|
+
* - 失败则 exit 2,阻塞 extend-worktree 流程(与 init-worktree 一致)
|
|
12
|
+
*
|
|
13
|
+
* 退出码:
|
|
14
|
+
* 0 - 成功 / 跳过
|
|
15
|
+
* 1 - 参数错误
|
|
16
|
+
* 2 - cbb init 执行失败(**阻塞 extend-worktree**,用户必须手动恢复)
|
|
17
|
+
*/
|
|
18
|
+
const { execSync } = require('child_process');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const wsRoot = process.argv[2];
|
|
23
|
+
const branch = process.argv[3];
|
|
24
|
+
|
|
25
|
+
if (!wsRoot || !branch) {
|
|
26
|
+
console.error('❌ 用法:node install-ai.js <ws_root> <branch>');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wtRoot = path.join(wsRoot, '.worktrees', 'worktree-' + branch);
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(wtRoot)) {
|
|
33
|
+
console.error('❌ workspace worktree 不存在:' + wtRoot);
|
|
34
|
+
console.error(' → 请先确保 init-worktree 已创建 workspace worktree');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(cmd, cwd, env) {
|
|
39
|
+
try {
|
|
40
|
+
return { ok: true, out: execSync(cmd, { cwd: cwd || process.cwd(), encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], env: env || process.env, input: '\n' }).trim() };
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 判重:init-worktree 已经 init 过的 workspace,跳过
|
|
47
|
+
const marker = path.join(wtRoot, '.cbb', '.managed-by-cbb');
|
|
48
|
+
if (fs.existsSync(marker)) {
|
|
49
|
+
console.log('⏭️ workspace worktree AI 配置已存在(.cbb/.managed-by-cbb),跳过');
|
|
50
|
+
console.log('---');
|
|
51
|
+
console.log('AI_INIT_OK');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 查找 cbb 可执行文件
|
|
56
|
+
function findCbb() {
|
|
57
|
+
let r = run('cbb --version');
|
|
58
|
+
if (r.ok) return { cmd: 'cbb', version: r.out };
|
|
59
|
+
|
|
60
|
+
const npmRoot = run('npm root -g');
|
|
61
|
+
if (npmRoot.ok) {
|
|
62
|
+
const candidates = [];
|
|
63
|
+
if (process.platform === 'win32') {
|
|
64
|
+
candidates.push(path.join(npmRoot.out, 'cbb.cmd'));
|
|
65
|
+
candidates.push(path.join(npmRoot.out, '..', 'cbb.cmd'));
|
|
66
|
+
candidates.push(path.join(npmRoot.out, '..', 'bin', 'cbb.cmd'));
|
|
67
|
+
const appdata = process.env.APPDATA || process.env.USERPROFILE;
|
|
68
|
+
if (appdata) candidates.push(path.join(appdata, 'npm', 'cbb.cmd'));
|
|
69
|
+
} else {
|
|
70
|
+
candidates.push(path.join(npmRoot.out, '..', 'bin', 'cbb'));
|
|
71
|
+
candidates.push(path.join(npmRoot.out, '..', 'cbb'));
|
|
72
|
+
}
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
if (fs.existsSync(c)) {
|
|
75
|
+
r = run('"' + c + '" --version');
|
|
76
|
+
if (r.ok) return { cmd: '"' + c + '"', version: r.out };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 1. 检测/安装 cbb
|
|
84
|
+
let cbbInfo = findCbb();
|
|
85
|
+
if (!cbbInfo) {
|
|
86
|
+
console.log('WARN: cbb not globally installed, try auto-install...');
|
|
87
|
+
const install = run('npm install -g @jspg-ai/coding-bb');
|
|
88
|
+
if (!install.ok) {
|
|
89
|
+
console.error('ERROR: cbb auto-install failed: ' + install.err);
|
|
90
|
+
console.error('');
|
|
91
|
+
console.error('━━━ extend-worktree 已中断 ━━━');
|
|
92
|
+
console.error('请手动安装 cbb 后重跑 extend-worktree:');
|
|
93
|
+
console.error(' 1. npm install -g @jspg-ai/coding-bb');
|
|
94
|
+
console.error(' 2. cd "' + wtRoot + '" && cbb init --yes');
|
|
95
|
+
console.error(' 3. 重跑 extend-worktree');
|
|
96
|
+
console.log('---');
|
|
97
|
+
console.log('AI_INIT_FAILED');
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
cbbInfo = findCbb();
|
|
101
|
+
if (!cbbInfo) {
|
|
102
|
+
console.error('ERROR: cbb not found after install. Likely npm PATH cache issue.');
|
|
103
|
+
console.error('');
|
|
104
|
+
console.error('━━━ extend-worktree 已中断 ━━━');
|
|
105
|
+
console.error('手动恢复:cd "' + wtRoot + '" && cbb init --yes');
|
|
106
|
+
console.error('然后重跑 extend-worktree。');
|
|
107
|
+
console.log('---');
|
|
108
|
+
console.log('AI_INIT_FAILED');
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
console.log('OK: cbb located: ' + cbbInfo.version);
|
|
112
|
+
} else {
|
|
113
|
+
console.log('OK: cbb found: ' + cbbInfo.version);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. 在 workspace worktree 中执行 cbb init --yes
|
|
117
|
+
console.log('Installing AI config in workspace worktree: ' + wtRoot);
|
|
118
|
+
const initCmd = cbbInfo.cmd + ' init --yes';
|
|
119
|
+
const init = run(initCmd, wtRoot);
|
|
120
|
+
if (init.ok) {
|
|
121
|
+
console.log('✅ workspace worktree AI 配置安装完成');
|
|
122
|
+
console.log('---');
|
|
123
|
+
console.log('AI_INIT_OK');
|
|
124
|
+
process.exit(0);
|
|
125
|
+
} else {
|
|
126
|
+
console.error('ERROR: AI config install failed: ' + init.err);
|
|
127
|
+
console.error('');
|
|
128
|
+
console.error('━━━ extend-worktree 已中断 ━━━');
|
|
129
|
+
console.error('原因:cbb init 是必要步骤。AI 配置缺失将导致后续 openspec-* ');
|
|
130
|
+
console.error(' (propose/apply/verify/ff 等)技能全部不可用。');
|
|
131
|
+
console.error('');
|
|
132
|
+
console.error('━━━ 手动恢复步骤(请按顺序执行)━━━');
|
|
133
|
+
console.error('');
|
|
134
|
+
console.error('1. 验证环境');
|
|
135
|
+
console.error(' $ cbb --version');
|
|
136
|
+
console.error(' $ node --version # 需要 >= 16.7.0');
|
|
137
|
+
console.error('');
|
|
138
|
+
console.error('2. 若 cbb 未安装,先安装');
|
|
139
|
+
console.error(' $ npm install -g @jspg-ai/coding-bb');
|
|
140
|
+
console.error('');
|
|
141
|
+
console.error('3. 进入 workspace worktree 并初始化');
|
|
142
|
+
console.error(' $ cd "' + wtRoot + '"');
|
|
143
|
+
console.error(' $ cbb init --yes');
|
|
144
|
+
console.error('');
|
|
145
|
+
console.error('4. 重新执行 extend-worktree');
|
|
146
|
+
console.error(' (脚本会用 .cbb/.managed-by-cbb 判重自动跳过已恢复部分)');
|
|
147
|
+
console.log('---');
|
|
148
|
+
console.log('AI_INIT_FAILED');
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* openspec-extend-worktree 计算"未关联"应用清单
|
|
5
|
+
*
|
|
6
|
+
* 用法:node list-available-apps.js <ws_root> <branch>
|
|
7
|
+
*
|
|
8
|
+
* 从 CONFIG.json 读取全部应用,减去目标 worktree 中已存在的应用子目录。
|
|
9
|
+
*
|
|
10
|
+
* 输出末行:LIST_RESULT { available: [...], already: [...] }
|
|
11
|
+
*
|
|
12
|
+
* 退出码:
|
|
13
|
+
* 0 - 有可用应用
|
|
14
|
+
* 1 - 参数错误
|
|
15
|
+
* 2 - 所有应用都已关联(无新增项)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const wsRoot = process.argv[2];
|
|
22
|
+
const branch = process.argv[3];
|
|
23
|
+
|
|
24
|
+
if (!wsRoot || !branch) {
|
|
25
|
+
console.error('❌ 用法:node list-available-apps.js <ws_root> <branch>');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const configPath = path.join(wsRoot, 'CONFIG.json');
|
|
30
|
+
if (!fs.existsSync(configPath)) {
|
|
31
|
+
console.error('❌ CONFIG.json 不存在:' + configPath);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let config;
|
|
36
|
+
try {
|
|
37
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('❌ CONFIG.json 解析失败:' + e.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const apps = config.apps || [];
|
|
44
|
+
const wtDir = path.join(wsRoot, '.worktrees', 'worktree-' + branch);
|
|
45
|
+
if (!fs.existsSync(wtDir)) {
|
|
46
|
+
console.error('❌ 目标 worktree 不存在:' + wtDir);
|
|
47
|
+
console.error(' 请先执行 openspec-init-worktree 创建该需求的工作空间');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const available = [];
|
|
52
|
+
const already = [];
|
|
53
|
+
|
|
54
|
+
for (const app of apps) {
|
|
55
|
+
const wtSub = path.join(wtDir, app.name);
|
|
56
|
+
if (fs.existsSync(wtSub) && fs.existsSync(path.join(wtSub, '.git'))) {
|
|
57
|
+
already.push(app);
|
|
58
|
+
} else {
|
|
59
|
+
available.push(app);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('=== 已关联应用(跳过) ===');
|
|
64
|
+
if (already.length === 0) {
|
|
65
|
+
console.log(' (无)');
|
|
66
|
+
} else {
|
|
67
|
+
already.forEach(a => {
|
|
68
|
+
console.log(' [' + (a.side || 'back') + '] ' + a.name);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('=== 可补加应用 ===');
|
|
73
|
+
if (available.length === 0) {
|
|
74
|
+
console.log(' (无)');
|
|
75
|
+
} else {
|
|
76
|
+
available.forEach(a => {
|
|
77
|
+
console.log(' [' + (a.side || 'back') + '] ' + a.name);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('---');
|
|
82
|
+
const result = {
|
|
83
|
+
available: available.map(a => a.name),
|
|
84
|
+
already: already.map(a => a.name),
|
|
85
|
+
availableDetails: available,
|
|
86
|
+
};
|
|
87
|
+
console.log('LIST_RESULT', JSON.stringify(result));
|
|
88
|
+
process.exit(available.length === 0 ? 2 : 0);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require('./silence-popup');
|
|
2
|
+
/**
|
|
3
|
+
* openspec-shared 推送核心模块
|
|
4
|
+
*
|
|
5
|
+
* 唯一真源:cbb/worktrees/_shared/scripts/push-core.js
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init-worktree / extend-worktree / push-worktrees)。
|
|
7
|
+
*
|
|
8
|
+
* 公开 API:
|
|
9
|
+
* - tryExec(cmd, cwd, execImpl) 包装 execSync,统一返回 {ok, out|err}
|
|
10
|
+
* - escapeBranch(name) 分支名 shell 转义(防命令注入)
|
|
11
|
+
* - classifyError(stderr) 将 stderr 归类为 8 种错误码之一
|
|
12
|
+
* - pushSingle(repoPath, repoName, branch) 推送单个仓库并建立 upstream
|
|
13
|
+
* - collectRemoteMetadata(repoPath, branch, remote) 采集远程 commit 元信息
|
|
14
|
+
*
|
|
15
|
+
* 错误码契约(classifyError 输出):
|
|
16
|
+
* - ssh-auth-failed Permission denied (publickey)
|
|
17
|
+
* - https-auth-failed Authentication failed
|
|
18
|
+
* - network-unreachable Could not resolve host / hung up / connection refused
|
|
19
|
+
* - branch-protected protected branch / deny updating
|
|
20
|
+
* - remote-exists already exists
|
|
21
|
+
* - no-remote (pushSingle 内部判定)
|
|
22
|
+
* - non-fast-forward non-fast-forward
|
|
23
|
+
* - unknown 兜底
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { execSync: realExecSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
const PUSH_TIMEOUT_MS = 60000;
|
|
29
|
+
|
|
30
|
+
function tryExec(cmd, cwd, execImpl) {
|
|
31
|
+
const execSync = (execImpl || realExecSync);
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: PUSH_TIMEOUT_MS }).toString().trim(),
|
|
36
|
+
};
|
|
37
|
+
} catch (e) {
|
|
38
|
+
const stderr = (e.stderr ? e.stderr.toString() : e.message || String(e)).trim();
|
|
39
|
+
return { ok: false, err: stderr };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function escapeBranch(name) {
|
|
44
|
+
return '"' + String(name).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function classifyError(stderr) {
|
|
48
|
+
const s = (stderr || '').toLowerCase();
|
|
49
|
+
if (s.includes('permission denied (publickey)')) return 'ssh-auth-failed';
|
|
50
|
+
if (s.includes('authentication failed')) return 'https-auth-failed';
|
|
51
|
+
if (s.includes('could not resolve host') || s.includes('hung up') || s.includes('connection refused')) {
|
|
52
|
+
return 'network-unreachable';
|
|
53
|
+
}
|
|
54
|
+
if (s.includes('protected branch') || s.includes('deny updating')) return 'branch-protected';
|
|
55
|
+
if (s.includes('already exists')) return 'remote-exists';
|
|
56
|
+
if (s.includes('non-fast-forward')) return 'non-fast-forward';
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 采集远程分支的元信息(hash/author/date/message + 本地 hash)
|
|
62
|
+
* 任意子命令失败时返回 null(不抛错)
|
|
63
|
+
* @param {string} repoPath
|
|
64
|
+
* @param {string} branch
|
|
65
|
+
* @param {string} remote
|
|
66
|
+
* @param {{ execSyncImpl?: Function }} [opts]
|
|
67
|
+
* @returns {{ remote: {hash, author, date, message}, local: {hash} } | null}
|
|
68
|
+
*/
|
|
69
|
+
function collectRemoteMetadata(repoPath, branch, remote, opts) {
|
|
70
|
+
const execImpl = opts && opts.execSyncImpl;
|
|
71
|
+
try {
|
|
72
|
+
const remoteHash = tryExec('git rev-parse ' + escapeBranch(remote + '/' + branch), repoPath, execImpl);
|
|
73
|
+
if (!remoteHash.ok || !remoteHash.out) return null;
|
|
74
|
+
|
|
75
|
+
const remoteLog = tryExec('git log -1 --format="%an <%ae> | %ad | %s" ' + escapeBranch(remote + '/' + branch), repoPath, execImpl);
|
|
76
|
+
const localHash = tryExec('git rev-parse ' + escapeBranch(branch), repoPath, execImpl);
|
|
77
|
+
if (!remoteLog.ok || !localHash.ok || !localHash.out) return null;
|
|
78
|
+
|
|
79
|
+
// 解析 "author <email> | date | message"
|
|
80
|
+
const parts = remoteLog.out.split(' | ');
|
|
81
|
+
const author = parts[0] || '';
|
|
82
|
+
const date = parts[1] || '';
|
|
83
|
+
const message = parts.slice(2).join(' | ') || '';
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
remote: {
|
|
87
|
+
hash: remoteHash.out,
|
|
88
|
+
author,
|
|
89
|
+
date,
|
|
90
|
+
message,
|
|
91
|
+
},
|
|
92
|
+
local: {
|
|
93
|
+
hash: localHash.out,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 推送单个仓库的分支并建立 upstream
|
|
103
|
+
* @param {string} repoPath
|
|
104
|
+
* @param {string} repoName
|
|
105
|
+
* @param {string} branch
|
|
106
|
+
* @param {{ execSyncImpl?: Function }} [opts]
|
|
107
|
+
* @returns {{ name: string, status: string, upstream?: string, error?: string, message?: string, metadata?: object }}
|
|
108
|
+
*/
|
|
109
|
+
function pushSingle(repoPath, repoName, branch, opts) {
|
|
110
|
+
const execImpl = opts && opts.execSyncImpl;
|
|
111
|
+
const remote = tryExec('git remote', repoPath, execImpl);
|
|
112
|
+
if (!remote.ok || !remote.out) {
|
|
113
|
+
return { name: repoName, status: 'failed', error: 'no-remote', message: '无远程配置' };
|
|
114
|
+
}
|
|
115
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
116
|
+
const branchEsc = escapeBranch(branch);
|
|
117
|
+
const remoteEsc = '"' + String(r).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
118
|
+
const pushResult = tryExec('git push -u ' + remoteEsc + ' ' + branchEsc, repoPath, execImpl);
|
|
119
|
+
if (pushResult.ok) {
|
|
120
|
+
return { name: repoName, status: 'pushed', upstream: r + '/' + branch };
|
|
121
|
+
}
|
|
122
|
+
const errCode = classifyError(pushResult.err);
|
|
123
|
+
if (errCode === 'remote-exists') {
|
|
124
|
+
const metadata = collectRemoteMetadata(repoPath, branch, r, opts);
|
|
125
|
+
return {
|
|
126
|
+
name: repoName,
|
|
127
|
+
status: 'remote-exists',
|
|
128
|
+
error: errCode,
|
|
129
|
+
message: '远程分支已存在,需用户决策',
|
|
130
|
+
metadata,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { name: repoName, status: 'failed', error: errCode, message: pushResult.err.split(/\r?\n/)[0] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { tryExec, escapeBranch, classifyError, pushSingle, collectRemoteMetadata };
|