mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +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