fix state error
Browse files- .agent/memory/session.json +2 -2
- auth.ts +7 -6
- dist/constants/index.js +45 -0
- dist/lib/db/index.js +21 -0
- dist/lib/db/schema.js +98 -0
- dist/lib/docker/manager.js +428 -0
- dist/lib/env-config.js +50 -0
- dist/lib/hf/storage.js +119 -0
- dist/lib/idx/idx-engine.js +213 -0
- dist/lib/jobs/auto-sleep.js +65 -0
- dist/server.js +285 -0
- lib/db/index.ts +1 -6
- package.json +1 -0
- server.ts +131 -216
.agent/memory/session.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
-
"session_id": "
|
| 4 |
-
"started_at": "2026-04-
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
|
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
+
"session_id": "ca245f40",
|
| 4 |
+
"started_at": "2026-04-08T00:02:12.484470+05:30",
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
auth.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { client as db } from "@/lib/db";
|
|
| 5 |
import { randomUUID } from "crypto";
|
| 6 |
import type { Adapter, AdapterUser, AdapterAccount, AdapterSession, VerificationToken } from "@auth/core/adapters";
|
| 7 |
import { JWT } from "next-auth/jwt";
|
|
|
|
| 8 |
|
| 9 |
/**
|
| 10 |
* 🛰️ CodeVerse Authentication Core
|
|
@@ -274,10 +275,10 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
| 274 |
async authorize(credentials) {
|
| 275 |
if (credentials?.username === "dev" || credentials?.username === "guest") {
|
| 276 |
return {
|
| 277 |
-
id:
|
| 278 |
-
name:
|
| 279 |
-
email:
|
| 280 |
-
image:
|
| 281 |
};
|
| 282 |
}
|
| 283 |
return null;
|
|
@@ -285,7 +286,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
| 285 |
})
|
| 286 |
] : []),
|
| 287 |
],
|
| 288 |
-
session: { strategy: "jwt" },
|
| 289 |
callbacks: {
|
| 290 |
async jwt({ token, user }) {
|
| 291 |
if (user) {
|
|
@@ -303,7 +304,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
| 303 |
},
|
| 304 |
},
|
| 305 |
pages: {
|
| 306 |
-
signIn:
|
| 307 |
},
|
| 308 |
debug: process.env.NODE_ENV === "development",
|
| 309 |
});
|
|
|
|
| 5 |
import { randomUUID } from "crypto";
|
| 6 |
import type { Adapter, AdapterUser, AdapterAccount, AdapterSession, VerificationToken } from "@auth/core/adapters";
|
| 7 |
import { JWT } from "next-auth/jwt";
|
| 8 |
+
import { AUTH_CONFIG, ROUTES } from "@/constants";
|
| 9 |
|
| 10 |
/**
|
| 11 |
* 🛰️ CodeVerse Authentication Core
|
|
|
|
| 275 |
async authorize(credentials) {
|
| 276 |
if (credentials?.username === "dev" || credentials?.username === "guest") {
|
| 277 |
return {
|
| 278 |
+
id: AUTH_CONFIG.DEV_USER_ID,
|
| 279 |
+
name: AUTH_CONFIG.DEV_USER_NAME,
|
| 280 |
+
email: AUTH_CONFIG.DEV_USER_EMAIL,
|
| 281 |
+
image: AUTH_CONFIG.DEV_AVATAR,
|
| 282 |
};
|
| 283 |
}
|
| 284 |
return null;
|
|
|
|
| 286 |
})
|
| 287 |
] : []),
|
| 288 |
],
|
| 289 |
+
session: { strategy: AUTH_CONFIG.SESSION_STRATEGY as "jwt" | "database" },
|
| 290 |
callbacks: {
|
| 291 |
async jwt({ token, user }) {
|
| 292 |
if (user) {
|
|
|
|
| 304 |
},
|
| 305 |
},
|
| 306 |
pages: {
|
| 307 |
+
signIn: ROUTES.LOGIN,
|
| 308 |
},
|
| 309 |
debug: process.env.NODE_ENV === "development",
|
| 310 |
});
|
dist/constants/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
/**
|
| 3 |
+
* 🛠️ CodeVerse Global Constants
|
| 4 |
+
* Centralized source of truth for all non-changeable text, configuration, and magic numbers.
|
| 5 |
+
* Adheres to Production-Grade Standards for consistency and maintainability.
|
| 6 |
+
*/
|
| 7 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 8 |
+
exports.ROUTES = exports.UI_STRINGS = exports.INFRA_CONFIG = exports.AUTH_CONFIG = exports.APP_CONFIG = void 0;
|
| 9 |
+
exports.APP_CONFIG = {
|
| 10 |
+
NAME: "CodeVerse",
|
| 11 |
+
VERSION: "1.0.0-stable",
|
| 12 |
+
DESCRIPTION: "The fully managed, agentic VS Code environment running securely in the browser.",
|
| 13 |
+
LOGO_SIZE: 40,
|
| 14 |
+
DEFAULT_LANGUAGE: "typescript",
|
| 15 |
+
};
|
| 16 |
+
exports.AUTH_CONFIG = {
|
| 17 |
+
DEV_USER_ID: "dev-user-id",
|
| 18 |
+
DEV_USER_NAME: "Developer Guest",
|
| 19 |
+
DEV_USER_EMAIL: "dev@codeverse.local",
|
| 20 |
+
DEV_AVATAR: "https://github.com/identicons/dev.png",
|
| 21 |
+
SESSION_STRATEGY: "jwt",
|
| 22 |
+
};
|
| 23 |
+
exports.INFRA_CONFIG = {
|
| 24 |
+
WORKSPACE_ROOT: process.env.WORKSPACE_ROOT || "/home/node/w",
|
| 25 |
+
TMPDIR: process.env.TMPDIR || "/tmp",
|
| 26 |
+
HF_HOME: process.env.HF_HOME || "/tmp/.cache/huggingface",
|
| 27 |
+
DEFAULT_PORT_START: 3001,
|
| 28 |
+
IDLE_TIMEOUT_MS: 30 * 60 * 1000, // 30 minutes
|
| 29 |
+
PERSISTENCE_INTERVAL_MS: 60 * 1000, // 1 minute
|
| 30 |
+
};
|
| 31 |
+
exports.UI_STRINGS = {
|
| 32 |
+
MAINTENANCE_TITLE: "Infrastructure Locked",
|
| 33 |
+
MAINTENANCE_MESSAGE: "CodeVerse is currently initializing hardware. Please wait while we secure your environment.",
|
| 34 |
+
LOGIN_TITLE: "CodeVerse Studio",
|
| 35 |
+
LOGIN_SUBTITLE: "The fully managed, agentic VS Code environment running securely in the browser.",
|
| 36 |
+
BYPASS_LABEL: "Bypass Login (Local Dev Only)",
|
| 37 |
+
};
|
| 38 |
+
exports.ROUTES = {
|
| 39 |
+
HOME: "/",
|
| 40 |
+
LOGIN: "/login",
|
| 41 |
+
API: {
|
| 42 |
+
AUTH: "/api/auth",
|
| 43 |
+
WORKSPACE: "/api/workspace",
|
| 44 |
+
}
|
| 45 |
+
};
|
dist/lib/db/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.db = exports.client = void 0;
|
| 4 |
+
const client_1 = require("@libsql/client");
|
| 5 |
+
const url = process.env.TURSO_URL || "file:local.db";
|
| 6 |
+
const authToken = process.env.TURSO_AUTH_TOKEN;
|
| 7 |
+
/**
|
| 8 |
+
* 🛠️ LibSQL Client Singleton (Native Edition)
|
| 9 |
+
* Provides 100% stable, high-performance database access without Drizzle overhead.
|
| 10 |
+
* Optimized for sandboxed execution on Hugging Face Spaces.
|
| 11 |
+
*/
|
| 12 |
+
exports.client = (0, client_1.createClient)({
|
| 13 |
+
url,
|
| 14 |
+
authToken,
|
| 15 |
+
});
|
| 16 |
+
/**
|
| 17 |
+
* 🛡️ Database Client Export
|
| 18 |
+
* Used by authentication and action layers.
|
| 19 |
+
*/
|
| 20 |
+
exports.db = exports.client;
|
| 21 |
+
// 🚀 Database lifecycle is managed by the server entrypoint to ensure proper synchronization.
|
dist/lib/db/schema.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.rawSchema = void 0;
|
| 4 |
+
exports.initDb = initDb;
|
| 5 |
+
exports.rawSchema = `
|
| 6 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 7 |
+
id TEXT PRIMARY KEY,
|
| 8 |
+
name TEXT,
|
| 9 |
+
email TEXT UNIQUE,
|
| 10 |
+
image TEXT,
|
| 11 |
+
github_username TEXT,
|
| 12 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
CREATE TABLE IF NOT EXISTS accounts (
|
| 16 |
+
id TEXT PRIMARY KEY,
|
| 17 |
+
userId TEXT NOT NULL,
|
| 18 |
+
provider TEXT NOT NULL,
|
| 19 |
+
providerAccountId TEXT NOT NULL,
|
| 20 |
+
refresh_token TEXT,
|
| 21 |
+
access_token TEXT,
|
| 22 |
+
expires_at INTEGER,
|
| 23 |
+
token_type TEXT,
|
| 24 |
+
scope TEXT,
|
| 25 |
+
id_token TEXT,
|
| 26 |
+
session_state TEXT,
|
| 27 |
+
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
CREATE TABLE IF NOT EXISTS workspaces (
|
| 31 |
+
id TEXT PRIMARY KEY,
|
| 32 |
+
user_id TEXT NOT NULL,
|
| 33 |
+
project_name TEXT NOT NULL,
|
| 34 |
+
container_id TEXT,
|
| 35 |
+
android_container_id TEXT,
|
| 36 |
+
status TEXT DEFAULT 'stopped',
|
| 37 |
+
port_mapping INTEGER,
|
| 38 |
+
android_port INTEGER,
|
| 39 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 40 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 41 |
+
);
|
| 42 |
+
|
| 43 |
+
CREATE TABLE IF NOT EXISTS chat_history (
|
| 44 |
+
id TEXT PRIMARY KEY,
|
| 45 |
+
user_id TEXT NOT NULL,
|
| 46 |
+
workspace_id TEXT NOT NULL,
|
| 47 |
+
role TEXT NOT NULL,
|
| 48 |
+
content TEXT NOT NULL,
|
| 49 |
+
tool_invocations TEXT,
|
| 50 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 51 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 52 |
+
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
|
| 53 |
+
);
|
| 54 |
+
CREATE TABLE IF NOT EXISTS communities (
|
| 55 |
+
id TEXT PRIMARY KEY,
|
| 56 |
+
name TEXT NOT NULL UNIQUE,
|
| 57 |
+
description TEXT,
|
| 58 |
+
owner_id TEXT NOT NULL,
|
| 59 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 60 |
+
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
| 61 |
+
);
|
| 62 |
+
|
| 63 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 64 |
+
id TEXT PRIMARY KEY,
|
| 65 |
+
sessionToken TEXT UNIQUE NOT NULL,
|
| 66 |
+
userId TEXT NOT NULL,
|
| 67 |
+
expires DATETIME NOT NULL,
|
| 68 |
+
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
CREATE TABLE IF NOT EXISTS verification_tokens (
|
| 72 |
+
identifier TEXT NOT NULL,
|
| 73 |
+
token TEXT NOT NULL,
|
| 74 |
+
expires DATETIME NOT NULL,
|
| 75 |
+
PRIMARY KEY (identifier, token)
|
| 76 |
+
);
|
| 77 |
+
`;
|
| 78 |
+
/**
|
| 79 |
+
* 🚀 Database Initialization Routine
|
| 80 |
+
* Hardened for Hugging Face Spaces: Ensures schema exists and dev user is seeded.
|
| 81 |
+
*/
|
| 82 |
+
async function initDb(client) {
|
| 83 |
+
// Use raw execution for robustness during cold start
|
| 84 |
+
await client.executeMultiple(exports.rawSchema);
|
| 85 |
+
// 🌱 SEEDING (Dev Only): Create a 'Developer Guest' user for the login bypass
|
| 86 |
+
if (process.env.NODE_ENV === "development" || process.env.NEXT_PUBLIC_NODE_ENV === "development") {
|
| 87 |
+
try {
|
| 88 |
+
await client.execute({
|
| 89 |
+
sql: "INSERT OR IGNORE INTO users (id, name, email, image) VALUES (?, ?, ?, ?)",
|
| 90 |
+
args: ["dev-user-id", "Developer Guest", "dev@codeverse.local", "https://github.com/identicons/dev.png"]
|
| 91 |
+
});
|
| 92 |
+
console.log("[SYSTEM] Dev Seeding: 'Developer Guest' user ready.");
|
| 93 |
+
}
|
| 94 |
+
catch (e) {
|
| 95 |
+
console.error("[SYSTEM] Dev Seeding failed:", e);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
dist/lib/docker/manager.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.dockerManager = exports.DockerManager = exports.pendingProvisioning = exports.provisioningBus = exports.nativeProcesses = void 0;
|
| 7 |
+
exports.isNativeWorkspaceRunning = isNativeWorkspaceRunning;
|
| 8 |
+
exports.getWorkspaceStatus = getWorkspaceStatus;
|
| 9 |
+
exports.isDockerAvailable = isDockerAvailable;
|
| 10 |
+
exports.stopNativeWorkspace = stopNativeWorkspace;
|
| 11 |
+
exports.getNativeWorkspacePort = getNativeWorkspacePort;
|
| 12 |
+
exports.getAndroidPort = getAndroidPort;
|
| 13 |
+
exports.prewarmWorkspace = prewarmWorkspace;
|
| 14 |
+
exports.startWorkspaceContainer = startWorkspaceContainer;
|
| 15 |
+
exports.reconnectRunningWorkspaces = reconnectRunningWorkspaces;
|
| 16 |
+
exports.stopWorkspaceContainer = stopWorkspaceContainer;
|
| 17 |
+
const fs_1 = __importDefault(require("fs"));
|
| 18 |
+
const child_process_1 = require("child_process");
|
| 19 |
+
const path_1 = __importDefault(require("path"));
|
| 20 |
+
const dockerode_1 = __importDefault(require("dockerode"));
|
| 21 |
+
const events_1 = require("events");
|
| 22 |
+
const idx_engine_1 = require("../idx/idx-engine");
|
| 23 |
+
const storage_1 = require("../hf/storage");
|
| 24 |
+
/**
|
| 25 |
+
* Registry for native workspace processes (IDE instances running outside Docker)
|
| 26 |
+
* Map<workspaceId, { pid: number; port: number; process: WorkspaceProcess }>
|
| 27 |
+
*/
|
| 28 |
+
exports.nativeProcesses = new Map();
|
| 29 |
+
/**
|
| 30 |
+
* Internal Provisioning Bus to multicast logs to concurrent clients.
|
| 31 |
+
*/
|
| 32 |
+
class ProvisioningBus extends events_1.EventEmitter {
|
| 33 |
+
}
|
| 34 |
+
exports.provisioningBus = new ProvisioningBus();
|
| 35 |
+
/**
|
| 36 |
+
* Map to track active provisioning promises to prevent redundant creation loops.
|
| 37 |
+
*/
|
| 38 |
+
exports.pendingProvisioning = new Map();
|
| 39 |
+
/**
|
| 40 |
+
* Checks if a native workspace is currently running.
|
| 41 |
+
* Supports fuzzy matching for reconnected sessions that might use a prefix key.
|
| 42 |
+
*/
|
| 43 |
+
function isNativeWorkspaceRunning(id) {
|
| 44 |
+
if (exports.nativeProcesses.has(id))
|
| 45 |
+
return true;
|
| 46 |
+
// Prefix fallback for reconnected sessions
|
| 47 |
+
return Array.from(exports.nativeProcesses.keys()).some(k => id.startsWith(k));
|
| 48 |
+
}
|
| 49 |
+
/**
|
| 50 |
+
* Returns the current runtime status of a workspace.
|
| 51 |
+
*/
|
| 52 |
+
function getWorkspaceStatus(id) {
|
| 53 |
+
if (exports.nativeProcesses.has(id))
|
| 54 |
+
return "ready";
|
| 55 |
+
if (exports.pendingProvisioning.has(id))
|
| 56 |
+
return "provisioning";
|
| 57 |
+
return "offline";
|
| 58 |
+
}
|
| 59 |
+
/**
|
| 60 |
+
* Helper for async delays.
|
| 61 |
+
*/
|
| 62 |
+
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
| 63 |
+
/**
|
| 64 |
+
* Finds an available port in the 8100-9000 range.
|
| 65 |
+
*/
|
| 66 |
+
function findAvailablePort() {
|
| 67 |
+
const occupiedPorts = Array.from(exports.nativeProcesses.values()).map(p => p.port);
|
| 68 |
+
let port = Math.floor(Math.random() * (9000 - 8100) + 8100);
|
| 69 |
+
while (occupiedPorts.includes(port)) {
|
| 70 |
+
port = Math.floor(Math.random() * (9000 - 8100) + 8100);
|
| 71 |
+
}
|
| 72 |
+
return port;
|
| 73 |
+
}
|
| 74 |
+
/**
|
| 75 |
+
* Checks if Docker is available in the current environment.
|
| 76 |
+
*/
|
| 77 |
+
async function isDockerAvailable() {
|
| 78 |
+
const socketPath = process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock';
|
| 79 |
+
if (process.env.SIMULATE_HF === 'true') {
|
| 80 |
+
return { available: false, reason: "Hugging Face Simulation Mode (Artificial Sandbox)" };
|
| 81 |
+
}
|
| 82 |
+
if (process.env.SPACE_ID) {
|
| 83 |
+
return { available: false, reason: "Hugging Face Space (Native Sandboxed)" };
|
| 84 |
+
}
|
| 85 |
+
try {
|
| 86 |
+
const docker = new dockerode_1.default({ socketPath: process.platform === 'win32' ? undefined : socketPath });
|
| 87 |
+
await docker.ping();
|
| 88 |
+
return { available: true };
|
| 89 |
+
}
|
| 90 |
+
catch (_a) {
|
| 91 |
+
return { available: false, reason: "Docker daemon unreachable" };
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
/**
|
| 95 |
+
* Stops a native workspace process.
|
| 96 |
+
*/
|
| 97 |
+
async function stopNativeWorkspace(id) {
|
| 98 |
+
const entry = exports.nativeProcesses.get(id);
|
| 99 |
+
if (entry) {
|
| 100 |
+
try {
|
| 101 |
+
entry.process.kill();
|
| 102 |
+
exports.nativeProcesses.delete(id);
|
| 103 |
+
return true;
|
| 104 |
+
}
|
| 105 |
+
catch (e) {
|
| 106 |
+
console.error(`[MANAGER] Failed to kill code-server ${id}:`, e);
|
| 107 |
+
exports.nativeProcesses.delete(id);
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
return false;
|
| 111 |
+
}
|
| 112 |
+
/**
|
| 113 |
+
* Gets the internal port for a native workspace process.
|
| 114 |
+
* Supports fuzzy matching for reconnected sessions.
|
| 115 |
+
*/
|
| 116 |
+
function getNativeWorkspacePort(id) {
|
| 117 |
+
var _a;
|
| 118 |
+
const entry = exports.nativeProcesses.get(id);
|
| 119 |
+
if (entry)
|
| 120 |
+
return entry.port;
|
| 121 |
+
// Prefix fallback
|
| 122 |
+
const key = Array.from(exports.nativeProcesses.keys()).find(k => id.startsWith(k));
|
| 123 |
+
return key ? (_a = exports.nativeProcesses.get(key)) === null || _a === void 0 ? void 0 : _a.port : undefined;
|
| 124 |
+
}
|
| 125 |
+
/**
|
| 126 |
+
* Returns the global unified Android VNC port.
|
| 127 |
+
*/
|
| 128 |
+
function getAndroidPort() {
|
| 129 |
+
return 6080;
|
| 130 |
+
}
|
| 131 |
+
/**
|
| 132 |
+
* PREDICTIVE HYDRATION: Pre-warms Nix profile and SDKs.
|
| 133 |
+
*/
|
| 134 |
+
async function prewarmWorkspace(config) {
|
| 135 |
+
// CRITICAL (April 2026): Shorten paths to avoid Unix Domain Socket (UDS) path limit (104 chars)
|
| 136 |
+
const workspaceRoot = process.env.WORKSPACE_ROOT || path_1.default.join(/*turbopackIgnore: true*/ '/home/node/w');
|
| 137 |
+
const workspacePath = path_1.default.join(/*turbopackIgnore: true*/ workspaceRoot, config.id.slice(0, 8));
|
| 138 |
+
if (!fs_1.default.existsSync(workspacePath)) {
|
| 139 |
+
fs_1.default.mkdirSync(workspacePath, { recursive: true });
|
| 140 |
+
}
|
| 141 |
+
const idxConfig = idx_engine_1.IdxEngine.getIdxConfig(workspacePath);
|
| 142 |
+
if (idxConfig) {
|
| 143 |
+
// Run Nix sync in background if not already warmed
|
| 144 |
+
idx_engine_1.IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => {
|
| 145 |
+
exports.provisioningBus.emit(`log:${config.id}`, `[HYDRATE] ${msg}`);
|
| 146 |
+
});
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
/**
|
| 150 |
+
* INTERNAL: Core provisioning logic with IDX support and auto-provisioning baseline.
|
| 151 |
+
*/
|
| 152 |
+
async function performProvisioning(config) {
|
| 153 |
+
const log = (msg) => {
|
| 154 |
+
if (config.onLog)
|
| 155 |
+
config.onLog(`[IDX:ENGINE] ${msg}`);
|
| 156 |
+
exports.provisioningBus.emit(`log:${config.id}`, msg);
|
| 157 |
+
};
|
| 158 |
+
try {
|
| 159 |
+
log(`Provisioning hermetic environment for '${config.projectName}'...`);
|
| 160 |
+
// 0. HF PERSISTENCE: Restore profile from Dataset if available
|
| 161 |
+
try {
|
| 162 |
+
await storage_1.HFStorage.syncFromDataset((msg) => log(msg));
|
| 163 |
+
}
|
| 164 |
+
catch (e) {
|
| 165 |
+
log(`[WARN] Persistent profile restoration failed: ${e instanceof Error ? e.message : String(e)}. Proceeding with clean environment.`);
|
| 166 |
+
}
|
| 167 |
+
// 1. Prepare Workspace Directory
|
| 168 |
+
const workspaceRoot = process.env.WORKSPACE_ROOT || path_1.default.join(/*turbopackIgnore: true*/ '/home/node/w');
|
| 169 |
+
const workspacePath = path_1.default.join(/*turbopackIgnore: true*/ workspaceRoot, config.id.slice(0, 8));
|
| 170 |
+
const userDataPath = path_1.default.join(/*turbopackIgnore: true*/ workspacePath, '.vscode-server');
|
| 171 |
+
if (!fs_1.default.existsSync(workspacePath)) {
|
| 172 |
+
fs_1.default.mkdirSync(workspacePath, { recursive: true });
|
| 173 |
+
// Store full ID for reliable reconnection after server restarts
|
| 174 |
+
fs_1.default.writeFileSync(path_1.default.join(workspacePath, '.codeverse-id'), config.id);
|
| 175 |
+
log(`Allocated isolated filesystem segment: ${config.id.slice(0, 8)}`);
|
| 176 |
+
}
|
| 177 |
+
// 2. IDX Engine: Sync Environment
|
| 178 |
+
const idxConfig = idx_engine_1.IdxEngine.getIdxConfig(workspacePath);
|
| 179 |
+
log(`Declarative config detected (Packages: ${idxConfig.packages.length}). Initializing synchronization...`);
|
| 180 |
+
await idx_engine_1.IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => log(msg));
|
| 181 |
+
const flagPath = path_1.default.join(/*turbopackIgnore: true*/ workspacePath, '.idx-created');
|
| 182 |
+
if (!fs_1.default.existsSync(flagPath)) {
|
| 183 |
+
if (idxConfig.onCreate) {
|
| 184 |
+
log(`Executing onCreate lifecycle hook...`);
|
| 185 |
+
await idx_engine_1.IdxEngine.runHook(workspacePath, 'onCreate', idxConfig.onCreate, (msg) => log(msg));
|
| 186 |
+
}
|
| 187 |
+
fs_1.default.writeFileSync(flagPath, new Date().toISOString());
|
| 188 |
+
}
|
| 189 |
+
// 4. Identify Target Port
|
| 190 |
+
const port = findAvailablePort();
|
| 191 |
+
// 5. Spawn code-server
|
| 192 |
+
const shellCommand = process.platform === 'win32' ? 'npx' : 'code-server';
|
| 193 |
+
const args = process.platform === 'win32' ? ['code-server'] : [];
|
| 194 |
+
const baseArgs = [
|
| 195 |
+
'--auth', 'none',
|
| 196 |
+
'--bind-addr', `127.0.0.1:${port}`,
|
| 197 |
+
'--user-data-dir', userDataPath,
|
| 198 |
+
'--disable-telemetry',
|
| 199 |
+
'--disable-update-check',
|
| 200 |
+
workspacePath
|
| 201 |
+
];
|
| 202 |
+
const spawnEnv = { ...process.env, HOME: workspacePath };
|
| 203 |
+
delete spawnEnv.PORT;
|
| 204 |
+
delete spawnEnv.SERVER_PORT;
|
| 205 |
+
const child = (0, child_process_1.spawn)(shellCommand, [...args, ...baseArgs], {
|
| 206 |
+
env: spawnEnv,
|
| 207 |
+
cwd: workspacePath,
|
| 208 |
+
shell: process.platform === 'win32'
|
| 209 |
+
});
|
| 210 |
+
log(`Spawning VS Code Orchestrator (PID: ${child.pid})...`);
|
| 211 |
+
child.on('error', (err) => log(`[FATAL] IDE binary failure: ${err.message}`));
|
| 212 |
+
child.stdout.on('data', (data) => {
|
| 213 |
+
const out = data.toString().trim();
|
| 214 |
+
if (out.includes('listening on'))
|
| 215 |
+
log(`[IDX:UP] ${out}`);
|
| 216 |
+
else if (out.length > 0)
|
| 217 |
+
log(`[IDE:CORE] ${out}`);
|
| 218 |
+
});
|
| 219 |
+
child.stderr.on('data', (data) => {
|
| 220 |
+
const err = data.toString().trim();
|
| 221 |
+
if (err.length > 0)
|
| 222 |
+
log(`[IDE:ERR] ${err}`);
|
| 223 |
+
});
|
| 224 |
+
child.on('close', (code, signal) => {
|
| 225 |
+
log(`[IDE:EXIT] IDE process died with code ${code} (Signal: ${signal})`);
|
| 226 |
+
});
|
| 227 |
+
// 6. Register in active pool
|
| 228 |
+
exports.nativeProcesses.set(config.id, { pid: child.pid, port, process: child });
|
| 229 |
+
// 7. Handshake Loop
|
| 230 |
+
let attempts = 0;
|
| 231 |
+
while (attempts < 60) {
|
| 232 |
+
try {
|
| 233 |
+
const res = await fetch(`http://127.0.0.1:${port}`);
|
| 234 |
+
if (res.ok) {
|
| 235 |
+
log(`Handshake verified. Studio Engine Online.`);
|
| 236 |
+
if (idxConfig.onStart) {
|
| 237 |
+
log(`Executing background onStart lifecycle hooks...`);
|
| 238 |
+
idx_engine_1.IdxEngine.runHook(workspacePath, 'onStart', idxConfig.onStart, (msg) => log(msg), true);
|
| 239 |
+
}
|
| 240 |
+
const finalResult = {
|
| 241 |
+
success: true,
|
| 242 |
+
containerId: `native-${config.id}`,
|
| 243 |
+
androidPort: config.withAndroidEmulator ? 6080 : undefined,
|
| 244 |
+
port: port
|
| 245 |
+
};
|
| 246 |
+
exports.provisioningBus.emit(`ready:${config.id}`, finalResult);
|
| 247 |
+
return finalResult;
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
catch (_a) {
|
| 251 |
+
if (attempts % 5 === 0)
|
| 252 |
+
log(`[INFO] Scanning for IDE heartbeat... (Attempt ${attempts}/60)`);
|
| 253 |
+
if (attempts === 15)
|
| 254 |
+
log(`[INFO] Nix evaluation in progress. Cold boot detected.`);
|
| 255 |
+
if (attempts === 45)
|
| 256 |
+
log(`[WARN] Handshake threshold approaching. IDE core high load.`);
|
| 257 |
+
await delay(1000);
|
| 258 |
+
attempts++;
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
log(`[FATAL] Handshake timeout on 127.0.0.1:${port}.`);
|
| 262 |
+
const entry = exports.nativeProcesses.get(config.id);
|
| 263 |
+
if (entry) {
|
| 264 |
+
entry.process.kill();
|
| 265 |
+
exports.nativeProcesses.delete(config.id);
|
| 266 |
+
}
|
| 267 |
+
const errResult = { success: false, error: "IDE_HANDSHAKE_TIMEOUT" };
|
| 268 |
+
exports.provisioningBus.emit(`error:${config.id}`, errResult);
|
| 269 |
+
return errResult;
|
| 270 |
+
}
|
| 271 |
+
catch (e) {
|
| 272 |
+
const error = e instanceof Error ? e.message : String(e);
|
| 273 |
+
log(`[FATAL] Provisioning pipeline collapsed: ${error}`);
|
| 274 |
+
const errResult = { success: false, error: "PROVISIONING_FAILED" };
|
| 275 |
+
exports.provisioningBus.emit(`error:${config.id}`, errResult);
|
| 276 |
+
return errResult;
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
/**
|
| 280 |
+
* Workspace provisioner with ATOMIC single-instance locking.
|
| 281 |
+
*/
|
| 282 |
+
async function startWorkspaceContainer(config) {
|
| 283 |
+
if (exports.nativeProcesses.has(config.id)) {
|
| 284 |
+
return {
|
| 285 |
+
success: true,
|
| 286 |
+
containerId: `native-${config.id}`,
|
| 287 |
+
port: exports.nativeProcesses.get(config.id).port
|
| 288 |
+
};
|
| 289 |
+
}
|
| 290 |
+
let pending = exports.pendingProvisioning.get(config.id);
|
| 291 |
+
if (!pending) {
|
| 292 |
+
pending = performProvisioning(config).finally(() => {
|
| 293 |
+
exports.pendingProvisioning.delete(config.id);
|
| 294 |
+
});
|
| 295 |
+
exports.pendingProvisioning.set(config.id, pending);
|
| 296 |
+
}
|
| 297 |
+
return await pending;
|
| 298 |
+
}
|
| 299 |
+
/**
|
| 300 |
+
* 🛠️ SELF-HEALING: Scans for running code-server instances to repopulate the proxy map.
|
| 301 |
+
* This allows the IDE to survive server restarts or cold boots by probing active ports.
|
| 302 |
+
*/
|
| 303 |
+
async function reconnectRunningWorkspaces() {
|
| 304 |
+
const workspaceRoot = process.env.WORKSPACE_ROOT || '/home/node/w';
|
| 305 |
+
console.log(`[BOOT] Probing filesystem segment: ${workspaceRoot} for existing sessions...`);
|
| 306 |
+
try {
|
| 307 |
+
// Find all code-server processes
|
| 308 |
+
// Note: Using a more robust ps grep that works across most POSIX environments
|
| 309 |
+
const psCmd = process.platform === 'win32' ? 'tasklist' : "ps aux | grep code-server | grep -v grep";
|
| 310 |
+
const output = (0, child_process_1.execSync)(psCmd).toString();
|
| 311 |
+
const lines = output.split('\n');
|
| 312 |
+
for (const line of lines) {
|
| 313 |
+
// Looking for: ... --bind-addr 127.0.0.1:8548 ... w/44c7597c
|
| 314 |
+
const bindMatch = line.match(/--bind-addr 127\.0\.0\.1:(\d+)/);
|
| 315 |
+
// Flexible path match: look for the ID prefix after 'w/' or 'workspaces/' or just the root
|
| 316 |
+
const pathMatch = line.match(/[ /](?:w|workspaces)\/([a-zA-Z0-9]{8})/);
|
| 317 |
+
if (bindMatch && pathMatch) {
|
| 318 |
+
const port = parseInt(bindMatch[1]);
|
| 319 |
+
const shortId = pathMatch[1];
|
| 320 |
+
const fullPath = path_1.default.join(workspaceRoot, shortId);
|
| 321 |
+
const idFile = path_1.default.join(fullPath, '.codeverse-id');
|
| 322 |
+
let foundFullId = "";
|
| 323 |
+
if (fs_1.default.existsSync(idFile)) {
|
| 324 |
+
foundFullId = fs_1.default.readFileSync(idFile, 'utf-8').trim();
|
| 325 |
+
}
|
| 326 |
+
else {
|
| 327 |
+
// Fallback: If no ID file, we use the shortId as the temporary key.
|
| 328 |
+
foundFullId = shortId;
|
| 329 |
+
console.warn(`[RECONNECT:WARN] No .codeverse-id for session ${shortId}. Using prefix mapping.`);
|
| 330 |
+
}
|
| 331 |
+
if (foundFullId && !exports.nativeProcesses.has(foundFullId)) {
|
| 332 |
+
// Capture PID reliably from ps output (column 2)
|
| 333 |
+
const psParts = line.trim().split(/\s+/);
|
| 334 |
+
const pid = parseInt(psParts[1]);
|
| 335 |
+
console.log(`[RECONNECT] Identified active IDE ${foundFullId} (PID: ${pid}) on port ${port}. Restoration complete.`);
|
| 336 |
+
exports.nativeProcesses.set(foundFullId, {
|
| 337 |
+
pid,
|
| 338 |
+
port,
|
| 339 |
+
process: {
|
| 340 |
+
pid,
|
| 341 |
+
kill: () => {
|
| 342 |
+
try {
|
| 343 |
+
process.kill(pid, 'SIGKILL');
|
| 344 |
+
return true;
|
| 345 |
+
}
|
| 346 |
+
catch (_a) {
|
| 347 |
+
try {
|
| 348 |
+
(0, child_process_1.execSync)(`fuser -k ${port}/tcp`);
|
| 349 |
+
}
|
| 350 |
+
catch (_b) { }
|
| 351 |
+
return true;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
});
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
catch (e) {
|
| 361 |
+
// No processes found or ps failed
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
/**
|
| 365 |
+
* 🟢 ENGINE WATCHDOG: Background health monitor for native IDE processes.
|
| 366 |
+
*/
|
| 367 |
+
function startEngineWatchdog() {
|
| 368 |
+
setInterval(async () => {
|
| 369 |
+
for (const [id, entry] of exports.nativeProcesses.entries()) {
|
| 370 |
+
try {
|
| 371 |
+
// 1. Zombie Check
|
| 372 |
+
try {
|
| 373 |
+
process.kill(entry.pid, 0);
|
| 374 |
+
}
|
| 375 |
+
catch (_a) {
|
| 376 |
+
console.log(`[WATCHDOG] Process ${entry.pid} for ${id} is missing. Pruning.`);
|
| 377 |
+
exports.nativeProcesses.delete(id);
|
| 378 |
+
continue;
|
| 379 |
+
}
|
| 380 |
+
// 2. Healthz Polling
|
| 381 |
+
const controller = new AbortController();
|
| 382 |
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
| 383 |
+
try {
|
| 384 |
+
const res = await fetch(`http://127.0.0.1:${entry.port}`, { signal: controller.signal });
|
| 385 |
+
if (!res.ok)
|
| 386 |
+
throw new Error('Unhealthy');
|
| 387 |
+
}
|
| 388 |
+
catch (_b) {
|
| 389 |
+
console.warn(`[WATCHDOG] IDE ${id} (Port ${entry.port}) is non-responsive.`);
|
| 390 |
+
// Optional: force restart if unhealthy for multiple cycles
|
| 391 |
+
}
|
| 392 |
+
finally {
|
| 393 |
+
clearTimeout(timeoutId);
|
| 394 |
+
}
|
| 395 |
+
}
|
| 396 |
+
catch (e) {
|
| 397 |
+
console.error(`[WATCHDOG:ERR] ${e}`);
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
}, 60000);
|
| 401 |
+
}
|
| 402 |
+
startEngineWatchdog();
|
| 403 |
+
/**
|
| 404 |
+
* Standardized stop method.
|
| 405 |
+
*/
|
| 406 |
+
async function stopWorkspaceContainer(id) {
|
| 407 |
+
const success = await stopNativeWorkspace(id);
|
| 408 |
+
return { success: success || true };
|
| 409 |
+
}
|
| 410 |
+
/**
|
| 411 |
+
* Modern Docker Manager class.
|
| 412 |
+
*/
|
| 413 |
+
class DockerManager {
|
| 414 |
+
async getContainerStatus(id) {
|
| 415 |
+
if (isNativeWorkspaceRunning(id))
|
| 416 |
+
return "running";
|
| 417 |
+
return "stopped";
|
| 418 |
+
}
|
| 419 |
+
async stopContainer(id) {
|
| 420 |
+
return stopNativeWorkspace(id);
|
| 421 |
+
}
|
| 422 |
+
async startWorkspace(config) {
|
| 423 |
+
const result = await startWorkspaceContainer(config);
|
| 424 |
+
return result.success;
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
exports.DockerManager = DockerManager;
|
| 428 |
+
exports.dockerManager = new DockerManager();
|
dist/lib/env-config.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.ENV_CONFIG = void 0;
|
| 4 |
+
exports.validateEnvironment = validateEnvironment;
|
| 5 |
+
/**
|
| 6 |
+
* CodeVerse Environment Configuration & Requirements Manifest.
|
| 7 |
+
* Centralizing all system variables for production-grade reliability.
|
| 8 |
+
*/
|
| 9 |
+
exports.ENV_CONFIG = {
|
| 10 |
+
// 1. Storage & Persistence
|
| 11 |
+
HF_TOKEN: process.env.HF_TOKEN || process.env.hfToken || process.env.HF_SPACE || process.env.HuggingFaceToken,
|
| 12 |
+
HF_DATASET_ID: process.env.HF_DATASET_ID || process.env.hfDataset || process.env.HF_DATASET,
|
| 13 |
+
WORKSPACE_ROOT: process.env.WORKSPACE_ROOT || '/home/node/w',
|
| 14 |
+
// 2. Build Acceleration
|
| 15 |
+
CACHIX_CACHE_NAME: process.env.CACHIX_CACHE_NAME || 'code-nix',
|
| 16 |
+
CACHIX_AUTH_TOKEN: process.env.CACHIX_AUTH_TOKEN,
|
| 17 |
+
// 3. Infrastructure State
|
| 18 |
+
NODE_ENV: process.env.NODE_ENV || 'production',
|
| 19 |
+
SPACE_ID: process.env.SPACE_ID,
|
| 20 |
+
APP_BASE_URL: process.env.NEXTAUTH_URL || 'http://localhost:7860',
|
| 21 |
+
IS_SBC: !!process.env.SPACE_ID,
|
| 22 |
+
// 4. Database & Auth
|
| 23 |
+
AUTH_SECRET: process.env.AUTH_SECRET || process.env.authSecret,
|
| 24 |
+
TURSO_URL: process.env.TURSO_URL || process.env.turso_url || process.env.database_url || process.env.TURSO_DATABASE_URL || process.env.DB_URL || process.env.turso_database_url,
|
| 25 |
+
TURSO_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN || process.env.turso_auth_token || process.env.DB_TOKEN,
|
| 26 |
+
TMPDIR: '/tmp',
|
| 27 |
+
HF_HOME: '/tmp/.cache/huggingface',
|
| 28 |
+
};
|
| 29 |
+
/**
|
| 30 |
+
* Validates that all critical infrastructure secrets are available.
|
| 31 |
+
*/
|
| 32 |
+
function validateEnvironment() {
|
| 33 |
+
const missing = [];
|
| 34 |
+
if (!exports.ENV_CONFIG.HF_TOKEN)
|
| 35 |
+
missing.push('HF_TOKEN (Missing Persistence Link)');
|
| 36 |
+
if (!exports.ENV_CONFIG.HF_DATASET_ID)
|
| 37 |
+
missing.push('HF_DATASET_ID (Missing Data Segment)');
|
| 38 |
+
if (!exports.ENV_CONFIG.AUTH_SECRET)
|
| 39 |
+
missing.push('AUTH_SECRET (Security Risk)');
|
| 40 |
+
if (!exports.ENV_CONFIG.TURSO_URL)
|
| 41 |
+
missing.push('TURSO_URL (Database Missing)');
|
| 42 |
+
// Strategic Dataset Validation
|
| 43 |
+
if (exports.ENV_CONFIG.HF_DATASET_ID && !exports.ENV_CONFIG.HF_DATASET_ID.includes('/')) {
|
| 44 |
+
return { valid: false, missing: ['HF_DATASET_ID_FORMAT_ERROR: Must be "username/dataset"'] };
|
| 45 |
+
}
|
| 46 |
+
return {
|
| 47 |
+
valid: missing.length === 0,
|
| 48 |
+
missing: missing
|
| 49 |
+
};
|
| 50 |
+
}
|
dist/lib/hf/storage.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.HFStorage = void 0;
|
| 7 |
+
const child_process_1 = require("child_process");
|
| 8 |
+
const path_1 = __importDefault(require("path"));
|
| 9 |
+
const fs_1 = __importDefault(require("fs"));
|
| 10 |
+
/**
|
| 11 |
+
* Hugging Face Storage Utility for 2026 CodeVerse Persistence.
|
| 12 |
+
*/
|
| 13 |
+
class HFStorage {
|
| 14 |
+
/**
|
| 15 |
+
* Internal helper for asynchronous execution with logging.
|
| 16 |
+
*/
|
| 17 |
+
static async execAsync(command, onLog) {
|
| 18 |
+
return new Promise((resolve, reject) => {
|
| 19 |
+
const spawnEnv = {
|
| 20 |
+
...process.env,
|
| 21 |
+
HF_TOKEN: this.HF_TOKEN,
|
| 22 |
+
HF_HOME: '/tmp/.cache/huggingface',
|
| 23 |
+
TMPDIR: '/tmp',
|
| 24 |
+
PATH: `/home/node/.local/bin:/home/node/.nix-profile/bin:/usr/local/bin:/usr/bin:${process.env.PATH}`
|
| 25 |
+
};
|
| 26 |
+
const child = (0, child_process_1.spawn)('/bin/bash', ['-c', command], {
|
| 27 |
+
env: spawnEnv
|
| 28 |
+
});
|
| 29 |
+
child.stdout.on('data', (data) => onLog === null || onLog === void 0 ? void 0 : onLog(data.toString().trim()));
|
| 30 |
+
child.stderr.on('data', (data) => {
|
| 31 |
+
const msg = data.toString().trim();
|
| 32 |
+
if (msg.includes('command not found')) {
|
| 33 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[CRITICAL] Binary missing: ${msg}. Current PATH: ${spawnEnv.PATH}`);
|
| 34 |
+
}
|
| 35 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] ${msg}`);
|
| 36 |
+
});
|
| 37 |
+
child.on('close', (code) => {
|
| 38 |
+
if (code === 0)
|
| 39 |
+
resolve();
|
| 40 |
+
else
|
| 41 |
+
reject(new Error(`Command failed with code ${code}: ${command}`));
|
| 42 |
+
});
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
/**
|
| 46 |
+
* Synchronizes the environment FROM the Hugging Face Dataset (Pull).
|
| 47 |
+
*/
|
| 48 |
+
static async syncFromDataset(onLog) {
|
| 49 |
+
if (!this.HF_TOKEN || !this.HF_DATASET_ID) {
|
| 50 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Persistence layer inactive. Missing credentials.`);
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Pulling persistent profile from '${this.HF_DATASET_ID}'...`);
|
| 54 |
+
try {
|
| 55 |
+
const tmpDir = '/tmp/hf-sync';
|
| 56 |
+
if (!fs_1.default.existsSync(tmpDir))
|
| 57 |
+
fs_1.default.mkdirSync(tmpDir, { recursive: true });
|
| 58 |
+
// 🟢 DELTA SYNCING: Only sync the specific workspace and IDE state directories
|
| 59 |
+
const home = process.env.HOME || '/home/node';
|
| 60 |
+
const persistDirs = ['w', '.vscode-server', '.config/code-server'];
|
| 61 |
+
for (const dir of persistDirs) {
|
| 62 |
+
const localPath = path_1.default.join(home, dir);
|
| 63 |
+
if (!fs_1.default.existsSync(localPath))
|
| 64 |
+
fs_1.default.mkdirSync(localPath, { recursive: true });
|
| 65 |
+
const cmd = `(command -v huggingface-cli >/dev/null && huggingface-cli download ${this.HF_DATASET_ID} --local-dir ${localPath} --include "${dir}/*" --token ${this.HF_TOKEN}) || (hf download ${this.HF_DATASET_ID} --local-dir ${localPath} --include "${dir}/*" --token ${this.HF_TOKEN})`;
|
| 66 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Restoring ${dir} from differential profile...`);
|
| 67 |
+
await this.execAsync(cmd, onLog).catch(() => { }); // Continue if one dir fails
|
| 68 |
+
}
|
| 69 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Profile restoration complete.`);
|
| 70 |
+
}
|
| 71 |
+
catch (e) {
|
| 72 |
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
| 73 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] Profile restoration failed: ${errorMessage}`);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
/**
|
| 77 |
+
* Synchronizes the environment TO the Hugging Face Dataset (Push).
|
| 78 |
+
*/
|
| 79 |
+
static async syncToDataset(onLog) {
|
| 80 |
+
if (!this.HF_TOKEN || !this.HF_DATASET_ID)
|
| 81 |
+
return;
|
| 82 |
+
try {
|
| 83 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Saving persistent profile to '${this.HF_DATASET_ID}'...`);
|
| 84 |
+
const home = process.env.HOME || '/home/node';
|
| 85 |
+
const persistDirs = ['w', '.vscode-server', '.config/code-server'];
|
| 86 |
+
for (const dir of persistDirs) {
|
| 87 |
+
const localPath = path_1.default.join(home, dir);
|
| 88 |
+
if (!fs_1.default.existsSync(localPath))
|
| 89 |
+
continue;
|
| 90 |
+
const cmd = `(command -v huggingface-cli >/dev/null && huggingface-cli upload ${this.HF_DATASET_ID} ${localPath} ${dir} --token ${this.HF_TOKEN} --message "CodeVerse Sync [${dir}]: ${new Date().toISOString()}" --exclude "node_modules/*" --exclude ".nix/*" --exclude ".direnv/*" --exclude ".cache/*") || (hf upload ${this.HF_DATASET_ID} ${localPath} ${dir} --token ${this.HF_TOKEN})`;
|
| 91 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Performing differential backup of ${dir}...`);
|
| 92 |
+
await this.execAsync(cmd, onLog).catch(err => onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] Sync failed for ${dir}: ${err.message}`));
|
| 93 |
+
}
|
| 94 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[HF:STORAGE] Profile backup successful.`);
|
| 95 |
+
}
|
| 96 |
+
catch (e) {
|
| 97 |
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
| 98 |
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] Profile synchronization failed: ${errorMessage}`);
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
static startAutoSave(intervalMs = 300000) {
|
| 102 |
+
if (this.autoSaveStarted)
|
| 103 |
+
return;
|
| 104 |
+
this.autoSaveStarted = true;
|
| 105 |
+
console.log(`[HF:STORAGE] Persistence heartbeat initialized (Interval: ${intervalMs}ms)`);
|
| 106 |
+
setInterval(async () => {
|
| 107 |
+
await this.syncToDataset((msg) => console.log(msg)).catch(() => { });
|
| 108 |
+
}, intervalMs);
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
exports.HFStorage = HFStorage;
|
| 112 |
+
HFStorage.HF_TOKEN = process.env.HF_TOKEN;
|
| 113 |
+
HFStorage.HF_DATASET_ID = process.env.HF_DATASET_ID;
|
| 114 |
+
HFStorage.PROFILE_PATH = path_1.default.join(process.env.HOME || '/home/node', '.nix-profile');
|
| 115 |
+
HFStorage.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/home/node/w';
|
| 116 |
+
/**
|
| 117 |
+
* Starts the periodic persistence interval (Singleton Heartbeat).
|
| 118 |
+
*/
|
| 119 |
+
HFStorage.autoSaveStarted = false;
|
dist/lib/idx/idx-engine.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.IdxEngine = void 0;
|
| 7 |
+
const fs_1 = __importDefault(require("fs"));
|
| 8 |
+
const path_1 = __importDefault(require("path"));
|
| 9 |
+
const child_process_1 = require("child_process");
|
| 10 |
+
/**
|
| 11 |
+
* IDX Engine for declarative workspace environments.
|
| 12 |
+
* Refactored for 2026 Asynchronous Execution to prevent Event Loop blocking.
|
| 13 |
+
*/
|
| 14 |
+
class IdxEngine {
|
| 15 |
+
/**
|
| 16 |
+
* Returns a robust baseline configuration for workspaces without a dev.nix.
|
| 17 |
+
*/
|
| 18 |
+
static getDefaultConfig() {
|
| 19 |
+
return {
|
| 20 |
+
packages: ['pkgs.nodejs', 'pkgs.go', 'pkgs.python3', 'pkgs.docker', 'pkgs.python3Packages.huggingface-hub'],
|
| 21 |
+
onCreate: 'npm install',
|
| 22 |
+
onStart: 'sleep 5 && npm run dev'
|
| 23 |
+
};
|
| 24 |
+
}
|
| 25 |
+
/**
|
| 26 |
+
* Detects and parses the .idx/dev.nix file in the workspace root.
|
| 27 |
+
*/
|
| 28 |
+
static getIdxConfig(workspacePath) {
|
| 29 |
+
const configPath = path_1.default.join(/*turbopackIgnore: true*/ workspacePath, '.idx', 'dev.nix');
|
| 30 |
+
if (!fs_1.default.existsSync(configPath))
|
| 31 |
+
return this.getDefaultConfig();
|
| 32 |
+
try {
|
| 33 |
+
const content = fs_1.default.readFileSync(configPath, 'utf8');
|
| 34 |
+
const packagesMatch = content.match(/packages\s*=\s*\[([\s\S]*?)\]/);
|
| 35 |
+
const onCreateMatch = content.match(/onCreate\s*=\s*"{1,3}([\s\S]*?)"{1,3}/);
|
| 36 |
+
const onStartMatch = content.match(/onStart\s*=\s*"{1,3}([\s\S]*?)"{1,3}/);
|
| 37 |
+
const config = {
|
| 38 |
+
packages: packagesMatch ? packagesMatch[1].split(/[\s\n,]+/).map(p => p.trim()).filter(p => p.length > 0) : [],
|
| 39 |
+
onCreate: onCreateMatch ? onCreateMatch[1].trim() : undefined,
|
| 40 |
+
onStart: onStartMatch ? onStartMatch[1].trim() : undefined
|
| 41 |
+
};
|
| 42 |
+
// Ensure baseline safety
|
| 43 |
+
if (config.packages.length === 0)
|
| 44 |
+
config.packages = this.getDefaultConfig().packages;
|
| 45 |
+
return config;
|
| 46 |
+
}
|
| 47 |
+
catch (e) {
|
| 48 |
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
| 49 |
+
console.warn(`[IDX-ENGINE] Failed to parse dev.nix, falling back to baseline:`, errorMessage);
|
| 50 |
+
return this.getDefaultConfig();
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
/**
|
| 54 |
+
* Synchronizes the Nix environment based on the declarative packages.
|
| 55 |
+
* ASYNCHRONOUS Spawning to prevent Blocking.
|
| 56 |
+
*/
|
| 57 |
+
static async syncNixEnvironment(workspacePath, config, onLog) {
|
| 58 |
+
if (!config.packages || config.packages.length === 0)
|
| 59 |
+
return;
|
| 60 |
+
const log = (msg) => { if (onLog)
|
| 61 |
+
onLog(`[IDX:NIX] ${msg}`); };
|
| 62 |
+
log(`Syncing system packages: ${config.packages.join(', ')}...`);
|
| 63 |
+
// CACHIX ACCELERATION: Robust check for binary existence to prevent ENOENT crash
|
| 64 |
+
const cachixName = process.env.CACHIX_CACHE_NAME || 'code-nix';
|
| 65 |
+
let hasCachix = false;
|
| 66 |
+
try {
|
| 67 |
+
await new Promise((resolve) => {
|
| 68 |
+
const check = (0, child_process_1.spawn)('command', ['-v', 'cachix'], { shell: true });
|
| 69 |
+
check.on('close', (code) => {
|
| 70 |
+
hasCachix = (code === 0);
|
| 71 |
+
resolve();
|
| 72 |
+
});
|
| 73 |
+
check.on('error', () => {
|
| 74 |
+
hasCachix = false;
|
| 75 |
+
resolve();
|
| 76 |
+
});
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
catch (_a) {
|
| 80 |
+
hasCachix = false;
|
| 81 |
+
}
|
| 82 |
+
if (hasCachix) {
|
| 83 |
+
const cachixToken = process.env.CACHIX_AUTH_TOKEN;
|
| 84 |
+
if (cachixToken) {
|
| 85 |
+
log(`Cachix authentication detected. Configuring access...`);
|
| 86 |
+
await new Promise((resolve) => {
|
| 87 |
+
const auth = (0, child_process_1.spawn)('cachix', ['authtoken', cachixToken], { env: { ...process.env, HOME: workspacePath } });
|
| 88 |
+
auth.on('close', () => resolve());
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
log(`Cachix acceleration detected. Setting up cache: ${cachixName}...`);
|
| 92 |
+
try {
|
| 93 |
+
await new Promise((resolve, reject) => {
|
| 94 |
+
const child = (0, child_process_1.spawn)('cachix', ['use', cachixName], {
|
| 95 |
+
cwd: workspacePath,
|
| 96 |
+
env: { ...process.env, HOME: workspacePath }
|
| 97 |
+
});
|
| 98 |
+
child.on('error', (err) => reject(err));
|
| 99 |
+
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`Cachix failed with code ${code}`)));
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
catch (_b) {
|
| 103 |
+
log(`[WARN] Cachix setup bypassed. Falling back to default binary cache.`);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
// 🟢 HYDRATION GUARD: Skip synchronization if packages haven't changed or if pre-baked baseline is available
|
| 107 |
+
const idxDir = path_1.default.join(workspacePath, '.idx');
|
| 108 |
+
if (!fs_1.default.existsSync(idxDir))
|
| 109 |
+
fs_1.default.mkdirSync(idxDir, { recursive: true });
|
| 110 |
+
const manifestPath = path_1.default.join(idxDir, 'packages.json');
|
| 111 |
+
const bakedManifestPath = '/home/node/.idx/baked-packages.json';
|
| 112 |
+
// 1. Check local manifest
|
| 113 |
+
if (fs_1.default.existsSync(manifestPath)) {
|
| 114 |
+
try {
|
| 115 |
+
const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
| 116 |
+
const currentSorted = [...config.packages].sort();
|
| 117 |
+
const manifestSorted = [...(manifest.packages || [])].sort();
|
| 118 |
+
if (JSON.stringify(currentSorted) === JSON.stringify(manifestSorted)) {
|
| 119 |
+
log(`Environment already synchronized. Skipping profile update.`);
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
catch (e) {
|
| 124 |
+
log(`[WARN] Manifest corruption detected. Forcing re-sync.`);
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
// 2. Check pre-baked manifest (for default configs)
|
| 128 |
+
const sortedDefault = [...IdxEngine.getDefaultConfig().packages].sort();
|
| 129 |
+
const sortedCurrent = [...config.packages].sort();
|
| 130 |
+
const isDefaultConfig = JSON.stringify(sortedCurrent) === JSON.stringify(sortedDefault);
|
| 131 |
+
if (isDefaultConfig && fs_1.default.existsSync(bakedManifestPath)) {
|
| 132 |
+
log(`Pre-baked baseline detected. Hydrating instance instantly...`);
|
| 133 |
+
try {
|
| 134 |
+
fs_1.default.copyFileSync(bakedManifestPath, manifestPath);
|
| 135 |
+
log(`Hydration complete. Workspace ready.`);
|
| 136 |
+
return;
|
| 137 |
+
}
|
| 138 |
+
catch (e) {
|
| 139 |
+
log(`[WARN] Hydration failed: ${e instanceof Error ? e.message : String(e)}`);
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
// CACHIX ...
|
| 143 |
+
// ... (Cachix code remains the same or slightly optimized)
|
| 144 |
+
const batchTargets = config.packages.map(pkg => `nixpkgs#${pkg.replace('pkgs.', '')}`);
|
| 145 |
+
log(`Batch installing: ${batchTargets.join(', ')}...`);
|
| 146 |
+
await new Promise((resolve, reject) => {
|
| 147 |
+
const child = (0, child_process_1.spawn)('nix', ['profile', 'add', ...batchTargets], {
|
| 148 |
+
cwd: workspacePath,
|
| 149 |
+
env: {
|
| 150 |
+
...process.env,
|
| 151 |
+
HOME: workspacePath,
|
| 152 |
+
NIX_CONFIG: 'experimental-features = nix-command flakes'
|
| 153 |
+
}
|
| 154 |
+
});
|
| 155 |
+
child.stdout.on('data', (data) => log(data.toString().trim()));
|
| 156 |
+
child.stderr.on('data', (data) => log(`[INFO] ${data.toString().trim()}`));
|
| 157 |
+
child.on('close', (code) => {
|
| 158 |
+
if (code === 0) {
|
| 159 |
+
fs_1.default.writeFileSync(manifestPath, JSON.stringify({ packages: config.packages, timestamp: new Date().toISOString() }));
|
| 160 |
+
resolve();
|
| 161 |
+
}
|
| 162 |
+
else {
|
| 163 |
+
reject(new Error(`Batch Nix installation failed with code ${code}`));
|
| 164 |
+
}
|
| 165 |
+
});
|
| 166 |
+
}).catch((err) => {
|
| 167 |
+
const errMsg = err instanceof Error ? err.message : String(err);
|
| 168 |
+
log(`[ERROR] ${errMsg}`);
|
| 169 |
+
});
|
| 170 |
+
log(`Environment synchronized successfully.`);
|
| 171 |
+
}
|
| 172 |
+
/**
|
| 173 |
+
* Executes the 'onCreate' and 'onStart' hooks.
|
| 174 |
+
* supports background execution for 'onStart' to prevent blocking the IDE handshake.
|
| 175 |
+
*/
|
| 176 |
+
static async runHook(workspacePath, hookName, script, onLog, background = false) {
|
| 177 |
+
const log = (msg) => { if (onLog)
|
| 178 |
+
onLog(`[IDX:HOOK] ${hookName}: ${msg}`); };
|
| 179 |
+
log(`Executing script... ${background ? '(Background)' : ''}`);
|
| 180 |
+
const hookPromise = new Promise((resolve, reject) => {
|
| 181 |
+
// 🟢 PORT DE-CONFLICTION: Ensure hooks don't inherit the main orchestrator's port 7860
|
| 182 |
+
const spawnEnv = { ...process.env, HOME: workspacePath };
|
| 183 |
+
delete spawnEnv.PORT;
|
| 184 |
+
delete spawnEnv.SERVER_PORT;
|
| 185 |
+
const child = (0, child_process_1.spawn)('/bin/bash', ['-c', script], {
|
| 186 |
+
cwd: workspacePath,
|
| 187 |
+
env: spawnEnv
|
| 188 |
+
});
|
| 189 |
+
child.stdout.on('data', (data) => log(data.toString().trim()));
|
| 190 |
+
child.stderr.on('data', (data) => log(`[WARN] ${data.toString().trim()}`));
|
| 191 |
+
child.on('close', (code) => {
|
| 192 |
+
if (code === 0) {
|
| 193 |
+
log(`Hook ${hookName} completed successfully.`);
|
| 194 |
+
resolve();
|
| 195 |
+
}
|
| 196 |
+
else {
|
| 197 |
+
const err = new Error(`Hook ${hookName} failed with code ${code}`);
|
| 198 |
+
log(`[ERROR] ${err.message}`);
|
| 199 |
+
reject(err);
|
| 200 |
+
}
|
| 201 |
+
});
|
| 202 |
+
// If background, resolve immediately after spawn
|
| 203 |
+
if (background) {
|
| 204 |
+
log(`Hook detached and running in baseline context.`);
|
| 205 |
+
resolve();
|
| 206 |
+
}
|
| 207 |
+
});
|
| 208 |
+
if (!background) {
|
| 209 |
+
await hookPromise.catch(() => { }); // Catch handled in promise
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
exports.IdxEngine = IdxEngine;
|
dist/lib/jobs/auto-sleep.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.checkIdleContainers = checkIdleContainers;
|
| 7 |
+
exports.startAutoSleepCron = startAutoSleepCron;
|
| 8 |
+
const dockerode_1 = __importDefault(require("dockerode"));
|
| 9 |
+
const manager_1 = require("../docker/manager");
|
| 10 |
+
const db_1 = require("../db");
|
| 11 |
+
const docker = new dockerode_1.default({ socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock' });
|
| 12 |
+
// Store previous network RX bytes to calculate delta
|
| 13 |
+
const networkThresholds = new Map();
|
| 14 |
+
async function checkIdleContainers() {
|
| 15 |
+
const status = await (0, manager_1.isDockerAvailable)();
|
| 16 |
+
if (!status.available) {
|
| 17 |
+
return;
|
| 18 |
+
}
|
| 19 |
+
console.log("[Auto-Sleep] Running idle container check...");
|
| 20 |
+
try {
|
| 21 |
+
const containers = await docker.listContainers({
|
| 22 |
+
filters: { name: ["codeverse-workspace-"] }
|
| 23 |
+
});
|
| 24 |
+
for (const c of containers) {
|
| 25 |
+
const containerName = c.Names[0].replace("/", "");
|
| 26 |
+
const workspaceId = containerName.replace("codeverse-workspace-", "");
|
| 27 |
+
const container = docker.getContainer(c.Id);
|
| 28 |
+
const stats = await container.stats({ stream: false });
|
| 29 |
+
// Extract network Rx (received) bytes
|
| 30 |
+
let currentRx = 0;
|
| 31 |
+
if (stats.networks && stats.networks.eth0) {
|
| 32 |
+
currentRx = stats.networks.eth0.rx_bytes;
|
| 33 |
+
}
|
| 34 |
+
const previousRx = networkThresholds.get(workspaceId);
|
| 35 |
+
if (previousRx !== undefined) {
|
| 36 |
+
const delta = currentRx - previousRx;
|
| 37 |
+
// If delta is less than 5KB over the interval, it's considered idle
|
| 38 |
+
if (delta < 5000) {
|
| 39 |
+
console.log(`[Auto-Sleep] Workspace ${workspaceId} is idle. Shutting down.`);
|
| 40 |
+
await (0, manager_1.stopWorkspaceContainer)(workspaceId);
|
| 41 |
+
// Update DB
|
| 42 |
+
await db_1.db.execute({
|
| 43 |
+
sql: "UPDATE workspaces SET status = 'sleeping' WHERE id = ?",
|
| 44 |
+
args: [workspaceId]
|
| 45 |
+
});
|
| 46 |
+
networkThresholds.delete(workspaceId);
|
| 47 |
+
continue; // Skip setting new threshold
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
// Set threshold for next check
|
| 51 |
+
networkThresholds.set(workspaceId, currentRx);
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
catch (e) {
|
| 55 |
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
| 56 |
+
console.error("[Auto-Sleep] Error running cron:", errorMessage);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
function startAutoSleepCron() {
|
| 60 |
+
// Run every 30 minutes (1800000 ms)
|
| 61 |
+
// For testing/dev we run it every 5 minutes (300000 ms)
|
| 62 |
+
const interval = process.env.NODE_ENV === "production" ? 1800000 : 300000;
|
| 63 |
+
setInterval(checkIdleContainers, interval);
|
| 64 |
+
console.log(`[Auto-Sleep] Cron initialized. Running every ${interval / 60000} minutes.`);
|
| 65 |
+
}
|
dist/server.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
| 3 |
+
if (k2 === undefined) k2 = k;
|
| 4 |
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
| 5 |
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
| 6 |
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
| 7 |
+
}
|
| 8 |
+
Object.defineProperty(o, k2, desc);
|
| 9 |
+
}) : (function(o, m, k, k2) {
|
| 10 |
+
if (k2 === undefined) k2 = k;
|
| 11 |
+
o[k2] = m[k];
|
| 12 |
+
}));
|
| 13 |
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
| 14 |
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
| 15 |
+
}) : function(o, v) {
|
| 16 |
+
o["default"] = v;
|
| 17 |
+
});
|
| 18 |
+
var __importStar = (this && this.__importStar) || (function () {
|
| 19 |
+
var ownKeys = function(o) {
|
| 20 |
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
| 21 |
+
var ar = [];
|
| 22 |
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
| 23 |
+
return ar;
|
| 24 |
+
};
|
| 25 |
+
return ownKeys(o);
|
| 26 |
+
};
|
| 27 |
+
return function (mod) {
|
| 28 |
+
if (mod && mod.__esModule) return mod;
|
| 29 |
+
var result = {};
|
| 30 |
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
| 31 |
+
__setModuleDefault(result, mod);
|
| 32 |
+
return result;
|
| 33 |
+
};
|
| 34 |
+
})();
|
| 35 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 36 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 37 |
+
};
|
| 38 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 39 |
+
/**
|
| 40 |
+
* 🛰️ GLOBAL STABILIZATION (April 2026): Catch unhandled errors that cause HF Space restarts.
|
| 41 |
+
*/
|
| 42 |
+
process.on('uncaughtException', (err) => { console.error('[FATAL:EXCEPTION]', err); });
|
| 43 |
+
process.on('unhandledRejection', (reason) => { console.error('[FATAL:REJECTION]', reason); });
|
| 44 |
+
const http_1 = require("http");
|
| 45 |
+
const next_1 = __importDefault(require("next"));
|
| 46 |
+
const socket_io_1 = require("socket.io");
|
| 47 |
+
const ws_1 = require("ws");
|
| 48 |
+
const Y = __importStar(require("yjs"));
|
| 49 |
+
const awarenessProtocol = __importStar(require("y-protocols/awareness"));
|
| 50 |
+
const syncProtocol = __importStar(require("y-protocols/sync"));
|
| 51 |
+
const encoding = __importStar(require("lib0/encoding"));
|
| 52 |
+
const decoding = __importStar(require("lib0/decoding"));
|
| 53 |
+
const map = __importStar(require("lib0/map"));
|
| 54 |
+
const pty = __importStar(require("node-pty"));
|
| 55 |
+
const os_1 = __importDefault(require("os"));
|
| 56 |
+
const auto_sleep_1 = require("./lib/jobs/auto-sleep");
|
| 57 |
+
const manager_1 = require("./lib/docker/manager");
|
| 58 |
+
const schema_1 = require("./lib/db/schema");
|
| 59 |
+
const db_1 = require("./lib/db");
|
| 60 |
+
const storage_1 = require("./lib/hf/storage");
|
| 61 |
+
const env_config_1 = require("./lib/env-config");
|
| 62 |
+
const http_proxy_1 = __importDefault(require("http-proxy"));
|
| 63 |
+
const constants_1 = require("./constants");
|
| 64 |
+
/**
|
| 65 |
+
* PRODUCTION HARDENING (April 2026): Force writable temp paths for HF Spaces.
|
| 66 |
+
*/
|
| 67 |
+
process.env.TMPDIR = constants_1.INFRA_CONFIG.TMPDIR;
|
| 68 |
+
process.env.HF_HOME = constants_1.INFRA_CONFIG.HF_HOME;
|
| 69 |
+
if (!process.env.HOME)
|
| 70 |
+
process.env.HOME = '/home/node';
|
| 71 |
+
const dev = process.env.NODE_ENV !== "production";
|
| 72 |
+
const app = (0, next_1.default)({ dev });
|
| 73 |
+
const handle = app.getRequestHandler();
|
| 74 |
+
const docs = new Map();
|
| 75 |
+
const getOrCreateDoc = (docName) => {
|
| 76 |
+
return map.setIfUndefined(docs, docName, () => {
|
| 77 |
+
const doc = new Y.Doc();
|
| 78 |
+
const awareness = new awarenessProtocol.Awareness(doc);
|
| 79 |
+
return { doc, awareness };
|
| 80 |
+
});
|
| 81 |
+
};
|
| 82 |
+
const proxy = http_proxy_1.default.createProxyServer({
|
| 83 |
+
ws: true,
|
| 84 |
+
xfwd: true,
|
| 85 |
+
timeout: 30000,
|
| 86 |
+
proxyTimeout: 30000
|
| 87 |
+
});
|
| 88 |
+
function renderProxyError(res, error, id) {
|
| 89 |
+
res.writeHead(502, { 'Content-Type': 'text/html' });
|
| 90 |
+
res.end(`
|
| 91 |
+
<!DOCTYPE html>
|
| 92 |
+
<html lang="en">
|
| 93 |
+
<head>
|
| 94 |
+
<meta charset="UTF-8">
|
| 95 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 96 |
+
<title>Workspace Connection Failure</title>
|
| 97 |
+
<style>
|
| 98 |
+
body { background: #0f1117; color: #e2e8f0; font-family: -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
| 99 |
+
.card { background: #1e293b; padding: 2.5rem; border-radius: 1rem; border: 1px solid #334155; text-align: center; max-width: 450px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
|
| 100 |
+
h1 { color: #f87171; font-size: 1.5rem; margin-bottom: 1rem; }
|
| 101 |
+
p { font-size: 0.875rem; color: #94a3b8; line-height: 1.6; }
|
| 102 |
+
.id { font-family: monospace; background: #0f172a; padding: 0.4rem 0.6rem; border-radius: 0.4rem; color: #38bdf8; font-size: 0.8rem; }
|
| 103 |
+
.btn { display: inline-block; background: #38bdf8; color: #0f172a; padding: 0.6rem 1.2rem; border-radius: 0.4rem; text-decoration: none; font-weight: bold; margin-top: 1.5rem; transition: transform 0.2s; }
|
| 104 |
+
.btn:hover { transform: scale(1.05); }
|
| 105 |
+
</style>
|
| 106 |
+
</head>
|
| 107 |
+
<body>
|
| 108 |
+
<div class="card">
|
| 109 |
+
<h1>Workspace Connection Restricted</h1>
|
| 110 |
+
<p>Native isolation link for <span class="id">${id}</span> failed.</p>
|
| 111 |
+
<p style="margin-top: 1rem; text-align: left; padding: 1rem; background: #0f172a; border-radius: 0.5rem; font-size: 0.75rem; color: #64748b;">
|
| 112 |
+
<b>Diagnostic:</b> ${error}<br>
|
| 113 |
+
<b>Target:</b> Hugging Face Space (Sandboxed)
|
| 114 |
+
</p>
|
| 115 |
+
<a href="/dashboard/booting?id=${id}" class="btn">Auto-Repair & Boot</a>
|
| 116 |
+
</div>
|
| 117 |
+
</body>
|
| 118 |
+
</html>
|
| 119 |
+
`);
|
| 120 |
+
}
|
| 121 |
+
proxy.on("error", (err, req, res) => {
|
| 122 |
+
const host = req.headers.host || "";
|
| 123 |
+
const fullUrl = new URL(req.url || "/", `http://${host}`);
|
| 124 |
+
const pathname = fullUrl.pathname;
|
| 125 |
+
const headerId = req.headers['x-codeverse-id'];
|
| 126 |
+
const workspaceHostMatch = host.match(/^workspace-([a-zA-Z0-9-]+)\./);
|
| 127 |
+
const id = headerId || (workspaceHostMatch ? workspaceHostMatch[1] : (pathname.split("/")[2] || "unknown"));
|
| 128 |
+
console.error(`[Proxy Connection Error] ${err.message} for workspace/${id}`);
|
| 129 |
+
if (res instanceof http_1.ServerResponse) {
|
| 130 |
+
renderProxyError(res, err.message, id);
|
| 131 |
+
}
|
| 132 |
+
});
|
| 133 |
+
// Port and Host logic
|
| 134 |
+
const PORT = Number(process.env.PORT) || 7860;
|
| 135 |
+
const HOST = '0.0.0.0';
|
| 136 |
+
let isAppReady = false;
|
| 137 |
+
let envStatus = { valid: true, missing: [] };
|
| 138 |
+
/**
|
| 139 |
+
*Autoritative Entrypoint: We initialize the HTTP server immediately to satisfy HF Spaces health checks.
|
| 140 |
+
*/
|
| 141 |
+
const server = (0, http_1.createServer)((req, res) => {
|
| 142 |
+
var _a;
|
| 143 |
+
const host = req.headers.host || "localhost";
|
| 144 |
+
const fullUrl = new URL(req.url || "/", `http://${host}`);
|
| 145 |
+
const { pathname } = fullUrl;
|
| 146 |
+
// 1. Initializing State
|
| 147 |
+
if (!isAppReady && !pathname.startsWith("/_next/static") && pathname !== "/favicon.ico") {
|
| 148 |
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
| 149 |
+
return res.end(`
|
| 150 |
+
<html>
|
| 151 |
+
<head>
|
| 152 |
+
<title>CodeVerse | Initializing</title>
|
| 153 |
+
<style>
|
| 154 |
+
body { background: #09090b; color: #71717a; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
| 155 |
+
.container { text-align: center; border: 1px solid #27272a; padding: 2.5rem; border-radius: 1rem; background: #111113; max-width: 400px; }
|
| 156 |
+
.spinner { width: 30px; height: 30px; border: 2px solid #3f3f46; border-top-color: #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }
|
| 157 |
+
h1 { color: #f4f4f5; font-size: 1.1rem; margin: 0; }
|
| 158 |
+
p { font-size: 0.85rem; margin-top: 0.5rem; }
|
| 159 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 160 |
+
</style>
|
| 161 |
+
<script>setTimeout(() => window.location.reload(), 5000);</script>
|
| 162 |
+
</head>
|
| 163 |
+
<body>
|
| 164 |
+
<div class="container">
|
| 165 |
+
<div class="spinner"></div>
|
| 166 |
+
<h1>CodeVerse is waking up</h1>
|
| 167 |
+
<p>Restoring your environment and securing persistent volumes...</p>
|
| 168 |
+
</div>
|
| 169 |
+
</body>
|
| 170 |
+
</html>
|
| 171 |
+
`);
|
| 172 |
+
}
|
| 173 |
+
// 2. Maintenance / Misconfiguration State
|
| 174 |
+
if (isAppReady && !envStatus.valid && pathname !== "/api/health" && !pathname.startsWith("/_next/")) {
|
| 175 |
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
| 176 |
+
return res.end(`
|
| 177 |
+
<html>
|
| 178 |
+
<body style="background:#09090b;color:#f87171;padding:2rem;font-family:sans-serif;">
|
| 179 |
+
<h1>Infrastructure Error</h1>
|
| 180 |
+
<p>${constants_1.UI_STRINGS.MAINTENANCE_MESSAGE}</p>
|
| 181 |
+
<ul>${envStatus.missing.map(m => `<li>${m}</li>`).join('')}</ul>
|
| 182 |
+
</body>
|
| 183 |
+
</html>
|
| 184 |
+
`);
|
| 185 |
+
}
|
| 186 |
+
// 3. Workspace Proxying
|
| 187 |
+
const workspaceHostMatch = host.match(/^workspace-([a-zA-Z0-9-]+)\./);
|
| 188 |
+
const id = workspaceHostMatch ? workspaceHostMatch[1] : ((pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/workspace/")) ? pathname.split("/")[2] : null);
|
| 189 |
+
if (id) {
|
| 190 |
+
const isReady = (0, manager_1.isNativeWorkspaceRunning)(id);
|
| 191 |
+
if (isReady) {
|
| 192 |
+
const port = (0, manager_1.getNativeWorkspacePort)(id) || 8080;
|
| 193 |
+
req.headers['x-codeverse-id'] = id;
|
| 194 |
+
req.headers['x-codeverse-type'] = 'workspace';
|
| 195 |
+
const prefix = `/workspace/${id}`;
|
| 196 |
+
if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(prefix)) {
|
| 197 |
+
req.url = req.url.substring(prefix.length);
|
| 198 |
+
if (!req.url.startsWith("/"))
|
| 199 |
+
req.url = "/" + req.url;
|
| 200 |
+
}
|
| 201 |
+
return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
// 4. Default Next.js Handle
|
| 205 |
+
handle(req, res);
|
| 206 |
+
});
|
| 207 |
+
// Setup Sockets
|
| 208 |
+
const io = new socket_io_1.Server(server, { path: "/api/socketio" });
|
| 209 |
+
const yjsWss = new ws_1.WebSocketServer({ noServer: true });
|
| 210 |
+
// Start Listening Immediately
|
| 211 |
+
server.listen(PORT, HOST, () => {
|
| 212 |
+
console.log('----------------------------------------------------');
|
| 213 |
+
console.log(`[READY] ${constants_1.APP_CONFIG.NAME} ${constants_1.APP_CONFIG.VERSION}`);
|
| 214 |
+
console.log(`[READY] Interface: ${HOST}:${PORT}`);
|
| 215 |
+
console.log('----------------------------------------------------');
|
| 216 |
+
});
|
| 217 |
+
// Background Async Initialization
|
| 218 |
+
(async () => {
|
| 219 |
+
try {
|
| 220 |
+
console.log("[BOOT] Starting Next.js preparation...");
|
| 221 |
+
await app.prepare();
|
| 222 |
+
console.log("[BOOT] Next.js payload ready.");
|
| 223 |
+
envStatus = (0, env_config_1.validateEnvironment)();
|
| 224 |
+
if (envStatus.valid) {
|
| 225 |
+
console.log("[BOOT] Environment validated. Synchronizing database...");
|
| 226 |
+
await (0, schema_1.initDb)(db_1.client);
|
| 227 |
+
console.log("[BOOT] Database synchronized.");
|
| 228 |
+
// Reconnect and Warmup
|
| 229 |
+
(0, manager_1.reconnectRunningWorkspaces)().catch(() => { });
|
| 230 |
+
(0, manager_1.prewarmWorkspace)({ id: 'baseline-warmup', userId: 'system', projectName: 'CodeVerse-Internal' }).catch(() => { });
|
| 231 |
+
// Crons and Persistence
|
| 232 |
+
storage_1.HFStorage.startAutoSave(constants_1.INFRA_CONFIG.PERSISTENCE_INTERVAL_MS * 5);
|
| 233 |
+
(0, auto_sleep_1.startAutoSleepCron)();
|
| 234 |
+
}
|
| 235 |
+
isAppReady = true;
|
| 236 |
+
console.log("[BOOT] Global state stabilized. Application is fully operational.");
|
| 237 |
+
}
|
| 238 |
+
catch (err) {
|
| 239 |
+
console.error("[BOOT:ERROR] Fatal initialization failure:", err);
|
| 240 |
+
}
|
| 241 |
+
})();
|
| 242 |
+
// Terminal and Collaboration Handlers
|
| 243 |
+
server.on("upgrade", (req, socket, head) => {
|
| 244 |
+
const { pathname } = new URL(req.url || "/", `http://${req.headers.host}`);
|
| 245 |
+
if (pathname === "/api/collab") {
|
| 246 |
+
yjsWss.handleUpgrade(req, socket, head, (ws) => {
|
| 247 |
+
yjsWss.emit("connection", ws, req);
|
| 248 |
+
});
|
| 249 |
+
}
|
| 250 |
+
});
|
| 251 |
+
yjsWss.on("connection", (conn, request) => {
|
| 252 |
+
const { doc, awareness } = getOrCreateDoc(new URL(request.url || "/", "http://l").searchParams.get('doc') || "default");
|
| 253 |
+
conn.binaryType = "arraybuffer";
|
| 254 |
+
const encoder = encoding.createEncoder();
|
| 255 |
+
encoding.writeVarUint(encoder, 0);
|
| 256 |
+
syncProtocol.writeSyncStep1(encoder, doc);
|
| 257 |
+
conn.send(encoding.toUint8Array(encoder));
|
| 258 |
+
conn.on("message", (message) => {
|
| 259 |
+
const encoder = encoding.createEncoder();
|
| 260 |
+
const decoder = decoding.createDecoder(new Uint8Array(message));
|
| 261 |
+
const messageType = decoding.readVarUint(decoder);
|
| 262 |
+
if (messageType === 0) {
|
| 263 |
+
encoding.writeVarUint(encoder, 0);
|
| 264 |
+
syncProtocol.readSyncMessage(decoder, encoder, doc, null);
|
| 265 |
+
if (encoding.length(encoder) > 1)
|
| 266 |
+
conn.send(encoding.toUint8Array(encoder));
|
| 267 |
+
}
|
| 268 |
+
});
|
| 269 |
+
});
|
| 270 |
+
io.on("connection", (socket) => {
|
| 271 |
+
let shell = null;
|
| 272 |
+
socket.on("terminal:start", ({ cols, rows }) => {
|
| 273 |
+
shell = pty.spawn(process.env.SHELL || (os_1.default.platform() === "win32" ? "powershell.exe" : "bash"), [], {
|
| 274 |
+
cols: cols || 80,
|
| 275 |
+
rows: rows || 24,
|
| 276 |
+
cwd: constants_1.INFRA_CONFIG.WORKSPACE_ROOT,
|
| 277 |
+
env: process.env,
|
| 278 |
+
});
|
| 279 |
+
shell.onData((data) => socket.emit("terminal:data", data));
|
| 280 |
+
});
|
| 281 |
+
socket.on("terminal:write", (data) => { if (shell)
|
| 282 |
+
shell.write(data); });
|
| 283 |
+
socket.on("disconnect", () => { if (shell)
|
| 284 |
+
shell.kill(); });
|
| 285 |
+
});
|
lib/db/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import { createClient, type Client } from "@libsql/client";
|
| 2 |
-
import { initDb } from "./schema";
|
| 3 |
|
| 4 |
const url = process.env.TURSO_URL || "file:local.db";
|
| 5 |
const authToken = process.env.TURSO_AUTH_TOKEN;
|
|
@@ -20,8 +19,4 @@ export const client: Client = createClient({
|
|
| 20 |
*/
|
| 21 |
export const db = client;
|
| 22 |
|
| 23 |
-
// 🚀
|
| 24 |
-
// We pass the client directly to avoid circular dependency with schema.ts
|
| 25 |
-
initDb(client).catch((err: unknown) => {
|
| 26 |
-
console.error("[DB:ERROR] Database bootstrap failed:", err);
|
| 27 |
-
});
|
|
|
|
| 1 |
import { createClient, type Client } from "@libsql/client";
|
|
|
|
| 2 |
|
| 3 |
const url = process.env.TURSO_URL || "file:local.db";
|
| 4 |
const authToken = process.env.TURSO_AUTH_TOKEN;
|
|
|
|
| 19 |
*/
|
| 20 |
export const db = client;
|
| 21 |
|
| 22 |
+
// 🚀 Database lifecycle is managed by the server entrypoint to ensure proper synchronization.
|
|
|
|
|
|
|
|
|
|
|
|
package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"dev": "next dev",
|
|
|
|
| 7 |
"build": "next build && tsc server.ts --esModuleInterop --skipLibCheck --target es2018 --module commonjs --moduleResolution node --downlevelIteration --outDir ./dist",
|
| 8 |
"start": "node dist/server.js",
|
| 9 |
"lint": "eslint . --ignore-pattern dist/ --ignore-pattern .next/"
|
|
|
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"dev": "next dev",
|
| 7 |
+
"prebuild": "npx rimraf dist",
|
| 8 |
"build": "next build && tsc server.ts --esModuleInterop --skipLibCheck --target es2018 --module commonjs --moduleResolution node --downlevelIteration --outDir ./dist",
|
| 9 |
"start": "node dist/server.js",
|
| 10 |
"lint": "eslint . --ignore-pattern dist/ --ignore-pattern .next/"
|
server.ts
CHANGED
|
@@ -18,7 +18,7 @@ import * as pty from "node-pty";
|
|
| 18 |
import os from "os";
|
| 19 |
import { Duplex } from "stream";
|
| 20 |
import { startAutoSleepCron } from "./lib/jobs/auto-sleep";
|
| 21 |
-
import { getNativeWorkspacePort,
|
| 22 |
import { initDb } from "./lib/db/schema";
|
| 23 |
import { client as dbClient } from "./lib/db";
|
| 24 |
import { HFStorage } from "./lib/hf/storage";
|
|
@@ -46,9 +46,6 @@ const getOrCreateDoc = (docName: string) => {
|
|
| 46 |
});
|
| 47 |
};
|
| 48 |
|
| 49 |
-
/**
|
| 50 |
-
* PRODUCTION PROXY CONFIG (2026 Optimized)
|
| 51 |
-
*/
|
| 52 |
const proxy = httpProxy.createProxyServer({
|
| 53 |
ws: true,
|
| 54 |
xfwd: true,
|
|
@@ -56,17 +53,6 @@ const proxy = httpProxy.createProxyServer({
|
|
| 56 |
proxyTimeout: 30000
|
| 57 |
});
|
| 58 |
|
| 59 |
-
// 🟢 Production Pre-flight Diagnostics (April 2026)
|
| 60 |
-
console.log('----------------------------------------------------');
|
| 61 |
-
console.log(`[BOOT] ${APP_CONFIG.NAME} ${APP_CONFIG.VERSION} Initialized.`);
|
| 62 |
-
console.log(`[BOOT] Environment: ${process.env.NODE_ENV || 'development'}`);
|
| 63 |
-
console.log(`[BOOT] Database State: ${process.env.TURSO_URL ? '✅ CONFIGURED' : '❌ MISSING'}`);
|
| 64 |
-
console.log(`[BOOT] Persistence Link: ${process.env.HF_TOKEN ? '✅ CONFIGURED' : '⚠️ UNLINKED'}`);
|
| 65 |
-
console.log('----------------------------------------------------');
|
| 66 |
-
|
| 67 |
-
/**
|
| 68 |
-
* Custom renderer for Proxy Errors and Booting screens.
|
| 69 |
-
*/
|
| 70 |
function renderProxyError(res: ServerResponse, error: string, id: string) {
|
| 71 |
res.writeHead(502, { 'Content-Type': 'text/html' });
|
| 72 |
res.end(`
|
|
@@ -117,239 +103,168 @@ proxy.on("error", (err: Error, req: IncomingMessage, res: ServerResponse | Duple
|
|
| 117 |
}
|
| 118 |
});
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
if (id && type) {
|
| 124 |
-
proxyReq.setHeader('x-codeverse-id', id);
|
| 125 |
-
proxyReq.setHeader('x-codeverse-type', type);
|
| 126 |
-
}
|
| 127 |
-
});
|
| 128 |
-
|
| 129 |
-
proxy.on("proxyRes", (proxyRes, req: IncomingMessage) => {
|
| 130 |
-
const id = req.headers['x-codeverse-id'] as string;
|
| 131 |
-
const type = req.headers['x-codeverse-type'] as string;
|
| 132 |
-
if (id && type && proxyRes.headers.location) {
|
| 133 |
-
const originalLocation = proxyRes.headers.location;
|
| 134 |
-
if (originalLocation.startsWith('/') && !originalLocation.startsWith(`/${type}/${id}`)) {
|
| 135 |
-
proxyRes.headers.location = `/${type}/${id}${originalLocation}`;
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
});
|
| 139 |
-
|
| 140 |
-
app.prepare()
|
| 141 |
-
.then(() => {
|
| 142 |
-
// Validate Production Environment (April 2026 Resilience)
|
| 143 |
-
const envStatus = validateEnvironment();
|
| 144 |
-
if (!envStatus.valid) {
|
| 145 |
-
console.error("[BOOT:ERROR] Infrastructure missing core secrets:", envStatus.missing.join(', '));
|
| 146 |
-
}
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
initDb(dbClient)
|
| 151 |
-
.then(() => {
|
| 152 |
-
console.log("[BOOT] Database synchronized.");
|
| 153 |
-
prewarmWorkspace({
|
| 154 |
-
id: 'baseline-warmup',
|
| 155 |
-
userId: 'system',
|
| 156 |
-
projectName: 'CodeVerse-Internal'
|
| 157 |
-
}).catch(err => console.error("[BOOT] Warmup failed:", err));
|
| 158 |
-
})
|
| 159 |
-
.catch(err => console.error("[BOOT] Database init failed:", err));
|
| 160 |
-
|
| 161 |
-
// 🛠️ Self-Healing: Reconnect to orphans from previous instance
|
| 162 |
-
reconnectRunningWorkspaces().catch(err => console.error("[BOOT] Reconnection failed:", err));
|
| 163 |
-
|
| 164 |
-
// 🛡️ Persistence: Global heartbeat
|
| 165 |
-
HFStorage.startAutoSave(INFRA_CONFIG.PERSISTENCE_INTERVAL_MS * 5);
|
| 166 |
-
startAutoSleepCron();
|
| 167 |
-
}
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
<html>
|
| 180 |
<head>
|
| 181 |
-
<title>
|
| 182 |
<style>
|
| 183 |
-
body { background: #09090b; color: #
|
| 184 |
-
.
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
.item.missing { color: #f87171; border-color: #450a0a; }
|
| 190 |
</style>
|
|
|
|
| 191 |
</head>
|
| 192 |
<body>
|
| 193 |
-
<div class="
|
| 194 |
-
<
|
| 195 |
-
<
|
| 196 |
-
<
|
| 197 |
-
${envStatus.missing.map(m => `<div class="item missing"><span>❌</span> ${m}</div>`).join('')}
|
| 198 |
-
</div>
|
| 199 |
</div>
|
| 200 |
</body>
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
const workspaceHostMatch = host.match(/^workspace-([a-zA-Z0-9-]+)\./);
|
| 206 |
-
const id = workspaceHostMatch ? workspaceHostMatch[1] : (pathname?.startsWith("/workspace/") ? pathname.split("/")[2] : null);
|
| 207 |
-
|
| 208 |
-
if (id) {
|
| 209 |
-
const isReady = isNativeWorkspaceRunning(id);
|
| 210 |
-
if (isReady) {
|
| 211 |
-
const port = getNativeWorkspacePort(id) || 8080;
|
| 212 |
-
req.headers['x-codeverse-id'] = id;
|
| 213 |
-
req.headers['x-codeverse-type'] = 'workspace';
|
| 214 |
-
|
| 215 |
-
const prefix = `/workspace/${id}`;
|
| 216 |
-
if (req.url?.startsWith(prefix)) {
|
| 217 |
-
req.url = req.url.substring(prefix.length);
|
| 218 |
-
if (!req.url.startsWith("/")) req.url = "/" + req.url;
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
|
| 222 |
-
} else if (!pathname?.startsWith("/api/")) {
|
| 223 |
-
res.writeHead(503, { 'Content-Type': 'text/html', 'Retry-After': '5' });
|
| 224 |
-
res.end(`
|
| 225 |
-
<html>
|
| 226 |
-
<head>
|
| 227 |
-
<title>${APP_CONFIG.NAME} | Booting Workspace</title>
|
| 228 |
-
<style>
|
| 229 |
-
body { background: #09090b; color: #71717a; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
| 230 |
-
.container { text-align: center; border: 1px solid #27272a; padding: 2rem; border-radius: 1rem; background: #111113; }
|
| 231 |
-
.spinner { width: 40px; height: 40px; border: 3px solid #3f3f46; border-top-color: #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }
|
| 232 |
-
h1 { color: #f4f4f5; font-size: 1.25rem; }
|
| 233 |
-
@keyframes spin { to { transform: rotate(360deg); } }
|
| 234 |
-
</style>
|
| 235 |
-
<script>setTimeout(() => window.location.reload(), 3000);</script>
|
| 236 |
-
</head>
|
| 237 |
-
<body>
|
| 238 |
-
<div class="container">
|
| 239 |
-
<div class="spinner"></div>
|
| 240 |
-
<h1>Workspace is Booting</h1>
|
| 241 |
-
<p>Preparing your agentic session...</p>
|
| 242 |
-
</div>
|
| 243 |
-
</body>
|
| 244 |
-
</html>
|
| 245 |
-
`);
|
| 246 |
-
return;
|
| 247 |
-
}
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
handle(req, res);
|
| 251 |
-
});
|
| 252 |
-
|
| 253 |
-
const io = new Server(server, { path: "/api/socketio" });
|
| 254 |
-
const yjsWss = new WebSocketServer({ noServer: true });
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
|
|
|
| 263 |
|
| 264 |
-
|
|
|
|
|
|
|
| 265 |
const port = getNativeWorkspacePort(id) || 8080;
|
| 266 |
req.headers['x-codeverse-id'] = id;
|
| 267 |
req.headers['x-codeverse-type'] = 'workspace';
|
| 268 |
-
|
| 269 |
const prefix = `/workspace/${id}`;
|
| 270 |
if (req.url?.startsWith(prefix)) {
|
| 271 |
req.url = req.url.substring(prefix.length);
|
| 272 |
if (!req.url.startsWith("/")) req.url = "/" + req.url;
|
| 273 |
}
|
| 274 |
-
return proxy.
|
| 275 |
}
|
|
|
|
| 276 |
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
});
|
| 281 |
-
return;
|
| 282 |
-
}
|
| 283 |
-
});
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
const docName = fullUrl.searchParams.get('doc') || "default";
|
| 289 |
-
const { doc, awareness } = getOrCreateDoc(docName);
|
| 290 |
-
conn.binaryType = "arraybuffer";
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
}
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
}
|
| 324 |
-
};
|
| 325 |
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
|
|
|
| 330 |
});
|
| 331 |
-
}
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
| 349 |
});
|
|
|
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
});
|
|
|
|
|
|
|
| 355 |
});
|
|
|
|
| 18 |
import os from "os";
|
| 19 |
import { Duplex } from "stream";
|
| 20 |
import { startAutoSleepCron } from "./lib/jobs/auto-sleep";
|
| 21 |
+
import { getNativeWorkspacePort, isNativeWorkspaceRunning, prewarmWorkspace, reconnectRunningWorkspaces } from "./lib/docker/manager";
|
| 22 |
import { initDb } from "./lib/db/schema";
|
| 23 |
import { client as dbClient } from "./lib/db";
|
| 24 |
import { HFStorage } from "./lib/hf/storage";
|
|
|
|
| 46 |
});
|
| 47 |
};
|
| 48 |
|
|
|
|
|
|
|
|
|
|
| 49 |
const proxy = httpProxy.createProxyServer({
|
| 50 |
ws: true,
|
| 51 |
xfwd: true,
|
|
|
|
| 53 |
proxyTimeout: 30000
|
| 54 |
});
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
function renderProxyError(res: ServerResponse, error: string, id: string) {
|
| 57 |
res.writeHead(502, { 'Content-Type': 'text/html' });
|
| 58 |
res.end(`
|
|
|
|
| 103 |
}
|
| 104 |
});
|
| 105 |
|
| 106 |
+
// Port and Host logic
|
| 107 |
+
const PORT = Number(process.env.PORT) || 7860;
|
| 108 |
+
const HOST = '0.0.0.0';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
let isAppReady = false;
|
| 111 |
+
let envStatus = { valid: true, missing: [] as string[] };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
+
/**
|
| 114 |
+
*Autoritative Entrypoint: We initialize the HTTP server immediately to satisfy HF Spaces health checks.
|
| 115 |
+
*/
|
| 116 |
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
| 117 |
+
const host = req.headers.host || "localhost";
|
| 118 |
+
const fullUrl = new URL(req.url || "/", `http://${host}`);
|
| 119 |
+
const { pathname } = fullUrl;
|
| 120 |
|
| 121 |
+
// 1. Initializing State
|
| 122 |
+
if (!isAppReady && !pathname.startsWith("/_next/static") && pathname !== "/favicon.ico") {
|
| 123 |
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
| 124 |
+
return res.end(`
|
| 125 |
+
<html>
|
|
|
|
| 126 |
<head>
|
| 127 |
+
<title>CodeVerse | Initializing</title>
|
| 128 |
<style>
|
| 129 |
+
body { background: #09090b; color: #71717a; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
| 130 |
+
.container { text-align: center; border: 1px solid #27272a; padding: 2.5rem; border-radius: 1rem; background: #111113; max-width: 400px; }
|
| 131 |
+
.spinner { width: 30px; height: 30px; border: 2px solid #3f3f46; border-top-color: #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }
|
| 132 |
+
h1 { color: #f4f4f5; font-size: 1.1rem; margin: 0; }
|
| 133 |
+
p { font-size: 0.85rem; margin-top: 0.5rem; }
|
| 134 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
| 135 |
</style>
|
| 136 |
+
<script>setTimeout(() => window.location.reload(), 5000);</script>
|
| 137 |
</head>
|
| 138 |
<body>
|
| 139 |
+
<div class="container">
|
| 140 |
+
<div class="spinner"></div>
|
| 141 |
+
<h1>CodeVerse is waking up</h1>
|
| 142 |
+
<p>Restoring your environment and securing persistent volumes...</p>
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
</body>
|
| 145 |
+
</html>
|
| 146 |
+
`);
|
| 147 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
+
// 2. Maintenance / Misconfiguration State
|
| 150 |
+
if (isAppReady && !envStatus.valid && pathname !== "/api/health" && !pathname.startsWith("/_next/")) {
|
| 151 |
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
| 152 |
+
return res.end(`
|
| 153 |
+
<html>
|
| 154 |
+
<body style="background:#09090b;color:#f87171;padding:2rem;font-family:sans-serif;">
|
| 155 |
+
<h1>Infrastructure Error</h1>
|
| 156 |
+
<p>${UI_STRINGS.MAINTENANCE_MESSAGE}</p>
|
| 157 |
+
<ul>${envStatus.missing.map(m => `<li>${m}</li>`).join('')}</ul>
|
| 158 |
+
</body>
|
| 159 |
+
</html>
|
| 160 |
+
`);
|
| 161 |
+
}
|
| 162 |
|
| 163 |
+
// 3. Workspace Proxying
|
| 164 |
+
const workspaceHostMatch = host.match(/^workspace-([a-zA-Z0-9-]+)\./);
|
| 165 |
+
const id = workspaceHostMatch ? workspaceHostMatch[1] : (pathname?.startsWith("/workspace/") ? pathname.split("/")[2] : null);
|
| 166 |
|
| 167 |
+
if (id) {
|
| 168 |
+
const isReady = isNativeWorkspaceRunning(id);
|
| 169 |
+
if (isReady) {
|
| 170 |
const port = getNativeWorkspacePort(id) || 8080;
|
| 171 |
req.headers['x-codeverse-id'] = id;
|
| 172 |
req.headers['x-codeverse-type'] = 'workspace';
|
|
|
|
| 173 |
const prefix = `/workspace/${id}`;
|
| 174 |
if (req.url?.startsWith(prefix)) {
|
| 175 |
req.url = req.url.substring(prefix.length);
|
| 176 |
if (!req.url.startsWith("/")) req.url = "/" + req.url;
|
| 177 |
}
|
| 178 |
+
return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
|
| 179 |
}
|
| 180 |
+
}
|
| 181 |
|
| 182 |
+
// 4. Default Next.js Handle
|
| 183 |
+
handle(req, res);
|
| 184 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
// Setup Sockets
|
| 187 |
+
const io = new Server(server, { path: "/api/socketio" });
|
| 188 |
+
const yjsWss = new WebSocketServer({ noServer: true });
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
+
// Start Listening Immediately
|
| 191 |
+
server.listen(PORT, HOST, () => {
|
| 192 |
+
console.log('----------------------------------------------------');
|
| 193 |
+
console.log(`[READY] ${APP_CONFIG.NAME} ${APP_CONFIG.VERSION}`);
|
| 194 |
+
console.log(`[READY] Interface: ${HOST}:${PORT}`);
|
| 195 |
+
console.log('----------------------------------------------------');
|
| 196 |
+
});
|
| 197 |
|
| 198 |
+
// Background Async Initialization
|
| 199 |
+
(async () => {
|
| 200 |
+
try {
|
| 201 |
+
console.log("[BOOT] Starting Next.js preparation...");
|
| 202 |
+
await app.prepare();
|
| 203 |
+
console.log("[BOOT] Next.js payload ready.");
|
| 204 |
|
| 205 |
+
envStatus = validateEnvironment();
|
| 206 |
+
if (envStatus.valid) {
|
| 207 |
+
console.log("[BOOT] Environment validated. Synchronizing database...");
|
| 208 |
+
await initDb(dbClient);
|
| 209 |
+
console.log("[BOOT] Database synchronized.");
|
| 210 |
+
|
| 211 |
+
// Reconnect and Warmup
|
| 212 |
+
reconnectRunningWorkspaces().catch(() => {});
|
| 213 |
+
prewarmWorkspace({ id: 'baseline-warmup', userId: 'system', projectName: 'CodeVerse-Internal' }).catch(() => {});
|
| 214 |
+
|
| 215 |
+
// Crons and Persistence
|
| 216 |
+
HFStorage.startAutoSave(INFRA_CONFIG.PERSISTENCE_INTERVAL_MS * 5);
|
| 217 |
+
startAutoSleepCron();
|
| 218 |
+
}
|
| 219 |
|
| 220 |
+
isAppReady = true;
|
| 221 |
+
console.log("[BOOT] Global state stabilized. Application is fully operational.");
|
| 222 |
+
} catch (err) {
|
| 223 |
+
console.error("[BOOT:ERROR] Fatal initialization failure:", err);
|
| 224 |
+
}
|
| 225 |
+
})();
|
|
|
|
|
|
|
| 226 |
|
| 227 |
+
// Terminal and Collaboration Handlers
|
| 228 |
+
server.on("upgrade", (req, socket, head) => {
|
| 229 |
+
const { pathname } = new URL(req.url || "/", `http://${req.headers.host}`);
|
| 230 |
+
if (pathname === "/api/collab") {
|
| 231 |
+
yjsWss.handleUpgrade(req, socket, head, (ws) => {
|
| 232 |
+
yjsWss.emit("connection", ws, req);
|
| 233 |
});
|
| 234 |
+
}
|
| 235 |
+
});
|
| 236 |
|
| 237 |
+
yjsWss.on("connection", (conn: WebSocket, request: IncomingMessage) => {
|
| 238 |
+
const { doc, awareness } = getOrCreateDoc(new URL(request.url || "/", "http://l").searchParams.get('doc') || "default");
|
| 239 |
+
conn.binaryType = "arraybuffer";
|
| 240 |
+
const encoder = encoding.createEncoder();
|
| 241 |
+
encoding.writeVarUint(encoder, 0);
|
| 242 |
+
syncProtocol.writeSyncStep1(encoder, doc);
|
| 243 |
+
conn.send(encoding.toUint8Array(encoder));
|
| 244 |
+
|
| 245 |
+
conn.on("message", (message: ArrayBuffer) => {
|
| 246 |
+
const encoder = encoding.createEncoder();
|
| 247 |
+
const decoder = decoding.createDecoder(new Uint8Array(message));
|
| 248 |
+
const messageType = decoding.readVarUint(decoder);
|
| 249 |
+
if (messageType === 0) {
|
| 250 |
+
encoding.writeVarUint(encoder, 0);
|
| 251 |
+
syncProtocol.readSyncMessage(decoder, encoder, doc, null);
|
| 252 |
+
if (encoding.length(encoder) > 1) conn.send(encoding.toUint8Array(encoder));
|
| 253 |
+
}
|
| 254 |
});
|
| 255 |
+
});
|
| 256 |
|
| 257 |
+
io.on("connection", (socket) => {
|
| 258 |
+
let shell: pty.IPty | null = null;
|
| 259 |
+
socket.on("terminal:start", ({ cols, rows }) => {
|
| 260 |
+
shell = pty.spawn(process.env.SHELL || (os.platform() === "win32" ? "powershell.exe" : "bash"), [], {
|
| 261 |
+
cols: cols || 80,
|
| 262 |
+
rows: rows || 24,
|
| 263 |
+
cwd: INFRA_CONFIG.WORKSPACE_ROOT,
|
| 264 |
+
env: process.env as Record<string, string>,
|
| 265 |
+
});
|
| 266 |
+
shell.onData((data: string) => socket.emit("terminal:data", data));
|
| 267 |
});
|
| 268 |
+
socket.on("terminal:write", (data) => { if (shell) shell.write(data); });
|
| 269 |
+
socket.on("disconnect", () => { if (shell) shell.kill(); });
|
| 270 |
});
|