@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.
@@ -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. Install Cursor commands
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
- // 4. Create example tasks
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
- // 5. Summary
282
+ // 6. Summary
218
283
  logger.section('✅ CursorFlow initialized successfully!');
219
284
 
220
285
  console.log('\n📚 Next steps:\n');
@@ -1,29 +1,216 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CursorFlow monitor command (stub)
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: args[0],
11
- watch: args.includes('--watch'),
12
- interval: 2,
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
- logger.info('This command will be fully implemented in the next phase');
22
- logger.info(`Run directory: ${options.runDir || 'latest'}`);
23
- logger.info(`Watch mode: ${options.watch}`);
173
+ if (!fs.existsSync(runDir)) {
174
+ logger.error(`Run directory not found: ${runDir}`);
175
+ process.exit(1);
176
+ }
24
177
 
25
- logger.warn('\n⚠️ Implementation pending');
26
- logger.info('This will show real-time lane status from logs');
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;
@@ -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
- const out = execSync('cursor-agent create-chat', {
23
- encoding: 'utf8',
24
- stdio: 'pipe',
25
- });
26
- const lines = out.split('\n').filter(Boolean);
27
- return lines[lines.length - 1] || null;
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
- const msg = res.stderr?.trim() || res.stdout?.trim() || `exit=${res.status}`;
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: msg,
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
+ }