From bdb65f57294ed44601ab2666ec86d257d007f990 Mon Sep 17 00:00:00 2001 From: Kacper Date: Fri, 19 Dec 2025 23:30:35 +0100 Subject: [PATCH] feat: add @automaker/platform package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- libs/platform/package.json | 21 ++++ libs/platform/src/index.ts | 36 ++++++ libs/platform/src/paths.ts | 91 ++++++++++++++ libs/platform/src/security.ts | 63 ++++++++++ libs/platform/src/subprocess.ts | 206 ++++++++++++++++++++++++++++++++ libs/platform/tsconfig.json | 20 ++++ 6 files changed, 437 insertions(+) create mode 100644 libs/platform/package.json create mode 100644 libs/platform/src/index.ts create mode 100644 libs/platform/src/paths.ts create mode 100644 libs/platform/src/security.ts create mode 100644 libs/platform/src/subprocess.ts create mode 100644 libs/platform/tsconfig.json diff --git a/libs/platform/package.json b/libs/platform/package.json new file mode 100644 index 00000000..c9f4cb45 --- /dev/null +++ b/libs/platform/package.json @@ -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" + } +} diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts new file mode 100644 index 00000000..017792e6 --- /dev/null +++ b/libs/platform/src/index.ts @@ -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'; diff --git a/libs/platform/src/paths.ts b/libs/platform/src/paths.ts new file mode 100644 index 00000000..e11c6d7b --- /dev/null +++ b/libs/platform/src/paths.ts @@ -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 { + const automakerDir = getAutomakerDir(projectPath); + await fs.mkdir(automakerDir, { recursive: true }); + return automakerDir; +} diff --git a/libs/platform/src/security.ts b/libs/platform/src/security.ts new file mode 100644 index 00000000..7525d82f --- /dev/null +++ b/libs/platform/src/security.ts @@ -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(); + +/** + * 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); +} diff --git a/libs/platform/src/subprocess.ts b/libs/platform/src/subprocess.ts new file mode 100644 index 00000000..bb03d288 --- /dev/null +++ b/libs/platform/src/subprocess.ts @@ -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; + 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 { + 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((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 { + 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); + }); + }); +} diff --git a/libs/platform/tsconfig.json b/libs/platform/tsconfig.json new file mode 100644 index 00000000..54e9774b --- /dev/null +++ b/libs/platform/tsconfig.json @@ -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"] +}