mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: add @automaker/platform package
- Extract automaker path utilities (getFeatureDir, etc.) - Extract subprocess management (spawnJSONLProcess, spawnProcess) - Extract security/path validation utilities Provides platform-specific utilities for: - Managing .automaker directory structure - Spawning and managing child processes - Path validation and security Dependencies: @automaker/types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
21
libs/platform/package.json
Normal file
21
libs/platform/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@automaker/platform",
|
||||
"version": "1.0.0",
|
||||
"description": "Platform-specific utilities for AutoMaker",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"keywords": ["automaker", "platform"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@automaker/types": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
36
libs/platform/src/index.ts
Normal file
36
libs/platform/src/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @automaker/platform
|
||||
* Platform-specific utilities for AutoMaker
|
||||
*/
|
||||
|
||||
// Path utilities
|
||||
export {
|
||||
getAutomakerDir,
|
||||
getFeaturesDir,
|
||||
getFeatureDir,
|
||||
getFeatureImagesDir,
|
||||
getBoardDir,
|
||||
getImagesDir,
|
||||
getContextDir,
|
||||
getWorktreesDir,
|
||||
getAppSpecPath,
|
||||
getBranchTrackingPath,
|
||||
ensureAutomakerDir,
|
||||
} from './paths';
|
||||
|
||||
// Subprocess management
|
||||
export {
|
||||
spawnJSONLProcess,
|
||||
spawnProcess,
|
||||
type SubprocessOptions,
|
||||
type SubprocessResult,
|
||||
} from './subprocess';
|
||||
|
||||
// Security
|
||||
export {
|
||||
initAllowedPaths,
|
||||
addAllowedPath,
|
||||
isPathAllowed,
|
||||
validatePath,
|
||||
getAllowedPaths,
|
||||
} from './security';
|
||||
91
libs/platform/src/paths.ts
Normal file
91
libs/platform/src/paths.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Automaker Paths - Utilities for managing automaker data storage
|
||||
*
|
||||
* Stores project data inside the project directory at {projectPath}/.automaker/
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Get the automaker data directory for a project
|
||||
* This is stored inside the project at .automaker/
|
||||
*/
|
||||
export function getAutomakerDir(projectPath: string): string {
|
||||
return path.join(projectPath, ".automaker");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the features directory for a project
|
||||
*/
|
||||
export function getFeaturesDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "features");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for a specific feature
|
||||
*/
|
||||
export function getFeatureDir(projectPath: string, featureId: string): string {
|
||||
return path.join(getFeaturesDir(projectPath), featureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the images directory for a feature
|
||||
*/
|
||||
export function getFeatureImagesDir(
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
): string {
|
||||
return path.join(getFeatureDir(projectPath, featureId), "images");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the board directory for a project (board backgrounds, etc.)
|
||||
*/
|
||||
export function getBoardDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "board");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the images directory for a project (general images)
|
||||
*/
|
||||
export function getImagesDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "images");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context files directory for a project (user-added context files)
|
||||
*/
|
||||
export function getContextDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "context");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the worktrees metadata directory for a project
|
||||
*/
|
||||
export function getWorktreesDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "worktrees");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the app spec file path for a project
|
||||
*/
|
||||
export function getAppSpecPath(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "app_spec.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branch tracking file path for a project
|
||||
*/
|
||||
export function getBranchTrackingPath(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), "active-branches.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the automaker directory structure exists for a project
|
||||
*/
|
||||
export async function ensureAutomakerDir(projectPath: string): Promise<string> {
|
||||
const automakerDir = getAutomakerDir(projectPath);
|
||||
await fs.mkdir(automakerDir, { recursive: true });
|
||||
return automakerDir;
|
||||
}
|
||||
63
libs/platform/src/security.ts
Normal file
63
libs/platform/src/security.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Security utilities for path validation
|
||||
* Note: All permission checks have been disabled to allow unrestricted access
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
|
||||
// Allowed project directories - kept for API compatibility
|
||||
const allowedPaths = new Set<string>();
|
||||
|
||||
/**
|
||||
* Initialize allowed paths from environment variable
|
||||
* Note: All paths are now allowed regardless of this setting
|
||||
*/
|
||||
export function initAllowedPaths(): void {
|
||||
const dirs = process.env.ALLOWED_PROJECT_DIRS;
|
||||
if (dirs) {
|
||||
for (const dir of dirs.split(",")) {
|
||||
const trimmed = dir.trim();
|
||||
if (trimmed) {
|
||||
allowedPaths.add(path.resolve(trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dataDir = process.env.DATA_DIR;
|
||||
if (dataDir) {
|
||||
allowedPaths.add(path.resolve(dataDir));
|
||||
}
|
||||
|
||||
const workspaceDir = process.env.WORKSPACE_DIR;
|
||||
if (workspaceDir) {
|
||||
allowedPaths.add(path.resolve(workspaceDir));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a path to the allowed list (no-op, all paths allowed)
|
||||
*/
|
||||
export function addAllowedPath(filePath: string): void {
|
||||
allowedPaths.add(path.resolve(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is allowed - always returns true
|
||||
*/
|
||||
export function isPathAllowed(_filePath: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a path - just resolves the path without checking permissions
|
||||
*/
|
||||
export function validatePath(filePath: string): string {
|
||||
return path.resolve(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of allowed paths (for debugging)
|
||||
*/
|
||||
export function getAllowedPaths(): string[] {
|
||||
return Array.from(allowedPaths);
|
||||
}
|
||||
206
libs/platform/src/subprocess.ts
Normal file
206
libs/platform/src/subprocess.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Subprocess management utilities for CLI providers
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "child_process";
|
||||
import readline from "readline";
|
||||
|
||||
export interface SubprocessOptions {
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
env?: Record<string, string>;
|
||||
abortController?: AbortController;
|
||||
timeout?: number; // Milliseconds of no output before timeout
|
||||
}
|
||||
|
||||
export interface SubprocessResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a subprocess and streams JSONL output line-by-line
|
||||
*/
|
||||
export async function* spawnJSONLProcess(
|
||||
options: SubprocessOptions
|
||||
): AsyncGenerator<unknown> {
|
||||
const { command, args, cwd, env, abortController, timeout = 30000 } = options;
|
||||
|
||||
const processEnv = {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
|
||||
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"],
|
||||
});
|
||||
|
||||
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) => {
|
||||
const text = data.toString();
|
||||
stderrOutput += text;
|
||||
console.error(`[SubprocessManager] stderr: ${text}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup timeout detection
|
||||
const resetTimeout = () => {
|
||||
lastOutputTime = Date.now();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
timeoutHandle = setTimeout(() => {
|
||||
const elapsed = Date.now() - lastOutputTime;
|
||||
if (elapsed >= timeout) {
|
||||
console.error(
|
||||
`[SubprocessManager] Process timeout: no output for ${timeout}ms`
|
||||
);
|
||||
childProcess.kill("SIGTERM");
|
||||
}
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
resetTimeout();
|
||||
|
||||
// Setup abort handling
|
||||
if (abortController) {
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
console.log("[SubprocessManager] Abort signal received, killing process");
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
childProcess.kill("SIGTERM");
|
||||
});
|
||||
}
|
||||
|
||||
// Parse stdout as JSONL (one JSON object per line)
|
||||
if (childProcess.stdout) {
|
||||
const rl = readline.createInterface({
|
||||
input: childProcess.stdout,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const line of rl) {
|
||||
resetTimeout();
|
||||
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
yield parsed;
|
||||
} catch (parseError) {
|
||||
console.error(
|
||||
`[SubprocessManager] Failed to parse JSONL line: ${line}`,
|
||||
parseError
|
||||
);
|
||||
// Yield error but continue processing
|
||||
yield {
|
||||
type: "error",
|
||||
error: `Failed to parse output: ${line}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[SubprocessManager] Error reading stdout:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for process to exit
|
||||
const exitCode = await new Promise<number | null>((resolve) => {
|
||||
childProcess.on("exit", (code) => {
|
||||
console.log(`[SubprocessManager] Process exited with code: ${code}`);
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
childProcess.on("error", (error) => {
|
||||
console.error("[SubprocessManager] Process error:", error);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle non-zero exit codes
|
||||
if (exitCode !== 0 && exitCode !== null) {
|
||||
const errorMessage = stderrOutput || `Process exited with code ${exitCode}`;
|
||||
console.error(`[SubprocessManager] Process failed: ${errorMessage}`);
|
||||
yield {
|
||||
type: "error",
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Process completed successfully
|
||||
if (exitCode === 0 && !stderrOutput) {
|
||||
console.log("[SubprocessManager] Process completed successfully");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a subprocess and collects all output
|
||||
*/
|
||||
export async function spawnProcess(
|
||||
options: SubprocessOptions
|
||||
): Promise<SubprocessResult> {
|
||||
const { command, args, cwd, env, abortController } = options;
|
||||
|
||||
const processEnv = {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn(command, args, {
|
||||
cwd,
|
||||
env: processEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
if (childProcess.stdout) {
|
||||
childProcess.stdout.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (childProcess.stderr) {
|
||||
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"));
|
||||
});
|
||||
}
|
||||
|
||||
childProcess.on("exit", (code) => {
|
||||
resolve({ stdout, stderr, exitCode: code });
|
||||
});
|
||||
|
||||
childProcess.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
20
libs/platform/tsconfig.json
Normal file
20
libs/platform/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"types": ["node"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user