| |
| |
| |
| |
| |
| |
|
|
| import { spawn } from 'child_process'; |
| import path from 'path'; |
| import { createLogger } from '@automaker/utils'; |
| import { systemPathExists, getShellPaths, findGitBashPath } from '@automaker/platform'; |
| import { findCommand } from '../lib/cli-detection.js'; |
| import type { EventEmitter } from '../lib/events.js'; |
| import { readWorktreeMetadata, writeWorktreeMetadata } from '../lib/worktree-metadata.js'; |
| import * as secureFs from '../lib/secure-fs.js'; |
|
|
| const logger = createLogger('InitScript'); |
|
|
| export interface InitScriptOptions { |
| |
| projectPath: string; |
| |
| worktreePath: string; |
| |
| branch: string; |
| |
| emitter: EventEmitter; |
| } |
|
|
| interface ShellCommand { |
| shell: string; |
| args: string[]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export class InitScriptService { |
| private cachedShellCommand: ShellCommand | null | undefined = undefined; |
|
|
| |
| |
| |
| getInitScriptPath(projectPath: string): string { |
| return path.join(projectPath, '.automaker', 'worktree-init.sh'); |
| } |
|
|
| |
| |
| |
| async hasInitScriptRun(projectPath: string, branch: string): Promise<boolean> { |
| const metadata = await readWorktreeMetadata(projectPath, branch); |
| return metadata?.initScriptRan === true; |
| } |
|
|
| |
| |
| |
| |
| async findShellCommand(): Promise<ShellCommand | null> { |
| |
| if (this.cachedShellCommand !== undefined) { |
| return this.cachedShellCommand; |
| } |
|
|
| if (process.platform === 'win32') { |
| |
| |
|
|
| |
| const gitBashPath = await findGitBashPath(); |
| if (gitBashPath) { |
| logger.debug(`Found Git Bash at: ${gitBashPath}`); |
| this.cachedShellCommand = { shell: gitBashPath, args: [] }; |
| return this.cachedShellCommand; |
| } |
|
|
| |
| const bashInPath = await findCommand(['bash']); |
| if (bashInPath && !bashInPath.toLowerCase().includes('system32')) { |
| logger.debug(`Found bash in PATH at: ${bashInPath}`); |
| this.cachedShellCommand = { shell: bashInPath, args: [] }; |
| return this.cachedShellCommand; |
| } |
|
|
| logger.warn('Git Bash not found. WSL bash was skipped to avoid compatibility issues.'); |
| this.cachedShellCommand = null; |
| return null; |
| } |
|
|
| |
| const shellPaths = getShellPaths(); |
| const posixShells = shellPaths.filter( |
| (p) => p.includes('bash') || p === '/bin/sh' || p === '/usr/bin/sh' |
| ); |
|
|
| for (const shellPath of posixShells) { |
| try { |
| if (systemPathExists(shellPath)) { |
| this.cachedShellCommand = { shell: shellPath, args: [] }; |
| return this.cachedShellCommand; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| if (systemPathExists('/bin/sh')) { |
| this.cachedShellCommand = { shell: '/bin/sh', args: [] }; |
| return this.cachedShellCommand; |
| } |
|
|
| this.cachedShellCommand = null; |
| return null; |
| } |
|
|
| |
| |
| |
| |
| async runInitScript(options: InitScriptOptions): Promise<void> { |
| const { projectPath, worktreePath, branch, emitter } = options; |
|
|
| const scriptPath = this.getInitScriptPath(projectPath); |
|
|
| |
| try { |
| await secureFs.access(scriptPath); |
| } catch { |
| logger.debug(`No init script found at ${scriptPath}`); |
| return; |
| } |
|
|
| |
| if (await this.hasInitScriptRun(projectPath, branch)) { |
| logger.info(`Init script already ran for branch "${branch}", skipping`); |
| return; |
| } |
|
|
| |
| const shellCmd = await this.findShellCommand(); |
| if (!shellCmd) { |
| const error = |
| process.platform === 'win32' |
| ? 'Git Bash not found. Please install Git for Windows to run init scripts.' |
| : 'No shell found (/bin/bash or /bin/sh)'; |
| logger.error(error); |
|
|
| |
| const existingMetadata = await readWorktreeMetadata(projectPath, branch); |
| await writeWorktreeMetadata(projectPath, branch, { |
| branch, |
| createdAt: existingMetadata?.createdAt || new Date().toISOString(), |
| pr: existingMetadata?.pr, |
| initScriptRan: true, |
| initScriptStatus: 'failed', |
| initScriptError: error, |
| }); |
|
|
| emitter.emit('worktree:init-completed', { |
| projectPath, |
| worktreePath, |
| branch, |
| success: false, |
| error, |
| }); |
| return; |
| } |
|
|
| logger.info(`Running init script for branch "${branch}" in ${worktreePath}`); |
| logger.debug(`Using shell: ${shellCmd.shell}`); |
|
|
| |
| const existingMetadata = await readWorktreeMetadata(projectPath, branch); |
| await writeWorktreeMetadata(projectPath, branch, { |
| branch, |
| createdAt: existingMetadata?.createdAt || new Date().toISOString(), |
| pr: existingMetadata?.pr, |
| initScriptRan: false, |
| initScriptStatus: 'running', |
| }); |
|
|
| |
| emitter.emit('worktree:init-started', { |
| projectPath, |
| worktreePath, |
| branch, |
| }); |
|
|
| |
| |
| const safeEnv: Record<string, string> = { |
| |
| AUTOMAKER_PROJECT_PATH: projectPath, |
| AUTOMAKER_WORKTREE_PATH: worktreePath, |
| AUTOMAKER_BRANCH: branch, |
|
|
| |
| PATH: process.env.PATH || '', |
| HOME: process.env.HOME || '', |
| USER: process.env.USER || '', |
| TMPDIR: process.env.TMPDIR || process.env.TEMP || process.env.TMP || '/tmp', |
|
|
| |
| SHELL: process.env.SHELL || '', |
| LANG: process.env.LANG || 'en_US.UTF-8', |
| LC_ALL: process.env.LC_ALL || '', |
|
|
| |
| FORCE_COLOR: '1', |
| npm_config_color: 'always', |
| CLICOLOR_FORCE: '1', |
|
|
| |
| GIT_TERMINAL_PROMPT: '0', |
| }; |
|
|
| |
| if (process.platform === 'win32') { |
| safeEnv.USERPROFILE = process.env.USERPROFILE || ''; |
| safeEnv.APPDATA = process.env.APPDATA || ''; |
| safeEnv.LOCALAPPDATA = process.env.LOCALAPPDATA || ''; |
| safeEnv.SystemRoot = process.env.SystemRoot || 'C:\\Windows'; |
| safeEnv.TEMP = process.env.TEMP || ''; |
| } |
|
|
| |
| const child = spawn(shellCmd.shell, [...shellCmd.args, scriptPath], { |
| cwd: worktreePath, |
| env: safeEnv, |
| stdio: ['ignore', 'pipe', 'pipe'], |
| }); |
|
|
| |
| child.stdout?.on('data', (data: Buffer) => { |
| const content = data.toString(); |
| emitter.emit('worktree:init-output', { |
| projectPath, |
| branch, |
| type: 'stdout', |
| content, |
| }); |
| }); |
|
|
| |
| child.stderr?.on('data', (data: Buffer) => { |
| const content = data.toString(); |
| emitter.emit('worktree:init-output', { |
| projectPath, |
| branch, |
| type: 'stderr', |
| content, |
| }); |
| }); |
|
|
| |
| child.on('exit', async (code) => { |
| const success = code === 0; |
| const status = success ? 'success' : 'failed'; |
|
|
| logger.info(`Init script for branch "${branch}" ${status} with exit code ${code}`); |
|
|
| |
| const metadata = await readWorktreeMetadata(projectPath, branch); |
| await writeWorktreeMetadata(projectPath, branch, { |
| branch, |
| createdAt: metadata?.createdAt || new Date().toISOString(), |
| pr: metadata?.pr, |
| initScriptRan: true, |
| initScriptStatus: status, |
| initScriptError: success ? undefined : `Exit code: ${code}`, |
| }); |
|
|
| |
| emitter.emit('worktree:init-completed', { |
| projectPath, |
| worktreePath, |
| branch, |
| success, |
| exitCode: code, |
| }); |
| }); |
|
|
| child.on('error', async (error) => { |
| logger.error(`Init script error for branch "${branch}":`, error); |
|
|
| |
| const metadata = await readWorktreeMetadata(projectPath, branch); |
| await writeWorktreeMetadata(projectPath, branch, { |
| branch, |
| createdAt: metadata?.createdAt || new Date().toISOString(), |
| pr: metadata?.pr, |
| initScriptRan: true, |
| initScriptStatus: 'failed', |
| initScriptError: error.message, |
| }); |
|
|
| |
| emitter.emit('worktree:init-completed', { |
| projectPath, |
| worktreePath, |
| branch, |
| success: false, |
| error: error.message, |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| async forceRunInitScript(options: InitScriptOptions): Promise<void> { |
| const { projectPath, branch } = options; |
|
|
| |
| const metadata = await readWorktreeMetadata(projectPath, branch); |
| if (metadata) { |
| await writeWorktreeMetadata(projectPath, branch, { |
| ...metadata, |
| initScriptRan: false, |
| initScriptStatus: undefined, |
| initScriptError: undefined, |
| }); |
| } |
|
|
| |
| await this.runInitScript(options); |
| } |
| } |
|
|
| |
| let initScriptService: InitScriptService | null = null; |
|
|
| |
| |
| |
| export function getInitScriptService(): InitScriptService { |
| if (!initScriptService) { |
| initScriptService = new InitScriptService(); |
| } |
| return initScriptService; |
| } |
|
|
| |
| export const getInitScriptPath = (projectPath: string) => |
| getInitScriptService().getInitScriptPath(projectPath); |
|
|
| export const hasInitScriptRun = (projectPath: string, branch: string) => |
| getInitScriptService().hasInitScriptRun(projectPath, branch); |
|
|
| export const runInitScript = (options: InitScriptOptions) => |
| getInitScriptService().runInitScript(options); |
|
|
| export const forceRunInitScript = (options: InitScriptOptions) => |
| getInitScriptService().forceRunInitScript(options); |
|
|