projax 1.3.8 → 1.3.10

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.
@@ -40,6 +40,7 @@ exports.loadProcesses = loadProcesses;
40
40
  exports.removeProcess = removeProcess;
41
41
  exports.getProjectProcesses = getProjectProcesses;
42
42
  exports.getRunningProcesses = getRunningProcesses;
43
+ exports.getRunningProcessesClean = getRunningProcessesClean;
43
44
  exports.stopScript = stopScript;
44
45
  exports.stopScriptByPort = stopScriptByPort;
45
46
  exports.stopProjectProcesses = stopProjectProcesses;
@@ -532,11 +533,61 @@ function getProjectProcesses(projectPath) {
532
533
  return processes.filter(p => p.projectPath === projectPath);
533
534
  }
534
535
  /**
535
- * Get all running processes
536
+ * Check if a process is still running
537
+ */
538
+ async function isProcessRunning(pid) {
539
+ try {
540
+ if (os.platform() === 'win32') {
541
+ const { exec } = require('child_process');
542
+ const { promisify } = require('util');
543
+ const execAsync = promisify(exec);
544
+ const { stdout } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`);
545
+ return stdout.includes(`"${pid}"`);
546
+ }
547
+ else {
548
+ // On Unix-like systems, kill with signal 0 checks if process exists
549
+ try {
550
+ process.kill(pid, 0);
551
+ return true;
552
+ }
553
+ catch {
554
+ return false;
555
+ }
556
+ }
557
+ }
558
+ catch {
559
+ return false;
560
+ }
561
+ }
562
+ /**
563
+ * Clean up dead processes from tracking
564
+ */
565
+ async function cleanupDeadProcesses() {
566
+ const processes = loadProcesses();
567
+ const aliveProcesses = [];
568
+ for (const proc of processes) {
569
+ const isAlive = await isProcessRunning(proc.pid);
570
+ if (isAlive) {
571
+ aliveProcesses.push(proc);
572
+ }
573
+ }
574
+ if (aliveProcesses.length !== processes.length) {
575
+ saveProcesses(aliveProcesses);
576
+ }
577
+ }
578
+ /**
579
+ * Get all running processes (synchronous version that returns potentially stale data)
536
580
  */
537
581
  function getRunningProcesses() {
538
582
  return loadProcesses();
539
583
  }
584
+ /**
585
+ * Get all running processes with cleanup (async version)
586
+ */
587
+ async function getRunningProcessesClean() {
588
+ await cleanupDeadProcesses();
589
+ return loadProcesses();
590
+ }
540
591
  /**
541
592
  * Extract URLs from text output
542
593
  */
@@ -744,35 +795,43 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
744
795
  fs.mkdirSync(logsDir, { recursive: true });
745
796
  }
746
797
  const logFile = path.join(logsDir, `process-${Date.now()}-${scriptName}.log`);
747
- // Create write stream for log file (keep reference to prevent GC)
748
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
798
+ // Open log file with file descriptors that can be inherited by child process
799
+ const logFd = fs.openSync(logFile, 'a');
749
800
  // Spawn process in detached mode with output redirected to log file
750
801
  let child;
751
802
  try {
752
803
  child = (0, child_process_1.spawn)(command, commandArgs, {
753
804
  cwd: projectPath,
754
- stdio: ['ignore', logStream, logStream], // Redirect stdout and stderr to log file
805
+ stdio: ['ignore', logFd, logFd], // Redirect stdout and stderr to log file descriptor
755
806
  detached: true,
756
807
  shell: process.platform === 'win32',
757
808
  });
758
809
  }
759
810
  catch (spawnError) {
760
- logStream.end();
811
+ fs.closeSync(logFd);
761
812
  reject(new Error(`Failed to spawn process: ${spawnError instanceof Error ? spawnError.message : String(spawnError)}`));
762
813
  return;
763
814
  }
764
815
  if (!child.pid) {
765
- logStream.end();
816
+ fs.closeSync(logFd);
766
817
  reject(new Error('Failed to start process: no PID assigned'));
767
818
  return;
768
819
  }
769
820
  // Handle spawn errors
770
821
  child.on('error', (error) => {
771
- logStream.end();
822
+ fs.closeSync(logFd);
772
823
  reject(new Error(`Process spawn error: ${error.message}`));
773
824
  });
774
- // Don't close the stream - let it stay open for the child process
775
- // The stream will be closed when the child process exits
825
+ // Close the file descriptor in parent process after a short delay
826
+ // The child process will have its own copy
827
+ setTimeout(() => {
828
+ try {
829
+ fs.closeSync(logFd);
830
+ }
831
+ catch {
832
+ // Already closed or error, ignore
833
+ }
834
+ }, 1000);
776
835
  // Store process info
777
836
  const processInfo = {
778
837
  pid: child.pid,
@@ -784,6 +843,18 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
784
843
  logFile,
785
844
  };
786
845
  addProcess(processInfo);
846
+ // Listen for process exit to clean up tracking
847
+ child.on('exit', (code, signal) => {
848
+ // Remove from tracking when process exits
849
+ removeProcess(child.pid);
850
+ // Log exit information
851
+ if (code !== null) {
852
+ fs.appendFileSync(logFile, `\n\n[Process exited with code ${code}]\n`);
853
+ }
854
+ else if (signal !== null) {
855
+ fs.appendFileSync(logFile, `\n\n[Process killed by signal ${signal}]\n`);
856
+ }
857
+ });
787
858
  // Unref so parent can exit and process runs independently
788
859
  child.unref();
789
860
  // Show minimal output
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
38
  const path = __importStar(require("path"));
39
39
  const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
40
41
  const http = __importStar(require("http"));
41
42
  const core_bridge_1 = require("./core-bridge");
42
43
  const script_runner_1 = require("./script-runner");
@@ -724,7 +725,7 @@ program
724
725
  // CD command - change to project directory (outputs shell command for eval)
725
726
  program
726
727
  .command('cd')
727
- .description('Change to a project directory (use with: eval $(prx cd <project>))')
728
+ .description('Change to a project directory')
728
729
  .argument('[project]', 'Project ID or name (leave empty for interactive selection)')
729
730
  .action(async (projectIdentifier) => {
730
731
  try {
@@ -762,17 +763,117 @@ program
762
763
  console.error('Error: No project selected');
763
764
  process.exit(1);
764
765
  }
765
- // Output a shell command that can be evaluated to change directory
766
- // This allows: eval $(prx cd <project>)
767
- // Or create a shell function: prxcd() { eval $(prx cd "$@"); }
766
+ // Output a shell command that changes directory
767
+ // To use: eval "$(prx cd <project>)"
768
768
  const escapedPath = project.path.replace(/'/g, "'\\''");
769
- console.log(`cd '${escapedPath}'`);
769
+ console.log(`cd '${escapedPath}' && echo "Changed to: ${project.name}"`);
770
770
  }
771
771
  catch (error) {
772
772
  console.error('Error changing to project directory:', error instanceof Error ? error.message : error);
773
773
  process.exit(1);
774
774
  }
775
775
  });
776
+ // Run script command
777
+ program
778
+ .command('run <project> <script>')
779
+ .description('Run a script from a project')
780
+ .option('-b, --background', 'Run script in background')
781
+ .option('-f, --force', 'Force run (kill conflicting processes on ports)')
782
+ .action(async (projectIdentifier, scriptName, options) => {
783
+ try {
784
+ const projects = (0, core_bridge_1.getAllProjects)();
785
+ // Find project by ID or name
786
+ let project;
787
+ const numericId = parseInt(projectIdentifier, 10);
788
+ if (!isNaN(numericId)) {
789
+ project = projects.find((p) => p.id === numericId);
790
+ }
791
+ if (!project) {
792
+ project = projects.find((p) => p.name === projectIdentifier);
793
+ }
794
+ if (!project) {
795
+ console.error(`Error: Project not found: ${projectIdentifier}`);
796
+ process.exit(1);
797
+ }
798
+ const projectScripts = (0, script_runner_1.getProjectScripts)(project.path);
799
+ if (!projectScripts.scripts.has(scriptName)) {
800
+ console.error(`Error: Script "${scriptName}" not found in project "${project.name}"`);
801
+ console.error(`\nAvailable scripts:`);
802
+ projectScripts.scripts.forEach((script) => {
803
+ console.error(` ${script.name}`);
804
+ });
805
+ process.exit(1);
806
+ }
807
+ if (options.background) {
808
+ await (0, script_runner_1.runScriptInBackground)(project.path, project.name, scriptName, [], options.force || false);
809
+ }
810
+ else {
811
+ await (0, script_runner_1.runScript)(project.path, scriptName, [], options.force || false);
812
+ }
813
+ }
814
+ catch (error) {
815
+ console.error('Error running script:', error instanceof Error ? error.message : error);
816
+ process.exit(1);
817
+ }
818
+ });
819
+ // List running processes command
820
+ program
821
+ .command('ps')
822
+ .description('List running background processes')
823
+ .action(async () => {
824
+ try {
825
+ const { getRunningProcessesClean } = await Promise.resolve().then(() => __importStar(require('./script-runner')));
826
+ const processes = await getRunningProcessesClean();
827
+ if (processes.length === 0) {
828
+ console.log('No running background processes.');
829
+ return;
830
+ }
831
+ console.log(`\nRunning processes (${processes.length}):\n`);
832
+ for (const proc of processes) {
833
+ const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
834
+ const minutes = Math.floor(uptime / 60);
835
+ const seconds = uptime % 60;
836
+ const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
837
+ console.log(` PID ${proc.pid}: ${proc.projectName} (${proc.scriptName}) - ${uptimeStr}`);
838
+ console.log(` Command: ${proc.command}`);
839
+ console.log(` Logs: ${proc.logFile}`);
840
+ if (proc.detectedUrls && proc.detectedUrls.length > 0) {
841
+ console.log(` URLs: ${proc.detectedUrls.join(', ')}`);
842
+ }
843
+ console.log('');
844
+ }
845
+ }
846
+ catch (error) {
847
+ console.error('Error listing processes:', error instanceof Error ? error.message : error);
848
+ process.exit(1);
849
+ }
850
+ });
851
+ // Stop process command
852
+ program
853
+ .command('stop <pid>')
854
+ .description('Stop a running background process')
855
+ .action(async (pidStr) => {
856
+ try {
857
+ const pid = parseInt(pidStr, 10);
858
+ if (isNaN(pid)) {
859
+ console.error('Error: Invalid PID');
860
+ process.exit(1);
861
+ }
862
+ const { stopScript } = await Promise.resolve().then(() => __importStar(require('./script-runner')));
863
+ const success = await stopScript(pid);
864
+ if (success) {
865
+ console.log(`✓ Stopped process ${pid}`);
866
+ }
867
+ else {
868
+ console.error(`Failed to stop process ${pid}`);
869
+ process.exit(1);
870
+ }
871
+ }
872
+ catch (error) {
873
+ console.error('Error stopping process:', error instanceof Error ? error.message : error);
874
+ process.exit(1);
875
+ }
876
+ });
776
877
  // Start Desktop UI command
777
878
  program
778
879
  .command('web')
@@ -782,6 +883,25 @@ program
782
883
  .option('--dev', 'Start in development mode (with hot reload)')
783
884
  .action(async (options) => {
784
885
  try {
886
+ // Clear Electron cache to prevent stale module issues
887
+ if (os.platform() === 'darwin') {
888
+ const cacheDirs = [
889
+ path.join(os.homedir(), 'Library', 'Application Support', 'Electron', 'Cache'),
890
+ path.join(os.homedir(), 'Library', 'Application Support', 'Electron', 'Code Cache'),
891
+ path.join(os.homedir(), 'Library', 'Application Support', 'Electron', 'GPUCache'),
892
+ path.join(os.homedir(), 'Library', 'Caches', 'Electron'),
893
+ ];
894
+ for (const dir of cacheDirs) {
895
+ try {
896
+ if (fs.existsSync(dir)) {
897
+ fs.rmSync(dir, { recursive: true, force: true });
898
+ }
899
+ }
900
+ catch (error) {
901
+ // Ignore cache clear errors
902
+ }
903
+ }
904
+ }
785
905
  // Ensure API server is running before starting Desktop app
786
906
  await ensureAPIServerRunning(false);
787
907
  // Check for bundled Desktop app first (in dist/desktop when installed globally)
@@ -1031,7 +1151,7 @@ program
1031
1151
  // Check if first argument is not a known command
1032
1152
  (async () => {
1033
1153
  const args = process.argv.slice(2);
1034
- const knownCommands = ['prxi', 'i', 'add', 'list', 'scan', 'remove', 'rn', 'rename', 'cd', 'pwd', 'web', 'desktop', 'ui', 'scripts', 'scan-ports', 'api', '--help', '-h', '--version', '-V'];
1154
+ const knownCommands = ['prxi', 'i', 'add', 'list', 'scan', 'remove', 'rn', 'rename', 'cd', 'pwd', 'run', 'ps', 'stop', 'web', 'desktop', 'ui', 'scripts', 'scan-ports', 'api', '--help', '-h', '--version', '-V'];
1035
1155
  // If we have at least 1 argument and first is not a known command, treat as project identifier
1036
1156
  if (args.length >= 1 && !knownCommands.includes(args[0])) {
1037
1157
  const projectIdentifier = args[0];
@@ -44,9 +44,13 @@ export declare function removeProcess(pid: number): void;
44
44
  */
45
45
  export declare function getProjectProcesses(projectPath: string): BackgroundProcess[];
46
46
  /**
47
- * Get all running processes
47
+ * Get all running processes (synchronous version that returns potentially stale data)
48
48
  */
49
49
  export declare function getRunningProcesses(): BackgroundProcess[];
50
+ /**
51
+ * Get all running processes with cleanup (async version)
52
+ */
53
+ export declare function getRunningProcessesClean(): Promise<BackgroundProcess[]>;
50
54
  /**
51
55
  * Stop a script by PID
52
56
  */
@@ -40,6 +40,7 @@ exports.loadProcesses = loadProcesses;
40
40
  exports.removeProcess = removeProcess;
41
41
  exports.getProjectProcesses = getProjectProcesses;
42
42
  exports.getRunningProcesses = getRunningProcesses;
43
+ exports.getRunningProcessesClean = getRunningProcessesClean;
43
44
  exports.stopScript = stopScript;
44
45
  exports.stopScriptByPort = stopScriptByPort;
45
46
  exports.stopProjectProcesses = stopProjectProcesses;
@@ -532,11 +533,61 @@ function getProjectProcesses(projectPath) {
532
533
  return processes.filter(p => p.projectPath === projectPath);
533
534
  }
534
535
  /**
535
- * Get all running processes
536
+ * Check if a process is still running
537
+ */
538
+ async function isProcessRunning(pid) {
539
+ try {
540
+ if (os.platform() === 'win32') {
541
+ const { exec } = require('child_process');
542
+ const { promisify } = require('util');
543
+ const execAsync = promisify(exec);
544
+ const { stdout } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`);
545
+ return stdout.includes(`"${pid}"`);
546
+ }
547
+ else {
548
+ // On Unix-like systems, kill with signal 0 checks if process exists
549
+ try {
550
+ process.kill(pid, 0);
551
+ return true;
552
+ }
553
+ catch {
554
+ return false;
555
+ }
556
+ }
557
+ }
558
+ catch {
559
+ return false;
560
+ }
561
+ }
562
+ /**
563
+ * Clean up dead processes from tracking
564
+ */
565
+ async function cleanupDeadProcesses() {
566
+ const processes = loadProcesses();
567
+ const aliveProcesses = [];
568
+ for (const proc of processes) {
569
+ const isAlive = await isProcessRunning(proc.pid);
570
+ if (isAlive) {
571
+ aliveProcesses.push(proc);
572
+ }
573
+ }
574
+ if (aliveProcesses.length !== processes.length) {
575
+ saveProcesses(aliveProcesses);
576
+ }
577
+ }
578
+ /**
579
+ * Get all running processes (synchronous version that returns potentially stale data)
536
580
  */
