From e26705f016a13fc5b2c571e6a2ee0c04b0244b55 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 6 Mar 2026 09:20:49 -0800 Subject: [PATCH] Use separated client/proxy dev ports across worktrees --- apps/yaak-proxy/vite.config.ts | 16 ++ crates-tauri/yaak-app-proxy/tauri.conf.json | 33 ++++ scripts/git-hooks/post-checkout.mjs | 157 ++++++++++++++------ scripts/run-dev.mjs | 109 +++++++++++--- 4 files changed, 247 insertions(+), 68 deletions(-) create mode 100644 apps/yaak-proxy/vite.config.ts create mode 100644 crates-tauri/yaak-app-proxy/tauri.conf.json diff --git a/apps/yaak-proxy/vite.config.ts b/apps/yaak-proxy/vite.config.ts new file mode 100644 index 00000000..3d56934f --- /dev/null +++ b/apps/yaak-proxy/vite.config.ts @@ -0,0 +1,16 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: "../../dist/apps/yaak-proxy", + emptyOutDir: true, + }, + clearScreen: false, + server: { + port: parseInt(process.env.YAAK_PROXY_DEV_PORT ?? "2420", 10), + strictPort: true, + }, + envPrefix: ["VITE_", "TAURI_"], +}); diff --git a/crates-tauri/yaak-app-proxy/tauri.conf.json b/crates-tauri/yaak-app-proxy/tauri.conf.json new file mode 100644 index 00000000..83acb27c --- /dev/null +++ b/crates-tauri/yaak-app-proxy/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "productName": "Yaak Proxy", + "version": "0.0.0", + "identifier": "app.yaak.proxy", + "build": { + "beforeBuildCommand": "npm --prefix ../.. run proxy:tauri-before-build", + "beforeDevCommand": "npm --prefix ../.. run proxy:tauri-before-dev", + "devUrl": "http://localhost:2420", + "frontendDist": "../../dist/apps/yaak-proxy" + }, + "app": { + "withGlobalTauri": false, + "windows": [ + { + "label": "main", + "title": "Yaak Proxy", + "width": 1000, + "height": 700, + "minWidth": 720, + "minHeight": 480 + } + ] + }, + "bundle": { + "icon": [ + "../yaak-app-client/icons/release/32x32.png", + "../yaak-app-client/icons/release/128x128.png", + "../yaak-app-client/icons/release/128x128@2x.png", + "../yaak-app-client/icons/release/icon.icns", + "../yaak-app-client/icons/release/icon.ico" + ] + } +} diff --git a/scripts/git-hooks/post-checkout.mjs b/scripts/git-hooks/post-checkout.mjs index 55700776..383422c4 100644 --- a/scripts/git-hooks/post-checkout.mjs +++ b/scripts/git-hooks/post-checkout.mjs @@ -10,55 +10,61 @@ * process.argv[4] - flag (1 = branch checkout, 0 = file checkout) */ -import fs from 'fs'; -import path from 'path'; -import { execSync, execFileSync } from 'child_process'; -import { fileURLToPath } from 'url'; +import fs from "fs"; +import path from "path"; +import { execSync, execFileSync } from "child_process"; +import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const isBranchCheckout = process.argv[4] === '1'; +const isBranchCheckout = process.argv[4] === "1"; if (!isBranchCheckout) { process.exit(0); } // Check if we're in a worktree by looking for .git file (not directory) -const gitPath = path.join(process.cwd(), '.git'); +const gitPath = path.join(process.cwd(), ".git"); const isWorktree = fs.existsSync(gitPath) && fs.statSync(gitPath).isFile(); if (!isWorktree) { process.exit(0); } -const envLocalPath = path.join(process.cwd(), '.env.local'); +const envLocalPath = path.join(process.cwd(), ".env.local"); // Don't overwrite existing .env.local if (fs.existsSync(envLocalPath)) { process.exit(0); } -console.log('Detected new worktree - configuring ports in .env.local'); +console.log("Detected new worktree - configuring ports in .env.local"); -// Find the highest ports in use across all worktrees -// Main worktree (first in list) is assumed to use default ports 1420/64343 +const CLIENT_PORT_BASE = 1420; +const PROXY_PORT_BASE = 2420; // Keep proxy +1000 from client to avoid cross-app conflicts + +// Find the highest worktree index and MCP port in use across all worktrees. +// Main worktree (first in list) is assumed to use index 0: +// client=1420, proxy=2420, mcp=64343. let maxMcpPort = 64343; -let maxDevPort = 1420; +let maxWorktreeIndex = 0; try { - const worktreeList = execSync('git worktree list --porcelain', { encoding: 'utf8' }); + const worktreeList = execSync("git worktree list --porcelain", { + encoding: "utf8", + }); const worktreePaths = worktreeList - .split('\n') - .filter(line => line.startsWith('worktree ')) - .map(line => line.replace('worktree ', '').trim()); + .split("\n") + .filter((line) => line.startsWith("worktree ")) + .map((line) => line.replace("worktree ", "").trim()); // Skip the first worktree (main) since it uses default ports for (let i = 1; i < worktreePaths.length; i++) { const worktreePath = worktreePaths[i]; - const envPath = path.join(worktreePath, '.env.local'); + const envPath = path.join(worktreePath, ".env.local"); if (fs.existsSync(envPath)) { - const content = fs.readFileSync(envPath, 'utf8'); + const content = fs.readFileSync(envPath, "utf8"); const mcpMatch = content.match(/^YAAK_PLUGIN_MCP_SERVER_PORT=(\d+)/m); if (mcpMatch) { @@ -68,24 +74,42 @@ try { } } - const devMatch = content.match(/^YAAK_DEV_PORT=(\d+)/m); - if (devMatch) { - const port = parseInt(devMatch[1], 10); - if (port > maxDevPort) { - maxDevPort = port; + const clientDevMatch = content.match( + /^(?:YAAK_CLIENT_DEV_PORT|YAAK_DEV_PORT)=(\d+)/m, + ); + if (clientDevMatch) { + const port = parseInt(clientDevMatch[1], 10); + const index = port - CLIENT_PORT_BASE; + if (index > maxWorktreeIndex) { + maxWorktreeIndex = index; + } + } + + const proxyDevMatch = content.match(/^YAAK_PROXY_DEV_PORT=(\d+)/m); + if (proxyDevMatch) { + const port = parseInt(proxyDevMatch[1], 10); + const index = port - PROXY_PORT_BASE; + if (index > maxWorktreeIndex) { + maxWorktreeIndex = index; } } } } - // Increment to get the next available port - maxDevPort++; + // Increment MCP to get the next available port maxMcpPort++; } catch (err) { - console.error('Warning: Could not check other worktrees for port conflicts:', err.message); + console.error( + "Warning: Could not check other worktrees for port conflicts:", + err.message, + ); // Continue with default ports } +const nextWorktreeIndex = maxWorktreeIndex + 1; +const nextClientDevPort = CLIENT_PORT_BASE + nextWorktreeIndex; +const nextProxyDevPort = PROXY_PORT_BASE + nextWorktreeIndex; + // Get worktree name from current directory const worktreeName = path.basename(process.cwd()); @@ -93,44 +117,82 @@ const worktreeName = path.basename(process.cwd()); const envContent = `# Auto-generated by git post-checkout hook # This file configures ports for this worktree to avoid conflicts -# Vite dev server port (main worktree uses 1420) -YAAK_DEV_PORT=${maxDevPort} +# Vite dev server port for the client app (main worktree uses ${CLIENT_PORT_BASE}) +YAAK_CLIENT_DEV_PORT=${nextClientDevPort} + +# Vite dev server port for the proxy app (main worktree uses ${PROXY_PORT_BASE}) +YAAK_PROXY_DEV_PORT=${nextProxyDevPort} # MCP Server port (main worktree uses 64343) YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort} `; -fs.writeFileSync(envLocalPath, envContent, 'utf8'); -console.log(`Created .env.local with YAAK_DEV_PORT=${maxDevPort} and YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}`); +fs.writeFileSync(envLocalPath, envContent, "utf8"); +console.log( + `Created .env.local with YAAK_CLIENT_DEV_PORT=${nextClientDevPort}, YAAK_PROXY_DEV_PORT=${nextProxyDevPort}, and YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}`, +); // Create tauri.worktree.conf.json with unique app identifier for complete isolation // This gives each worktree its own app data directory, avoiding the need for DB path prefixes -const tauriWorktreeConfig = { +const clientTauriWorktreeConfig = { identifier: `app.yaak.desktop.dev.${worktreeName}`, - productName: `Daak (${worktreeName})` + productName: `Daak (${worktreeName})`, }; -const tauriConfigPath = path.join(process.cwd(), 'crates-tauri', 'yaak-app', 'tauri.worktree.conf.json'); -fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriWorktreeConfig, null, 2) + '\n', 'utf8'); -console.log(`Created tauri.worktree.conf.json with identifier: ${tauriWorktreeConfig.identifier}`); +const clientTauriConfigPath = path.join( + process.cwd(), + "crates-tauri", + "yaak-app-client", + "tauri.worktree.conf.json", +); +fs.writeFileSync( + clientTauriConfigPath, + JSON.stringify(clientTauriWorktreeConfig, null, 2) + "\n", + "utf8", +); +console.log( + `Created client tauri.worktree.conf.json with identifier: ${clientTauriWorktreeConfig.identifier}`, +); + +const proxyTauriWorktreeConfig = { + identifier: `app.yaak.proxy.dev.${worktreeName}`, + productName: `Yaak Proxy (${worktreeName})`, +}; + +const proxyTauriConfigPath = path.join( + process.cwd(), + "crates-tauri", + "yaak-app-proxy", + "tauri.worktree.conf.json", +); +fs.writeFileSync( + proxyTauriConfigPath, + JSON.stringify(proxyTauriWorktreeConfig, null, 2) + "\n", + "utf8", +); +console.log( + `Created proxy tauri.worktree.conf.json with identifier: ${proxyTauriWorktreeConfig.identifier}`, +); // Copy gitignored editor config folders from main worktree (.zed, .vscode, .claude, etc.) // This ensures your editor settings, tasks, and configurations are available in the new worktree // without needing to manually copy them or commit them to git. try { - const worktreeList = execSync('git worktree list --porcelain', { encoding: 'utf8' }); + const worktreeList = execSync("git worktree list --porcelain", { + encoding: "utf8", + }); const mainWorktreePath = worktreeList - .split('\n') - .find(line => line.startsWith('worktree ')) - ?.replace('worktree ', '') + .split("\n") + .find((line) => line.startsWith("worktree ")) + ?.replace("worktree ", "") .trim(); if (mainWorktreePath) { // Find all .* folders in main worktree root that are gitignored const entries = fs.readdirSync(mainWorktreePath, { withFileTypes: true }); const dotFolders = entries - .filter(entry => entry.isDirectory() && entry.name.startsWith('.')) - .map(entry => entry.name); + .filter((entry) => entry.isDirectory() && entry.name.startsWith(".")) + .map((entry) => entry.name); for (const folder of dotFolders) { const sourcePath = path.join(mainWorktreePath, folder); @@ -138,9 +200,9 @@ try { try { // Check if it's gitignored - run from main worktree directory - execFileSync('git', ['check-ignore', '-q', folder], { - stdio: 'pipe', - cwd: mainWorktreePath + execFileSync("git", ["check-ignore", "-q", folder], { + stdio: "pipe", + cwd: mainWorktreePath, }); // It's gitignored, copy it @@ -152,8 +214,13 @@ try { } } } catch (err) { - console.warn('Warning: Could not copy files from main worktree:', err.message); + console.warn( + "Warning: Could not copy files from main worktree:", + err.message, + ); // Continue anyway } -console.log('\n✓ Worktree setup complete! Run `npm run init` to install dependencies.'); +console.log( + "\n✓ Worktree setup complete! Run `npm run init` to install dependencies.", +); diff --git a/scripts/run-dev.mjs b/scripts/run-dev.mjs index e6ded1c3..9c981ae3 100644 --- a/scripts/run-dev.mjs +++ b/scripts/run-dev.mjs @@ -1,27 +1,26 @@ #!/usr/bin/env node /** - * Script to run Tauri dev server with dynamic port configuration. - * Loads port from .env.local if present, otherwise uses default port 1420. + * Script to run a Tauri app dev server with dynamic port configuration. */ -import { spawnSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import { spawnSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const rootDir = path.join(__dirname, '..'); +const rootDir = path.join(__dirname, ".."); // Load .env.local if it exists -const envLocalPath = path.join(rootDir, '.env.local'); +const envLocalPath = path.join(rootDir, ".env.local"); if (fs.existsSync(envLocalPath)) { - const envContent = fs.readFileSync(envLocalPath, 'utf8'); + const envContent = fs.readFileSync(envLocalPath, "utf8"); const envVars = envContent - .split('\n') - .filter(line => line && !line.startsWith('#')) + .split("\n") + .filter((line) => line && !line.startsWith("#")) .reduce((acc, line) => { - const [key, value] = line.split('='); + const [key, value] = line.split("="); if (key && value) { acc[key.trim()] = value.trim(); } @@ -31,23 +30,87 @@ if (fs.existsSync(envLocalPath)) { Object.assign(process.env, envVars); } -const port = process.env.YAAK_DEV_PORT || '1420'; -const config = JSON.stringify({ build: { devUrl: `http://localhost:${port}` } }); +const [appName = "client", ...additionalArgs] = process.argv.slice(2); +const appConfigs = { + client: { + appDir: "crates-tauri/yaak-app-client", + devPortEnv: "YAAK_CLIENT_DEV_PORT", + fallbackDevPortEnv: "YAAK_DEV_PORT", + defaultPort: "1420", + tauriConfig: "tauri.conf.json", + tauriDevConfig: "tauri.development.conf.json", + }, + proxy: { + appDir: "crates-tauri/yaak-app-proxy", + devPortEnv: "YAAK_PROXY_DEV_PORT", + fallbackDevPortEnv: null, + defaultPort: "2420", + tauriConfig: "tauri.conf.json", + tauriDevConfig: "tauri.development.conf.json", + }, +}; -// Get additional arguments passed after npm run app-dev -- -const additionalArgs = process.argv.slice(2); +const appConfig = appConfigs[appName]; +if (appConfig == null) { + console.error(`Unknown Tauri app "${appName}"`); + process.exit(1); +} + +const port = + process.env[appConfig.devPortEnv] || + (appConfig.fallbackDevPortEnv + ? process.env[appConfig.fallbackDevPortEnv] + : undefined) || + appConfig.defaultPort; +const config = JSON.stringify({ + build: { devUrl: `http://localhost:${port}` }, +}); + +// Normalize extra `--config` path args to absolute paths from repo root so +// callers can keep passing root-relative config files. +const normalizedAdditionalArgs = []; +for (let i = 0; i < additionalArgs.length; i++) { + const arg = additionalArgs[i]; + if (arg === "--config" && i + 1 < additionalArgs.length) { + const value = additionalArgs[i + 1]; + const isInlineJson = value.trimStart().startsWith("{"); + normalizedAdditionalArgs.push("--config"); + normalizedAdditionalArgs.push( + !isInlineJson && !path.isAbsolute(value) + ? path.join(rootDir, value) + : value, + ); + i++; + continue; + } + normalizedAdditionalArgs.push(arg); +} const args = [ - 'dev', - '--no-watch', - '--config', 'crates-tauri/yaak-app/tauri.development.conf.json', - '--config', config, - ...additionalArgs + "dev", + "--no-watch", + "--config", + appConfig.tauriConfig, + "--config", + appConfig.tauriDevConfig, + "--config", + config, + ...normalizedAdditionalArgs, ]; // Invoke the tauri CLI JS entry point directly via node to avoid shell escaping issues on Windows -const tauriJs = path.join(rootDir, 'node_modules', '@tauri-apps', 'cli', 'tauri.js'); +const tauriJs = path.join( + rootDir, + "node_modules", + "@tauri-apps", + "cli", + "tauri.js", +); -const result = spawnSync(process.execPath, [tauriJs, ...args], { stdio: 'inherit', env: process.env }); +const result = spawnSync(process.execPath, [tauriJs, ...args], { + cwd: path.join(rootDir, appConfig.appDir), + stdio: "inherit", + env: process.env, +}); process.exit(result.status || 0);