├── app_icon.rc ├── dashboard ├── public │ ├── robots.txt │ └── favicon.ico ├── assets │ ├── img │ │ ├── logo.png │ │ ├── logo-2.png │ │ ├── portkill.png │ │ ├── screenshot.png │ │ ├── kage-long-blue.png │ │ ├── kage-long-white.png │ │ └── portkill-dashboard.png │ └── css │ │ └── main.css ├── app.vue ├── .gitignore ├── tsconfig.json ├── package.json ├── tailwind.config.js ├── nuxt.config.ts ├── demo-servers.sh ├── components │ ├── StatsCard.vue │ ├── Sidebar.vue │ └── KillConfirmModal.vue ├── server │ └── api │ │ ├── filters.get.ts │ │ ├── history │ │ ├── clear.post.ts │ │ ├── root-cause.get.ts │ │ ├── stats.get.ts │ │ ├── suggestions.get.ts │ │ ├── offenders.get.ts │ │ └── index.get.ts │ │ ├── guard │ │ └── status.get.ts │ │ ├── security │ │ └── audit.get.ts │ │ └── processes │ │ ├── tree.get.ts │ │ ├── [pid].delete.ts │ │ ├── restart.post.ts │ │ ├── kill-group.post.ts │ │ └── kill-project.post.ts └── README.md ├── mcp ├── src │ └── types.d.ts ├── tsconfig.json ├── package.json └── README.md ├── image-short.png ├── assets ├── port-kill.ico ├── port-kill.png ├── portkill-dashboard.png └── port-kill.svg ├── src ├── cache │ ├── mod.rs │ ├── js_pm.rs │ ├── npx.rs │ ├── restore.rs │ ├── types.rs │ ├── clean.rs │ ├── output.rs │ ├── list.rs │ ├── doctor.rs │ └── backup.rs ├── lib.rs └── system_monitor.rs ├── .cursor └── mcp.json ├── examples ├── port-guard-simple.js ├── file-guard-simple.js ├── file-guard-whitelist.js ├── port-guard-whitelist.js ├── file-cleanup.js ├── port-cleanup.js ├── cache-management.js ├── port-guard-multi.js └── development-guard.js ├── .gitignore ├── run-dashboard.bat ├── run-dashboard.sh ├── shell.nix ├── Cargo.toml ├── test-nix.sh ├── verify-installation.bat ├── validate-nix.sh ├── run-windows.bat ├── run-linux.sh ├── run.sh ├── flake.nix ├── manual-download.bat ├── .github └── workflows │ ├── nix-build.yml │ ├── release.yml │ └── debug.yml ├── install-release.bat ├── release.sh ├── install-release.sh ├── ci-samples ├── gitlab-ci.yml ├── github-actions.yml ├── README.md └── circleci-config.yml ├── validate-config.sh ├── diagnose-installation.bat ├── LICENSE └── install.sh /app_icon.rc: -------------------------------------------------------------------------------- 1 | 1 ICON "assets/port-kill.ico" 2 | -------------------------------------------------------------------------------- /dashboard/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /mcp/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@modelcontextprotocol/sdk'; 2 | 3 | -------------------------------------------------------------------------------- /image-short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/image-short.png -------------------------------------------------------------------------------- /assets/port-kill.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/assets/port-kill.ico -------------------------------------------------------------------------------- /assets/port-kill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/assets/port-kill.png -------------------------------------------------------------------------------- /assets/portkill-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/assets/portkill-dashboard.png -------------------------------------------------------------------------------- /dashboard/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/logo.png -------------------------------------------------------------------------------- /dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/public/favicon.ico -------------------------------------------------------------------------------- /dashboard/assets/img/logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/logo-2.png -------------------------------------------------------------------------------- /dashboard/assets/img/portkill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/portkill.png -------------------------------------------------------------------------------- /dashboard/assets/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/screenshot.png -------------------------------------------------------------------------------- /dashboard/assets/img/kage-long-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/kage-long-blue.png -------------------------------------------------------------------------------- /dashboard/assets/img/kage-long-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/kage-long-white.png -------------------------------------------------------------------------------- /dashboard/assets/img/portkill-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treadiehq/port-kill/HEAD/dashboard/assets/img/portkill-dashboard.png -------------------------------------------------------------------------------- /dashboard/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod backup; 2 | pub mod clean; 3 | pub mod detect; 4 | pub mod doctor; 5 | pub mod js_pm; 6 | pub mod list; 7 | pub mod npx; 8 | pub mod output; 9 | pub mod restore; 10 | pub mod types; 11 | -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "port-kill": { 4 | "command": "npm", 5 | "args": ["run", "dev"], 6 | "cwd": "./mcp", 7 | "env": { 8 | "PORT_KILL_CWD": "${workspaceRoot}" 9 | } 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /mcp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | 15 | -------------------------------------------------------------------------------- /examples/port-guard-simple.js: -------------------------------------------------------------------------------- 1 | // Simple Port Guard Script 2 | // Automatically kills any process that binds to port 3000 3 | 4 | log("Starting simple port guard for port 3000"); 5 | 6 | // Guard port 3000 - kill any process that tries to use it 7 | guardPort(3000); 8 | 9 | log("Port guard activated. Any process on port 3000 will be killed."); 10 | -------------------------------------------------------------------------------- /src/cache/js_pm.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Debug, Serialize)] 4 | pub struct JsPmReport { 5 | pub npm: bool, 6 | pub pnpm: bool, 7 | pub yarn: bool, 8 | } 9 | 10 | pub async fn scan_js_pm() -> JsPmReport { 11 | JsPmReport { 12 | npm: false, 13 | pnpm: false, 14 | yarn: false, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/file-guard-simple.js: -------------------------------------------------------------------------------- 1 | // Simple File Guard Script 2 | // Automatically kills any process that opens a specific file 3 | 4 | log("Starting simple file guard for package.json"); 5 | 6 | // Guard package.json - kill any process that opens it 7 | guardFile("package.json"); 8 | 9 | log("File guard activated. Any process that opens package.json will be killed."); 10 | -------------------------------------------------------------------------------- /examples/file-guard-whitelist.js: -------------------------------------------------------------------------------- 1 | // File Guard with Whitelist Script 2 | // Only allows specific processes to open certain files 3 | 4 | log("Starting file guard with whitelist for .env files"); 5 | 6 | // Only allow "npm" to open .env files, kill everything else 7 | guardFile(".env", "npm"); 8 | 9 | log("File guard activated. Only 'npm' is allowed to open .env files."); 10 | -------------------------------------------------------------------------------- /examples/port-guard-whitelist.js: -------------------------------------------------------------------------------- 1 | // Port Guard with Whitelist Script 2 | // Only allows specific processes on port 3000, kills everything else 3 | 4 | log("Starting port guard with whitelist for port 3000"); 5 | 6 | // Only allow "my-dev-server" on port 3000, kill everything else 7 | guardPort(3000, "my-dev-server"); 8 | 9 | log("Port guard activated. Only 'my-dev-server' is allowed on port 3000."); 10 | -------------------------------------------------------------------------------- /dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "files": [], 4 | "references": [ 5 | { 6 | "path": "./.nuxt/tsconfig.app.json" 7 | }, 8 | { 9 | "path": "./.nuxt/tsconfig.server.json" 10 | }, 11 | { 12 | "path": "./.nuxt/tsconfig.shared.json" 13 | }, 14 | { 15 | "path": "./.nuxt/tsconfig.node.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | 6 | # IDE 7 | .vscode/ 8 | .idea/ 9 | *.swp 10 | *.swo 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Logs 17 | *.log 18 | 19 | # Environment variables 20 | .env 21 | .env.local 22 | 23 | # Node 24 | node_modules/ 25 | **/node_modules/ 26 | mcp/node_modules/ 27 | dashboard/node_modules/ 28 | 29 | # Dashboard (excluded from repo) 30 | REMOVED.md 31 | -------------------------------------------------------------------------------- /examples/file-cleanup.js: -------------------------------------------------------------------------------- 1 | // File Cleanup Script 2 | // Kills processes with common problematic files open 3 | 4 | log("Starting file cleanup script..."); 5 | 6 | // Kill processes with lock files open 7 | killFileExt(".lock"); 8 | 9 | // Kill processes with log files open 10 | killFileExt(".log"); 11 | 12 | // Kill processes with specific files open 13 | killFile("package-lock.json"); 14 | killFile("yarn.lock"); 15 | 16 | log("File cleanup completed. Lock and log files should be free."); 17 | -------------------------------------------------------------------------------- /examples/port-cleanup.js: -------------------------------------------------------------------------------- 1 | // Port Cleanup Script 2 | // Automatically cleans up development ports 3 | 4 | log("Starting port cleanup script") 5 | 6 | // Clean up common development ports 7 | clearPort(3000) 8 | clearPort(3001) 9 | clearPort(5000) 10 | clearPort(8000) 11 | clearPort(8080) 12 | clearPort(9000) 13 | 14 | log("Port cleanup completed") 15 | 16 | // Monitor for new processes 17 | onPort(3000, callback) 18 | onPort(8080, callback) 19 | 20 | log("Monitoring active - press Ctrl+C to stop") 21 | -------------------------------------------------------------------------------- /examples/cache-management.js: -------------------------------------------------------------------------------- 1 | // Cache Management Example 2 | // This script demonstrates cache management capabilities 3 | 4 | log("Starting cache management example...") 5 | 6 | // List all detected caches 7 | log("Listing all development caches...") 8 | listCaches() 9 | 10 | // Run system diagnostics 11 | log("Running cache diagnostics...") 12 | cacheDoctor() 13 | 14 | // Clean caches with safe backup 15 | log("Cleaning caches with safe backup...") 16 | cleanCaches() 17 | 18 | log("Cache management example complete!") 19 | -------------------------------------------------------------------------------- /src/cache/npx.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Debug, Serialize)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct NpxPackageInfo { 6 | pub name: String, 7 | pub version: Option, 8 | pub size_bytes: u64, 9 | pub last_used_at: Option, 10 | pub stale: bool, 11 | } 12 | 13 | #[derive(Debug, Serialize)] 14 | pub struct NpxReport { 15 | pub packages: Vec, 16 | } 17 | 18 | pub async fn analyze_npx(_stale_days: Option) -> NpxReport { 19 | NpxReport { packages: vec![] } 20 | } 21 | -------------------------------------------------------------------------------- /examples/port-guard-multi.js: -------------------------------------------------------------------------------- 1 | // Multi-Port Guard Script 2 | // Guards multiple ports with different policies 3 | 4 | log("Starting multi-port guard system"); 5 | 6 | // Guard port 3000 - kill any process 7 | guardPort(3000); 8 | 9 | // Guard port 8080 - only allow "nginx" 10 | guardPort(8080, "nginx"); 11 | 12 | // Guard port 9000 - kill any process 13 | guardPort(9000); 14 | 15 | log("Multi-port guard activated:"); 16 | log(" - Port 3000: Kill all processes"); 17 | log(" - Port 8080: Only allow 'nginx'"); 18 | log(" - Port 9000: Kill all processes"); 19 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod cli; 3 | pub mod console_app; 4 | pub mod endpoint_monitor; 5 | pub mod file_monitor; 6 | pub mod orchestrator; 7 | pub mod port_guard; 8 | pub mod preset_manager; 9 | pub mod process_monitor; 10 | pub mod restart_manager; 11 | pub mod scripting; 12 | pub mod security_audit; 13 | pub mod service_detector; 14 | pub mod smart_filter; 15 | pub mod system_monitor; 16 | pub mod types; 17 | pub mod update_check; 18 | 19 | // macOS-specific modules (only compiled on macOS) 20 | #[cfg(target_os = "macos")] 21 | pub mod app; 22 | #[cfg(target_os = "macos")] 23 | pub mod tray_menu; 24 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "port-kill-dashboard", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev --port 3002", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "start": "nuxt start --port 3002" 12 | }, 13 | "dependencies": { 14 | "@headlessui/vue": "^1.7.23", 15 | "@heroicons/vue": "^2.2.0", 16 | "@nuxtjs/color-mode": "^3.5.2", 17 | "@nuxtjs/tailwindcss": "^6.14.0", 18 | "@vueuse/nuxt": "^13.9.0", 19 | "nuxt": "^4.0.1", 20 | "vue": "^3.5.18", 21 | "vue-router": "^4.5.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mcp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "port-kill-mcp", 3 | "version": "0.1.1", 4 | "private": true, 5 | "type": "commonjs", 6 | "main": "dist/server.js", 7 | "bin": { 8 | "port-kill-mcp": "dist/server.js" 9 | }, 10 | "files": [ 11 | "dist", 12 | "package.json", 13 | "package-lock.json" 14 | ], 15 | "scripts": { 16 | "build": "tsc -p .", 17 | "start": "node dist/server.js", 18 | "dev": "tsx src/server.ts" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^24.5.2", 22 | "tsx": "^4.19.2", 23 | "typescript": "^5.6.3" 24 | }, 25 | "dependencies": { 26 | "@modelcontextprotocol/sdk": "^1.18.0", 27 | "zod": "^3.23.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/port-kill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/development-guard.js: -------------------------------------------------------------------------------- 1 | // Development Environment Guard Script 2 | // Protects your development environment from file conflicts 3 | 4 | log("Starting development environment guard..."); 5 | 6 | // Guard critical development files 7 | guardFile("package.json"); 8 | guardFile("package-lock.json"); 9 | guardFile(".env"); 10 | 11 | // Kill processes with lock files that might cause conflicts 12 | killFileExt(".lock"); 13 | 14 | // Guard your main development port 15 | guardPort(3000); 16 | 17 | log("Development environment guard activated:"); 18 | log(" - package.json: Protected"); 19 | log(" - package-lock.json: Protected"); 20 | log(" - .env: Protected"); 21 | log(" - .lock files: Cleared"); 22 | log(" - Port 3000: Guarded"); 23 | -------------------------------------------------------------------------------- /dashboard/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./components/**/*.{js,vue,ts}", 5 | "./layouts/**/*.vue", 6 | "./pages/**/*.vue", 7 | "./plugins/**/*.{js,ts}", 8 | "./app.vue", 9 | "./error.vue" 10 | ], 11 | darkMode: 'class', 12 | theme: { 13 | extend: { 14 | fontFamily: { 15 | sans: ['Inter', 'system-ui', 'sans-serif'], 16 | }, 17 | animation: { 18 | 'pulse-slow': 'pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', 19 | }, 20 | keyframes: { 21 | 'pulse-slow': { 22 | '0%, 100%': { opacity: '1' }, 23 | '50%': { opacity: '0.5' }, 24 | } 25 | } 26 | }, 27 | }, 28 | plugins: [], 29 | } 30 | -------------------------------------------------------------------------------- /src/cache/restore.rs: -------------------------------------------------------------------------------- 1 | use super::backup::{find_latest_backup, restore_from_backup}; 2 | use super::types::RestoreResponse; 3 | 4 | pub async fn restore_last_backup() -> RestoreResponse { 5 | match find_latest_backup() { 6 | Ok(Some(backup_path)) => match restore_from_backup(&backup_path).await { 7 | Ok(count) => RestoreResponse { 8 | restored_from: backup_path.to_string_lossy().to_string(), 9 | restored_count: count, 10 | }, 11 | Err(e) => { 12 | eprintln!("Error restoring backup: {}", e); 13 | RestoreResponse { 14 | restored_from: String::new(), 15 | restored_count: 0, 16 | } 17 | } 18 | }, 19 | Ok(None) => { 20 | eprintln!("No backup found to restore"); 21 | RestoreResponse { 22 | restored_from: String::new(), 23 | restored_count: 0, 24 | } 25 | } 26 | Err(e) => { 27 | eprintln!("Error finding backup: {}", e); 28 | RestoreResponse { 29 | restored_from: String::new(), 30 | restored_count: 0, 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /run-dashboard.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Port Kill Dashboard Launcher for Windows 3 | REM This script builds the Rust application and starts the dashboard 4 | 5 | echo 🚀 Starting Port Kill Dashboard... 6 | 7 | REM Check if we're in the right directory 8 | if not exist "Cargo.toml" ( 9 | echo ❌ Error: Please run this script from the port-kill root directory 10 | exit /b 1 11 | ) 12 | 13 | REM Build the Rust application 14 | echo 🔨 Building Port Kill Rust application... 15 | cargo build --release 16 | 17 | REM Check if build was successful 18 | if not exist "target\release\port-kill-console.exe" ( 19 | echo ❌ Error: Failed to build port-kill-console binary 20 | exit /b 1 21 | ) 22 | 23 | echo ✅ Rust application built successfully 24 | 25 | REM Check if dashboard directory exists 26 | if not exist "dashboard" ( 27 | echo ❌ Error: Dashboard directory not found. Please ensure dashboard exists. 28 | exit /b 1 29 | ) 30 | 31 | REM Install dashboard dependencies if needed 32 | if not exist "dashboard\node_modules" ( 33 | echo 📦 Installing dashboard dependencies... 34 | cd dashboard 35 | npm install 36 | cd .. 37 | ) 38 | 39 | REM Start the dashboard 40 | echo 🌐 Starting dashboard on http://localhost:3002... 41 | cd dashboard 42 | npm run dev 43 | -------------------------------------------------------------------------------- /run-dashboard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Port Kill Dashboard Launcher 4 | # This script builds the Rust application and starts the dashboard 5 | 6 | set -e 7 | 8 | echo "🚀 Starting Port Kill Dashboard..." 9 | 10 | # Check if we're in the right directory 11 | if [ ! -f "Cargo.toml" ]; then 12 | echo "❌ Error: Please run this script from the port-kill root directory" 13 | exit 1 14 | fi 15 | 16 | # Build the Rust application 17 | echo "🔨 Building Port Kill Rust application..." 18 | cargo build --release 19 | 20 | # Check if build was successful 21 | if [ ! -f "target/release/port-kill-console" ]; then 22 | echo "❌ Error: Failed to build port-kill-console binary" 23 | exit 1 24 | fi 25 | 26 | echo "✅ Rust application built successfully" 27 | 28 | # Check if dashboard directory exists 29 | if [ ! -d "dashboard" ]; then 30 | echo "❌ Error: Dashboard directory not found. Please ensure dashboard exists." 31 | exit 1 32 | fi 33 | 34 | # Install dashboard dependencies if needed 35 | if [ ! -d "dashboard/node_modules" ]; then 36 | echo "📦 Installing dashboard dependencies..." 37 | cd dashboard 38 | npm install 39 | cd .. 40 | fi 41 | 42 | # Start the dashboard 43 | echo "🌐 Starting dashboard on http://localhost:3002..." 44 | cd dashboard 45 | npm run dev 46 | -------------------------------------------------------------------------------- /dashboard/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: true }, 4 | modules: [ 5 | '@nuxtjs/tailwindcss', 6 | '@nuxtjs/color-mode', 7 | '@vueuse/nuxt' 8 | ], 9 | colorMode: { 10 | preference: 'dark', // default value of $colorMode.preference 11 | fallback: 'dark', // fallback value if not system preference found 12 | hid: 'nuxt-color-mode-script', 13 | globalName: '__NUXT_COLOR_MODE__', 14 | componentName: 'ColorScheme', 15 | classPrefix: '', 16 | classSuffix: '', 17 | storageKey: 'nuxt-color-mode' 18 | }, 19 | runtimeConfig: { 20 | // Private keys (only available on server-side) 21 | portKillBinaryPath: process.env.PORT_KILL_BINARY_PATH || '/Users/dantelex/port-kill/target/release/port-kill-console', 22 | remoteHost: process.env.REMOTE_HOST || '', 23 | remoteMode: process.env.REMOTE_MODE === 'true', 24 | 25 | // Public keys (exposed to client-side) 26 | public: { 27 | apiBase: process.env.API_BASE || 'http://localhost:3000/api', 28 | remoteMode: process.env.REMOTE_MODE === 'true', 29 | remoteHost: process.env.REMOTE_HOST || '' 30 | } 31 | }, 32 | nitro: { 33 | experimental: { 34 | wasm: true 35 | } 36 | } 37 | }) -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | # Rust toolchain 6 | rustc 7 | cargo 8 | rust-analyzer 9 | clippy 10 | rustfmt 11 | 12 | # Build tools 13 | pkg-config 14 | 15 | # Platform-specific dependencies 16 | ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ 17 | # Linux GUI dependencies 18 | gtk3 19 | libappindicator-gtk3 20 | atk 21 | gdk-pixbuf 22 | cairo 23 | pango 24 | libxdo 25 | ]; 26 | 27 | # Environment variables 28 | RUST_BACKTRACE = "1"; 29 | CARGO_TARGET_DIR = "./target"; 30 | 31 | shellHook = '' 32 | echo "🦀 Port Kill Development Environment" 33 | echo "==================================" 34 | echo "Rust version: $(rustc --version)" 35 | echo "Cargo version: $(cargo --version)" 36 | echo "" 37 | echo "Available commands:" 38 | echo " cargo build - Build the project" 39 | echo " cargo test - Run tests" 40 | echo " cargo clippy - Run linter" 41 | echo " cargo fmt - Format code" 42 | echo "" 43 | echo "Platform-specific builds:" 44 | echo " cargo build --target x86_64-unknown-linux-gnu" 45 | echo " cargo build --target x86_64-pc-windows-gnu" 46 | echo " cargo build --target x86_64-apple-darwin" 47 | echo " cargo build --target aarch64-apple-darwin" 48 | echo "" 49 | ''; 50 | } 51 | -------------------------------------------------------------------------------- /src/cache/types.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct CacheEntry { 7 | pub id: String, 8 | pub kind: String, 9 | pub name: String, 10 | pub path: String, 11 | pub size_bytes: u64, 12 | pub last_used_at: Option>, 13 | pub stale: bool, 14 | pub details: serde_json::Value, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct ListSummary { 20 | pub total_size_bytes: u64, 21 | pub count: usize, 22 | pub stale_count: usize, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub struct ListResponse { 27 | pub entries: Vec, 28 | pub summary: ListSummary, 29 | } 30 | 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct CleanSummary { 34 | pub freed_bytes: u64, 35 | pub deleted_count: usize, 36 | } 37 | 38 | #[derive(Debug, Clone, Serialize, Deserialize)] 39 | #[serde(rename_all = "camelCase")] 40 | pub struct CleanResponse { 41 | pub deleted: Vec, 42 | pub backed_up_to: Option, 43 | pub summary: CleanSummary, 44 | } 45 | 46 | #[derive(Debug, Clone, Serialize, Deserialize)] 47 | #[serde(rename_all = "camelCase")] 48 | pub struct RestoreResponse { 49 | pub restored_from: String, 50 | pub restored_count: usize, 51 | } 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "port-kill" 3 | version = "0.5.29" 4 | edition = "2021" 5 | authors = ["Treadie "] 6 | description = "A CLI tool to help you find and free ports blocking your dev work, plus manage development caches" 7 | license = "FSL-1.1-MIT" 8 | repository = "https://github.com/treadiehq/port-kill" 9 | build = "build.rs" 10 | 11 | [lib] 12 | name = "port_kill" 13 | path = "src/lib.rs" 14 | 15 | [[bin]] 16 | name = "port-kill" 17 | path = "src/main.rs" 18 | 19 | [[bin]] 20 | name = "port-kill-console" 21 | path = "src/main_console.rs" 22 | 23 | [dependencies] 24 | # Platform-agnostic dependencies (used by both GUI and console) 25 | crossbeam-channel = "0.5" 26 | tokio = { version = "1.0", features = ["full"] } 27 | serde = { version = "1.0", features = ["derive"] } 28 | serde_json = "1.0" 29 | serde_yaml = "0.9" 30 | anyhow = "1.0" 31 | thiserror = "1.0" 32 | log = "0.4" 33 | env_logger = "0.10" 34 | clap = { version = "4.0", features = ["derive"] } 35 | regex = "1.0" 36 | sysinfo = "0.30" 37 | chrono = { version = "0.4", features = ["serde"] } 38 | reqwest = { version = "0.11", features = ["json", "blocking"] } 39 | walkdir = "2" 40 | 41 | [build-dependencies] 42 | embed-resource = "1.8" 43 | 44 | [features] 45 | default = [] 46 | embed_icon = [] 47 | 48 | # GUI-specific dependencies (only for macOS tray icon) 49 | [target.'cfg(target_os = "macos")'.dependencies] 50 | tray-icon = "0.10" 51 | winit = "0.29" 52 | 53 | # Unix-specific dependencies (for process management) 54 | [target.'cfg(not(target_os = "windows"))'.dependencies] 55 | nix = { version = "0.27", features = ["signal", "process", "fs"] } 56 | -------------------------------------------------------------------------------- /test-nix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple Nix test script 4 | echo "🧪 Testing Nix Configuration" 5 | echo "============================" 6 | echo "" 7 | 8 | # Check if Nix is available 9 | if ! command -v nix &> /dev/null; then 10 | echo "❌ Nix is not installed" 11 | echo " Install Nix from: https://nixos.org/download.html" 12 | echo " Then run: echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf" 13 | exit 1 14 | fi 15 | 16 | echo "✅ Nix is available: $(nix --version)" 17 | echo "" 18 | 19 | # Test flake check 20 | echo "🔍 Checking flake configuration..." 21 | if nix flake check . 2>/dev/null; then 22 | echo "✅ Flake configuration is valid" 23 | else 24 | echo "❌ Flake configuration has issues:" 25 | nix flake check . 2>&1 | head -10 26 | exit 1 27 | fi 28 | echo "" 29 | 30 | # Test development shell 31 | echo "🧪 Testing development shell..." 32 | if nix develop --dry-run . 2>/dev/null; then 33 | echo "✅ Development shell configuration is valid" 34 | else 35 | echo "❌ Development shell has issues" 36 | exit 1 37 | fi 38 | echo "" 39 | 40 | # Test build (dry run) 41 | echo "🔨 Testing build (dry run)..." 42 | if nix build --dry-run .#default 2>/dev/null; then 43 | echo "✅ Build configuration is valid" 44 | else 45 | echo "❌ Build configuration has issues:" 46 | nix build --dry-run .#default 2>&1 | head -10 47 | exit 1 48 | fi 49 | echo "" 50 | 51 | echo "🎉 All Nix tests passed!" 52 | echo "" 53 | echo "📋 Next steps:" 54 | echo " 1. Enter development shell: nix develop" 55 | echo " 2. Build the project: nix build" 56 | echo " 3. Run the binary: ./result/bin/port-kill --help" 57 | echo "" 58 | -------------------------------------------------------------------------------- /verify-installation.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableextensions 3 | 4 | echo Port Kill Installation Verification 5 | echo =================================== 6 | echo. 7 | 8 | set "INSTALL_DIR=%USERPROFILE%\AppData\Local\port-kill" 9 | 10 | echo Checking installation directory: %INSTALL_DIR% 11 | echo. 12 | 13 | if exist "%INSTALL_DIR%\port-kill.exe" ( 14 | echo ✅ port-kill.exe found 15 | ) else ( 16 | echo ❌ port-kill.exe NOT found 17 | ) 18 | 19 | if exist "%INSTALL_DIR%\port-kill-console.exe" ( 20 | echo ✅ port-kill-console.exe found 21 | ) else ( 22 | echo ❌ port-kill-console.exe NOT found 23 | ) 24 | 25 | echo. 26 | echo Checking PATH environment variable... 27 | echo %PATH% | findstr /i "%INSTALL_DIR%" >nul 28 | if %errorlevel% equ 0 ( 29 | echo ✅ Installation directory is in PATH 30 | ) else ( 31 | echo ❌ Installation directory is NOT in PATH 32 | echo. 33 | echo To fix this, run the install script again or manually add to PATH: 34 | echo %INSTALL_DIR% 35 | ) 36 | 37 | echo. 38 | echo Current PATH entries containing 'port-kill': 39 | echo %PATH% | findstr /i "port-kill" 40 | 41 | echo. 42 | echo Testing executables... 43 | if exist "%INSTALL_DIR%\port-kill-console.exe" ( 44 | echo Testing port-kill-console.exe... 45 | "%INSTALL_DIR%\port-kill-console.exe" --help >nul 2>&1 46 | if %errorlevel% equ 0 ( 47 | echo ✅ port-kill-console.exe is working 48 | ) else ( 49 | echo ❌ port-kill-console.exe failed to run 50 | ) 51 | ) 52 | 53 | echo. 54 | echo If you see ❌ errors above, please: 55 | echo 1. Run the install script again: install-release.bat 56 | echo 2. Restart your terminal 57 | echo 3. Try running: port-kill-console --console --ports 3000,8000 58 | -------------------------------------------------------------------------------- /dashboard/demo-servers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Demo script to start some development servers for testing the dashboard 4 | # This will start several Node.js servers on different ports 5 | 6 | echo "🚀 Starting demo development servers for Port Kill Dashboard..." 7 | 8 | # Function to start a server on a specific port 9 | start_server() { 10 | local port=$1 11 | local name=$2 12 | local file="server-${port}.js" 13 | 14 | # Create a simple Node.js server 15 | cat > "$file" << EOF 16 | const http = require('http'); 17 | const port = ${port}; 18 | 19 | const server = http.createServer((req, res) => { 20 | res.writeHead(200, {'Content-Type': 'text/html'}); 21 | res.end(\` 22 | 23 | ${name} - Port \${port} 24 | 25 |

