mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
refactor: consolidate shared packages and eliminate code duplication
- Update 26+ files to import secureFs from @automaker/platform - Create shared type files: github.ts, worktree.ts, claude.ts in libs/types - Create exec-utils.ts for cross-platform shell execution - Delete redundant wrapper files: secure-fs.ts, stream-processor.ts, enhancement-prompts.ts - Update GitHub routes to use createLogError pattern - Add isENOENT helper to routes/common.ts - Fix test imports to use @automaker/prompts and @automaker/platform 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
99
CLAUDE.md
Normal file
99
CLAUDE.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Automaker is an autonomous AI development studio - a Kanban-based application where users describe features and AI agents (powered by Claude Agent SDK) automatically implement them. It runs as an Electron desktop app or in web browser mode.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build shared packages (REQUIRED before running)
|
||||||
|
npm run build:packages
|
||||||
|
|
||||||
|
# Development
|
||||||
|
npm run dev # Interactive mode selector
|
||||||
|
npm run dev:electron # Electron desktop app
|
||||||
|
npm run dev:web # Web browser mode (localhost:3007)
|
||||||
|
npm run dev:server # Backend server only
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npm run test # UI E2E tests (Playwright)
|
||||||
|
npm run test:headed # UI tests with visible browser
|
||||||
|
npm run test:server # Server unit tests (Vitest)
|
||||||
|
npm run test:packages # Shared package tests
|
||||||
|
|
||||||
|
# Linting & Formatting
|
||||||
|
npm run lint # ESLint
|
||||||
|
npm run format # Prettier
|
||||||
|
npm run format:check # Check formatting
|
||||||
|
|
||||||
|
# Building
|
||||||
|
npm run build # Build Next.js app
|
||||||
|
npm run build:electron # Build Electron distribution
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo Structure (npm workspaces)
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/
|
||||||
|
├── ui/ # Electron + Vite + React frontend (@automaker/ui)
|
||||||
|
└── server/ # Express + WebSocket backend (@automaker/server)
|
||||||
|
|
||||||
|
libs/ # Shared packages (@automaker/*)
|
||||||
|
├── types/ # Shared TypeScript interfaces
|
||||||
|
├── utils/ # Common utilities
|
||||||
|
├── prompts/ # AI prompt templates
|
||||||
|
├── platform/ # Platform-specific code (paths, security)
|
||||||
|
├── git-utils/ # Git operations
|
||||||
|
├── model-resolver/ # AI model configuration
|
||||||
|
└── dependency-resolver/ # Dependency management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
**State Management (UI)**: Zustand stores in `apps/ui/src/store/`
|
||||||
|
|
||||||
|
- `app-store.ts` - Main application state (features, settings, themes)
|
||||||
|
- `setup-store.ts` - Project setup wizard state
|
||||||
|
|
||||||
|
**Routing (UI)**: TanStack Router with file-based routes in `apps/ui/src/routes/`
|
||||||
|
|
||||||
|
**Backend Services**: Express + WebSocket in `apps/server/src/`
|
||||||
|
|
||||||
|
- Services in `/services/` handle business logic
|
||||||
|
- Routes in `/routes/` define API endpoints
|
||||||
|
- Providers in `/providers/` abstract AI model integrations
|
||||||
|
|
||||||
|
**Provider Architecture**: Model-based routing via `ProviderFactory`
|
||||||
|
|
||||||
|
- `ClaudeProvider` wraps @anthropic-ai/claude-agent-sdk
|
||||||
|
- Designed for easy addition of other providers
|
||||||
|
|
||||||
|
**Feature Storage**: Features stored in `.automaker/features/{id}/feature.json`
|
||||||
|
|
||||||
|
**Communication**:
|
||||||
|
|
||||||
|
- Electron: IPC via preload script (`apps/ui/src/preload.ts`)
|
||||||
|
- Web: HTTP API client (`apps/ui/src/lib/http-api-client.ts`)
|
||||||
|
|
||||||
|
### Important Files
|
||||||
|
|
||||||
|
- `apps/ui/src/main.ts` - Electron main process
|
||||||
|
- `apps/server/src/index.ts` - Server entry point
|
||||||
|
- `apps/ui/src/lib/electron.ts` - IPC type definitions
|
||||||
|
- `apps/server/src/services/agent-service.ts` - AI agent session management
|
||||||
|
- `apps/server/src/providers/provider-factory.ts` - Model routing
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- Always run `npm run build:packages` after modifying any `libs/*` package
|
||||||
|
- Server runs on port 3008 by default
|
||||||
|
- UI runs on port 3007 in web mode
|
||||||
|
- Authentication: Set `ANTHROPIC_API_KEY` env var or configure via Settings
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Enhancement Prompts - Re-exported from @automaker/prompts
|
|
||||||
*
|
|
||||||
* This file now re-exports enhancement prompts from the shared @automaker/prompts package
|
|
||||||
* to maintain backward compatibility with existing imports in the server codebase.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {
|
|
||||||
IMPROVE_SYSTEM_PROMPT,
|
|
||||||
TECHNICAL_SYSTEM_PROMPT,
|
|
||||||
SIMPLIFY_SYSTEM_PROMPT,
|
|
||||||
ACCEPTANCE_SYSTEM_PROMPT,
|
|
||||||
IMPROVE_EXAMPLES,
|
|
||||||
TECHNICAL_EXAMPLES,
|
|
||||||
SIMPLIFY_EXAMPLES,
|
|
||||||
ACCEPTANCE_EXAMPLES,
|
|
||||||
getEnhancementPrompt,
|
|
||||||
getSystemPrompt,
|
|
||||||
getExamples,
|
|
||||||
buildUserPrompt,
|
|
||||||
isValidEnhancementMode,
|
|
||||||
getAvailableEnhancementModes,
|
|
||||||
} from '@automaker/prompts';
|
|
||||||
|
|
||||||
export type { EnhancementMode, EnhancementExample } from '@automaker/prompts';
|
|
||||||
69
apps/server/src/lib/exec-utils.ts
Normal file
69
apps/server/src/lib/exec-utils.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Shell execution utilities
|
||||||
|
*
|
||||||
|
* Provides cross-platform shell execution with extended PATH
|
||||||
|
* to find tools like git and gh in Electron environments.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisified exec for async/await usage
|
||||||
|
*/
|
||||||
|
export const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path separator for the current platform
|
||||||
|
*/
|
||||||
|
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional paths to search for executables.
|
||||||
|
* Electron apps don't inherit the user's shell PATH, so we need to add
|
||||||
|
* common tool installation locations.
|
||||||
|
*/
|
||||||
|
const additionalPaths: string[] = [];
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows paths for Git and other tools
|
||||||
|
if (process.env.LOCALAPPDATA) {
|
||||||
|
additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`);
|
||||||
|
}
|
||||||
|
if (process.env.PROGRAMFILES) {
|
||||||
|
additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`);
|
||||||
|
}
|
||||||
|
if (process.env['ProgramFiles(x86)']) {
|
||||||
|
additionalPaths.push(`${process.env['ProgramFiles(x86)']}\\Git\\cmd`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unix/Mac paths
|
||||||
|
additionalPaths.push(
|
||||||
|
'/opt/homebrew/bin', // Homebrew on Apple Silicon
|
||||||
|
'/usr/local/bin', // Homebrew on Intel Mac, common Linux location
|
||||||
|
'/home/linuxbrew/.linuxbrew/bin', // Linuxbrew
|
||||||
|
`${process.env.HOME}/.local/bin` // pipx, other user installs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended PATH that includes common tool installation locations.
|
||||||
|
*/
|
||||||
|
export const extendedPath = [process.env.PATH, ...additionalPaths.filter(Boolean)]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(pathSeparator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variables with extended PATH for executing shell commands.
|
||||||
|
*/
|
||||||
|
export const execEnv = {
|
||||||
|
...process.env,
|
||||||
|
PATH: extendedPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is ENOENT (file/path not found or spawn failed)
|
||||||
|
*/
|
||||||
|
export function isENOENT(error: unknown): boolean {
|
||||||
|
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/**
|
|
||||||
* Re-export secure file system utilities from @automaker/platform
|
|
||||||
* This file exists for backward compatibility with existing imports
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { secureFs } from '@automaker/platform';
|
|
||||||
|
|
||||||
export const {
|
|
||||||
access,
|
|
||||||
readFile,
|
|
||||||
writeFile,
|
|
||||||
mkdir,
|
|
||||||
readdir,
|
|
||||||
stat,
|
|
||||||
rm,
|
|
||||||
unlink,
|
|
||||||
copyFile,
|
|
||||||
appendFile,
|
|
||||||
rename,
|
|
||||||
lstat,
|
|
||||||
joinPath,
|
|
||||||
resolvePath,
|
|
||||||
} = secureFs;
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stream Processor - Unified stream handling for provider messages
|
|
||||||
*
|
|
||||||
* Eliminates duplication of the stream processing pattern that was
|
|
||||||
* repeated 4x in auto-mode-service.ts (main execution, revision,
|
|
||||||
* task execution, continuation).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ProviderMessage, ContentBlock } from '@automaker/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callbacks for handling different stream events
|
|
||||||
*/
|
|
||||||
export interface StreamHandlers {
|
|
||||||
/** Called for each text block in the stream */
|
|
||||||
onText?: (text: string) => void | Promise<void>;
|
|
||||||
/** Called for each tool use in the stream */
|
|
||||||
onToolUse?: (name: string, input: unknown) => void | Promise<void>;
|
|
||||||
/** Called when an error occurs in the stream */
|
|
||||||
onError?: (error: string) => void | Promise<void>;
|
|
||||||
/** Called when the stream completes successfully */
|
|
||||||
onComplete?: (result: string) => void | Promise<void>;
|
|
||||||
/** Called for thinking blocks (if present) */
|
|
||||||
onThinking?: (thinking: string) => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result from processing a stream
|
|
||||||
*/
|
|
||||||
export interface StreamResult {
|
|
||||||
/** All accumulated text from the stream */
|
|
||||||
text: string;
|
|
||||||
/** Whether the stream completed successfully */
|
|
||||||
success: boolean;
|
|
||||||
/** Error message if stream failed */
|
|
||||||
error?: string;
|
|
||||||
/** Final result message if stream completed */
|
|
||||||
result?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a provider message stream with unified handling
|
|
||||||
*
|
|
||||||
* This eliminates the repeated pattern of:
|
|
||||||
* ```
|
|
||||||
* for await (const msg of stream) {
|
|
||||||
* if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
* for (const block of msg.message.content) {
|
|
||||||
* if (block.type === 'text') { ... }
|
|
||||||
* else if (block.type === 'tool_use') { ... }
|
|
||||||
* }
|
|
||||||
* } else if (msg.type === 'error') { ... }
|
|
||||||
* else if (msg.type === 'result') { ... }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param stream - The async generator from provider.executeQuery()
|
|
||||||
* @param handlers - Callbacks for different event types
|
|
||||||
* @returns Accumulated result with text and status
|
|
||||||
*/
|
|
||||||
export async function processStream(
|
|
||||||
stream: AsyncGenerator<ProviderMessage>,
|
|
||||||
handlers: StreamHandlers
|
|
||||||
): Promise<StreamResult> {
|
|
||||||
let accumulatedText = '';
|
|
||||||
let success = true;
|
|
||||||
let errorMessage: string | undefined;
|
|
||||||
let resultMessage: string | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const msg of stream) {
|
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
await processContentBlock(block, handlers, (text) => {
|
|
||||||
accumulatedText += text;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'error') {
|
|
||||||
success = false;
|
|
||||||
errorMessage = msg.error || 'Unknown error';
|
|
||||||
if (handlers.onError) {
|
|
||||||
await handlers.onError(errorMessage);
|
|
||||||
}
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
|
||||||
resultMessage = msg.result || '';
|
|
||||||
if (handlers.onComplete) {
|
|
||||||
await handlers.onComplete(resultMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!errorMessage) {
|
|
||||||
success = false;
|
|
||||||
errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: accumulatedText,
|
|
||||||
success,
|
|
||||||
error: errorMessage,
|
|
||||||
result: resultMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a single content block
|
|
||||||
*/
|
|
||||||
async function processContentBlock(
|
|
||||||
block: ContentBlock,
|
|
||||||
handlers: StreamHandlers,
|
|
||||||
appendText: (text: string) => void
|
|
||||||
): Promise<void> {
|
|
||||||
switch (block.type) {
|
|
||||||
case 'text':
|
|
||||||
if (block.text) {
|
|
||||||
appendText(block.text);
|
|
||||||
if (handlers.onText) {
|
|
||||||
await handlers.onText(block.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tool_use':
|
|
||||||
if (block.name && handlers.onToolUse) {
|
|
||||||
await handlers.onToolUse(block.name, block.input);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'thinking':
|
|
||||||
if (block.thinking && handlers.onThinking) {
|
|
||||||
await handlers.onThinking(block.thinking);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// tool_result blocks are handled internally by the SDK
|
|
||||||
case 'tool_result':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a simple stream processor that just collects text
|
|
||||||
*
|
|
||||||
* Useful for cases where you just need the final text output
|
|
||||||
* without any side effects during streaming.
|
|
||||||
*/
|
|
||||||
export async function collectStreamText(stream: AsyncGenerator<ProviderMessage>): Promise<string> {
|
|
||||||
const result = await processStream(stream, {});
|
|
||||||
return result.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process stream with progress callback
|
|
||||||
*
|
|
||||||
* Simplified interface for the common case of just wanting
|
|
||||||
* text updates during streaming.
|
|
||||||
*/
|
|
||||||
export async function processStreamWithProgress(
|
|
||||||
stream: AsyncGenerator<ProviderMessage>,
|
|
||||||
onProgress: (text: string) => void
|
|
||||||
): Promise<StreamResult> {
|
|
||||||
return processStream(stream, {
|
|
||||||
onText: onProgress,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a stream result contains a specific marker
|
|
||||||
*
|
|
||||||
* Useful for detecting spec generation markers like [SPEC_GENERATED]
|
|
||||||
*/
|
|
||||||
export function hasMarker(result: StreamResult, marker: string): boolean {
|
|
||||||
return result.text.includes(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract content before a marker
|
|
||||||
*
|
|
||||||
* Useful for extracting spec content before [SPEC_GENERATED] marker
|
|
||||||
*/
|
|
||||||
export function extractBeforeMarker(text: string, marker: string): string | null {
|
|
||||||
const index = text.indexOf(marker);
|
|
||||||
if (index === -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return text.substring(0, index).trim();
|
|
||||||
}
|
|
||||||
@@ -3,26 +3,16 @@
|
|||||||
* Stores worktree-specific data in .automaker/worktrees/:branch/worktree.json
|
* Stores worktree-specific data in .automaker/worktrees/:branch/worktree.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as secureFs from './secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import type { WorktreePRInfo, WorktreeMetadata } from '@automaker/types';
|
||||||
|
|
||||||
|
// Re-export types for convenience
|
||||||
|
export type { WorktreePRInfo, WorktreeMetadata } from '@automaker/types';
|
||||||
|
|
||||||
/** Maximum length for sanitized branch names in filesystem paths */
|
/** Maximum length for sanitized branch names in filesystem paths */
|
||||||
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
|
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
|
||||||
|
|
||||||
export interface WorktreePRInfo {
|
|
||||||
number: number;
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
state: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorktreeMetadata {
|
|
||||||
branch: string;
|
|
||||||
createdAt: string;
|
|
||||||
pr?: WorktreePRInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize branch name for cross-platform filesystem safety
|
* Sanitize branch name for cross-platform filesystem safety
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
SimpleQueryResult,
|
SimpleQueryResult,
|
||||||
StreamingQueryOptions,
|
StreamingQueryOptions,
|
||||||
StreamingQueryResult,
|
StreamingQueryResult,
|
||||||
|
PromptContentBlock,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
export class ClaudeProvider extends BaseProvider {
|
export class ClaudeProvider extends BaseProvider {
|
||||||
@@ -335,7 +336,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
/**
|
/**
|
||||||
* Create a multi-part prompt generator for content blocks
|
* Create a multi-part prompt generator for content blocks
|
||||||
*/
|
*/
|
||||||
private createPromptGenerator(content: Array<{ type: string; text?: string; source?: object }>) {
|
private createPromptGenerator(content: PromptContentBlock[]) {
|
||||||
// Return an async generator that yields SDK user messages
|
// Return an async generator that yields SDK user messages
|
||||||
// The SDK expects this format for multi-part prompts
|
// The SDK expects this format for multi-part prompts
|
||||||
return (async function* () {
|
return (async function* () {
|
||||||
|
|||||||
@@ -4,12 +4,11 @@
|
|||||||
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { ProviderFactory } from '../../providers/provider-factory.js';
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
||||||
import { getAppSpecPath } from '@automaker/platform';
|
import { getAppSpecPath, secureFs } from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger('SpecRegeneration');
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import {
|
import {
|
||||||
specOutputSchema,
|
specOutputSchema,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { ProviderFactory } from '../../providers/provider-factory.js';
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
||||||
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
|
import { ensureAutomakerDir, getAppSpecPath, secureFs } from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger('SpecRegeneration');
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getFeaturesDir } from '@automaker/platform';
|
import { getFeaturesDir, secureFs } from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger('SpecRegeneration');
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Claude Usage types for CLI-based usage tracking
|
* Claude Usage types for CLI-based usage tracking
|
||||||
|
* Re-exported from @automaker/types for convenience
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ClaudeUsage = {
|
export type { ClaudeUsage, ClaudeStatus } from '@automaker/types';
|
||||||
sessionTokensUsed: number;
|
|
||||||
sessionLimit: number;
|
|
||||||
sessionPercentage: number;
|
|
||||||
sessionResetTime: string; // ISO date string
|
|
||||||
sessionResetText: string; // Raw text like "Resets 10:59am (Asia/Dubai)"
|
|
||||||
|
|
||||||
weeklyTokensUsed: number;
|
|
||||||
weeklyLimit: number;
|
|
||||||
weeklyPercentage: number;
|
|
||||||
weeklyResetTime: string; // ISO date string
|
|
||||||
weeklyResetText: string; // Raw text like "Resets Dec 22 at 7:59pm (Asia/Dubai)"
|
|
||||||
|
|
||||||
sonnetWeeklyTokensUsed: number;
|
|
||||||
sonnetWeeklyPercentage: number;
|
|
||||||
sonnetResetText: string; // Raw text like "Resets Dec 27 at 9:59am (Asia/Dubai)"
|
|
||||||
|
|
||||||
costUsed: number | null;
|
|
||||||
costLimit: number | null;
|
|
||||||
costCurrency: string | null;
|
|
||||||
|
|
||||||
lastUpdated: string; // ISO date string
|
|
||||||
userTimezone: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClaudeStatus = {
|
|
||||||
indicator: {
|
|
||||||
color: 'green' | 'yellow' | 'orange' | 'red' | 'gray';
|
|
||||||
};
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ export {
|
|||||||
getGitRepositoryDiffs,
|
getGitRepositoryDiffs,
|
||||||
} from '@automaker/git-utils';
|
} from '@automaker/git-utils';
|
||||||
|
|
||||||
type Logger = ReturnType<typeof createLogger>;
|
// Re-export error utilities from shared package
|
||||||
|
export { getErrorMessage } from '@automaker/utils';
|
||||||
|
|
||||||
/**
|
// Re-export exec utilities
|
||||||
* Get error message from error object
|
export { execAsync, execEnv, isENOENT } from '../lib/exec-utils.js';
|
||||||
*/
|
|
||||||
export function getErrorMessage(error: unknown): string {
|
type Logger = ReturnType<typeof createLogger>;
|
||||||
return error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a logError function for a specific logger
|
* Create a logError function for a specific logger
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
import { PathNotAllowedError, secureFs } from '@automaker/platform';
|
||||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
const logger = createLogger('DescribeFile');
|
const logger = createLogger('DescribeFile');
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
buildUserPrompt,
|
buildUserPrompt,
|
||||||
isValidEnhancementMode,
|
isValidEnhancementMode,
|
||||||
type EnhancementMode,
|
type EnhancementMode,
|
||||||
} from '../../../lib/enhancement-prompts.js';
|
} from '@automaker/prompts';
|
||||||
|
|
||||||
const logger = createLogger('EnhancePrompt');
|
const logger = createLogger('EnhancePrompt');
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getAllowedRootDirectory, PathNotAllowedError } from '@automaker/platform';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getAllowedRootDirectory, PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createBrowseHandler() {
|
export function createBrowseHandler() {
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getBoardDir } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getBoardDir } from '@automaker/platform';
|
|
||||||
|
|
||||||
export function createDeleteBoardBackgroundHandler() {
|
export function createDeleteBoardBackgroundHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createDeleteHandler() {
|
export function createDeleteHandler() {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createExistsHandler() {
|
export function createExistsHandler() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createImageHandler() {
|
export function createImageHandler() {
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createMkdirHandler() {
|
export function createMkdirHandler() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
import { getErrorMessage, logError, isENOENT } from '../common.js';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
|
||||||
|
|
||||||
// Optional files that are expected to not exist in new projects
|
// Optional files that are expected to not exist in new projects
|
||||||
// Don't log ENOENT errors for these to reduce noise
|
// Don't log ENOENT errors for these to reduce noise
|
||||||
@@ -15,10 +14,6 @@ function isOptionalFile(filePath: string): boolean {
|
|||||||
return OPTIONAL_FILES.some((optionalFile) => filePath.endsWith(optionalFile));
|
return OPTIONAL_FILES.some((optionalFile) => filePath.endsWith(optionalFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isENOENT(error: unknown): boolean {
|
|
||||||
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createReadHandler() {
|
export function createReadHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createReaddirHandler() {
|
export function createReaddirHandler() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getBoardDir } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getBoardDir } from '@automaker/platform';
|
|
||||||
|
|
||||||
export function createSaveBoardBackgroundHandler() {
|
export function createSaveBoardBackgroundHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getImagesDir } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getImagesDir } from '@automaker/platform';
|
|
||||||
|
|
||||||
export function createSaveImageHandler() {
|
export function createSaveImageHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createStatHandler() {
|
export function createStatHandler() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, isPathAllowed } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { isPathAllowed } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createValidatePathHandler() {
|
export function createValidatePathHandler() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { mkdirSafe } from '@automaker/utils';
|
import { mkdirSafe } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
import type { GitHubRemoteStatus } from '@automaker/types';
|
||||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||||
|
|
||||||
export interface GitHubRemoteStatus {
|
// Re-export type for convenience
|
||||||
hasGitHubRemote: boolean;
|
export type { GitHubRemoteStatus } from '@automaker/types';
|
||||||
remoteUrl: string | null;
|
|
||||||
owner: string | null;
|
|
||||||
repo: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkGitHubRemote(projectPath: string): Promise<GitHubRemoteStatus> {
|
export async function checkGitHubRemote(projectPath: string): Promise<GitHubRemoteStatus> {
|
||||||
const status: GitHubRemoteStatus = {
|
const status: GitHubRemoteStatus = {
|
||||||
|
|||||||
@@ -2,34 +2,16 @@
|
|||||||
* Common utilities for GitHub routes
|
* Common utilities for GitHub routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec } from 'child_process';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { promisify } from 'util';
|
import { createLogError, getErrorMessage } from '../../common.js';
|
||||||
|
import { execAsync, execEnv } from '../../../lib/exec-utils.js';
|
||||||
|
|
||||||
export const execAsync = promisify(exec);
|
const logger = createLogger('GitHub');
|
||||||
|
|
||||||
// Extended PATH to include common tool installation locations
|
// Re-export exec utilities for convenience
|
||||||
export const extendedPath = [
|
export { execAsync, execEnv } from '../../../lib/exec-utils.js';
|
||||||
process.env.PATH,
|
|
||||||
'/opt/homebrew/bin',
|
|
||||||
'/usr/local/bin',
|
|
||||||
'/home/linuxbrew/.linuxbrew/bin',
|
|
||||||
`${process.env.HOME}/.local/bin`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(':');
|
|
||||||
|
|
||||||
export const execEnv = {
|
// Re-export error utilities
|
||||||
...process.env,
|
export { getErrorMessage } from '../../common.js';
|
||||||
PATH: extendedPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getErrorMessage(error: unknown): string {
|
export const logError = createLogError(logger);
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
return String(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logError(error: unknown, context: string): void {
|
|
||||||
console.error(`[GitHub] ${context}:`, error);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,35 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
import type { GitHubIssue, ListIssuesResult } from '@automaker/types';
|
||||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||||
import { checkGitHubRemote } from './check-github-remote.js';
|
import { checkGitHubRemote } from './check-github-remote.js';
|
||||||
|
|
||||||
export interface GitHubLabel {
|
// Re-export types for convenience
|
||||||
name: string;
|
export type { GitHubLabel, GitHubAuthor, GitHubIssue, ListIssuesResult } from '@automaker/types';
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GitHubAuthor {
|
|
||||||
login: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GitHubIssue {
|
|
||||||
number: number;
|
|
||||||
title: string;
|
|
||||||
state: string;
|
|
||||||
author: GitHubAuthor;
|
|
||||||
createdAt: string;
|
|
||||||
labels: GitHubLabel[];
|
|
||||||
url: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListIssuesResult {
|
|
||||||
success: boolean;
|
|
||||||
openIssues?: GitHubIssue[];
|
|
||||||
closedIssues?: GitHubIssue[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createListIssuesHandler() {
|
export function createListIssuesHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -3,39 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
import type { GitHubPR, ListPRsResult } from '@automaker/types';
|
||||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||||
import { checkGitHubRemote } from './check-github-remote.js';
|
import { checkGitHubRemote } from './check-github-remote.js';
|
||||||
|
|
||||||
export interface GitHubLabel {
|
// Re-export types for convenience
|
||||||
name: string;
|
export type { GitHubLabel, GitHubAuthor, GitHubPR, ListPRsResult } from '@automaker/types';
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GitHubAuthor {
|
|
||||||
login: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GitHubPR {
|
|
||||||
number: number;
|
|
||||||
title: string;
|
|
||||||
state: string;
|
|
||||||
author: GitHubAuthor;
|
|
||||||
createdAt: string;
|
|
||||||
labels: GitHubLabel[];
|
|
||||||
url: string;
|
|
||||||
isDraft: boolean;
|
|
||||||
headRefName: string;
|
|
||||||
reviewDecision: string | null;
|
|
||||||
mergeable: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListPRsResult {
|
|
||||||
success: boolean;
|
|
||||||
openPRs?: GitHubPR[];
|
|
||||||
mergedPRs?: GitHubPR[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createListPRsHandler() {
|
export function createListPRsHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, PathNotAllowedError } from '@automaker/platform';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
|
||||||
import { logger, getErrorMessage, logError } from '../common.js';
|
import { logger, getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createCloneHandler() {
|
export function createCloneHandler() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getAllowedRootDirectory, getDataDirectory } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getAllowedRootDirectory, getDataDirectory } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createConfigHandler() {
|
export function createConfigHandler() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getAllowedRootDirectory } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getAllowedRootDirectory } from '@automaker/platform';
|
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createDirectoriesHandler() {
|
export function createDirectoriesHandler() {
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { exec } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import path from 'path';
|
|
||||||
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
|
import { execAsync, execEnv, isENOENT } from '../../lib/exec-utils.js';
|
||||||
import { FeatureLoader } from '../../services/feature-loader.js';
|
import { FeatureLoader } from '../../services/feature-loader.js';
|
||||||
|
|
||||||
const logger = createLogger('Worktree');
|
const logger = createLogger('Worktree');
|
||||||
export const execAsync = promisify(exec);
|
|
||||||
const featureLoader = new FeatureLoader();
|
const featureLoader = new FeatureLoader();
|
||||||
|
|
||||||
|
// Re-export exec utilities for convenience
|
||||||
|
export { execAsync, execEnv, isENOENT } from '../../lib/exec-utils.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -20,48 +20,6 @@ const featureLoader = new FeatureLoader();
|
|||||||
/** Maximum allowed length for git branch names */
|
/** Maximum allowed length for git branch names */
|
||||||
export const MAX_BRANCH_NAME_LENGTH = 250;
|
export const MAX_BRANCH_NAME_LENGTH = 250;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Extended PATH configuration for Electron apps
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
|
||||||
const additionalPaths: string[] = [];
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
// Windows paths
|
|
||||||
if (process.env.LOCALAPPDATA) {
|
|
||||||
additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`);
|
|
||||||
}
|
|
||||||
if (process.env.PROGRAMFILES) {
|
|
||||||
additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`);
|
|
||||||
}
|
|
||||||
if (process.env['ProgramFiles(x86)']) {
|
|
||||||
additionalPaths.push(`${process.env['ProgramFiles(x86)']}\\Git\\cmd`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Unix/Mac paths
|
|
||||||
additionalPaths.push(
|
|
||||||
'/opt/homebrew/bin', // Homebrew on Apple Silicon
|
|
||||||
'/usr/local/bin', // Homebrew on Intel Mac, common Linux location
|
|
||||||
'/home/linuxbrew/.linuxbrew/bin', // Linuxbrew
|
|
||||||
`${process.env.HOME}/.local/bin` // pipx, other user installs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const extendedPath = [process.env.PATH, ...additionalPaths.filter(Boolean)]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(pathSeparator);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variables with extended PATH for executing shell commands.
|
|
||||||
* Electron apps don't inherit the user's shell PATH, so we need to add
|
|
||||||
* common tool installation locations.
|
|
||||||
*/
|
|
||||||
export const execEnv = {
|
|
||||||
...process.env,
|
|
||||||
PATH: extendedPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Validation utilities
|
// Validation utilities
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -111,14 +69,6 @@ export async function isGitRepo(repoPath: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an error is ENOENT (file/path not found or spawn failed)
|
|
||||||
* These are expected in test environments with mock paths
|
|
||||||
*/
|
|
||||||
export function isENOENT(error: unknown): boolean {
|
|
||||||
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is a mock/test path that doesn't exist
|
* Check if a path is a mock/test path that doesn't exist
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,15 +5,12 @@
|
|||||||
* can switch between branches even after worktrees are removed.
|
* can switch between branches even after worktrees are removed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs, getBranchTrackingPath, ensureAutomakerDir } from '@automaker/platform';
|
||||||
|
import type { TrackedBranch } from '@automaker/types';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getBranchTrackingPath, ensureAutomakerDir } from '@automaker/platform';
|
|
||||||
|
|
||||||
export interface TrackedBranch {
|
// Re-export type for convenience
|
||||||
name: string;
|
export type { TrackedBranch } from '@automaker/types';
|
||||||
createdAt: string;
|
|
||||||
lastActivatedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BranchTrackingData {
|
interface BranchTrackingData {
|
||||||
branches: TrackedBranch[];
|
branches: TrackedBranch[];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { Request, Response } from 'express';
|
|||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import {
|
import {
|
||||||
isGitRepo,
|
isGitRepo,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getGitRepositoryDiffs } from '../../common.js';
|
import { getGitRepositoryDiffs } from '../../common.js';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Request, Response } from 'express';
|
|||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { generateSyntheticDiffForNewFile } from '../../common.js';
|
import { generateSyntheticDiffForNewFile } from '../../common.js';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Request, Response } from 'express';
|
|||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import { isGitRepo } from '@automaker/git-utils';
|
import { isGitRepo } from '@automaker/git-utils';
|
||||||
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
||||||
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
|
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
import type { PRComment, PRInfo } from '@automaker/types';
|
||||||
import {
|
import {
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
logError,
|
logError,
|
||||||
@@ -12,26 +13,8 @@ import {
|
|||||||
isGhCliAvailable,
|
isGhCliAvailable,
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
|
|
||||||
export interface PRComment {
|
// Re-export types for convenience
|
||||||
id: number;
|
export type { PRComment, PRInfo } from '@automaker/types';
|
||||||
author: string;
|
|
||||||
body: string;
|
|
||||||
path?: string;
|
|
||||||
line?: number;
|
|
||||||
createdAt: string;
|
|
||||||
isReviewComment: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PRInfo {
|
|
||||||
number: number;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
state: string;
|
|
||||||
author: string;
|
|
||||||
body: string;
|
|
||||||
comments: PRComment[];
|
|
||||||
reviewComments: PRComment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPRInfoHandler() {
|
export function createPRInfoHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Request, Response } from 'express';
|
|||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../lib/secure-fs.js';
|
|
||||||
import type { EventEmitter } from '../lib/events.js';
|
import type { EventEmitter } from '../lib/events.js';
|
||||||
import type { ExecuteOptions } from '@automaker/types';
|
import type { ExecuteOptions } from '@automaker/types';
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
} from '@automaker/utils';
|
} from '@automaker/utils';
|
||||||
import { ProviderFactory } from '../providers/provider-factory.js';
|
import { ProviderFactory } from '../providers/provider-factory.js';
|
||||||
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
import { PathNotAllowedError, secureFs } from '@automaker/platform';
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import {
|
|||||||
loadContextFiles,
|
loadContextFiles,
|
||||||
createLogger,
|
createLogger,
|
||||||
sleep,
|
sleep,
|
||||||
|
processStream,
|
||||||
|
extractBeforeMarker,
|
||||||
} from '@automaker/utils';
|
} from '@automaker/utils';
|
||||||
|
import { secureFs, getFeatureDir } from '@automaker/platform';
|
||||||
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
||||||
import { getFeatureDir } from '@automaker/platform';
|
|
||||||
import {
|
import {
|
||||||
getPlanningPromptPrefix,
|
getPlanningPromptPrefix,
|
||||||
parseTasksFromSpec,
|
parseTasksFromSpec,
|
||||||
@@ -31,10 +33,8 @@ import {
|
|||||||
buildContinuationPrompt,
|
buildContinuationPrompt,
|
||||||
} from '@automaker/prompts';
|
} from '@automaker/prompts';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../lib/secure-fs.js';
|
|
||||||
import type { EventEmitter } from '../lib/events.js';
|
import type { EventEmitter } from '../lib/events.js';
|
||||||
import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
import { processStream, extractBeforeMarker } from '../lib/stream-processor.js';
|
|
||||||
import { FeatureLoader } from './feature-loader.js';
|
import { FeatureLoader } from './feature-loader.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
shortHash,
|
shortHash,
|
||||||
} from '@automaker/git-utils';
|
} from '@automaker/git-utils';
|
||||||
import { extractTitleFromDescription } from '@automaker/prompts';
|
import { extractTitleFromDescription } from '@automaker/prompts';
|
||||||
import { getFeatureDir } from '@automaker/platform';
|
import { getFeatureDir, secureFs } from '@automaker/platform';
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import type { Feature } from '@automaker/types';
|
import type { Feature } from '@automaker/types';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Used to persist agent output to agent-output.md in the feature directory.
|
* Used to persist agent output to agent-output.md in the feature directory.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExecuteOptions } from '@automaker/types';
|
import type { ExecuteOptions } from '@automaker/types';
|
||||||
import { createLogger, classifyError } from '@automaker/utils';
|
import { createLogger, classifyError, processStream } from '@automaker/utils';
|
||||||
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
||||||
import { getAutomakerDir } from '@automaker/platform';
|
import { getAutomakerDir, secureFs } from '@automaker/platform';
|
||||||
import { ProviderFactory } from '../../providers/provider-factory.js';
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { validateWorkingDirectory } from '../../lib/sdk-options.js';
|
import { validateWorkingDirectory } from '../../lib/sdk-options.js';
|
||||||
import { processStream } from '../../lib/stream-processor.js';
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import type { ExecuteOptions, ParsedTask } from '@automaker/types';
|
|||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import type { BaseProvider } from '../../providers/base-provider.js';
|
import type { BaseProvider } from '../../providers/base-provider.js';
|
||||||
import { buildTaskPrompt } from '@automaker/prompts';
|
import { buildTaskPrompt } from '@automaker/prompts';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger, processStream } from '@automaker/utils';
|
||||||
import { processStream } from '../../lib/stream-processor.js';
|
|
||||||
import type { TaskExecutionContext, TaskProgress } from './types.js';
|
import type { TaskExecutionContext, TaskProgress } from './types.js';
|
||||||
|
|
||||||
const logger = createLogger('TaskExecutor');
|
const logger = createLogger('TaskExecutor');
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, execSync, type ChildProcess } from 'child_process';
|
import { spawn, execSync, type ChildProcess } from 'child_process';
|
||||||
import * as secureFs from '../lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import path from 'path';
|
|||||||
import type { Feature, PlanSpec, FeatureStatus } from '@automaker/types';
|
import type { Feature, PlanSpec, FeatureStatus } from '@automaker/types';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver';
|
import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver';
|
||||||
import * as secureFs from '../lib/secure-fs.js';
|
|
||||||
import {
|
import {
|
||||||
getFeaturesDir,
|
getFeaturesDir,
|
||||||
getFeatureDir,
|
getFeatureDir,
|
||||||
getFeatureImagesDir,
|
getFeatureImagesDir,
|
||||||
ensureAutomakerDir,
|
ensureAutomakerDir,
|
||||||
|
secureFs,
|
||||||
} from '@automaker/platform';
|
} from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger('FeatureLoader');
|
const logger = createLogger('FeatureLoader');
|
||||||
|
|||||||
@@ -8,14 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import * as secureFs from '../lib/secure-fs.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getGlobalSettingsPath,
|
getGlobalSettingsPath,
|
||||||
getCredentialsPath,
|
getCredentialsPath,
|
||||||
getProjectSettingsPath,
|
getProjectSettingsPath,
|
||||||
ensureDataDir,
|
ensureDataDir,
|
||||||
ensureAutomakerDir,
|
ensureAutomakerDir,
|
||||||
|
secureFs,
|
||||||
} from '@automaker/platform';
|
} from '@automaker/platform';
|
||||||
import type {
|
import type {
|
||||||
GlobalSettings,
|
GlobalSettings,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
SIMPLIFY_EXAMPLES,
|
SIMPLIFY_EXAMPLES,
|
||||||
ACCEPTANCE_EXAMPLES,
|
ACCEPTANCE_EXAMPLES,
|
||||||
type EnhancementMode,
|
type EnhancementMode,
|
||||||
} from '@/lib/enhancement-prompts.js';
|
} from '@automaker/prompts';
|
||||||
|
|
||||||
describe('enhancement-prompts.ts', () => {
|
describe('enhancement-prompts.ts', () => {
|
||||||
describe('System Prompt Constants', () => {
|
describe('System Prompt Constants', () => {
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ vi.mock('child_process', () => ({
|
|||||||
execSync: vi.fn(),
|
execSync: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock secure-fs
|
// Mock secure-fs from @automaker/platform
|
||||||
vi.mock('@/lib/secure-fs.js', () => ({
|
vi.mock('@automaker/platform', async () => {
|
||||||
access: vi.fn(),
|
const actual = await vi.importActual('@automaker/platform');
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
secureFs: {
|
||||||
|
access: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Mock net
|
// Mock net
|
||||||
vi.mock('net', () => ({
|
vi.mock('net', () => ({
|
||||||
@@ -24,7 +30,7 @@ vi.mock('net', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { spawn, execSync } from 'child_process';
|
import { spawn, execSync } from 'child_process';
|
||||||
import * as secureFs from '@/lib/secure-fs.js';
|
import { secureFs } from '@automaker/platform';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
describe('dev-server-service.ts', () => {
|
describe('dev-server-service.ts', () => {
|
||||||
|
|||||||
671
docs/auto-mode-refactoring-plan.md
Normal file
671
docs/auto-mode-refactoring-plan.md
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
# Auto Mode Service Refactoring Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document proposes a refactoring of `apps/server/src/services/auto-mode-service.ts` (2,497 lines) into smaller, focused modules while leveraging shared packages per `docs/llm-shared-packages.md`.
|
||||||
|
|
||||||
|
## Current Problems
|
||||||
|
|
||||||
|
1. **Monolithic Service** - 2,497 lines with 16+ public methods mixing concerns
|
||||||
|
2. **Giant Method** - `runAgent()` is 658 lines handling planning, approval, task execution, file I/O
|
||||||
|
3. **Code Duplication** - Stream processing repeated 4x, types duplicated in 3 files
|
||||||
|
4. **Missing Shared Package Usage** - Local types that exist in `@automaker/types`, prompts not in `@automaker/prompts`
|
||||||
|
5. **No Structured Logging** - Uses `console.log` instead of `createLogger`
|
||||||
|
6. **Untyped Events** - Event strings scattered, no type safety
|
||||||
|
|
||||||
|
## Proposed Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/server/src/services/
|
||||||
|
├── auto-mode/
|
||||||
|
│ ├── index.ts # Re-exports AutoModeService (facade)
|
||||||
|
│ ├── auto-mode-service.ts # Orchestrator (~300 lines)
|
||||||
|
│ ├── feature-executor.ts # Feature execution logic
|
||||||
|
│ ├── plan-approval-service.ts # Plan approval flow
|
||||||
|
│ ├── task-executor.ts # Multi-task execution
|
||||||
|
│ ├── worktree-manager.ts # Git worktree operations
|
||||||
|
│ ├── output-writer.ts # Incremental file writing
|
||||||
|
│ └── types.ts # Internal types (RunningFeature, etc.)
|
||||||
|
├── feature-loader.ts # Existing - extend with status updates
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
libs/types/src/
|
||||||
|
├── planning.ts # NEW: ParsedTask, PlanSpec, AutoModeEventType
|
||||||
|
├── feature.ts # Update: use PlanSpec from planning.ts
|
||||||
|
└── index.ts # Export planning types
|
||||||
|
|
||||||
|
libs/prompts/src/
|
||||||
|
├── planning.ts # NEW: PLANNING_PROMPTS, task parsing, buildTaskPrompt
|
||||||
|
├── enhancement.ts # Existing
|
||||||
|
└── index.ts # Export planning functions
|
||||||
|
|
||||||
|
apps/server/src/lib/
|
||||||
|
├── stream-processor.ts # NEW: Reusable stream processing utility
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 1: Shared Package Updates (No Breaking Changes)
|
||||||
|
|
||||||
|
### 1.1 Add Planning Types to `@automaker/types`
|
||||||
|
|
||||||
|
Create `libs/types/src/planning.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Task and plan status types
|
||||||
|
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||||
|
export type PlanSpecStatus = 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||||
|
|
||||||
|
export interface ParsedTask {
|
||||||
|
id: string; // e.g., "T001"
|
||||||
|
description: string; // e.g., "Create user model"
|
||||||
|
filePath?: string; // e.g., "src/models/user.ts"
|
||||||
|
phase?: string; // e.g., "Phase 1: Foundation"
|
||||||
|
status: TaskStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanSpec {
|
||||||
|
status: PlanSpecStatus;
|
||||||
|
content?: string;
|
||||||
|
version: number;
|
||||||
|
generatedAt?: string;
|
||||||
|
approvedAt?: string;
|
||||||
|
reviewedByUser: boolean;
|
||||||
|
tasksCompleted?: number;
|
||||||
|
tasksTotal?: number;
|
||||||
|
currentTaskId?: string;
|
||||||
|
tasks?: ParsedTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-mode event types for type safety
|
||||||
|
export type AutoModeEventType =
|
||||||
|
| 'auto_mode_started'
|
||||||
|
| 'auto_mode_stopped'
|
||||||
|
| 'auto_mode_idle'
|
||||||
|
| 'auto_mode_feature_start'
|
||||||
|
| 'auto_mode_feature_complete'
|
||||||
|
| 'auto_mode_progress'
|
||||||
|
| 'auto_mode_tool'
|
||||||
|
| 'auto_mode_error'
|
||||||
|
| 'auto_mode_task_started'
|
||||||
|
| 'auto_mode_task_complete'
|
||||||
|
| 'auto_mode_phase_complete'
|
||||||
|
| 'planning_started'
|
||||||
|
| 'plan_approval_required'
|
||||||
|
| 'plan_approved'
|
||||||
|
| 'plan_rejected'
|
||||||
|
| 'plan_auto_approved'
|
||||||
|
| 'plan_revision_requested';
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `libs/types/src/feature.ts` to import `PlanSpec`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { PlanSpec } from './planning.js';
|
||||||
|
|
||||||
|
export interface Feature {
|
||||||
|
// ... existing fields ...
|
||||||
|
planSpec?: PlanSpec; // Now references shared type
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Add Planning Prompts to `@automaker/prompts`
|
||||||
|
|
||||||
|
Create `libs/prompts/src/planning.ts`:
|
||||||
|
|
||||||
|
````typescript
|
||||||
|
import type { PlanningMode, ParsedTask } from '@automaker/types';
|
||||||
|
|
||||||
|
// Planning mode prompts (moved from auto-mode-service.ts lines 57-211)
|
||||||
|
export const PLANNING_PROMPTS = {
|
||||||
|
lite: `## Planning Phase (Lite Mode)...`,
|
||||||
|
lite_with_approval: `## Planning Phase (Lite Mode)...`,
|
||||||
|
spec: `## Specification Phase (Spec Mode)...`,
|
||||||
|
full: `## Full Specification Phase (Full SDD Mode)...`,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get planning prompt for a mode
|
||||||
|
*/
|
||||||
|
export function getPlanningPrompt(mode: PlanningMode, requireApproval?: boolean): string {
|
||||||
|
if (mode === 'skip') return '';
|
||||||
|
if (mode === 'lite' && requireApproval) return PLANNING_PROMPTS.lite_with_approval;
|
||||||
|
return PLANNING_PROMPTS[mode] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse tasks from generated spec content
|
||||||
|
* Looks for ```tasks code block and extracts task lines
|
||||||
|
*/
|
||||||
|
export function parseTasksFromSpec(specContent: string): ParsedTask[] {
|
||||||
|
// ... moved from auto-mode-service.ts lines 218-265
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single task line
|
||||||
|
* Format: - [ ] T###: Description | File: path/to/file
|
||||||
|
*/
|
||||||
|
export function parseTaskLine(line: string, currentPhase?: string): ParsedTask | null {
|
||||||
|
// ... moved from auto-mode-service.ts lines 271-295
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a focused prompt for executing a single task
|
||||||
|
*/
|
||||||
|
export function buildTaskPrompt(
|
||||||
|
task: ParsedTask,
|
||||||
|
allTasks: ParsedTask[],
|
||||||
|
taskIndex: number,
|
||||||
|
planContent: string,
|
||||||
|
userFeedback?: string
|
||||||
|
): string {
|
||||||
|
// ... moved from auto-mode-service.ts lines 2389-2458
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
|
## Phase 2: Extract Utility Classes
|
||||||
|
|
||||||
|
### 2.1 Create Stream Processor
|
||||||
|
|
||||||
|
Create `apps/server/src/lib/stream-processor.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { ProviderMessage } from '../providers/types.js';
|
||||||
|
|
||||||
|
export interface StreamHandlers {
|
||||||
|
onText?: (text: string) => void;
|
||||||
|
onToolUse?: (name: string, input: unknown) => void;
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
onComplete?: (result: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process provider message stream with unified handling
|
||||||
|
* Eliminates the 4x duplicated stream processing pattern
|
||||||
|
*/
|
||||||
|
export async function* processStream(
|
||||||
|
stream: AsyncGenerator<ProviderMessage>,
|
||||||
|
handlers: StreamHandlers
|
||||||
|
): AsyncGenerator<{ text: string; isComplete: boolean }> {
|
||||||
|
for await (const msg of stream) {
|
||||||
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
handlers.onText?.(block.text || '');
|
||||||
|
yield { text: block.text || '', isComplete: false };
|
||||||
|
} else if (block.type === 'tool_use') {
|
||||||
|
handlers.onToolUse?.(block.name, block.input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'error') {
|
||||||
|
handlers.onError?.(msg.error || 'Unknown error');
|
||||||
|
throw new Error(msg.error || 'Unknown error');
|
||||||
|
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
||||||
|
handlers.onComplete?.(msg.result || '');
|
||||||
|
yield { text: msg.result || '', isComplete: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Create Output Writer
|
||||||
|
|
||||||
|
Create `apps/server/src/services/auto-mode/output-writer.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incremental, debounced file writing for agent output
|
||||||
|
*/
|
||||||
|
export class OutputWriter {
|
||||||
|
private content = '';
|
||||||
|
private writeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private readonly debounceMs: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly outputPath: string,
|
||||||
|
debounceMs = 500
|
||||||
|
) {
|
||||||
|
this.debounceMs = debounceMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(text: string): void {
|
||||||
|
this.content += text;
|
||||||
|
this.scheduleWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent(): string {
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleWrite(): void {
|
||||||
|
if (this.writeTimeout) {
|
||||||
|
clearTimeout(this.writeTimeout);
|
||||||
|
}
|
||||||
|
this.writeTimeout = setTimeout(() => this.flush(), this.debounceMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
if (this.writeTimeout) {
|
||||||
|
clearTimeout(this.writeTimeout);
|
||||||
|
this.writeTimeout = null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await secureFs.mkdir(path.dirname(this.outputPath), { recursive: true });
|
||||||
|
await secureFs.writeFile(this.outputPath, this.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[OutputWriter] Failed to write: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: Extract Service Classes
|
||||||
|
|
||||||
|
### 3.1 Plan Approval Service
|
||||||
|
|
||||||
|
Create `apps/server/src/services/auto-mode/plan-approval-service.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
|
import type { PlanSpec, AutoModeEventType } from '@automaker/types';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
|
interface PendingApproval {
|
||||||
|
resolve: (result: ApprovalResult) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
featureId: string;
|
||||||
|
projectPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApprovalResult {
|
||||||
|
approved: boolean;
|
||||||
|
editedPlan?: string;
|
||||||
|
feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = createLogger('PlanApprovalService');
|
||||||
|
|
||||||
|
export class PlanApprovalService {
|
||||||
|
private pendingApprovals = new Map<string, PendingApproval>();
|
||||||
|
|
||||||
|
constructor(private events: EventEmitter) {}
|
||||||
|
|
||||||
|
waitForApproval(featureId: string, projectPath: string): Promise<ApprovalResult> {
|
||||||
|
logger.debug(`Registering pending approval for feature ${featureId}`);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pendingApprovals.set(featureId, { resolve, reject, featureId, projectPath });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolve(
|
||||||
|
featureId: string,
|
||||||
|
approved: boolean,
|
||||||
|
editedPlan?: string,
|
||||||
|
feedback?: string
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const pending = this.pendingApprovals.get(featureId);
|
||||||
|
if (!pending) {
|
||||||
|
return { success: false, error: `No pending approval for ${featureId}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.resolve({ approved, editedPlan, feedback });
|
||||||
|
this.pendingApprovals.delete(featureId);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(featureId: string): void {
|
||||||
|
const pending = this.pendingApprovals.get(featureId);
|
||||||
|
if (pending) {
|
||||||
|
pending.reject(new Error('Plan approval cancelled'));
|
||||||
|
this.pendingApprovals.delete(featureId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPending(featureId: string): boolean {
|
||||||
|
return this.pendingApprovals.has(featureId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Task Executor
|
||||||
|
|
||||||
|
Create `apps/server/src/services/auto-mode/task-executor.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { ExecuteOptions, ParsedTask } from '@automaker/types';
|
||||||
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
|
import type { BaseProvider } from '../../providers/base-provider.js';
|
||||||
|
import { buildTaskPrompt } from '@automaker/prompts';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
|
const logger = createLogger('TaskExecutor');
|
||||||
|
|
||||||
|
interface TaskExecutionContext {
|
||||||
|
workDir: string;
|
||||||
|
featureId: string;
|
||||||
|
projectPath: string;
|
||||||
|
provider: BaseProvider;
|
||||||
|
model: string;
|
||||||
|
maxTurns: number;
|
||||||
|
allowedTools?: string[];
|
||||||
|
abortController: AbortController;
|
||||||
|
planContent: string;
|
||||||
|
userFeedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskProgress {
|
||||||
|
taskId: string;
|
||||||
|
taskIndex: number;
|
||||||
|
tasksTotal: number;
|
||||||
|
status: 'started' | 'completed' | 'failed';
|
||||||
|
output?: string;
|
||||||
|
phaseComplete?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TaskExecutor {
|
||||||
|
constructor(private events: EventEmitter) {}
|
||||||
|
|
||||||
|
async *executeAll(
|
||||||
|
tasks: ParsedTask[],
|
||||||
|
context: TaskExecutionContext
|
||||||
|
): AsyncGenerator<TaskProgress> {
|
||||||
|
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
|
||||||
|
const task = tasks[taskIndex];
|
||||||
|
|
||||||
|
if (context.abortController.signal.aborted) {
|
||||||
|
throw new Error('Feature execution aborted');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Starting task ${task.id}: ${task.description}`);
|
||||||
|
yield {
|
||||||
|
taskId: task.id,
|
||||||
|
taskIndex,
|
||||||
|
tasksTotal: tasks.length,
|
||||||
|
status: 'started',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskPrompt = buildTaskPrompt(
|
||||||
|
task,
|
||||||
|
tasks,
|
||||||
|
taskIndex,
|
||||||
|
context.planContent,
|
||||||
|
context.userFeedback
|
||||||
|
);
|
||||||
|
|
||||||
|
const taskStream = context.provider.executeQuery({
|
||||||
|
prompt: taskPrompt,
|
||||||
|
model: context.model,
|
||||||
|
maxTurns: Math.min(context.maxTurns, 50),
|
||||||
|
cwd: context.workDir,
|
||||||
|
allowedTools: context.allowedTools,
|
||||||
|
abortController: context.abortController,
|
||||||
|
});
|
||||||
|
|
||||||
|
let taskOutput = '';
|
||||||
|
for await (const msg of taskStream) {
|
||||||
|
// Process stream messages...
|
||||||
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
taskOutput += block.text || '';
|
||||||
|
this.events.emit('auto-mode:event', {
|
||||||
|
type: 'auto_mode_progress',
|
||||||
|
featureId: context.featureId,
|
||||||
|
content: block.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Task ${task.id} completed`);
|
||||||
|
yield {
|
||||||
|
taskId: task.id,
|
||||||
|
taskIndex,
|
||||||
|
tasksTotal: tasks.length,
|
||||||
|
status: 'completed',
|
||||||
|
output: taskOutput,
|
||||||
|
phaseComplete: this.checkPhaseComplete(task, tasks, taskIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkPhaseComplete(
|
||||||
|
task: ParsedTask,
|
||||||
|
allTasks: ParsedTask[],
|
||||||
|
taskIndex: number
|
||||||
|
): number | undefined {
|
||||||
|
if (!task.phase) return undefined;
|
||||||
|
|
||||||
|
const nextTask = allTasks[taskIndex + 1];
|
||||||
|
if (!nextTask || nextTask.phase !== task.phase) {
|
||||||
|
const phaseMatch = task.phase.match(/Phase\s*(\d+)/i);
|
||||||
|
return phaseMatch ? parseInt(phaseMatch[1], 10) : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Worktree Manager
|
||||||
|
|
||||||
|
Create `apps/server/src/services/auto-mode/worktree-manager.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import path from 'path';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
const logger = createLogger('WorktreeManager');
|
||||||
|
|
||||||
|
export class WorktreeManager {
|
||||||
|
/**
|
||||||
|
* Find existing worktree path for a branch
|
||||||
|
*/
|
||||||
|
async findWorktreeForBranch(projectPath: string, branchName: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('git worktree list --porcelain', {
|
||||||
|
cwd: projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = stdout.split('\n');
|
||||||
|
let currentPath: string | null = null;
|
||||||
|
let currentBranch: string | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('worktree ')) {
|
||||||
|
currentPath = line.slice(9);
|
||||||
|
} else if (line.startsWith('branch ')) {
|
||||||
|
currentBranch = line.slice(7).replace('refs/heads/', '');
|
||||||
|
} else if (line === '' && currentPath && currentBranch) {
|
||||||
|
if (currentBranch === branchName) {
|
||||||
|
return path.isAbsolute(currentPath)
|
||||||
|
? path.resolve(currentPath)
|
||||||
|
: path.resolve(projectPath, currentPath);
|
||||||
|
}
|
||||||
|
currentPath = null;
|
||||||
|
currentBranch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check last entry
|
||||||
|
if (currentPath && currentBranch === branchName) {
|
||||||
|
return path.isAbsolute(currentPath)
|
||||||
|
? path.resolve(currentPath)
|
||||||
|
: path.resolve(projectPath, currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to find worktree for branch ${branchName}: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve working directory for feature execution
|
||||||
|
*/
|
||||||
|
async resolveWorkDir(
|
||||||
|
projectPath: string,
|
||||||
|
branchName: string | undefined,
|
||||||
|
useWorktrees: boolean
|
||||||
|
): Promise<{ workDir: string; worktreePath: string | null }> {
|
||||||
|
let worktreePath: string | null = null;
|
||||||
|
|
||||||
|
if (useWorktrees && branchName) {
|
||||||
|
worktreePath = await this.findWorktreeForBranch(projectPath, branchName);
|
||||||
|
if (worktreePath) {
|
||||||
|
logger.info(`Using worktree for branch "${branchName}": ${worktreePath}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Worktree for branch "${branchName}" not found, using project path`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workDir = worktreePath ? path.resolve(worktreePath) : path.resolve(projectPath);
|
||||||
|
return { workDir, worktreePath };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4: Refactor AutoModeService
|
||||||
|
|
||||||
|
### 4.1 Simplified AutoModeService
|
||||||
|
|
||||||
|
The refactored `auto-mode-service.ts` (~300-400 lines) becomes an orchestrator:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Feature, ExecuteOptions, PlanSpec, ParsedTask } from '@automaker/types';
|
||||||
|
import { createLogger, classifyError, buildPromptWithImages, loadContextFiles } from '@automaker/utils';
|
||||||
|
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
||||||
|
import { getPlanningPrompt, parseTasksFromSpec } from '@automaker/prompts';
|
||||||
|
import { getFeatureDir } from '@automaker/platform';
|
||||||
|
import { ProviderFactory } from '../providers/provider-factory.js';
|
||||||
|
import { validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
|
import type { EventEmitter } from '../lib/events.js';
|
||||||
|
|
||||||
|
import { PlanApprovalService } from './auto-mode/plan-approval-service.js';
|
||||||
|
import { TaskExecutor } from './auto-mode/task-executor.js';
|
||||||
|
import { WorktreeManager } from './auto-mode/worktree-manager.js';
|
||||||
|
import { OutputWriter } from './auto-mode/output-writer.js';
|
||||||
|
import { FeatureLoader } from './feature-loader.js';
|
||||||
|
|
||||||
|
const logger = createLogger('AutoModeService');
|
||||||
|
|
||||||
|
export class AutoModeService {
|
||||||
|
private events: EventEmitter;
|
||||||
|
private runningFeatures = new Map<string, RunningFeature>();
|
||||||
|
private featureLoader = new FeatureLoader();
|
||||||
|
private planApproval: PlanApprovalService;
|
||||||
|
private taskExecutor: TaskExecutor;
|
||||||
|
private worktreeManager: WorktreeManager;
|
||||||
|
|
||||||
|
constructor(events: EventEmitter) {
|
||||||
|
this.events = events;
|
||||||
|
this.planApproval = new PlanApprovalService(events);
|
||||||
|
this.taskExecutor = new TaskExecutor(events);
|
||||||
|
this.worktreeManager = new WorktreeManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public methods remain the same API, but delegate to sub-services
|
||||||
|
async executeFeature(...): Promise<void> {
|
||||||
|
// Validation
|
||||||
|
validateWorkingDirectory(projectPath);
|
||||||
|
|
||||||
|
// Resolve work directory via WorktreeManager
|
||||||
|
const { workDir, worktreePath } = await this.worktreeManager.resolveWorkDir(
|
||||||
|
projectPath, feature.branchName, useWorktrees
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build prompt (simplified)
|
||||||
|
const prompt = getPlanningPrompt(feature.planningMode, feature.requirePlanApproval)
|
||||||
|
+ this.buildFeaturePrompt(feature);
|
||||||
|
|
||||||
|
// Execute agent (delegated to runAgent which uses TaskExecutor)
|
||||||
|
await this.runAgent(workDir, featureId, prompt, ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan approval delegated to PlanApprovalService
|
||||||
|
waitForPlanApproval = this.planApproval.waitForApproval.bind(this.planApproval);
|
||||||
|
resolvePlanApproval = this.planApproval.resolve.bind(this.planApproval);
|
||||||
|
cancelPlanApproval = this.planApproval.cancel.bind(this.planApproval);
|
||||||
|
hasPendingApproval = this.planApproval.hasPending.bind(this.planApproval);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Changes Summary
|
||||||
|
|
||||||
|
### New Files to Create
|
||||||
|
|
||||||
|
| File | Purpose | Est. Lines |
|
||||||
|
| ------------------------------------------------------------- | -------------------------- | ---------- |
|
||||||
|
| `libs/types/src/planning.ts` | Planning types | ~50 |
|
||||||
|
| `libs/prompts/src/planning.ts` | Planning prompts & parsing | ~200 |
|
||||||
|
| `apps/server/src/lib/stream-processor.ts` | Stream utility | ~50 |
|
||||||
|
| `apps/server/src/services/auto-mode/index.ts` | Re-exports | ~10 |
|
||||||
|
| `apps/server/src/services/auto-mode/plan-approval-service.ts` | Approval logic | ~100 |
|
||||||
|
| `apps/server/src/services/auto-mode/task-executor.ts` | Task execution | ~150 |
|
||||||
|
| `apps/server/src/services/auto-mode/worktree-manager.ts` | Git worktrees | ~80 |
|
||||||
|
| `apps/server/src/services/auto-mode/output-writer.ts` | File I/O | ~60 |
|
||||||
|
| `apps/server/src/services/auto-mode/types.ts` | Internal types | ~40 |
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
| ------------------------------------------------------- | ---------------------------------- |
|
||||||
|
| `libs/types/src/index.ts` | Export planning types |
|
||||||
|
| `libs/types/src/feature.ts` | Import PlanSpec |
|
||||||
|
| `libs/prompts/src/index.ts` | Export planning functions |
|
||||||
|
| `apps/server/src/services/auto-mode-service.ts` | Refactor to orchestrator |
|
||||||
|
| `apps/ui/src/store/app-store.ts` | Import types from @automaker/types |
|
||||||
|
| `apps/ui/src/components/.../planning-mode-selector.tsx` | Import PlanningMode |
|
||||||
|
| `apps/server/tests/.../auto-mode-task-parsing.test.ts` | Import from @automaker/prompts |
|
||||||
|
|
||||||
|
### Files to Delete (after refactoring)
|
||||||
|
|
||||||
|
None - old file becomes the slim orchestrator.
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
After refactoring, verify:
|
||||||
|
|
||||||
|
- [ ] `npm run build:packages` succeeds
|
||||||
|
- [ ] `npm run lint` passes
|
||||||
|
- [ ] `npm run test:packages` passes
|
||||||
|
- [ ] `npm run test:server` passes
|
||||||
|
- [ ] `npm run test` (E2E) passes
|
||||||
|
- [ ] Feature execution works in UI
|
||||||
|
- [ ] Plan approval flow works (spec/full modes)
|
||||||
|
- [ ] Task progress events appear correctly
|
||||||
|
- [ ] Resume feature works
|
||||||
|
- [ ] Follow-up feature works
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
1. **Phase 1**: Add shared package updates (non-breaking)
|
||||||
|
2. **Phase 2**: Extract utilities (stream-processor, output-writer)
|
||||||
|
3. **Phase 3**: Extract services one at a time, keeping old code as fallback
|
||||||
|
4. **Phase 4**: Wire up orchestrator, remove old code
|
||||||
|
5. **Phase 5**: Update tests to use new imports
|
||||||
|
|
||||||
|
Each phase should be a separate PR for easier review.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
| Metric | Before | After |
|
||||||
|
| ---------------- | -------------------- | -------------------- |
|
||||||
|
| Main file lines | 2,497 | ~400 |
|
||||||
|
| Largest method | 658 lines | ~100 lines |
|
||||||
|
| Code duplication | 4x stream processing | 1 utility |
|
||||||
|
| Type safety | None for events | Full |
|
||||||
|
| Testability | Hard (monolith) | Easy (focused units) |
|
||||||
|
| Logging | console.log | createLogger |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Should `startAutoLoop`/`stopAutoLoop` remain in AutoModeService or become a separate `AutoLoopService`?
|
||||||
|
2. Should we add a `FeatureRepository` class to consolidate all feature file operations?
|
||||||
|
3. Is the recovery mechanism in `resolvePlanApproval` still needed with the refactored architecture?
|
||||||
35
libs/types/src/claude.ts
Normal file
35
libs/types/src/claude.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Claude Usage types for CLI-based usage tracking
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ClaudeUsage {
|
||||||
|
sessionTokensUsed: number;
|
||||||
|
sessionLimit: number;
|
||||||
|
sessionPercentage: number;
|
||||||
|
sessionResetTime: string; // ISO date string
|
||||||
|
sessionResetText: string; // Raw text like "Resets 10:59am (Asia/Dubai)"
|
||||||
|
|
||||||
|
weeklyTokensUsed: number;
|
||||||
|
weeklyLimit: number;
|
||||||
|
weeklyPercentage: number;
|
||||||
|
weeklyResetTime: string; // ISO date string
|
||||||
|
weeklyResetText: string; // Raw text like "Resets Dec 22 at 7:59pm (Asia/Dubai)"
|
||||||
|
|
||||||
|
sonnetWeeklyTokensUsed: number;
|
||||||
|
sonnetWeeklyPercentage: number;
|
||||||
|
sonnetResetText: string; // Raw text like "Resets Dec 27 at 9:59am (Asia/Dubai)"
|
||||||
|
|
||||||
|
costUsed: number | null;
|
||||||
|
costLimit: number | null;
|
||||||
|
costCurrency: string | null;
|
||||||
|
|
||||||
|
lastUpdated: string; // ISO date string
|
||||||
|
userTimezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaudeStatus {
|
||||||
|
indicator: {
|
||||||
|
color: 'green' | 'yellow' | 'orange' | 'red' | 'gray';
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
59
libs/types/src/github.ts
Normal file
59
libs/types/src/github.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* GitHub-related types shared across server and UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface GitHubLabel {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubAuthor {
|
||||||
|
login: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubIssue {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
author: GitHubAuthor;
|
||||||
|
createdAt: string;
|
||||||
|
labels: GitHubLabel[];
|
||||||
|
url: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubPR {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
author: GitHubAuthor;
|
||||||
|
createdAt: string;
|
||||||
|
labels: GitHubLabel[];
|
||||||
|
url: string;
|
||||||
|
isDraft: boolean;
|
||||||
|
headRefName: string;
|
||||||
|
reviewDecision: string | null;
|
||||||
|
mergeable: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubRemoteStatus {
|
||||||
|
hasGitHubRemote: boolean;
|
||||||
|
remoteUrl: string | null;
|
||||||
|
owner: string | null;
|
||||||
|
repo: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListPRsResult {
|
||||||
|
success: boolean;
|
||||||
|
openPRs?: GitHubPR[];
|
||||||
|
mergedPRs?: GitHubPR[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListIssuesResult {
|
||||||
|
success: boolean;
|
||||||
|
openIssues?: GitHubIssue[];
|
||||||
|
closedIssues?: GitHubIssue[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
@@ -93,3 +93,28 @@ export type {
|
|||||||
TaskProgressPayload,
|
TaskProgressPayload,
|
||||||
PlanApprovalPayload,
|
PlanApprovalPayload,
|
||||||
} from './planning.js';
|
} from './planning.js';
|
||||||
|
|
||||||
|
// GitHub types
|
||||||
|
export type {
|
||||||
|
GitHubLabel,
|
||||||
|
GitHubAuthor,
|
||||||
|
GitHubIssue,
|
||||||
|
GitHubPR,
|
||||||
|
GitHubRemoteStatus,
|
||||||
|
ListPRsResult,
|
||||||
|
ListIssuesResult,
|
||||||
|
} from './github.js';
|
||||||
|
|
||||||
|
// Worktree types
|
||||||
|
export type {
|
||||||
|
WorktreePRInfo,
|
||||||
|
WorktreeMetadata,
|
||||||
|
WorktreeListItem,
|
||||||
|
PRComment,
|
||||||
|
PRInfo,
|
||||||
|
DevServerInfo,
|
||||||
|
TrackedBranch,
|
||||||
|
} from './worktree.js';
|
||||||
|
|
||||||
|
// Claude usage types
|
||||||
|
export type { ClaudeUsage, ClaudeStatus } from './claude.js';
|
||||||
|
|||||||
61
libs/types/src/worktree.ts
Normal file
61
libs/types/src/worktree.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Worktree-related types shared across server and UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WorktreePRInfo {
|
||||||
|
number: number;
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorktreeMetadata {
|
||||||
|
branch: string;
|
||||||
|
createdAt: string;
|
||||||
|
pr?: WorktreePRInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorktreeListItem {
|
||||||
|
path: string;
|
||||||
|
branch: string;
|
||||||
|
isMain: boolean;
|
||||||
|
isCurrent: boolean;
|
||||||
|
hasWorktree: boolean;
|
||||||
|
hasChanges?: boolean;
|
||||||
|
changedFilesCount?: number;
|
||||||
|
pr?: WorktreePRInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PRComment {
|
||||||
|
id: number;
|
||||||
|
author: string;
|
||||||
|
body: string;
|
||||||
|
path?: string;
|
||||||
|
line?: number;
|
||||||
|
createdAt: string;
|
||||||
|
isReviewComment: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PRInfo {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
state: string;
|
||||||
|
author: string;
|
||||||
|
body: string;
|
||||||
|
comments: PRComment[];
|
||||||
|
reviewComments: PRComment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DevServerInfo {
|
||||||
|
worktreePath: string;
|
||||||
|
port: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrackedBranch {
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastActivatedAt?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user