@jojonax/codex-copilot 1.0.2 → 1.1.0

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/src/utils/git.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Git 操作工具模块
2
+ * Git operations utility module
3
3
  */
4
4
 
5
5
  import { execSync } from 'child_process';
@@ -17,17 +17,17 @@ function execSafe(cmd, cwd) {
17
17
  }
18
18
  }
19
19
 
20
- // 校验分支名,防止 shell 注入
20
+ // Validate branch name to prevent shell injection
21
21
  function validateBranch(name) {
22
- if (!name || typeof name !== 'string') throw new Error('分支名不能为空');
23
- if (/[;&|`$(){}\[\]!\\<>"'\s]/.test(name)) {
24
- throw new Error(`分支名包含不安全字符: ${name}`);
22
+ if (!name || typeof name !== 'string') throw new Error('Branch name cannot be empty');
23
+ if (/[;&|`$(){}[\]!\\<>"'\s]/.test(name)) {
24
+ throw new Error(`Branch name contains unsafe characters: ${name}`);
25
25
  }
26
26
  return name;
27
27
  }
28
28
 
29
29
  /**
30
- * 检查 git 仓库是否干净
30
+ * Check if the git working tree is clean
31
31
  */
32
32
  export function isClean(cwd) {
33
33
  const result = exec('git status --porcelain', cwd);
@@ -35,25 +35,25 @@ export function isClean(cwd) {
35
35
  }
36
36
 
37
37
  /**
38
- * 获取当前分支名
38
+ * Get current branch name
39
39
  */
40
40
  export function currentBranch(cwd) {
41
41
  return exec('git branch --show-current', cwd);
42
42
  }
43
43
 
44
44
  /**
45
- * 获取 remote owner/repo
45
+ * Get remote owner/repo info
46
46
  */
47
47
  export function getRepoInfo(cwd) {
48
48
  const url = exec('git remote get-url origin', cwd);
49
- // 支持 https://github.com/owner/repo.git git@github.com:owner/repo.git
49
+ // Supports https://github.com/owner/repo.git and git@github.com:owner/repo.git
50
50
  const match = url.match(/github\.com[:/](.+?)\/(.+?)(?:\.git)?$/);
51
- if (!match) throw new Error(`无法解析 GitHub 仓库地址: ${url}`);
51
+ if (!match) throw new Error(`Cannot parse GitHub repository URL: ${url}`);
52
52
  return { owner: match[1], repo: match[2] };
53
53
  }
54
54
 
55
55
  /**
56
- * 切换到目标分支(如不存在则创建)
56
+ * Switch to target branch (create if not exists)
57
57
  */
58
58
  export function checkoutBranch(cwd, branch, baseBranch = 'main') {
59
59
  validateBranch(branch);
@@ -61,11 +61,11 @@ export function checkoutBranch(cwd, branch, baseBranch = 'main') {
61
61
  const current = currentBranch(cwd);
62
62
  if (current === branch) return;
63
63
 
64
- // 先切到 base 并拉取最新
64
+ // Switch to base and pull latest
65
65
  execSafe(`git checkout ${baseBranch}`, cwd);
66
66
  execSafe(`git pull origin ${baseBranch}`, cwd);
67
67
 
68
- // 尝试切换,不存在则创建
68
+ // Try to switch, create if not exists
69
69
  const result = execSafe(`git checkout ${branch}`, cwd);
70
70
  if (!result.ok) {
71
71
  exec(`git checkout -b ${branch}`, cwd);
@@ -73,13 +73,13 @@ export function checkoutBranch(cwd, branch, baseBranch = 'main') {
73
73
  }
74
74
 
75
75
  /**
76
- * 提交所有变更
76
+ * Commit all changes
77
77
  */
78
78
  export function commitAll(cwd, message) {
79
79
  exec('git add -A', cwd);
80
80
  const result = execSafe(`git diff --cached --quiet`, cwd);
81
81
  if (result.ok) {
82
- log.dim('没有变更需要提交');
82
+ log.dim('No changes to commit');
83
83
  return false;
84
84
  }
85
85
  exec(`git commit -m ${shellEscape(message)}`, cwd);
@@ -91,15 +91,19 @@ function shellEscape(str) {
91
91
  }
92
92
 
93
93
  /**
94
- * 推送分支
94
+ * Push branch to remote (handles new branches without upstream)
95
95
  */
96
96
  export function pushBranch(cwd, branch) {
97
97
  validateBranch(branch);
98
- exec(`git push origin ${branch} --force-with-lease`, cwd);
98
+ const result = execSafe(`git push origin ${branch} --force-with-lease`, cwd);
99
+ if (!result.ok) {
100
+ // Force-with-lease fails on new branches — fall back to regular push with upstream
101
+ exec(`git push -u origin ${branch}`, cwd);
102
+ }
99
103
  }
100
104
 
101
105
  /**
102
- * 切回主分支
106
+ * Switch back to main branch
103
107
  */
104
108
  export function checkoutMain(cwd, baseBranch = 'main') {
105
109
  validateBranch(baseBranch);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GitHub 操作模块 - 通过 gh CLI 操作 PR Review
2
+ * GitHub operations module - PR and Review management via gh CLI
3
3
  */
4
4
 
5
5
  import { execSync } from 'child_process';
@@ -9,6 +9,14 @@ function gh(cmd, cwd) {
9
9
  return execSync(`gh ${cmd}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
10
10
  }
11
11
 
12
+ function ghSafe(cmd, cwd) {
13
+ try {
14
+ return { ok: true, output: gh(cmd, cwd) };
15
+ } catch (err) {
16
+ return { ok: false, output: err.stderr || err.message };
17
+ }
18
+ }
19
+
12
20
  function ghJSON(cmd, cwd) {
13
21
  const output = gh(cmd, cwd);
14
22
  if (!output) return null;
@@ -19,12 +27,20 @@ function ghJSON(cmd, cwd) {
19
27
  }
20
28
  }
21
29
 
30
+ function ghJSONSafe(cmd, cwd) {
31
+ try {
32
+ return ghJSON(cmd, cwd);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
22
38
  function shellEscape(str) {
23
39
  return `'${str.replace(/'/g, "'\\''")}'`
24
40
  }
25
41
 
26
42
  /**
27
- * 检查 gh CLI 是否登录
43
+ * Check if gh CLI is authenticated
28
44
  */
29
45
  export function checkGhAuth() {
30
46
  try {
@@ -36,28 +52,146 @@ export function checkGhAuth() {
36
52
  }
37
53
 
38
54
  /**
39
- * 创建 PR
55
+ * Ensure the base branch exists on remote.
56
+ * If not, push it first.
57
+ */
58
+ export function ensureRemoteBranch(cwd, branch) {
59
+ try {
60
+ const result = execSync(
61
+ `git ls-remote --heads origin ${branch}`,
62
+ { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
63
+ ).trim();
64
+ if (!result) {
65
+ // Branch doesn't exist on remote, push it
66
+ log.info(`Base branch '${branch}' not found on remote, pushing...`);
67
+ execSync(`git push origin ${branch}`, {
68
+ cwd,
69
+ encoding: 'utf-8',
70
+ stdio: ['pipe', 'pipe', 'pipe'],
71
+ });
72
+ log.info(`Base branch '${branch}' pushed to remote`);
73
+ }
74
+ return true;
75
+ } catch (err) {
76
+ log.warn(`Failed to verify remote branch '${branch}': ${err.message}`);
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Check if there are commits between base and head branches
83
+ */
84
+ export function hasCommitsBetween(cwd, base, head) {
85
+ try {
86
+ const result = execSync(
87
+ `git log ${base}..${head} --oneline`,
88
+ { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
89
+ ).trim();
90
+ return result.length > 0;
91
+ } catch {
92
+ // If comparison fails (e.g., branches diverged), assume there are commits
93
+ return true;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Find an existing PR for a given head branch
99
+ * @returns {{ number: number, url: string } | null}
100
+ */
101
+ export function findExistingPR(cwd, head) {
102
+ try {
103
+ const prs = ghJSON(
104
+ `pr list --head ${shellEscape(head)} --json number,url --limit 1`,
105
+ cwd
106
+ );
107
+ if (prs && prs.length > 0) {
108
+ return { number: prs[0].number, url: prs[0].url || '' };
109
+ }
110
+ return null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Create a pull request with pre-checks and auto-recovery
40
118
  */
41
119
  export function createPR(cwd, { title, body, base = 'main', head }) {
120
+ // Pre-check: ensure base branch exists on remote
121
+ ensureRemoteBranch(cwd, base);
122
+
42
123
  const output = gh(
43
- `pr create --title ${shellEscape(title)} --body ${shellEscape(body)} --base ${base} --head ${head}`,
124
+ `pr create --title ${shellEscape(title)} --body ${shellEscape(body)} --base ${shellEscape(base)} --head ${shellEscape(head)}`,
44
125
  cwd
45
126
  );
46
- // 从输出中提取 PR URL 和编号
127
+ // Extract PR URL and number from output
47
128
  const urlMatch = output.match(/https:\/\/github\.com\/.+\/pull\/(\d+)/);
48
129
  if (urlMatch) {
49
130
  return { url: urlMatch[0], number: parseInt(urlMatch[1]) };
50
131
  }
51
- // 如果已存在
132
+ // May already exist
52
133
  const existingMatch = output.match(/already exists.+\/pull\/(\d+)/);
53
134
  if (existingMatch) {
54
135
  return { url: output, number: parseInt(existingMatch[1]) };
55
136
  }
56
- throw new Error(`创建 PR 失败: ${output}`);
137
+ throw new Error(`Failed to create PR: ${output}`);
138
+ }
139
+
140
+ /**
141
+ * Create PR with full auto-recovery: pre-checks → create → fallback to find existing
142
+ * @returns {{ number: number, url: string }}
143
+ */
144
+ export function createPRWithRecovery(cwd, { title, body, base = 'main', head }) {
145
+ // Step 1: Try to create the PR
146
+ try {
147
+ return createPR(cwd, { title, body, base, head });
148
+ } catch (err) {
149
+ log.warn(`PR creation failed: ${err.message}`);
150
+ }
151
+
152
+ // Step 2: Auto-find existing PR for this branch
153
+ log.info('Searching for existing PR...');
154
+ const existing = findExistingPR(cwd, head);
155
+ if (existing) {
156
+ log.info(`Found existing PR #${existing.number}`);
157
+ return existing;
158
+ }
159
+
160
+ // Step 3: Check if there are actually commits to create a PR for
161
+ if (!hasCommitsBetween(cwd, base, head)) {
162
+ log.warn('No commits between base and head branch');
163
+ log.info('This may be a new repo or empty branch. Attempting to push base branch and retry...');
164
+
165
+ // Ensure both branches are pushed
166
+ ensureRemoteBranch(cwd, base);
167
+ ensureRemoteBranch(cwd, head);
168
+
169
+ // Retry PR creation
170
+ try {
171
+ return createPR(cwd, { title, body, base, head });
172
+ } catch (retryErr) {
173
+ log.warn(`PR retry also failed: ${retryErr.message}`);
174
+ }
175
+
176
+ // Last resort: try to find PR again
177
+ const retryExisting = findExistingPR(cwd, head);
178
+ if (retryExisting) {
179
+ log.info(`Found existing PR #${retryExisting.number}`);
180
+ return retryExisting;
181
+ }
182
+ }
183
+
184
+ // Step 4: If all else fails, throw with actionable message
185
+ throw new Error(
186
+ `Cannot create or find PR for branch '${head}'. ` +
187
+ `Possible causes: base branch '${base}' may not exist on remote, ` +
188
+ `or there are no commits between '${base}' and '${head}'. ` +
189
+ `Try: git push origin ${base} && git push origin ${head}`
190
+ );
57
191
  }
58
192
 
59
193
  /**
60
- * 获取 PR Review 列表
194
+ * Get PR review list
61
195
  */
62
196
  export function getReviews(cwd, prNumber) {
63
197
  try {
@@ -69,7 +203,7 @@ export function getReviews(cwd, prNumber) {
69
203
  }
70
204
 
71
205
  /**
72
- * 获取 PR Review 评论
206
+ * Get PR review comments
73
207
  */
74
208
  export function getReviewComments(cwd, prNumber) {
75
209
  try {
@@ -81,7 +215,7 @@ export function getReviewComments(cwd, prNumber) {
81
215
  }
82
216
 
83
217
  /**
84
- * 获取 PR issue 评论(包含 bot 评论)
218
+ * Get PR issue comments (including bot comments)
85
219
  */
86
220
  export function getIssueComments(cwd, prNumber) {
87
221
  try {
@@ -93,14 +227,14 @@ export function getIssueComments(cwd, prNumber) {
93
227
  }
94
228
 
95
229
  /**
96
- * 检查最新 Review 状态
230
+ * Check the latest review state
97
231
  * @returns {'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'PENDING' | null}
98
232
  */
99
233
  export function getLatestReviewState(cwd, prNumber) {
100
234
  const reviews = getReviews(cwd, prNumber);
101
235
  if (!reviews || reviews.length === 0) return null;
102
236
 
103
- // 过滤掉 PENDING DISMISSED
237
+ // Filter out PENDING and DISMISSED
104
238
  const active = reviews.filter(r => r.state !== 'PENDING' && r.state !== 'DISMISSED');
105
239
  if (active.length === 0) return null;
106
240
 
@@ -108,25 +242,25 @@ export function getLatestReviewState(cwd, prNumber) {
108
242
  }
109
243
 
110
244
  /**
111
- * 合并 PR
245
+ * Merge a pull request
112
246
  */
113
247
  export function mergePR(cwd, prNumber, method = 'squash') {
114
248
  const num = validatePRNumber(prNumber);
115
249
  const validMethods = ['squash', 'merge', 'rebase'];
116
250
  if (!validMethods.includes(method)) {
117
- throw new Error(`无效的合并方式: ${method}`);
251
+ throw new Error(`Invalid merge method: ${method}`);
118
252
  }
119
253
  gh(`pr merge ${num} --${method} --delete-branch`, cwd);
120
254
  }
121
255
 
122
256
  function validatePRNumber(prNumber) {
123
257
  const num = parseInt(prNumber, 10);
124
- if (isNaN(num) || num <= 0) throw new Error(`无效的 PR 编号: ${prNumber}`);
258
+ if (isNaN(num) || num <= 0) throw new Error(`Invalid PR number: ${prNumber}`);
125
259
  return num;
126
260
  }
127
261
 
128
262
  /**
129
- * 收集所有 Review 意见为结构化文本
263
+ * Collect all review feedback as structured text
130
264
  */
131
265
  export function collectReviewFeedback(cwd, prNumber) {
132
266
  const reviews = getReviews(cwd, prNumber);
@@ -135,19 +269,19 @@ export function collectReviewFeedback(cwd, prNumber) {
135
269
 
136
270
  let feedback = '';
137
271
 
138
- // Review 总评
272
+ // Review summary
139
273
  for (const r of reviews) {
140
274
  if (r.body && r.body.trim()) {
141
275
  feedback += `### Review (${r.state})\n${r.body}\n\n`;
142
276
  }
143
277
  }
144
278
 
145
- // 行内评论
279
+ // Inline comments
146
280
  for (const c of comments) {
147
281
  feedback += `### ${c.path}:L${c.line || c.original_line}\n${c.body}\n\n`;
148
282
  }
149
283
 
150
- // Bot 评论(Gemini Code Assist 等)
284
+ // Bot comments (Gemini Code Assist, etc.)
151
285
  for (const c of issueComments) {
152
286
  if (c.user?.type === 'Bot' || c.user?.login?.includes('bot')) {
153
287
  feedback += `### Bot Review (${c.user.login})\n${c.body}\n\n`;
@@ -158,6 +292,8 @@ export function collectReviewFeedback(cwd, prNumber) {
158
292
  }
159
293
 
160
294
  export const github = {
161
- checkGhAuth, createPR, getReviews, getReviewComments,
162
- getIssueComments, getLatestReviewState, mergePR, collectReviewFeedback,
295
+ checkGhAuth, createPR, createPRWithRecovery, findExistingPR,
296
+ ensureRemoteBranch, hasCommitsBetween,
297
+ getReviews, getReviewComments, getIssueComments,
298
+ getLatestReviewState, mergePR, collectReviewFeedback,
163
299
  };
@@ -1,30 +1,29 @@
1
1
  /**
2
- * Logger utility - 统一终端输出样式
2
+ * Logger utility - Unified terminal output styles
3
3
  */
4
4
 
5
5
  const COLORS = {
6
- reset: '\x1b[0m',
7
6
  green: '\x1b[32m',
8
7
  yellow: '\x1b[33m',
9
8
  red: '\x1b[31m',
10
- blue: '\x1b[34m',
11
9
  cyan: '\x1b[36m',
12
10
  dim: '\x1b[2m',
13
11
  bold: '\x1b[1m',
12
+ reset: '\x1b[0m',
14
13
  };
15
14
 
16
15
  export const log = {
17
- info: (msg) => console.log(` ${COLORS.green}✔${COLORS.reset} ${msg}`),
18
- warn: (msg) => console.log(` ${COLORS.yellow}⚠${COLORS.reset} ${msg}`),
19
- error: (msg) => console.log(` ${COLORS.red}✖${COLORS.reset} ${msg}`),
20
- step: (msg) => console.log(` ${COLORS.blue}▶${COLORS.reset} ${msg}`),
21
- dim: (msg) => console.log(` ${COLORS.dim}${msg}${COLORS.reset}`),
22
- title: (msg) => console.log(` ${COLORS.bold}${COLORS.cyan}${msg}${COLORS.reset}`),
23
- blank: () => console.log(''),
16
+ info: (msg) => console.log(` ${COLORS.green}✔${COLORS.reset} ${msg}`),
17
+ warn: (msg) => console.log(` ${COLORS.yellow}⚠${COLORS.reset} ${msg}`),
18
+ error: (msg) => console.log(` ${COLORS.red}✗${COLORS.reset} ${msg}`),
19
+ step: (msg) => console.log(` ${COLORS.cyan}▶${COLORS.reset} ${msg}`),
20
+ dim: (msg) => console.log(` ${COLORS.dim}${msg}${COLORS.reset}`),
21
+ title: (msg) => console.log(` ${COLORS.bold}${msg}${COLORS.reset}`),
22
+ blank: () => console.log(''),
24
23
  };
25
24
 
26
25
  /**
27
- * 进度条显示
26
+ * Display a progress bar
28
27
  */
29
28
  export function progressBar(current, total, label = '') {
30
29
  const width = 30;
@@ -33,8 +32,10 @@ export function progressBar(current, total, label = '') {
33
32
  console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} 0% ${label}`);
34
33
  return;
35
34
  }
36
- const filled = Math.round((current / total) * width);
35
+ const rawRatio = current / total;
36
+ const ratio = Number.isNaN(rawRatio) ? 0 : Math.max(0, Math.min(1, rawRatio));
37
+ const filled = Math.round(ratio * width);
37
38
  const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
38
- const pct = Math.round((current / total) * 100);
39
+ const pct = Math.round(ratio * 100);
39
40
  console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} ${pct}% ${label}`);
40
41
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 交互式终端工具
2
+ * Interactive terminal utilities
3
3
  */
4
4
 
5
5
  import { createInterface } from 'readline';
@@ -7,7 +7,7 @@ import { createInterface } from 'readline';
7
7
  const rl = createInterface({ input: process.stdin, output: process.stdout });
8
8
 
9
9
  /**
10
- * 提问并等待用户输入
10
+ * Ask a question and wait for user input
11
11
  */
12
12
  export function ask(question) {
13
13
  return new Promise((resolve) => {
@@ -18,7 +18,7 @@ export function ask(question) {
18
18
  }
19
19
 
20
20
  /**
21
- * 是/否确认
21
+ * Yes/No confirmation
22
22
  */
23
23
  export async function confirm(question, defaultYes = true) {
24
24
  const hint = defaultYes ? '(Y/n)' : '(y/N)';
@@ -28,21 +28,21 @@ export async function confirm(question, defaultYes = true) {
28
28
  }
29
29
 
30
30
  /**
31
- * 从列表中选择
31
+ * Select from a list
32
32
  */
33
33
  export async function select(question, choices) {
34
34
  console.log(` ${question}`);
35
35
  for (let i = 0; i < choices.length; i++) {
36
36
  console.log(` ${i + 1}. ${choices[i].label}`);
37
37
  }
38
- const answer = await ask('请输入编号:');
38
+ const answer = await ask('Enter number:');
39
39
  const idx = parseInt(answer) - 1;
40
40
  if (idx >= 0 && idx < choices.length) return choices[idx];
41
- return choices[0]; // 默认第一个
41
+ return choices[0]; // Default to first
42
42
  }
43
43
 
44
44
  /**
45
- * 关闭 readline
45
+ * Close readline interface
46
46
  */
47
47
  export function closePrompt() {
48
48
  rl.close();