@jojonax/codex-copilot 1.0.1 → 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/package.json +1 -1
- package/src/commands/init.js +10 -7
- package/src/commands/run.js +31 -10
- package/src/commands/status.js +9 -2
- package/src/utils/detect-prd.js +2 -2
- package/src/utils/git.js +13 -0
- package/src/utils/github.js +24 -5
- package/src/utils/logger.js +5 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
11
|
-
import { resolve
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
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
|
@@ -25,10 +25,27 @@ export async function run(projectDir) {
|
|
|
25
25
|
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
26
26
|
const configPath = resolve(projectDir, '.codex-copilot/config.json');
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
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
|
+
}
|
|
30
39
|
const config = existsSync(configPath) ? readJSON(configPath) : {};
|
|
31
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
|
+
|
|
32
49
|
const baseBranch = config.base_branch || 'main';
|
|
33
50
|
const maxReviewRounds = config.max_review_rounds || 2;
|
|
34
51
|
const pollInterval = config.review_poll_interval || 60;
|
|
@@ -79,7 +96,7 @@ export async function run(projectDir) {
|
|
|
79
96
|
});
|
|
80
97
|
|
|
81
98
|
// ===== 阶段 4: 合并 =====
|
|
82
|
-
await mergePhase(projectDir, task, prInfo);
|
|
99
|
+
await mergePhase(projectDir, task, prInfo, baseBranch);
|
|
83
100
|
|
|
84
101
|
// 更新任务状态
|
|
85
102
|
task.status = 'completed';
|
|
@@ -189,7 +206,12 @@ async function prPhase(projectDir, task, baseBranch) {
|
|
|
189
206
|
// PR 可能已存在
|
|
190
207
|
log.warn(`创建 PR 异常: ${err.message}`);
|
|
191
208
|
const prNumber = await ask('请输入已存在的 PR 编号:');
|
|
192
|
-
|
|
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: '' };
|
|
193
215
|
}
|
|
194
216
|
}
|
|
195
217
|
|
|
@@ -276,7 +298,7 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
276
298
|
const hasNewReview = currentReviews.length > startReviewCount;
|
|
277
299
|
const hasBotComment = currentComments.some(c =>
|
|
278
300
|
(c.user?.type === 'Bot' || c.user?.login?.includes('bot')) &&
|
|
279
|
-
new Date(c.created_at) >
|
|
301
|
+
new Date(c.created_at).getTime() > (Date.now() - elapsed * 1000)
|
|
280
302
|
);
|
|
281
303
|
|
|
282
304
|
if (hasNewReview || hasBotComment) {
|
|
@@ -343,7 +365,7 @@ ${feedback}
|
|
|
343
365
|
// ──────────────────────────────────────────────
|
|
344
366
|
// 阶段 4: 合并
|
|
345
367
|
// ──────────────────────────────────────────────
|
|
346
|
-
async function mergePhase(projectDir, task, prInfo) {
|
|
368
|
+
async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
347
369
|
log.step('阶段 4/4: 合并 PR');
|
|
348
370
|
|
|
349
371
|
const doMerge = await confirm(`合并 PR #${prInfo.number}?`);
|
|
@@ -362,9 +384,7 @@ async function mergePhase(projectDir, task, prInfo) {
|
|
|
362
384
|
}
|
|
363
385
|
|
|
364
386
|
// 切回主分支
|
|
365
|
-
|
|
366
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
367
|
-
git.checkoutMain(projectDir, config.base_branch || 'main');
|
|
387
|
+
git.checkoutMain(projectDir, baseBranch);
|
|
368
388
|
}
|
|
369
389
|
|
|
370
390
|
function buildDevPrompt(task) {
|
|
@@ -392,7 +412,8 @@ function sleep(ms) {
|
|
|
392
412
|
|
|
393
413
|
function isCodexAvailable() {
|
|
394
414
|
try {
|
|
395
|
-
|
|
415
|
+
const cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
|
|
416
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
396
417
|
return true;
|
|
397
418
|
} catch {
|
|
398
419
|
return false;
|
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, extname } from 'path';
|
|
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
|
|
|
@@ -83,6 +94,7 @@ function shellEscape(str) {
|
|
|
83
94
|
* 推送分支
|
|
84
95
|
*/
|
|
85
96
|
export function pushBranch(cwd, branch) {
|
|
97
|
+
validateBranch(branch);
|
|
86
98
|
exec(`git push origin ${branch} --force-with-lease`, cwd);
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -90,6 +102,7 @@ export function pushBranch(cwd, branch) {
|
|
|
90
102
|
* 切回主分支
|
|
91
103
|
*/
|
|
92
104
|
export function checkoutMain(cwd, baseBranch = 'main') {
|
|
105
|
+
validateBranch(baseBranch);
|
|
93
106
|
execSafe(`git checkout ${baseBranch}`, cwd);
|
|
94
107
|
}
|
|
95
108
|
|
package/src/utils/github.js
CHANGED
|
@@ -11,7 +11,12 @@ 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
|
+
}
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
function shellEscape(str) {
|
|
@@ -56,7 +61,8 @@ export function createPR(cwd, { title, body, base = 'main', head }) {
|
|
|
56
61
|
*/
|
|
57
62
|
export function getReviews(cwd, prNumber) {
|
|
58
63
|
try {
|
|
59
|
-
|
|
64
|
+
const num = validatePRNumber(prNumber);
|
|
65
|
+
return ghJSON(`api repos/{owner}/{repo}/pulls/${num}/reviews`, cwd) || [];
|
|
60
66
|
} catch {
|
|
61
67
|
return [];
|
|
62
68
|
}
|
|
@@ -67,7 +73,8 @@ export function getReviews(cwd, prNumber) {
|
|
|
67
73
|
*/
|
|
68
74
|
export function getReviewComments(cwd, prNumber) {
|
|
69
75
|
try {
|
|
70
|
-
|
|
76
|
+
const num = validatePRNumber(prNumber);
|
|
77
|
+
return ghJSON(`api repos/{owner}/{repo}/pulls/${num}/comments`, cwd) || [];
|
|
71
78
|
} catch {
|
|
72
79
|
return [];
|
|
73
80
|
}
|
|
@@ -78,7 +85,8 @@ export function getReviewComments(cwd, prNumber) {
|
|
|
78
85
|
*/
|
|
79
86
|
export function getIssueComments(cwd, prNumber) {
|
|
80
87
|
try {
|
|
81
|
-
|
|
88
|
+
const num = validatePRNumber(prNumber);
|
|
89
|
+
return ghJSON(`api repos/{owner}/{repo}/issues/${num}/comments`, cwd) || [];
|
|
82
90
|
} catch {
|
|
83
91
|
return [];
|
|
84
92
|
}
|
|
@@ -103,7 +111,18 @@ export function getLatestReviewState(cwd, prNumber) {
|
|
|
103
111
|
* 合并 PR
|
|
104
112
|
*/
|
|
105
113
|
export function mergePR(cwd, prNumber, method = 'squash') {
|
|
106
|
-
|
|
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;
|
|
107
126
|
}
|
|
108
127
|
|
|
109
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);
|