@profoundlogic/coderflow-cli 0.2.1

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,537 @@
1
+ /**
2
+ * Server management commands
3
+ *
4
+ * NOTE: This command requires the server package to be installed locally.
5
+ * It manages a local CoderFlow server instance on the same machine.
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+ import { promises as fs } from 'fs';
10
+ import path from 'path';
11
+ import os from 'os';
12
+ import { fileURLToPath } from 'url';
13
+ import { createRequire } from 'module';
14
+ import {
15
+ saveCoderSetupPath,
16
+ getCoderSetupPath,
17
+ saveServerPort,
18
+ getServerPort,
19
+ getServerUrl
20
+ } from '../config.js';
21
+
22
+ const PID_FILE = path.join(os.homedir(), '.coder', 'server.pid');
23
+ const LOG_FILE = path.join(os.homedir(), '.coder', 'server.log');
24
+
25
+ /**
26
+ * Get the server package directory and entry script
27
+ * Returns { packagePath, scriptPath } or null if server package is not installed
28
+ */
29
+ async function getServerPackageInfo() {
30
+ // In development, prefer source (start.js) over dist for easier iteration
31
+ // Use CODER_USE_DIST=1 to force using dist in development
32
+ const useDistInDev = process.env.CODER_USE_DIST === '1';
33
+
34
+ // First, try to find server package installed via npm (or via workspaces)
35
+ try {
36
+ const require = createRequire(import.meta.url);
37
+ const serverPkgPath = require.resolve('@profoundlogic/coderflow-server/package.json');
38
+ const packagePath = path.dirname(serverPkgPath);
39
+ const srcScript = path.join(packagePath, 'start.js');
40
+ const distScript = path.join(packagePath, 'dist', 'start.js');
41
+
42
+ // Check if source exists (indicates development/workspace setup)
43
+ let sourceExists = false;
44
+ try {
45
+ await fs.access(srcScript);
46
+ sourceExists = true;
47
+ } catch {
48
+ // Source doesn't exist - this is a truly installed package
49
+ }
50
+
51
+ // Prefer source in development unless CODER_USE_DIST=1
52
+ if (sourceExists && !useDistInDev) {
53
+ return { packagePath, scriptPath: srcScript };
54
+ }
55
+
56
+ // Use dist (for installed packages or when CODER_USE_DIST=1)
57
+ try {
58
+ await fs.access(distScript);
59
+ return { packagePath, scriptPath: distScript };
60
+ } catch {
61
+ // dist/start.js not found
62
+ }
63
+
64
+ // Fall back to source if dist doesn't exist
65
+ if (sourceExists) {
66
+ return { packagePath, scriptPath: srcScript };
67
+ }
68
+ } catch {
69
+ // Not installed as npm package, try monorepo development path
70
+ }
71
+
72
+ // Try relative path for monorepo development (fallback if require.resolve fails)
73
+ const currentFile = fileURLToPath(import.meta.url);
74
+ const cliDir = path.dirname(path.dirname(path.dirname(currentFile)));
75
+ const monorepoServerPath = path.join(cliDir, '..', 'server');
76
+
77
+ const srcScript = path.join(monorepoServerPath, 'start.js');
78
+ const distScript = path.join(monorepoServerPath, 'dist', 'start.js');
79
+
80
+ if (!useDistInDev) {
81
+ // Prefer source for development
82
+ try {
83
+ await fs.access(srcScript);
84
+ return { packagePath: monorepoServerPath, scriptPath: srcScript };
85
+ } catch {
86
+ // source doesn't exist, try dist
87
+ }
88
+ }
89
+
90
+ try {
91
+ await fs.access(distScript);
92
+ return { packagePath: monorepoServerPath, scriptPath: distScript };
93
+ } catch {
94
+ // dist doesn't exist either
95
+ }
96
+
97
+ // If useDistInDev was set but dist doesn't exist, fall back to source
98
+ if (useDistInDev) {
99
+ try {
100
+ await fs.access(srcScript);
101
+ return { packagePath: monorepoServerPath, scriptPath: srcScript };
102
+ } catch {
103
+ // Server package not found
104
+ }
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Check if server package is available and show error if not
112
+ * Returns { packagePath, scriptPath }
113
+ */
114
+ async function requireServerPackage() {
115
+ const serverInfo = await getServerPackageInfo();
116
+ if (!serverInfo) {
117
+ console.error('Error: Server package is not installed locally.');
118
+ console.error('');
119
+ console.error('The `coder server` command manages a local CoderFlow server.');
120
+ console.error('To use this command, install the server package:');
121
+ console.error('');
122
+ console.error(' npm install -g @profoundlogic/coderflow-server');
123
+ console.error('');
124
+ console.error('If you want to connect to a remote server instead, use:');
125
+ console.error(' coder config set serverUrl http://your-server:3000');
126
+ console.error(' coder login');
127
+ process.exit(1);
128
+ }
129
+ return serverInfo;
130
+ }
131
+
132
+ /**
133
+ * Check if server is running
134
+ */
135
+ async function isServerRunning() {
136
+ try {
137
+ const pidData = await fs.readFile(PID_FILE, 'utf-8');
138
+ const pid = parseInt(pidData.trim(), 10);
139
+
140
+ // Check if process exists
141
+ try {
142
+ process.kill(pid, 0); // Signal 0 just checks if process exists
143
+ return { running: true, pid };
144
+ } catch {
145
+ // Process doesn't exist, clean up PID file
146
+ await fs.unlink(PID_FILE).catch(() => {});
147
+ return { running: false, pid: null };
148
+ }
149
+ } catch {
150
+ return { running: false, pid: null };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Get server process info
156
+ */
157
+ async function getServerInfo() {
158
+ const { running, pid } = await isServerRunning();
159
+ const setupPath = await getCoderSetupPath();
160
+ const port = await getServerPort();
161
+ const serverUrl = await getServerUrl();
162
+
163
+ return {
164
+ running,
165
+ pid,
166
+ setupPath,
167
+ port,
168
+ serverUrl
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Start the server
174
+ */
175
+ async function startServer(args) {
176
+ const { running, pid } = await isServerRunning();
177
+
178
+ if (running) {
179
+ console.log(`Server is already running (PID: ${pid})`);
180
+ console.log(`URL: ${await getServerUrl()}`);
181
+ return;
182
+ }
183
+
184
+ // Parse arguments
185
+ let setupPath = null;
186
+ let port = null;
187
+ let daemon = false;
188
+
189
+ for (const arg of args) {
190
+ if (arg.startsWith('--setup-path=')) {
191
+ setupPath = arg.substring('--setup-path='.length);
192
+ } else if (arg.startsWith('--port=')) {
193
+ port = parseInt(arg.substring('--port='.length), 10);
194
+ } else if (arg === '--daemon' || arg === '-d') {
195
+ daemon = true;
196
+ }
197
+ }
198
+
199
+ // Get setup path from args, config, or env
200
+ if (!setupPath) {
201
+ setupPath = await getCoderSetupPath();
202
+ }
203
+
204
+ if (!setupPath) {
205
+ console.error('Error: No coder-setup path configured');
206
+ console.error('');
207
+ console.error('Please provide a setup path:');
208
+ console.error(' coder server start --setup-path=/path/to/coder-setup');
209
+ console.error('');
210
+ console.error('Or set environment variable:');
211
+ console.error(' export CODER_SETUP_PATH=/path/to/coder-setup');
212
+ process.exit(1);
213
+ }
214
+
215
+ // Resolve to absolute path
216
+ setupPath = path.resolve(setupPath);
217
+
218
+ // Verify setup path exists
219
+ try {
220
+ const stat = await fs.stat(setupPath);
221
+ if (!stat.isDirectory()) {
222
+ console.error(`Error: Setup path is not a directory: ${setupPath}`);
223
+ process.exit(1);
224
+ }
225
+ } catch (error) {
226
+ console.error(`Error: Setup path does not exist: ${setupPath}`);
227
+ process.exit(1);
228
+ }
229
+
230
+ // Save setup path to config for future use
231
+ await saveCoderSetupPath(setupPath);
232
+ console.log(`✓ Saved setup path to config: ${setupPath}`);
233
+
234
+ // Get port from args, config, or default
235
+ if (!port) {
236
+ port = await getServerPort();
237
+ }
238
+
239
+ // Save port to config
240
+ if (port !== 3000) {
241
+ await saveServerPort(port);
242
+ console.log(`✓ Saved server port to config: ${port}`);
243
+ }
244
+
245
+ // Ensure server package is installed (scriptPath already verified to exist)
246
+ const { scriptPath: serverScript } = await requireServerPackage();
247
+
248
+ console.log('');
249
+ console.log('Starting CoderFlow Server...');
250
+ console.log(` Setup Path: ${setupPath}`);
251
+ console.log(` Port: ${port}`);
252
+ console.log(` Mode: ${daemon ? 'daemon' : 'foreground'}`);
253
+ console.log('');
254
+
255
+ const env = {
256
+ ...process.env,
257
+ CODER_SETUP_PATH: setupPath,
258
+ PORT: port.toString()
259
+ };
260
+
261
+ const spawnArgs = [serverScript];
262
+
263
+ // Allow the coder server itself to be debugged.
264
+ if (typeof process.env.CODER_SERVER_INSPECT === "string" && process.env.CODER_SERVER_INSPECT.length > 0) {
265
+ spawnArgs.unshift('--inspect');
266
+ }
267
+
268
+ if (daemon) {
269
+ // Start server as daemon
270
+ const logStream = await fs.open(LOG_FILE, 'a');
271
+
272
+ const serverProcess = spawn('node', spawnArgs, {
273
+ env,
274
+ detached: true,
275
+ stdio: ['ignore', logStream.fd, logStream.fd]
276
+ });
277
+
278
+ serverProcess.unref();
279
+
280
+ // Save PID
281
+ const pidDir = path.dirname(PID_FILE);
282
+ await fs.mkdir(pidDir, { recursive: true });
283
+ await fs.writeFile(PID_FILE, serverProcess.pid.toString(), 'utf-8');
284
+
285
+ console.log(`✓ Server started in daemon mode (PID: ${serverProcess.pid})`);
286
+ console.log(` URL: http://localhost:${port}`);
287
+ console.log(` Logs: ${LOG_FILE}`);
288
+ console.log('');
289
+ console.log('Use "coder server status" to check server status');
290
+ console.log('Use "coder server stop" to stop the server');
291
+ console.log('Use "coder server logs" to view logs');
292
+ } else {
293
+ // Start server in foreground
294
+ const serverProcess = spawn('node', spawnArgs, {
295
+ env,
296
+ stdio: 'inherit'
297
+ });
298
+
299
+ // Save PID
300
+ const pidDir = path.dirname(PID_FILE);
301
+ await fs.mkdir(pidDir, { recursive: true });
302
+ await fs.writeFile(PID_FILE, serverProcess.pid.toString(), 'utf-8');
303
+
304
+ // Handle termination
305
+ const cleanup = async () => {
306
+ await fs.unlink(PID_FILE).catch(() => {});
307
+ };
308
+
309
+ serverProcess.on('exit', cleanup);
310
+ process.on('SIGINT', () => {
311
+ cleanup().then(() => process.exit(0));
312
+ });
313
+ process.on('SIGTERM', () => {
314
+ cleanup().then(() => process.exit(0));
315
+ });
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Stop the server
321
+ */
322
+ async function stopServer() {
323
+ const { running, pid } = await isServerRunning();
324
+
325
+ if (!running) {
326
+ console.log('Server is not running');
327
+ return;
328
+ }
329
+
330
+ console.log(`Stopping server (PID: ${pid})...`);
331
+
332
+ try {
333
+ process.kill(pid, 'SIGTERM');
334
+
335
+ // Wait for process to exit (max 5 seconds)
336
+ for (let i = 0; i < 50; i++) {
337
+ try {
338
+ process.kill(pid, 0);
339
+ await new Promise(resolve => setTimeout(resolve, 100));
340
+ } catch {
341
+ // Process exited
342
+ break;
343
+ }
344
+ }
345
+
346
+ // Check if still running, force kill if needed
347
+ try {
348
+ process.kill(pid, 0);
349
+ console.log('Server did not stop gracefully, forcing...');
350
+ process.kill(pid, 'SIGKILL');
351
+ } catch {
352
+ // Already stopped
353
+ }
354
+
355
+ // Clean up PID file
356
+ await fs.unlink(PID_FILE).catch(() => {});
357
+
358
+ console.log('✓ Server stopped');
359
+ } catch (error) {
360
+ console.error(`Error stopping server: ${error.message}`);
361
+ process.exit(1);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Show server status
367
+ */
368
+ async function showStatus() {
369
+ const info = await getServerInfo();
370
+
371
+ console.log('CoderFlow Server Status:');
372
+ console.log('');
373
+ console.log(` Status: ${info.running ? '✓ Running' : '✗ Not running'}`);
374
+
375
+ if (info.running) {
376
+ console.log(` PID: ${info.pid}`);
377
+ console.log(` URL: ${info.serverUrl}`);
378
+ }
379
+
380
+ if (info.setupPath) {
381
+ console.log(` Setup Path: ${info.setupPath}`);
382
+ } else {
383
+ console.log(` Setup Path: (not configured)`);
384
+ }
385
+
386
+ console.log(` Port: ${info.port}`);
387
+ console.log('');
388
+
389
+ if (!info.running && info.setupPath) {
390
+ console.log('Start the server with:');
391
+ console.log(' coder server start');
392
+ } else if (!info.running) {
393
+ console.log('Configure and start the server with:');
394
+ console.log(' coder server start --setup-path=/path/to/coder-setup');
395
+ } else {
396
+ console.log('Stop the server with:');
397
+ console.log(' coder server stop');
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Show server logs
403
+ */
404
+ async function showLogs(args) {
405
+ const { running } = await isServerRunning();
406
+
407
+ if (!running) {
408
+ console.log('Server is not running');
409
+ return;
410
+ }
411
+
412
+ // Parse arguments
413
+ let follow = false;
414
+ let lines = null;
415
+
416
+ for (const arg of args) {
417
+ if (arg === '--follow' || arg === '-f') {
418
+ follow = true;
419
+ } else if (arg.startsWith('--tail=')) {
420
+ lines = parseInt(arg.substring('--tail='.length), 10);
421
+ }
422
+ }
423
+
424
+ try {
425
+ const logContent = await fs.readFile(LOG_FILE, 'utf-8');
426
+ const logLines = logContent.split('\n');
427
+
428
+ if (lines) {
429
+ console.log(logLines.slice(-lines).join('\n'));
430
+ } else {
431
+ console.log(logContent);
432
+ }
433
+
434
+ if (follow) {
435
+ console.log('\n--- Following logs (Ctrl+C to stop) ---\n');
436
+
437
+ // Simple tail -f implementation
438
+ let lastSize = (await fs.stat(LOG_FILE)).size;
439
+
440
+ const interval = setInterval(async () => {
441
+ try {
442
+ const currentSize = (await fs.stat(LOG_FILE)).size;
443
+ if (currentSize > lastSize) {
444
+ const stream = await fs.open(LOG_FILE, 'r');
445
+ const buffer = Buffer.alloc(currentSize - lastSize);
446
+ await stream.read(buffer, 0, buffer.length, lastSize);
447
+ await stream.close();
448
+ process.stdout.write(buffer.toString('utf-8'));
449
+ lastSize = currentSize;
450
+ }
451
+ } catch (error) {
452
+ clearInterval(interval);
453
+ }
454
+ }, 500);
455
+
456
+ process.on('SIGINT', () => {
457
+ clearInterval(interval);
458
+ process.exit(0);
459
+ });
460
+ }
461
+ } catch (error) {
462
+ if (error.code === 'ENOENT') {
463
+ console.log('No log file found');
464
+ console.log('Server may have been started in foreground mode');
465
+ } else {
466
+ console.error(`Error reading logs: ${error.message}`);
467
+ }
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Restart the server
473
+ */
474
+ async function restartServer(args) {
475
+ const { running } = await isServerRunning();
476
+
477
+ if (running) {
478
+ console.log('Stopping server...');
479
+ await stopServer();
480
+ console.log('');
481
+ }
482
+
483
+ console.log('Starting server...');
484
+ await startServer(args);
485
+ }
486
+
487
+ /**
488
+ * Handle server commands
489
+ */
490
+ export async function handleServer(args) {
491
+ const subcommand = args[0];
492
+
493
+ // Check for server package availability for commands that need it
494
+ // (start, restart require the package; stop/status/logs work with running server)
495
+ if (subcommand === 'start' || subcommand === 'restart') {
496
+ await requireServerPackage();
497
+ }
498
+
499
+ switch (subcommand) {
500
+ case 'start':
501
+ await startServer(args.slice(1));
502
+ break;
503
+
504
+ case 'stop':
505
+ await stopServer();
506
+ break;
507
+
508
+ case 'status':
509
+ await showStatus();
510
+ break;
511
+
512
+ case 'logs':
513
+ await showLogs(args.slice(1));
514
+ break;
515
+
516
+ case 'restart':
517
+ await restartServer(args.slice(1));
518
+ break;
519
+
520
+ case undefined:
521
+ console.error('Error: No subcommand provided');
522
+ console.error('');
523
+ console.error('Available subcommands:');
524
+ console.error(' coder server start [--setup-path=PATH] [--port=PORT] [--daemon]');
525
+ console.error(' coder server stop');
526
+ console.error(' coder server status');
527
+ console.error(' coder server logs [--follow] [--tail=N]');
528
+ console.error(' coder server restart [--setup-path=PATH] [--port=PORT] [--daemon]');
529
+ process.exit(1);
530
+ break;
531
+
532
+ default:
533
+ console.error(`Error: Unknown subcommand "${subcommand}"`);
534
+ console.error('Run "coder server" for available subcommands');
535
+ process.exit(1);
536
+ }
537
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Command: coder status - Check status of a task
3
+ */
4
+
5
+ import { request } from '../http-client.js';
6
+
7
+ export async function getStatus(taskId) {
8
+ if (!taskId) {
9
+ console.error('Error: Task ID required');
10
+ console.error('Usage: coder status <task-id>');
11
+ process.exit(1);
12
+ }
13
+
14
+ console.log(`Fetching status for task ${taskId}...`);
15
+
16
+ const data = await request(`/tasks/${taskId}`);
17
+
18
+ console.log(`\nTask Status:`);
19
+ console.log(` Task ID: ${data.taskId}`);
20
+ console.log(` Status: ${data.status}`);
21
+ console.log(` Created: ${data.createdAt}`);
22
+
23
+ if (data.finishedAt) {
24
+ console.log(` Finished: ${data.finishedAt}`);
25
+ }
26
+
27
+ if (data.exitCode !== undefined) {
28
+ console.log(` Exit Code: ${data.exitCode}`);
29
+ }
30
+
31
+ if (data.status === 'completed') {
32
+ console.log(`\n✓ Task completed successfully`);
33
+ console.log(`Use "coder results ${taskId}" to get full results`);
34
+ } else if (data.status === 'failed') {
35
+ console.log(`\n✗ Task failed`);
36
+ } else if (data.status === 'running') {
37
+ console.log(`\n⋯ Task is still running`);
38
+ }
39
+ }