treesap 0.1.13 ā 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -192
- package/dist/app.d.ts +28 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +184 -0
- package/dist/app.js.map +1 -0
- package/dist/context.d.ts +36 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +95 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -9
- package/dist/index.js.map +1 -1
- package/dist/middleware/cors.d.ts +11 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +34 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/serve-static.d.ts +6 -0
- package/dist/middleware/serve-static.d.ts.map +1 -0
- package/dist/middleware/serve-static.js +68 -0
- package/dist/middleware/serve-static.js.map +1 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +52 -0
- package/dist/node.js.map +1 -0
- package/dist/path.d.ts +10 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +45 -0
- package/dist/path.js.map +1 -0
- package/dist/vite.d.ts +31 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +278 -0
- package/dist/vite.js.map +1 -0
- package/package.json +33 -40
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -137
- package/dist/cli.js.map +0 -1
- package/dist/components/BaseHead.d.ts +0 -5
- package/dist/components/BaseHead.d.ts.map +0 -1
- package/dist/components/BaseHead.js +0 -161
- package/dist/components/BaseHead.js.map +0 -1
- package/dist/components/ChatInput.d.ts +0 -7
- package/dist/components/ChatInput.d.ts.map +0 -1
- package/dist/components/ChatInput.js +0 -11
- package/dist/components/ChatInput.js.map +0 -1
- package/dist/components/Sidebar.d.ts +0 -8
- package/dist/components/Sidebar.d.ts.map +0 -1
- package/dist/components/Sidebar.js +0 -7
- package/dist/components/Sidebar.js.map +0 -1
- package/dist/components/SimpleLivePreview.d.ts +0 -7
- package/dist/components/SimpleLivePreview.d.ts.map +0 -1
- package/dist/components/SimpleLivePreview.js +0 -7
- package/dist/components/SimpleLivePreview.js.map +0 -1
- package/dist/components/Terminal.d.ts +0 -7
- package/dist/components/Terminal.d.ts.map +0 -1
- package/dist/components/Terminal.js +0 -14
- package/dist/components/Terminal.js.map +0 -1
- package/dist/components/VoiceRecorder.d.ts +0 -4
- package/dist/components/VoiceRecorder.d.ts.map +0 -1
- package/dist/components/VoiceRecorder.js +0 -5
- package/dist/components/VoiceRecorder.js.map +0 -1
- package/dist/components/icons/GeminiLogo.d.ts +0 -7
- package/dist/components/icons/GeminiLogo.d.ts.map +0 -1
- package/dist/components/icons/GeminiLogo.js +0 -5
- package/dist/components/icons/GeminiLogo.js.map +0 -1
- package/dist/components/icons/OllamaLogo.d.ts +0 -2
- package/dist/components/icons/OllamaLogo.d.ts.map +0 -1
- package/dist/components/icons/OllamaLogo.js +0 -5
- package/dist/components/icons/OllamaLogo.js.map +0 -1
- package/dist/layouts/Layout.d.ts +0 -9
- package/dist/layouts/Layout.d.ts.map +0 -1
- package/dist/layouts/Layout.js +0 -9
- package/dist/layouts/Layout.js.map +0 -1
- package/dist/layouts/NotFoundLayout.d.ts +0 -2
- package/dist/layouts/NotFoundLayout.d.ts.map +0 -1
- package/dist/layouts/NotFoundLayout.js +0 -6
- package/dist/layouts/NotFoundLayout.js.map +0 -1
- package/dist/pages/Code.d.ts +0 -7
- package/dist/pages/Code.d.ts.map +0 -1
- package/dist/pages/Code.js +0 -8
- package/dist/pages/Code.js.map +0 -1
- package/dist/pages/Home.d.ts +0 -7
- package/dist/pages/Home.d.ts.map +0 -1
- package/dist/pages/Home.js +0 -8
- package/dist/pages/Home.js.map +0 -1
- package/dist/pages/Welcome.d.ts +0 -2
- package/dist/pages/Welcome.d.ts.map +0 -1
- package/dist/pages/Welcome.js +0 -6
- package/dist/pages/Welcome.js.map +0 -1
- package/dist/server.d.ts +0 -11
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -434
- package/dist/server.js.map +0 -1
- package/dist/services/dev-server.d.ts +0 -29
- package/dist/services/dev-server.d.ts.map +0 -1
- package/dist/services/dev-server.js +0 -201
- package/dist/services/dev-server.js.map +0 -1
- package/dist/services/terminal.d.ts +0 -46
- package/dist/services/terminal.d.ts.map +0 -1
- package/dist/services/terminal.js +0 -264
- package/dist/services/terminal.js.map +0 -1
- package/dist/services/websocket.d.ts +0 -48
- package/dist/services/websocket.d.ts.map +0 -1
- package/dist/services/websocket.js +0 -332
- package/dist/services/websocket.js.map +0 -1
- package/dist/static/components/ChatInput.js +0 -237
- package/dist/static/components/Sidebar.js +0 -225
- package/dist/static/components/SimpleLivePreview.js +0 -305
- package/dist/static/components/Terminal.js +0 -461
- package/dist/static/components/TerminalTabs.js +0 -383
- package/dist/static/favicon.svg +0 -14
- package/dist/static/signals/LivePreviewSignal.js +0 -71
- package/dist/static/signals/SidebarSignal.js +0 -123
- package/dist/static/signals/TerminalSignal.js +0 -273
- package/dist/static/styles/main.css +0 -1761
- package/src/cli.ts +0 -155
- package/src/components/BaseHead.ts +0 -164
- package/src/components/ChatInput.tsx +0 -56
- package/src/components/Sidebar.tsx +0 -99
- package/src/components/SimpleLivePreview.tsx +0 -40
- package/src/components/Terminal.tsx +0 -40
- package/src/components/VoiceRecorder.tsx +0 -33
- package/src/components/icons/GeminiLogo.tsx +0 -10
- package/src/components/icons/OllamaLogo.tsx +0 -5
- package/src/index.tsx +0 -12
- package/src/layouts/Layout.tsx +0 -41
- package/src/layouts/NotFoundLayout.tsx +0 -15
- package/src/pages/Code.tsx +0 -34
- package/src/pages/Welcome.tsx +0 -56
- package/src/server.tsx +0 -519
- package/src/services/dev-server.ts +0 -234
- package/src/services/terminal.ts +0 -325
- package/src/services/websocket.ts +0 -405
- package/src/static/components/ChatInput.js +0 -237
- package/src/static/components/Sidebar.js +0 -225
- package/src/static/components/SimpleLivePreview.js +0 -305
- package/src/static/components/Terminal.js +0 -461
- package/src/static/components/TerminalTabs.js +0 -383
- package/src/static/favicon.svg +0 -14
- package/src/static/signals/LivePreviewSignal.js +0 -71
- package/src/static/signals/SidebarSignal.js +0 -123
- package/src/static/signals/TerminalSignal.js +0 -273
- package/src/static/styles/main.css +0 -1761
- package/src/styles/input.css +0 -3
- package/tailwind.config.ts +0 -22
- package/tsconfig.json +0 -37
package/src/layouts/Layout.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
interface BaseLayoutProps {
|
|
2
|
-
title?: string;
|
|
3
|
-
description?: string;
|
|
4
|
-
head?: any;
|
|
5
|
-
children: any;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function BaseLayout(props: BaseLayoutProps) {
|
|
9
|
-
return (
|
|
10
|
-
<>
|
|
11
|
-
<div dangerouslySetInnerHTML={{ __html: '<!DOCTYPE html>' }} />
|
|
12
|
-
<html lang="en">
|
|
13
|
-
<head>
|
|
14
|
-
<title>{props.title || "Treesap"}</title>
|
|
15
|
-
<meta name="description" content={props.description || "A modern web application"} />
|
|
16
|
-
<meta charset="UTF-8" />
|
|
17
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
18
|
-
<link rel="stylesheet" href="/styles/main.css" />
|
|
19
|
-
{/* Sapling Islands */}
|
|
20
|
-
<script type="module" src="https://sapling-is.land"></script>
|
|
21
|
-
<style
|
|
22
|
-
dangerouslySetInnerHTML={{
|
|
23
|
-
__html: `
|
|
24
|
-
sapling-island{display:contents}
|
|
25
|
-
`,
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
<script
|
|
29
|
-
src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"
|
|
30
|
-
defer
|
|
31
|
-
>
|
|
32
|
-
</script>
|
|
33
|
-
{props.head}
|
|
34
|
-
</head>
|
|
35
|
-
<body class="bg-white text-black">
|
|
36
|
-
{props.children}
|
|
37
|
-
</body>
|
|
38
|
-
</html>
|
|
39
|
-
</>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import Layout from "./Layout.js";
|
|
2
|
-
|
|
3
|
-
export default async function NotFoundLayout() {
|
|
4
|
-
return (
|
|
5
|
-
<Layout>
|
|
6
|
-
<main
|
|
7
|
-
class="flex-1 flex flex-col justify-center items-center min-h-screen"
|
|
8
|
-
>
|
|
9
|
-
<h1 class="text-4xl font-bold font-heading leading-tight mb-8">
|
|
10
|
-
Page not found
|
|
11
|
-
</h1>
|
|
12
|
-
</main>
|
|
13
|
-
</Layout>
|
|
14
|
-
);
|
|
15
|
-
}
|
package/src/pages/Code.tsx
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import Layout from "../layouts/Layout.js";
|
|
2
|
-
import { Sidebar } from "../components/Sidebar.js";
|
|
3
|
-
import { SimpleLivePreview } from "../components/SimpleLivePreview.js";
|
|
4
|
-
|
|
5
|
-
interface TerminalProps {
|
|
6
|
-
previewPort?: number;
|
|
7
|
-
workingDirectory?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function Code({ previewPort = 1234, workingDirectory }: TerminalProps) {
|
|
11
|
-
return (
|
|
12
|
-
<Layout title="Code Editor">
|
|
13
|
-
<div id="code-container" class="h-screen flex bg-[#1e1e1e] relative">
|
|
14
|
-
{/* Mobile toggle button */}
|
|
15
|
-
<button
|
|
16
|
-
type="button"
|
|
17
|
-
id="mobile-sidebar-toggle"
|
|
18
|
-
class="fixed top-4 left-4 z-60 p-3 bg-[#2d2d30] border border-[#3c3c3c] rounded-lg shadow-xl hover:bg-[#3c3c3c] transition-all md:hidden"
|
|
19
|
-
title="Toggle Sidebar"
|
|
20
|
-
>
|
|
21
|
-
<iconify-icon icon="tabler:menu-2" width="20" height="20" class="text-[#cccccc]"></iconify-icon>
|
|
22
|
-
</button>
|
|
23
|
-
|
|
24
|
-
{/* Sidebar */}
|
|
25
|
-
<Sidebar id="sidebar" previewPort={previewPort} workingDirectory={workingDirectory} />
|
|
26
|
-
|
|
27
|
-
{/* Main Content - Live Preview */}
|
|
28
|
-
<div class="flex-1 md:flex-1">
|
|
29
|
-
<SimpleLivePreview id="live-preview" previewPort={previewPort} />
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</Layout>
|
|
33
|
-
);
|
|
34
|
-
}
|
package/src/pages/Welcome.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import Layout from "../layouts/Layout.js";
|
|
2
|
-
|
|
3
|
-
export function Welcome() {
|
|
4
|
-
return (
|
|
5
|
-
<Layout title="Welcome to Treesap">
|
|
6
|
-
<div class="h-screen flex flex-col items-center justify-center bg-[#1e1e1e] text-white p-8">
|
|
7
|
-
{/* Header with logo and title */}
|
|
8
|
-
<div class="text-center mb-12">
|
|
9
|
-
<div class="flex items-center justify-center mb-4">
|
|
10
|
-
<div class="w-12 h-12 bg-gradient-to-br from-green-400 to-green-600 rounded-lg flex items-center justify-center mr-4">
|
|
11
|
-
<span class="text-2xl">š³</span>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="text-left">
|
|
14
|
-
<h1 class="text-3xl font-semibold text-white">Treesap</h1>
|
|
15
|
-
<p class="text-gray-400 text-sm">Free ⢠Development Environment</p>
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
{/* Main action buttons */}
|
|
21
|
-
<div class="flex gap-8 mb-16">
|
|
22
|
-
<a
|
|
23
|
-
href="/code"
|
|
24
|
-
class="flex flex-col items-center p-6 bg-[#2a2a2a] hover:bg-[#3a3a3a] rounded-lg transition-colors group min-w-[140px]"
|
|
25
|
-
>
|
|
26
|
-
<div class="w-8 h-8 mb-3 text-gray-300 group-hover:text-white transition-colors">
|
|
27
|
-
<iconify-icon icon="tabler:folder-open" width="32" height="32"></iconify-icon>
|
|
28
|
-
</div>
|
|
29
|
-
<span class="text-gray-300 group-hover:text-white transition-colors">Open project</span>
|
|
30
|
-
</a>
|
|
31
|
-
|
|
32
|
-
<button
|
|
33
|
-
class="flex flex-col items-center p-6 bg-[#2a2a2a] hover:bg-[#3a3a3a] rounded-lg transition-colors group min-w-[140px] opacity-50 cursor-not-allowed"
|
|
34
|
-
disabled
|
|
35
|
-
>
|
|
36
|
-
<div class="w-8 h-8 mb-3 text-gray-500">
|
|
37
|
-
<iconify-icon icon="tabler:git-branch" width="32" height="32"></iconify-icon>
|
|
38
|
-
</div>
|
|
39
|
-
<span class="text-gray-500">Clone repo</span>
|
|
40
|
-
</button>
|
|
41
|
-
|
|
42
|
-
<button
|
|
43
|
-
class="flex flex-col items-center p-6 bg-[#2a2a2a] hover:bg-[#3a3a3a] rounded-lg transition-colors group min-w-[140px] opacity-50 cursor-not-allowed"
|
|
44
|
-
disabled
|
|
45
|
-
>
|
|
46
|
-
<div class="w-8 h-8 mb-3 text-gray-500">
|
|
47
|
-
<iconify-icon icon="tabler:terminal" width="32" height="32"></iconify-icon>
|
|
48
|
-
</div>
|
|
49
|
-
<span class="text-gray-500">Connect via SSH</span>
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
</div>
|
|
54
|
-
</Layout>
|
|
55
|
-
);
|
|
56
|
-
}
|
package/src/server.tsx
DELETED
|
@@ -1,519 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Hono, type Context } from "hono";
|
|
3
|
-
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4
|
-
import NotFoundLayout from "./layouts/NotFoundLayout.js";
|
|
5
|
-
import { Welcome } from "./pages/Welcome.js";
|
|
6
|
-
import { Code } from "./pages/Code.js";
|
|
7
|
-
import { DevServerManager } from "./services/dev-server.js";
|
|
8
|
-
import { TerminalService } from "./services/terminal.js";
|
|
9
|
-
import { WebSocketTerminalService } from "./services/websocket.js";
|
|
10
|
-
import * as path from 'node:path';
|
|
11
|
-
import process from "node:process";
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import type { Server } from 'http';
|
|
14
|
-
|
|
15
|
-
export interface TreesapConfig {
|
|
16
|
-
port?: number;
|
|
17
|
-
projectRoot?: string;
|
|
18
|
-
previewPort?: number;
|
|
19
|
-
devCommand?: string;
|
|
20
|
-
devPort?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function startServer(config: TreesapConfig & { autoStartDev?: boolean } = {}) {
|
|
24
|
-
const {
|
|
25
|
-
port = 1234,
|
|
26
|
-
projectRoot = process.cwd(),
|
|
27
|
-
previewPort = 5173,
|
|
28
|
-
devCommand,
|
|
29
|
-
devPort,
|
|
30
|
-
autoStartDev = false
|
|
31
|
-
} = config;
|
|
32
|
-
|
|
33
|
-
// Change the current working directory to the project root
|
|
34
|
-
process.chdir(projectRoot);
|
|
35
|
-
|
|
36
|
-
// Initialize dev server manager if dev command is provided
|
|
37
|
-
let devServerManager: DevServerManager | null = null;
|
|
38
|
-
if (devCommand) {
|
|
39
|
-
devServerManager = new DevServerManager(devCommand, devPort);
|
|
40
|
-
devServerManager.setupGracefulShutdown();
|
|
41
|
-
|
|
42
|
-
// Auto-start dev server if enabled
|
|
43
|
-
if (autoStartDev) {
|
|
44
|
-
devServerManager.start().catch(error => {
|
|
45
|
-
console.error("Failed to auto-start dev server:", error);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const app = new Hono();
|
|
51
|
-
|
|
52
|
-
// Resolve module static directory (so consumers can see bundled assets)
|
|
53
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
-
const moduleStaticRoot = path.join(moduleDir, 'static');
|
|
55
|
-
|
|
56
|
-
// Welcome page (default)
|
|
57
|
-
app.get("/", (c: Context) => {
|
|
58
|
-
// Additional cache prevention headers
|
|
59
|
-
c.header('Cache-Control', 'no-cache, no-store, must-revalidate, private');
|
|
60
|
-
c.header('Pragma', 'no-cache');
|
|
61
|
-
c.header('Expires', '0');
|
|
62
|
-
return c.html(<Welcome />);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Code page
|
|
66
|
-
app.get("/code", (c: Context) => {
|
|
67
|
-
// Additional cache prevention headers
|
|
68
|
-
c.header('Cache-Control', 'no-cache, no-store, must-revalidate, private');
|
|
69
|
-
c.header('Pragma', 'no-cache');
|
|
70
|
-
c.header('Expires', '0');
|
|
71
|
-
return c.html(<Code previewPort={previewPort} />);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Claude page
|
|
75
|
-
app.get("/claude", (c: Context) => {
|
|
76
|
-
// Additional cache prevention headers
|
|
77
|
-
c.header('Cache-Control', 'no-cache, no-store, must-revalidate, private');
|
|
78
|
-
c.header('Pragma', 'no-cache');
|
|
79
|
-
c.header('Expires', '0');
|
|
80
|
-
return c.html(<Code previewPort={previewPort} workingDirectory={projectRoot} />);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Dev server management endpoints
|
|
85
|
-
app.get("/api/dev-status", (c: Context) => {
|
|
86
|
-
if (!devServerManager) {
|
|
87
|
-
return c.json({ error: "No dev server configured" }, 400);
|
|
88
|
-
}
|
|
89
|
-
return c.json(devServerManager.getStatus());
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
app.post("/api/dev-restart", async (c: Context) => {
|
|
93
|
-
if (!devServerManager) {
|
|
94
|
-
return c.json({ error: "No dev server configured" }, 400);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
await devServerManager.restart();
|
|
99
|
-
return c.json({ success: true, message: "Dev server restarted" });
|
|
100
|
-
} catch (error) {
|
|
101
|
-
return c.json({
|
|
102
|
-
error: "Failed to restart dev server",
|
|
103
|
-
details: error instanceof Error ? error.message : String(error)
|
|
104
|
-
}, 500);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
app.post("/api/dev-start", async (c: Context) => {
|
|
109
|
-
if (!devServerManager) {
|
|
110
|
-
return c.json({ error: "No dev server configured" }, 400);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await devServerManager.start();
|
|
115
|
-
return c.json({ success: true, message: "Dev server started" });
|
|
116
|
-
} catch (error) {
|
|
117
|
-
return c.json({
|
|
118
|
-
error: "Failed to start dev server",
|
|
119
|
-
details: error instanceof Error ? error.message : String(error)
|
|
120
|
-
}, 500);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
app.post("/api/dev-stop", async (c: Context) => {
|
|
125
|
-
if (!devServerManager) {
|
|
126
|
-
return c.json({ error: "No dev server configured" }, 400);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await devServerManager.stop();
|
|
131
|
-
return c.json({ success: true, message: "Dev server stopped" });
|
|
132
|
-
} catch (error) {
|
|
133
|
-
return c.json({
|
|
134
|
-
error: "Failed to stop dev server",
|
|
135
|
-
details: error instanceof Error ? error.message : String(error)
|
|
136
|
-
}, 500);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
app.get("/api/dev-logs", (c: Context) => {
|
|
141
|
-
if (!devServerManager) {
|
|
142
|
-
return c.json({ error: "No dev server configured" }, 400);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const logs = devServerManager.getLogs();
|
|
146
|
-
return c.json({ logs });
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// List active terminal sessions with WebSocket client info
|
|
150
|
-
app.get("/api/terminal/sessions", (c: Context) => {
|
|
151
|
-
const sessions = TerminalService.getAllSessions();
|
|
152
|
-
const wsActiveSessions = WebSocketTerminalService.getActiveSessions();
|
|
153
|
-
|
|
154
|
-
return c.json({
|
|
155
|
-
sessions: sessions.map(session => {
|
|
156
|
-
const wsInfo = wsActiveSessions.find(ws => ws.sessionId === session.id);
|
|
157
|
-
return {
|
|
158
|
-
id: session.id,
|
|
159
|
-
createdAt: session.createdAt,
|
|
160
|
-
lastActivity: session.lastActivity,
|
|
161
|
-
connectedClients: wsInfo ? wsInfo.clientCount : 0
|
|
162
|
-
};
|
|
163
|
-
}),
|
|
164
|
-
totalConnectedClients: WebSocketTerminalService.getConnectedClients()
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// Get specific terminal session status
|
|
169
|
-
app.get("/api/terminal/sessions/:sessionId/status", (c: Context) => {
|
|
170
|
-
const sessionId = c.req.param('sessionId');
|
|
171
|
-
const session = TerminalService.getSession(sessionId);
|
|
172
|
-
|
|
173
|
-
if (!session) {
|
|
174
|
-
return c.json({ error: "Session not found" }, 404);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const clients = WebSocketTerminalService.getSessionClients(sessionId);
|
|
178
|
-
|
|
179
|
-
return c.json({
|
|
180
|
-
id: session.id,
|
|
181
|
-
createdAt: session.createdAt,
|
|
182
|
-
lastActivity: session.lastActivity,
|
|
183
|
-
connectedClients: clients.length,
|
|
184
|
-
clientIds: clients
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// Send command to terminal via API
|
|
189
|
-
app.post("/api/terminal/sessions/:sessionId/command", async (c: Context) => {
|
|
190
|
-
const sessionId = c.req.param('sessionId');
|
|
191
|
-
const body = await c.req.json();
|
|
192
|
-
const { command } = body;
|
|
193
|
-
|
|
194
|
-
if (!command) {
|
|
195
|
-
return c.json({ error: "Command is required" }, 400);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Get or create terminal session
|
|
199
|
-
let session = TerminalService.getSession(sessionId);
|
|
200
|
-
if (!session) {
|
|
201
|
-
session = TerminalService.createSession(sessionId);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const success = WebSocketTerminalService.sendCommandToSession(sessionId, command);
|
|
205
|
-
|
|
206
|
-
if (success) {
|
|
207
|
-
return c.json({
|
|
208
|
-
success: true,
|
|
209
|
-
message: `Command sent to session ${sessionId}`,
|
|
210
|
-
connectedClients: WebSocketTerminalService.getSessionClients(sessionId).length
|
|
211
|
-
});
|
|
212
|
-
} else {
|
|
213
|
-
return c.json({ error: "Failed to send command to terminal" }, 500);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Get recent output from terminal session (for API clients)
|
|
218
|
-
app.get("/api/terminal/sessions/:sessionId/output", async (c: Context) => {
|
|
219
|
-
const sessionId = c.req.param('sessionId');
|
|
220
|
-
const session = TerminalService.getSession(sessionId);
|
|
221
|
-
|
|
222
|
-
if (!session) {
|
|
223
|
-
return c.json({ error: "Session not found" }, 404);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Note: This is a basic implementation. For production, you'd want to
|
|
227
|
-
// store recent output history in the TerminalService
|
|
228
|
-
return c.json({
|
|
229
|
-
sessionId,
|
|
230
|
-
message: "Output streaming available via WebSocket connection",
|
|
231
|
-
connectedClients: WebSocketTerminalService.getSessionClients(sessionId).length,
|
|
232
|
-
websocketUrl: `ws://${c.req.header('host')}/terminal/ws`
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// Terminal endpoints
|
|
237
|
-
app.post("/terminal/execute/:sessionId", async (c: Context) => {
|
|
238
|
-
const sessionId = c.req.param('sessionId');
|
|
239
|
-
const body = await c.req.json();
|
|
240
|
-
const { command } = body;
|
|
241
|
-
|
|
242
|
-
if (!command) {
|
|
243
|
-
return c.json({ error: "Command is required" }, 400);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Get or create terminal session
|
|
247
|
-
let session = TerminalService.getSession(sessionId);
|
|
248
|
-
if (!session) {
|
|
249
|
-
session = TerminalService.createSession(sessionId);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const success = TerminalService.executeCommand(sessionId, command);
|
|
253
|
-
|
|
254
|
-
if (success) {
|
|
255
|
-
return c.json({ success: true });
|
|
256
|
-
} else {
|
|
257
|
-
return c.json({ error: "Failed to execute command" }, 500);
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
app.post("/terminal/input/:sessionId", async (c: Context) => {
|
|
262
|
-
const sessionId = c.req.param('sessionId');
|
|
263
|
-
const body = await c.req.json();
|
|
264
|
-
const { input } = body;
|
|
265
|
-
|
|
266
|
-
if (input === undefined) {
|
|
267
|
-
return c.json({ error: "Input is required" }, 400);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Get or create terminal session
|
|
271
|
-
let session = TerminalService.getSession(sessionId);
|
|
272
|
-
if (!session) {
|
|
273
|
-
session = TerminalService.createSession(sessionId);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Send input directly to PTY
|
|
277
|
-
try {
|
|
278
|
-
session.lastActivity = new Date();
|
|
279
|
-
session.process.write(input);
|
|
280
|
-
return c.json({ success: true });
|
|
281
|
-
} catch (error) {
|
|
282
|
-
console.error(`Error sending input to session ${sessionId}:`, error);
|
|
283
|
-
return c.json({ error: "Failed to send input" }, 500);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
app.get("/terminal/stream/:sessionId", async (c: Context) => {
|
|
288
|
-
const sessionId = c.req.param('sessionId');
|
|
289
|
-
|
|
290
|
-
// Get or create terminal session
|
|
291
|
-
let session = TerminalService.getSession(sessionId);
|
|
292
|
-
if (!session) {
|
|
293
|
-
session = TerminalService.createSession(sessionId);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Set SSE headers
|
|
297
|
-
c.header('Content-Type', 'text/event-stream');
|
|
298
|
-
c.header('Cache-Control', 'no-cache');
|
|
299
|
-
c.header('Connection', 'keep-alive');
|
|
300
|
-
c.header('Access-Control-Allow-Origin', '*');
|
|
301
|
-
|
|
302
|
-
const stream = new ReadableStream({
|
|
303
|
-
start(controller) {
|
|
304
|
-
const encoder = new TextEncoder();
|
|
305
|
-
|
|
306
|
-
// Send initial connection event
|
|
307
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`));
|
|
308
|
-
|
|
309
|
-
// Set up output listener
|
|
310
|
-
const handleOutput = (data: any) => {
|
|
311
|
-
try {
|
|
312
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
313
|
-
} catch (error) {
|
|
314
|
-
console.error('Error sending terminal output:', error);
|
|
315
|
-
// Remove listener on error to prevent memory leaks
|
|
316
|
-
session!.eventEmitter.removeListener('output', handleOutput);
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
session!.eventEmitter.on('output', handleOutput);
|
|
321
|
-
|
|
322
|
-
// Handle client disconnect
|
|
323
|
-
const handleDisconnect = () => {
|
|
324
|
-
try {
|
|
325
|
-
session!.eventEmitter.removeListener('output', handleOutput);
|
|
326
|
-
} catch (error) {
|
|
327
|
-
console.error('Error removing output listener:', error);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
// Clean up on stream close
|
|
332
|
-
return () => {
|
|
333
|
-
handleDisconnect();
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
return new Response(stream, {
|
|
339
|
-
headers: {
|
|
340
|
-
'Content-Type': 'text/event-stream',
|
|
341
|
-
'Cache-Control': 'no-cache',
|
|
342
|
-
'Connection': 'keep-alive',
|
|
343
|
-
'Access-Control-Allow-Origin': '*'
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Delete terminal session
|
|
349
|
-
app.delete("/terminal/session/:sessionId", async (c: Context) => {
|
|
350
|
-
const sessionId = c.req.param('sessionId');
|
|
351
|
-
|
|
352
|
-
if (!sessionId) {
|
|
353
|
-
return c.json({ error: "Session ID is required" }, 400);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const success = TerminalService.destroySession(sessionId);
|
|
357
|
-
|
|
358
|
-
if (success) {
|
|
359
|
-
|
|
360
|
-
return c.json({ message: `Terminal session ${sessionId} destroyed successfully` });
|
|
361
|
-
} else {
|
|
362
|
-
console.log(`Terminal session not found: ${sessionId}`);
|
|
363
|
-
return c.json({ error: `Terminal session ${sessionId} not found` }, 404);
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Claude Code subprocess management
|
|
368
|
-
let claudeCodeManager: DevServerManager | null = null;
|
|
369
|
-
|
|
370
|
-
app.post("/api/claude-start", async (c: Context) => {
|
|
371
|
-
try {
|
|
372
|
-
if (claudeCodeManager && claudeCodeManager.getStatus().running) {
|
|
373
|
-
return c.json({ error: "Claude Code is already running" }, 400);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
claudeCodeManager = new DevServerManager("claude");
|
|
377
|
-
claudeCodeManager.setupGracefulShutdown();
|
|
378
|
-
await claudeCodeManager.start();
|
|
379
|
-
|
|
380
|
-
return c.json({ success: true, message: "Claude Code started" });
|
|
381
|
-
} catch (error) {
|
|
382
|
-
return c.json({
|
|
383
|
-
error: "Failed to start Claude Code",
|
|
384
|
-
details: error instanceof Error ? error.message : String(error)
|
|
385
|
-
}, 500);
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
app.get("/api/claude-status", (c: Context) => {
|
|
390
|
-
if (!claudeCodeManager) {
|
|
391
|
-
return c.json({ running: false });
|
|
392
|
-
}
|
|
393
|
-
return c.json(claudeCodeManager.getStatus());
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
app.post("/api/claude-stop", async (c: Context) => {
|
|
397
|
-
if (!claudeCodeManager) {
|
|
398
|
-
return c.json({ error: "No Claude Code process running" }, 400);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
await claudeCodeManager.stop();
|
|
403
|
-
claudeCodeManager = null;
|
|
404
|
-
return c.json({ success: true, message: "Claude Code stopped" });
|
|
405
|
-
} catch (error) {
|
|
406
|
-
return c.json({
|
|
407
|
-
error: "Failed to stop Claude Code",
|
|
408
|
-
details: error instanceof Error ? error.message : String(error)
|
|
409
|
-
}, 500);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
app.post("/api/claude-send", async (c: Context) => {
|
|
414
|
-
if (!claudeCodeManager) {
|
|
415
|
-
return c.json({ error: "Claude Code process not running" }, 400);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const body = await c.req.json();
|
|
420
|
-
const { command } = body;
|
|
421
|
-
|
|
422
|
-
if (!command) {
|
|
423
|
-
return c.json({ error: "Command is required" }, 400);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Send command to Claude Code stdin
|
|
427
|
-
const success = claudeCodeManager.sendCommand(command);
|
|
428
|
-
|
|
429
|
-
if (success) {
|
|
430
|
-
return c.json({ success: true, message: "Command sent to Claude Code" });
|
|
431
|
-
} else {
|
|
432
|
-
return c.json({ error: "Failed to send command - process may not be running" }, 500);
|
|
433
|
-
}
|
|
434
|
-
} catch (error) {
|
|
435
|
-
return c.json({
|
|
436
|
-
error: "Failed to send command to Claude Code",
|
|
437
|
-
details: error instanceof Error ? error.message : String(error)
|
|
438
|
-
}, 500);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Serve static files from the module's bundled static directory
|
|
443
|
-
app.get("*", serveStatic({ root: moduleStaticRoot }));
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// 404 Handler
|
|
447
|
-
app.notFound((c: Context) => c.html(<NotFoundLayout />));
|
|
448
|
-
|
|
449
|
-
const { serve } = await import('@hono/node-server');
|
|
450
|
-
|
|
451
|
-
// Start the server and initialize WebSocket
|
|
452
|
-
const server = serve({
|
|
453
|
-
fetch: app.fetch,
|
|
454
|
-
port,
|
|
455
|
-
}) as Server;
|
|
456
|
-
|
|
457
|
-
// Initialize WebSocket service
|
|
458
|
-
WebSocketTerminalService.initialize(server);
|
|
459
|
-
|
|
460
|
-
// Setup global graceful shutdown for all subprocess managers
|
|
461
|
-
const setupGlobalShutdown = () => {
|
|
462
|
-
const cleanup = async () => {
|
|
463
|
-
console.log('\nš Shutting down server and all subprocesses...');
|
|
464
|
-
|
|
465
|
-
// Clean up WebSocket connections
|
|
466
|
-
WebSocketTerminalService.cleanup();
|
|
467
|
-
|
|
468
|
-
// Stop Claude Code process if running
|
|
469
|
-
if (claudeCodeManager) {
|
|
470
|
-
try {
|
|
471
|
-
await claudeCodeManager.stop();
|
|
472
|
-
claudeCodeManager = null;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
console.error('Error stopping Claude Code process:', error);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Stop dev server if running
|
|
479
|
-
if (devServerManager) {
|
|
480
|
-
try {
|
|
481
|
-
await devServerManager.stop();
|
|
482
|
-
} catch (error) {
|
|
483
|
-
console.error('Error stopping dev server:', error);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
process.exit(0);
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
// Handle Ctrl+C (SIGINT)
|
|
491
|
-
process.on('SIGINT', () => {
|
|
492
|
-
cleanup().catch(() => process.exit(1));
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Handle termination signal
|
|
496
|
-
process.on('SIGTERM', () => {
|
|
497
|
-
cleanup().catch(() => process.exit(1));
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// Handle process exit (synchronous cleanup only)
|
|
501
|
-
process.on('exit', () => {
|
|
502
|
-
if (claudeCodeManager) {
|
|
503
|
-
try {
|
|
504
|
-
claudeCodeManager.stop();
|
|
505
|
-
} catch {
|
|
506
|
-
// Silently handle cleanup errors during exit
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
setupGlobalShutdown();
|
|
513
|
-
|
|
514
|
-
// Initialize terminal service cleanup
|
|
515
|
-
TerminalService.setupGlobalCleanup();
|
|
516
|
-
|
|
517
|
-
console.log(`\nš³ Treesap server running at http://localhost:${port}`);
|
|
518
|
-
console.log(`š WebSocket terminal server available at ws://localhost:${port}/terminal/ws\n`);
|
|
519
|
-
}
|