mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
modified: server/routes.ts
This commit is contained in:
parent
773cc74c33
commit
f97122135d
2 changed files with 131 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ import { useLabTerminal } from "@/hooks/use-lab-terminal";
|
|||
import { ArrowLeft, Terminal as TerminalIcon, Copy, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
|
||||
interface TerminalLine {
|
||||
type: 'input' | 'output' | 'error' | 'system';
|
||||
|
|
@ -21,6 +22,18 @@ export default function Terminal() {
|
|||
},
|
||||
]);
|
||||
|
||||
const [cliCommand, setCliCommand] = useState<string>("build");
|
||||
const [cliStatus, setCliStatus] = useState<"idle" | "running" | "done" | "error">("idle");
|
||||
const [cliLabel, setCliLabel] = useState<string>("");
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const currentRunId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
eventSourceRef.current?.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||
|
|
@ -154,6 +167,74 @@ export default function Terminal() {
|
|||
setHistoryIndex(-1);
|
||||
};
|
||||
|
||||
const appendCliLine = (type: TerminalLine['type'], content: string) => {
|
||||
setLines((prev) => [...prev, { type, content, timestamp: Date.now() }]);
|
||||
};
|
||||
|
||||
const stopCli = async () => {
|
||||
if (!currentRunId.current) return;
|
||||
try {
|
||||
await fetch(`/api/admin/cli/stop/${currentRunId.current}`, { method: "POST", credentials: "include" });
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
eventSourceRef.current?.close();
|
||||
eventSourceRef.current = null;
|
||||
currentRunId.current = null;
|
||||
setCliStatus("idle");
|
||||
};
|
||||
|
||||
const startCli = async () => {
|
||||
if (cliStatus === "running") return;
|
||||
setCliStatus("running");
|
||||
appendCliLine('system', `▸ Running ${cliCommand}...`);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/cli/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ command: cliCommand })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
appendCliLine('error', `Start failed: ${text || res.status}`);
|
||||
setCliStatus("error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
currentRunId.current = data.id;
|
||||
setCliLabel(data.label);
|
||||
|
||||
const es = new EventSource(`/api/admin/cli/stream/${data.id}`, { withCredentials: true } as any);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.addEventListener('message', (evt) => {
|
||||
appendCliLine('output', evt.data);
|
||||
});
|
||||
|
||||
es.addEventListener('error', (evt) => {
|
||||
appendCliLine('error', 'Stream error');
|
||||
setCliStatus("error");
|
||||
es.close();
|
||||
});
|
||||
|
||||
es.addEventListener('done', (evt) => {
|
||||
const status = evt.data === 'success' ? 'done' : 'error';
|
||||
setCliStatus(status as any);
|
||||
appendCliLine(status === 'done' ? 'system' : 'error', `▸ ${cliLabel || cliCommand} ${status}`);
|
||||
es.close();
|
||||
currentRunId.current = null;
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
appendCliLine('error', 'Failed to start CLI command');
|
||||
setCliStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
processCommand(input);
|
||||
|
|
@ -216,6 +297,43 @@ export default function Terminal() {
|
|||
</div>
|
||||
|
||||
{/* Terminal Output */}
|
||||
|
||||
{/* Admin CLI runner */}
|
||||
<div className="border-b border-[#2b2d30] bg-[#121216] px-4 py-3 flex flex-wrap items-center gap-3 text-sm">
|
||||
<div className="text-white/80 font-semibold">AeThex CLI</div>
|
||||
<Select value={cliCommand} onValueChange={setCliCommand}>
|
||||
<SelectTrigger className="w-48 bg-[#0f0f12] border-white/10 text-white/80">
|
||||
<SelectValue placeholder="Select command" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0f0f12] text-white/80 border-white/10">
|
||||
<SelectItem value="build">build (npm run build)</SelectItem>
|
||||
<SelectItem value="migrate-status">migrate-status (drizzle status)</SelectItem>
|
||||
<SelectItem value="migrate">migrate (drizzle migrate:push)</SelectItem>
|
||||
<SelectItem value="seed">seed (ts-node script/seed.ts)</SelectItem>
|
||||
<SelectItem value="test">test (./test-implementation.sh)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={startCli}
|
||||
disabled={cliStatus === "running"}
|
||||
className="bg-cyan-600 hover:bg-cyan-500"
|
||||
>
|
||||
{cliStatus === "running" ? "Running..." : "Run"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={stopCli}
|
||||
disabled={!currentRunId.current}
|
||||
className="border-white/10 text-white/70 hover:text-white"
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
<div className="text-xs text-white/50">
|
||||
Status: {cliStatus.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-1 bg-[#0a0a0c]">
|
||||
{lines.map((line, i) => (
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { Express, Request, Response, NextFunction } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "child_process";
|
||||
import { randomUUID } from "crypto";
|
||||
import { storage } from "./storage.js";
|
||||
import { loginSchema, signupSchema } from "../shared/schema.js";
|
||||
import { supabase } from "./supabase.js";
|
||||
|
|
@ -39,6 +41,17 @@ export async function registerRoutes(
|
|||
httpServer: Server,
|
||||
app: Express
|
||||
): Promise<Server> {
|
||||
|
||||
// ===== Admin CLI process registry =====
|
||||
const CLI_ALLOWLIST: Record<string, { cmd: string; args: string[]; label: string }> = {
|
||||
build: { cmd: "npm", args: ["run", "build"], label: "npm run build" },
|
||||
"migrate-status": { cmd: "npx", args: ["drizzle-kit", "status"], label: "drizzle status" },
|
||||
migrate: { cmd: "npx", args: ["drizzle-kit", "migrate:push"], label: "drizzle migrate" },
|
||||
seed: { cmd: "npx", args: ["ts-node", "script/seed.ts"], label: "seed" },
|
||||
test: { cmd: "bash", args: ["./test-implementation.sh"], label: "test-implementation" },
|
||||
};
|
||||
|
||||
const cliProcesses = new Map<string, { proc: ChildProcessWithoutNullStreams; status: "running" | "exited" | "error" }>();
|
||||
|
||||
// Apply capability guard to Hub and OS routes
|
||||
app.use("/api/hub/*", capabilityGuard);
|
||||
|
|
|
|||
Loading…
Reference in a new issue