mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
Merge branch 'main' of github.com:webdevcody/automaker
This commit is contained in:
@@ -19,6 +19,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { useFileBrowser } from "@/contexts/file-browser-context";
|
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface InterviewMessage {
|
interface InterviewMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -290,7 +291,8 @@ export function InterviewView() {
|
|||||||
const handleSelectDirectory = async () => {
|
const handleSelectDirectory = async () => {
|
||||||
const selectedPath = await openFileBrowser({
|
const selectedPath = await openFileBrowser({
|
||||||
title: "Select Base Directory",
|
title: "Select Base Directory",
|
||||||
description: "Choose the parent directory where your new project will be created",
|
description:
|
||||||
|
"Choose the parent directory where your new project will be created",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedPath) {
|
if (selectedPath) {
|
||||||
@@ -306,12 +308,23 @@ export function InterviewView() {
|
|||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
// Use platform-specific path separator
|
// Use platform-specific path separator
|
||||||
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
const pathSep =
|
||||||
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
typeof window !== "undefined" && (window as any).electronAPI
|
||||||
|
? navigator.platform.indexOf("Win") !== -1
|
||||||
|
? "\\"
|
||||||
|
: "/"
|
||||||
|
: "/";
|
||||||
const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
|
const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
await api.mkdir(fullProjectPath);
|
const mkdirResult = await api.mkdir(fullProjectPath);
|
||||||
|
if (!mkdirResult.success) {
|
||||||
|
toast.error("Failed to create project directory", {
|
||||||
|
description: mkdirResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
setIsGenerating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Write app_spec.txt with generated content
|
// Write app_spec.txt with generated content
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
|
|||||||
@@ -236,7 +236,13 @@ export function WelcomeView() {
|
|||||||
const projectPath = `${parentDir}/${projectName}`;
|
const projectPath = `${parentDir}/${projectName}`;
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
await api.mkdir(projectPath);
|
const mkdirResult = await api.mkdir(projectPath);
|
||||||
|
if (!mkdirResult.success) {
|
||||||
|
toast.error("Failed to create project directory", {
|
||||||
|
description: mkdirResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize .automaker directory with all necessary files
|
// Initialize .automaker directory with all necessary files
|
||||||
const initResult = await initializeProject(projectPath);
|
const initResult = await initializeProject(projectPath);
|
||||||
|
|||||||
@@ -33,16 +33,21 @@ export function useAutoMode() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Helper to look up project ID from path
|
// Helper to look up project ID from path
|
||||||
const getProjectIdFromPath = useCallback((path: string): string | undefined => {
|
const getProjectIdFromPath = useCallback(
|
||||||
const project = projects.find(p => p.path === path);
|
(path: string): string | undefined => {
|
||||||
return project?.id;
|
const project = projects.find((p) => p.path === path);
|
||||||
}, [projects]);
|
return project?.id;
|
||||||
|
},
|
||||||
|
[projects]
|
||||||
|
);
|
||||||
|
|
||||||
// Get project-specific auto mode state
|
// Get project-specific auto mode state
|
||||||
const projectId = currentProject?.id;
|
const projectId = currentProject?.id;
|
||||||
const projectAutoModeState = useMemo(() => {
|
const projectAutoModeState = useMemo(() => {
|
||||||
if (!projectId) return { isRunning: false, runningTasks: [] };
|
if (!projectId) return { isRunning: false, runningTasks: [] };
|
||||||
return autoModeByProject[projectId] || { isRunning: false, runningTasks: [] };
|
return (
|
||||||
|
autoModeByProject[projectId] || { isRunning: false, runningTasks: [] }
|
||||||
|
);
|
||||||
}, [autoModeByProject, projectId]);
|
}, [autoModeByProject, projectId]);
|
||||||
|
|
||||||
const isAutoModeRunning = projectAutoModeState.isRunning;
|
const isAutoModeRunning = projectAutoModeState.isRunning;
|
||||||
@@ -62,10 +67,10 @@ export function useAutoMode() {
|
|||||||
// Events include projectPath from backend - use it to look up project ID
|
// Events include projectPath from backend - use it to look up project ID
|
||||||
// Fall back to current projectId if not provided in event
|
// Fall back to current projectId if not provided in event
|
||||||
let eventProjectId: string | undefined;
|
let eventProjectId: string | undefined;
|
||||||
if ('projectPath' in event && event.projectPath) {
|
if ("projectPath" in event && event.projectPath) {
|
||||||
eventProjectId = getProjectIdFromPath(event.projectPath);
|
eventProjectId = getProjectIdFromPath(event.projectPath);
|
||||||
}
|
}
|
||||||
if (!eventProjectId && 'projectId' in event && event.projectId) {
|
if (!eventProjectId && "projectId" in event && event.projectId) {
|
||||||
eventProjectId = event.projectId;
|
eventProjectId = event.projectId;
|
||||||
}
|
}
|
||||||
if (!eventProjectId) {
|
if (!eventProjectId) {
|
||||||
@@ -74,7 +79,10 @@ export function useAutoMode() {
|
|||||||
|
|
||||||
// Skip event if we couldn't determine the project
|
// Skip event if we couldn't determine the project
|
||||||
if (!eventProjectId) {
|
if (!eventProjectId) {
|
||||||
console.warn("[AutoMode] Could not determine project for event:", event);
|
console.warn(
|
||||||
|
"[AutoMode] Could not determine project for event:",
|
||||||
|
event
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,20 +119,41 @@ export function useAutoMode() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_complete":
|
case "auto_mode_stopped":
|
||||||
// All features completed for this project
|
// Auto mode was explicitly stopped (by user or error)
|
||||||
setAutoModeRunning(eventProjectId, false);
|
setAutoModeRunning(eventProjectId, false);
|
||||||
clearRunningTasks(eventProjectId);
|
clearRunningTasks(eventProjectId);
|
||||||
console.log("[AutoMode] All features completed!");
|
console.log("[AutoMode] Auto mode stopped");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_mode_started":
|
||||||
|
// Auto mode started - ensure UI reflects running state
|
||||||
|
console.log("[AutoMode] Auto mode started:", event.message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_mode_idle":
|
||||||
|
// Auto mode is running but has no pending features to pick up
|
||||||
|
// This is NOT a stop - auto mode keeps running and will pick up new features
|
||||||
|
console.log("[AutoMode] Auto mode idle - waiting for new features");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_mode_complete":
|
||||||
|
// Legacy event - only handle if it looks like a stop (for backwards compatibility)
|
||||||
|
if (event.message === "Auto mode stopped") {
|
||||||
|
setAutoModeRunning(eventProjectId, false);
|
||||||
|
clearRunningTasks(eventProjectId);
|
||||||
|
console.log("[AutoMode] Auto mode stopped (legacy event)");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_error":
|
case "auto_mode_error":
|
||||||
console.error("[AutoMode Error]", event.error);
|
console.error("[AutoMode Error]", event.error);
|
||||||
if (event.featureId && event.error) {
|
if (event.featureId && event.error) {
|
||||||
// Check for authentication errors and provide a more helpful message
|
// Check for authentication errors and provide a more helpful message
|
||||||
const isAuthError = event.errorType === "authentication" ||
|
const isAuthError =
|
||||||
event.error.includes("Authentication failed") ||
|
event.errorType === "authentication" ||
|
||||||
event.error.includes("Invalid API key");
|
event.error.includes("Authentication failed") ||
|
||||||
|
event.error.includes("Invalid API key");
|
||||||
|
|
||||||
const errorMessage = isAuthError
|
const errorMessage = isAuthError
|
||||||
? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.`
|
? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.`
|
||||||
@@ -202,11 +231,12 @@ export function useAutoMode() {
|
|||||||
if (!api?.autoMode) return;
|
if (!api?.autoMode) return;
|
||||||
|
|
||||||
// Find all projects that have auto mode marked as running
|
// Find all projects that have auto mode marked as running
|
||||||
const projectsToRestart: Array<{ projectId: string; projectPath: string }> = [];
|
const projectsToRestart: Array<{ projectId: string; projectPath: string }> =
|
||||||
|
[];
|
||||||
for (const [projectId, state] of Object.entries(autoModeByProject)) {
|
for (const [projectId, state] of Object.entries(autoModeByProject)) {
|
||||||
if (state.isRunning) {
|
if (state.isRunning) {
|
||||||
// Find the project path for this project ID
|
// Find the project path for this project ID
|
||||||
const project = projects.find(p => p.id === projectId);
|
const project = projects.find((p) => p.id === projectId);
|
||||||
if (project) {
|
if (project) {
|
||||||
projectsToRestart.push({ projectId, projectPath: project.path });
|
projectsToRestart.push({ projectId, projectPath: project.path });
|
||||||
}
|
}
|
||||||
@@ -216,18 +246,27 @@ export function useAutoMode() {
|
|||||||
// Restart auto mode for each project
|
// Restart auto mode for each project
|
||||||
for (const { projectId, projectPath } of projectsToRestart) {
|
for (const { projectId, projectPath } of projectsToRestart) {
|
||||||
console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`);
|
console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`);
|
||||||
api.autoMode.start(projectPath, maxConcurrency).then(result => {
|
api.autoMode
|
||||||
if (!result.success) {
|
.start(projectPath, maxConcurrency)
|
||||||
console.error(`[AutoMode] Failed to restore auto mode for ${projectPath}:`, result.error);
|
.then((result) => {
|
||||||
// Mark as not running if we couldn't restart
|
if (!result.success) {
|
||||||
|
console.error(
|
||||||
|
`[AutoMode] Failed to restore auto mode for ${projectPath}:`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
// Mark as not running if we couldn't restart
|
||||||
|
setAutoModeRunning(projectId, false);
|
||||||
|
} else {
|
||||||
|
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
`[AutoMode] Error restoring auto mode for ${projectPath}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
setAutoModeRunning(projectId, false);
|
setAutoModeRunning(projectId, false);
|
||||||
} else {
|
});
|
||||||
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error(`[AutoMode] Error restoring auto mode for ${projectPath}:`, error);
|
|
||||||
setAutoModeRunning(projectId, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Only run once on mount - intentionally empty dependency array
|
// Only run once on mount - intentionally empty dependency array
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -246,11 +285,16 @@ export function useAutoMode() {
|
|||||||
throw new Error("Auto mode API not available");
|
throw new Error("Auto mode API not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.autoMode.start(currentProject.path, maxConcurrency);
|
const result = await api.autoMode.start(
|
||||||
|
currentProject.path,
|
||||||
|
maxConcurrency
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setAutoModeRunning(currentProject.id, true);
|
setAutoModeRunning(currentProject.id, true);
|
||||||
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`);
|
console.log(
|
||||||
|
`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to start:", result.error);
|
console.error("[AutoMode] Failed to start:", result.error);
|
||||||
throw new Error(result.error || "Failed to start auto mode");
|
throw new Error(result.error || "Failed to start auto mode");
|
||||||
@@ -285,7 +329,9 @@ export function useAutoMode() {
|
|||||||
// Stopping auto mode only turns off the toggle to prevent new features
|
// Stopping auto mode only turns off the toggle to prevent new features
|
||||||
// from being picked up. Running tasks will complete naturally and be
|
// from being picked up. Running tasks will complete naturally and be
|
||||||
// removed via the auto_mode_feature_complete event.
|
// removed via the auto_mode_feature_complete event.
|
||||||
console.log("[AutoMode] Stopped successfully - running tasks will continue");
|
console.log(
|
||||||
|
"[AutoMode] Stopped successfully - running tasks will continue"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to stop:", result.error);
|
console.error("[AutoMode] Failed to stop:", result.error);
|
||||||
throw new Error(result.error || "Failed to stop auto mode");
|
throw new Error(result.error || "Failed to stop auto mode");
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const REQUIRED_STRUCTURE: {
|
|||||||
".automaker/features",
|
".automaker/features",
|
||||||
".automaker/images",
|
".automaker/images",
|
||||||
],
|
],
|
||||||
files: {},
|
files: {
|
||||||
|
".automaker/categories.json": "[]",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ export interface ShortcutKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to parse shortcut string to ShortcutKey object
|
// Helper to parse shortcut string to ShortcutKey object
|
||||||
export function parseShortcut(shortcut: string | undefined | null): ShortcutKey {
|
export function parseShortcut(
|
||||||
|
shortcut: string | undefined | null
|
||||||
|
): ShortcutKey {
|
||||||
if (!shortcut) return { key: "" };
|
if (!shortcut) return { key: "" };
|
||||||
const parts = shortcut.split("+").map((p) => p.trim());
|
const parts = shortcut.split("+").map((p) => p.trim());
|
||||||
const result: ShortcutKey = { key: parts[parts.length - 1] };
|
const result: ShortcutKey = { key: parts[parts.length - 1] };
|
||||||
@@ -82,7 +84,10 @@ export function parseShortcut(shortcut: string | undefined | null): ShortcutKey
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to format ShortcutKey to display string
|
// Helper to format ShortcutKey to display string
|
||||||
export function formatShortcut(shortcut: string | undefined | null, forDisplay = false): string {
|
export function formatShortcut(
|
||||||
|
shortcut: string | undefined | null,
|
||||||
|
forDisplay = false
|
||||||
|
): string {
|
||||||
if (!shortcut) return "";
|
if (!shortcut) return "";
|
||||||
const parsed = parseShortcut(shortcut);
|
const parsed = parseShortcut(shortcut);
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@@ -179,7 +184,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
|
|||||||
context: "C",
|
context: "C",
|
||||||
settings: "S",
|
settings: "S",
|
||||||
profiles: "M",
|
profiles: "M",
|
||||||
terminal: "Cmd+`",
|
terminal: "T",
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
toggleSidebar: "`",
|
toggleSidebar: "`",
|
||||||
@@ -308,7 +313,12 @@ export interface ProjectAnalysis {
|
|||||||
// Terminal panel layout types (recursive for splits)
|
// Terminal panel layout types (recursive for splits)
|
||||||
export type TerminalPanelContent =
|
export type TerminalPanelContent =
|
||||||
| { type: "terminal"; sessionId: string; size?: number; fontSize?: number }
|
| { type: "terminal"; sessionId: string; size?: number; fontSize?: number }
|
||||||
| { type: "split"; direction: "horizontal" | "vertical"; panels: TerminalPanelContent[]; size?: number };
|
| {
|
||||||
|
type: "split";
|
||||||
|
direction: "horizontal" | "vertical";
|
||||||
|
panels: TerminalPanelContent[];
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// Terminal tab - each tab has its own layout
|
// Terminal tab - each tab has its own layout
|
||||||
export interface TerminalTab {
|
export interface TerminalTab {
|
||||||
@@ -600,7 +610,11 @@ export interface AppActions {
|
|||||||
// Terminal actions
|
// Terminal actions
|
||||||
setTerminalUnlocked: (unlocked: boolean, token?: string) => void;
|
setTerminalUnlocked: (unlocked: boolean, token?: string) => void;
|
||||||
setActiveTerminalSession: (sessionId: string | null) => void;
|
setActiveTerminalSession: (sessionId: string | null) => void;
|
||||||
addTerminalToLayout: (sessionId: string, direction?: "horizontal" | "vertical", targetSessionId?: string) => void;
|
addTerminalToLayout: (
|
||||||
|
sessionId: string,
|
||||||
|
direction?: "horizontal" | "vertical",
|
||||||
|
targetSessionId?: string
|
||||||
|
) => void;
|
||||||
removeTerminalFromLayout: (sessionId: string) => void;
|
removeTerminalFromLayout: (sessionId: string) => void;
|
||||||
swapTerminals: (sessionId1: string, sessionId2: string) => void;
|
swapTerminals: (sessionId1: string, sessionId2: string) => void;
|
||||||
clearTerminalState: () => void;
|
clearTerminalState: () => void;
|
||||||
@@ -610,7 +624,11 @@ export interface AppActions {
|
|||||||
setActiveTerminalTab: (tabId: string) => void;
|
setActiveTerminalTab: (tabId: string) => void;
|
||||||
renameTerminalTab: (tabId: string, name: string) => void;
|
renameTerminalTab: (tabId: string, name: string) => void;
|
||||||
moveTerminalToTab: (sessionId: string, targetTabId: string | "new") => void;
|
moveTerminalToTab: (sessionId: string, targetTabId: string | "new") => void;
|
||||||
addTerminalToTab: (sessionId: string, tabId: string, direction?: "horizontal" | "vertical") => void;
|
addTerminalToTab: (
|
||||||
|
sessionId: string,
|
||||||
|
tabId: string,
|
||||||
|
direction?: "horizontal" | "vertical"
|
||||||
|
) => void;
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
@@ -1331,8 +1349,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
|
|
||||||
resetAIProfiles: () => {
|
resetAIProfiles: () => {
|
||||||
// Merge: keep user-created profiles, but refresh all built-in profiles to latest defaults
|
// Merge: keep user-created profiles, but refresh all built-in profiles to latest defaults
|
||||||
const defaultProfileIds = new Set(DEFAULT_AI_PROFILES.map(p => p.id));
|
const defaultProfileIds = new Set(DEFAULT_AI_PROFILES.map((p) => p.id));
|
||||||
const userProfiles = get().aiProfiles.filter(p => !p.isBuiltIn && !defaultProfileIds.has(p.id));
|
const userProfiles = get().aiProfiles.filter(
|
||||||
|
(p) => !p.isBuiltIn && !defaultProfileIds.has(p.id)
|
||||||
|
);
|
||||||
set({ aiProfiles: [...DEFAULT_AI_PROFILES, ...userProfiles] });
|
set({ aiProfiles: [...DEFAULT_AI_PROFILES, ...userProfiles] });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1528,9 +1548,17 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addTerminalToLayout: (sessionId, direction = "horizontal", targetSessionId) => {
|
addTerminalToLayout: (
|
||||||
|
sessionId,
|
||||||
|
direction = "horizontal",
|
||||||
|
targetSessionId
|
||||||
|
) => {
|
||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const newTerminal: TerminalPanelContent = { type: "terminal", sessionId, size: 50 };
|
const newTerminal: TerminalPanelContent = {
|
||||||
|
type: "terminal",
|
||||||
|
sessionId,
|
||||||
|
size: 50,
|
||||||
|
};
|
||||||
|
|
||||||
// If no tabs, create first tab
|
// If no tabs, create first tab
|
||||||
if (current.tabs.length === 0) {
|
if (current.tabs.length === 0) {
|
||||||
@@ -1538,7 +1566,13 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
set({
|
set({
|
||||||
terminalState: {
|
terminalState: {
|
||||||
...current,
|
...current,
|
||||||
tabs: [{ id: newTabId, name: "Terminal 1", layout: { type: "terminal", sessionId, size: 100 } }],
|
tabs: [
|
||||||
|
{
|
||||||
|
id: newTabId,
|
||||||
|
name: "Terminal 1",
|
||||||
|
layout: { type: "terminal", sessionId, size: 100 },
|
||||||
|
},
|
||||||
|
],
|
||||||
activeTabId: newTabId,
|
activeTabId: newTabId,
|
||||||
activeSessionId: sessionId,
|
activeSessionId: sessionId,
|
||||||
},
|
},
|
||||||
@@ -1547,7 +1581,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add to active tab's layout
|
// Add to active tab's layout
|
||||||
const activeTab = current.tabs.find(t => t.id === current.activeTabId);
|
const activeTab = current.tabs.find(
|
||||||
|
(t) => t.id === current.activeTabId
|
||||||
|
);
|
||||||
if (!activeTab) return;
|
if (!activeTab) return;
|
||||||
|
|
||||||
// If targetSessionId is provided, find and split that specific terminal
|
// If targetSessionId is provided, find and split that specific terminal
|
||||||
@@ -1571,7 +1607,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
// It's a split - recurse into panels
|
// It's a split - recurse into panels
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
panels: node.panels.map(p => splitTargetTerminal(p, targetId, targetDirection)),
|
panels: node.panels.map((p) =>
|
||||||
|
splitTargetTerminal(p, targetId, targetDirection)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1592,7 +1630,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const newSize = 100 / (node.panels.length + 1);
|
const newSize = 100 / (node.panels.length + 1);
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
panels: [...node.panels.map(p => ({ ...p, size: newSize })), { ...newTerminal, size: newSize }],
|
panels: [
|
||||||
|
...node.panels.map((p) => ({ ...p, size: newSize })),
|
||||||
|
{ ...newTerminal, size: newSize },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Different direction, wrap in new split
|
// Different direction, wrap in new split
|
||||||
@@ -1607,12 +1648,16 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
if (!activeTab.layout) {
|
if (!activeTab.layout) {
|
||||||
newLayout = { type: "terminal", sessionId, size: 100 };
|
newLayout = { type: "terminal", sessionId, size: 100 };
|
||||||
} else if (targetSessionId) {
|
} else if (targetSessionId) {
|
||||||
newLayout = splitTargetTerminal(activeTab.layout, targetSessionId, direction);
|
newLayout = splitTargetTerminal(
|
||||||
|
activeTab.layout,
|
||||||
|
targetSessionId,
|
||||||
|
direction
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
newLayout = addToRootLayout(activeTab.layout, direction);
|
newLayout = addToRootLayout(activeTab.layout, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTabs = current.tabs.map(t =>
|
const newTabs = current.tabs.map((t) =>
|
||||||
t.id === current.activeTabId ? { ...t, layout: newLayout } : t
|
t.id === current.activeTabId ? { ...t, layout: newLayout } : t
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1630,7 +1675,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
if (current.tabs.length === 0) return;
|
if (current.tabs.length === 0) return;
|
||||||
|
|
||||||
// Find which tab contains this session
|
// Find which tab contains this session
|
||||||
const findFirstTerminal = (node: TerminalPanelContent | null): string | null => {
|
const findFirstTerminal = (
|
||||||
|
node: TerminalPanelContent | null
|
||||||
|
): string | null => {
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
if (node.type === "terminal") return node.sessionId;
|
if (node.type === "terminal") return node.sessionId;
|
||||||
for (const panel of node.panels) {
|
for (const panel of node.panels) {
|
||||||
@@ -1640,7 +1687,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeAndCollapse = (node: TerminalPanelContent): TerminalPanelContent | null => {
|
const removeAndCollapse = (
|
||||||
|
node: TerminalPanelContent
|
||||||
|
): TerminalPanelContent | null => {
|
||||||
if (node.type === "terminal") {
|
if (node.type === "terminal") {
|
||||||
return node.sessionId === sessionId ? null : node;
|
return node.sessionId === sessionId ? null : node;
|
||||||
}
|
}
|
||||||
@@ -1654,19 +1703,27 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
return { ...node, panels: newPanels };
|
return { ...node, panels: newPanels };
|
||||||
};
|
};
|
||||||
|
|
||||||
let newTabs = current.tabs.map(tab => {
|
let newTabs = current.tabs.map((tab) => {
|
||||||
if (!tab.layout) return tab;
|
if (!tab.layout) return tab;
|
||||||
const newLayout = removeAndCollapse(tab.layout);
|
const newLayout = removeAndCollapse(tab.layout);
|
||||||
return { ...tab, layout: newLayout };
|
return { ...tab, layout: newLayout };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove empty tabs
|
// Remove empty tabs
|
||||||
newTabs = newTabs.filter(tab => tab.layout !== null);
|
newTabs = newTabs.filter((tab) => tab.layout !== null);
|
||||||
|
|
||||||
// Determine new active session
|
// Determine new active session
|
||||||
const newActiveTabId = newTabs.length > 0 ? (current.activeTabId && newTabs.find(t => t.id === current.activeTabId) ? current.activeTabId : newTabs[0].id) : null;
|
const newActiveTabId =
|
||||||
|
newTabs.length > 0
|
||||||
|
? current.activeTabId &&
|
||||||
|
newTabs.find((t) => t.id === current.activeTabId)
|
||||||
|
? current.activeTabId
|
||||||
|
: newTabs[0].id
|
||||||
|
: null;
|
||||||
const newActiveSessionId = newActiveTabId
|
const newActiveSessionId = newActiveTabId
|
||||||
? findFirstTerminal(newTabs.find(t => t.id === newActiveTabId)?.layout || null)
|
? findFirstTerminal(
|
||||||
|
newTabs.find((t) => t.id === newActiveTabId)?.layout || null
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
set({
|
set({
|
||||||
@@ -1683,16 +1740,20 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
if (current.tabs.length === 0) return;
|
if (current.tabs.length === 0) return;
|
||||||
|
|
||||||
const swapInLayout = (node: TerminalPanelContent): TerminalPanelContent => {
|
const swapInLayout = (
|
||||||
|
node: TerminalPanelContent
|
||||||
|
): TerminalPanelContent => {
|
||||||
if (node.type === "terminal") {
|
if (node.type === "terminal") {
|
||||||
if (node.sessionId === sessionId1) return { ...node, sessionId: sessionId2 };
|
if (node.sessionId === sessionId1)
|
||||||
if (node.sessionId === sessionId2) return { ...node, sessionId: sessionId1 };
|
return { ...node, sessionId: sessionId2 };
|
||||||
|
if (node.sessionId === sessionId2)
|
||||||
|
return { ...node, sessionId: sessionId1 };
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
return { ...node, panels: node.panels.map(swapInLayout) };
|
return { ...node, panels: node.panels.map(swapInLayout) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const newTabs = current.tabs.map(tab => ({
|
const newTabs = current.tabs.map((tab) => ({
|
||||||
...tab,
|
...tab,
|
||||||
layout: tab.layout ? swapInLayout(tab.layout) : null,
|
layout: tab.layout ? swapInLayout(tab.layout) : null,
|
||||||
}));
|
}));
|
||||||
@@ -1719,7 +1780,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const clampedSize = Math.max(8, Math.min(32, fontSize));
|
const clampedSize = Math.max(8, Math.min(32, fontSize));
|
||||||
|
|
||||||
const updateFontSize = (node: TerminalPanelContent): TerminalPanelContent => {
|
const updateFontSize = (
|
||||||
|
node: TerminalPanelContent
|
||||||
|
): TerminalPanelContent => {
|
||||||
if (node.type === "terminal") {
|
if (node.type === "terminal") {
|
||||||
if (node.sessionId === sessionId) {
|
if (node.sessionId === sessionId) {
|
||||||
return { ...node, fontSize: clampedSize };
|
return { ...node, fontSize: clampedSize };
|
||||||
@@ -1729,7 +1792,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
return { ...node, panels: node.panels.map(updateFontSize) };
|
return { ...node, panels: node.panels.map(updateFontSize) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const newTabs = current.tabs.map(tab => {
|
const newTabs = current.tabs.map((tab) => {
|
||||||
if (!tab.layout) return tab;
|
if (!tab.layout) return tab;
|
||||||
return { ...tab, layout: updateFontSize(tab.layout) };
|
return { ...tab, layout: updateFontSize(tab.layout) };
|
||||||
});
|
});
|
||||||
@@ -1743,7 +1806,11 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const newTabId = `tab-${Date.now()}`;
|
const newTabId = `tab-${Date.now()}`;
|
||||||
const tabNumber = current.tabs.length + 1;
|
const tabNumber = current.tabs.length + 1;
|
||||||
const newTab: TerminalTab = { id: newTabId, name: name || `Terminal ${tabNumber}`, layout: null };
|
const newTab: TerminalTab = {
|
||||||
|
id: newTabId,
|
||||||
|
name: name || `Terminal ${tabNumber}`,
|
||||||
|
layout: null,
|
||||||
|
};
|
||||||
set({
|
set({
|
||||||
terminalState: {
|
terminalState: {
|
||||||
...current,
|
...current,
|
||||||
@@ -1756,14 +1823,14 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
|
|
||||||
removeTerminalTab: (tabId) => {
|
removeTerminalTab: (tabId) => {
|
||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const newTabs = current.tabs.filter(t => t.id !== tabId);
|
const newTabs = current.tabs.filter((t) => t.id !== tabId);
|
||||||
let newActiveTabId = current.activeTabId;
|
let newActiveTabId = current.activeTabId;
|
||||||
let newActiveSessionId = current.activeSessionId;
|
let newActiveSessionId = current.activeSessionId;
|
||||||
|
|
||||||
if (current.activeTabId === tabId) {
|
if (current.activeTabId === tabId) {
|
||||||
newActiveTabId = newTabs.length > 0 ? newTabs[0].id : null;
|
newActiveTabId = newTabs.length > 0 ? newTabs[0].id : null;
|
||||||
if (newActiveTabId) {
|
if (newActiveTabId) {
|
||||||
const newActiveTab = newTabs.find(t => t.id === newActiveTabId);
|
const newActiveTab = newTabs.find((t) => t.id === newActiveTabId);
|
||||||
const findFirst = (node: TerminalPanelContent): string | null => {
|
const findFirst = (node: TerminalPanelContent): string | null => {
|
||||||
if (node.type === "terminal") return node.sessionId;
|
if (node.type === "terminal") return node.sessionId;
|
||||||
for (const p of node.panels) {
|
for (const p of node.panels) {
|
||||||
@@ -1772,20 +1839,27 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
newActiveSessionId = newActiveTab?.layout ? findFirst(newActiveTab.layout) : null;
|
newActiveSessionId = newActiveTab?.layout
|
||||||
|
? findFirst(newActiveTab.layout)
|
||||||
|
: null;
|
||||||
} else {
|
} else {
|
||||||
newActiveSessionId = null;
|
newActiveSessionId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set({
|
set({
|
||||||
terminalState: { ...current, tabs: newTabs, activeTabId: newActiveTabId, activeSessionId: newActiveSessionId },
|
terminalState: {
|
||||||
|
...current,
|
||||||
|
tabs: newTabs,
|
||||||
|
activeTabId: newActiveTabId,
|
||||||
|
activeSessionId: newActiveSessionId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setActiveTerminalTab: (tabId) => {
|
setActiveTerminalTab: (tabId) => {
|
||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const tab = current.tabs.find(t => t.id === tabId);
|
const tab = current.tabs.find((t) => t.id === tabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
let newActiveSessionId = current.activeSessionId;
|
let newActiveSessionId = current.activeSessionId;
|
||||||
@@ -1802,13 +1876,19 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set({
|
set({
|
||||||
terminalState: { ...current, activeTabId: tabId, activeSessionId: newActiveSessionId },
|
terminalState: {
|
||||||
|
...current,
|
||||||
|
activeTabId: tabId,
|
||||||
|
activeSessionId: newActiveSessionId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renameTerminalTab: (tabId, name) => {
|
renameTerminalTab: (tabId, name) => {
|
||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const newTabs = current.tabs.map(t => t.id === tabId ? { ...t, name } : t);
|
const newTabs = current.tabs.map((t) =>
|
||||||
|
t.id === tabId ? { ...t, name } : t
|
||||||
|
);
|
||||||
set({
|
set({
|
||||||
terminalState: { ...current, tabs: newTabs },
|
terminalState: { ...current, tabs: newTabs },
|
||||||
});
|
});
|
||||||
@@ -1818,9 +1898,13 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
|
|
||||||
let sourceTabId: string | null = null;
|
let sourceTabId: string | null = null;
|
||||||
let originalTerminalNode: (TerminalPanelContent & { type: "terminal" }) | null = null;
|
let originalTerminalNode:
|
||||||
|
| (TerminalPanelContent & { type: "terminal" })
|
||||||
|
| null = null;
|
||||||
|
|
||||||
const findTerminal = (node: TerminalPanelContent): (TerminalPanelContent & { type: "terminal" }) | null => {
|
const findTerminal = (
|
||||||
|
node: TerminalPanelContent
|
||||||
|
): (TerminalPanelContent & { type: "terminal" }) | null => {
|
||||||
if (node.type === "terminal") {
|
if (node.type === "terminal") {
|
||||||
return node.sessionId === sessionId ? node : null;
|
return node.sessionId === sessionId ? node : null;
|
||||||
}
|
}
|
||||||
@@ -1844,10 +1928,12 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
if (!sourceTabId || !originalTerminalNode) return;
|
if (!sourceTabId || !originalTerminalNode) return;
|
||||||
if (sourceTabId === targetTabId) return;
|
if (sourceTabId === targetTabId) return;
|
||||||
|
|
||||||
const sourceTab = current.tabs.find(t => t.id === sourceTabId);
|
const sourceTab = current.tabs.find((t) => t.id === sourceTabId);
|
||||||
if (!sourceTab?.layout) return;
|
if (!sourceTab?.layout) return;
|
||||||
|
|
||||||
const removeAndCollapse = (node: TerminalPanelContent): TerminalPanelContent | null => {
|
const removeAndCollapse = (
|
||||||
|
node: TerminalPanelContent
|
||||||
|
): TerminalPanelContent | null => {
|
||||||
if (node.type === "terminal") {
|
if (node.type === "terminal") {
|
||||||
return node.sessionId === sessionId ? null : node;
|
return node.sessionId === sessionId ? null : node;
|
||||||
}
|
}
|
||||||
@@ -1869,21 +1955,42 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
if (targetTabId === "new") {
|
if (targetTabId === "new") {
|
||||||
const newTabId = `tab-${Date.now()}`;
|
const newTabId = `tab-${Date.now()}`;
|
||||||
const sourceWillBeRemoved = !newSourceLayout;
|
const sourceWillBeRemoved = !newSourceLayout;
|
||||||
const tabName = sourceWillBeRemoved ? sourceTab.name : `Terminal ${current.tabs.length + 1}`;
|
const tabName = sourceWillBeRemoved
|
||||||
|
? sourceTab.name
|
||||||
|
: `Terminal ${current.tabs.length + 1}`;
|
||||||
newTabs = [
|
newTabs = [
|
||||||
...current.tabs,
|
...current.tabs,
|
||||||
{ id: newTabId, name: tabName, layout: { type: "terminal", sessionId, size: 100, fontSize: originalTerminalNode.fontSize } },
|
{
|
||||||
|
id: newTabId,
|
||||||
|
name: tabName,
|
||||||
|
layout: {
|
||||||
|
type: "terminal",
|
||||||
|
sessionId,
|
||||||
|
size: 100,
|
||||||
|
fontSize: originalTerminalNode.fontSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
finalTargetTabId = newTabId;
|
finalTargetTabId = newTabId;
|
||||||
} else {
|
} else {
|
||||||
const targetTab = current.tabs.find(t => t.id === targetTabId);
|
const targetTab = current.tabs.find((t) => t.id === targetTabId);
|
||||||
if (!targetTab) return;
|
if (!targetTab) return;
|
||||||
|
|
||||||
const terminalNode: TerminalPanelContent = { type: "terminal", sessionId, size: 50, fontSize: originalTerminalNode.fontSize };
|
const terminalNode: TerminalPanelContent = {
|
||||||
|
type: "terminal",
|
||||||
|
sessionId,
|
||||||
|
size: 50,
|
||||||
|
fontSize: originalTerminalNode.fontSize,
|
||||||
|
};
|
||||||
let newTargetLayout: TerminalPanelContent;
|
let newTargetLayout: TerminalPanelContent;
|
||||||
|
|
||||||
if (!targetTab.layout) {
|
if (!targetTab.layout) {
|
||||||
newTargetLayout = { type: "terminal", sessionId, size: 100, fontSize: originalTerminalNode.fontSize };
|
newTargetLayout = {
|
||||||
|
type: "terminal",
|
||||||
|
sessionId,
|
||||||
|
size: 100,
|
||||||
|
fontSize: originalTerminalNode.fontSize,
|
||||||
|
};
|
||||||
} else if (targetTab.layout.type === "terminal") {
|
} else if (targetTab.layout.type === "terminal") {
|
||||||
newTargetLayout = {
|
newTargetLayout = {
|
||||||
type: "split",
|
type: "split",
|
||||||
@@ -1897,15 +2004,15 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
newTabs = current.tabs.map(t =>
|
newTabs = current.tabs.map((t) =>
|
||||||
t.id === targetTabId ? { ...t, layout: newTargetLayout } : t
|
t.id === targetTabId ? { ...t, layout: newTargetLayout } : t
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newSourceLayout) {
|
if (!newSourceLayout) {
|
||||||
newTabs = newTabs.filter(t => t.id !== sourceTabId);
|
newTabs = newTabs.filter((t) => t.id !== sourceTabId);
|
||||||
} else {
|
} else {
|
||||||
newTabs = newTabs.map(t =>
|
newTabs = newTabs.map((t) =>
|
||||||
t.id === sourceTabId ? { ...t, layout: newSourceLayout } : t
|
t.id === sourceTabId ? { ...t, layout: newSourceLayout } : t
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1922,10 +2029,14 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
|
|
||||||
addTerminalToTab: (sessionId, tabId, direction = "horizontal") => {
|
addTerminalToTab: (sessionId, tabId, direction = "horizontal") => {
|
||||||
const current = get().terminalState;
|
const current = get().terminalState;
|
||||||
const tab = current.tabs.find(t => t.id === tabId);
|
const tab = current.tabs.find((t) => t.id === tabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const terminalNode: TerminalPanelContent = { type: "terminal", sessionId, size: 50 };
|
const terminalNode: TerminalPanelContent = {
|
||||||
|
type: "terminal",
|
||||||
|
sessionId,
|
||||||
|
size: 50,
|
||||||
|
};
|
||||||
let newLayout: TerminalPanelContent;
|
let newLayout: TerminalPanelContent;
|
||||||
|
|
||||||
if (!tab.layout) {
|
if (!tab.layout) {
|
||||||
@@ -1941,7 +2052,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const newSize = 100 / (tab.layout.panels.length + 1);
|
const newSize = 100 / (tab.layout.panels.length + 1);
|
||||||
newLayout = {
|
newLayout = {
|
||||||
...tab.layout,
|
...tab.layout,
|
||||||
panels: [...tab.layout.panels.map(p => ({ ...p, size: newSize })), { ...terminalNode, size: newSize }],
|
panels: [
|
||||||
|
...tab.layout.panels.map((p) => ({ ...p, size: newSize })),
|
||||||
|
{ ...terminalNode, size: newSize },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
newLayout = {
|
newLayout = {
|
||||||
@@ -1952,7 +2066,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTabs = current.tabs.map(t =>
|
const newTabs = current.tabs.map((t) =>
|
||||||
t.id === tabId ? { ...t, layout: newLayout } : t
|
t.id === tabId ? { ...t, layout: newLayout } : t
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1971,7 +2085,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "automaker-storage",
|
name: "automaker-storage",
|
||||||
version: 1, // Increment when making breaking changes to persisted state
|
version: 2, // Increment when making breaking changes to persisted state
|
||||||
migrate: (persistedState: unknown, version: number) => {
|
migrate: (persistedState: unknown, version: number) => {
|
||||||
const state = persistedState as Partial<AppState>;
|
const state = persistedState as Partial<AppState>;
|
||||||
|
|
||||||
@@ -1983,6 +2097,21 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration from version 1 to version 2:
|
||||||
|
// - Change terminal shortcut from "Cmd+`" to "T"
|
||||||
|
if (version <= 1) {
|
||||||
|
if (
|
||||||
|
state.keyboardShortcuts?.terminal === "Cmd+`" ||
|
||||||
|
state.keyboardShortcuts?.terminal === undefined
|
||||||
|
) {
|
||||||
|
state.keyboardShortcuts = {
|
||||||
|
...DEFAULT_KEYBOARD_SHORTCUTS,
|
||||||
|
...state.keyboardShortcuts,
|
||||||
|
terminal: "T",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state as AppState;
|
return state as AppState;
|
||||||
},
|
},
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
|
|||||||
18
apps/app/src/types/electron.d.ts
vendored
18
apps/app/src/types/electron.d.ts
vendored
@@ -203,6 +203,24 @@ export type AutoModeEvent =
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_stopped";
|
||||||
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_started";
|
||||||
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "auto_mode_idle";
|
||||||
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_phase";
|
type: "auto_mode_phase";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ import { createSpecRegenerationRoutes } from "./routes/spec-regeneration.js";
|
|||||||
import { createRunningAgentsRoutes } from "./routes/running-agents.js";
|
import { createRunningAgentsRoutes } from "./routes/running-agents.js";
|
||||||
import { createWorkspaceRoutes } from "./routes/workspace.js";
|
import { createWorkspaceRoutes } from "./routes/workspace.js";
|
||||||
import { createTemplatesRoutes } from "./routes/templates.js";
|
import { createTemplatesRoutes } from "./routes/templates.js";
|
||||||
import { createTerminalRoutes, validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired } from "./routes/terminal.js";
|
import {
|
||||||
|
createTerminalRoutes,
|
||||||
|
validateTerminalToken,
|
||||||
|
isTerminalEnabled,
|
||||||
|
isTerminalPasswordRequired,
|
||||||
|
} from "./routes/terminal.js";
|
||||||
import { AgentService } from "./services/agent-service.js";
|
import { AgentService } from "./services/agent-service.js";
|
||||||
import { FeatureLoader } from "./services/feature-loader.js";
|
import { FeatureLoader } from "./services/feature-loader.js";
|
||||||
import { AutoModeService } from "./services/auto-mode-service.js";
|
import { AutoModeService } from "./services/auto-mode-service.js";
|
||||||
@@ -64,7 +69,9 @@ if (!hasAnthropicKey && !hasOAuthToken) {
|
|||||||
╚═══════════════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════════════╝
|
||||||
`);
|
`);
|
||||||
} else if (hasOAuthToken) {
|
} else if (hasOAuthToken) {
|
||||||
console.log("[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)");
|
console.log(
|
||||||
|
"[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
|
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
|
||||||
}
|
}
|
||||||
@@ -130,7 +137,10 @@ const terminalService = getTerminalService();
|
|||||||
|
|
||||||
// Handle HTTP upgrade requests manually to route to correct WebSocket server
|
// Handle HTTP upgrade requests manually to route to correct WebSocket server
|
||||||
server.on("upgrade", (request, socket, head) => {
|
server.on("upgrade", (request, socket, head) => {
|
||||||
const { pathname } = new URL(request.url || "", `http://${request.headers.host}`);
|
const { pathname } = new URL(
|
||||||
|
request.url || "",
|
||||||
|
`http://${request.headers.host}`
|
||||||
|
);
|
||||||
|
|
||||||
if (pathname === "/api/events") {
|
if (pathname === "/api/events") {
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
@@ -171,152 +181,198 @@ wss.on("connection", (ws: WebSocket) => {
|
|||||||
const terminalConnections: Map<string, Set<WebSocket>> = new Map();
|
const terminalConnections: Map<string, Set<WebSocket>> = new Map();
|
||||||
|
|
||||||
// Terminal WebSocket connection handler
|
// Terminal WebSocket connection handler
|
||||||
terminalWss.on("connection", (ws: WebSocket, req: import("http").IncomingMessage) => {
|
terminalWss.on(
|
||||||
// Parse URL to get session ID and token
|
"connection",
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
(ws: WebSocket, req: import("http").IncomingMessage) => {
|
||||||
const sessionId = url.searchParams.get("sessionId");
|
// Parse URL to get session ID and token
|
||||||
const token = url.searchParams.get("token");
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
const sessionId = url.searchParams.get("sessionId");
|
||||||
|
const token = url.searchParams.get("token");
|
||||||
|
|
||||||
console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`);
|
console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`);
|
||||||
|
|
||||||
// Check if terminal is enabled
|
// Check if terminal is enabled
|
||||||
if (!isTerminalEnabled()) {
|
if (!isTerminalEnabled()) {
|
||||||
console.log("[Terminal WS] Terminal is disabled");
|
console.log("[Terminal WS] Terminal is disabled");
|
||||||
ws.close(4003, "Terminal access is disabled");
|
ws.close(4003, "Terminal access is disabled");
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// Validate token if password is required
|
|
||||||
if (isTerminalPasswordRequired() && !validateTerminalToken(token || undefined)) {
|
|
||||||
console.log("[Terminal WS] Invalid or missing token");
|
|
||||||
ws.close(4001, "Authentication required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
console.log("[Terminal WS] No session ID provided");
|
|
||||||
ws.close(4002, "Session ID required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if session exists
|
|
||||||
const session = terminalService.getSession(sessionId);
|
|
||||||
if (!session) {
|
|
||||||
console.log(`[Terminal WS] Session ${sessionId} not found`);
|
|
||||||
ws.close(4004, "Session not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Terminal WS] Client connected to session ${sessionId}`);
|
|
||||||
|
|
||||||
// Track this connection
|
|
||||||
if (!terminalConnections.has(sessionId)) {
|
|
||||||
terminalConnections.set(sessionId, new Set());
|
|
||||||
}
|
|
||||||
terminalConnections.get(sessionId)!.add(ws);
|
|
||||||
|
|
||||||
// Subscribe to terminal data
|
|
||||||
const unsubscribeData = terminalService.onData((sid, data) => {
|
|
||||||
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify({ type: "data", data }));
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to terminal exit
|
// Validate token if password is required
|
||||||
const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
|
if (
|
||||||
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
isTerminalPasswordRequired() &&
|
||||||
ws.send(JSON.stringify({ type: "exit", exitCode }));
|
!validateTerminalToken(token || undefined)
|
||||||
ws.close(1000, "Session ended");
|
) {
|
||||||
|
console.log("[Terminal WS] Invalid or missing token");
|
||||||
|
ws.close(4001, "Authentication required");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Handle incoming messages
|
if (!sessionId) {
|
||||||
ws.on("message", (message) => {
|
console.log("[Terminal WS] No session ID provided");
|
||||||
try {
|
ws.close(4002, "Session ID required");
|
||||||
const msg = JSON.parse(message.toString());
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (msg.type) {
|
// Check if session exists
|
||||||
case "input":
|
const session = terminalService.getSession(sessionId);
|
||||||
// Write user input to terminal
|
if (!session) {
|
||||||
terminalService.write(sessionId, msg.data);
|
console.log(`[Terminal WS] Session ${sessionId} not found`);
|
||||||
break;
|
ws.close(4004, "Session not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case "resize":
|
console.log(`[Terminal WS] Client connected to session ${sessionId}`);
|
||||||
// Resize terminal
|
|
||||||
if (msg.cols && msg.rows) {
|
|
||||||
terminalService.resize(sessionId, msg.cols, msg.rows);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ping":
|
// Track this connection
|
||||||
// Respond to ping
|
if (!terminalConnections.has(sessionId)) {
|
||||||
ws.send(JSON.stringify({ type: "pong" }));
|
terminalConnections.set(sessionId, new Set());
|
||||||
break;
|
}
|
||||||
|
terminalConnections.get(sessionId)!.add(ws);
|
||||||
|
|
||||||
default:
|
// Subscribe to terminal data
|
||||||
console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
|
const unsubscribeData = terminalService.onData((sid, data) => {
|
||||||
|
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: "data", data }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
console.error("[Terminal WS] Error processing message:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("close", () => {
|
// Subscribe to terminal exit
|
||||||
console.log(`[Terminal WS] Client disconnected from session ${sessionId}`);
|
const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
|
||||||
unsubscribeData();
|
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
||||||
unsubscribeExit();
|
ws.send(JSON.stringify({ type: "exit", exitCode }));
|
||||||
|
ws.close(1000, "Session ended");
|
||||||
// Remove from connections tracking
|
|
||||||
const connections = terminalConnections.get(sessionId);
|
|
||||||
if (connections) {
|
|
||||||
connections.delete(ws);
|
|
||||||
if (connections.size === 0) {
|
|
||||||
terminalConnections.delete(sessionId);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle incoming messages
|
||||||
|
ws.on("message", (message) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message.toString());
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "input":
|
||||||
|
// Write user input to terminal
|
||||||
|
terminalService.write(sessionId, msg.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "resize":
|
||||||
|
// Resize terminal
|
||||||
|
if (msg.cols && msg.rows) {
|
||||||
|
terminalService.resize(sessionId, msg.cols, msg.rows);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ping":
|
||||||
|
// Respond to ping
|
||||||
|
ws.send(JSON.stringify({ type: "pong" }));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Terminal WS] Error processing message:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log(
|
||||||
|
`[Terminal WS] Client disconnected from session ${sessionId}`
|
||||||
|
);
|
||||||
|
unsubscribeData();
|
||||||
|
unsubscribeExit();
|
||||||
|
|
||||||
|
// Remove from connections tracking
|
||||||
|
const connections = terminalConnections.get(sessionId);
|
||||||
|
if (connections) {
|
||||||
|
connections.delete(ws);
|
||||||
|
if (connections.size === 0) {
|
||||||
|
terminalConnections.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (error) => {
|
||||||
|
console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
|
||||||
|
unsubscribeData();
|
||||||
|
unsubscribeExit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send initial connection success
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "connected",
|
||||||
|
sessionId,
|
||||||
|
shell: session.shell,
|
||||||
|
cwd: session.cwd,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send scrollback buffer to replay previous output
|
||||||
|
const scrollback = terminalService.getScrollback(sessionId);
|
||||||
|
if (scrollback && scrollback.length > 0) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "scrollback",
|
||||||
|
data: scrollback,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("error", (error) => {
|
|
||||||
console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
|
|
||||||
unsubscribeData();
|
|
||||||
unsubscribeExit();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send initial connection success
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: "connected",
|
|
||||||
sessionId,
|
|
||||||
shell: session.shell,
|
|
||||||
cwd: session.cwd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Send scrollback buffer to replay previous output
|
|
||||||
const scrollback = terminalService.getScrollback(sessionId);
|
|
||||||
if (scrollback && scrollback.length > 0) {
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: "scrollback",
|
|
||||||
data: scrollback,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Start server
|
// Start server with error handling for port conflicts
|
||||||
server.listen(PORT, () => {
|
const startServer = (port: number) => {
|
||||||
const terminalStatus = isTerminalEnabled()
|
server.listen(port, () => {
|
||||||
? (isTerminalPasswordRequired() ? "enabled (password protected)" : "enabled")
|
const terminalStatus = isTerminalEnabled()
|
||||||
: "disabled";
|
? isTerminalPasswordRequired()
|
||||||
console.log(`
|
? "enabled (password protected)"
|
||||||
|
: "enabled"
|
||||||
|
: "disabled";
|
||||||
|
const portStr = port.toString().padEnd(4);
|
||||||
|
console.log(`
|
||||||
╔═══════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════╗
|
||||||
║ Automaker Backend Server ║
|
║ Automaker Backend Server ║
|
||||||
╠═══════════════════════════════════════════════════════╣
|
╠═══════════════════════════════════════════════════════╣
|
||||||
║ HTTP API: http://localhost:${PORT} ║
|
║ HTTP API: http://localhost:${portStr} ║
|
||||||
║ WebSocket: ws://localhost:${PORT}/api/events ║
|
║ WebSocket: ws://localhost:${portStr}/api/events ║
|
||||||
║ Terminal: ws://localhost:${PORT}/api/terminal/ws ║
|
║ Terminal: ws://localhost:${portStr}/api/terminal/ws ║
|
||||||
║ Health: http://localhost:${PORT}/api/health ║
|
║ Health: http://localhost:${portStr}/api/health ║
|
||||||
║ Terminal: ${terminalStatus.padEnd(37)}║
|
║ Terminal: ${terminalStatus.padEnd(37)}║
|
||||||
╚═══════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════╝
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.on("error", (error: NodeJS.ErrnoException) => {
|
||||||
|
if (error.code === "EADDRINUSE") {
|
||||||
|
console.error(`
|
||||||
|
╔═══════════════════════════════════════════════════════╗
|
||||||
|
║ ❌ ERROR: Port ${port} is already in use ║
|
||||||
|
╠═══════════════════════════════════════════════════════╣
|
||||||
|
║ Another process is using this port. ║
|
||||||
|
║ ║
|
||||||
|
║ To fix this, try one of: ║
|
||||||
|
║ ║
|
||||||
|
║ 1. Kill the process using the port: ║
|
||||||
|
║ lsof -ti:${port} | xargs kill -9 ║
|
||||||
|
║ ║
|
||||||
|
║ 2. Use a different port: ║
|
||||||
|
║ PORT=${port + 1} npm run dev:server ║
|
||||||
|
║ ║
|
||||||
|
║ 3. Use the init.sh script which handles this: ║
|
||||||
|
║ ./init.sh ║
|
||||||
|
╚═══════════════════════════════════════════════════════╝
|
||||||
|
`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.error("[Server] Error starting server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
startServer(PORT);
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ export function initAllowedPaths(): void {
|
|||||||
if (dataDir) {
|
if (dataDir) {
|
||||||
allowedPaths.add(path.resolve(dataDir));
|
allowedPaths.add(path.resolve(dataDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always allow the workspace directory (where projects are created)
|
||||||
|
const workspaceDir = process.env.WORKSPACE_DIR;
|
||||||
|
if (workspaceDir) {
|
||||||
|
allowedPaths.add(path.resolve(workspaceDir));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +64,9 @@ export function validatePath(filePath: string): string {
|
|||||||
const resolved = path.resolve(filePath);
|
const resolved = path.resolve(filePath);
|
||||||
|
|
||||||
if (!isPathAllowed(resolved)) {
|
if (!isPathAllowed(resolved)) {
|
||||||
throw new Error(`Access denied: ${filePath} is not in an allowed directory`);
|
throw new Error(
|
||||||
|
`Access denied: ${filePath} is not in an allowed directory`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import { BaseProvider } from "./base-provider.js";
|
import { BaseProvider } from "./base-provider.js";
|
||||||
import { convertHistoryToMessages, normalizeContentBlocks } from "../lib/conversation-utils.js";
|
import {
|
||||||
|
convertHistoryToMessages,
|
||||||
|
normalizeContentBlocks,
|
||||||
|
} from "../lib/conversation-utils.js";
|
||||||
import type {
|
import type {
|
||||||
ExecuteOptions,
|
ExecuteOptions,
|
||||||
ProviderMessage,
|
ProviderMessage,
|
||||||
@@ -23,7 +26,9 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
/**
|
/**
|
||||||
* Execute a query using Claude Agent SDK
|
* Execute a query using Claude Agent SDK
|
||||||
*/
|
*/
|
||||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
async *executeQuery(
|
||||||
|
options: ExecuteOptions
|
||||||
|
): AsyncGenerator<ProviderMessage> {
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
@@ -36,21 +41,24 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Build Claude SDK options
|
// Build Claude SDK options
|
||||||
|
const defaultTools = [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
];
|
||||||
|
const toolsToUse = allowedTools || defaultTools;
|
||||||
|
|
||||||
const sdkOptions: Options = {
|
const sdkOptions: Options = {
|
||||||
model,
|
model,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
maxTurns,
|
maxTurns,
|
||||||
cwd,
|
cwd,
|
||||||
allowedTools: allowedTools || [
|
allowedTools: toolsToUse,
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
permissionMode: "acceptEdits",
|
permissionMode: "acceptEdits",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -60,32 +68,68 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build prompt payload with conversation history
|
// Build prompt payload with conversation history
|
||||||
let promptPayload: string | AsyncGenerator<any, void, unknown>;
|
let promptPayload: string | AsyncGenerator<any, void, unknown> | Array<any>;
|
||||||
|
|
||||||
if (conversationHistory && conversationHistory.length > 0) {
|
if (conversationHistory && conversationHistory.length > 0) {
|
||||||
// Multi-turn conversation with history
|
// Multi-turn conversation with history
|
||||||
promptPayload = (async function* () {
|
// Convert history to SDK message format
|
||||||
// Yield history messages using utility
|
// Note: When using async generator, SDK only accepts SDKUserMessage (type: 'user')
|
||||||
const historyMessages = convertHistoryToMessages(conversationHistory);
|
// So we filter to only include user messages to avoid SDK errors
|
||||||
for (const msg of historyMessages) {
|
const historyMessages = convertHistoryToMessages(conversationHistory);
|
||||||
yield msg;
|
const hasAssistantMessages = historyMessages.some(
|
||||||
}
|
(msg) => msg.type === "assistant"
|
||||||
|
);
|
||||||
|
|
||||||
// Yield current prompt
|
if (hasAssistantMessages) {
|
||||||
yield {
|
// If we have assistant messages, use async generator but filter to only user messages
|
||||||
type: "user" as const,
|
// This maintains conversation flow while respecting SDK type constraints
|
||||||
session_id: "",
|
promptPayload = (async function* () {
|
||||||
message: {
|
// Filter to only user messages - SDK async generator only accepts SDKUserMessage
|
||||||
role: "user" as const,
|
const userHistoryMessages = historyMessages.filter(
|
||||||
content: normalizeContentBlocks(prompt),
|
(msg) => msg.type === "user"
|
||||||
},
|
);
|
||||||
parent_tool_use_id: null,
|
for (const msg of userHistoryMessages) {
|
||||||
};
|
yield msg;
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
// Yield current prompt
|
||||||
|
const normalizedPrompt = normalizeContentBlocks(prompt);
|
||||||
|
const currentPrompt = {
|
||||||
|
type: "user" as const,
|
||||||
|
session_id: "",
|
||||||
|
message: {
|
||||||
|
role: "user" as const,
|
||||||
|
content: normalizedPrompt,
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
};
|
||||||
|
yield currentPrompt;
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
// Only user messages in history - can use async generator normally
|
||||||
|
promptPayload = (async function* () {
|
||||||
|
for (const msg of historyMessages) {
|
||||||
|
yield msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yield current prompt
|
||||||
|
const normalizedPrompt = normalizeContentBlocks(prompt);
|
||||||
|
const currentPrompt = {
|
||||||
|
type: "user" as const,
|
||||||
|
session_id: "",
|
||||||
|
message: {
|
||||||
|
role: "user" as const,
|
||||||
|
content: normalizedPrompt,
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
};
|
||||||
|
yield currentPrompt;
|
||||||
|
})();
|
||||||
|
}
|
||||||
} else if (Array.isArray(prompt)) {
|
} else if (Array.isArray(prompt)) {
|
||||||
// Multi-part prompt (with images) - no history
|
// Multi-part prompt (with images) - no history
|
||||||
promptPayload = (async function* () {
|
promptPayload = (async function* () {
|
||||||
yield {
|
const multiPartPrompt = {
|
||||||
type: "user" as const,
|
type: "user" as const,
|
||||||
session_id: "",
|
session_id: "",
|
||||||
message: {
|
message: {
|
||||||
@@ -94,6 +138,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
},
|
},
|
||||||
parent_tool_use_id: null,
|
parent_tool_use_id: null,
|
||||||
};
|
};
|
||||||
|
yield multiPartPrompt;
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
// Simple text prompt - no history
|
// Simple text prompt - no history
|
||||||
@@ -101,11 +146,19 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute via Claude Agent SDK
|
// Execute via Claude Agent SDK
|
||||||
const stream = query({ prompt: promptPayload, options: sdkOptions });
|
try {
|
||||||
|
const stream = query({ prompt: promptPayload, options: sdkOptions });
|
||||||
|
|
||||||
// Stream messages directly - they're already in the correct format
|
// Stream messages directly - they're already in the correct format
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
yield msg as ProviderMessage;
|
yield msg as ProviderMessage;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[ClaudeProvider] executeQuery() error during execution:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,22 +167,25 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
*/
|
*/
|
||||||
async detectInstallation(): Promise<InstallationStatus> {
|
async detectInstallation(): Promise<InstallationStatus> {
|
||||||
// Claude SDK is always available since it's a dependency
|
// Claude SDK is always available since it's a dependency
|
||||||
const hasApiKey =
|
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
||||||
!!process.env.ANTHROPIC_API_KEY || !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||||
|
const hasApiKey = hasAnthropicKey || hasOAuthToken;
|
||||||
|
|
||||||
return {
|
const status: InstallationStatus = {
|
||||||
installed: true,
|
installed: true,
|
||||||
method: "sdk",
|
method: "sdk",
|
||||||
hasApiKey,
|
hasApiKey,
|
||||||
authenticated: hasApiKey,
|
authenticated: hasApiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get available Claude models
|
* Get available Claude models
|
||||||
*/
|
*/
|
||||||
getAvailableModels(): ModelDefinition[] {
|
getAvailableModels(): ModelDefinition[] {
|
||||||
return [
|
const models = [
|
||||||
{
|
{
|
||||||
id: "claude-opus-4-5-20251101",
|
id: "claude-opus-4-5-20251101",
|
||||||
name: "Claude Opus 4.5",
|
name: "Claude Opus 4.5",
|
||||||
@@ -140,7 +196,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
maxOutputTokens: 16000,
|
maxOutputTokens: 16000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "premium",
|
tier: "premium" as const,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -153,7 +209,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
maxOutputTokens: 16000,
|
maxOutputTokens: 16000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "standard",
|
tier: "standard" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-3-5-sonnet-20241022",
|
id: "claude-3-5-sonnet-20241022",
|
||||||
@@ -165,7 +221,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
maxOutputTokens: 8000,
|
maxOutputTokens: 8000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "standard",
|
tier: "standard" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-3-5-haiku-20241022",
|
id: "claude-3-5-haiku-20241022",
|
||||||
@@ -177,9 +233,10 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
maxOutputTokens: 8000,
|
maxOutputTokens: 8000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "basic",
|
tier: "basic" as const,
|
||||||
},
|
},
|
||||||
];
|
] satisfies ModelDefinition[];
|
||||||
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { Router, type Request, type Response } from "express";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js";
|
import {
|
||||||
|
validatePath,
|
||||||
|
addAllowedPath,
|
||||||
|
isPathAllowed,
|
||||||
|
} from "../lib/security.js";
|
||||||
import type { EventEmitter } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
|
|
||||||
export function createFsRoutes(_events: EventEmitter): Router {
|
export function createFsRoutes(_events: EventEmitter): Router {
|
||||||
@@ -69,9 +73,41 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(dirPath);
|
const resolvedPath = path.resolve(dirPath);
|
||||||
|
|
||||||
|
// Security check: allow paths in allowed directories OR within home directory
|
||||||
|
const isAllowed = (() => {
|
||||||
|
// Check if path or parent is in allowed paths
|
||||||
|
if (isPathAllowed(resolvedPath)) return true;
|
||||||
|
const parentPath = path.dirname(resolvedPath);
|
||||||
|
if (isPathAllowed(parentPath)) return true;
|
||||||
|
|
||||||
|
// Also allow within home directory (like the /browse endpoint)
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
const normalizedHome = path.normalize(homeDir);
|
||||||
|
if (
|
||||||
|
resolvedPath === normalizedHome ||
|
||||||
|
resolvedPath.startsWith(normalizedHome + path.sep)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: `Access denied: ${dirPath} is not in an allowed directory`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await fs.mkdir(resolvedPath, { recursive: true });
|
await fs.mkdir(resolvedPath, { recursive: true });
|
||||||
|
|
||||||
|
// Add the new directory to allowed paths so subsequent operations work
|
||||||
|
addAllowedPath(resolvedPath);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -197,7 +233,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const stats = await fs.stat(resolvedPath);
|
const stats = await fs.stat(resolvedPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Path is not a directory" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +267,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!directoryName) {
|
if (!directoryName) {
|
||||||
res.status(400).json({ success: false, error: "directoryName is required" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "directoryName is required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +294,16 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const searchPaths: string[] = [
|
const searchPaths: string[] = [
|
||||||
process.cwd(), // Current working directory
|
process.cwd(), // Current working directory
|
||||||
process.env.HOME || process.env.USERPROFILE || "", // User home
|
process.env.HOME || process.env.USERPROFILE || "", // User home
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Documents"),
|
path.join(
|
||||||
|
process.env.HOME || process.env.USERPROFILE || "",
|
||||||
|
"Documents"
|
||||||
|
),
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
||||||
// Common project locations
|
// Common project locations
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Projects"),
|
path.join(
|
||||||
|
process.env.HOME || process.env.USERPROFILE || "",
|
||||||
|
"Projects"
|
||||||
|
),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
// Also check parent of current working directory
|
// Also check parent of current working directory
|
||||||
@@ -275,7 +321,7 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
try {
|
try {
|
||||||
const candidatePath = path.join(searchPath, directoryName);
|
const candidatePath = path.join(searchPath, directoryName);
|
||||||
const stats = await fs.stat(candidatePath);
|
const stats = await fs.stat(candidatePath);
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
// Verify it matches by checking for sample files
|
// Verify it matches by checking for sample files
|
||||||
if (sampleFiles && sampleFiles.length > 0) {
|
if (sampleFiles && sampleFiles.length > 0) {
|
||||||
@@ -284,8 +330,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
// Remove directory name prefix from sample file path
|
// Remove directory name prefix from sample file path
|
||||||
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
||||||
? sampleFile.substring(directoryName.length + 1)
|
? sampleFile.substring(directoryName.length + 1)
|
||||||
: sampleFile.split("/").slice(1).join("/") || sampleFile.split("/").pop() || sampleFile;
|
: sampleFile.split("/").slice(1).join("/") ||
|
||||||
|
sampleFile.split("/").pop() ||
|
||||||
|
sampleFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(candidatePath, relativeFile);
|
const filePath = path.join(candidatePath, relativeFile);
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
@@ -294,7 +342,7 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
// File doesn't exist, continue checking
|
// File doesn't exist, continue checking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If at least one file matches, consider it a match
|
// If at least one file matches, consider it a match
|
||||||
if (matches === 0 && sampleFiles.length > 0) {
|
if (matches === 0 && sampleFiles.length > 0) {
|
||||||
continue; // Try next candidate
|
continue; // Try next candidate
|
||||||
@@ -405,7 +453,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const stats = await fs.stat(targetPath);
|
const stats = await fs.stat(targetPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Path is not a directory" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +488,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Failed to read directory",
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to read directory",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -464,8 +515,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const fullPath = path.isAbsolute(imagePath)
|
const fullPath = path.isAbsolute(imagePath)
|
||||||
? imagePath
|
? imagePath
|
||||||
: projectPath
|
: projectPath
|
||||||
? path.join(projectPath, imagePath)
|
? path.join(projectPath, imagePath)
|
||||||
: imagePath;
|
: imagePath;
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
try {
|
try {
|
||||||
@@ -490,7 +541,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
".bmp": "image/bmp",
|
".bmp": "image/bmp",
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
|
res.setHeader(
|
||||||
|
"Content-Type",
|
||||||
|
mimeTypes[ext] || "application/octet-stream"
|
||||||
|
);
|
||||||
res.setHeader("Cache-Control", "public, max-age=3600");
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
||||||
res.send(buffer);
|
res.send(buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -546,38 +600,42 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Delete board background image
|
// Delete board background image
|
||||||
router.post("/delete-board-background", async (req: Request, res: Response) => {
|
router.post(
|
||||||
try {
|
"/delete-board-background",
|
||||||
const { projectPath } = req.body as { projectPath: string };
|
async (req: Request, res: Response) => {
|
||||||
|
|
||||||
if (!projectPath) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: "projectPath is required",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boardDir = path.join(projectPath, ".automaker", "board");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to remove all files in the board directory
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
const files = await fs.readdir(boardDir);
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith("background")) {
|
|
||||||
await fs.unlink(path.join(boardDir, file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Directory may not exist, that's fine
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true });
|
if (!projectPath) {
|
||||||
} catch (error) {
|
res.status(400).json({
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
success: false,
|
||||||
res.status(500).json({ success: false, error: message });
|
error: "projectPath is required",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardDir = path.join(projectPath, ".automaker", "board");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to remove all files in the board directory
|
||||||
|
const files = await fs.readdir(boardDir);
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith("background")) {
|
||||||
|
await fs.unlink(path.join(boardDir, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Directory may not exist, that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
|
res.status(500).json({ success: false, error: message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Browse directories for file picker
|
// Browse directories for file picker
|
||||||
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
||||||
@@ -614,7 +672,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const normalizedHome = path.resolve(homeDir);
|
const normalizedHome = path.resolve(homeDir);
|
||||||
|
|
||||||
// Allow browsing within home directory
|
// Allow browsing within home directory
|
||||||
if (resolved === normalizedHome || resolved.startsWith(normalizedHome + path.sep)) {
|
if (
|
||||||
|
resolved === normalizedHome ||
|
||||||
|
resolved.startsWith(normalizedHome + path.sep)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +707,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
if (!isSafePath(targetPath)) {
|
if (!isSafePath(targetPath)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Access denied: browsing is restricted to your home directory and allowed project paths",
|
error:
|
||||||
|
"Access denied: browsing is restricted to your home directory and allowed project paths",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -655,7 +717,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
const stats = await fs.stat(targetPath);
|
const stats = await fs.stat(targetPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.status(400).json({ success: false, error: "Path is not a directory" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Path is not a directory" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +752,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Failed to read directory",
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to read directory",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import fs from "fs/promises";
|
|||||||
import type { EventEmitter } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
import { ProviderFactory } from "../providers/provider-factory.js";
|
||||||
import type { ExecuteOptions } from "../providers/types.js";
|
import type { ExecuteOptions } from "../providers/types.js";
|
||||||
import {
|
import { readImageAsBase64 } from "../lib/image-handler.js";
|
||||||
readImageAsBase64,
|
|
||||||
} from "../lib/image-handler.js";
|
|
||||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||||
import { getEffectiveModel } from "../lib/model-resolver.js";
|
import { getEffectiveModel } from "../lib/model-resolver.js";
|
||||||
import { isAbortError } from "../lib/error-handler.js";
|
import { isAbortError } from "../lib/error-handler.js";
|
||||||
@@ -136,7 +134,10 @@ export class AgentService {
|
|||||||
filename: imageData.filename,
|
filename: imageData.filename,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[AgentService] Failed to load image ${imagePath}:`, error);
|
console.error(
|
||||||
|
`[AgentService] Failed to load image ${imagePath}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +198,8 @@ export class AgentService {
|
|||||||
"WebFetch",
|
"WebFetch",
|
||||||
],
|
],
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
conversationHistory:
|
||||||
|
conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build prompt content with images
|
// Build prompt content with images
|
||||||
@@ -381,7 +383,11 @@ export class AgentService {
|
|||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(sessionFile, JSON.stringify(messages, null, 2), "utf-8");
|
await fs.writeFile(
|
||||||
|
sessionFile,
|
||||||
|
JSON.stringify(messages, null, 2),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
await this.updateSessionTimestamp(sessionId);
|
await this.updateSessionTimestamp(sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgentService] Failed to save session:", error);
|
console.error("[AgentService] Failed to save session:", error);
|
||||||
@@ -398,7 +404,11 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
||||||
await fs.writeFile(this.metadataFile, JSON.stringify(metadata, null, 2), "utf-8");
|
await fs.writeFile(
|
||||||
|
this.metadataFile,
|
||||||
|
JSON.stringify(metadata, null, 2),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSessionTimestamp(sessionId: string): Promise<void> {
|
async updateSessionTimestamp(sessionId: string): Promise<void> {
|
||||||
@@ -418,7 +428,8 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sessions.sort(
|
return sessions.sort(
|
||||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
(a, b) =>
|
||||||
|
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +516,10 @@ export class AgentService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitAgentEvent(sessionId: string, data: Record<string, unknown>): void {
|
private emitAgentEvent(
|
||||||
|
sessionId: string,
|
||||||
|
data: Record<string, unknown>
|
||||||
|
): void {
|
||||||
this.events.emit("agent:stream", { sessionId, ...data });
|
this.events.emit("agent:stream", { sessionId, ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,15 @@ interface Feature {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
spec?: string;
|
spec?: string;
|
||||||
model?: string; // Model to use for this feature
|
model?: string; // Model to use for this feature
|
||||||
imagePaths?: Array<string | { path: string; filename?: string; mimeType?: string; [key: string]: unknown }>;
|
imagePaths?: Array<
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
path: string;
|
||||||
|
filename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RunningFeature {
|
interface RunningFeature {
|
||||||
@@ -78,7 +86,7 @@ export class AutoModeService {
|
|||||||
projectPath,
|
projectPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emitAutoModeEvent("auto_mode_complete", {
|
this.emitAutoModeEvent("auto_mode_started", {
|
||||||
message: `Auto mode started with max ${maxConcurrency} concurrent features`,
|
message: `Auto mode started with max ${maxConcurrency} concurrent features`,
|
||||||
projectPath,
|
projectPath,
|
||||||
});
|
});
|
||||||
@@ -111,8 +119,9 @@ export class AutoModeService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (pendingFeatures.length === 0) {
|
if (pendingFeatures.length === 0) {
|
||||||
this.emitAutoModeEvent("auto_mode_complete", {
|
this.emitAutoModeEvent("auto_mode_idle", {
|
||||||
message: "No pending features - auto mode idle",
|
message: "No pending features - auto mode idle",
|
||||||
|
projectPath: this.config!.projectPath,
|
||||||
});
|
});
|
||||||
await this.sleep(10000);
|
await this.sleep(10000);
|
||||||
continue;
|
continue;
|
||||||
@@ -143,21 +152,27 @@ export class AutoModeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.autoLoopRunning = false;
|
this.autoLoopRunning = false;
|
||||||
this.emitAutoModeEvent("auto_mode_complete", {
|
|
||||||
message: "Auto mode stopped",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the auto mode loop
|
* Stop the auto mode loop
|
||||||
*/
|
*/
|
||||||
async stopAutoLoop(): Promise<number> {
|
async stopAutoLoop(): Promise<number> {
|
||||||
|
const wasRunning = this.autoLoopRunning;
|
||||||
this.autoLoopRunning = false;
|
this.autoLoopRunning = false;
|
||||||
if (this.autoLoopAbortController) {
|
if (this.autoLoopAbortController) {
|
||||||
this.autoLoopAbortController.abort();
|
this.autoLoopAbortController.abort();
|
||||||
this.autoLoopAbortController = null;
|
this.autoLoopAbortController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit stop event immediately when user explicitly stops
|
||||||
|
if (wasRunning) {
|
||||||
|
this.emitAutoModeEvent("auto_mode_stopped", {
|
||||||
|
message: "Auto mode stopped",
|
||||||
|
projectPath: this.config?.projectPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this.runningFeatures.size;
|
return this.runningFeatures.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,10 +245,19 @@ export class AutoModeService {
|
|||||||
|
|
||||||
// Get model from feature
|
// Get model from feature
|
||||||
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
|
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
|
||||||
console.log(`[AutoMode] Executing feature ${featureId} with model: ${model}`);
|
console.log(
|
||||||
|
`[AutoMode] Executing feature ${featureId} with model: ${model}`
|
||||||
|
);
|
||||||
|
|
||||||
// Run the agent with the feature's model and images
|
// Run the agent with the feature's model and images
|
||||||
await this.runAgent(workDir, featureId, prompt, abortController, imagePaths, model);
|
await this.runAgent(
|
||||||
|
workDir,
|
||||||
|
featureId,
|
||||||
|
prompt,
|
||||||
|
abortController,
|
||||||
|
imagePaths,
|
||||||
|
model
|
||||||
|
);
|
||||||
|
|
||||||
// Mark as waiting_approval for user review
|
// Mark as waiting_approval for user review
|
||||||
await this.updateFeatureStatus(
|
await this.updateFeatureStatus(
|
||||||
@@ -422,7 +446,9 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
try {
|
try {
|
||||||
// Get model from feature (already loaded above)
|
// Get model from feature (already loaded above)
|
||||||
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
|
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
|
||||||
console.log(`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`);
|
console.log(
|
||||||
|
`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`
|
||||||
|
);
|
||||||
|
|
||||||
// Update feature status to in_progress
|
// Update feature status to in_progress
|
||||||
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
||||||
@@ -458,9 +484,11 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
filename
|
filename
|
||||||
);
|
);
|
||||||
copiedImagePaths.push(relativePath);
|
copiedImagePaths.push(relativePath);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[AutoMode] Failed to copy follow-up image ${imagePath}:`, error);
|
console.error(
|
||||||
|
`[AutoMode] Failed to copy follow-up image ${imagePath}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,7 +534,14 @@ Address the follow-up instructions above. Review the previous work and make the
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use fullPrompt (already built above) with model and all images
|
// Use fullPrompt (already built above) with model and all images
|
||||||
await this.runAgent(workDir, featureId, fullPrompt, abortController, allImagePaths.length > 0 ? allImagePaths : imagePaths, model);
|
await this.runAgent(
|
||||||
|
workDir,
|
||||||
|
featureId,
|
||||||
|
fullPrompt,
|
||||||
|
abortController,
|
||||||
|
allImagePaths.length > 0 ? allImagePaths : imagePaths,
|
||||||
|
model
|
||||||
|
);
|
||||||
|
|
||||||
// Mark as waiting_approval for user review
|
// Mark as waiting_approval for user review
|
||||||
await this.updateFeatureStatus(
|
await this.updateFeatureStatus(
|
||||||
@@ -717,7 +752,10 @@ Format your response as a structured markdown document.`;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use default Claude model for analysis (can be overridden in the future)
|
// Use default Claude model for analysis (can be overridden in the future)
|
||||||
const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude);
|
const analysisModel = resolveModelString(
|
||||||
|
undefined,
|
||||||
|
DEFAULT_MODELS.claude
|
||||||
|
);
|
||||||
const provider = ProviderFactory.getProviderForModel(analysisModel);
|
const provider = ProviderFactory.getProviderForModel(analysisModel);
|
||||||
|
|
||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
@@ -917,7 +955,11 @@ Format your response as a structured markdown document.`;
|
|||||||
try {
|
try {
|
||||||
const data = await fs.readFile(featurePath, "utf-8");
|
const data = await fs.readFile(featurePath, "utf-8");
|
||||||
const feature = JSON.parse(data);
|
const feature = JSON.parse(data);
|
||||||
if (feature.status === "pending" || feature.status === "ready") {
|
if (
|
||||||
|
feature.status === "pending" ||
|
||||||
|
feature.status === "ready" ||
|
||||||
|
feature.status === "backlog"
|
||||||
|
) {
|
||||||
features.push(feature);
|
features.push(feature);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -998,9 +1040,15 @@ ${feature.spec}
|
|||||||
const imagesList = feature.imagePaths
|
const imagesList = feature.imagePaths
|
||||||
.map((img, idx) => {
|
.map((img, idx) => {
|
||||||
const path = typeof img === "string" ? img : img.path;
|
const path = typeof img === "string" ? img : img.path;
|
||||||
const filename = typeof img === "string" ? path.split("/").pop() : img.filename || path.split("/").pop();
|
const filename =
|
||||||
const mimeType = typeof img === "string" ? "image/*" : img.mimeType || "image/*";
|
typeof img === "string"
|
||||||
return ` ${idx + 1}. ${filename} (${mimeType})\n Path: ${path}`;
|
? path.split("/").pop()
|
||||||
|
: img.filename || path.split("/").pop();
|
||||||
|
const mimeType =
|
||||||
|
typeof img === "string" ? "image/*" : img.mimeType || "image/*";
|
||||||
|
return ` ${
|
||||||
|
idx + 1
|
||||||
|
}. ${filename} (${mimeType})\n Path: ${path}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
@@ -1038,7 +1086,9 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
model?: string
|
model?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
|
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
|
||||||
console.log(`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`);
|
console.log(
|
||||||
|
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
|
||||||
|
);
|
||||||
|
|
||||||
// Get provider for this model
|
// Get provider for this model
|
||||||
const provider = ProviderFactory.getProviderForModel(finalModel);
|
const provider = ProviderFactory.getProviderForModel(finalModel);
|
||||||
@@ -1060,14 +1110,7 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
model: finalModel,
|
model: finalModel,
|
||||||
maxTurns: 50,
|
maxTurns: 50,
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
allowedTools: [
|
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
],
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1089,12 +1132,15 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
responseText = block.text || "";
|
responseText = block.text || "";
|
||||||
|
|
||||||
// Check for authentication errors in the response
|
// Check for authentication errors in the response
|
||||||
if (block.text && (block.text.includes("Invalid API key") ||
|
if (
|
||||||
|
block.text &&
|
||||||
|
(block.text.includes("Invalid API key") ||
|
||||||
block.text.includes("authentication_failed") ||
|
block.text.includes("authentication_failed") ||
|
||||||
block.text.includes("Fix external API key"))) {
|
block.text.includes("Fix external API key"))
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Authentication failed: Invalid or expired API key. " +
|
"Authentication failed: Invalid or expired API key. " +
|
||||||
"Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate."
|
"Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,10 @@ describe("auto-mode-service.ts (integration)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Verify feature status was updated to backlog (error status)
|
// Verify feature status was updated to backlog (error status)
|
||||||
const feature = await featureLoader.get(testRepo.path, "test-feature-error");
|
const feature = await featureLoader.get(
|
||||||
|
testRepo.path,
|
||||||
|
"test-feature-error"
|
||||||
|
);
|
||||||
expect(feature?.status).toBe("backlog");
|
expect(feature?.status).toBe("backlog");
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
@@ -154,7 +157,10 @@ describe("auto-mode-service.ts (integration)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Feature should be updated successfully
|
// Feature should be updated successfully
|
||||||
const feature = await featureLoader.get(testRepo.path, "test-no-worktree");
|
const feature = await featureLoader.get(
|
||||||
|
testRepo.path,
|
||||||
|
"test-no-worktree"
|
||||||
|
);
|
||||||
expect(feature?.status).toBe("waiting_approval");
|
expect(feature?.status).toBe("waiting_approval");
|
||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
@@ -313,7 +319,9 @@ describe("auto-mode-service.ts (integration)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Should have used claude-sonnet-4-20250514
|
// Should have used claude-sonnet-4-20250514
|
||||||
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith("claude-sonnet-4-20250514");
|
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith(
|
||||||
|
"claude-sonnet-4-20250514"
|
||||||
|
);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -447,9 +455,11 @@ describe("auto-mode-service.ts (integration)", () => {
|
|||||||
await service.stopAutoLoop();
|
await service.stopAutoLoop();
|
||||||
await startPromise.catch(() => {});
|
await startPromise.catch(() => {});
|
||||||
|
|
||||||
// Check stop event was emitted (auto_mode_complete event)
|
// Check stop event was emitted (emitted immediately by stopAutoLoop)
|
||||||
const stopEvent = mockEvents.emit.mock.calls.find((call) =>
|
const stopEvent = mockEvents.emit.mock.calls.find(
|
||||||
call[1]?.type === "auto_mode_complete" || call[1]?.message?.includes("stopped")
|
(call) =>
|
||||||
|
call[1]?.type === "auto_mode_stopped" ||
|
||||||
|
call[1]?.message?.includes("Auto mode stopped")
|
||||||
);
|
);
|
||||||
expect(stopEvent).toBeTruthy();
|
expect(stopEvent).toBeTruthy();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@@ -476,12 +486,7 @@ describe("auto-mode-service.ts (integration)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Should not throw
|
// Should not throw
|
||||||
await service.executeFeature(
|
await service.executeFeature(testRepo.path, "error-feature", true, false);
|
||||||
testRepo.path,
|
|
||||||
"error-feature",
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// Feature should be marked as backlog (error status)
|
// Feature should be marked as backlog (error status)
|
||||||
const feature = await featureLoader.get(testRepo.path, "error-feature");
|
const feature = await featureLoader.get(testRepo.path, "error-feature");
|
||||||
|
|||||||
117
init.sh
117
init.sh
@@ -33,43 +33,38 @@ fi
|
|||||||
echo -e "${YELLOW}Checking Playwright browsers...${NC}"
|
echo -e "${YELLOW}Checking Playwright browsers...${NC}"
|
||||||
npx playwright install chromium 2>/dev/null || true
|
npx playwright install chromium 2>/dev/null || true
|
||||||
|
|
||||||
|
# Function to kill process on a port and wait for it to be freed
|
||||||
|
kill_port() {
|
||||||
|
local port=$1
|
||||||
|
local pids=$(lsof -ti:$port 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
echo -e "${YELLOW}Killing process(es) on port $port: $pids${NC}"
|
||||||
|
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait for port to be freed (max 5 seconds)
|
||||||
|
local retries=0
|
||||||
|
while [ $retries -lt 10 ]; do
|
||||||
|
if ! lsof -ti:$port >/dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}✓ Port $port is now free${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
retries=$((retries + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${RED}Warning: Port $port may still be in use${NC}"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ Port $port is available${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Kill any existing processes on required ports
|
# Kill any existing processes on required ports
|
||||||
echo -e "${YELLOW}Checking for processes on ports 3007 and 3008...${NC}"
|
echo -e "${YELLOW}Checking for processes on ports 3007 and 3008...${NC}"
|
||||||
lsof -ti:3007 | xargs kill -9 2>/dev/null || true
|
kill_port 3007
|
||||||
lsof -ti:3008 | xargs kill -9 2>/dev/null || true
|
kill_port 3008
|
||||||
|
|
||||||
# Start the backend server
|
|
||||||
echo -e "${BLUE}Starting backend server on port 3008...${NC}"
|
|
||||||
npm run dev:server > logs/server.log 2>&1 &
|
|
||||||
SERVER_PID=$!
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Waiting for server to be ready...${NC}"
|
|
||||||
|
|
||||||
# Wait for server health check
|
|
||||||
MAX_RETRIES=30
|
|
||||||
RETRY_COUNT=0
|
|
||||||
SERVER_READY=false
|
|
||||||
|
|
||||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
||||||
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
|
|
||||||
SERVER_READY=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
||||||
echo -n "."
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$SERVER_READY" = false ]; then
|
|
||||||
echo -e "${RED}Error: Server failed to start${NC}"
|
|
||||||
echo "Check logs/server.log for details"
|
|
||||||
kill $SERVER_PID 2>/dev/null || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}✓ Server is ready!${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Prompt user for application mode
|
# Prompt user for application mode
|
||||||
@@ -81,12 +76,59 @@ echo " 2) Desktop Application (Electron)"
|
|||||||
echo "═══════════════════════════════════════════════════════"
|
echo "═══════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
SERVER_PID=""
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
echo 'Cleaning up...'
|
||||||
|
if [ -n "$SERVER_PID" ]; then
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup INT TERM EXIT
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -p "Enter your choice (1 or 2): " choice
|
read -p "Enter your choice (1 or 2): " choice
|
||||||
case $choice in
|
case $choice in
|
||||||
1)
|
1)
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}Launching Web Application...${NC}"
|
echo -e "${BLUE}Launching Web Application...${NC}"
|
||||||
|
|
||||||
|
# Start the backend server (only needed for Web mode)
|
||||||
|
echo -e "${BLUE}Starting backend server on port 3008...${NC}"
|
||||||
|
mkdir -p logs
|
||||||
|
npm run dev:server > logs/server.log 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Waiting for server to be ready...${NC}"
|
||||||
|
|
||||||
|
# Wait for server health check
|
||||||
|
MAX_RETRIES=30
|
||||||
|
RETRY_COUNT=0
|
||||||
|
SERVER_READY=false
|
||||||
|
|
||||||
|
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||||
|
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
|
||||||
|
SERVER_READY=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||||
|
echo -n "."
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$SERVER_READY" = false ]; then
|
||||||
|
echo -e "${RED}Error: Server failed to start${NC}"
|
||||||
|
echo "Check logs/server.log for details"
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Server is ready!${NC}"
|
||||||
echo "The application will be available at: ${GREEN}http://localhost:3007${NC}"
|
echo "The application will be available at: ${GREEN}http://localhost:3007${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
npm run dev:web
|
npm run dev:web
|
||||||
@@ -95,6 +137,8 @@ while true; do
|
|||||||
2)
|
2)
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}Launching Desktop Application...${NC}"
|
echo -e "${BLUE}Launching Desktop Application...${NC}"
|
||||||
|
echo -e "${YELLOW}(Electron will start its own backend server)${NC}"
|
||||||
|
echo ""
|
||||||
npm run dev:electron
|
npm run dev:electron
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
@@ -103,6 +147,3 @@ while true; do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Cleanup on exit
|
|
||||||
trap "echo 'Cleaning up...'; kill $SERVER_PID 2>/dev/null || true; exit" INT TERM EXIT
|
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "automaker",
|
"name": "automaker",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"libs/*"
|
"libs/*"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"libs/*"
|
"libs/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\"",
|
||||||
"dev": "npm run dev --workspace=apps/app",
|
"dev": "npm run dev --workspace=apps/app",
|
||||||
"dev:web": "npm run dev:web --workspace=apps/app",
|
"dev:web": "npm run dev:web --workspace=apps/app",
|
||||||
"dev:electron": "npm run dev:electron --workspace=apps/app",
|
"dev:electron": "npm run dev:electron --workspace=apps/app",
|
||||||
|
|||||||
Reference in New Issue
Block a user