${name}

26 |

Server running on port \${port}

27 |

PID: \${process.pid}

28 |

Started: \${new Date().toISOString()}

29 | 30 | 31 | \`); 32 | }); 33 | 34 | server.listen(port, () => { 35 | // Server started 36 | }); 37 | EOF 38 | 39 | # Start the server in the background 40 | node "$file" & 41 | echo "✅ Started ${name} on port ${port} (PID: $!)" 42 | } 43 | 44 | # Start multiple demo servers 45 | start_server 3000 "React Dev Server" 46 | start_server 3001 "Vue Dev Server" 47 | start_server 4000 "Express API" 48 | start_server 5000 "Next.js App" 49 | start_server 6000 "Nuxt Dashboard" 50 | 51 | echo "" 52 | echo "🎉 Demo servers started!" 53 | echo "Now open the Port Kill Dashboard at http://localhost:3001" 54 | echo "" 55 | echo "To stop all demo servers, run:" 56 | echo "pkill -f 'node server-.*.js'" 57 | echo "" 58 | echo "Or use the dashboard to kill them individually!" 59 | -------------------------------------------------------------------------------- /validate-nix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Nix Configuration Validation Script 4 | # This script validates the Nix configuration files 5 | 6 | echo "🔍 Validating Nix Configuration" 7 | echo "===============================" 8 | echo "" 9 | 10 | # Check if Nix is available 11 | if ! command -v nix &> /dev/null; then 12 | echo "⚠️ Nix is not installed. Skipping validation." 13 | echo " Install Nix from: https://nixos.org/download.html" 14 | echo "" 15 | echo "📋 Manual validation checklist:" 16 | echo " ✅ flake.nix syntax looks correct" 17 | echo " ✅ shell.nix syntax looks correct" 18 | echo " ✅ GitHub Actions workflow created" 19 | echo " ✅ Documentation created" 20 | echo "" 21 | exit 0 22 | fi 23 | 24 | echo "✅ Nix is available: $(nix --version)" 25 | echo "" 26 | 27 | # Validate flake.nix 28 | echo "🔍 Validating flake.nix..." 29 | if nix flake check . 2>/dev/null; then 30 | echo "✅ flake.nix is valid" 31 | else 32 | echo "❌ flake.nix has issues:" 33 | nix flake check . 2>&1 | head -10 34 | fi 35 | echo "" 36 | 37 | # Show available packages 38 | echo "📦 Available packages:" 39 | nix flake show . 2>/dev/null || echo " (Cannot show packages without Nix)" 40 | echo "" 41 | 42 | # Test development shell 43 | echo "🧪 Testing development shell..." 44 | if nix develop --dry-run . 2>/dev/null; then 45 | echo "✅ Development shell configuration is valid" 46 | else 47 | echo "❌ Development shell has issues" 48 | fi 49 | echo "" 50 | 51 | echo "🎉 Nix configuration validation complete!" 52 | echo "" 53 | echo "📋 Next steps:" 54 | echo " 1. Install Nix: https://nixos.org/download.html" 55 | echo " 2. Enable flakes: echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf" 56 | echo " 3. Enter development shell: nix develop" 57 | echo " 4. Build: nix build" 58 | echo "" 59 | -------------------------------------------------------------------------------- /run-windows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Port Kill - Windows Run Script 3 | REM This script runs the port-kill application on Windows with logging enabled 4 | REM Usage: run-windows.bat [options] 5 | REM Examples: 6 | REM run-windows.bat REM Default: ports 2000-6000 7 | REM run-windows.bat --start-port 3000 REM Ports 3000-6000 8 | REM run-windows.bat --end-port 8080 REM Ports 2000-8080 9 | REM run-windows.bat --ports 3000,8000,8080 REM Specific ports only 10 | REM run-windows.bat --console REM Run in console mode 11 | REM run-windows.bat --verbose REM Enable verbose logging 12 | REM run-windows.bat --docker REM Enable Docker container monitoring 13 | REM run-windows.bat --docker --ports 3000,3001 REM Monitor specific ports with Docker 14 | REM run-windows.bat --show-pid REM Show process IDs in output 15 | REM run-windows.bat --console --show-pid REM Console mode with PIDs shown 16 | 17 | echo 🪟 Starting Port Kill on Windows... 18 | echo 📊 Status bar icon should appear shortly 19 | echo. 20 | 21 | REM Check if we're on Windows 22 | if not "%OS%"=="Windows_NT" ( 23 | echo ⚠️ Warning: This script is designed for Windows systems 24 | echo Current OS: %OS% 25 | echo For macOS, use: ./run.sh 26 | echo For Linux, use: ./run-linux.sh 27 | echo. 28 | ) 29 | 30 | REM Check if the Windows application is built 31 | if not exist ".\target\release\port-kill.exe" ( 32 | echo ❌ Windows application not built. Running Windows build first... 33 | call build-windows.bat 34 | if errorlevel 1 ( 35 | echo ❌ Windows build failed! 36 | exit /b 1 37 | ) 38 | ) 39 | 40 | REM Run the Windows application with logging and pass through all arguments 41 | set RUST_LOG=info 42 | .\target\release\port-kill.exe %* 43 | -------------------------------------------------------------------------------- /run-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Port Kill - Linux Run Script 4 | # This script runs the port-kill application on Linux with logging enabled 5 | # Usage: ./run-linux.sh [options] 6 | # Examples: 7 | # ./run-linux.sh # Default: ports 2000-6000 8 | # ./run-linux.sh --start-port 3000 # Ports 3000-6000 9 | # ./run-linux.sh --end-port 8080 # Ports 2000-8080 10 | # ./run-linux.sh --ports 3000,8000,8080 # Specific ports only 11 | # ./run-linux.sh --console # Run in console mode 12 | # ./run-linux.sh --verbose # Enable verbose logging 13 | # ./run-linux.sh --docker # Enable Docker container monitoring 14 | # ./run-linux.sh --docker --ports 3000,3001 # Monitor specific ports with Docker 15 | # ./run-linux.sh --show-pid # Show process IDs in output 16 | # ./run-linux.sh --console --show-pid # Console mode with PIDs shown 17 | 18 | echo "🐧 Starting Port Kill on Linux..." 19 | echo "📊 Status bar icon should appear shortly" 20 | echo "" 21 | 22 | # Check if we're on Linux 23 | if [[ "$OSTYPE" != "linux-gnu"* ]]; then 24 | echo "⚠️ Warning: This script is designed for Linux systems" 25 | echo " Current OS: $OSTYPE" 26 | echo " For macOS, use: ./run.sh" 27 | echo "" 28 | fi 29 | 30 | # Check if the Linux application is built 31 | if [ ! -f "./target/release/port-kill" ]; then 32 | echo "❌ Linux application not built. Running Linux build first..." 33 | ./build-linux.sh 34 | if [ $? -ne 0 ]; then 35 | echo "❌ Linux build failed!" 36 | exit 1 37 | fi 38 | fi 39 | 40 | # Run the Linux application with logging and pass through all arguments 41 | echo "🚀 Starting Port Kill..." 42 | echo "📊 If system tray is not available, it will automatically switch to console mode" 43 | echo "" 44 | 45 | RUST_LOG=info ./target/release/port-kill "$@" 46 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Port Kill - Easy Run Script 4 | # This script runs the port-kill application with logging enabled 5 | # Usage: ./run.sh [options] 6 | # Examples: 7 | # ./run.sh # Default: ports 2000-6000 8 | # ./run.sh --start-port 3000 # Ports 3000-6000 9 | # ./run.sh --end-port 8080 # Ports 2000-8080 10 | # ./run.sh --ports 3000,8000,8080 # Specific ports only 11 | # ./run.sh --console # Run in console mode 12 | # ./run.sh --verbose # Enable verbose logging 13 | # ./run.sh --docker # Enable Docker container monitoring 14 | # ./run.sh --docker --ports 3000,3001 # Monitor specific ports with Docker 15 | # ./run.sh --show-pid # Show process IDs in output 16 | # ./run.sh --console --show-pid # Console mode with PIDs shown 17 | 18 | # Check if console mode is requested 19 | CONSOLE_MODE=false 20 | for arg in "$@"; do 21 | if [ "$arg" = "--console" ]; then 22 | CONSOLE_MODE=true 23 | break 24 | fi 25 | done 26 | 27 | if [ "$CONSOLE_MODE" = true ]; then 28 | echo "🚀 Starting Port Kill (Console Mode)..." 29 | echo "📡 Console monitoring started" 30 | echo "" 31 | 32 | # Check if the console application is built 33 | if [ ! -f "./target/release/port-kill-console" ]; then 34 | echo "❌ Console application not built. Running build first..." 35 | cargo build --release 36 | if [ $? -ne 0 ]; then 37 | echo "❌ Build failed!" 38 | exit 1 39 | fi 40 | fi 41 | 42 | # Run the console application 43 | RUST_LOG=info ./target/release/port-kill-console "$@" 44 | else 45 | echo "🚀 Starting Port Kill..." 46 | echo "📊 Status bar icon should appear shortly" 47 | echo "" 48 | 49 | # Check if the application is built 50 | if [ ! -f "./target/release/port-kill" ]; then 51 | echo "❌ Application not built. Running build first..." 52 | cargo build --release 53 | if [ $? -ne 0 ]; then 54 | echo "❌ Build failed!" 55 | exit 1 56 | fi 57 | fi 58 | 59 | # Run the GUI application 60 | RUST_LOG=info ./target/release/port-kill "$@" 61 | fi 62 | -------------------------------------------------------------------------------- /dashboard/components/StatsCard.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 91 | -------------------------------------------------------------------------------- /dashboard/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html { 7 | font-family: 'Inter', system-ui, sans-serif; 8 | } 9 | 10 | body { 11 | @apply bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100; 12 | } 13 | } 14 | 15 | @layer components { 16 | .card { 17 | @apply rounded-xl border border-gray-500/10 bg-gray-500/5; 18 | } 19 | 20 | .status-indicator { 21 | @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; 22 | } 23 | 24 | .status-running { 25 | @apply bg-green-500/10 text-green-500; 26 | } 27 | 28 | .status-stopped { 29 | @apply bg-red-500/10 text-red-500; 30 | } 31 | 32 | .status-warning { 33 | @apply bg-yellow-500/10 text-yellow-500; 34 | } 35 | } 36 | 37 | /* Custom scrollbar */ 38 | ::-webkit-scrollbar { 39 | width: 6px; 40 | height: 6px; 41 | } 42 | 43 | ::-webkit-scrollbar-track { 44 | @apply bg-gray-100 dark:bg-gray-800; 45 | } 46 | 47 | ::-webkit-scrollbar-thumb { 48 | @apply bg-gray-300 dark:bg-gray-600 rounded-full; 49 | } 50 | 51 | ::-webkit-scrollbar-thumb:hover { 52 | @apply bg-gray-400 dark:bg-gray-500; 53 | } 54 | 55 | /* Animations */ 56 | @keyframes pulse-slow { 57 | 0%, 100% { 58 | opacity: 1; 59 | } 60 | 50% { 61 | opacity: 0.5; 62 | } 63 | } 64 | 65 | .animate-pulse-slow { 66 | animation: pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 67 | } 68 | 69 | /* Process table styles */ 70 | .process-table { 71 | @apply min-w-full divide-y divide-gray-200 dark:divide-gray-700; 72 | } 73 | 74 | .process-table th { 75 | @apply px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider; 76 | } 77 | 78 | .process-table td { 79 | @apply px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100; 80 | } 81 | 82 | /* Port badge styles */ 83 | .port-badge { 84 | @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-300/10 text-blue-300; 85 | } 86 | 87 | /* Container type styles */ 88 | .docker-container { 89 | @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-purple-400 bg-purple-400/10; 90 | } 91 | 92 | .host-container { 93 | @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-500/10 text-gray-400; 94 | } 95 | 96 | .container-badge { 97 | @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200; 98 | } 99 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Port Kill - A CLI tool to help you find and free ports blocking your dev work"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | in 14 | { 15 | # Development shell 16 | devShells.default = pkgs.mkShell { 17 | buildInputs = with pkgs; [ 18 | rustc 19 | cargo 20 | rust-analyzer 21 | clippy 22 | rustfmt 23 | pkg-config 24 | ]; 25 | 26 | shellHook = '' 27 | echo "🦀 Port Kill Development Environment" 28 | echo "==================================" 29 | echo "Rust version: $(rustc --version)" 30 | echo "Cargo version: $(cargo --version)" 31 | echo "" 32 | echo "Available commands:" 33 | echo " cargo build - Build the project" 34 | echo " cargo test - Run tests" 35 | echo " cargo clippy - Run linter" 36 | echo " cargo fmt - Format code" 37 | echo "" 38 | ''; 39 | 40 | RUST_BACKTRACE = "1"; 41 | CARGO_TARGET_DIR = "./target"; 42 | }; 43 | 44 | # Simple build 45 | packages.default = pkgs.stdenv.mkDerivation { 46 | pname = "port-kill"; 47 | version = "0.3.11"; 48 | src = ./.; 49 | 50 | nativeBuildInputs = with pkgs; [ 51 | rustc 52 | cargo 53 | ]; 54 | 55 | buildPhase = '' 56 | cargo build --release --bin port-kill --bin port-kill-console 57 | ''; 58 | 59 | installPhase = '' 60 | mkdir -p $out/bin 61 | cp target/release/port-kill $out/bin/ 62 | cp target/release/port-kill-console $out/bin/ 63 | ''; 64 | 65 | meta = with pkgs.lib; { 66 | description = "A CLI tool to help you find and free ports blocking your dev work"; 67 | homepage = "https://github.com/treadiehq/port-kill"; 68 | license = licenses.mit; 69 | maintainers = [ ]; 70 | platforms = platforms.all; 71 | }; 72 | }; 73 | 74 | checks.default = self.packages.${system}.default; 75 | }); 76 | } -------------------------------------------------------------------------------- /manual-download.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableextensions 3 | 4 | echo Port Kill Manual Download Tool 5 | echo ============================== 6 | echo. 7 | 8 | set "REPO=treadiehq/port-kill" 9 | set "BASE_URL=https://github.com/%REPO%/releases/latest/download" 10 | set "INSTALL_DIR=%USERPROFILE%\AppData\Local\port-kill" 11 | 12 | echo Creating installation directory... 13 | if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%" 14 | 15 | echo. 16 | echo Downloading port-kill... 17 | powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -UseBasicParsing '%BASE_URL%/port-kill-windows.exe' -OutFile '%INSTALL_DIR%\port-kill.exe'" 18 | if %errorlevel% neq 0 ( 19 | echo ❌ Failed to download port-kill 20 | exit /b 1 21 | ) 22 | echo ✅ Downloaded port-kill 23 | 24 | echo. 25 | echo Downloading port-kill-console... 26 | powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -UseBasicParsing '%BASE_URL%/port-kill-console-windows.exe' -OutFile '%INSTALL_DIR%\port-kill-console.exe'" 27 | if %errorlevel% neq 0 ( 28 | echo ❌ Failed to download port-kill-console 29 | exit /b 1 30 | ) 31 | echo ✅ Downloaded port-kill-console 32 | 33 | echo. 34 | echo Adding to PATH... 35 | powershell -NoProfile -Command "if (-not (Test-Path 'env:PATH' -PathType Container)) { [Environment]::SetEnvironmentVariable('PATH', '%INSTALL_DIR%', 'User') } else { $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User'); if ($currentPath -notlike '*%INSTALL_DIR%*') { [Environment]::SetEnvironmentVariable('PATH', $currentPath + ';%INSTALL_DIR%', 'User') } }" 36 | 37 | echo. 38 | echo ========================================== 39 | echo ✅ Installation Complete! 40 | echo ========================================== 41 | echo. 42 | echo 📁 Files installed to: %INSTALL_DIR% 43 | echo - port-kill.exe 44 | echo - port-kill-console.exe 45 | echo. 46 | echo ⚠️ CRITICAL: You MUST completely restart your terminal! 47 | echo. 48 | echo 🔄 Next steps: 49 | echo 1. Close this terminal window completely 50 | echo 2. Open a new terminal window 51 | echo 3. Test: port-kill --list 52 | echo. 53 | echo 🧪 Test NOW without restarting (use full path): 54 | echo "%INSTALL_DIR%\port-kill.exe" --list 55 | echo "%INSTALL_DIR%\port-kill-console.exe" --version 56 | echo. 57 | echo ❌ If you get 'not recognized' error AFTER restarting: 58 | echo Download diagnostics: 59 | echo powershell -Command "Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/treadiehq/port-kill/main/diagnose-installation.bat' -OutFile 'diagnose.bat'" ^&^& .\diagnose.bat 60 | -------------------------------------------------------------------------------- /.github/workflows/nix-build.yml: -------------------------------------------------------------------------------- 1 | name: Nix Build (Disabled) 2 | 3 | on: 4 | # Disabled due to persistent Nix build issues 5 | # push: 6 | # branches: [ main, master ] 7 | # pull_request: 8 | # branches: [ main, master ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | nix-build-linux: 16 | name: Build with Nix (Linux) 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Install Nix 23 | uses: cachix/install-nix-action@v22 24 | with: 25 | nix_path: nixpkgs=channel:nixos-24.05 26 | extra_nix_config: | 27 | experimental-features = nix-command flakes 28 | sandbox = false 29 | 30 | - name: Install cachix 31 | uses: cachix/cachix-action@v12 32 | with: 33 | name: nixpkgs 34 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 35 | 36 | - name: Show Nix version 37 | run: nix --version 38 | 39 | - name: Build with Nix 40 | run: nix build .#default --show-trace 41 | 42 | - name: Test binaries 43 | run: | 44 | ./result/bin/port-kill --help 45 | ./result/bin/port-kill-console --help 46 | 47 | - name: Upload artifacts 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: port-kill-linux 51 | path: result/bin/* 52 | 53 | nix-build-macos: 54 | name: Build with Nix (macOS) 55 | runs-on: macos-latest 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | 60 | - name: Install Rust via rustup (traditional approach) 61 | run: | 62 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 63 | source ~/.cargo/env 64 | rustup default stable 65 | 66 | - name: Install system dependencies 67 | run: | 68 | # Install dependencies that Nix would provide 69 | brew install pkg-config 70 | 71 | - name: Build with cargo (Nix-style reproducible) 72 | run: | 73 | # Use cargo with locked dependencies for reproducibility 74 | cargo build --release --bin port-kill --bin port-kill-console 75 | mkdir -p result/bin 76 | cp target/release/port-kill result/bin/ 77 | cp target/release/port-kill-console result/bin/ 78 | 79 | - name: Test binaries 80 | run: | 81 | ./result/bin/port-kill --help 82 | ./result/bin/port-kill-console --help 83 | 84 | - name: Upload artifacts 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: port-kill-macos 88 | path: result/bin/* -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | create-release: 13 | name: Create GitHub Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Create (or update) release 17 | uses: softprops/action-gh-release@v2 18 | with: 19 | tag_name: ${{ github.ref_name }} 20 | draft: false 21 | generate_release_notes: true 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | build-and-upload: 26 | name: Build (${{ matrix.os }}) and upload assets 27 | needs: create-release 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | matrix: 31 | include: 32 | - os: ubuntu-latest 33 | target: linux 34 | exe_ext: '' 35 | - os: macos-latest 36 | target: macos 37 | exe_ext: '' 38 | arch: arm64 39 | - os: macos-latest 40 | target: macos-intel 41 | exe_ext: '' 42 | arch: x86_64 43 | - os: windows-latest 44 | target: windows 45 | exe_ext: '.exe' 46 | 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | - name: Setup Rust 52 | uses: dtolnay/rust-toolchain@stable 53 | 54 | - name: Install cross-compilation target (Intel Mac) 55 | if: matrix.target == 'macos-intel' 56 | shell: bash 57 | run: rustup target add x86_64-apple-darwin 58 | 59 | - name: Build release 60 | shell: bash 61 | run: | 62 | if [[ "${{ matrix.target }}" == "macos-intel" ]]; then 63 | cargo build --release --target x86_64-apple-darwin 64 | else 65 | cargo build --release 66 | fi 67 | 68 | - name: Prepare assets 69 | shell: bash 70 | run: | 71 | mkdir -p release 72 | if [[ "${{ matrix.target }}" == "macos-intel" ]]; then 73 | SRC_MAIN="target/x86_64-apple-darwin/release/port-kill${{ matrix.exe_ext }}" 74 | SRC_CONSOLE="target/x86_64-apple-darwin/release/port-kill-console${{ matrix.exe_ext }}" 75 | else 76 | SRC_MAIN="target/release/port-kill${{ matrix.exe_ext }}" 77 | SRC_CONSOLE="target/release/port-kill-console${{ matrix.exe_ext }}" 78 | fi 79 | DEST_MAIN="release/port-kill-${{ matrix.target }}${{ matrix.exe_ext }}" 80 | DEST_CONSOLE="release/port-kill-console-${{ matrix.target }}${{ matrix.exe_ext }}" 81 | cp "$SRC_MAIN" "$DEST_MAIN" 82 | cp "$SRC_CONSOLE" "$DEST_CONSOLE" 83 | echo "ASSET_MAIN=$DEST_MAIN" >> $GITHUB_ENV 84 | echo "ASSET_CONSOLE=$DEST_CONSOLE" >> $GITHUB_ENV 85 | 86 | - name: Upload assets to GitHub Release 87 | uses: softprops/action-gh-release@v2 88 | with: 89 | tag_name: ${{ github.ref_name }} 90 | draft: false 91 | files: | 92 | ${{ env.ASSET_MAIN }} 93 | ${{ env.ASSET_CONSOLE }} 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /dashboard/server/api/filters.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { existsSync } from 'fs' 3 | 4 | export default defineEventHandler(async (event) => { 5 | const config = useRuntimeConfig() 6 | 7 | try { 8 | // Find the correct binary path 9 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 10 | if (!binaryPath) { 11 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 12 | } 13 | 14 | // Get filter information using the Rust application 15 | const filtersOutput = await getFiltersWithRustApp(binaryPath) 16 | 17 | return { 18 | success: true, 19 | filters: filtersOutput, 20 | timestamp: new Date().toISOString() 21 | } 22 | 23 | } catch (error: any) { 24 | console.error('Error getting filter information:', error) 25 | 26 | throw createError({ 27 | statusCode: 500, 28 | statusMessage: `Failed to get filter information: ${error.message}` 29 | }) 30 | } 31 | }) 32 | 33 | function findPortKillBinary(defaultPath: string): string | null { 34 | // Check if the default path exists 35 | if (existsSync(defaultPath)) { 36 | return defaultPath 37 | } 38 | 39 | // Try common locations 40 | const commonPaths = [ 41 | './target/release/port-kill-console', 42 | './target/release/port-kill-console.exe', 43 | './target/debug/port-kill-console', 44 | './target/debug/port-kill-console.exe', 45 | '../target/release/port-kill-console', 46 | '../target/release/port-kill-console.exe', 47 | '../target/debug/port-kill-console', 48 | '../target/debug/port-kill-console.exe', 49 | '/usr/local/bin/port-kill-console', 50 | '/opt/homebrew/bin/port-kill-console', 51 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 52 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 53 | ] 54 | 55 | for (const path of commonPaths) { 56 | if (existsSync(path)) { 57 | return path 58 | } 59 | } 60 | 61 | return null 62 | } 63 | 64 | async function getFiltersWithRustApp(binaryPath: string): Promise { 65 | return new Promise((resolve, reject) => { 66 | const args = ['--show-filters'] 67 | 68 | const rustApp = spawn(binaryPath, args, { 69 | stdio: ['pipe', 'pipe', 'pipe'], 70 | timeout: 5000 71 | }) 72 | 73 | let stdout = '' 74 | let stderr = '' 75 | 76 | rustApp.stdout.on('data', (data) => { 77 | stdout += data.toString() 78 | }) 79 | 80 | rustApp.stderr.on('data', (data) => { 81 | stderr += data.toString() 82 | }) 83 | 84 | rustApp.on('close', (code) => { 85 | if (code !== 0) { 86 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 87 | resolve('Failed to get filter information') 88 | return 89 | } 90 | 91 | resolve(stdout) 92 | }) 93 | 94 | rustApp.on('error', (error) => { 95 | console.warn(`Failed to spawn Rust app: ${error.message}`) 96 | resolve('Failed to get filter information') 97 | }) 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /install-release.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableextensions 3 | 4 | rem ---- Config 5 | set "REPO=treadiehq/port-kill" 6 | set "BASE_URL=https://github.com/%REPO%/releases/latest/download" 7 | set "INSTALL_DIR=%USERPROFILE%\AppData\Local\port-kill" 8 | 9 | echo(Port Kill Release Installer for Windows 10 | echo(========================================== 11 | echo( 12 | echo(Detected platform: Windows 13 | 14 | echo( 15 | echo(Preparing installation directory... 16 | if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%" 17 | 18 | echo(Installing to: %INSTALL_DIR% 19 | 20 | echo(Downloading port-kill... 21 | powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -UseBasicParsing '%BASE_URL%/port-kill-windows.exe' -OutFile '%INSTALL_DIR%\port-kill.exe'" || ( 22 | echo(Download failed (port-kill) 23 | exit /b 1 24 | ) 25 | 26 | echo(Downloading port-kill-console... 27 | powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -UseBasicParsing '%BASE_URL%/port-kill-console-windows.exe' -OutFile '%INSTALL_DIR%\port-kill-console.exe'" || ( 28 | echo(Download failed (port-kill-console) 29 | exit /b 1 30 | ) 31 | 32 | echo( 33 | echo(Verifying installation... 34 | if not exist "%INSTALL_DIR%\port-kill.exe" ( 35 | echo(ERROR: port-kill.exe not found after download 36 | exit /b 1 37 | ) 38 | if not exist "%INSTALL_DIR%\port-kill-console.exe" ( 39 | echo(ERROR: port-kill-console.exe not found after download 40 | exit /b 1 41 | ) 42 | echo(✅ Both binaries downloaded successfully 43 | 44 | echo( 45 | echo(Adding to PATH... 46 | powershell -NoProfile -Command "if (-not (Test-Path 'env:PATH' -PathType Container)) { [Environment]::SetEnvironmentVariable('PATH', '%INSTALL_DIR%', 'User') } else { $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User'); if ($currentPath -notlike '*%INSTALL_DIR%*') { [Environment]::SetEnvironmentVariable('PATH', $currentPath + ';%INSTALL_DIR%', 'User') } }" 47 | 48 | echo( 49 | echo(========================================== 50 | echo(✅ Installation Complete! 51 | echo(========================================== 52 | echo( 53 | echo(📁 Files installed to: %INSTALL_DIR% 54 | echo( - port-kill.exe 55 | echo( - port-kill-console.exe 56 | echo( 57 | echo(⚠️ IMPORTANT: You MUST restart your terminal for PATH changes to take effect! 58 | echo( 59 | echo(🔄 To apply changes: 60 | echo( 1. Close this terminal window completely 61 | echo( 2. Open a new terminal window 62 | echo( 3. Run: port-kill --list 63 | echo( 64 | echo(🧪 Test immediately without restarting (use full path): 65 | echo( "%INSTALL_DIR%\port-kill.exe" --list 66 | echo( "%INSTALL_DIR%\port-kill-console.exe" --version 67 | echo( 68 | echo(❌ If you get "not recognized" error after restarting: 69 | echo( Run diagnostics: 70 | echo( powershell -Command "Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/treadiehq/port-kill/main/diagnose-installation.bat' -OutFile 'diagnose.bat'" ^&^& .\diagnose.bat 71 | echo( 72 | echo(📖 Quick start (after restarting terminal): 73 | echo( port-kill --list 74 | echo( port-kill 3000 75 | echo( port-kill --guard 3000 76 | echo( port-kill cache --list 77 | -------------------------------------------------------------------------------- /dashboard/server/api/history/clear.post.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import { readFileSync, existsSync } from 'fs' 3 | import { join } from 'path' 4 | import { promisify } from 'util' 5 | 6 | const execAsync = promisify(exec) 7 | 8 | export default defineEventHandler(async (event) => { 9 | const config = useRuntimeConfig() 10 | 11 | try { 12 | // Try to find the port-kill-console binary 13 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 14 | 15 | if (!binaryPath) { 16 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 17 | } 18 | 19 | // Clear process history using our Rust application 20 | await clearProcessHistory(binaryPath) 21 | 22 | return { 23 | success: true, 24 | message: 'Process history cleared successfully', 25 | timestamp: new Date().toISOString() 26 | } 27 | 28 | } catch (error) { 29 | console.error('Error clearing process history:', error) 30 | 31 | return { 32 | success: false, 33 | error: error.message, 34 | timestamp: new Date().toISOString() 35 | } 36 | } 37 | }) 38 | 39 | function findPortKillBinary(defaultPath: string): string | null { 40 | // Check if the default path exists 41 | if (existsSync(defaultPath)) { 42 | return defaultPath 43 | } 44 | 45 | // Try common locations for port-kill-console 46 | const possiblePaths = [ 47 | './target/release/port-kill-console', 48 | './target/release/port-kill-console.exe', 49 | './target/debug/port-kill-console', 50 | './target/debug/port-kill-console.exe', 51 | '../target/release/port-kill-console', 52 | '../target/release/port-kill-console.exe', 53 | '../target/debug/port-kill-console', 54 | '../target/debug/port-kill-console.exe', 55 | '/usr/local/bin/port-kill-console', 56 | '/opt/homebrew/bin/port-kill-console', 57 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 58 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 59 | ] 60 | 61 | for (const path of possiblePaths) { 62 | if (existsSync(path)) { 63 | return path 64 | } 65 | } 66 | 67 | return null 68 | } 69 | 70 | async function clearProcessHistory(binaryPath: string): Promise { 71 | return new Promise((resolve, reject) => { 72 | // Build command arguments for clearing history 73 | const args = [ 74 | '--clear-history' 75 | ] 76 | 77 | const rustApp = spawn(binaryPath, args, { 78 | stdio: ['pipe', 'pipe', 'pipe'] 79 | }) 80 | 81 | let stdout = '' 82 | let stderr = '' 83 | 84 | rustApp.stdout.on('data', (data) => { 85 | stdout += data.toString() 86 | }) 87 | 88 | rustApp.stderr.on('data', (data) => { 89 | stderr += data.toString() 90 | }) 91 | 92 | rustApp.on('close', (code) => { 93 | if (code !== 0) { 94 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 95 | reject(new Error(`Failed to clear history: ${stderr}`)) 96 | return 97 | } 98 | 99 | resolve() 100 | }) 101 | 102 | rustApp.on('error', (error) => { 103 | console.warn(`Failed to spawn Rust app: ${error.message}`) 104 | reject(error) 105 | }) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /dashboard/server/api/history/root-cause.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const binaryPath = findPortKillBinary() 8 | 9 | if (!binaryPath) { 10 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 11 | } 12 | 13 | const analysis = await getRootCauseAnalysis(binaryPath) 14 | 15 | return { 16 | success: true, 17 | analysis, 18 | timestamp: new Date().toISOString() 19 | } 20 | 21 | } catch (error) { 22 | console.error('Error fetching root cause analysis:', error) 23 | 24 | return { 25 | success: false, 26 | error: error.message, 27 | analysis: null, 28 | timestamp: new Date().toISOString() 29 | } 30 | } 31 | }) 32 | 33 | function findPortKillBinary(): string | null { 34 | // Try common locations 35 | const commonPaths = [ 36 | join(process.cwd(), 'target', 'release', 'port-kill-console'), 37 | join(process.cwd(), 'target', 'release', 'port-kill-console.exe'), 38 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console'), 39 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console.exe'), 40 | '/usr/local/bin/port-kill-console', 41 | '/opt/port-kill/port-kill-console', 42 | ] 43 | 44 | for (const path of commonPaths) { 45 | if (existsSync(path)) { 46 | return path 47 | } 48 | } 49 | 50 | return null 51 | } 52 | 53 | async function getRootCauseAnalysis(binaryPath: string): Promise { 54 | return new Promise((resolve, reject) => { 55 | const args = ['--show-root-cause', '--json'] 56 | 57 | const rustApp = spawn(binaryPath, args, { 58 | stdio: ['pipe', 'pipe', 'pipe'] 59 | }) 60 | 61 | let stdout = '' 62 | let stderr = '' 63 | 64 | rustApp.stdout.on('data', (data) => { 65 | stdout += data.toString() 66 | }) 67 | 68 | rustApp.stderr.on('data', (data) => { 69 | stderr += data.toString() 70 | }) 71 | 72 | rustApp.on('close', (code) => { 73 | if (code !== 0) { 74 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 75 | resolve(null) 76 | return 77 | } 78 | 79 | try { 80 | // Filter out debug output and extract JSON 81 | const lines = stdout.split('\n') 82 | let jsonLine = '' 83 | 84 | for (const line of lines) { 85 | const trimmed = line.trim() 86 | // Look for lines that start with { (JSON) 87 | if (trimmed.startsWith('{') && trimmed.endsWith('}')) { 88 | jsonLine = trimmed 89 | break 90 | } 91 | } 92 | 93 | if (!jsonLine) { 94 | throw new Error('No valid JSON found in output') 95 | } 96 | 97 | const analysis = JSON.parse(jsonLine) 98 | resolve(analysis) 99 | } catch (error) { 100 | console.error('JSON parsing error:', error) 101 | console.error('Raw output:', stdout) 102 | reject(error) 103 | } 104 | }) 105 | 106 | rustApp.on('error', (error) => { 107 | console.warn(`Failed to spawn Rust app: ${error.message}`) 108 | resolve(null) 109 | }) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /src/cache/clean.rs: -------------------------------------------------------------------------------- 1 | use super::backup::safe_delete_entries; 2 | use super::detect::{ 3 | detect_cloudflare_caches, detect_hf_caches, detect_java_caches, detect_js_caches, 4 | detect_js_pm_caches, detect_npx_caches, detect_python_caches, detect_rust_caches, 5 | detect_torch_caches, detect_vercel_caches, 6 | }; 7 | use super::types::{CleanResponse, CleanSummary}; 8 | use std::path::Path; 9 | 10 | pub async fn clean_caches( 11 | lang: &str, 12 | include_npx: bool, 13 | include_js_pm: bool, 14 | safe_delete: bool, 15 | _force: bool, 16 | include_hf: bool, 17 | include_torch: bool, 18 | include_vercel: bool, 19 | include_cloudflare: bool, 20 | stale_days: Option, 21 | ) -> CleanResponse { 22 | let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); 23 | 24 | let mut entries = Vec::new(); 25 | 26 | // If specific flags are provided, only use those 27 | if include_npx 28 | || include_js_pm 29 | || include_hf 30 | || include_torch 31 | || include_vercel 32 | || include_cloudflare 33 | { 34 | // NPX caches 35 | if include_npx { 36 | entries.extend(detect_npx_caches(stale_days)); 37 | } 38 | 39 | // JS Package Manager caches 40 | if include_js_pm { 41 | entries.extend(detect_js_pm_caches()); 42 | } 43 | 44 | // Specialized integrations 45 | if include_hf { 46 | entries.extend(detect_hf_caches()); 47 | } 48 | 49 | if include_torch { 50 | entries.extend(detect_torch_caches()); 51 | } 52 | 53 | if include_vercel { 54 | entries.extend(detect_vercel_caches()); 55 | } 56 | 57 | if include_cloudflare { 58 | entries.extend(detect_cloudflare_caches()); 59 | } 60 | } else { 61 | // Use language-based detection 62 | // Rust caches 63 | if lang == "auto" || lang == "rust" { 64 | entries.extend(detect_rust_caches(Path::new(&cwd))); 65 | } 66 | 67 | // JavaScript/TypeScript caches 68 | if lang == "auto" || lang == "js" { 69 | entries.extend(detect_js_caches(Path::new(&cwd))); 70 | } 71 | 72 | // Python caches 73 | if lang == "auto" || lang == "py" { 74 | entries.extend(detect_python_caches()); 75 | } 76 | 77 | // Java caches 78 | if lang == "auto" || lang == "java" { 79 | entries.extend(detect_java_caches()); 80 | } 81 | } 82 | 83 | match safe_delete_entries(&entries, safe_delete).await { 84 | Ok((deleted, backup_path)) => { 85 | let freed_bytes: u64 = deleted.iter().map(|e| e.size_bytes).sum(); 86 | let deleted_count = deleted.len(); 87 | CleanResponse { 88 | deleted, 89 | backed_up_to: backup_path, 90 | summary: CleanSummary { 91 | freed_bytes: freed_bytes, 92 | deleted_count: deleted_count, 93 | }, 94 | } 95 | } 96 | Err(e) => { 97 | eprintln!("Error during cleanup: {}", e); 98 | CleanResponse { 99 | deleted: vec![], 100 | backed_up_to: None, 101 | summary: CleanSummary { 102 | freed_bytes: 0, 103 | deleted_count: 0, 104 | }, 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /dashboard/server/api/history/stats.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | 8 | try { 9 | // Try to find the port-kill-console binary 10 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 11 | 12 | if (!binaryPath) { 13 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 14 | } 15 | 16 | // Get statistics from Rust app 17 | const stats = await getHistoryStatistics(binaryPath) 18 | 19 | return { 20 | success: true, 21 | stats, 22 | timestamp: new Date().toISOString() 23 | } 24 | 25 | } catch (error) { 26 | console.error('Error fetching history statistics:', error) 27 | 28 | return { 29 | success: false, 30 | error: error.message, 31 | stats: null, 32 | timestamp: new Date().toISOString() 33 | } 34 | } 35 | }) 36 | 37 | function findPortKillBinary(defaultPath: string): string | null { 38 | // Check if the default path exists 39 | if (existsSync(defaultPath)) { 40 | return defaultPath 41 | } 42 | 43 | // Try common locations 44 | const commonPaths = [ 45 | join(process.cwd(), 'target', 'release', 'port-kill-console'), 46 | join(process.cwd(), 'target', 'release', 'port-kill-console.exe'), 47 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console'), 48 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console.exe'), 49 | './target/release/port-kill-console', 50 | './target/release/port-kill-console.exe', 51 | '../target/release/port-kill-console', 52 | '../target/release/port-kill-console.exe' 53 | ] 54 | 55 | for (const path of commonPaths) { 56 | if (existsSync(path)) { 57 | return path 58 | } 59 | } 60 | 61 | return null 62 | } 63 | 64 | async function getHistoryStatistics(binaryPath: string): Promise { 65 | return new Promise((resolve, reject) => { 66 | const rustApp = spawn(binaryPath, ['--show-stats', '--json'], { 67 | stdio: ['pipe', 'pipe', 'pipe'] 68 | }) 69 | 70 | let stdout = '' 71 | let stderr = '' 72 | 73 | rustApp.stdout.on('data', (data) => { 74 | stdout += data.toString() 75 | }) 76 | 77 | rustApp.stderr.on('data', (data) => { 78 | stderr += data.toString() 79 | }) 80 | 81 | rustApp.on('close', (code) => { 82 | if (code !== 0) { 83 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 84 | resolve(null) 85 | return 86 | } 87 | 88 | try { 89 | // Parse JSON output from Rust app 90 | const lines = stdout.trim().split('\n') 91 | let stats = null 92 | 93 | for (const line of lines) { 94 | if (line.trim()) { 95 | try { 96 | const parsed = JSON.parse(line) 97 | if (parsed.total_kills !== undefined) { 98 | stats = parsed 99 | break 100 | } 101 | } catch (e) { 102 | // Skip invalid JSON lines 103 | } 104 | } 105 | } 106 | 107 | resolve(stats) 108 | } catch (error) { 109 | reject(error) 110 | } 111 | }) 112 | 113 | rustApp.on('error', (error) => { 114 | console.warn(`Failed to spawn Rust app: ${error.message}`) 115 | resolve(null) 116 | }) 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /dashboard/server/api/history/suggestions.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | 8 | try { 9 | // Try to find the port-kill-console binary 10 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 11 | 12 | if (!binaryPath) { 13 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 14 | } 15 | 16 | // Get suggestions from Rust app 17 | const suggestions = await getIgnoreSuggestions(binaryPath) 18 | 19 | return { 20 | success: true, 21 | suggestions, 22 | timestamp: new Date().toISOString() 23 | } 24 | 25 | } catch (error) { 26 | console.error('Error fetching ignore suggestions:', error) 27 | 28 | return { 29 | success: false, 30 | error: error.message, 31 | suggestions: null, 32 | timestamp: new Date().toISOString() 33 | } 34 | } 35 | }) 36 | 37 | function findPortKillBinary(defaultPath: string): string | null { 38 | // Check if the default path exists 39 | if (existsSync(defaultPath)) { 40 | return defaultPath 41 | } 42 | 43 | // Try common locations 44 | const commonPaths = [ 45 | join(process.cwd(), 'target', 'release', 'port-kill-console'), 46 | join(process.cwd(), 'target', 'release', 'port-kill-console.exe'), 47 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console'), 48 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console.exe'), 49 | './target/release/port-kill-console', 50 | './target/release/port-kill-console.exe', 51 | '../target/release/port-kill-console', 52 | '../target/release/port-kill-console.exe' 53 | ] 54 | 55 | for (const path of commonPaths) { 56 | if (existsSync(path)) { 57 | return path 58 | } 59 | } 60 | 61 | return null 62 | } 63 | 64 | async function getIgnoreSuggestions(binaryPath: string): Promise { 65 | return new Promise((resolve, reject) => { 66 | const rustApp = spawn(binaryPath, ['--show-suggestions', '--json'], { 67 | stdio: ['pipe', 'pipe', 'pipe'] 68 | }) 69 | 70 | let stdout = '' 71 | let stderr = '' 72 | 73 | rustApp.stdout.on('data', (data) => { 74 | stdout += data.toString() 75 | }) 76 | 77 | rustApp.stderr.on('data', (data) => { 78 | stderr += data.toString() 79 | }) 80 | 81 | rustApp.on('close', (code) => { 82 | if (code !== 0) { 83 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 84 | resolve(null) 85 | return 86 | } 87 | 88 | try { 89 | // Parse JSON output from Rust app 90 | const lines = stdout.trim().split('\n') 91 | let suggestions = null 92 | 93 | for (const line of lines) { 94 | if (line.trim()) { 95 | try { 96 | const parsed = JSON.parse(line) 97 | if (parsed.suggested_ports !== undefined) { 98 | suggestions = parsed 99 | break 100 | } 101 | } catch (e) { 102 | // Skip invalid JSON lines 103 | } 104 | } 105 | } 106 | 107 | resolve(suggestions) 108 | } catch (error) { 109 | reject(error) 110 | } 111 | }) 112 | 113 | rustApp.on('error', (error) => { 114 | console.warn(`Failed to spawn Rust app: ${error.message}`) 115 | resolve(null) 116 | }) 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /dashboard/server/api/history/offenders.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | 8 | try { 9 | // Try to find the port-kill-console binary 10 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 11 | 12 | if (!binaryPath) { 13 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 14 | } 15 | 16 | // Get frequent offenders from Rust app 17 | const offenders = await getFrequentOffenders(binaryPath) 18 | 19 | return { 20 | success: true, 21 | offenders, 22 | count: offenders.length, 23 | timestamp: new Date().toISOString() 24 | } 25 | 26 | } catch (error) { 27 | console.error('Error fetching frequent offenders:', error) 28 | 29 | return { 30 | success: false, 31 | error: error.message, 32 | offenders: [], 33 | count: 0, 34 | timestamp: new Date().toISOString() 35 | } 36 | } 37 | }) 38 | 39 | function findPortKillBinary(defaultPath: string): string | null { 40 | // Check if the default path exists 41 | if (existsSync(defaultPath)) { 42 | return defaultPath 43 | } 44 | 45 | // Try common locations 46 | const commonPaths = [ 47 | join(process.cwd(), 'target', 'release', 'port-kill-console'), 48 | join(process.cwd(), 'target', 'release', 'port-kill-console.exe'), 49 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console'), 50 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console.exe'), 51 | './target/release/port-kill-console', 52 | './target/release/port-kill-console.exe', 53 | '../target/release/port-kill-console', 54 | '../target/release/port-kill-console.exe' 55 | ] 56 | 57 | for (const path of commonPaths) { 58 | if (existsSync(path)) { 59 | return path 60 | } 61 | } 62 | 63 | return null 64 | } 65 | 66 | async function getFrequentOffenders(binaryPath: string): Promise { 67 | return new Promise((resolve, reject) => { 68 | const rustApp = spawn(binaryPath, ['--show-offenders', '--json'], { 69 | stdio: ['pipe', 'pipe', 'pipe'] 70 | }) 71 | 72 | let stdout = '' 73 | let stderr = '' 74 | 75 | rustApp.stdout.on('data', (data) => { 76 | stdout += data.toString() 77 | }) 78 | 79 | rustApp.stderr.on('data', (data) => { 80 | stderr += data.toString() 81 | }) 82 | 83 | rustApp.on('close', (code) => { 84 | if (code !== 0) { 85 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 86 | resolve([]) 87 | return 88 | } 89 | 90 | try { 91 | // Parse JSON output from Rust app 92 | const lines = stdout.trim().split('\n') 93 | const offenders = [] 94 | 95 | for (const line of lines) { 96 | if (line.trim()) { 97 | try { 98 | const offender = JSON.parse(line) 99 | if (offender.process_name && offender.port) { 100 | offenders.push(offender) 101 | } 102 | } catch (e) { 103 | // Skip invalid JSON lines 104 | } 105 | } 106 | } 107 | 108 | resolve(offenders) 109 | } catch (error) { 110 | reject(error) 111 | } 112 | }) 113 | 114 | rustApp.on('error', (error) => { 115 | console.warn(`Failed to spawn Rust app: ${error.message}`) 116 | resolve([]) 117 | }) 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /src/cache/output.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::Serialize; 3 | 4 | pub fn print_or_json(value: &T, json: bool) { 5 | if json { 6 | match serde_json::to_string_pretty(value) { 7 | Ok(s) => println!("{}", s), 8 | Err(e) => eprintln!("Failed to serialize JSON: {}", e), 9 | } 10 | } else { 11 | println!("{:?}", value); 12 | } 13 | } 14 | 15 | pub fn human_size(bytes: u64) -> String { 16 | const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; 17 | if bytes == 0 { 18 | return "0 B".to_string(); 19 | } 20 | 21 | let mut size = bytes as f64; 22 | let mut unit_index = 0; 23 | 24 | while size >= 1024.0 && unit_index < UNITS.len() - 1 { 25 | size /= 1024.0; 26 | unit_index += 1; 27 | } 28 | 29 | format!("{:.2} {}", size, UNITS[unit_index]) 30 | } 31 | 32 | pub fn human_since(ts: Option>) -> String { 33 | match ts { 34 | None => "-".to_string(), 35 | Some(t) => { 36 | let now = Utc::now(); 37 | let dur = now.signed_duration_since(t); 38 | if dur.num_days() >= 1 { 39 | format!("{}d ago", dur.num_days()) 40 | } else if dur.num_hours() >= 1 { 41 | format!("{}h ago", dur.num_hours()) 42 | } else if dur.num_minutes() >= 1 { 43 | format!("{}m ago", dur.num_minutes()) 44 | } else { 45 | "just now".to_string() 46 | } 47 | } 48 | } 49 | } 50 | 51 | pub fn print_table(rows: &[(String, String, String, String, String)]) { 52 | // Columns: PACKAGE | KIND | SIZE | LAST USED | STALE? 53 | // Fixed column widths for proper alignment 54 | let package_width = 50; 55 | let kind_width = 8; 56 | let size_width = 12; 57 | let last_used_width = 16; 58 | let stale_width = 6; 59 | 60 | // Header 61 | println!("{: package_width { 72 | format!("{}...", &package[..package_width - 3]) 73 | } else { 74 | package.clone() 75 | }; 76 | 77 | println!("{: = HashMap::new(); 93 | for entry in &resp.entries { 94 | *kind_sizes.entry(entry.kind.clone()).or_insert(0) += entry.size_bytes; 95 | } 96 | 97 | if !kind_sizes.is_empty() { 98 | println!(); 99 | println!("Size by kind:"); 100 | let mut sorted_kinds: Vec<_> = kind_sizes.iter().collect(); 101 | sorted_kinds.sort_by(|a, b| b.1.cmp(a.1)); // Sort by size descending 102 | 103 | for (kind, size) in sorted_kinds { 104 | println!(" {}: {}", kind, human_size(*size)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Port Kill Release Script 4 | # This script creates a new release by creating a tag and pushing it to GitHub 5 | 6 | set -e 7 | 8 | if [ $# -eq 0 ]; then 9 | echo "🚀 Port Kill Release Script" 10 | echo "==========================" 11 | echo "" 12 | echo "Usage: ./release.sh " 13 | echo "" 14 | echo "Examples:" 15 | echo " ./release.sh 0.1.0 # Creates v0.1.0 release" 16 | echo " ./release.sh 0.2.0 # Creates v0.2.0 release" 17 | echo " ./release.sh 1.0.0 # Creates v1.0.0 release" 18 | echo "" 19 | echo "This will:" 20 | echo " 1. Create a git tag v" 21 | echo " 2. Push the tag to GitHub" 22 | echo " 3. Trigger automatic release creation" 23 | echo " 4. Build and upload binaries for all platforms" 24 | echo "" 25 | exit 1 26 | fi 27 | 28 | VERSION=$1 29 | 30 | # Validate version format (simple check) 31 | if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 32 | echo "❌ Invalid version format: $VERSION" 33 | echo " Please use semantic versioning (e.g., 0.1.0, 1.0.0, 2.1.3)" 34 | exit 1 35 | fi 36 | 37 | TAG="v$VERSION" 38 | 39 | echo "🚀 Creating release for Port Kill $VERSION" 40 | echo "==========================================" 41 | echo "" 42 | 43 | # Check if we're on main branch 44 | CURRENT_BRANCH=$(git branch --show-current) 45 | if [ "$CURRENT_BRANCH" != "main" ]; then 46 | echo "⚠️ Warning: You're not on the main branch (current: $CURRENT_BRANCH)" 47 | echo " It's recommended to create releases from the main branch" 48 | echo "" 49 | read -p "Continue anyway? (y/N): " -n 1 -r 50 | echo 51 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 52 | echo "❌ Release cancelled" 53 | exit 1 54 | fi 55 | fi 56 | 57 | # Check if tag already exists 58 | if git tag -l | grep -q "^$TAG$"; then 59 | echo "❌ Tag $TAG already exists!" 60 | echo " Please use a different version or delete the existing tag:" 61 | echo " git tag -d $TAG && git push origin :refs/tags/$TAG" 62 | exit 1 63 | fi 64 | 65 | # Check if there are uncommitted changes 66 | if ! git diff-index --quiet HEAD --; then 67 | echo "⚠️ Warning: You have uncommitted changes" 68 | echo " It's recommended to commit all changes before creating a release" 69 | echo "" 70 | read -p "Continue anyway? (y/N): " -n 1 -r 71 | echo 72 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 73 | echo "❌ Release cancelled" 74 | exit 1 75 | fi 76 | fi 77 | 78 | echo "📋 Release Summary:" 79 | echo " Version: $VERSION" 80 | echo " Tag: $TAG" 81 | echo " Branch: $CURRENT_BRANCH" 82 | echo "" 83 | 84 | # Create the tag 85 | echo "🏷️ Creating tag $TAG..." 86 | git tag $TAG 87 | 88 | if [ $? -eq 0 ]; then 89 | echo "✅ Tag created successfully" 90 | else 91 | echo "❌ Failed to create tag" 92 | exit 1 93 | fi 94 | 95 | # Push the tag 96 | echo "📤 Pushing tag to GitHub..." 97 | git push origin $TAG 98 | 99 | if [ $? -eq 0 ]; then 100 | echo "✅ Tag pushed successfully" 101 | else 102 | echo "❌ Failed to push tag" 103 | echo " You may need to:" 104 | echo " 1. Check your GitHub credentials" 105 | echo " 2. Ensure you have push access to the repository" 106 | exit 1 107 | fi 108 | 109 | echo "" 110 | echo "🎉 Release process started!" 111 | echo "" 112 | echo "📋 What happens next:" 113 | echo " 1. GitHub Actions will automatically create a release" 114 | echo " 2. Binaries will be built for all platforms (macOS, Linux, Windows)" 115 | echo " 3. Release assets will be uploaded" 116 | echo " 4. Install scripts will become available" 117 | echo "" 118 | echo "🔗 Monitor progress:" 119 | echo " - GitHub Actions: https://github.com/treadiehq/port-kill/actions" 120 | echo " - Releases: https://github.com/treadiehq/port-kill/releases" 121 | echo "" 122 | echo "⏱️ This process typically takes 5-10 minutes to complete." 123 | echo "" 124 | echo "�� Happy releasing!" 125 | -------------------------------------------------------------------------------- /dashboard/server/api/guard/status.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec } from 'child_process' 2 | import { existsSync, readFileSync } from 'fs' 3 | import { join } from 'path' 4 | import { homedir } from 'os' 5 | 6 | export default defineEventHandler(async (event) => { 7 | try { 8 | const binaryPath = findPortKillBinary() 9 | 10 | if (!binaryPath) { 11 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 12 | } 13 | 14 | const status = await getGuardStatus(binaryPath) 15 | 16 | return { 17 | success: true, 18 | status, 19 | timestamp: new Date().toISOString() 20 | } 21 | 22 | } catch (error) { 23 | console.error('Error fetching guard status:', error) 24 | 25 | return { 26 | success: false, 27 | error: error.message, 28 | status: null, 29 | timestamp: new Date().toISOString() 30 | } 31 | } 32 | }) 33 | 34 | function findPortKillBinary(): string | null { 35 | // Try common locations 36 | const commonPaths = [ 37 | join(process.cwd(), 'target', 'release', 'port-kill-console'), 38 | join(process.cwd(), 'target', 'release', 'port-kill-console.exe'), 39 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console'), 40 | join(process.cwd(), '..', 'target', 'release', 'port-kill-console.exe'), 41 | '/usr/local/bin/port-kill-console', 42 | '/opt/port-kill/port-kill-console', 43 | ] 44 | 45 | for (const path of commonPaths) { 46 | if (existsSync(path)) { 47 | return path 48 | } 49 | } 50 | 51 | return null 52 | } 53 | 54 | async function getGuardStatus(binaryPath: string): Promise { 55 | return new Promise((resolve, reject) => { 56 | // Use a simpler approach with timeout to avoid hanging 57 | 58 | exec('ps aux | grep "port-kill-console.*guard-mode" | grep -v grep', { timeout: 5000 }, (error: any, stdout: string, stderr: string) => { 59 | if (error && error.code !== 1) { // code 1 means no matches found, which is OK 60 | console.error('Error checking guard status:', error) 61 | reject(error) 62 | return 63 | } 64 | 65 | const isGuardRunning = stdout.trim().length > 0 66 | 67 | if (isGuardRunning) { 68 | // Extract guard ports from the command line 69 | const portMatch = stdout.match(/--guard-ports\s+([0-9,]+)/) 70 | const watchedPorts = portMatch ? portMatch[1].split(',').map(Number) : [3000, 3001, 3002, 8000, 8080, 9000] 71 | 72 | // Check for auto-resolve flag 73 | const autoResolve = stdout.includes('--auto-resolve') 74 | 75 | // Read actual reservations from file 76 | let activeReservations = [] 77 | try { 78 | const reservationsPath = join(homedir(), '.port-kill', 'reservations.json') 79 | if (existsSync(reservationsPath)) { 80 | const reservationsData = readFileSync(reservationsPath, 'utf8') 81 | const reservations = JSON.parse(reservationsData) 82 | activeReservations = Object.values(reservations) 83 | } 84 | } catch (error) { 85 | console.error('Error reading reservations:', error) 86 | } 87 | 88 | const status = { 89 | is_active: true, 90 | watched_ports: watchedPorts, 91 | active_reservations: activeReservations, 92 | conflicts_resolved: 0, 93 | last_activity: new Date().toISOString(), 94 | auto_resolve_enabled: autoResolve 95 | } 96 | 97 | resolve(status) 98 | } else { 99 | // Guard is not running 100 | const status = { 101 | is_active: false, 102 | watched_ports: [3000, 3001, 3002, 8000, 8080, 9000], 103 | active_reservations: [], 104 | conflicts_resolved: 0, 105 | last_activity: null, 106 | auto_resolve_enabled: false 107 | } 108 | 109 | resolve(status) 110 | } 111 | }) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /install-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Port Kill Release Installer 4 | # Downloads and installs the latest release for your platform 5 | 6 | set -e 7 | 8 | REPO="treadiehq/port-kill" 9 | LATEST_RELEASE_URL="https://api.github.com/repos/$REPO/releases/latest" 10 | 11 | echo "🚀 Port Kill Release Installer" 12 | echo "==============================" 13 | echo "" 14 | 15 | # Detect platform and architecture 16 | PLATFORM="" 17 | BINARY_NAME="" 18 | CONSOLE_BINARY_NAME="" 19 | 20 | if [[ "$OSTYPE" == "darwin"* ]]; then 21 | PLATFORM="macos" 22 | # Detect CPU architecture for macOS 23 | ARCH=$(uname -m) 24 | if [[ "$ARCH" == "arm64" ]]; then 25 | BINARY_NAME="port-kill-macos" 26 | CONSOLE_BINARY_NAME="port-kill-console-macos" 27 | echo "✅ Detected platform: macOS (Apple Silicon)" 28 | elif [[ "$ARCH" == "x86_64" ]]; then 29 | BINARY_NAME="port-kill-macos-intel" 30 | CONSOLE_BINARY_NAME="port-kill-console-macos-intel" 31 | echo "✅ Detected platform: macOS (Intel)" 32 | else 33 | echo "❌ Unsupported macOS architecture: $ARCH" 34 | echo " Please download manually from: https://github.com/$REPO/releases" 35 | exit 1 36 | fi 37 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 38 | PLATFORM="linux" 39 | BINARY_NAME="port-kill-linux" 40 | CONSOLE_BINARY_NAME="port-kill-console-linux" 41 | echo "✅ Detected platform: Linux" 42 | else 43 | echo "❌ Unsupported platform: $OSTYPE" 44 | echo " Please download manually from: https://github.com/$REPO/releases" 45 | exit 1 46 | fi 47 | 48 | # Get latest release info 49 | echo "📡 Fetching latest release information..." 50 | LATEST_TAG=$(curl -s "$LATEST_RELEASE_URL" | grep '"tag_name"' | cut -d'"' -f4) 51 | 52 | if [[ -z "$LATEST_TAG" ]]; then 53 | echo "❌ No releases found or failed to get latest release information" 54 | echo "" 55 | echo "📋 No releases are currently available. You have two options:" 56 | echo "" 57 | echo " 1. 🏗️ Build from source (recommended):" 58 | echo " ./install.sh" 59 | echo "" 60 | echo " 2. 📦 Wait for a release to be published:" 61 | echo " Visit: https://github.com/$REPO/releases" 62 | echo "" 63 | echo " To create a release, the repository owner needs to:" 64 | echo " - Go to GitHub repository" 65 | echo " - Click 'Releases' → 'Create a new release'" 66 | echo " - Set tag (e.g., v0.1.0) and publish" 67 | echo "" 68 | exit 1 69 | fi 70 | 71 | echo "📦 Latest release: $LATEST_TAG" 72 | 73 | # Create installation directory 74 | INSTALL_DIR="$HOME/.local/bin" 75 | mkdir -p "$INSTALL_DIR" 76 | 77 | echo "📁 Installing to: $INSTALL_DIR" 78 | 79 | # Download and install binary 80 | echo "⬇️ Downloading $BINARY_NAME..." 81 | DOWNLOAD_URL="https://github.com/$REPO/releases/download/$LATEST_TAG/$BINARY_NAME" 82 | if ! curl -L -o "$INSTALL_DIR/port-kill" "$DOWNLOAD_URL"; then 83 | echo "❌ Failed to download $BINARY_NAME" 84 | echo " URL: $DOWNLOAD_URL" 85 | echo " Please check if the release assets are available" 86 | exit 1 87 | fi 88 | chmod +x "$INSTALL_DIR/port-kill" 89 | 90 | # Download and install console binary 91 | echo "⬇️ Downloading $CONSOLE_BINARY_NAME..." 92 | CONSOLE_DOWNLOAD_URL="https://github.com/$REPO/releases/download/$LATEST_TAG/$CONSOLE_BINARY_NAME" 93 | if ! curl -L -o "$INSTALL_DIR/port-kill-console" "$CONSOLE_DOWNLOAD_URL"; then 94 | echo "❌ Failed to download $CONSOLE_BINARY_NAME" 95 | echo " URL: $CONSOLE_DOWNLOAD_URL" 96 | echo " Please check if the release assets are available" 97 | exit 1 98 | fi 99 | chmod +x "$INSTALL_DIR/port-kill-console" 100 | 101 | echo "" 102 | echo "✅ Installation complete!" 103 | echo "" 104 | echo "📋 Usage:" 105 | echo " System tray mode: port-kill --ports 3000,8000" 106 | echo " Console mode: port-kill-console --console --ports 3000,8000" 107 | echo "" 108 | echo "🔧 Make sure $INSTALL_DIR is in your PATH:" 109 | echo " export PATH=\"\$PATH:$INSTALL_DIR\"" 110 | echo "" 111 | echo "📖 For more options: port-kill --help" 112 | -------------------------------------------------------------------------------- /ci-samples/gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - cache-analysis 3 | - cache-cleanup 4 | - diagnostics 5 | 6 | variables: 7 | CACHE_THRESHOLD: "1000000000" # 1GB in bytes 8 | STALE_DAYS: "30" 9 | 10 | cache-analysis: 11 | stage: cache-analysis 12 | image: rust:1.75 13 | script: 14 | - echo "=== Building port-kill ===" 15 | - cargo build --release 16 | 17 | - echo "=== Cache Analysis ===" 18 | - ./target/release/port-kill-console cache --list --json > cache-analysis.json 19 | - echo "Cache size: $(jq -r '.summary.totalSizeBytes' cache-analysis.json) bytes" 20 | - echo "Cache entries: $(jq -r '.summary.count' cache-analysis.json)" 21 | - echo "Stale entries: $(jq -r '.summary.staleCount' cache-analysis.json)" 22 | 23 | # Set variables for next stage 24 | - echo "CACHE_SIZE=$(jq -r '.summary.totalSizeBytes' cache-analysis.json)" >> cache_vars.env 25 | - echo "CACHE_ENTRIES=$(jq -r '.summary.count' cache-analysis.json)" >> cache_vars.env 26 | - echo "STALE_ENTRIES=$(jq -r '.summary.staleCount' cache-analysis.json)" >> cache_vars.env 27 | artifacts: 28 | reports: 29 | dotenv: cache_vars.env 30 | paths: 31 | - cache-analysis.json 32 | - target/release/port-kill-console 33 | expire_in: 1 hour 34 | 35 | cache-cleanup: 36 | stage: cache-cleanup 37 | image: rust:1.75 38 | dependencies: 39 | - cache-analysis 40 | script: 41 | - echo "=== Cache Cleanup ===" 42 | - | 43 | if [ "$CACHE_SIZE" -gt "$CACHE_THRESHOLD" ]; then 44 | echo "Cache size ($CACHE_SIZE bytes) exceeds threshold ($CACHE_THRESHOLD bytes)" 45 | echo "Proceeding with cleanup..." 46 | 47 | # Clean based on cache type or all 48 | case "${CACHE_TYPE:-all}" in 49 | "rust") 50 | ./target/release/port-kill-console cache --lang rust --clean --safe-delete 51 | ;; 52 | "js") 53 | ./target/release/port-kill-console cache --lang js --clean --safe-delete 54 | ;; 55 | "python") 56 | ./target/release/port-kill-console cache --lang py --clean --safe-delete 57 | ;; 58 | "java") 59 | ./target/release/port-kill-console cache --lang java --clean --safe-delete 60 | ;; 61 | "npx") 62 | ./target/release/port-kill-console cache --npx --clean --safe-delete --stale-days $STALE_DAYS 63 | ;; 64 | "js-pm") 65 | ./target/release/port-kill-console cache --js-pm --clean --safe-delete 66 | ;; 67 | *) 68 | ./target/release/port-kill-console cache --clean --safe-delete 69 | ;; 70 | esac 71 | 72 | echo "Cache cleanup completed" 73 | else 74 | echo "Cache size ($CACHE_SIZE bytes) is within acceptable limits" 75 | echo "Skipping cleanup" 76 | fi 77 | rules: 78 | - if: $CI_PIPELINE_SOURCE == "schedule" 79 | - if: $CI_PIPELINE_SOURCE == "web" 80 | when: manual 81 | allow_failure: true 82 | 83 | diagnostics: 84 | stage: diagnostics 85 | image: rust:1.75 86 | dependencies: 87 | - cache-analysis 88 | script: 89 | - echo "=== System Diagnostics ===" 90 | - ./target/release/port-kill-console cache --doctor --json > diagnostics.json 91 | - echo "System health check completed" 92 | - cat diagnostics.json 93 | artifacts: 94 | paths: 95 | - diagnostics.json 96 | expire_in: 1 week 97 | rules: 98 | - if: $CI_PIPELINE_SOURCE == "schedule" 99 | - if: $CI_PIPELINE_SOURCE == "web" 100 | when: manual 101 | 102 | # Scheduled pipeline for daily cache cleanup 103 | cache-cleanup-scheduled: 104 | extends: cache-cleanup 105 | rules: 106 | - if: $CI_PIPELINE_SOURCE == "schedule" 107 | when: always 108 | variables: 109 | CACHE_TYPE: "all" 110 | 111 | # Manual pipeline for specific cache types 112 | cache-cleanup-manual: 113 | extends: cache-cleanup 114 | rules: 115 | - if: $CI_PIPELINE_SOURCE == "web" 116 | when: manual 117 | variables: 118 | CACHE_TYPE: "npx" 119 | -------------------------------------------------------------------------------- /src/cache/list.rs: -------------------------------------------------------------------------------- 1 | use super::detect::{ 2 | detect_cloudflare_caches, detect_hf_caches, detect_java_caches, detect_js_caches, 3 | detect_js_pm_caches, detect_npx_caches, detect_python_caches, detect_rust_caches, 4 | detect_torch_caches, detect_vercel_caches, 5 | }; 6 | use super::output::{human_since, human_size, print_cache_summary, print_table}; 7 | use super::types::{ListResponse, ListSummary}; 8 | use std::path::Path; 9 | 10 | pub async fn list_caches( 11 | lang: &str, 12 | include_npx: bool, 13 | include_js_pm: bool, 14 | include_hf: bool, 15 | include_torch: bool, 16 | include_vercel: bool, 17 | include_cloudflare: bool, 18 | stale_days: Option, 19 | ) -> ListResponse { 20 | let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); 21 | 22 | let mut entries = Vec::new(); 23 | 24 | // If specific flags are provided, only use those 25 | if include_npx 26 | || include_js_pm 27 | || include_hf 28 | || include_torch 29 | || include_vercel 30 | || include_cloudflare 31 | { 32 | // NPX caches 33 | if include_npx { 34 | entries.extend(detect_npx_caches(stale_days)); 35 | } 36 | 37 | // JS Package Manager caches 38 | if include_js_pm { 39 | entries.extend(detect_js_pm_caches()); 40 | } 41 | 42 | // Specialized integrations 43 | if include_hf { 44 | entries.extend(detect_hf_caches()); 45 | } 46 | 47 | if include_torch { 48 | entries.extend(detect_torch_caches()); 49 | } 50 | 51 | if include_vercel { 52 | entries.extend(detect_vercel_caches()); 53 | } 54 | 55 | if include_cloudflare { 56 | entries.extend(detect_cloudflare_caches()); 57 | } 58 | } else { 59 | // Use language-based detection 60 | // Rust caches 61 | if lang == "auto" || lang == "rust" { 62 | entries.extend(detect_rust_caches(Path::new(&cwd))); 63 | } 64 | 65 | // JavaScript/TypeScript caches 66 | if lang == "auto" || lang == "js" { 67 | entries.extend(detect_js_caches(Path::new(&cwd))); 68 | } 69 | 70 | // Python caches 71 | if lang == "auto" || lang == "py" { 72 | entries.extend(detect_python_caches()); 73 | } 74 | 75 | // Java caches 76 | if lang == "auto" || lang == "java" { 77 | entries.extend(detect_java_caches()); 78 | } 79 | } 80 | 81 | // summary 82 | let mut total = 0u64; 83 | let mut stale = 0usize; 84 | for e in &entries { 85 | total = total.saturating_add(e.size_bytes); 86 | if e.stale { 87 | stale += 1; 88 | } 89 | } 90 | let count = entries.len(); 91 | let resp = ListResponse { 92 | entries, 93 | summary: ListSummary { 94 | total_size_bytes: total, 95 | count, 96 | stale_count: stale, 97 | }, 98 | }; 99 | resp 100 | } 101 | 102 | pub fn print_list_table(resp: &ListResponse) { 103 | let rows = resp 104 | .entries 105 | .iter() 106 | .map(|e| { 107 | // For NPX entries, show package name and version instead of path 108 | let display_name = if e.kind == "npx" { 109 | let version = e 110 | .details 111 | .get("version") 112 | .and_then(|v| v.as_str()) 113 | .unwrap_or("unknown"); 114 | format!("{}:{}", e.name, version) 115 | } else { 116 | e.path.clone() 117 | }; 118 | 119 | ( 120 | display_name, 121 | e.kind.clone(), 122 | human_size(e.size_bytes), 123 | human_since(e.last_used_at), 124 | if e.stale { 125 | "Yes".to_string() 126 | } else { 127 | "No".to_string() 128 | }, 129 | ) 130 | }) 131 | .collect::>(); 132 | print_table(&rows); 133 | print_cache_summary(resp); 134 | } 135 | -------------------------------------------------------------------------------- /validate-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configuration validation script (works without Nix installed) 4 | echo "🔍 Validating Port Kill Configuration" 5 | echo "=====================================" 6 | echo "" 7 | 8 | # Check if Nix is available 9 | if command -v nix &> /dev/null; then 10 | echo "✅ Nix is available: $(nix --version)" 11 | echo "" 12 | 13 | # Test flake check 14 | echo "🔍 Checking flake configuration..." 15 | if nix flake check . 2>/dev/null; then 16 | echo "✅ Flake configuration is valid" 17 | else 18 | echo "❌ Flake configuration has issues:" 19 | nix flake check . 2>&1 | head -10 20 | exit 1 21 | fi 22 | echo "" 23 | 24 | # Test development shell 25 | echo "🧪 Testing development shell..." 26 | if nix develop --dry-run . 2>/dev/null; then 27 | echo "✅ Development shell configuration is valid" 28 | else 29 | echo "❌ Development shell has issues" 30 | exit 1 31 | fi 32 | echo "" 33 | 34 | # Test build (dry run) 35 | echo "🔨 Testing build (dry run)..." 36 | if nix build --dry-run .#default 2>/dev/null; then 37 | echo "✅ Build configuration is valid" 38 | else 39 | echo "❌ Build configuration has issues:" 40 | nix build --dry-run .#default 2>&1 | head -10 41 | exit 1 42 | fi 43 | echo "" 44 | 45 | echo "🎉 All Nix tests passed!" 46 | else 47 | echo "⚠️ Nix is not installed. Performing basic validation..." 48 | echo "" 49 | 50 | # Basic file validation 51 | echo "📁 Checking required files..." 52 | if [ -f "flake.nix" ]; then 53 | echo "✅ flake.nix exists" 54 | else 55 | echo "❌ flake.nix missing" 56 | exit 1 57 | fi 58 | 59 | if [ -f "shell.nix" ]; then 60 | echo "✅ shell.nix exists" 61 | else 62 | echo "❌ shell.nix missing" 63 | exit 1 64 | fi 65 | 66 | if [ -f ".github/workflows/nix-build.yml" ]; then 67 | echo "✅ GitHub Actions workflow exists" 68 | else 69 | echo "❌ GitHub Actions workflow missing" 70 | exit 1 71 | fi 72 | 73 | if [ -f "Cargo.toml" ]; then 74 | echo "✅ Cargo.toml exists" 75 | else 76 | echo "❌ Cargo.toml missing" 77 | exit 1 78 | fi 79 | 80 | if [ -f "Cargo.lock" ]; then 81 | echo "✅ Cargo.lock exists" 82 | else 83 | echo "❌ Cargo.lock missing" 84 | exit 1 85 | fi 86 | echo "" 87 | 88 | # Basic syntax validation 89 | echo "🔍 Checking basic syntax..." 90 | 91 | # Check if flake.nix has required structure 92 | if grep -q "description" flake.nix && grep -q "inputs" flake.nix && grep -q "outputs" flake.nix; then 93 | echo "✅ flake.nix has required structure" 94 | else 95 | echo "❌ flake.nix missing required structure" 96 | exit 1 97 | fi 98 | 99 | # Check if shell.nix has required structure 100 | if grep -q "mkShell" shell.nix; then 101 | echo "✅ shell.nix has required structure" 102 | else 103 | echo "❌ shell.nix missing required structure" 104 | exit 1 105 | fi 106 | 107 | # Check if GitHub Actions workflow has required structure 108 | if grep -q "runs-on:" .github/workflows/nix-build.yml && grep -q "nix build" .github/workflows/nix-build.yml; then 109 | echo "✅ GitHub Actions workflow has required structure" 110 | else 111 | echo "❌ GitHub Actions workflow missing required structure" 112 | exit 1 113 | fi 114 | echo "" 115 | 116 | echo "✅ Basic validation passed!" 117 | echo "" 118 | echo "📋 To fully test, install Nix:" 119 | echo " 1. Install Nix: https://nixos.org/download.html" 120 | echo " 2. Enable flakes: echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf" 121 | echo " 3. Run: ./test-nix.sh" 122 | fi 123 | 124 | echo "" 125 | echo "🎯 Configuration Summary:" 126 | echo " - Nix flake: ✅ Configured" 127 | echo " - Development shell: ✅ Configured" 128 | echo " - GitHub Actions: ✅ Configured" 129 | echo " - Traditional builds: ✅ Still working" 130 | echo "" 131 | echo "🚀 Ready for deployment!" 132 | -------------------------------------------------------------------------------- /dashboard/server/api/security/audit.get.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | 4 | const execAsync = promisify(exec); 5 | 6 | export default defineEventHandler(async (event) => { 7 | try { 8 | // Get query parameters 9 | const query = getQuery(event); 10 | const suspiciousOnly = query.suspicious_only === 'true'; 11 | const securityMode = query.security_mode === 'true'; 12 | const suspiciousPorts = query.suspicious_ports as string || '8444,4444,9999,14444,5555,6666,7777'; 13 | 14 | // For now, return mock data to test dashboard integration 15 | // TODO: Replace with actual command execution once performance is optimized 16 | const mockAuditResult = { 17 | audit_timestamp: new Date().toISOString(), 18 | total_ports_scanned: 5, 19 | suspicious_processes: [ 20 | { 21 | port: 8444, 22 | process_info: { 23 | pid: 12345, 24 | port: 8444, 25 | command: "suspicious-miner", 26 | name: "Crypto Miner Process", 27 | container_id: null, 28 | container_name: null, 29 | command_line: null, 30 | working_directory: "/tmp", 31 | process_group: "Mining", 32 | project_name: null, 33 | cpu_usage: null, 34 | memory_usage: null, 35 | }, 36 | suspicion_reason: "SuspiciousPort", 37 | risk_level: "Critical", 38 | binary_hash: "sha256:abc123def456", 39 | parent_process: "unknown", 40 | network_interface: "0.0.0.0", 41 | first_seen: new Date().toISOString(), 42 | }, 43 | { 44 | port: 3002, 45 | process_info: { 46 | pid: 44663, 47 | port: 3002, 48 | command: "node", 49 | name: "Node.js Process", 50 | container_id: null, 51 | container_name: null, 52 | command_line: null, 53 | working_directory: "/Users/dantelex/port-kill/dashboard", 54 | process_group: "Node.js", 55 | project_name: null, 56 | cpu_usage: null, 57 | memory_usage: null, 58 | }, 59 | suspicion_reason: "UnexpectedLocation", 60 | risk_level: "Medium", 61 | binary_hash: "sha256:def456ghi789", 62 | parent_process: "unknown", 63 | network_interface: "0.0.0.0", 64 | first_seen: new Date().toISOString(), 65 | } 66 | ], 67 | approved_processes: [ 68 | { 69 | port: 22, 70 | process_info: { 71 | pid: 123, 72 | port: 22, 73 | command: "sshd", 74 | name: "SSH Daemon", 75 | container_id: null, 76 | container_name: null, 77 | command_line: null, 78 | working_directory: "/usr/sbin", 79 | process_group: "SSH", 80 | project_name: null, 81 | cpu_usage: null, 82 | memory_usage: null, 83 | }, 84 | service_type: "SSH", 85 | expected_location: "/usr/sbin/", 86 | binary_hash: "sha256:ghi789jkl012", 87 | } 88 | ], 89 | security_score: 40.0, 90 | recommendations: [ 91 | { 92 | title: "Investigate Suspicious Processes", 93 | description: "2 suspicious processes detected on your system", 94 | action: "Review and terminate suspicious processes immediately", 95 | priority: "High", 96 | affected_processes: [8444, 3002], 97 | }, 98 | { 99 | title: "Secure Development Environment", 100 | description: "Node.js process running from non-standard location", 101 | action: "Move development processes to approved directories", 102 | priority: "Medium", 103 | affected_processes: [3002], 104 | } 105 | ], 106 | baseline_comparison: null, 107 | }; 108 | 109 | return { 110 | success: true, 111 | data: mockAuditResult, 112 | timestamp: new Date().toISOString(), 113 | }; 114 | 115 | } catch (error) { 116 | console.error('Security audit error:', error); 117 | 118 | return { 119 | success: false, 120 | error: error instanceof Error ? error.message : 'Unknown error', 121 | timestamp: new Date().toISOString(), 122 | }; 123 | } 124 | }); 125 | -------------------------------------------------------------------------------- /diagnose-installation.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableextensions 3 | 4 | echo Port Kill Installation Diagnostic Tool 5 | echo ===================================== 6 | echo. 7 | 8 | set "REPO=treadiehq/port-kill" 9 | set "BASE_URL=https://github.com/%REPO%/releases/latest/download" 10 | set "INSTALL_DIR=%USERPROFILE%\AppData\Local\port-kill" 11 | 12 | echo 1. Checking GitHub connectivity... 13 | for /f "usebackq delims=" %%i in (`powershell -NoProfile -Command "try { Invoke-WebRequest -UseBasicParsing 'https://github.com' -Method Head | Out-Null; 'OK' } catch { 'FAIL' }"`) do set "NET_STATUS=%%i" 14 | if /I "%NET_STATUS%" NEQ "OK" ( 15 | echo ❌ ERROR: Cannot reach GitHub. Please check your internet or proxy settings. 16 | exit /b 1 17 | ) 18 | 19 | echo ✅ Network OK 20 | 21 | echo. 22 | echo 2. Testing download URLs (latest release)... 23 | echo Testing port-kill download... 24 | powershell -NoProfile -Command "try { $r = Invoke-WebRequest -UseBasicParsing -Uri '%BASE_URL%/port-kill-windows.exe' -Method Head; Write-Host '✅ port-kill: OK (Size:' $r.Headers.'Content-Length' 'bytes)' } catch { Write-Host '❌ port-kill: FAILED -' $_.Exception.Message }" 25 | 26 | echo Testing port-kill-console download... 27 | powershell -NoProfile -Command "try { $r = Invoke-WebRequest -UseBasicParsing -Uri '%BASE_URL%/port-kill-console-windows.exe' -Method Head; Write-Host '✅ port-kill-console: OK (Size:' $r.Headers.'Content-Length' 'bytes)' } catch { Write-Host '❌ port-kill-console: FAILED -' $_.Exception.Message }" 28 | 29 | echo. 30 | echo 3. Checking installation directory... 31 | if exist "%INSTALL_DIR%" ( 32 | echo ✅ Installation directory exists: %INSTALL_DIR% 33 | echo. 34 | echo Files in installation directory: 35 | dir "%INSTALL_DIR%" /b 36 | ) else ( 37 | echo ❌ Installation directory does not exist: %INSTALL_DIR% 38 | echo This means the install script has not been run yet 39 | ) 40 | 41 | echo. 42 | echo 4. Checking PATH configuration... 43 | echo %PATH% | findstr /i "%INSTALL_DIR%" >nul 44 | if %errorlevel% equ 0 ( 45 | echo ✅ Installation directory is in PATH 46 | ) else ( 47 | echo ❌ Installation directory is NOT in PATH 48 | echo This is why 'port-kill-console.exe' is not recognized 49 | ) 50 | 51 | echo. 52 | echo 5. Testing if binaries work (without PATH)... 53 | if exist "%INSTALL_DIR%\port-kill.exe" ( 54 | "%INSTALL_DIR%\port-kill.exe" --version >nul 2>&1 55 | if %errorlevel% equ 0 ( 56 | echo ✅ port-kill.exe is executable 57 | ) else ( 58 | echo ❌ port-kill.exe exists but failed to run 59 | ) 60 | ) 61 | if exist "%INSTALL_DIR%\port-kill-console.exe" ( 62 | "%INSTALL_DIR%\port-kill-console.exe" --version >nul 2>&1 63 | if %errorlevel% equ 0 ( 64 | echo ✅ port-kill-console.exe is executable 65 | ) else ( 66 | echo ❌ port-kill-console.exe exists but failed to run 67 | ) 68 | ) 69 | 70 | echo. 71 | echo ========================================== 72 | echo 6. SOLUTIONS: 73 | echo ========================================== 74 | 75 | echo %PATH% | findstr /i "%INSTALL_DIR%" >nul 76 | if %errorlevel% neq 0 ( 77 | echo. 78 | echo ⚠️ MAIN ISSUE: Installation directory is NOT in your current terminal's PATH 79 | echo. 80 | echo 🔧 SOLUTION A - Restart Terminal (RECOMMENDED): 81 | echo 1. Close this terminal completely 82 | echo 2. Open a new terminal window 83 | echo 3. Try: port-kill --list 84 | echo. 85 | echo 🔧 SOLUTION B - Use Full Path (temporary workaround): 86 | echo "%INSTALL_DIR%\port-kill.exe" --list 87 | echo "%INSTALL_DIR%\port-kill-console.exe" --version 88 | echo. 89 | echo 🔧 SOLUTION C - Reinstall (if files are missing): 90 | echo powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -UseBasicParsing -Headers @{Pragma='no-cache'; 'Cache-Control'='no-cache'} -Uri 'https://raw.githubusercontent.com/treadiehq/port-kill/main/install-release.bat' -OutFile 'install-release.bat'" ^&^& .\install-release.bat 91 | ) else ( 92 | echo. 93 | echo ✅ PATH is configured correctly! 94 | echo. 95 | echo 🧪 Try running these commands: 96 | echo port-kill --list 97 | echo port-kill-console --version 98 | ) 99 | 100 | echo. 101 | echo 📞 If issues persist after trying these solutions, please: 102 | echo - Share this diagnostic output 103 | echo - Join our Discord: https://discord.gg/KqdBcqRk5E 104 | -------------------------------------------------------------------------------- /ci-samples/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: Cache Management 2 | 3 | on: 4 | schedule: 5 | # Run daily at 2 AM UTC 6 | - cron: '0 2 * * *' 7 | workflow_dispatch: 8 | inputs: 9 | cache_type: 10 | description: 'Type of cache to clean' 11 | required: true 12 | default: 'all' 13 | type: choice 14 | options: 15 | - all 16 | - rust 17 | - js 18 | - python 19 | - java 20 | - npx 21 | - js-pm 22 | 23 | jobs: 24 | cache-cleanup: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup Rust 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: stable 35 | components: rustfmt, clippy 36 | 37 | - name: Build port-kill 38 | run: | 39 | cargo build --release 40 | 41 | - name: Cache Analysis 42 | id: cache-analysis 43 | run: | 44 | echo "=== Cache Analysis ===" 45 | ./target/release/port-kill-console cache --list --json > cache-analysis.json 46 | echo "cache_size=$(jq -r '.summary.totalSizeBytes' cache-analysis.json)" >> $GITHUB_OUTPUT 47 | echo "cache_entries=$(jq -r '.summary.count' cache-analysis.json)" >> $GITHUB_OUTPUT 48 | echo "stale_entries=$(jq -r '.summary.staleCount' cache-analysis.json)" >> $GITHUB_OUTPUT 49 | 50 | - name: Clean Caches 51 | if: steps.cache-analysis.outputs.cache_size > 1000000000 # 1GB threshold 52 | run: | 53 | echo "=== Cleaning Caches ===" 54 | case "${{ github.event.inputs.cache_type || 'all' }}" in 55 | "all") 56 | ./target/release/port-kill-console cache --clean --safe-delete 57 | ;; 58 | "rust") 59 | ./target/release/port-kill-console cache --lang rust --clean --safe-delete 60 | ;; 61 | "js") 62 | ./target/release/port-kill-console cache --lang js --clean --safe-delete 63 | ;; 64 | "python") 65 | ./target/release/port-kill-console cache --lang py --clean --safe-delete 66 | ;; 67 | "java") 68 | ./target/release/port-kill-console cache --lang java --clean --safe-delete 69 | ;; 70 | "npx") 71 | ./target/release/port-kill-console cache --npx --clean --safe-delete --stale-days 30 72 | ;; 73 | "js-pm") 74 | ./target/release/port-kill-console cache --js-pm --clean --safe-delete 75 | ;; 76 | esac 77 | 78 | - name: System Diagnostics 79 | run: | 80 | echo "=== System Diagnostics ===" 81 | ./target/release/port-kill-console cache --doctor --json > diagnostics.json 82 | cat diagnostics.json 83 | 84 | - name: Upload Results 85 | uses: actions/upload-artifact@v4 86 | if: always() 87 | with: 88 | name: cache-cleanup-results 89 | path: | 90 | cache-analysis.json 91 | diagnostics.json 92 | 93 | - name: Comment on PR (if applicable) 94 | if: github.event_name == 'pull_request' 95 | uses: actions/github-script@v7 96 | with: 97 | script: | 98 | const fs = require('fs'); 99 | const analysis = JSON.parse(fs.readFileSync('cache-analysis.json', 'utf8')); 100 | const diagnostics = JSON.parse(fs.readFileSync('diagnostics.json', 'utf8')); 101 | 102 | const comment = `## 🧹 Cache Cleanup Results 103 | 104 | **Cache Analysis:** 105 | - Total size: ${(analysis.summary.totalSizeBytes / 1024 / 1024 / 1024).toFixed(2)} GB 106 | - Total entries: ${analysis.summary.count} 107 | - Stale entries: ${analysis.summary.staleCount} 108 | 109 | **System Health:** 110 | - Status: ${diagnostics.ok ? '✅ Healthy' : '❌ Issues detected'} 111 | - Warnings: ${diagnostics.warnings.length} 112 | - Errors: ${diagnostics.errors.length} 113 | 114 | ${diagnostics.warnings.length > 0 ? `**Warnings:**\n${diagnostics.warnings.map(w => `- ${w}`).join('\n')}` : ''} 115 | ${diagnostics.errors.length > 0 ? `**Errors:**\n${diagnostics.errors.map(e => `- ${e}`).join('\n')}` : ''} 116 | `; 117 | 118 | github.rest.issues.createComment({ 119 | issue_number: context.issue.number, 120 | owner: context.repo.owner, 121 | repo: context.repo.repo, 122 | body: comment 123 | }); 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Functional Source License, Version 1.1, MIT Future License 2 | 3 | ## Abbreviation 4 | 5 | FSL-1.1-MIT 6 | 7 | ## Notice 8 | 9 | Copyright (c) 2025 Treadie, Inc 10 | 11 | ## Terms and Conditions 12 | 13 | ### Licensor ("We") 14 | 15 | The party offering the Software under these Terms and Conditions. 16 | 17 | ### The Software 18 | 19 | The "Software" is each version of the software that we make available under 20 | these Terms and Conditions, as indicated by our inclusion of these Terms and 21 | Conditions with the Software. 22 | 23 | ### License Grant 24 | 25 | Subject to your compliance with this License Grant and the Patents, 26 | Redistribution and Trademark clauses below, we hereby grant you the right to 27 | use, copy, modify, create derivative works, publicly perform, publicly display 28 | and redistribute the Software for any Permitted Purpose identified below. 29 | 30 | ### Permitted Purpose 31 | 32 | A Permitted Purpose is any purpose other than a Competing Use. A Competing Use 33 | means making the Software available to others in a commercial product or 34 | service that: 35 | 36 | 1. substitutes for the Software; 37 | 38 | 2. substitutes for any other product or service we offer using the Software 39 | that exists as of the date we make the Software available; or 40 | 41 | 3. offers the same or substantially similar functionality as the Software. 42 | 43 | Permitted Purposes specifically include using the Software: 44 | 45 | 1. for your internal use and access; 46 | 47 | 2. for non-commercial education; 48 | 49 | 3. for non-commercial research; and 50 | 51 | 4. in connection with professional services that you provide to a licensee 52 | using the Software in accordance with these Terms and Conditions. 53 | 54 | ### Patents 55 | 56 | To the extent your use for a Permitted Purpose would necessarily infringe our 57 | patents, the license grant above includes a license under our patents. If you 58 | make a claim against any party that the Software infringes or contributes to 59 | the infringement of any patent, then your patent license to the Software ends 60 | immediately. 61 | 62 | ### Redistribution 63 | 64 | The Terms and Conditions apply to all copies, modifications and derivatives of 65 | the Software. 66 | 67 | If you redistribute any copies, modifications or derivatives of the Software, 68 | you must include a copy of or a link to these Terms and Conditions and not 69 | remove any copyright notices provided in or with the Software. 70 | 71 | ### Disclaimer 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR 75 | PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. 76 | 77 | IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE 78 | SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, 79 | EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. 80 | 81 | ### Trademarks 82 | 83 | Except for displaying the License Details and identifying us as the origin of 84 | the Software, you have no right under these Terms and Conditions to use our 85 | trademarks, trade names, service marks or product names. 86 | 87 | ## Grant of Future License 88 | 89 | We hereby irrevocably grant you an additional license to use the Software under 90 | the MIT license that is effective on the second anniversary of the date we make 91 | the Software available. On or after that date, you may use the Software under 92 | the MIT license, in which case the following will apply: 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy of 95 | this software and associated documentation files (the "Software"), to deal in 96 | the Software without restriction, including without limitation the rights to 97 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 98 | of the Software, and to permit persons to whom the Software is furnished to do 99 | so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. -------------------------------------------------------------------------------- /dashboard/server/api/history/index.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import { readFileSync, existsSync } from 'fs' 3 | import { join } from 'path' 4 | import { promisify } from 'util' 5 | 6 | const execAsync = promisify(exec) 7 | 8 | export default defineEventHandler(async (event) => { 9 | const config = useRuntimeConfig() 10 | 11 | try { 12 | // Get query parameters 13 | const query = getQuery(event) 14 | const limit = parseInt(query.limit as string) || 50 15 | const group = query.group as string || '' 16 | const project = query.project as string || '' 17 | 18 | // Try to find the port-kill-console binary 19 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 20 | 21 | if (!binaryPath) { 22 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 23 | } 24 | 25 | // Get process history from our Rust application 26 | const history = await getProcessHistory(binaryPath, limit, group, project) 27 | 28 | return { 29 | success: true, 30 | history, 31 | count: history.length, 32 | timestamp: new Date().toISOString() 33 | } 34 | 35 | } catch (error) { 36 | console.error('Error fetching process history:', error) 37 | 38 | return { 39 | success: false, 40 | error: error.message, 41 | history: [], 42 | count: 0, 43 | timestamp: new Date().toISOString() 44 | } 45 | } 46 | }) 47 | 48 | function findPortKillBinary(defaultPath: string): string | null { 49 | // Check if the default path exists 50 | if (existsSync(defaultPath)) { 51 | return defaultPath 52 | } 53 | 54 | // Try common locations for port-kill-console 55 | const possiblePaths = [ 56 | './target/release/port-kill-console', 57 | './target/release/port-kill-console.exe', 58 | './target/debug/port-kill-console', 59 | './target/debug/port-kill-console.exe', 60 | '../target/release/port-kill-console', 61 | '../target/release/port-kill-console.exe', 62 | '../target/debug/port-kill-console', 63 | '../target/debug/port-kill-console.exe', 64 | '/usr/local/bin/port-kill-console', 65 | '/opt/homebrew/bin/port-kill-console', 66 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 67 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 68 | ] 69 | 70 | for (const path of possiblePaths) { 71 | if (existsSync(path)) { 72 | return path 73 | } 74 | } 75 | 76 | return null 77 | } 78 | 79 | async function getProcessHistory( 80 | binaryPath: string, 81 | limit: number, 82 | group: string, 83 | project: string 84 | ): Promise { 85 | return new Promise((resolve, reject) => { 86 | // Build command arguments for history 87 | const args = [ 88 | '--show-history', 89 | '--json' 90 | ] 91 | 92 | // Add optional filters 93 | if (group) args.push('--only-groups', group) 94 | if (project) args.push('--project', project) 95 | 96 | const rustApp = spawn(binaryPath, args, { 97 | stdio: ['pipe', 'pipe', 'pipe'] 98 | }) 99 | 100 | let stdout = '' 101 | let stderr = '' 102 | 103 | rustApp.stdout.on('data', (data) => { 104 | stdout += data.toString() 105 | }) 106 | 107 | rustApp.stderr.on('data', (data) => { 108 | stderr += data.toString() 109 | }) 110 | 111 | rustApp.on('close', (code) => { 112 | if (code !== 0) { 113 | // For now, return empty history if Rust app fails 114 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 115 | resolve([]) 116 | return 117 | } 118 | 119 | try { 120 | // Parse JSON output from Rust app 121 | const lines = stdout.trim().split('\n') 122 | const history = [] 123 | 124 | for (const line of lines) { 125 | if (line.trim()) { 126 | try { 127 | const historyEntry = JSON.parse(line) 128 | if (historyEntry.pid && historyEntry.port) { 129 | history.push(historyEntry) 130 | } 131 | } catch (e) { 132 | // Skip invalid JSON lines 133 | } 134 | } 135 | } 136 | 137 | // Limit results 138 | const limitedHistory = history.slice(0, limit) 139 | resolve(limitedHistory) 140 | } catch (error) { 141 | reject(error) 142 | } 143 | }) 144 | 145 | rustApp.on('error', (error) => { 146 | // Return empty history if spawn fails 147 | console.warn(`Failed to spawn Rust app: ${error.message}`) 148 | resolve([]) 149 | }) 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /ci-samples/README.md: -------------------------------------------------------------------------------- 1 | # CI Integration Samples 2 | 3 | This directory contains sample CI configurations for integrating port-kill cache management into your CI/CD pipelines. 4 | 5 | ## Available Samples 6 | 7 | ### 1. GitHub Actions (`github-actions.yml`) 8 | - **Scheduled runs**: Daily at 2 AM UTC 9 | - **Manual triggers**: Workflow dispatch with cache type selection 10 | - **Features**: 11 | - Cache analysis with size thresholds 12 | - Conditional cleanup based on cache size 13 | - System diagnostics 14 | - PR comments with cache analysis results 15 | - Artifact upload for results 16 | 17 | ### 2. GitLab CI (`gitlab-ci.yml`) 18 | - **Scheduled runs**: Daily cache cleanup 19 | - **Manual triggers**: Web interface with cache type selection 20 | - **Features**: 21 | - Multi-stage pipeline (analysis → cleanup → diagnostics) 22 | - Environment variables for configuration 23 | - Artifact management 24 | - Conditional execution based on cache size 25 | 26 | ### 3. CircleCI (`circleci-config.yml`) 27 | - **Scheduled runs**: Daily at 2 AM UTC 28 | - **Manual triggers**: Multiple workflow types 29 | - **Features**: 30 | - Parameterized workflows 31 | - Multiple cleanup strategies (all, rust, js, python, java, npx, js-pm) 32 | - Workspace persistence 33 | - Artifact storage 34 | 35 | ## Usage 36 | 37 | ### GitHub Actions 38 | 1. Copy `github-actions.yml` to `.github/workflows/cache-management.yml` 39 | 2. Customize the schedule and thresholds as needed 40 | 3. The workflow will run automatically and can be triggered manually 41 | 42 | ### GitLab CI 43 | 1. Copy `gitlab-ci.yml` to your GitLab project as `.gitlab-ci.yml` 44 | 2. Configure the `CACHE_THRESHOLD` and `STALE_DAYS` variables 45 | 3. Set up scheduled pipelines in GitLab project settings 46 | 47 | ### CircleCI 48 | 1. Copy `circleci-config.yml` to `.circleci/config.yml` 49 | 2. Configure parameters in CircleCI project settings 50 | 3. Set up scheduled workflows in CircleCI dashboard 51 | 52 | ## Configuration Options 53 | 54 | ### Cache Types 55 | - `all` - Clean all detected caches 56 | - `rust` - Clean Rust caches (target/, ~/.cargo) 57 | - `js` - Clean JavaScript/TypeScript caches (node_modules, .next, .vite, etc.) 58 | - `python` - Clean Python caches (__pycache__, .venv, .pytest_cache) 59 | - `java` - Clean Java caches (.gradle, build, ~/.m2) 60 | - `npx` - Clean NPX caches with stale filtering 61 | - `js-pm` - Clean JS package manager caches (npm, pnpm, yarn) 62 | 63 | ### Thresholds 64 | - **Cache size threshold**: Default 1GB, configurable 65 | - **Stale days**: Default 30 days for NPX packages 66 | - **Disk usage warnings**: 80% and 90% thresholds 67 | 68 | ### Safety Features 69 | - **Safe delete**: All cleanups use `--safe-delete` by default 70 | - **Backup creation**: Automatic backup before cleanup 71 | - **Restore capability**: `--restore-last` command available 72 | - **Dry run**: Use `--dry-run` to preview changes 73 | 74 | ## Examples 75 | 76 | ### Clean only NPX caches older than 7 days 77 | ```bash 78 | ./port-kill-console cache --npx --clean --stale-days 7 79 | ``` 80 | 81 | ### Clean all caches with system diagnostics 82 | ```bash 83 | ./port-kill-console cache --clean --doctor 84 | ``` 85 | 86 | ### Analyze cache without cleaning 87 | ```bash 88 | ./port-kill-console cache --list --json 89 | ``` 90 | 91 | ## Monitoring 92 | 93 | All CI samples include: 94 | - **Cache analysis**: Size, entry count, stale count 95 | - **System diagnostics**: Disk usage, warnings, errors 96 | - **Artifact storage**: Results saved for review 97 | - **Conditional execution**: Only clean when thresholds exceeded 98 | 99 | ## Best Practices 100 | 101 | 1. **Start with analysis**: Always run `--list` before `--clean` 102 | 2. **Use safe delete**: Enable `--safe-delete` for production 103 | 3. **Set appropriate thresholds**: Don't clean too aggressively 104 | 4. **Monitor results**: Review diagnostics and artifacts 105 | 5. **Test locally**: Verify commands work before CI deployment 106 | 6. **Backup strategy**: Keep backups for critical caches 107 | 7. **Staged rollout**: Start with less critical cache types 108 | 109 | ## Troubleshooting 110 | 111 | ### Common Issues 112 | - **Permission errors**: Ensure CI has write access to cache directories 113 | - **Large cache sizes**: Adjust thresholds or use specific cache types 114 | - **Build failures**: Check Rust toolchain and dependencies 115 | - **Missing caches**: Verify cache detection is working correctly 116 | 117 | ### Debug Commands 118 | ```bash 119 | # Check system health 120 | ./port-kill-console cache --doctor 121 | 122 | # Analyze specific cache types 123 | ./port-kill-console cache --lang rust --list 124 | ./port-kill-console cache --npx --list 125 | 126 | # Test cleanup without execution 127 | ./port-kill-console cache --clean --dry-run 128 | ``` 129 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Port Kill Dashboard 2 | 3 | A modern web dashboard for monitoring and managing development processes and ports, built with Nuxt.js and Tailwind CSS. This dashboard provides a comprehensive interface for the Port Kill Rust application, offering real-time process monitoring, advanced filtering, and process management capabilities. 4 | 5 | ## Features 6 | 7 | ### 🚀 Core Features 8 | 9 | - **Real-time Process Monitoring**: Live updates of all running processes on monitored ports 10 | - **Advanced Filtering**: Search by process name, port, PID, or container name 11 | - **Process Management**: Kill individual processes or all processes at once 12 | - **Port Range Configuration**: Monitor specific ports or port ranges 13 | - **Docker Integration**: Monitor and manage Docker containers 14 | - **System Resource Monitoring**: Real-time CPU, memory, and disk usage tracking 15 | 16 | ## Prerequisites 17 | 18 | - Node.js 20.19.0 or later 19 | - npm or yarn package manager 20 | - Port Kill Rust application (built and available) 21 | - **Windows**: The binary should be at `target\release\port-kill-console.exe` 22 | - **macOS/Linux**: The binary should be at `target/release/port-kill-console` 23 | 24 | ## Installation 25 | 26 | 1. **Navigate to the dashboard directory:** 27 | ```bash 28 | cd dashboard 29 | ``` 30 | 31 | 2. **Install dependencies:** 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | 3. **Build the Port Kill Rust application:** 37 | ```bash 38 | # From the parent directory 39 | cd .. 40 | cargo build --release 41 | ``` 42 | 43 | 4. **Start the development server:** 44 | ```bash 45 | npm run dev 46 | ``` 47 | 48 | 5. **Start the Rust backend:** 49 | ```bash 50 | # From the parent directory 51 | cd .. 52 | cargo run --release 53 | ``` 54 | 55 | 6. **Open your browser:** 56 | Navigate to `http://localhost:3002` 57 | 58 | ## Configuration 59 | 60 | The dashboard can be configured through the settings panel or environment variables: 61 | 62 | ### Environment Variables 63 | 64 | Create a `.env` file in the dashboard root: 65 | 66 | ```env 67 | # Port Kill binary path (optional, will auto-detect) 68 | PORT_KILL_BINARY_PATH=./target/release/port-kill-console 69 | 70 | # API base URL (optional) 71 | API_BASE=http://localhost:3001/api 72 | ``` 73 | 74 | 75 | 76 | ## Dashboard Walkthrough 77 | 78 | ### 🎯 Header Section 79 | 80 | #### **Connection Status (Green/Red Dot + "Connected/Disconnected")** 81 | - **Green dot + "Connected"**: Dashboard is successfully connected to the Rust backend 82 | - **Red dot + "Disconnected"**: Backend isn't running or connection failed 83 | - Indicates real-time data flow status 84 | 85 | #### **Refresh Button** 86 | - Manual refresh button to immediately update all data 87 | - Useful when you want to see the latest process information without waiting for auto-refresh 88 | 89 | 90 | ### 📊 Stats Overview Cards 91 | 92 | #### System Load 93 | - Displays current system load status 94 | - Shows "Normal" when system is running smoothly 95 | - Would show "High" or "Critical" if system is under heavy load 96 | 97 | 98 | ### 💻 System Resources Section 99 | 100 | #### Header with "Live" Status** 101 | - **"Live" indicator**: Shows real-time data is being updated 102 | - **Refresh button**: Manual refresh for system resources 103 | - Updates every 5 seconds automatically 104 | 105 | #### Resource Cards 106 | 107 | ##### CPU Usage 108 | - **Current**: Shows CPU usage percentage 109 | - **Cores**: Number of CPU cores available 110 | - **Load**: System load average 111 | - **Progress bar**: Visual representation of CPU usage 112 | 113 | ##### Memory Usage 114 | - **Current**: Shows memory usage percentage 115 | - **Used/Total**: Memory usage in bytes (formatted as KB/MB/GB) 116 | - **Progress bar**: Visual memory usage indicator 117 | 118 | ##### Disk Usage 119 | - **Current**: Shows disk usage percentage 120 | - **Used/Total**: Disk space usage 121 | - **Progress bar**: Visual disk usage indicator 122 | 123 | ##### System Uptime (Yellow Card) 124 | - **Current**: Shows how long the system has been running 125 | - **Last updated**: Timestamp of last data refresh 126 | - **Format**: Shows in minutes, hours, or days 127 | 128 | #### Load Average Chart 129 | - **1 min, 5 min, 15 min**: System load averages over different time periods 130 | - **Color-coded**: Blue, Green, Purple for easy reading 131 | - **Purpose**: Shows system performance trends 132 | 133 | **Load Average Explanation:** 134 | - **Normal range**: 0.00-1.00 per CPU core (you have 14 cores, so normal would be 0-14) 135 | - **Your current load**: 4.09 means your system is working at about 29% capacity 136 | - **High load**: Values above 1.0 per core indicate the system is under stress 137 | - **Trend analysis**: Compare 1min vs 5min vs 15min to see if load is increasing or decreasing 138 | 139 | -------------------------------------------------------------------------------- /.github/workflows/debug.yml: -------------------------------------------------------------------------------- 1 | name: Debug Build Issues 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | platform: 7 | description: 'Platform to debug' 8 | required: true 9 | default: 'all' 10 | type: choice 11 | options: 12 | - all 13 | - windows 14 | - linux 15 | - console 16 | 17 | jobs: 18 | debug-windows: 19 | if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }} 20 | runs-on: windows-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Install Rust 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | override: true 30 | 31 | - name: Debug Windows Environment 32 | run: | 33 | Write-Host "=== Windows Environment Debug ===" 34 | Write-Host "OS: $env:OS" 35 | Write-Host "Rust version: $(rustc --version)" 36 | Write-Host "Cargo version: $(cargo --version)" 37 | Write-Host "Current directory: $(Get-Location)" 38 | Write-Host "Files in current directory:" 39 | Get-ChildItem | ForEach-Object { Write-Host " $_" } 40 | 41 | Write-Host "=== Cargo.toml content ===" 42 | Get-Content "Cargo.toml" 43 | 44 | Write-Host "=== Source files ===" 45 | Get-ChildItem "src" -Recurse | ForEach-Object { Write-Host " $_" } 46 | shell: pwsh 47 | 48 | - name: Debug Windows Build 49 | run: | 50 | Write-Host "=== Testing Windows Build ===" 51 | 52 | # Test basic cargo check 53 | Write-Host "Testing cargo check..." 54 | cargo check --bin port-kill-console 55 | 56 | # Test basic build 57 | Write-Host "Testing basic build..." 58 | cargo build --bin port-kill-console 59 | 60 | # Check if binary was created 61 | if (Test-Path ".\target\debug\port-kill-console.exe") { 62 | Write-Host "✅ Console binary created" 63 | Get-ChildItem ".\target\debug\port-kill-console.exe" | ForEach-Object { Write-Host " $_" } 64 | } else { 65 | Write-Host "❌ Console binary not found" 66 | Write-Host "Files in target/debug:" 67 | Get-ChildItem ".\target\debug\" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $_" } 68 | } 69 | shell: pwsh 70 | 71 | debug-console: 72 | if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'console' }} 73 | runs-on: ubuntu-latest 74 | steps: 75 | - name: Checkout code 76 | uses: actions/checkout@v4 77 | 78 | - name: Install Rust 79 | uses: actions-rs/toolchain@v1 80 | with: 81 | toolchain: stable 82 | override: true 83 | 84 | - name: Debug Console Environment 85 | run: | 86 | echo "=== Console Environment Debug ===" 87 | echo "OS: $OSTYPE" 88 | echo "Rust version: $(rustc --version)" 89 | echo "Cargo version: $(cargo --version)" 90 | echo "Current directory: $(pwd)" 91 | echo "Files in current directory:" 92 | ls -la 93 | 94 | echo "=== Cargo.toml content ===" 95 | cat Cargo.toml 96 | 97 | echo "=== Source files ===" 98 | find src -type f | head -20 99 | shell: bash 100 | 101 | - name: Debug Console Build 102 | run: | 103 | echo "=== Testing Console Build ===" 104 | 105 | # Test basic cargo check 106 | echo "Testing cargo check..." 107 | cargo check --bin port-kill-console 108 | 109 | # Test basic build 110 | echo "Testing basic build..." 111 | cargo build --bin port-kill-console 112 | 113 | # Check if binary was created 114 | if [ -f "./target/debug/port-kill-console" ]; then 115 | echo "✅ Console binary created" 116 | ls -la ./target/debug/port-kill-console 117 | file ./target/debug/port-kill-console 118 | else 119 | echo "❌ Console binary not found" 120 | echo "Files in target/debug:" 121 | ls -la ./target/debug/ 2>/dev/null || echo "target/debug directory not found" 122 | fi 123 | shell: bash 124 | 125 | - name: Debug Console Execution 126 | run: | 127 | echo "=== Testing Console Execution ===" 128 | 129 | if [ -f "./target/debug/port-kill-console" ]; then 130 | echo "Testing --version..." 131 | ./target/debug/port-kill-console --version || echo "Version test failed" 132 | 133 | echo "Testing --help..." 134 | ./target/debug/port-kill-console --help || echo "Help test failed" 135 | else 136 | echo "No binary to test" 137 | fi 138 | shell: bash 139 | -------------------------------------------------------------------------------- /src/cache/doctor.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | 5 | #[derive(Debug, Serialize)] 6 | pub struct DoctorReport { 7 | pub ok: bool, 8 | pub notes: Vec, 9 | pub warnings: Vec, 10 | pub errors: Vec, 11 | } 12 | 13 | pub async fn doctor() -> DoctorReport { 14 | let mut notes = Vec::new(); 15 | let mut warnings = Vec::new(); 16 | let mut errors = Vec::new(); 17 | 18 | // Check system environment 19 | if let Ok(home) = std::env::var("HOME") { 20 | notes.push(format!("Home directory: {}", home)); 21 | 22 | // Check for common cache directories 23 | let cache_dirs = [ 24 | (".cargo", "Rust toolchain cache"), 25 | (".npm", "npm cache"), 26 | (".pnpm-store", "pnpm store"), 27 | (".yarn/cache", "yarn cache"), 28 | (".cache/huggingface", "Hugging Face cache"), 29 | (".cache/torch", "PyTorch cache"), 30 | (".vercel", "Vercel cache"), 31 | (".cloudflare", "Cloudflare cache"), 32 | (".m2", "Maven cache"), 33 | ]; 34 | 35 | for (dir, description) in &cache_dirs { 36 | let path = PathBuf::from(&home).join(dir); 37 | if path.exists() { 38 | if let Ok(metadata) = fs::metadata(&path) { 39 | if metadata.is_dir() { 40 | notes.push(format!("Found {}: {}", description, path.display())); 41 | } 42 | } 43 | } 44 | } 45 | 46 | // Check disk space (Unix only) 47 | #[cfg(not(target_os = "windows"))] 48 | { 49 | if let Ok(statvfs) = nix::sys::statvfs::statvfs(home.as_str()) { 50 | let total_space = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64); 51 | let free_space = 52 | (statvfs.blocks_available() as u64) * (statvfs.fragment_size() as u64); 53 | let used_percent = ((total_space - free_space) as f64 / total_space as f64) * 100.0; 54 | 55 | notes.push(format!("Disk usage: {:.1}% used", used_percent)); 56 | 57 | if used_percent > 90.0 { 58 | warnings.push("Disk usage is above 90% - consider cleaning caches".to_string()); 59 | } else if used_percent > 80.0 { 60 | warnings.push("Disk usage is above 80% - monitor cache sizes".to_string()); 61 | } 62 | } 63 | } 64 | 65 | #[cfg(target_os = "windows")] 66 | { 67 | notes.push("Disk usage: Check manually on Windows".to_string()); 68 | } 69 | 70 | // Check for large cache directories 71 | let large_caches = [ 72 | (".cargo", 1_000_000_000), // 1GB 73 | (".npm", 500_000_000), // 500MB 74 | (".pnpm-store", 1_000_000_000), // 1GB 75 | ]; 76 | 77 | for (dir, threshold) in &large_caches { 78 | let path = PathBuf::from(&home).join(dir); 79 | if path.exists() { 80 | if let Ok(size) = get_dir_size(&path) { 81 | if size > *threshold { 82 | warnings.push(format!( 83 | "Large cache detected: {} ({:.1} MB)", 84 | dir, 85 | size as f64 / 1_000_000.0 86 | )); 87 | } 88 | } 89 | } 90 | } 91 | } else { 92 | errors.push("HOME environment variable not set".to_string()); 93 | } 94 | 95 | // Check for backup directories 96 | let backup_dir = PathBuf::from(".cachekill-backup"); 97 | if backup_dir.exists() { 98 | if let Ok(entries) = fs::read_dir(&backup_dir) { 99 | let count = entries.count(); 100 | notes.push(format!( 101 | "Found {} backup(s) from previous clean operations", 102 | count 103 | )); 104 | } 105 | } 106 | 107 | // Check current working directory for cache files 108 | let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); 109 | let cache_patterns = ["target", "node_modules", "__pycache__", ".gradle", "build"]; 110 | 111 | for pattern in &cache_patterns { 112 | let path = cwd.join(pattern); 113 | if path.exists() { 114 | notes.push(format!("Found {} in current directory", pattern)); 115 | } 116 | } 117 | 118 | let ok = errors.is_empty(); 119 | DoctorReport { 120 | ok, 121 | notes, 122 | warnings, 123 | errors, 124 | } 125 | } 126 | 127 | fn get_dir_size(path: &std::path::Path) -> Result { 128 | let mut total = 0u64; 129 | for entry in walkdir::WalkDir::new(path) { 130 | if let Ok(entry) = entry { 131 | if let Ok(metadata) = entry.metadata() { 132 | if metadata.is_file() { 133 | total += metadata.len(); 134 | } 135 | } 136 | } 137 | } 138 | Ok(total) 139 | } 140 | -------------------------------------------------------------------------------- /src/cache/backup.rs: -------------------------------------------------------------------------------- 1 | use super::types::CacheEntry; 2 | use chrono::{DateTime, Utc}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs; 5 | use std::path::{Path, PathBuf}; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | pub struct BackupManifest { 9 | pub timestamp: DateTime, 10 | pub entries: Vec, 11 | pub backup_dir: String, 12 | } 13 | 14 | pub fn get_backup_dir() -> PathBuf { 15 | let home = std::env::var("HOME") 16 | .or_else(|_| std::env::var("USERPROFILE")) 17 | .unwrap_or_else(|_| "/tmp".to_string()); 18 | PathBuf::from(home).join(".cachekill-backup") 19 | } 20 | 21 | pub fn create_backup_dir() -> Result { 22 | let backup_dir = get_backup_dir(); 23 | fs::create_dir_all(&backup_dir)?; 24 | Ok(backup_dir) 25 | } 26 | 27 | pub fn get_timestamped_dir() -> PathBuf { 28 | let backup_dir = get_backup_dir(); 29 | let timestamp = Utc::now().format("%Y-%m-%dT%H-%M-%SZ"); 30 | backup_dir.join(timestamp.to_string()) 31 | } 32 | 33 | pub async fn safe_delete_entries( 34 | entries: &[CacheEntry], 35 | safe_delete: bool, 36 | ) -> Result<(Vec, Option), std::io::Error> { 37 | let mut deleted = Vec::new(); 38 | let mut backup_path = None; 39 | 40 | if safe_delete { 41 | let timestamped_dir = get_timestamped_dir(); 42 | fs::create_dir_all(×tamped_dir)?; 43 | backup_path = Some(timestamped_dir.to_string_lossy().to_string()); 44 | 45 | // Create manifest 46 | let manifest = BackupManifest { 47 | timestamp: Utc::now(), 48 | entries: entries.to_vec(), 49 | backup_dir: timestamped_dir.to_string_lossy().to_string(), 50 | }; 51 | 52 | let manifest_path = timestamped_dir.join("manifest.json"); 53 | let manifest_json = serde_json::to_string_pretty(&manifest)?; 54 | fs::write(&manifest_path, manifest_json)?; 55 | 56 | // Move entries to backup 57 | for entry in entries { 58 | let src = Path::new(&entry.path); 59 | if src.exists() { 60 | let dst = timestamped_dir.join(src.file_name().unwrap_or_default()); 61 | if let Err(e) = fs::rename(src, &dst) { 62 | eprintln!("Warning: Failed to backup {}: {}", entry.path, e); 63 | } else { 64 | deleted.push(entry.clone()); 65 | } 66 | } 67 | } 68 | } else { 69 | // Direct deletion without backup 70 | for entry in entries { 71 | let path = Path::new(&entry.path); 72 | if path.exists() { 73 | if path.is_dir() { 74 | if let Err(e) = fs::remove_dir_all(path) { 75 | eprintln!("Warning: Failed to delete {}: {}", entry.path, e); 76 | } else { 77 | deleted.push(entry.clone()); 78 | } 79 | } else if path.is_file() { 80 | if let Err(e) = fs::remove_file(path) { 81 | eprintln!("Warning: Failed to delete {}: {}", entry.path, e); 82 | } else { 83 | deleted.push(entry.clone()); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | Ok((deleted, backup_path)) 91 | } 92 | 93 | pub fn find_latest_backup() -> Result, std::io::Error> { 94 | let backup_dir = get_backup_dir(); 95 | if !backup_dir.exists() { 96 | return Ok(None); 97 | } 98 | 99 | let mut entries: Vec<_> = fs::read_dir(&backup_dir)? 100 | .filter_map(|e| e.ok()) 101 | .filter(|e| e.path().is_dir()) 102 | .collect(); 103 | 104 | entries.sort_by_key(|e| { 105 | e.metadata() 106 | .ok() 107 | .and_then(|m| m.modified().ok()) 108 | .unwrap_or(std::time::UNIX_EPOCH) 109 | }); 110 | 111 | Ok(entries.last().map(|e| e.path())) 112 | } 113 | 114 | pub async fn restore_from_backup(backup_path: &Path) -> Result { 115 | let manifest_path = backup_path.join("manifest.json"); 116 | let manifest_content = fs::read_to_string(&manifest_path)?; 117 | let manifest: BackupManifest = serde_json::from_str(&manifest_content)?; 118 | 119 | let mut restored_count = 0; 120 | 121 | for entry in &manifest.entries { 122 | let backup_file = backup_path.join(Path::new(&entry.path).file_name().unwrap_or_default()); 123 | let original_path = Path::new(&entry.path); 124 | 125 | if backup_file.exists() { 126 | // Ensure parent directory exists 127 | if let Some(parent) = original_path.parent() { 128 | fs::create_dir_all(parent)?; 129 | } 130 | 131 | if let Err(e) = fs::rename(&backup_file, original_path) { 132 | eprintln!("Warning: Failed to restore {}: {}", entry.path, e); 133 | } else { 134 | restored_count += 1; 135 | } 136 | } 137 | } 138 | 139 | // Clean up backup directory after successful restore 140 | if restored_count > 0 { 141 | let _ = fs::remove_dir_all(backup_path); 142 | } 143 | 144 | Ok(restored_count) 145 | } 146 | -------------------------------------------------------------------------------- /dashboard/server/api/processes/tree.get.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { promisify } from 'util' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | try { 8 | // Get query parameters 9 | const query = getQuery(event) 10 | const ports = String(query.ports || '3000,3001,3002,3003,3004,4000,9000,9001') 11 | const ignorePorts = String(query.ignorePorts || '5353,5000,7000') 12 | const ignoreProcesses = String(query.ignoreProcesses || 'Chrome,ControlCe,rapportd') 13 | const ignorePatterns = String(query.ignorePatterns || '') 14 | const ignoreGroups = String(query.ignoreGroups || '') 15 | const onlyGroups = String(query.onlyGroups || '') 16 | const smartFilter = query.smartFilter === 'true' || query.smartFilter === true 17 | const performance = query.performance === 'true' || query.performance === true 18 | const showContext = query.showContext === 'true' || query.showContext === true 19 | const docker = query.docker === 'true' || query.docker === true 20 | const verbose = query.verbose === 'true' || query.verbose === true 21 | 22 | // Find the correct binary path 23 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 24 | if (!binaryPath) { 25 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 26 | } 27 | 28 | // Get process tree using the Rust application 29 | const treeOutput = await getProcessTreeWithRustApp( 30 | binaryPath, 31 | ports, 32 | ignorePorts, 33 | ignoreProcesses, 34 | ignorePatterns, 35 | ignoreGroups, 36 | onlyGroups, 37 | smartFilter, 38 | performance, 39 | showContext, 40 | docker, 41 | verbose 42 | ) 43 | 44 | return { 45 | success: true, 46 | tree: treeOutput, 47 | timestamp: new Date().toISOString() 48 | } 49 | 50 | } catch (error: any) { 51 | console.error('Error getting process tree:', error) 52 | 53 | throw createError({ 54 | statusCode: 500, 55 | statusMessage: `Failed to get process tree: ${error.message}` 56 | }) 57 | } 58 | }) 59 | 60 | function findPortKillBinary(defaultPath: string): string | null { 61 | // Check if the default path exists 62 | if (existsSync(defaultPath)) { 63 | return defaultPath 64 | } 65 | 66 | // Try common locations 67 | const commonPaths = [ 68 | './target/release/port-kill-console', 69 | './target/release/port-kill-console.exe', 70 | './target/debug/port-kill-console', 71 | './target/debug/port-kill-console.exe', 72 | '../target/release/port-kill-console', 73 | '../target/release/port-kill-console.exe', 74 | '../target/debug/port-kill-console', 75 | '../target/debug/port-kill-console.exe', 76 | '/usr/local/bin/port-kill-console', 77 | '/opt/homebrew/bin/port-kill-console', 78 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 79 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 80 | ] 81 | 82 | for (const path of commonPaths) { 83 | if (existsSync(path)) { 84 | return path 85 | } 86 | } 87 | 88 | return null 89 | } 90 | 91 | async function getProcessTreeWithRustApp( 92 | binaryPath: string, 93 | ports: string, 94 | ignorePorts: string, 95 | ignoreProcesses: string, 96 | ignorePatterns: string, 97 | ignoreGroups: string, 98 | onlyGroups: string, 99 | smartFilter: boolean, 100 | performance: boolean, 101 | showContext: boolean, 102 | docker: boolean, 103 | verbose: boolean 104 | ): Promise { 105 | return new Promise((resolve, reject) => { 106 | const args = [ 107 | '--ports', ports, 108 | '--show-tree' 109 | ] 110 | if (ignorePorts) args.push('--ignore-ports', ignorePorts) 111 | if (ignoreProcesses) args.push('--ignore-processes', ignoreProcesses) 112 | if (ignorePatterns) args.push('--ignore-patterns', ignorePatterns) 113 | if (ignoreGroups) args.push('--ignore-groups', ignoreGroups) 114 | if (onlyGroups) args.push('--only-groups', onlyGroups) 115 | if (smartFilter) args.push('--smart-filter') 116 | if (performance) args.push('--performance') 117 | if (showContext) args.push('--show-context') 118 | if (docker) args.push('--docker') 119 | if (verbose) args.push('--verbose') 120 | 121 | const rustApp = spawn(binaryPath, args, { 122 | stdio: ['pipe', 'pipe', 'pipe'], 123 | timeout: 10000 124 | }) 125 | 126 | let stdout = '' 127 | let stderr = '' 128 | 129 | rustApp.stdout.on('data', (data) => { 130 | stdout += data.toString() 131 | }) 132 | 133 | rustApp.stderr.on('data', (data) => { 134 | stderr += data.toString() 135 | }) 136 | 137 | rustApp.on('close', (code) => { 138 | if (code !== 0) { 139 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 140 | resolve('Failed to get process tree') 141 | return 142 | } 143 | 144 | resolve(stdout) 145 | }) 146 | 147 | rustApp.on('error', (error) => { 148 | console.warn(`Failed to spawn Rust app: ${error.message}`) 149 | resolve('Failed to get process tree') 150 | }) 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /ci-samples/circleci-config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | cache_type: 5 | type: string 6 | default: "all" 7 | description: "Type of cache to clean (all, rust, js, python, java, npx, js-pm)" 8 | stale_days: 9 | type: integer 10 | default: 30 11 | description: "Number of days to consider NPX packages stale" 12 | cache_threshold: 13 | type: integer 14 | default: 1000000000 15 | description: "Cache size threshold in bytes (1GB default)" 16 | 17 | orbs: 18 | rust: circleci/rust@1.0.0 19 | 20 | jobs: 21 | cache-analysis: 22 | docker: 23 | - image: cimg/rust:1.75 24 | steps: 25 | - checkout 26 | - run: 27 | name: Build port-kill 28 | command: cargo build --release 29 | - run: 30 | name: Cache Analysis 31 | command: | 32 | echo "=== Cache Analysis ===" 33 | ./target/release/port-kill-console cache --list --json > cache-analysis.json 34 | echo "Cache size: $(jq -r '.summary.totalSizeBytes' cache-analysis.json) bytes" 35 | echo "Cache entries: $(jq -r '.summary.count' cache-analysis.json)" 36 | echo "Stale entries: $(jq -r '.summary.staleCount' cache-analysis.json)" 37 | - persist_to_workspace: 38 | root: . 39 | paths: 40 | - target/release/port-kill-console 41 | - cache-analysis.json 42 | 43 | cache-cleanup: 44 | docker: 45 | - image: cimg/rust:1.75 46 | steps: 47 | - attach_workspace: 48 | at: . 49 | - run: 50 | name: Cache Cleanup 51 | command: | 52 | echo "=== Cache Cleanup ===" 53 | CACHE_SIZE=$(jq -r '.summary.totalSizeBytes' cache-analysis.json) 54 | echo "Current cache size: $CACHE_SIZE bytes" 55 | echo "Threshold: << pipeline.parameters.cache_threshold >> bytes" 56 | 57 | if [ "$CACHE_SIZE" -gt "<< pipeline.parameters.cache_threshold >>" ]; then 58 | echo "Cache size exceeds threshold, proceeding with cleanup..." 59 | 60 | case "<< pipeline.parameters.cache_type >>" in 61 | "rust") 62 | ./target/release/port-kill-console cache --lang rust --clean --safe-delete 63 | ;; 64 | "js") 65 | ./target/release/port-kill-console cache --lang js --clean --safe-delete 66 | ;; 67 | "python") 68 | ./target/release/port-kill-console cache --lang py --clean --safe-delete 69 | ;; 70 | "java") 71 | ./target/release/port-kill-console cache --lang java --clean --safe-delete 72 | ;; 73 | "npx") 74 | ./target/release/port-kill-console cache --npx --clean --safe-delete --stale-days << pipeline.parameters.stale_days >> 75 | ;; 76 | "js-pm") 77 | ./target/release/port-kill-console cache --js-pm --clean --safe-delete 78 | ;; 79 | *) 80 | ./target/release/port-kill-console cache --clean --safe-delete 81 | ;; 82 | esac 83 | 84 | echo "Cache cleanup completed" 85 | else 86 | echo "Cache size is within acceptable limits, skipping cleanup" 87 | fi 88 | 89 | diagnostics: 90 | docker: 91 | - image: cimg/rust:1.75 92 | steps: 93 | - attach_workspace: 94 | at: . 95 | - run: 96 | name: System Diagnostics 97 | command: | 98 | echo "=== System Diagnostics ===" 99 | ./target/release/port-kill-console cache --doctor --json > diagnostics.json 100 | echo "System health check completed" 101 | cat diagnostics.json 102 | - store_artifacts: 103 | path: diagnostics.json 104 | destination: diagnostics.json 105 | 106 | workflows: 107 | version: 2 108 | 109 | # Scheduled daily cache cleanup 110 | scheduled-cleanup: 111 | triggers: 112 | - schedule: 113 | cron: "0 2 * * *" # Daily at 2 AM UTC 114 | filters: 115 | branches: 116 | only: 117 | - main 118 | jobs: 119 | - cache-analysis 120 | - cache-cleanup: 121 | requires: 122 | - cache-analysis 123 | - diagnostics: 124 | requires: 125 | - cache-analysis 126 | 127 | # Manual cache cleanup 128 | manual-cleanup: 129 | jobs: 130 | - cache-analysis 131 | - cache-cleanup: 132 | requires: 133 | - cache-analysis 134 | - diagnostics: 135 | requires: 136 | - cache-analysis 137 | 138 | # NPX-specific cleanup 139 | npx-cleanup: 140 | jobs: 141 | - cache-analysis 142 | - cache-cleanup: 143 | requires: 144 | - cache-analysis 145 | parameters: 146 | cache_type: "npx" 147 | stale_days: 7 148 | - diagnostics: 149 | requires: 150 | - cache-analysis 151 | 152 | # Rust-specific cleanup 153 | rust-cleanup: 154 | jobs: 155 | - cache-analysis 156 | - cache-cleanup: 157 | requires: 158 | - cache-analysis 159 | parameters: 160 | cache_type: "rust" 161 | - diagnostics: 162 | requires: 163 | - cache-analysis 164 | -------------------------------------------------------------------------------- /src/system_monitor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use sysinfo::{Pid, System}; 3 | 4 | pub struct SystemMonitor { 5 | system: System, 6 | last_cpu_times: HashMap, 7 | } 8 | 9 | impl SystemMonitor { 10 | pub fn new() -> Self { 11 | let mut system = System::new_all(); 12 | system.refresh_all(); 13 | 14 | Self { 15 | system, 16 | last_cpu_times: HashMap::new(), 17 | } 18 | } 19 | 20 | pub fn refresh(&mut self) { 21 | self.system.refresh_all(); 22 | } 23 | 24 | pub fn get_process_cpu_usage(&mut self, pid: i32) -> Option { 25 | let pid = Pid::from_u32(pid as u32); 26 | 27 | if let Some(process) = self.system.process(pid) { 28 | let current_cpu_time = process.cpu_usage() as f64; 29 | 30 | // Calculate CPU usage since last check 31 | let cpu_usage = if let Some(last_time) = self.last_cpu_times.get(&pid) { 32 | let delta = current_cpu_time - last_time; 33 | // Cap at 100% and ensure non-negative 34 | delta.min(100.0).max(0.0) 35 | } else { 36 | // First time seeing this process, return current usage 37 | current_cpu_time.min(100.0).max(0.0) 38 | }; 39 | 40 | // Store current time for next calculation 41 | self.last_cpu_times.insert(pid, current_cpu_time); 42 | 43 | Some(cpu_usage) 44 | } else { 45 | // Process not found, remove from tracking 46 | self.last_cpu_times.remove(&pid); 47 | None 48 | } 49 | } 50 | 51 | pub fn get_process_memory_usage(&self, pid: i32) -> Option<(u64, f64)> { 52 | let pid = Pid::from_u32(pid as u32); 53 | 54 | if let Some(process) = self.system.process(pid) { 55 | let memory_bytes = process.memory() * 1024; // Convert from KB to bytes 56 | let total_memory = self.system.total_memory() * 1024; // Convert from KB to bytes 57 | let memory_percentage = if total_memory > 0 { 58 | (memory_bytes as f64 / total_memory as f64) * 100.0 59 | } else { 60 | 0.0 61 | }; 62 | 63 | Some((memory_bytes, memory_percentage)) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | pub fn get_system_info(&self) -> SystemInfo { 70 | SystemInfo { 71 | total_memory: self.system.total_memory() * 1024, // Convert to bytes 72 | used_memory: self.system.used_memory() * 1024, // Convert to bytes 73 | total_swap: self.system.total_swap() * 1024, // Convert to bytes 74 | used_swap: self.system.used_swap() * 1024, // Convert to bytes 75 | cpu_count: self.system.cpus().len(), 76 | load_average: sysinfo::System::load_average(), 77 | } 78 | } 79 | 80 | pub fn cleanup_old_processes(&mut self) { 81 | // Remove processes that are no longer running 82 | let current_pids: std::collections::HashSet = self 83 | .system 84 | .processes() 85 | .iter() 86 | .map(|(pid, _)| *pid) 87 | .collect(); 88 | 89 | self.last_cpu_times 90 | .retain(|pid, _| current_pids.contains(pid)); 91 | } 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct SystemInfo { 96 | pub total_memory: u64, 97 | pub used_memory: u64, 98 | pub total_swap: u64, 99 | pub used_swap: u64, 100 | pub cpu_count: usize, 101 | pub load_average: sysinfo::LoadAvg, 102 | } 103 | 104 | impl SystemInfo { 105 | pub fn memory_percentage(&self) -> f64 { 106 | if self.total_memory > 0 { 107 | (self.used_memory as f64 / self.total_memory as f64) * 100.0 108 | } else { 109 | 0.0 110 | } 111 | } 112 | 113 | pub fn swap_percentage(&self) -> f64 { 114 | if self.total_swap > 0 { 115 | (self.used_swap as f64 / self.total_swap as f64) * 100.0 116 | } else { 117 | 0.0 118 | } 119 | } 120 | 121 | pub fn format_memory(&self, bytes: u64) -> String { 122 | const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; 123 | const THRESHOLD: u64 = 1024; 124 | 125 | let mut size = bytes as f64; 126 | let mut unit_index = 0; 127 | 128 | while size >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 { 129 | size /= THRESHOLD as f64; 130 | unit_index += 1; 131 | } 132 | 133 | if unit_index == 0 { 134 | format!("{} {}", size as u64, UNITS[unit_index]) 135 | } else { 136 | format!("{:.1} {}", size, UNITS[unit_index]) 137 | } 138 | } 139 | } 140 | 141 | #[cfg(test)] 142 | mod tests { 143 | use super::*; 144 | 145 | #[test] 146 | fn test_memory_formatting() { 147 | let system_info = SystemInfo { 148 | total_memory: 0, 149 | used_memory: 0, 150 | total_swap: 0, 151 | used_swap: 0, 152 | cpu_count: 4, 153 | load_average: sysinfo::LoadAvg { 154 | one: 0.0, 155 | five: 0.0, 156 | fifteen: 0.0, 157 | }, 158 | }; 159 | 160 | assert_eq!(system_info.format_memory(1024), "1.0 KB"); 161 | assert_eq!(system_info.format_memory(1048576), "1.0 MB"); 162 | assert_eq!(system_info.format_memory(1073741824), "1.0 GB"); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /dashboard/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Installation script for port-kill 4 | # This script builds and installs the port-kill application for your platform 5 | 6 | set -e 7 | 8 | echo "🚀 Port Kill Installation Script" 9 | echo "================================" 10 | echo "" 11 | 12 | # Usage/help 13 | if [[ "$1" == "-h" || "$1" == "--help" ]]; then 14 | echo "Usage: ./install.sh [--dashboard] [--all]" 15 | echo "" 16 | echo "Options:" 17 | echo " --dashboard Install dashboard dependencies only (Nuxt)" 18 | echo " --all Build Rust binary and install dashboard dependencies" 19 | echo "" 20 | exit 0 21 | fi 22 | 23 | # Flags 24 | DASHBOARD_ONLY=false 25 | ALL_SETUP=false 26 | 27 | for arg in "$@"; do 28 | case "$arg" in 29 | --dashboard) 30 | DASHBOARD_ONLY=true 31 | ;; 32 | --all) 33 | ALL_SETUP=true 34 | ;; 35 | esac 36 | done 37 | 38 | # Detect the operating system 39 | if [[ "$OSTYPE" == "darwin"* ]]; then 40 | PLATFORM="macOS" 41 | BUILD_SCRIPT="./build-macos.sh" 42 | RUN_SCRIPT="./run.sh" 43 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 44 | PLATFORM="Linux" 45 | BUILD_SCRIPT="./build-linux.sh" 46 | RUN_SCRIPT="./run-linux.sh" 47 | elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then 48 | PLATFORM="Windows" 49 | BUILD_SCRIPT="build-windows.bat" 50 | RUN_SCRIPT="run-windows.bat" 51 | else 52 | echo "⚠️ Unknown operating system: $OSTYPE" 53 | echo " Attempting generic build..." 54 | PLATFORM="Unknown" 55 | BUILD_SCRIPT="cargo build --release" 56 | RUN_SCRIPT="./target/release/port-kill" 57 | fi 58 | 59 | echo "📋 Detected Platform: $PLATFORM" 60 | echo "🔨 Build Script: $BUILD_SCRIPT" 61 | echo "▶️ Run Script: $RUN_SCRIPT" 62 | echo "" 63 | 64 | # If dashboard-only, skip Rust checks 65 | if [[ "$DASHBOARD_ONLY" == false ]]; then 66 | # Check if Rust is installed 67 | if ! command -v cargo &> /dev/null; then 68 | echo "❌ Rust is not installed or not in PATH" 69 | echo "" 70 | echo "📦 Please install Rust first:" 71 | echo " Visit: https://rustup.rs/" 72 | echo "" 73 | echo " Or run:" 74 | echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" 75 | echo "" 76 | exit 1 77 | fi 78 | 79 | echo "✅ Rust detected: $(cargo --version)" 80 | echo "" 81 | fi 82 | 83 | # Build the application using platform-specific script 84 | if [[ "$DASHBOARD_ONLY" == false ]]; then 85 | echo "🔨 Building port-kill for $PLATFORM..." 86 | 87 | if [[ "$PLATFORM" == "Windows" ]]; then 88 | # For Windows, we need to use the batch file 89 | if command -v cmd &> /dev/null; then 90 | cmd //c "$BUILD_SCRIPT" 91 | else 92 | echo "❌ Windows build script not available or cmd not found" 93 | echo " Please run: $BUILD_SCRIPT" 94 | exit 1 95 | fi 96 | else 97 | # For Unix-like systems, use the shell script 98 | if [ -f "$BUILD_SCRIPT" ]; then 99 | bash "$BUILD_SCRIPT" 100 | else 101 | echo "❌ Build script not found: $BUILD_SCRIPT" 102 | echo " Falling back to generic build..." 103 | cargo build --release 104 | fi 105 | fi 106 | 107 | echo "" 108 | echo "✅ Build completed successfully!" 109 | echo "" 110 | fi 111 | 112 | # Check if the binary was created 113 | if [[ "$DASHBOARD_ONLY" == true ]] || [ -f "target/release/port-kill" ] || [ -f "target/release/port-kill.exe" ]; then 114 | echo "📦 Binary created successfully!" 115 | echo "" 116 | echo "🎯 Quick Start:" 117 | if [[ "$DASHBOARD_ONLY" == false ]]; then 118 | echo " $RUN_SCRIPT" 119 | else 120 | echo " cd dashboard && npm run dev" 121 | fi 122 | echo "" 123 | echo "🔧 Common Usage Examples:" 124 | echo "" 125 | echo " # Default monitoring (ports 2000-6000):" 126 | echo " $RUN_SCRIPT" 127 | echo "" 128 | echo " # Monitor specific ports:" 129 | echo " $RUN_SCRIPT --ports 3000,8000,8080" 130 | echo "" 131 | echo " # Console mode with verbose logging:" 132 | echo " $RUN_SCRIPT --console --verbose" 133 | echo "" 134 | echo " # Ignore system processes:" 135 | echo " $RUN_SCRIPT --ignore-ports 5353,5000,7000 --ignore-processes Chrome,ControlCe" 136 | echo "" 137 | echo " # Docker support:" 138 | echo " $RUN_SCRIPT --docker --ports 3000,8000" 139 | echo "" 140 | if [[ "$DASHBOARD_ONLY" == false ]]; then 141 | echo "📖 For more options:" 142 | echo " $RUN_SCRIPT --help" 143 | echo "" 144 | fi 145 | 146 | # Dashboard setup if requested 147 | if [[ "$DASHBOARD_ONLY" == true || "$ALL_SETUP" == true ]]; then 148 | echo "🧩 Dashboard setup (Nuxt)" 149 | if ! command -v npm &> /dev/null; then 150 | echo "❌ npm not found. Please install Node.js (v18+) first: https://nodejs.org/" 151 | exit 1 152 | fi 153 | pushd dashboard >/dev/null 154 | echo "📦 Installing dashboard dependencies..." 155 | npm install 156 | echo "✅ Done." 157 | echo "" 158 | echo "▶️ Start dashboard:" 159 | echo " npm run dev # http://localhost:3002" 160 | echo "" 161 | echo "🔨 Build dashboard (Nitro):" 162 | echo " npm run build # server output in .output/" 163 | echo "" 164 | echo "📦 Static generate (optional):" 165 | echo " npm run generate # static output in dist/" 166 | popd >/dev/null 167 | fi 168 | 169 | echo "🎉 Installation complete! Happy coding! 🚀" 170 | else 171 | echo "❌ Error: Binary not found" 172 | echo "" 173 | echo "💡 Troubleshooting:" 174 | echo " 1. Check if Rust is properly installed: cargo --version" 175 | echo " 2. Try building manually: $BUILD_SCRIPT" 176 | echo " 3. Check for error messages above" 177 | echo "" 178 | exit 1 179 | fi 180 | -------------------------------------------------------------------------------- /dashboard/server/api/processes/[pid].delete.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | 3 | export default defineEventHandler(async (event) => { 4 | const pid = getRouterParam(event, 'pid') 5 | 6 | if (!pid) { 7 | throw createError({ 8 | statusCode: 400, 9 | statusMessage: 'PID is required' 10 | }) 11 | } 12 | 13 | const pidNum = parseInt(pid) 14 | if (isNaN(pidNum)) { 15 | throw createError({ 16 | statusCode: 400, 17 | statusMessage: 'Invalid PID format' 18 | }) 19 | } 20 | 21 | try { 22 | // First, get the process details to determine how to kill it 23 | const processDetails = await getProcessDetails(pidNum) 24 | 25 | if (!processDetails) { 26 | throw createError({ 27 | statusCode: 404, 28 | statusMessage: `Process ${pid} not found` 29 | }) 30 | } 31 | 32 | // Handle different types of processes 33 | if (processDetails.containerId && processDetails.containerId !== 'host-process' && processDetails.containerId !== 'docker-daemon') { 34 | // This is a Docker container, use docker stop 35 | await killDockerContainer(processDetails.containerId) 36 | } else { 37 | // This is a host process or Docker daemon, use regular kill 38 | await killProcess(pidNum) 39 | } 40 | 41 | return { 42 | success: true, 43 | message: `Process ${pid} killed successfully`, 44 | timestamp: new Date().toISOString() 45 | } 46 | 47 | } catch (error) { 48 | console.error(`Error killing process ${pid}:`, error) 49 | 50 | throw createError({ 51 | statusCode: 500, 52 | statusMessage: `Failed to kill process ${pid}: ${error.message}` 53 | }) 54 | } 55 | }) 56 | 57 | async function killProcess(pid: number): Promise { 58 | return new Promise((resolve, reject) => { 59 | // First try SIGTERM (graceful termination) 60 | const kill = spawn('kill', ['-TERM', pid.toString()], { 61 | stdio: ['pipe', 'pipe', 'pipe'] 62 | }) 63 | 64 | kill.on('close', (code) => { 65 | // Wait a bit for graceful termination 66 | setTimeout(() => { 67 | // Check if process is still running 68 | checkProcessExists(pid).then(exists => { 69 | if (exists) { 70 | // Process still running, send SIGKILL 71 | const forceKill = spawn('kill', ['-KILL', pid.toString()], { 72 | stdio: ['pipe', 'pipe', 'pipe'] 73 | }) 74 | 75 | forceKill.on('close', (forceCode) => { 76 | // Check again if process still exists after SIGKILL 77 | checkProcessExists(pid).then(stillExists => { 78 | if (!stillExists) { 79 | resolve() // Process was killed successfully 80 | } else { 81 | reject(new Error(`Failed to force kill process ${pid}`)) 82 | } 83 | }).catch(reject) 84 | }) 85 | 86 | forceKill.on('error', (error) => { 87 | reject(error) 88 | }) 89 | } else { 90 | resolve() // Process was killed by SIGTERM 91 | } 92 | }).catch(reject) 93 | }, 500) 94 | }) 95 | 96 | kill.on('error', (error) => { 97 | reject(error) 98 | }) 99 | }) 100 | } 101 | 102 | async function getProcessDetails(pid: number): Promise { 103 | return new Promise((resolve) => { 104 | const ps = spawn('ps', ['-p', pid.toString(), '-o', 'pid,command'], { 105 | stdio: ['pipe', 'pipe', 'pipe'] 106 | }) 107 | 108 | let output = '' 109 | ps.stdout.on('data', (data) => { 110 | output += data.toString() 111 | }) 112 | 113 | ps.on('close', (code) => { 114 | if (code === 0 && output.trim()) { 115 | const lines = output.trim().split('\n') 116 | if (lines.length > 1) { 117 | const parts = lines[1].trim().split(/\s+/) 118 | if (parts.length >= 2) { 119 | const command = parts.slice(1).join(' ') 120 | const containerId = command.includes('com.docke') ? 'docker-daemon' : null 121 | const containerName = command.includes('com.docke') ? 'Docker Daemon' : null 122 | 123 | resolve({ 124 | pid: parseInt(parts[0]), 125 | command, 126 | containerId, 127 | containerName 128 | }) 129 | return 130 | } 131 | } 132 | } 133 | resolve(null) 134 | }) 135 | 136 | ps.on('error', () => { 137 | resolve(null) 138 | }) 139 | }) 140 | } 141 | 142 | async function killDockerContainer(containerId: string): Promise { 143 | return new Promise((resolve, reject) => { 144 | // First try docker stop (graceful) 145 | const stop = spawn('docker', ['stop', containerId], { 146 | stdio: ['pipe', 'pipe', 'pipe'] 147 | }) 148 | 149 | stop.on('close', (code) => { 150 | if (code === 0) { 151 | resolve() 152 | } else { 153 | // If stop failed, try docker kill (force) 154 | const kill = spawn('docker', ['kill', containerId], { 155 | stdio: ['pipe', 'pipe', 'pipe'] 156 | }) 157 | 158 | kill.on('close', (killCode) => { 159 | if (killCode === 0) { 160 | resolve() 161 | } else { 162 | reject(new Error(`Failed to stop Docker container ${containerId}`)) 163 | } 164 | }) 165 | 166 | kill.on('error', (error) => { 167 | reject(error) 168 | }) 169 | } 170 | }) 171 | 172 | stop.on('error', (error) => { 173 | reject(error) 174 | }) 175 | }) 176 | } 177 | 178 | async function checkProcessExists(pid: number): Promise { 179 | return new Promise((resolve) => { 180 | const ps = spawn('ps', ['-p', pid.toString()], { 181 | stdio: ['pipe', 'pipe', 'pipe'] 182 | }) 183 | 184 | ps.on('close', (code) => { 185 | resolve(code === 0) 186 | }) 187 | 188 | ps.on('error', () => { 189 | resolve(false) 190 | }) 191 | }) 192 | } -------------------------------------------------------------------------------- /mcp/README.md: -------------------------------------------------------------------------------- 1 | # Port Kill MCP Server 2 | 3 | Expose Port Kill as MCP tools for Cursor. 4 | 5 | ## Tools 6 | 7 | ### Port Management 8 | - `list(ports?, docker?, verbose?, remote?)` 9 | - `kill(ports, remote?)` 10 | - `reset(remote?)` 11 | - `audit(suspiciousOnly?, remote?)` 12 | - `guardStatus(baseUrl?)` (uses dashboard API) 13 | 14 | ### Cache Management 15 | - `cacheList(lang?, includeNpx?, includeJsPm?, includeHf?, includeTorch?, includeVercel?, includeCloudflare?, staleDays?)` 16 | - `cacheClean(lang?, includeNpx?, includeJsPm?, includeHf?, includeTorch?, includeVercel?, includeCloudflare?, safeDelete?, force?, staleDays?)` 17 | - `cacheRestore()` 18 | - `cacheDoctor()` 19 | 20 | 21 | ## Quick Install 22 | 23 | Add the following to your MCP config e.g. `.cursor/mcp.json` 24 | 25 | ```json 26 | { 27 | "mcpServers": { 28 | "port-kill-mcp": { 29 | "command": "npx", 30 | "args": ["-y", "https://gitpkg.vercel.app/treadiehq/port-kill/mcp?main"] 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | ## Installation From Source 37 | 38 | Alternatively, if you don't want to use `npx 'https://gitpkg.vercel.app/treadiehq/port-kill/mcp?main'`. 39 | 40 | Checkout the repo, build the server & install `port-kill-mcp` globally: 41 | ```bash 42 | npm install 43 | npm run build 44 | npm install -g . 45 | ``` 46 | 47 | Then add to your config e.g. `.cursor/mcp.json` 48 | 49 | ```json 50 | { 51 | "mcpServers": { 52 | "port-kill-mcp": { 53 | "command": "port-kill-mcp" 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | ## Usage 60 | 61 | Ask your AI to use the tools. 62 | 63 | ```text 64 | What process is running on port 3000? 65 | ``` 66 | 67 | ```text 68 | Kill the process running on port 3000 69 | ``` 70 | 71 | ```text 72 | Kill all processes using dev ports 73 | ``` 74 | 75 | ```text 76 | List all my development caches 77 | ``` 78 | 79 | ```text 80 | Clean up my NPX cache 81 | ``` 82 | 83 | ```text 84 | Show me system diagnostics for cache health 85 | ``` 86 | 87 | 88 | ## Tools Extended 89 | 90 | ### 1. **`list`** - List Processes on Ports 91 | **Purpose**: List all processes running on specified ports with detailed information 92 | 93 | **Example Usage**: 94 | ```text 95 | "What processes are running on port 3000?" 96 | "List all processes on development ports" 97 | "Show me what's running on ports 3000, 8000, and 8080" 98 | ``` 99 | 100 | ### 2. **`kill`** - Kill Processes on Ports 101 | **Purpose**: Kill all processes running on specified ports 102 | 103 | **Example Usage**: 104 | ```text 105 | "Kill the process running on port 3000" 106 | "Kill all processes on ports 3000, 8000, and 8080" 107 | "Free up port 5432" 108 | ``` 109 | 110 | ### 3. **`reset`** - Reset Common Dev Ports 111 | **Purpose**: Kill processes on common development ports (3000, 5000, 8000, 5432, 3306, 6379, 27017, 8080, 9000) 112 | 113 | **Example Usage**: 114 | ```text 115 | "Reset all development ports" 116 | "Kill all processes using common dev ports" 117 | "Clean up my development environment" 118 | ``` 119 | 120 | ### 4. **`audit`** - Security Audit 121 | **Purpose**: Run security audit on processes and ports to identify suspicious or unauthorized processes 122 | 123 | **Example Usage**: 124 | ```text 125 | "Run a security audit on all ports" 126 | "Check for suspicious processes on development ports" 127 | "Audit port 3000 for security issues" 128 | ``` 129 | 130 | ### 5. **`guardStatus`** - Port Guard Status 131 | **Purpose**: Check the status of Port Guard if running via dashboard API 132 | 133 | **Example Usage**: 134 | ```text 135 | "Check the port guard status" 136 | "What's the current guard configuration?" 137 | "Show me the dashboard guard status" 138 | ``` 139 | 140 | ### 6. **`cacheList`** - List Development Caches 141 | **Purpose**: List all detected development caches with size and metadata 142 | 143 | **Example Usage**: 144 | ```text 145 | "List all my development caches" 146 | "Show me NPX cache usage" 147 | "List Python caches in my project" 148 | "Show me JavaScript package manager caches" 149 | ``` 150 | 151 | ### 7. **`cacheClean`** - Clean Development Caches 152 | **Purpose**: Clean detected caches with safe backup functionality 153 | 154 | **Example Usage**: 155 | ```text 156 | "Clean up my NPX cache" 157 | "Remove all Rust build caches" 158 | "Clean Python cache files" 159 | "Clean JavaScript package manager caches" 160 | ``` 161 | 162 | ### 8. **`cacheRestore`** - Restore Cache Backup 163 | **Purpose**: Restore the most recent cache backup 164 | 165 | **Example Usage**: 166 | ```text 167 | "Restore my last cache backup" 168 | "Undo the last cache cleanup" 169 | ``` 170 | 171 | ### 9. **`cacheDoctor`** - System Diagnostics 172 | **Purpose**: Run system diagnostics and health checks for cache management 173 | 174 | **Example Usage**: 175 | ```text 176 | "Show me system diagnostics for cache health" 177 | "Check my cache storage usage" 178 | "Run cache health diagnostics" 179 | ``` 180 | 181 | 182 | ## Dev 183 | 184 | ```bash 185 | cd mcp 186 | npm install 187 | npm run dev 188 | ``` 189 | 190 | Cursor config: `.cursor/mcp.json` is included. Restart Cursor to detect the server. 191 | 192 | Environment variables: 193 | 194 | ```bash 195 | # Override binary path (defaults to ./target/release/port-kill-console) 196 | export PORT_KILL_BIN=/abs/path/to/port-kill-console 197 | 198 | # Override working directory for commands (defaults to repo root) 199 | export PORT_KILL_CWD=/abs/path/for/commands 200 | 201 | # Override per-tool timeout in seconds (default 300 = 5 minutes) 202 | export PORT_KILL_MCP_TOOL_TIMEOUT_SECONDS=60 203 | ``` 204 | 205 | 206 | ### Optional HTTP wrapper (use outside MCP/Cursor) 207 | 208 | ```bash 209 | # Start the MCP server with an HTTP wrapper for tools (POST /tool) 210 | HTTP_PORT=8787 npm run dev 211 | 212 | # Call a tool over HTTP 213 | curl -s -X POST \ 214 | -H 'content-type: application/json' \ 215 | --data '{ 216 | "name": "list", 217 | "args": { "ports": "3000,8000" } 218 | }' \ 219 | http://localhost:8787/tool 220 | 221 | # Override binary and working dir if needed 222 | PORT_KILL_BIN=/abs/path/to/port-kill-console \ 223 | PORT_KILL_CWD=/abs/path/to/project \ 224 | PORT_KILL_MCP_TOOL_TIMEOUT_SECONDS=60 \ 225 | HTTP_PORT=8787 npm run dev 226 | ``` 227 | 228 | 229 | -------------------------------------------------------------------------------- /dashboard/server/api/processes/restart.post.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { promisify } from 'util' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | try { 8 | // Get query parameters 9 | const query = getQuery(event) 10 | const ports = String(query.ports || '3000,3001,3002,3003,3004,4000,9000,9001') 11 | const ignorePorts = String(query.ignorePorts || '5353,5000,7000') 12 | const ignoreProcesses = String(query.ignoreProcesses || 'Chrome,ControlCe,rapportd') 13 | const ignorePatterns = String(query.ignorePatterns || '') 14 | const ignoreGroups = String(query.ignoreGroups || '') 15 | const onlyGroups = String(query.onlyGroups || '') 16 | const smartFilter = query.smartFilter === 'true' || query.smartFilter === true 17 | const performance = query.performance === 'true' || query.performance === true 18 | const showContext = query.showContext === 'true' || query.showContext === true 19 | const docker = query.docker === 'true' || query.docker === true 20 | const verbose = query.verbose === 'true' || query.verbose === true 21 | 22 | // Find the correct binary path 23 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 24 | if (!binaryPath) { 25 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 26 | } 27 | 28 | // Restart processes using the Rust application 29 | const results = await restartProcessesWithRustApp( 30 | binaryPath, 31 | ports, 32 | ignorePorts, 33 | ignoreProcesses, 34 | ignorePatterns, 35 | ignoreGroups, 36 | onlyGroups, 37 | smartFilter, 38 | performance, 39 | showContext, 40 | docker, 41 | verbose 42 | ) 43 | 44 | return { 45 | success: true, 46 | message: `Restarted ${results.restartedCount} processes${results.failedCount > 0 ? `, ${results.failedCount} failed` : ''}`, 47 | restartedCount: results.restartedCount, 48 | failedCount: results.failedCount, 49 | timestamp: new Date().toISOString() 50 | } 51 | 52 | } catch (error: any) { 53 | console.error('Error restarting processes:', error) 54 | 55 | throw createError({ 56 | statusCode: 500, 57 | statusMessage: `Failed to restart processes: ${error.message}` 58 | }) 59 | } 60 | }) 61 | 62 | function findPortKillBinary(defaultPath: string): string | null { 63 | // Check if the default path exists 64 | if (existsSync(defaultPath)) { 65 | return defaultPath 66 | } 67 | 68 | // Try common locations 69 | const commonPaths = [ 70 | './target/release/port-kill-console', 71 | './target/release/port-kill-console.exe', 72 | './target/debug/port-kill-console', 73 | './target/debug/port-kill-console.exe', 74 | '../target/release/port-kill-console', 75 | '../target/release/port-kill-console.exe', 76 | '../target/debug/port-kill-console', 77 | '../target/debug/port-kill-console.exe', 78 | '/usr/local/bin/port-kill-console', 79 | '/opt/homebrew/bin/port-kill-console', 80 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 81 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 82 | ] 83 | 84 | for (const path of commonPaths) { 85 | if (existsSync(path)) { 86 | return path 87 | } 88 | } 89 | 90 | return null 91 | } 92 | 93 | async function restartProcessesWithRustApp( 94 | binaryPath: string, 95 | ports: string, 96 | ignorePorts: string, 97 | ignoreProcesses: string, 98 | ignorePatterns: string, 99 | ignoreGroups: string, 100 | onlyGroups: string, 101 | smartFilter: boolean, 102 | performance: boolean, 103 | showContext: boolean, 104 | docker: boolean, 105 | verbose: boolean 106 | ): Promise<{ restartedCount: number; failedCount: number }> { 107 | return new Promise((resolve, reject) => { 108 | const args = [ 109 | '--ports', ports, 110 | '--restart' 111 | ] 112 | if (ignorePorts) args.push('--ignore-ports', ignorePorts) 113 | if (ignoreProcesses) args.push('--ignore-processes', ignoreProcesses) 114 | if (ignorePatterns) args.push('--ignore-patterns', ignorePatterns) 115 | if (ignoreGroups) args.push('--ignore-groups', ignoreGroups) 116 | if (onlyGroups) args.push('--only-groups', onlyGroups) 117 | if (smartFilter) args.push('--smart-filter') 118 | if (performance) args.push('--performance') 119 | if (showContext) args.push('--show-context') 120 | if (docker) args.push('--docker') 121 | if (verbose) args.push('--verbose') 122 | 123 | const rustApp = spawn(binaryPath, args, { 124 | stdio: ['pipe', 'pipe', 'pipe'], 125 | timeout: 15000 // Longer timeout for restart 126 | }) 127 | 128 | let stdout = '' 129 | let stderr = '' 130 | let restartedCount = 0 131 | let failedCount = 0 132 | 133 | rustApp.stdout.on('data', (data) => { 134 | const output = data.toString() 135 | stdout += output 136 | 137 | // Count restarted processes from output 138 | const restartMatches = output.match(/✅ Restarted \d+\/\d+ processes/g) 139 | if (restartMatches) { 140 | const match = restartMatches[0] 141 | const countMatch = match.match(/✅ Restarted (\d+)\/\d+ processes/) 142 | if (countMatch) { 143 | restartedCount = parseInt(countMatch[1]) 144 | } 145 | } 146 | 147 | const failMatches = output.match(/❌ Failed to restart/g) 148 | if (failMatches) { 149 | failedCount += failMatches.length 150 | } 151 | }) 152 | 153 | rustApp.stderr.on('data', (data) => { 154 | stderr += data.toString() 155 | }) 156 | 157 | rustApp.on('close', (code) => { 158 | if (code !== 0) { 159 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 160 | } 161 | 162 | resolve({ restartedCount, failedCount }) 163 | }) 164 | 165 | rustApp.on('error', (error) => { 166 | console.warn(`Failed to spawn Rust app: ${error.message}`) 167 | resolve({ restartedCount: 0, failedCount: 0 }) 168 | }) 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /dashboard/components/KillConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 150 | -------------------------------------------------------------------------------- /dashboard/server/api/processes/kill-group.post.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { promisify } from 'util' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | try { 8 | // Get query parameters 9 | const query = getQuery(event) 10 | const groups = String(query.groups || '') 11 | const ports = String(query.ports || '3000,3001,3002,3003,3004,4000,9000,9001') 12 | const ignorePorts = String(query.ignorePorts || '5353,5000,7000') 13 | const ignoreProcesses = String(query.ignoreProcesses || 'Chrome,ControlCe,rapportd') 14 | const ignorePatterns = String(query.ignorePatterns || '') 15 | const ignoreGroups = String(query.ignoreGroups || '') 16 | const onlyGroups = String(query.onlyGroups || '') 17 | const smartFilter = query.smartFilter === 'true' || query.smartFilter === true 18 | const performance = query.performance === 'true' || query.performance === true 19 | const showContext = query.showContext === 'true' || query.showContext === true 20 | const docker = query.docker === 'true' || query.docker === true 21 | const verbose = query.verbose === 'true' || query.verbose === true 22 | 23 | if (!groups) { 24 | throw createError({ 25 | statusCode: 400, 26 | statusMessage: 'Groups parameter is required' 27 | }) 28 | } 29 | 30 | // Find the correct binary path 31 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 32 | if (!binaryPath) { 33 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 34 | } 35 | 36 | // Kill processes by group using the Rust application 37 | const results = await killGroupProcessesWithRustApp( 38 | binaryPath, 39 | groups, 40 | ports, 41 | ignorePorts, 42 | ignoreProcesses, 43 | ignorePatterns, 44 | ignoreGroups, 45 | onlyGroups, 46 | smartFilter, 47 | performance, 48 | showContext, 49 | docker, 50 | verbose 51 | ) 52 | 53 | return { 54 | success: true, 55 | message: `Killed ${results.killedCount} processes from groups: ${groups}${results.failedCount > 0 ? `, ${results.failedCount} failed` : ''}`, 56 | killedCount: results.killedCount, 57 | failedCount: results.failedCount, 58 | groups: groups.split(','), 59 | timestamp: new Date().toISOString() 60 | } 61 | 62 | } catch (error: any) { 63 | console.error('Error killing processes by group:', error) 64 | 65 | throw createError({ 66 | statusCode: 500, 67 | statusMessage: `Failed to kill processes by group: ${error.message}` 68 | }) 69 | } 70 | }) 71 | 72 | function findPortKillBinary(defaultPath: string): string | null { 73 | // Check if the default path exists 74 | if (existsSync(defaultPath)) { 75 | return defaultPath 76 | } 77 | 78 | // Try common locations 79 | const commonPaths = [ 80 | './target/release/port-kill-console', 81 | './target/release/port-kill-console.exe', 82 | './target/debug/port-kill-console', 83 | './target/debug/port-kill-console.exe', 84 | '../target/release/port-kill-console', 85 | '../target/release/port-kill-console.exe', 86 | '../target/debug/port-kill-console', 87 | '../target/debug/port-kill-console.exe', 88 | '/usr/local/bin/port-kill-console', 89 | '/opt/homebrew/bin/port-kill-console', 90 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 91 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 92 | ] 93 | 94 | for (const path of commonPaths) { 95 | if (existsSync(path)) { 96 | return path 97 | } 98 | } 99 | 100 | return null 101 | } 102 | 103 | async function killGroupProcessesWithRustApp( 104 | binaryPath: string, 105 | groups: string, 106 | ports: string, 107 | ignorePorts: string, 108 | ignoreProcesses: string, 109 | ignorePatterns: string, 110 | ignoreGroups: string, 111 | onlyGroups: string, 112 | smartFilter: boolean, 113 | performance: boolean, 114 | showContext: boolean, 115 | docker: boolean, 116 | verbose: boolean 117 | ): Promise<{ killedCount: number; failedCount: number }> { 118 | return new Promise((resolve, reject) => { 119 | const args = [ 120 | '--ports', ports, 121 | '--kill-group', groups 122 | ] 123 | if (ignorePorts) args.push('--ignore-ports', ignorePorts) 124 | if (ignoreProcesses) args.push('--ignore-processes', ignoreProcesses) 125 | if (ignorePatterns) args.push('--ignore-patterns', ignorePatterns) 126 | if (ignoreGroups) args.push('--ignore-groups', ignoreGroups) 127 | if (onlyGroups) args.push('--only-groups', onlyGroups) 128 | if (smartFilter) args.push('--smart-filter') 129 | if (performance) args.push('--performance') 130 | if (showContext) args.push('--show-context') 131 | if (docker) args.push('--docker') 132 | if (verbose) args.push('--verbose') 133 | 134 | const rustApp = spawn(binaryPath, args, { 135 | stdio: ['pipe', 'pipe', 'pipe'], 136 | timeout: 10000 137 | }) 138 | 139 | let stdout = '' 140 | let stderr = '' 141 | let killedCount = 0 142 | let failedCount = 0 143 | 144 | rustApp.stdout.on('data', (data) => { 145 | const output = data.toString() 146 | stdout += output 147 | 148 | // Count killed processes from output 149 | const killMatches = output.match(/✅ Killed \d+\/\d+ processes from groups:/g) 150 | if (killMatches) { 151 | const match = killMatches[0] 152 | const countMatch = match.match(/✅ Killed (\d+)\/\d+ processes from groups:/) 153 | if (countMatch) { 154 | killedCount = parseInt(countMatch[1]) 155 | } 156 | } 157 | 158 | const failMatches = output.match(/❌ Failed to kill/g) 159 | if (failMatches) { 160 | failedCount += failMatches.length 161 | } 162 | }) 163 | 164 | rustApp.stderr.on('data', (data) => { 165 | stderr += data.toString() 166 | }) 167 | 168 | rustApp.on('close', (code) => { 169 | if (code !== 0) { 170 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 171 | } 172 | 173 | resolve({ killedCount, failedCount }) 174 | }) 175 | 176 | rustApp.on('error', (error) => { 177 | console.warn(`Failed to spawn Rust app: ${error.message}`) 178 | resolve({ killedCount: 0, failedCount: 0 }) 179 | }) 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /dashboard/server/api/processes/kill-project.post.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import { existsSync } from 'fs' 3 | import { promisify } from 'util' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = useRuntimeConfig() 7 | try { 8 | // Get query parameters 9 | const query = getQuery(event) 10 | const projects = String(query.projects || '') 11 | const ports = String(query.ports || '3000,3001,3002,3003,3004,4000,9000,9001') 12 | const ignorePorts = String(query.ignorePorts || '5353,5000,7000') 13 | const ignoreProcesses = String(query.ignoreProcesses || 'Chrome,ControlCe,rapportd') 14 | const ignorePatterns = String(query.ignorePatterns || '') 15 | const ignoreGroups = String(query.ignoreGroups || '') 16 | const onlyGroups = String(query.onlyGroups || '') 17 | const smartFilter = query.smartFilter === 'true' || query.smartFilter === true 18 | const performance = query.performance === 'true' || query.performance === true 19 | const showContext = query.showContext === 'true' || query.showContext === true 20 | const docker = query.docker === 'true' || query.docker === true 21 | const verbose = query.verbose === 'true' || query.verbose === true 22 | 23 | if (!projects) { 24 | throw createError({ 25 | statusCode: 400, 26 | statusMessage: 'Projects parameter is required' 27 | }) 28 | } 29 | 30 | // Find the correct binary path 31 | const binaryPath = findPortKillBinary(config.portKillBinaryPath) 32 | if (!binaryPath) { 33 | throw new Error('Port Kill binary not found. Please build the Rust application first.') 34 | } 35 | 36 | // Kill processes by project using the Rust application 37 | const results = await killProjectProcessesWithRustApp( 38 | binaryPath, 39 | projects, 40 | ports, 41 | ignorePorts, 42 | ignoreProcesses, 43 | ignorePatterns, 44 | ignoreGroups, 45 | onlyGroups, 46 | smartFilter, 47 | performance, 48 | showContext, 49 | docker, 50 | verbose 51 | ) 52 | 53 | return { 54 | success: true, 55 | message: `Killed ${results.killedCount} processes from projects: ${projects}${results.failedCount > 0 ? `, ${results.failedCount} failed` : ''}`, 56 | killedCount: results.killedCount, 57 | failedCount: results.failedCount, 58 | projects: projects.split(','), 59 | timestamp: new Date().toISOString() 60 | } 61 | 62 | } catch (error: any) { 63 | console.error('Error killing processes by project:', error) 64 | 65 | throw createError({ 66 | statusCode: 500, 67 | statusMessage: `Failed to kill processes by project: ${error.message}` 68 | }) 69 | } 70 | }) 71 | 72 | function findPortKillBinary(defaultPath: string): string | null { 73 | // Check if the default path exists 74 | if (existsSync(defaultPath)) { 75 | return defaultPath 76 | } 77 | 78 | // Try common locations 79 | const commonPaths = [ 80 | './target/release/port-kill-console', 81 | './target/release/port-kill-console.exe', 82 | './target/debug/port-kill-console', 83 | './target/debug/port-kill-console.exe', 84 | '../target/release/port-kill-console', 85 | '../target/release/port-kill-console.exe', 86 | '../target/debug/port-kill-console', 87 | '../target/debug/port-kill-console.exe', 88 | '/usr/local/bin/port-kill-console', 89 | '/opt/homebrew/bin/port-kill-console', 90 | 'C:\\Program Files\\port-kill\\port-kill-console.exe', 91 | join(process.env.USERPROFILE || '', 'AppData', 'Local', 'port-kill', 'port-kill-console.exe') 92 | ] 93 | 94 | for (const path of commonPaths) { 95 | if (existsSync(path)) { 96 | return path 97 | } 98 | } 99 | 100 | return null 101 | } 102 | 103 | async function killProjectProcessesWithRustApp( 104 | binaryPath: string, 105 | projects: string, 106 | ports: string, 107 | ignorePorts: string, 108 | ignoreProcesses: string, 109 | ignorePatterns: string, 110 | ignoreGroups: string, 111 | onlyGroups: string, 112 | smartFilter: boolean, 113 | performance: boolean, 114 | showContext: boolean, 115 | docker: boolean, 116 | verbose: boolean 117 | ): Promise<{ killedCount: number; failedCount: number }> { 118 | return new Promise((resolve, reject) => { 119 | const args = [ 120 | '--ports', ports, 121 | '--kill-project', projects 122 | ] 123 | if (ignorePorts) args.push('--ignore-ports', ignorePorts) 124 | if (ignoreProcesses) args.push('--ignore-processes', ignoreProcesses) 125 | if (ignorePatterns) args.push('--ignore-patterns', ignorePatterns) 126 | if (ignoreGroups) args.push('--ignore-groups', ignoreGroups) 127 | if (onlyGroups) args.push('--only-groups', onlyGroups) 128 | if (smartFilter) args.push('--smart-filter') 129 | if (performance) args.push('--performance') 130 | if (showContext) args.push('--show-context') 131 | if (docker) args.push('--docker') 132 | if (verbose) args.push('--verbose') 133 | 134 | const rustApp = spawn(binaryPath, args, { 135 | stdio: ['pipe', 'pipe', 'pipe'], 136 | timeout: 10000 137 | }) 138 | 139 | let stdout = '' 140 | let stderr = '' 141 | let killedCount = 0 142 | let failedCount = 0 143 | 144 | rustApp.stdout.on('data', (data) => { 145 | const output = data.toString() 146 | stdout += output 147 | 148 | // Count killed processes from output 149 | const killMatches = output.match(/✅ Killed \d+\/\d+ processes from projects:/g) 150 | if (killMatches) { 151 | const match = killMatches[0] 152 | const countMatch = match.match(/✅ Killed (\d+)\/\d+ processes from projects:/) 153 | if (countMatch) { 154 | killedCount = parseInt(countMatch[1]) 155 | } 156 | } 157 | 158 | const failMatches = output.match(/❌ Failed to kill/g) 159 | if (failMatches) { 160 | failedCount += failMatches.length 161 | } 162 | }) 163 | 164 | rustApp.stderr.on('data', (data) => { 165 | stderr += data.toString() 166 | }) 167 | 168 | rustApp.on('close', (code) => { 169 | if (code !== 0) { 170 | console.warn(`Rust app failed with code ${code}: ${stderr}`) 171 | } 172 | 173 | resolve({ killedCount, failedCount }) 174 | }) 175 | 176 | rustApp.on('error', (error) => { 177 | console.warn(`Failed to spawn Rust app: ${error.message}`) 178 | resolve({ killedCount: 0, failedCount: 0 }) 179 | }) 180 | }) 181 | } 182 | --------------------------------------------------------------------------------