537
581
  function getRunningProcesses() {
538
582
  return loadProcesses();
539
583
  }
584
+ /**
585
+ * Get all running processes with cleanup (async version)
586
+ */
587
+ async function getRunningProcessesClean() {
588
+ await cleanupDeadProcesses();
589
+ return loadProcesses();
590
+ }
540
591
  /**
541
592
  * Extract URLs from text output
542
593
  */
@@ -744,35 +795,43 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
744
795
  fs.mkdirSync(logsDir, { recursive: true });
745
796
  }
746
797
  const logFile = path.join(logsDir, `process-${Date.now()}-${scriptName}.log`);
747
- // Create write stream for log file (keep reference to prevent GC)
748
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
798
+ // Open log file with file descriptors that can be inherited by child process
799
+ const logFd = fs.openSync(logFile, 'a');
749
800
  // Spawn process in detached mode with output redirected to log file
750
801
  let child;
751
802
  try {
752
803
  child = (0, child_process_1.spawn)(command, commandArgs, {
753
804
  cwd: projectPath,
754
- stdio: ['ignore', logStream, logStream], // Redirect stdout and stderr to log file
805
+ stdio: ['ignore', logFd, logFd], // Redirect stdout and stderr to log file descriptor
755
806
  detached: true,
756
807
  shell: process.platform === 'win32',
757
808
  });
758
809
  }
