feat: Python and Java support with URL routing and HF Spaces Dockerfile
Browse files- Dockerfile +38 -0
- packages/client/src/App.tsx +6 -2
- packages/client/src/components/ide/EnvironmentSelector.tsx +52 -0
- packages/client/src/components/layout/IDEShell.tsx +60 -7
- packages/client/src/components/layout/PanelLayout.tsx +13 -2
- packages/client/src/components/layout/TopBar.tsx +30 -0
- packages/client/src/components/terminal/TerminalPanel.tsx +57 -22
- packages/client/src/hooks/useCodeCompletion.ts +1 -1
- packages/client/src/stores/envStore.ts +24 -0
- packages/client/vite.config.d.ts +2 -0
- packages/client/vite.config.js +24 -0
- packages/server/src/agents/orchestrator.ts +1 -1
- packages/server/src/index.ts +17 -0
- packages/server/src/middleware/auth.middleware.ts +1 -1
- packages/server/src/middleware/error-handler.middleware.ts +2 -2
- packages/server/src/services/cache.service.ts +2 -2
- packages/server/src/websocket/events.ts +4 -0
- packages/server/src/websocket/execution.ts +104 -0
- packages/server/src/websocket/handler.ts +7 -1
- packages/shared/package.json +4 -3
- packages/shared/src/constants.ts +9 -1
- packages/shared/src/index.ts +3 -3
- packages/shared/src/types.ts +5 -0
- packages/shared/src/utils.ts +1 -1
Dockerfile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-bookworm
|
| 2 |
+
|
| 3 |
+
# Install Python and Java
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
python3 \
|
| 6 |
+
openjdk-17-jdk \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Set working directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Copy root package files
|
| 13 |
+
COPY package.json package-lock.json ./
|
| 14 |
+
|
| 15 |
+
# Copy workspace package files
|
| 16 |
+
COPY packages/shared/package.json ./packages/shared/
|
| 17 |
+
COPY packages/server/package.json ./packages/server/
|
| 18 |
+
COPY packages/client/package.json ./packages/client/
|
| 19 |
+
|
| 20 |
+
# Install dependencies
|
| 21 |
+
RUN npm ci
|
| 22 |
+
|
| 23 |
+
# Copy full source
|
| 24 |
+
COPY . .
|
| 25 |
+
|
| 26 |
+
# Build packages
|
| 27 |
+
RUN npm run build
|
| 28 |
+
|
| 29 |
+
# Set environment variables for Hugging Face Spaces
|
| 30 |
+
ENV NODE_ENV=production
|
| 31 |
+
ENV PORT=7860
|
| 32 |
+
ENV CLIENT_URL=http://localhost:7860
|
| 33 |
+
|
| 34 |
+
# Expose the HF port
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
|
| 37 |
+
# Start the server
|
| 38 |
+
CMD ["npm", "run", "start", "-w", "packages/server"]
|
packages/client/src/App.tsx
CHANGED
|
@@ -1,13 +1,17 @@
|
|
| 1 |
-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
| 2 |
import LandingPage from './components/landing/LandingPage';
|
| 3 |
import IDEShell from './components/layout/IDEShell';
|
| 4 |
|
|
|
|
|
|
|
| 5 |
function App() {
|
| 6 |
return (
|
| 7 |
<BrowserRouter>
|
| 8 |
<Routes>
|
| 9 |
<Route path="/" element={<LandingPage />} />
|
| 10 |
-
<Route path="/ide" element={<
|
|
|
|
|
|
|
| 11 |
</Routes>
|
| 12 |
</BrowserRouter>
|
| 13 |
);
|
|
|
|
| 1 |
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
| 2 |
import LandingPage from './components/landing/LandingPage';
|
| 3 |
import IDEShell from './components/layout/IDEShell';
|
| 4 |
|
| 5 |
+
import EnvironmentSelector from './components/ide/EnvironmentSelector';
|
| 6 |
+
|
| 7 |
function App() {
|
| 8 |
return (
|
| 9 |
<BrowserRouter>
|
| 10 |
<Routes>
|
| 11 |
<Route path="/" element={<LandingPage />} />
|
| 12 |
+
<Route path="/ide" element={<EnvironmentSelector />} />
|
| 13 |
+
<Route path="/ide/:env" element={<IDEShell />} />
|
| 14 |
+
<Route path="*" element={<Navigate to="/" replace />} />
|
| 15 |
</Routes>
|
| 16 |
</BrowserRouter>
|
| 17 |
);
|
packages/client/src/components/ide/EnvironmentSelector.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Monitor, Code2, Terminal } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
export default function EnvironmentSelector() {
|
| 6 |
+
const navigate = useNavigate();
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-950 text-zinc-100 p-4">
|
| 10 |
+
<div className="max-w-3xl w-full">
|
| 11 |
+
<h1 className="text-3xl font-bold text-center mb-2">Select Environment</h1>
|
| 12 |
+
<p className="text-center text-zinc-400 mb-10">
|
| 13 |
+
Choose your project type to launch the IDE
|
| 14 |
+
</p>
|
| 15 |
+
|
| 16 |
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
| 17 |
+
<EnvCard
|
| 18 |
+
title="Web Development"
|
| 19 |
+
description="HTML, CSS, JS/TS, React with Live Preview"
|
| 20 |
+
icon={<Monitor className="w-10 h-10 mb-4 text-blue-400" />}
|
| 21 |
+
onClick={() => navigate('/ide/web')}
|
| 22 |
+
/>
|
| 23 |
+
<EnvCard
|
| 24 |
+
title="Java"
|
| 25 |
+
description="Java JDK 17 with Console Output"
|
| 26 |
+
icon={<Code2 className="w-10 h-10 mb-4 text-orange-400" />}
|
| 27 |
+
onClick={() => navigate('/ide/java')}
|
| 28 |
+
/>
|
| 29 |
+
<EnvCard
|
| 30 |
+
title="Python"
|
| 31 |
+
description="Python 3 Environment with Console Output"
|
| 32 |
+
icon={<Terminal className="w-10 h-10 mb-4 text-green-400" />}
|
| 33 |
+
onClick={() => navigate('/ide/python')}
|
| 34 |
+
/>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function EnvCard({ title, description, icon, onClick }: { title: string, description: string, icon: React.ReactNode, onClick: () => void }) {
|
| 42 |
+
return (
|
| 43 |
+
<div
|
| 44 |
+
onClick={onClick}
|
| 45 |
+
className="flex flex-col items-center justify-center p-8 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 hover:bg-zinc-800/80 cursor-pointer rounded-xl transition-all duration-200 shadow-sm"
|
| 46 |
+
>
|
| 47 |
+
{icon}
|
| 48 |
+
<h3 className="text-xl font-medium mb-3">{title}</h3>
|
| 49 |
+
<p className="text-sm text-center text-zinc-400 leading-relaxed">{description}</p>
|
| 50 |
+
</div>
|
| 51 |
+
);
|
| 52 |
+
}
|
packages/client/src/components/layout/IDEShell.tsx
CHANGED
|
@@ -1,17 +1,27 @@
|
|
| 1 |
import { useState, useEffect } from 'react';
|
|
|
|
| 2 |
import TopBar from './TopBar';
|
| 3 |
import StatusBar from './StatusBar';
|
| 4 |
import PanelLayout from './PanelLayout';
|
| 5 |
import { useFileStore } from '@/stores/fileStore';
|
| 6 |
import { useEditorStore } from '@/stores/editorStore';
|
|
|
|
| 7 |
|
| 8 |
export default function IDEShell() {
|
|
|
|
|
|
|
|
|
|
| 9 |
const [layout, setLayout] = useState<'editor' | 'split' | 'preview'>('split');
|
| 10 |
const initialize = useFileStore((s) => s.initialize);
|
| 11 |
const initialized = useFileStore((s) => s.initialized);
|
| 12 |
const files = useFileStore((s) => s.files);
|
|
|
|
|
|
|
| 13 |
const addFile = useEditorStore((s) => s.addFile);
|
|
|
|
| 14 |
const openFiles = useEditorStore((s) => s.openFiles);
|
|
|
|
|
|
|
| 15 |
const [hasAutoOpened, setHasAutoOpened] = useState(false);
|
| 16 |
|
| 17 |
useEffect(() => {
|
|
@@ -19,13 +29,56 @@ export default function IDEShell() {
|
|
| 19 |
}, [initialize]);
|
| 20 |
|
| 21 |
useEffect(() => {
|
| 22 |
-
if (
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
setHasAutoOpened(true);
|
| 27 |
}
|
| 28 |
-
}, [initialized, hasAutoOpened, files, openFiles, addFile]);
|
| 29 |
|
| 30 |
if (!initialized) {
|
| 31 |
return (
|
|
@@ -37,9 +90,9 @@ export default function IDEShell() {
|
|
| 37 |
|
| 38 |
return (
|
| 39 |
<div className="h-screen flex flex-col bg-background overflow-hidden">
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
</div>
|
| 44 |
);
|
| 45 |
}
|
|
|
|
| 1 |
import { useState, useEffect } from 'react';
|
| 2 |
+
import { useParams, useNavigate } from 'react-router-dom';
|
| 3 |
import TopBar from './TopBar';
|
| 4 |
import StatusBar from './StatusBar';
|
| 5 |
import PanelLayout from './PanelLayout';
|
| 6 |
import { useFileStore } from '@/stores/fileStore';
|
| 7 |
import { useEditorStore } from '@/stores/editorStore';
|
| 8 |
+
import { useEnvStore, Environment } from '@/stores/envStore';
|
| 9 |
|
| 10 |
export default function IDEShell() {
|
| 11 |
+
const { env } = useParams<{ env: string }>();
|
| 12 |
+
const navigate = useNavigate();
|
| 13 |
+
|
| 14 |
const [layout, setLayout] = useState<'editor' | 'split' | 'preview'>('split');
|
| 15 |
const initialize = useFileStore((s) => s.initialize);
|
| 16 |
const initialized = useFileStore((s) => s.initialized);
|
| 17 |
const files = useFileStore((s) => s.files);
|
| 18 |
+
const createFile = useFileStore((s) => s.createFile);
|
| 19 |
+
const deleteFile = useFileStore((s) => s.deleteFile);
|
| 20 |
const addFile = useEditorStore((s) => s.addFile);
|
| 21 |
+
const removeFile = useEditorStore((s) => s.removeFile);
|
| 22 |
const openFiles = useEditorStore((s) => s.openFiles);
|
| 23 |
+
const environment = useEnvStore((s) => s.environment);
|
| 24 |
+
const setEnvironment = useEnvStore((s) => s.setEnvironment);
|
| 25 |
const [hasAutoOpened, setHasAutoOpened] = useState(false);
|
| 26 |
|
| 27 |
useEffect(() => {
|
|
|
|
| 29 |
}, [initialize]);
|
| 30 |
|
| 31 |
useEffect(() => {
|
| 32 |
+
if (env && !['web', 'java', 'python'].includes(env)) {
|
| 33 |
+
navigate('/ide', { replace: true });
|
| 34 |
+
}
|
| 35 |
+
}, [env, navigate]);
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
if (!initialized || !env || !['web', 'java', 'python'].includes(env)) return;
|
| 39 |
+
|
| 40 |
+
const targetEnv = env as Environment;
|
| 41 |
+
|
| 42 |
+
// If URL environment differs from store, clean up and scaffold
|
| 43 |
+
if (environment !== targetEnv) {
|
| 44 |
+
setEnvironment(targetEnv);
|
| 45 |
+
|
| 46 |
+
// Cleanup old files
|
| 47 |
+
Object.keys(files).forEach(f => {
|
| 48 |
+
if (f === 'README.md') return;
|
| 49 |
+
const isJava = f.endsWith('.java');
|
| 50 |
+
const isPython = f.endsWith('.py');
|
| 51 |
+
const isWeb = ['.html', '.css', '.js', '.ts', '.tsx', '.jsx'].some(ext => f.endsWith(ext));
|
| 52 |
+
|
| 53 |
+
if (targetEnv === 'java' && !isJava) { deleteFile(f); removeFile(f); }
|
| 54 |
+
if (targetEnv === 'python' && !isPython) { deleteFile(f); removeFile(f); }
|
| 55 |
+
if (targetEnv === 'web' && !isWeb) { deleteFile(f); removeFile(f); }
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
// Scaffold files
|
| 59 |
+
if (targetEnv === 'web') {
|
| 60 |
+
const content = `<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Document</title>\n <style>\n body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }\n </style>\n</head>\n<body>\n <h1>Hello Web!</h1>\n</body>\n</html>`;
|
| 61 |
+
if (!files['index.html']) createFile('index.html', content);
|
| 62 |
+
} else if (targetEnv === 'java') {
|
| 63 |
+
const content = `public class Main {\n public static void main(String[] args) {\n System.out.println("Hello, Java!");\n }\n}`;
|
| 64 |
+
if (!files['Main.java']) createFile('Main.java', content);
|
| 65 |
+
} else if (targetEnv === 'python') {
|
| 66 |
+
const content = `print("Hello, Python!")`;
|
| 67 |
+
if (!files['main.py']) createFile('main.py', content);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}, [env, environment, initialized]); // Missing files to avoid loop on scaffolding
|
| 71 |
+
|
| 72 |
+
useEffect(() => {
|
| 73 |
+
if (initialized && environment === env && !hasAutoOpened) {
|
| 74 |
+
if (Object.keys(openFiles).length === 0) {
|
| 75 |
+
if (environment === 'web' && files['index.html']) addFile('index.html', files['index.html'], 'html');
|
| 76 |
+
else if (environment === 'java' && files['Main.java']) addFile('Main.java', files['Main.java'], 'java');
|
| 77 |
+
else if (environment === 'python' && files['main.py']) addFile('main.py', files['main.py'], 'python');
|
| 78 |
}
|
| 79 |
setHasAutoOpened(true);
|
| 80 |
}
|
| 81 |
+
}, [initialized, environment, env, hasAutoOpened, files, openFiles, addFile]);
|
| 82 |
|
| 83 |
if (!initialized) {
|
| 84 |
return (
|
|
|
|
| 90 |
|
| 91 |
return (
|
| 92 |
<div className="h-screen flex flex-col bg-background overflow-hidden">
|
| 93 |
+
<TopBar layout={layout} onLayoutChange={setLayout} />
|
| 94 |
+
<PanelLayout layout={layout} />
|
| 95 |
+
<StatusBar />
|
| 96 |
</div>
|
| 97 |
);
|
| 98 |
}
|
packages/client/src/components/layout/PanelLayout.tsx
CHANGED
|
@@ -2,7 +2,9 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
| 2 |
import Sidebar from './Sidebar';
|
| 3 |
import MultiFileEditor from '@/components/editor/MultiFileEditor';
|
| 4 |
import LivePreview from '@/components/preview/LivePreview';
|
|
|
|
| 5 |
import AIChatPanel from '@/components/ai/AIChatPanel';
|
|
|
|
| 6 |
|
| 7 |
interface PanelLayoutProps {
|
| 8 |
layout: 'editor' | 'split' | 'preview';
|
|
@@ -19,6 +21,15 @@ function ResizeHandle({ direction = 'vertical' }: { direction?: 'vertical' | 'ho
|
|
| 19 |
}
|
| 20 |
|
| 21 |
export default function PanelLayout({ layout }: PanelLayoutProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
return (
|
| 23 |
<PanelGroup direction="horizontal" className="flex-1 min-h-0">
|
| 24 |
{/* Left: Sidebar */}
|
|
@@ -31,7 +42,7 @@ export default function PanelLayout({ layout }: PanelLayoutProps) {
|
|
| 31 |
{/* Center: Editor + Preview */}
|
| 32 |
<Panel defaultSize={52} minSize={30} className="flex flex-col overflow-hidden">
|
| 33 |
{layout === 'editor' && <MultiFileEditor />}
|
| 34 |
-
{layout === 'preview' &&
|
| 35 |
{layout === 'split' && (
|
| 36 |
<PanelGroup direction="horizontal">
|
| 37 |
<Panel defaultSize={50}>
|
|
@@ -39,7 +50,7 @@ export default function PanelLayout({ layout }: PanelLayoutProps) {
|
|
| 39 |
</Panel>
|
| 40 |
<ResizeHandle direction="vertical" />
|
| 41 |
<Panel defaultSize={50}>
|
| 42 |
-
|
| 43 |
</Panel>
|
| 44 |
</PanelGroup>
|
| 45 |
)}
|
|
|
|
| 2 |
import Sidebar from './Sidebar';
|
| 3 |
import MultiFileEditor from '@/components/editor/MultiFileEditor';
|
| 4 |
import LivePreview from '@/components/preview/LivePreview';
|
| 5 |
+
import TerminalPanel from '@/components/terminal/TerminalPanel';
|
| 6 |
import AIChatPanel from '@/components/ai/AIChatPanel';
|
| 7 |
+
import { useEnvStore } from '@/stores/envStore';
|
| 8 |
|
| 9 |
interface PanelLayoutProps {
|
| 10 |
layout: 'editor' | 'split' | 'preview';
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
export default function PanelLayout({ layout }: PanelLayoutProps) {
|
| 24 |
+
const environment = useEnvStore((s) => s.environment);
|
| 25 |
+
|
| 26 |
+
const renderSecondaryPanel = () => {
|
| 27 |
+
if (environment === 'java' || environment === 'python') {
|
| 28 |
+
return <TerminalPanel />;
|
| 29 |
+
}
|
| 30 |
+
return <LivePreview />;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
return (
|
| 34 |
<PanelGroup direction="horizontal" className="flex-1 min-h-0">
|
| 35 |
{/* Left: Sidebar */}
|
|
|
|
| 42 |
{/* Center: Editor + Preview */}
|
| 43 |
<Panel defaultSize={52} minSize={30} className="flex flex-col overflow-hidden">
|
| 44 |
{layout === 'editor' && <MultiFileEditor />}
|
| 45 |
+
{layout === 'preview' && renderSecondaryPanel()}
|
| 46 |
{layout === 'split' && (
|
| 47 |
<PanelGroup direction="horizontal">
|
| 48 |
<Panel defaultSize={50}>
|
|
|
|
| 50 |
</Panel>
|
| 51 |
<ResizeHandle direction="vertical" />
|
| 52 |
<Panel defaultSize={50}>
|
| 53 |
+
{renderSecondaryPanel()}
|
| 54 |
</Panel>
|
| 55 |
</PanelGroup>
|
| 56 |
)}
|
packages/client/src/components/layout/TopBar.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
import { useState } from 'react';
|
|
|
|
| 2 |
import { Menu, Play, Shield, Github, Monitor, Columns, Eye } from 'lucide-react';
|
| 3 |
import { Button } from '@/components/ui/button';
|
| 4 |
import { useAgentReview } from '@/hooks/useAgentReview';
|
| 5 |
import RepoImporter from '@/components/github/RepoImporter';
|
|
|
|
| 6 |
|
| 7 |
interface TopBarProps {
|
| 8 |
layout: 'editor' | 'split' | 'preview';
|
|
@@ -10,8 +12,16 @@ interface TopBarProps {
|
|
| 10 |
}
|
| 11 |
|
| 12 |
export default function TopBar({ layout, onLayoutChange }: TopBarProps) {
|
|
|
|
| 13 |
const { reviewWorkspace, isReviewing } = useAgentReview();
|
| 14 |
const [showImporter, setShowImporter] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
return (
|
| 17 |
<>
|
|
@@ -45,6 +55,17 @@ export default function TopBar({ layout, onLayoutChange }: TopBarProps) {
|
|
| 45 |
))}
|
| 46 |
</div>
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
<Button
|
| 49 |
size="sm"
|
| 50 |
variant="ghost"
|
|
@@ -65,6 +86,15 @@ export default function TopBar({ layout, onLayoutChange }: TopBarProps) {
|
|
| 65 |
<Github className="w-3.5 h-3.5" />
|
| 66 |
Import
|
| 67 |
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
</div>
|
| 69 |
|
| 70 |
<RepoImporter open={showImporter} onClose={() => setShowImporter(false)} />
|
|
|
|
| 1 |
import { useState } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
import { Menu, Play, Shield, Github, Monitor, Columns, Eye } from 'lucide-react';
|
| 4 |
import { Button } from '@/components/ui/button';
|
| 5 |
import { useAgentReview } from '@/hooks/useAgentReview';
|
| 6 |
import RepoImporter from '@/components/github/RepoImporter';
|
| 7 |
+
import { useEnvStore } from '@/stores/envStore';
|
| 8 |
|
| 9 |
interface TopBarProps {
|
| 10 |
layout: 'editor' | 'split' | 'preview';
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
export default function TopBar({ layout, onLayoutChange }: TopBarProps) {
|
| 15 |
+
const navigate = useNavigate();
|
| 16 |
const { reviewWorkspace, isReviewing } = useAgentReview();
|
| 17 |
const [showImporter, setShowImporter] = useState(false);
|
| 18 |
+
const environment = useEnvStore((s) => s.environment);
|
| 19 |
+
|
| 20 |
+
// Define global window method or a custom hook to trigger execution that terminal listens to?
|
| 21 |
+
// We'll dispatch a custom event for now that terminal/execution handler can listen to.
|
| 22 |
+
const executeCode = () => {
|
| 23 |
+
window.dispatchEvent(new CustomEvent('asipilot:execute'));
|
| 24 |
+
};
|
| 25 |
|
| 26 |
return (
|
| 27 |
<>
|
|
|
|
| 55 |
))}
|
| 56 |
</div>
|
| 57 |
|
| 58 |
+
{(environment === 'java' || environment === 'python') && (
|
| 59 |
+
<Button
|
| 60 |
+
size="sm"
|
| 61 |
+
onClick={executeCode}
|
| 62 |
+
className="gap-1.5 text-xs bg-green-600 hover:bg-green-700 text-white border-0 shadow-sm transition-all"
|
| 63 |
+
>
|
| 64 |
+
<Play className="w-3.5 h-3.5 fill-current" />
|
| 65 |
+
Run
|
| 66 |
+
</Button>
|
| 67 |
+
)}
|
| 68 |
+
|
| 69 |
<Button
|
| 70 |
size="sm"
|
| 71 |
variant="ghost"
|
|
|
|
| 86 |
<Github className="w-3.5 h-3.5" />
|
| 87 |
Import
|
| 88 |
</Button>
|
| 89 |
+
|
| 90 |
+
<Button
|
| 91 |
+
size="sm"
|
| 92 |
+
variant="ghost"
|
| 93 |
+
onClick={() => navigate('/ide')}
|
| 94 |
+
className="gap-1.5 text-xs text-muted-foreground hover:text-foreground ml-2"
|
| 95 |
+
>
|
| 96 |
+
Change Environment
|
| 97 |
+
</Button>
|
| 98 |
</div>
|
| 99 |
|
| 100 |
<RepoImporter open={showImporter} onClose={() => setShowImporter(false)} />
|
packages/client/src/components/terminal/TerminalPanel.tsx
CHANGED
|
@@ -1,11 +1,19 @@
|
|
| 1 |
import { useRef, useEffect } from 'react';
|
| 2 |
import { Terminal } from 'xterm';
|
| 3 |
import { FitAddon } from 'xterm-addon-fit';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import 'xterm/css/xterm.css';
|
| 5 |
|
| 6 |
export default function TerminalPanel() {
|
| 7 |
const containerRef = useRef<HTMLDivElement>(null);
|
| 8 |
const terminalRef = useRef<Terminal>();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
useEffect(() => {
|
| 11 |
if (!containerRef.current) return;
|
|
@@ -41,28 +49,6 @@ export default function TerminalPanel() {
|
|
| 41 |
terminal.writeln('');
|
| 42 |
terminal.write('\x1b[32m❯\x1b[0m ');
|
| 43 |
|
| 44 |
-
// Simple echo
|
| 45 |
-
let currentLine = '';
|
| 46 |
-
terminal.onKey(({ key, domEvent }) => {
|
| 47 |
-
const char = key;
|
| 48 |
-
if (domEvent.keyCode === 13) {
|
| 49 |
-
terminal.writeln('');
|
| 50 |
-
if (currentLine.trim()) {
|
| 51 |
-
terminal.writeln(`\x1b[90m$ ${currentLine}\x1b[0m`);
|
| 52 |
-
}
|
| 53 |
-
currentLine = '';
|
| 54 |
-
terminal.write('\x1b[32m❯\x1b[0m ');
|
| 55 |
-
} else if (domEvent.keyCode === 8) {
|
| 56 |
-
if (currentLine.length > 0) {
|
| 57 |
-
currentLine = currentLine.slice(0, -1);
|
| 58 |
-
terminal.write('\b \b');
|
| 59 |
-
}
|
| 60 |
-
} else if (char.length === 1 && !domEvent.ctrlKey && !domEvent.altKey) {
|
| 61 |
-
currentLine += char;
|
| 62 |
-
terminal.write(char);
|
| 63 |
-
}
|
| 64 |
-
});
|
| 65 |
-
|
| 66 |
terminalRef.current = terminal;
|
| 67 |
|
| 68 |
const resizeObserver = new ResizeObserver(() => fitAddon.fit());
|
|
@@ -74,5 +60,54 @@ export default function TerminalPanel() {
|
|
| 74 |
};
|
| 75 |
}, []);
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return <div ref={containerRef} className="h-full w-full" />;
|
| 78 |
}
|
|
|
|
| 1 |
import { useRef, useEffect } from 'react';
|
| 2 |
import { Terminal } from 'xterm';
|
| 3 |
import { FitAddon } from 'xterm-addon-fit';
|
| 4 |
+
import { getSocket } from '@/services/socket';
|
| 5 |
+
import { WS_EVENTS } from '@asipilot/shared';
|
| 6 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 7 |
+
import { useEnvStore } from '@/stores/envStore';
|
| 8 |
import 'xterm/css/xterm.css';
|
| 9 |
|
| 10 |
export default function TerminalPanel() {
|
| 11 |
const containerRef = useRef<HTMLDivElement>(null);
|
| 12 |
const terminalRef = useRef<Terminal>();
|
| 13 |
+
|
| 14 |
+
const activeFilePath = useEditorStore((s) => s.activeFilePath);
|
| 15 |
+
const openFiles = useEditorStore((s) => s.openFiles);
|
| 16 |
+
const environment = useEnvStore((s) => s.environment);
|
| 17 |
|
| 18 |
useEffect(() => {
|
| 19 |
if (!containerRef.current) return;
|
|
|
|
| 49 |
terminal.writeln('');
|
| 50 |
terminal.write('\x1b[32m❯\x1b[0m ');
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
terminalRef.current = terminal;
|
| 53 |
|
| 54 |
const resizeObserver = new ResizeObserver(() => fitAddon.fit());
|
|
|
|
| 60 |
};
|
| 61 |
}, []);
|
| 62 |
|
| 63 |
+
useEffect(() => {
|
| 64 |
+
const handleExecute = () => {
|
| 65 |
+
if (!activeFilePath) return;
|
| 66 |
+
const file = openFiles[activeFilePath];
|
| 67 |
+
if (!file || !environment) return;
|
| 68 |
+
|
| 69 |
+
terminalRef.current?.clear();
|
| 70 |
+
terminalRef.current?.writeln(`\x1b[33mRunning ${file.path}...\x1b[0m\n`);
|
| 71 |
+
|
| 72 |
+
const socket = getSocket();
|
| 73 |
+
socket.emit(WS_EVENTS.EXECUTE_REQUEST, {
|
| 74 |
+
language: environment,
|
| 75 |
+
content: file.content
|
| 76 |
+
});
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
window.addEventListener('asipilot:execute', handleExecute);
|
| 80 |
+
return () => window.removeEventListener('asipilot:execute', handleExecute);
|
| 81 |
+
}, [activeFilePath, openFiles, environment]);
|
| 82 |
+
|
| 83 |
+
useEffect(() => {
|
| 84 |
+
const socket = getSocket();
|
| 85 |
+
|
| 86 |
+
const onToken = (data: { token: string; isError?: boolean }) => {
|
| 87 |
+
const color = data.isError ? '\x1b[31m' : '\x1b[0m';
|
| 88 |
+
const text = data.token.replace(/\n/g, '\r\n');
|
| 89 |
+
terminalRef.current?.write(`${color}${text}\x1b[0m`);
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
const onComplete = () => {
|
| 93 |
+
terminalRef.current?.writeln('\n\n\x1b[32m❯ Execution finished.\x1b[0m ');
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
const onError = (data: { error: string }) => {
|
| 97 |
+
terminalRef.current?.writeln(`\r\n\x1b[31m[Error] ${data.error}\x1b[0m\r\n`);
|
| 98 |
+
terminalRef.current?.writeln('\x1b[32m❯\x1b[0m ');
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
socket.on(WS_EVENTS.EXECUTE_TOKEN, onToken);
|
| 102 |
+
socket.on(WS_EVENTS.EXECUTE_COMPLETE, onComplete);
|
| 103 |
+
socket.on(WS_EVENTS.EXECUTE_ERROR, onError);
|
| 104 |
+
|
| 105 |
+
return () => {
|
| 106 |
+
socket.off(WS_EVENTS.EXECUTE_TOKEN, onToken);
|
| 107 |
+
socket.off(WS_EVENTS.EXECUTE_COMPLETE, onComplete);
|
| 108 |
+
socket.off(WS_EVENTS.EXECUTE_ERROR, onError);
|
| 109 |
+
};
|
| 110 |
+
}, []);
|
| 111 |
+
|
| 112 |
return <div ref={containerRef} className="h-full w-full" />;
|
| 113 |
}
|
packages/client/src/hooks/useCodeCompletion.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function useCodeCompletion() {
|
|
| 35 |
.filter((f) => f !== filePath)
|
| 36 |
.slice(0, 3)
|
| 37 |
.map((f) => {
|
| 38 |
-
const file = editorState.openFiles
|
| 39 |
return file ? { path: f, content: file.content } : null;
|
| 40 |
})
|
| 41 |
.filter(Boolean);
|
|
|
|
| 35 |
.filter((f) => f !== filePath)
|
| 36 |
.slice(0, 3)
|
| 37 |
.map((f) => {
|
| 38 |
+
const file = editorState.openFiles[f];
|
| 39 |
return file ? { path: f, content: file.content } : null;
|
| 40 |
})
|
| 41 |
.filter(Boolean);
|
packages/client/src/stores/envStore.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
| 3 |
+
|
| 4 |
+
export type Environment = 'web' | 'java' | 'python' | null;
|
| 5 |
+
|
| 6 |
+
interface EnvState {
|
| 7 |
+
environment: Environment;
|
| 8 |
+
setEnvironment: (env: Environment) => void;
|
| 9 |
+
resetEnvironment: () => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const useEnvStore = create<EnvState>()(
|
| 13 |
+
persist(
|
| 14 |
+
(set) => ({
|
| 15 |
+
environment: null,
|
| 16 |
+
setEnvironment: (env) => set({ environment: env }),
|
| 17 |
+
resetEnvironment: () => set({ environment: null }),
|
| 18 |
+
}),
|
| 19 |
+
{
|
| 20 |
+
name: 'env-storage',
|
| 21 |
+
storage: createJSONStorage(() => localStorage),
|
| 22 |
+
}
|
| 23 |
+
)
|
| 24 |
+
);
|
packages/client/vite.config.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
declare const _default: import("vite").UserConfig;
|
| 2 |
+
export default _default;
|
packages/client/vite.config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
resolve: {
|
| 7 |
+
alias: {
|
| 8 |
+
'@': path.resolve(__dirname, './src'),
|
| 9 |
+
},
|
| 10 |
+
},
|
| 11 |
+
server: {
|
| 12 |
+
port: 5173,
|
| 13 |
+
proxy: {
|
| 14 |
+
'/api': {
|
| 15 |
+
target: 'http://localhost:3001',
|
| 16 |
+
changeOrigin: true,
|
| 17 |
+
},
|
| 18 |
+
'/socket.io': {
|
| 19 |
+
target: 'http://localhost:3001',
|
| 20 |
+
ws: true,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
},
|
| 24 |
+
});
|
packages/server/src/agents/orchestrator.ts
CHANGED
|
@@ -17,7 +17,7 @@ export class AgentOrchestrator extends EventEmitter {
|
|
| 17 |
|
| 18 |
constructor() {
|
| 19 |
super();
|
| 20 |
-
this.agents = new Map([
|
| 21 |
['security', new SecurityAgent()],
|
| 22 |
['performance', new PerformanceAgent()],
|
| 23 |
['style', new StyleAgent()],
|
|
|
|
| 17 |
|
| 18 |
constructor() {
|
| 19 |
super();
|
| 20 |
+
this.agents = new Map<AgentType, BaseAgent>([
|
| 21 |
['security', new SecurityAgent()],
|
| 22 |
['performance', new PerformanceAgent()],
|
| 23 |
['style', new StyleAgent()],
|
packages/server/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import express from 'express';
|
|
| 2 |
import { createServer } from 'http';
|
| 3 |
import { Server as SocketServer } from 'socket.io';
|
| 4 |
import helmet from 'helmet';
|
|
|
|
|
|
|
| 5 |
import { config } from './config/env.js';
|
| 6 |
import { corsMiddleware } from './middleware/cors.middleware.js';
|
| 7 |
import { errorHandler } from './middleware/error-handler.middleware.js';
|
|
@@ -37,6 +39,21 @@ app.use('/api/v1', apiRoutes);
|
|
| 37 |
// Error handler
|
| 38 |
app.use(errorHandler);
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
// WebSocket
|
| 41 |
setupWebSocketHandlers(io);
|
| 42 |
|
|
|
|
| 2 |
import { createServer } from 'http';
|
| 3 |
import { Server as SocketServer } from 'socket.io';
|
| 4 |
import helmet from 'helmet';
|
| 5 |
+
import path from 'path';
|
| 6 |
+
import { fileURLToPath } from 'url';
|
| 7 |
import { config } from './config/env.js';
|
| 8 |
import { corsMiddleware } from './middleware/cors.middleware.js';
|
| 9 |
import { errorHandler } from './middleware/error-handler.middleware.js';
|
|
|
|
| 39 |
// Error handler
|
| 40 |
app.use(errorHandler);
|
| 41 |
|
| 42 |
+
// Serve static frontend in production
|
| 43 |
+
if (config.NODE_ENV === 'production') {
|
| 44 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 45 |
+
const __dirname = path.dirname(__filename);
|
| 46 |
+
|
| 47 |
+
// In dist/index.js, __dirname is packages/server/dist
|
| 48 |
+
// So we go up 3 levels: dist -> server -> packages -> then into client/dist
|
| 49 |
+
const clientPath = path.join(__dirname, '../../client/dist');
|
| 50 |
+
app.use(express.static(clientPath));
|
| 51 |
+
|
| 52 |
+
app.get('*', (req, res) => {
|
| 53 |
+
res.sendFile(path.join(clientPath, 'index.html'));
|
| 54 |
+
});
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
// WebSocket
|
| 58 |
setupWebSocketHandlers(io);
|
| 59 |
|
packages/server/src/middleware/auth.middleware.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { config } from '../config/env.js';
|
|
| 6 |
* If AUTH_API_KEY is set in env, requires it in the Authorization header.
|
| 7 |
*/
|
| 8 |
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
| 9 |
-
const apiKey = (config as Record<string, string>)['AUTH_API_KEY'];
|
| 10 |
if (!apiKey) return next();
|
| 11 |
|
| 12 |
const authHeader = req.headers.authorization;
|
|
|
|
| 6 |
* If AUTH_API_KEY is set in env, requires it in the Authorization header.
|
| 7 |
*/
|
| 8 |
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
| 9 |
+
const apiKey = (config as unknown as Record<string, string>)['AUTH_API_KEY'];
|
| 10 |
if (!apiKey) return next();
|
| 11 |
|
| 12 |
const authHeader = req.headers.authorization;
|
packages/server/src/middleware/error-handler.middleware.ts
CHANGED
|
@@ -9,8 +9,8 @@ export function errorHandler(err: Error, req: Request, res: Response, _next: Nex
|
|
| 9 |
stack: err.stack,
|
| 10 |
});
|
| 11 |
|
| 12 |
-
const statusCode = (err as Record<string, unknown>).statusCode as number || 500;
|
| 13 |
-
const code = (err as Record<string, unknown>).code as string || 'INTERNAL_ERROR';
|
| 14 |
|
| 15 |
res.status(statusCode).json({
|
| 16 |
success: false,
|
|
|
|
| 9 |
stack: err.stack,
|
| 10 |
});
|
| 11 |
|
| 12 |
+
const statusCode = (err as unknown as Record<string, unknown>).statusCode as number || 500;
|
| 13 |
+
const code = (err as unknown as Record<string, unknown>).code as string || 'INTERNAL_ERROR';
|
| 14 |
|
| 15 |
res.status(statusCode).json({
|
| 16 |
success: false,
|
packages/server/src/services/cache.service.ts
CHANGED
|
@@ -12,8 +12,8 @@ class CacheService {
|
|
| 12 |
this.redis = new Redis(config.REDIS_URL, {
|
| 13 |
maxRetriesPerRequest: 3,
|
| 14 |
retryStrategy: (times: number) => {
|
| 15 |
-
|
| 16 |
-
return
|
| 17 |
},
|
| 18 |
lazyConnect: true,
|
| 19 |
});
|
|
|
|
| 12 |
this.redis = new Redis(config.REDIS_URL, {
|
| 13 |
maxRetriesPerRequest: 3,
|
| 14 |
retryStrategy: (times: number) => {
|
| 15 |
+
// Do not retry. Fail immediately to gracefully disable caching and prevent log spam.
|
| 16 |
+
return null;
|
| 17 |
},
|
| 18 |
lazyConnect: true,
|
| 19 |
});
|
packages/server/src/websocket/events.ts
CHANGED
|
@@ -11,4 +11,8 @@ export const WS_EVENTS = {
|
|
| 11 |
CHAT_COMPLETE: 'chat:complete',
|
| 12 |
CHAT_STOP: 'chat:stop',
|
| 13 |
CHAT_ERROR: 'chat:error',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
} as const;
|
|
|
|
| 11 |
CHAT_COMPLETE: 'chat:complete',
|
| 12 |
CHAT_STOP: 'chat:stop',
|
| 13 |
CHAT_ERROR: 'chat:error',
|
| 14 |
+
EXECUTE_REQUEST: 'execute:request',
|
| 15 |
+
EXECUTE_TOKEN: 'execute:token',
|
| 16 |
+
EXECUTE_COMPLETE: 'execute:complete',
|
| 17 |
+
EXECUTE_ERROR: 'execute:error',
|
| 18 |
} as const;
|
packages/server/src/websocket/execution.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Socket } from 'socket.io';
|
| 2 |
+
import { spawn, exec } from 'child_process';
|
| 3 |
+
import { promises as fs } from 'fs';
|
| 4 |
+
import path from 'path';
|
| 5 |
+
import os from 'os';
|
| 6 |
+
import { WS_EVENTS } from '@asipilot/shared';
|
| 7 |
+
import { logger } from '../utils/logger.js';
|
| 8 |
+
import type { WSExecuteRequest } from '@asipilot/shared';
|
| 9 |
+
|
| 10 |
+
// Time limit for execution to prevent infinite loops (10 seconds)
|
| 11 |
+
const EXECUTION_TIMEOUT_MS = 10000;
|
| 12 |
+
|
| 13 |
+
export async function handleCodeExecution(socket: Socket, data: WSExecuteRequest) {
|
| 14 |
+
const { language, content } = data;
|
| 15 |
+
let tempDir: string | null = null;
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'asipilot-exec-'));
|
| 19 |
+
|
| 20 |
+
if (language === 'python') {
|
| 21 |
+
if (!tempDir) throw new Error("Temp dir not created");
|
| 22 |
+
const dir = tempDir;
|
| 23 |
+
const filePath = path.join(dir, 'main.py');
|
| 24 |
+
await fs.writeFile(filePath, content, 'utf-8');
|
| 25 |
+
|
| 26 |
+
await runProcess(socket, 'python3', [filePath]);
|
| 27 |
+
} else if (language === 'java') {
|
| 28 |
+
if (!tempDir) throw new Error("Temp dir not created");
|
| 29 |
+
const dir = tempDir;
|
| 30 |
+
const filePath = path.join(dir, 'Main.java');
|
| 31 |
+
await fs.writeFile(filePath, content, 'utf-8');
|
| 32 |
+
|
| 33 |
+
// Compile Java First
|
| 34 |
+
await new Promise<void>((resolve, reject) => {
|
| 35 |
+
exec(`javac Main.java`, { cwd: dir, timeout: 5000 }, (error: Error | null, stdout: string, stderr: string) => {
|
| 36 |
+
if (error) {
|
| 37 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: stderr || error.message, isError: true });
|
| 38 |
+
reject(new Error('Compilation Failed'));
|
| 39 |
+
} else {
|
| 40 |
+
resolve();
|
| 41 |
+
}
|
| 42 |
+
});
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Run Java class
|
| 46 |
+
await runProcess(socket, 'java', ['Main'], tempDir);
|
| 47 |
+
} else {
|
| 48 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: `Unsupported language: ${language}`, isError: true });
|
| 49 |
+
socket.emit(WS_EVENTS.EXECUTE_COMPLETE, {});
|
| 50 |
+
}
|
| 51 |
+
} catch (err) {
|
| 52 |
+
logger.error('Execution setup error', err);
|
| 53 |
+
socket.emit(WS_EVENTS.EXECUTE_ERROR, { error: (err as Error).message });
|
| 54 |
+
socket.emit(WS_EVENTS.EXECUTE_COMPLETE, {});
|
| 55 |
+
} finally {
|
| 56 |
+
// Cleanup
|
| 57 |
+
if (tempDir) {
|
| 58 |
+
try {
|
| 59 |
+
await fs.rm(tempDir, { recursive: true, force: true });
|
| 60 |
+
} catch (e) {
|
| 61 |
+
logger.error(`Failed to cleanup temp dir ${tempDir}`, e);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function runProcess(socket: Socket, command: string, args: string[], cwd?: string): Promise<void> {
|
| 68 |
+
return new Promise((resolve, reject) => {
|
| 69 |
+
const child = spawn(command, args, { cwd });
|
| 70 |
+
|
| 71 |
+
let isFinished = false;
|
| 72 |
+
|
| 73 |
+
const timeoutPath = setTimeout(() => {
|
| 74 |
+
if (!isFinished) {
|
| 75 |
+
child.kill('SIGTERM');
|
| 76 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: '\n[Execution Terminated: Timeout (10s)]', isError: true });
|
| 77 |
+
}
|
| 78 |
+
}, EXECUTION_TIMEOUT_MS);
|
| 79 |
+
|
| 80 |
+
child.stdout.on('data', (data) => {
|
| 81 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: data.toString() });
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
child.stderr.on('data', (data) => {
|
| 85 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: data.toString(), isError: true });
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
child.on('close', (code) => {
|
| 89 |
+
isFinished = true;
|
| 90 |
+
clearTimeout(timeoutPath);
|
| 91 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: `\n[Process exited with code ${code}]` });
|
| 92 |
+
socket.emit(WS_EVENTS.EXECUTE_COMPLETE, {});
|
| 93 |
+
resolve();
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
child.on('error', (err) => {
|
| 97 |
+
isFinished = true;
|
| 98 |
+
clearTimeout(timeoutPath);
|
| 99 |
+
socket.emit(WS_EVENTS.EXECUTE_TOKEN, { token: `\n[Process error: ${err.message}]`, isError: true });
|
| 100 |
+
socket.emit(WS_EVENTS.EXECUTE_COMPLETE, {});
|
| 101 |
+
reject(err);
|
| 102 |
+
});
|
| 103 |
+
});
|
| 104 |
+
}
|
packages/server/src/websocket/handler.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { WS_EVENTS } from './events.js';
|
|
| 3 |
import { asi1 } from '../services/asi1-client.js';
|
| 4 |
import { orchestrator } from '../agents/orchestrator.js';
|
| 5 |
import { logger } from '../utils/logger.js';
|
| 6 |
-
import
|
|
|
|
| 7 |
|
| 8 |
const activeAbortControllers = new Map<string, AbortController>();
|
| 9 |
|
|
@@ -65,6 +66,11 @@ export function setupWebSocketHandlers(io: SocketServer) {
|
|
| 65 |
}
|
| 66 |
});
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
// Chat with streaming
|
| 69 |
socket.on(WS_EVENTS.CHAT_MESSAGE, async (data: WSChatMessage) => {
|
| 70 |
const abortController = new AbortController();
|
|
|
|
| 3 |
import { asi1 } from '../services/asi1-client.js';
|
| 4 |
import { orchestrator } from '../agents/orchestrator.js';
|
| 5 |
import { logger } from '../utils/logger.js';
|
| 6 |
+
import { handleCodeExecution } from './execution.js';
|
| 7 |
+
import type { WSCompletionRequest, WSReviewRequest, WSChatMessage, AgentType, WSExecuteRequest } from '@asipilot/shared';
|
| 8 |
|
| 9 |
const activeAbortControllers = new Map<string, AbortController>();
|
| 10 |
|
|
|
|
| 66 |
}
|
| 67 |
});
|
| 68 |
|
| 69 |
+
// Code execution
|
| 70 |
+
socket.on(WS_EVENTS.EXECUTE_REQUEST, async (data: WSExecuteRequest) => {
|
| 71 |
+
await handleCodeExecution(socket, data);
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
// Chat with streaming
|
| 75 |
socket.on(WS_EVENTS.CHAT_MESSAGE, async (data: WSChatMessage) => {
|
| 76 |
const abortController = new AbortController();
|
packages/shared/package.json
CHANGED
|
@@ -2,10 +2,11 @@
|
|
| 2 |
"name": "@asipilot/shared",
|
| 3 |
"version": "1.0.0",
|
| 4 |
"private": true,
|
| 5 |
-
"
|
| 6 |
-
"
|
|
|
|
| 7 |
"exports": {
|
| 8 |
-
".": "./
|
| 9 |
},
|
| 10 |
"scripts": {
|
| 11 |
"build": "tsc",
|
|
|
|
| 2 |
"name": "@asipilot/shared",
|
| 3 |
"version": "1.0.0",
|
| 4 |
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"main": "./dist/index.js",
|
| 7 |
+
"types": "./dist/index.d.ts",
|
| 8 |
"exports": {
|
| 9 |
+
".": "./dist/index.js"
|
| 10 |
},
|
| 11 |
"scripts": {
|
| 12 |
"build": "tsc",
|
packages/shared/src/constants.ts
CHANGED
|
@@ -25,6 +25,12 @@ export const WS_EVENTS = {
|
|
| 25 |
REVIEW_FILE: 'review:file',
|
| 26 |
REVIEW_FILE_RESULT: 'review:file:result',
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
// Chat
|
| 29 |
CHAT_MESSAGE: 'chat:message',
|
| 30 |
CHAT_TOKEN: 'chat:token',
|
|
@@ -37,7 +43,7 @@ export const WS_EVENTS = {
|
|
| 37 |
export const SUPPORTED_LANGUAGES = [
|
| 38 |
'html', 'css', 'scss', 'less', 'javascript', 'typescript',
|
| 39 |
'javascriptreact', 'typescriptreact', 'vue', 'svelte',
|
| 40 |
-
'json', 'markdown', 'yaml', 'xml', 'svg',
|
| 41 |
] as const;
|
| 42 |
|
| 43 |
// File Extension to Language Mapping
|
|
@@ -64,6 +70,8 @@ export const EXTENSION_MAP: Record<string, string> = {
|
|
| 64 |
'.yml': 'yaml',
|
| 65 |
'.xml': 'xml',
|
| 66 |
'.svg': 'svg',
|
|
|
|
|
|
|
| 67 |
'.env': 'plaintext',
|
| 68 |
'.gitignore': 'plaintext',
|
| 69 |
'.prettierrc': 'json',
|
|
|
|
| 25 |
REVIEW_FILE: 'review:file',
|
| 26 |
REVIEW_FILE_RESULT: 'review:file:result',
|
| 27 |
|
| 28 |
+
// Execution
|
| 29 |
+
EXECUTE_REQUEST: 'execute:request',
|
| 30 |
+
EXECUTE_TOKEN: 'execute:token',
|
| 31 |
+
EXECUTE_COMPLETE: 'execute:complete',
|
| 32 |
+
EXECUTE_ERROR: 'execute:error',
|
| 33 |
+
|
| 34 |
// Chat
|
| 35 |
CHAT_MESSAGE: 'chat:message',
|
| 36 |
CHAT_TOKEN: 'chat:token',
|
|
|
|
| 43 |
export const SUPPORTED_LANGUAGES = [
|
| 44 |
'html', 'css', 'scss', 'less', 'javascript', 'typescript',
|
| 45 |
'javascriptreact', 'typescriptreact', 'vue', 'svelte',
|
| 46 |
+
'json', 'markdown', 'yaml', 'xml', 'svg', 'java', 'python'
|
| 47 |
] as const;
|
| 48 |
|
| 49 |
// File Extension to Language Mapping
|
|
|
|
| 70 |
'.yml': 'yaml',
|
| 71 |
'.xml': 'xml',
|
| 72 |
'.svg': 'svg',
|
| 73 |
+
'.java': 'java',
|
| 74 |
+
'.py': 'python',
|
| 75 |
'.env': 'plaintext',
|
| 76 |
'.gitignore': 'plaintext',
|
| 77 |
'.prettierrc': 'json',
|
packages/shared/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
export * from './types';
|
| 2 |
-
export * from './constants';
|
| 3 |
-
export * from './utils';
|
|
|
|
| 1 |
+
export * from './types.js';
|
| 2 |
+
export * from './constants.js';
|
| 3 |
+
export * from './utils.js';
|
packages/shared/src/types.ts
CHANGED
|
@@ -240,6 +240,11 @@ export interface WSChatMessage {
|
|
| 240 |
history: ASI1Message[];
|
| 241 |
}
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
// ===== API Response Wrappers =====
|
| 244 |
|
| 245 |
export interface ApiResponse<T> {
|
|
|
|
| 240 |
history: ASI1Message[];
|
| 241 |
}
|
| 242 |
|
| 243 |
+
export interface WSExecuteRequest {
|
| 244 |
+
language: string;
|
| 245 |
+
content: string;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
// ===== API Response Wrappers =====
|
| 249 |
|
| 250 |
export interface ApiResponse<T> {
|
packages/shared/src/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { EXTENSION_MAP, EXCLUDED_DIRECTORIES, FRONTEND_EXTENSIONS } from './constants';
|
| 2 |
|
| 3 |
/**
|
| 4 |
* Estimate token count from text (~4 chars per token heuristic).
|
|
|
|
| 1 |
+
import { EXTENSION_MAP, EXCLUDED_DIRECTORIES, FRONTEND_EXTENSIONS } from './constants.js';
|
| 2 |
|
| 3 |
/**
|
| 4 |
* Estimate token count from text (~4 chars per token heuristic).
|