mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
feat: Add run init script functionality for worktrees
This commit introduces the ability to run initialization scripts for worktrees, enhancing the setup process. Key changes include: 1. **New API Endpoint**: Added a POST endpoint to run the init script for a specified worktree. 2. **Worktree Routes**: Updated worktree routes to include the new run init script handler. 3. **Init Script Service**: Enhanced the Init Script Service to support running scripts asynchronously and handling errors. 4. **UI Updates**: Added UI components to check for the existence of init scripts and trigger their execution, providing user feedback through toast notifications. 5. **Event Handling**: Implemented event handling for init script execution status, allowing real-time updates in the UI. This feature streamlines the workflow for users by automating the execution of setup scripts, improving overall project management.
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
|||||||
createGetInitScriptHandler,
|
createGetInitScriptHandler,
|
||||||
createPutInitScriptHandler,
|
createPutInitScriptHandler,
|
||||||
createDeleteInitScriptHandler,
|
createDeleteInitScriptHandler,
|
||||||
|
createRunInitScriptHandler,
|
||||||
} from './routes/init-script.js';
|
} from './routes/init-script.js';
|
||||||
|
|
||||||
export function createWorktreeRoutes(events: EventEmitter): Router {
|
export function createWorktreeRoutes(events: EventEmitter): Router {
|
||||||
@@ -97,6 +98,11 @@ export function createWorktreeRoutes(events: EventEmitter): Router {
|
|||||||
router.post('/init-script', validatePathParams('projectPath'), createGetInitScriptHandler());
|
router.post('/init-script', validatePathParams('projectPath'), createGetInitScriptHandler());
|
||||||
router.put('/init-script', validatePathParams('projectPath'), createPutInitScriptHandler());
|
router.put('/init-script', validatePathParams('projectPath'), createPutInitScriptHandler());
|
||||||
router.delete('/init-script', validatePathParams('projectPath'), createDeleteInitScriptHandler());
|
router.delete('/init-script', validatePathParams('projectPath'), createDeleteInitScriptHandler());
|
||||||
|
router.post(
|
||||||
|
'/run-init-script',
|
||||||
|
validatePathParams('projectPath', 'worktreePath'),
|
||||||
|
createRunInitScriptHandler(events)
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Init Script routes - Read/write the worktree-init.sh file
|
* Init Script routes - Read/write/run the worktree-init.sh file
|
||||||
*
|
*
|
||||||
* GET /init-script - Read the init script content
|
* POST /init-script - Read the init script content
|
||||||
* PUT /init-script - Write content to the init script file
|
* PUT /init-script - Write content to the init script file
|
||||||
* DELETE /init-script - Delete the init script file
|
* DELETE /init-script - Delete the init script file
|
||||||
|
* POST /run-init-script - Run the init script for a worktree
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
@@ -11,6 +12,8 @@ import path from 'path';
|
|||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import type { EventEmitter } from '../../../lib/events.js';
|
||||||
|
import { forceRunInitScript } from '../../../services/init-script-service.js';
|
||||||
|
|
||||||
const logger = createLogger('InitScript');
|
const logger = createLogger('InitScript');
|
||||||
|
|
||||||
@@ -160,3 +163,77 @@ export function createDeleteInitScriptHandler() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /run-init-script - Run (or re-run) the init script for a worktree
|
||||||
|
*/
|
||||||
|
export function createRunInitScriptHandler(events: EventEmitter) {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { projectPath, worktreePath, branch } = req.body as {
|
||||||
|
projectPath: string;
|
||||||
|
worktreePath: string;
|
||||||
|
branch: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'projectPath is required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!worktreePath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'worktreePath is required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branch) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'branch is required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptPath = getInitScriptPath(projectPath);
|
||||||
|
|
||||||
|
// Check if script exists
|
||||||
|
try {
|
||||||
|
await secureFs.access(scriptPath);
|
||||||
|
} catch {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'No init script found. Create one in Settings > Worktrees.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Running init script for branch "${branch}" (forced)`);
|
||||||
|
|
||||||
|
// Run the script asynchronously (non-blocking)
|
||||||
|
forceRunInitScript({
|
||||||
|
projectPath,
|
||||||
|
worktreePath,
|
||||||
|
branch,
|
||||||
|
emitter: events,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return immediately - progress will be streamed via WebSocket events
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Init script started',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Run init script failed');
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,65 +7,15 @@
|
|||||||
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
|
||||||
import { createLogger } from '@automaker/utils';
|
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 type { EventEmitter } from '../lib/events.js';
|
||||||
import {
|
import { readWorktreeMetadata, writeWorktreeMetadata } from '../lib/worktree-metadata.js';
|
||||||
readWorktreeMetadata,
|
import * as secureFs from '../lib/secure-fs.js';
|
||||||
writeWorktreeMetadata,
|
|
||||||
} from '../lib/worktree-metadata.js';
|
|
||||||
|
|
||||||
const logger = createLogger('InitScript');
|
const logger = createLogger('InitScript');
|
||||||
|
|
||||||
/** Common Git Bash installation paths on Windows */
|
|
||||||
const GIT_BASH_PATHS = [
|
|
||||||
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
||||||
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
||||||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'bin', 'bash.exe'),
|
|
||||||
path.join(process.env.USERPROFILE || '', 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find Git Bash executable on Windows
|
|
||||||
*/
|
|
||||||
function findGitBash(): string | null {
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const bashPath of GIT_BASH_PATHS) {
|
|
||||||
if (fs.existsSync(bashPath)) {
|
|
||||||
return bashPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the shell command for running scripts
|
|
||||||
* Returns [shellPath, shellArgs] for cross-platform compatibility
|
|
||||||
*/
|
|
||||||
function getShellCommand(): { shell: string; args: string[] } | null {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const gitBash = findGitBash();
|
|
||||||
if (!gitBash) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { shell: gitBash, args: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unix-like systems: prefer bash, fall back to sh
|
|
||||||
if (fs.existsSync('/bin/bash')) {
|
|
||||||
return { shell: '/bin/bash', args: [] };
|
|
||||||
}
|
|
||||||
if (fs.existsSync('/bin/sh')) {
|
|
||||||
return { shell: '/bin/sh', args: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitScriptOptions {
|
export interface InitScriptOptions {
|
||||||
/** Absolute path to the project root */
|
/** Absolute path to the project root */
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@@ -77,182 +27,307 @@ export interface InitScriptOptions {
|
|||||||
emitter: EventEmitter;
|
emitter: EventEmitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface ShellCommand {
|
||||||
* Check if init script exists for a project
|
shell: string;
|
||||||
*/
|
args: string[];
|
||||||
export function getInitScriptPath(projectPath: string): string {
|
|
||||||
return path.join(projectPath, '.automaker', 'worktree-init.sh');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the init script has already been run for a worktree
|
* Init Script Service
|
||||||
|
*
|
||||||
|
* Handles execution of worktree initialization scripts with cross-platform
|
||||||
|
* shell detection and proper streaming of output via WebSocket events.
|
||||||
*/
|
*/
|
||||||
export async function hasInitScriptRun(
|
export class InitScriptService {
|
||||||
projectPath: string,
|
private cachedShellCommand: ShellCommand | null | undefined = undefined;
|
||||||
branch: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
const metadata = await readWorktreeMetadata(projectPath, branch);
|
|
||||||
return metadata?.initScriptRan === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the worktree initialization script
|
* Get the path to the init script for a project
|
||||||
* Non-blocking - returns immediately after spawning
|
*/
|
||||||
*/
|
getInitScriptPath(projectPath: string): string {
|
||||||
export async function runInitScript(options: InitScriptOptions): Promise<void> {
|
return path.join(projectPath, '.automaker', 'worktree-init.sh');
|
||||||
const { projectPath, worktreePath, branch, emitter } = options;
|
|
||||||
|
|
||||||
const scriptPath = getInitScriptPath(projectPath);
|
|
||||||
|
|
||||||
// Check if script exists
|
|
||||||
if (!fs.existsSync(scriptPath)) {
|
|
||||||
logger.debug(`No init script found at ${scriptPath}`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already run
|
/**
|
||||||
if (await hasInitScriptRun(projectPath, branch)) {
|
* Check if the init script has already been run for a worktree
|
||||||
logger.info(`Init script already ran for branch "${branch}", skipping`);
|
*/
|
||||||
return;
|
async hasInitScriptRun(projectPath: string, branch: string): Promise<boolean> {
|
||||||
}
|
|
||||||
|
|
||||||
// Get shell command
|
|
||||||
const shellCmd = getShellCommand();
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Update metadata with error
|
|
||||||
await writeWorktreeMetadata(projectPath, branch, {
|
|
||||||
branch,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
// Update metadata to mark as running
|
|
||||||
const existingMetadata = await readWorktreeMetadata(projectPath, branch);
|
|
||||||
await writeWorktreeMetadata(projectPath, branch, {
|
|
||||||
branch,
|
|
||||||
createdAt: existingMetadata?.createdAt || new Date().toISOString(),
|
|
||||||
pr: existingMetadata?.pr,
|
|
||||||
initScriptRan: false,
|
|
||||||
initScriptStatus: 'running',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit started event
|
|
||||||
emitter.emit('worktree:init-started', {
|
|
||||||
projectPath,
|
|
||||||
worktreePath,
|
|
||||||
branch,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spawn the script
|
|
||||||
const child = spawn(shellCmd.shell, [...shellCmd.args, scriptPath], {
|
|
||||||
cwd: worktreePath,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
// Provide useful env vars to the script
|
|
||||||
AUTOMAKER_PROJECT_PATH: projectPath,
|
|
||||||
AUTOMAKER_WORKTREE_PATH: worktreePath,
|
|
||||||
AUTOMAKER_BRANCH: branch,
|
|
||||||
// Force color output even though we're not a TTY
|
|
||||||
FORCE_COLOR: '1',
|
|
||||||
npm_config_color: 'always',
|
|
||||||
CLICOLOR_FORCE: '1',
|
|
||||||
// Git colors
|
|
||||||
GIT_TERMINAL_PROMPT: '0',
|
|
||||||
},
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stream stdout
|
|
||||||
child.stdout?.on('data', (data: Buffer) => {
|
|
||||||
const content = data.toString();
|
|
||||||
emitter.emit('worktree:init-output', {
|
|
||||||
projectPath,
|
|
||||||
branch,
|
|
||||||
type: 'stdout',
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stream stderr
|
|
||||||
child.stderr?.on('data', (data: Buffer) => {
|
|
||||||
const content = data.toString();
|
|
||||||
emitter.emit('worktree:init-output', {
|
|
||||||
projectPath,
|
|
||||||
branch,
|
|
||||||
type: 'stderr',
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle completion
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
// Update metadata
|
|
||||||
const metadata = await readWorktreeMetadata(projectPath, branch);
|
const metadata = await readWorktreeMetadata(projectPath, branch);
|
||||||
|
return metadata?.initScriptRan === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the appropriate shell for running scripts
|
||||||
|
* Uses findGitBashPath() on Windows to avoid WSL bash, then falls back to PATH
|
||||||
|
*/
|
||||||
|
async findShellCommand(): Promise<ShellCommand | null> {
|
||||||
|
// Return cached result if available
|
||||||
|
if (this.cachedShellCommand !== undefined) {
|
||||||
|
return this.cachedShellCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// On Windows, prioritize Git Bash over WSL bash (C:\Windows\System32\bash.exe)
|
||||||
|
// WSL bash may not be properly configured and causes ENOENT errors
|
||||||
|
|
||||||
|
// First try known Git Bash installation paths
|
||||||
|
const gitBashPath = await findGitBashPath();
|
||||||
|
if (gitBashPath) {
|
||||||
|
logger.debug(`Found Git Bash at: ${gitBashPath}`);
|
||||||
|
this.cachedShellCommand = { shell: gitBashPath, args: [] };
|
||||||
|
return this.cachedShellCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to finding bash in PATH, but skip WSL bash
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix-like systems: use getShellPaths() and check existence
|
||||||
|
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 {
|
||||||
|
// Path not allowed or doesn't exist, continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ultimate fallback
|
||||||
|
if (systemPathExists('/bin/sh')) {
|
||||||
|
this.cachedShellCommand = { shell: '/bin/sh', args: [] };
|
||||||
|
return this.cachedShellCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedShellCommand = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the worktree initialization script
|
||||||
|
* Non-blocking - returns immediately after spawning
|
||||||
|
*/
|
||||||
|
async runInitScript(options: InitScriptOptions): Promise<void> {
|
||||||
|
const { projectPath, worktreePath, branch, emitter } = options;
|
||||||
|
|
||||||
|
const scriptPath = this.getInitScriptPath(projectPath);
|
||||||
|
|
||||||
|
// Check if script exists using secureFs (respects ALLOWED_ROOT_DIRECTORY)
|
||||||
|
try {
|
||||||
|
await secureFs.access(scriptPath);
|
||||||
|
} catch {
|
||||||
|
logger.debug(`No init script found at ${scriptPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already run
|
||||||
|
if (await this.hasInitScriptRun(projectPath, branch)) {
|
||||||
|
logger.info(`Init script already ran for branch "${branch}", skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get shell command
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Update metadata with error
|
||||||
|
await writeWorktreeMetadata(projectPath, branch, {
|
||||||
|
branch,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
// Update metadata to mark as running
|
||||||
|
const existingMetadata = await readWorktreeMetadata(projectPath, branch);
|
||||||
await writeWorktreeMetadata(projectPath, branch, {
|
await writeWorktreeMetadata(projectPath, branch, {
|
||||||
branch,
|
branch,
|
||||||
createdAt: metadata?.createdAt || new Date().toISOString(),
|
createdAt: existingMetadata?.createdAt || new Date().toISOString(),
|
||||||
pr: metadata?.pr,
|
pr: existingMetadata?.pr,
|
||||||
initScriptRan: true,
|
initScriptRan: false,
|
||||||
initScriptStatus: status,
|
initScriptStatus: 'running',
|
||||||
initScriptError: success ? undefined : `Exit code: ${code}`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit completion event
|
// Emit started event
|
||||||
emitter.emit('worktree:init-completed', {
|
emitter.emit('worktree:init-started', {
|
||||||
projectPath,
|
projectPath,
|
||||||
worktreePath,
|
worktreePath,
|
||||||
branch,
|
branch,
|
||||||
success,
|
|
||||||
exitCode: code,
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', async (error) => {
|
// Spawn the script
|
||||||
logger.error(`Init script error for branch "${branch}":`, error);
|
const child = spawn(shellCmd.shell, [...shellCmd.args, scriptPath], {
|
||||||
|
cwd: worktreePath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Provide useful env vars to the script
|
||||||
|
AUTOMAKER_PROJECT_PATH: projectPath,
|
||||||
|
AUTOMAKER_WORKTREE_PATH: worktreePath,
|
||||||
|
AUTOMAKER_BRANCH: branch,
|
||||||
|
// Force color output even though we're not a TTY
|
||||||
|
FORCE_COLOR: '1',
|
||||||
|
npm_config_color: 'always',
|
||||||
|
CLICOLOR_FORCE: '1',
|
||||||
|
// Git colors
|
||||||
|
GIT_TERMINAL_PROMPT: '0',
|
||||||
|
},
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
|
||||||
// Update metadata
|
// Stream stdout
|
||||||
|
child.stdout?.on('data', (data: Buffer) => {
|
||||||
|
const content = data.toString();
|
||||||
|
emitter.emit('worktree:init-output', {
|
||||||
|
projectPath,
|
||||||
|
branch,
|
||||||
|
type: 'stdout',
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stream stderr
|
||||||
|
child.stderr?.on('data', (data: Buffer) => {
|
||||||
|
const content = data.toString();
|
||||||
|
emitter.emit('worktree:init-output', {
|
||||||
|
projectPath,
|
||||||
|
branch,
|
||||||
|
type: 'stderr',
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle completion
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
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}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit completion event
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit completion with error
|
||||||
|
emitter.emit('worktree:init-completed', {
|
||||||
|
projectPath,
|
||||||
|
worktreePath,
|
||||||
|
branch,
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force re-run the worktree initialization script
|
||||||
|
* Ignores the initScriptRan flag - useful for testing or re-setup
|
||||||
|
*/
|
||||||
|
async forceRunInitScript(options: InitScriptOptions): Promise<void> {
|
||||||
|
const { projectPath, branch } = options;
|
||||||
|
|
||||||
|
// Reset the initScriptRan flag so the script will run
|
||||||
const metadata = await readWorktreeMetadata(projectPath, branch);
|
const metadata = await readWorktreeMetadata(projectPath, branch);
|
||||||
await writeWorktreeMetadata(projectPath, branch, {
|
if (metadata) {
|
||||||
branch,
|
await writeWorktreeMetadata(projectPath, branch, {
|
||||||
createdAt: metadata?.createdAt || new Date().toISOString(),
|
...metadata,
|
||||||
pr: metadata?.pr,
|
initScriptRan: false,
|
||||||
initScriptRan: true,
|
initScriptStatus: undefined,
|
||||||
initScriptStatus: 'failed',
|
initScriptError: undefined,
|
||||||
initScriptError: error.message,
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Emit completion with error
|
// Now run the script
|
||||||
emitter.emit('worktree:init-completed', {
|
await this.runInitScript(options);
|
||||||
projectPath,
|
}
|
||||||
worktreePath,
|
|
||||||
branch,
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton instance for convenience
|
||||||
|
let initScriptService: InitScriptService | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton InitScriptService instance
|
||||||
|
*/
|
||||||
|
export function getInitScriptService(): InitScriptService {
|
||||||
|
if (!initScriptService) {
|
||||||
|
initScriptService = new InitScriptService();
|
||||||
|
}
|
||||||
|
return initScriptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export convenience functions that use the singleton
|
||||||
|
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);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
GitMerge,
|
GitMerge,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
|
import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
|
||||||
@@ -50,6 +51,8 @@ interface WorktreeActionsDropdownProps {
|
|||||||
onStartDevServer: (worktree: WorktreeInfo) => void;
|
onStartDevServer: (worktree: WorktreeInfo) => void;
|
||||||
onStopDevServer: (worktree: WorktreeInfo) => void;
|
onStopDevServer: (worktree: WorktreeInfo) => void;
|
||||||
onOpenDevServerUrl: (worktree: WorktreeInfo) => void;
|
onOpenDevServerUrl: (worktree: WorktreeInfo) => void;
|
||||||
|
onRunInitScript: (worktree: WorktreeInfo) => void;
|
||||||
|
hasInitScript: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorktreeActionsDropdown({
|
export function WorktreeActionsDropdown({
|
||||||
@@ -76,6 +79,8 @@ export function WorktreeActionsDropdown({
|
|||||||
onStartDevServer,
|
onStartDevServer,
|
||||||
onStopDevServer,
|
onStopDevServer,
|
||||||
onOpenDevServerUrl,
|
onOpenDevServerUrl,
|
||||||
|
onRunInitScript,
|
||||||
|
hasInitScript,
|
||||||
}: WorktreeActionsDropdownProps) {
|
}: WorktreeActionsDropdownProps) {
|
||||||
// Check if there's a PR associated with this worktree from stored metadata
|
// Check if there's a PR associated with this worktree from stored metadata
|
||||||
const hasPR = !!worktree.pr;
|
const hasPR = !!worktree.pr;
|
||||||
@@ -204,6 +209,12 @@ export function WorktreeActionsDropdown({
|
|||||||
<ExternalLink className="w-3.5 h-3.5 mr-2" />
|
<ExternalLink className="w-3.5 h-3.5 mr-2" />
|
||||||
Open in {defaultEditorName}
|
Open in {defaultEditorName}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{!worktree.isMain && hasInitScript && (
|
||||||
|
<DropdownMenuItem onClick={() => onRunInitScript(worktree)} className="text-xs">
|
||||||
|
<RefreshCw className="w-3.5 h-3.5 mr-2" />
|
||||||
|
Re-run Init Script
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{worktree.hasChanges && (
|
{worktree.hasChanges && (
|
||||||
<TooltipWrapper
|
<TooltipWrapper
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface WorktreeTabProps {
|
|||||||
onStartDevServer: (worktree: WorktreeInfo) => void;
|
onStartDevServer: (worktree: WorktreeInfo) => void;
|
||||||
onStopDevServer: (worktree: WorktreeInfo) => void;
|
onStopDevServer: (worktree: WorktreeInfo) => void;
|
||||||
onOpenDevServerUrl: (worktree: WorktreeInfo) => void;
|
onOpenDevServerUrl: (worktree: WorktreeInfo) => void;
|
||||||
|
onRunInitScript: (worktree: WorktreeInfo) => void;
|
||||||
|
hasInitScript: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorktreeTab({
|
export function WorktreeTab({
|
||||||
@@ -87,6 +89,8 @@ export function WorktreeTab({
|
|||||||
onStartDevServer,
|
onStartDevServer,
|
||||||
onStopDevServer,
|
onStopDevServer,
|
||||||
onOpenDevServerUrl,
|
onOpenDevServerUrl,
|
||||||
|
onRunInitScript,
|
||||||
|
hasInitScript,
|
||||||
}: WorktreeTabProps) {
|
}: WorktreeTabProps) {
|
||||||
let prBadge: JSX.Element | null = null;
|
let prBadge: JSX.Element | null = null;
|
||||||
if (worktree.pr) {
|
if (worktree.pr) {
|
||||||
@@ -336,6 +340,8 @@ export function WorktreeTab({
|
|||||||
onStartDevServer={onStartDevServer}
|
onStartDevServer={onStartDevServer}
|
||||||
onStopDevServer={onStopDevServer}
|
onStopDevServer={onStopDevServer}
|
||||||
onOpenDevServerUrl={onOpenDevServerUrl}
|
onOpenDevServerUrl={onOpenDevServerUrl}
|
||||||
|
onRunInitScript={onRunInitScript}
|
||||||
|
hasInitScript={hasInitScript}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { GitBranch, Plus, RefreshCw } from 'lucide-react';
|
import { GitBranch, Plus, RefreshCw } from 'lucide-react';
|
||||||
import { cn, pathsEqual } from '@/lib/utils';
|
import { cn, pathsEqual } from '@/lib/utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
||||||
import {
|
import {
|
||||||
useWorktrees,
|
useWorktrees,
|
||||||
@@ -82,6 +84,28 @@ export function WorktreePanel({
|
|||||||
features,
|
features,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track whether init script exists for the project
|
||||||
|
const [hasInitScript, setHasInitScript] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectPath) {
|
||||||
|
setHasInitScript(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkInitScript = async () => {
|
||||||
|
try {
|
||||||
|
const api = getHttpApiClient();
|
||||||
|
const result = await api.worktree.getInitScript(projectPath);
|
||||||
|
setHasInitScript(result.success && result.exists);
|
||||||
|
} catch {
|
||||||
|
setHasInitScript(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkInitScript();
|
||||||
|
}, [projectPath]);
|
||||||
|
|
||||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
// Periodic interval check (5 seconds) to detect branch changes on disk
|
||||||
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -116,6 +140,33 @@ export function WorktreePanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRunInitScript = useCallback(
|
||||||
|
async (worktree: WorktreeInfo) => {
|
||||||
|
if (!projectPath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getHttpApiClient();
|
||||||
|
const result = await api.worktree.runInitScript(
|
||||||
|
projectPath,
|
||||||
|
worktree.path,
|
||||||
|
worktree.branch
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
toast.error('Failed to run init script', {
|
||||||
|
description: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Success feedback will come via WebSocket events (init-started, init-output, init-completed)
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to run init script', {
|
||||||
|
description: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[projectPath]
|
||||||
|
);
|
||||||
|
|
||||||
const mainWorktree = worktrees.find((w) => w.isMain);
|
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||||
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
|
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
|
||||||
|
|
||||||
@@ -166,6 +217,8 @@ export function WorktreePanel({
|
|||||||
onStartDevServer={handleStartDevServer}
|
onStartDevServer={handleStartDevServer}
|
||||||
onStopDevServer={handleStopDevServer}
|
onStopDevServer={handleStopDevServer}
|
||||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||||
|
onRunInitScript={handleRunInitScript}
|
||||||
|
hasInitScript={hasInitScript}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -221,6 +274,8 @@ export function WorktreePanel({
|
|||||||
onStartDevServer={handleStartDevServer}
|
onStartDevServer={handleStartDevServer}
|
||||||
onStopDevServer={handleStopDevServer}
|
onStopDevServer={handleStopDevServer}
|
||||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||||
|
onRunInitScript={handleRunInitScript}
|
||||||
|
hasInitScript={hasInitScript}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1716,6 +1716,47 @@ function createMockWorktreeAPI(): WorktreeAPI {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitScript: async (projectPath: string) => {
|
||||||
|
console.log('[Mock] Getting init script:', { projectPath });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
exists: false,
|
||||||
|
content: '',
|
||||||
|
path: `${projectPath}/.automaker/worktree-init.sh`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
setInitScript: async (projectPath: string, content: string) => {
|
||||||
|
console.log('[Mock] Setting init script:', { projectPath, content });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: `${projectPath}/.automaker/worktree-init.sh`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteInitScript: async (projectPath: string) => {
|
||||||
|
console.log('[Mock] Deleting init script:', { projectPath });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
runInitScript: async (projectPath: string, worktreePath: string, branch: string) => {
|
||||||
|
console.log('[Mock] Running init script:', { projectPath, worktreePath, branch });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Init script started (mock)',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onInitScriptEvent: (callback) => {
|
||||||
|
console.log('[Mock] Subscribing to init script events');
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
console.log('[Mock] Unsubscribing from init script events');
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -499,7 +499,10 @@ type EventType =
|
|||||||
| 'issue-validation:event'
|
| 'issue-validation:event'
|
||||||
| 'backlog-plan:event'
|
| 'backlog-plan:event'
|
||||||
| 'ideation:stream'
|
| 'ideation:stream'
|
||||||
| 'ideation:analysis';
|
| 'ideation:analysis'
|
||||||
|
| 'worktree:init-started'
|
||||||
|
| 'worktree:init-output'
|
||||||
|
| 'worktree:init-completed';
|
||||||
|
|
||||||
type EventCallback = (payload: unknown) => void;
|
type EventCallback = (payload: unknown) => void;
|
||||||
|
|
||||||
@@ -825,13 +828,14 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async httpDelete<T>(endpoint: string): Promise<T> {
|
private async httpDelete<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||||
// Ensure API key is initialized before making request
|
// Ensure API key is initialized before making request
|
||||||
await waitForApiKeyInit();
|
await waitForApiKeyInit();
|
||||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
credentials: 'include', // Include cookies for session auth
|
credentials: 'include', // Include cookies for session auth
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
@@ -1609,12 +1613,13 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
getPRInfo: (worktreePath: string, branchName: string) =>
|
getPRInfo: (worktreePath: string, branchName: string) =>
|
||||||
this.post('/api/worktree/pr-info', { worktreePath, branchName }),
|
this.post('/api/worktree/pr-info', { worktreePath, branchName }),
|
||||||
// Init script methods
|
// Init script methods
|
||||||
getInitScript: (projectPath: string) =>
|
getInitScript: (projectPath: string) => this.post('/api/worktree/init-script', { projectPath }),
|
||||||
this.post('/api/worktree/init-script', { projectPath }),
|
|
||||||
setInitScript: (projectPath: string, content: string) =>
|
setInitScript: (projectPath: string, content: string) =>
|
||||||
this.put('/api/worktree/init-script', { projectPath, content }),
|
this.put('/api/worktree/init-script', { projectPath, content }),
|
||||||
deleteInitScript: (projectPath: string) =>
|
deleteInitScript: (projectPath: string) =>
|
||||||
this.delete('/api/worktree/init-script', { projectPath }),
|
this.httpDelete('/api/worktree/init-script', { projectPath }),
|
||||||
|
runInitScript: (projectPath: string, worktreePath: string, branch: string) =>
|
||||||
|
this.post('/api/worktree/run-init-script', { projectPath, worktreePath, branch }),
|
||||||
onInitScriptEvent: (
|
onInitScriptEvent: (
|
||||||
callback: (event: {
|
callback: (event: {
|
||||||
type: 'worktree:init-started' | 'worktree:init-output' | 'worktree:init-completed';
|
type: 'worktree:init-started' | 'worktree:init-output' | 'worktree:init-completed';
|
||||||
|
|||||||
44
apps/ui/src/types/electron.d.ts
vendored
44
apps/ui/src/types/electron.d.ts
vendored
@@ -976,6 +976,50 @@ export interface WorktreeAPI {
|
|||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
// Get init script content for a project
|
||||||
|
getInitScript: (projectPath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
exists: boolean;
|
||||||
|
content: string;
|
||||||
|
path: string;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Set init script content for a project
|
||||||
|
setInitScript: (
|
||||||
|
projectPath: string,
|
||||||
|
content: string
|
||||||
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
path?: string;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Delete init script for a project
|
||||||
|
deleteInitScript: (projectPath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Run (or re-run) init script for a worktree
|
||||||
|
runInitScript: (
|
||||||
|
projectPath: string,
|
||||||
|
worktreePath: string,
|
||||||
|
branch: string
|
||||||
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Subscribe to init script events
|
||||||
|
onInitScriptEvent: (
|
||||||
|
callback: (event: {
|
||||||
|
type: 'worktree:init-started' | 'worktree:init-output' | 'worktree:init-completed';
|
||||||
|
payload: unknown;
|
||||||
|
}) => void
|
||||||
|
) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GitAPI {
|
export interface GitAPI {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export {
|
|||||||
getCodexCliPaths,
|
getCodexCliPaths,
|
||||||
getCodexConfigDir,
|
getCodexConfigDir,
|
||||||
getCodexAuthPath,
|
getCodexAuthPath,
|
||||||
|
getGitBashPaths,
|
||||||
getOpenCodeCliPaths,
|
getOpenCodeCliPaths,
|
||||||
getOpenCodeConfigDir,
|
getOpenCodeConfigDir,
|
||||||
getOpenCodeAuthPath,
|
getOpenCodeAuthPath,
|
||||||
@@ -129,6 +130,7 @@ export {
|
|||||||
findCodexCliPath,
|
findCodexCliPath,
|
||||||
getCodexAuthIndicators,
|
getCodexAuthIndicators,
|
||||||
type CodexAuthIndicators,
|
type CodexAuthIndicators,
|
||||||
|
findGitBashPath,
|
||||||
findOpenCodeCliPath,
|
findOpenCodeCliPath,
|
||||||
getOpenCodeAuthIndicators,
|
getOpenCodeAuthIndicators,
|
||||||
type OpenCodeAuthIndicators,
|
type OpenCodeAuthIndicators,
|
||||||
|
|||||||
@@ -232,6 +232,57 @@ export function getClaudeProjectsDir(): string {
|
|||||||
return path.join(getClaudeConfigDir(), 'projects');
|
return path.join(getClaudeConfigDir(), 'projects');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common Git Bash installation paths on Windows
|
||||||
|
* Git Bash is needed for running shell scripts cross-platform
|
||||||
|
*/
|
||||||
|
export function getGitBashPaths(): string[] {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
return [
|
||||||
|
// Standard Git for Windows installations
|
||||||
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
||||||
|
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
||||||
|
// User-local installations
|
||||||
|
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'bin', 'bash.exe'),
|
||||||
|
// Scoop package manager
|
||||||
|
path.join(homeDir, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
|
||||||
|
// Chocolatey
|
||||||
|
path.join(
|
||||||
|
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
||||||
|
'lib',
|
||||||
|
'git',
|
||||||
|
'tools',
|
||||||
|
'bin',
|
||||||
|
'bash.exe'
|
||||||
|
),
|
||||||
|
// winget typical location
|
||||||
|
path.join(
|
||||||
|
process.env.LOCALAPPDATA || '',
|
||||||
|
'Microsoft',
|
||||||
|
'WinGet',
|
||||||
|
'Packages',
|
||||||
|
'Git.Git_*',
|
||||||
|
'bin',
|
||||||
|
'bash.exe'
|
||||||
|
),
|
||||||
|
// GitHub Desktop bundled Git
|
||||||
|
path.join(
|
||||||
|
process.env.LOCALAPPDATA || '',
|
||||||
|
'GitHubDesktop',
|
||||||
|
'app-*',
|
||||||
|
'resources',
|
||||||
|
'app',
|
||||||
|
'git',
|
||||||
|
'cmd',
|
||||||
|
'bash.exe'
|
||||||
|
),
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get common shell paths for shell detection
|
* Get common shell paths for shell detection
|
||||||
* Includes both full paths and short names to match $SHELL or PATH entries
|
* Includes both full paths and short names to match $SHELL or PATH entries
|
||||||
@@ -550,6 +601,8 @@ function getAllAllowedSystemPaths(): string[] {
|
|||||||
getOpenCodeAuthPath(),
|
getOpenCodeAuthPath(),
|
||||||
// Shell paths
|
// Shell paths
|
||||||
...getShellPaths(),
|
...getShellPaths(),
|
||||||
|
// Git Bash paths (for Windows cross-platform shell script execution)
|
||||||
|
...getGitBashPaths(),
|
||||||
// Node.js system paths
|
// Node.js system paths
|
||||||
...getNodeSystemPaths(),
|
...getNodeSystemPaths(),
|
||||||
getScoopNodePath(),
|
getScoopNodePath(),
|
||||||
@@ -883,6 +936,13 @@ export async function findCodexCliPath(): Promise<string | null> {
|
|||||||
return findFirstExistingPath(getCodexCliPaths());
|
return findFirstExistingPath(getCodexCliPaths());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find Git Bash on Windows and return its path
|
||||||
|
*/
|
||||||
|
export async function findGitBashPath(): Promise<string | null> {
|
||||||
|
return findFirstExistingPath(getGitBashPaths());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Claude authentication status by checking various indicators
|
* Get Claude authentication status by checking various indicators
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user