@jojonax/codex-copilot 1.0.2 → 1.1.0
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 +111 -103
- package/src/commands/reset.js +25 -17
- package/src/commands/run.js +311 -210
- package/src/commands/status.js +37 -20
- package/src/utils/automator.js +279 -0
- package/src/utils/checkpoint.js +129 -0
- package/src/utils/detect-prd.js +18 -18
- package/src/utils/git.js +22 -18
- package/src/utils/github.js +157 -21
- package/src/utils/logger.js +14 -13
- package/src/utils/prompt.js +7 -7
- package/src/utils/provider.js +332 -0
- package/src/utils/update-check.js +103 -0
package/src/commands/status.js
CHANGED
|
@@ -1,59 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* codex-copilot status -
|
|
2
|
+
* codex-copilot status - Show current progress
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
import { log, progressBar } from '../utils/logger.js';
|
|
8
|
+
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
8
9
|
|
|
9
10
|
export async function status(projectDir) {
|
|
10
11
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
11
|
-
const
|
|
12
|
+
const checkpoint = createCheckpoint(projectDir);
|
|
12
13
|
|
|
13
14
|
let tasks, state;
|
|
14
15
|
try {
|
|
15
16
|
tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
16
|
-
state =
|
|
17
|
+
state = checkpoint.load();
|
|
17
18
|
} catch (err) {
|
|
18
|
-
log.error(
|
|
19
|
-
log.warn('
|
|
19
|
+
log.error(`Failed to read files: ${err.message}`);
|
|
20
|
+
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
log.title(`📊
|
|
24
|
+
log.title(`📊 Project: ${tasks.project}`);
|
|
24
25
|
log.blank();
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// Progress bar
|
|
27
28
|
const completed = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
28
29
|
const inProgress = tasks.tasks.filter(t => t.status === 'in_progress' || t.status === 'developed').length;
|
|
29
30
|
const pending = tasks.tasks.filter(t => t.status === 'pending').length;
|
|
30
31
|
const skipped = tasks.tasks.filter(t => t.status === 'skipped').length;
|
|
31
32
|
|
|
32
|
-
progressBar(completed, tasks.total, `${completed}/${tasks.total}
|
|
33
|
+
progressBar(completed, tasks.total, `${completed}/${tasks.total} done`);
|
|
33
34
|
log.blank();
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
-
log.info(`✅
|
|
37
|
-
if (inProgress > 0) log.info(`🔄
|
|
38
|
-
log.info(`⏳
|
|
39
|
-
if (skipped > 0) log.warn(`⏭
|
|
36
|
+
// Stats
|
|
37
|
+
log.info(`✅ Completed: ${completed}`);
|
|
38
|
+
if (inProgress > 0) log.info(`🔄 In progress: ${inProgress}`);
|
|
39
|
+
log.info(`⏳ Pending: ${pending}`);
|
|
40
|
+
if (skipped > 0) log.warn(`⏭ Skipped: ${skipped}`);
|
|
40
41
|
log.blank();
|
|
41
42
|
|
|
42
|
-
//
|
|
43
|
-
if (state.
|
|
44
|
-
|
|
43
|
+
// Current checkpoint state
|
|
44
|
+
if (state.phase) {
|
|
45
|
+
const phaseLabels = {
|
|
46
|
+
develop: '🔨 Develop',
|
|
47
|
+
pr: '📦 PR',
|
|
48
|
+
review: '👀 Review',
|
|
49
|
+
merge: '🔀 Merge',
|
|
50
|
+
};
|
|
51
|
+
const phaseLabel = phaseLabels[state.phase] || state.phase;
|
|
52
|
+
log.info(`📍 Checkpoint: Task #${state.current_task} — ${phaseLabel} → ${state.phase_step}`);
|
|
53
|
+
if (state.branch) log.dim(` Branch: ${state.branch}`);
|
|
54
|
+
if (state.current_pr) log.dim(` PR: #${state.current_pr}`);
|
|
55
|
+
if (state.review_round > 0) log.dim(` Review round: ${state.review_round}`);
|
|
56
|
+
if (state.last_updated) log.dim(` Last saved: ${state.last_updated}`);
|
|
57
|
+
} else if (state.current_task > 0) {
|
|
58
|
+
log.info(`Last completed task: #${state.current_task}`);
|
|
45
59
|
}
|
|
46
60
|
log.blank();
|
|
47
61
|
|
|
48
|
-
//
|
|
49
|
-
log.title('
|
|
62
|
+
// Task list
|
|
63
|
+
log.title('Task list:');
|
|
50
64
|
log.blank();
|
|
51
65
|
for (const task of tasks.tasks) {
|
|
66
|
+
const isCurrentTask = state.current_task === task.id && state.phase;
|
|
52
67
|
const icon = task.status === 'completed' ? '✅' :
|
|
68
|
+
isCurrentTask ? '🔄' :
|
|
53
69
|
task.status === 'in_progress' ? '🔄' :
|
|
54
70
|
task.status === 'developed' ? '📦' :
|
|
55
71
|
task.status === 'skipped' ? '⏭ ' : '⬜';
|
|
56
|
-
|
|
72
|
+
const suffix = isCurrentTask ? ` ← ${state.phase}:${state.phase_step}` : '';
|
|
73
|
+
console.log(` ${icon} #${task.id} ${task.title} [${task.branch}]${suffix}`);
|
|
57
74
|
}
|
|
58
75
|
log.blank();
|
|
59
76
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop IDE Automator — platform-aware process detection + auto-paste
|
|
3
|
+
*
|
|
4
|
+
* Supports macOS (AppleScript/osascript) and Windows (PowerShell/SendKeys).
|
|
5
|
+
* Each IDE has its own keystroke recipe.
|
|
6
|
+
* Falls back gracefully if automation fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { log } from './logger.js';
|
|
11
|
+
|
|
12
|
+
const IS_MAC = process.platform === 'darwin';
|
|
13
|
+
const IS_WIN = process.platform === 'win32';
|
|
14
|
+
|
|
15
|
+
// ──────────────────────────────────────────────
|
|
16
|
+
// Per-IDE Automation Recipes
|
|
17
|
+
// ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Each recipe defines:
|
|
21
|
+
* processName: { mac, win } — binary/process name to detect
|
|
22
|
+
* appName: { mac, win } — display name for activation
|
|
23
|
+
* preKeys: keystroke sequence BEFORE paste (e.g., open Composer in Cursor)
|
|
24
|
+
* preDelay: ms to wait after preKeys
|
|
25
|
+
* postKeys: keystroke sequence AFTER paste (usually Enter to send)
|
|
26
|
+
* pasteDelay: ms to wait between paste and postKeys
|
|
27
|
+
*/
|
|
28
|
+
const IDE_RECIPES = {
|
|
29
|
+
'codex-desktop': {
|
|
30
|
+
processName: { mac: 'Codex', win: 'Codex' },
|
|
31
|
+
appName: { mac: 'Codex', win: 'Codex' },
|
|
32
|
+
// Codex Desktop: input is focused on activate → paste → Enter
|
|
33
|
+
preKeys: null,
|
|
34
|
+
preDelay: 0,
|
|
35
|
+
postKeys: 'return',
|
|
36
|
+
pasteDelay: 300,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
'cursor': {
|
|
40
|
+
processName: { mac: 'Cursor', win: 'Cursor' },
|
|
41
|
+
appName: { mac: 'Cursor', win: 'Cursor' },
|
|
42
|
+
// Cursor: Cmd/Ctrl+I opens Composer, then paste, then Enter
|
|
43
|
+
preKeys: IS_MAC
|
|
44
|
+
? 'keystroke "i" using {command down}'
|
|
45
|
+
: '^i',
|
|
46
|
+
preDelay: 800,
|
|
47
|
+
postKeys: 'return',
|
|
48
|
+
pasteDelay: 300,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
'antigravity': {
|
|
52
|
+
processName: { mac: 'Antigravity', win: 'Antigravity' },
|
|
53
|
+
appName: { mac: 'Antigravity', win: 'Antigravity' },
|
|
54
|
+
// Antigravity: chat input auto-focused → paste → Enter
|
|
55
|
+
preKeys: null,
|
|
56
|
+
preDelay: 0,
|
|
57
|
+
postKeys: 'return',
|
|
58
|
+
pasteDelay: 300,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
// Process Detection
|
|
64
|
+
// ──────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a process is running
|
|
68
|
+
* @param {string} processName - Name of the process
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function isProcessRunning(processName) {
|
|
72
|
+
try {
|
|
73
|
+
if (IS_MAC) {
|
|
74
|
+
const result = execSync(
|
|
75
|
+
`pgrep -x "${processName}" || pgrep -fi "${processName}"`,
|
|
76
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
77
|
+
).trim();
|
|
78
|
+
return result.length > 0;
|
|
79
|
+
} else if (IS_WIN) {
|
|
80
|
+
const result = execSync(
|
|
81
|
+
`tasklist /FI "IMAGENAME eq ${processName}.exe" /NH`,
|
|
82
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
83
|
+
).trim();
|
|
84
|
+
return !result.includes('No tasks');
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ──────────────────────────────────────────────
|
|
93
|
+
// macOS Automation (AppleScript)
|
|
94
|
+
// ──────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function buildAppleScript(recipe) {
|
|
97
|
+
const appName = recipe.appName.mac;
|
|
98
|
+
const lines = [];
|
|
99
|
+
|
|
100
|
+
// Activate the application
|
|
101
|
+
lines.push(`tell application "${appName}" to activate`);
|
|
102
|
+
lines.push('delay 0.5');
|
|
103
|
+
|
|
104
|
+
// System Events block for keystrokes
|
|
105
|
+
lines.push('tell application "System Events"');
|
|
106
|
+
|
|
107
|
+
// Pre-keys (e.g., Cmd+I for Cursor Composer)
|
|
108
|
+
if (recipe.preKeys) {
|
|
109
|
+
lines.push(` ${recipe.preKeys}`);
|
|
110
|
+
lines.push(` delay ${recipe.preDelay / 1000}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Paste: Cmd+V
|
|
114
|
+
lines.push(' keystroke "v" using {command down}');
|
|
115
|
+
lines.push(` delay ${recipe.pasteDelay / 1000}`);
|
|
116
|
+
|
|
117
|
+
// Post-keys: usually Enter to send
|
|
118
|
+
if (recipe.postKeys === 'return') {
|
|
119
|
+
lines.push(' keystroke return');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push('end tell');
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runAppleScript(script) {
|
|
128
|
+
try {
|
|
129
|
+
execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
|
|
130
|
+
encoding: 'utf-8',
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
timeout: 10000,
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
log.warn(`AppleScript failed: ${err.message}`);
|
|
137
|
+
if (err.message.includes('not allowed assistive access')) {
|
|
138
|
+
log.error('⚠ Accessibility permission required:');
|
|
139
|
+
log.error(' System Settings → Privacy & Security → Accessibility');
|
|
140
|
+
log.error(' Add your terminal app (Terminal / iTerm2 / Warp)');
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ──────────────────────────────────────────────
|
|
147
|
+
// Windows Automation (PowerShell)
|
|
148
|
+
// ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function buildPowerShell(recipe) {
|
|
151
|
+
const appName = recipe.appName.win;
|
|
152
|
+
const lines = [];
|
|
153
|
+
|
|
154
|
+
lines.push('Add-Type -AssemblyName System.Windows.Forms');
|
|
155
|
+
lines.push('Add-Type -AssemblyName Microsoft.VisualBasic');
|
|
156
|
+
|
|
157
|
+
// Activate window
|
|
158
|
+
lines.push(`[Microsoft.VisualBasic.Interaction]::AppActivate("${appName}")`);
|
|
159
|
+
lines.push('Start-Sleep -Milliseconds 500');
|
|
160
|
+
|
|
161
|
+
// Pre-keys
|
|
162
|
+
if (recipe.preKeys) {
|
|
163
|
+
lines.push(`[System.Windows.Forms.SendKeys]::SendWait("${recipe.preKeys}")`);
|
|
164
|
+
lines.push(`Start-Sleep -Milliseconds ${recipe.preDelay}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Paste: Ctrl+V
|
|
168
|
+
lines.push('[System.Windows.Forms.SendKeys]::SendWait("^v")');
|
|
169
|
+
lines.push(`Start-Sleep -Milliseconds ${recipe.pasteDelay}`);
|
|
170
|
+
|
|
171
|
+
// Post-keys: Enter
|
|
172
|
+
if (recipe.postKeys === 'return') {
|
|
173
|
+
lines.push('[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('; ');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runPowerShell(script) {
|
|
180
|
+
try {
|
|
181
|
+
execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, {
|
|
182
|
+
encoding: 'utf-8',
|
|
183
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
|
+
timeout: 10000,
|
|
185
|
+
});
|
|
186
|
+
return true;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log.warn(`PowerShell automation failed: ${err.message}`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ──────────────────────────────────────────────
|
|
194
|
+
// Public API
|
|
195
|
+
// ──────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if a specific IDE has a known automation recipe
|
|
199
|
+
*/
|
|
200
|
+
export function hasRecipe(providerId) {
|
|
201
|
+
return providerId in IDE_RECIPES;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if the IDE is currently running
|
|
206
|
+
*/
|
|
207
|
+
export function isIDERunning(providerId) {
|
|
208
|
+
const recipe = IDE_RECIPES[providerId];
|
|
209
|
+
if (!recipe) return false;
|
|
210
|
+
|
|
211
|
+
const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
|
|
212
|
+
return isProcessRunning(processName);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Attempt to auto-paste prompt into the IDE and trigger send
|
|
217
|
+
*
|
|
218
|
+
* Flow: copy to clipboard → activate window → pre-keys → paste → post-keys
|
|
219
|
+
*
|
|
220
|
+
* @param {string} providerId - IDE provider ID
|
|
221
|
+
* @param {string} text - Prompt text to paste
|
|
222
|
+
* @returns {boolean} true if automation succeeded
|
|
223
|
+
*/
|
|
224
|
+
export function activateAndPaste(providerId, text) {
|
|
225
|
+
const recipe = IDE_RECIPES[providerId];
|
|
226
|
+
if (!recipe) {
|
|
227
|
+
log.warn(`No automation recipe for '${providerId}'`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 1: Check process is running
|
|
232
|
+
const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
|
|
233
|
+
if (!isProcessRunning(processName)) {
|
|
234
|
+
log.warn(`${recipe.appName[IS_MAC ? 'mac' : 'win']} is not running`);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Step 2: Copy to clipboard (handled by caller, but ensure it's done)
|
|
239
|
+
copyToSystemClipboard(text);
|
|
240
|
+
|
|
241
|
+
// Step 3: Run platform-specific automation
|
|
242
|
+
log.info(`Auto-pasting into ${recipe.appName[IS_MAC ? 'mac' : 'win']}...`);
|
|
243
|
+
|
|
244
|
+
if (IS_MAC) {
|
|
245
|
+
const script = buildAppleScript(recipe);
|
|
246
|
+
return runAppleScript(script);
|
|
247
|
+
} else if (IS_WIN) {
|
|
248
|
+
const script = buildPowerShell(recipe);
|
|
249
|
+
return runPowerShell(script);
|
|
250
|
+
} else {
|
|
251
|
+
log.warn('Desktop automation not supported on this platform');
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Copy text to system clipboard (cross-platform)
|
|
258
|
+
*/
|
|
259
|
+
function copyToSystemClipboard(text) {
|
|
260
|
+
try {
|
|
261
|
+
if (IS_MAC) {
|
|
262
|
+
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
263
|
+
} else if (IS_WIN) {
|
|
264
|
+
execSync('clip', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
265
|
+
} else {
|
|
266
|
+
try {
|
|
267
|
+
execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
268
|
+
} catch {
|
|
269
|
+
execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Clipboard failure is non-fatal — user can copy file manually
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const automator = {
|
|
278
|
+
isProcessRunning, hasRecipe, isIDERunning, activateAndPaste,
|
|
279
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint state manager - Fine-grained state persistence
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic save/load for checkpoint state, enabling
|
|
5
|
+
* resume from any sub-step within a task after interruption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a checkpoint manager for a project
|
|
13
|
+
* @param {string} projectDir - Project root directory
|
|
14
|
+
*/
|
|
15
|
+
export function createCheckpoint(projectDir) {
|
|
16
|
+
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
17
|
+
const tempPath = resolve(projectDir, '.codex-copilot/state.json.tmp');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load current state from disk
|
|
21
|
+
*/
|
|
22
|
+
function load() {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return getDefaultState();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save state atomically (write to temp file, then rename)
|
|
32
|
+
* @param {object} update - Partial state to merge
|
|
33
|
+
*/
|
|
34
|
+
function save(update) {
|
|
35
|
+
const current = load();
|
|
36
|
+
const merged = {
|
|
37
|
+
...current,
|
|
38
|
+
...update,
|
|
39
|
+
last_updated: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
// Atomic write: temp file + rename prevents corruption on crash
|
|
42
|
+
writeFileSync(tempPath, JSON.stringify(merged, null, 2));
|
|
43
|
+
renameSync(tempPath, statePath);
|
|
44
|
+
return merged;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mark a specific phase & step as reached
|
|
49
|
+
*/
|
|
50
|
+
function saveStep(taskId, phase, phaseStep, extra = {}) {
|
|
51
|
+
return save({
|
|
52
|
+
current_task: taskId,
|
|
53
|
+
phase,
|
|
54
|
+
phase_step: phaseStep,
|
|
55
|
+
...extra,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mark current task as fully completed, clear phase info
|
|
61
|
+
*/
|
|
62
|
+
function completeTask(taskId) {
|
|
63
|
+
return save({
|
|
64
|
+
current_task: taskId,
|
|
65
|
+
phase: null,
|
|
66
|
+
phase_step: null,
|
|
67
|
+
current_pr: null,
|
|
68
|
+
review_round: 0,
|
|
69
|
+
branch: null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if a specific phase+step has been reached for a task
|
|
75
|
+
* Returns true if the saved state is at or past the given step
|
|
76
|
+
*/
|
|
77
|
+
function isStepDone(taskId, phase, phaseStep) {
|
|
78
|
+
const state = load();
|
|
79
|
+
if (state.current_task !== taskId) return false;
|
|
80
|
+
if (!state.phase) return false;
|
|
81
|
+
|
|
82
|
+
const phaseOrder = ['develop', 'pr', 'review', 'merge'];
|
|
83
|
+
const savedPhaseIdx = phaseOrder.indexOf(state.phase);
|
|
84
|
+
const targetPhaseIdx = phaseOrder.indexOf(phase);
|
|
85
|
+
|
|
86
|
+
// If saved phase is ahead, this step is done
|
|
87
|
+
if (savedPhaseIdx > targetPhaseIdx) return true;
|
|
88
|
+
// If saved phase is behind, this step is not done
|
|
89
|
+
if (savedPhaseIdx < targetPhaseIdx) return false;
|
|
90
|
+
|
|
91
|
+
// Same phase: compare steps within phase
|
|
92
|
+
const stepOrders = {
|
|
93
|
+
develop: ['branch_created', 'prompt_ready', 'codex_complete'],
|
|
94
|
+
pr: ['committed', 'pushed', 'pr_created'],
|
|
95
|
+
review: ['waiting_review', 'feedback_received', 'fix_applied'],
|
|
96
|
+
merge: ['merged'],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const steps = stepOrders[phase] || [];
|
|
100
|
+
const savedStepIdx = steps.indexOf(state.phase_step);
|
|
101
|
+
const targetStepIdx = steps.indexOf(phaseStep);
|
|
102
|
+
|
|
103
|
+
return savedStepIdx >= targetStepIdx;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Full reset to initial state
|
|
108
|
+
*/
|
|
109
|
+
function reset() {
|
|
110
|
+
const state = getDefaultState();
|
|
111
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { load, save, saveStep, completeTask, isStepDone, reset };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getDefaultState() {
|
|
119
|
+
return {
|
|
120
|
+
current_task: 0,
|
|
121
|
+
phase: null,
|
|
122
|
+
phase_step: null,
|
|
123
|
+
current_pr: null,
|
|
124
|
+
review_round: 0,
|
|
125
|
+
branch: null,
|
|
126
|
+
status: 'initialized',
|
|
127
|
+
last_updated: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/utils/detect-prd.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PRD
|
|
3
|
-
*
|
|
2
|
+
* PRD auto-detection module
|
|
3
|
+
* Automatically finds PRD documents in the project directory
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
7
7
|
import { resolve, extname, relative } from 'path';
|
|
8
8
|
import { log } from './logger.js';
|
|
9
9
|
|
|
10
|
-
// PRD
|
|
10
|
+
// PRD filename patterns (ordered by priority, highest first)
|
|
11
11
|
const PRD_PATTERNS = [
|
|
12
12
|
/prd/i,
|
|
13
13
|
/product.?requirement/i,
|
|
@@ -18,9 +18,9 @@ const PRD_PATTERNS = [
|
|
|
18
18
|
/spec/i,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// Directories to search (ordered by priority)
|
|
22
22
|
const SEARCH_DIRS = [
|
|
23
|
-
'.', //
|
|
23
|
+
'.', // Project root
|
|
24
24
|
'docs',
|
|
25
25
|
'doc',
|
|
26
26
|
'PRD',
|
|
@@ -30,16 +30,16 @@ const SEARCH_DIRS = [
|
|
|
30
30
|
'文档',
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Directories to ignore
|
|
34
34
|
const IGNORE_DIRS = new Set([
|
|
35
35
|
'node_modules', '.git', '.next', 'dist', 'build', 'vendor',
|
|
36
36
|
'.codex-copilot', '.vscode', '.idea', '__pycache__', 'coverage',
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param {string} projectDir -
|
|
42
|
-
* @returns {Array<{path: string, score: number, name: string}>}
|
|
40
|
+
* Auto-detect PRD files in the project
|
|
41
|
+
* @param {string} projectDir - Project root directory
|
|
42
|
+
* @returns {Array<{path: string, score: number, name: string}>} Candidate PRD files sorted by match score
|
|
43
43
|
*/
|
|
44
44
|
export function detectPRD(projectDir) {
|
|
45
45
|
const candidates = [];
|
|
@@ -55,14 +55,14 @@ export function detectPRD(projectDir) {
|
|
|
55
55
|
scanDir(searchPath, projectDir, candidates, 0);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Sort by match score (descending)
|
|
59
59
|
candidates.sort((a, b) => b.score - a.score);
|
|
60
60
|
|
|
61
61
|
return candidates;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function scanDir(dir, projectDir, candidates, depth) {
|
|
65
|
-
if (depth > 3) return; //
|
|
65
|
+
if (depth > 3) return; // Max 3 levels deep
|
|
66
66
|
|
|
67
67
|
let entries;
|
|
68
68
|
try {
|
|
@@ -81,10 +81,10 @@ function scanDir(dir, projectDir, candidates, depth) {
|
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// Only scan markdown files
|
|
85
85
|
if (extname(entry.name).toLowerCase() !== '.md') continue;
|
|
86
86
|
|
|
87
|
-
//
|
|
87
|
+
// Calculate match score
|
|
88
88
|
const score = scorePRDMatch(entry.name, fullPath);
|
|
89
89
|
if (score > 0) {
|
|
90
90
|
candidates.push({
|
|
@@ -101,21 +101,21 @@ function scorePRDMatch(filename, fullPath) {
|
|
|
101
101
|
let score = 0;
|
|
102
102
|
const lower = filename.toLowerCase();
|
|
103
103
|
|
|
104
|
-
//
|
|
104
|
+
// Filename pattern matching
|
|
105
105
|
for (let i = 0; i < PRD_PATTERNS.length; i++) {
|
|
106
106
|
if (PRD_PATTERNS[i].test(filename)) {
|
|
107
|
-
score += (PRD_PATTERNS.length - i) * 10; //
|
|
107
|
+
score += (PRD_PATTERNS.length - i) * 10; // Higher priority patterns get higher scores
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
//
|
|
111
|
+
// File size bonus (PRDs are typically longer)
|
|
112
112
|
try {
|
|
113
113
|
const stat = statSync(fullPath);
|
|
114
114
|
if (stat.size > 5000) score += 5; // > 5KB
|
|
115
115
|
if (stat.size > 20000) score += 5; // > 20KB
|
|
116
116
|
} catch {}
|
|
117
117
|
|
|
118
|
-
//
|
|
118
|
+
// Content sampling (read first 2000 chars)
|
|
119
119
|
if (score > 0) {
|
|
120
120
|
try {
|
|
121
121
|
const content = readFileSync(fullPath, 'utf-8').slice(0, 2000);
|
|
@@ -130,7 +130,7 @@ function scorePRDMatch(filename, fullPath) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
133
|
+
* Read PRD file content
|
|
134
134
|
*/
|
|
135
135
|
export function readPRD(prdPath) {
|
|
136
136
|
return readFileSync(prdPath, 'utf-8');
|