mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 22:40:26 +01:00
Support running multiple Yaak instances via git worktrees (#341)
This commit is contained in:
153
scripts/git-hooks/post-checkout.mjs
Normal file
153
scripts/git-hooks/post-checkout.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Git post-checkout hook for auto-configuring worktree environments.
|
||||
* This runs after 'git checkout' or 'git worktree add'.
|
||||
*
|
||||
* Args from git:
|
||||
* process.argv[2] - previous HEAD ref
|
||||
* process.argv[3] - new HEAD ref
|
||||
* 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';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
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 isWorktree = fs.existsSync(gitPath) && fs.statSync(gitPath).isFile();
|
||||
|
||||
if (!isWorktree) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// Find the highest ports in use across all worktrees
|
||||
// Main worktree (first in list) is assumed to use default ports 1420/64343
|
||||
let maxMcpPort = 64343;
|
||||
let maxDevPort = 1420;
|
||||
|
||||
try {
|
||||
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());
|
||||
|
||||
// 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');
|
||||
|
||||
if (fs.existsSync(envPath)) {
|
||||
const content = fs.readFileSync(envPath, 'utf8');
|
||||
|
||||
const mcpMatch = content.match(/^YAAK_PLUGIN_MCP_SERVER_PORT=(\d+)/m);
|
||||
if (mcpMatch) {
|
||||
const port = parseInt(mcpMatch[1], 10);
|
||||
if (port > maxMcpPort) {
|
||||
maxMcpPort = port;
|
||||
}
|
||||
}
|
||||
|
||||
const devMatch = content.match(/^YAAK_DEV_PORT=(\d+)/m);
|
||||
if (devMatch) {
|
||||
const port = parseInt(devMatch[1], 10);
|
||||
if (port > maxDevPort) {
|
||||
maxDevPort = port;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment to get the next available port
|
||||
maxDevPort++;
|
||||
maxMcpPort++;
|
||||
} catch (err) {
|
||||
console.error('Warning: Could not check other worktrees for port conflicts:', err.message);
|
||||
// Continue with default ports
|
||||
}
|
||||
|
||||
// Create .env.local with unique ports
|
||||
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}
|
||||
|
||||
# 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}`);
|
||||
|
||||
// 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 mainWorktreePath = worktreeList
|
||||
.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);
|
||||
|
||||
for (const folder of dotFolders) {
|
||||
const sourcePath = path.join(mainWorktreePath, folder);
|
||||
const destPath = path.join(process.cwd(), folder);
|
||||
|
||||
try {
|
||||
// Check if it's gitignored - run from main worktree directory
|
||||
execFileSync('git', ['check-ignore', '-q', folder], {
|
||||
stdio: 'pipe',
|
||||
cwd: mainWorktreePath
|
||||
});
|
||||
|
||||
// It's gitignored, copy it
|
||||
fs.cpSync(sourcePath, destPath, { recursive: true });
|
||||
console.log(`Copied ${folder} from main worktree`);
|
||||
} catch {
|
||||
// Not gitignored or doesn't exist, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Warning: Could not copy files from main worktree:', err.message);
|
||||
// Continue anyway
|
||||
}
|
||||
|
||||
// Run npm run init to install dependencies and bootstrap
|
||||
console.log('\nRunning npm run init to install dependencies and bootstrap...');
|
||||
try {
|
||||
execSync('npm run init', { stdio: 'inherit' });
|
||||
console.log('\n✓ Worktree setup complete!');
|
||||
} catch (err) {
|
||||
console.error('\n✗ Failed to run npm run init. You may need to run it manually.');
|
||||
process.exit(1);
|
||||
}
|
||||
49
scripts/run-dev.mjs
Normal file
49
scripts/run-dev.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/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.
|
||||
*/
|
||||
|
||||
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));
|
||||
|
||||
// Load .env.local if it exists
|
||||
const envLocalPath = path.join(__dirname, '..', '.env.local');
|
||||
if (fs.existsSync(envLocalPath)) {
|
||||
const envContent = fs.readFileSync(envLocalPath, 'utf8');
|
||||
const envVars = envContent
|
||||
.split('\n')
|
||||
.filter(line => line && !line.startsWith('#'))
|
||||
.reduce((acc, line) => {
|
||||
const [key, value] = line.split('=');
|
||||
if (key && value) {
|
||||
acc[key.trim()] = value.trim();
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.assign(process.env, envVars);
|
||||
}
|
||||
|
||||
const port = process.env.YAAK_DEV_PORT || '1420';
|
||||
const config = JSON.stringify({ build: { devUrl: `http://localhost:${port}` } });
|
||||
|
||||
// Get additional arguments passed after npm run app-dev --
|
||||
const additionalArgs = process.argv.slice(2);
|
||||
|
||||
const args = [
|
||||
'dev',
|
||||
'--no-watch',
|
||||
'--config', './src-tauri/tauri.development.conf.json',
|
||||
'--config', config,
|
||||
...additionalArgs
|
||||
];
|
||||
|
||||
const result = spawnSync('tauri', args, { stdio: 'inherit', shell: false, env: process.env });
|
||||
|
||||
process.exit(result.status || 0);
|
||||
Reference in New Issue
Block a user