@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/bin/cli.js +33 -22
- package/package.json +1 -1
- package/src/commands/init.js +82 -82
- package/src/commands/reset.js +10 -10
- package/src/commands/run.js +121 -115
- package/src/commands/status.js +15 -15
- package/src/utils/detect-prd.js +18 -18
- package/src/utils/git.js +17 -17
- package/src/utils/github.js +19 -19
- package/src/utils/logger.js +14 -13
- package/src/utils/prompt.js +7 -7
- package/src/utils/update-check.js +103 -0
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
|
-
//
|
|
20
|
+
// Validate branch name to prevent shell injection
|
|
21
21
|
function validateBranch(name) {
|
|
22
|
-
if (!name || typeof name !== 'string') throw new Error('
|
|
23
|
-
if (/[;&|`$(){}
|
|
24
|
-
throw new Error(
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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);
|
package/src/utils/github.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GitHub
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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(
|
|
56
|
+
throw new Error(`Failed to create PR: ${output}`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
124
|
+
if (isNaN(num) || num <= 0) throw new Error(`Invalid PR number: ${prNumber}`);
|
|
125
125
|
return num;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
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
|
|
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`;
|
package/src/utils/logger.js
CHANGED
|
@@ -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:
|
|
18
|
-
warn:
|
|
19
|
-
error: (msg) => console.log(` ${COLORS.red}
|
|
20
|
-
step:
|
|
21
|
-
dim:
|
|
22
|
-
title: (msg) => console.log(` ${COLORS.bold}${
|
|
23
|
-
blank: ()
|
|
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
|
|
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(
|
|
39
|
+
const pct = Math.round(ratio * 100);
|
|
39
40
|
console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} ${pct}% ${label}`);
|
|
40
41
|
}
|
package/src/utils/prompt.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
+
}
|