mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: Implement worktree initialization script functionality
This commit introduces a new feature for managing worktree initialization scripts, allowing users to configure and execute scripts upon worktree creation. Key changes include: 1. **New API Endpoints**: Added endpoints for getting, setting, and deleting init scripts. 2. **Worktree Routes**: Updated worktree routes to include init script handling. 3. **Init Script Service**: Created a service to execute the init scripts asynchronously, with support for cross-platform compatibility. 4. **UI Components**: Added UI components for displaying and editing init scripts, including a dedicated section in the settings view. 5. **Event Handling**: Implemented event handling for init script execution status, providing real-time feedback in the UI. This enhancement improves the user experience by allowing automated setup processes for new worktrees, streamlining project workflows.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import type { EventEmitter } from '../../lib/events.js';
|
||||
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||
import { requireValidWorktree, requireValidProject, requireGitRepoOnly } from './middleware.js';
|
||||
import { createInfoHandler } from './routes/info.js';
|
||||
@@ -30,8 +31,13 @@ import { createMigrateHandler } from './routes/migrate.js';
|
||||
import { createStartDevHandler } from './routes/start-dev.js';
|
||||
import { createStopDevHandler } from './routes/stop-dev.js';
|
||||
import { createListDevServersHandler } from './routes/list-dev-servers.js';
|
||||
import {
|
||||
createGetInitScriptHandler,
|
||||
createPutInitScriptHandler,
|
||||
createDeleteInitScriptHandler,
|
||||
} from './routes/init-script.js';
|
||||
|
||||
export function createWorktreeRoutes(): Router {
|
||||
export function createWorktreeRoutes(events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post('/info', validatePathParams('projectPath'), createInfoHandler());
|
||||
@@ -45,7 +51,7 @@ export function createWorktreeRoutes(): Router {
|
||||
requireValidProject,
|
||||
createMergeHandler()
|
||||
);
|
||||
router.post('/create', validatePathParams('projectPath'), createCreateHandler());
|
||||
router.post('/create', validatePathParams('projectPath'), createCreateHandler(events));
|
||||
router.post('/delete', validatePathParams('projectPath', 'worktreePath'), createDeleteHandler());
|
||||
router.post('/create-pr', createCreatePRHandler());
|
||||
router.post('/pr-info', createPRInfoHandler());
|
||||
@@ -87,5 +93,10 @@ export function createWorktreeRoutes(): Router {
|
||||
router.post('/stop-dev', createStopDevHandler());
|
||||
router.post('/list-dev-servers', createListDevServersHandler());
|
||||
|
||||
// Init script routes
|
||||
router.post('/init-script', validatePathParams('projectPath'), createGetInitScriptHandler());
|
||||
router.put('/init-script', validatePathParams('projectPath'), createPutInitScriptHandler());
|
||||
router.delete('/init-script', validatePathParams('projectPath'), createDeleteInitScriptHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
import {
|
||||
isGitRepo,
|
||||
getErrorMessage,
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
} from '../common.js';
|
||||
import { trackBranch } from './branch-tracking.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { runInitScript, getInitScriptPath, hasInitScriptRun } from '../../../services/init-script-service.js';
|
||||
import fs from 'fs';
|
||||
|
||||
const logger = createLogger('Worktree');
|
||||
|
||||
@@ -77,7 +80,7 @@ async function findExistingWorktreeForBranch(
|
||||
}
|
||||
}
|
||||
|
||||
export function createCreateHandler() {
|
||||
export function createCreateHandler(events: EventEmitter) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, branchName, baseBranch } = req.body as {
|
||||
@@ -177,6 +180,13 @@ export function createCreateHandler() {
|
||||
// Resolve to absolute path for cross-platform compatibility
|
||||
// normalizePath converts to forward slashes for API consistency
|
||||
const absoluteWorktreePath = path.resolve(worktreePath);
|
||||
|
||||
// Check if init script exists and should be run (only for new worktrees)
|
||||
const initScriptPath = getInitScriptPath(projectPath);
|
||||
const hasInitScript = fs.existsSync(initScriptPath);
|
||||
const alreadyRan = await hasInitScriptRun(projectPath, branchName);
|
||||
|
||||
// Respond immediately (non-blocking)
|
||||
res.json({
|
||||
success: true,
|
||||
worktree: {
|
||||
@@ -185,6 +195,19 @@ export function createCreateHandler() {
|
||||
isNew: !branchExists,
|
||||
},
|
||||
});
|
||||
|
||||
// Trigger init script asynchronously after response
|
||||
if (hasInitScript && !alreadyRan) {
|
||||
logger.info(`Triggering init script for worktree: ${branchName}`);
|
||||
runInitScript({
|
||||
projectPath,
|
||||
worktreePath: absoluteWorktreePath,
|
||||
branch: branchName,
|
||||
emitter: events,
|
||||
}).catch((err) => {
|
||||
logger.error(`Init script failed for ${branchName}:`, err);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, 'Create worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
|
||||
162
apps/server/src/routes/worktree/routes/init-script.ts
Normal file
162
apps/server/src/routes/worktree/routes/init-script.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Init Script routes - Read/write the worktree-init.sh file
|
||||
*
|
||||
* GET /init-script - Read the init script content
|
||||
* PUT /init-script - Write content to the init script file
|
||||
* DELETE /init-script - Delete the init script file
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('InitScript');
|
||||
|
||||
/** Fixed path for init script within .automaker directory */
|
||||
const INIT_SCRIPT_FILENAME = 'worktree-init.sh';
|
||||
|
||||
/**
|
||||
* Get the full path to the init script for a project
|
||||
*/
|
||||
function getInitScriptPath(projectPath: string): string {
|
||||
return path.join(projectPath, '.automaker', INIT_SCRIPT_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /init-script - Read the init script content
|
||||
*/
|
||||
export function createGetInitScriptHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptPath = getInitScriptPath(projectPath);
|
||||
|
||||
try {
|
||||
const content = await secureFs.readFile(scriptPath, 'utf-8');
|
||||
res.json({
|
||||
success: true,
|
||||
exists: true,
|
||||
content: content as string,
|
||||
path: scriptPath,
|
||||
});
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
res.json({
|
||||
success: true,
|
||||
exists: false,
|
||||
content: '',
|
||||
path: scriptPath,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, 'Read init script failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /init-script - Write content to the init script file
|
||||
*/
|
||||
export function createPutInitScriptHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, content } = req.body as {
|
||||
projectPath: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content !== 'string') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'content must be a string',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptPath = getInitScriptPath(projectPath);
|
||||
const automakerDir = path.dirname(scriptPath);
|
||||
|
||||
// Ensure .automaker directory exists
|
||||
await secureFs.mkdir(automakerDir, { recursive: true });
|
||||
|
||||
// Write the script content
|
||||
await secureFs.writeFile(scriptPath, content, 'utf-8');
|
||||
|
||||
logger.info(`Wrote init script to ${scriptPath}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
path: scriptPath,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Write init script failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /init-script - Delete the init script file
|
||||
*/
|
||||
export function createDeleteInitScriptHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptPath = getInitScriptPath(projectPath);
|
||||
|
||||
try {
|
||||
await secureFs.rm(scriptPath, { force: true });
|
||||
logger.info(`Deleted init script at ${scriptPath}`);
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
} catch {
|
||||
// File doesn't exist - still success
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, 'Delete init script failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user