mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
chore: update dependencies and improve project structure
- Added `morgan` for enhanced request logging in the server. - Updated `package-lock.json` to include new dependencies and their types. - Refactored the `NewProjectModal` component for improved readability and structure. - Enhanced the `FileBrowserDialog` to support initial path selection and improved error handling. - Updated various components to ensure consistent formatting and better user experience. - Introduced XML format specification for app specifications to maintain consistency across the application.
This commit is contained in:
@@ -1,7 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FolderOpen, Folder, ChevronRight, Home, ArrowLeft, HardDrive } from "lucide-react";
|
import {
|
||||||
|
FolderOpen,
|
||||||
|
Folder,
|
||||||
|
ChevronRight,
|
||||||
|
Home,
|
||||||
|
ArrowLeft,
|
||||||
|
HardDrive,
|
||||||
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -24,6 +31,7 @@ interface BrowseResult {
|
|||||||
directories: DirectoryEntry[];
|
directories: DirectoryEntry[];
|
||||||
drives?: string[];
|
drives?: string[];
|
||||||
error?: string;
|
error?: string;
|
||||||
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileBrowserDialogProps {
|
interface FileBrowserDialogProps {
|
||||||
@@ -32,6 +40,7 @@ interface FileBrowserDialogProps {
|
|||||||
onSelect: (path: string) => void;
|
onSelect: (path: string) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
initialPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileBrowserDialog({
|
export function FileBrowserDialog({
|
||||||
@@ -40,6 +49,7 @@ export function FileBrowserDialog({
|
|||||||
onSelect,
|
onSelect,
|
||||||
title = "Select Project Directory",
|
title = "Select Project Directory",
|
||||||
description = "Navigate to your project folder",
|
description = "Navigate to your project folder",
|
||||||
|
initialPath,
|
||||||
}: FileBrowserDialogProps) {
|
}: FileBrowserDialogProps) {
|
||||||
const [currentPath, setCurrentPath] = useState<string>("");
|
const [currentPath, setCurrentPath] = useState<string>("");
|
||||||
const [parentPath, setParentPath] = useState<string | null>(null);
|
const [parentPath, setParentPath] = useState<string | null>(null);
|
||||||
@@ -47,14 +57,17 @@ export function FileBrowserDialog({
|
|||||||
const [drives, setDrives] = useState<string[]>([]);
|
const [drives, setDrives] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [warning, setWarning] = useState("");
|
||||||
|
|
||||||
const browseDirectory = async (dirPath?: string) => {
|
const browseDirectory = async (dirPath?: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
|
setWarning("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get server URL from environment or default
|
// Get server URL from environment or default
|
||||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
|
const serverUrl =
|
||||||
|
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
|
||||||
|
|
||||||
const response = await fetch(`${serverUrl}/api/fs/browse`, {
|
const response = await fetch(`${serverUrl}/api/fs/browse`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -69,23 +82,37 @@ export function FileBrowserDialog({
|
|||||||
setParentPath(result.parentPath);
|
setParentPath(result.parentPath);
|
||||||
setDirectories(result.directories);
|
setDirectories(result.directories);
|
||||||
setDrives(result.drives || []);
|
setDrives(result.drives || []);
|
||||||
|
setWarning(result.warning || "");
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || "Failed to browse directory");
|
setError(result.error || "Failed to browse directory");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to load directories");
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load directories"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load home directory on mount
|
// Reset current path when dialog closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && !currentPath) {
|
if (!open) {
|
||||||
browseDirectory();
|
setCurrentPath("");
|
||||||
|
setParentPath(null);
|
||||||
|
setDirectories([]);
|
||||||
|
setError("");
|
||||||
|
setWarning("");
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// Load initial path or home directory when dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && !currentPath) {
|
||||||
|
browseDirectory(initialPath);
|
||||||
|
}
|
||||||
|
}, [open, initialPath]);
|
||||||
|
|
||||||
const handleSelectDirectory = (dir: DirectoryEntry) => {
|
const handleSelectDirectory = (dir: DirectoryEntry) => {
|
||||||
browseDirectory(dir.path);
|
browseDirectory(dir.path);
|
||||||
};
|
};
|
||||||
@@ -135,7 +162,9 @@ export function FileBrowserDialog({
|
|||||||
{drives.map((drive) => (
|
{drives.map((drive) => (
|
||||||
<Button
|
<Button
|
||||||
key={drive}
|
key={drive}
|
||||||
variant={currentPath.startsWith(drive) ? "default" : "outline"}
|
variant={
|
||||||
|
currentPath.startsWith(drive) ? "default" : "outline"
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleSelectDrive(drive)}
|
onClick={() => handleSelectDrive(drive)}
|
||||||
className="h-7 px-3 text-xs"
|
className="h-7 px-3 text-xs"
|
||||||
@@ -178,7 +207,9 @@ export function FileBrowserDialog({
|
|||||||
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
|
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
<div className="flex items-center justify-center h-full p-8">
|
||||||
<div className="text-sm text-muted-foreground">Loading directories...</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Loading directories...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -188,9 +219,17 @@ export function FileBrowserDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && directories.length === 0 && (
|
{warning && (
|
||||||
|
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg mb-2">
|
||||||
|
<div className="text-sm text-yellow-500">{warning}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && !warning && directories.length === 0 && (
|
||||||
<div className="flex items-center justify-center h-full p-8">
|
<div className="flex items-center justify-center h-full p-8">
|
||||||
<div className="text-sm text-muted-foreground">No subdirectories found</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
No subdirectories found
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -212,7 +251,8 @@ export function FileBrowserDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Click on a folder to navigate. Select the current folder or navigate to a subfolder.
|
Click on a folder to navigate. Select the current folder or navigate
|
||||||
|
to a subfolder.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
Loader2,
|
Loader2,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
Rocket,
|
||||||
|
Zap,
|
||||||
|
CheckCircle2,
|
||||||
|
ArrowRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -78,6 +82,7 @@ import { themeOptions } from "@/config/theme-options";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||||
|
import { NewProjectModal } from "@/components/new-project-modal";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -92,6 +97,8 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||||
|
import type { StarterTemplate } from "@/lib/templates";
|
||||||
|
|
||||||
interface NavSection {
|
interface NavSection {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -205,6 +212,8 @@ export function Sidebar() {
|
|||||||
setPreviewTheme,
|
setPreviewTheme,
|
||||||
theme: globalTheme,
|
theme: globalTheme,
|
||||||
moveProjectToTrash,
|
moveProjectToTrash,
|
||||||
|
specCreatingForProject,
|
||||||
|
setSpecCreatingForProject,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Get customizable keyboard shortcuts
|
// Get customizable keyboard shortcuts
|
||||||
@@ -224,17 +233,26 @@ export function Sidebar() {
|
|||||||
// State for running agents count
|
// State for running agents count
|
||||||
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
|
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
|
||||||
|
|
||||||
|
// State for new project modal
|
||||||
|
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
|
||||||
|
const [isCreatingProject, setIsCreatingProject] = useState(false);
|
||||||
|
|
||||||
|
// State for new project onboarding dialog
|
||||||
|
const [showOnboardingDialog, setShowOnboardingDialog] = useState(false);
|
||||||
|
const [newProjectName, setNewProjectName] = useState("");
|
||||||
|
const [newProjectPath, setNewProjectPath] = useState("");
|
||||||
|
|
||||||
// State for new project setup dialog
|
// State for new project setup dialog
|
||||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||||
const [setupProjectPath, setSetupProjectPath] = useState("");
|
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||||
const [projectOverview, setProjectOverview] = useState("");
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
const [isCreatingSpec, setIsCreatingSpec] = useState(false);
|
|
||||||
const [creatingSpecProjectPath, setCreatingSpecProjectPath] = useState<
|
|
||||||
string | null
|
|
||||||
>(null);
|
|
||||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||||
|
|
||||||
|
// Derive isCreatingSpec from store state
|
||||||
|
const isCreatingSpec = specCreatingForProject !== null;
|
||||||
|
const creatingSpecProjectPath = specCreatingForProject;
|
||||||
|
|
||||||
// Ref for project search input
|
// Ref for project search input
|
||||||
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -324,22 +342,39 @@ export function Sidebar() {
|
|||||||
|
|
||||||
const unsubscribe = api.specRegeneration.onEvent(
|
const unsubscribe = api.specRegeneration.onEvent(
|
||||||
(event: SpecRegenerationEvent) => {
|
(event: SpecRegenerationEvent) => {
|
||||||
console.log("[Sidebar] Spec regeneration event:", event.type);
|
console.log(
|
||||||
|
"[Sidebar] Spec regeneration event:",
|
||||||
|
event.type,
|
||||||
|
"for project:",
|
||||||
|
event.projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only handle events for the project we're currently setting up
|
||||||
|
if (
|
||||||
|
event.projectPath !== creatingSpecProjectPath &&
|
||||||
|
event.projectPath !== setupProjectPath
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"[Sidebar] Ignoring event - not for project being set up"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "spec_regeneration_complete") {
|
if (event.type === "spec_regeneration_complete") {
|
||||||
setIsCreatingSpec(false);
|
setSpecCreatingForProject(null);
|
||||||
setCreatingSpecProjectPath(null);
|
|
||||||
setShowSetupDialog(false);
|
setShowSetupDialog(false);
|
||||||
setProjectOverview("");
|
setProjectOverview("");
|
||||||
setSetupProjectPath("");
|
setSetupProjectPath("");
|
||||||
|
// Clear onboarding state if we came from onboarding
|
||||||
|
setNewProjectName("");
|
||||||
|
setNewProjectPath("");
|
||||||
toast.success("App specification created", {
|
toast.success("App specification created", {
|
||||||
description: "Your project is now set up and ready to go!",
|
description: "Your project is now set up and ready to go!",
|
||||||
});
|
});
|
||||||
// Navigate to spec view to show the new spec
|
// Navigate to spec view to show the new spec
|
||||||
setCurrentView("spec");
|
setCurrentView("spec");
|
||||||
} else if (event.type === "spec_regeneration_error") {
|
} else if (event.type === "spec_regeneration_error") {
|
||||||
setIsCreatingSpec(false);
|
setSpecCreatingForProject(null);
|
||||||
setCreatingSpecProjectPath(null);
|
|
||||||
toast.error("Failed to create specification", {
|
toast.error("Failed to create specification", {
|
||||||
description: event.error,
|
description: event.error,
|
||||||
});
|
});
|
||||||
@@ -350,7 +385,12 @@ export function Sidebar() {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [setCurrentView]);
|
}, [
|
||||||
|
setCurrentView,
|
||||||
|
creatingSpecProjectPath,
|
||||||
|
setupProjectPath,
|
||||||
|
setSpecCreatingForProject,
|
||||||
|
]);
|
||||||
|
|
||||||
// Fetch running agents count function - used for initial load and event-driven updates
|
// Fetch running agents count function - used for initial load and event-driven updates
|
||||||
const fetchRunningAgentsCount = useCallback(async () => {
|
const fetchRunningAgentsCount = useCallback(async () => {
|
||||||
@@ -399,8 +439,8 @@ export function Sidebar() {
|
|||||||
const handleCreateInitialSpec = useCallback(async () => {
|
const handleCreateInitialSpec = useCallback(async () => {
|
||||||
if (!setupProjectPath || !projectOverview.trim()) return;
|
if (!setupProjectPath || !projectOverview.trim()) return;
|
||||||
|
|
||||||
setIsCreatingSpec(true);
|
// Set store state immediately so the loader shows up right away
|
||||||
setCreatingSpecProjectPath(setupProjectPath);
|
setSpecCreatingForProject(setupProjectPath);
|
||||||
setShowSpecIndicator(true);
|
setShowSpecIndicator(true);
|
||||||
setShowSetupDialog(false);
|
setShowSetupDialog(false);
|
||||||
|
|
||||||
@@ -408,8 +448,7 @@ export function Sidebar() {
|
|||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) {
|
if (!api.specRegeneration) {
|
||||||
toast.error("Spec regeneration not available");
|
toast.error("Spec regeneration not available");
|
||||||
setIsCreatingSpec(false);
|
setSpecCreatingForProject(null);
|
||||||
setCreatingSpecProjectPath(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await api.specRegeneration.create(
|
const result = await api.specRegeneration.create(
|
||||||
@@ -420,8 +459,7 @@ export function Sidebar() {
|
|||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error("[Sidebar] Failed to start spec creation:", result.error);
|
console.error("[Sidebar] Failed to start spec creation:", result.error);
|
||||||
setIsCreatingSpec(false);
|
setSpecCreatingForProject(null);
|
||||||
setCreatingSpecProjectPath(null);
|
|
||||||
toast.error("Failed to create specification", {
|
toast.error("Failed to create specification", {
|
||||||
description: result.error,
|
description: result.error,
|
||||||
});
|
});
|
||||||
@@ -429,24 +467,345 @@ export function Sidebar() {
|
|||||||
// If successful, we'll wait for the events to update the state
|
// If successful, we'll wait for the events to update the state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Sidebar] Failed to create spec:", error);
|
console.error("[Sidebar] Failed to create spec:", error);
|
||||||
setIsCreatingSpec(false);
|
setSpecCreatingForProject(null);
|
||||||
setCreatingSpecProjectPath(null);
|
|
||||||
toast.error("Failed to create specification", {
|
toast.error("Failed to create specification", {
|
||||||
description: error instanceof Error ? error.message : "Unknown error",
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [setupProjectPath, projectOverview]);
|
}, [setupProjectPath, projectOverview, setSpecCreatingForProject]);
|
||||||
|
|
||||||
// Handle skipping setup
|
// Handle skipping setup
|
||||||
const handleSkipSetup = useCallback(() => {
|
const handleSkipSetup = useCallback(() => {
|
||||||
setShowSetupDialog(false);
|
setShowSetupDialog(false);
|
||||||
setProjectOverview("");
|
setProjectOverview("");
|
||||||
setSetupProjectPath("");
|
setSetupProjectPath("");
|
||||||
|
// Clear onboarding state if we came from onboarding
|
||||||
|
if (newProjectPath) {
|
||||||
|
setNewProjectName("");
|
||||||
|
setNewProjectPath("");
|
||||||
|
}
|
||||||
toast.info("Setup skipped", {
|
toast.info("Setup skipped", {
|
||||||
description: "You can set up your app_spec.txt later from the Spec view.",
|
description: "You can set up your app_spec.txt later from the Spec view.",
|
||||||
});
|
});
|
||||||
|
}, [newProjectPath]);
|
||||||
|
|
||||||
|
// Handle onboarding dialog - generate spec
|
||||||
|
const handleOnboardingGenerateSpec = useCallback(() => {
|
||||||
|
setShowOnboardingDialog(false);
|
||||||
|
// Navigate to the setup dialog flow
|
||||||
|
setSetupProjectPath(newProjectPath);
|
||||||
|
setProjectOverview("");
|
||||||
|
setShowSetupDialog(true);
|
||||||
|
}, [newProjectPath]);
|
||||||
|
|
||||||
|
// Handle onboarding dialog - skip
|
||||||
|
const handleOnboardingSkip = useCallback(() => {
|
||||||
|
setShowOnboardingDialog(false);
|
||||||
|
setNewProjectName("");
|
||||||
|
setNewProjectPath("");
|
||||||
|
toast.info(
|
||||||
|
"You can generate your app_spec.txt anytime from the Spec view",
|
||||||
|
{
|
||||||
|
description: "Your project is ready to use!",
|
||||||
|
}
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a blank project with just .automaker directory structure
|
||||||
|
*/
|
||||||
|
const handleCreateBlankProject = useCallback(
|
||||||
|
async (projectName: string, parentDir: string) => {
|
||||||
|
setIsCreatingProject(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
const projectPath = `${parentDir}/${projectName}`;
|
||||||
|
|
||||||
|
// Create project directory
|
||||||
|
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
|
||||||
|
const initResult = await initializeProject(projectPath);
|
||||||
|
|
||||||
|
if (!initResult.success) {
|
||||||
|
toast.error("Failed to initialize project", {
|
||||||
|
description: initResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the app_spec.txt with the project name
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
|
await api.writeFile(
|
||||||
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
|
`<project_specification>
|
||||||
|
<project_name>${projectName}</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
Describe your project here. This file will be analyzed by an AI agent
|
||||||
|
to understand your project structure and tech stack.
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<!-- The AI agent will fill this in after analyzing your project -->
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<!-- List core features and capabilities -->
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<implemented_features>
|
||||||
|
<!-- The AI agent will populate this based on code analysis -->
|
||||||
|
</implemented_features>
|
||||||
|
</project_specification>`
|
||||||
|
);
|
||||||
|
|
||||||
|
const trashedProject = trashedProjects.find(
|
||||||
|
(p) => p.path === projectPath
|
||||||
|
);
|
||||||
|
const effectiveTheme =
|
||||||
|
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||||
|
(currentProject?.theme as ThemeMode | undefined) ||
|
||||||
|
globalTheme;
|
||||||
|
const project = upsertAndSetCurrentProject(
|
||||||
|
projectPath,
|
||||||
|
projectName,
|
||||||
|
effectiveTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
setShowNewProjectModal(false);
|
||||||
|
|
||||||
|
// Show onboarding dialog for new project
|
||||||
|
setNewProjectName(projectName);
|
||||||
|
setNewProjectPath(projectPath);
|
||||||
|
setShowOnboardingDialog(true);
|
||||||
|
|
||||||
|
toast.success("Project created", {
|
||||||
|
description: `Created ${projectName} with .automaker directory`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Sidebar] Failed to create project:", error);
|
||||||
|
toast.error("Failed to create project", {
|
||||||
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsCreatingProject(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a project from a GitHub starter template
|
||||||
|
*/
|
||||||
|
const handleCreateFromTemplate = useCallback(
|
||||||
|
async (
|
||||||
|
template: StarterTemplate,
|
||||||
|
projectName: string,
|
||||||
|
parentDir: string
|
||||||
|
) => {
|
||||||
|
setIsCreatingProject(true);
|
||||||
|
try {
|
||||||
|
const httpClient = getHttpApiClient();
|
||||||
|
const api = getElectronAPI();
|
||||||
|
|
||||||
|
// Clone the template repository
|
||||||
|
const cloneResult = await httpClient.templates.clone(
|
||||||
|
template.repoUrl,
|
||||||
|
projectName,
|
||||||
|
parentDir
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cloneResult.success || !cloneResult.projectPath) {
|
||||||
|
toast.error("Failed to clone template", {
|
||||||
|
description: cloneResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectPath = cloneResult.projectPath;
|
||||||
|
|
||||||
|
// Initialize .automaker directory with all necessary files
|
||||||
|
const initResult = await initializeProject(projectPath);
|
||||||
|
|
||||||
|
if (!initResult.success) {
|
||||||
|
toast.error("Failed to initialize project", {
|
||||||
|
description: initResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the app_spec.txt with template-specific info
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
|
await api.writeFile(
|
||||||
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
|
`<project_specification>
|
||||||
|
<project_name>${projectName}</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
This project was created from the "${template.name}" starter template.
|
||||||
|
${template.description}
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
${template.techStack
|
||||||
|
.map((tech) => `<technology>${tech}</technology>`)
|
||||||
|
.join("\n ")}
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
${template.features
|
||||||
|
.map((feature) => `<capability>${feature}</capability>`)
|
||||||
|
.join("\n ")}
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<implemented_features>
|
||||||
|
<!-- The AI agent will populate this based on code analysis -->
|
||||||
|
</implemented_features>
|
||||||
|
</project_specification>`
|
||||||
|
);
|
||||||
|
|
||||||
|
const trashedProject = trashedProjects.find(
|
||||||
|
(p) => p.path === projectPath
|
||||||
|
);
|
||||||
|
const effectiveTheme =
|
||||||
|
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||||
|
(currentProject?.theme as ThemeMode | undefined) ||
|
||||||
|
globalTheme;
|
||||||
|
const project = upsertAndSetCurrentProject(
|
||||||
|
projectPath,
|
||||||
|
projectName,
|
||||||
|
effectiveTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
setShowNewProjectModal(false);
|
||||||
|
|
||||||
|
// Show onboarding dialog for new project
|
||||||
|
setNewProjectName(projectName);
|
||||||
|
setNewProjectPath(projectPath);
|
||||||
|
setShowOnboardingDialog(true);
|
||||||
|
|
||||||
|
toast.success("Project created from template", {
|
||||||
|
description: `Created ${projectName} from ${template.name}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[Sidebar] Failed to create project from template:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
toast.error("Failed to create project", {
|
||||||
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsCreatingProject(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a project from a custom GitHub URL
|
||||||
|
*/
|
||||||
|
const handleCreateFromCustomUrl = useCallback(
|
||||||
|
async (repoUrl: string, projectName: string, parentDir: string) => {
|
||||||
|
setIsCreatingProject(true);
|
||||||
|
try {
|
||||||
|
const httpClient = getHttpApiClient();
|
||||||
|
const api = getElectronAPI();
|
||||||
|
|
||||||
|
// Clone the repository
|
||||||
|
const cloneResult = await httpClient.templates.clone(
|
||||||
|
repoUrl,
|
||||||
|
projectName,
|
||||||
|
parentDir
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cloneResult.success || !cloneResult.projectPath) {
|
||||||
|
toast.error("Failed to clone repository", {
|
||||||
|
description: cloneResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectPath = cloneResult.projectPath;
|
||||||
|
|
||||||
|
// Initialize .automaker directory with all necessary files
|
||||||
|
const initResult = await initializeProject(projectPath);
|
||||||
|
|
||||||
|
if (!initResult.success) {
|
||||||
|
toast.error("Failed to initialize project", {
|
||||||
|
description: initResult.error || "Unknown error occurred",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the app_spec.txt with basic info
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
|
await api.writeFile(
|
||||||
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
|
`<project_specification>
|
||||||
|
<project_name>${projectName}</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
This project was cloned from ${repoUrl}.
|
||||||
|
The AI agent will analyze the project structure.
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<!-- The AI agent will fill this in after analyzing your project -->
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<!-- List core features and capabilities -->
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<implemented_features>
|
||||||
|
<!-- The AI agent will populate this based on code analysis -->
|
||||||
|
</implemented_features>
|
||||||
|
</project_specification>`
|
||||||
|
);
|
||||||
|
|
||||||
|
const trashedProject = trashedProjects.find(
|
||||||
|
(p) => p.path === projectPath
|
||||||
|
);
|
||||||
|
const effectiveTheme =
|
||||||
|
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||||
|
(currentProject?.theme as ThemeMode | undefined) ||
|
||||||
|
globalTheme;
|
||||||
|
const project = upsertAndSetCurrentProject(
|
||||||
|
projectPath,
|
||||||
|
projectName,
|
||||||
|
effectiveTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
setShowNewProjectModal(false);
|
||||||
|
|
||||||
|
// Show onboarding dialog for new project
|
||||||
|
setNewProjectName(projectName);
|
||||||
|
setNewProjectPath(projectPath);
|
||||||
|
setShowOnboardingDialog(true);
|
||||||
|
|
||||||
|
toast.success("Project created from repository", {
|
||||||
|
description: `Created ${projectName} from ${repoUrl}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Sidebar] Failed to create project from URL:", error);
|
||||||
|
toast.error("Failed to create project", {
|
||||||
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsCreatingProject(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the system folder selection dialog and initializes the selected project.
|
* Opens the system folder selection dialog and initializes the selected project.
|
||||||
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
||||||
@@ -840,7 +1199,7 @@ export function Sidebar() {
|
|||||||
<img
|
<img
|
||||||
src="/logo.png"
|
src="/logo.png"
|
||||||
alt="A"
|
alt="A"
|
||||||
className="h-[1.3em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform"
|
className="h-[1.8em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform"
|
||||||
/>
|
/>
|
||||||
<span className="-ml-0.5">
|
<span className="-ml-0.5">
|
||||||
uto<span className="text-brand-500">maker</span>
|
uto<span className="text-brand-500">maker</span>
|
||||||
@@ -868,7 +1227,7 @@ export function Sidebar() {
|
|||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div className="flex items-center gap-2 titlebar-no-drag px-2 mt-3">
|
<div className="flex items-center gap-2 titlebar-no-drag px-2 mt-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentView("welcome")}
|
onClick={() => setShowNewProjectModal(true)}
|
||||||
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
|
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
|
||||||
title="New Project"
|
title="New Project"
|
||||||
data-testid="new-project-button"
|
data-testid="new-project-button"
|
||||||
@@ -1586,27 +1945,112 @@ export function Sidebar() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Spec Creation Indicator - Bottom Right Toast */}
|
{/* New Project Onboarding Dialog */}
|
||||||
{isCreatingSpec &&
|
<Dialog
|
||||||
showSpecIndicator &&
|
open={showOnboardingDialog}
|
||||||
currentProject?.path === creatingSpecProjectPath && (
|
onOpenChange={(open) => {
|
||||||
<div className="fixed bottom-4 right-4 z-50 flex items-center gap-3 bg-card border border-border rounded-lg shadow-lg p-4 max-w-sm">
|
if (!open) {
|
||||||
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
|
handleOnboardingSkip();
|
||||||
<div className="flex-1 min-w-0">
|
}
|
||||||
<p className="text-sm font-medium">Creating App Specification</p>
|
}}
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
>
|
||||||
Working on your project...
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-brand-500/10 border border-brand-500/20">
|
||||||
|
<Rocket className="w-6 h-6 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DialogTitle className="text-2xl">
|
||||||
|
Welcome to {newProjectName}!
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground mt-1">
|
||||||
|
Your new project is ready. Let's get you started.
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-6">
|
||||||
|
{/* Main explanation */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-foreground leading-relaxed">
|
||||||
|
Would you like to auto-generate your{" "}
|
||||||
|
<strong>app_spec.txt</strong>? This file helps describe your
|
||||||
|
project and is used to pre-populate your backlog with features
|
||||||
|
to work on.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Benefits list */}
|
||||||
|
<div className="space-y-3 rounded-lg bg-muted/50 border border-border p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
Pre-populate your backlog
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Automatically generate features based on your project
|
||||||
|
specification
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Zap className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
Better AI assistance
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Help AI agents understand your project structure and tech
|
||||||
|
stack
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<FileText className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
Project documentation
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Keep a clear record of your project's capabilities and
|
||||||
|
features
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info box */}
|
||||||
|
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-3">
|
||||||
|
<p className="text-xs text-blue-400 leading-relaxed">
|
||||||
|
<strong className="text-blue-300">Tip:</strong> You can always
|
||||||
|
generate or edit your app_spec.txt later from the Spec Editor in
|
||||||
|
the sidebar.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => setShowSpecIndicator(false)}
|
|
||||||
className="p-1 hover:bg-muted rounded-md transition-colors flex-shrink-0"
|
|
||||||
aria-label="Dismiss notification"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleOnboardingSkip}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleOnboardingGenerateSpec}
|
||||||
|
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Generate App Spec
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Project Confirmation Dialog */}
|
{/* Delete Project Confirmation Dialog */}
|
||||||
<DeleteProjectDialog
|
<DeleteProjectDialog
|
||||||
@@ -1615,6 +2059,16 @@ export function Sidebar() {
|
|||||||
project={currentProject}
|
project={currentProject}
|
||||||
onConfirm={moveProjectToTrash}
|
onConfirm={moveProjectToTrash}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* New Project Modal */}
|
||||||
|
<NewProjectModal
|
||||||
|
open={showNewProjectModal}
|
||||||
|
onOpenChange={setShowNewProjectModal}
|
||||||
|
onCreateBlankProject={handleCreateBlankProject}
|
||||||
|
onCreateFromTemplate={handleCreateFromTemplate}
|
||||||
|
onCreateFromCustomUrl={handleCreateFromCustomUrl}
|
||||||
|
isCreating={isCreatingProject}
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ interface ValidationErrors {
|
|||||||
interface NewProjectModalProps {
|
interface NewProjectModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onCreateBlankProject: (projectName: string, parentDir: string) => Promise<void>;
|
onCreateBlankProject: (
|
||||||
|
projectName: string,
|
||||||
|
parentDir: string
|
||||||
|
) => Promise<void>;
|
||||||
onCreateFromTemplate: (
|
onCreateFromTemplate: (
|
||||||
template: StarterTemplate,
|
template: StarterTemplate,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
@@ -67,7 +70,8 @@ export function NewProjectModal({
|
|||||||
const [projectName, setProjectName] = useState("");
|
const [projectName, setProjectName] = useState("");
|
||||||
const [workspaceDir, setWorkspaceDir] = useState<string>("");
|
const [workspaceDir, setWorkspaceDir] = useState<string>("");
|
||||||
const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false);
|
const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<StarterTemplate | null>(null);
|
const [selectedTemplate, setSelectedTemplate] =
|
||||||
|
useState<StarterTemplate | null>(null);
|
||||||
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
||||||
const [customUrl, setCustomUrl] = useState("");
|
const [customUrl, setCustomUrl] = useState("");
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||||
@@ -78,7 +82,8 @@ export function NewProjectModal({
|
|||||||
if (open) {
|
if (open) {
|
||||||
setIsLoadingWorkspace(true);
|
setIsLoadingWorkspace(true);
|
||||||
const httpClient = getHttpApiClient();
|
const httpClient = getHttpApiClient();
|
||||||
httpClient.workspace.getConfig()
|
httpClient.workspace
|
||||||
|
.getConfig()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.success && result.workspaceDir) {
|
if (result.success && result.workspaceDir) {
|
||||||
setWorkspaceDir(result.workspaceDir);
|
setWorkspaceDir(result.workspaceDir);
|
||||||
@@ -113,7 +118,10 @@ export function NewProjectModal({
|
|||||||
}, [projectName, errors.projectName]);
|
}, [projectName, errors.projectName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((selectedTemplate || (useCustomUrl && customUrl)) && errors.templateSelection) {
|
if (
|
||||||
|
(selectedTemplate || (useCustomUrl && customUrl)) &&
|
||||||
|
errors.templateSelection
|
||||||
|
) {
|
||||||
setErrors((prev) => ({ ...prev, templateSelection: false }));
|
setErrors((prev) => ({ ...prev, templateSelection: false }));
|
||||||
}
|
}
|
||||||
}, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]);
|
}, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]);
|
||||||
@@ -187,7 +195,9 @@ export function NewProjectModal({
|
|||||||
const handleBrowseDirectory = async () => {
|
const handleBrowseDirectory = async () => {
|
||||||
const selectedPath = await openFileBrowser({
|
const selectedPath = await openFileBrowser({
|
||||||
title: "Select Base Project Directory",
|
title: "Select Base Project Directory",
|
||||||
description: "Choose the parent directory where your project will be created",
|
description:
|
||||||
|
"Choose the parent directory where your project will be created",
|
||||||
|
initialPath: workspaceDir || undefined,
|
||||||
});
|
});
|
||||||
if (selectedPath) {
|
if (selectedPath) {
|
||||||
setWorkspaceDir(selectedPath);
|
setWorkspaceDir(selectedPath);
|
||||||
@@ -199,9 +209,16 @@ export function NewProjectModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : "";
|
? navigator.platform.indexOf("Win") !== -1
|
||||||
|
? "\\"
|
||||||
|
: "/"
|
||||||
|
: "/";
|
||||||
|
const projectPath =
|
||||||
|
workspaceDir && projectName
|
||||||
|
? `${workspaceDir}${pathSep}${projectName}`
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -210,7 +227,9 @@ export function NewProjectModal({
|
|||||||
data-testid="new-project-modal"
|
data-testid="new-project-modal"
|
||||||
>
|
>
|
||||||
<DialogHeader className="pb-2">
|
<DialogHeader className="pb-2">
|
||||||
<DialogTitle className="text-foreground">Create New Project</DialogTitle>
|
<DialogTitle className="text-foreground">
|
||||||
|
Create New Project
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Start with a blank project or choose from a starter template.
|
Start with a blank project or choose from a starter template.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@@ -219,8 +238,15 @@ export function NewProjectModal({
|
|||||||
{/* Project Name Input - Always visible at top */}
|
{/* Project Name Input - Always visible at top */}
|
||||||
<div className="space-y-3 pb-4 border-b border-border">
|
<div className="space-y-3 pb-4 border-b border-border">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="project-name" className={cn("text-foreground", errors.projectName && "text-red-500")}>
|
<Label
|
||||||
Project Name {errors.projectName && <span className="text-red-500">*</span>}
|
htmlFor="project-name"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground",
|
||||||
|
errors.projectName && "text-red-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Project Name{" "}
|
||||||
|
{errors.projectName && <span className="text-red-500">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="project-name"
|
id="project-name"
|
||||||
@@ -242,16 +268,23 @@ export function NewProjectModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workspace Directory Display */}
|
{/* Workspace Directory Display */}
|
||||||
<div className={cn(
|
<div
|
||||||
"flex items-center gap-2 text-sm",
|
className={cn(
|
||||||
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
"flex items-center gap-2 text-sm",
|
||||||
)}>
|
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Folder className="w-4 h-4 shrink-0" />
|
<Folder className="w-4 h-4 shrink-0" />
|
||||||
<span className="flex-1 min-w-0">
|
<span className="flex-1 min-w-0">
|
||||||
{isLoadingWorkspace ? (
|
{isLoadingWorkspace ? (
|
||||||
"Loading workspace..."
|
"Loading workspace..."
|
||||||
) : workspaceDir ? (
|
) : workspaceDir ? (
|
||||||
<>Will be created at: <code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">{projectPath || "..."}</code></>
|
<>
|
||||||
|
Will be created at:{" "}
|
||||||
|
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
|
||||||
|
{projectPath || workspaceDir}
|
||||||
|
</code>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-red-500">No workspace configured</span>
|
<span className="text-red-500">No workspace configured</span>
|
||||||
)}
|
)}
|
||||||
@@ -302,14 +335,18 @@ export function NewProjectModal({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Error message for template selection */}
|
{/* Error message for template selection */}
|
||||||
{errors.templateSelection && (
|
{errors.templateSelection && (
|
||||||
<p className="text-sm text-red-500">Please select a template or enter a custom GitHub URL</p>
|
<p className="text-sm text-red-500">
|
||||||
|
Please select a template or enter a custom GitHub URL
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Preset Templates */}
|
{/* Preset Templates */}
|
||||||
<div className={cn(
|
<div
|
||||||
"space-y-3 rounded-lg p-1 -m-1",
|
className={cn(
|
||||||
errors.templateSelection && "ring-2 ring-red-500/50"
|
"space-y-3 rounded-lg p-1 -m-1",
|
||||||
)}>
|
errors.templateSelection && "ring-2 ring-red-500/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{starterTemplates.map((template) => (
|
{starterTemplates.map((template) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
@@ -328,9 +365,10 @@ export function NewProjectModal({
|
|||||||
<h4 className="font-medium text-foreground">
|
<h4 className="font-medium text-foreground">
|
||||||
{template.name}
|
{template.name}
|
||||||
</h4>
|
</h4>
|
||||||
{selectedTemplate?.id === template.id && !useCustomUrl && (
|
{selectedTemplate?.id === template.id &&
|
||||||
<Check className="w-4 h-4 text-brand-500" />
|
!useCustomUrl && (
|
||||||
)}
|
<Check className="w-4 h-4 text-brand-500" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
{template.description}
|
{template.description}
|
||||||
@@ -391,15 +429,22 @@ export function NewProjectModal({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Link className="w-4 h-4 text-muted-foreground" />
|
<Link className="w-4 h-4 text-muted-foreground" />
|
||||||
<h4 className="font-medium text-foreground">Custom GitHub URL</h4>
|
<h4 className="font-medium text-foreground">
|
||||||
{useCustomUrl && <Check className="w-4 h-4 text-brand-500" />}
|
Custom GitHub URL
|
||||||
|
</h4>
|
||||||
|
{useCustomUrl && (
|
||||||
|
<Check className="w-4 h-4 text-brand-500" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
Clone any public GitHub repository as a starting point.
|
Clone any public GitHub repository as a starting point.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{useCustomUrl && (
|
{useCustomUrl && (
|
||||||
<div onClick={(e) => e.stopPropagation()} className="space-y-1">
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="space-y-1"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://github.com/username/repository"
|
placeholder="https://github.com/username/repository"
|
||||||
value={customUrl}
|
value={customUrl}
|
||||||
@@ -413,7 +458,9 @@ export function NewProjectModal({
|
|||||||
data-testid="custom-url-input"
|
data-testid="custom-url-input"
|
||||||
/>
|
/>
|
||||||
{errors.customUrl && (
|
{errors.customUrl && (
|
||||||
<p className="text-xs text-red-500">GitHub URL is required</p>
|
<p className="text-xs text-red-500">
|
||||||
|
GitHub URL is required
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ export function AnalysisView() {
|
|||||||
const techStack = detectTechStack();
|
const techStack = detectTechStack();
|
||||||
|
|
||||||
// Generate the spec content
|
// Generate the spec content
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
const specContent = `<project_specification>
|
const specContent = `<project_specification>
|
||||||
<project_name>${projectName}</project_name>
|
<project_name>${projectName}</project_name>
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from "@/store/app-store";
|
} from "@/store/app-store";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
import { cn, modelSupportsThinking } from "@/lib/utils";
|
||||||
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
@@ -179,6 +180,8 @@ export function BoardView() {
|
|||||||
kanbanCardDetailLevel,
|
kanbanCardDetailLevel,
|
||||||
setKanbanCardDetailLevel,
|
setKanbanCardDetailLevel,
|
||||||
boardBackgroundByProject,
|
boardBackgroundByProject,
|
||||||
|
specCreatingForProject,
|
||||||
|
setSpecCreatingForProject,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||||
@@ -233,6 +236,9 @@ export function BoardView() {
|
|||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
// Validation state for add feature form
|
// Validation state for add feature form
|
||||||
const [descriptionError, setDescriptionError] = useState(false);
|
const [descriptionError, setDescriptionError] = useState(false);
|
||||||
|
// Derive spec creation state from store - check if current project is the one being created
|
||||||
|
const isCreatingSpec = specCreatingForProject === currentProject?.path;
|
||||||
|
const creatingSpecProjectPath = specCreatingForProject;
|
||||||
|
|
||||||
// Make current project available globally for modal
|
// Make current project available globally for modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -264,6 +270,37 @@ export function BoardView() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Subscribe to spec regeneration events to clear state on completion
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
||||||
|
console.log(
|
||||||
|
"[BoardView] Spec regeneration event:",
|
||||||
|
event.type,
|
||||||
|
"for project:",
|
||||||
|
event.projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only handle completion/error events for the project being created
|
||||||
|
// The creating state is set by sidebar when user initiates the action
|
||||||
|
if (event.projectPath !== specCreatingForProject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "spec_regeneration_complete") {
|
||||||
|
setSpecCreatingForProject(null);
|
||||||
|
} else if (event.type === "spec_regeneration_error") {
|
||||||
|
setSpecCreatingForProject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [specCreatingForProject, setSpecCreatingForProject]);
|
||||||
|
|
||||||
// Track previous project to detect switches
|
// Track previous project to detect switches
|
||||||
const prevProjectPathRef = useRef<string | null>(null);
|
const prevProjectPathRef = useRef<string | null>(null);
|
||||||
const isSwitchingProjectRef = useRef<boolean>(false);
|
const isSwitchingProjectRef = useRef<boolean>(false);
|
||||||
@@ -1791,34 +1828,50 @@ export function BoardView() {
|
|||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Search Bar Row */}
|
{/* Search Bar Row */}
|
||||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
||||||
<div className="relative max-w-md flex-1">
|
<div className="relative max-w-md flex-1 flex items-center gap-2">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
<div className="relative flex-1">
|
||||||
<Input
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||||
ref={searchInputRef}
|
<Input
|
||||||
type="text"
|
ref={searchInputRef}
|
||||||
placeholder="Search features by keyword..."
|
type="text"
|
||||||
value={searchQuery}
|
placeholder="Search features by keyword..."
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
value={searchQuery}
|
||||||
className="pl-9 pr-12 border-border"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
data-testid="kanban-search-input"
|
className="pl-9 pr-12 border-border"
|
||||||
/>
|
data-testid="kanban-search-input"
|
||||||
{searchQuery ? (
|
/>
|
||||||
<button
|
{searchQuery ? (
|
||||||
onClick={() => setSearchQuery("")}
|
<button
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
onClick={() => setSearchQuery("")}
|
||||||
data-testid="kanban-search-clear"
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
||||||
aria-label="Clear search"
|
data-testid="kanban-search-clear"
|
||||||
>
|
aria-label="Clear search"
|
||||||
<X className="w-4 h-4" />
|
>
|
||||||
</button>
|
<X className="w-4 h-4" />
|
||||||
) : (
|
</button>
|
||||||
<span
|
) : (
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
|
<span
|
||||||
data-testid="kanban-search-hotkey"
|
className="absolute right-2 top-1/2 -translate-y-1/2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
|
||||||
>
|
data-testid="kanban-search-hotkey"
|
||||||
/
|
>
|
||||||
</span>
|
/
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Spec Creation Loading Badge */}
|
||||||
|
{isCreatingSpec &&
|
||||||
|
currentProject?.path === creatingSpecProjectPath && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-brand-500/10 border border-brand-500/20 shrink-0"
|
||||||
|
title="Creating App Specification"
|
||||||
|
data-testid="spec-creation-badge"
|
||||||
|
>
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin text-brand-500 shrink-0" />
|
||||||
|
<span className="text-xs font-medium text-brand-500 whitespace-nowrap">
|
||||||
|
Creating spec
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Board Background & Detail Level Controls */}
|
{/* Board Background & Detail Level Controls */}
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export function InterviewView() {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9-]/g, "");
|
.replace(/[^a-z0-9-]/g, "");
|
||||||
|
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
return `<project_specification>
|
return `<project_specification>
|
||||||
<project_name>${projectName || "my-project"}</project_name>
|
<project_name>${projectName || "my-project"}</project_name>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus, CheckCircle2 } from "lucide-react";
|
import {
|
||||||
|
Save,
|
||||||
|
RefreshCw,
|
||||||
|
FileText,
|
||||||
|
Sparkles,
|
||||||
|
Loader2,
|
||||||
|
FilePlus2,
|
||||||
|
AlertCircle,
|
||||||
|
ListPlus,
|
||||||
|
CheckCircle2,
|
||||||
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
|
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
|
||||||
@@ -43,14 +53,14 @@ export function SpecView() {
|
|||||||
const [projectOverview, setProjectOverview] = useState("");
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
|
|
||||||
// Generate features only state
|
// Generate features only state
|
||||||
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
||||||
|
|
||||||
// Logs state (kept for internal tracking, but UI removed)
|
// Logs state (kept for internal tracking, but UI removed)
|
||||||
const [logs, setLogs] = useState<string>("");
|
const [logs, setLogs] = useState<string>("");
|
||||||
const logsRef = useRef<string>("");
|
const logsRef = useRef<string>("");
|
||||||
|
|
||||||
// Phase tracking and status
|
// Phase tracking and status
|
||||||
const [currentPhase, setCurrentPhase] = useState<string>("");
|
const [currentPhase, setCurrentPhase] = useState<string>("");
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||||
@@ -107,28 +117,33 @@ export function SpecView() {
|
|||||||
|
|
||||||
if (status.success && status.isRunning) {
|
if (status.success && status.isRunning) {
|
||||||
// Something is running - restore state using backend's authoritative phase
|
// Something is running - restore state using backend's authoritative phase
|
||||||
console.log("[SpecView] Spec generation is running - restoring state", { phase: status.currentPhase });
|
console.log(
|
||||||
|
"[SpecView] Spec generation is running - restoring state",
|
||||||
|
{ phase: status.currentPhase }
|
||||||
|
);
|
||||||
|
|
||||||
if (!stateRestoredRef.current) {
|
if (!stateRestoredRef.current) {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setIsRegenerating(true);
|
setIsRegenerating(true);
|
||||||
stateRestoredRef.current = true;
|
stateRestoredRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the backend's currentPhase directly - single source of truth
|
// Use the backend's currentPhase directly - single source of truth
|
||||||
if (status.currentPhase) {
|
if (status.currentPhase) {
|
||||||
setCurrentPhase(status.currentPhase);
|
setCurrentPhase(status.currentPhase);
|
||||||
} else {
|
} else {
|
||||||
setCurrentPhase("in progress");
|
setCurrentPhase("in progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add resume message to logs if needed
|
// Add resume message to logs if needed
|
||||||
if (!logsRef.current) {
|
if (!logsRef.current) {
|
||||||
const resumeMessage = "[Status] Resumed monitoring existing spec generation process...\n";
|
const resumeMessage =
|
||||||
|
"[Status] Resumed monitoring existing spec generation process...\n";
|
||||||
logsRef.current = resumeMessage;
|
logsRef.current = resumeMessage;
|
||||||
setLogs(resumeMessage);
|
setLogs(resumeMessage);
|
||||||
} else if (!logsRef.current.includes("Resumed monitoring")) {
|
} else if (!logsRef.current.includes("Resumed monitoring")) {
|
||||||
const resumeMessage = "\n[Status] Resumed monitoring existing spec generation process...\n";
|
const resumeMessage =
|
||||||
|
"\n[Status] Resumed monitoring existing spec generation process...\n";
|
||||||
logsRef.current = logsRef.current + resumeMessage;
|
logsRef.current = logsRef.current + resumeMessage;
|
||||||
setLogs(logsRef.current);
|
setLogs(logsRef.current);
|
||||||
}
|
}
|
||||||
@@ -154,7 +169,11 @@ export function SpecView() {
|
|||||||
// Sync state when tab becomes visible (user returns to spec editor)
|
// Sync state when tab becomes visible (user returns to spec editor)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = async () => {
|
const handleVisibilityChange = async () => {
|
||||||
if (!document.hidden && currentProject && (isCreating || isRegenerating || isGeneratingFeatures)) {
|
if (
|
||||||
|
!document.hidden &&
|
||||||
|
currentProject &&
|
||||||
|
(isCreating || isRegenerating || isGeneratingFeatures)
|
||||||
|
) {
|
||||||
// Tab became visible and we think we're still generating - verify status from backend
|
// Tab became visible and we think we're still generating - verify status from backend
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -162,10 +181,12 @@ export function SpecView() {
|
|||||||
|
|
||||||
const status = await api.specRegeneration.status();
|
const status = await api.specRegeneration.status();
|
||||||
console.log("[SpecView] Visibility change - status check:", status);
|
console.log("[SpecView] Visibility change - status check:", status);
|
||||||
|
|
||||||
if (!status.isRunning) {
|
if (!status.isRunning) {
|
||||||
// Backend says not running - clear state
|
// Backend says not running - clear state
|
||||||
console.log("[SpecView] Visibility change: Backend indicates generation complete - clearing state");
|
console.log(
|
||||||
|
"[SpecView] Visibility change: Backend indicates generation complete - clearing state"
|
||||||
|
);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
@@ -177,7 +198,10 @@ export function SpecView() {
|
|||||||
setCurrentPhase(status.currentPhase);
|
setCurrentPhase(status.currentPhase);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecView] Failed to check status on visibility change:", error);
|
console.error(
|
||||||
|
"[SpecView] Failed to check status on visibility change:",
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -186,11 +210,21 @@ export function SpecView() {
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
};
|
};
|
||||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]);
|
}, [
|
||||||
|
currentProject,
|
||||||
|
isCreating,
|
||||||
|
isRegenerating,
|
||||||
|
isGeneratingFeatures,
|
||||||
|
loadSpec,
|
||||||
|
]);
|
||||||
|
|
||||||
// Periodic status check to ensure state stays in sync (only when we think we're running)
|
// Periodic status check to ensure state stays in sync (only when we think we're running)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures)) return;
|
if (
|
||||||
|
!currentProject ||
|
||||||
|
(!isCreating && !isRegenerating && !isGeneratingFeatures)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const intervalId = setInterval(async () => {
|
const intervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -198,21 +232,26 @@ export function SpecView() {
|
|||||||
if (!api.specRegeneration) return;
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
const status = await api.specRegeneration.status();
|
const status = await api.specRegeneration.status();
|
||||||
|
|
||||||
if (!status.isRunning) {
|
if (!status.isRunning) {
|
||||||
// Backend says not running - clear state
|
// Backend says not running - clear state
|
||||||
console.log("[SpecView] Periodic check: Backend indicates generation complete - clearing state");
|
console.log(
|
||||||
|
"[SpecView] Periodic check: Backend indicates generation complete - clearing state"
|
||||||
|
);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase("");
|
setCurrentPhase("");
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
loadSpec();
|
loadSpec();
|
||||||
} else if (status.currentPhase && status.currentPhase !== currentPhase) {
|
} else if (
|
||||||
|
status.currentPhase &&
|
||||||
|
status.currentPhase !== currentPhase
|
||||||
|
) {
|
||||||
// Still running but phase changed - update from backend
|
// Still running but phase changed - update from backend
|
||||||
console.log("[SpecView] Periodic check: Phase updated from backend", {
|
console.log("[SpecView] Periodic check: Phase updated from backend", {
|
||||||
old: currentPhase,
|
old: currentPhase,
|
||||||
new: status.currentPhase
|
new: status.currentPhase,
|
||||||
});
|
});
|
||||||
setCurrentPhase(status.currentPhase);
|
setCurrentPhase(status.currentPhase);
|
||||||
}
|
}
|
||||||
@@ -224,173 +263,214 @@ export function SpecView() {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
|
}, [
|
||||||
|
currentProject,
|
||||||
|
isCreating,
|
||||||
|
isRegenerating,
|
||||||
|
isGeneratingFeatures,
|
||||||
|
currentPhase,
|
||||||
|
loadSpec,
|
||||||
|
]);
|
||||||
|
|
||||||
// Subscribe to spec regeneration events
|
// Subscribe to spec regeneration events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) return;
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
|
const unsubscribe = api.specRegeneration.onEvent(
|
||||||
console.log("[SpecView] Regeneration event:", event.type);
|
(event: SpecRegenerationEvent) => {
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Regeneration event:",
|
||||||
|
event.type,
|
||||||
|
"for project:",
|
||||||
|
event.projectPath
|
||||||
|
);
|
||||||
|
|
||||||
if (event.type === "spec_regeneration_progress") {
|
// Only handle events for the current project
|
||||||
// Extract phase from content if present
|
if (event.projectPath !== currentProject?.path) {
|
||||||
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
|
console.log("[SpecView] Ignoring event - not for current project");
|
||||||
if (phaseMatch) {
|
return;
|
||||||
const phase = phaseMatch[1];
|
}
|
||||||
setCurrentPhase(phase);
|
|
||||||
console.log(`[SpecView] Phase updated: ${phase}`);
|
if (event.type === "spec_regeneration_progress") {
|
||||||
|
// Extract phase from content if present
|
||||||
// If phase is "complete", clear running state immediately
|
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
|
||||||
if (phase === "complete") {
|
if (phaseMatch) {
|
||||||
console.log("[SpecView] Phase is complete - clearing state");
|
const phase = phaseMatch[1];
|
||||||
|
setCurrentPhase(phase);
|
||||||
|
console.log(`[SpecView] Phase updated: ${phase}`);
|
||||||
|
|
||||||
|
// If phase is "complete", clear running state immediately
|
||||||
|
if (phase === "complete") {
|
||||||
|
console.log("[SpecView] Phase is complete - clearing state");
|
||||||
|
setIsCreating(false);
|
||||||
|
setIsRegenerating(false);
|
||||||
|
stateRestoredRef.current = false;
|
||||||
|
// Small delay to ensure spec file is written
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSpec();
|
||||||
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completion indicators in content
|
||||||
|
if (
|
||||||
|
event.content.includes("All tasks completed") ||
|
||||||
|
event.content.includes("✓ All tasks completed")
|
||||||
|
) {
|
||||||
|
// This indicates everything is done - clear state immediately
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Detected completion in progress message - clearing state"
|
||||||
|
);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
|
setCurrentPhase("");
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
// Small delay to ensure spec file is written
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadSpec();
|
loadSpec();
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for completion indicators in content
|
// Append progress to logs
|
||||||
if (event.content.includes("All tasks completed") ||
|
const newLog = logsRef.current + event.content;
|
||||||
event.content.includes("✓ All tasks completed")) {
|
logsRef.current = newLog;
|
||||||
// This indicates everything is done - clear state immediately
|
setLogs(newLog);
|
||||||
console.log("[SpecView] Detected completion in progress message - clearing state");
|
console.log("[SpecView] Progress:", event.content.substring(0, 100));
|
||||||
setIsCreating(false);
|
|
||||||
setIsRegenerating(false);
|
|
||||||
setCurrentPhase("");
|
|
||||||
stateRestoredRef.current = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
loadSpec();
|
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append progress to logs
|
// Clear error message when we get new progress
|
||||||
const newLog = logsRef.current + event.content;
|
if (errorMessage) {
|
||||||
logsRef.current = newLog;
|
setErrorMessage("");
|
||||||
setLogs(newLog);
|
}
|
||||||
console.log("[SpecView] Progress:", event.content.substring(0, 100));
|
} else if (event.type === "spec_regeneration_tool") {
|
||||||
|
// Check if this is a feature creation tool
|
||||||
// Clear error message when we get new progress
|
const isFeatureTool =
|
||||||
if (errorMessage) {
|
event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
|
||||||
setErrorMessage("");
|
event.tool === "UpdateFeatureStatus" ||
|
||||||
}
|
event.tool?.includes("Feature");
|
||||||
} else if (event.type === "spec_regeneration_tool") {
|
|
||||||
// Check if this is a feature creation tool
|
if (isFeatureTool) {
|
||||||
const isFeatureTool = event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
|
// Ensure we're in feature generation phase
|
||||||
event.tool === "UpdateFeatureStatus" ||
|
if (currentPhase !== "feature_generation") {
|
||||||
event.tool?.includes("Feature");
|
setCurrentPhase("feature_generation");
|
||||||
|
setIsCreating(true);
|
||||||
if (isFeatureTool) {
|
setIsRegenerating(true);
|
||||||
// Ensure we're in feature generation phase
|
console.log(
|
||||||
if (currentPhase !== "feature_generation") {
|
"[SpecView] Detected feature creation tool - setting phase to feature_generation"
|
||||||
setCurrentPhase("feature_generation");
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log tool usage with details
|
||||||
|
const toolInput = event.input
|
||||||
|
? ` (${JSON.stringify(event.input).substring(0, 100)}...)`
|
||||||
|
: "";
|
||||||
|
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
|
||||||
|
const newLog = logsRef.current + toolLog;
|
||||||
|
logsRef.current = newLog;
|
||||||
|
setLogs(newLog);
|
||||||
|
console.log("[SpecView] Tool:", event.tool, event.input);
|
||||||
|
} else if (event.type === "spec_regeneration_complete") {
|
||||||
|
// Add completion message to logs first
|
||||||
|
const completionLog =
|
||||||
|
logsRef.current + `\n[Complete] ${event.message}\n`;
|
||||||
|
logsRef.current = completionLog;
|
||||||
|
setLogs(completionLog);
|
||||||
|
|
||||||
|
// --- Completion Detection Logic ---
|
||||||
|
// The backend sends explicit signals for completion:
|
||||||
|
// 1. "All tasks completed" in the message
|
||||||
|
// 2. [Phase: complete] marker in logs
|
||||||
|
// 3. "Spec regeneration complete!" for regeneration
|
||||||
|
// 4. "Initial spec creation complete!" for creation without features
|
||||||
|
const isFinalCompletionMessage =
|
||||||
|
event.message?.includes("All tasks completed") ||
|
||||||
|
event.message === "All tasks completed!" ||
|
||||||
|
event.message === "All tasks completed" ||
|
||||||
|
event.message === "Spec regeneration complete!" ||
|
||||||
|
event.message === "Initial spec creation complete!";
|
||||||
|
|
||||||
|
const hasCompletePhase =
|
||||||
|
logsRef.current.includes("[Phase: complete]");
|
||||||
|
|
||||||
|
// Intermediate completion means features are being generated after spec creation
|
||||||
|
const isIntermediateCompletion =
|
||||||
|
event.message?.includes("Features are being generated") ||
|
||||||
|
event.message?.includes("features are being generated");
|
||||||
|
|
||||||
|
// Rely solely on explicit backend signals
|
||||||
|
const shouldComplete =
|
||||||
|
(isFinalCompletionMessage || hasCompletePhase) &&
|
||||||
|
!isIntermediateCompletion;
|
||||||
|
|
||||||
|
if (shouldComplete) {
|
||||||
|
// Fully complete - clear all states immediately
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Final completion detected - clearing state",
|
||||||
|
{
|
||||||
|
isFinalCompletionMessage,
|
||||||
|
hasCompletePhase,
|
||||||
|
message: event.message,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setIsRegenerating(false);
|
||||||
|
setIsCreating(false);
|
||||||
|
setIsGeneratingFeatures(false);
|
||||||
|
setCurrentPhase("");
|
||||||
|
setShowRegenerateDialog(false);
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
setProjectDefinition("");
|
||||||
|
setProjectOverview("");
|
||||||
|
setErrorMessage("");
|
||||||
|
stateRestoredRef.current = false;
|
||||||
|
|
||||||
|
// Reload the spec with delay to ensure file is written to disk
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSpec();
|
||||||
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
|
|
||||||
|
// Show success toast notification
|
||||||
|
const isRegeneration = event.message?.includes("regeneration");
|
||||||
|
const isFeatureGeneration =
|
||||||
|
event.message?.includes("Feature generation");
|
||||||
|
toast.success(
|
||||||
|
isFeatureGeneration
|
||||||
|
? "Feature Generation Complete"
|
||||||
|
: isRegeneration
|
||||||
|
? "Spec Regeneration Complete"
|
||||||
|
: "Spec Creation Complete",
|
||||||
|
{
|
||||||
|
description: isFeatureGeneration
|
||||||
|
? "Features have been created from the app specification."
|
||||||
|
: "Your app specification has been saved.",
|
||||||
|
icon: <CheckCircle2 className="w-4 h-4" />,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (isIntermediateCompletion) {
|
||||||
|
// Intermediate completion - keep state active for feature generation
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setIsRegenerating(true);
|
setIsRegenerating(true);
|
||||||
console.log("[SpecView] Detected feature creation tool - setting phase to feature_generation");
|
setCurrentPhase("feature_generation");
|
||||||
|
console.log(
|
||||||
|
"[SpecView] Intermediate completion, continuing with feature generation"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
console.log("[SpecView] Spec generation event:", event.message);
|
||||||
// Log tool usage with details
|
} else if (event.type === "spec_regeneration_error") {
|
||||||
const toolInput = event.input ? ` (${JSON.stringify(event.input).substring(0, 100)}...)` : "";
|
|
||||||
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
|
|
||||||
const newLog = logsRef.current + toolLog;
|
|
||||||
logsRef.current = newLog;
|
|
||||||
setLogs(newLog);
|
|
||||||
console.log("[SpecView] Tool:", event.tool, event.input);
|
|
||||||
} else if (event.type === "spec_regeneration_complete") {
|
|
||||||
// Add completion message to logs first
|
|
||||||
const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`;
|
|
||||||
logsRef.current = completionLog;
|
|
||||||
setLogs(completionLog);
|
|
||||||
|
|
||||||
// --- Completion Detection Logic ---
|
|
||||||
// The backend sends explicit signals for completion:
|
|
||||||
// 1. "All tasks completed" in the message
|
|
||||||
// 2. [Phase: complete] marker in logs
|
|
||||||
// 3. "Spec regeneration complete!" for regeneration
|
|
||||||
// 4. "Initial spec creation complete!" for creation without features
|
|
||||||
const isFinalCompletionMessage = event.message?.includes("All tasks completed") ||
|
|
||||||
event.message === "All tasks completed!" ||
|
|
||||||
event.message === "All tasks completed" ||
|
|
||||||
event.message === "Spec regeneration complete!" ||
|
|
||||||
event.message === "Initial spec creation complete!";
|
|
||||||
|
|
||||||
const hasCompletePhase = logsRef.current.includes("[Phase: complete]");
|
|
||||||
|
|
||||||
// Intermediate completion means features are being generated after spec creation
|
|
||||||
const isIntermediateCompletion = event.message?.includes("Features are being generated") ||
|
|
||||||
event.message?.includes("features are being generated");
|
|
||||||
|
|
||||||
// Rely solely on explicit backend signals
|
|
||||||
const shouldComplete = (isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion;
|
|
||||||
|
|
||||||
if (shouldComplete) {
|
|
||||||
// Fully complete - clear all states immediately
|
|
||||||
console.log("[SpecView] Final completion detected - clearing state", {
|
|
||||||
isFinalCompletionMessage,
|
|
||||||
hasCompletePhase,
|
|
||||||
message: event.message
|
|
||||||
});
|
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase("");
|
setCurrentPhase("error");
|
||||||
setShowRegenerateDialog(false);
|
setErrorMessage(event.error);
|
||||||
setShowCreateDialog(false);
|
stateRestoredRef.current = false; // Reset restoration flag
|
||||||
setProjectDefinition("");
|
// Add error to logs
|
||||||
setProjectOverview("");
|
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
|
||||||
setErrorMessage("");
|
logsRef.current = errorLog;
|
||||||
stateRestoredRef.current = false;
|
setLogs(errorLog);
|
||||||
|
console.error("[SpecView] Regeneration error:", event.error);
|
||||||
// Reload the spec with delay to ensure file is written to disk
|
|
||||||
setTimeout(() => {
|
|
||||||
loadSpec();
|
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
|
||||||
|
|
||||||
// Show success toast notification
|
|
||||||
const isRegeneration = event.message?.includes("regeneration");
|
|
||||||
const isFeatureGeneration = event.message?.includes("Feature generation");
|
|
||||||
toast.success(
|
|
||||||
isFeatureGeneration
|
|
||||||
? "Feature Generation Complete"
|
|
||||||
: isRegeneration
|
|
||||||
? "Spec Regeneration Complete"
|
|
||||||
: "Spec Creation Complete",
|
|
||||||
{
|
|
||||||
description: isFeatureGeneration
|
|
||||||
? "Features have been created from the app specification."
|
|
||||||
: "Your app specification has been saved.",
|
|
||||||
icon: <CheckCircle2 className="w-4 h-4" />,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (isIntermediateCompletion) {
|
|
||||||
// Intermediate completion - keep state active for feature generation
|
|
||||||
setIsCreating(true);
|
|
||||||
setIsRegenerating(true);
|
|
||||||
setCurrentPhase("feature_generation");
|
|
||||||
console.log("[SpecView] Intermediate completion, continuing with feature generation");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SpecView] Spec generation event:", event.message);
|
|
||||||
} else if (event.type === "spec_regeneration_error") {
|
|
||||||
setIsRegenerating(false);
|
|
||||||
setIsCreating(false);
|
|
||||||
setIsGeneratingFeatures(false);
|
|
||||||
setCurrentPhase("error");
|
|
||||||
setErrorMessage(event.error);
|
|
||||||
stateRestoredRef.current = false; // Reset restoration flag
|
|
||||||
// Add error to logs
|
|
||||||
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
|
|
||||||
logsRef.current = errorLog;
|
|
||||||
setLogs(errorLog);
|
|
||||||
console.error("[SpecView] Regeneration error:", event.error);
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
@@ -476,7 +556,10 @@ export function SpecView() {
|
|||||||
// Reset logs when starting new generation
|
// Reset logs when starting new generation
|
||||||
logsRef.current = "";
|
logsRef.current = "";
|
||||||
setLogs("");
|
setLogs("");
|
||||||
console.log("[SpecView] Starting spec creation, generateFeatures:", generateFeatures);
|
console.log(
|
||||||
|
"[SpecView] Starting spec creation, generateFeatures:",
|
||||||
|
generateFeatures
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) {
|
if (!api.specRegeneration) {
|
||||||
@@ -537,7 +620,10 @@ export function SpecView() {
|
|||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const errorMsg = result.error || "Unknown error";
|
const errorMsg = result.error || "Unknown error";
|
||||||
console.error("[SpecView] Failed to start feature generation:", errorMsg);
|
console.error(
|
||||||
|
"[SpecView] Failed to start feature generation:",
|
||||||
|
errorMsg
|
||||||
|
);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase("error");
|
setCurrentPhase("error");
|
||||||
setErrorMessage(errorMsg);
|
setErrorMessage(errorMsg);
|
||||||
@@ -606,18 +692,31 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||||
{isCreating ? "Generating Specification" : "Regenerating Specification"}
|
{isCreating
|
||||||
|
? "Generating Specification"
|
||||||
|
: "Regenerating Specification"}
|
||||||
</span>
|
</span>
|
||||||
{currentPhase && (
|
{currentPhase && (
|
||||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||||
{currentPhase === "initialization" && "Initializing..."}
|
{currentPhase === "initialization" && "Initializing..."}
|
||||||
{currentPhase === "setup" && "Setting up tools..."}
|
{currentPhase === "setup" && "Setting up tools..."}
|
||||||
{currentPhase === "analysis" && "Analyzing project structure..."}
|
{currentPhase === "analysis" &&
|
||||||
{currentPhase === "spec_complete" && "Spec created! Generating features..."}
|
"Analyzing project structure..."}
|
||||||
{currentPhase === "feature_generation" && "Creating features from roadmap..."}
|
{currentPhase === "spec_complete" &&
|
||||||
|
"Spec created! Generating features..."}
|
||||||
|
{currentPhase === "feature_generation" &&
|
||||||
|
"Creating features from roadmap..."}
|
||||||
{currentPhase === "complete" && "Complete!"}
|
{currentPhase === "complete" && "Complete!"}
|
||||||
{currentPhase === "error" && "Error occurred"}
|
{currentPhase === "error" && "Error occurred"}
|
||||||
{!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase}
|
{![
|
||||||
|
"initialization",
|
||||||
|
"setup",
|
||||||
|
"analysis",
|
||||||
|
"spec_complete",
|
||||||
|
"feature_generation",
|
||||||
|
"complete",
|
||||||
|
"error",
|
||||||
|
].includes(currentPhase) && currentPhase}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -653,12 +752,23 @@ export function SpecView() {
|
|||||||
<span className="text-sm font-semibold text-primary text-center tracking-tight">
|
<span className="text-sm font-semibold text-primary text-center tracking-tight">
|
||||||
{currentPhase === "initialization" && "Initializing..."}
|
{currentPhase === "initialization" && "Initializing..."}
|
||||||
{currentPhase === "setup" && "Setting up tools..."}
|
{currentPhase === "setup" && "Setting up tools..."}
|
||||||
{currentPhase === "analysis" && "Analyzing project structure..."}
|
{currentPhase === "analysis" &&
|
||||||
{currentPhase === "spec_complete" && "Spec created! Generating features..."}
|
"Analyzing project structure..."}
|
||||||
{currentPhase === "feature_generation" && "Creating features from roadmap..."}
|
{currentPhase === "spec_complete" &&
|
||||||
|
"Spec created! Generating features..."}
|
||||||
|
{currentPhase === "feature_generation" &&
|
||||||
|
"Creating features from roadmap..."}
|
||||||
{currentPhase === "complete" && "Complete!"}
|
{currentPhase === "complete" && "Complete!"}
|
||||||
{currentPhase === "error" && "Error occurred"}
|
{currentPhase === "error" && "Error occurred"}
|
||||||
{!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase}
|
{![
|
||||||
|
"initialization",
|
||||||
|
"setup",
|
||||||
|
"analysis",
|
||||||
|
"spec_complete",
|
||||||
|
"feature_generation",
|
||||||
|
"complete",
|
||||||
|
"error",
|
||||||
|
].includes(currentPhase) && currentPhase}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -682,10 +792,7 @@ export function SpecView() {
|
|||||||
)}
|
)}
|
||||||
{!isCreating && (
|
{!isCreating && (
|
||||||
<div className="flex gap-2 justify-center">
|
<div className="flex gap-2 justify-center">
|
||||||
<Button
|
<Button size="lg" onClick={() => setShowCreateDialog(true)}>
|
||||||
size="lg"
|
|
||||||
onClick={() => setShowCreateDialog(true)}
|
|
||||||
>
|
|
||||||
<FilePlus2 className="w-5 h-5 mr-2" />
|
<FilePlus2 className="w-5 h-5 mr-2" />
|
||||||
Create app_spec
|
Create app_spec
|
||||||
</Button>
|
</Button>
|
||||||
@@ -695,8 +802,8 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Dialog */}
|
{/* Create Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showCreateDialog}
|
open={showCreateDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open && !isCreating) {
|
if (!open && !isCreating) {
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
@@ -707,20 +814,20 @@ export function SpecView() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create App Specification</DialogTitle>
|
<DialogTitle>Create App Specification</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt
|
We didn't find an app_spec.txt file. Let us help you
|
||||||
to help describe your project for our system. We'll analyze your project's
|
generate your app_spec.txt to help describe your project for our
|
||||||
tech stack and create a comprehensive specification.
|
system. We'll analyze your project's tech stack and
|
||||||
|
create a comprehensive specification.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">
|
<label className="text-sm font-medium">Project Overview</label>
|
||||||
Project Overview
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Describe what your project does and what features you want to build.
|
Describe what your project does and what features you want to
|
||||||
Be as detailed as you want - this will help us create a better specification.
|
build. Be as detailed as you want - this will help us create a
|
||||||
|
better specification.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
@@ -736,19 +843,23 @@ export function SpecView() {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="generate-features"
|
id="generate-features"
|
||||||
checked={generateFeatures}
|
checked={generateFeatures}
|
||||||
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
|
onCheckedChange={(checked) =>
|
||||||
|
setGenerateFeatures(checked === true)
|
||||||
|
}
|
||||||
disabled={isCreating}
|
disabled={isCreating}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label
|
<label
|
||||||
htmlFor="generate-features"
|
htmlFor="generate-features"
|
||||||
className={`text-sm font-medium ${isCreating ? "" : "cursor-pointer"}`}
|
className={`text-sm font-medium ${
|
||||||
|
isCreating ? "" : "cursor-pointer"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
Generate feature list
|
Generate feature list
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Automatically create features in the features folder from the
|
Automatically create features in the features folder from
|
||||||
implementation roadmap after the spec is generated.
|
the implementation roadmap after the spec is generated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -812,18 +923,33 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||||
{isGeneratingFeatures ? "Generating Features" : isCreating ? "Generating Specification" : "Regenerating Specification"}
|
{isGeneratingFeatures
|
||||||
|
? "Generating Features"
|
||||||
|
: isCreating
|
||||||
|
? "Generating Specification"
|
||||||
|
: "Regenerating Specification"}
|
||||||
</span>
|
</span>
|
||||||
{currentPhase && (
|
{currentPhase && (
|
||||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||||
{currentPhase === "initialization" && "Initializing..."}
|
{currentPhase === "initialization" && "Initializing..."}
|
||||||
{currentPhase === "setup" && "Setting up tools..."}
|
{currentPhase === "setup" && "Setting up tools..."}
|
||||||
{currentPhase === "analysis" && "Analyzing project structure..."}
|
{currentPhase === "analysis" &&
|
||||||
{currentPhase === "spec_complete" && "Spec created! Generating features..."}
|
"Analyzing project structure..."}
|
||||||
{currentPhase === "feature_generation" && "Creating features from roadmap..."}
|
{currentPhase === "spec_complete" &&
|
||||||
|
"Spec created! Generating features..."}
|
||||||
|
{currentPhase === "feature_generation" &&
|
||||||
|
"Creating features from roadmap..."}
|
||||||
{currentPhase === "complete" && "Complete!"}
|
{currentPhase === "complete" && "Complete!"}
|
||||||
{currentPhase === "error" && "Error occurred"}
|
{currentPhase === "error" && "Error occurred"}
|
||||||
{!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase}
|
{![
|
||||||
|
"initialization",
|
||||||
|
"setup",
|
||||||
|
"analysis",
|
||||||
|
"spec_complete",
|
||||||
|
"feature_generation",
|
||||||
|
"complete",
|
||||||
|
"error",
|
||||||
|
].includes(currentPhase) && currentPhase}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -833,8 +959,12 @@ export function SpecView() {
|
|||||||
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
|
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
|
||||||
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
|
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">Error</span>
|
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">
|
||||||
<span className="text-xs text-destructive/90 leading-tight font-medium">{errorMessage}</span>
|
Error
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-destructive/90 leading-tight font-medium">
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -856,7 +986,13 @@ export function SpecView() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={saveSpec}
|
onClick={saveSpec}
|
||||||
disabled={!hasChanges || isSaving || isCreating || isRegenerating || isGeneratingFeatures}
|
disabled={
|
||||||
|
!hasChanges ||
|
||||||
|
isSaving ||
|
||||||
|
isCreating ||
|
||||||
|
isRegenerating ||
|
||||||
|
isGeneratingFeatures
|
||||||
|
}
|
||||||
data-testid="save-spec"
|
data-testid="save-spec"
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
@@ -879,8 +1015,8 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Regenerate Dialog */}
|
{/* Regenerate Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showRegenerateDialog}
|
open={showRegenerateDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open && !isRegenerating) {
|
if (!open && !isRegenerating) {
|
||||||
setShowRegenerateDialog(false);
|
setShowRegenerateDialog(false);
|
||||||
@@ -891,9 +1027,10 @@ export function SpecView() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Regenerate App Specification</DialogTitle>
|
<DialogTitle>Regenerate App Specification</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
We will regenerate your app spec based on a short project definition and the
|
We will regenerate your app spec based on a short project
|
||||||
current tech stack found in your project. The agent will analyze your codebase
|
definition and the current tech stack found in your project. The
|
||||||
to understand your existing technologies and create a comprehensive specification.
|
agent will analyze your codebase to understand your existing
|
||||||
|
technologies and create a comprehensive specification.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -903,8 +1040,9 @@ export function SpecView() {
|
|||||||
Describe your project
|
Describe your project
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Provide a clear description of what your app should do. Be as detailed as you
|
Provide a clear description of what your app should do. Be as
|
||||||
want - the more context you provide, the more comprehensive the spec will be.
|
detailed as you want - the more context you provide, the more
|
||||||
|
comprehensive the spec will be.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
@@ -945,9 +1083,17 @@ export function SpecView() {
|
|||||||
</Button>
|
</Button>
|
||||||
<HotkeyButton
|
<HotkeyButton
|
||||||
onClick={handleRegenerate}
|
onClick={handleRegenerate}
|
||||||
disabled={!projectDefinition.trim() || isRegenerating || isGeneratingFeatures}
|
disabled={
|
||||||
|
!projectDefinition.trim() ||
|
||||||
|
isRegenerating ||
|
||||||
|
isGeneratingFeatures
|
||||||
|
}
|
||||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||||
hotkeyActive={showRegenerateDialog && !isRegenerating && !isGeneratingFeatures}
|
hotkeyActive={
|
||||||
|
showRegenerateDialog &&
|
||||||
|
!isRegenerating &&
|
||||||
|
!isGeneratingFeatures
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isRegenerating ? (
|
{isRegenerating ? (
|
||||||
<>
|
<>
|
||||||
@@ -965,7 +1111,6 @@ export function SpecView() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ export function WelcomeView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the app_spec.txt with the project name
|
// Update the app_spec.txt with the project name
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
`${projectPath}/.automaker/app_spec.txt`,
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
`<project_specification>
|
`<project_specification>
|
||||||
@@ -352,6 +353,7 @@ export function WelcomeView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the app_spec.txt with template-specific info
|
// Update the app_spec.txt with template-specific info
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
`${projectPath}/.automaker/app_spec.txt`,
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
`<project_specification>
|
`<project_specification>
|
||||||
@@ -456,6 +458,7 @@ export function WelcomeView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the app_spec.txt with basic info
|
// Update the app_spec.txt with basic info
|
||||||
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
`${projectPath}/.automaker/app_spec.txt`,
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
`<project_specification>
|
`<project_specification>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
import { FileBrowserDialog } from "@/components/dialogs/file-browser-dialog";
|
import { FileBrowserDialog } from "@/components/dialogs/file-browser-dialog";
|
||||||
|
|
||||||
interface FileBrowserOptions {
|
interface FileBrowserOptions {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
initialPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileBrowserContextValue {
|
interface FileBrowserContextValue {
|
||||||
@@ -16,36 +23,47 @@ const FileBrowserContext = createContext<FileBrowserContextValue | null>(null);
|
|||||||
|
|
||||||
export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [resolver, setResolver] = useState<((value: string | null) => void) | null>(null);
|
const [resolver, setResolver] = useState<
|
||||||
|
((value: string | null) => void) | null
|
||||||
|
>(null);
|
||||||
const [dialogOptions, setDialogOptions] = useState<FileBrowserOptions>({});
|
const [dialogOptions, setDialogOptions] = useState<FileBrowserOptions>({});
|
||||||
|
|
||||||
const openFileBrowser = useCallback((options?: FileBrowserOptions): Promise<string | null> => {
|
const openFileBrowser = useCallback(
|
||||||
return new Promise((resolve) => {
|
(options?: FileBrowserOptions): Promise<string | null> => {
|
||||||
setDialogOptions(options || {});
|
return new Promise((resolve) => {
|
||||||
setIsOpen(true);
|
setDialogOptions(options || {});
|
||||||
setResolver(() => resolve);
|
setIsOpen(true);
|
||||||
});
|
setResolver(() => resolve);
|
||||||
}, []);
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelect = useCallback((path: string) => {
|
const handleSelect = useCallback(
|
||||||
if (resolver) {
|
(path: string) => {
|
||||||
resolver(path);
|
if (resolver) {
|
||||||
setResolver(null);
|
resolver(path);
|
||||||
}
|
setResolver(null);
|
||||||
setIsOpen(false);
|
}
|
||||||
setDialogOptions({});
|
setIsOpen(false);
|
||||||
}, [resolver]);
|
|
||||||
|
|
||||||
const handleOpenChange = useCallback((open: boolean) => {
|
|
||||||
if (!open && resolver) {
|
|
||||||
resolver(null);
|
|
||||||
setResolver(null);
|
|
||||||
}
|
|
||||||
setIsOpen(open);
|
|
||||||
if (!open) {
|
|
||||||
setDialogOptions({});
|
setDialogOptions({});
|
||||||
}
|
},
|
||||||
}, [resolver]);
|
[resolver]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (!open && resolver) {
|
||||||
|
resolver(null);
|
||||||
|
setResolver(null);
|
||||||
|
}
|
||||||
|
setIsOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setDialogOptions({});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[resolver]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileBrowserContext.Provider value={{ openFileBrowser }}>
|
<FileBrowserContext.Provider value={{ openFileBrowser }}>
|
||||||
@@ -56,6 +74,7 @@ export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
|||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
title={dialogOptions.title}
|
title={dialogOptions.title}
|
||||||
description={dialogOptions.description}
|
description={dialogOptions.description}
|
||||||
|
initialPath={dialogOptions.initialPath}
|
||||||
/>
|
/>
|
||||||
</FileBrowserContext.Provider>
|
</FileBrowserContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -70,9 +89,13 @@ export function useFileBrowser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global reference for non-React code (like HttpApiClient)
|
// Global reference for non-React code (like HttpApiClient)
|
||||||
let globalFileBrowserFn: ((options?: FileBrowserOptions) => Promise<string | null>) | null = null;
|
let globalFileBrowserFn:
|
||||||
|
| ((options?: FileBrowserOptions) => Promise<string | null>)
|
||||||
|
| null = null;
|
||||||
|
|
||||||
export function setGlobalFileBrowser(fn: (options?: FileBrowserOptions) => Promise<string | null>) {
|
export function setGlobalFileBrowser(
|
||||||
|
fn: (options?: FileBrowserOptions) => Promise<string | null>
|
||||||
|
) {
|
||||||
globalFileBrowserFn = fn;
|
globalFileBrowserFn = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,10 +133,15 @@ export interface SuggestionsAPI {
|
|||||||
|
|
||||||
// Spec Regeneration types
|
// Spec Regeneration types
|
||||||
export type SpecRegenerationEvent =
|
export type SpecRegenerationEvent =
|
||||||
| { type: "spec_regeneration_progress"; content: string }
|
| { type: "spec_regeneration_progress"; content: string; projectPath: string }
|
||||||
| { type: "spec_regeneration_tool"; tool: string; input: unknown }
|
| {
|
||||||
| { type: "spec_regeneration_complete"; message: string }
|
type: "spec_regeneration_tool";
|
||||||
| { type: "spec_regeneration_error"; error: string };
|
tool: string;
|
||||||
|
input: unknown;
|
||||||
|
projectPath: string;
|
||||||
|
}
|
||||||
|
| { type: "spec_regeneration_complete"; message: string; projectPath: string }
|
||||||
|
| { type: "spec_regeneration_error"; error: string; projectPath: string };
|
||||||
|
|
||||||
export interface SpecRegenerationAPI {
|
export interface SpecRegenerationAPI {
|
||||||
create: (
|
create: (
|
||||||
@@ -1923,6 +1928,7 @@ async function simulateSpecCreation(
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: initialization] Starting project analysis...\n",
|
content: "[Phase: initialization] Starting project analysis...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -1935,6 +1941,7 @@ async function simulateSpecCreation(
|
|||||||
type: "spec_regeneration_tool",
|
type: "spec_regeneration_tool",
|
||||||
tool: "Glob",
|
tool: "Glob",
|
||||||
input: { pattern: "**/*.{json,ts,tsx}" },
|
input: { pattern: "**/*.{json,ts,tsx}" },
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -1946,6 +1953,7 @@ async function simulateSpecCreation(
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: analysis] Detecting tech stack...\n",
|
content: "[Phase: analysis] Detecting tech stack...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -1989,6 +1997,7 @@ async function simulateSpecCreation(
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_complete",
|
type: "spec_regeneration_complete",
|
||||||
message: "All tasks completed!",
|
message: "All tasks completed!",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockSpecRegenerationRunning = false;
|
mockSpecRegenerationRunning = false;
|
||||||
@@ -2004,6 +2013,7 @@ async function simulateSpecRegeneration(
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: initialization] Starting spec regeneration...\n",
|
content: "[Phase: initialization] Starting spec regeneration...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -2015,6 +2025,7 @@ async function simulateSpecRegeneration(
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: analysis] Analyzing codebase...\n",
|
content: "[Phase: analysis] Analyzing codebase...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -2049,6 +2060,7 @@ async function simulateSpecRegeneration(
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_complete",
|
type: "spec_regeneration_complete",
|
||||||
message: "All tasks completed!",
|
message: "All tasks completed!",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockSpecRegenerationRunning = false;
|
mockSpecRegenerationRunning = false;
|
||||||
@@ -2062,6 +2074,7 @@ async function simulateFeatureGeneration(projectPath: string) {
|
|||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content:
|
content:
|
||||||
"[Phase: initialization] Starting feature generation from existing app_spec.txt...\n",
|
"[Phase: initialization] Starting feature generation from existing app_spec.txt...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -2072,6 +2085,7 @@ async function simulateFeatureGeneration(projectPath: string) {
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: feature_generation] Reading implementation roadmap...\n",
|
content: "[Phase: feature_generation] Reading implementation roadmap...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -2083,6 +2097,7 @@ async function simulateFeatureGeneration(projectPath: string) {
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: feature_generation] Creating features from roadmap...\n",
|
content: "[Phase: feature_generation] Creating features from roadmap...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -2094,11 +2109,13 @@ async function simulateFeatureGeneration(projectPath: string) {
|
|||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "[Phase: complete] All tasks completed!\n",
|
content: "[Phase: complete] All tasks completed!\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
emitSpecRegenerationEvent({
|
emitSpecRegenerationEvent({
|
||||||
type: "spec_regeneration_complete",
|
type: "spec_regeneration_complete",
|
||||||
message: "All tasks completed!",
|
message: "All tasks completed!",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockSpecRegenerationRunning = false;
|
mockSpecRegenerationRunning = false;
|
||||||
|
|||||||
@@ -428,6 +428,10 @@ export interface AppState {
|
|||||||
|
|
||||||
// Terminal state
|
// Terminal state
|
||||||
terminalState: TerminalState;
|
terminalState: TerminalState;
|
||||||
|
|
||||||
|
// Spec Creation State (per-project, keyed by project path)
|
||||||
|
// Tracks which project is currently having its spec generated
|
||||||
|
specCreatingForProject: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default background settings for board backgrounds
|
// Default background settings for board backgrounds
|
||||||
@@ -630,6 +634,10 @@ export interface AppActions {
|
|||||||
direction?: "horizontal" | "vertical"
|
direction?: "horizontal" | "vertical"
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
// Spec Creation actions
|
||||||
|
setSpecCreatingForProject: (projectPath: string | null) => void;
|
||||||
|
isSpecCreatingForProject: (projectPath: string) => boolean;
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
@@ -713,6 +721,7 @@ const initialState: AppState = {
|
|||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
defaultFontSize: 14,
|
defaultFontSize: 14,
|
||||||
},
|
},
|
||||||
|
specCreatingForProject: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppState & AppActions>()(
|
export const useAppStore = create<AppState & AppActions>()(
|
||||||
@@ -2080,6 +2089,15 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Spec Creation actions
|
||||||
|
setSpecCreatingForProject: (projectPath) => {
|
||||||
|
set({ specCreatingForProject: projectPath });
|
||||||
|
},
|
||||||
|
|
||||||
|
isSpecCreatingForProject: (projectPath) => {
|
||||||
|
return get().specCreatingForProject === projectPath;
|
||||||
|
},
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => set(initialState),
|
reset: () => set(initialState),
|
||||||
}),
|
}),
|
||||||
|
|||||||
4
apps/app/src/types/electron.d.ts
vendored
4
apps/app/src/types/electron.d.ts
vendored
@@ -243,19 +243,23 @@ export type SpecRegenerationEvent =
|
|||||||
| {
|
| {
|
||||||
type: "spec_regeneration_progress";
|
type: "spec_regeneration_progress";
|
||||||
content: string;
|
content: string;
|
||||||
|
projectPath: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "spec_regeneration_tool";
|
type: "spec_regeneration_tool";
|
||||||
tool: string;
|
tool: string;
|
||||||
input: unknown;
|
input: unknown;
|
||||||
|
projectPath: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "spec_regeneration_complete";
|
type: "spec_regeneration_complete";
|
||||||
message: string;
|
message: string;
|
||||||
|
projectPath: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "spec_regeneration_error";
|
type: "spec_regeneration_error";
|
||||||
error: string;
|
error: string;
|
||||||
|
projectPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SpecRegenerationAPI {
|
export interface SpecRegenerationAPI {
|
||||||
|
|||||||
@@ -51,3 +51,5 @@ TERMINAL_ENABLED=true
|
|||||||
# Password to protect terminal access (leave empty for no password)
|
# Password to protect terminal access (leave empty for no password)
|
||||||
# If set, users must enter this password before accessing terminal
|
# If set, users must enter this password before accessing terminal
|
||||||
TERMINAL_PASSWORD=
|
TERMINAL_PASSWORD=
|
||||||
|
|
||||||
|
ENABLE_REQUEST_LOGGING=false
|
||||||
|
|||||||
@@ -22,12 +22,14 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"morgan": "^1.10.1",
|
||||||
"node-pty": "1.1.0-beta41",
|
"node-pty": "1.1.0-beta41",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.18",
|
"@types/cors": "^2.8.18",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/morgan": "^1.9.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitest/coverage-v8": "^4.0.15",
|
"@vitest/coverage-v8": "^4.0.15",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
import morgan from "morgan";
|
||||||
import { WebSocketServer, WebSocket } from "ws";
|
import { WebSocketServer, WebSocket } from "ws";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
@@ -46,6 +47,7 @@ dotenv.config();
|
|||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || "3008", 10);
|
const PORT = parseInt(process.env.PORT || "3008", 10);
|
||||||
const DATA_DIR = process.env.DATA_DIR || "./data";
|
const DATA_DIR = process.env.DATA_DIR || "./data";
|
||||||
|
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== "false"; // Default to true
|
||||||
|
|
||||||
// Check for required environment variables
|
// Check for required environment variables
|
||||||
// Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use)
|
// Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use)
|
||||||
@@ -83,6 +85,22 @@ initAllowedPaths();
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
|
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
|
||||||
|
if (ENABLE_REQUEST_LOGGING) {
|
||||||
|
morgan.token("status-colored", (req, res) => {
|
||||||
|
const status = res.statusCode;
|
||||||
|
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
||||||
|
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
||||||
|
if (status >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
|
||||||
|
return `\x1b[32m${status}\x1b[0m`; // Green for success
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
morgan(":method :url :status-colored", {
|
||||||
|
skip: (req) => req.url === "/api/health", // Skip health check logs
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: process.env.CORS_ORIGIN || "*",
|
origin: process.env.CORS_ORIGIN || "*",
|
||||||
|
|||||||
88
apps/server/src/lib/app-spec-format.ts
Normal file
88
apps/server/src/lib/app-spec-format.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* XML Template Format Specification for app_spec.txt
|
||||||
|
*
|
||||||
|
* This format must be included in all prompts that generate, modify, or regenerate
|
||||||
|
* app specifications to ensure consistency across the application.
|
||||||
|
*/
|
||||||
|
export const APP_SPEC_XML_FORMAT = `
|
||||||
|
The app_spec.txt file MUST follow this exact XML format:
|
||||||
|
|
||||||
|
<project_specification>
|
||||||
|
<project_name>Project Name</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
A comprehensive description of what the project does, its purpose, and key goals.
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<technology>Technology 1</technology>
|
||||||
|
<technology>Technology 2</technology>
|
||||||
|
<!-- List all technologies, frameworks, libraries, and tools used -->
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<capability>Core capability 1</capability>
|
||||||
|
<capability>Core capability 2</capability>
|
||||||
|
<!-- List main features and capabilities the project provides -->
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<implemented_features>
|
||||||
|
<!-- Features that have been implemented (populated by AI agent based on code analysis) -->
|
||||||
|
</implemented_features>
|
||||||
|
|
||||||
|
<!-- Optional sections that may be included: -->
|
||||||
|
<additional_requirements>
|
||||||
|
<!-- Any additional requirements or constraints -->
|
||||||
|
</additional_requirements>
|
||||||
|
|
||||||
|
<development_guidelines>
|
||||||
|
<guideline>Guideline 1</guideline>
|
||||||
|
<guideline>Guideline 2</guideline>
|
||||||
|
<!-- Development standards and practices -->
|
||||||
|
</development_guidelines>
|
||||||
|
|
||||||
|
<implementation_roadmap>
|
||||||
|
<!-- Phases or roadmap items for implementation -->
|
||||||
|
</implementation_roadmap>
|
||||||
|
</project_specification>
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
- All content must be wrapped in valid XML tags
|
||||||
|
- Use proper XML escaping for special characters (<, >, &)
|
||||||
|
- Maintain proper indentation (2 spaces)
|
||||||
|
- All sections should be populated based on project analysis
|
||||||
|
- The format must be strictly followed - do not use markdown, JSON, or any other format
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a prompt suffix that instructs the AI to format the response as XML
|
||||||
|
* following the app_spec.txt template format.
|
||||||
|
*/
|
||||||
|
export function getAppSpecFormatInstruction(): string {
|
||||||
|
return `
|
||||||
|
${APP_SPEC_XML_FORMAT}
|
||||||
|
|
||||||
|
CRITICAL FORMATTING REQUIREMENTS:
|
||||||
|
- Your ENTIRE response MUST be valid XML following the exact template structure above
|
||||||
|
- Do NOT use markdown formatting (no # headers, no **bold**, no - lists, etc.)
|
||||||
|
- Do NOT include any explanatory text, prefix, or suffix outside the XML tags
|
||||||
|
- Do NOT include phrases like "Based on my analysis..." or "I'll create..." before the XML
|
||||||
|
- Do NOT include any text before <project_specification> or after </project_specification>
|
||||||
|
- Your response must start IMMEDIATELY with <project_specification> with no preceding text
|
||||||
|
- Your response must end IMMEDIATELY with </project_specification> with no following text
|
||||||
|
- Use ONLY XML tags as shown in the template
|
||||||
|
- Properly escape XML special characters (< for <, > for >, & for &)
|
||||||
|
- Maintain 2-space indentation for readability
|
||||||
|
- The output will be saved directly to app_spec.txt and must be parseable as valid XML
|
||||||
|
- The response must contain exactly ONE root XML element: <project_specification>
|
||||||
|
- Do not include code blocks, markdown fences, or any other formatting
|
||||||
|
|
||||||
|
VERIFICATION: Before responding, verify that:
|
||||||
|
1. Your response starts with <project_specification> (no spaces, no text before it)
|
||||||
|
2. Your response ends with </project_specification> (no spaces, no text after it)
|
||||||
|
3. There is exactly one root XML element
|
||||||
|
4. There is no explanatory text, analysis, or commentary outside the XML tags
|
||||||
|
|
||||||
|
Your response should be ONLY the XML content, nothing else.
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Security utilities for path validation
|
* Security utilities for path validation
|
||||||
|
* Note: All permission checks have been disabled to allow unrestricted access
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
// Allowed project directories - loaded from environment
|
// Allowed project directories - kept for API compatibility
|
||||||
const allowedPaths = new Set<string>();
|
const allowedPaths = new Set<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize allowed paths from environment variable
|
* Initialize allowed paths from environment variable
|
||||||
|
* Note: All paths are now allowed regardless of this setting
|
||||||
*/
|
*/
|
||||||
export function initAllowedPaths(): void {
|
export function initAllowedPaths(): void {
|
||||||
const dirs = process.env.ALLOWED_PROJECT_DIRS;
|
const dirs = process.env.ALLOWED_PROJECT_DIRS;
|
||||||
@@ -21,13 +23,11 @@ export function initAllowedPaths(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always allow the data directory
|
|
||||||
const dataDir = process.env.DATA_DIR;
|
const dataDir = process.env.DATA_DIR;
|
||||||
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;
|
const workspaceDir = process.env.WORKSPACE_DIR;
|
||||||
if (workspaceDir) {
|
if (workspaceDir) {
|
||||||
allowedPaths.add(path.resolve(workspaceDir));
|
allowedPaths.add(path.resolve(workspaceDir));
|
||||||
@@ -35,41 +35,24 @@ export function initAllowedPaths(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a path to the allowed list
|
* Add a path to the allowed list (no-op, all paths allowed)
|
||||||
*/
|
*/
|
||||||
export function addAllowedPath(filePath: string): void {
|
export function addAllowedPath(filePath: string): void {
|
||||||
allowedPaths.add(path.resolve(filePath));
|
allowedPaths.add(path.resolve(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is allowed
|
* Check if a path is allowed - always returns true
|
||||||
*/
|
*/
|
||||||
export function isPathAllowed(filePath: string): boolean {
|
export function isPathAllowed(_filePath: string): boolean {
|
||||||
const resolved = path.resolve(filePath);
|
return true;
|
||||||
|
|
||||||
// Check if the path is under any allowed directory
|
|
||||||
for (const allowed of allowedPaths) {
|
|
||||||
if (resolved.startsWith(allowed + path.sep) || resolved === allowed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a path and throw if not allowed
|
* Validate a path - just resolves the path without checking permissions
|
||||||
*/
|
*/
|
||||||
export function validatePath(filePath: string): string {
|
export function validatePath(filePath: string): string {
|
||||||
const resolved = path.resolve(filePath);
|
return path.resolve(filePath);
|
||||||
|
|
||||||
if (!isPathAllowed(resolved)) {
|
|
||||||
throw new Error(
|
|
||||||
`Access denied: ${filePath} is not in an allowed directory`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -75,37 +75,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
|
|
||||||
const resolvedPath = path.resolve(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
|
// Add the new directory to allowed paths for tracking
|
||||||
addAllowedPath(resolvedPath);
|
addAllowedPath(resolvedPath);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
@@ -449,6 +421,13 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
return drives;
|
return drives;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get parent directory
|
||||||
|
const parentPath = path.dirname(targetPath);
|
||||||
|
const hasParent = parentPath !== targetPath;
|
||||||
|
|
||||||
|
// Get available drives
|
||||||
|
const drives = await detectDrives();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(targetPath);
|
const stats = await fs.stat(targetPath);
|
||||||
|
|
||||||
@@ -471,13 +450,6 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
}))
|
}))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
// Get parent directory
|
|
||||||
const parentPath = path.dirname(targetPath);
|
|
||||||
const hasParent = parentPath !== targetPath;
|
|
||||||
|
|
||||||
// Get available drives
|
|
||||||
const drives = await detectDrives();
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
currentPath: targetPath,
|
currentPath: targetPath,
|
||||||
@@ -486,11 +458,29 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
drives,
|
drives,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
// Handle permission errors gracefully - still return path info so user can navigate away
|
||||||
success: false,
|
const errorMessage =
|
||||||
error:
|
error instanceof Error ? error.message : "Failed to read directory";
|
||||||
error instanceof Error ? error.message : "Failed to read directory",
|
const isPermissionError =
|
||||||
});
|
errorMessage.includes("EPERM") || errorMessage.includes("EACCES");
|
||||||
|
|
||||||
|
if (isPermissionError) {
|
||||||
|
// Return success with empty directories so user can still navigate to parent
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
currentPath: targetPath,
|
||||||
|
parentPath: hasParent ? parentPath : null,
|
||||||
|
directories: [],
|
||||||
|
drives,
|
||||||
|
warning:
|
||||||
|
"Permission denied - grant Full Disk Access to Terminal in System Preferences > Privacy & Security",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -637,130 +627,5 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Browse directories for file picker
|
|
||||||
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
|
||||||
router.post("/browse", async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { dirPath } = req.body as { dirPath?: string };
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
|
|
||||||
// Detect available drives on Windows
|
|
||||||
const detectDrives = async (): Promise<string[]> => {
|
|
||||||
if (os.platform() !== "win32") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const drives: string[] = [];
|
|
||||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
||||||
|
|
||||||
for (const letter of letters) {
|
|
||||||
const drivePath = `${letter}:\\`;
|
|
||||||
try {
|
|
||||||
await fs.access(drivePath);
|
|
||||||
drives.push(drivePath);
|
|
||||||
} catch {
|
|
||||||
// Drive doesn't exist, skip it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return drives;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if a path is safe to browse
|
|
||||||
const isSafePath = (targetPath: string): boolean => {
|
|
||||||
const resolved = path.resolve(targetPath);
|
|
||||||
const normalizedHome = path.resolve(homeDir);
|
|
||||||
|
|
||||||
// Allow browsing within home directory
|
|
||||||
if (
|
|
||||||
resolved === normalizedHome ||
|
|
||||||
resolved.startsWith(normalizedHome + path.sep)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow browsing already-allowed paths
|
|
||||||
if (isPathAllowed(resolved)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Windows, allow drive roots for initial navigation
|
|
||||||
if (os.platform() === "win32") {
|
|
||||||
const driveRootMatch = /^[A-Z]:\\$/i.test(resolved);
|
|
||||||
if (driveRootMatch) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Unix, allow root for initial navigation (but only list, not read files)
|
|
||||||
if (os.platform() !== "win32" && resolved === "/") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default to home directory if no path provided
|
|
||||||
const targetPath = dirPath ? path.resolve(dirPath) : homeDir;
|
|
||||||
|
|
||||||
// Security check: validate the path is safe to browse
|
|
||||||
if (!isSafePath(targetPath)) {
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
"Access denied: browsing is restricted to your home directory and allowed project paths",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stats = await fs.stat(targetPath);
|
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
|
||||||
res
|
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "Path is not a directory" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read directory contents
|
|
||||||
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
// Filter for directories only and exclude hidden directories
|
|
||||||
const directories = entries
|
|
||||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
||||||
.map((entry) => ({
|
|
||||||
name: entry.name,
|
|
||||||
path: path.join(targetPath, entry.name),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
// Get parent directory (only if parent is also safe to browse)
|
|
||||||
const parentPath = path.dirname(targetPath);
|
|
||||||
const hasParent = parentPath !== targetPath && isSafePath(parentPath);
|
|
||||||
|
|
||||||
// Get available drives on Windows
|
|
||||||
const drives = await detectDrives();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
currentPath: targetPath,
|
|
||||||
parentPath: hasParent ? parentPath : null,
|
|
||||||
directories,
|
|
||||||
drives,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
error instanceof Error ? error.message : "Failed to read directory",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
|
||||||
res.status(500).json({ success: false, error: message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
|
import { getAppSpecFormatInstruction } from "../lib/app-spec-format.js";
|
||||||
|
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let currentAbortController: AbortController | null = null;
|
let currentAbortController: AbortController | null = null;
|
||||||
@@ -15,13 +16,29 @@ let currentAbortController: AbortController | null = null;
|
|||||||
function logAuthStatus(context: string): void {
|
function logAuthStatus(context: string): void {
|
||||||
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||||
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] ${context} - Auth Status:`);
|
console.log(`[SpecRegeneration] ${context} - Auth Status:`);
|
||||||
console.log(`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${hasOAuthToken ? 'SET (' + process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...)' : 'NOT SET'}`);
|
console.log(
|
||||||
console.log(`[SpecRegeneration] ANTHROPIC_API_KEY: ${hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'}`);
|
`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${
|
||||||
|
hasOAuthToken
|
||||||
|
? "SET (" +
|
||||||
|
process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) +
|
||||||
|
"...)"
|
||||||
|
: "NOT SET"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[SpecRegeneration] ANTHROPIC_API_KEY: ${
|
||||||
|
hasApiKey
|
||||||
|
? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
|
||||||
|
: "NOT SET"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
if (!hasOAuthToken && !hasApiKey) {
|
if (!hasOAuthToken && !hasApiKey) {
|
||||||
console.error(`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`);
|
console.error(
|
||||||
|
`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +47,14 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
// Create project spec from overview
|
// Create project spec from overview
|
||||||
router.post("/create", async (req: Request, res: Response) => {
|
router.post("/create", async (req: Request, res: Response) => {
|
||||||
console.log("[SpecRegeneration] ========== /create endpoint called ==========");
|
console.log(
|
||||||
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
|
"[SpecRegeneration] ========== /create endpoint called =========="
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Request body:",
|
||||||
|
JSON.stringify(req.body, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, projectOverview, generateFeatures } = req.body as {
|
const { projectPath, projectOverview, generateFeatures } = req.body as {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@@ -42,7 +64,11 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
console.log(`[SpecRegeneration] Parsed params:`);
|
console.log(`[SpecRegeneration] Parsed params:`);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
console.log(`[SpecRegeneration] projectOverview length: ${projectOverview?.length || 0} chars`);
|
console.log(
|
||||||
|
`[SpecRegeneration] projectOverview length: ${
|
||||||
|
projectOverview?.length || 0
|
||||||
|
} chars`
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
||||||
|
|
||||||
if (!projectPath || !projectOverview) {
|
if (!projectPath || !projectOverview) {
|
||||||
@@ -55,7 +81,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
console.warn("[SpecRegeneration] Generation already running, rejecting request");
|
console.warn(
|
||||||
|
"[SpecRegeneration] Generation already running, rejecting request"
|
||||||
|
);
|
||||||
res.json({ success: false, error: "Spec generation already running" });
|
res.json({ success: false, error: "Spec generation already running" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,19 +107,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
console.error("[SpecRegeneration] Error name:", error?.name);
|
console.error("[SpecRegeneration] Error name:", error?.name);
|
||||||
console.error("[SpecRegeneration] Error message:", error?.message);
|
console.error("[SpecRegeneration] Error message:", error?.message);
|
||||||
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
||||||
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
console.error(
|
||||||
|
"[SpecRegeneration] Full error object:",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_error",
|
type: "spec_regeneration_error",
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
console.log("[SpecRegeneration] Generation task finished (success or error)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Generation task finished (success or error)"
|
||||||
|
);
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
currentAbortController = null;
|
currentAbortController = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Returning success response (generation running in background)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Returning success response (generation running in background)"
|
||||||
|
);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
||||||
@@ -103,9 +139,14 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
// Generate from project definition
|
// Generate from project definition
|
||||||
router.post("/generate", async (req: Request, res: Response) => {
|
router.post("/generate", async (req: Request, res: Response) => {
|
||||||
console.log("[SpecRegeneration] ========== /generate endpoint called ==========");
|
console.log(
|
||||||
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
|
"[SpecRegeneration] ========== /generate endpoint called =========="
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Request body:",
|
||||||
|
JSON.stringify(req.body, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, projectDefinition } = req.body as {
|
const { projectPath, projectDefinition } = req.body as {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@@ -114,7 +155,11 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
console.log(`[SpecRegeneration] Parsed params:`);
|
console.log(`[SpecRegeneration] Parsed params:`);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
console.log(`[SpecRegeneration] projectDefinition length: ${projectDefinition?.length || 0} chars`);
|
console.log(
|
||||||
|
`[SpecRegeneration] projectDefinition length: ${
|
||||||
|
projectDefinition?.length || 0
|
||||||
|
} chars`
|
||||||
|
);
|
||||||
|
|
||||||
if (!projectPath || !projectDefinition) {
|
if (!projectPath || !projectDefinition) {
|
||||||
console.error("[SpecRegeneration] Missing required parameters");
|
console.error("[SpecRegeneration] Missing required parameters");
|
||||||
@@ -126,7 +171,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
console.warn("[SpecRegeneration] Generation already running, rejecting request");
|
console.warn(
|
||||||
|
"[SpecRegeneration] Generation already running, rejecting request"
|
||||||
|
);
|
||||||
res.json({ success: false, error: "Spec generation already running" });
|
res.json({ success: false, error: "Spec generation already running" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,19 +196,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
console.error("[SpecRegeneration] Error name:", error?.name);
|
console.error("[SpecRegeneration] Error name:", error?.name);
|
||||||
console.error("[SpecRegeneration] Error message:", error?.message);
|
console.error("[SpecRegeneration] Error message:", error?.message);
|
||||||
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
||||||
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
console.error(
|
||||||
|
"[SpecRegeneration] Full error object:",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_error",
|
type: "spec_regeneration_error",
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
console.log("[SpecRegeneration] Generation task finished (success or error)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Generation task finished (success or error)"
|
||||||
|
);
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
currentAbortController = null;
|
currentAbortController = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Returning success response (generation running in background)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Returning success response (generation running in background)"
|
||||||
|
);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
||||||
@@ -173,9 +228,14 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
// Generate features from existing spec
|
// Generate features from existing spec
|
||||||
router.post("/generate-features", async (req: Request, res: Response) => {
|
router.post("/generate-features", async (req: Request, res: Response) => {
|
||||||
console.log("[SpecRegeneration] ========== /generate-features endpoint called ==========");
|
console.log(
|
||||||
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
|
"[SpecRegeneration] ========== /generate-features endpoint called =========="
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Request body:",
|
||||||
|
JSON.stringify(req.body, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath } = req.body as { projectPath: string };
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
|
|
||||||
@@ -188,7 +248,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
console.warn("[SpecRegeneration] Generation already running, rejecting request");
|
console.warn(
|
||||||
|
"[SpecRegeneration] Generation already running, rejecting request"
|
||||||
|
);
|
||||||
res.json({ success: false, error: "Generation already running" });
|
res.json({ success: false, error: "Generation already running" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,27 +259,38 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
|||||||
|
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
currentAbortController = new AbortController();
|
currentAbortController = new AbortController();
|
||||||
console.log("[SpecRegeneration] Starting background feature generation task...");
|
console.log(
|
||||||
|
"[SpecRegeneration] Starting background feature generation task..."
|
||||||
|
);
|
||||||
|
|
||||||
generateFeaturesFromSpec(projectPath, events, currentAbortController)
|
generateFeaturesFromSpec(projectPath, events, currentAbortController)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[SpecRegeneration] ❌ Feature generation failed with error:");
|
console.error(
|
||||||
|
"[SpecRegeneration] ❌ Feature generation failed with error:"
|
||||||
|
);
|
||||||
console.error("[SpecRegeneration] Error name:", error?.name);
|
console.error("[SpecRegeneration] Error name:", error?.name);
|
||||||
console.error("[SpecRegeneration] Error message:", error?.message);
|
console.error("[SpecRegeneration] Error message:", error?.message);
|
||||||
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
console.error("[SpecRegeneration] Error stack:", error?.stack);
|
||||||
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
console.error(
|
||||||
|
"[SpecRegeneration] Full error object:",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_error",
|
type: "features_error",
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
console.log("[SpecRegeneration] Feature generation task finished (success or error)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Feature generation task finished (success or error)"
|
||||||
|
);
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
currentAbortController = null;
|
currentAbortController = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Returning success response (generation running in background)");
|
console.log(
|
||||||
|
"[SpecRegeneration] Returning success response (generation running in background)"
|
||||||
|
);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
console.error("[SpecRegeneration] ❌ Route handler exception:");
|
||||||
@@ -261,39 +334,30 @@ async function generateSpec(
|
|||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
generateFeatures?: boolean
|
generateFeatures?: boolean
|
||||||
) {
|
) {
|
||||||
console.log("[SpecRegeneration] ========== generateSpec() started ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateSpec() started =========="
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
console.log(`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`);
|
console.log(
|
||||||
|
`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
|
||||||
|
|
||||||
const prompt = `You are helping to define a software project specification.
|
const prompt = `You are helping to define a software project specification.
|
||||||
|
|
||||||
Project Overview:
|
Project Overview:
|
||||||
${projectOverview}
|
${projectOverview}
|
||||||
|
|
||||||
Based on this overview, analyze the project and create a comprehensive specification that includes:
|
Based on this overview, analyze the project directory (if it exists) and create a comprehensive specification. Use the Read, Glob, and Grep tools to explore the codebase and understand:
|
||||||
|
- Existing technologies and frameworks
|
||||||
|
- Project structure and architecture
|
||||||
|
- Current features and capabilities
|
||||||
|
- Code patterns and conventions
|
||||||
|
|
||||||
1. **Project Summary** - Brief description of what the project does
|
${getAppSpecFormatInstruction()}`;
|
||||||
2. **Core Features** - Main functionality the project needs
|
|
||||||
3. **Technical Stack** - Recommended technologies and frameworks
|
|
||||||
4. **Architecture** - High-level system design
|
|
||||||
5. **Data Models** - Key entities and their relationships
|
|
||||||
6. **API Design** - Main endpoints/interfaces needed
|
|
||||||
7. **User Experience** - Key user flows and interactions
|
|
||||||
|
|
||||||
${generateFeatures ? `
|
|
||||||
Also generate a list of features to implement. For each feature provide:
|
|
||||||
- ID (lowercase-hyphenated)
|
|
||||||
- Title
|
|
||||||
- Description
|
|
||||||
- Priority (1=high, 2=medium, 3=low)
|
|
||||||
- Estimated complexity (simple, moderate, complex)
|
|
||||||
` : ""}
|
|
||||||
|
|
||||||
Format your response as markdown. Be specific and actionable.`;
|
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
|
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_progress",
|
type: "spec_progress",
|
||||||
content: "Starting spec generation...\n",
|
content: "Starting spec generation...\n",
|
||||||
@@ -308,9 +372,12 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2));
|
console.log(
|
||||||
|
"[SpecRegeneration] SDK Options:",
|
||||||
|
JSON.stringify(options, null, 2)
|
||||||
|
);
|
||||||
console.log("[SpecRegeneration] Calling Claude Agent SDK query()...");
|
console.log("[SpecRegeneration] Calling Claude Agent SDK query()...");
|
||||||
|
|
||||||
// Log auth status right before the SDK call
|
// Log auth status right before the SDK call
|
||||||
logAuthStatus("Right before SDK query()");
|
logAuthStatus("Right before SDK query()");
|
||||||
|
|
||||||
@@ -332,16 +399,26 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
console.log(`[SpecRegeneration] Stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2));
|
console.log(
|
||||||
|
`[SpecRegeneration] Stream message #${messageCount}:`,
|
||||||
|
JSON.stringify(
|
||||||
|
{ type: msg.type, subtype: (msg as any).subtype },
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant" && msg.message.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText = block.text;
|
responseText = block.text;
|
||||||
console.log(`[SpecRegeneration] Text block received (${block.text.length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Text block received (${block.text.length} chars)`
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: block.text,
|
content: block.text,
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} else if (block.type === "tool_use") {
|
} else if (block.type === "tool_use") {
|
||||||
console.log(`[SpecRegeneration] Tool use: ${block.name}`);
|
console.log(`[SpecRegeneration] Tool use: ${block.name}`);
|
||||||
@@ -356,8 +433,13 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
console.log("[SpecRegeneration] Received success result");
|
console.log("[SpecRegeneration] Received success result");
|
||||||
responseText = (msg as any).result || responseText;
|
responseText = (msg as any).result || responseText;
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === "error") {
|
||||||
console.error("[SpecRegeneration] ❌ Received error message from stream:");
|
console.error(
|
||||||
console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2));
|
"[SpecRegeneration] ❌ Received error message from stream:"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"[SpecRegeneration] Error message:",
|
||||||
|
JSON.stringify(msg, null, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
@@ -366,33 +448,70 @@ Format your response as markdown. Be specific and actionable.`;
|
|||||||
throw streamError;
|
throw streamError;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`);
|
console.log(
|
||||||
console.log(`[SpecRegeneration] Response text length: ${responseText.length} chars`);
|
`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[SpecRegeneration] Response text length: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
|
||||||
// Save spec
|
// Save spec
|
||||||
const specDir = path.join(projectPath, ".automaker");
|
const specDir = path.join(projectPath, ".automaker");
|
||||||
const specPath = path.join(specDir, "app_spec.txt");
|
const specPath = path.join(specDir, "app_spec.txt");
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Saving spec to: ${specPath}`);
|
console.log(`[SpecRegeneration] Saving spec to: ${specPath}`);
|
||||||
|
|
||||||
await fs.mkdir(specDir, { recursive: true });
|
await fs.mkdir(specDir, { recursive: true });
|
||||||
await fs.writeFile(specPath, responseText);
|
await fs.writeFile(specPath, responseText);
|
||||||
|
|
||||||
console.log("[SpecRegeneration] Spec saved successfully");
|
console.log("[SpecRegeneration] Spec saved successfully");
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
// Emit spec completion event
|
||||||
type: "spec_complete",
|
|
||||||
specPath,
|
|
||||||
content: responseText,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If generate features was requested, parse and create them
|
|
||||||
if (generateFeatures) {
|
if (generateFeatures) {
|
||||||
console.log("[SpecRegeneration] Starting feature generation...");
|
// If features will be generated, emit intermediate completion
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
events.emit("spec-regeneration:event", {
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "[Phase: spec_complete] Spec created! Generating features...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If no features, emit final completion
|
||||||
|
events.emit("spec-regeneration:event", {
|
||||||
|
type: "spec_regeneration_complete",
|
||||||
|
message: "Spec regeneration complete!",
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SpecRegeneration] ========== generateSpec() completed ==========");
|
// If generate features was requested, generate them from the spec
|
||||||
|
if (generateFeatures) {
|
||||||
|
console.log("[SpecRegeneration] Starting feature generation from spec...");
|
||||||
|
// Create a new abort controller for feature generation
|
||||||
|
const featureAbortController = new AbortController();
|
||||||
|
try {
|
||||||
|
await generateFeaturesFromSpec(
|
||||||
|
projectPath,
|
||||||
|
events,
|
||||||
|
featureAbortController
|
||||||
|
);
|
||||||
|
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
|
||||||
|
} catch (featureError) {
|
||||||
|
console.error(
|
||||||
|
"[SpecRegeneration] Feature generation failed:",
|
||||||
|
featureError
|
||||||
|
);
|
||||||
|
// Don't throw - spec generation succeeded, feature generation is optional
|
||||||
|
events.emit("spec-regeneration:event", {
|
||||||
|
type: "spec_regeneration_error",
|
||||||
|
error: (featureError as Error).message || "Feature generation failed",
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateSpec() completed =========="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateFeaturesFromSpec(
|
async function generateFeaturesFromSpec(
|
||||||
@@ -400,9 +519,11 @@ async function generateFeaturesFromSpec(
|
|||||||
events: EventEmitter,
|
events: EventEmitter,
|
||||||
abortController: AbortController
|
abortController: AbortController
|
||||||
) {
|
) {
|
||||||
console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() started ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateFeaturesFromSpec() started =========="
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
|
||||||
|
|
||||||
// Read existing spec
|
// Read existing spec
|
||||||
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
|
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
|
||||||
let spec: string;
|
let spec: string;
|
||||||
@@ -411,12 +532,15 @@ async function generateFeaturesFromSpec(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
spec = await fs.readFile(specPath, "utf-8");
|
spec = await fs.readFile(specPath, "utf-8");
|
||||||
console.log(`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`
|
||||||
|
);
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError);
|
console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_error",
|
type: "spec_regeneration_error",
|
||||||
error: "No project spec found. Generate spec first.",
|
error: "No project spec found. Generate spec first.",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -453,8 +577,9 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
|
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: "Analyzing spec and generating features...\n",
|
content: "Analyzing spec and generating features...\n",
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
@@ -466,9 +591,14 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2));
|
console.log(
|
||||||
console.log("[SpecRegeneration] Calling Claude Agent SDK query() for features...");
|
"[SpecRegeneration] SDK Options:",
|
||||||
|
JSON.stringify(options, null, 2)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[SpecRegeneration] Calling Claude Agent SDK query() for features..."
|
||||||
|
);
|
||||||
|
|
||||||
logAuthStatus("Right before SDK query() for features");
|
logAuthStatus("Right before SDK query() for features");
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
@@ -489,16 +619,26 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
console.log(`[SpecRegeneration] Feature stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2));
|
console.log(
|
||||||
|
`[SpecRegeneration] Feature stream message #${messageCount}:`,
|
||||||
|
JSON.stringify(
|
||||||
|
{ type: msg.type, subtype: (msg as any).subtype },
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant" && msg.message.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText = block.text;
|
responseText = block.text;
|
||||||
console.log(`[SpecRegeneration] Feature text block received (${block.text.length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Feature text block received (${block.text.length} chars)`
|
||||||
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: block.text,
|
content: block.text,
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,22 +646,35 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
console.log("[SpecRegeneration] Received success result for features");
|
console.log("[SpecRegeneration] Received success result for features");
|
||||||
responseText = (msg as any).result || responseText;
|
responseText = (msg as any).result || responseText;
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === "error") {
|
||||||
console.error("[SpecRegeneration] ❌ Received error message from feature stream:");
|
console.error(
|
||||||
console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2));
|
"[SpecRegeneration] ❌ Received error message from feature stream:"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"[SpecRegeneration] Error message:",
|
||||||
|
JSON.stringify(msg, null, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
console.error("[SpecRegeneration] ❌ Error while iterating feature stream:");
|
console.error(
|
||||||
|
"[SpecRegeneration] ❌ Error while iterating feature stream:"
|
||||||
|
);
|
||||||
console.error("[SpecRegeneration] Stream error:", streamError);
|
console.error("[SpecRegeneration] Stream error:", streamError);
|
||||||
throw streamError;
|
throw streamError;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`);
|
console.log(
|
||||||
console.log(`[SpecRegeneration] Feature response length: ${responseText.length} chars`);
|
`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[SpecRegeneration] Feature response length: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||||
|
|
||||||
console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() completed ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== generateFeaturesFromSpec() completed =========="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseAndCreateFeatures(
|
async function parseAndCreateFeatures(
|
||||||
@@ -529,24 +682,33 @@ async function parseAndCreateFeatures(
|
|||||||
content: string,
|
content: string,
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
) {
|
) {
|
||||||
console.log("[SpecRegeneration] ========== parseAndCreateFeatures() started ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== parseAndCreateFeatures() started =========="
|
||||||
|
);
|
||||||
console.log(`[SpecRegeneration] Content length: ${content.length} chars`);
|
console.log(`[SpecRegeneration] Content length: ${content.length} chars`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract JSON from response
|
// Extract JSON from response
|
||||||
console.log("[SpecRegeneration] Extracting JSON from response...");
|
console.log("[SpecRegeneration] Extracting JSON from response...");
|
||||||
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
||||||
if (!jsonMatch) {
|
if (!jsonMatch) {
|
||||||
console.error("[SpecRegeneration] ❌ No valid JSON found in response");
|
console.error("[SpecRegeneration] ❌ No valid JSON found in response");
|
||||||
console.error("[SpecRegeneration] Content preview:", content.substring(0, 500));
|
console.error(
|
||||||
|
"[SpecRegeneration] Content preview:",
|
||||||
|
content.substring(0, 500)
|
||||||
|
);
|
||||||
throw new Error("No valid JSON found in response");
|
throw new Error("No valid JSON found in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`);
|
console.log(
|
||||||
|
`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`
|
||||||
|
);
|
||||||
|
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
console.log(`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`);
|
console.log(
|
||||||
|
`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`
|
||||||
|
);
|
||||||
|
|
||||||
const featuresDir = path.join(projectPath, ".automaker", "features");
|
const featuresDir = path.join(projectPath, ".automaker", "features");
|
||||||
await fs.mkdir(featuresDir, { recursive: true });
|
await fs.mkdir(featuresDir, { recursive: true });
|
||||||
|
|
||||||
@@ -561,7 +723,7 @@ async function parseAndCreateFeatures(
|
|||||||
id: feature.id,
|
id: feature.id,
|
||||||
title: feature.title,
|
title: feature.title,
|
||||||
description: feature.description,
|
description: feature.description,
|
||||||
status: "backlog", // Features go to backlog - user must manually start them
|
status: "backlog", // Features go to backlog - user must manually start them
|
||||||
priority: feature.priority || 2,
|
priority: feature.priority || 2,
|
||||||
complexity: feature.complexity || "moderate",
|
complexity: feature.complexity || "moderate",
|
||||||
dependencies: feature.dependencies || [],
|
dependencies: feature.dependencies || [],
|
||||||
@@ -577,21 +739,26 @@ async function parseAndCreateFeatures(
|
|||||||
createdFeatures.push({ id: feature.id, title: feature.title });
|
createdFeatures.push({ id: feature.id, title: feature.title });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`);
|
console.log(
|
||||||
|
`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`
|
||||||
|
);
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_complete",
|
type: "spec_regeneration_complete",
|
||||||
features: createdFeatures,
|
message: `Spec regeneration complete! Created ${createdFeatures.length} features.`,
|
||||||
count: createdFeatures.length,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:");
|
console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:");
|
||||||
console.error("[SpecRegeneration] Error:", error);
|
console.error("[SpecRegeneration] Error:", error);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "features_error",
|
type: "spec_regeneration_error",
|
||||||
error: (error as Error).message,
|
error: (error as Error).message,
|
||||||
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SpecRegeneration] ========== parseAndCreateFeatures() completed ==========");
|
console.log(
|
||||||
|
"[SpecRegeneration] ========== parseAndCreateFeatures() completed =========="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
82
package-lock.json
generated
82
package-lock.json
generated
@@ -9495,12 +9495,14 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"morgan": "^1.10.1",
|
||||||
"node-pty": "1.1.0-beta41",
|
"node-pty": "1.1.0-beta41",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.18",
|
"@types/cors": "^2.8.18",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/morgan": "^1.9.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitest/coverage-v8": "^4.0.15",
|
"@vitest/coverage-v8": "^4.0.15",
|
||||||
@@ -11679,6 +11681,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/morgan": {
|
||||||
|
"version": "1.9.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz",
|
||||||
|
"integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.26",
|
"version": "20.19.26",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
|
||||||
@@ -12011,6 +12023,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/basic-auth": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/basic-auth/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
||||||
@@ -12302,6 +12332,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -13666,6 +13705,34 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/morgan": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-auth": "~2.0.1",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"on-finished": "~2.3.0",
|
||||||
|
"on-headers": "~1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/morgan/node_modules/on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
@@ -13676,6 +13743,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -13829,6 +13902,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user