fix error
Browse files- .agent/memory/session.json +2 -2
- Dockerfile +15 -10
- app/api/system/vitals/route.ts +37 -0
- app/api/workspace/route.ts +5 -1
- app/dashboard/booting/BootSequenceClient.tsx +4 -2
- components/DashboardVitals.tsx +87 -0
- components/dashboard/Dashboard.tsx +15 -0
- lib/docker/manager.ts +30 -4
- lib/env-config.ts +40 -0
- lib/hf/storage.ts +73 -0
- lib/idx/idx-engine.ts +52 -27
- lib/mcp/bus.ts +38 -0
- next.config.ts +1 -0
.agent/memory/session.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
-
"session_id": "
|
| 4 |
-
"started_at": "2026-04-06T17:
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
|
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
+
"session_id": "c1981984",
|
| 4 |
+
"started_at": "2026-04-06T17:31:46.672890+05:30",
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
Dockerfile
CHANGED
|
@@ -7,24 +7,29 @@ ENV WORKSPACE_ROOT=/app/workspaces
|
|
| 7 |
ENV ANDROID_SDK_ROOT=/app/android-sdk
|
| 8 |
ENV PATH="/home/node/.nix-profile/bin:/usr/local/bin:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools:${PATH}"
|
| 9 |
|
| 10 |
-
# 2. Modern Infrastructure Layer (
|
| 11 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 12 |
-
|
| 13 |
openjdk-17-jdk xvfb fluxbox novnc websockify libnss3 libatk-bridge2.0-0 \
|
| 14 |
-
libcups2 libgtk-3-0
|
| 15 |
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
|
| 17 |
-
# 3. code-server Modernization (v4.114.0 -
|
| 18 |
RUN curl -fL https://github.com/coder/code-server/releases/download/v4.114.0/code-server-4.114.0-linux-amd64.tar.gz \
|
| 19 |
| tar -C /usr/local/lib -xz \
|
| 20 |
&& ln -s /usr/local/lib/code-server-4.114.0-linux-amd64/bin/code-server /usr/local/bin/code-server
|
| 21 |
|
| 22 |
-
# 4. Determinate Systems Nix Installer (Unprivileged
|
| 23 |
RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \
|
| 24 |
--init none --no-confirm --extra-conf "experimental-features = nix-command flakes" \
|
|
|
|
| 25 |
&& chown -R node:node /nix /home/node
|
| 26 |
|
| 27 |
-
# 5.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
WORKDIR /app
|
| 29 |
RUN mkdir -p ${ANDROID_SDK_ROOT} && \
|
| 30 |
curl -fL https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -o cmdline.zip && \
|
|
@@ -33,24 +38,24 @@ RUN mkdir -p ${ANDROID_SDK_ROOT} && \
|
|
| 33 |
rm cmdline.zip && \
|
| 34 |
yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} "platform-tools" "platforms;android-33"
|
| 35 |
|
| 36 |
-
#
|
| 37 |
WORKDIR /app
|
| 38 |
COPY package*.json ./
|
| 39 |
RUN npm install
|
| 40 |
|
| 41 |
COPY . .
|
| 42 |
|
| 43 |
-
#
|
| 44 |
RUN npm run build && \
|
| 45 |
mkdir -p ${WORKSPACE_ROOT} && \
|
| 46 |
chown -R node:node /app ${WORKSPACE_ROOT}
|
| 47 |
|
| 48 |
-
# User context for Hugging Face Spaces (UID 1000)
|
| 49 |
USER node
|
| 50 |
EXPOSE 7860
|
| 51 |
|
| 52 |
# idx-start signal for boot manager
|
| 53 |
-
LABEL org.opencontainers.image.source=https://github.com/shubhjn/codeverse
|
| 54 |
LABEL idx.studio.version="2026.04"
|
|
|
|
|
|
|
| 55 |
|
| 56 |
CMD ["npm", "run", "start"]
|
|
|
|
| 7 |
ENV ANDROID_SDK_ROOT=/app/android-sdk
|
| 8 |
ENV PATH="/home/node/.nix-profile/bin:/usr/local/bin:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools:${PATH}"
|
| 9 |
|
| 10 |
+
# 2. Modern Infrastructure Layer (Optimized for Hugging Face Spaces)
|
| 11 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 12 |
+
python3 python3-pip make g++ git curl ca-certificates tar unzip bzip2 xz-utils \
|
| 13 |
openjdk-17-jdk xvfb fluxbox novnc websockify libnss3 libatk-bridge2.0-0 \
|
| 14 |
+
libcups2 libgtk-3-0 procps net-tools iptables \
|
| 15 |
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
|
| 17 |
+
# 3. code-server Modernization (v4.114.0 - Stable 2026)
|
| 18 |
RUN curl -fL https://github.com/coder/code-server/releases/download/v4.114.0/code-server-4.114.0-linux-amd64.tar.gz \
|
| 19 |
| tar -C /usr/local/lib -xz \
|
| 20 |
&& ln -s /usr/local/lib/code-server-4.114.0-linux-amd64/bin/code-server /usr/local/bin/code-server
|
| 21 |
|
| 22 |
+
# 4. Determinate Systems Nix Installer (Unprivileged, Single-User)
|
| 23 |
RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \
|
| 24 |
--init none --no-confirm --extra-conf "experimental-features = nix-command flakes" \
|
| 25 |
+
--extra-conf "sandbox = false" \
|
| 26 |
&& chown -R node:node /nix /home/node
|
| 27 |
|
| 28 |
+
# 5. Advanced Acceleration Layer (Cachix & Hugging Face Hub CLI)
|
| 29 |
+
RUN pip3 install --no-cache-dir huggingface-hub --break-system-packages \
|
| 30 |
+
&& curl -fL https://cachix.org/api/v1/install | sh
|
| 31 |
+
|
| 32 |
+
# 6. Android SDK (Studio Preview Engine Integration)
|
| 33 |
WORKDIR /app
|
| 34 |
RUN mkdir -p ${ANDROID_SDK_ROOT} && \
|
| 35 |
curl -fL https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -o cmdline.zip && \
|
|
|
|
| 38 |
rm cmdline.zip && \
|
| 39 |
yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} "platform-tools" "platforms;android-33"
|
| 40 |
|
| 41 |
+
# 7. CodeVerse Application Layer (Strict Production Build)
|
| 42 |
WORKDIR /app
|
| 43 |
COPY package*.json ./
|
| 44 |
RUN npm install
|
| 45 |
|
| 46 |
COPY . .
|
| 47 |
|
| 48 |
+
# 8. Final Sanity Check & Build (Strict Targets)
|
| 49 |
RUN npm run build && \
|
| 50 |
mkdir -p ${WORKSPACE_ROOT} && \
|
| 51 |
chown -R node:node /app ${WORKSPACE_ROOT}
|
| 52 |
|
|
|
|
| 53 |
USER node
|
| 54 |
EXPOSE 7860
|
| 55 |
|
| 56 |
# idx-start signal for boot manager
|
|
|
|
| 57 |
LABEL idx.studio.version="2026.04"
|
| 58 |
+
LABEL idx.optimization.cachix="true"
|
| 59 |
+
LABEL idx.optimization.hfsync="true"
|
| 60 |
|
| 61 |
CMD ["npm", "run", "start"]
|
app/api/system/vitals/route.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
import os from 'os';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Infrastructure Health & Vitals API.
|
| 6 |
+
* Provides real-time metrics for Hugging Face Spaces monitoring.
|
| 7 |
+
*/
|
| 8 |
+
export async function GET() {
|
| 9 |
+
const memory = process.memoryUsage();
|
| 10 |
+
const systemMemory = {
|
| 11 |
+
free: os.freemem(),
|
| 12 |
+
total: os.totalmem(),
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
const uptime = process.uptime();
|
| 16 |
+
const load = os.loadavg();
|
| 17 |
+
|
| 18 |
+
return NextResponse.json({
|
| 19 |
+
success: true,
|
| 20 |
+
vitals: {
|
| 21 |
+
process: {
|
| 22 |
+
rss: (memory.rss / 1024 / 1024).toFixed(2), // MB
|
| 23 |
+
heapUsed: (memory.heapUsed / 1024 / 1024).toFixed(2), // MB
|
| 24 |
+
heapTotal: (memory.heapTotal / 1024 / 1024).toFixed(2), // MB
|
| 25 |
+
},
|
| 26 |
+
system: {
|
| 27 |
+
freeMB: (systemMemory.free / 1024 / 1024).toFixed(0),
|
| 28 |
+
totalMB: (systemMemory.total / 1024 / 1024).toFixed(0),
|
| 29 |
+
loadAverage: load[0].toFixed(2),
|
| 30 |
+
},
|
| 31 |
+
status: {
|
| 32 |
+
uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
|
| 33 |
+
nodeVersion: process.version,
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
}
|
app/api/workspace/route.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { NextResponse } from "next/server";
|
| 2 |
-
import { startWorkspaceContainer, stopWorkspaceContainer } from "@/lib/docker/manager";
|
| 3 |
import { auth } from "@/auth";
|
| 4 |
import { db } from "@/lib/db";
|
| 5 |
|
|
@@ -107,6 +107,10 @@ export async function POST(req: Request) {
|
|
| 107 |
});
|
| 108 |
}
|
| 109 |
return NextResponse.json(result);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
}
|
| 111 |
|
| 112 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
|
|
| 1 |
import { NextResponse } from "next/server";
|
| 2 |
+
import { startWorkspaceContainer, stopWorkspaceContainer, prewarmWorkspace } from "@/lib/docker/manager";
|
| 3 |
import { auth } from "@/auth";
|
| 4 |
import { db } from "@/lib/db";
|
| 5 |
|
|
|
|
| 107 |
});
|
| 108 |
}
|
| 109 |
return NextResponse.json(result);
|
| 110 |
+
} else if (action === "prewarm") {
|
| 111 |
+
// FIRE AND FORGET: Hydrate the Nix store and Android SDKs
|
| 112 |
+
prewarmWorkspace({ id, userId: session.user.id, projectName }).catch(console.error);
|
| 113 |
+
return NextResponse.json({ success: true, status: 'hydrating' });
|
| 114 |
}
|
| 115 |
|
| 116 |
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
app/dashboard/booting/BootSequenceClient.tsx
CHANGED
|
@@ -115,10 +115,12 @@ export default function BootSequenceClient() {
|
|
| 115 |
<span className="text-blue-400/80">{log}</span>
|
| 116 |
) : log.startsWith("[IDX:NIX]") ? (
|
| 117 |
<span className="text-purple-400/80">{log}</span>
|
|
|
|
|
|
|
| 118 |
) : log.startsWith("[IDX:HOOK]") ? (
|
| 119 |
<span className="text-amber-400/80">{log}</span>
|
| 120 |
-
) : log.startsWith("[IDX:UP]") ? (
|
| 121 |
-
<span className="text-zinc-400">{log}</span>
|
| 122 |
) : (
|
| 123 |
<span className="text-zinc-600">{log}</span>
|
| 124 |
)}
|
|
|
|
| 115 |
<span className="text-blue-400/80">{log}</span>
|
| 116 |
) : log.startsWith("[IDX:NIX]") ? (
|
| 117 |
<span className="text-purple-400/80">{log}</span>
|
| 118 |
+
) : log.startsWith("[HF:STORAGE]") ? (
|
| 119 |
+
<span className="text-cyan-400/80">{log}</span>
|
| 120 |
) : log.startsWith("[IDX:HOOK]") ? (
|
| 121 |
<span className="text-amber-400/80">{log}</span>
|
| 122 |
+
) : log.startsWith("[IDX:UP]") || log.includes("Cachix") ? (
|
| 123 |
+
<span className="text-zinc-400 font-medium italic">{log}</span>
|
| 124 |
) : (
|
| 125 |
<span className="text-zinc-600">{log}</span>
|
| 126 |
)}
|
components/DashboardVitals.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from "react";
|
| 4 |
+
import { Activity, Cpu, Database, Server } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
interface VitalsData {
|
| 7 |
+
process: {
|
| 8 |
+
rss: string;
|
| 9 |
+
heapUsed: string;
|
| 10 |
+
heapTotal: string;
|
| 11 |
+
};
|
| 12 |
+
system: {
|
| 13 |
+
freeMB: string;
|
| 14 |
+
totalMB: string;
|
| 15 |
+
loadAverage: string;
|
| 16 |
+
};
|
| 17 |
+
status: {
|
| 18 |
+
uptime: string;
|
| 19 |
+
nodeVersion: string;
|
| 20 |
+
};
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default function DashboardVitals() {
|
| 24 |
+
const [vitals, setVitals] = useState<VitalsData | null>(null);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
const fetchVitals = async () => {
|
| 28 |
+
try {
|
| 29 |
+
const res = await fetch("/api/system/vitals");
|
| 30 |
+
const data = await res.json();
|
| 31 |
+
if (data.success) setVitals(data.vitals);
|
| 32 |
+
} catch (e) {
|
| 33 |
+
console.error("Vitals fetch failed:", e);
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
fetchVitals();
|
| 38 |
+
const interval = setInterval(fetchVitals, 10000); // 10s refresh
|
| 39 |
+
return () => clearInterval(interval);
|
| 40 |
+
}, []);
|
| 41 |
+
|
| 42 |
+
if (!vitals) return <div className="h-20 animate-pulse bg-zinc-900/50 rounded-xl border border-white/5" />;
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
| 46 |
+
<div className="bg-[#0d1117] border border-[#30363d] rounded-xl p-4 flex items-center gap-4 hover:border-blue-500/30 transition-all group">
|
| 47 |
+
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400 group-hover:scale-110 transition-transform">
|
| 48 |
+
<Activity size={20} />
|
| 49 |
+
</div>
|
| 50 |
+
<div>
|
| 51 |
+
<div className="text-[10px] text-zinc-500 uppercase font-bold tracking-widest">Process Load</div>
|
| 52 |
+
<div className="text-lg font-mono text-zinc-200">{vitals.system.loadAverage}</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div className="bg-[#0d1117] border border-[#30363d] rounded-xl p-4 flex items-center gap-4 hover:border-green-500/30 transition-all group">
|
| 57 |
+
<div className="p-3 rounded-lg bg-green-500/10 text-green-400 group-hover:scale-110 transition-transform">
|
| 58 |
+
<Database size={20} />
|
| 59 |
+
</div>
|
| 60 |
+
<div>
|
| 61 |
+
<div className="text-[10px] text-zinc-500 uppercase font-bold tracking-widest">Memory RSS</div>
|
| 62 |
+
<div className="text-lg font-mono text-zinc-200">{vitals.process.rss} MB</div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<div className="bg-[#0d1117] border border-[#30363d] rounded-xl p-4 flex items-center gap-4 hover:border-purple-500/30 transition-all group">
|
| 67 |
+
<div className="p-3 rounded-lg bg-purple-500/10 text-purple-400 group-hover:scale-110 transition-transform">
|
| 68 |
+
<Server size={20} />
|
| 69 |
+
</div>
|
| 70 |
+
<div>
|
| 71 |
+
<div className="text-[10px] text-zinc-500 uppercase font-bold tracking-widest">Uptime</div>
|
| 72 |
+
<div className="text-lg font-mono text-zinc-200">{vitals.status.uptime}</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div className="bg-[#0d1117] border border-[#30363d] rounded-xl p-4 flex items-center gap-4 hover:border-amber-500/30 transition-all group">
|
| 77 |
+
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400 group-hover:scale-110 transition-transform">
|
| 78 |
+
<Cpu size={20} />
|
| 79 |
+
</div>
|
| 80 |
+
<div>
|
| 81 |
+
<div className="text-[10px] text-zinc-500 uppercase font-bold tracking-widest">Free System</div>
|
| 82 |
+
<div className="text-lg font-mono text-zinc-200">{vitals.system.freeMB} MB</div>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
);
|
| 87 |
+
}
|
components/dashboard/Dashboard.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
| 13 |
import { TEMPLATE_REGISTRY } from "@/constants/extensions";
|
| 14 |
import Image from "next/image";
|
| 15 |
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
|
|
| 16 |
|
| 17 |
interface Project {
|
| 18 |
id: string;
|
|
@@ -67,6 +68,15 @@ export default function Dashboard() {
|
|
| 67 |
}, []);
|
| 68 |
useEffect(() => {
|
| 69 |
fetchProjects();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}, [fetchProjects]);
|
| 71 |
|
| 72 |
|
|
@@ -216,6 +226,11 @@ export default function Dashboard() {
|
|
| 216 |
|
| 217 |
{/* Body */}
|
| 218 |
<div className="flex-1 overflow-hidden dashboard-grid">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
{/* Left Panel */}
|
| 220 |
<div className="border-r border-(--border-subtle) flex flex-col overflow-y-auto p-6 gap-6">
|
| 221 |
{/* Greeting */}
|
|
|
|
| 13 |
import { TEMPLATE_REGISTRY } from "@/constants/extensions";
|
| 14 |
import Image from "next/image";
|
| 15 |
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
| 16 |
+
import DashboardVitals from "../DashboardVitals";
|
| 17 |
|
| 18 |
interface Project {
|
| 19 |
id: string;
|
|
|
|
| 68 |
}, []);
|
| 69 |
useEffect(() => {
|
| 70 |
fetchProjects();
|
| 71 |
+
|
| 72 |
+
// PREDICTIVE HYDRATION: Pre-warm the largest or most recent project
|
| 73 |
+
if (projects.length > 0) {
|
| 74 |
+
const latest = projects[0];
|
| 75 |
+
fetch("/api/workspace", {
|
| 76 |
+
method: "POST",
|
| 77 |
+
body: JSON.stringify({ action: "prewarm", id: latest.id })
|
| 78 |
+
}).catch(console.error);
|
| 79 |
+
}
|
| 80 |
}, [fetchProjects]);
|
| 81 |
|
| 82 |
|
|
|
|
| 226 |
|
| 227 |
{/* Body */}
|
| 228 |
<div className="flex-1 overflow-hidden dashboard-grid">
|
| 229 |
+
{/* Dashboard Vitals (Strategic Integration) */}
|
| 230 |
+
<div className="col-span-full px-8 pt-6">
|
| 231 |
+
<DashboardVitals />
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
{/* Left Panel */}
|
| 235 |
<div className="border-r border-(--border-subtle) flex flex-col overflow-y-auto p-6 gap-6">
|
| 236 |
{/* Greeting */}
|
lib/docker/manager.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
| 4 |
import Docker from 'dockerode';
|
| 5 |
import { EventEmitter } from 'events';
|
| 6 |
import { IdxEngine } from '../idx/idx-engine';
|
|
|
|
| 7 |
|
| 8 |
/**
|
| 9 |
* Registry for native workspace processes (IDE instances running outside Docker)
|
|
@@ -113,6 +114,27 @@ export interface WorkspaceOperationResult {
|
|
| 113 |
port?: string | number;
|
| 114 |
appetizeUrl?: string;
|
| 115 |
error?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
/**
|
|
@@ -126,6 +148,10 @@ async function performProvisioning(config: WorkspaceConfig): Promise<WorkspaceOp
|
|
| 126 |
|
| 127 |
log(`Provisioning hermetic environment for '${config.projectName}'...`);
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
// 1. Prepare Workspace Directory
|
| 130 |
const workspaceRoot = process.env.WORKSPACE_ROOT || path.join(process.cwd(), 'workspaces');
|
| 131 |
const workspacePath = path.join(workspaceRoot, config.id);
|
|
@@ -151,24 +177,24 @@ async function performProvisioning(config: WorkspaceConfig): Promise<WorkspaceOp
|
|
| 151 |
log(`Baseline provisioned successfully.`);
|
| 152 |
}
|
| 153 |
|
| 154 |
-
// 3. IDX Engine: Sync Environment
|
| 155 |
const idxConfig = IdxEngine.getIdxConfig(workspacePath);
|
| 156 |
if (idxConfig) {
|
| 157 |
log(`Declarative config detected. Initializing synchronization...`);
|
| 158 |
-
IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => log(msg));
|
| 159 |
|
| 160 |
const flagPath = path.join(workspacePath, '.idx-created');
|
| 161 |
if (!fs.existsSync(flagPath)) {
|
| 162 |
if (idxConfig.onCreate) {
|
| 163 |
log(`Executing onCreate lifecycle hook...`);
|
| 164 |
-
IdxEngine.runHook(workspacePath, 'onCreate', idxConfig.onCreate, (msg) => log(msg));
|
| 165 |
}
|
| 166 |
fs.writeFileSync(flagPath, new Date().toISOString());
|
| 167 |
}
|
| 168 |
|
| 169 |
if (idxConfig.onStart) {
|
| 170 |
log(`Executing onStart lifecycle hook...`);
|
| 171 |
-
IdxEngine.runHook(workspacePath, 'onStart', idxConfig.onStart, (msg) => log(msg));
|
| 172 |
}
|
| 173 |
}
|
| 174 |
|
|
|
|
| 4 |
import Docker from 'dockerode';
|
| 5 |
import { EventEmitter } from 'events';
|
| 6 |
import { IdxEngine } from '../idx/idx-engine';
|
| 7 |
+
import { HFStorage } from '../hf/storage';
|
| 8 |
|
| 9 |
/**
|
| 10 |
* Registry for native workspace processes (IDE instances running outside Docker)
|
|
|
|
| 114 |
port?: string | number;
|
| 115 |
appetizeUrl?: string;
|
| 116 |
error?: string;
|
| 117 |
+
status?: 'hydrating' | 'ready';
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* PREDICTIVE HYDRATION: Pre-warms Nix profile and SDKs.
|
| 122 |
+
*/
|
| 123 |
+
export async function prewarmWorkspace(config: WorkspaceConfig): Promise<void> {
|
| 124 |
+
const workspaceRoot = process.env.WORKSPACE_ROOT || path.join(process.cwd(), 'workspaces');
|
| 125 |
+
const workspacePath = path.join(workspaceRoot, config.id);
|
| 126 |
+
|
| 127 |
+
if (!fs.existsSync(workspacePath)) {
|
| 128 |
+
fs.mkdirSync(workspacePath, { recursive: true });
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
const idxConfig = IdxEngine.getIdxConfig(workspacePath);
|
| 132 |
+
if (idxConfig) {
|
| 133 |
+
// Run Nix sync and onCreate in background if not already warmed
|
| 134 |
+
IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => {
|
| 135 |
+
provisioningBus.emit(`log:${config.id}`, `[HYDRATE] ${msg}`);
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
}
|
| 139 |
|
| 140 |
/**
|
|
|
|
| 148 |
|
| 149 |
log(`Provisioning hermetic environment for '${config.projectName}'...`);
|
| 150 |
|
| 151 |
+
// 0. HF PERSISTENCE: Restore profile from Dataset if available
|
| 152 |
+
await HFStorage.syncFromDataset((msg) => log(msg));
|
| 153 |
+
HFStorage.startAutoSave(300000); // Start 5m auto-save loop
|
| 154 |
+
|
| 155 |
// 1. Prepare Workspace Directory
|
| 156 |
const workspaceRoot = process.env.WORKSPACE_ROOT || path.join(process.cwd(), 'workspaces');
|
| 157 |
const workspacePath = path.join(workspaceRoot, config.id);
|
|
|
|
| 177 |
log(`Baseline provisioned successfully.`);
|
| 178 |
}
|
| 179 |
|
| 180 |
+
// 3. IDX Engine: Sync Environment (Async/Non-blocking)
|
| 181 |
const idxConfig = IdxEngine.getIdxConfig(workspacePath);
|
| 182 |
if (idxConfig) {
|
| 183 |
log(`Declarative config detected. Initializing synchronization...`);
|
| 184 |
+
await IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => log(msg));
|
| 185 |
|
| 186 |
const flagPath = path.join(workspacePath, '.idx-created');
|
| 187 |
if (!fs.existsSync(flagPath)) {
|
| 188 |
if (idxConfig.onCreate) {
|
| 189 |
log(`Executing onCreate lifecycle hook...`);
|
| 190 |
+
await IdxEngine.runHook(workspacePath, 'onCreate', idxConfig.onCreate, (msg) => log(msg));
|
| 191 |
}
|
| 192 |
fs.writeFileSync(flagPath, new Date().toISOString());
|
| 193 |
}
|
| 194 |
|
| 195 |
if (idxConfig.onStart) {
|
| 196 |
log(`Executing onStart lifecycle hook...`);
|
| 197 |
+
await IdxEngine.runHook(workspacePath, 'onStart', idxConfig.onStart, (msg) => log(msg));
|
| 198 |
}
|
| 199 |
}
|
| 200 |
|
lib/env-config.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CodeVerse Environment Configuration & Requirements Manifest.
|
| 3 |
+
* Centralizing all system variables for production-grade reliability.
|
| 4 |
+
*/
|
| 5 |
+
export const ENV_CONFIG = {
|
| 6 |
+
// 1. Storage & Persistence
|
| 7 |
+
HF_TOKEN: process.env.HF_TOKEN,
|
| 8 |
+
HF_DATASET_ID: process.env.HF_DATASET_ID,
|
| 9 |
+
WORKSPACE_ROOT: process.env.WORKSPACE_ROOT || '/app/workspaces',
|
| 10 |
+
|
| 11 |
+
// 2. Build Acceleration
|
| 12 |
+
CACHIX_CACHE_NAME: process.env.CACHIX_CACHE_NAME,
|
| 13 |
+
CACHIX_AUTH_TOKEN: process.env.CACHIX_AUTH_TOKEN,
|
| 14 |
+
|
| 15 |
+
// 3. Infrastructure State
|
| 16 |
+
NODE_ENV: process.env.NODE_ENV || 'production',
|
| 17 |
+
SPACE_ID: process.env.SPACE_ID, // Hugging Face Space Identity
|
| 18 |
+
APP_BASE_URL: process.env.NEXTAUTH_URL || 'http://localhost:7860',
|
| 19 |
+
|
| 20 |
+
// 4. Database & Auth
|
| 21 |
+
AUTH_SECRET: process.env.AUTH_SECRET,
|
| 22 |
+
DATABASE_URL: process.env.TURSO_URL,
|
| 23 |
+
DATABASE_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN,
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Validates that all critical infrastructure secrets are available.
|
| 28 |
+
*/
|
| 29 |
+
export function validateEnvironment() {
|
| 30 |
+
const missing = [];
|
| 31 |
+
if (!ENV_CONFIG.HF_TOKEN) missing.push('HF_TOKEN');
|
| 32 |
+
if (!ENV_CONFIG.HF_DATASET_ID) missing.push('HF_DATASET_ID');
|
| 33 |
+
if (!ENV_CONFIG.AUTH_SECRET) missing.push('AUTH_SECRET');
|
| 34 |
+
if (!ENV_CONFIG.DATABASE_URL) missing.push('TURSO_URL');
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
valid: missing.length === 0,
|
| 38 |
+
missing: missing
|
| 39 |
+
};
|
| 40 |
+
}
|
lib/hf/storage.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { execSync } from 'child_process';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import fs from 'fs';
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Hugging Face Storage Utility for 2026 CodeVerse Persistence.
|
| 7 |
+
*/
|
| 8 |
+
export class HFStorage {
|
| 9 |
+
private static readonly HF_TOKEN = process.env.HF_TOKEN;
|
| 10 |
+
private static readonly HF_DATASET_ID = process.env.HF_DATASET_ID;
|
| 11 |
+
private static readonly PROFILE_PATH = path.join(process.env.HOME || '/home/node', '.nix-profile');
|
| 12 |
+
private static readonly WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/app/workspaces';
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Synchronizes the environment FROM the Hugging Face Dataset (Pull).
|
| 16 |
+
*/
|
| 17 |
+
static async syncFromDataset(onLog?: (msg: string) => void): Promise<void> {
|
| 18 |
+
if (!this.HF_TOKEN || !this.HF_DATASET_ID) {
|
| 19 |
+
onLog?.(`[HF:STORAGE] Persistence layer inactive. HF_TOKEN or HF_DATASET_ID missing.`);
|
| 20 |
+
return;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
onLog?.(`[HF:STORAGE] Pulling persistent profile from '${this.HF_DATASET_ID}'...`);
|
| 24 |
+
try {
|
| 25 |
+
// Using huggingface-cli to download the profile tarball
|
| 26 |
+
const tmpDir = '/tmp/hf-sync';
|
| 27 |
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
| 28 |
+
|
| 29 |
+
execSync(`huggingface-cli download ${this.HF_DATASET_ID} profile.tar.gz --local-dir ${tmpDir} --token ${this.HF_TOKEN}`);
|
| 30 |
+
|
| 31 |
+
if (fs.existsSync(path.join(tmpDir, 'profile.tar.gz'))) {
|
| 32 |
+
onLog?.(`[HF:STORAGE] Restoring Nix profile...`);
|
| 33 |
+
execSync(`tar -xzf ${path.join(tmpDir, 'profile.tar.gz')} -C ${process.env.HOME || '/home/node'}`);
|
| 34 |
+
onLog?.(`[HF:STORAGE] Profile restored successfully.`);
|
| 35 |
+
}
|
| 36 |
+
} catch (e) {
|
| 37 |
+
onLog?.(`[ERROR] Profile restoration failed. Starting with fresh environment.`);
|
| 38 |
+
console.error(e);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Synchronizes the environment TO the Hugging Face Dataset (Push).
|
| 44 |
+
*/
|
| 45 |
+
static async syncToDataset(onLog?: (msg: string) => void): Promise<void> {
|
| 46 |
+
if (!this.HF_TOKEN || !this.HF_DATASET_ID) return;
|
| 47 |
+
|
| 48 |
+
onLog?.(`[HF:STORAGE] Saving persistent profile to '${this.HF_DATASET_ID}'...`);
|
| 49 |
+
try {
|
| 50 |
+
const tmpDir = '/tmp/hf-sync';
|
| 51 |
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
| 52 |
+
|
| 53 |
+
// Create a tarball of the .nix-profile
|
| 54 |
+
execSync(`tar -czf ${path.join(tmpDir, 'profile.tar.gz')} -C ${process.env.HOME || '/home/node'} .nix-profile`);
|
| 55 |
+
|
| 56 |
+
// Use huggingface-cli to upload
|
| 57 |
+
execSync(`huggingface-cli upload ${this.HF_DATASET_ID} ${tmpDir}/profile.tar.gz profile.tar.gz --token ${this.HF_TOKEN}`);
|
| 58 |
+
onLog?.(`[HF:STORAGE] Profile synchronized successfully.`);
|
| 59 |
+
} catch (e) {
|
| 60 |
+
onLog?.(`[ERROR] Profile synchronization failed.`);
|
| 61 |
+
console.error(e);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* Starts the periodic persistence interval.
|
| 67 |
+
*/
|
| 68 |
+
static startAutoSave(intervalMs: number = 300000) { // 5 minutes default
|
| 69 |
+
setInterval(async () => {
|
| 70 |
+
await this.syncToDataset((msg) => console.log(msg));
|
| 71 |
+
}, intervalMs);
|
| 72 |
+
}
|
| 73 |
+
}
|
lib/idx/idx-engine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import fs from 'fs';
|
| 2 |
import path from 'path';
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
/**
|
| 6 |
* Interface representing the .idx/dev.nix configuration.
|
|
@@ -13,6 +13,7 @@ export interface IdxConfig {
|
|
| 13 |
|
| 14 |
/**
|
| 15 |
* IDX Engine for declarative workspace environments.
|
|
|
|
| 16 |
*/
|
| 17 |
export class IdxEngine {
|
| 18 |
/**
|
|
@@ -25,7 +26,6 @@ export class IdxEngine {
|
|
| 25 |
try {
|
| 26 |
const content = fs.readFileSync(configPath, 'utf8');
|
| 27 |
|
| 28 |
-
// Simple regex-based parser for .idx/dev.nix (HCI: Nix syntax is complex, but we target common patterns)
|
| 29 |
const packagesMatch = content.match(/packages\s*=\s*\[([\s\S]*?)\]/);
|
| 30 |
const onCreateMatch = content.match(/onCreate\s*=\s*"{1,3}([\s\S]*?)"{1,3}/);
|
| 31 |
const onStartMatch = content.match(/onStart\s*=\s*"{1,3}([\s\S]*?)"{1,3}/);
|
|
@@ -43,50 +43,75 @@ export class IdxEngine {
|
|
| 43 |
|
| 44 |
/**
|
| 45 |
* Synchronizes the Nix environment based on the declarative packages.
|
|
|
|
| 46 |
*/
|
| 47 |
-
static syncNixEnvironment(workspacePath: string, config: IdxConfig, onLog?: (msg: string) => void) {
|
| 48 |
if (!config.packages || config.packages.length === 0) return;
|
| 49 |
|
| 50 |
const log = (msg: string) => { if (onLog) onLog(`[IDX:NIX] ${msg}`); };
|
| 51 |
log(`Syncing system packages: ${config.packages.join(', ')}...`);
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
for
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
| 63 |
});
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
-
log(`Environment synchronized successfully.`);
|
| 66 |
-
} catch (e) {
|
| 67 |
-
log(`[ERROR] Nix sync failed. Reverting to base image SDKs.`);
|
| 68 |
-
console.error(e);
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
/**
|
| 73 |
* Executes the 'onCreate' and 'onStart' hooks.
|
|
|
|
| 74 |
*/
|
| 75 |
-
static runHook(workspacePath: string, hookName: 'onCreate' | 'onStart', script: string, onLog?: (msg: string) => void) {
|
| 76 |
const log = (msg: string) => { if (onLog) onLog(`[IDX:HOOK] ${hookName}: ${msg}`); };
|
| 77 |
log(`Executing script...`);
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
execSync(script, {
|
| 82 |
cwd: workspacePath,
|
| 83 |
-
shell: '/bin/bash',
|
| 84 |
env: { ...process.env, HOME: workspacePath }
|
| 85 |
});
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
log(`[
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
}
|
|
|
|
| 1 |
import fs from 'fs';
|
| 2 |
import path from 'path';
|
| 3 |
+
import { spawn } from 'child_process';
|
| 4 |
|
| 5 |
/**
|
| 6 |
* Interface representing the .idx/dev.nix configuration.
|
|
|
|
| 13 |
|
| 14 |
/**
|
| 15 |
* IDX Engine for declarative workspace environments.
|
| 16 |
+
* Refactored for 2026 Asynchronous Execution to prevent Event Loop blocking.
|
| 17 |
*/
|
| 18 |
export class IdxEngine {
|
| 19 |
/**
|
|
|
|
| 26 |
try {
|
| 27 |
const content = fs.readFileSync(configPath, 'utf8');
|
| 28 |
|
|
|
|
| 29 |
const packagesMatch = content.match(/packages\s*=\s*\[([\s\S]*?)\]/);
|
| 30 |
const onCreateMatch = content.match(/onCreate\s*=\s*"{1,3}([\s\S]*?)"{1,3}/);
|
| 31 |
const onStartMatch = content.match(/onStart\s*=\s*"{1,3}([\s\S]*?)"{1,3}/);
|
|
|
|
| 43 |
|
| 44 |
/**
|
| 45 |
* Synchronizes the Nix environment based on the declarative packages.
|
| 46 |
+
* ASYNCHRONOUS Spawning to prevent Blocking.
|
| 47 |
*/
|
| 48 |
+
static async syncNixEnvironment(workspacePath: string, config: IdxConfig, onLog?: (msg: string) => void): Promise<void> {
|
| 49 |
if (!config.packages || config.packages.length === 0) return;
|
| 50 |
|
| 51 |
const log = (msg: string) => { if (onLog) onLog(`[IDX:NIX] ${msg}`); };
|
| 52 |
log(`Syncing system packages: ${config.packages.join(', ')}...`);
|
| 53 |
|
| 54 |
+
// CACHIX ACCELERATION
|
| 55 |
+
const cachixName = process.env.CACHIX_CACHE_NAME;
|
| 56 |
+
if (cachixName) {
|
| 57 |
+
log(`Cachix acceleration detected for '${cachixName}'. Setting up cache...`);
|
| 58 |
+
try {
|
| 59 |
+
await new Promise<void>((resolve, reject) => {
|
| 60 |
+
const child = spawn('cachix', ['use', cachixName], {
|
| 61 |
+
cwd: workspacePath,
|
| 62 |
+
env: { ...process.env, HOME: workspacePath }
|
| 63 |
+
});
|
| 64 |
+
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`Cachix setup failed with code ${code}`)));
|
| 65 |
});
|
| 66 |
+
} catch (e) {
|
| 67 |
+
log(`[WARN] Cachix setup failed. Falling back to default binary cache.`);
|
| 68 |
+
console.error(e);
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
+
|
| 72 |
+
for (const pkg of config.packages) {
|
| 73 |
+
log(`Installing ${pkg}...`);
|
| 74 |
+
const pkgName = pkg.replace('pkgs.', '');
|
| 75 |
+
|
| 76 |
+
await new Promise<void>((resolve, reject) => {
|
| 77 |
+
const child = spawn('nix-env', ['-iA', `nixpkgs.${pkgName}`], {
|
| 78 |
+
cwd: workspacePath,
|
| 79 |
+
env: { ...process.env, HOME: workspacePath, NIX_PATH: `nixpkgs=https://github.com/NixOS/nixpkgs/archive/master.tar.gz` }
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
child.stdout.on('data', (data) => log(data.toString().trim()));
|
| 83 |
+
child.stderr.on('data', (data) => log(`[WARN] ${data.toString().trim()}`));
|
| 84 |
+
|
| 85 |
+
child.on('close', (code) => {
|
| 86 |
+
if (code === 0) resolve();
|
| 87 |
+
else reject(new Error(`Nix installation of ${pkgName} failed with code ${code}`));
|
| 88 |
+
});
|
| 89 |
+
}).catch(err => log(`[ERROR] ${err.message}`));
|
| 90 |
+
}
|
| 91 |
+
log(`Environment synchronized successfully.`);
|
| 92 |
}
|
| 93 |
|
| 94 |
/**
|
| 95 |
* Executes the 'onCreate' and 'onStart' hooks.
|
| 96 |
+
* ASYNCHRONOUS Spawning to prevent Blocking.
|
| 97 |
*/
|
| 98 |
+
static async runHook(workspacePath: string, hookName: 'onCreate' | 'onStart', script: string, onLog?: (msg: string) => void): Promise<void> {
|
| 99 |
const log = (msg: string) => { if (onLog) onLog(`[IDX:HOOK] ${hookName}: ${msg}`); };
|
| 100 |
log(`Executing script...`);
|
| 101 |
|
| 102 |
+
await new Promise<void>((resolve, reject) => {
|
| 103 |
+
const child = spawn('/bin/bash', ['-c', script], {
|
|
|
|
| 104 |
cwd: workspacePath,
|
|
|
|
| 105 |
env: { ...process.env, HOME: workspacePath }
|
| 106 |
});
|
| 107 |
+
|
| 108 |
+
child.stdout.on('data', (data) => log(data.toString().trim()));
|
| 109 |
+
child.stderr.on('data', (data) => log(`[WARN] ${data.toString().trim()}`));
|
| 110 |
+
|
| 111 |
+
child.on('close', (code) => {
|
| 112 |
+
if (code === 0) resolve();
|
| 113 |
+
else reject(new Error(`Hook ${hookName} failed with code ${code}`));
|
| 114 |
+
});
|
| 115 |
+
}).catch(err => log(`[ERROR] ${err.message}`));
|
| 116 |
}
|
| 117 |
}
|
lib/mcp/bus.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createTools } from "./tools";
|
| 2 |
+
import type { ToolSet } from "ai";
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Model Context Protocol (MCP) Workspace Bus.
|
| 6 |
+
* Orchestrates multi-agent tool sharing across isolated CodeVerse environments.
|
| 7 |
+
*/
|
| 8 |
+
export class WorkspaceBus {
|
| 9 |
+
private static activeToolsets = new Map<string, ToolSet>();
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Registers a workspace toolset on the shared collaboration bus.
|
| 13 |
+
*/
|
| 14 |
+
static register(id: string, userId: string, workspaceName: string): ToolSet {
|
| 15 |
+
if (this.activeToolsets.has(id)) {
|
| 16 |
+
return this.activeToolsets.get(id)!;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const tools = createTools(userId, workspaceName);
|
| 20 |
+
this.activeToolsets.set(id, tools);
|
| 21 |
+
return tools;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Retrieves a toolset for a specific workspace context.
|
| 26 |
+
* Enables agents to "switch" between environments during complex multi-phase tasks.
|
| 27 |
+
*/
|
| 28 |
+
static getToolset(id: string): ToolSet | undefined {
|
| 29 |
+
return this.activeToolsets.get(id);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Lists all currently active workspace identities on the bus.
|
| 34 |
+
*/
|
| 35 |
+
static listActiveWorkspaces(): string[] {
|
| 36 |
+
return Array.from(this.activeToolsets.keys());
|
| 37 |
+
}
|
| 38 |
+
}
|
next.config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
|
|
|
| 4 |
serverExternalPackages: ["dockerode", "ssh2"],
|
| 5 |
};
|
| 6 |
|
|
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
+
enablePrerenderSourceMaps: true,
|
| 5 |
serverExternalPackages: ["dockerode", "ssh2"],
|
| 6 |
};
|
| 7 |
|