diff --git a/apps/app/src/components/dialogs/file-browser-dialog.tsx b/apps/app/src/components/dialogs/file-browser-dialog.tsx index 0042f146..351534d5 100644 --- a/apps/app/src/components/dialogs/file-browser-dialog.tsx +++ b/apps/app/src/components/dialogs/file-browser-dialog.tsx @@ -33,6 +33,7 @@ interface BrowseResult { directories: DirectoryEntry[]; drives?: string[]; error?: string; + warning?: string; } interface FileBrowserDialogProps { @@ -41,6 +42,7 @@ interface FileBrowserDialogProps { onSelect: (path: string) => void; title?: string; description?: string; + initialPath?: string; } export function FileBrowserDialog({ @@ -49,6 +51,7 @@ export function FileBrowserDialog({ onSelect, title = "Select Project Directory", description = "Navigate to your project folder or paste a path directly", + initialPath, }: FileBrowserDialogProps) { const [currentPath, setCurrentPath] = useState(""); const [pathInput, setPathInput] = useState(""); @@ -57,11 +60,13 @@ export function FileBrowserDialog({ const [drives, setDrives] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); + const [warning, setWarning] = useState(""); const pathInputRef = useRef(null); const browseDirectory = async (dirPath?: string) => { setLoading(true); setError(""); + setWarning(""); try { // Get server URL from environment or default @@ -82,6 +87,7 @@ export function FileBrowserDialog({ setParentPath(result.parentPath); setDirectories(result.directories); setDrives(result.drives || []); + setWarning(result.warning || ""); } else { setError(result.error || "Failed to browse directory"); } @@ -94,13 +100,25 @@ export function FileBrowserDialog({ } }; - // Load home directory on mount + // Reset current path when dialog closes useEffect(() => { - if (open && !currentPath) { - browseDirectory(); + if (!open) { + setCurrentPath(""); + setPathInput(""); + setParentPath(null); + setDirectories([]); + setError(""); + setWarning(""); } }, [open]); + // Load initial path or home directory when dialog opens + useEffect(() => { + if (open && !currentPath) { + browseDirectory(initialPath); + } + }, [open, initialPath]); + const handleSelectDirectory = (dir: DirectoryEntry) => { browseDirectory(dir.path); }; @@ -246,7 +264,13 @@ export function FileBrowserDialog({ )} - {!loading && !error && directories.length === 0 && ( + {warning && ( +
+
{warning}
+
+ )} + + {!loading && !error && !warning && directories.length === 0 && (
No subdirectories found diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/app/src/components/layout/sidebar.tsx index 2a094481..26ad3e69 100644 --- a/apps/app/src/components/layout/sidebar.tsx +++ b/apps/app/src/components/layout/sidebar.tsx @@ -34,6 +34,10 @@ import { Sparkles, Loader2, Terminal, + Rocket, + Zap, + CheckCircle2, + ArrowRight, } from "lucide-react"; import { DropdownMenu, @@ -78,6 +82,7 @@ import { themeOptions } from "@/config/theme-options"; import { Checkbox } from "@/components/ui/checkbox"; import type { SpecRegenerationEvent } from "@/types/electron"; import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog"; +import { NewProjectModal } from "@/components/new-project-modal"; import { DndContext, DragEndEvent, @@ -92,6 +97,8 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { getHttpApiClient } from "@/lib/http-api-client"; +import type { StarterTemplate } from "@/lib/templates"; interface NavSection { label?: string; @@ -205,6 +212,8 @@ export function Sidebar() { setPreviewTheme, theme: globalTheme, moveProjectToTrash, + specCreatingForProject, + setSpecCreatingForProject, } = useAppStore(); // Environment variable flags for hiding sidebar items @@ -234,17 +243,26 @@ export function Sidebar() { // State for running agents count 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 const [showSetupDialog, setShowSetupDialog] = useState(false); const [setupProjectPath, setSetupProjectPath] = useState(""); const [projectOverview, setProjectOverview] = useState(""); - const [isCreatingSpec, setIsCreatingSpec] = useState(false); - const [creatingSpecProjectPath, setCreatingSpecProjectPath] = useState< - string | null - >(null); const [generateFeatures, setGenerateFeatures] = useState(true); const [showSpecIndicator, setShowSpecIndicator] = useState(true); + // Derive isCreatingSpec from store state + const isCreatingSpec = specCreatingForProject !== null; + const creatingSpecProjectPath = specCreatingForProject; + // Ref for project search input const projectSearchInputRef = useRef(null); @@ -334,22 +352,39 @@ export function Sidebar() { const unsubscribe = api.specRegeneration.onEvent( (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") { - setIsCreatingSpec(false); - setCreatingSpecProjectPath(null); + setSpecCreatingForProject(null); setShowSetupDialog(false); setProjectOverview(""); setSetupProjectPath(""); + // Clear onboarding state if we came from onboarding + setNewProjectName(""); + setNewProjectPath(""); toast.success("App specification created", { description: "Your project is now set up and ready to go!", }); // Navigate to spec view to show the new spec setCurrentView("spec"); } else if (event.type === "spec_regeneration_error") { - setIsCreatingSpec(false); - setCreatingSpecProjectPath(null); + setSpecCreatingForProject(null); toast.error("Failed to create specification", { description: event.error, }); @@ -360,7 +395,12 @@ export function Sidebar() { return () => { unsubscribe(); }; - }, [setCurrentView]); + }, [ + setCurrentView, + creatingSpecProjectPath, + setupProjectPath, + setSpecCreatingForProject, + ]); // Fetch running agents count function - used for initial load and event-driven updates const fetchRunningAgentsCount = useCallback(async () => { @@ -409,8 +449,8 @@ export function Sidebar() { const handleCreateInitialSpec = useCallback(async () => { if (!setupProjectPath || !projectOverview.trim()) return; - setIsCreatingSpec(true); - setCreatingSpecProjectPath(setupProjectPath); + // Set store state immediately so the loader shows up right away + setSpecCreatingForProject(setupProjectPath); setShowSpecIndicator(true); setShowSetupDialog(false); @@ -418,8 +458,7 @@ export function Sidebar() { const api = getElectronAPI(); if (!api.specRegeneration) { toast.error("Spec regeneration not available"); - setIsCreatingSpec(false); - setCreatingSpecProjectPath(null); + setSpecCreatingForProject(null); return; } const result = await api.specRegeneration.create( @@ -430,8 +469,7 @@ export function Sidebar() { if (!result.success) { console.error("[Sidebar] Failed to start spec creation:", result.error); - setIsCreatingSpec(false); - setCreatingSpecProjectPath(null); + setSpecCreatingForProject(null); toast.error("Failed to create specification", { description: result.error, }); @@ -439,24 +477,345 @@ export function Sidebar() { // If successful, we'll wait for the events to update the state } catch (error) { console.error("[Sidebar] Failed to create spec:", error); - setIsCreatingSpec(false); - setCreatingSpecProjectPath(null); + setSpecCreatingForProject(null); toast.error("Failed to create specification", { description: error instanceof Error ? error.message : "Unknown error", }); } - }, [setupProjectPath, projectOverview]); + }, [setupProjectPath, projectOverview, setSpecCreatingForProject]); // Handle skipping setup const handleSkipSetup = useCallback(() => { setShowSetupDialog(false); setProjectOverview(""); setSetupProjectPath(""); + // Clear onboarding state if we came from onboarding + if (newProjectPath) { + setNewProjectName(""); + setNewProjectPath(""); + } toast.info("Setup skipped", { 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`, + ` + ${projectName} + + + Describe your project here. This file will be analyzed by an AI agent + to understand your project structure and tech stack. + + + + + + + + + + + + + +` + ); + + 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`, + ` + ${projectName} + + + This project was created from the "${template.name}" starter template. + ${template.description} + + + + ${template.techStack + .map((tech) => `${tech}`) + .join("\n ")} + + + + ${template.features + .map((feature) => `${feature}`) + .join("\n ")} + + + + + +` + ); + + 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`, + ` + ${projectName} + + + This project was cloned from ${repoUrl}. + The AI agent will analyze the project structure. + + + + + + + + + + + + + +` + ); + + 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. * Used by both the 'O' keyboard shortcut and the folder icon button. @@ -871,7 +1230,7 @@ export function Sidebar() { A utomaker @@ -899,7 +1258,7 @@ export function Sidebar() { {sidebarOpen && (
- )} + + + + + + + {/* Delete Project Confirmation Dialog */} + + {/* New Project Modal */} + ); } diff --git a/apps/app/src/components/new-project-modal.tsx b/apps/app/src/components/new-project-modal.tsx index 1f8677f7..addefb73 100644 --- a/apps/app/src/components/new-project-modal.tsx +++ b/apps/app/src/components/new-project-modal.tsx @@ -197,6 +197,7 @@ export function NewProjectModal({ title: "Select Base Project Directory", description: "Choose the parent directory where your project will be created", + initialPath: workspaceDir || undefined, }); if (selectedPath) { setWorkspaceDir(selectedPath); @@ -281,7 +282,7 @@ export function NewProjectModal({ <> Will be created at:{" "} - {projectPath || "..."} + {projectPath || workspaceDir} ) : ( diff --git a/apps/app/src/components/views/analysis-view.tsx b/apps/app/src/components/views/analysis-view.tsx index a7a9e85f..5f67106b 100644 --- a/apps/app/src/components/views/analysis-view.tsx +++ b/apps/app/src/components/views/analysis-view.tsx @@ -345,6 +345,7 @@ export function AnalysisView() { const techStack = detectTechStack(); // Generate the spec content + // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts const specContent = ` ${projectName} diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 44692786..b39bb953 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -28,6 +28,7 @@ import { } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { cn, modelSupportsThinking } from "@/lib/utils"; +import type { SpecRegenerationEvent } from "@/types/electron"; import { Card, CardDescription, @@ -179,6 +180,8 @@ export function BoardView() { kanbanCardDetailLevel, setKanbanCardDetailLevel, boardBackgroundByProject, + specCreatingForProject, + setSpecCreatingForProject, } = useAppStore(); const shortcuts = useKeyboardShortcutsConfig(); const [activeFeature, setActiveFeature] = useState(null); @@ -233,6 +236,9 @@ export function BoardView() { const [searchQuery, setSearchQuery] = useState(""); // Validation state for add feature form 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 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 const prevProjectPathRef = useRef(null); const isSwitchingProjectRef = useRef(false); @@ -1791,34 +1828,50 @@ export function BoardView() {
{/* Search Bar Row */}
-
- - setSearchQuery(e.target.value)} - className="pl-9 pr-12 border-border" - data-testid="kanban-search-input" - /> - {searchQuery ? ( - - ) : ( - - / - - )} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9 pr-12 border-border" + data-testid="kanban-search-input" + /> + {searchQuery ? ( + + ) : ( + + / + + )} +
+ {/* Spec Creation Loading Badge */} + {isCreatingSpec && + currentProject?.path === creatingSpecProjectPath && ( +
+ + + Creating spec + +
+ )}
{/* Board Background & Detail Level Controls */} diff --git a/apps/app/src/components/views/interview-view.tsx b/apps/app/src/components/views/interview-view.tsx index 8fd74073..34b7f452 100644 --- a/apps/app/src/components/views/interview-view.tsx +++ b/apps/app/src/components/views/interview-view.tsx @@ -248,6 +248,7 @@ export function InterviewView() { .toLowerCase() .replace(/[^a-z0-9-]/g, ""); + // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts return ` ${projectName || "my-project"} diff --git a/apps/app/src/components/views/spec-view.tsx b/apps/app/src/components/views/spec-view.tsx index a6c7b387..ea9fe36d 100644 --- a/apps/app/src/components/views/spec-view.tsx +++ b/apps/app/src/components/views/spec-view.tsx @@ -279,7 +279,18 @@ export function SpecView() { const unsubscribe = api.specRegeneration.onEvent( (event: SpecRegenerationEvent) => { - console.log("[SpecView] Regeneration event:", event.type); + console.log( + "[SpecView] Regeneration event:", + event.type, + "for project:", + event.projectPath + ); + + // Only handle events for the current project + if (event.projectPath !== currentProject?.path) { + console.log("[SpecView] Ignoring event - not for current project"); + return; + } if (event.type === "spec_regeneration_progress") { // Extract phase from content if present diff --git a/apps/app/src/components/views/welcome-view.tsx b/apps/app/src/components/views/welcome-view.tsx index 1965479c..749f8703 100644 --- a/apps/app/src/components/views/welcome-view.tsx +++ b/apps/app/src/components/views/welcome-view.tsx @@ -255,6 +255,7 @@ export function WelcomeView() { } // 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`, ` @@ -352,6 +353,7 @@ export function WelcomeView() { } // 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`, ` @@ -456,6 +458,7 @@ export function WelcomeView() { } // 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`, ` diff --git a/apps/app/src/contexts/file-browser-context.tsx b/apps/app/src/contexts/file-browser-context.tsx index b4c0b4ee..6a6a5cd3 100644 --- a/apps/app/src/contexts/file-browser-context.tsx +++ b/apps/app/src/contexts/file-browser-context.tsx @@ -1,11 +1,18 @@ "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"; interface FileBrowserOptions { title?: string; description?: string; + initialPath?: string; } interface FileBrowserContextValue { @@ -16,36 +23,47 @@ const FileBrowserContext = createContext(null); export function FileBrowserProvider({ children }: { children: ReactNode }) { 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({}); - const openFileBrowser = useCallback((options?: FileBrowserOptions): Promise => { - return new Promise((resolve) => { - setDialogOptions(options || {}); - setIsOpen(true); - setResolver(() => resolve); - }); - }, []); + const openFileBrowser = useCallback( + (options?: FileBrowserOptions): Promise => { + return new Promise((resolve) => { + setDialogOptions(options || {}); + setIsOpen(true); + setResolver(() => resolve); + }); + }, + [] + ); - const handleSelect = useCallback((path: string) => { - if (resolver) { - resolver(path); - setResolver(null); - } - setIsOpen(false); - setDialogOptions({}); - }, [resolver]); - - const handleOpenChange = useCallback((open: boolean) => { - if (!open && resolver) { - resolver(null); - setResolver(null); - } - setIsOpen(open); - if (!open) { + const handleSelect = useCallback( + (path: string) => { + if (resolver) { + resolver(path); + setResolver(null); + } + setIsOpen(false); setDialogOptions({}); - } - }, [resolver]); + }, + [resolver] + ); + + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open && resolver) { + resolver(null); + setResolver(null); + } + setIsOpen(open); + if (!open) { + setDialogOptions({}); + } + }, + [resolver] + ); return ( @@ -56,6 +74,7 @@ export function FileBrowserProvider({ children }: { children: ReactNode }) { onSelect={handleSelect} title={dialogOptions.title} description={dialogOptions.description} + initialPath={dialogOptions.initialPath} /> ); @@ -70,9 +89,13 @@ export function useFileBrowser() { } // Global reference for non-React code (like HttpApiClient) -let globalFileBrowserFn: ((options?: FileBrowserOptions) => Promise) | null = null; +let globalFileBrowserFn: + | ((options?: FileBrowserOptions) => Promise) + | null = null; -export function setGlobalFileBrowser(fn: (options?: FileBrowserOptions) => Promise) { +export function setGlobalFileBrowser( + fn: (options?: FileBrowserOptions) => Promise +) { globalFileBrowserFn = fn; } diff --git a/apps/app/src/lib/electron.ts b/apps/app/src/lib/electron.ts index d244b34b..ace8e2af 100644 --- a/apps/app/src/lib/electron.ts +++ b/apps/app/src/lib/electron.ts @@ -133,10 +133,15 @@ export interface SuggestionsAPI { // Spec Regeneration types export type SpecRegenerationEvent = - | { type: "spec_regeneration_progress"; content: string } - | { type: "spec_regeneration_tool"; tool: string; input: unknown } - | { type: "spec_regeneration_complete"; message: string } - | { type: "spec_regeneration_error"; error: string }; + | { type: "spec_regeneration_progress"; content: string; projectPath: string } + | { + type: "spec_regeneration_tool"; + 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 { create: ( @@ -1923,6 +1928,7 @@ async function simulateSpecCreation( emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: initialization] Starting project analysis...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -1935,6 +1941,7 @@ async function simulateSpecCreation( type: "spec_regeneration_tool", tool: "Glob", input: { pattern: "**/*.{json,ts,tsx}" }, + projectPath: projectPath, }); await new Promise((resolve) => { @@ -1946,6 +1953,7 @@ async function simulateSpecCreation( emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: analysis] Detecting tech stack...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -1989,6 +1997,7 @@ async function simulateSpecCreation( emitSpecRegenerationEvent({ type: "spec_regeneration_complete", message: "All tasks completed!", + projectPath: projectPath, }); mockSpecRegenerationRunning = false; @@ -2004,6 +2013,7 @@ async function simulateSpecRegeneration( emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: initialization] Starting spec regeneration...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -2015,6 +2025,7 @@ async function simulateSpecRegeneration( emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: analysis] Analyzing codebase...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -2049,6 +2060,7 @@ async function simulateSpecRegeneration( emitSpecRegenerationEvent({ type: "spec_regeneration_complete", message: "All tasks completed!", + projectPath: projectPath, }); mockSpecRegenerationRunning = false; @@ -2062,6 +2074,7 @@ async function simulateFeatureGeneration(projectPath: string) { type: "spec_regeneration_progress", content: "[Phase: initialization] Starting feature generation from existing app_spec.txt...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -2072,6 +2085,7 @@ async function simulateFeatureGeneration(projectPath: string) { emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: feature_generation] Reading implementation roadmap...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -2083,6 +2097,7 @@ async function simulateFeatureGeneration(projectPath: string) { emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: feature_generation] Creating features from roadmap...\n", + projectPath: projectPath, }); await new Promise((resolve) => { @@ -2094,11 +2109,13 @@ async function simulateFeatureGeneration(projectPath: string) { emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: complete] All tasks completed!\n", + projectPath: projectPath, }); emitSpecRegenerationEvent({ type: "spec_regeneration_complete", message: "All tasks completed!", + projectPath: projectPath, }); mockSpecRegenerationRunning = false; diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts index 4b4d947f..4ca63e0b 100644 --- a/apps/app/src/store/app-store.ts +++ b/apps/app/src/store/app-store.ts @@ -428,6 +428,10 @@ export interface AppState { // Terminal state 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 @@ -630,6 +634,10 @@ export interface AppActions { direction?: "horizontal" | "vertical" ) => void; + // Spec Creation actions + setSpecCreatingForProject: (projectPath: string | null) => void; + isSpecCreatingForProject: (projectPath: string) => boolean; + // Reset reset: () => void; } @@ -713,6 +721,7 @@ const initialState: AppState = { activeSessionId: null, defaultFontSize: 14, }, + specCreatingForProject: null, }; export const useAppStore = create()( @@ -2080,6 +2089,15 @@ export const useAppStore = create()( }); }, + // Spec Creation actions + setSpecCreatingForProject: (projectPath) => { + set({ specCreatingForProject: projectPath }); + }, + + isSpecCreatingForProject: (projectPath) => { + return get().specCreatingForProject === projectPath; + }, + // Reset reset: () => set(initialState), }), diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index 0a086fbd..ea57c322 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -243,19 +243,23 @@ export type SpecRegenerationEvent = | { type: "spec_regeneration_progress"; content: string; + projectPath: string; } | { type: "spec_regeneration_tool"; 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 { diff --git a/apps/server/.env.example b/apps/server/.env.example index 564f0a64..5d9b7118 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -51,3 +51,5 @@ TERMINAL_ENABLED=true # Password to protect terminal access (leave empty for no password) # If set, users must enter this password before accessing terminal TERMINAL_PASSWORD= + +ENABLE_REQUEST_LOGGING=false diff --git a/apps/server/package.json b/apps/server/package.json index 239fe558..7b5edd5d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -22,12 +22,14 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "morgan": "^1.10.1", "node-pty": "1.1.0-beta41", "ws": "^8.18.0" }, "devDependencies": { "@types/cors": "^2.8.18", "@types/express": "^5.0.1", + "@types/morgan": "^1.9.10", "@types/node": "^20", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.0.15", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index c8b7291d..b89469b3 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -8,6 +8,7 @@ import express from "express"; import cors from "cors"; +import morgan from "morgan"; import { WebSocketServer, WebSocket } from "ws"; import { createServer } from "http"; import dotenv from "dotenv"; @@ -46,6 +47,7 @@ dotenv.config(); const PORT = parseInt(process.env.PORT || "3008", 10); 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 // Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use) @@ -83,6 +85,22 @@ initAllowedPaths(); const app = express(); // 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( cors({ origin: process.env.CORS_ORIGIN || "*", diff --git a/apps/server/src/lib/security.ts b/apps/server/src/lib/security.ts index d580cd41..7525d82f 100644 --- a/apps/server/src/lib/security.ts +++ b/apps/server/src/lib/security.ts @@ -1,14 +1,16 @@ /** * Security utilities for path validation + * Note: All permission checks have been disabled to allow unrestricted access */ import path from "path"; -// Allowed project directories - loaded from environment +// Allowed project directories - kept for API compatibility const allowedPaths = new Set(); /** * Initialize allowed paths from environment variable + * Note: All paths are now allowed regardless of this setting */ export function initAllowedPaths(): void { 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; if (dataDir) { allowedPaths.add(path.resolve(dataDir)); } - // Always allow the workspace directory (where projects are created) const workspaceDir = process.env.WORKSPACE_DIR; if (workspaceDir) { allowedPaths.add(path.resolve(workspaceDir)); @@ -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 { 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 { - const resolved = path.resolve(filePath); - - // 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; +export function isPathAllowed(_filePath: string): boolean { + return true; } /** - * Validate a path and throw if not allowed + * Validate a path - just resolves the path without checking permissions */ export function validatePath(filePath: string): string { - const resolved = path.resolve(filePath); - - if (!isPathAllowed(resolved)) { - throw new Error( - `Access denied: ${filePath} is not in an allowed directory` - ); - } - - return resolved; + return path.resolve(filePath); } /** diff --git a/apps/server/src/routes/fs.ts b/apps/server/src/routes/fs.ts index 8ade7901..65268790 100644 --- a/apps/server/src/routes/fs.ts +++ b/apps/server/src/routes/fs.ts @@ -75,37 +75,9 @@ export function createFsRoutes(_events: EventEmitter): Router { 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 }); - // Add the new directory to allowed paths so subsequent operations work + // Add the new directory to allowed paths for tracking addAllowedPath(resolvedPath); res.json({ success: true }); @@ -449,6 +421,13 @@ export function createFsRoutes(_events: EventEmitter): Router { return drives; }; + // Get parent directory + const parentPath = path.dirname(targetPath); + const hasParent = parentPath !== targetPath; + + // Get available drives + const drives = await detectDrives(); + try { const stats = await fs.stat(targetPath); @@ -471,13 +450,6 @@ export function createFsRoutes(_events: EventEmitter): Router { })) .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({ success: true, currentPath: targetPath, @@ -486,11 +458,29 @@ export function createFsRoutes(_events: EventEmitter): Router { drives, }); } catch (error) { - res.status(400).json({ - success: false, - error: - error instanceof Error ? error.message : "Failed to read directory", - }); + // Handle permission errors gracefully - still return path info so user can navigate away + const errorMessage = + 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) { 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 => { - 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; } diff --git a/apps/server/src/routes/spec-regeneration.ts b/apps/server/src/routes/spec-regeneration.ts index 7349adbf..05664a08 100644 --- a/apps/server/src/routes/spec-regeneration.ts +++ b/apps/server/src/routes/spec-regeneration.ts @@ -7,6 +7,7 @@ import { query, type Options } from "@anthropic-ai/claude-agent-sdk"; import path from "path"; import fs from "fs/promises"; import type { EventEmitter } from "../lib/events.js"; +import { getAppSpecFormatInstruction } from "../lib/app-spec-format.js"; let isRunning = false; let currentAbortController: AbortController | null = null; @@ -111,8 +112,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { JSON.stringify(error, Object.getOwnPropertyNames(error), 2) ); events.emit("spec-regeneration:event", { - type: "spec_error", + type: "spec_regeneration_error", error: error.message || String(error), + projectPath: projectPath, }); }) .finally(() => { @@ -199,8 +201,9 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { JSON.stringify(error, Object.getOwnPropertyNames(error), 2) ); events.emit("spec-regeneration:event", { - type: "spec_error", + type: "spec_regeneration_error", error: error.message || String(error), + projectPath: projectPath, }); }) .finally(() => { @@ -345,30 +348,13 @@ async function generateSpec( Project Overview: ${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 -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.`; +${getAppSpecFormatInstruction()}`; console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`); @@ -430,8 +416,9 @@ Format your response as markdown. Be specific and actionable.`; `[SpecRegeneration] Text block received (${block.text.length} chars)` ); events.emit("spec-regeneration:event", { - type: "spec_progress", + type: "spec_regeneration_progress", content: block.text, + projectPath: projectPath, }); } else if (block.type === "tool_use") { console.log(`[SpecRegeneration] Tool use: ${block.name}`); @@ -479,16 +466,47 @@ Format your response as markdown. Be specific and actionable.`; console.log("[SpecRegeneration] Spec saved successfully"); - events.emit("spec-regeneration:event", { - type: "spec_complete", - specPath, - content: responseText, - }); - - // If generate features was requested, parse and create them + // Emit spec completion event if (generateFeatures) { - console.log("[SpecRegeneration] Starting feature generation..."); - await parseAndCreateFeatures(projectPath, responseText, events); + // If features will be generated, emit intermediate completion + 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, + }); + } + + // 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( @@ -520,8 +538,9 @@ async function generateFeaturesFromSpec( } catch (readError) { console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError); events.emit("spec-regeneration:event", { - type: "features_error", + type: "spec_regeneration_error", error: "No project spec found. Generate spec first.", + projectPath: projectPath, }); return; } @@ -558,8 +577,9 @@ Generate 5-15 features that build on each other logically.`; console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`); events.emit("spec-regeneration:event", { - type: "features_progress", + type: "spec_regeneration_progress", content: "Analyzing spec and generating features...\n", + projectPath: projectPath, }); const options: Options = { @@ -616,8 +636,9 @@ Generate 5-15 features that build on each other logically.`; `[SpecRegeneration] Feature text block received (${block.text.length} chars)` ); events.emit("spec-regeneration:event", { - type: "features_progress", + type: "spec_regeneration_progress", content: block.text, + projectPath: projectPath, }); } } @@ -723,16 +744,17 @@ async function parseAndCreateFeatures( ); events.emit("spec-regeneration:event", { - type: "features_complete", - features: createdFeatures, - count: createdFeatures.length, + type: "spec_regeneration_complete", + message: `Spec regeneration complete! Created ${createdFeatures.length} features.`, + projectPath: projectPath, }); } catch (error) { console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:"); console.error("[SpecRegeneration] Error:", error); events.emit("spec-regeneration:event", { - type: "features_error", + type: "spec_regeneration_error", error: (error as Error).message, + projectPath: projectPath, }); } diff --git a/apps/server/tests/unit/lib/security.test.ts b/apps/server/tests/unit/lib/security.test.ts index 84b16a20..b078ca2f 100644 --- a/apps/server/tests/unit/lib/security.test.ts +++ b/apps/server/tests/unit/lib/security.test.ts @@ -113,7 +113,7 @@ describe("security.ts", () => { }); describe("isPathAllowed", () => { - it("should allow paths under allowed directories", async () => { + it("should allow all paths (permissions disabled)", async () => { process.env.ALLOWED_PROJECT_DIRS = "/allowed/project"; process.env.DATA_DIR = ""; @@ -122,96 +122,17 @@ describe("security.ts", () => { ); initAllowedPaths(); + // All paths are now allowed regardless of configuration expect(isPathAllowed("/allowed/project/file.txt")).toBe(true); - expect(isPathAllowed("/allowed/project/subdir/file.txt")).toBe(true); - expect(isPathAllowed("/allowed/project/deep/nested/file.txt")).toBe(true); - }); - - it("should allow the exact allowed directory", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed/project"; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - expect(isPathAllowed("/allowed/project")).toBe(true); - }); - - it("should reject paths outside allowed directories", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed/project"; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - expect(isPathAllowed("/not/allowed/file.txt")).toBe(false); - expect(isPathAllowed("/tmp/file.txt")).toBe(false); - expect(isPathAllowed("/etc/passwd")).toBe(false); - }); - - it("should block path traversal attempts", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed/project"; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - // These should resolve outside the allowed directory - expect(isPathAllowed("/allowed/project/../../../etc/passwd")).toBe(false); - expect(isPathAllowed("/allowed/project/../../other/file.txt")).toBe(false); - }); - - it("should resolve relative paths correctly", async () => { - const cwd = process.cwd(); - process.env.ALLOWED_PROJECT_DIRS = cwd; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - expect(isPathAllowed("./file.txt")).toBe(true); - expect(isPathAllowed("./subdir/file.txt")).toBe(true); - }); - - it("should reject paths that are parents of allowed directories", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed/project/subdir"; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - expect(isPathAllowed("/allowed/project")).toBe(false); - expect(isPathAllowed("/allowed")).toBe(false); - }); - - it("should handle multiple allowed directories", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/path1,/path2,/path3"; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - expect(isPathAllowed("/path1/file.txt")).toBe(true); - expect(isPathAllowed("/path2/file.txt")).toBe(true); - expect(isPathAllowed("/path3/file.txt")).toBe(true); - expect(isPathAllowed("/path4/file.txt")).toBe(false); + expect(isPathAllowed("/not/allowed/file.txt")).toBe(true); + expect(isPathAllowed("/tmp/file.txt")).toBe(true); + expect(isPathAllowed("/etc/passwd")).toBe(true); + expect(isPathAllowed("/any/path")).toBe(true); }); }); describe("validatePath", () => { - it("should return resolved path for allowed paths", async () => { + it("should return resolved path for any path (permissions disabled)", async () => { process.env.ALLOWED_PROJECT_DIRS = "/allowed"; process.env.DATA_DIR = ""; @@ -224,7 +145,7 @@ describe("security.ts", () => { expect(result).toBe(path.resolve("/allowed/file.txt")); }); - it("should throw error for disallowed paths", async () => { + it("should not throw error for any path (permissions disabled)", async () => { process.env.ALLOWED_PROJECT_DIRS = "/allowed"; process.env.DATA_DIR = ""; @@ -233,25 +154,14 @@ describe("security.ts", () => { ); initAllowedPaths(); - expect(() => validatePath("/disallowed/file.txt")).toThrow("Access denied"); - expect(() => validatePath("/disallowed/file.txt")).toThrow( - "not in an allowed directory" + // All paths are now allowed, no errors thrown + expect(() => validatePath("/disallowed/file.txt")).not.toThrow(); + expect(validatePath("/disallowed/file.txt")).toBe( + path.resolve("/disallowed/file.txt") ); }); - it("should include the file path in error message", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed"; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, validatePath } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - expect(() => validatePath("/bad/path.txt")).toThrow("/bad/path.txt"); - }); - - it("should resolve paths before validation", async () => { + it("should resolve relative paths", async () => { const cwd = process.cwd(); process.env.ALLOWED_PROJECT_DIRS = cwd; process.env.DATA_DIR = ""; diff --git a/package-lock.json b/package-lock.json index 991ffdf6..4813729a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,9 @@ "hasInstallScript": true, "license": "Unlicense", "dependencies": { - "@codemirror/lang-xml": "^6.1.0", - "@codemirror/theme-one-dark": "^6.1.3", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@lezer/highlight": "^1.2.3", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -35,7 +32,6 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.12", - "@uiw/react-codemirror": "^4.25.4", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", @@ -2521,6 +2517,13 @@ "@types/node": "*" } }, + "apps/app/node_modules/@types/hast": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "apps/app/node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -2552,6 +2555,13 @@ "xmlbuilder": ">=11.0.1" } }, + "apps/app/node_modules/@types/react": { + "version": "19.2.7", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, "apps/app/node_modules/@types/react-dom": { "version": "19.2.3", "devOptional": true, @@ -2560,6 +2570,10 @@ "@types/react": "^19.2.0" } }, + "apps/app/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "apps/app/node_modules/@types/verror": { "version": "1.10.11", "dev": true, @@ -3723,6 +3737,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "apps/app/node_modules/character-entities": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/character-entities-html4": { "version": "2.1.0", "license": "MIT", @@ -3731,6 +3753,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "apps/app/node_modules/character-entities-legacy": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "apps/app/node_modules/character-reference-invalid": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/chownr": { "version": "2.0.0", "dev": true, @@ -3867,6 +3905,14 @@ "node": ">= 0.8" } }, + "apps/app/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/commander": { "version": "5.1.0", "dev": true, @@ -4005,6 +4051,10 @@ "optional": true, "peer": true }, + "apps/app/node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, "apps/app/node_modules/damerau-levenshtein": { "version": "1.0.8", "dev": true, @@ -4073,6 +4123,17 @@ } } }, + "apps/app/node_modules/decode-named-character-reference": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -5601,6 +5662,26 @@ "node": ">= 12" } }, + "apps/app/node_modules/is-alphabetical": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "apps/app/node_modules/is-alphanumerical": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/is-array-buffer": { "version": "3.0.5", "dev": true, @@ -5750,6 +5831,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "apps/app/node_modules/is-decimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -5801,6 +5890,14 @@ "node": ">=0.10.0" } }, + "apps/app/node_modules/is-hexadecimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/is-interactive": { "version": "1.0.0", "dev": true, @@ -7447,6 +7544,27 @@ "node": ">=6" } }, + "apps/app/node_modules/parse-entities": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "apps/app/node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "license": "MIT" + }, "apps/app/node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -7665,6 +7783,14 @@ "react-is": "^16.13.1" } }, + "apps/app/node_modules/property-information": { + "version": "7.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/proxy-from-env": { "version": "1.1.0", "dev": true, @@ -8219,6 +8345,14 @@ "source-map": "^0.6.0" } }, + "apps/app/node_modules/space-separated-tokens": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "apps/app/node_modules/ssri": { "version": "12.0.0", "dev": true, @@ -9361,12 +9495,14 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "morgan": "^1.10.1", "node-pty": "1.1.0-beta41", "ws": "^8.18.0" }, "devDependencies": { "@types/cors": "^2.8.18", "@types/express": "^5.0.1", + "@types/morgan": "^1.9.10", "@types/node": "^20", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.0.15", @@ -9727,15 +9863,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", @@ -9760,113 +9887,6 @@ "node": ">=18" } }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", - "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", - "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-xml": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", - "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.4.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/xml": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", - "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz", - "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.35.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", - "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", - "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.39.4", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", - "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -10870,47 +10890,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lezer/common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", - "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz", - "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/xml": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", - "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, "node_modules/@next/env": { "version": "16.0.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", @@ -11678,15 +11657,6 @@ "@types/send": "*" } }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -11711,6 +11681,16 @@ "@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": { "version": "20.19.26", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", @@ -11735,15 +11715,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -11775,12 +11746,6 @@ "@types/node": "*" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -11802,59 +11767,6 @@ "@types/node": "*" } }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.4.tgz", - "integrity": "sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/react-codemirror": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.4.tgz", - "integrity": "sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.25.4", - "codemirror": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, "node_modules/@vitest/coverage-v8": { "version": "4.0.15", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.15.tgz", @@ -12111,6 +12023,24 @@ "dev": true, "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": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -12301,36 +12231,6 @@ "node": ">=18" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -12350,21 +12250,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -12385,16 +12270,6 @@ "dev": true, "license": "MIT" }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -12442,12 +12317,6 @@ "node": ">= 0.10" } }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -12463,23 +12332,13 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "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": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "ms": "2.0.0" } }, "node_modules/decompress-response": { @@ -13405,40 +13264,6 @@ "node": ">= 0.10" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -13449,16 +13274,6 @@ "node": ">=8" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -13890,6 +13705,34 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -13900,6 +13743,12 @@ "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": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -14053,6 +13902,15 @@ "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": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -14088,31 +13946,6 @@ "node": ">=8" } }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -14203,16 +14036,6 @@ "node": ">=0.4.0" } }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14815,16 +14638,6 @@ "node": ">=0.10.0" } }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -14856,12 +14669,6 @@ "dev": true, "license": "MIT" }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" - }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -15780,12 +15587,6 @@ } } }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",