759
810
  catch (spawnError) {
760
- logStream.end();
811
+ fs.closeSync(logFd);
761
812
  reject(new Error(`Failed to spawn process: ${spawnError instanceof Error ? spawnError.message : String(spawnError)}`));
762
813
  return;
763
814
  }
764
815
  if (!child.pid) {
765
- logStream.end();
816
+ fs.closeSync(logFd);
766
817
  reject(new Error('Failed to start process: no PID assigned'));
767
818
  return;
768
819
  }
769
820
  // Handle spawn errors
770
821
  child.on('error', (error) => {
771
- logStream.end();
822
+ fs.closeSync(logFd);
772
823
  reject(new Error(`Process spawn error: ${error.message}`));
773
824
  });
774
- // Don't close the stream - let it stay open for the child process
775
- // The stream will be closed when the child process exits
825
+ // Close the file descriptor in parent process after a short delay
826
+ // The child process will have its own copy
827
+ setTimeout(() => {
828
+ try {
829
+ fs.closeSync(logFd);
830
+ }
831
+ catch {
832
+ // Already closed or error, ignore
833
+ }
834
+ }, 1000);
776
835
  // Store process info
777
836
  const processInfo = {
778
837
  pid: child.pid,
@@ -784,6 +843,18 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
784
843
  logFile,
785
844
  };
786
845
  addProcess(processInfo);
846
+ // Listen for process exit to clean up tracking
847
+ child.on('exit', (code, signal) => {
848
+ // Remove from tracking when process exits
849
+ removeProcess(child.pid);
850
+ // Log exit information
851
+ if (code !== null) {
852
+ fs.appendFileSync(logFile, `\n\n[Process exited with code ${code}]\n`);
853
+ }
854
+ else if (signal !== null) {
855
+ fs.appendFileSync(logFile, `\n\n[Process killed by signal ${signal}]\n`);
856
+ }
857
+ });
787
858
  // Unref so parent can exit and process runs independently
788
859
  child.unref();
789
860
  // Show minimal output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projax",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "CLI tool for managing local development projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {