projax 0.1.29

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.
Files changed (38) hide show
  1. package/LINKING.md +86 -0
  2. package/README.md +141 -0
  3. package/dist/core/database.d.ts +66 -0
  4. package/dist/core/database.js +312 -0
  5. package/dist/core/detector.d.ts +9 -0
  6. package/dist/core/detector.js +149 -0
  7. package/dist/core/index.d.ts +11 -0
  8. package/dist/core/index.js +43 -0
  9. package/dist/core/scanner.d.ts +8 -0
  10. package/dist/core/scanner.js +114 -0
  11. package/dist/electron/main.d.ts +1 -0
  12. package/dist/electron/main.js +310 -0
  13. package/dist/electron/port-extractor.d.ts +9 -0
  14. package/dist/electron/port-extractor.js +351 -0
  15. package/dist/electron/port-scanner.d.ts +13 -0
  16. package/dist/electron/port-scanner.js +93 -0
  17. package/dist/electron/port-utils.d.ts +21 -0
  18. package/dist/electron/port-utils.js +200 -0
  19. package/dist/electron/preload.d.ts +49 -0
  20. package/dist/electron/preload.js +21 -0
  21. package/dist/electron/renderer/assets/index-BZ6USRnW.js +42 -0
  22. package/dist/electron/renderer/assets/index-DNtxfrZe.js +42 -0
  23. package/dist/electron/renderer/assets/index-khk3K-qG.css +1 -0
  24. package/dist/electron/renderer/index.html +15 -0
  25. package/dist/electron/script-runner.d.ts +40 -0
  26. package/dist/electron/script-runner.js +651 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +971 -0
  29. package/dist/port-extractor.d.ts +9 -0
  30. package/dist/port-extractor.js +351 -0
  31. package/dist/port-scanner.d.ts +13 -0
  32. package/dist/port-scanner.js +93 -0
  33. package/dist/port-utils.d.ts +21 -0
  34. package/dist/port-utils.js +200 -0
  35. package/dist/script-runner.d.ts +40 -0
  36. package/dist/script-runner.js +651 -0
  37. package/package.json +40 -0
  38. package/rebuild-sqlite.js +82 -0
