@jojonax/codex-copilot 1.0.2 → 1.0.3

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,7 +91,7 @@ function shellEscape(str) {
91
91
  }
92
92
 
93
93
  /**
94
- * 推送分支
94
+ * Push branch to remote
95
95
  */
96
96
  export function pushBranch(cwd, branch) {
97
97
  validateBranch(branch);
@@ -99,7 +99,7 @@ export function pushBranch(cwd, branch) {
99
99
  }
100
100
 
101
101
  /**
102
- * 切回主分支
102
+ * Switch back to main branch
103
103
  */
104
104
  export function checkoutMain(cwd, baseBranch = 'main') {
105
105
  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';
@@ -24,7 +24,7 @@ function shellEscape(str) {
24
24
  }
25
25
 
26
26
  /**
27
- * 检查 gh CLI 是否登录
27
+ * Check if gh CLI is authenticated
28
28
  */
29
29
  export function checkGhAuth() {
30
30
  try {
@@ -36,28 +36,28 @@ export function checkGhAuth() {
36
36
  }
37
37
 
38
38
  /**
39
- * 创建 PR
39
+ * Create a pull request
40
40
  */
41
41
  export function createPR(cwd, { title, body, base = 'main', head }) {
42
42
  const output = gh(
43
- `pr create --title ${shellEscape(title)} --body ${shellEscape(body)} --base ${base} --head ${head}`,
43
+ `pr create --title ${shellEscape(title)} --body ${shellEscape(body)} --base ${shellEscape(base)} --head ${shellEscape(head)}`,
44
44
  cwd
45
45
  );
46
- // 从输出中提取 PR URL 和编号
46
+ // Extract PR URL and number from output
47
47
  const urlMatch = output.match(/https:\/\/github\.com\/.+\/pull\/(\d+)/);
48
48
  if (urlMatch) {
49
49
  return { url: urlMatch[0], number: parseInt(urlMatch[1]) };
50
50
  }
51
- // 如果已存在
51
+ // May already exist
52
52
  const existingMatch = output.match(/already exists.+\/pull\/(\d+)/);
53
53
  if (existingMatch) {
54
54
  return { url: output, number: parseInt(existingMatch[1]) };
55
55
  }
56
- throw new Error(`创建 PR 失败: ${output}`);
56
+ throw new Error(`Failed to create PR: ${output}`);
57
57
  }
58
58
 
59
59
  /**
60
- * 获取 PR Review 列表
60
+ * Get PR review list
61
61
  */
62
62
  export function getReviews(cwd, prNumber) {
63
63
  try {
@@ -69,7 +69,7 @@ export function getReviews(cwd, prNumber) {
69
69
  }
70
70
 
71
71
  /**
72
- * 获取 PR Review 评论
72
+ * Get PR review comments
73
73
  */
74
74
  export function getReviewComments(cwd, prNumber) {
75
75
  try {
@@ -81,7 +81,7 @@ export function getReviewComments(cwd, prNumber) {
81
81
  }
82
82
 
83
83
  /**
84
- * 获取 PR issue 评论(包含 bot 评论)
84
+ * Get PR issue comments (including bot comments)
85
85
  */
86
86
  export function getIssueComments(cwd, prNumber) {
87
87
  try {
@@ -93,14 +93,14 @@ export function getIssueComments(cwd, prNumber) {
93
93
  }
94
94
 
95
95
  /**
96
- * 检查最新 Review 状态
96
+ * Check the latest review state
97
97
  * @returns {'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'PENDING' | null}
98
98
  */
99
99
  export function getLatestReviewState(cwd, prNumber) {
100
100
  const reviews = getReviews(cwd, prNumber);
101
101
  if (!reviews || reviews.length === 0) return null;
102
102
 
103
- // 过滤掉 PENDING DISMISSED
103
+ // Filter out PENDING and DISMISSED
104
104
  const active = reviews.filter(r => r.state !== 'PENDING' && r.state !== 'DISMISSED');
105
105
  if (active.length === 0) return null;
106
106
 
@@ -108,25 +108,25 @@ export function getLatestReviewState(cwd, prNumber) {
108
108
  }
109
109
 
110
110
  /**
111
- * 合并 PR
111
+ * Merge a pull request
112
112
  */
113
113
  export function mergePR(cwd, prNumber, method = 'squash') {
114
114
  const num = validatePRNumber(prNumber);
115
115
  const validMethods = ['squash', 'merge', 'rebase'];
116
116
  if (!validMethods.includes(method)) {
117
- throw new Error(`无效的合并方式: ${method}`);
117
+ throw new Error(`Invalid merge method: ${method}`);
118
118
  }
119
119
  gh(`pr merge ${num} --${method} --delete-branch`, cwd);
120
120
  }
121
121
 
122
122
  function validatePRNumber(prNumber) {
123
123
  const num = parseInt(prNumber, 10);
124
- if (isNaN(num) || num <= 0) throw new Error(`无效的 PR 编号: ${prNumber}`);
124
+ if (isNaN(num) || num <= 0) throw new Error(`Invalid PR number: ${prNumber}`);
125
125
  return num;
126
126
  }
127
127
 
128
128
  /**
129
- * 收集所有 Review 意见为结构化文本
129
+ * Collect all review feedback as structured text
130
130
  */
131
131
  export function collectReviewFeedback(cwd, prNumber) {
132
132
  const reviews = getReviews(cwd, prNumber);
@@ -135,19 +135,19 @@ export function collectReviewFeedback(cwd, prNumber) {
135
135
 
136
136
  let feedback = '';
137
137
 
138
- // Review 总评
138
+ // Review summary
139
139
  for (const r of reviews) {
140
140
  if (r.body && r.body.trim()) {
141
141
  feedback += `### Review (${r.state})\n${r.body}\n\n`;
142
142
  }
143
143
  }
144
144
 
145
- // 行内评论
145
+ // Inline comments
146
146
  for (const c of comments) {
147
147
  feedback += `### ${c.path}:L${c.line || c.original_line}\n${c.body}\n\n`;
148
148
  }
149
149
 
150
- // Bot 评论(Gemini Code Assist 等)
150
+ // Bot comments (Gemini Code Assist, etc.)
151
151
  for (const c of issueComments) {
152
152
  if (c.user?.type === 'Bot' || c.user?.login?.includes('bot')) {
153
153
  feedback += `### Bot Review (${c.user.login})\n${c.body}\n\n`;
@@ -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();
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Version update checker
3
+ *
4
+ * - Checks npm registry for latest version on startup
5
+ * - 24h cache to avoid frequent network requests
6
+ * - Prints update prompt if a newer version is available
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { resolve } from 'path';
12
+ import { homedir } from 'os';
13
+ import { log } from './logger.js';
14
+
15
+ const PACKAGE_NAME = '@jojonax/codex-copilot';
16
+ const CACHE_FILE = resolve(homedir(), '.codex-copilot-update-cache.json');
17
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
18
+
19
+ /**
20
+ * Read cached version info
21
+ */
22
+ function readCache() {
23
+ try {
24
+ const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
25
+ if (Date.now() - data.timestamp < CACHE_TTL) {
26
+ return data.latestVersion;
27
+ }
28
+ } catch {
29
+ // Cache miss or corrupt — ignore
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Write version info to cache
36
+ */
37
+ function writeCache(latestVersion) {
38
+ try {
39
+ writeFileSync(CACHE_FILE, JSON.stringify({
40
+ latestVersion,
41
+ timestamp: Date.now(),
42
+ }));
43
+ } catch {
44
+ // Permission error — ignore silently
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fetch latest version from npm registry
50
+ */
51
+ function fetchLatestVersion() {
52
+ try {
53
+ const output = execSync(`npm view ${PACKAGE_NAME} version`, {
54
+ encoding: 'utf-8',
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ timeout: 5000, // 5s timeout
57
+ }).trim();
58
+ return output || null;
59
+ } catch {
60
+ // Network error, npm not available — ignore
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Compare semantic version strings
67
+ * @returns {boolean} true if latest > current
68
+ */
69
+ function isNewer(current, latest) {
70
+ if (!current || !latest) return false;
71
+ const c = current.split('.').map(Number);
72
+ const l = latest.split('.').map(Number);
73
+ for (let i = 0; i < 3; i++) {
74
+ if ((l[i] || 0) > (c[i] || 0)) return true;
75
+ if ((l[i] || 0) < (c[i] || 0)) return false;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ /**
81
+ * Check for updates and print notification
82
+ * @param {string} currentVersion - Current installed version
83
+ */
84
+ export function checkForUpdates(currentVersion) {
85
+ // 1. Check cache first
86
+ let latest = readCache();
87
+
88
+ // 2. Cache miss → fetch from npm
89
+ if (!latest) {
90
+ latest = fetchLatestVersion();
91
+ if (latest) {
92
+ writeCache(latest);
93
+ }
94
+ }
95
+
96
+ // 3. Compare and notify
97
+ if (latest && isNewer(currentVersion, latest)) {
98
+ log.blank();
99
+ log.warn(`Update available: v${currentVersion} → v${latest}`);
100
+ log.dim(` Run the following command to update:`);
101
+ log.dim(` npm install -g ${PACKAGE_NAME}@${latest}`);
102
+ }
103
+ }