chore: archive AgentDock v1 implementation
This commit is contained in:
+365
@@ -0,0 +1,365 @@
|
||||
import crypto from "node:crypto";
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import express from "express";
|
||||
import pty from "node-pty";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { buildCommand, SECRET_ENV_ALLOWLIST, TOOLS, toolById } from "./registry.js";
|
||||
import { importCcLiveConfig, integrationSnapshot, switchCcProvider } from "./integrations.js";
|
||||
import { relayMulticaCallback } from "./multica-callback.js";
|
||||
import { listProjects, resolveWorkspacePath } from "./path-policy.js";
|
||||
import { SessionStore } from "./session-store.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const PORT = Number(process.env.RUNNER_PORT || 4174);
|
||||
const TOKEN = process.env.RUNNER_TOKEN || "";
|
||||
const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || "/workspace";
|
||||
const SESSION_DIRECTORY = process.env.SESSION_DIRECTORY || "/home/ai/.ai-console/sessions";
|
||||
|
||||
if (TOKEN.length < 32) throw new Error("RUNNER_TOKEN must contain at least 32 characters");
|
||||
|
||||
const store = new SessionStore(SESSION_DIRECTORY);
|
||||
await store.init();
|
||||
|
||||
function tokenMatches(candidate = "") {
|
||||
const left = Buffer.from(candidate);
|
||||
const right = Buffer.from(TOKEN);
|
||||
return left.length === right.length && crypto.timingSafeEqual(left, right);
|
||||
}
|
||||
|
||||
function requireRunnerToken(req, res, next) {
|
||||
const candidate = req.headers.authorization?.replace(/^Bearer\s+/i, "") || "";
|
||||
if (!tokenMatches(candidate)) return res.status(401).json({ error: "Unauthorized" });
|
||||
next();
|
||||
}
|
||||
|
||||
function jsonError(res, error, status = 400) {
|
||||
const message = error instanceof Error ? error.message : "Request failed";
|
||||
return res.status(status).json({ error: message });
|
||||
}
|
||||
|
||||
async function commandVersion(command) {
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(command, ["--version"], {
|
||||
timeout: 8000,
|
||||
maxBuffer: 256 * 1024,
|
||||
env: process.env,
|
||||
});
|
||||
return { installed: true, version: (stdout || stderr).trim().split(/\r?\n/)[0] || "installed" };
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return { installed: false, version: null };
|
||||
return { installed: true, version: "unknown", error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function processResources(pid) {
|
||||
if (!pid) return { cpu: null, memoryBytes: null };
|
||||
try {
|
||||
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "%cpu=,rss="], {
|
||||
timeout: 3000,
|
||||
maxBuffer: 64 * 1024,
|
||||
});
|
||||
const [cpu, rssKb] = stdout.trim().split(/\s+/);
|
||||
return {
|
||||
cpu: Number.isFinite(Number(cpu)) ? Number(cpu) : null,
|
||||
memoryBytes: Number.isFinite(Number(rssKb)) ? Number(rssKb) * 1024 : null,
|
||||
};
|
||||
} catch {
|
||||
return { cpu: null, memoryBytes: null };
|
||||
}
|
||||
}
|
||||
|
||||
function activeSessionForTool(toolId) {
|
||||
return store.list().find((session) => session.tool === toolId && session.status === "running");
|
||||
}
|
||||
|
||||
function broadcast(session, message) {
|
||||
const payload = JSON.stringify(message);
|
||||
for (const client of session.clients) {
|
||||
if (client.readyState === WebSocket.OPEN) client.send(payload);
|
||||
}
|
||||
}
|
||||
|
||||
function appendOutput(session, data) {
|
||||
if (!data) return;
|
||||
store.append(session, data);
|
||||
broadcast(session, { type: "output", data });
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.json({ limit: "128kb" }));
|
||||
app.get("/health", (_req, res) => res.json({ ok: true, service: "agentdock-runner" }));
|
||||
app.use(requireRunnerToken);
|
||||
|
||||
app.get("/tools", async (_req, res) => {
|
||||
const records = await Promise.all(Object.values(TOOLS).filter((tool) => tool.visible !== false).map(async (tool) => {
|
||||
const version = await commandVersion(tool.command);
|
||||
const active = activeSessionForTool(tool.id);
|
||||
const resources = active ? await processResources(active.pid) : { cpu: null, memoryBytes: null };
|
||||
return {
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
vendor: tool.vendor,
|
||||
mono: tool.mono,
|
||||
command: tool.command,
|
||||
profiles: tool.profiles || [],
|
||||
...version,
|
||||
status: active ? "running" : version.installed ? "stopped" : "error",
|
||||
sessionId: active?.id || null,
|
||||
project: active?.project || null,
|
||||
resources,
|
||||
};
|
||||
}));
|
||||
res.json({ tools: records });
|
||||
});
|
||||
|
||||
app.get("/projects", async (_req, res) => {
|
||||
try {
|
||||
res.json({ projects: await listProjects(WORKSPACE_ROOT) });
|
||||
} catch (error) {
|
||||
jsonError(res, error, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/sessions", (_req, res) => res.json({ sessions: store.list() }));
|
||||
|
||||
app.get("/integrations", async (req, res) => {
|
||||
try {
|
||||
res.json({ integrations: await integrationSnapshot({ force: req.query.force === "1" }) });
|
||||
} catch (error) {
|
||||
jsonError(res, error, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/integrations/cc-switch/:app/switch", async (req, res) => {
|
||||
try {
|
||||
await switchCcProvider(req.params.app, req.body?.providerId);
|
||||
res.json({ integrations: await integrationSnapshot({ force: true }) });
|
||||
} catch (error) {
|
||||
jsonError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/integrations/cc-switch/:app/import-live", async (req, res) => {
|
||||
try {
|
||||
await importCcLiveConfig(req.params.app);
|
||||
res.json({ integrations: await integrationSnapshot({ force: true }) });
|
||||
} catch (error) {
|
||||
jsonError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/integrations/multica/callback", async (req, res) => {
|
||||
try {
|
||||
res.json(await relayMulticaCallback(req.body?.callbackUrl));
|
||||
} catch (error) {
|
||||
jsonError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/sessions/:id/output", async (req, res) => {
|
||||
const session = store.get(req.params.id);
|
||||
if (!session) return res.status(404).json({ error: "Session not found" });
|
||||
res.type("text/plain").send(await store.readOutput(session));
|
||||
});
|
||||
|
||||
app.get("/sessions/:id/diff", async (req, res) => {
|
||||
const session = store.get(req.params.id);
|
||||
if (!session) return res.status(404).json({ error: "Session not found" });
|
||||
try {
|
||||
const cwd = await resolveWorkspacePath(WORKSPACE_ROOT, session.project);
|
||||
const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff", "--no-color"], {
|
||||
cwd,
|
||||
timeout: 10000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
res.type("text/plain").send(stdout || "");
|
||||
} catch (error) {
|
||||
if (error.code === 1 && typeof error.stdout === "string") return res.type("text/plain").send(error.stdout);
|
||||
jsonError(res, error, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/sessions", async (req, res) => {
|
||||
const tool = toolById(req.body?.tool);
|
||||
if (!tool) return res.status(400).json({ error: "Unsupported CLI tool" });
|
||||
try {
|
||||
const cwd = await resolveWorkspacePath(WORKSPACE_ROOT, req.body.project || ".");
|
||||
const request = {
|
||||
purpose: req.body.purpose === "authorization" ? "authorization" : "task",
|
||||
profile: req.body.profile,
|
||||
model: req.body.model,
|
||||
initialInput: typeof req.body.initialInput === "string" ? req.body.initialInput.slice(0, 32000) : "",
|
||||
serverUrl: typeof req.body.serverUrl === "string" ? req.body.serverUrl.trim().slice(0, 2048) : "",
|
||||
appUrl: typeof req.body.appUrl === "string" ? req.body.appUrl.trim().slice(0, 2048) : "",
|
||||
};
|
||||
const built = buildCommand(tool, request);
|
||||
const environment = {};
|
||||
for (const [name, value] of Object.entries(req.body.environment || {})) {
|
||||
if (SECRET_ENV_ALLOWLIST.has(name) && tool.secretNames.includes(name) && typeof value === "string") {
|
||||
environment[name] = value;
|
||||
}
|
||||
}
|
||||
const id = crypto.randomUUID();
|
||||
const processEnvironment = {
|
||||
...process.env,
|
||||
...environment,
|
||||
TERM: built.interactive ? "xterm-256color" : "dumb",
|
||||
COLORTERM: built.interactive ? "truecolor" : "",
|
||||
NO_COLOR: built.interactive ? (process.env.NO_COLOR || "") : "1",
|
||||
CI: built.interactive ? (process.env.CI || "") : "1",
|
||||
};
|
||||
const baseRecord = {
|
||||
id,
|
||||
tool: tool.id,
|
||||
toolName: tool.name,
|
||||
project: path.relative(await fs.realpath(WORKSPACE_ROOT), cwd) || ".",
|
||||
purpose: request.purpose,
|
||||
initialInput: request.initialInput,
|
||||
profile: request.profile || null,
|
||||
model: typeof req.body.model === "string" ? req.body.model.slice(0, 128) : null,
|
||||
status: "running",
|
||||
createdAt: new Date().toISOString(),
|
||||
endedAt: null,
|
||||
exitCode: null,
|
||||
signal: null,
|
||||
interactive: built.interactive,
|
||||
environment,
|
||||
};
|
||||
let session;
|
||||
|
||||
if (built.interactive) {
|
||||
const processHandle = pty.spawn(built.command, built.args, {
|
||||
name: "xterm-256color",
|
||||
cols: Math.max(40, Math.min(300, Number(req.body.cols) || 120)),
|
||||
rows: Math.max(12, Math.min(120, Number(req.body.rows) || 32)),
|
||||
cwd,
|
||||
env: processEnvironment,
|
||||
});
|
||||
session = store.add({ ...baseRecord, pid: processHandle.pid, pty: processHandle });
|
||||
processHandle.onData((data) => appendOutput(session, data));
|
||||
processHandle.onExit(({ exitCode, signal }) => {
|
||||
session.processExited = true;
|
||||
const status = session.status === "stopped" ? "stopped" : exitCode === 0 ? "exited" : "failed";
|
||||
store.finish(session, status, exitCode, signal);
|
||||
broadcast(session, { type: "exit", exitCode, signal, status });
|
||||
});
|
||||
} else {
|
||||
const childProcess = spawn(built.command, built.args, {
|
||||
cwd,
|
||||
env: processEnvironment,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
session = store.add({ ...baseRecord, pid: childProcess.pid, childProcess });
|
||||
childProcess.stdout.setEncoding("utf8");
|
||||
childProcess.stderr.setEncoding("utf8");
|
||||
childProcess.stdout.on("data", (data) => appendOutput(session, data));
|
||||
childProcess.stderr.on("data", (data) => {
|
||||
session.errorBuffer = `${session.errorBuffer}${data}`.slice(-256 * 1024);
|
||||
broadcast(session, { type: "activity", label: "CLI 正在处理任务" });
|
||||
});
|
||||
childProcess.on("error", (error) => {
|
||||
session.errorBuffer = error.message;
|
||||
});
|
||||
childProcess.on("close", (exitCode, signal) => {
|
||||
session.processExited = true;
|
||||
const status = session.status === "stopped" ? "stopped" : exitCode === 0 ? "exited" : "failed";
|
||||
if (!session.buffer.trim()) {
|
||||
const fallback = status === "failed"
|
||||
? session.errorBuffer.trim() || `CLI 退出,代码 ${exitCode ?? "unknown"}`
|
||||
: "任务已完成,但 CLI 没有返回文本。";
|
||||
appendOutput(session, fallback);
|
||||
}
|
||||
store.finish(session, status, exitCode, signal);
|
||||
broadcast(session, { type: "exit", exitCode, signal, status });
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({ session: store.publicRecord(session) });
|
||||
} catch (error) {
|
||||
jsonError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/sessions/:id/input", (req, res) => {
|
||||
const session = store.get(req.params.id);
|
||||
if (!session) return res.status(404).json({ error: "Session not found" });
|
||||
if (session.status !== "running" || !session.pty || !session.interactive) return res.status(409).json({ error: "Only interactive sessions accept terminal input" });
|
||||
const data = typeof req.body?.data === "string" ? req.body.data.slice(0, 65536) : "";
|
||||
session.pty.write(data);
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
app.post("/sessions/:id/resize", (req, res) => {
|
||||
const session = store.get(req.params.id);
|
||||
if (!session) return res.status(404).json({ error: "Session not found" });
|
||||
if (session.status !== "running" || !session.pty || !session.interactive) return res.status(409).json({ error: "Only interactive sessions can be resized" });
|
||||
const cols = Math.max(40, Math.min(300, Number(req.body?.cols) || 120));
|
||||
const rows = Math.max(12, Math.min(120, Number(req.body?.rows) || 32));
|
||||
session.pty.resize(cols, rows);
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
app.post("/sessions/:id/stop", (req, res) => {
|
||||
const session = store.get(req.params.id);
|
||||
if (!session) return res.status(404).json({ error: "Session not found" });
|
||||
if (session.status === "running" && (session.pty || session.childProcess)) {
|
||||
store.finish(session, "stopped");
|
||||
const handle = session.pty || session.childProcess;
|
||||
handle.kill("SIGTERM");
|
||||
setTimeout(() => {
|
||||
if (!session.processExited) handle.kill("SIGKILL");
|
||||
}, 5000).unref();
|
||||
}
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
const server = http.createServer(app);
|
||||
const sockets = new WebSocketServer({ noServer: true });
|
||||
const socketHeartbeat = setInterval(() => {
|
||||
for (const client of sockets.clients) {
|
||||
if (client.readyState === WebSocket.OPEN) client.ping();
|
||||
}
|
||||
}, 25_000);
|
||||
socketHeartbeat.unref();
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
const url = new URL(request.url, "http://runner.internal");
|
||||
const match = url.pathname.match(/^\/ws\/sessions\/([a-f0-9-]+)$/);
|
||||
if (!match || !tokenMatches(url.searchParams.get("token") || "")) {
|
||||
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
||||
return socket.destroy();
|
||||
}
|
||||
const session = store.get(match[1]);
|
||||
if (!session) {
|
||||
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
||||
return socket.destroy();
|
||||
}
|
||||
sockets.handleUpgrade(request, socket, head, async (client) => {
|
||||
session.clients.add(client);
|
||||
client.send(JSON.stringify({ type: "snapshot", data: await store.readOutput(session), session: store.publicRecord(session) }));
|
||||
client.on("message", (raw) => {
|
||||
if (session.status !== "running" || !session.pty || !session.interactive) return;
|
||||
try {
|
||||
const message = JSON.parse(raw.toString());
|
||||
if (message.type === "input" && typeof message.data === "string") session.pty.write(message.data.slice(0, 65536));
|
||||
if (message.type === "resize") {
|
||||
const cols = Math.max(40, Math.min(300, Number(message.cols) || 120));
|
||||
const rows = Math.max(12, Math.min(120, Number(message.rows) || 32));
|
||||
session.pty.resize(cols, rows);
|
||||
}
|
||||
} catch {
|
||||
client.send(JSON.stringify({ type: "error", error: "Invalid WebSocket message" }));
|
||||
}
|
||||
});
|
||||
client.on("close", () => session.clients.delete(client));
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`agentdock runner listening on ${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const HOME = process.env.HOME || "/home/ai";
|
||||
const CC_SWITCH_DATABASE = path.join(HOME, ".cc-switch", "cc-switch.db");
|
||||
const ANSI = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
||||
const SAFE_ID = /^[a-zA-Z0-9._-]{1,128}$/;
|
||||
|
||||
export const CC_SWITCH_APPS = Object.freeze([
|
||||
{ id: "claude", toolId: "claude", name: "Claude Code" },
|
||||
{ id: "codex", toolId: "codex", name: "Codex" },
|
||||
{ id: "open-code", toolId: "opencode", name: "OpenCode" },
|
||||
]);
|
||||
|
||||
export const MULTICA_TOOL_IDS = Object.freeze(["claude", "codex", "codebuddy", "kimi", "opencode"]);
|
||||
|
||||
function clean(value) {
|
||||
return String(value || "").replace(ANSI, "").trim();
|
||||
}
|
||||
|
||||
function publicEndpoint(value) {
|
||||
const candidate = clean(value);
|
||||
if (!candidate || candidate === "N/A") return null;
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
return url.origin;
|
||||
} catch {
|
||||
return "configured";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCcProviderList(output) {
|
||||
const providers = [];
|
||||
let currentProviderId = null;
|
||||
for (const rawLine of clean(output).split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
const current = line.match(/^→\s*Current:\s*(\S+)/);
|
||||
if (current) currentProviderId = current[1];
|
||||
if (!line.startsWith("│") || !line.includes("┆")) continue;
|
||||
const columns = line.slice(1, line.endsWith("│") ? -1 : undefined).split("┆").map(clean);
|
||||
if (columns.length < 4 || columns[1] === "ID") continue;
|
||||
const [marker, id, name, endpoint] = columns;
|
||||
if (!SAFE_ID.test(id)) continue;
|
||||
providers.push({
|
||||
id,
|
||||
name: name || id,
|
||||
endpoint: publicEndpoint(endpoint),
|
||||
active: marker.includes("✓"),
|
||||
});
|
||||
}
|
||||
if (!currentProviderId) currentProviderId = providers.find((provider) => provider.active)?.id || null;
|
||||
return { currentProviderId, providers };
|
||||
}
|
||||
|
||||
export function parseMulticaConfig(output) {
|
||||
const values = {};
|
||||
for (const line of clean(output).split(/\r?\n/)) {
|
||||
const match = line.match(/^([a-z_]+):\s*(.*)$/);
|
||||
if (match) values[match[1]] = match[2] === "(not set)" ? null : match[2];
|
||||
}
|
||||
return {
|
||||
serverUrl: values.server_url || null,
|
||||
appUrl: values.app_url || null,
|
||||
workspaceId: values.workspace_id || null,
|
||||
configured: Boolean(values.server_url && values.workspace_id),
|
||||
};
|
||||
}
|
||||
|
||||
async function run(command, args, options = {}) {
|
||||
const { stdout, stderr } = await execFileAsync(command, args, {
|
||||
timeout: options.timeout || 10000,
|
||||
maxBuffer: options.maxBuffer || 1024 * 1024,
|
||||
env: { ...process.env, NO_COLOR: "1", TERM: "dumb" },
|
||||
});
|
||||
return clean(stdout || stderr);
|
||||
}
|
||||
|
||||
async function versionOf(command, args = ["--version"]) {
|
||||
try {
|
||||
const output = await run(command, args, { timeout: 8000 });
|
||||
return { installed: true, version: output.split(/\r?\n/)[0] || "installed" };
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return { installed: false, version: null };
|
||||
return { installed: true, version: "unknown", error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function ccSwitchInitialized() {
|
||||
try {
|
||||
await fs.access(CC_SWITCH_DATABASE);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ccSwitchSnapshot() {
|
||||
const availability = await versionOf("cc-switch");
|
||||
const initialized = availability.installed && await ccSwitchInitialized();
|
||||
const apps = await Promise.all(CC_SWITCH_APPS.map(async (app) => {
|
||||
if (!initialized) return { ...app, currentProviderId: null, providers: [] };
|
||||
try {
|
||||
return { ...app, ...parseCcProviderList(await run("cc-switch", ["--app", app.id, "provider", "list"])) };
|
||||
} catch (error) {
|
||||
return { ...app, currentProviderId: null, providers: [], error: error.message };
|
||||
}
|
||||
}));
|
||||
return {
|
||||
...availability,
|
||||
initialized,
|
||||
apps,
|
||||
unsupportedToolIds: ["codebuddy", "kimi", "qwen", "dsh"],
|
||||
};
|
||||
}
|
||||
|
||||
async function multicaSnapshot() {
|
||||
const availability = await versionOf("multica", ["version"]);
|
||||
if (!availability.installed) {
|
||||
return { ...availability, daemon: { status: "unavailable" }, config: { configured: false }, supportedToolIds: MULTICA_TOOL_IDS };
|
||||
}
|
||||
let daemon = { status: "unknown" };
|
||||
let config = { configured: false, serverUrl: null, appUrl: null, workspaceId: null };
|
||||
try {
|
||||
daemon = JSON.parse(await run("multica", ["daemon", "status", "--output", "json"]));
|
||||
} catch (error) {
|
||||
daemon = { status: "unknown", error: error.message };
|
||||
}
|
||||
try {
|
||||
config = parseMulticaConfig(await run("multica", ["config", "show"]));
|
||||
} catch (error) {
|
||||
config = { ...config, error: error.message };
|
||||
}
|
||||
return { ...availability, daemon, config, supportedToolIds: MULTICA_TOOL_IDS };
|
||||
}
|
||||
|
||||
let cachedSnapshot = null;
|
||||
let cachedAt = 0;
|
||||
|
||||
export async function integrationSnapshot({ force = false } = {}) {
|
||||
if (!force && cachedSnapshot && Date.now() - cachedAt < 5000) return cachedSnapshot;
|
||||
const [ccSwitch, multica] = await Promise.all([ccSwitchSnapshot(), multicaSnapshot()]);
|
||||
cachedSnapshot = { ccSwitch, multica };
|
||||
cachedAt = Date.now();
|
||||
return cachedSnapshot;
|
||||
}
|
||||
|
||||
function validateCcSwitchApp(appId) {
|
||||
if (!CC_SWITCH_APPS.some((app) => app.id === appId)) throw new Error("Unsupported CC Switch application");
|
||||
}
|
||||
|
||||
let mutationQueue = Promise.resolve();
|
||||
function serializeMutation(operation) {
|
||||
const result = mutationQueue.then(operation, operation);
|
||||
mutationQueue = result.catch(() => {});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function switchCcProvider(appId, providerId) {
|
||||
validateCcSwitchApp(appId);
|
||||
if (!SAFE_ID.test(providerId || "")) throw new Error("Invalid provider ID");
|
||||
return serializeMutation(async () => {
|
||||
const output = await run("cc-switch", ["--app", appId, "provider", "switch", providerId], { timeout: 30000 });
|
||||
cachedSnapshot = null;
|
||||
return { ok: true, output: output.split(/\r?\n/).slice(0, 4).join("\n") };
|
||||
});
|
||||
}
|
||||
|
||||
export async function importCcLiveConfig(appId) {
|
||||
validateCcSwitchApp(appId);
|
||||
return serializeMutation(async () => {
|
||||
const output = await run("cc-switch", ["--app", appId, "provider", "import-live"], { timeout: 30000 });
|
||||
cachedSnapshot = null;
|
||||
return { ok: true, output: output.split(/\r?\n/).slice(0, 6).join("\n") };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { parseCcProviderList, parseMulticaConfig } from "./integrations.js";
|
||||
|
||||
test("parses CC Switch provider table without exposing URL paths", () => {
|
||||
const result = parseCcProviderList(`
|
||||
┌───┬────────────────┬─────────────────┬────────────────────────────┐
|
||||
│ ┆ ID ┆ Name ┆ API URL │
|
||||
╞═══╪════════════════╪═════════════════╪════════════════════════════╡
|
||||
│ ✓ ┆ codex-official ┆ OpenAI Official ┆ N/A │
|
||||
│ ┆ relay-one ┆ Relay One ┆ https://api.example/a/key │
|
||||
└───┴────────────────┴─────────────────┴────────────────────────────┘
|
||||
→ Current: codex-official
|
||||
`);
|
||||
assert.equal(result.currentProviderId, "codex-official");
|
||||
assert.deepEqual(result.providers, [
|
||||
{ id: "codex-official", name: "OpenAI Official", endpoint: null, active: true },
|
||||
{ id: "relay-one", name: "Relay One", endpoint: "https://api.example", active: false },
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses Multica config and marks an authenticated workspace configured", () => {
|
||||
const result = parseMulticaConfig(`
|
||||
Config file: /home/ai/.multica/config.json
|
||||
server_url: https://multica-api.example
|
||||
app_url: https://multica.example
|
||||
workspace_id: ws-123
|
||||
`);
|
||||
assert.deepEqual(result, {
|
||||
serverUrl: "https://multica-api.example",
|
||||
appUrl: "https://multica.example",
|
||||
workspaceId: "ws-123",
|
||||
configured: true,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
||||
|
||||
export function parseMulticaCallbackUrl(value) {
|
||||
if (typeof value !== "string" || !value || value.length > 12_000) {
|
||||
throw new Error("请粘贴完整的 Multica 回调链接");
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new Error("回调链接格式不正确");
|
||||
}
|
||||
|
||||
const port = Number(url.port);
|
||||
if (url.protocol !== "http:" || !LOOPBACK_HOSTS.has(url.hostname) || url.pathname !== "/callback") {
|
||||
throw new Error("只接受 Multica 生成的 localhost 回调链接");
|
||||
}
|
||||
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
||||
throw new Error("回调链接缺少有效端口");
|
||||
}
|
||||
if (!url.searchParams.get("token") || !url.searchParams.get("state")) {
|
||||
throw new Error("回调链接缺少 token 或 state");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function relayMulticaCallback(value, fetchImpl = fetch) {
|
||||
const url = parseMulticaCallbackUrl(value);
|
||||
const response = await fetchImpl(url, {
|
||||
method: "GET",
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Multica 回调未被接受 (${response.status})`);
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { parseMulticaCallbackUrl, relayMulticaCallback } from "./multica-callback.js";
|
||||
|
||||
test("accepts a complete Multica loopback callback", () => {
|
||||
const url = parseMulticaCallbackUrl("http://localhost:42957/callback?token=secret&state=nonce");
|
||||
assert.equal(url.port, "42957");
|
||||
});
|
||||
|
||||
test("rejects non-loopback callback targets", () => {
|
||||
assert.throws(
|
||||
() => parseMulticaCallbackUrl("https://example.com/callback?token=secret&state=nonce"),
|
||||
/localhost/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects callbacks without OAuth parameters", () => {
|
||||
assert.throws(() => parseMulticaCallbackUrl("http://localhost:42957/callback"), /token/);
|
||||
});
|
||||
|
||||
test("relays the callback without exposing its response", async () => {
|
||||
let receivedUrl;
|
||||
const result = await relayMulticaCallback(
|
||||
"http://127.0.0.1:42957/callback?token=secret&state=nonce",
|
||||
async (url) => {
|
||||
receivedUrl = url;
|
||||
return { ok: true, status: 200 };
|
||||
},
|
||||
);
|
||||
assert.equal(receivedUrl.hostname, "127.0.0.1");
|
||||
assert.deepEqual(result, { ok: true });
|
||||
});
|
||||
Generated
+924
@@ -0,0 +1,924 @@
|
||||
{
|
||||
"name": "agentdock-runner",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agentdock-runner",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
|
||||
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
|
||||
"integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agentdock-runner",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export async function resolveWorkspacePath(root, project = ".") {
|
||||
if (typeof project !== "string" || project.includes("\0")) {
|
||||
throw new Error("Invalid project path");
|
||||
}
|
||||
const rootReal = await fs.realpath(root);
|
||||
const candidate = path.resolve(rootReal, project || ".");
|
||||
const candidateReal = await fs.realpath(candidate);
|
||||
if (candidateReal !== rootReal && !candidateReal.startsWith(`${rootReal}${path.sep}`)) {
|
||||
throw new Error("Project must be below /workspace");
|
||||
}
|
||||
const stat = await fs.stat(candidateReal);
|
||||
if (!stat.isDirectory()) throw new Error("Project is not a directory");
|
||||
return candidateReal;
|
||||
}
|
||||
|
||||
export async function listProjects(root) {
|
||||
const rootReal = await fs.realpath(root);
|
||||
const entries = await fs.readdir(rootReal, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
||||
.map((entry) => entry.name)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { listProjects, resolveWorkspacePath } from "./path-policy.js";
|
||||
|
||||
test("workspace policy accepts child directories and rejects traversal", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "agentdock-workspace-"));
|
||||
await fs.mkdir(path.join(root, "demo"));
|
||||
assert.equal(await resolveWorkspacePath(root, "demo"), path.join(root, "demo"));
|
||||
await assert.rejects(() => resolveWorkspacePath(root, ".."));
|
||||
});
|
||||
|
||||
test("project listing hides dot directories", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "agentdock-projects-"));
|
||||
await fs.mkdir(path.join(root, "zeta"));
|
||||
await fs.mkdir(path.join(root, "alpha"));
|
||||
await fs.mkdir(path.join(root, ".cache"));
|
||||
assert.deepEqual(await listProjects(root), ["alpha", "zeta"]);
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
export const TOOLS = Object.freeze({
|
||||
codex: {
|
||||
id: "codex",
|
||||
command: "codex",
|
||||
name: "Codex",
|
||||
vendor: "OpenAI",
|
||||
mono: "CX",
|
||||
modelFlag: "--model",
|
||||
secretNames: ["OPENAI_API_KEY"],
|
||||
},
|
||||
claude: {
|
||||
id: "claude",
|
||||
command: "claude",
|
||||
name: "Claude Code",
|
||||
vendor: "Anthropic",
|
||||
mono: "CC",
|
||||
modelFlag: "--model",
|
||||
secretNames: ["ANTHROPIC_API_KEY"],
|
||||
},
|
||||
codebuddy: {
|
||||
id: "codebuddy",
|
||||
command: "codebuddy",
|
||||
name: "CodeBuddy",
|
||||
vendor: "Tencent",
|
||||
mono: "CB",
|
||||
modelFlag: "--model",
|
||||
secretNames: ["CODEBUDDY_API_KEY"],
|
||||
},
|
||||
kimi: {
|
||||
id: "kimi",
|
||||
command: "kimi",
|
||||
name: "Kimi CLI",
|
||||
vendor: "Moonshot AI",
|
||||
mono: "KM",
|
||||
modelFlag: "--model",
|
||||
secretNames: ["MOONSHOT_API_KEY"],
|
||||
},
|
||||
opencode: {
|
||||
id: "opencode",
|
||||
command: "opencode",
|
||||
name: "OpenCode",
|
||||
vendor: "SST / Open source",
|
||||
mono: "OC",
|
||||
modelFlag: "--model",
|
||||
secretNames: [
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
],
|
||||
},
|
||||
qwen: {
|
||||
id: "qwen",
|
||||
command: "qwen",
|
||||
name: "Qwen Code",
|
||||
vendor: "Alibaba",
|
||||
mono: "QW",
|
||||
modelFlag: "--model",
|
||||
secretNames: ["DASHSCOPE_API_KEY"],
|
||||
},
|
||||
dsh: {
|
||||
id: "dsh",
|
||||
command: "dsh",
|
||||
name: "DeepSeek Harness",
|
||||
vendor: "DeepSeek",
|
||||
mono: "DS",
|
||||
secretNames: ["DEEPSEEK_API_KEY"],
|
||||
profiles: ["headless"],
|
||||
},
|
||||
ccswitch: {
|
||||
id: "ccswitch",
|
||||
command: "cc-switch",
|
||||
name: "CC Switch",
|
||||
vendor: "Community CLI",
|
||||
mono: "CS",
|
||||
secretNames: [],
|
||||
visible: false,
|
||||
integration: true,
|
||||
},
|
||||
multica: {
|
||||
id: "multica",
|
||||
command: "multica-setup",
|
||||
name: "Multica Runtime",
|
||||
vendor: "Multica",
|
||||
mono: "MU",
|
||||
secretNames: [],
|
||||
visible: false,
|
||||
integration: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const SECRET_ENV_ALLOWLIST = new Set(
|
||||
Object.values(TOOLS).flatMap((tool) => tool.secretNames),
|
||||
);
|
||||
|
||||
export function toolById(id) {
|
||||
return typeof id === "string" ? TOOLS[id] : undefined;
|
||||
}
|
||||
|
||||
export function buildCommand(tool, request = {}) {
|
||||
const args = [];
|
||||
const input = request.initialInput?.trim() || "";
|
||||
|
||||
if (tool.integration) {
|
||||
if (request.purpose !== "authorization") throw new Error("Integration tools only support setup sessions");
|
||||
if (tool.id === "multica") {
|
||||
for (const [flag, value] of [["--server-url", request.serverUrl], ["--app-url", request.appUrl]]) {
|
||||
if (!value) continue;
|
||||
let url;
|
||||
try { url = new URL(value); } catch { throw new Error(`${flag} must be a valid URL`); }
|
||||
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error(`${flag} must be an HTTP(S) URL without embedded credentials`);
|
||||
args.push(flag, url.toString().replace(/\/$/, ""));
|
||||
}
|
||||
}
|
||||
return { command: tool.command, args, interactive: true };
|
||||
}
|
||||
|
||||
if (request.purpose === "task" && !input) {
|
||||
throw new Error("A task message is required");
|
||||
}
|
||||
|
||||
if (request.purpose === "task") {
|
||||
switch (tool.id) {
|
||||
case "codex":
|
||||
args.push("exec", "--color", "never", "--skip-git-repo-check");
|
||||
break;
|
||||
case "claude":
|
||||
case "codebuddy":
|
||||
args.push("--print", "--output-format", "text");
|
||||
break;
|
||||
case "kimi":
|
||||
args.push("--prompt", input, "--output-format", "text");
|
||||
break;
|
||||
case "opencode":
|
||||
args.push("run", "--format", "default");
|
||||
break;
|
||||
case "qwen":
|
||||
args.push("--prompt", input, "--output-format", "text");
|
||||
break;
|
||||
case "dsh":
|
||||
args.push("--profile", "headless", input);
|
||||
return { command: tool.command, args, interactive: false };
|
||||
default:
|
||||
throw new Error("Unsupported CLI task mode");
|
||||
}
|
||||
|
||||
if (tool.modelFlag && typeof request.model === "string" && request.model.trim()) {
|
||||
args.push(tool.modelFlag, request.model.trim().slice(0, 128));
|
||||
}
|
||||
if (!["kimi", "qwen"].includes(tool.id)) args.push(input);
|
||||
return { command: tool.command, args, interactive: false };
|
||||
}
|
||||
|
||||
if (tool.id === "dsh") {
|
||||
const profile = request.profile || "headless";
|
||||
if (!tool.profiles.includes(profile)) {
|
||||
throw new Error("DeepSeek Harness profile must be headless or web");
|
||||
}
|
||||
if (profile === "headless" && !request.initialInput?.trim()) {
|
||||
throw new Error("DeepSeek Harness requires a task before starting");
|
||||
}
|
||||
args.push("--profile", profile);
|
||||
if (profile === "headless" && request.initialInput) args.push(request.initialInput);
|
||||
} else if (tool.modelFlag && typeof request.model === "string" && request.model.trim()) {
|
||||
args.push(tool.modelFlag, request.model.trim().slice(0, 128));
|
||||
}
|
||||
return { command: tool.command, args, interactive: true };
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildCommand, toolById } from "./registry.js";
|
||||
|
||||
test("DeepSeek Harness requires a headless task", () => {
|
||||
const tool = toolById("dsh");
|
||||
assert.throws(() => buildCommand(tool, { profile: "headless", initialInput: "" }), /requires a task/);
|
||||
assert.deepEqual(
|
||||
buildCommand(tool, { profile: "headless", initialInput: "run tests" }),
|
||||
{ command: "dsh", args: ["--profile", "headless", "run tests"], interactive: true },
|
||||
);
|
||||
});
|
||||
|
||||
test("DeepSeek Harness rejects unavailable profiles", () => {
|
||||
assert.throws(() => buildCommand(toolById("dsh"), { profile: "tui", initialInput: "run tests" }), /must be headless/);
|
||||
});
|
||||
|
||||
test("task commands use non-interactive CLI modes", () => {
|
||||
const task = { purpose: "task", initialInput: "review this", model: "test-model" };
|
||||
assert.deepEqual(buildCommand(toolById("codex"), task), {
|
||||
command: "codex",
|
||||
args: ["exec", "--color", "never", "--skip-git-repo-check", "--model", "test-model", "review this"],
|
||||
interactive: false,
|
||||
});
|
||||
assert.deepEqual(buildCommand(toolById("claude"), task), {
|
||||
command: "claude",
|
||||
args: ["--print", "--output-format", "text", "--model", "test-model", "review this"],
|
||||
interactive: false,
|
||||
});
|
||||
assert.deepEqual(buildCommand(toolById("kimi"), task), {
|
||||
command: "kimi",
|
||||
args: ["--prompt", "review this", "--output-format", "text", "--model", "test-model"],
|
||||
interactive: false,
|
||||
});
|
||||
assert.deepEqual(buildCommand(toolById("opencode"), task), {
|
||||
command: "opencode",
|
||||
args: ["run", "--format", "default", "--model", "test-model", "review this"],
|
||||
interactive: false,
|
||||
});
|
||||
assert.deepEqual(buildCommand(toolById("qwen"), task), {
|
||||
command: "qwen",
|
||||
args: ["--prompt", "review this", "--output-format", "text", "--model", "test-model"],
|
||||
interactive: false,
|
||||
});
|
||||
assert.deepEqual(buildCommand(toolById("dsh"), task), {
|
||||
command: "dsh",
|
||||
args: ["--profile", "headless", "review this"],
|
||||
interactive: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("task commands require a message", () => {
|
||||
assert.throws(
|
||||
() => buildCommand(toolById("codex"), { purpose: "task", initialInput: "" }),
|
||||
/task message is required/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("Multica setup accepts only HTTP URLs and stays interactive", () => {
|
||||
const built = buildCommand(toolById("multica"), {
|
||||
purpose: "authorization",
|
||||
serverUrl: "https://api.example.test/",
|
||||
appUrl: "https://app.example.test/",
|
||||
});
|
||||
assert.deepEqual(built, {
|
||||
command: "multica-setup",
|
||||
args: ["--server-url", "https://api.example.test", "--app-url", "https://app.example.test"],
|
||||
interactive: true,
|
||||
});
|
||||
assert.throws(() => buildCommand(toolById("multica"), {
|
||||
purpose: "authorization",
|
||||
serverUrl: "file:///etc/passwd",
|
||||
}), /HTTP\(S\)/);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import fs from "node:fs";
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
export class SessionStore {
|
||||
constructor(directory) {
|
||||
this.directory = directory;
|
||||
this.sessions = new Map();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await fsp.mkdir(this.directory, { recursive: true, mode: 0o700 });
|
||||
const files = await fsp.readdir(this.directory);
|
||||
for (const file of files.filter((name) => name.endsWith(".json"))) {
|
||||
try {
|
||||
const record = JSON.parse(await fsp.readFile(path.join(this.directory, file), "utf8"));
|
||||
if (record.status === "running") {
|
||||
record.status = "interrupted";
|
||||
record.endedAt = new Date().toISOString();
|
||||
}
|
||||
this.sessions.set(record.id, { ...record, archived: true, clients: new Set(), buffer: "" });
|
||||
} catch {
|
||||
// A partial metadata file must not prevent the runner from starting.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(record) {
|
||||
const session = { ...record, clients: new Set(), buffer: "", errorBuffer: "", archived: false };
|
||||
this.sessions.set(session.id, session);
|
||||
this.persist(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.sessions.get(id);
|
||||
}
|
||||
|
||||
publicRecord(session) {
|
||||
const {
|
||||
clients,
|
||||
pty,
|
||||
childProcess,
|
||||
buffer,
|
||||
errorBuffer,
|
||||
logStream,
|
||||
environment,
|
||||
archived,
|
||||
...record
|
||||
} = session;
|
||||
return { ...record, connectedClients: clients?.size || 0, archived: Boolean(archived) };
|
||||
}
|
||||
|
||||
list() {
|
||||
return [...this.sessions.values()]
|
||||
.map((session) => this.publicRecord(session))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
|
||||
append(session, data) {
|
||||
session.buffer += data;
|
||||
if (Buffer.byteLength(session.buffer) > MAX_BUFFER_BYTES) {
|
||||
session.buffer = session.buffer.slice(-MAX_BUFFER_BYTES);
|
||||
}
|
||||
if (!session.logStream) {
|
||||
session.logStream = fs.createWriteStream(path.join(this.directory, `${session.id}.log`), {
|
||||
flags: "a",
|
||||
mode: 0o600,
|
||||
});
|
||||
}
|
||||
session.logStream.write(data);
|
||||
}
|
||||
|
||||
async readOutput(session) {
|
||||
if (session.buffer) return session.buffer;
|
||||
try {
|
||||
return await fsp.readFile(path.join(this.directory, `${session.id}.log`), "utf8");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
finish(session, status, exitCode = null, signal = null) {
|
||||
session.status = status;
|
||||
session.exitCode = exitCode;
|
||||
session.signal = signal;
|
||||
session.endedAt = new Date().toISOString();
|
||||
session.logStream?.end();
|
||||
session.logStream = undefined;
|
||||
this.persist(session);
|
||||
}
|
||||
|
||||
persist(session) {
|
||||
const target = path.join(this.directory, `${session.id}.json`);
|
||||
const temp = `${target}.tmp`;
|
||||
const body = `${JSON.stringify(this.publicRecord(session), null, 2)}\n`;
|
||||
fsp.writeFile(temp, body, { mode: 0o600 })
|
||||
.then(() => fsp.rename(temp, target))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user