@litmers/cursorflow-orchestrator 0.1.0 → 0.1.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/README.md +375 -310
- package/commands/cursorflow-clean.md +162 -162
- package/commands/cursorflow-init.md +67 -67
- package/commands/cursorflow-monitor.md +131 -131
- package/commands/cursorflow-prepare.md +134 -134
- package/commands/cursorflow-resume.md +181 -181
- package/commands/cursorflow-review.md +220 -220
- package/commands/cursorflow-run.md +129 -129
- package/package.json +13 -4
- package/scripts/ai-security-check.js +224 -0
- package/scripts/release.sh +109 -0
- package/scripts/setup-security.sh +105 -0
- package/src/cli/init.js +69 -4
- package/src/cli/monitor.js +196 -9
- package/src/core/runner.js +187 -9
- package/src/utils/config.js +2 -2
- package/src/utils/cursor-agent.js +96 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# 보안 검사 설정 스크립트
|
|
4
|
+
# GitHub Secrets 설정을 위한 가이드
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# 색상 정의
|
|
9
|
+
RED='\033[0;31m'
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
YELLOW='\033[1;33m'
|
|
12
|
+
BLUE='\033[0;34m'
|
|
13
|
+
NC='\033[0m'
|
|
14
|
+
|
|
15
|
+
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
|
16
|
+
echo -e "${BLUE}║ 🔒 Security Scan Setup Guide ║${NC}"
|
|
17
|
+
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}\n"
|
|
18
|
+
|
|
19
|
+
# GitHub CLI 확인
|
|
20
|
+
if ! command -v gh &> /dev/null; then
|
|
21
|
+
echo -e "${YELLOW}⚠️ GitHub CLI (gh) is not installed.${NC}"
|
|
22
|
+
echo -e "Install it from: https://cli.github.com/\n"
|
|
23
|
+
USE_GH_CLI=false
|
|
24
|
+
else
|
|
25
|
+
echo -e "${GREEN}✓ GitHub CLI detected${NC}\n"
|
|
26
|
+
USE_GH_CLI=true
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
echo -e "${BLUE}Required GitHub Secrets:${NC}\n"
|
|
30
|
+
|
|
31
|
+
echo "1. NPM_TOKEN (필수 - npm 배포용)"
|
|
32
|
+
echo " - Visit: https://www.npmjs.com"
|
|
33
|
+
echo " - Profile → Access Tokens → Generate New Token"
|
|
34
|
+
echo " - Type: Automation"
|
|
35
|
+
echo ""
|
|
36
|
+
|
|
37
|
+
echo "2. SNYK_TOKEN (권장 - 강화된 의존성 스캔)"
|
|
38
|
+
echo " - Visit: https://snyk.io"
|
|
39
|
+
echo " - Settings → General → Auth Token"
|
|
40
|
+
echo ""
|
|
41
|
+
|
|
42
|
+
echo "3. OPENAI_API_KEY (선택 - AI 보안 검사)"
|
|
43
|
+
echo " - Visit: https://platform.openai.com/api-keys"
|
|
44
|
+
echo " - Create new secret key"
|
|
45
|
+
echo " - Cost: ~$0.01-0.10 per PR"
|
|
46
|
+
echo ""
|
|
47
|
+
|
|
48
|
+
if [ "$USE_GH_CLI" = true ]; then
|
|
49
|
+
echo -e "\n${BLUE}Would you like to set up secrets now using GitHub CLI?${NC}"
|
|
50
|
+
read -p "Continue? (y/N): " -n 1 -r
|
|
51
|
+
echo
|
|
52
|
+
|
|
53
|
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
54
|
+
# NPM_TOKEN
|
|
55
|
+
echo -e "\n${YELLOW}Setting up NPM_TOKEN...${NC}"
|
|
56
|
+
read -p "Enter your NPM token (or press Enter to skip): " NPM_TOKEN
|
|
57
|
+
if [ -n "$NPM_TOKEN" ]; then
|
|
58
|
+
gh secret set NPM_TOKEN -b "$NPM_TOKEN"
|
|
59
|
+
echo -e "${GREEN}✓ NPM_TOKEN set${NC}"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# SNYK_TOKEN
|
|
63
|
+
echo -e "\n${YELLOW}Setting up SNYK_TOKEN (optional)...${NC}"
|
|
64
|
+
read -p "Enter your Snyk token (or press Enter to skip): " SNYK_TOKEN
|
|
65
|
+
if [ -n "$SNYK_TOKEN" ]; then
|
|
66
|
+
gh secret set SNYK_TOKEN -b "$SNYK_TOKEN"
|
|
67
|
+
echo -e "${GREEN}✓ SNYK_TOKEN set${NC}"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# OPENAI_API_KEY
|
|
71
|
+
echo -e "\n${YELLOW}Setting up OPENAI_API_KEY (optional)...${NC}"
|
|
72
|
+
read -p "Enter your OpenAI API key (or press Enter to skip): " OPENAI_KEY
|
|
73
|
+
if [ -n "$OPENAI_KEY" ]; then
|
|
74
|
+
gh secret set OPENAI_API_KEY -b "$OPENAI_KEY"
|
|
75
|
+
echo -e "${GREEN}✓ OPENAI_API_KEY set${NC}"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo -e "\n${GREEN}✅ Secrets configuration complete!${NC}"
|
|
79
|
+
fi
|
|
80
|
+
else
|
|
81
|
+
echo -e "${YELLOW}Manual setup required:${NC}"
|
|
82
|
+
echo "1. Go to: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/settings/secrets/actions"
|
|
83
|
+
echo "2. Click 'New repository secret'"
|
|
84
|
+
echo "3. Add the secrets listed above"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
88
|
+
echo -e "${BLUE}Security Checks Enabled:${NC}\n"
|
|
89
|
+
echo "✅ NPM Audit (automatic)"
|
|
90
|
+
echo "✅ TruffleHog Secret Scanning (automatic)"
|
|
91
|
+
echo "✅ Semgrep Static Analysis (automatic)"
|
|
92
|
+
echo "✅ Trivy Filesystem Scan (automatic)"
|
|
93
|
+
echo "✅ CodeQL Analysis (automatic)"
|
|
94
|
+
echo "⚙️ Snyk Scan (requires SNYK_TOKEN)"
|
|
95
|
+
echo "⚙️ AI Security Review (requires OPENAI_API_KEY)"
|
|
96
|
+
|
|
97
|
+
echo -e "\n${BLUE}Test security scan locally:${NC}"
|
|
98
|
+
echo -e "${GREEN}npm audit${NC}"
|
|
99
|
+
echo -e "${GREEN}npm audit --audit-level=high${NC}"
|
|
100
|
+
|
|
101
|
+
echo -e "\n${BLUE}For more information:${NC}"
|
|
102
|
+
echo "📖 docs/SECURITY_CHECKS.md"
|
|
103
|
+
|
|
104
|
+
echo -e "\n${GREEN}Setup complete! 🎉${NC}\n"
|
|
105
|
+
|
package/src/cli/init.js
CHANGED
|
@@ -17,6 +17,7 @@ function parseArgs(args) {
|
|
|
17
17
|
withCommands: true,
|
|
18
18
|
configOnly: false,
|
|
19
19
|
force: false,
|
|
20
|
+
gitignore: true,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -38,6 +39,9 @@ function parseArgs(args) {
|
|
|
38
39
|
case '--force':
|
|
39
40
|
options.force = true;
|
|
40
41
|
break;
|
|
42
|
+
case '--no-gitignore':
|
|
43
|
+
options.gitignore = false;
|
|
44
|
+
break;
|
|
41
45
|
case '--help':
|
|
42
46
|
case '-h':
|
|
43
47
|
printHelp();
|
|
@@ -59,6 +63,7 @@ Options:
|
|
|
59
63
|
--example Create example tasks
|
|
60
64
|
--with-commands Install Cursor commands (default: true)
|
|
61
65
|
--no-commands Skip Cursor commands installation
|
|
66
|
+
--no-gitignore Skip adding _cursorflow to .gitignore
|
|
62
67
|
--config-only Only create config file
|
|
63
68
|
--force Overwrite existing files
|
|
64
69
|
--help, -h Show help
|
|
@@ -67,6 +72,7 @@ Examples:
|
|
|
67
72
|
cursorflow init
|
|
68
73
|
cursorflow init --example
|
|
69
74
|
cursorflow init --config-only
|
|
75
|
+
cursorflow init --no-gitignore
|
|
70
76
|
`);
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -155,6 +161,54 @@ cursorflow run ${config.tasksDir}/example/
|
|
|
155
161
|
logger.success(`Created example README: ${path.relative(projectRoot, readmePath)}`);
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Add _cursorflow to .gitignore
|
|
166
|
+
*/
|
|
167
|
+
function updateGitignore(projectRoot) {
|
|
168
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
169
|
+
const entry = '_cursorflow/';
|
|
170
|
+
|
|
171
|
+
// Check if .gitignore exists
|
|
172
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
173
|
+
// Create new .gitignore
|
|
174
|
+
fs.writeFileSync(gitignorePath, `# CursorFlow\n${entry}\n`, 'utf8');
|
|
175
|
+
logger.success('Created .gitignore with _cursorflow/');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Read existing .gitignore
|
|
180
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
181
|
+
|
|
182
|
+
// Check if already included
|
|
183
|
+
const lines = content.split('\n');
|
|
184
|
+
const hasEntry = lines.some(line => {
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
return trimmed === '_cursorflow' ||
|
|
187
|
+
trimmed === '_cursorflow/' ||
|
|
188
|
+
trimmed === '/_cursorflow' ||
|
|
189
|
+
trimmed === '/_cursorflow/';
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (hasEntry) {
|
|
193
|
+
logger.info('_cursorflow/ already in .gitignore');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add entry
|
|
198
|
+
let newContent = content;
|
|
199
|
+
|
|
200
|
+
// Add newline if file doesn't end with one
|
|
201
|
+
if (!content.endsWith('\n')) {
|
|
202
|
+
newContent += '\n';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add section header and entry
|
|
206
|
+
newContent += `\n# CursorFlow\n${entry}\n`;
|
|
207
|
+
|
|
208
|
+
fs.writeFileSync(gitignorePath, newContent, 'utf8');
|
|
209
|
+
logger.success('Added _cursorflow/ to .gitignore');
|
|
210
|
+
}
|
|
211
|
+
|
|
158
212
|
async function init(args) {
|
|
159
213
|
logger.section('🚀 Initializing CursorFlow');
|
|
160
214
|
|
|
@@ -172,7 +226,7 @@ async function init(args) {
|
|
|
172
226
|
logger.info('Use --force to overwrite');
|
|
173
227
|
} else {
|
|
174
228
|
try {
|
|
175
|
-
createDefaultConfig(projectRoot);
|
|
229
|
+
createDefaultConfig(projectRoot, options.force);
|
|
176
230
|
logger.success(`Created config file: cursorflow.config.js`);
|
|
177
231
|
} catch (error) {
|
|
178
232
|
if (error.message.includes('already exists') && !options.force) {
|
|
@@ -197,7 +251,18 @@ async function init(args) {
|
|
|
197
251
|
logger.info('\n📁 Creating directories...');
|
|
198
252
|
createDirectories(projectRoot, config);
|
|
199
253
|
|
|
200
|
-
// 3.
|
|
254
|
+
// 3. Update .gitignore
|
|
255
|
+
if (options.gitignore) {
|
|
256
|
+
logger.info('\n📝 Updating .gitignore...');
|
|
257
|
+
try {
|
|
258
|
+
updateGitignore(projectRoot);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.warn(`Failed to update .gitignore: ${error.message}`);
|
|
261
|
+
logger.info('You can manually add "_cursorflow/" to your .gitignore');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 4. Install Cursor commands
|
|
201
266
|
if (options.withCommands) {
|
|
202
267
|
logger.info('\n📋 Installing Cursor commands...');
|
|
203
268
|
try {
|
|
@@ -208,13 +273,13 @@ async function init(args) {
|
|
|
208
273
|
}
|
|
209
274
|
}
|
|
210
275
|
|
|
211
|
-
//
|
|
276
|
+
// 5. Create example tasks
|
|
212
277
|
if (options.example) {
|
|
213
278
|
logger.info('\n📝 Creating example tasks...');
|
|
214
279
|
createExampleTasks(projectRoot, config);
|
|
215
280
|
}
|
|
216
281
|
|
|
217
|
-
//
|
|
282
|
+
// 6. Summary
|
|
218
283
|
logger.section('✅ CursorFlow initialized successfully!');
|
|
219
284
|
|
|
220
285
|
console.log('\n📚 Next steps:\n');
|
package/src/cli/monitor.js
CHANGED
|
@@ -1,29 +1,216 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* CursorFlow monitor command
|
|
3
|
+
* CursorFlow monitor command
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
6
8
|
const logger = require('../utils/logger');
|
|
9
|
+
const { loadState } = require('../utils/state');
|
|
10
|
+
const { loadConfig } = require('../utils/config');
|
|
7
11
|
|
|
8
12
|
function parseArgs(args) {
|
|
13
|
+
const watch = args.includes('--watch');
|
|
14
|
+
const intervalIdx = args.indexOf('--interval');
|
|
15
|
+
const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1]) || 2 : 2;
|
|
16
|
+
|
|
17
|
+
// Find run directory (first non-option argument)
|
|
18
|
+
const runDir = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
|
|
19
|
+
|
|
9
20
|
return {
|
|
10
|
-
runDir
|
|
11
|
-
watch
|
|
12
|
-
interval
|
|
21
|
+
runDir,
|
|
22
|
+
watch,
|
|
23
|
+
interval,
|
|
13
24
|
};
|
|
14
25
|
}
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Find the latest run directory
|
|
29
|
+
*/
|
|
30
|
+
function findLatestRunDir(logsDir) {
|
|
31
|
+
const runsDir = path.join(logsDir, 'runs');
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(runsDir)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const runs = fs.readdirSync(runsDir)
|
|
38
|
+
.filter(d => d.startsWith('run-'))
|
|
39
|
+
.map(d => ({
|
|
40
|
+
name: d,
|
|
41
|
+
path: path.join(runsDir, d),
|
|
42
|
+
mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime(),
|
|
43
|
+
}))
|
|
44
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
45
|
+
|
|
46
|
+
return runs.length > 0 ? runs[0].path : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* List all lanes in a run directory
|
|
51
|
+
*/
|
|
52
|
+
function listLanes(runDir) {
|
|
53
|
+
const lanesDir = path.join(runDir, 'lanes');
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(lanesDir)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return fs.readdirSync(lanesDir)
|
|
60
|
+
.filter(d => {
|
|
61
|
+
const stat = fs.statSync(path.join(lanesDir, d));
|
|
62
|
+
return stat.isDirectory();
|
|
63
|
+
})
|
|
64
|
+
.map(name => ({
|
|
65
|
+
name,
|
|
66
|
+
path: path.join(lanesDir, name),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get lane status
|
|
72
|
+
*/
|
|
73
|
+
function getLaneStatus(lanePath) {
|
|
74
|
+
const statePath = path.join(lanePath, 'state.json');
|
|
75
|
+
const state = loadState(statePath);
|
|
76
|
+
|
|
77
|
+
if (!state) {
|
|
78
|
+
return {
|
|
79
|
+
status: 'no state',
|
|
80
|
+
currentTask: '-',
|
|
81
|
+
totalTasks: '?',
|
|
82
|
+
progress: '0%',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const progress = state.totalTasks > 0
|
|
87
|
+
? Math.round((state.currentTaskIndex / state.totalTasks) * 100)
|
|
88
|
+
: 0;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
status: state.status || 'unknown',
|
|
92
|
+
currentTask: state.currentTaskIndex + 1,
|
|
93
|
+
totalTasks: state.totalTasks || '?',
|
|
94
|
+
progress: `${progress}%`,
|
|
95
|
+
pipelineBranch: state.pipelineBranch || '-',
|
|
96
|
+
chatId: state.chatId || '-',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Display lane status table
|
|
102
|
+
*/
|
|
103
|
+
function displayStatus(runDir, lanes) {
|
|
104
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
105
|
+
console.log(`📊 Run: ${path.basename(runDir)}`);
|
|
106
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
107
|
+
|
|
108
|
+
if (lanes.length === 0) {
|
|
109
|
+
console.log(' No lanes found\n');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Calculate column widths
|
|
114
|
+
const maxNameLen = Math.max(...lanes.map(l => l.name.length), 10);
|
|
115
|
+
|
|
116
|
+
// Header
|
|
117
|
+
console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Tasks`);
|
|
118
|
+
console.log(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(18)} ${'─'.repeat(8)} ${'─'.repeat(10)}`);
|
|
119
|
+
|
|
120
|
+
// Lanes
|
|
121
|
+
for (const lane of lanes) {
|
|
122
|
+
const status = getLaneStatus(lane.path);
|
|
123
|
+
const statusIcon = getStatusIcon(status.status);
|
|
124
|
+
const statusText = `${statusIcon} ${status.status}`.padEnd(18);
|
|
125
|
+
const progressText = status.progress.padEnd(8);
|
|
126
|
+
const tasksText = `${status.currentTask}/${status.totalTasks}`;
|
|
127
|
+
|
|
128
|
+
console.log(` ${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${tasksText}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get status icon
|
|
136
|
+
*/
|
|
137
|
+
function getStatusIcon(status) {
|
|
138
|
+
const icons = {
|
|
139
|
+
'running': '🔄',
|
|
140
|
+
'completed': '✅',
|
|
141
|
+
'failed': '❌',
|
|
142
|
+
'blocked_dependency': '🚫',
|
|
143
|
+
'no state': '⚪',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return icons[status] || '❓';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Monitor lanes
|
|
151
|
+
*/
|
|
16
152
|
async function monitor(args) {
|
|
17
153
|
logger.section('📡 Monitoring Lane Execution');
|
|
18
154
|
|
|
19
155
|
const options = parseArgs(args);
|
|
156
|
+
const config = loadConfig();
|
|
157
|
+
|
|
158
|
+
// Determine run directory
|
|
159
|
+
let runDir = options.runDir;
|
|
160
|
+
|
|
161
|
+
if (!runDir || runDir === 'latest') {
|
|
162
|
+
runDir = findLatestRunDir(config.logsDir);
|
|
163
|
+
|
|
164
|
+
if (!runDir) {
|
|
165
|
+
logger.error('No run directories found');
|
|
166
|
+
logger.info(`Runs directory: ${path.join(config.logsDir, 'runs')}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
logger.info(`Using latest run: ${path.basename(runDir)}`);
|
|
171
|
+
}
|
|
20
172
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
173
|
+
if (!fs.existsSync(runDir)) {
|
|
174
|
+
logger.error(`Run directory not found: ${runDir}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
24
177
|
|
|
25
|
-
|
|
26
|
-
|
|
178
|
+
// Watch mode
|
|
179
|
+
if (options.watch) {
|
|
180
|
+
logger.info(`Watch mode: every ${options.interval}s (Ctrl+C to stop)\n`);
|
|
181
|
+
|
|
182
|
+
let iteration = 0;
|
|
183
|
+
|
|
184
|
+
const refresh = () => {
|
|
185
|
+
if (iteration > 0) {
|
|
186
|
+
// Clear screen
|
|
187
|
+
process.stdout.write('\x1Bc');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lanes = listLanes(runDir);
|
|
191
|
+
displayStatus(runDir, lanes);
|
|
192
|
+
|
|
193
|
+
iteration++;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Initial display
|
|
197
|
+
refresh();
|
|
198
|
+
|
|
199
|
+
// Set up interval
|
|
200
|
+
const intervalId = setInterval(refresh, options.interval * 1000);
|
|
201
|
+
|
|
202
|
+
// Handle Ctrl+C
|
|
203
|
+
process.on('SIGINT', () => {
|
|
204
|
+
clearInterval(intervalId);
|
|
205
|
+
console.log('\n👋 Monitoring stopped\n');
|
|
206
|
+
process.exit(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
} else {
|
|
210
|
+
// Single shot
|
|
211
|
+
const lanes = listLanes(runDir);
|
|
212
|
+
displayStatus(runDir, lanes);
|
|
213
|
+
}
|
|
27
214
|
}
|
|
28
215
|
|
|
29
216
|
module.exports = monitor;
|
package/src/core/runner.js
CHANGED
|
@@ -15,16 +15,66 @@ const { ensureCursorAgent, checkCursorApiKey } = require('../utils/cursor-agent'
|
|
|
15
15
|
const { saveState, loadState, appendLog, createConversationEntry, createGitLogEntry } = require('../utils/state');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Execute cursor-agent command
|
|
18
|
+
* Execute cursor-agent command with timeout and better error handling
|
|
19
19
|
*/
|
|
20
20
|
function cursorAgentCreateChat() {
|
|
21
21
|
const { execSync } = require('child_process');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const out = execSync('cursor-agent create-chat', {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: 'pipe',
|
|
27
|
+
timeout: 30000, // 30 second timeout
|
|
28
|
+
});
|
|
29
|
+
const lines = out.split('\n').filter(Boolean);
|
|
30
|
+
const chatId = lines[lines.length - 1] || null;
|
|
31
|
+
|
|
32
|
+
if (!chatId) {
|
|
33
|
+
throw new Error('Failed to get chat ID from cursor-agent');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.info(`Created chat session: ${chatId}`);
|
|
37
|
+
return chatId;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// Check for common errors
|
|
40
|
+
if (error.message.includes('ENOENT')) {
|
|
41
|
+
throw new Error('cursor-agent CLI not found. Install with: npm install -g @cursor/agent');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error.message.includes('ETIMEDOUT') || error.killed) {
|
|
45
|
+
throw new Error('cursor-agent timed out. Check your internet connection and Cursor authentication.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error.stderr) {
|
|
49
|
+
const stderr = error.stderr.toString();
|
|
50
|
+
|
|
51
|
+
// Check for authentication errors
|
|
52
|
+
if (stderr.includes('not authenticated') ||
|
|
53
|
+
stderr.includes('login') ||
|
|
54
|
+
stderr.includes('auth')) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Cursor authentication failed. Please:\n' +
|
|
57
|
+
' 1. Open Cursor IDE\n' +
|
|
58
|
+
' 2. Sign in to your account\n' +
|
|
59
|
+
' 3. Verify you can use AI features\n' +
|
|
60
|
+
' 4. Try running cursorflow again\n\n' +
|
|
61
|
+
`Original error: ${stderr.trim()}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for API key errors
|
|
66
|
+
if (stderr.includes('api key') || stderr.includes('API_KEY')) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Cursor API key error. Please check your Cursor account and subscription.\n' +
|
|
69
|
+
`Error: ${stderr.trim()}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(`cursor-agent error: ${stderr.trim()}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`Failed to create chat: ${error.message}`);
|
|
77
|
+
}
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
function parseJsonFromStdout(stdout) {
|
|
@@ -57,19 +107,67 @@ function cursorAgentSend({ workspaceDir, chatId, prompt, model }) {
|
|
|
57
107
|
prompt,
|
|
58
108
|
];
|
|
59
109
|
|
|
110
|
+
logger.info('Executing cursor-agent...');
|
|
111
|
+
|
|
60
112
|
const res = spawnSync('cursor-agent', args, {
|
|
61
113
|
encoding: 'utf8',
|
|
62
114
|
stdio: 'pipe',
|
|
115
|
+
timeout: 300000, // 5 minute timeout for LLM response
|
|
63
116
|
});
|
|
64
117
|
|
|
118
|
+
// Check for timeout
|
|
119
|
+
if (res.error) {
|
|
120
|
+
if (res.error.code === 'ETIMEDOUT') {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
exitCode: -1,
|
|
124
|
+
error: 'cursor-agent timed out after 5 minutes. The LLM request may be taking too long or there may be network issues.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
exitCode: -1,
|
|
131
|
+
error: `cursor-agent error: ${res.error.message}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
const json = parseJsonFromStdout(res.stdout);
|
|
66
136
|
|
|
67
137
|
if (res.status !== 0 || !json || json.type !== 'result') {
|
|
68
|
-
|
|
138
|
+
let errorMsg = res.stderr?.trim() || res.stdout?.trim() || `exit=${res.status}`;
|
|
139
|
+
|
|
140
|
+
// Check for authentication errors
|
|
141
|
+
if (errorMsg.includes('not authenticated') ||
|
|
142
|
+
errorMsg.includes('login') ||
|
|
143
|
+
errorMsg.includes('auth')) {
|
|
144
|
+
errorMsg = 'Authentication error. Please:\n' +
|
|
145
|
+
' 1. Open Cursor IDE\n' +
|
|
146
|
+
' 2. Sign in to your account\n' +
|
|
147
|
+
' 3. Verify AI features are working\n' +
|
|
148
|
+
' 4. Try again\n\n' +
|
|
149
|
+
`Details: ${errorMsg}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for rate limit errors
|
|
153
|
+
if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
|
|
154
|
+
errorMsg = 'API rate limit or quota exceeded. Please:\n' +
|
|
155
|
+
' 1. Check your Cursor subscription\n' +
|
|
156
|
+
' 2. Wait a few minutes and try again\n\n' +
|
|
157
|
+
`Details: ${errorMsg}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for model errors
|
|
161
|
+
if (errorMsg.includes('model')) {
|
|
162
|
+
errorMsg = `Model error (requested: ${model || 'default'}). ` +
|
|
163
|
+
'Please check if the model is available in your Cursor subscription.\n\n' +
|
|
164
|
+
`Details: ${errorMsg}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
69
167
|
return {
|
|
70
168
|
ok: false,
|
|
71
169
|
exitCode: res.status,
|
|
72
|
-
error:
|
|
170
|
+
error: errorMsg,
|
|
73
171
|
};
|
|
74
172
|
}
|
|
75
173
|
|
|
@@ -251,8 +349,35 @@ async function runTask({
|
|
|
251
349
|
* Run all tasks in sequence
|
|
252
350
|
*/
|
|
253
351
|
async function runTasks(config, runDir) {
|
|
352
|
+
const { checkCursorAuth, printAuthHelp } = require('../utils/cursor-agent');
|
|
353
|
+
|
|
354
|
+
// Ensure cursor-agent is installed
|
|
254
355
|
ensureCursorAgent();
|
|
255
356
|
|
|
357
|
+
// Check authentication before starting
|
|
358
|
+
logger.info('Checking Cursor authentication...');
|
|
359
|
+
const authStatus = checkCursorAuth();
|
|
360
|
+
|
|
361
|
+
if (!authStatus.authenticated) {
|
|
362
|
+
logger.error('❌ Cursor authentication failed');
|
|
363
|
+
logger.error(` ${authStatus.message}`);
|
|
364
|
+
|
|
365
|
+
if (authStatus.details) {
|
|
366
|
+
logger.error(` Details: ${authStatus.details}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (authStatus.help) {
|
|
370
|
+
logger.error(` ${authStatus.help}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
console.log('');
|
|
374
|
+
printAuthHelp();
|
|
375
|
+
|
|
376
|
+
throw new Error('Cursor authentication required. Please authenticate and try again.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
logger.success('✓ Cursor authentication OK');
|
|
380
|
+
|
|
256
381
|
const repoRoot = git.getRepoRoot();
|
|
257
382
|
const pipelineBranch = config.pipelineBranch || `${config.branchPrefix}${Date.now().toString(36)}`;
|
|
258
383
|
const worktreeDir = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
@@ -269,6 +394,7 @@ async function runTasks(config, runDir) {
|
|
|
269
394
|
});
|
|
270
395
|
|
|
271
396
|
// Create chat
|
|
397
|
+
logger.info('Creating chat session...');
|
|
272
398
|
const chatId = cursorAgentCreateChat();
|
|
273
399
|
|
|
274
400
|
// Save initial state
|
|
@@ -341,3 +467,55 @@ module.exports = {
|
|
|
341
467
|
runTasks,
|
|
342
468
|
runTask,
|
|
343
469
|
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* CLI entry point
|
|
473
|
+
*/
|
|
474
|
+
if (require.main === module) {
|
|
475
|
+
const args = process.argv.slice(2);
|
|
476
|
+
|
|
477
|
+
if (args.length < 1) {
|
|
478
|
+
console.error('Usage: node runner.js <tasks-file> --run-dir <dir> --executor <executor>');
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const tasksFile = args[0];
|
|
483
|
+
const runDirIdx = args.indexOf('--run-dir');
|
|
484
|
+
const executorIdx = args.indexOf('--executor');
|
|
485
|
+
|
|
486
|
+
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1] : '.';
|
|
487
|
+
const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
|
|
488
|
+
|
|
489
|
+
if (!fs.existsSync(tasksFile)) {
|
|
490
|
+
console.error(`Tasks file not found: ${tasksFile}`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Load tasks configuration
|
|
495
|
+
let config;
|
|
496
|
+
try {
|
|
497
|
+
config = JSON.parse(fs.readFileSync(tasksFile, 'utf8'));
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error(`Failed to load tasks file: ${error.message}`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Add dependency policy defaults
|
|
504
|
+
config.dependencyPolicy = config.dependencyPolicy || {
|
|
505
|
+
allowDependencyChange: false,
|
|
506
|
+
lockfileReadOnly: true,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Run tasks
|
|
510
|
+
runTasks(config, runDir)
|
|
511
|
+
.then(() => {
|
|
512
|
+
process.exit(0);
|
|
513
|
+
})
|
|
514
|
+
.catch(error => {
|
|
515
|
+
console.error(`Runner failed: ${error.message}`);
|
|
516
|
+
if (process.env.DEBUG) {
|
|
517
|
+
console.error(error.stack);
|
|
518
|
+
}
|
|
519
|
+
process.exit(1);
|
|
520
|
+
});
|
|
521
|
+
}
|