diff --git a/.gitignore b/.gitignore index 29eabff9..bfa9040c 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,7 @@ backend/bindings build-npm-package-codesign.sh npx-cli/dist -db.sqlite \ No newline at end of file +backend/db.sqlite + +# Development ports file +.dev-ports.json \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index e915798e..85273bde 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -187,10 +187,11 @@ async fn main() -> anyhow::Result<()> { .layer(Extension(app_state)) .layer(CorsLayer::permissive()); - let port: u16 = std::env::var("PORT") + let port: u16 = std::env::var("BACKEND_PORT") + .or_else(|_| std::env::var("PORT")) .ok() .and_then(|p| p.parse().ok()) - .unwrap_or(if cfg!(debug_assertions) { 3001 } else { 0 }); + .unwrap_or(0); // Use 0 to find free port if no specific port provided let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?; let actual_port = listener.local_addr()?.port(); // get → 53427 (example) diff --git a/backend/src/utils.rs b/backend/src/utils.rs index dc3116be..c45af2a4 100644 --- a/backend/src/utils.rs +++ b/backend/src/utils.rs @@ -5,6 +5,8 @@ use directories::ProjectDirs; pub mod shell; pub mod text; +const PROJECT_ROOT: &str = env!("CARGO_MANIFEST_DIR"); + /// Cache for WSL2 detection result static WSL2_CACHE: OnceLock = OnceLock::new(); @@ -31,18 +33,18 @@ pub fn is_wsl2() -> bool { } pub fn asset_dir() -> std::path::PathBuf { - let proj = if cfg!(debug_assertions) { - ProjectDirs::from("ai", "bloop-dev", env!("CARGO_PKG_NAME")) - .expect("OS didn't give us a home directory") + if cfg!(debug_assertions) { + std::path::PathBuf::from(PROJECT_ROOT).join("../dev_assets") } else { ProjectDirs::from("ai", "bloop", env!("CARGO_PKG_NAME")) .expect("OS didn't give us a home directory") - }; + .data_dir() + .to_path_buf() + } // ✔ macOS → ~/Library/Application Support/MyApp // ✔ Linux → ~/.local/share/myapp (respects XDG_DATA_HOME) // ✔ Windows → %APPDATA%\Example\MyApp - proj.data_dir().to_path_buf() } pub fn config_path() -> std::path::PathBuf { diff --git a/dev_assets/config.json b/dev_assets/config.json new file mode 100644 index 00000000..87dfb145 --- /dev/null +++ b/dev_assets/config.json @@ -0,0 +1,19 @@ +{ + "theme": "light", + "executor": { + "type": "claude" + }, + "disclaimer_acknowledged": true, + "onboarding_acknowledged": true, + "sound_alerts": true, + "sound_file": "abstract-sound4", + "push_notifications": true, + "editor": { + "editor_type": "vscode", + "custom_command": null + }, + "github": { + "token": "", + "default_pr_base": "main" + } +} diff --git a/dev_assets/db.sqlite b/dev_assets/db.sqlite new file mode 100644 index 00000000..fcc02fb9 Binary files /dev/null and b/dev_assets/db.sqlite differ diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 708e52b7..a28b6dd9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,10 +11,10 @@ export default defineConfig({ }, }, server: { - port: 3000, + port: parseInt(process.env.FRONTEND_PORT || '3000'), proxy: { '/api': { - target: 'http://localhost:3001', + target: `http://localhost:${process.env.BACKEND_PORT || '3001'}`, changeOrigin: true, }, }, diff --git a/package.json b/package.json index 4cd5f9e0..55c2eb00 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,20 @@ "version": "0.0.32", "private": true, "scripts": { - "dev": "concurrently \"cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'\" \"npm run frontend:dev\"", + "dev": "export FRONTEND_PORT=$(node scripts/manage-dev-ports.js frontend) && export BACKEND_PORT=$(node scripts/manage-dev-ports.js backend) && concurrently \"cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'\" \"npm run frontend:dev\"", "build": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml && cargo build --release --bin mcp_task_server --manifest-path backend/Cargo.toml", "build:single": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml", "build:npm": "./build-npm-package.sh", "test:npm": "./test-npm-package.sh", - "frontend:dev": "cd frontend && npm run dev", + "frontend:dev": "cd frontend && npm run dev -- --port ${FRONTEND_PORT:-3000} --open", "frontend:build": "cd frontend && npm run build", - "backend:dev": "cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'", + "backend:dev": "BACKEND_PORT=$(node scripts/manage-dev-ports.js backend) cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'", "backend:build": "cargo build --release --manifest-path backend/Cargo.toml", "backend:run": "cargo run --manifest-path backend/Cargo.toml", "backend:test": "cargo test --lib", "generate-types": "cd backend && cargo run --bin generate_types", - "prepare-db": "node scripts/prepare-db.js" + "prepare-db": "node scripts/prepare-db.js", + "dev:clear-ports": "node scripts/manage-dev-ports.js clear" }, "devDependencies": { "concurrently": "^8.2.2", diff --git a/scripts/manage-dev-ports.js b/scripts/manage-dev-ports.js new file mode 100644 index 00000000..4f4775ab --- /dev/null +++ b/scripts/manage-dev-ports.js @@ -0,0 +1,199 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); +const net = require("net"); + +const PORTS_FILE = path.join(__dirname, "..", ".dev-ports.json"); + +/** + * Check if a port is available + */ +function isPortAvailable(port) { + return new Promise((resolve) => { + const sock = net.createConnection({ port, host: "localhost" }); + sock.on("connect", () => { + sock.destroy(); + resolve(false); + }); + sock.on("error", () => resolve(true)); + }); +} + +/** + * Find a free port starting from a given port + */ +async function findFreePort(startPort = 3000) { + let port = startPort; + while (!(await isPortAvailable(port))) { + port++; + if (port > 65535) { + throw new Error("No available ports found"); + } + } + return port; +} + +/** + * Load existing ports from file + */ +function loadPorts() { + try { + if (fs.existsSync(PORTS_FILE)) { + const data = fs.readFileSync(PORTS_FILE, "utf8"); + return JSON.parse(data); + } + } catch (error) { + console.warn("Failed to load existing ports:", error.message); + } + return null; +} + +/** + * Save ports to file + */ +function savePorts(ports) { + try { + fs.writeFileSync(PORTS_FILE, JSON.stringify(ports, null, 2)); + } catch (error) { + console.error("Failed to save ports:", error.message); + throw error; + } +} + +/** + * Verify that saved ports are still available + */ +async function verifyPorts(ports) { + const frontendAvailable = await isPortAvailable(ports.frontend); + const backendAvailable = await isPortAvailable(ports.backend); + + if (process.argv[2] === "get" && (!frontendAvailable || !backendAvailable)) { + console.log( + `Port availability check failed: frontend:${ports.frontend}=${frontendAvailable}, backend:${ports.backend}=${backendAvailable}` + ); + } + + return frontendAvailable && backendAvailable; +} + +/** + * Allocate ports for development + */ +async function allocatePorts() { + // Try to load existing ports first + const existingPorts = loadPorts(); + + if (existingPorts) { + // Verify existing ports are still available + if (await verifyPorts(existingPorts)) { + if (process.argv[2] === "get") { + console.log("Reusing existing dev ports:"); + console.log(`Frontend: ${existingPorts.frontend}`); + console.log(`Backend: ${existingPorts.backend}`); + } + return existingPorts; + } else { + if (process.argv[2] === "get") { + console.log( + "Existing ports are no longer available, finding new ones..." + ); + } + } + } + + // Find new free ports + const frontendPort = await findFreePort(3000); + const backendPort = await findFreePort(frontendPort + 1); + + const ports = { + frontend: frontendPort, + backend: backendPort, + timestamp: new Date().toISOString(), + }; + + savePorts(ports); + + if (process.argv[2] === "get") { + console.log("Allocated new dev ports:"); + console.log(`Frontend: ${ports.frontend}`); + console.log(`Backend: ${ports.backend}`); + } + + return ports; +} + +/** + * Get ports (allocate if needed) + */ +async function getPorts() { + const ports = await allocatePorts(); + return ports; +} + +/** + * Clear saved ports + */ +function clearPorts() { + try { + if (fs.existsSync(PORTS_FILE)) { + fs.unlinkSync(PORTS_FILE); + console.log("Cleared saved dev ports"); + } else { + console.log("No saved ports to clear"); + } + } catch (error) { + console.error("Failed to clear ports:", error.message); + } +} + +// CLI interface +if (require.main === module) { + const command = process.argv[2]; + + switch (command) { + case "get": + getPorts() + .then((ports) => { + console.log(JSON.stringify(ports)); + }) + .catch(console.error); + break; + + case "clear": + clearPorts(); + break; + + case "frontend": + getPorts() + .then((ports) => { + console.log(ports.frontend); + }) + .catch(console.error); + break; + + case "backend": + getPorts() + .then((ports) => { + console.log(ports.backend); + }) + .catch(console.error); + break; + + default: + console.log("Usage:"); + console.log( + " node manage-dev-ports.js get - Get all ports (allocate if needed)" + ); + console.log( + " node manage-dev-ports.js frontend - Get frontend port only" + ); + console.log( + " node manage-dev-ports.js backend - Get backend port only" + ); + console.log(" node manage-dev-ports.js clear - Clear saved ports"); + break; + } +} + +module.exports = { getPorts, clearPorts, findFreePort };