@jojonax/codex-copilot 1.0.0 → 1.0.2
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 +9 -2
- package/bin/cli.js +1 -2
- package/package.json +1 -1
- package/src/commands/init.js +11 -8
- package/src/commands/run.js +55 -40
- package/src/commands/status.js +9 -2
- package/src/utils/detect-prd.js +2 -2
- package/src/utils/git.js +18 -1
- package/src/utils/github.js +29 -6
- package/src/utils/logger.js +5 -0
package/README.md
CHANGED
|
@@ -12,10 +12,10 @@ PRD → Tasks → CodeX Dev → PR → AI Review → Fix → Merge → Next Task
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# Install globally
|
|
15
|
-
npm install -g codex-copilot
|
|
15
|
+
npm install -g @jojonax/codex-copilot
|
|
16
16
|
|
|
17
17
|
# Or run directly without installing
|
|
18
|
-
npx codex-copilot
|
|
18
|
+
npx @jojonax/codex-copilot
|
|
19
19
|
|
|
20
20
|
# In your project directory:
|
|
21
21
|
codex-copilot init # Detect PRD, generate task queue
|
|
@@ -122,6 +122,13 @@ CodeX develops feature
|
|
|
122
122
|
- [ ] GitHub Action for fully server-side automation
|
|
123
123
|
- [ ] Support for monorepo / multi-package projects
|
|
124
124
|
|
|
125
|
+
## Contributing
|
|
126
|
+
|
|
127
|
+
1. Fork the repo
|
|
128
|
+
2. Create a feature branch (`git checkout -b feat/awesome`)
|
|
129
|
+
3. Commit your changes (`git commit -m 'feat: add awesome feature'`)
|
|
130
|
+
4. Push and open a PR
|
|
131
|
+
|
|
125
132
|
## License
|
|
126
133
|
|
|
127
134
|
MIT © Jonas Qin
|
package/bin/cli.js
CHANGED
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* 4. 创建 .codex-copilot/ 目录结构
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync
|
|
11
|
-
import { resolve
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
10
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
13
12
|
import { execSync } from 'child_process';
|
|
14
13
|
import { detectPRD, readPRD } from '../utils/detect-prd.js';
|
|
15
14
|
import { log } from '../utils/logger.js';
|
|
@@ -17,8 +16,6 @@ import { ask, confirm, select, closePrompt } from '../utils/prompt.js';
|
|
|
17
16
|
import { git } from '../utils/git.js';
|
|
18
17
|
import { github } from '../utils/github.js';
|
|
19
18
|
|
|
20
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
|
|
22
19
|
export async function init(projectDir) {
|
|
23
20
|
log.title('📋 初始化 Codex-Copilot');
|
|
24
21
|
log.blank();
|
|
@@ -66,6 +63,12 @@ export async function init(projectDir) {
|
|
|
66
63
|
log.warn('未自动检测到 PRD 文档');
|
|
67
64
|
const manualPath = await ask('请输入 PRD 文件路径(相对或绝对路径):');
|
|
68
65
|
prdPath = resolve(projectDir, manualPath);
|
|
66
|
+
// 防止路径穿越:确保 PRD 路径在项目目录内或是绝对路径
|
|
67
|
+
if (!prdPath.startsWith(resolve(projectDir)) && !manualPath.startsWith('/')) {
|
|
68
|
+
log.error('PRD 路径不在项目目录内');
|
|
69
|
+
closePrompt();
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
69
72
|
if (!existsSync(prdPath)) {
|
|
70
73
|
log.error(`文件不存在: ${prdPath}`);
|
|
71
74
|
closePrompt();
|
|
@@ -187,8 +190,7 @@ export async function init(projectDir) {
|
|
|
187
190
|
if (autoparse) {
|
|
188
191
|
log.step('调用 CodeX CLI 拆解 PRD...');
|
|
189
192
|
try {
|
|
190
|
-
|
|
191
|
-
execSync(`codex -q "${parsePrompt.slice(0, 2000)}"`, {
|
|
193
|
+
execSync(`codex -q --file .codex-copilot/parse-prd-prompt.md`, {
|
|
192
194
|
cwd: projectDir,
|
|
193
195
|
stdio: 'inherit',
|
|
194
196
|
});
|
|
@@ -204,7 +206,8 @@ export async function init(projectDir) {
|
|
|
204
206
|
|
|
205
207
|
function checkCodexCLI() {
|
|
206
208
|
try {
|
|
207
|
-
|
|
209
|
+
const cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
|
|
210
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
208
211
|
return true;
|
|
209
212
|
} catch {
|
|
210
213
|
return false;
|
package/src/commands/run.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
8
|
import { resolve } from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
9
10
|
import { log, progressBar } from '../utils/logger.js';
|
|
10
11
|
import { git } from '../utils/git.js';
|
|
11
12
|
import { github } from '../utils/github.js';
|
|
@@ -24,10 +25,27 @@ export async function run(projectDir) {
|
|
|
24
25
|
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
25
26
|
const configPath = resolve(projectDir, '.codex-copilot/config.json');
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
let state
|
|
28
|
+
let tasks;
|
|
29
|
+
let state;
|
|
30
|
+
try {
|
|
31
|
+
tasks = readJSON(tasksPath);
|
|
32
|
+
state = readJSON(statePath);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
log.error(`读取任务/状态文件失败: ${err.message}`);
|
|
35
|
+
log.warn('文件可能已损坏,请运行 codex-copilot reset');
|
|
36
|
+
closePrompt();
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
29
39
|
const config = existsSync(configPath) ? readJSON(configPath) : {};
|
|
30
40
|
|
|
41
|
+
// 校验 tasks.json 结构
|
|
42
|
+
if (!tasks.tasks || !Array.isArray(tasks.tasks) || !tasks.total) {
|
|
43
|
+
log.error('tasks.json 格式无效: 缺少 tasks 数组或 total 字段');
|
|
44
|
+
log.warn('请重新运行 codex-copilot init 并让 CodeX 重新生成 tasks.json');
|
|
45
|
+
closePrompt();
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
const baseBranch = config.base_branch || 'main';
|
|
32
50
|
const maxReviewRounds = config.max_review_rounds || 2;
|
|
33
51
|
const pollInterval = config.review_poll_interval || 60;
|
|
@@ -78,7 +96,7 @@ export async function run(projectDir) {
|
|
|
78
96
|
});
|
|
79
97
|
|
|
80
98
|
// ===== 阶段 4: 合并 =====
|
|
81
|
-
await mergePhase(projectDir, task, prInfo);
|
|
99
|
+
await mergePhase(projectDir, task, prInfo, baseBranch);
|
|
82
100
|
|
|
83
101
|
// 更新任务状态
|
|
84
102
|
task.status = 'completed';
|
|
@@ -113,22 +131,14 @@ async function developPhase(projectDir, task, baseBranch) {
|
|
|
113
131
|
const devPrompt = buildDevPrompt(task);
|
|
114
132
|
|
|
115
133
|
// 检查是否有 CodeX CLI
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const { execSync } = await import('child_process');
|
|
119
|
-
execSync('which codex', { stdio: 'pipe' });
|
|
120
|
-
codexAvailable = true;
|
|
121
|
-
} catch {}
|
|
134
|
+
const codexAvailable = isCodexAvailable();
|
|
122
135
|
|
|
123
136
|
if (codexAvailable) {
|
|
124
137
|
log.info('检测到 CodeX CLI,自动执行...');
|
|
125
138
|
const autoRun = await confirm('自动调用 CodeX 开发?');
|
|
126
139
|
if (autoRun) {
|
|
127
140
|
try {
|
|
128
|
-
const { execSync } = await import('child_process');
|
|
129
|
-
// 将 prompt 写入临时文件
|
|
130
141
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
131
|
-
const { writeFileSync } = await import('fs');
|
|
132
142
|
writeFileSync(promptPath, devPrompt);
|
|
133
143
|
execSync(`codex -q --file .codex-copilot/_current_prompt.md`, {
|
|
134
144
|
cwd: projectDir,
|
|
@@ -154,17 +164,12 @@ async function developPhase(projectDir, task, baseBranch) {
|
|
|
154
164
|
|
|
155
165
|
// 同时将 Prompt 保存到文件,方便复制
|
|
156
166
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
157
|
-
|
|
158
|
-
ws(promptPath, devPrompt);
|
|
167
|
+
writeFileSync(promptPath, devPrompt);
|
|
159
168
|
log.dim(`Prompt 已保存到 .codex-copilot/_current_prompt.md (可直接复制文件内容)`);
|
|
160
169
|
log.blank();
|
|
161
170
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const { execSync } = await import('child_process');
|
|
165
|
-
execSync(`echo '${devPrompt.replace(/'/g, "\\'")}' | pbcopy`, { stdio: 'pipe' });
|
|
166
|
-
log.info('📋 已复制到剪贴板!直接到 CodeX 中粘贴即可');
|
|
167
|
-
} catch {}
|
|
171
|
+
// 尝试复制到剪贴板(通过 stdin 传入,避免 shell 注入)
|
|
172
|
+
copyToClipboard(devPrompt);
|
|
168
173
|
|
|
169
174
|
await ask('CodeX 开发完成后按 Enter 继续...');
|
|
170
175
|
}
|
|
@@ -201,14 +206,20 @@ async function prPhase(projectDir, task, baseBranch) {
|
|
|
201
206
|
// PR 可能已存在
|
|
202
207
|
log.warn(`创建 PR 异常: ${err.message}`);
|
|
203
208
|
const prNumber = await ask('请输入已存在的 PR 编号:');
|
|
204
|
-
|
|
209
|
+
const parsed = parseInt(prNumber, 10);
|
|
210
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
211
|
+
log.error('无效的 PR 编号');
|
|
212
|
+
throw new Error('无效的 PR 编号');
|
|
213
|
+
}
|
|
214
|
+
return { number: parsed, url: '' };
|
|
205
215
|
}
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
// ──────────────────────────────────────────────
|
|
209
219
|
// 阶段 3: Review 循环
|
|
210
220
|
// ──────────────────────────────────────────────
|
|
211
|
-
async function reviewLoop(projectDir, task, prInfo, { maxRounds, pollInterval, waitTimeout }) {
|
|
221
|
+
async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }) {
|
|
222
|
+
let maxRounds = _maxRounds;
|
|
212
223
|
log.step('阶段 3/4: 等待 Review');
|
|
213
224
|
|
|
214
225
|
for (let round = 1; round <= maxRounds; round++) {
|
|
@@ -287,7 +298,7 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
287
298
|
const hasNewReview = currentReviews.length > startReviewCount;
|
|
288
299
|
const hasBotComment = currentComments.some(c =>
|
|
289
300
|
(c.user?.type === 'Bot' || c.user?.login?.includes('bot')) &&
|
|
290
|
-
new Date(c.created_at) >
|
|
301
|
+
new Date(c.created_at).getTime() > (Date.now() - elapsed * 1000)
|
|
291
302
|
);
|
|
292
303
|
|
|
293
304
|
if (hasNewReview || hasBotComment) {
|
|
@@ -320,22 +331,15 @@ ${feedback}
|
|
|
320
331
|
|
|
321
332
|
// 保存到文件并提示用户
|
|
322
333
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
323
|
-
const { writeFileSync } = await import('fs');
|
|
324
334
|
writeFileSync(promptPath, fixPrompt);
|
|
325
335
|
|
|
326
336
|
// 尝试 CodeX CLI
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
const { execSync } = await import('child_process');
|
|
330
|
-
execSync('which codex', { stdio: 'pipe' });
|
|
331
|
-
codexAvailable = true;
|
|
332
|
-
} catch {}
|
|
337
|
+
const codexAvailable = isCodexAvailable();
|
|
333
338
|
|
|
334
339
|
if (codexAvailable) {
|
|
335
340
|
const autoFix = await confirm('自动调用 CodeX 修复?');
|
|
336
341
|
if (autoFix) {
|
|
337
342
|
try {
|
|
338
|
-
const { execSync } = await import('child_process');
|
|
339
343
|
execSync(`codex -q --file .codex-copilot/_current_prompt.md`, {
|
|
340
344
|
cwd: projectDir,
|
|
341
345
|
stdio: 'inherit',
|
|
@@ -353,11 +357,7 @@ ${feedback}
|
|
|
353
357
|
log.dim(`Review 修复 Prompt 已保存到 .codex-copilot/_current_prompt.md`);
|
|
354
358
|
log.dim('请将文件内容粘贴到 CodeX 执行');
|
|
355
359
|
|
|
356
|
-
|
|
357
|
-
const { execSync } = await import('child_process');
|
|
358
|
-
execSync(`cat .codex-copilot/_current_prompt.md | pbcopy`, { cwd: projectDir, stdio: 'pipe' });
|
|
359
|
-
log.info('📋 已复制到剪贴板');
|
|
360
|
-
} catch {}
|
|
360
|
+
copyToClipboard(fixPrompt);
|
|
361
361
|
|
|
362
362
|
await ask('CodeX 修复完成后按 Enter 继续...');
|
|
363
363
|
}
|
|
@@ -365,7 +365,7 @@ ${feedback}
|
|
|
365
365
|
// ──────────────────────────────────────────────
|
|
366
366
|
// 阶段 4: 合并
|
|
367
367
|
// ──────────────────────────────────────────────
|
|
368
|
-
async function mergePhase(projectDir, task, prInfo) {
|
|
368
|
+
async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
369
369
|
log.step('阶段 4/4: 合并 PR');
|
|
370
370
|
|
|
371
371
|
const doMerge = await confirm(`合并 PR #${prInfo.number}?`);
|
|
@@ -384,9 +384,7 @@ async function mergePhase(projectDir, task, prInfo) {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
// 切回主分支
|
|
387
|
-
|
|
388
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
389
|
-
git.checkoutMain(projectDir, config.base_branch || 'main');
|
|
387
|
+
git.checkoutMain(projectDir, baseBranch);
|
|
390
388
|
}
|
|
391
389
|
|
|
392
390
|
function buildDevPrompt(task) {
|
|
@@ -411,3 +409,20 @@ ${task.acceptance.map(a => `- ${a}`).join('\n')}
|
|
|
411
409
|
function sleep(ms) {
|
|
412
410
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
413
411
|
}
|
|
412
|
+
|
|
413
|
+
function isCodexAvailable() {
|
|
414
|
+
try {
|
|
415
|
+
const cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
|
|
416
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
417
|
+
return true;
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function copyToClipboard(text) {
|
|
424
|
+
try {
|
|
425
|
+
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
426
|
+
log.info('📋 已复制到剪贴板');
|
|
427
|
+
} catch {}
|
|
428
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -10,8 +10,15 @@ export async function status(projectDir) {
|
|
|
10
10
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
11
11
|
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
let tasks, state;
|
|
14
|
+
try {
|
|
15
|
+
tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
16
|
+
state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
log.error(`读取文件失败: ${err.message}`);
|
|
19
|
+
log.warn('文件可能已损坏,请运行 codex-copilot reset');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
log.title(`📊 项目: ${tasks.project}`);
|
|
17
24
|
log.blank();
|
package/src/utils/detect-prd.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
7
|
-
import { resolve,
|
|
7
|
+
import { resolve, extname, relative } from 'path';
|
|
8
8
|
import { log } from './logger.js';
|
|
9
9
|
|
|
10
10
|
// PRD 文件名匹配模式(优先级由高到低)
|
|
@@ -89,7 +89,7 @@ function scanDir(dir, projectDir, candidates, depth) {
|
|
|
89
89
|
if (score > 0) {
|
|
90
90
|
candidates.push({
|
|
91
91
|
path: fullPath,
|
|
92
|
-
relativePath:
|
|
92
|
+
relativePath: relative(projectDir, fullPath),
|
|
93
93
|
name: entry.name,
|
|
94
94
|
score,
|
|
95
95
|
});
|
package/src/utils/git.js
CHANGED
|
@@ -17,6 +17,15 @@ function execSafe(cmd, cwd) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// 校验分支名,防止 shell 注入
|
|
21
|
+
function validateBranch(name) {
|
|
22
|
+
if (!name || typeof name !== 'string') throw new Error('分支名不能为空');
|
|
23
|
+
if (/[;&|`$(){}\[\]!\\<>"'\s]/.test(name)) {
|
|
24
|
+
throw new Error(`分支名包含不安全字符: ${name}`);
|
|
25
|
+
}
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* 检查 git 仓库是否干净
|
|
22
31
|
*/
|
|
@@ -47,6 +56,8 @@ export function getRepoInfo(cwd) {
|
|
|
47
56
|
* 切换到目标分支(如不存在则创建)
|
|
48
57
|
*/
|
|
49
58
|
export function checkoutBranch(cwd, branch, baseBranch = 'main') {
|
|
59
|
+
validateBranch(branch);
|
|
60
|
+
validateBranch(baseBranch);
|
|
50
61
|
const current = currentBranch(cwd);
|
|
51
62
|
if (current === branch) return;
|
|
52
63
|
|
|
@@ -71,14 +82,19 @@ export function commitAll(cwd, message) {
|
|
|
71
82
|
log.dim('没有变更需要提交');
|
|
72
83
|
return false;
|
|
73
84
|
}
|
|
74
|
-
exec(`git commit -m
|
|
85
|
+
exec(`git commit -m ${shellEscape(message)}`, cwd);
|
|
75
86
|
return true;
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function shellEscape(str) {
|
|
90
|
+
return `'${str.replace(/'/g, "'\\''")}'`
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
/**
|
|
79
94
|
* 推送分支
|
|
80
95
|
*/
|
|
81
96
|
export function pushBranch(cwd, branch) {
|
|
97
|
+
validateBranch(branch);
|
|
82
98
|
exec(`git push origin ${branch} --force-with-lease`, cwd);
|
|
83
99
|
}
|
|
84
100
|
|
|
@@ -86,6 +102,7 @@ export function pushBranch(cwd, branch) {
|
|
|
86
102
|
* 切回主分支
|
|
87
103
|
*/
|
|
88
104
|
export function checkoutMain(cwd, baseBranch = 'main') {
|
|
105
|
+
validateBranch(baseBranch);
|
|
89
106
|
execSafe(`git checkout ${baseBranch}`, cwd);
|
|
90
107
|
}
|
|
91
108
|
|
package/src/utils/github.js
CHANGED
|
@@ -11,7 +11,16 @@ function gh(cmd, cwd) {
|
|
|
11
11
|
|
|
12
12
|
function ghJSON(cmd, cwd) {
|
|
13
13
|
const output = gh(cmd, cwd);
|
|
14
|
-
|
|
14
|
+
if (!output) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(output);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shellEscape(str) {
|
|
23
|
+
return `'${str.replace(/'/g, "'\\''")}'`
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
/**
|
|
@@ -31,7 +40,7 @@ export function checkGhAuth() {
|
|
|
31
40
|
*/
|
|
32
41
|
export function createPR(cwd, { title, body, base = 'main', head }) {
|
|
33
42
|
const output = gh(
|
|
34
|
-
`pr create --title
|
|
43
|
+
`pr create --title ${shellEscape(title)} --body ${shellEscape(body)} --base ${base} --head ${head}`,
|
|
35
44
|
cwd
|
|
36
45
|
);
|
|
37
46
|
// 从输出中提取 PR URL 和编号
|
|
@@ -52,7 +61,8 @@ export function createPR(cwd, { title, body, base = 'main', head }) {
|
|
|
52
61
|
*/
|
|
53
62
|
export function getReviews(cwd, prNumber) {
|
|
54
63
|
try {
|
|
55
|
-
|
|
64
|
+
const num = validatePRNumber(prNumber);
|
|
65
|
+
return ghJSON(`api repos/{owner}/{repo}/pulls/${num}/reviews`, cwd) || [];
|
|
56
66
|
} catch {
|
|
57
67
|
return [];
|
|
58
68
|
}
|
|
@@ -63,7 +73,8 @@ export function getReviews(cwd, prNumber) {
|
|
|
63
73
|
*/
|
|
64
74
|
export function getReviewComments(cwd, prNumber) {
|
|
65
75
|
try {
|
|
66
|
-
|
|
76
|
+
const num = validatePRNumber(prNumber);
|
|
77
|
+
return ghJSON(`api repos/{owner}/{repo}/pulls/${num}/comments`, cwd) || [];
|
|
67
78
|
} catch {
|
|
68
79
|
return [];
|
|
69
80
|
}
|
|
@@ -74,7 +85,8 @@ export function getReviewComments(cwd, prNumber) {
|
|
|
74
85
|
*/
|
|
75
86
|
export function getIssueComments(cwd, prNumber) {
|
|
76
87
|
try {
|
|
77
|
-
|
|
88
|
+
const num = validatePRNumber(prNumber);
|
|
89
|
+
return ghJSON(`api repos/{owner}/{repo}/issues/${num}/comments`, cwd) || [];
|
|
78
90
|
} catch {
|
|
79
91
|
return [];
|
|
80
92
|
}
|
|
@@ -99,7 +111,18 @@ export function getLatestReviewState(cwd, prNumber) {
|
|
|
99
111
|
* 合并 PR
|
|
100
112
|
*/
|
|
101
113
|
export function mergePR(cwd, prNumber, method = 'squash') {
|
|
102
|
-
|
|
114
|
+
const num = validatePRNumber(prNumber);
|
|
115
|
+
const validMethods = ['squash', 'merge', 'rebase'];
|
|
116
|
+
if (!validMethods.includes(method)) {
|
|
117
|
+
throw new Error(`无效的合并方式: ${method}`);
|
|
118
|
+
}
|
|
119
|
+
gh(`pr merge ${num} --${method} --delete-branch`, cwd);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validatePRNumber(prNumber) {
|
|
123
|
+
const num = parseInt(prNumber, 10);
|
|
124
|
+
if (isNaN(num) || num <= 0) throw new Error(`无效的 PR 编号: ${prNumber}`);
|
|
125
|
+
return num;
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
/**
|
package/src/utils/logger.js
CHANGED
|
@@ -28,6 +28,11 @@ export const log = {
|
|
|
28
28
|
*/
|
|
29
29
|
export function progressBar(current, total, label = '') {
|
|
30
30
|
const width = 30;
|
|
31
|
+
if (total <= 0) {
|
|
32
|
+
const bar = '░'.repeat(width);
|
|
33
|
+
console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} 0% ${label}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
31
36
|
const filled = Math.round((current / total) * width);
|
|
32
37
|
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
33
38
|
const pct = Math.round((current / total) * 100);
|