mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
Merge branch 'main' into fix/electron-node-path-finder-launch-v2
This commit is contained in:
@@ -9,8 +9,8 @@
|
||||
* Directory creation is handled separately by ensure* functions.
|
||||
*/
|
||||
|
||||
import * as secureFs from "./secure-fs.js";
|
||||
import path from "path";
|
||||
import * as secureFs from './secure-fs.js';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Get the automaker data directory root for a project
|
||||
@@ -22,7 +22,7 @@ import path from "path";
|
||||
* @returns Absolute path to {projectPath}/.automaker
|
||||
*/
|
||||
export function getAutomakerDir(projectPath: string): string {
|
||||
return path.join(projectPath, ".automaker");
|
||||
return path.join(projectPath, '.automaker');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ export function getAutomakerDir(projectPath: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/features
|
||||
*/
|
||||
export function getFeaturesDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "features");
|
||||
return path.join(getAutomakerDir(projectPath), 'features');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,11 +59,8 @@ export function getFeatureDir(projectPath: string, featureId: string): string {
|
||||
* @param featureId - Feature identifier
|
||||
* @returns Absolute path to {projectPath}/.automaker/features/{featureId}/images
|
||||
*/
|
||||
export function getFeatureImagesDir(
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
): string {
|
||||
return path.join(getFeatureDir(projectPath, featureId), "images");
|
||||
export function getFeatureImagesDir(projectPath: string, featureId: string): string {
|
||||
return path.join(getFeatureDir(projectPath, featureId), 'images');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +72,7 @@ export function getFeatureImagesDir(
|
||||
* @returns Absolute path to {projectPath}/.automaker/board
|
||||
*/
|
||||
export function getBoardDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "board");
|
||||
return path.join(getAutomakerDir(projectPath), 'board');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +84,7 @@ export function getBoardDir(projectPath: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/images
|
||||
*/
|
||||
export function getImagesDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "images");
|
||||
return path.join(getAutomakerDir(projectPath), 'images');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +96,7 @@ export function getImagesDir(projectPath: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/context
|
||||
*/
|
||||
export function getContextDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "context");
|
||||
return path.join(getAutomakerDir(projectPath), 'context');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +108,7 @@ export function getContextDir(projectPath: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/worktrees
|
||||
*/
|
||||
export function getWorktreesDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "worktrees");
|
||||
return path.join(getAutomakerDir(projectPath), 'worktrees');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +120,7 @@ export function getWorktreesDir(projectPath: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/app_spec.txt
|
||||
*/
|
||||
export function getAppSpecPath(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "app_spec.txt");
|
||||
return path.join(getAutomakerDir(projectPath), 'app_spec.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +132,7 @@ export function getAppSpecPath(projectPath: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/active-branches.json
|
||||
*/
|
||||
export function getBranchTrackingPath(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "active-branches.json");
|
||||
return path.join(getAutomakerDir(projectPath), 'active-branches.json');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +169,7 @@ export async function ensureAutomakerDir(projectPath: string): Promise<string> {
|
||||
* @returns Absolute path to {dataDir}/settings.json
|
||||
*/
|
||||
export function getGlobalSettingsPath(dataDir: string): string {
|
||||
return path.join(dataDir, "settings.json");
|
||||
return path.join(dataDir, 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +182,7 @@ export function getGlobalSettingsPath(dataDir: string): string {
|
||||
* @returns Absolute path to {dataDir}/credentials.json
|
||||
*/
|
||||
export function getCredentialsPath(dataDir: string): string {
|
||||
return path.join(dataDir, "credentials.json");
|
||||
return path.join(dataDir, 'credentials.json');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +195,7 @@ export function getCredentialsPath(dataDir: string): string {
|
||||
* @returns Absolute path to {projectPath}/.automaker/settings.json
|
||||
*/
|
||||
export function getProjectSettingsPath(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "settings.json");
|
||||
return path.join(getAutomakerDir(projectPath), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* not just at the API layer. This provides defense-in-depth security.
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import type { Dirent } from "fs";
|
||||
import path from "path";
|
||||
import { validatePath } from "./security.js";
|
||||
import fs from 'fs/promises';
|
||||
import type { Dirent } from 'fs';
|
||||
import path from 'path';
|
||||
import { validatePath } from './security.js';
|
||||
|
||||
/**
|
||||
* Wrapper around fs.access that validates path first
|
||||
@@ -108,11 +108,7 @@ export async function unlink(filePath: string): Promise<void> {
|
||||
/**
|
||||
* Wrapper around fs.copyFile that validates both paths first
|
||||
*/
|
||||
export async function copyFile(
|
||||
src: string,
|
||||
dest: string,
|
||||
mode?: number
|
||||
): Promise<void> {
|
||||
export async function copyFile(src: string, dest: string, mode?: number): Promise<void> {
|
||||
const validatedSrc = validatePath(src);
|
||||
const validatedDest = validatePath(dest);
|
||||
return fs.copyFile(validatedSrc, validatedDest, mode);
|
||||
@@ -133,10 +129,7 @@ export async function appendFile(
|
||||
/**
|
||||
* Wrapper around fs.rename that validates both paths first
|
||||
*/
|
||||
export async function rename(
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
): Promise<void> {
|
||||
export async function rename(oldPath: string, newPath: string): Promise<void> {
|
||||
const validatedOldPath = validatePath(oldPath);
|
||||
const validatedNewPath = validatePath(newPath);
|
||||
return fs.rename(validatedOldPath, validatedNewPath);
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
* Enforces ALLOWED_ROOT_DIRECTORY constraint with appData exception
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Error thrown when a path is not allowed by security policy
|
||||
*/
|
||||
export class PathNotAllowedError extends Error {
|
||||
constructor(filePath: string) {
|
||||
super(
|
||||
`Path not allowed: ${filePath}. Must be within ALLOWED_ROOT_DIRECTORY or DATA_DIR.`
|
||||
);
|
||||
this.name = "PathNotAllowedError";
|
||||
super(`Path not allowed: ${filePath}. Must be within ALLOWED_ROOT_DIRECTORY or DATA_DIR.`);
|
||||
this.name = 'PathNotAllowedError';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,13 +31,9 @@ export function initAllowedPaths(): void {
|
||||
const rootDir = process.env.ALLOWED_ROOT_DIRECTORY;
|
||||
if (rootDir) {
|
||||
allowedRootDirectory = path.resolve(rootDir);
|
||||
console.log(
|
||||
`[Security] ✓ ALLOWED_ROOT_DIRECTORY configured: ${allowedRootDirectory}`
|
||||
);
|
||||
console.log(`[Security] ✓ ALLOWED_ROOT_DIRECTORY configured: ${allowedRootDirectory}`);
|
||||
} else {
|
||||
console.log(
|
||||
"[Security] ⚠️ ALLOWED_ROOT_DIRECTORY not set - allowing access to all paths"
|
||||
);
|
||||
console.log('[Security] ⚠️ ALLOWED_ROOT_DIRECTORY not set - allowing access to all paths');
|
||||
}
|
||||
|
||||
// Load DATA_DIR (appData exception - always allowed)
|
||||
@@ -72,10 +66,7 @@ export function isPathAllowed(filePath: string): boolean {
|
||||
}
|
||||
|
||||
// Allow if within ALLOWED_ROOT_DIRECTORY
|
||||
if (
|
||||
allowedRootDirectory &&
|
||||
isPathWithinDirectory(resolvedPath, allowedRootDirectory)
|
||||
) {
|
||||
if (allowedRootDirectory && isPathWithinDirectory(resolvedPath, allowedRootDirectory)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -101,17 +92,14 @@ export function validatePath(filePath: string): string {
|
||||
* Check if a path is within a directory, with protection against path traversal
|
||||
* Returns true only if resolvedPath is within directoryPath
|
||||
*/
|
||||
export function isPathWithinDirectory(
|
||||
resolvedPath: string,
|
||||
directoryPath: string
|
||||
): boolean {
|
||||
export function isPathWithinDirectory(resolvedPath: string, directoryPath: string): boolean {
|
||||
// Get the relative path from directory to the target
|
||||
const relativePath = path.relative(directoryPath, resolvedPath);
|
||||
|
||||
// If relative path starts with "..", it's outside the directory
|
||||
// If relative path is absolute, it's outside the directory
|
||||
// If relative path is empty or ".", it's the directory itself
|
||||
return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
||||
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Subprocess management utilities for CLI providers
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "child_process";
|
||||
import readline from "readline";
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import readline from 'readline';
|
||||
|
||||
export interface SubprocessOptions {
|
||||
command: string;
|
||||
@@ -23,9 +23,7 @@ export interface SubprocessResult {
|
||||
/**
|
||||
* Spawns a subprocess and streams JSONL output line-by-line
|
||||
*/
|
||||
export async function* spawnJSONLProcess(
|
||||
options: SubprocessOptions
|
||||
): AsyncGenerator<unknown> {
|
||||
export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGenerator<unknown> {
|
||||
const { command, args, cwd, env, abortController, timeout = 30000 } = options;
|
||||
|
||||
const processEnv = {
|
||||
@@ -33,22 +31,22 @@ export async function* spawnJSONLProcess(
|
||||
...env,
|
||||
};
|
||||
|
||||
console.log(`[SubprocessManager] Spawning: ${command} ${args.slice(0, -1).join(" ")}`);
|
||||
console.log(`[SubprocessManager] Spawning: ${command} ${args.slice(0, -1).join(' ')}`);
|
||||
console.log(`[SubprocessManager] Working directory: ${cwd}`);
|
||||
|
||||
const childProcess: ChildProcess = spawn(command, args, {
|
||||
cwd,
|
||||
env: processEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stderrOutput = "";
|
||||
let stderrOutput = '';
|
||||
let lastOutputTime = Date.now();
|
||||
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||
|
||||
// Collect stderr for error reporting
|
||||
if (childProcess.stderr) {
|
||||
childProcess.stderr.on("data", (data: Buffer) => {
|
||||
childProcess.stderr.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stderrOutput += text;
|
||||
console.error(`[SubprocessManager] stderr: ${text}`);
|
||||
@@ -64,10 +62,8 @@ export async function* spawnJSONLProcess(
|
||||
timeoutHandle = setTimeout(() => {
|
||||
const elapsed = Date.now() - lastOutputTime;
|
||||
if (elapsed >= timeout) {
|
||||
console.error(
|
||||
`[SubprocessManager] Process timeout: no output for ${timeout}ms`
|
||||
);
|
||||
childProcess.kill("SIGTERM");
|
||||
console.error(`[SubprocessManager] Process timeout: no output for ${timeout}ms`);
|
||||
childProcess.kill('SIGTERM');
|
||||
}
|
||||
}, timeout);
|
||||
};
|
||||
@@ -76,12 +72,12 @@ export async function* spawnJSONLProcess(
|
||||
|
||||
// Setup abort handling
|
||||
if (abortController) {
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
console.log("[SubprocessManager] Abort signal received, killing process");
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
console.log('[SubprocessManager] Abort signal received, killing process');
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
childProcess.kill("SIGTERM");
|
||||
childProcess.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,19 +98,16 @@ export async function* spawnJSONLProcess(
|
||||
const parsed = JSON.parse(line);
|
||||
yield parsed;
|
||||
} catch (parseError) {
|
||||
console.error(
|
||||
`[SubprocessManager] Failed to parse JSONL line: ${line}`,
|
||||
parseError
|
||||
);
|
||||
console.error(`[SubprocessManager] Failed to parse JSONL line: ${line}`, parseError);
|
||||
// Yield error but continue processing
|
||||
yield {
|
||||
type: "error",
|
||||
type: 'error',
|
||||
error: `Failed to parse output: ${line}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[SubprocessManager] Error reading stdout:", error);
|
||||
console.error('[SubprocessManager] Error reading stdout:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeoutHandle) {
|
||||
@@ -125,13 +118,13 @@ export async function* spawnJSONLProcess(
|
||||
|
||||
// Wait for process to exit
|
||||
const exitCode = await new Promise<number | null>((resolve) => {
|
||||
childProcess.on("exit", (code) => {
|
||||
childProcess.on('exit', (code) => {
|
||||
console.log(`[SubprocessManager] Process exited with code: ${code}`);
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
childProcess.on("error", (error) => {
|
||||
console.error("[SubprocessManager] Process error:", error);
|
||||
childProcess.on('error', (error) => {
|
||||
console.error('[SubprocessManager] Process error:', error);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
@@ -141,23 +134,21 @@ export async function* spawnJSONLProcess(
|
||||
const errorMessage = stderrOutput || `Process exited with code ${exitCode}`;
|
||||
console.error(`[SubprocessManager] Process failed: ${errorMessage}`);
|
||||
yield {
|
||||
type: "error",
|
||||
type: 'error',
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Process completed successfully
|
||||
if (exitCode === 0 && !stderrOutput) {
|
||||
console.log("[SubprocessManager] Process completed successfully");
|
||||
console.log('[SubprocessManager] Process completed successfully');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a subprocess and collects all output
|
||||
*/
|
||||
export async function spawnProcess(
|
||||
options: SubprocessOptions
|
||||
): Promise<SubprocessResult> {
|
||||
export async function spawnProcess(options: SubprocessOptions): Promise<SubprocessResult> {
|
||||
const { command, args, cwd, env, abortController } = options;
|
||||
|
||||
const processEnv = {
|
||||
@@ -169,37 +160,37 @@ export async function spawnProcess(
|
||||
const childProcess = spawn(command, args, {
|
||||
cwd,
|
||||
env: processEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (childProcess.stdout) {
|
||||
childProcess.stdout.on("data", (data: Buffer) => {
|
||||
childProcess.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (childProcess.stderr) {
|
||||
childProcess.stderr.on("data", (data: Buffer) => {
|
||||
childProcess.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup abort handling
|
||||
if (abortController) {
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
childProcess.kill("SIGTERM");
|
||||
reject(new Error("Process aborted"));
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
childProcess.kill('SIGTERM');
|
||||
reject(new Error('Process aborted'));
|
||||
});
|
||||
}
|
||||
|
||||
childProcess.on("exit", (code) => {
|
||||
childProcess.on('exit', (code) => {
|
||||
resolve({ stdout, stderr, exitCode: code });
|
||||
});
|
||||
|
||||
childProcess.on("error", (error) => {
|
||||
childProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user