fix ai asistance and make it per user friendly isolated enviroment
Browse files- .agent/memory/session.json +2 -2
- app/api/agent/route.ts +137 -11
- app/api/fs/route.ts +27 -15
- app/api/git/route.ts +35 -18
- app/api/lsp/route.ts +26 -12
- app/api/projects/route.ts +27 -33
- app/api/workspace/route.ts +18 -4
- app/api/workspace/stream/route.ts +25 -3
- auth.ts +1 -0
- components/dashboard/Dashboard.tsx +3 -3
- components/workspace/AIAssistantSidebar.tsx +385 -0
- components/workspace/IDEClient.tsx +48 -9
- lib/db/schema.ts +12 -0
- lib/docker/manager.ts +8 -4
- lib/fs/isolation.ts +50 -0
- lib/git/index.ts +29 -22
- lib/mcp/tools.ts +102 -73
- package-lock.json +152 -0
- package.json +2 -0
.agent/memory/session.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
-
"session_id": "
|
| 4 |
-
"started_at": "2026-03-
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
|
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
+
"session_id": "f5a7d65f",
|
| 4 |
+
"started_at": "2026-03-25T20:17:36.335488+05:30",
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
app/api/agent/route.ts
CHANGED
|
@@ -1,16 +1,89 @@
|
|
| 1 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import { z } from "zod";
|
| 3 |
import { getModel } from "@/lib/agents/registry";
|
| 4 |
-
import {
|
| 5 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
export async function POST(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
try {
|
| 9 |
-
const { messages, modelId, mode = "execute"
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
if (mode === "plan") {
|
| 13 |
-
// In plan mode, we don't stream immediately. We generate a structured JSON plan first.
|
| 14 |
const result = await generateObject({
|
| 15 |
model,
|
| 16 |
system: systemPrompt,
|
|
@@ -27,22 +100,75 @@ export async function POST(req: NextRequest) {
|
|
| 27 |
}),
|
| 28 |
});
|
| 29 |
|
| 30 |
-
return
|
| 31 |
-
headers: { "Content-Type": "application/json" }
|
| 32 |
-
});
|
| 33 |
}
|
| 34 |
|
| 35 |
-
// Execute mode: Standard stream-text with workspace tool calls enabled
|
| 36 |
const result = streamText({
|
| 37 |
model,
|
| 38 |
system: systemPrompt,
|
| 39 |
messages,
|
| 40 |
-
tools
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
});
|
| 42 |
|
| 43 |
return result.toTextStreamResponse();
|
| 44 |
} catch (e: unknown) {
|
|
|
|
| 45 |
const error = e instanceof Error ? e : new Error(String(e));
|
| 46 |
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
|
| 47 |
}
|
| 48 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
streamText,
|
| 3 |
+
generateObject,
|
| 4 |
+
LanguageModel,
|
| 5 |
+
AssistantModelMessage,
|
| 6 |
+
UserModelMessage,
|
| 7 |
+
SystemModelMessage,
|
| 8 |
+
ToolModelMessage
|
| 9 |
+
} from "ai";
|
| 10 |
import { z } from "zod";
|
| 11 |
import { getModel } from "@/lib/agents/registry";
|
| 12 |
+
import { createTools } from "@/lib/mcp/tools";
|
| 13 |
+
import { auth } from "@/auth";
|
| 14 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 15 |
+
import { db } from "@/lib/db";
|
| 16 |
+
import { randomUUID } from "crypto";
|
| 17 |
+
|
| 18 |
+
// Define a strict union type for core messages to avoid 'any'
|
| 19 |
+
type CoreChatMessage =
|
| 20 |
+
| UserModelMessage
|
| 21 |
+
| AssistantModelMessage
|
| 22 |
+
| SystemModelMessage
|
| 23 |
+
| ToolModelMessage;
|
| 24 |
|
| 25 |
export async function POST(req: NextRequest) {
|
| 26 |
+
const session = await auth();
|
| 27 |
+
if (!session?.user?.id) return new Response("Unauthorized", { status: 401 });
|
| 28 |
+
const userId = session.user.id;
|
| 29 |
+
|
| 30 |
try {
|
| 31 |
+
const { messages, modelId, workspaceName, mode = "execute" } = await req.json() as {
|
| 32 |
+
messages: CoreChatMessage[];
|
| 33 |
+
modelId?: string;
|
| 34 |
+
workspaceName: string;
|
| 35 |
+
mode?: "plan" | "execute";
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
if (!workspaceName) return new Response("Missing workspaceName", { status: 400 });
|
| 39 |
+
|
| 40 |
+
// Resolve workspaceId
|
| 41 |
+
const wsRes = await db.execute({
|
| 42 |
+
sql: "SELECT id FROM workspaces WHERE user_id = ? AND project_name = ?",
|
| 43 |
+
args: [userId, workspaceName]
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
if (wsRes.rows.length === 0) {
|
| 47 |
+
return new Response("Workspace not found", { status: 404 });
|
| 48 |
+
}
|
| 49 |
+
const workspaceId = wsRes.rows[0].id as string;
|
| 50 |
+
|
| 51 |
+
const model: LanguageModel = getModel(modelId as string, req);
|
| 52 |
+
const tools = createTools(userId, workspaceName);
|
| 53 |
+
|
| 54 |
+
const systemPrompt = `You are CodeVerse AI, a world-class autonomous coding agent.
|
| 55 |
+
You are embedded in a premium "AI Studio" environment.
|
| 56 |
+
Current Workspace: ${workspaceName} (Path: workspaces/${userId}/${workspaceName}/)
|
| 57 |
+
|
| 58 |
+
CORE PRINCIPLES:
|
| 59 |
+
1. PLAN BEFORE ACTION: Always explain high-level strategy before using tools.
|
| 60 |
+
2. PRECISION: Read files before editing. Ensure syntax is correct.
|
| 61 |
+
3. CONTEXT AWARENESS: You are part of an IDE. Help with refactoring, debugging, and feature development.
|
| 62 |
+
4. SAFETY: You have shell access but should remain within the workspace. Never attempt to escape the sandbox.
|
| 63 |
+
|
| 64 |
+
You have access to:
|
| 65 |
+
- read_file: View content.
|
| 66 |
+
- write_file: Create/edit files (overwrite mode).
|
| 67 |
+
- terminal_command: Run builds, tests, or scripts.
|
| 68 |
+
- search_code: Find patterns.
|
| 69 |
+
- list_files: Explore structure.
|
| 70 |
+
|
| 71 |
+
Respond in professional Markdown. Use code blocks for all technical output.`;
|
| 72 |
+
|
| 73 |
+
// Save incoming user message to history
|
| 74 |
+
const latestMessage = messages[messages.length - 1];
|
| 75 |
+
if (latestMessage && latestMessage.role === "user") {
|
| 76 |
+
const content = Array.isArray(latestMessage.content)
|
| 77 |
+
? latestMessage.content.map(p => ('text' in p ? p.text : '')).join('')
|
| 78 |
+
: latestMessage.content;
|
| 79 |
+
|
| 80 |
+
await db.execute({
|
| 81 |
+
sql: "INSERT INTO chat_history (id, user_id, workspace_id, role, content) VALUES (?, ?, ?, ?, ?)",
|
| 82 |
+
args: [randomUUID(), userId as string, workspaceId as string, latestMessage.role, content]
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
|
| 86 |
if (mode === "plan") {
|
|
|
|
| 87 |
const result = await generateObject({
|
| 88 |
model,
|
| 89 |
system: systemPrompt,
|
|
|
|
| 100 |
}),
|
| 101 |
});
|
| 102 |
|
| 103 |
+
return NextResponse.json(result.object);
|
|
|
|
|
|
|
| 104 |
}
|
| 105 |
|
|
|
|
| 106 |
const result = streamText({
|
| 107 |
model,
|
| 108 |
system: systemPrompt,
|
| 109 |
messages,
|
| 110 |
+
tools, // Assuming createTools returns compatible types now
|
| 111 |
+
onFinish: async (completion) => {
|
| 112 |
+
// Save assistant response to DB
|
| 113 |
+
try {
|
| 114 |
+
await db.execute({
|
| 115 |
+
sql: "INSERT INTO chat_history (id, user_id, workspace_id, role, content, tool_invocations) VALUES (?, ?, ?, ?, ?, ?)",
|
| 116 |
+
args: [
|
| 117 |
+
randomUUID(),
|
| 118 |
+
userId as string,
|
| 119 |
+
workspaceId as string,
|
| 120 |
+
"assistant",
|
| 121 |
+
completion.text,
|
| 122 |
+
JSON.stringify(completion.toolCalls || [])
|
| 123 |
+
]
|
| 124 |
+
});
|
| 125 |
+
} catch (dbErr) {
|
| 126 |
+
console.error("Failed to save assistant response to history:", dbErr);
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
});
|
| 130 |
|
| 131 |
return result.toTextStreamResponse();
|
| 132 |
} catch (e: unknown) {
|
| 133 |
+
console.error("[AGENT_ROUTE_ERROR]", e);
|
| 134 |
const error = e instanceof Error ? e : new Error(String(e));
|
| 135 |
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
|
| 136 |
}
|
| 137 |
}
|
| 138 |
+
|
| 139 |
+
export async function GET(req: NextRequest) {
|
| 140 |
+
const session = await auth();
|
| 141 |
+
if (!session?.user?.id) return new Response("Unauthorized", { status: 401 });
|
| 142 |
+
const userId = session.user.id;
|
| 143 |
+
|
| 144 |
+
const { searchParams } = new URL(req.url);
|
| 145 |
+
const workspaceName = searchParams.get("workspaceName");
|
| 146 |
+
if (!workspaceName) return new Response("Missing workspaceName", { status: 400 });
|
| 147 |
+
|
| 148 |
+
try {
|
| 149 |
+
const wsRes = await db.execute({
|
| 150 |
+
sql: "SELECT id FROM workspaces WHERE user_id = ? AND project_name = ?",
|
| 151 |
+
args: [userId, workspaceName]
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
if (wsRes.rows.length === 0) return NextResponse.json({ messages: [] });
|
| 155 |
+
const workspaceId = wsRes.rows[0].id as string;
|
| 156 |
+
|
| 157 |
+
const res = await db.execute({
|
| 158 |
+
sql: "SELECT role, content, tool_invocations as toolInvocations, created_at FROM chat_history WHERE user_id = ? AND workspace_id = ? ORDER BY created_at ASC",
|
| 159 |
+
args: [userId, workspaceId]
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
const messages = res.rows.map(row => ({
|
| 163 |
+
id: randomUUID(),
|
| 164 |
+
role: row.role as string,
|
| 165 |
+
content: row.content as string,
|
| 166 |
+
toolInvocations: row.toolInvocations ? JSON.parse(row.toolInvocations as string) : undefined
|
| 167 |
+
}));
|
| 168 |
+
|
| 169 |
+
return NextResponse.json({ messages });
|
| 170 |
+
} catch (e) {
|
| 171 |
+
console.error("[FETCH_HISTORY_ERROR]", e);
|
| 172 |
+
return NextResponse.json({ messages: [] });
|
| 173 |
+
}
|
| 174 |
+
}
|
app/api/fs/route.ts
CHANGED
|
@@ -1,50 +1,62 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
import { readDir, readFile, writeFile, deletePath } from "@/lib/fs";
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export async function GET(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
| 5 |
const { searchParams } = new URL(req.url);
|
| 6 |
-
const
|
| 7 |
const action = searchParams.get("action") || "list";
|
| 8 |
|
| 9 |
try {
|
|
|
|
|
|
|
| 10 |
if (action === "list") {
|
| 11 |
-
const nodes = await readDir(
|
| 12 |
return NextResponse.json(nodes);
|
| 13 |
}
|
| 14 |
if (action === "read") {
|
| 15 |
-
const content = await readFile(
|
| 16 |
return NextResponse.json({ content });
|
| 17 |
}
|
| 18 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
| 19 |
} catch (e: unknown) {
|
| 20 |
-
|
| 21 |
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
| 22 |
}
|
| 23 |
}
|
| 24 |
|
| 25 |
export async function POST(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
| 26 |
try {
|
| 27 |
-
const { path, content } = await req.json();
|
| 28 |
-
if (!
|
| 29 |
|
| 30 |
-
await
|
|
|
|
| 31 |
return NextResponse.json({ success: true });
|
| 32 |
} catch (e: unknown) {
|
| 33 |
-
|
| 34 |
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
| 35 |
}
|
| 36 |
}
|
| 37 |
|
| 38 |
export async function DELETE(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
| 39 |
const { searchParams } = new URL(req.url);
|
| 40 |
-
const
|
| 41 |
|
| 42 |
try {
|
| 43 |
-
if (!
|
| 44 |
-
await
|
|
|
|
| 45 |
return NextResponse.json({ success: true });
|
| 46 |
} catch (e: unknown) {
|
| 47 |
-
|
| 48 |
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
| 49 |
}
|
| 50 |
}
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
import { readDir, readFile, writeFile, deletePath } from "@/lib/fs";
|
| 3 |
+
import { auth } from "@/auth";
|
| 4 |
+
import { resolveSafePath } from "@/lib/fs/isolation";
|
| 5 |
|
| 6 |
export async function GET(req: NextRequest) {
|
| 7 |
+
const session = await auth();
|
| 8 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 9 |
+
|
| 10 |
const { searchParams } = new URL(req.url);
|
| 11 |
+
const subPath = searchParams.get("path") || "";
|
| 12 |
const action = searchParams.get("action") || "list";
|
| 13 |
|
| 14 |
try {
|
| 15 |
+
const fullPath = await resolveSafePath(session.user.id, subPath);
|
| 16 |
+
|
| 17 |
if (action === "list") {
|
| 18 |
+
const nodes = await readDir(fullPath);
|
| 19 |
return NextResponse.json(nodes);
|
| 20 |
}
|
| 21 |
if (action === "read") {
|
| 22 |
+
const content = await readFile(fullPath);
|
| 23 |
return NextResponse.json({ content });
|
| 24 |
}
|
| 25 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
| 26 |
} catch (e: unknown) {
|
| 27 |
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
|
|
| 28 |
}
|
| 29 |
}
|
| 30 |
|
| 31 |
export async function POST(req: NextRequest) {
|
| 32 |
+
const session = await auth();
|
| 33 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 34 |
+
|
| 35 |
try {
|
| 36 |
+
const { path: subPath, content } = await req.json();
|
| 37 |
+
if (!subPath) return NextResponse.json({ error: "Path required" }, { status: 400 });
|
| 38 |
|
| 39 |
+
const fullPath = await resolveSafePath(session.user.id, subPath);
|
| 40 |
+
await writeFile(fullPath, content);
|
| 41 |
return NextResponse.json({ success: true });
|
| 42 |
} catch (e: unknown) {
|
| 43 |
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
|
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
export async function DELETE(req: NextRequest) {
|
| 48 |
+
const session = await auth();
|
| 49 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 50 |
+
|
| 51 |
const { searchParams } = new URL(req.url);
|
| 52 |
+
const subPath = searchParams.get("path");
|
| 53 |
|
| 54 |
try {
|
| 55 |
+
if (!subPath) return NextResponse.json({ error: "Path required" }, { status: 400 });
|
| 56 |
+
const fullPath = await resolveSafePath(session.user.id, subPath);
|
| 57 |
+
await deletePath(fullPath);
|
| 58 |
return NextResponse.json({ success: true });
|
| 59 |
} catch (e: unknown) {
|
| 60 |
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
|
|
| 61 |
}
|
| 62 |
}
|
app/api/git/route.ts
CHANGED
|
@@ -7,29 +7,41 @@ import {
|
|
| 7 |
pushBranch,
|
| 8 |
pullBranch,
|
| 9 |
checkoutBranch,
|
| 10 |
-
|
| 11 |
} from "@/lib/git";
|
|
|
|
|
|
|
| 12 |
|
| 13 |
export async function GET(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
| 14 |
const { searchParams } = new URL(req.url);
|
| 15 |
const action = searchParams.get("action");
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
try {
|
|
|
|
|
|
|
|
|
|
| 18 |
switch (action) {
|
| 19 |
case "status":
|
| 20 |
-
return NextResponse.json(await getGitStatus());
|
| 21 |
case "branches":
|
| 22 |
-
return NextResponse.json(await getBranchList());
|
| 23 |
case "diff":
|
| 24 |
const file = searchParams.get("file");
|
| 25 |
if (!file) return NextResponse.json({ error: "File required" }, { status: 400 });
|
| 26 |
-
return NextResponse.json({ diff: await getFileDiff(file) });
|
| 27 |
case "log":
|
| 28 |
-
const log = await
|
| 29 |
return NextResponse.json(log.all);
|
| 30 |
case "checkConfig":
|
| 31 |
-
const
|
| 32 |
-
const
|
|
|
|
| 33 |
return NextResponse.json({
|
| 34 |
configured: !!(name.value && email.value),
|
| 35 |
name: name.value,
|
|
@@ -39,46 +51,51 @@ export async function GET(req: NextRequest) {
|
|
| 39 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
| 40 |
}
|
| 41 |
} catch (e: unknown) {
|
| 42 |
-
|
| 43 |
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
export async function POST(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
| 48 |
try {
|
| 49 |
const body = await req.json();
|
| 50 |
-
const { action } = body;
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
switch (action) {
|
| 53 |
case "commit":
|
| 54 |
if (!body.message) return NextResponse.json({ error: "Message required" }, { status: 400 });
|
| 55 |
-
const res = await commitFiles(body.message, body.files);
|
| 56 |
return NextResponse.json({ success: true, commit: res.commit });
|
| 57 |
|
| 58 |
case "push":
|
| 59 |
-
await pushBranch();
|
| 60 |
return NextResponse.json({ success: true });
|
| 61 |
|
| 62 |
case "pull":
|
| 63 |
-
await pullBranch();
|
| 64 |
return NextResponse.json({ success: true });
|
| 65 |
|
| 66 |
case "checkout":
|
| 67 |
if (!body.branch) return NextResponse.json({ error: "Branch required" }, { status: 400 });
|
| 68 |
-
await checkoutBranch(body.branch, body.create);
|
| 69 |
return NextResponse.json({ success: true });
|
| 70 |
|
| 71 |
case "setConfig":
|
| 72 |
if (!body.name || !body.email) return NextResponse.json({ error: "Name and email required" }, { status: 400 });
|
| 73 |
-
|
| 74 |
-
await
|
|
|
|
| 75 |
return NextResponse.json({ success: true });
|
| 76 |
|
| 77 |
default:
|
| 78 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
| 79 |
}
|
| 80 |
} catch (e: unknown) {
|
| 81 |
-
|
| 82 |
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
| 83 |
}
|
| 84 |
}
|
|
|
|
| 7 |
pushBranch,
|
| 8 |
pullBranch,
|
| 9 |
checkoutBranch,
|
| 10 |
+
getGit
|
| 11 |
} from "@/lib/git";
|
| 12 |
+
import { auth } from "@/auth";
|
| 13 |
+
import { resolveSafePath } from "@/lib/fs/isolation";
|
| 14 |
|
| 15 |
export async function GET(req: NextRequest) {
|
| 16 |
+
const session = await auth();
|
| 17 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 18 |
+
|
| 19 |
const { searchParams } = new URL(req.url);
|
| 20 |
const action = searchParams.get("action");
|
| 21 |
+
const workspaceName = searchParams.get("workspaceName");
|
| 22 |
+
|
| 23 |
+
if (!workspaceName) return NextResponse.json({ error: "workspaceName required" }, { status: 400 });
|
| 24 |
|
| 25 |
try {
|
| 26 |
+
const userId = session.user.id;
|
| 27 |
+
const baseDir = await resolveSafePath(userId, workspaceName);
|
| 28 |
+
|
| 29 |
switch (action) {
|
| 30 |
case "status":
|
| 31 |
+
return NextResponse.json(await getGitStatus(baseDir));
|
| 32 |
case "branches":
|
| 33 |
+
return NextResponse.json(await getBranchList(baseDir));
|
| 34 |
case "diff":
|
| 35 |
const file = searchParams.get("file");
|
| 36 |
if (!file) return NextResponse.json({ error: "File required" }, { status: 400 });
|
| 37 |
+
return NextResponse.json({ diff: await getFileDiff(file, baseDir) });
|
| 38 |
case "log":
|
| 39 |
+
const log = await getGit(baseDir).log({ maxCount: 50 });
|
| 40 |
return NextResponse.json(log.all);
|
| 41 |
case "checkConfig":
|
| 42 |
+
const instance = getGit(baseDir);
|
| 43 |
+
const name = await instance.getConfig("user.name", "local");
|
| 44 |
+
const email = await instance.getConfig("user.email", "local");
|
| 45 |
return NextResponse.json({
|
| 46 |
configured: !!(name.value && email.value),
|
| 47 |
name: name.value,
|
|
|
|
| 51 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
| 52 |
}
|
| 53 |
} catch (e: unknown) {
|
| 54 |
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
|
|
| 55 |
}
|
| 56 |
}
|
| 57 |
|
| 58 |
export async function POST(req: NextRequest) {
|
| 59 |
+
const session = await auth();
|
| 60 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 61 |
+
|
| 62 |
try {
|
| 63 |
const body = await req.json();
|
| 64 |
+
const { action, workspaceName } = body;
|
| 65 |
+
if (!workspaceName) return NextResponse.json({ error: "Workspace required" }, { status: 400 });
|
| 66 |
+
|
| 67 |
+
const baseDir = await resolveSafePath(session.user.id, workspaceName);
|
| 68 |
|
| 69 |
switch (action) {
|
| 70 |
case "commit":
|
| 71 |
if (!body.message) return NextResponse.json({ error: "Message required" }, { status: 400 });
|
| 72 |
+
const res = await commitFiles(body.message, body.files, baseDir);
|
| 73 |
return NextResponse.json({ success: true, commit: res.commit });
|
| 74 |
|
| 75 |
case "push":
|
| 76 |
+
await pushBranch(baseDir);
|
| 77 |
return NextResponse.json({ success: true });
|
| 78 |
|
| 79 |
case "pull":
|
| 80 |
+
await pullBranch(baseDir);
|
| 81 |
return NextResponse.json({ success: true });
|
| 82 |
|
| 83 |
case "checkout":
|
| 84 |
if (!body.branch) return NextResponse.json({ error: "Branch required" }, { status: 400 });
|
| 85 |
+
await checkoutBranch(body.branch, body.create, baseDir);
|
| 86 |
return NextResponse.json({ success: true });
|
| 87 |
|
| 88 |
case "setConfig":
|
| 89 |
if (!body.name || !body.email) return NextResponse.json({ error: "Name and email required" }, { status: 400 });
|
| 90 |
+
const instance = getGit(baseDir);
|
| 91 |
+
await instance.addConfig("user.name", body.name, false, "local");
|
| 92 |
+
await instance.addConfig("user.email", body.email, false, "local");
|
| 93 |
return NextResponse.json({ success: true });
|
| 94 |
|
| 95 |
default:
|
| 96 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
| 97 |
}
|
| 98 |
} catch (e: unknown) {
|
| 99 |
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
|
|
| 100 |
}
|
| 101 |
}
|
app/api/lsp/route.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
import { spawn } from "child_process";
|
|
|
|
|
|
|
| 3 |
|
| 4 |
// Language Server mapping: extension → command to start the language server
|
| 5 |
const LSP_SERVERS: Record<string, { cmd: string; args: string[] }> = {
|
|
@@ -15,6 +17,10 @@ const LSP_SERVERS: Record<string, { cmd: string; args: string[] }> = {
|
|
| 15 |
const activeServers = new Map<string, ReturnType<typeof spawn>>();
|
| 16 |
|
| 17 |
export async function GET(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
const { searchParams } = new URL(req.url);
|
| 19 |
const lang = searchParams.get("lang");
|
| 20 |
|
|
@@ -22,7 +28,7 @@ export async function GET(req: NextRequest) {
|
|
| 22 |
return NextResponse.json({
|
| 23 |
available: Object.keys(LSP_SERVERS),
|
| 24 |
status: Object.fromEntries(
|
| 25 |
-
Object.keys(LSP_SERVERS).map(l => [l, activeServers.has(l) ? "running" : "idle"])
|
| 26 |
)
|
| 27 |
});
|
| 28 |
}
|
|
@@ -40,49 +46,57 @@ export async function GET(req: NextRequest) {
|
|
| 40 |
}
|
| 41 |
|
| 42 |
export async function POST(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
try {
|
| 44 |
-
const { action, lang } = await req.json();
|
| 45 |
|
| 46 |
if (action === "start") {
|
| 47 |
if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
|
|
|
|
| 48 |
|
| 49 |
const config = LSP_SERVERS[lang];
|
| 50 |
if (!config) return NextResponse.json({ error: `No LSP for .${lang}` }, { status: 404 });
|
| 51 |
|
| 52 |
-
|
|
|
|
| 53 |
return NextResponse.json({ status: "already_running" });
|
| 54 |
}
|
| 55 |
|
| 56 |
try {
|
|
|
|
| 57 |
const proc = spawn(config.cmd, config.args, {
|
| 58 |
-
cwd:
|
| 59 |
stdio: ["pipe", "pipe", "pipe"],
|
| 60 |
shell: process.platform === "win32"
|
| 61 |
});
|
| 62 |
|
| 63 |
-
activeServers.set(
|
| 64 |
|
| 65 |
-
proc.on("exit", () => activeServers.delete(
|
| 66 |
proc.on("error", (err) => {
|
| 67 |
-
console.warn(`LSP ${lang}
|
| 68 |
-
activeServers.delete(
|
| 69 |
});
|
| 70 |
|
| 71 |
return NextResponse.json({ status: "started", pid: proc.pid });
|
| 72 |
-
} catch {
|
| 73 |
return NextResponse.json({
|
| 74 |
status: "unavailable",
|
| 75 |
-
message:
|
| 76 |
});
|
| 77 |
}
|
| 78 |
}
|
| 79 |
|
| 80 |
if (action === "stop") {
|
| 81 |
if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
|
| 82 |
-
const
|
|
|
|
| 83 |
if (proc) {
|
| 84 |
proc.kill();
|
| 85 |
-
activeServers.delete(
|
| 86 |
}
|
| 87 |
return NextResponse.json({ status: "stopped" });
|
| 88 |
}
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
import { spawn } from "child_process";
|
| 3 |
+
import { auth } from "@/auth";
|
| 4 |
+
import { resolveSafePath } from "@/lib/fs/isolation";
|
| 5 |
|
| 6 |
// Language Server mapping: extension → command to start the language server
|
| 7 |
const LSP_SERVERS: Record<string, { cmd: string; args: string[] }> = {
|
|
|
|
| 17 |
const activeServers = new Map<string, ReturnType<typeof spawn>>();
|
| 18 |
|
| 19 |
export async function GET(req: NextRequest) {
|
| 20 |
+
const session = await auth();
|
| 21 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 22 |
+
const userId = session.user.id;
|
| 23 |
+
|
| 24 |
const { searchParams } = new URL(req.url);
|
| 25 |
const lang = searchParams.get("lang");
|
| 26 |
|
|
|
|
| 28 |
return NextResponse.json({
|
| 29 |
available: Object.keys(LSP_SERVERS),
|
| 30 |
status: Object.fromEntries(
|
| 31 |
+
Object.keys(LSP_SERVERS).map(l => [l, activeServers.has(`${userId}:${l}`) ? "running" : "idle"])
|
| 32 |
)
|
| 33 |
});
|
| 34 |
}
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
export async function POST(req: NextRequest) {
|
| 49 |
+
const session = await auth();
|
| 50 |
+
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 51 |
+
const userId = session.user.id;
|
| 52 |
+
|
| 53 |
try {
|
| 54 |
+
const { action, lang, workspaceName } = await req.json();
|
| 55 |
|
| 56 |
if (action === "start") {
|
| 57 |
if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
|
| 58 |
+
if (!workspaceName) return NextResponse.json({ error: "workspaceName required" }, { status: 400 });
|
| 59 |
|
| 60 |
const config = LSP_SERVERS[lang];
|
| 61 |
if (!config) return NextResponse.json({ error: `No LSP for .${lang}` }, { status: 404 });
|
| 62 |
|
| 63 |
+
const serverKey = `${userId}:${lang}`;
|
| 64 |
+
if (activeServers.has(serverKey)) {
|
| 65 |
return NextResponse.json({ status: "already_running" });
|
| 66 |
}
|
| 67 |
|
| 68 |
try {
|
| 69 |
+
const projectRoot = await resolveSafePath(userId, workspaceName);
|
| 70 |
const proc = spawn(config.cmd, config.args, {
|
| 71 |
+
cwd: projectRoot,
|
| 72 |
stdio: ["pipe", "pipe", "pipe"],
|
| 73 |
shell: process.platform === "win32"
|
| 74 |
});
|
| 75 |
|
| 76 |
+
activeServers.set(serverKey, proc);
|
| 77 |
|
| 78 |
+
proc.on("exit", () => activeServers.delete(serverKey));
|
| 79 |
proc.on("error", (err) => {
|
| 80 |
+
console.warn(`LSP ${lang} for ${userId}: ${err.message}`);
|
| 81 |
+
activeServers.delete(serverKey);
|
| 82 |
});
|
| 83 |
|
| 84 |
return NextResponse.json({ status: "started", pid: proc.pid });
|
| 85 |
+
} catch (e: unknown) {
|
| 86 |
return NextResponse.json({
|
| 87 |
status: "unavailable",
|
| 88 |
+
message: (e as Error).message
|
| 89 |
});
|
| 90 |
}
|
| 91 |
}
|
| 92 |
|
| 93 |
if (action === "stop") {
|
| 94 |
if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
|
| 95 |
+
const serverKey = `${userId}:${lang}`;
|
| 96 |
+
const proc = activeServers.get(serverKey);
|
| 97 |
if (proc) {
|
| 98 |
proc.kill();
|
| 99 |
+
activeServers.delete(serverKey);
|
| 100 |
}
|
| 101 |
return NextResponse.json({ status: "stopped" });
|
| 102 |
}
|
app/api/projects/route.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import path from "path";
|
| 3 |
import fs from "fs/promises";
|
| 4 |
import simpleGit from "simple-git";
|
| 5 |
import { spawn } from "child_process";
|
|
@@ -8,11 +7,9 @@ import { auth } from "@/auth";
|
|
| 8 |
import { db } from "@/lib/db";
|
| 9 |
import { randomUUID } from "crypto";
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
-
|
| 14 |
-
await fs.mkdir(WORKSPACE_ROOT, { recursive: true });
|
| 15 |
-
}
|
| 16 |
|
| 17 |
// POST /api/projects
|
| 18 |
export async function POST(req: NextRequest) {
|
|
@@ -37,7 +34,11 @@ export async function POST(req: NextRequest) {
|
|
| 37 |
}
|
| 38 |
|
| 39 |
export async function GET() {
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
export async function DELETE(req: NextRequest) {
|
|
@@ -65,13 +66,9 @@ export async function DELETE(req: NextRequest) {
|
|
| 65 |
}
|
| 66 |
|
| 67 |
const projectName = res.rows[0].project_name as string;
|
| 68 |
-
const
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
const targetPath = path.join(WORKSPACE_ROOT, safeName);
|
| 72 |
-
if (targetPath.startsWith(WORKSPACE_ROOT)) {
|
| 73 |
-
await fs.rm(targetPath, { recursive: true, force: true });
|
| 74 |
-
}
|
| 75 |
|
| 76 |
// Remove from database
|
| 77 |
await db.execute({
|
|
@@ -88,30 +85,29 @@ export async function DELETE(req: NextRequest) {
|
|
| 88 |
}
|
| 89 |
}
|
| 90 |
|
| 91 |
-
async function handleList() {
|
| 92 |
-
const session = await auth();
|
| 93 |
-
if (!session?.user?.id) {
|
| 94 |
-
return NextResponse.json({ projects: [] });
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
try {
|
| 98 |
const res = await db.execute({
|
| 99 |
sql: "SELECT * FROM workspaces WHERE user_id = ?",
|
| 100 |
-
args: [
|
| 101 |
});
|
| 102 |
|
| 103 |
-
const projects = res.rows.map(row =>
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
| 111 |
}));
|
| 112 |
|
| 113 |
return NextResponse.json({ projects });
|
| 114 |
-
} catch {
|
|
|
|
| 115 |
return NextResponse.json({ projects: [] });
|
| 116 |
}
|
| 117 |
}
|
|
@@ -122,9 +118,8 @@ async function handleClone(req: NextRequest, userId: string) {
|
|
| 122 |
return NextResponse.json({ error: "repoUrl and projectName are required" }, { status: 400 });
|
| 123 |
}
|
| 124 |
|
| 125 |
-
await ensureWorkspaceRoot();
|
| 126 |
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 127 |
-
const dest =
|
| 128 |
|
| 129 |
// Stream progress via SSE
|
| 130 |
const encoder = new TextEncoder();
|
|
@@ -173,9 +168,8 @@ async function handleScaffold(req: NextRequest, userId: string) {
|
|
| 173 |
return NextResponse.json({ error: "templateId and projectName are required" }, { status: 400 });
|
| 174 |
}
|
| 175 |
|
| 176 |
-
await ensureWorkspaceRoot();
|
| 177 |
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 178 |
-
const dest =
|
| 179 |
await fs.mkdir(dest, { recursive: true });
|
| 180 |
|
| 181 |
// Template scaffold commands (IDs must exactly match TEMPLATE_REGISTRY ids in constants/extensions.ts)
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
| 2 |
import fs from "fs/promises";
|
| 3 |
import simpleGit from "simple-git";
|
| 4 |
import { spawn } from "child_process";
|
|
|
|
| 7 |
import { db } from "@/lib/db";
|
| 8 |
import { randomUUID } from "crypto";
|
| 9 |
|
| 10 |
+
import { resolveSafePath } from "@/lib/fs/isolation";
|
| 11 |
|
| 12 |
+
// Removed global WORKSPACE_ROOT
|
|
|
|
|
|
|
| 13 |
|
| 14 |
// POST /api/projects
|
| 15 |
export async function POST(req: NextRequest) {
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
export async function GET() {
|
| 37 |
+
const session = await auth();
|
| 38 |
+
if (!session?.user?.id) {
|
| 39 |
+
return NextResponse.json({ projects: [] });
|
| 40 |
+
}
|
| 41 |
+
return handleList(session.user.id);
|
| 42 |
}
|
| 43 |
|
| 44 |
export async function DELETE(req: NextRequest) {
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
const projectName = res.rows[0].project_name as string;
|
| 69 |
+
const targetPath = await resolveSafePath(session.user.id, projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60));
|
| 70 |
+
|
| 71 |
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
// Remove from database
|
| 74 |
await db.execute({
|
|
|
|
| 85 |
}
|
| 86 |
}
|
| 87 |
|
| 88 |
+
async function handleList(userId: string) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
try {
|
| 90 |
const res = await db.execute({
|
| 91 |
sql: "SELECT * FROM workspaces WHERE user_id = ?",
|
| 92 |
+
args: [userId]
|
| 93 |
});
|
| 94 |
|
| 95 |
+
const projects = await Promise.all(res.rows.map(async row => {
|
| 96 |
+
const safeName = (row.project_name as string).replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 97 |
+
return {
|
| 98 |
+
id: row.id,
|
| 99 |
+
name: row.project_name,
|
| 100 |
+
path: await resolveSafePath(userId, safeName),
|
| 101 |
+
containerStatus: row.status,
|
| 102 |
+
gitRemote: "",
|
| 103 |
+
hasPackageJson: true,
|
| 104 |
+
starred: false
|
| 105 |
+
};
|
| 106 |
}));
|
| 107 |
|
| 108 |
return NextResponse.json({ projects });
|
| 109 |
+
} catch (e) {
|
| 110 |
+
console.error("[PROJECTS_LIST_ERROR]", e);
|
| 111 |
return NextResponse.json({ projects: [] });
|
| 112 |
}
|
| 113 |
}
|
|
|
|
| 118 |
return NextResponse.json({ error: "repoUrl and projectName are required" }, { status: 400 });
|
| 119 |
}
|
| 120 |
|
|
|
|
| 121 |
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 122 |
+
const dest = await resolveSafePath(userId, safeName);
|
| 123 |
|
| 124 |
// Stream progress via SSE
|
| 125 |
const encoder = new TextEncoder();
|
|
|
|
| 168 |
return NextResponse.json({ error: "templateId and projectName are required" }, { status: 400 });
|
| 169 |
}
|
| 170 |
|
|
|
|
| 171 |
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 172 |
+
const dest = await resolveSafePath(userId, safeName);
|
| 173 |
await fs.mkdir(dest, { recursive: true });
|
| 174 |
|
| 175 |
// Template scaffold commands (IDs must exactly match TEMPLATE_REGISTRY ids in constants/extensions.ts)
|
app/api/workspace/route.ts
CHANGED
|
@@ -49,9 +49,9 @@ export async function POST(req: Request) {
|
|
| 49 |
return NextResponse.json({ error: "Missing workspace id" }, { status: 400 });
|
| 50 |
}
|
| 51 |
|
| 52 |
-
// Verify ownership
|
| 53 |
const verifyObj = await db.execute({
|
| 54 |
-
sql: "SELECT id FROM workspaces WHERE id = ? AND user_id = ?",
|
| 55 |
args: [id, session.user.id]
|
| 56 |
});
|
| 57 |
|
|
@@ -59,9 +59,17 @@ export async function POST(req: Request) {
|
|
| 59 |
return NextResponse.json({ error: "Workspace not found or unauthorized" }, { status: 404 });
|
| 60 |
}
|
| 61 |
|
|
|
|
|
|
|
| 62 |
if (action === "start") {
|
| 63 |
const { withAndroidEmulator } = body;
|
| 64 |
-
const result = await startWorkspaceContainer({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
if (result.success) {
|
| 66 |
await db.execute({
|
| 67 |
sql: "UPDATE workspaces SET status = 'running', container_id = ?, android_container_id = ?, android_port = ? WHERE id = ?",
|
|
@@ -84,7 +92,13 @@ export async function POST(req: Request) {
|
|
| 84 |
await stopWorkspaceContainer(id, true);
|
| 85 |
|
| 86 |
// 2. Recreate them (this will pick up codeverse.json changes)
|
| 87 |
-
const result = await startWorkspaceContainer({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
if (result.success) {
|
| 90 |
await db.execute({
|
|
|
|
| 49 |
return NextResponse.json({ error: "Missing workspace id" }, { status: 400 });
|
| 50 |
}
|
| 51 |
|
| 52 |
+
// Verify ownership and get project name
|
| 53 |
const verifyObj = await db.execute({
|
| 54 |
+
sql: "SELECT id, project_name FROM workspaces WHERE id = ? AND user_id = ?",
|
| 55 |
args: [id, session.user.id]
|
| 56 |
});
|
| 57 |
|
|
|
|
| 59 |
return NextResponse.json({ error: "Workspace not found or unauthorized" }, { status: 404 });
|
| 60 |
}
|
| 61 |
|
| 62 |
+
const projectName = verifyObj.rows[0].project_name as string;
|
| 63 |
+
|
| 64 |
if (action === "start") {
|
| 65 |
const { withAndroidEmulator } = body;
|
| 66 |
+
const result = await startWorkspaceContainer({
|
| 67 |
+
id,
|
| 68 |
+
userId: session.user.id,
|
| 69 |
+
projectName,
|
| 70 |
+
image,
|
| 71 |
+
withAndroidEmulator
|
| 72 |
+
});
|
| 73 |
if (result.success) {
|
| 74 |
await db.execute({
|
| 75 |
sql: "UPDATE workspaces SET status = 'running', container_id = ?, android_container_id = ?, android_port = ? WHERE id = ?",
|
|
|
|
| 92 |
await stopWorkspaceContainer(id, true);
|
| 93 |
|
| 94 |
// 2. Recreate them (this will pick up codeverse.json changes)
|
| 95 |
+
const result = await startWorkspaceContainer({
|
| 96 |
+
id,
|
| 97 |
+
userId: session.user.id,
|
| 98 |
+
projectName,
|
| 99 |
+
image,
|
| 100 |
+
withAndroidEmulator
|
| 101 |
+
});
|
| 102 |
|
| 103 |
if (result.success) {
|
| 104 |
await db.execute({
|
app/api/workspace/stream/route.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
| 1 |
import { NextRequest } from 'next/server';
|
| 2 |
import { startWorkspaceContainer } from '@/lib/docker/manager';
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export const dynamic = 'force-dynamic';
|
| 5 |
|
| 6 |
export async function GET(req: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
const searchParams = req.nextUrl.searchParams;
|
| 8 |
const id = searchParams.get('id');
|
| 9 |
const withAndroid = searchParams.get('withAndroid') === 'true';
|
|
@@ -12,6 +18,18 @@ export async function GET(req: NextRequest) {
|
|
| 12 |
return new Response('Missing workspace id', { status: 400 });
|
| 13 |
}
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
const encoder = new TextEncoder();
|
| 16 |
|
| 17 |
const stream = new ReadableStream({
|
|
@@ -24,9 +42,13 @@ export async function GET(req: NextRequest) {
|
|
| 24 |
|
| 25 |
try {
|
| 26 |
// Initialize workspace and pipe logs directly from the Docker builder engine to SSE client
|
| 27 |
-
const result = await startWorkspaceContainer(
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
// Completed
|
| 32 |
sendEvent('ready', {
|
|
|
|
| 1 |
import { NextRequest } from 'next/server';
|
| 2 |
import { startWorkspaceContainer } from '@/lib/docker/manager';
|
| 3 |
+
import { auth } from '@/auth';
|
| 4 |
+
import { db } from '@/lib/db';
|
| 5 |
|
| 6 |
export const dynamic = 'force-dynamic';
|
| 7 |
|
| 8 |
export async function GET(req: NextRequest) {
|
| 9 |
+
const session = await auth();
|
| 10 |
+
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
|
| 11 |
+
const userId = session.user.id;
|
| 12 |
+
|
| 13 |
const searchParams = req.nextUrl.searchParams;
|
| 14 |
const id = searchParams.get('id');
|
| 15 |
const withAndroid = searchParams.get('withAndroid') === 'true';
|
|
|
|
| 18 |
return new Response('Missing workspace id', { status: 400 });
|
| 19 |
}
|
| 20 |
|
| 21 |
+
// Verify ownership and get project name
|
| 22 |
+
const verifyObj = await db.execute({
|
| 23 |
+
sql: "SELECT project_name FROM workspaces WHERE id = ? AND user_id = ?",
|
| 24 |
+
args: [id, session.user.id]
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
if (verifyObj.rows.length === 0) {
|
| 28 |
+
return new Response('Workspace not found or unauthorized', { status: 404 });
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const projectName = verifyObj.rows[0].project_name as string;
|
| 32 |
+
|
| 33 |
const encoder = new TextEncoder();
|
| 34 |
|
| 35 |
const stream = new ReadableStream({
|
|
|
|
| 42 |
|
| 43 |
try {
|
| 44 |
// Initialize workspace and pipe logs directly from the Docker builder engine to SSE client
|
| 45 |
+
const result = await startWorkspaceContainer({
|
| 46 |
+
id: id as string,
|
| 47 |
+
userId: userId,
|
| 48 |
+
projectName: projectName,
|
| 49 |
+
withAndroidEmulator: withAndroid,
|
| 50 |
+
onLog: (msg) => sendEvent('log', msg)
|
| 51 |
+
});
|
| 52 |
|
| 53 |
// Completed
|
| 54 |
sendEvent('ready', {
|
auth.ts
CHANGED
|
@@ -205,6 +205,7 @@ const TursoAdapter = {
|
|
| 205 |
const authOptions = {
|
| 206 |
adapter: TursoAdapter as unknown as Adapter,
|
| 207 |
trustHost: true,
|
|
|
|
| 208 |
providers: [
|
| 209 |
GitHubProvider({
|
| 210 |
clientId: process.env.GITHUB_ID ?? "",
|
|
|
|
| 205 |
const authOptions = {
|
| 206 |
adapter: TursoAdapter as unknown as Adapter,
|
| 207 |
trustHost: true,
|
| 208 |
+
secret: process.env.AUTH_SECRET,
|
| 209 |
providers: [
|
| 210 |
GitHubProvider({
|
| 211 |
clientId: process.env.GITHUB_ID ?? "",
|
components/dashboard/Dashboard.tsx
CHANGED
|
@@ -190,8 +190,8 @@ export default function Dashboard() {
|
|
| 190 |
<div className="h-4 w-px bg-(--border)" />
|
| 191 |
<div className="flex items-center gap-3 pl-1">
|
| 192 |
<div className="flex flex-col items-end">
|
| 193 |
-
<span className="text-sm font-semibold text-(--text) leading-tight">{session?.user?.name || "
|
| 194 |
-
<span className="text-[10px] text-(--text-muted) font-mono">{session?.user?.email || "
|
| 195 |
</div>
|
| 196 |
{session?.user?.image ? (
|
| 197 |
<Image unoptimized src={session.user.image} alt="Profile" width={36} height={36} className="w-9 h-9 rounded-full ring-2 ring-(--border-subtle)" />
|
|
@@ -211,7 +211,7 @@ export default function Dashboard() {
|
|
| 211 |
{/* Greeting */}
|
| 212 |
<div>
|
| 213 |
<h1 className="text-2xl font-bold text-(--text)">
|
| 214 |
-
Hello, <span className="text-(--accent)">
|
| 215 |
</h1>
|
| 216 |
<p className="text-(--text-2) text-sm mt-1">Welcome back to CodeVerse Studio</p>
|
| 217 |
</div>
|
|
|
|
| 190 |
<div className="h-4 w-px bg-(--border)" />
|
| 191 |
<div className="flex items-center gap-3 pl-1">
|
| 192 |
<div className="flex flex-col items-end">
|
| 193 |
+
<span className="text-sm font-semibold text-(--text) leading-tight">{session?.user?.name || "User"}</span>
|
| 194 |
+
<span className="text-[10px] text-(--text-muted) font-mono">{session?.user?.email || "anonymous@codeverse.io"}</span>
|
| 195 |
</div>
|
| 196 |
{session?.user?.image ? (
|
| 197 |
<Image unoptimized src={session.user.image} alt="Profile" width={36} height={36} className="w-9 h-9 rounded-full ring-2 ring-(--border-subtle)" />
|
|
|
|
| 211 |
{/* Greeting */}
|
| 212 |
<div>
|
| 213 |
<h1 className="text-2xl font-bold text-(--text)">
|
| 214 |
+
Hello, <span className="text-(--accent)">{session?.user?.name?.split(" ")[0] || "User"}</span>
|
| 215 |
</h1>
|
| 216 |
<p className="text-(--text-2) text-sm mt-1">Welcome back to CodeVerse Studio</p>
|
| 217 |
</div>
|
components/workspace/AIAssistantSidebar.tsx
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useChat } from "@ai-sdk/react";
|
| 4 |
+
import React, { useState, useRef, useEffect, useMemo, ChangeEvent, FormEvent } from "react";
|
| 5 |
+
import { motion, AnimatePresence } from "framer-motion";
|
| 6 |
+
import {
|
| 7 |
+
Bot,
|
| 8 |
+
User,
|
| 9 |
+
Send,
|
| 10 |
+
X,
|
| 11 |
+
Plus,
|
| 12 |
+
Wand2,
|
| 13 |
+
Trash2,
|
| 14 |
+
Terminal,
|
| 15 |
+
FileText,
|
| 16 |
+
Search as SearchIcon,
|
| 17 |
+
Check,
|
| 18 |
+
MessageSquare,
|
| 19 |
+
} from "lucide-react";
|
| 20 |
+
import ReactMarkdown from "react-markdown";
|
| 21 |
+
import remarkGfm from "remark-gfm";
|
| 22 |
+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
| 23 |
+
import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
| 24 |
+
|
| 25 |
+
// Standard AI Studio message shape for strict typing
|
| 26 |
+
export interface StudioMessage {
|
| 27 |
+
id: string;
|
| 28 |
+
role: "system" | "user" | "assistant" | "data" | "tool";
|
| 29 |
+
content: string;
|
| 30 |
+
toolInvocations?: StudioToolInvocation[];
|
| 31 |
+
createdAt?: Date;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface StudioToolInvocation {
|
| 35 |
+
state: "call" | "result";
|
| 36 |
+
toolCallId: string;
|
| 37 |
+
toolName: string;
|
| 38 |
+
args: Record<string, unknown>;
|
| 39 |
+
result?: unknown;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Interface for the hook results to satisfy TSC without 'any'
|
| 43 |
+
interface ChatHelpers {
|
| 44 |
+
messages: StudioMessage[];
|
| 45 |
+
input: string;
|
| 46 |
+
handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
| 47 |
+
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
| 48 |
+
isLoading: boolean;
|
| 49 |
+
setMessages: (messages: StudioMessage[]) => void;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
interface AIAssistantSidebarProps {
|
| 53 |
+
workspaceName: string;
|
| 54 |
+
isOpen: boolean;
|
| 55 |
+
onClose: () => void;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
interface PlanStep {
|
| 59 |
+
id: string;
|
| 60 |
+
description: string;
|
| 61 |
+
filesData?: string[];
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
interface PlanContent {
|
| 65 |
+
goal: string;
|
| 66 |
+
steps: PlanStep[];
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
export function AIAssistantSidebar({ workspaceName, isOpen, onClose }: AIAssistantSidebarProps) {
|
| 70 |
+
const [isPlanMode, setIsPlanMode] = useState(false);
|
| 71 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 72 |
+
|
| 73 |
+
const welcomeMsg: StudioMessage = {
|
| 74 |
+
id: "welcome",
|
| 75 |
+
role: "assistant",
|
| 76 |
+
content: `Hi! I'm your CodeVerse AI Assistant. I can help you explore this project, write code, or run terminal commands. How can I help you with **${workspaceName}** today?`,
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
// Safe bridge to bypass AI SDK v3 type-brand mismatches while keeping internals strict
|
| 80 |
+
const chatOptions = {
|
| 81 |
+
api: "/api/agent",
|
| 82 |
+
body: {
|
| 83 |
+
workspaceName,
|
| 84 |
+
mode: isPlanMode ? "plan" : "execute",
|
| 85 |
+
},
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
// Casting the hook result to our strictly-typed internal interface
|
| 89 |
+
const chat = useChat(chatOptions as unknown as Parameters<typeof useChat>[0]) as unknown as ChatHelpers;
|
| 90 |
+
|
| 91 |
+
const {
|
| 92 |
+
messages: chatMessages,
|
| 93 |
+
input,
|
| 94 |
+
handleInputChange,
|
| 95 |
+
handleSubmit,
|
| 96 |
+
isLoading,
|
| 97 |
+
setMessages
|
| 98 |
+
} = chat;
|
| 99 |
+
|
| 100 |
+
// Fetch history on mount
|
| 101 |
+
useEffect(() => {
|
| 102 |
+
const fetchHistory = async () => {
|
| 103 |
+
try {
|
| 104 |
+
const res = await fetch(`/api/agent?workspaceName=${workspaceName}`);
|
| 105 |
+
if (res.ok) {
|
| 106 |
+
const data = await res.json() as { messages: StudioMessage[] };
|
| 107 |
+
if (data.messages && data.messages.length > 0) {
|
| 108 |
+
setMessages(data.messages);
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
} catch (err) {
|
| 112 |
+
console.error("Failed to fetch chat history:", err);
|
| 113 |
+
}
|
| 114 |
+
};
|
| 115 |
+
fetchHistory();
|
| 116 |
+
}, [workspaceName, setMessages]);
|
| 117 |
+
|
| 118 |
+
const clearHistory = () => {
|
| 119 |
+
setMessages([]);
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
const messages = useMemo<StudioMessage[]>(
|
| 123 |
+
() => (chatMessages.length === 0 ? [welcomeMsg] : chatMessages),
|
| 124 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 125 |
+
[chatMessages, workspaceName]
|
| 126 |
+
);
|
| 127 |
+
|
| 128 |
+
useEffect(() => {
|
| 129 |
+
if (scrollRef.current) {
|
| 130 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 131 |
+
}
|
| 132 |
+
}, [messages]);
|
| 133 |
+
|
| 134 |
+
const renderToolInvocation = (tool: StudioToolInvocation) => {
|
| 135 |
+
const isDone = tool.state === "result";
|
| 136 |
+
const iconMap: Record<string, React.ReactNode> = {
|
| 137 |
+
read_file: <FileText size={14} />,
|
| 138 |
+
write_file: <Plus size={14} />,
|
| 139 |
+
terminal_command: <Terminal size={14} />,
|
| 140 |
+
search_code: <SearchIcon size={14} />,
|
| 141 |
+
list_files: <MessageSquare size={14} />,
|
| 142 |
+
};
|
| 143 |
+
const icon = iconMap[tool.toolName] || <Wand2 size={14} />;
|
| 144 |
+
|
| 145 |
+
return (
|
| 146 |
+
<div key={tool.toolCallId} className="flex flex-col gap-2 p-3 my-2 rounded-xl bg-(--bg-2) border border-(--border-subtle) overflow-hidden shadow-sm">
|
| 147 |
+
<div className="flex items-center justify-between">
|
| 148 |
+
<div className="flex items-center gap-2 text-[10px] font-bold text-(--text-2) uppercase tracking-wider">
|
| 149 |
+
<span className={isDone ? "text-(--success)" : "text-(--accent) animate-pulse"}>
|
| 150 |
+
{icon}
|
| 151 |
+
</span>
|
| 152 |
+
<span>{tool.toolName.replace(/_/g, " ")}</span>
|
| 153 |
+
</div>
|
| 154 |
+
{isDone && <Check size={12} className="text-(--success)" />}
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
{!isDone && (
|
| 158 |
+
<div className="text-[10px] font-mono text-(--text-muted) truncate opacity-70 italic">
|
| 159 |
+
{JSON.stringify(tool.args)}
|
| 160 |
+
</div>
|
| 161 |
+
)}
|
| 162 |
+
|
| 163 |
+
{isDone && !!tool.result && (
|
| 164 |
+
<div className="text-[10px] font-mono bg-(--bg-3) p-2 rounded border border-(--border) text-(--text-muted) max-h-32 overflow-y-auto custom-scrollbar whitespace-pre-wrap">
|
| 165 |
+
{typeof tool.result === "string"
|
| 166 |
+
? (tool.result.length > 500 ? tool.result.slice(0, 500) + "..." : tool.result)
|
| 167 |
+
: JSON.stringify(tool.result, null, 2).slice(0, 500) + (JSON.stringify(tool.result).length > 500 ? "..." : "")}
|
| 168 |
+
</div>
|
| 169 |
+
)}
|
| 170 |
+
</div>
|
| 171 |
+
);
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
return (
|
| 175 |
+
<AnimatePresence>
|
| 176 |
+
{isOpen && (
|
| 177 |
+
<motion.div
|
| 178 |
+
initial={{ x: "100%" }}
|
| 179 |
+
animate={{ x: 0 }}
|
| 180 |
+
exit={{ x: "100%" }}
|
| 181 |
+
transition={{ type: "spring", damping: 28, stiffness: 220 }}
|
| 182 |
+
className="fixed top-0 right-0 h-full w-[440px] bg-(--surface) border-l border-(--border) shadow-2xl z-50 flex flex-col glassmorphism overflow-hidden"
|
| 183 |
+
>
|
| 184 |
+
{/* Header */}
|
| 185 |
+
<div className="p-4 border-b border-(--border-subtle) flex items-center justify-between bg-(--bg-2) bg-opacity-70 backdrop-blur-md">
|
| 186 |
+
<div className="flex items-center gap-3">
|
| 187 |
+
<div className="w-9 h-9 rounded-xl bg-(--accent) bg-opacity-10 flex items-center justify-center text-(--accent) shadow-inner border border-(--accent) border-opacity-20">
|
| 188 |
+
<Bot size={22} />
|
| 189 |
+
</div>
|
| 190 |
+
<div className="flex flex-col">
|
| 191 |
+
<span className="text-sm font-black text-(--text) tracking-tight">AI STUDIO</span>
|
| 192 |
+
<span className="text-[10px] text-(--text-muted) flex items-center gap-1 uppercase font-bold tracking-tighter">
|
| 193 |
+
<div className="w-1.5 h-1.5 rounded-full bg-(--success) animate-pulse shadow-[0_0_8px_var(--success)]" />
|
| 194 |
+
Isolated • {workspaceName}
|
| 195 |
+
</span>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
<div className="flex items-center gap-1.5">
|
| 199 |
+
<button
|
| 200 |
+
onClick={() => setIsPlanMode(!isPlanMode)}
|
| 201 |
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-black transition-all border shadow-sm ${
|
| 202 |
+
isPlanMode
|
| 203 |
+
? "bg-(--warning) bg-opacity-15 text-(--warning) border-(--warning) border-opacity-40"
|
| 204 |
+
: "bg-(--bg-3) text-(--text-muted) border-transparent hover:text-(--text) hover:bg-(--surface-hover)"
|
| 205 |
+
}`}
|
| 206 |
+
>
|
| 207 |
+
<Wand2 size={12} />
|
| 208 |
+
{isPlanMode ? "PLAN" : "DIRECT"}
|
| 209 |
+
</button>
|
| 210 |
+
<div className="w-px h-4 bg-(--border-subtle) mx-1" />
|
| 211 |
+
<button
|
| 212 |
+
onClick={clearHistory}
|
| 213 |
+
className="p-2 hover:bg-(--error) hover:bg-opacity-10 rounded-xl transition-all text-(--text-muted) hover:text-(--error)"
|
| 214 |
+
title="Clear History"
|
| 215 |
+
>
|
| 216 |
+
<Trash2 size={18} />
|
| 217 |
+
</button>
|
| 218 |
+
<button
|
| 219 |
+
onClick={onClose}
|
| 220 |
+
className="p-2 hover:bg-(--surface-hover) rounded-xl transition-all text-(--text-muted) hover:text-(--text)"
|
| 221 |
+
>
|
| 222 |
+
<X size={22} />
|
| 223 |
+
</button>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
{/* Messages Container */}
|
| 228 |
+
<div ref={scrollRef} className="flex-1 overflow-y-auto p-5 space-y-8 custom-scrollbar scroll-smooth">
|
| 229 |
+
{messages.map((m) => (
|
| 230 |
+
<div key={m.id} className={`flex gap-4 ${m.role === "user" ? "flex-row-reverse" : ""}`}>
|
| 231 |
+
<div className={`w-9 h-9 rounded-xl shrink-0 flex items-center justify-center shadow-sm border ${
|
| 232 |
+
m.role === "user"
|
| 233 |
+
? "bg-(--bg-3) text-(--text) border-(--border-subtle)"
|
| 234 |
+
: "bg-(--accent) bg-opacity-10 text-(--accent) border-(--accent) border-opacity-20"
|
| 235 |
+
}`}>
|
| 236 |
+
{m.role === "user" ? <User size={18} /> : <Bot size={18} />}
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<div className={`flex flex-col gap-2 max-w-[85%] ${m.role === "user" ? "items-end" : ""}`}>
|
| 240 |
+
<div className={`px-4 py-3.5 rounded-2xl text-sm leading-relaxed shadow-md border animate-in fade-in slide-in-from-bottom-2 duration-300 ${
|
| 241 |
+
m.role === "user"
|
| 242 |
+
? "bg-(--accent) text-white rounded-tr-sm border-transparent"
|
| 243 |
+
: "bg-(--bg-2) text-(--text) rounded-tl-sm border-(--border-subtle)"
|
| 244 |
+
}`}>
|
| 245 |
+
{m.role === "assistant" && m.content.startsWith("{") && m.content.includes("goal") ? (
|
| 246 |
+
<div className="flex flex-col gap-4 py-1">
|
| 247 |
+
<div className="flex items-center gap-2 text-(--accent) font-black uppercase tracking-[0.2em] text-[9px]">
|
| 248 |
+
<Check size={12} className="stroke-[3px]" /> System Plan Generated
|
| 249 |
+
</div>
|
| 250 |
+
{(() => {
|
| 251 |
+
try {
|
| 252 |
+
const plan = JSON.parse(m.content) as PlanContent;
|
| 253 |
+
return (
|
| 254 |
+
<div className="space-y-4">
|
| 255 |
+
<div className="font-bold text-sm text-(--text) bg-(--bg-3) p-3 rounded-xl border border-(--border-subtle) shadow-inner">
|
| 256 |
+
{String(plan.goal)}
|
| 257 |
+
</div>
|
| 258 |
+
<div className="space-y-2.5">
|
| 259 |
+
{(plan.steps || []).map((s, i) => (
|
| 260 |
+
<div key={s.id || String(i)} className="flex gap-3 p-3 rounded-xl bg-(--surface) bg-opacity-50 hover:bg-(--bg-3) transition-all border border-(--border-subtle) group">
|
| 261 |
+
<span className="flex items-center justify-center w-6 h-6 rounded-lg bg-(--accent) bg-opacity-10 text-(--accent) text-[11px] font-black shrink-0 group-hover:scale-110 transition-transform">
|
| 262 |
+
{i+1}
|
| 263 |
+
</span>
|
| 264 |
+
<span className="text-xs text-(--text-2) font-medium leading-[1.6]">{String(s.description)}</span>
|
| 265 |
+
</div>
|
| 266 |
+
))}
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
);
|
| 270 |
+
} catch { return <ReactMarkdown remarkPlugins={[remarkGfm]}>{m.content}</ReactMarkdown>; }
|
| 271 |
+
})()}
|
| 272 |
+
</div>
|
| 273 |
+
) : (
|
| 274 |
+
<div className="prose prose-sm prose-invert max-w-full">
|
| 275 |
+
<ReactMarkdown
|
| 276 |
+
remarkPlugins={[remarkGfm]}
|
| 277 |
+
components={{
|
| 278 |
+
code({ className, children, ...props }: React.ComponentPropsWithoutRef<'code'>) {
|
| 279 |
+
const match = /language-(\w+)/.exec(className || "");
|
| 280 |
+
const isInline = !match;
|
| 281 |
+
return isInline ? (
|
| 282 |
+
<code className="bg-(--bg-3) px-1.5 py-0.5 rounded-md text-(--accent) font-mono text-[0.9em]" {...props}>
|
| 283 |
+
{children}
|
| 284 |
+
</code>
|
| 285 |
+
) : (
|
| 286 |
+
<div className="my-4 rounded-xl overflow-hidden border border-(--border-subtle) shadow-lg">
|
| 287 |
+
<div className="bg-(--bg-3) px-4 py-1.5 border-b border-(--border-subtle) flex items-center justify-between">
|
| 288 |
+
<span className="text-[10px] font-black text-(--text-muted) uppercase tracking-widest">{match[1]}</span>
|
| 289 |
+
</div>
|
| 290 |
+
<SyntaxHighlighter
|
| 291 |
+
style={vscDarkPlus}
|
| 292 |
+
language={match[1]}
|
| 293 |
+
PreTag="div"
|
| 294 |
+
className="m-0! text-xs! bg-(--bg-2)!"
|
| 295 |
+
>
|
| 296 |
+
{String(children).replace(/\n$/, "")}
|
| 297 |
+
</SyntaxHighlighter>
|
| 298 |
+
</div>
|
| 299 |
+
);
|
| 300 |
+
},
|
| 301 |
+
}}
|
| 302 |
+
>
|
| 303 |
+
{m.content}
|
| 304 |
+
</ReactMarkdown>
|
| 305 |
+
</div>
|
| 306 |
+
)}
|
| 307 |
+
</div>
|
| 308 |
+
|
| 309 |
+
{/* Tool Invocations */}
|
| 310 |
+
{m.toolInvocations?.map((tool: StudioToolInvocation) => renderToolInvocation(tool))}
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
))}
|
| 314 |
+
|
| 315 |
+
{isLoading && (
|
| 316 |
+
<div className="flex items-center gap-4 animate-in fade-in duration-500">
|
| 317 |
+
<div className="w-9 h-9 rounded-xl bg-(--accent) bg-opacity-10 flex items-center justify-center text-(--accent) border border-(--accent) border-opacity-10">
|
| 318 |
+
<Bot size={18} className="animate-pulse" />
|
| 319 |
+
</div>
|
| 320 |
+
<div className="flex gap-1.5 p-3 px-4 bg-(--bg-2) rounded-2xl border border-(--border-subtle)">
|
| 321 |
+
<span className="w-1.5 h-1.5 rounded-full bg-(--accent) animate-bounce [animation-delay:-0.3s]" />
|
| 322 |
+
<span className="w-1.5 h-1.5 rounded-full bg-(--accent) animate-bounce [animation-delay:-0.15s]" />
|
| 323 |
+
<span className="w-1.5 h-1.5 rounded-full bg-(--accent) animate-bounce" />
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
)}
|
| 327 |
+
</div>
|
| 328 |
+
|
| 329 |
+
{/* Input Area */}
|
| 330 |
+
<div className="p-5 bg-(--bg-2) bg-opacity-80 backdrop-blur-md border-t border-(--border-subtle)">
|
| 331 |
+
<form
|
| 332 |
+
onSubmit={(e: FormEvent<HTMLFormElement>) => {
|
| 333 |
+
e.preventDefault();
|
| 334 |
+
handleSubmit(e);
|
| 335 |
+
}}
|
| 336 |
+
className="relative group"
|
| 337 |
+
>
|
| 338 |
+
<textarea
|
| 339 |
+
value={input}
|
| 340 |
+
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => handleInputChange(e)}
|
| 341 |
+
onKeyDown={(e) => {
|
| 342 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 343 |
+
e.preventDefault();
|
| 344 |
+
if (input.trim()) (e.currentTarget.form)?.requestSubmit();
|
| 345 |
+
}
|
| 346 |
+
}}
|
| 347 |
+
placeholder={isPlanMode ? "Define an architecture or complex goal..." : "How can I help you today?"}
|
| 348 |
+
rows={1}
|
| 349 |
+
className="w-full bg-(--surface) text-(--text) text-sm rounded-2xl pl-11 pr-14 py-4 border border-(--border) focus:border-(--accent) focus:ring-4 focus:ring-(--accent) focus:ring-opacity-5 outline-none resize-none min-h-[56px] max-h-[240px] transition-all scrollbar-hide shadow-lg group-hover:border-(--border-subtle)"
|
| 350 |
+
style={{ height: "auto" }}
|
| 351 |
+
onInput={(e) => {
|
| 352 |
+
const target = e.target as HTMLTextAreaElement;
|
| 353 |
+
target.style.height = "auto";
|
| 354 |
+
target.style.height = `${Math.min(target.scrollHeight, 240)}px`;
|
| 355 |
+
}}
|
| 356 |
+
/>
|
| 357 |
+
<div className="absolute left-4 top-[17px] text-(--text-muted) transition-colors group-focus-within:text-(--accent)">
|
| 358 |
+
{isPlanMode ? <Wand2 size={20} className="text-(--warning)" /> : <MessageSquare size={20} />}
|
| 359 |
+
</div>
|
| 360 |
+
<button
|
| 361 |
+
type="submit"
|
| 362 |
+
disabled={isLoading || !input.trim()}
|
| 363 |
+
className="absolute right-2.5 top-2.5 w-[36px] h-[36px] rounded-xl bg-(--accent) text-white hover:scale-105 active:scale-95 disabled:opacity-20 disabled:grayscale disabled:cursor-not-allowed transition-all shadow-md flex items-center justify-center overflow-hidden"
|
| 364 |
+
>
|
| 365 |
+
{isLoading ? (
|
| 366 |
+
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
| 367 |
+
) : (
|
| 368 |
+
<Send size={18} className="translate-x-px -translate-y-px" />
|
| 369 |
+
)}
|
| 370 |
+
</button>
|
| 371 |
+
</form>
|
| 372 |
+
<div className="flex items-center justify-between px-1 mt-3.5">
|
| 373 |
+
<p className="text-[9px] text-(--text-muted) flex items-center gap-1.5 font-bold uppercase tracking-widest opacity-60">
|
| 374 |
+
<Check size={10} className="text-(--success)" /> Context Captured
|
| 375 |
+
</p>
|
| 376 |
+
<p className="text-[9px] text-(--text-muted) flex items-center gap-1.5 font-bold uppercase tracking-widest opacity-60">
|
| 377 |
+
Shift+Enter for multi-line
|
| 378 |
+
</p>
|
| 379 |
+
</div>
|
| 380 |
+
</div>
|
| 381 |
+
</motion.div>
|
| 382 |
+
)}
|
| 383 |
+
</AnimatePresence>
|
| 384 |
+
);
|
| 385 |
+
}
|
components/workspace/IDEClient.tsx
CHANGED
|
@@ -5,6 +5,8 @@ import dynamic from "next/dynamic";
|
|
| 5 |
import { useSearchParams } from "next/navigation";
|
| 6 |
import { Toaster, toast } from "sonner";
|
| 7 |
import { VSCodeFrame } from "@/components/workspace/VSCodeFrame";
|
|
|
|
|
|
|
| 8 |
import Link from "next/link";
|
| 9 |
import type { Session } from "next-auth";
|
| 10 |
|
|
@@ -14,15 +16,28 @@ const Dashboard = dynamic(() => import("@/components/dashboard/Dashboard"), { ss
|
|
| 14 |
export default function IDEClient({ session }: { session: Session | null }) {
|
| 15 |
const searchParams = useSearchParams();
|
| 16 |
const workspaceParam = searchParams?.get("workspace");
|
|
|
|
| 17 |
const [theme, setTheme] = useState<"dark" | "light">("dark");
|
| 18 |
const [refreshKey, setRefreshKey] = useState(0);
|
| 19 |
|
| 20 |
// Apply theme globally
|
| 21 |
useEffect(() => {
|
| 22 |
document.documentElement.setAttribute("data-theme", theme);
|
| 23 |
-
|
| 24 |
}, [theme]);
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
// If no specific workspace is requested, render the Firebase-style Dashboard Control Plane
|
| 27 |
if (!workspaceParam) {
|
| 28 |
return (
|
|
@@ -60,18 +75,19 @@ export default function IDEClient({ session }: { session: Session | null }) {
|
|
| 60 |
|
| 61 |
// Otherwise, render the dedicated VS Code Server instance mapped to this workspace
|
| 62 |
return (
|
| 63 |
-
<div data-theme={theme} className="h-dvh w-screen flex flex-col bg-(--bg) overflow-hidden">
|
| 64 |
-
<div className="h-10 flex items-center justify-between px-4 bg-(--activity-bar) border-b border-(--border-subtle) shrink-0">
|
| 65 |
<div className="flex items-center gap-3">
|
| 66 |
-
<Link href="/" className="text-(--accent) font-bold text-sm tracking-wide hover:opacity-80 transition-opacity">
|
| 67 |
-
|
|
|
|
| 68 |
</Link>
|
| 69 |
<div className="h-4 w-px bg-(--border) mx-1" />
|
| 70 |
<span className="text-xs text-(--text-muted) font-mono">Workspace: {workspaceParam}</span>
|
| 71 |
</div>
|
| 72 |
<div className="flex items-center gap-2">
|
| 73 |
{session?.user && (
|
| 74 |
-
<div className="flex items-center gap-2 mr-2 px-2 py-0.5 bg-(--border) rounded-full border border-(--border-subtle)">
|
| 75 |
<div className="w-4 h-4 rounded-full bg-(--accent) flex items-center justify-center text-[10px] text-white font-bold">
|
| 76 |
{session.user.name?.[0] || session.user.email?.[0] || "?"}
|
| 77 |
</div>
|
|
@@ -80,18 +96,41 @@ export default function IDEClient({ session }: { session: Session | null }) {
|
|
| 80 |
</span>
|
| 81 |
</div>
|
| 82 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
<button
|
| 84 |
onClick={handleRebuild}
|
| 85 |
className="px-3 py-1 text-xs font-semibold bg-(--border) hover:bg-(--border-subtle) text-(--text) rounded-md transition-colors border border-(--border-subtle) shadow-sm"
|
| 86 |
title="Apply codeverse.json changes and restart container"
|
| 87 |
>
|
| 88 |
-
↻ Rebuild
|
| 89 |
</button>
|
| 90 |
</div>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
-
<div className="flex-1 relative w-full h-full">
|
| 94 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
</div>
|
| 96 |
|
| 97 |
<Toaster position="bottom-right" theme={theme} richColors />
|
|
|
|
| 5 |
import { useSearchParams } from "next/navigation";
|
| 6 |
import { Toaster, toast } from "sonner";
|
| 7 |
import { VSCodeFrame } from "@/components/workspace/VSCodeFrame";
|
| 8 |
+
import { AIAssistantSidebar } from "@/components/workspace/AIAssistantSidebar";
|
| 9 |
+
import { Sparkles } from "lucide-react";
|
| 10 |
import Link from "next/link";
|
| 11 |
import type { Session } from "next-auth";
|
| 12 |
|
|
|
|
| 16 |
export default function IDEClient({ session }: { session: Session | null }) {
|
| 17 |
const searchParams = useSearchParams();
|
| 18 |
const workspaceParam = searchParams?.get("workspace");
|
| 19 |
+
const [isAiOpen, setIsAiOpen] = useState(false);
|
| 20 |
const [theme, setTheme] = useState<"dark" | "light">("dark");
|
| 21 |
const [refreshKey, setRefreshKey] = useState(0);
|
| 22 |
|
| 23 |
// Apply theme globally
|
| 24 |
useEffect(() => {
|
| 25 |
document.documentElement.setAttribute("data-theme", theme);
|
| 26 |
+
// Cleanup if needed
|
| 27 |
}, [theme]);
|
| 28 |
|
| 29 |
+
// Keyboard shortcut for AI
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
const handleKeyDown = (e: KeyboardEvent) => {
|
| 32 |
+
if ((e.ctrlKey || e.metaKey) && e.key === "i") {
|
| 33 |
+
e.preventDefault();
|
| 34 |
+
setIsAiOpen(prev => !prev);
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
window.addEventListener("keydown", handleKeyDown);
|
| 38 |
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
| 39 |
+
}, []);
|
| 40 |
+
|
| 41 |
// If no specific workspace is requested, render the Firebase-style Dashboard Control Plane
|
| 42 |
if (!workspaceParam) {
|
| 43 |
return (
|
|
|
|
| 75 |
|
| 76 |
// Otherwise, render the dedicated VS Code Server instance mapped to this workspace
|
| 77 |
return (
|
| 78 |
+
<div data-theme={theme} className="h-dvh w-screen flex flex-col bg-(--bg) overflow-hidden relative">
|
| 79 |
+
<div className="h-10 flex items-center justify-between px-4 bg-(--activity-bar) border-b border-(--border-subtle) shrink-0 z-40">
|
| 80 |
<div className="flex items-center gap-3">
|
| 81 |
+
<Link href="/" className="text-(--accent) font-bold text-sm tracking-wide hover:opacity-80 transition-opacity flex items-center gap-1.5">
|
| 82 |
+
<div className="w-5 h-5 rounded bg-(--accent) flex items-center justify-center text-white text-[10px]">⬡</div>
|
| 83 |
+
CodeVerse
|
| 84 |
</Link>
|
| 85 |
<div className="h-4 w-px bg-(--border) mx-1" />
|
| 86 |
<span className="text-xs text-(--text-muted) font-mono">Workspace: {workspaceParam}</span>
|
| 87 |
</div>
|
| 88 |
<div className="flex items-center gap-2">
|
| 89 |
{session?.user && (
|
| 90 |
+
<div className="hidden md:flex items-center gap-2 mr-2 px-2 py-0.5 bg-(--border) rounded-full border border-(--border-subtle)">
|
| 91 |
<div className="w-4 h-4 rounded-full bg-(--accent) flex items-center justify-center text-[10px] text-white font-bold">
|
| 92 |
{session.user.name?.[0] || session.user.email?.[0] || "?"}
|
| 93 |
</div>
|
|
|
|
| 96 |
</span>
|
| 97 |
</div>
|
| 98 |
)}
|
| 99 |
+
|
| 100 |
+
<button
|
| 101 |
+
onClick={() => setIsAiOpen(!isAiOpen)}
|
| 102 |
+
className={`flex items-center gap-1.5 px-3 py-1 text-xs font-bold rounded-md transition-all border ${
|
| 103 |
+
isAiOpen
|
| 104 |
+
? "bg-(--accent) text-white border-(--accent) shadow-lg shadow-(--accent)/20"
|
| 105 |
+
: "bg-(--border) hover:bg-(--border-subtle) text-(--text) border-(--border-subtle)"
|
| 106 |
+
}`}
|
| 107 |
+
>
|
| 108 |
+
<Sparkles className={`w-3.5 h-3.5 ${isAiOpen ? "animate-pulse" : ""}`} />
|
| 109 |
+
{isAiOpen ? "AI Active" : "AI Studio"}
|
| 110 |
+
</button>
|
| 111 |
+
|
| 112 |
+
<div className="w-px h-4 bg-(--border) mx-1" />
|
| 113 |
+
|
| 114 |
<button
|
| 115 |
onClick={handleRebuild}
|
| 116 |
className="px-3 py-1 text-xs font-semibold bg-(--border) hover:bg-(--border-subtle) text-(--text) rounded-md transition-colors border border-(--border-subtle) shadow-sm"
|
| 117 |
title="Apply codeverse.json changes and restart container"
|
| 118 |
>
|
| 119 |
+
↻ Rebuild
|
| 120 |
</button>
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
|
| 124 |
+
<div className="flex-1 flex relative w-full h-full overflow-hidden">
|
| 125 |
+
<main className={`flex-1 relative transition-all duration-300 ${isAiOpen ? "mr-0 md:mr-80 lg:mr-96" : "mr-0"}`}>
|
| 126 |
+
<VSCodeFrame key={refreshKey} workspaceId={workspaceParam} />
|
| 127 |
+
</main>
|
| 128 |
+
|
| 129 |
+
<AIAssistantSidebar
|
| 130 |
+
workspaceName={workspaceParam}
|
| 131 |
+
isOpen={isAiOpen}
|
| 132 |
+
onClose={() => setIsAiOpen(false)}
|
| 133 |
+
/>
|
| 134 |
</div>
|
| 135 |
|
| 136 |
<Toaster position="bottom-right" theme={theme} richColors />
|
lib/db/schema.ts
CHANGED
|
@@ -78,6 +78,18 @@ CREATE TABLE IF NOT EXISTS user_settings (
|
|
| 78 |
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 79 |
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 80 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
`;
|
| 82 |
|
| 83 |
export async function initDb() {
|
|
|
|
| 78 |
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 79 |
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 80 |
);
|
| 81 |
+
|
| 82 |
+
CREATE TABLE IF NOT EXISTS chat_history (
|
| 83 |
+
id TEXT PRIMARY KEY,
|
| 84 |
+
user_id TEXT NOT NULL,
|
| 85 |
+
workspace_id TEXT NOT NULL,
|
| 86 |
+
role TEXT NOT NULL,
|
| 87 |
+
content TEXT NOT NULL,
|
| 88 |
+
tool_invocations TEXT,
|
| 89 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 90 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 91 |
+
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
|
| 92 |
+
);
|
| 93 |
`;
|
| 94 |
|
| 95 |
export async function initDb() {
|
lib/docker/manager.ts
CHANGED
|
@@ -7,7 +7,9 @@ const docker = new Docker({ socketPath: process.platform === 'win32' ? '//./pipe
|
|
| 7 |
|
| 8 |
export interface WorkspaceConfig {
|
| 9 |
id: string;
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
withAndroidEmulator?: boolean;
|
| 12 |
onLog?: (msg: string) => void;
|
| 13 |
}
|
|
@@ -17,7 +19,7 @@ export interface WorkspaceConfig {
|
|
| 17 |
* Optionally spins up a sidecar Android emulator container.
|
| 18 |
*/
|
| 19 |
export async function startWorkspaceContainer(config: WorkspaceConfig) {
|
| 20 |
-
const { id, withAndroidEmulator = false, onLog = console.log } = config;
|
| 21 |
const containerName = `codeverse-workspace-${id}`;
|
| 22 |
const androidContainerName = `codeverse-android-${id}`;
|
| 23 |
|
|
@@ -44,7 +46,8 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
|
|
| 44 |
}
|
| 45 |
|
| 46 |
// Map the local host path to the workspace
|
| 47 |
-
const
|
|
|
|
| 48 |
|
| 49 |
// --- WORKSPACE CONFIG LOGIC AND IMAGE BUILDING ---
|
| 50 |
const { buildWorkspaceImage } = await import('./builder');
|
|
@@ -161,7 +164,8 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
|
|
| 161 |
if (!appetizeUrl) {
|
| 162 |
try {
|
| 163 |
const fs = await import('fs/promises');
|
| 164 |
-
const
|
|
|
|
| 165 |
const configPath = path.join(dataPath, 'codeverse.json');
|
| 166 |
const configContent = await fs.readFile(configPath, 'utf8');
|
| 167 |
const customConfig = JSON.parse(configContent);
|
|
|
|
| 7 |
|
| 8 |
export interface WorkspaceConfig {
|
| 9 |
id: string;
|
| 10 |
+
userId: string;
|
| 11 |
+
projectName: string;
|
| 12 |
+
image?: string;
|
| 13 |
withAndroidEmulator?: boolean;
|
| 14 |
onLog?: (msg: string) => void;
|
| 15 |
}
|
|
|
|
| 19 |
* Optionally spins up a sidecar Android emulator container.
|
| 20 |
*/
|
| 21 |
export async function startWorkspaceContainer(config: WorkspaceConfig) {
|
| 22 |
+
const { id, userId, projectName, withAndroidEmulator = false, onLog = console.log } = config;
|
| 23 |
const containerName = `codeverse-workspace-${id}`;
|
| 24 |
const androidContainerName = `codeverse-android-${id}`;
|
| 25 |
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
// Map the local host path to the workspace
|
| 49 |
+
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 50 |
+
const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
|
| 51 |
|
| 52 |
// --- WORKSPACE CONFIG LOGIC AND IMAGE BUILDING ---
|
| 53 |
const { buildWorkspaceImage } = await import('./builder');
|
|
|
|
| 164 |
if (!appetizeUrl) {
|
| 165 |
try {
|
| 166 |
const fs = await import('fs/promises');
|
| 167 |
+
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
|
| 168 |
+
const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
|
| 169 |
const configPath = path.join(dataPath, 'codeverse.json');
|
| 170 |
const configContent = await fs.readFile(configPath, 'utf8');
|
| 171 |
const customConfig = JSON.parse(configContent);
|
lib/fs/isolation.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from "path";
|
| 2 |
+
import fs from "fs/promises";
|
| 3 |
+
|
| 4 |
+
const WORKSPACE_BASE = path.join(process.cwd(), "workspaces");
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Returns the root directory for a specific user's workspaces.
|
| 8 |
+
* e.g., /path/to/codeverse/workspaces/{userId}
|
| 9 |
+
*/
|
| 10 |
+
export async function getUserWorkspaceRoot(userId: string): Promise<string> {
|
| 11 |
+
const userRoot = path.join(WORKSPACE_BASE, userId);
|
| 12 |
+
await fs.mkdir(userRoot, { recursive: true });
|
| 13 |
+
return userRoot;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Resolves a safe, isolated path within a specific project in a user's workspace.
|
| 18 |
+
* Prevents project-level directory traversal.
|
| 19 |
+
*/
|
| 20 |
+
export async function resolveSafeProjectPath(userId: string, projectName: string, subPath: string = ""): Promise<string> {
|
| 21 |
+
const userRoot = await getUserWorkspaceRoot(userId);
|
| 22 |
+
const projectRoot = path.resolve(userRoot, projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60));
|
| 23 |
+
|
| 24 |
+
// Normalize and resolve the absolute path
|
| 25 |
+
const targetPath = path.resolve(projectRoot, subPath);
|
| 26 |
+
|
| 27 |
+
// Security Check: Ensure the resolved path is still within the project root
|
| 28 |
+
if (!targetPath.startsWith(projectRoot)) {
|
| 29 |
+
throw new Error("Security Violation: Path traversal detected.");
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return targetPath;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Resolves a safe, isolated path directly within a user's root workspace (e.g. for listing project names).
|
| 37 |
+
*/
|
| 38 |
+
export async function resolveSafePath(userId: string, subPath: string): Promise<string> {
|
| 39 |
+
const userRoot = await getUserWorkspaceRoot(userId);
|
| 40 |
+
|
| 41 |
+
// Normalize and resolve the absolute path
|
| 42 |
+
const targetPath = path.resolve(userRoot, subPath);
|
| 43 |
+
|
| 44 |
+
// Security Check: Ensure the resolved path is still within the user's root
|
| 45 |
+
if (!targetPath.startsWith(userRoot)) {
|
| 46 |
+
throw new Error("Security Violation: Path traversal detected.");
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return targetPath;
|
| 50 |
+
}
|
lib/git/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git';
|
| 2 |
|
| 3 |
-
const WORKSPACE_ROOT = process.cwd();
|
| 4 |
-
|
| 5 |
const options: Partial<SimpleGitOptions> = {
|
| 6 |
-
baseDir: WORKSPACE_ROOT,
|
| 7 |
binary: 'git',
|
| 8 |
maxConcurrentProcesses: 6,
|
| 9 |
trimmed: false,
|
| 10 |
};
|
| 11 |
|
| 12 |
-
export
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
export async function getGitStatus() {
|
| 15 |
-
const
|
|
|
|
| 16 |
if (!isRepo) return null;
|
| 17 |
|
| 18 |
-
const status = await
|
| 19 |
return {
|
| 20 |
currentBranch: status.current,
|
| 21 |
modified: status.modified,
|
|
@@ -28,38 +28,45 @@ export async function getGitStatus() {
|
|
| 28 |
};
|
| 29 |
}
|
| 30 |
|
| 31 |
-
export async function getBranchList() {
|
| 32 |
-
const
|
|
|
|
| 33 |
return {
|
| 34 |
all: branches.all,
|
| 35 |
current: branches.current
|
| 36 |
};
|
| 37 |
}
|
| 38 |
|
| 39 |
-
export async function commitFiles(message: string, files: string[] = []) {
|
|
|
|
| 40 |
if (files.length > 0) {
|
| 41 |
-
await
|
| 42 |
} else {
|
| 43 |
-
await
|
| 44 |
}
|
| 45 |
-
return
|
| 46 |
}
|
| 47 |
|
| 48 |
-
export async function getFileDiff(file: string) {
|
| 49 |
-
return
|
| 50 |
}
|
| 51 |
|
| 52 |
-
export async function pushBranch() {
|
| 53 |
-
return
|
| 54 |
}
|
| 55 |
|
| 56 |
-
export async function pullBranch() {
|
| 57 |
-
return
|
| 58 |
}
|
| 59 |
|
| 60 |
-
export async function checkoutBranch(branch: string, create: boolean = false) {
|
|
|
|
| 61 |
if (create) {
|
| 62 |
-
return
|
| 63 |
}
|
| 64 |
-
return
|
| 65 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git';
|
| 2 |
|
|
|
|
|
|
|
| 3 |
const options: Partial<SimpleGitOptions> = {
|
|
|
|
| 4 |
binary: 'git',
|
| 5 |
maxConcurrentProcesses: 6,
|
| 6 |
trimmed: false,
|
| 7 |
};
|
| 8 |
|
| 9 |
+
export function getGit(baseDir: string = process.cwd()): SimpleGit {
|
| 10 |
+
return simpleGit({ ...options, baseDir });
|
| 11 |
+
}
|
| 12 |
|
| 13 |
+
export async function getGitStatus(baseDir?: string) {
|
| 14 |
+
const instance = getGit(baseDir);
|
| 15 |
+
const isRepo = await instance.checkIsRepo();
|
| 16 |
if (!isRepo) return null;
|
| 17 |
|
| 18 |
+
const status = await instance.status();
|
| 19 |
return {
|
| 20 |
currentBranch: status.current,
|
| 21 |
modified: status.modified,
|
|
|
|
| 28 |
};
|
| 29 |
}
|
| 30 |
|
| 31 |
+
export async function getBranchList(baseDir?: string) {
|
| 32 |
+
const instance = getGit(baseDir);
|
| 33 |
+
const branches = await instance.branch();
|
| 34 |
return {
|
| 35 |
all: branches.all,
|
| 36 |
current: branches.current
|
| 37 |
};
|
| 38 |
}
|
| 39 |
|
| 40 |
+
export async function commitFiles(message: string, files: string[] = [], baseDir?: string) {
|
| 41 |
+
const instance = getGit(baseDir);
|
| 42 |
if (files.length > 0) {
|
| 43 |
+
await instance.add(files);
|
| 44 |
} else {
|
| 45 |
+
await instance.add('.');
|
| 46 |
}
|
| 47 |
+
return instance.commit(message);
|
| 48 |
}
|
| 49 |
|
| 50 |
+
export async function getFileDiff(file: string, baseDir?: string) {
|
| 51 |
+
return getGit(baseDir).diff([file]);
|
| 52 |
}
|
| 53 |
|
| 54 |
+
export async function pushBranch(baseDir?: string) {
|
| 55 |
+
return getGit(baseDir).push();
|
| 56 |
}
|
| 57 |
|
| 58 |
+
export async function pullBranch(baseDir?: string) {
|
| 59 |
+
return getGit(baseDir).pull();
|
| 60 |
}
|
| 61 |
|
| 62 |
+
export async function checkoutBranch(branch: string, create: boolean = false, baseDir?: string) {
|
| 63 |
+
const instance = getGit(baseDir);
|
| 64 |
if (create) {
|
| 65 |
+
return instance.checkoutLocalBranch(branch);
|
| 66 |
}
|
| 67 |
+
return instance.checkout(branch);
|
| 68 |
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
// Export for legacy access if needed, but discouraged
|
| 72 |
+
// export { git } from 'simple-git';
|
lib/mcp/tools.ts
CHANGED
|
@@ -1,89 +1,118 @@
|
|
| 1 |
-
|
| 2 |
-
import {
|
| 3 |
import { z } from "zod";
|
| 4 |
import fs from "fs/promises";
|
| 5 |
import path from "path";
|
| 6 |
import { exec } from "child_process";
|
| 7 |
import { promisify } from "util";
|
|
|
|
| 8 |
|
| 9 |
const execAsync = promisify(exec);
|
| 10 |
-
const cwd = process.cwd();
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
-
}),
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
}
|
| 46 |
},
|
| 47 |
-
}),
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
-
|
| 64 |
-
return { success: true, stdout, stderr };
|
| 65 |
-
} catch (e: unknown) {
|
| 66 |
-
return { success: false, error: (e as Error).message, stderr: (e as { stderr?: string }).stderr };
|
| 67 |
-
}
|
| 68 |
},
|
| 69 |
-
}),
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
| 87 |
},
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { zodSchema } from "ai";
|
| 2 |
+
import type { ToolSet } from "ai";
|
| 3 |
import { z } from "zod";
|
| 4 |
import fs from "fs/promises";
|
| 5 |
import path from "path";
|
| 6 |
import { exec } from "child_process";
|
| 7 |
import { promisify } from "util";
|
| 8 |
+
import { resolveSafeProjectPath } from "../fs/isolation";
|
| 9 |
|
| 10 |
const execAsync = promisify(exec);
|
|
|
|
| 11 |
|
| 12 |
+
/**
|
| 13 |
+
* Factory that creates user-isolated workspace tools for the AI agent.
|
| 14 |
+
* Each tool is sandboxed to workspaces/{userId}/{workspaceName}/
|
| 15 |
+
*
|
| 16 |
+
* Note: Tools are built as plain objects using `zodSchema()` for `inputSchema`
|
| 17 |
+
* instead of the `tool()` helper, because `tool()` in AI SDK v6 has broken
|
| 18 |
+
* TypeScript overloads when `execute` returns union types in strict mode.
|
| 19 |
+
*/
|
| 20 |
+
export function createTools(userId: string, workspaceName: string): ToolSet {
|
| 21 |
+
return {
|
| 22 |
+
read_file: {
|
| 23 |
+
description: "Read the full content of a file within the workspace",
|
| 24 |
+
inputSchema: zodSchema(z.object({
|
| 25 |
+
filePath: z.string().describe("Relative path from workspace root"),
|
| 26 |
+
})),
|
| 27 |
+
execute: async ({ filePath }: { filePath: string }) => {
|
| 28 |
+
try {
|
| 29 |
+
const fullPath = await resolveSafeProjectPath(userId, workspaceName, filePath);
|
| 30 |
+
const content = await fs.readFile(fullPath, "utf-8");
|
| 31 |
+
return { success: true, content };
|
| 32 |
+
} catch (e) {
|
| 33 |
+
return { success: false, error: (e as Error).message };
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
},
|
|
|
|
| 37 |
|
| 38 |
+
write_file: {
|
| 39 |
+
description: "Write/overwrite a file within the workspace",
|
| 40 |
+
inputSchema: zodSchema(z.object({
|
| 41 |
+
filePath: z.string().describe("Relative path to the file"),
|
| 42 |
+
content: z.string().describe("Full file content to write"),
|
| 43 |
+
})),
|
| 44 |
+
execute: async ({ filePath, content }: { filePath: string; content: string }) => {
|
| 45 |
+
try {
|
| 46 |
+
const fullPath = await resolveSafeProjectPath(userId, workspaceName, filePath);
|
| 47 |
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
| 48 |
+
await fs.writeFile(fullPath, content, "utf-8");
|
| 49 |
+
return { success: true, path: filePath };
|
| 50 |
+
} catch (e) {
|
| 51 |
+
return { success: false, error: (e as Error).message };
|
| 52 |
+
}
|
| 53 |
+
},
|
|
|
|
| 54 |
},
|
|
|
|
| 55 |
|
| 56 |
+
terminal_command: {
|
| 57 |
+
description: "Run a shell command inside the workspace directory",
|
| 58 |
+
inputSchema: zodSchema(z.object({
|
| 59 |
+
command: z.string().describe("Shell command to execute"),
|
| 60 |
+
background: z.boolean().optional().describe("Run async without capturing output"),
|
| 61 |
+
})),
|
| 62 |
+
execute: async ({ command, background }: { command: string; background?: boolean }) => {
|
| 63 |
+
try {
|
| 64 |
+
const cwd = await resolveSafeProjectPath(userId, workspaceName);
|
| 65 |
+
if (background) {
|
| 66 |
+
exec(command, { cwd });
|
| 67 |
+
return { success: true, output: "Started in background" };
|
| 68 |
+
}
|
| 69 |
+
const { stdout, stderr } = await execAsync(command, { cwd });
|
| 70 |
+
return { success: true, stdout, stderr };
|
| 71 |
+
} catch (e) {
|
| 72 |
+
return { success: false, error: (e as Error).message };
|
| 73 |
}
|
| 74 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
},
|
|
|
|
| 76 |
|
| 77 |
+
search_code: {
|
| 78 |
+
description: "Search for a text pattern across all workspace files",
|
| 79 |
+
inputSchema: zodSchema(z.object({
|
| 80 |
+
pattern: z.string().describe("Literal string or regex pattern"),
|
| 81 |
+
})),
|
| 82 |
+
execute: async ({ pattern }: { pattern: string }) => {
|
| 83 |
+
try {
|
| 84 |
+
const cwd = await resolveSafeProjectPath(userId, workspaceName);
|
| 85 |
+
const { stdout } = await execAsync(
|
| 86 |
+
`git grep -n "${pattern}" 2>/dev/null || grep -rn "${pattern}" . 2>/dev/null || true`,
|
| 87 |
+
{ cwd }
|
| 88 |
+
);
|
| 89 |
+
return { success: true, matches: stdout.split("\n").filter(Boolean) };
|
| 90 |
+
} catch (e) {
|
| 91 |
+
return { success: false, error: (e as Error).message };
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
},
|
| 95 |
+
|
| 96 |
+
list_files: {
|
| 97 |
+
description: "List files in the project to understand its structure",
|
| 98 |
+
inputSchema: zodSchema(z.object({
|
| 99 |
+
recursive: z.boolean().optional().describe("Recursive listing (default: true)"),
|
| 100 |
+
})),
|
| 101 |
+
execute: async ({ recursive }: { recursive?: boolean }) => {
|
| 102 |
+
const isRecursive = recursive !== false;
|
| 103 |
+
try {
|
| 104 |
+
const cwd = await resolveSafeProjectPath(userId, workspaceName);
|
| 105 |
+
const cmd = isRecursive
|
| 106 |
+
? "find . -maxdepth 4 -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/.next/*'"
|
| 107 |
+
: "ls -F";
|
| 108 |
+
const { stdout } = await execAsync(cmd, { cwd });
|
| 109 |
+
return { success: true, files: stdout.split("\n").filter(Boolean) };
|
| 110 |
+
} catch (e) {
|
| 111 |
+
return { success: false, error: (e as Error).message };
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
},
|
| 115 |
+
} as ToolSet;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export type WorkspaceTools = ReturnType<typeof createTools>;
|
package-lock.json
CHANGED
|
@@ -45,6 +45,7 @@
|
|
| 45 |
"react": "19.2.3",
|
| 46 |
"react-dom": "19.2.3",
|
| 47 |
"react-markdown": "^10.1.0",
|
|
|
|
| 48 |
"recharts": "^3.7.0",
|
| 49 |
"remark-gfm": "^4.0.1",
|
| 50 |
"simple-git": "^3.32.3",
|
|
@@ -67,6 +68,7 @@
|
|
| 67 |
"@types/node": "^20",
|
| 68 |
"@types/react": "^19",
|
| 69 |
"@types/react-dom": "^19",
|
|
|
|
| 70 |
"@types/tar-fs": "^2.0.4",
|
| 71 |
"eslint": "^9",
|
| 72 |
"eslint-config-next": "16.1.6",
|
|
@@ -437,6 +439,15 @@
|
|
| 437 |
"node": ">=6.0.0"
|
| 438 |
}
|
| 439 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
"node_modules/@babel/template": {
|
| 441 |
"version": "7.28.6",
|
| 442 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
|
@@ -3726,6 +3737,12 @@
|
|
| 3726 |
"undici-types": "~6.21.0"
|
| 3727 |
}
|
| 3728 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3729 |
"node_modules/@types/react": {
|
| 3730 |
"version": "19.2.14",
|
| 3731 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
|
@@ -3747,6 +3764,16 @@
|
|
| 3747 |
"@types/react": "^19.2.0"
|
| 3748 |
}
|
| 3749 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3750 |
"node_modules/@types/ssh2": {
|
| 3751 |
"version": "1.15.5",
|
| 3752 |
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
|
|
@@ -6933,6 +6960,19 @@
|
|
| 6933 |
"reusify": "^1.0.4"
|
| 6934 |
}
|
| 6935 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6936 |
"node_modules/fetch-blob": {
|
| 6937 |
"version": "3.2.0",
|
| 6938 |
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
|
@@ -7057,6 +7097,14 @@
|
|
| 7057 |
"url": "https://github.com/sponsors/ljharb"
|
| 7058 |
}
|
| 7059 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7060 |
"node_modules/formdata-polyfill": {
|
| 7061 |
"version": "4.0.10",
|
| 7062 |
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
|
@@ -7434,6 +7482,19 @@
|
|
| 7434 |
"node": ">= 0.4"
|
| 7435 |
}
|
| 7436 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7437 |
"node_modules/hast-util-to-jsx-runtime": {
|
| 7438 |
"version": "2.3.6",
|
| 7439 |
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
|
@@ -7474,6 +7535,23 @@
|
|
| 7474 |
"url": "https://opencollective.com/unified"
|
| 7475 |
}
|
| 7476 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7477 |
"node_modules/hermes-estree": {
|
| 7478 |
"version": "0.25.1",
|
| 7479 |
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
|
@@ -7491,6 +7569,21 @@
|
|
| 7491 |
"hermes-estree": "0.25.1"
|
| 7492 |
}
|
| 7493 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7494 |
"node_modules/hono": {
|
| 7495 |
"version": "4.12.3",
|
| 7496 |
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
|
|
@@ -8749,6 +8842,20 @@
|
|
| 8749 |
"loose-envify": "cli.js"
|
| 8750 |
}
|
| 8751 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8752 |
"node_modules/lru-cache": {
|
| 8753 |
"version": "5.1.1",
|
| 8754 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
@@ -10504,6 +10611,15 @@
|
|
| 10504 |
"node": ">= 0.8.0"
|
| 10505 |
}
|
| 10506 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10507 |
"node_modules/promise-limit": {
|
| 10508 |
"version": "2.7.0",
|
| 10509 |
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
|
|
@@ -10799,6 +10915,26 @@
|
|
| 10799 |
}
|
| 10800 |
}
|
| 10801 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10802 |
"node_modules/readable-stream": {
|
| 10803 |
"version": "3.6.2",
|
| 10804 |
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
|
@@ -10882,6 +11018,22 @@
|
|
| 10882 |
"url": "https://github.com/sponsors/ljharb"
|
| 10883 |
}
|
| 10884 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10885 |
"node_modules/regexp.prototype.flags": {
|
| 10886 |
"version": "1.5.4",
|
| 10887 |
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
|
|
|
| 45 |
"react": "19.2.3",
|
| 46 |
"react-dom": "19.2.3",
|
| 47 |
"react-markdown": "^10.1.0",
|
| 48 |
+
"react-syntax-highlighter": "^16.1.1",
|
| 49 |
"recharts": "^3.7.0",
|
| 50 |
"remark-gfm": "^4.0.1",
|
| 51 |
"simple-git": "^3.32.3",
|
|
|
|
| 68 |
"@types/node": "^20",
|
| 69 |
"@types/react": "^19",
|
| 70 |
"@types/react-dom": "^19",
|
| 71 |
+
"@types/react-syntax-highlighter": "^15.5.13",
|
| 72 |
"@types/tar-fs": "^2.0.4",
|
| 73 |
"eslint": "^9",
|
| 74 |
"eslint-config-next": "16.1.6",
|
|
|
|
| 439 |
"node": ">=6.0.0"
|
| 440 |
}
|
| 441 |
},
|
| 442 |
+
"node_modules/@babel/runtime": {
|
| 443 |
+
"version": "7.29.2",
|
| 444 |
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
| 445 |
+
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
| 446 |
+
"license": "MIT",
|
| 447 |
+
"engines": {
|
| 448 |
+
"node": ">=6.9.0"
|
| 449 |
+
}
|
| 450 |
+
},
|
| 451 |
"node_modules/@babel/template": {
|
| 452 |
"version": "7.28.6",
|
| 453 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
|
|
|
| 3737 |
"undici-types": "~6.21.0"
|
| 3738 |
}
|
| 3739 |
},
|
| 3740 |
+
"node_modules/@types/prismjs": {
|
| 3741 |
+
"version": "1.26.6",
|
| 3742 |
+
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
|
| 3743 |
+
"integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
|
| 3744 |
+
"license": "MIT"
|
| 3745 |
+
},
|
| 3746 |
"node_modules/@types/react": {
|
| 3747 |
"version": "19.2.14",
|
| 3748 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
|
|
|
| 3764 |
"@types/react": "^19.2.0"
|
| 3765 |
}
|
| 3766 |
},
|
| 3767 |
+
"node_modules/@types/react-syntax-highlighter": {
|
| 3768 |
+
"version": "15.5.13",
|
| 3769 |
+
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
| 3770 |
+
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
| 3771 |
+
"dev": true,
|
| 3772 |
+
"license": "MIT",
|
| 3773 |
+
"dependencies": {
|
| 3774 |
+
"@types/react": "*"
|
| 3775 |
+
}
|
| 3776 |
+
},
|
| 3777 |
"node_modules/@types/ssh2": {
|
| 3778 |
"version": "1.15.5",
|
| 3779 |
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
|
|
|
|
| 6960 |
"reusify": "^1.0.4"
|
| 6961 |
}
|
| 6962 |
},
|
| 6963 |
+
"node_modules/fault": {
|
| 6964 |
+
"version": "1.0.4",
|
| 6965 |
+
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
| 6966 |
+
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
| 6967 |
+
"license": "MIT",
|
| 6968 |
+
"dependencies": {
|
| 6969 |
+
"format": "^0.2.0"
|
| 6970 |
+
},
|
| 6971 |
+
"funding": {
|
| 6972 |
+
"type": "github",
|
| 6973 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6974 |
+
}
|
| 6975 |
+
},
|
| 6976 |
"node_modules/fetch-blob": {
|
| 6977 |
"version": "3.2.0",
|
| 6978 |
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
|
|
|
| 7097 |
"url": "https://github.com/sponsors/ljharb"
|
| 7098 |
}
|
| 7099 |
},
|
| 7100 |
+
"node_modules/format": {
|
| 7101 |
+
"version": "0.2.2",
|
| 7102 |
+
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
| 7103 |
+
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
| 7104 |
+
"engines": {
|
| 7105 |
+
"node": ">=0.4.x"
|
| 7106 |
+
}
|
| 7107 |
+
},
|
| 7108 |
"node_modules/formdata-polyfill": {
|
| 7109 |
"version": "4.0.10",
|
| 7110 |
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
|
|
|
| 7482 |
"node": ">= 0.4"
|
| 7483 |
}
|
| 7484 |
},
|
| 7485 |
+
"node_modules/hast-util-parse-selector": {
|
| 7486 |
+
"version": "4.0.0",
|
| 7487 |
+
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
| 7488 |
+
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
| 7489 |
+
"license": "MIT",
|
| 7490 |
+
"dependencies": {
|
| 7491 |
+
"@types/hast": "^3.0.0"
|
| 7492 |
+
},
|
| 7493 |
+
"funding": {
|
| 7494 |
+
"type": "opencollective",
|
| 7495 |
+
"url": "https://opencollective.com/unified"
|
| 7496 |
+
}
|
| 7497 |
+
},
|
| 7498 |
"node_modules/hast-util-to-jsx-runtime": {
|
| 7499 |
"version": "2.3.6",
|
| 7500 |
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
|
|
|
| 7535 |
"url": "https://opencollective.com/unified"
|
| 7536 |
}
|
| 7537 |
},
|
| 7538 |
+
"node_modules/hastscript": {
|
| 7539 |
+
"version": "9.0.1",
|
| 7540 |
+
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
| 7541 |
+
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
| 7542 |
+
"license": "MIT",
|
| 7543 |
+
"dependencies": {
|
| 7544 |
+
"@types/hast": "^3.0.0",
|
| 7545 |
+
"comma-separated-tokens": "^2.0.0",
|
| 7546 |
+
"hast-util-parse-selector": "^4.0.0",
|
| 7547 |
+
"property-information": "^7.0.0",
|
| 7548 |
+
"space-separated-tokens": "^2.0.0"
|
| 7549 |
+
},
|
| 7550 |
+
"funding": {
|
| 7551 |
+
"type": "opencollective",
|
| 7552 |
+
"url": "https://opencollective.com/unified"
|
| 7553 |
+
}
|
| 7554 |
+
},
|
| 7555 |
"node_modules/hermes-estree": {
|
| 7556 |
"version": "0.25.1",
|
| 7557 |
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
|
|
|
| 7569 |
"hermes-estree": "0.25.1"
|
| 7570 |
}
|
| 7571 |
},
|
| 7572 |
+
"node_modules/highlight.js": {
|
| 7573 |
+
"version": "10.7.3",
|
| 7574 |
+
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
| 7575 |
+
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
| 7576 |
+
"license": "BSD-3-Clause",
|
| 7577 |
+
"engines": {
|
| 7578 |
+
"node": "*"
|
| 7579 |
+
}
|
| 7580 |
+
},
|
| 7581 |
+
"node_modules/highlightjs-vue": {
|
| 7582 |
+
"version": "1.0.0",
|
| 7583 |
+
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
| 7584 |
+
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
| 7585 |
+
"license": "CC0-1.0"
|
| 7586 |
+
},
|
| 7587 |
"node_modules/hono": {
|
| 7588 |
"version": "4.12.3",
|
| 7589 |
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
|
|
|
|
| 8842 |
"loose-envify": "cli.js"
|
| 8843 |
}
|
| 8844 |
},
|
| 8845 |
+
"node_modules/lowlight": {
|
| 8846 |
+
"version": "1.20.0",
|
| 8847 |
+
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
| 8848 |
+
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
| 8849 |
+
"license": "MIT",
|
| 8850 |
+
"dependencies": {
|
| 8851 |
+
"fault": "^1.0.0",
|
| 8852 |
+
"highlight.js": "~10.7.0"
|
| 8853 |
+
},
|
| 8854 |
+
"funding": {
|
| 8855 |
+
"type": "github",
|
| 8856 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 8857 |
+
}
|
| 8858 |
+
},
|
| 8859 |
"node_modules/lru-cache": {
|
| 8860 |
"version": "5.1.1",
|
| 8861 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
|
|
| 10611 |
"node": ">= 0.8.0"
|
| 10612 |
}
|
| 10613 |
},
|
| 10614 |
+
"node_modules/prismjs": {
|
| 10615 |
+
"version": "1.30.0",
|
| 10616 |
+
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
| 10617 |
+
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
| 10618 |
+
"license": "MIT",
|
| 10619 |
+
"engines": {
|
| 10620 |
+
"node": ">=6"
|
| 10621 |
+
}
|
| 10622 |
+
},
|
| 10623 |
"node_modules/promise-limit": {
|
| 10624 |
"version": "2.7.0",
|
| 10625 |
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
|
|
|
|
| 10915 |
}
|
| 10916 |
}
|
| 10917 |
},
|
| 10918 |
+
"node_modules/react-syntax-highlighter": {
|
| 10919 |
+
"version": "16.1.1",
|
| 10920 |
+
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz",
|
| 10921 |
+
"integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==",
|
| 10922 |
+
"license": "MIT",
|
| 10923 |
+
"dependencies": {
|
| 10924 |
+
"@babel/runtime": "^7.28.4",
|
| 10925 |
+
"highlight.js": "^10.4.1",
|
| 10926 |
+
"highlightjs-vue": "^1.0.0",
|
| 10927 |
+
"lowlight": "^1.17.0",
|
| 10928 |
+
"prismjs": "^1.30.0",
|
| 10929 |
+
"refractor": "^5.0.0"
|
| 10930 |
+
},
|
| 10931 |
+
"engines": {
|
| 10932 |
+
"node": ">= 16.20.2"
|
| 10933 |
+
},
|
| 10934 |
+
"peerDependencies": {
|
| 10935 |
+
"react": ">= 0.14.0"
|
| 10936 |
+
}
|
| 10937 |
+
},
|
| 10938 |
"node_modules/readable-stream": {
|
| 10939 |
"version": "3.6.2",
|
| 10940 |
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
|
|
|
| 11018 |
"url": "https://github.com/sponsors/ljharb"
|
| 11019 |
}
|
| 11020 |
},
|
| 11021 |
+
"node_modules/refractor": {
|
| 11022 |
+
"version": "5.0.0",
|
| 11023 |
+
"resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
|
| 11024 |
+
"integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
|
| 11025 |
+
"license": "MIT",
|
| 11026 |
+
"dependencies": {
|
| 11027 |
+
"@types/hast": "^3.0.0",
|
| 11028 |
+
"@types/prismjs": "^1.0.0",
|
| 11029 |
+
"hastscript": "^9.0.0",
|
| 11030 |
+
"parse-entities": "^4.0.0"
|
| 11031 |
+
},
|
| 11032 |
+
"funding": {
|
| 11033 |
+
"type": "github",
|
| 11034 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 11035 |
+
}
|
| 11036 |
+
},
|
| 11037 |
"node_modules/regexp.prototype.flags": {
|
| 11038 |
"version": "1.5.4",
|
| 11039 |
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
package.json
CHANGED
|
@@ -46,6 +46,7 @@
|
|
| 46 |
"react": "19.2.3",
|
| 47 |
"react-dom": "19.2.3",
|
| 48 |
"react-markdown": "^10.1.0",
|
|
|
|
| 49 |
"recharts": "^3.7.0",
|
| 50 |
"remark-gfm": "^4.0.1",
|
| 51 |
"simple-git": "^3.32.3",
|
|
@@ -68,6 +69,7 @@
|
|
| 68 |
"@types/node": "^20",
|
| 69 |
"@types/react": "^19",
|
| 70 |
"@types/react-dom": "^19",
|
|
|
|
| 71 |
"@types/tar-fs": "^2.0.4",
|
| 72 |
"eslint": "^9",
|
| 73 |
"eslint-config-next": "16.1.6",
|
|
|
|
| 46 |
"react": "19.2.3",
|
| 47 |
"react-dom": "19.2.3",
|
| 48 |
"react-markdown": "^10.1.0",
|
| 49 |
+
"react-syntax-highlighter": "^16.1.1",
|
| 50 |
"recharts": "^3.7.0",
|
| 51 |
"remark-gfm": "^4.0.1",
|
| 52 |
"simple-git": "^3.32.3",
|
|
|
|
| 69 |
"@types/node": "^20",
|
| 70 |
"@types/react": "^19",
|
| 71 |
"@types/react-dom": "^19",
|
| 72 |
+
"@types/react-syntax-highlighter": "^15.5.13",
|
| 73 |
"@types/tar-fs": "^2.0.4",
|
| 74 |
"eslint": "^9",
|
| 75 |
"eslint-config-next": "16.1.6",
|