mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
This commit updates various modules to utilize the secure file system operations from the secureFs module instead of the native fs module. Key changes include: - Replaced fs imports with secureFs in multiple route handlers and services to enhance security and consistency in file operations. - Added centralized validation for working directories in the sdk-options module to ensure all AI model invocations are secure. These changes aim to improve the security and maintainability of file handling across the application.
205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
/**
|
|
* POST /clone endpoint - Clone a GitHub template to a new project directory
|
|
*/
|
|
|
|
import type { Request, Response } from 'express';
|
|
import { spawn } from 'child_process';
|
|
import path from 'path';
|
|
import * as secureFs from '../../../lib/secure-fs.js';
|
|
import { PathNotAllowedError } from '@automaker/platform';
|
|
import { logger, getErrorMessage, logError } from '../common.js';
|
|
|
|
export function createCloneHandler() {
|
|
return async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const { repoUrl, projectName, parentDir } = req.body as {
|
|
repoUrl: string;
|
|
projectName: string;
|
|
parentDir: string;
|
|
};
|
|
|
|
// Validate inputs
|
|
if (!repoUrl || !projectName || !parentDir) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'repoUrl, projectName, and parentDir are required',
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info(
|
|
`[Templates] Clone request - Repo: ${repoUrl}, Project: ${projectName}, Parent: ${parentDir}`
|
|
);
|
|
|
|
// Validate repo URL is a valid GitHub URL
|
|
const githubUrlPattern = /^https:\/\/github\.com\/[\w-]+\/[\w.-]+$/;
|
|
if (!githubUrlPattern.test(repoUrl)) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Invalid GitHub repository URL',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Sanitize project name (allow alphanumeric, dash, underscore)
|
|
const sanitizedName = projectName.replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
if (sanitizedName !== projectName) {
|
|
logger.info(`[Templates] Sanitized project name: ${projectName} -> ${sanitizedName}`);
|
|
}
|
|
|
|
// Build full project path
|
|
const projectPath = path.join(parentDir, sanitizedName);
|
|
|
|
const resolvedParent = path.resolve(parentDir);
|
|
const resolvedProject = path.resolve(projectPath);
|
|
const relativePath = path.relative(resolvedParent, resolvedProject);
|
|
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Invalid project name; potential path traversal attempt.',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Check if directory already exists (secureFs.access also validates path is allowed)
|
|
try {
|
|
await secureFs.access(projectPath);
|
|
res.status(400).json({
|
|
success: false,
|
|
error: `Directory "${sanitizedName}" already exists in ${parentDir}`,
|
|
});
|
|
return;
|
|
} catch (accessError) {
|
|
if (accessError instanceof PathNotAllowedError) {
|
|
res.status(403).json({
|
|
success: false,
|
|
error: `Project path not allowed: ${projectPath}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
|
});
|
|
return;
|
|
}
|
|
// Directory doesn't exist, which is what we want
|
|
}
|
|
|
|
// Ensure parent directory exists
|
|
try {
|
|
// Check if parentDir is a root path (Windows: C:\, D:\, etc. or Unix: /)
|
|
const isWindowsRoot = /^[A-Za-z]:\\?$/.test(parentDir);
|
|
const isUnixRoot = parentDir === '/' || parentDir === '';
|
|
const isRoot = isWindowsRoot || isUnixRoot;
|
|
|
|
if (isRoot) {
|
|
// Root paths always exist, just verify access
|
|
logger.info(`[Templates] Using root path: ${parentDir}`);
|
|
await secureFs.access(parentDir);
|
|
} else {
|
|
// Check if parent directory exists
|
|
let parentExists = false;
|
|
try {
|
|
await secureFs.access(parentDir);
|
|
parentExists = true;
|
|
} catch {
|
|
parentExists = false;
|
|
}
|
|
|
|
if (!parentExists) {
|
|
logger.info(`[Templates] Creating parent directory: ${parentDir}`);
|
|
await secureFs.mkdir(parentDir, { recursive: true });
|
|
} else {
|
|
logger.info(`[Templates] Parent directory exists: ${parentDir}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.error('[Templates] Failed to access parent directory:', parentDir, error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: `Failed to access parent directory: ${errorMessage}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info(`[Templates] Cloning ${repoUrl} to ${projectPath}`);
|
|
|
|
// Clone the repository
|
|
const cloneResult = await new Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}>((resolve) => {
|
|
const gitProcess = spawn('git', ['clone', repoUrl, projectPath], {
|
|
cwd: parentDir,
|
|
});
|
|
|
|
let stderr = '';
|
|
|
|
gitProcess.stderr.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
gitProcess.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve({ success: true });
|
|
} else {
|
|
resolve({
|
|
success: false,
|
|
error: stderr || `Git clone failed with code ${code}`,
|
|
});
|
|
}
|
|
});
|
|
|
|
gitProcess.on('error', (error) => {
|
|
resolve({
|
|
success: false,
|
|
error: `Failed to spawn git: ${error.message}`,
|
|
});
|
|
});
|
|
});
|
|
|
|
if (!cloneResult.success) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: cloneResult.error || 'Failed to clone repository',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Remove .git directory to start fresh
|
|
try {
|
|
const gitDir = path.join(projectPath, '.git');
|
|
await secureFs.rm(gitDir, { recursive: true, force: true });
|
|
logger.info('[Templates] Removed .git directory');
|
|
} catch (error) {
|
|
logger.warn('[Templates] Could not remove .git directory:', error);
|
|
// Continue anyway - not critical
|
|
}
|
|
|
|
// Initialize a fresh git repository
|
|
await new Promise<void>((resolve) => {
|
|
const gitInit = spawn('git', ['init'], {
|
|
cwd: projectPath,
|
|
});
|
|
|
|
gitInit.on('close', () => {
|
|
logger.info('[Templates] Initialized fresh git repository');
|
|
resolve();
|
|
});
|
|
|
|
gitInit.on('error', () => {
|
|
logger.warn('[Templates] Could not initialize git');
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
logger.info(`[Templates] Successfully cloned template to ${projectPath}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
projectPath,
|
|
projectName: sanitizedName,
|
|
});
|
|
} catch (error) {
|
|
logError(error, 'Clone template failed');
|
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
|
}
|
|
};
|
|
}
|