package/dist/index.js ADDED
@@ -0,0 +1,971 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const core_1 = require("./core");
41
+ const script_runner_1 = require("./script-runner");
42
+ const port_scanner_1 = require("./port-scanner");
43
+ // Read version from package.json
44
+ const packageJson = require('../package.json');
45
+ const program = new commander_1.Command();
46
+ program
47
+ .name('prx')
48
+ .description('Project management dashboard CLI')
49
+ .version(packageJson.version);
50
+ // Add project command
51
+ program
52
+ .command('add')
53
+ .description('Add a project to the dashboard')
54
+ .argument('[path]', 'Path to the project directory')
55
+ .option('-n, --name <name>', 'Custom name for the project (defaults to directory name)')
56
+ .action(async (projectPath, options) => {
57
+ try {
58
+ let finalPath = projectPath;
59
+ if (!finalPath) {
60
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
61
+ const answer = await inquirer.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'path',
65
+ message: 'Enter the path to your project:',
66
+ validate: (input) => {
67
+ if (!input.trim()) {
68
+ return 'Path is required';
69
+ }
70
+ const resolvedPath = path.resolve(input);
71
+ if (!fs.existsSync(resolvedPath)) {
72
+ return 'Path does not exist';
73
+ }
74
+ if (!fs.statSync(resolvedPath).isDirectory()) {
75
+ return 'Path must be a directory';
76
+ }
77
+ return true;
78
+ },
79
+ },
80
+ ]);
81
+ finalPath = answer.path;
82
+ }
83
+ const resolvedPath = path.resolve(finalPath);
84
+ if (!fs.existsSync(resolvedPath)) {
85
+ console.error(`Error: Path does not exist: ${resolvedPath}`);
86
+ process.exit(1);
87
+ }
88
+ if (!fs.statSync(resolvedPath).isDirectory()) {
89
+ console.error(`Error: Path is not a directory: ${resolvedPath}`);
90
+ process.exit(1);
91
+ }
92
+ const db = (0, core_1.getDatabaseManager)();
93
+ const existingProject = db.getProjectByPath(resolvedPath);
94
+ if (existingProject) {
95
+ console.log(`Project already exists: ${existingProject.name} (ID: ${existingProject.id})`);
96
+ return;
97
+ }
98
+ // Determine project name: use custom name if provided, otherwise prompt or use basename
99
+ let projectName;
100
+ if (options?.name) {
101
+ projectName = options.name.trim();
102
+ }
103
+ else {
104
+ const defaultName = path.basename(resolvedPath);
105
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
106
+ const nameAnswer = await inquirer.prompt([
107
+ {
108
+ type: 'input',
109
+ name: 'name',
110
+ message: 'Enter a name for this project:',
111
+ default: defaultName,
112
+ validate: (input) => {
113
+ if (!input.trim()) {
114
+ return 'Project name is required';
115
+ }
116
+ return true;
117
+ },
118
+ },
119
+ ]);
120
+ projectName = nameAnswer.name.trim();
121
+ }
122
+ const project = db.addProject(projectName, resolvedPath);
123
+ console.log(`✓ Added project: ${project.name} (ID: ${project.id})`);
124
+ console.log(` Path: ${project.path}`);
125
+ // Ask if user wants to scan for tests
126
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
127
+ const scanAnswer = await inquirer.prompt([
128
+ {
129
+ type: 'confirm',
130
+ name: 'scan',
131
+ message: 'Would you like to scan for tests now?',
132
+ default: true,
133
+ },
134
+ ]);
135
+ if (scanAnswer.scan) {
136
+ console.log('Scanning for tests...');
137
+ const result = (0, core_1.scanProject)(project.id);
138
+ console.log(`✓ Found ${result.testsFound} test file(s)`);
139
+ if (result.tests.length > 0) {
140
+ console.log(' Test files:');
141
+ result.tests.forEach(test => {
142
+ console.log(` - ${test.file_path}${test.framework ? ` (${test.framework})` : ''}`);
143
+ });
144
+ }
145
+ }
146
+ // Scan for ports in background
147
+ console.log('Scanning for ports...');
148
+ try {
149
+ await (0, port_scanner_1.scanProjectPorts)(project.id);
150
+ const ports = db.getProjectPorts(project.id);
151
+ if (ports.length > 0) {
152
+ console.log(`✓ Found ${ports.length} port(s)`);
153
+ const portList = ports.map(p => p.port).sort((a, b) => a - b).join(', ');
154
+ console.log(` Ports: ${portList}`);
155
+ }
156
+ else {
157
+ console.log(' No ports detected');
158
+ }
159
+ }
160
+ catch (error) {
161
+ // Ignore port scanning errors
162
+ console.log(' Port scanning skipped');
163
+ }
164
+ }
165
+ catch (error) {
166
+ console.error('Error adding project:', error instanceof Error ? error.message : error);
167
+ process.exit(1);
168
+ }
169
+ });
170
+ // List projects command
171
+ program
172
+ .command('list')
173
+ .description('List all tracked projects')
174
+ .option('-v, --verbose', 'Show detailed information')
175
+ .option('--ports', 'Show detailed port information per script')
176
+ .action(async (options) => {
177
+ try {
178
+ const db = (0, core_1.getDatabaseManager)();
179
+ const projects = (0, core_1.getAllProjects)();
180
+ if (projects.length === 0) {
181
+ console.log('No projects tracked yet. Use "prx add" to add a project.');
182
+ return;
183
+ }
184
+ // Check if ports need rescanning and do it in background if needed
185
+ for (const project of projects) {
186
+ if ((0, port_scanner_1.shouldRescanPorts)(project.id)) {
187
+ // Rescan ports asynchronously (don't wait)
188
+ (0, port_scanner_1.scanProjectPorts)(project.id).catch(() => {
189
+ // Ignore errors in background scanning
190
+ });
191
+ }
192
+ }
193
+ if (options.ports) {
194
+ // Detailed port view
195
+ console.log('\nProjects with Port Information:\n');
196
+ for (const project of projects) {
197
+ const ports = db.getProjectPorts(project.id);
198
+ const tests = db.getTestsByProject(project.id);
199
+ const lastScanned = project.last_scanned
200
+ ? new Date(project.last_scanned * 1000).toLocaleString()
201
+ : 'Never';
202
+ console.log(`${project.id}. ${project.name}`);
203
+ console.log(` Path: ${project.path}`);
204
+ console.log(` Tests: ${tests.length} | Last scanned: ${lastScanned}`);
205
+ if (ports.length === 0) {
206
+ console.log(` Ports: N/A`);
207
+ }
208
+ else {
209
+ console.log(` Ports:`);
210
+ // Group by script
211
+ const portsByScript = new Map();
212
+ for (const port of ports) {
213
+ const script = port.script_name || 'general';
214
+ if (!portsByScript.has(script)) {
215
+ portsByScript.set(script, []);
216
+ }
217
+ portsByScript.get(script).push(port.port);
218
+ }
219
+ for (const [script, portList] of portsByScript.entries()) {
220
+ const scriptLabel = script === 'general' ? ' (general)' : ` ${script}:`;
221
+ console.log(` ${scriptLabel} ${portList.sort((a, b) => a - b).join(', ')}`);
222
+ }
223
+ }
224
+ console.log('');
225
+ }
226
+ }
227
+ else {
228
+ // Table format
229
+ console.log(`\nTracked Projects (${projects.length}):\n`);
230
+ // Calculate column widths
231
+ const idWidth = Math.max(3, projects.length.toString().length);
232
+ const nameWidth = Math.max(4, ...projects.map(p => p.name.length));
233
+ const pathWidth = Math.max(4, Math.min(40, ...projects.map(p => p.path.length)));
234
+ const portsWidth = 12;
235
+ const testsWidth = 6;
236
+ const scannedWidth = 20;
237
+ // Header
238
+ const header = [
239
+ 'ID'.padEnd(idWidth),
240
+ 'Name'.padEnd(nameWidth),
241
+ 'Path'.padEnd(pathWidth),
242
+ 'Ports'.padEnd(portsWidth),
243
+ 'Tests'.padEnd(testsWidth),
244
+ 'Last Scanned'.padEnd(scannedWidth),
245
+ ].join(' | ');
246
+ console.log(header);
247
+ console.log('-'.repeat(header.length));
248
+ // Rows
249
+ for (const project of projects) {
250
+ const ports = db.getProjectPorts(project.id);
251
+ const tests = db.getTestsByProject(project.id);
252
+ const lastScanned = project.last_scanned
253
+ ? new Date(project.last_scanned * 1000).toLocaleString()
254
+ : 'Never';
255
+ const portStr = ports.length > 0
256
+ ? ports.map(p => p.port).sort((a, b) => a - b).join(', ')
257
+ : 'N/A';
258
+ const pathDisplay = project.path.length > 40
259
+ ? '...' + project.path.slice(-37)
260
+ : project.path;
261
+ const row = [
262
+ project.id.toString().padEnd(idWidth),
263
+ project.name.padEnd(nameWidth),
264
+ pathDisplay.padEnd(pathWidth),
265
+ portStr.padEnd(portsWidth),
266
+ tests.length.toString().padEnd(testsWidth),
267
+ lastScanned.padEnd(scannedWidth),
268
+ ].join(' | ');
269
+ console.log(row);
270
+ }
271
+ console.log('');
272
+ }
273
+ }
274
+ catch (error) {
275
+ console.error('Error listing projects:', error instanceof Error ? error.message : error);
276
+ process.exit(1);
277
+ }
278
+ });
279
+ // Scan command
280
+ program
281
+ .command('scan')
282
+ .description('Scan projects for test files')
283
+ .argument('[project]', 'Project ID or name to scan (leave empty to scan all)')
284
+ .action(async (projectIdentifier) => {
285
+ try {
286
+ const db = (0, core_1.getDatabaseManager)();
287
+ if (projectIdentifier) {
288
+ // Find project by ID or name
289
+ const projects = (0, core_1.getAllProjects)();
290
+ const project = projects.find((p) => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
291
+ if (!project) {
292
+ console.error(`Error: Project not found: ${projectIdentifier}`);
293
+ process.exit(1);
294
+ }
295
+ console.log(`Scanning project: ${project.name}...`);
296
+ const result = (0, core_1.scanProject)(project.id);
297
+ console.log(`✓ Found ${result.testsFound} test file(s)`);
298
+ if (result.tests.length > 0) {
299
+ console.log('\nTest files:');
300
+ result.tests.forEach(test => {
301
+ console.log(` - ${test.file_path}${test.framework ? ` (${test.framework})` : ''}`);
302
+ });
303
+ }
304
+ // Also scan ports
305
+ console.log('\nScanning for ports...');
306
+ try {
307
+ await (0, port_scanner_1.scanProjectPorts)(project.id);
308
+ const ports = db.getProjectPorts(project.id);
309
+ if (ports.length > 0) {
310
+ console.log(`✓ Found ${ports.length} port(s)`);
311
+ const portList = ports.map(p => p.port).sort((a, b) => a - b).join(', ');
312
+ console.log(` Ports: ${portList}`);
313
+ }
314
+ else {
315
+ console.log(' No ports detected');
316
+ }
317
+ }
318
+ catch (error) {
319
+ console.log(' Port scanning failed');
320
+ }
321
+ }
322
+ else {
323
+ // Scan all projects
324
+ console.log('Scanning all projects...\n');
325
+ const results = (0, core_1.scanAllProjects)();
326
+ for (const result of results) {
327
+ console.log(`${result.project.name}: ${result.testsFound} test file(s)`);
328
+ }
329
+ const totalTests = results.reduce((sum, r) => sum + r.testsFound, 0);
330
+ console.log(`\n✓ Total: ${totalTests} test file(s) found across ${results.length} project(s)`);
331
+ // Scan ports for all projects
332
+ console.log('\nScanning ports for all projects...');
333
+ try {
334
+ const { scanAllProjectPorts } = await Promise.resolve().then(() => __importStar(require('./port-scanner')));
335
+ await scanAllProjectPorts();
336
+ console.log('✓ Port scanning completed');
337
+ }
338
+ catch (error) {
339
+ console.log(' Port scanning failed');
340
+ }
341
+ }
342
+ }
343
+ catch (error) {
344
+ console.error('Error scanning projects:', error instanceof Error ? error.message : error);
345
+ process.exit(1);
346
+ }
347
+ });
348
+ // Rename command
349
+ program
350
+ .command('rn')
351
+ .alias('rename')
352
+ .description('Rename a project')
353
+ .argument('<project>', 'Project ID or name to rename')
354
+ .argument('<newName>', 'New name for the project')
355
+ .action((projectIdentifier, newName) => {
356
+ try {
357
+ const db = (0, core_1.getDatabaseManager)();
358
+ const projects = (0, core_1.getAllProjects)();
359
+ const project = projects.find(p => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
360
+ if (!project) {
361
+ console.error(`Error: Project not found: ${projectIdentifier}`);
362
+ process.exit(1);
363
+ }
364
+ if (!newName || !newName.trim()) {
365
+ console.error('Error: New name cannot be empty');
366
+ process.exit(1);
367
+ }
368
+ const trimmedName = newName.trim();
369
+ const updated = db.updateProjectName(project.id, trimmedName);
370
+ console.log(`✓ Renamed project from "${project.name}" to "${updated.name}"`);
371
+ }
372
+ catch (error) {
373
+ console.error('Error renaming project:', error instanceof Error ? error.message : error);
374
+ process.exit(1);
375
+ }
376
+ });
377
+ // Remove command
378
+ program
379
+ .command('remove')
380
+ .description('Remove a project from the dashboard')
381
+ .argument('<project>', 'Project ID or name to remove')
382
+ .option('-f, --force', 'Skip confirmation')
383
+ .action(async (projectIdentifier, options) => {
384
+ try {
385
+ const db = (0, core_1.getDatabaseManager)();
386
+ const projects = (0, core_1.getAllProjects)();
387
+ const project = projects.find(p => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
388
+ if (!project) {
389
+ console.error(`Error: Project not found: ${projectIdentifier}`);
390
+ process.exit(1);
391
+ }
392
+ if (!options.force) {
393
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
394
+ const answer = await inquirer.prompt([
395
+ {
396
+ type: 'confirm',
397
+ name: 'confirm',
398
+ message: `Are you sure you want to remove "${project.name}"?`,
399
+ default: false,
400
+ },
401
+ ]);
402
+ if (!answer.confirm) {
403
+ console.log('Cancelled.');
404
+ return;
405
+ }
406
+ }
407
+ (0, core_1.removeProject)(project.id);
408
+ console.log(`✓ Removed project: ${project.name}`);
409
+ }
410
+ catch (error) {
411
+ console.error('Error removing project:', error instanceof Error ? error.message : error);
412
+ process.exit(1);
413
+ }
414
+ });
415
+ // Scripts command - list available scripts for a project
416
+ program
417
+ .command('scripts')
418
+ .description('List available scripts for a project')
419
+ .argument('[project]', 'Project ID or name (leave empty for interactive selection)')
420
+ .action(async (projectIdentifier) => {
421
+ try {
422
+ const projects = (0, core_1.getAllProjects)();
423
+ if (projects.length === 0) {
424
+ console.error('Error: No projects tracked yet. Use "prx add" to add a project.');
425
+ process.exit(1);
426
+ }
427
+ let project;
428
+ if (projectIdentifier) {
429
+ project = projects.find((p) => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
430
+ if (!project) {
431
+ console.error(`Error: Project not found: ${projectIdentifier}`);
432
+ process.exit(1);
433
+ }
434
+ }
435
+ else {
436
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
437
+ const answer = await inquirer.prompt([
438
+ {
439
+ type: 'list',
440
+ name: 'project',
441
+ message: 'Select a project:',
442
+ choices: projects.map((p) => ({
443
+ name: `${p.id}. ${p.name} (${p.path})`,
444
+ value: p,
445
+ })),
446
+ },
447
+ ]);
448
+ project = answer.project;
449
+ }
450
+ if (!project) {
451
+ console.error('Error: No project selected');
452
+ process.exit(1);
453
+ }
454
+ if (!fs.existsSync(project.path)) {
455
+ console.error(`Error: Project path does not exist: ${project.path}`);
456
+ process.exit(1);
457
+ }
458
+ const projectScripts = (0, script_runner_1.getProjectScripts)(project.path);
459
+ console.log(`\nAvailable scripts for "${project.name}":`);
460
+ console.log(`Project type: ${projectScripts.type}`);
461
+ console.log(`Path: ${project.path}\n`);
462
+ if (projectScripts.scripts.size === 0) {
463
+ console.log('No scripts found in this project.');
464
+ }
465
+ else {
466
+ projectScripts.scripts.forEach((script) => {
467
+ console.log(` ${script.name}`);
468
+ console.log(` Command: ${script.command}`);
469
+ console.log(` Runner: ${script.runner}`);
470
+ console.log('');
471
+ });
472
+ }
473
+ }
474
+ catch (error) {
475
+ console.error('Error listing scripts:', error instanceof Error ? error.message : error);
476
+ process.exit(1);
477
+ }
478
+ });
479
+ // PWD command - get the path to a project directory
480
+ program
481
+ .command('pwd')
482
+ .description('Get the path to a project directory (use with: cd $(prx pwd <project>))')
483
+ .argument('[project]', 'Project ID or name (leave empty for interactive selection)')
484
+ .action(async (projectIdentifier) => {
485
+ try {
486
+ const projects = (0, core_1.getAllProjects)();
487
+ if (projects.length === 0) {
488
+ console.error('Error: No projects tracked yet. Use "prx add" to add a project.');
489
+ process.exit(1);
490
+ }
491
+ let project;
492
+ if (projectIdentifier) {
493
+ // Find project by ID or name
494
+ project = projects.find((p) => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
495
+ if (!project) {
496
+ console.error(`Error: Project not found: ${projectIdentifier}`);
497
+ process.exit(1);
498
+ }
499
+ }
500
+ else {
501
+ // Interactive selection
502
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
503
+ const answer = await inquirer.prompt([
504
+ {
505
+ type: 'list',
506
+ name: 'project',
507
+ message: 'Select a project:',
508
+ choices: projects.map((p) => ({
509
+ name: `${p.id}. ${p.name} (${p.path})`,
510
+ value: p,
511
+ })),
512
+ },
513
+ ]);
514
+ project = answer.project;
515
+ }
516
+ if (!project) {
517
+ console.error('Error: No project selected');
518
+ process.exit(1);
519
+ }
520
+ // Output only the path (for use with command substitution)
521
+ // This allows: cd $(prx pwd <project>)
522
+ console.log(project.path);
523
+ }
524
+ catch (error) {
525
+ console.error('Error getting project path:', error instanceof Error ? error.message : error);
526
+ process.exit(1);
527
+ }
528
+ });
529
+ // CD command - change to project directory (outputs shell command for eval)
530
+ program
531
+ .command('cd')
532
+ .description('Change to a project directory (use with: eval $(prx cd <project>))')
533
+ .argument('[project]', 'Project ID or name (leave empty for interactive selection)')
534
+ .action(async (projectIdentifier) => {
535
+ try {
536
+ const projects = (0, core_1.getAllProjects)();
537
+ if (projects.length === 0) {
538
+ console.error('Error: No projects tracked yet. Use "prx add" to add a project.');
539
+ process.exit(1);
540
+ }
541
+ let project;
542
+ if (projectIdentifier) {
543
+ // Find project by ID or name
544
+ project = projects.find((p) => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
545
+ if (!project) {
546
+ console.error(`Error: Project not found: ${projectIdentifier}`);
547
+ process.exit(1);
548
+ }
549
+ }
550
+ else {
551
+ // Interactive selection
552
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
553
+ const answer = await inquirer.prompt([
554
+ {
555
+ type: 'list',
556
+ name: 'project',
557
+ message: 'Select a project:',
558
+ choices: projects.map((p) => ({
559
+ name: `${p.id}. ${p.name} (${p.path})`,
560
+ value: p,
561
+ })),
562
+ },
563
+ ]);
564
+ project = answer.project;
565
+ }
566
+ if (!project) {
567
+ console.error('Error: No project selected');
568
+ process.exit(1);
569
+ }
570
+ // Output a shell command that can be evaluated to change directory
571
+ // This allows: eval $(prx cd <project>)
572
+ // Or create a shell function: prxcd() { eval $(prx cd "$@"); }
573
+ const escapedPath = project.path.replace(/'/g, "'\\''");
574
+ console.log(`cd '${escapedPath}'`);
575
+ }
576
+ catch (error) {
577
+ console.error('Error changing to project directory:', error instanceof Error ? error.message : error);
578
+ process.exit(1);
579
+ }
580
+ });
581
+ // Start Electron UI command
582
+ program
583
+ .command('web')
584
+ .description('Start the Electron web interface')
585
+ .option('--dev', 'Start in development mode (with hot reload)')
586
+ .action(async (options) => {
587
+ try {
588
+ // Check for bundled Electron app first (in dist/electron when installed globally)
589
+ // Then check for local development (packages/cli/dist -> packages/electron)
590
+ const bundledElectronPath = path.join(__dirname, 'electron');
591
+ const bundledElectronMain = path.join(bundledElectronPath, 'main.js');
592
+ const localElectronPath = path.join(__dirname, '..', '..', 'electron');
593
+ const localElectronMain = path.join(localElectronPath, 'dist', 'main.js');
594
+ // Check if bundled electron exists (global install)
595
+ const hasBundledElectron = fs.existsSync(bundledElectronMain);
596
+ // Check if local electron exists (development mode)
597
+ const isLocalDev = fs.existsSync(localElectronPath) && fs.existsSync(path.join(localElectronPath, 'package.json'));
598
+ let electronPackagePath;
599
+ let electronMainPath;
600
+ if (hasBundledElectron) {
601
+ // Bundled Electron app (global install)
602
+ electronPackagePath = bundledElectronPath;
603
+ electronMainPath = bundledElectronMain;
604
+ }
605
+ else if (isLocalDev) {
606
+ // Local development - use relative path
607
+ electronPackagePath = localElectronPath;
608
+ electronMainPath = localElectronMain;
609
+ }
610
+ else {
611
+ console.error('Error: Electron app not found.');
612
+ console.error('\nThe Electron web interface is not available.');
613
+ console.error('This may be a packaging issue. Please report this error.');
614
+ process.exit(1);
615
+ }
616
+ if (options.dev) {
617
+ // Development mode - start Vite dev server and Electron
618
+ if (!isLocalDev) {
619
+ console.error('Error: Development mode is only available in local development.');
620
+ console.error('The Electron app must be built for production use.');
621
+ process.exit(1);
622
+ }
623
+ console.log('Starting Electron app in development mode...');
624
+ console.log('Starting Vite dev server on port 7898...');
625
+ const { spawn } = require('child_process');
626
+ const electron = require('electron');
627
+ // Start Vite dev server in background
628
+ const viteProcess = spawn('npm', ['run', 'dev:renderer'], {
629
+ cwd: electronPackagePath,
630
+ stdio: 'pipe',
631
+ detached: false,
632
+ });
633
+ viteProcess.stdout.on('data', (data) => {
634
+ const output = data.toString();
635
+ process.stdout.write(output);
636
+ // Wait for Vite to be ready
637
+ if (output.includes('Local:') || output.includes('ready')) {
638
+ setTimeout(() => {
639
+ console.log('\nStarting Electron window...');
640
+ spawn(electron, [electronMainPath], {
641
+ stdio: 'inherit',
642
+ detached: true,
643
+ env: { ...process.env, NODE_ENV: 'development' },
644
+ }).unref();
645
+ }, 2000);
646
+ }
647
+ });
648
+ viteProcess.stderr.on('data', (data) => {
649
+ process.stderr.write(data);
650
+ });
651
+ // Keep the process alive
652
+ process.on('SIGINT', () => {
653
+ viteProcess.kill();
654
+ process.exit(0);
655
+ });
656
+ return;
657
+ }
658
+ // Production mode - check if built
659
+ if (!fs.existsSync(electronMainPath)) {
660
+ if (!isLocalDev) {
661
+ console.error('Error: Electron app is not built.');
662
+ console.error('The @projax/electron package needs to be built.');
663
+ console.error('Please contact the package maintainer or build it locally.');
664
+ process.exit(1);
665
+ }
666
+ console.log('Electron app not built.');
667
+ console.log('Building Electron app...');
668
+ const { execSync } = require('child_process');
669
+ try {
670
+ // When in local dev, electronPackagePath points to packages/electron
671
+ // So go up two levels to get to project root
672
+ const projectRoot = path.join(electronPackagePath, '..', '..');
673
+ execSync('npm run build:electron', {
674
+ cwd: projectRoot,
675
+ stdio: 'inherit'
676
+ });
677
+ }
678
+ catch (error) {
679
+ console.error('\nBuild failed. Try running in dev mode: prx web --dev');
680
+ console.error('Or manually build: npm run build:electron');
681
+ process.exit(1);
682
+ }
683
+ }
684
+ // Check if renderer is built
685
+ let rendererIndex;
686
+ if (hasBundledElectron) {
687
+ // Bundled: renderer is in dist/electron/renderer
688
+ rendererIndex = path.join(electronPackagePath, 'renderer', 'index.html');
689
+ }
690
+ else {
691
+ // Local dev: renderer is in dist/renderer
692
+ rendererIndex = path.join(electronPackagePath, 'dist', 'renderer', 'index.html');
693
+ }
694
+ if (!fs.existsSync(rendererIndex)) {
695
+ if (hasBundledElectron) {
696
+ console.error('Error: Renderer files not found in bundled Electron app.');
697
+ console.error('This is a packaging issue. Please report this error.');
698
+ process.exit(1);
699
+ }
700
+ else {
701
+ console.log('Renderer not built. Starting in dev mode...');
702
+ process.env.NODE_ENV = 'development';
703
+ }
704
+ }
705
+ else {
706
+ // Ensure NODE_ENV is not set to development when using bundled files
707
+ process.env.NODE_ENV = 'production';
708
+ }
709
+ // Automatically rebuild better-sqlite3 for Electron if needed
710
+ // This ensures it's compiled for Electron's Node.js version
711
+ if (hasBundledElectron) {
712
+ try {
713
+ const electronPkg = require('electron/package.json');
714
+ let rebuild;
715
+ try {
716
+ rebuild = require('@electron/rebuild').rebuild;
717
+ }
718
+ catch {
719
+ // @electron/rebuild not available, skip auto-rebuild
720
+ }
721
+ if (rebuild) {
722
+ const sqlitePath = require.resolve('better-sqlite3');
723
+ // Find the package root - better-sqlite3 is in projax/node_modules/better-sqlite3
724
+ // Path structure: projax/node_modules/better-sqlite3/lib/index.js
725
+ // So we need to go: sqlitePath -> lib -> better-sqlite3 -> node_modules -> projax (package root)
726
+ const sqliteDir = path.dirname(sqlitePath); // .../better-sqlite3/lib
727
+ const betterSqlite3Dir = path.dirname(sqliteDir); // .../better-sqlite3
728
+ const nodeModulesDir = path.dirname(betterSqlite3Dir); // .../node_modules
729
+ // The package root is the directory containing node_modules (i.e., projax package root)
730
+ const packageRoot = path.dirname(nodeModulesDir); // .../projax
731
+ console.log('Ensuring better-sqlite3 is built for Electron...');
732
+ try {
733
+ await new Promise((resolve, reject) => {
734
+ rebuild({
735
+ buildPath: packageRoot,
736
+ electronVersion: electronPkg.version,
737
+ onlyModules: ['better-sqlite3'],
738
+ force: true,
739
+ })
740
+ .then(() => {
741
+ console.log('✓ better-sqlite3 ready for Electron');
742
+ resolve();
743
+ })
744
+ .catch((err) => {
745
+ // Don't fail if rebuild has issues, just warn
746
+ console.warn('⚠️ Could not rebuild better-sqlite3 automatically:', err.message);
747
+ console.warn(' The app may still work, but if you see errors, run:');
748
+ console.warn(` cd ${packageRoot}`);
749
+ console.warn(' npm rebuild better-sqlite3 --build-from-source');
750
+ resolve(); // Continue anyway
751
+ });
752
+ });
753
+ }
754
+ catch (rebuildError) {
755
+ // Continue even if rebuild fails
756
+ console.warn('⚠️ Rebuild check failed, continuing anyway...');
757
+ }
758
+ }
759
+ }
760
+ catch (checkError) {
761
+ // If we can't check, just continue - the error will show when Electron tries to use it
762
+ }
763
+ }
764
+ console.log('Starting Electron app...');
765
+ const { spawn } = require('child_process');
766
+ const electron = require('electron');
767
+ spawn(electron, [electronMainPath], {
768
+ stdio: 'inherit',
769
+ detached: true,
770
+ env: { ...process.env },
771
+ }).unref();
772
+ }
773
+ catch (error) {
774
+ console.error('Error starting Electron app:', error instanceof Error ? error.message : error);
775
+ console.log('\nTroubleshooting:');
776
+ console.log('1. Try dev mode: prx web --dev');
777
+ console.log('2. Or build manually: npm run build:electron');
778
+ console.log('3. Or run dev server: cd packages/electron && npm run dev');
779
+ process.exit(1);
780
+ }
781
+ });
782
+ // Scan-ports command
783
+ program
784
+ .command('scan-ports')
785
+ .description('Scan projects for port information')
786
+ .argument('[project]', 'Project ID or name to scan (leave empty to scan all)')
787
+ .action(async (projectIdentifier) => {
788
+ try {
789
+ const { scanProjectPorts, scanAllProjectPorts } = await Promise.resolve().then(() => __importStar(require('./port-scanner')));
790
+ const db = (0, core_1.getDatabaseManager)();
791
+ if (projectIdentifier) {
792
+ const projects = (0, core_1.getAllProjects)();
793
+ const project = projects.find((p) => p.id.toString() === projectIdentifier || p.name === projectIdentifier);
794
+ if (!project) {
795
+ console.error(`Error: Project not found: ${projectIdentifier}`);
796
+ process.exit(1);
797
+ }
798
+ console.log(`Scanning ports for project: ${project.name}...`);
799
+ await scanProjectPorts(project.id);
800
+ const ports = db.getProjectPorts(project.id);
801
+ if (ports.length > 0) {
802
+ console.log(`✓ Found ${ports.length} port(s)`);
803
+ ports.forEach(port => {
804
+ const scriptLabel = port.script_name ? ` (${port.script_name})` : '';
805
+ console.log(` Port ${port.port}${scriptLabel} - ${port.config_source}`);
806
+ });
807
+ }
808
+ else {
809
+ console.log(' No ports detected');
810
+ }
811
+ }
812
+ else {
813
+ console.log('Scanning ports for all projects...\n');
814
+ await scanAllProjectPorts();
815
+ const projects = (0, core_1.getAllProjects)();
816
+ for (const project of projects) {
817
+ const ports = db.getProjectPorts(project.id);
818
+ if (ports.length > 0) {
819
+ const portList = ports.map(p => p.port).sort((a, b) => a - b).join(', ');
820
+ console.log(`${project.name}: ${portList}`);
821
+ }
822
+ }
823
+ console.log('\n✓ Port scanning completed');
824
+ }
825
+ }
826
+ catch (error) {
827
+ console.error('Error scanning ports:', error instanceof Error ? error.message : error);
828
+ process.exit(1);
829
+ }
830
+ });
831
+ // Handle script execution before parsing
832
+ // Check if first argument is not a known command
833
+ (async () => {
834
+ const args = process.argv.slice(2);
835
+ const knownCommands = ['add', 'list', 'scan', 'remove', 'rn', 'rename', 'cd', 'pwd', 'web', 'scripts', 'scan-ports', '--help', '-h', '--version', '-v'];
836
+ // If we have at least 1 argument and first is not a known command, treat as project identifier
837
+ if (args.length >= 1 && !knownCommands.includes(args[0])) {
838
+ const projectIdentifier = args[0];
839
+ // Check if it's actually a project (not a flag)
840
+ if (!projectIdentifier.startsWith('-')) {
841
+ try {
842
+ const projects = (0, core_1.getAllProjects)();
843
+ // Try to find project by ID (if identifier is numeric) or by name
844
+ let project;
845
+ const numericId = parseInt(projectIdentifier, 10);
846
+ if (!isNaN(numericId)) {
847
+ // Try to find by numeric ID first
848
+ project = projects.find((p) => p.id === numericId);
849
+ }
850
+ // If not found by ID, try by name
851
+ if (!project) {
852
+ project = projects.find((p) => p.name === projectIdentifier);
853
+ }
854
+ if (project) {
855
+ // Found a project
856
+ if (!fs.existsSync(project.path)) {
857
+ console.error(`Error: Project path does not exist: ${project.path}`);
858
+ process.exit(1);
859
+ }
860
+ const projectScripts = (0, script_runner_1.getProjectScripts)(project.path);
861
+ if (projectScripts.scripts.size === 0) {
862
+ console.error(`Error: No scripts found in project "${project.name}"`);
863
+ console.error(`Project type: ${projectScripts.type}`);
864
+ console.error(`Path: ${project.path}`);
865
+ process.exit(1);
866
+ }
867
+ // Check for background mode flags (-M, --background, -b, --daemon)
868
+ const backgroundFlags = ['-M', '--background', '-b', '--daemon'];
869
+ const isBackgroundMode = args.some(arg => backgroundFlags.includes(arg));
870
+ // Check for force flags (--force, -F)
871
+ const forceFlags = ['--force', '-F'];
872
+ const isForceMode = args.some(arg => forceFlags.includes(arg));
873
+ // Filter out background and force flags from args
874
+ const filteredArgs = args.filter(arg => !backgroundFlags.includes(arg) && !forceFlags.includes(arg));
875
+ // If script name is provided (filteredArgs.length >= 2), run that script
876
+ if (filteredArgs.length >= 2) {
877
+ const scriptName = filteredArgs[1];
878
+ const scriptArgs = filteredArgs.slice(2);
879
+ // Check if script exists
880
+ if (!projectScripts.scripts.has(scriptName)) {
881
+ console.error(`Error: Script "${scriptName}" not found in project "${project.name}"`);
882
+ console.error(`\nAvailable scripts:`);
883
+ projectScripts.scripts.forEach((script) => {
884
+ console.error(` ${script.name}`);
885
+ });
886
+ process.exit(1);
887
+ }
888
+ // Run the script (in background or foreground)
889
+ try {
890
+ if (isBackgroundMode) {
891
+ await (0, script_runner_1.runScriptInBackground)(project.path, project.name, scriptName, scriptArgs, isForceMode);
892
+ }
893
+ else {
894
+ await (0, script_runner_1.runScript)(project.path, scriptName, scriptArgs, isForceMode);
895
+ }
896
+ process.exit(0);
897
+ }
898
+ catch (error) {
899
+ console.error('Error running script:', error instanceof Error ? error.message : error);
900
+ process.exit(1);
901
+ }
902
+ }
903
+ else {
904
+ // No script name provided - intelligent script selection
905
+ const hasStart = projectScripts.scripts.has('start');
906
+ const hasDev = projectScripts.scripts.has('dev');
907
+ let selectedScript;
908
+ // Rule 1: If "start" exists but "dev" doesn't, run "start"
909
+ if (hasStart && !hasDev) {
910
+ selectedScript = 'start';
911
+ }
912
+ // Rule 2: If "dev" exists but "start" doesn't, run "dev"
913
+ else if (hasDev && !hasStart) {
914
+ selectedScript = 'dev';
915
+ }
916
+ // Rule 3: If neither case applies, show interactive selection
917
+ else {
918
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
919
+ const scriptChoices = Array.from(projectScripts.scripts.values()).map((script) => ({
920
+ name: `${script.name} - ${script.command}`,
921
+ value: script.name,
922
+ }));
923
+ const answer = await inquirer.prompt([
924
+ {
925
+ type: 'list',
926
+ name: 'script',
927
+ message: `Select a script to run for "${project.name}":`,
928
+ choices: scriptChoices,
929
+ },
930
+ ]);
931
+ selectedScript = answer.script;
932
+ }
933
+ if (selectedScript) {
934
+ try {
935
+ if (isBackgroundMode) {
936
+ await (0, script_runner_1.runScriptInBackground)(project.path, project.name, selectedScript, [], isForceMode);
937
+ }
938
+ else {
939
+ await (0, script_runner_1.runScript)(project.path, selectedScript, [], isForceMode);
940
+ }
941
+ process.exit(0);
942
+ }
943
+ catch (error) {
944
+ console.error('Error running script:', error instanceof Error ? error.message : error);
945
+ process.exit(1);
946
+ }
947
+ }
948
+ else {
949
+ console.error('Error: No script selected');
950
+ process.exit(1);
951
+ }
952
+ }
953
+ }
954
+ else {
955
+ // Project not found - show helpful error
956
+ console.error(`Error: Project not found: ${projectIdentifier}`);
957
+ console.error(`\nAvailable projects:`);
958
+ projects.forEach((p) => {
959
+ console.error(` ${p.id}. ${p.name}`);
960
+ });
961
+ process.exit(1);
962
+ }
963
+ }
964
+ catch (error) {
965
+ // If there's an error, fall through to normal command parsing
966
+ }
967
+ }
968
+ }
969
+ // If we get here, proceed with normal command parsing
970
+ program.parse();
971
+ })();