mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
chore: update project management and API integration
- Added new scripts for server development and full application startup in package.json. - Enhanced project management by checking for existing projects to avoid duplicates. - Improved API integration with better error handling and connection checks in the Electron API. - Updated UI components to reflect changes in project and session management. - Refactored authentication status display to include more detailed information on methods used.
This commit is contained in:
132
apps/server/src/routes/agent.ts
Normal file
132
apps/server/src/routes/agent.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Agent routes - HTTP API for Claude agent interactions
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { AgentService } from "../services/agent-service.js";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
|
||||
export function createAgentRoutes(
|
||||
agentService: AgentService,
|
||||
_events: EventEmitter
|
||||
): Router {
|
||||
const router = Router();
|
||||
|
||||
// Start a conversation
|
||||
router.post("/start", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId, workingDirectory } = req.body as {
|
||||
sessionId: string;
|
||||
workingDirectory?: string;
|
||||
};
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: "sessionId is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentService.startConversation({
|
||||
sessionId,
|
||||
workingDirectory,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Send a message
|
||||
router.post("/send", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId, message, workingDirectory, imagePaths } = req.body as {
|
||||
sessionId: string;
|
||||
message: string;
|
||||
workingDirectory?: string;
|
||||
imagePaths?: string[];
|
||||
};
|
||||
|
||||
if (!sessionId || !message) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "sessionId and message are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the message processing (don't await - it streams via WebSocket)
|
||||
agentService
|
||||
.sendMessage({
|
||||
sessionId,
|
||||
message,
|
||||
workingDirectory,
|
||||
imagePaths,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[Agent Route] Error sending message:", error);
|
||||
});
|
||||
|
||||
// Return immediately - responses come via WebSocket
|
||||
res.json({ success: true, message: "Message sent" });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get conversation history
|
||||
router.post("/history", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: "sessionId is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = agentService.getHistory(sessionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop execution
|
||||
router.post("/stop", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: "sessionId is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentService.stopExecution(sessionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Clear conversation
|
||||
router.post("/clear", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: "sessionId is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentService.clearSession(sessionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
263
apps/server/src/routes/auto-mode.ts
Normal file
263
apps/server/src/routes/auto-mode.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Auto Mode routes - HTTP API for autonomous feature implementation
|
||||
*
|
||||
* Uses the AutoModeService for real feature execution with Claude Agent SDK
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
import { AutoModeService } from "../services/auto-mode-service.js";
|
||||
|
||||
export function createAutoModeRoutes(events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
const autoModeService = new AutoModeService(events);
|
||||
|
||||
// Start auto mode loop
|
||||
router.post("/start", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, maxConcurrency } = req.body as {
|
||||
projectPath: string;
|
||||
maxConcurrency?: number;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop auto mode loop
|
||||
router.post("/stop", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const runningCount = await autoModeService.stopAutoLoop();
|
||||
res.json({ success: true, runningFeatures: runningCount });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop a specific feature
|
||||
router.post("/stop-feature", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { featureId } = req.body as { featureId: string };
|
||||
|
||||
if (!featureId) {
|
||||
res.status(400).json({ success: false, error: "featureId is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const stopped = await autoModeService.stopFeature(featureId);
|
||||
res.json({ success: true, stopped });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get auto mode status
|
||||
router.post("/status", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const status = autoModeService.getStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
...status,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Run a single feature
|
||||
router.post("/run-feature", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId, useWorktrees } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
useWorktrees?: boolean;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start execution in background
|
||||
autoModeService
|
||||
.executeFeature(projectPath, featureId, useWorktrees ?? true, false)
|
||||
.catch((error) => {
|
||||
console.error(`[AutoMode] Feature ${featureId} error:`, error);
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify a feature
|
||||
router.post("/verify-feature", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const passes = await autoModeService.verifyFeature(projectPath, featureId);
|
||||
res.json({ success: true, passes });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Resume a feature
|
||||
router.post("/resume-feature", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId, useWorktrees } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
useWorktrees?: boolean;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start resume in background
|
||||
autoModeService
|
||||
.resumeFeature(projectPath, featureId, useWorktrees ?? true)
|
||||
.catch((error) => {
|
||||
console.error(`[AutoMode] Resume feature ${featureId} error:`, error);
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if context exists for a feature
|
||||
router.post("/context-exists", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = await autoModeService.contextExists(projectPath, featureId);
|
||||
res.json({ success: true, exists });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze project
|
||||
router.post("/analyze-project", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start analysis in background
|
||||
autoModeService.analyzeProject(projectPath).catch((error) => {
|
||||
console.error(`[AutoMode] Project analysis error:`, error);
|
||||
});
|
||||
|
||||
res.json({ success: true, message: "Project analysis started" });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Follow up on a feature
|
||||
router.post("/follow-up-feature", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId, prompt, imagePaths } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
prompt: string;
|
||||
imagePaths?: string[];
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId || !prompt) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath, featureId, and prompt are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start follow-up in background
|
||||
autoModeService
|
||||
.followUpFeature(projectPath, featureId, prompt, imagePaths)
|
||||
.catch((error) => {
|
||||
console.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Commit feature changes
|
||||
router.post("/commit-feature", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const commitHash = await autoModeService.commitFeature(projectPath, featureId);
|
||||
res.json({ success: true, commitHash });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
159
apps/server/src/routes/features.ts
Normal file
159
apps/server/src/routes/features.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Features routes - HTTP API for feature management
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { FeatureLoader, type Feature } from "../services/feature-loader.js";
|
||||
import { addAllowedPath } from "../lib/security.js";
|
||||
|
||||
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
|
||||
const router = Router();
|
||||
|
||||
// List all features for a project
|
||||
router.post("/list", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
const features = await featureLoader.getAll(projectPath);
|
||||
res.json({ success: true, features });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single feature
|
||||
router.post("/get", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const feature = await featureLoader.get(projectPath, featureId);
|
||||
if (!feature) {
|
||||
res.status(404).json({ success: false, error: "Feature not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, feature });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new feature
|
||||
router.post("/create", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, feature } = req.body as {
|
||||
projectPath: string;
|
||||
feature: Partial<Feature>;
|
||||
};
|
||||
|
||||
if (!projectPath || !feature) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and feature are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
const created = await featureLoader.create(projectPath, feature);
|
||||
res.json({ success: true, feature: created });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a feature
|
||||
router.post("/update", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId, updates } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
updates: Partial<Feature>;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId || !updates) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath, featureId, and updates are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await featureLoader.update(projectPath, featureId, updates);
|
||||
res.json({ success: true, feature: updated });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a feature
|
||||
router.post("/delete", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await featureLoader.delete(projectPath, featureId);
|
||||
res.json({ success });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get agent output for a feature
|
||||
router.post("/agent-output", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await featureLoader.getAgentOutput(projectPath, featureId);
|
||||
res.json({ success: true, content });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
221
apps/server/src/routes/fs.ts
Normal file
221
apps/server/src/routes/fs.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* File system routes
|
||||
* Provides REST API equivalents for Electron IPC file operations
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
|
||||
export function createFsRoutes(_events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
// Read file
|
||||
router.post("/read", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
const content = await fs.readFile(resolvedPath, "utf-8");
|
||||
|
||||
res.json({ success: true, content });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Write file
|
||||
router.post("/write", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filePath, content } = req.body as {
|
||||
filePath: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
||||
await fs.writeFile(resolvedPath, content, "utf-8");
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create directory
|
||||
router.post("/mkdir", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { dirPath } = req.body as { dirPath: string };
|
||||
|
||||
if (!dirPath) {
|
||||
res.status(400).json({ success: false, error: "dirPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(dirPath);
|
||||
await fs.mkdir(resolvedPath, { recursive: true });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Read directory
|
||||
router.post("/readdir", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { dirPath } = req.body as { dirPath: string };
|
||||
|
||||
if (!dirPath) {
|
||||
res.status(400).json({ success: false, error: "dirPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(dirPath);
|
||||
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
||||
|
||||
const result = entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
isFile: entry.isFile(),
|
||||
}));
|
||||
|
||||
res.json({ success: true, entries: result });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if file/directory exists
|
||||
router.post("/exists", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// For exists, we check but don't require the path to be pre-allowed
|
||||
// This allows the UI to validate user-entered paths
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
try {
|
||||
await fs.access(resolvedPath);
|
||||
res.json({ success: true, exists: true });
|
||||
} catch {
|
||||
res.json({ success: true, exists: false });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get file stats
|
||||
router.post("/stat", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
stats: {
|
||||
isDirectory: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
size: stats.size,
|
||||
mtime: stats.mtime,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete file
|
||||
router.post("/delete", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
await fs.rm(resolvedPath, { recursive: true });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Validate and add path to allowed list
|
||||
// This is the web equivalent of dialog:openDirectory
|
||||
router.post("/validate-path", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
// Check if path exists
|
||||
try {
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to allowed paths
|
||||
addAllowedPath(resolvedPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
path: resolvedPath,
|
||||
isAllowed: isPathAllowed(resolvedPath),
|
||||
});
|
||||
} catch {
|
||||
res.status(400).json({ success: false, error: "Path does not exist" });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
102
apps/server/src/routes/git.ts
Normal file
102
apps/server/src/routes/git.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Git routes - HTTP API for git operations (non-worktree)
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export function createGitRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Get diffs for the main project
|
||||
router.post("/diffs", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
||||
cwd: projectPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
const files = status
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const statusChar = line[0];
|
||||
const filePath = line.slice(3);
|
||||
const statusMap: Record<string, string> = {
|
||||
M: "Modified",
|
||||
A: "Added",
|
||||
D: "Deleted",
|
||||
R: "Renamed",
|
||||
C: "Copied",
|
||||
U: "Updated",
|
||||
"?": "Untracked",
|
||||
};
|
||||
return {
|
||||
status: statusChar,
|
||||
path: filePath,
|
||||
statusText: statusMap[statusChar] || "Unknown",
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
diff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
});
|
||||
} catch {
|
||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diff for a specific file
|
||||
router.post("/file-diff", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, filePath } = req.body as {
|
||||
projectPath: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !filePath) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and filePath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: diff } = await execAsync(`git diff HEAD -- "${filePath}"`, {
|
||||
cwd: projectPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
res.json({ success: true, diff, filePath });
|
||||
} catch {
|
||||
res.json({ success: true, diff: "", filePath });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
39
apps/server/src/routes/health.ts
Normal file
39
apps/server/src/routes/health.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Health check routes
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { getAuthStatus } from "../lib/auth.js";
|
||||
|
||||
export function createHealthRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Basic health check
|
||||
router.get("/", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || "0.1.0",
|
||||
});
|
||||
});
|
||||
|
||||
// Detailed health check
|
||||
router.get("/detailed", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || "0.1.0",
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
dataDir: process.env.DATA_DIR || "./data",
|
||||
auth: getAuthStatus(),
|
||||
env: {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
128
apps/server/src/routes/models.ts
Normal file
128
apps/server/src/routes/models.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Models routes - HTTP API for model providers and availability
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
|
||||
interface ModelDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
contextWindow: number;
|
||||
maxOutputTokens: number;
|
||||
supportsVision: boolean;
|
||||
supportsTools: boolean;
|
||||
}
|
||||
|
||||
interface ProviderStatus {
|
||||
available: boolean;
|
||||
hasApiKey: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function createModelsRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Get available models
|
||||
router.get("/available", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const models: ModelDefinition[] = [
|
||||
{
|
||||
id: "claude-opus-4-5-20251101",
|
||||
name: "Claude Opus 4.5",
|
||||
provider: "anthropic",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "Claude Sonnet 4",
|
||||
provider: "anthropic",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "claude-3-5-sonnet-20241022",
|
||||
name: "Claude 3.5 Sonnet",
|
||||
provider: "anthropic",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "claude-3-5-haiku-20241022",
|
||||
name: "Claude 3.5 Haiku",
|
||||
provider: "anthropic",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
provider: "openai",
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-4o-mini",
|
||||
name: "GPT-4o Mini",
|
||||
provider: "openai",
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "o1",
|
||||
name: "o1",
|
||||
provider: "openai",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 100000,
|
||||
supportsVision: true,
|
||||
supportsTools: false,
|
||||
},
|
||||
];
|
||||
|
||||
res.json({ success: true, models });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Check provider status
|
||||
router.get("/providers", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const providers: Record<string, ProviderStatus> = {
|
||||
anthropic: {
|
||||
available: !!process.env.ANTHROPIC_API_KEY,
|
||||
hasApiKey: !!process.env.ANTHROPIC_API_KEY,
|
||||
},
|
||||
openai: {
|
||||
available: !!process.env.OPENAI_API_KEY,
|
||||
hasApiKey: !!process.env.OPENAI_API_KEY,
|
||||
},
|
||||
google: {
|
||||
available: !!process.env.GOOGLE_API_KEY,
|
||||
hasApiKey: !!process.env.GOOGLE_API_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
res.json({ success: true, providers });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
70
apps/server/src/routes/running-agents.ts
Normal file
70
apps/server/src/routes/running-agents.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Running Agents routes - HTTP API for tracking active agent executions
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import path from "path";
|
||||
|
||||
interface RunningAgent {
|
||||
featureId: string;
|
||||
projectPath: string;
|
||||
projectName: string;
|
||||
isAutoMode: boolean;
|
||||
}
|
||||
|
||||
// In-memory tracking of running agents (shared with auto-mode service via reference)
|
||||
const runningAgentsMap = new Map<string, RunningAgent>();
|
||||
let autoLoopRunning = false;
|
||||
|
||||
export function createRunningAgentsRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Get all running agents
|
||||
router.get("/", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const runningAgents = Array.from(runningAgentsMap.values());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: runningAgents.length,
|
||||
autoLoopRunning,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
// Export functions to update running agents from other services
|
||||
export function registerRunningAgent(
|
||||
featureId: string,
|
||||
projectPath: string,
|
||||
isAutoMode: boolean
|
||||
): void {
|
||||
runningAgentsMap.set(featureId, {
|
||||
featureId,
|
||||
projectPath,
|
||||
projectName: path.basename(projectPath),
|
||||
isAutoMode,
|
||||
});
|
||||
}
|
||||
|
||||
export function unregisterRunningAgent(featureId: string): void {
|
||||
runningAgentsMap.delete(featureId);
|
||||
}
|
||||
|
||||
export function setAutoLoopRunning(running: boolean): void {
|
||||
autoLoopRunning = running;
|
||||
}
|
||||
|
||||
export function getRunningAgentsCount(): number {
|
||||
return runningAgentsMap.size;
|
||||
}
|
||||
|
||||
export function isAgentRunning(featureId: string): boolean {
|
||||
return runningAgentsMap.has(featureId);
|
||||
}
|
||||
126
apps/server/src/routes/sessions.ts
Normal file
126
apps/server/src/routes/sessions.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Sessions routes - HTTP API for session management
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { AgentService } from "../services/agent-service.js";
|
||||
|
||||
export function createSessionsRoutes(agentService: AgentService): Router {
|
||||
const router = Router();
|
||||
|
||||
// List all sessions
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const includeArchived = req.query.includeArchived === "true";
|
||||
const sessions = await agentService.listSessions(includeArchived);
|
||||
res.json({ success: true, sessions });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new session
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { name, projectPath, workingDirectory } = req.body as {
|
||||
name: string;
|
||||
projectPath?: string;
|
||||
workingDirectory?: string;
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ success: false, error: "name is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await agentService.createSession(
|
||||
name,
|
||||
projectPath,
|
||||
workingDirectory
|
||||
);
|
||||
res.json({ success: true, session });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a session
|
||||
router.put("/:sessionId", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { name, tags } = req.body as {
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
const session = await agentService.updateSession(sessionId, { name, tags });
|
||||
if (!session) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, session });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Archive a session
|
||||
router.post("/:sessionId/archive", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const success = await agentService.archiveSession(sessionId);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Unarchive a session
|
||||
router.post("/:sessionId/unarchive", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const success = await agentService.unarchiveSession(sessionId);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a session
|
||||
router.delete("/:sessionId", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const success = await agentService.deleteSession(sessionId);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
407
apps/server/src/routes/setup.ts
Normal file
407
apps/server/src/routes/setup.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Setup routes - HTTP API for CLI detection, API keys, and platform info
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Storage for API keys (in-memory for now, should be persisted)
|
||||
const apiKeys: Record<string, string> = {};
|
||||
|
||||
export function createSetupRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Get Claude CLI status
|
||||
router.get("/claude-status", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
let installed = false;
|
||||
let version = "";
|
||||
let cliPath = "";
|
||||
let method = "none";
|
||||
|
||||
// Try to find Claude CLI
|
||||
try {
|
||||
const { stdout } = await execAsync("which claude || where claude 2>/dev/null");
|
||||
cliPath = stdout.trim();
|
||||
installed = true;
|
||||
method = "path";
|
||||
|
||||
// Get version
|
||||
try {
|
||||
const { stdout: versionOut } = await execAsync("claude --version");
|
||||
version = versionOut.trim();
|
||||
} catch {
|
||||
// Version command might not be available
|
||||
}
|
||||
} catch {
|
||||
// Not in PATH, try common locations
|
||||
const commonPaths = [
|
||||
path.join(os.homedir(), ".local", "bin", "claude"),
|
||||
"/usr/local/bin/claude",
|
||||
path.join(os.homedir(), ".npm-global", "bin", "claude"),
|
||||
];
|
||||
|
||||
for (const p of commonPaths) {
|
||||
try {
|
||||
await fs.access(p);
|
||||
cliPath = p;
|
||||
installed = true;
|
||||
method = "local";
|
||||
break;
|
||||
} catch {
|
||||
// Not found at this path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication - detect all possible auth methods
|
||||
let auth = {
|
||||
authenticated: false,
|
||||
method: "none" as string,
|
||||
hasCredentialsFile: false,
|
||||
hasToken: false,
|
||||
hasStoredOAuthToken: false,
|
||||
hasStoredApiKey: !!apiKeys.anthropic,
|
||||
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
|
||||
hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
||||
// Additional fields for detailed status
|
||||
oauthTokenValid: false,
|
||||
apiKeyValid: false,
|
||||
};
|
||||
|
||||
// Check for credentials file (OAuth tokens from claude login)
|
||||
const credentialsPath = path.join(os.homedir(), ".claude", "credentials.json");
|
||||
try {
|
||||
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
|
||||
const credentials = JSON.parse(credentialsContent);
|
||||
auth.hasCredentialsFile = true;
|
||||
|
||||
// Check what type of token is in credentials
|
||||
if (credentials.oauth_token || credentials.access_token) {
|
||||
auth.hasStoredOAuthToken = true;
|
||||
auth.oauthTokenValid = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "oauth_token"; // Stored OAuth token from credentials file
|
||||
} else if (credentials.api_key) {
|
||||
auth.apiKeyValid = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "api_key"; // Stored API key in credentials file
|
||||
}
|
||||
} catch {
|
||||
// No credentials file or invalid format
|
||||
}
|
||||
|
||||
// Environment variables override stored credentials (higher priority)
|
||||
if (auth.hasEnvOAuthToken) {
|
||||
auth.authenticated = true;
|
||||
auth.oauthTokenValid = true;
|
||||
auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
|
||||
} else if (auth.hasEnvApiKey) {
|
||||
auth.authenticated = true;
|
||||
auth.apiKeyValid = true;
|
||||
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
|
||||
}
|
||||
|
||||
// In-memory stored API key (from settings UI)
|
||||
if (!auth.authenticated && apiKeys.anthropic) {
|
||||
auth.authenticated = true;
|
||||
auth.method = "api_key"; // Manually stored API key
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
status: installed ? "installed" : "not_installed",
|
||||
installed,
|
||||
method,
|
||||
version,
|
||||
path: cliPath,
|
||||
auth,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get Codex CLI status
|
||||
router.get("/codex-status", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
let installed = false;
|
||||
let version = "";
|
||||
let cliPath = "";
|
||||
let method = "none";
|
||||
|
||||
// Try to find Codex CLI
|
||||
try {
|
||||
const { stdout } = await execAsync("which codex || where codex 2>/dev/null");
|
||||
cliPath = stdout.trim();
|
||||
installed = true;
|
||||
method = "path";
|
||||
|
||||
try {
|
||||
const { stdout: versionOut } = await execAsync("codex --version");
|
||||
version = versionOut.trim();
|
||||
} catch {
|
||||
// Version command might not be available
|
||||
}
|
||||
} catch {
|
||||
// Not found
|
||||
}
|
||||
|
||||
// Check for OpenAI/Codex authentication
|
||||
let auth = {
|
||||
authenticated: false,
|
||||
method: "none" as string,
|
||||
hasAuthFile: false,
|
||||
hasEnvKey: !!process.env.OPENAI_API_KEY,
|
||||
hasStoredApiKey: !!apiKeys.openai,
|
||||
hasEnvApiKey: !!process.env.OPENAI_API_KEY,
|
||||
// Additional fields for subscription/account detection
|
||||
hasSubscription: false,
|
||||
cliLoggedIn: false,
|
||||
};
|
||||
|
||||
// Check for OpenAI CLI auth file (~/.codex/auth.json or similar)
|
||||
const codexAuthPaths = [
|
||||
path.join(os.homedir(), ".codex", "auth.json"),
|
||||
path.join(os.homedir(), ".openai", "credentials"),
|
||||
path.join(os.homedir(), ".config", "openai", "credentials.json"),
|
||||
];
|
||||
|
||||
for (const authPath of codexAuthPaths) {
|
||||
try {
|
||||
const authContent = await fs.readFile(authPath, "utf-8");
|
||||
const authData = JSON.parse(authContent);
|
||||
auth.hasAuthFile = true;
|
||||
|
||||
// Check for subscription/tokens
|
||||
if (authData.subscription || authData.plan || authData.account_type) {
|
||||
auth.hasSubscription = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "subscription"; // Codex subscription (Plus/Team)
|
||||
} else if (authData.access_token || authData.api_key) {
|
||||
auth.cliLoggedIn = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "cli_verified"; // CLI logged in with account
|
||||
}
|
||||
break;
|
||||
} catch {
|
||||
// Auth file not found at this path
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variable has highest priority
|
||||
if (auth.hasEnvApiKey) {
|
||||
auth.authenticated = true;
|
||||
auth.method = "env"; // OPENAI_API_KEY environment variable
|
||||
}
|
||||
|
||||
// In-memory stored API key (from settings UI)
|
||||
if (!auth.authenticated && apiKeys.openai) {
|
||||
auth.authenticated = true;
|
||||
auth.method = "api_key"; // Manually stored API key
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
status: installed ? "installed" : "not_installed",
|
||||
method,
|
||||
version,
|
||||
path: cliPath,
|
||||
auth,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Install Claude CLI
|
||||
router.post("/install-claude", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
// In web mode, we can't install CLIs directly
|
||||
// Return instructions instead
|
||||
res.json({
|
||||
success: false,
|
||||
error:
|
||||
"CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Install Codex CLI
|
||||
router.post("/install-codex", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({
|
||||
success: false,
|
||||
error:
|
||||
"CLI installation requires terminal access. Please install manually using: npm install -g @openai/codex",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Auth Claude
|
||||
router.post("/auth-claude", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: "claude login",
|
||||
message: "Please run 'claude login' in your terminal to authenticate",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Auth Codex
|
||||
router.post("/auth-codex", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { apiKey } = req.body as { apiKey?: string };
|
||||
|
||||
if (apiKey) {
|
||||
apiKeys.openai = apiKey;
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: "codex auth login",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Store API key
|
||||
router.post("/store-api-key", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { provider, apiKey } = req.body as { provider: string; apiKey: string };
|
||||
|
||||
if (!provider || !apiKey) {
|
||||
res.status(400).json({ success: false, error: "provider and apiKey required" });
|
||||
return;
|
||||
}
|
||||
|
||||
apiKeys[provider] = apiKey;
|
||||
|
||||
// Also set as environment variable
|
||||
if (provider === "anthropic") {
|
||||
process.env.ANTHROPIC_API_KEY = apiKey;
|
||||
} else if (provider === "openai") {
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
} else if (provider === "google") {
|
||||
process.env.GOOGLE_API_KEY = apiKey;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get API keys status
|
||||
router.get("/api-keys", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
hasAnthropicKey: !!apiKeys.anthropic || !!process.env.ANTHROPIC_API_KEY,
|
||||
hasOpenAIKey: !!apiKeys.openai || !!process.env.OPENAI_API_KEY,
|
||||
hasGoogleKey: !!apiKeys.google || !!process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Codex MCP
|
||||
router.post("/configure-codex-mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create .codex directory and config
|
||||
const codexDir = path.join(projectPath, ".codex");
|
||||
await fs.mkdir(codexDir, { recursive: true });
|
||||
|
||||
const configPath = path.join(codexDir, "config.toml");
|
||||
const config = `# Codex configuration
|
||||
[mcp]
|
||||
enabled = true
|
||||
`;
|
||||
await fs.writeFile(configPath, config);
|
||||
|
||||
res.json({ success: true, configPath });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get platform info
|
||||
router.get("/platform", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const platform = os.platform();
|
||||
res.json({
|
||||
success: true,
|
||||
platform,
|
||||
arch: os.arch(),
|
||||
homeDir: os.homedir(),
|
||||
isWindows: platform === "win32",
|
||||
isMac: platform === "darwin",
|
||||
isLinux: platform === "linux",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Test OpenAI connection
|
||||
router.post("/test-openai", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { apiKey } = req.body as { apiKey?: string };
|
||||
const key = apiKey || apiKeys.openai || process.env.OPENAI_API_KEY;
|
||||
|
||||
if (!key) {
|
||||
res.json({ success: false, error: "No OpenAI API key provided" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple test - just verify the key format
|
||||
if (!key.startsWith("sk-")) {
|
||||
res.json({ success: false, error: "Invalid OpenAI API key format" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, message: "API key format is valid" });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
412
apps/server/src/routes/spec-regeneration.ts
Normal file
412
apps/server/src/routes/spec-regeneration.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* Spec Regeneration routes - HTTP API for AI-powered spec generation
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
|
||||
let isRunning = false;
|
||||
let currentAbortController: AbortController | null = null;
|
||||
|
||||
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
// Create project spec from overview
|
||||
router.post("/create", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, projectOverview, generateFeatures } = req.body as {
|
||||
projectPath: string;
|
||||
projectOverview: string;
|
||||
generateFeatures?: boolean;
|
||||
};
|
||||
|
||||
if (!projectPath || !projectOverview) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and projectOverview required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
res.json({ success: false, error: "Spec generation already running" });
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
currentAbortController = new AbortController();
|
||||
|
||||
// Start generation in background
|
||||
generateSpec(
|
||||
projectPath,
|
||||
projectOverview,
|
||||
events,
|
||||
currentAbortController,
|
||||
generateFeatures
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("[SpecRegeneration] Error:", error);
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
isRunning = false;
|
||||
currentAbortController = null;
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Generate from project definition
|
||||
router.post("/generate", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, projectDefinition } = req.body as {
|
||||
projectPath: string;
|
||||
projectDefinition: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !projectDefinition) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and projectDefinition required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
res.json({ success: false, error: "Spec generation already running" });
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
currentAbortController = new AbortController();
|
||||
|
||||
generateSpec(
|
||||
projectPath,
|
||||
projectDefinition,
|
||||
events,
|
||||
currentAbortController,
|
||||
false
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("[SpecRegeneration] Error:", error);
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
isRunning = false;
|
||||
currentAbortController = null;
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Generate features from existing spec
|
||||
router.post("/generate-features", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
res.json({ success: false, error: "Generation already running" });
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
currentAbortController = new AbortController();
|
||||
|
||||
generateFeaturesFromSpec(projectPath, events, currentAbortController)
|
||||
.catch((error) => {
|
||||
console.error("[SpecRegeneration] Error:", error);
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "features_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
isRunning = false;
|
||||
currentAbortController = null;
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop generation
|
||||
router.post("/stop", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
if (currentAbortController) {
|
||||
currentAbortController.abort();
|
||||
}
|
||||
isRunning = false;
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get status
|
||||
router.get("/status", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({ success: true, isRunning });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function generateSpec(
|
||||
projectPath: string,
|
||||
projectOverview: string,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController,
|
||||
generateFeatures?: boolean
|
||||
) {
|
||||
const prompt = `You are helping to define a software project specification.
|
||||
|
||||
Project Overview:
|
||||
${projectOverview}
|
||||
|
||||
Based on this overview, analyze the project and create a comprehensive specification that includes:
|
||||
|
||||
1. **Project Summary** - Brief description of what the project does
|
||||
2. **Core Features** - Main functionality the project needs
|
||||
3. **Technical Stack** - Recommended technologies and frameworks
|
||||
4. **Architecture** - High-level system design
|
||||
5. **Data Models** - Key entities and their relationships
|
||||
6. **API Design** - Main endpoints/interfaces needed
|
||||
7. **User Experience** - Key user flows and interactions
|
||||
|
||||
${generateFeatures ? `
|
||||
Also generate a list of features to implement. For each feature provide:
|
||||
- ID (lowercase-hyphenated)
|
||||
- Title
|
||||
- Description
|
||||
- Priority (1=high, 2=medium, 3=low)
|
||||
- Estimated complexity (simple, moderate, complex)
|
||||
` : ""}
|
||||
|
||||
Format your response as markdown. Be specific and actionable.`;
|
||||
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_progress",
|
||||
content: "Starting spec generation...\n",
|
||||
});
|
||||
|
||||
const options: Options = {
|
||||
model: "claude-opus-4-5-20251101",
|
||||
maxTurns: 10,
|
||||
cwd: projectPath,
|
||||
allowedTools: ["Read", "Glob", "Grep"],
|
||||
permissionMode: "acceptEdits",
|
||||
abortController,
|
||||
};
|
||||
|
||||
const stream = query({ prompt, options });
|
||||
let responseText = "";
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant" && msg.message.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
responseText = block.text;
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_progress",
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||
responseText = msg.result || responseText;
|
||||
}
|
||||
}
|
||||
|
||||
// Save spec
|
||||
const specDir = path.join(projectPath, ".automaker");
|
||||
const specPath = path.join(specDir, "project-spec.md");
|
||||
|
||||
await fs.mkdir(specDir, { recursive: true });
|
||||
await fs.writeFile(specPath, responseText);
|
||||
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_complete",
|
||||
specPath,
|
||||
content: responseText,
|
||||
});
|
||||
|
||||
// If generate features was requested, parse and create them
|
||||
if (generateFeatures) {
|
||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateFeaturesFromSpec(
|
||||
projectPath: string,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController
|
||||
) {
|
||||
// Read existing spec
|
||||
const specPath = path.join(projectPath, ".automaker", "project-spec.md");
|
||||
let spec: string;
|
||||
|
||||
try {
|
||||
spec = await fs.readFile(specPath, "utf-8");
|
||||
} catch {
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "features_error",
|
||||
error: "No project spec found. Generate spec first.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = `Based on this project specification:
|
||||
|
||||
${spec}
|
||||
|
||||
Generate a prioritized list of implementable features. For each feature provide:
|
||||
|
||||
1. **id**: A unique lowercase-hyphenated identifier
|
||||
2. **title**: Short descriptive title
|
||||
3. **description**: What this feature does (2-3 sentences)
|
||||
4. **priority**: 1 (high), 2 (medium), or 3 (low)
|
||||
5. **complexity**: "simple", "moderate", or "complex"
|
||||
6. **dependencies**: Array of feature IDs this depends on (can be empty)
|
||||
|
||||
Format as JSON:
|
||||
{
|
||||
"features": [
|
||||
{
|
||||
"id": "feature-id",
|
||||
"title": "Feature Title",
|
||||
"description": "What it does",
|
||||
"priority": 1,
|
||||
"complexity": "moderate",
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generate 5-15 features that build on each other logically.`;
|
||||
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "features_progress",
|
||||
content: "Analyzing spec and generating features...\n",
|
||||
});
|
||||
|
||||
const options: Options = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
maxTurns: 5,
|
||||
cwd: projectPath,
|
||||
allowedTools: ["Read", "Glob"],
|
||||
permissionMode: "acceptEdits",
|
||||
abortController,
|
||||
};
|
||||
|
||||
const stream = query({ prompt, options });
|
||||
let responseText = "";
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant" && msg.message.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
responseText = block.text;
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "features_progress",
|
||||
content: block.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||
responseText = msg.result || responseText;
|
||||
}
|
||||
}
|
||||
|
||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||
}
|
||||
|
||||
async function parseAndCreateFeatures(
|
||||
projectPath: string,
|
||||
content: string,
|
||||
events: EventEmitter
|
||||
) {
|
||||
try {
|
||||
// Extract JSON from response
|
||||
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error("No valid JSON found in response");
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const featuresDir = path.join(projectPath, ".automaker", "features");
|
||||
await fs.mkdir(featuresDir, { recursive: true });
|
||||
|
||||
const createdFeatures: Array<{ id: string; title: string }> = [];
|
||||
|
||||
for (const feature of parsed.features) {
|
||||
const featureDir = path.join(featuresDir, feature.id);
|
||||
await fs.mkdir(featureDir, { recursive: true });
|
||||
|
||||
const featureData = {
|
||||
id: feature.id,
|
||||
title: feature.title,
|
||||
description: feature.description,
|
||||
status: "pending",
|
||||
priority: feature.priority || 2,
|
||||
complexity: feature.complexity || "moderate",
|
||||
dependencies: feature.dependencies || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(featureDir, "feature.json"),
|
||||
JSON.stringify(featureData, null, 2)
|
||||
);
|
||||
|
||||
createdFeatures.push({ id: feature.id, title: feature.title });
|
||||
}
|
||||
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "features_complete",
|
||||
features: createdFeatures,
|
||||
count: createdFeatures.length,
|
||||
});
|
||||
} catch (error) {
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "features_error",
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
192
apps/server/src/routes/suggestions.ts
Normal file
192
apps/server/src/routes/suggestions.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Suggestions routes - HTTP API for AI-powered feature suggestions
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
|
||||
let isRunning = false;
|
||||
let currentAbortController: AbortController | null = null;
|
||||
|
||||
export function createSuggestionsRoutes(events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
// Generate suggestions
|
||||
router.post("/generate", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, suggestionType = "features" } = req.body as {
|
||||
projectPath: string;
|
||||
suggestionType?: string;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
res.json({ success: false, error: "Suggestions generation is already running" });
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
currentAbortController = new AbortController();
|
||||
|
||||
// Start generation in background
|
||||
generateSuggestions(projectPath, suggestionType, events, currentAbortController)
|
||||
.catch((error) => {
|
||||
console.error("[Suggestions] Error:", error);
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
isRunning = false;
|
||||
currentAbortController = null;
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop suggestions generation
|
||||
router.post("/stop", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
if (currentAbortController) {
|
||||
currentAbortController.abort();
|
||||
}
|
||||
isRunning = false;
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get status
|
||||
router.get("/status", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({ success: true, isRunning });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function generateSuggestions(
|
||||
projectPath: string,
|
||||
suggestionType: string,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController
|
||||
) {
|
||||
const typePrompts: Record<string, string> = {
|
||||
features: "Analyze this project and suggest new features that would add value.",
|
||||
refactoring: "Analyze this project and identify refactoring opportunities.",
|
||||
security: "Analyze this project for security vulnerabilities and suggest fixes.",
|
||||
performance: "Analyze this project for performance issues and suggest optimizations.",
|
||||
};
|
||||
|
||||
const prompt = `${typePrompts[suggestionType] || typePrompts.features}
|
||||
|
||||
Look at the codebase and provide 3-5 concrete suggestions.
|
||||
|
||||
For each suggestion, provide:
|
||||
1. A category (e.g., "User Experience", "Security", "Performance")
|
||||
2. A clear description of what to implement
|
||||
3. Concrete steps to implement it
|
||||
4. Priority (1=high, 2=medium, 3=low)
|
||||
5. Brief reasoning for why this would help
|
||||
|
||||
Format your response as JSON:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "suggestion-123",
|
||||
"category": "Category",
|
||||
"description": "What to implement",
|
||||
"steps": ["Step 1", "Step 2"],
|
||||
"priority": 1,
|
||||
"reasoning": "Why this helps"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_progress",
|
||||
content: `Starting ${suggestionType} analysis...\n`,
|
||||
});
|
||||
|
||||
const options: Options = {
|
||||
model: "claude-opus-4-5-20251101",
|
||||
maxTurns: 5,
|
||||
cwd: projectPath,
|
||||
allowedTools: ["Read", "Glob", "Grep"],
|
||||
permissionMode: "acceptEdits",
|
||||
abortController,
|
||||
};
|
||||
|
||||
const stream = query({ prompt, options });
|
||||
let responseText = "";
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant" && msg.message.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
responseText = block.text;
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_progress",
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||
responseText = msg.result || responseText;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse suggestions from response
|
||||
try {
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_complete",
|
||||
suggestions: parsed.suggestions.map((s: Record<string, unknown>, i: number) => ({
|
||||
...s,
|
||||
id: s.id || `suggestion-${Date.now()}-${i}`,
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
throw new Error("No valid JSON found in response");
|
||||
}
|
||||
} catch (error) {
|
||||
// Return generic suggestions if parsing fails
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_complete",
|
||||
suggestions: [
|
||||
{
|
||||
id: `suggestion-${Date.now()}-0`,
|
||||
category: "Analysis",
|
||||
description: "Review the AI analysis output for insights",
|
||||
steps: ["Review the generated analysis"],
|
||||
priority: 1,
|
||||
reasoning: "The AI provided analysis but suggestions need manual review",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
355
apps/server/src/routes/worktree.ts
Normal file
355
apps/server/src/routes/worktree.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* Worktree routes - HTTP API for git worktree operations
|
||||
*/
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export function createWorktreeRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Check if a path is a git repo
|
||||
async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get worktree info
|
||||
router.post("/info", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if worktree exists
|
||||
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
worktreePath,
|
||||
branchName: stdout.trim(),
|
||||
});
|
||||
} catch {
|
||||
res.json({ success: true, worktreePath: null, branchName: null });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get worktree status
|
||||
router.post("/status", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const files = status
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => line.slice(3));
|
||||
const { stdout: diffStat } = await execAsync("git diff --stat", {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const { stdout: logOutput } = await execAsync(
|
||||
'git log --oneline -5 --format="%h %s"',
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
modifiedFiles: files.length,
|
||||
files,
|
||||
diffStat: diffStat.trim(),
|
||||
recentCommits: logOutput.trim().split("\n").filter(Boolean),
|
||||
});
|
||||
} catch {
|
||||
res.json({
|
||||
success: true,
|
||||
modifiedFiles: 0,
|
||||
files: [],
|
||||
diffStat: "",
|
||||
recentCommits: [],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// List all worktrees
|
||||
router.post("/list", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
res.json({ success: true, worktrees: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
const worktrees: Array<{ path: string; branch: string }> = [];
|
||||
const lines = stdout.split("\n");
|
||||
let current: { path?: string; branch?: string } = {};
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
current.path = line.slice(9);
|
||||
} else if (line.startsWith("branch ")) {
|
||||
current.branch = line.slice(7).replace("refs/heads/", "");
|
||||
} else if (line === "") {
|
||||
if (current.path && current.branch) {
|
||||
worktrees.push({ path: current.path, branch: current.branch });
|
||||
}
|
||||
current = {};
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, worktrees });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diffs for a worktree
|
||||
router.post("/diffs", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
||||
cwd: worktreePath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
|
||||
const files = status
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const statusChar = line[0];
|
||||
const filePath = line.slice(3);
|
||||
const statusMap: Record<string, string> = {
|
||||
M: "Modified",
|
||||
A: "Added",
|
||||
D: "Deleted",
|
||||
R: "Renamed",
|
||||
C: "Copied",
|
||||
U: "Updated",
|
||||
"?": "Untracked",
|
||||
};
|
||||
return {
|
||||
status: statusChar,
|
||||
path: filePath,
|
||||
statusText: statusMap[statusChar] || "Unknown",
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
diff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
});
|
||||
} catch {
|
||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diff for a specific file
|
||||
router.post("/file-diff", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId, filePath } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId || !filePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath, featureId, and filePath required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout: diff } = await execAsync(`git diff HEAD -- "${filePath}"`, {
|
||||
cwd: worktreePath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
res.json({ success: true, diff, filePath });
|
||||
} catch {
|
||||
res.json({ success: true, diff: "", filePath });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Revert feature (remove worktree)
|
||||
router.post("/revert", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
|
||||
|
||||
try {
|
||||
// Remove worktree
|
||||
await execAsync(`git worktree remove "${worktreePath}" --force`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
// Delete branch
|
||||
await execAsync(`git branch -D feature/${featureId}`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, removedPath: worktreePath });
|
||||
} catch (error) {
|
||||
// Worktree might not exist
|
||||
res.json({ success: true, removedPath: null });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Merge feature (merge worktree branch into main)
|
||||
router.post("/merge", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath, featureId, options } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
options?: { squash?: boolean; message?: string };
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and featureId required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const branchName = `feature/${featureId}`;
|
||||
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranch } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
|
||||
// Merge the feature branch
|
||||
const mergeCmd = options?.squash
|
||||
? `git merge --squash ${branchName}`
|
||||
: `git merge ${branchName} -m "${options?.message || `Merge ${branchName}`}"`;
|
||||
|
||||
await execAsync(mergeCmd, { cwd: projectPath });
|
||||
|
||||
// If squash merge, need to commit
|
||||
if (options?.squash) {
|
||||
await execAsync(
|
||||
`git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up worktree and branch
|
||||
try {
|
||||
await execAsync(`git worktree remove "${worktreePath}" --force`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
|
||||
} catch {
|
||||
// Cleanup errors are non-fatal
|
||||
}
|
||||
|
||||
res.json({ success: true, mergedBranch: branchName });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user