mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: enhance project initialization and improve logging in auto mode service
- Added a default categories.json file to the project initialization structure. - Improved code formatting and readability in the auto-mode-service.ts file by restructuring console log statements and method calls. - Updated feature status checks to include "backlog" in addition to "pending" and "ready".
This commit is contained in:
@@ -19,6 +19,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { useFileBrowser } from "@/contexts/file-browser-context";
|
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface InterviewMessage {
|
interface InterviewMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -290,7 +291,8 @@ export function InterviewView() {
|
|||||||
const handleSelectDirectory = async () => {
|
const handleSelectDirectory = async () => {
|
||||||
const selectedPath = await openFileBrowser({
|
const selectedPath = await openFileBrowser({
|
||||||
title: "Select Base Directory",
|
title: "Select Base Directory",
|
||||||
description: "Choose the parent directory where your new project will be created",
|
description:
|
||||||
|
"Choose the parent directory where your new project will be created",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedPath) {
|
if (selectedPath) {
|
||||||
@@ -306,12 +308,23 @@ export function InterviewView() {
|
|||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
// Use platform-specific path separator
|
// Use platform-specific path separator
|
||||||
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
const pathSep =
|
||||||
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
typeof window !== "undefined" && (window as any).electronAPI
|
||||||
|
? navigator.platform.indexOf("Win") !== -1
|
||||||
|
? "\\"
|
||||||
|
: "/"
|
||||||
|
: "/";
|
||||||
const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
|
const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
await api.mkdir(fullProjectPath);
|
const mkdirResult = await api.mkdir(fullProjectPath);
|
||||||
|
if (!mkdirResult.success) {
|
||||||
|
toast.error("Failed to create project directory", {
|
||||||
|
description: mkdirResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
setIsGenerating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Write app_spec.txt with generated content
|
// Write app_spec.txt with generated content
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
|
|||||||
@@ -236,7 +236,13 @@ export function WelcomeView() {
|
|||||||
const projectPath = `${parentDir}/${projectName}`;
|
const projectPath = `${parentDir}/${projectName}`;
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
await api.mkdir(projectPath);
|
const mkdirResult = await api.mkdir(projectPath);
|
||||||
|
if (!mkdirResult.success) {
|
||||||
|
toast.error("Failed to create project directory", {
|
||||||
|
description: mkdirResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize .automaker directory with all necessary files
|
// Initialize .automaker directory with all necessary files
|
||||||
const initResult = await initializeProject(projectPath);
|
const initResult = await initializeProject(projectPath);
|
||||||
|
|||||||
@@ -33,16 +33,21 @@ export function useAutoMode() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Helper to look up project ID from path
|
// Helper to look up project ID from path
|
||||||
const getProjectIdFromPath = useCallback((path: string): string | undefined => {
|
const getProjectIdFromPath = useCallback(
|
||||||
const project = projects.find(p => p.path === path);
|
(path: string): string | undefined => {
|
||||||
return project?.id;
|
const project = projects.find((p) => p.path === path);
|
||||||
}, [projects]);
|
return project?.id;
|
||||||
|
},
|
||||||
|
[projects]
|
||||||
|
);
|
||||||
|
|
||||||
// Get project-specific auto mode state
|
// Get project-specific auto mode state
|
||||||
const projectId = currentProject?.id;
|
const projectId = currentProject?.id;
|
||||||
const projectAutoModeState = useMemo(() => {
|
const projectAutoModeState = useMemo(() => {
|
||||||
if (!projectId) return { isRunning: false, runningTasks: [] };
|
if (!projectId) return { isRunning: false, runningTasks: [] };
|
||||||
return autoModeByProject[projectId] || { isRunning: false, runningTasks: [] };
|
return (
|
||||||
|
autoModeByProject[projectId] || { isRunning: false, runningTasks: [] }
|
||||||
|
);
|
||||||
}, [autoModeByProject, projectId]);
|
}, [autoModeByProject, projectId]);
|
||||||
|
|
||||||
const isAutoModeRunning = projectAutoModeState.isRunning;
|
const isAutoModeRunning = projectAutoModeState.isRunning;
|
||||||
@@ -62,10 +67,10 @@ export function useAutoMode() {
|
|||||||
// Events include projectPath from backend - use it to look up project ID
|
// Events include projectPath from backend - use it to look up project ID
|
||||||
// Fall back to current projectId if not provided in event
|
// Fall back to current projectId if not provided in event
|
||||||
let eventProjectId: string | undefined;
|
let eventProjectId: string | undefined;
|
||||||
if ('projectPath' in event && event.projectPath) {
|
if ("projectPath" in event && event.projectPath) {
|
||||||
eventProjectId = getProjectIdFromPath(event.projectPath);
|
eventProjectId = getProjectIdFromPath(event.projectPath);
|
||||||
}
|
}
|
||||||
if (!eventProjectId && 'projectId' in event && event.projectId) {
|
if (!eventProjectId && "projectId" in event && event.projectId) {
|
||||||
eventProjectId = event.projectId;
|
eventProjectId = event.projectId;
|
||||||
}
|
}
|
||||||
if (!eventProjectId) {
|
if (!eventProjectId) {
|
||||||
@@ -74,7 +79,10 @@ export function useAutoMode() {
|
|||||||
|
|
||||||
// Skip event if we couldn't determine the project
|
// Skip event if we couldn't determine the project
|
||||||
if (!eventProjectId) {
|
if (!eventProjectId) {
|
||||||
console.warn("[AutoMode] Could not determine project for event:", event);
|
console.warn(
|
||||||
|
"[AutoMode] Could not determine project for event:",
|
||||||
|
event
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,20 +119,41 @@ export function useAutoMode() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_complete":
|
case "auto_mode_stopped":
|
||||||
// All features completed for this project
|
// Auto mode was explicitly stopped (by user or error)
|
||||||
setAutoModeRunning(eventProjectId, false);
|
setAutoModeRunning(eventProjectId, false);
|
||||||
clearRunningTasks(eventProjectId);
|
clearRunningTasks(eventProjectId);
|
||||||
console.log("[AutoMode] All features completed!");
|
console.log("[AutoMode] Auto mode stopped");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_mode_started":
|
||||||
|
// Auto mode started - ensure UI reflects running state
|
||||||
|
console.log("[AutoMode] Auto mode started:", event.message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_mode_idle":
|
||||||
|
// Auto mode is running but has no pending features to pick up
|
||||||
|
// This is NOT a stop - auto mode keeps running and will pick up new features
|
||||||
|
console.log("[AutoMode] Auto mode idle - waiting for new features");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_mode_complete":
|
||||||
|
// Legacy event - only handle if it looks like a stop (for backwards compatibility)
|
||||||
|
if (event.message === "Auto mode stopped") {
|
||||||
|
setAutoModeRunning(eventProjectId, false);
|
||||||
|
clearRunningTasks(eventProjectId);
|
||||||
|
console.log("[AutoMode] Auto mode stopped (legacy event)");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_error":
|
case "auto_mode_error":
|
||||||
console.error("[AutoMode Error]", event.error);
|
console.error("[AutoMode Error]", event.error);
|
||||||
if (event.featureId && event.error) {
|
if (event.featureId && event.error) {
|
||||||
// Check for authentication errors and provide a more helpful message
|
// Check for authentication errors and provide a more helpful message
|
||||||
const isAuthError = event.errorType === "authentication" ||
|
const isAuthError =
|
||||||
event.error.includes("Authentication failed") ||
|
event.errorType === "authentication" ||
|
||||||
event.error.includes("Invalid API key");
|
event.error.includes("Authentication failed") ||
|
||||||
|
event.error.includes("Invalid API key");
|
||||||
|
|
||||||
const errorMessage = isAuthError
|
const errorMessage = isAuthError
|
||||||
? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.`
|
? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.`
|
||||||
@@ -202,11 +231,12 @@ export function useAutoMode() {
|
|||||||
if (!api?.autoMode) return;
|
if (!api?.autoMode) return;
|
||||||
|
|
||||||
// Find all projects that have auto mode marked as running
|
// Find all projects that have auto mode marked as running
|
||||||
const projectsToRestart: Array<{ projectId: string; projectPath: string }> = [];
|
const projectsToRestart: Array<{ projectId: string; projectPath: string }> =
|
||||||
|
[];
|
||||||
for (const [projectId, state] of Object.entries(autoModeByProject)) {
|
for (const [projectId, state] of Object.entries(autoModeByProject)) {
|
||||||
if (state.isRunning) {
|
if (state.isRunning) {
|
||||||
// Find the project path for this project ID
|
// Find the project path for this project ID
|
||||||
const project = projects.find(p => p.id === projectId);
|
const project = projects.find((p) => p.id === projectId);
|
||||||
if (project) {
|
if (project) {
|
||||||
projectsToRestart.push({ projectId, projectPath: project.path });
|
projectsToRestart.push({ projectId, projectPath: project.path });
|
||||||
}
|
}
|
||||||
@@ -216,18 +246,27 @@ export function useAutoMode() {
|
|||||||
// Restart auto mode for each project
|
// Restart auto mode for each project
|
||||||
for (const { projectId, projectPath } of projectsToRestart) {
|
for (const { projectId, projectPath } of projectsToRestart) {
|
||||||
console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`);
|
console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`);
|
||||||
api.autoMode.start(projectPath, maxConcurrency).then(result => {
|
api.autoMode
|
||||||
if (!result.success) {
|
.start(projectPath, maxConcurrency)
|
||||||
console.error(`[AutoMode] Failed to restore auto mode for ${projectPath}:`, result.error);
|
.then((result) => {
|
||||||
// Mark as not running if we couldn't restart
|
if (!result.success) {
|
||||||
|
console.error(
|
||||||
|
`[AutoMode] Failed to restore auto mode for ${projectPath}:`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
// Mark as not running if we couldn't restart
|
||||||
|
setAutoModeRunning(projectId, false);
|
||||||
|
} else {
|
||||||
|
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
`[AutoMode] Error restoring auto mode for ${projectPath}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
setAutoModeRunning(projectId, false);
|
setAutoModeRunning(projectId, false);
|
||||||
} else {
|
});
|
||||||
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error(`[AutoMode] Error restoring auto mode for ${projectPath}:`, error);
|
|
||||||
setAutoModeRunning(projectId, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Only run once on mount - intentionally empty dependency array
|
// Only run once on mount - intentionally empty dependency array
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -246,11 +285,16 @@ export function useAutoMode() {
|
|||||||
throw new Error("Auto mode API not available");
|
throw new Error("Auto mode API not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.autoMode.start(currentProject.path, maxConcurrency);
|
const result = await api.autoMode.start(
|
||||||
|
currentProject.path,
|
||||||
|
maxConcurrency
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setAutoModeRunning(currentProject.id, true);
|
setAutoModeRunning(currentProject.id, true);
|
||||||
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`);
|
console.log(
|
||||||
|
`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to start:", result.error);
|
console.error("[AutoMode] Failed to start:", result.error);
|
||||||
throw new Error(result.error || "Failed to start auto mode");
|
throw new Error(result.error || "Failed to start auto mode");
|
||||||
@@ -285,7 +329,9 @@ export function useAutoMode() {
|
|||||||
// Stopping auto mode only turns off the toggle to prevent new features
|
// Stopping auto mode only turns off the toggle to prevent new features
|
||||||
// from being picked up. Running tasks will complete naturally and be
|
// from being picked up. Running tasks will complete naturally and be
|
||||||
// removed via the auto_mode_feature_complete event.
|
// removed via the auto_mode_feature_complete event.
|
||||||
console.log("[AutoMode] Stopped successfully - running tasks will continue");
|
console.log(
|
||||||
|
"[AutoMode] Stopped successfully - running tasks will continue"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to stop:", result.error);
|
console.error("[AutoMode] Failed to stop:", result.error);
|
||||||
throw new Error(result.error || "Failed to stop auto mode");
|
throw new Error(result.error || "Failed to stop auto mode");
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const REQUIRED_STRUCTURE: {
|
|||||||
".automaker/features",
|
".automaker/features",
|
||||||
".automaker/images",
|
".automaker/images",
|
||||||
],
|
],
|
||||||
files: {},
|
files: {
|
||||||
|
".automaker/categories.json": "[]",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
18
apps/app/src/types/electron.d.ts
vendored
18
apps/app/src/types/electron.d.ts
vendored
@@ -203,6 +203,24 @@ export type AutoModeEvent =
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_stopped";
|
||||||
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_started";
|
||||||
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_idle";
|
||||||
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_phase";
|
type: "auto_mode_phase";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ export function initAllowedPaths(): void {
|
|||||||
if (dataDir) {
|
if (dataDir) {
|
||||||
allowedPaths.add(path.resolve(dataDir));
|
allowedPaths.add(path.resolve(dataDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always allow the workspace directory (where projects are created)
|
||||||
|
const workspaceDir = process.env.WORKSPACE_DIR;
|
||||||
|
if (workspaceDir) {
|
||||||
|
allowedPaths.add(path.resolve(workspaceDir));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +64,9 @@ export function validatePath(filePath: string): string {
|
|||||||
const resolved = path.resolve(filePath);
|
const resolved = path.resolve(filePath);
|
||||||
|
|
||||||
if (!isPathAllowed(resolved)) {
|
if (!isPathAllowed(resolved)) {
|
||||||
throw new Error(`Access denied: ${filePath} is not in an allowed directory`);
|
throw new Error(
|
||||||
|
`Access denied: ${filePath} is not in an allowed directory`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { Router, type Request, type Response } from "express";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js";
|
import {
|
||||||
|
validatePath,
|
||||||
|
addAllowedPath,
|
||||||
|
isPathAllowed,
|
||||||
|
} from "../lib/security.js";
|
||||||
import type { EventEmitter } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
|
|
||||||
export function createFsRoutes(_events: EventEmitter): Router {
|
export function createFsRoutes(_events: EventEmitter): Router {
|
||||||
@@ -69,9 +73,41 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(dirPath);
|
const resolvedPath = path.resolve(dirPath);
|
||||||
|
|
||||||
|
// Security check: allow paths in allowed directories OR within home directory
|
||||||
|
const isAllowed = (() => {
|
||||||
|
// Check if path or parent is in allowed paths
|
||||||
|
if (isPathAllowed(resolvedPath)) return true;
|
||||||
|
const parentPath = path.dirname(resolvedPath);
|
||||||
|
if (isPathAllowed(parentPath)) return true;
|
||||||
|
|
||||||
|
// Also allow within home directory (like the /browse endpoint)
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
const normalizedHome = path.normalize(homeDir);
|
||||||
|
if (
|
||||||
|
resolvedPath === normalizedHome ||
|
||||||
|
resolvedPath.startsWith(normalizedHome + path.sep)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: `Access denied: ${dirPath} is not in an allowed directory`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await fs.mkdir(resolvedPath, { recursive: true });
|
await fs.mkdir(resolvedPath, { recursive: true });
|
||||||
|
|
||||||
|
// Add the new directory to allowed paths so subsequent operations work
|
||||||
|
addAllowedPath(resolvedPath);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -197,7 +233,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const stats = await fs.stat(resolvedPath);
|
const stats = await fs.stat(resolvedPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Path is not a directory" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +267,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!directoryName) {
|
if (!directoryName) {
|
||||||
res.status(400).json({ success: false, error: "directoryName is required" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "directoryName is required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +294,16 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const searchPaths: string[] = [
|
const searchPaths: string[] = [
|
||||||
process.cwd(), // Current working directory
|
process.cwd(), // Current working directory
|
||||||
process.env.HOME || process.env.USERPROFILE || "", // User home
|
process.env.HOME || process.env.USERPROFILE || "", // User home
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Documents"),
|
path.join(
|
||||||
|
process.env.HOME || process.env.USERPROFILE || "",
|
||||||
|
"Documents"
|
||||||
|
),
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
||||||
// Common project locations
|
// Common project locations
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Projects"),
|
path.join(
|
||||||
|
process.env.HOME || process.env.USERPROFILE || "",
|
||||||
|
"Projects"
|
||||||
|
),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
// Also check parent of current working directory
|
// Also check parent of current working directory
|
||||||
@@ -275,7 +321,7 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
try {
|
try {
|
||||||
const candidatePath = path.join(searchPath, directoryName);
|
const candidatePath = path.join(searchPath, directoryName);
|
||||||
const stats = await fs.stat(candidatePath);
|
const stats = await fs.stat(candidatePath);
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
// Verify it matches by checking for sample files
|
// Verify it matches by checking for sample files
|
||||||
if (sampleFiles && sampleFiles.length > 0) {
|
if (sampleFiles && sampleFiles.length > 0) {
|
||||||
@@ -284,8 +330,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
// Remove directory name prefix from sample file path
|
// Remove directory name prefix from sample file path
|
||||||
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
||||||
? sampleFile.substring(directoryName.length + 1)
|
? sampleFile.substring(directoryName.length + 1)
|
||||||
: sampleFile.split("/").slice(1).join("/") || sampleFile.split("/").pop() || sampleFile;
|
: sampleFile.split("/").slice(1).join("/") ||
|
||||||
|
sampleFile.split("/").pop() ||
|
||||||
|
sampleFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(candidatePath, relativeFile);
|
const filePath = path.join(candidatePath, relativeFile);
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
@@ -294,7 +342,7 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
// File doesn't exist, continue checking
|
// File doesn't exist, continue checking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If at least one file matches, consider it a match
|
// If at least one file matches, consider it a match
|
||||||
if (matches === 0 && sampleFiles.length > 0) {
|
if (matches === 0 && sampleFiles.length > 0) {
|
||||||
continue; // Try next candidate
|
continue; // Try next candidate
|
||||||
@@ -405,7 +453,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const stats = await fs.stat(targetPath);
|
const stats = await fs.stat(targetPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Path is not a directory" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +488,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Failed to read directory",
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to read directory",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -464,8 +515,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const fullPath = path.isAbsolute(imagePath)
|
const fullPath = path.isAbsolute(imagePath)
|
||||||
? imagePath
|
? imagePath
|
||||||
: projectPath
|
: projectPath
|
||||||
? path.join(projectPath, imagePath)
|
? path.join(projectPath, imagePath)
|
||||||
: imagePath;
|
: imagePath;
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
try {
|
try {
|
||||||
@@ -490,7 +541,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
".bmp": "image/bmp",
|
".bmp": "image/bmp",
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
|
res.setHeader(
|
||||||
|
"Content-Type",
|
||||||
|
mimeTypes[ext] || "application/octet-stream"
|
||||||
|
);
|
||||||
res.setHeader("Cache-Control", "public, max-age=3600");
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
||||||
res.send(buffer);
|
res.send(buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -546,38 +600,42 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Delete board background image
|
// Delete board background image
|
||||||
router.post("/delete-board-background", async (req: Request, res: Response) => {
|
router.post(
|
||||||
try {
|
"/delete-board-background",
|
||||||
const { projectPath } = req.body as { projectPath: string };
|
async (req: Request, res: Response) => {
|
||||||
|
|
||||||
if (!projectPath) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: "projectPath is required",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boardDir = path.join(projectPath, ".automaker", "board");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to remove all files in the board directory
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
const files = await fs.readdir(boardDir);
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith("background")) {
|
|
||||||
await fs.unlink(path.join(boardDir, file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Directory may not exist, that's fine
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true });
|
if (!projectPath) {
|
||||||
} catch (error) {
|
res.status(400).json({
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
success: false,
|
||||||
res.status(500).json({ success: false, error: message });
|
error: "projectPath is required",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardDir = path.join(projectPath, ".automaker", "board");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to remove all files in the board directory
|
||||||
|
const files = await fs.readdir(boardDir);
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith("background")) {
|
||||||
|
await fs.unlink(path.join(boardDir, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Directory may not exist, that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
|
res.status(500).json({ success: false, error: message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Browse directories for file picker
|
// Browse directories for file picker
|
||||||
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
||||||
@@ -614,7 +672,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const normalizedHome = path.resolve(homeDir);
|
const normalizedHome = path.resolve(homeDir);
|
||||||
|
|
||||||
// Allow browsing within home directory
|
// Allow browsing within home directory
|
||||||
if (resolved === normalizedHome || resolved.startsWith(normalizedHome + path.sep)) {
|
if (
|
||||||
|
resolved === normalizedHome ||
|
||||||
|
resolved.startsWith(normalizedHome + path.sep)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +707,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
if (!isSafePath(targetPath)) {
|
if (!isSafePath(targetPath)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Access denied: browsing is restricted to your home directory and allowed project paths",
|
error:
|
||||||
|
"Access denied: browsing is restricted to your home directory and allowed project paths",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -655,7 +717,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const stats = await fs.stat(targetPath);
|
const stats = await fs.stat(targetPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Path is not a directory" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +752,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Failed to read directory",
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to read directory",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -32,7 +32,15 @@ interface Feature {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
spec?: string;
|
spec?: string;
|
||||||
model?: string; // Model to use for this feature
|
model?: string; // Model to use for this feature
|
||||||
imagePaths?: Array<string | { path: string; filename?: string; mimeType?: string; [key: string]: unknown }>;
|
imagePaths?: Array<
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
path: string;
|
||||||
|
filename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RunningFeature {
|
interface RunningFeature {
|
||||||
@@ -78,7 +86,7 @@ export class AutoModeService {
|
|||||||
projectPath,
|
projectPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emitAutoModeEvent("auto_mode_complete", {
|
this.emitAutoModeEvent("auto_mode_started", {
|
||||||
message: `Auto mode started with max ${maxConcurrency} concurrent features`,
|
message: `Auto mode started with max ${maxConcurrency} concurrent features`,
|
||||||
projectPath,
|
projectPath,
|
||||||
});
|
});
|
||||||
@@ -111,8 +119,9 @@ export class AutoModeService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (pendingFeatures.length === 0) {
|
if (pendingFeatures.length === 0) {
|
||||||
this.emitAutoModeEvent("auto_mode_complete", {
|
this.emitAutoModeEvent("auto_mode_idle", {
|
||||||
message: "No pending features - auto mode idle",
|
message: "No pending features - auto mode idle",
|
||||||
|
projectPath: this.config!.projectPath,
|
||||||
});
|
});
|
||||||
await this.sleep(10000);
|
await this.sleep(10000);
|
||||||
continue;
|
continue;
|
||||||
@@ -143,8 +152,9 @@ export class AutoModeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.autoLoopRunning = false;
|
this.autoLoopRunning = false;
|
||||||
this.emitAutoModeEvent("auto_mode_complete", {
|
this.emitAutoModeEvent("auto_mode_stopped", {
|
||||||
message: "Auto mode stopped",
|
message: "Auto mode stopped",
|
||||||
|
projectPath: this.config?.projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,10 +240,19 @@ export class AutoModeService {
|
|||||||
|
|
||||||
// Get model from feature
|
// Get model from feature
|
||||||
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
|
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
|
||||||
console.log(`[AutoMode] Executing feature ${featureId} with model: ${model}`);
|
console.log(
|
||||||
|
`[AutoMode] Executing feature ${featureId} with model: ${model}`
|
||||||
|
);
|
||||||
|
|
||||||
// Run the agent with the feature's model and images
|
// Run the agent with the feature's model and images
|
||||||
await this.runAgent(workDir, featureId, prompt, abortController, imagePaths, model);
|
await this.runAgent(
|
||||||
|
workDir,
|
||||||
|
featureId,
|
||||||
|
prompt,
|
||||||
|
abortController,
|
||||||
|
imagePaths,
|
||||||
|
model
|
||||||
|
);
|
||||||
|
|
||||||
// Mark as waiting_approval for user review
|
// Mark as waiting_approval for user review
|
||||||
await this.updateFeatureStatus(
|
await this.updateFeatureStatus(
|
||||||
@@ -422,7 +441,9 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
try {
|
try {
|
||||||
// Get model from feature (already loaded above)
|
// Get model from feature (already loaded above)
|
||||||
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
|
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
|
||||||
console.log(`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`);
|
console.log(
|
||||||
|
`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`
|
||||||
|
);
|
||||||
|
|
||||||
// Update feature status to in_progress
|
// Update feature status to in_progress
|
||||||
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
||||||
@@ -458,9 +479,11 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
filename
|
filename
|
||||||
);
|
);
|
||||||
copiedImagePaths.push(relativePath);
|
copiedImagePaths.push(relativePath);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[AutoMode] Failed to copy follow-up image ${imagePath}:`, error);
|
console.error(
|
||||||
|
`[AutoMode] Failed to copy follow-up image ${imagePath}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,7 +529,14 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use fullPrompt (already built above) with model and all images
|
// Use fullPrompt (already built above) with model and all images
|
||||||
await this.runAgent(workDir, featureId, fullPrompt, abortController, allImagePaths.length > 0 ? allImagePaths : imagePaths, model);
|
await this.runAgent(
|
||||||
|
workDir,
|
||||||
|
featureId,
|
||||||
|
fullPrompt,
|
||||||
|
abortController,
|
||||||
|
allImagePaths.length > 0 ? allImagePaths : imagePaths,
|
||||||
|
model
|
||||||
|
);
|
||||||
|
|
||||||
// Mark as waiting_approval for user review
|
// Mark as waiting_approval for user review
|
||||||
await this.updateFeatureStatus(
|
await this.updateFeatureStatus(
|
||||||
@@ -717,7 +747,10 @@ Format your response as a structured markdown document.`;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use default Claude model for analysis (can be overridden in the future)
|
// Use default Claude model for analysis (can be overridden in the future)
|
||||||
const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude);
|
const analysisModel = resolveModelString(
|
||||||
|
undefined,
|
||||||
|
DEFAULT_MODELS.claude
|
||||||
|
);
|
||||||
const provider = ProviderFactory.getProviderForModel(analysisModel);
|
const provider = ProviderFactory.getProviderForModel(analysisModel);
|
||||||
|
|
||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
@@ -917,7 +950,11 @@ Format your response as a structured markdown document.`;
|
|||||||
try {
|
try {
|
||||||
const data = await fs.readFile(featurePath, "utf-8");
|
const data = await fs.readFile(featurePath, "utf-8");
|
||||||
const feature = JSON.parse(data);
|
const feature = JSON.parse(data);
|
||||||
if (feature.status === "pending" || feature.status === "ready") {
|
if (
|
||||||
|
feature.status === "pending" ||
|
||||||
|
feature.status === "ready" ||
|
||||||
|
feature.status === "backlog"
|
||||||
|
) {
|
||||||
features.push(feature);
|
features.push(feature);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -998,9 +1035,15 @@ ${feature.spec}
|
|||||||
const imagesList = feature.imagePaths
|
const imagesList = feature.imagePaths
|
||||||
.map((img, idx) => {
|
.map((img, idx) => {
|
||||||
const path = typeof img === "string" ? img : img.path;
|
const path = typeof img === "string" ? img : img.path;
|
||||||
const filename = typeof img === "string" ? path.split("/").pop() : img.filename || path.split("/").pop();
|
const filename =
|
||||||
const mimeType = typeof img === "string" ? "image/*" : img.mimeType || "image/*";
|
typeof img === "string"
|
||||||
return ` ${idx + 1}. ${filename} (${mimeType})\n Path: ${path}`;
|
? path.split("/").pop()
|
||||||
|
: img.filename || path.split("/").pop();
|
||||||
|
const mimeType =
|
||||||
|
typeof img === "string" ? "image/*" : img.mimeType || "image/*";
|
||||||
|
return ` ${
|
||||||
|
idx + 1
|
||||||
|
}. ${filename} (${mimeType})\n Path: ${path}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
@@ -1038,7 +1081,9 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
model?: string
|
model?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
|
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
|
||||||
console.log(`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`);
|
console.log(
|
||||||
|
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
|
||||||
|
);
|
||||||
|
|
||||||
// Get provider for this model
|
// Get provider for this model
|
||||||
const provider = ProviderFactory.getProviderForModel(finalModel);
|
const provider = ProviderFactory.getProviderForModel(finalModel);
|
||||||
@@ -1060,14 +1105,7 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
model: finalModel,
|
model: finalModel,
|
||||||
maxTurns: 50,
|
maxTurns: 50,
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
allowedTools: [
|
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
],
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1089,12 +1127,15 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
responseText = block.text || "";
|
responseText = block.text || "";
|
||||||
|
|
||||||
// Check for authentication errors in the response
|
// Check for authentication errors in the response
|
||||||
if (block.text && (block.text.includes("Invalid API key") ||
|
if (
|
||||||
|
block.text &&
|
||||||
|
(block.text.includes("Invalid API key") ||
|
||||||
block.text.includes("authentication_failed") ||
|
block.text.includes("authentication_failed") ||
|
||||||
block.text.includes("Fix external API key"))) {
|
block.text.includes("Fix external API key"))
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Authentication failed: Invalid or expired API key. " +
|
"Authentication failed: Invalid or expired API key. " +
|
||||||
"Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate."
|
"Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user