PROJAX
Cross-platform project management dashboard for managing local development projects across different tech stacks.
Preview
Key Features
Project List
See all your projects at a glance โ what's running, what has uncommitted changes, what port each is using.
Smart Port Allocation
Automatically finds available ports. Never fight with 'port already in use' again.
Git Status
Branch name, uncommitted files, ahead/behind remote โ without cd-ing into each folder.
Multiple Interfaces
CLI for quick queries, TUI for browsing, desktop app for always-on monitoring, VS Code extension for IDE integration.
Zero Config
Point it at your Developer folder and it figures everything out. No setup required.
Cleanup Tools
Find and delete old node_modules to reclaim disk space.
I have too many projects. Like, way too many.
My Developer folder is a graveyard of good intentions. Half-finished ideas, client work from 2019, experiments that made sense at 2am, and at least three folders called "test" that I'm afraid to delete.
At last count: 50+ projects. Most of them have some combination of uncommitted changes, running dev servers, or dependencies that haven't been updated since the Obama administration.
The Breaking Point
I was working on a project when my port got stolen. Not by a hacker โ by MYSELF. I had a dev server running on port 3000 from some project I'd forgotten about, and when I tried to start my current project, it just failed silently.
Fifteen minutes of debugging later: "Oh, right, I left that other thing running from Tuesday."
This happened at least once a week. I needed a system.
What Projax Does
Think of it as Mission Control for your local development environment.
projax list shows every project, what's running, what has uncommitted changes, and what port each thing is using. At a glance, I can see:
- โขqortr โ Running on port 3000, 3 uncommitted files
- โขcrativo.xyz โ Running on port 3001, clean
- โขthat-thing-from-tuesday โ Not running, but has 47 uncommitted files (oops)
projax run my-project starts the dev server with smart port allocation. If 3000 is taken, it finds the next available port and remembers it.
projax status gives me the big picture: total projects, how many are dirty, what's consumi...
Built With
Impact
npm Downloads
8,700+
Monthly
1,750+
Under the Hood
A peek at the implementation โ the kind of code that powers PROJAX.
1// The port detection that handles 90% of cases
2// (The other 10% are crimes against configuration)
3
4interface PortInfo {
5 configured: number[]; // Explicitly set in config
6 conventional: number[]; // Framework defaults
7 running: number[]; // Currently bound
8}
9
10async function detectPorts(projectPath: string): Promise<PortInfo> {
11 const pkg = await readPackageJson(projectPath);
12 const scripts = pkg?.scripts || {};
13
14 const configured: Set<number> = new Set();
15
16 // 1. Check scripts for --port arguments
17 for (const script of Object.values(scripts)) {
18 const match = script.match(/--port[=s]+(d+)/i);
19 if (match) configured.add(parseInt(match[1]));
20 }
21
22 // 2. Check config files
23 const configPorts = await Promise.all([
24 extractFromViteConfig(projectPath),
25 extractFromNextConfig(projectPath),
26 extractFromWebpackConfig(projectPath),
27 ]);
28 configPorts.flat().forEach(p => configured.add(p));
29
30 // 3. Check .env files
31 const envPorts = await extractFromEnvFiles(projectPath);
32 envPorts.forEach(p => configured.add(p));
33
34 // 4. Apply convention if nothing configured
35 let conventional: number[] = [];
36 if (configured.size === 0) {
37 conventional = inferFromFramework(scripts);
38 }
39
40 // 5. Check what's actually running
41 const allPorts = [...configured, ...conventional];
42 const running = await checkRunningPorts(allPorts, projectPath);
43
44 return {
45 configured: [...configured],
46 conventional,
47 running
48 };
49}
50
51function inferFromFramework(scripts: Record<string, string>): number[] {
52 const text = JSON.stringify(scripts).toLowerCase();
53
54 if (text.includes('vite')) return [5173];
55 if (text.includes('next')) return [3000];
56 if (text.includes('nuxt')) return [3000];
57 if (text.includes('angular')) return [4200];
58 if (text.includes('gatsby')) return [8000];
59 if (text.includes('astro')) return [4321];
60 if (text.includes('remix')) return [3000];
61
62 // Generic Node.js
63 if (text.includes('node ') || text.includes('nodemon')) return [3000];
64
65 return [];
66}
67
68async function checkRunningPorts(ports: number[], projectPath: string): Promise<number[]> {
69 const running: number[] = [];
70
71 for (const port of ports) {
72 try {
73 // lsof tells us what's using the port
74 const { stdout } = await exec(`lsof -i :${port} -t`);
75 if (stdout.trim()) {
76 // Check if it's OUR project
77 const pid = parseInt(stdout.trim().split('\n')[0]);
78 const cwd = await getProcessCwd(pid);
79 if (cwd.includes(projectPath)) {
80 running.push(port);
81 }
82 }
83 } catch {
84 // Port not in use
85 }
86 }
87
88 return running;
89}