E5K7 commited on
Commit
9a3a32e
·
1 Parent(s): 965bbbc

feat: Python and Java support with URL routing and HF Spaces Dockerfile

Browse files
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={<IDEShell />} />
 
 
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 (initialized && !hasAutoOpened) {
23
- if (Object.keys(openFiles).length === 0 && files['index.html']) {
24
- addFile('index.html', files['index.html'], 'html');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <TopBar layout={layout} onLayoutChange={setLayout} />
41
- <PanelLayout layout={layout} />
42
- <StatusBar />
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' && <LivePreview />}
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
- <LivePreview />
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.get(f);
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
- if (times > 5) return null;
16
- return Math.min(times * 200, 2000);
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 type { WSCompletionRequest, WSReviewRequest, WSChatMessage, AgentType } from '@asipilot/shared';
 
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
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
 
7
  "exports": {
8
- ".": "./src/index.ts"
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).