@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jojonax/codex-copilot",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "PRD-driven automated development orchestrator for CodeX / Cursor",
5
5
  "bin": {
6
6
  "codex-copilot": "./bin/cli.js"
@@ -8,8 +8,7 @@
8
8
  */
9
9
 
10
10
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
11
- import { resolve, dirname } from 'path';
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
- const { execSync } = await import('child_process');
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
- execSync('which codex', { stdio: 'pipe' });
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;
@@ -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
- const tasks = readJSON(tasksPath);
29
- let state = readJSON(statePath);
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
- return { number: parseInt(prNumber), url: '' };
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) > new Date(Date.now() - elapsed * 1000)
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
- const configPath = resolve(projectDir, '.codex-copilot/config.json');
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
- execSync('which codex', { stdio: 'pipe' });
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;
@@ -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
- const tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
14
- const state = JSON.parse(readFileSync(statePath, 'utf-8'));
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();
@@ -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: fullPath.replace(projectDir + '/', ''),
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
 
@@ -11,7 +11,12 @@ function gh(cmd, cwd) {
11
11
 
12
12
  function ghJSON(cmd, cwd) {
13
13
  const output = gh(cmd, cwd);
14
- return output ? JSON.parse(output) : null;
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
- return ghJSON(`api repos/{owner}/{repo}/pulls/${prNumber}/reviews`, cwd) || [];
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
- return ghJSON(`api repos/{owner}/{repo}/pulls/${prNumber}/comments`, cwd) || [];
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
- return ghJSON(`api repos/{owner}/{repo}/issues/${prNumber}/comments`, cwd) || [];
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
- gh(`pr merge ${prNumber} --${method} --delete-branch`, cwd);
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
  /**
@@ -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);