Dev server updates (#52)

* Start dev server on any port

* Move dev DB into project folder

* Choose free ports for dev server

* Reliability

* Both processes get env vars

* And open browser
This commit is contained in:
Louis Knight-Webb
2025-07-02 18:24:26 +01:00
committed by GitHub
parent 3dcd2ef701
commit fcaf99ebf6
8 changed files with 239 additions and 14 deletions

5
.gitignore vendored
View File

@@ -74,4 +74,7 @@ backend/bindings
build-npm-package-codesign.sh build-npm-package-codesign.sh
npx-cli/dist npx-cli/dist
db.sqlite backend/db.sqlite
# Development ports file
.dev-ports.json

View File

@@ -187,10 +187,11 @@ async fn main() -> anyhow::Result<()> {
.layer(Extension(app_state)) .layer(Extension(app_state))
.layer(CorsLayer::permissive()); .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() .ok()
.and_then(|p| p.parse().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 listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
let actual_port = listener.local_addr()?.port(); // get → 53427 (example) let actual_port = listener.local_addr()?.port(); // get → 53427 (example)

View File

@@ -5,6 +5,8 @@ use directories::ProjectDirs;
pub mod shell; pub mod shell;
pub mod text; pub mod text;
const PROJECT_ROOT: &str = env!("CARGO_MANIFEST_DIR");
/// Cache for WSL2 detection result /// Cache for WSL2 detection result
static WSL2_CACHE: OnceLock<bool> = OnceLock::new(); static WSL2_CACHE: OnceLock<bool> = OnceLock::new();
@@ -31,18 +33,18 @@ pub fn is_wsl2() -> bool {
} }
pub fn asset_dir() -> std::path::PathBuf { pub fn asset_dir() -> std::path::PathBuf {
let proj = if cfg!(debug_assertions) { if cfg!(debug_assertions) {
ProjectDirs::from("ai", "bloop-dev", env!("CARGO_PKG_NAME")) std::path::PathBuf::from(PROJECT_ROOT).join("../dev_assets")
.expect("OS didn't give us a home directory")
} else { } else {
ProjectDirs::from("ai", "bloop", env!("CARGO_PKG_NAME")) ProjectDirs::from("ai", "bloop", env!("CARGO_PKG_NAME"))
.expect("OS didn't give us a home directory") .expect("OS didn't give us a home directory")
}; .data_dir()
.to_path_buf()
}
// ✔ macOS → ~/Library/Application Support/MyApp // ✔ macOS → ~/Library/Application Support/MyApp
// ✔ Linux → ~/.local/share/myapp (respects XDG_DATA_HOME) // ✔ Linux → ~/.local/share/myapp (respects XDG_DATA_HOME)
// ✔ Windows → %APPDATA%\Example\MyApp // ✔ Windows → %APPDATA%\Example\MyApp
proj.data_dir().to_path_buf()
} }
pub fn config_path() -> std::path::PathBuf { pub fn config_path() -> std::path::PathBuf {

19
dev_assets/config.json Normal file
View File

@@ -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"
}
}

BIN
dev_assets/db.sqlite Normal file

Binary file not shown.

View File

@@ -11,10 +11,10 @@ export default defineConfig({
}, },
}, },
server: { server: {
port: 3000, port: parseInt(process.env.FRONTEND_PORT || '3000'),
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3001', target: `http://localhost:${process.env.BACKEND_PORT || '3001'}`,
changeOrigin: true, changeOrigin: true,
}, },
}, },

View File

@@ -3,19 +3,20 @@
"version": "0.0.32", "version": "0.0.32",
"private": true, "private": true,
"scripts": { "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": "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:single": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml",
"build:npm": "./build-npm-package.sh", "build:npm": "./build-npm-package.sh",
"test:npm": "./test-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", "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:build": "cargo build --release --manifest-path backend/Cargo.toml",
"backend:run": "cargo run --manifest-path backend/Cargo.toml", "backend:run": "cargo run --manifest-path backend/Cargo.toml",
"backend:test": "cargo test --lib", "backend:test": "cargo test --lib",
"generate-types": "cd backend && cargo run --bin generate_types", "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": { "devDependencies": {
"concurrently": "^8.2.2", "concurrently": "^8.2.2",

199
scripts/manage-dev-ports.js Normal file
View File

@@ -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 };