mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge branch 'main' of github.com:webdevcody/automaker
This commit is contained in:
@@ -7,6 +7,7 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
||||
const fs = require("fs/promises");
|
||||
const agentService = require("./agent-service");
|
||||
const autoModeService = require("./auto-mode-service");
|
||||
const worktreeManager = require("./services/worktree-manager");
|
||||
const featureSuggestionsService = require("./services/feature-suggestions-service");
|
||||
const specRegenerationService = require("./services/spec-regeneration-service");
|
||||
|
||||
@@ -61,6 +62,21 @@ app.whenReady().then(async () => {
|
||||
const appDataPath = app.getPath("userData");
|
||||
await agentService.initialize(appDataPath);
|
||||
|
||||
// Pre-load allowed paths from agent history to prevent breaking "Recent Projects"
|
||||
try {
|
||||
const sessions = await agentService.listSessions({ includeArchived: true });
|
||||
sessions.forEach((session) => {
|
||||
if (session.projectPath) {
|
||||
addAllowedPath(session.projectPath);
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
`[Security] Pre-loaded ${allowedPaths.size} allowed paths from history`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load sessions for security whitelist:", error);
|
||||
}
|
||||
|
||||
createWindow();
|
||||
|
||||
app.on("activate", () => {
|
||||
@@ -76,6 +92,43 @@ app.on("window-all-closed", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Track allowed paths for file operations (security)
|
||||
const allowedPaths = new Set();
|
||||
|
||||
/**
|
||||
* Add a path to the allowed list
|
||||
*/
|
||||
function addAllowedPath(pathToAdd) {
|
||||
if (!pathToAdd) return;
|
||||
allowedPaths.add(path.resolve(pathToAdd));
|
||||
console.log(`[Security] Added allowed path: ${pathToAdd}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path is allowed (must be within an allowed directory)
|
||||
*/
|
||||
function isPathAllowed(filePath) {
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
// Allow access to app data directory (for logs, temp images etc)
|
||||
const appDataPath = app.getPath("userData");
|
||||
if (resolvedPath.startsWith(appDataPath)) return true;
|
||||
|
||||
// Check against all allowed project paths
|
||||
for (const allowedPath of allowedPaths) {
|
||||
// Check if path starts with allowed directory
|
||||
// Ensure we don't match "/foo/bar" against "/foo/b"
|
||||
if (
|
||||
resolvedPath === allowedPath ||
|
||||
resolvedPath.startsWith(allowedPath + path.sep)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// IPC Handlers
|
||||
|
||||
// Dialog handlers
|
||||
@@ -83,6 +136,11 @@ ipcMain.handle("dialog:openDirectory", async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
result.filePaths.forEach((p) => addAllowedPath(p));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -91,12 +149,26 @@ ipcMain.handle("dialog:openFile", async (_, options = {}) => {
|
||||
properties: ["openFile"],
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
// Allow reading the specific file selected
|
||||
result.filePaths.forEach((p) => addAllowedPath(p));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// File system handlers
|
||||
ipcMain.handle("fs:readFile", async (_, filePath) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(filePath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return { success: true, content };
|
||||
} catch (error) {
|
||||
@@ -106,6 +178,14 @@ ipcMain.handle("fs:readFile", async (_, filePath) => {
|
||||
|
||||
ipcMain.handle("fs:writeFile", async (_, filePath, content) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(filePath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, content, "utf-8");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -115,6 +195,14 @@ ipcMain.handle("fs:writeFile", async (_, filePath, content) => {
|
||||
|
||||
ipcMain.handle("fs:mkdir", async (_, dirPath) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(dirPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -124,6 +212,14 @@ ipcMain.handle("fs:mkdir", async (_, dirPath) => {
|
||||
|
||||
ipcMain.handle("fs:readdir", async (_, dirPath) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(dirPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
const result = entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
@@ -138,6 +234,11 @@ ipcMain.handle("fs:readdir", async (_, dirPath) => {
|
||||
|
||||
ipcMain.handle("fs:exists", async (_, filePath) => {
|
||||
try {
|
||||
// Exists check is generally safe, but we can restrict it too for strict privacy
|
||||
if (!isPathAllowed(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
@@ -147,6 +248,14 @@ ipcMain.handle("fs:exists", async (_, filePath) => {
|
||||
|
||||
ipcMain.handle("fs:stat", async (_, filePath) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(filePath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await fs.stat(filePath);
|
||||
return {
|
||||
success: true,
|
||||
@@ -164,6 +273,14 @@ ipcMain.handle("fs:stat", async (_, filePath) => {
|
||||
|
||||
ipcMain.handle("fs:deleteFile", async (_, filePath) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(filePath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
await fs.unlink(filePath);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -173,6 +290,14 @@ ipcMain.handle("fs:deleteFile", async (_, filePath) => {
|
||||
|
||||
ipcMain.handle("fs:trashItem", async (_, targetPath) => {
|
||||
try {
|
||||
// Security check
|
||||
if (!isPathAllowed(targetPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Access denied: Path is outside allowed project directories",
|
||||
};
|
||||
}
|
||||
|
||||
await shell.trashItem(targetPath);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -352,6 +477,10 @@ ipcMain.handle(
|
||||
"sessions:create",
|
||||
async (_, { name, projectPath, workingDirectory }) => {
|
||||
try {
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
if (workingDirectory) addAllowedPath(workingDirectory);
|
||||
|
||||
return await agentService.createSession({
|
||||
name,
|
||||
projectPath,
|
||||
@@ -423,6 +552,9 @@ ipcMain.handle(
|
||||
"auto-mode:start",
|
||||
async (_, { projectPath, maxConcurrency }) => {
|
||||
try {
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
@@ -470,7 +602,7 @@ ipcMain.handle("auto-mode:status", () => {
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"auto-mode:run-feature",
|
||||
async (_, { projectPath, featureId }) => {
|
||||
async (_, { projectPath, featureId, useWorktrees = false }) => {
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
@@ -482,6 +614,7 @@ ipcMain.handle(
|
||||
projectPath,
|
||||
featureId,
|
||||
sendToRenderer,
|
||||
useWorktrees,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:run-feature error:", error);
|
||||
@@ -581,6 +714,9 @@ ipcMain.handle(
|
||||
ipcMain.handle("auto-mode:analyze-project", async (_, { projectPath }) => {
|
||||
console.log("[IPC] auto-mode:analyze-project called with:", { projectPath });
|
||||
try {
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
@@ -672,6 +808,111 @@ ipcMain.handle(
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Claude CLI Detection IPC Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check Claude Code CLI installation status
|
||||
*/
|
||||
ipcMain.handle("claude:check-cli", async () => {
|
||||
try {
|
||||
const claudeCliDetector = require("./services/claude-cli-detector");
|
||||
const info = claudeCliDetector.getInstallationInfo();
|
||||
return { success: true, ...info };
|
||||
} catch (error) {
|
||||
console.error("[IPC] claude:check-cli error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Codex CLI Detection IPC Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check Codex CLI installation status
|
||||
*/
|
||||
ipcMain.handle("codex:check-cli", async () => {
|
||||
try {
|
||||
const codexCliDetector = require("./services/codex-cli-detector");
|
||||
const info = codexCliDetector.getInstallationInfo();
|
||||
return { success: true, ...info };
|
||||
} catch (error) {
|
||||
console.error("[IPC] codex:check-cli error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all available models from all providers
|
||||
*/
|
||||
ipcMain.handle("model:get-available", async () => {
|
||||
try {
|
||||
const { ModelProviderFactory } = require("./services/model-provider");
|
||||
const models = ModelProviderFactory.getAllModels();
|
||||
return { success: true, models };
|
||||
} catch (error) {
|
||||
console.error("[IPC] model:get-available error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Check all provider installation status
|
||||
*/
|
||||
ipcMain.handle("model:check-providers", async () => {
|
||||
try {
|
||||
const { ModelProviderFactory } = require("./services/model-provider");
|
||||
const status = await ModelProviderFactory.checkAllProviders();
|
||||
return { success: true, providers: status };
|
||||
} catch (error) {
|
||||
console.error("[IPC] model:check-providers error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// MCP Server IPC Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle MCP server callback for updating feature status
|
||||
* This can be called by the MCP server script via HTTP or other communication mechanism
|
||||
* Note: The MCP server script runs as a separate process, so it can't directly use Electron IPC.
|
||||
* For now, the MCP server calls featureLoader.updateFeatureStatus directly.
|
||||
* This handler is here for future extensibility (e.g., HTTP endpoint bridge).
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"mcp:update-feature-status",
|
||||
async (_, { featureId, status, projectPath, summary }) => {
|
||||
try {
|
||||
const featureLoader = require("./services/feature-loader");
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
status,
|
||||
projectPath,
|
||||
summary
|
||||
);
|
||||
|
||||
// Notify renderer if window is available
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("mcp:feature-status-updated", {
|
||||
featureId,
|
||||
status,
|
||||
projectPath,
|
||||
summary,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] mcp:update-feature-status error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Feature Suggestions IPC Handlers
|
||||
// ============================================================================
|
||||
@@ -682,53 +923,53 @@ let suggestionsExecution = null;
|
||||
/**
|
||||
* Generate feature suggestions by analyzing the project
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"suggestions:generate",
|
||||
async (_, { projectPath }) => {
|
||||
console.log("[IPC] suggestions:generate called with:", { projectPath });
|
||||
ipcMain.handle("suggestions:generate", async (_, { projectPath }) => {
|
||||
console.log("[IPC] suggestions:generate called with:", { projectPath });
|
||||
|
||||
try {
|
||||
// Check if already running
|
||||
if (suggestionsExecution && suggestionsExecution.isActive()) {
|
||||
return { success: false, error: "Suggestions generation is already running" };
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
suggestionsExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => suggestionsExecution !== null,
|
||||
try {
|
||||
// Check if already running
|
||||
if (suggestionsExecution && suggestionsExecution.isActive()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Suggestions generation is already running",
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("suggestions:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start generating suggestions (runs in background)
|
||||
featureSuggestionsService
|
||||
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] suggestions:generate background error:", error);
|
||||
sendToRenderer({
|
||||
type: "suggestions_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
suggestionsExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] suggestions:generate error:", error);
|
||||
suggestionsExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
suggestionsExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => suggestionsExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("suggestions:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start generating suggestions (runs in background)
|
||||
featureSuggestionsService
|
||||
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] suggestions:generate background error:", error);
|
||||
sendToRenderer({
|
||||
type: "suggestions_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
suggestionsExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] suggestions:generate error:", error);
|
||||
suggestionsExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Stop the current suggestions generation
|
||||
@@ -757,6 +998,79 @@ ipcMain.handle("suggestions:status", () => {
|
||||
};
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// OpenAI API Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Test OpenAI API connection
|
||||
*/
|
||||
ipcMain.handle("openai:test-connection", async (_, { apiKey }) => {
|
||||
try {
|
||||
// Simple test using fetch to OpenAI API
|
||||
const response = await fetch("https://api.openai.com/v1/models", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey || process.env.OPENAI_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
message: `Connected successfully. Found ${
|
||||
data.data?.length || 0
|
||||
} models.`,
|
||||
};
|
||||
} else {
|
||||
const error = await response.json();
|
||||
return {
|
||||
success: false,
|
||||
error: error.error?.message || "Failed to connect to OpenAI API",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[IPC] openai:test-connection error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Worktree Management IPC Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Revert feature changes by removing the worktree
|
||||
* This effectively discards all changes made by the agent
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"worktree:revert-feature",
|
||||
async (_, { projectPath, featureId }) => {
|
||||
console.log("[IPC] worktree:revert-feature called with:", {
|
||||
projectPath,
|
||||
featureId,
|
||||
});
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.revertFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:revert-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Spec Regeneration IPC Handlers
|
||||
// ============================================================================
|
||||
@@ -770,12 +1084,20 @@ let specRegenerationExecution = null;
|
||||
ipcMain.handle(
|
||||
"spec-regeneration:generate",
|
||||
async (_, { projectPath, projectDefinition }) => {
|
||||
console.log("[IPC] spec-regeneration:generate called with:", { projectPath });
|
||||
console.log("[IPC] spec-regeneration:generate called with:", {
|
||||
projectPath,
|
||||
});
|
||||
|
||||
try {
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
// Check if already running
|
||||
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||
return { success: false, error: "Spec regeneration is already running" };
|
||||
return {
|
||||
success: false,
|
||||
error: "Spec regeneration is already running",
|
||||
};
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
@@ -793,9 +1115,17 @@ ipcMain.handle(
|
||||
|
||||
// Start regenerating spec (runs in background)
|
||||
specRegenerationService
|
||||
.regenerateSpec(projectPath, projectDefinition, sendToRenderer, specRegenerationExecution)
|
||||
.regenerateSpec(
|
||||
projectPath,
|
||||
projectDefinition,
|
||||
sendToRenderer,
|
||||
specRegenerationExecution
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] spec-regeneration:generate background error:", error);
|
||||
console.error(
|
||||
"[IPC] spec-regeneration:generate background error:",
|
||||
error
|
||||
);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_error",
|
||||
error: error.message,
|
||||
@@ -821,7 +1151,10 @@ ipcMain.handle(
|
||||
ipcMain.handle("spec-regeneration:stop", async () => {
|
||||
console.log("[IPC] spec-regeneration:stop called");
|
||||
try {
|
||||
if (specRegenerationExecution && specRegenerationExecution.abortController) {
|
||||
if (
|
||||
specRegenerationExecution &&
|
||||
specRegenerationExecution.abortController
|
||||
) {
|
||||
specRegenerationExecution.abortController.abort();
|
||||
}
|
||||
specRegenerationExecution = null;
|
||||
@@ -838,7 +1171,9 @@ ipcMain.handle("spec-regeneration:stop", async () => {
|
||||
ipcMain.handle("spec-regeneration:status", () => {
|
||||
return {
|
||||
success: true,
|
||||
isRunning: specRegenerationExecution !== null && specRegenerationExecution.isActive(),
|
||||
isRunning:
|
||||
specRegenerationExecution !== null &&
|
||||
specRegenerationExecution.isActive(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -848,9 +1183,15 @@ ipcMain.handle("spec-regeneration:status", () => {
|
||||
ipcMain.handle(
|
||||
"spec-regeneration:create",
|
||||
async (_, { projectPath, projectOverview, generateFeatures = true }) => {
|
||||
console.log("[IPC] spec-regeneration:create called with:", { projectPath, generateFeatures });
|
||||
console.log("[IPC] spec-regeneration:create called with:", {
|
||||
projectPath,
|
||||
generateFeatures,
|
||||
});
|
||||
|
||||
try {
|
||||
// Add project path to allowed paths
|
||||
addAllowedPath(projectPath);
|
||||
|
||||
// Check if already running
|
||||
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||
return { success: false, error: "Spec creation is already running" };
|
||||
@@ -871,9 +1212,18 @@ ipcMain.handle(
|
||||
|
||||
// Start creating spec (runs in background)
|
||||
specRegenerationService
|
||||
.createInitialSpec(projectPath, projectOverview, sendToRenderer, specRegenerationExecution, generateFeatures)
|
||||
.createInitialSpec(
|
||||
projectPath,
|
||||
projectOverview,
|
||||
sendToRenderer,
|
||||
specRegenerationExecution,
|
||||
generateFeatures
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] spec-regeneration:create background error:", error);
|
||||
console.error(
|
||||
"[IPC] spec-regeneration:create background error:",
|
||||
error
|
||||
);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_error",
|
||||
error: error.message,
|
||||
@@ -892,3 +1242,124 @@ ipcMain.handle(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Merge feature worktree changes back to main branch
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"worktree:merge-feature",
|
||||
async (_, { projectPath, featureId, options }) => {
|
||||
console.log("[IPC] worktree:merge-feature called with:", {
|
||||
projectPath,
|
||||
featureId,
|
||||
options,
|
||||
});
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.mergeFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
options,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:merge-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Get worktree info for a feature
|
||||
*/
|
||||
ipcMain.handle("worktree:get-info", async (_, { projectPath, featureId }) => {
|
||||
try {
|
||||
return await autoModeService.getWorktreeInfo({ projectPath, featureId });
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:get-info error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get worktree status (changed files, commits)
|
||||
*/
|
||||
ipcMain.handle("worktree:get-status", async (_, { projectPath, featureId }) => {
|
||||
try {
|
||||
return await autoModeService.getWorktreeStatus({ projectPath, featureId });
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:get-status error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List all feature worktrees
|
||||
*/
|
||||
ipcMain.handle("worktree:list", async (_, { projectPath }) => {
|
||||
try {
|
||||
return await autoModeService.listWorktrees({ projectPath });
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:list error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get file diffs for a worktree
|
||||
*/
|
||||
ipcMain.handle("worktree:get-diffs", async (_, { projectPath, featureId }) => {
|
||||
try {
|
||||
return await autoModeService.getFileDiffs({ projectPath, featureId });
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:get-diffs error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get diff for a specific file in a worktree
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"worktree:get-file-diff",
|
||||
async (_, { projectPath, featureId, filePath }) => {
|
||||
try {
|
||||
return await autoModeService.getFileDiff({
|
||||
projectPath,
|
||||
featureId,
|
||||
filePath,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] worktree:get-file-diff error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get file diffs for the main project (non-worktree)
|
||||
*/
|
||||
ipcMain.handle("git:get-diffs", async (_, { projectPath }) => {
|
||||
try {
|
||||
return await worktreeManager.getFileDiffs(projectPath);
|
||||
} catch (error) {
|
||||
console.error("[IPC] git:get-diffs error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get diff for a specific file in the main project (non-worktree)
|
||||
*/
|
||||
ipcMain.handle("git:get-file-diff", async (_, { projectPath, filePath }) => {
|
||||
try {
|
||||
return await worktreeManager.getFileDiff(projectPath, filePath);
|
||||
} catch (error) {
|
||||
console.error("[IPC] git:get-file-diff error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user