Use separated client/proxy dev ports across worktrees

This commit is contained in:
Gregory Schier
2026-03-06 09:20:49 -08:00
parent 32f22aad67
commit e26705f016
4 changed files with 247 additions and 68 deletions

View File

@@ -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_"],
});

View File

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

View File

@@ -10,55 +10,61 @@
* process.argv[4] - flag (1 = branch checkout, 0 = file checkout) * process.argv[4] - flag (1 = branch checkout, 0 = file checkout)
*/ */
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import { execSync, execFileSync } from 'child_process'; import { execSync, execFileSync } from "child_process";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isBranchCheckout = process.argv[4] === '1'; const isBranchCheckout = process.argv[4] === "1";
if (!isBranchCheckout) { if (!isBranchCheckout) {
process.exit(0); process.exit(0);
} }
// Check if we're in a worktree by looking for .git file (not directory) // 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(); const isWorktree = fs.existsSync(gitPath) && fs.statSync(gitPath).isFile();
if (!isWorktree) { if (!isWorktree) {
process.exit(0); 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 // Don't overwrite existing .env.local
if (fs.existsSync(envLocalPath)) { if (fs.existsSync(envLocalPath)) {
process.exit(0); 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 const CLIENT_PORT_BASE = 1420;
// Main worktree (first in list) is assumed to use default ports 1420/64343 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 maxMcpPort = 64343;
let maxDevPort = 1420; let maxWorktreeIndex = 0;
try { try {
const worktreeList = execSync('git worktree list --porcelain', { encoding: 'utf8' }); const worktreeList = execSync("git worktree list --porcelain", {
encoding: "utf8",
});
const worktreePaths = worktreeList const worktreePaths = worktreeList
.split('\n') .split("\n")
.filter(line => line.startsWith('worktree ')) .filter((line) => line.startsWith("worktree "))
.map(line => line.replace('worktree ', '').trim()); .map((line) => line.replace("worktree ", "").trim());
// Skip the first worktree (main) since it uses default ports // Skip the first worktree (main) since it uses default ports
for (let i = 1; i < worktreePaths.length; i++) { for (let i = 1; i < worktreePaths.length; i++) {
const worktreePath = worktreePaths[i]; const worktreePath = worktreePaths[i];
const envPath = path.join(worktreePath, '.env.local'); const envPath = path.join(worktreePath, ".env.local");
if (fs.existsSync(envPath)) { 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); const mcpMatch = content.match(/^YAAK_PLUGIN_MCP_SERVER_PORT=(\d+)/m);
if (mcpMatch) { if (mcpMatch) {
@@ -68,24 +74,42 @@ try {
} }
} }
const devMatch = content.match(/^YAAK_DEV_PORT=(\d+)/m); const clientDevMatch = content.match(
if (devMatch) { /^(?:YAAK_CLIENT_DEV_PORT|YAAK_DEV_PORT)=(\d+)/m,
const port = parseInt(devMatch[1], 10); );
if (port > maxDevPort) { if (clientDevMatch) {
maxDevPort = port; 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 // Increment MCP to get the next available port
maxDevPort++;
maxMcpPort++; maxMcpPort++;
} catch (err) { } 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 // 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 // Get worktree name from current directory
const worktreeName = path.basename(process.cwd()); 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 const envContent = `# Auto-generated by git post-checkout hook
# This file configures ports for this worktree to avoid conflicts # This file configures ports for this worktree to avoid conflicts
# Vite dev server port (main worktree uses 1420) # Vite dev server port for the client app (main worktree uses ${CLIENT_PORT_BASE})
YAAK_DEV_PORT=${maxDevPort} 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) # MCP Server port (main worktree uses 64343)
YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort} YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}
`; `;
fs.writeFileSync(envLocalPath, envContent, 'utf8'); fs.writeFileSync(envLocalPath, envContent, "utf8");
console.log(`Created .env.local with YAAK_DEV_PORT=${maxDevPort} and YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}`); 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 // 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 // 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}`, 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'); const clientTauriConfigPath = path.join(
fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriWorktreeConfig, null, 2) + '\n', 'utf8'); process.cwd(),
console.log(`Created tauri.worktree.conf.json with identifier: ${tauriWorktreeConfig.identifier}`); "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.) // 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 // 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. // without needing to manually copy them or commit them to git.
try { try {
const worktreeList = execSync('git worktree list --porcelain', { encoding: 'utf8' }); const worktreeList = execSync("git worktree list --porcelain", {
encoding: "utf8",
});
const mainWorktreePath = worktreeList const mainWorktreePath = worktreeList
.split('\n') .split("\n")
.find(line => line.startsWith('worktree ')) .find((line) => line.startsWith("worktree "))
?.replace('worktree ', '') ?.replace("worktree ", "")
.trim(); .trim();
if (mainWorktreePath) { if (mainWorktreePath) {
// Find all .* folders in main worktree root that are gitignored // Find all .* folders in main worktree root that are gitignored
const entries = fs.readdirSync(mainWorktreePath, { withFileTypes: true }); const entries = fs.readdirSync(mainWorktreePath, { withFileTypes: true });
const dotFolders = entries const dotFolders = entries
.filter(entry => entry.isDirectory() && entry.name.startsWith('.')) .filter((entry) => entry.isDirectory() && entry.name.startsWith("."))
.map(entry => entry.name); .map((entry) => entry.name);
for (const folder of dotFolders) { for (const folder of dotFolders) {
const sourcePath = path.join(mainWorktreePath, folder); const sourcePath = path.join(mainWorktreePath, folder);
@@ -138,9 +200,9 @@ try {
try { try {
// Check if it's gitignored - run from main worktree directory // Check if it's gitignored - run from main worktree directory
execFileSync('git', ['check-ignore', '-q', folder], { execFileSync("git", ["check-ignore", "-q", folder], {
stdio: 'pipe', stdio: "pipe",
cwd: mainWorktreePath cwd: mainWorktreePath,
}); });
// It's gitignored, copy it // It's gitignored, copy it
@@ -152,8 +214,13 @@ try {
} }
} }
} catch (err) { } 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 // 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.",
);

View File

@@ -1,27 +1,26 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Script to run Tauri dev server with dynamic port configuration. * Script to run a Tauri app dev server with dynamic port configuration.
* Loads port from .env.local if present, otherwise uses default port 1420.
*/ */
import { spawnSync } from 'child_process'; import { spawnSync } from "child_process";
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.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 // Load .env.local if it exists
const envLocalPath = path.join(rootDir, '.env.local'); const envLocalPath = path.join(rootDir, ".env.local");
if (fs.existsSync(envLocalPath)) { if (fs.existsSync(envLocalPath)) {
const envContent = fs.readFileSync(envLocalPath, 'utf8'); const envContent = fs.readFileSync(envLocalPath, "utf8");
const envVars = envContent const envVars = envContent
.split('\n') .split("\n")
.filter(line => line && !line.startsWith('#')) .filter((line) => line && !line.startsWith("#"))
.reduce((acc, line) => { .reduce((acc, line) => {
const [key, value] = line.split('='); const [key, value] = line.split("=");
if (key && value) { if (key && value) {
acc[key.trim()] = value.trim(); acc[key.trim()] = value.trim();
} }
@@ -31,23 +30,87 @@ if (fs.existsSync(envLocalPath)) {
Object.assign(process.env, envVars); Object.assign(process.env, envVars);
} }
const port = process.env.YAAK_DEV_PORT || '1420'; const [appName = "client", ...additionalArgs] = process.argv.slice(2);
const config = JSON.stringify({ build: { devUrl: `http://localhost:${port}` } }); 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 appConfig = appConfigs[appName];
const additionalArgs = process.argv.slice(2); 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 = [ const args = [
'dev', "dev",
'--no-watch', "--no-watch",
'--config', 'crates-tauri/yaak-app/tauri.development.conf.json', "--config",
'--config', config, appConfig.tauriConfig,
...additionalArgs "--config",
appConfig.tauriDevConfig,
"--config",
config,
...normalizedAdditionalArgs,
]; ];
// Invoke the tauri CLI JS entry point directly via node to avoid shell escaping issues on Windows // 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); process.exit(result.status || 0);