// src/index.ts import { execFile } from "node:child_process"; import { readdirSync, realpathSync } from "node:fs"; import { readdir, readFile } from "node:fs/promises"; import { dirname, resolve as pathResolve, sep as sep2 } from "node:path"; import { createRequire } from "node:module"; import { homedir } from "node:os"; import { fileURLToPath, pathToFileURL } from "node:url"; // src/requestAuth.ts async function requestRejection(ctx, req) { try { const connection = typeof ctx.get === "function" ? ctx.get("connection") : void 0; if (!connection) return 503; if (typeof connection.admit === "function") { const admission = await connection.admit(req); if (!admission || typeof admission !== "object") return 503; if ("rejection" in admission) return admission.rejection === 401 ? 401 : 403; const peer = admission.peer; if (peer?.principal?.kind === "owner") return void 0; if (peer && peer === connection.operator && peer.principal === void 0) return void 0; return 403; } } catch { } return 503; } function rejectionText(status) { return status === 401 ? "unauthorized" : status === 403 ? "forbidden" : "Host authentication unavailable"; } // src/previewFiles.ts import { randomBytes } from "node:crypto"; import { open, realpath, stat } from "node:fs/promises"; import { isAbsolute, relative, resolve, sep } from "node:path"; var PREVIEW_PREFIX = "/api/worktable/preview-static"; var PREVIEW_SANDBOX = "allow-scripts allow-forms allow-downloads"; var PREVIEW_CSP = `sandbox ${PREVIEW_SANDBOX}; base-uri 'none'; object-src 'none'; frame-src 'none'`; var PREVIEW_MAX_BYTES = 40 * 1024 * 1024; var IDLE_MS = 30 * 60 * 1e3; var MAX_MOUNTS = 128; function inside(root, file) { const rel = relative(root, file); return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + sep); } function createPreviewFiles(now = () => Date.now()) { const mounts = /* @__PURE__ */ new Map(); const prune = () => { for (const [key, item] of mounts) if (now() - item.touched >= IDLE_MS) mounts.delete(key); }; return { async mount(root) { if (!isAbsolute(root)) throw new Error("Preview root must be absolute"); const canonical = await realpath(root); if (!(await stat(canonical)).isDirectory()) throw new Error("Preview root is not a directory"); prune(); if (mounts.size >= MAX_MOUNTS) throw new Error("Too many open previews; close an unused preview first"); const token = randomBytes(24).toString("base64url"); mounts.set(token, { root: canonical, touched: now() }); return token; }, revoke(token) { mounts.delete(token); }, clear() { mounts.clear(); }, async file(token, segments, headOnly = false) { prune(); const mount = mounts.get(token); if (!mount) return null; if (segments.some((s) => s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0"))) return null; let target = resolve(mount.root, ...segments); if (!inside(mount.root, target)) return null; let info = await stat(target).catch(() => null); if (info?.isDirectory()) target = resolve(target, "index.html"); const canonical = await realpath(target).catch(() => null); if (!canonical || !inside(mount.root, canonical)) return null; const handle = await open(canonical, "r").catch(() => null); if (!handle) return null; try { const before = await handle.stat({ bigint: true }); if (!before.isFile()) return null; const sameFile = (other) => other.isFile() && other.dev === before.dev && other.ino === before.ino && other.size === before.size && other.mtimeNs === before.mtimeNs && other.ctimeNs === before.ctimeNs; const stillValid = async () => { const current = await realpath(canonical); if (current !== canonical || !inside(mount.root, current)) return false; const named = await stat(current, { bigint: true }); const opened = await handle.stat({ bigint: true }); return sameFile(named) && sameFile(opened) && mounts.get(token) === mount && now() - mount.touched < IDLE_MS; }; const tooLarge = () => Object.assign(new Error("file too large"), { status: 413 }); if (before.size > BigInt(PREVIEW_MAX_BYTES)) throw tooLarge(); if (!await stillValid()) return null; const chunks = []; let total = 0; if (!headOnly) { while (total <= PREVIEW_MAX_BYTES) { const chunk = Buffer.alloc(Math.min(64 * 1024, PREVIEW_MAX_BYTES + 1 - total)); const { bytesRead } = await handle.read(chunk, 0, chunk.length, total); if (!bytesRead) break; total += bytesRead; if (total > PREVIEW_MAX_BYTES) throw tooLarge(); chunks.push(chunk.subarray(0, bytesRead)); } } if (!await stillValid() || !headOnly && BigInt(total) !== before.size) return null; mount.touched = now(); return { path: canonical, size: Number(before.size), data: headOnly ? void 0 : Buffer.concat(chunks, total) }; } catch (error) { if (error?.status === 413) throw error; return null; } finally { await handle.close(); } } }; } // template/dshell.css var dshell_default = `/* dsh-worktable \u539F\u751F\u76AE\u80A4 \xB7 DSH \u8BBE\u8BA1\u7CFB\u7EDF\u7EC4\u4EF6\u5E93\r \u7528\u6CD5\uFF1A\r \u6240\u6709\u989C\u8272\u8D70 DSH \u4E3B\u9898\u53D8\u91CF\uFF08--dsw-alias-*\uFF09\uFF0C\u81EA\u52A8\u9002\u914D\u660E\u6697\u4E3B\u9898\u3002 */\r :root { color-scheme: dark; }\r * { box-sizing: border-box; }\r html, body { margin: 0; padding: 0; }\r body {\r background: var(--dsw-alias-bg-base, #0b0e14);\r color: var(--dsw-alias-label-primary, #e6e8eb);\r font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;\r font-size: 13px;\r line-height: 1.6;\r }\r .dshell { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; min-height: 100%; }\r /* \u6587\u5B57\u5C42\u7EA7 */\r .dshell-title { margin: 0; font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e8eb); }\r .dshell-sub { margin: 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #9aa4b2); }\r .dshell-muted { color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 11.5px; }\r /* \u5361\u7247 */\r .dshell-card { border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 10px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); padding: 12px 14px; }\r .dshell-card + .dshell-card { margin-top: 10px; }\r /* \u6309\u94AE\uFF08\u7EFF\u8272\u4E3B\u6309\u94AE / \u5E7D\u7075\u6309\u94AE / \u5371\u9669\uFF09 */\r .dshell-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; border: 1px solid transparent; background: #3fb950; color: #0b0e14; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }\r .dshell-btn:hover { filter: brightness(1.08); }\r .dshell-btnGhost { background: transparent; border-color: var(--dsw-alias-border-l1, #262b36); color: var(--dsw-alias-label-secondary, #9aa4b2); }\r .dshell-btnGhost:hover { color: var(--dsw-alias-label-primary, #e6e8eb); border-color: var(--dsw-alias-border-l2, #3a4150); }\r .dshell-btnDanger { background: transparent; border-color: #f85149; color: #f85149; }\r /* \u72B6\u6001\u5FBD\u6807\uFF08\u5706\u70B9 + \u6587\u5B57\uFF1B\u7EFF=\u5DF2\u5B8C\u6210 \u9EC4=\u5F85\u529E/\u5F85\u53D1\u5E03 \u7070=\u672A\u5F00\u59CB\uFF09 */\r .dshell-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1, #262b36); font-size: 11.5px; color: var(--dsw-alias-label-secondary, #9aa4b2); background: var(--dsw-alias-fill-l1, rgba(255,255,255,.03)); }\r .dshell-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #6b7280); }\r .dshell-badgeDone { color: #3fb950; border-color: rgba(63,185,80,.4); }\r .dshell-badgeDone::before { background: #3fb950; box-shadow: 0 0 5px #3fb950; }\r .dshell-badgeWait { color: #d29922; border-color: rgba(210,153,34,.4); }\r .dshell-badgeWait::before { background: #d29922; box-shadow: 0 0 5px #d29922; }\r .dshell-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #6b7280); }\r .dshell-dotDone { background: #3fb950; box-shadow: 0 0 5px #3fb950; }\r .dshell-dotWait { background: #d29922; box-shadow: 0 0 5px #d29922; }\r /* \u6807\u7B7E\u9875 */\r .dshell-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l1, #262b36); }\r .dshell-tab { padding: 7px 12px; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #9aa4b2); cursor: pointer; border: none; background: none; font: inherit; border-bottom: 2px solid transparent; margin-bottom: -1px; }\r .dshell-tabOn { color: var(--dsw-alias-label-primary, #e6e8eb); border-bottom-color: var(--dsw-alias-state-accent-primary, #4f8ef7); }\r /* \u5217\u8868 */\r .dshell-list { display: flex; flex-direction: column; gap: 6px; }\r .dshell-listItem { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 8px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); cursor: pointer; }\r .dshell-listItem:hover { border-color: var(--dsw-alias-border-l2, #3a4150); background: var(--dsw-alias-fill-l1, rgba(255,255,255,.05)); }\r .dshell-listItemTitle { font-size: 12.5px; color: var(--dsw-alias-label-primary, #e6e8eb); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\r .dshell-listItemMeta { flex: none; font-size: 11px; color: var(--dsw-alias-label-tertiary, #6b7280); }\r /* \u7F51\u683C / \u7EDF\u8BA1 */\r .dshell-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }\r .dshell-stat { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 10px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); }\r .dshell-statLabel { font-size: 11px; color: var(--dsw-alias-label-secondary, #9aa4b2); }\r .dshell-statValue { font-size: 20px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e8eb); }\r .dshell-statDelta { font-size: 11px; color: #3fb950; }\r /* \u8FDB\u5EA6\u6761 */\r .dshell-progress { height: 6px; border-radius: 3px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.06)); overflow: hidden; }\r .dshell-progressBar { height: 100%; border-radius: 3px; background: #3fb950; }\r /* \u8F93\u5165 */\r .dshell-input, .dshell-textarea { width: 100%; padding: 7px 10px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 8px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.03)); color: var(--dsw-alias-label-primary, #e6e8eb); font: inherit; font-size: 12.5px; outline: none; }\r .dshell-input:focus, .dshell-textarea:focus { border-color: var(--dsw-alias-state-accent-primary, #4f8ef7); }\r /* \u8868\u683C */\r .dshell-table { width: 100%; border-collapse: collapse; font-size: 12px; }\r .dshell-table th, .dshell-table td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1, #262b36); }\r .dshell-table th { color: var(--dsw-alias-label-secondary, #9aa4b2); font-weight: 500; }\r /* \u952E\u503C\u5BF9 */\r .dshell-kv { display: flex; flex-direction: column; gap: 6px; }\r .dshell-kvRow { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }\r .dshell-kvKey { color: var(--dsw-alias-label-secondary, #9aa4b2); }\r .dshell-kvValue { color: var(--dsw-alias-label-primary, #e6e8eb); text-align: right; }\r /* \u5206\u5272\u7EBF */\r .dshell-divider { height: 1px; background: var(--dsw-alias-border-l1, #262b36); margin: 6px 0; }\r /* \u6EDA\u52A8\u6761 */\r ::-webkit-scrollbar { width: 10px; height: 10px; }\r ::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 5px; }\r ::-webkit-scrollbar-track { background: transparent; }\r `; // template/dshell.html var dshell_default2 = '\r\n\r\n\r\n\r\n \r\n \r\n \u6211\u7684\u7A97\u53E3\r\n \r\n\r\n\r\n
\r\n \r\n

\u7A97\u53E3\u6807\u9898

\r\n

\u4E00\u53E5\u8BDD\u8BF4\u660E\u8FD9\u4E2A\u7A97\u53E3\u505A\u4EC0\u4E48\u3002

\r\n\r\n \r\n
\r\n \u5DF2\u5B8C\u6210\r\n \u8FDB\u884C\u4E2D\r\n \u672A\u5F00\u59CB\r\n
\r\n\r\n \r\n
\r\n \r\n \r\n \r\n
\r\n\r\n \r\n
\r\n
\r\n
\u603B\u6570
\r\n
128
\r\n
+12.4%
\r\n
\r\n
\r\n
\u8FDB\u884C\u4E2D
\r\n
7
\r\n
\r\n
\r\n
\u5DF2\u5B8C\u6210
\r\n
121
\r\n
\r\n
\r\n\r\n \r\n
\r\n
\r\n \u6761\u76EE\u4E00\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898\r\n \u6628\u5929\r\n
\r\n
\r\n \u6761\u76EE\u4E8C\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898\r\n \u5DF2\u53D1\u5E03\r\n
\r\n
\r\n\r\n \r\n
\r\n

\u8BE6\u60C5

\r\n
\r\n
\u5B57\u6BB5 A\u503C A
\r\n
\u5B57\u6BB5 B\u503C B
\r\n
\r\n
\r\n
\r\n
\r\n\r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n\r\n\r\n'; // src/index.ts function baseDshHome() { const env = process.env.DSH_HOME; const value = env !== void 0 && env.trim().length > 0 ? env : pathResolve(homedir(), ".dsh"); if (value === "~") return homedir(); if (value.startsWith("~/") || value.startsWith("~\\")) return pathResolve(homedir(), value.slice(2)); return pathResolve(value); } var cachedDshHome = null; var dshHomeSource = "fallback"; function resolveDshHomeSafe() { if (cachedDshHome) return cachedDshHome; try { const pkg = loadPkg("@deepseek-ai/dsh-home-paths"); if (pkg && typeof pkg.resolveDshHome === "function") { const home = pkg.resolveDshHome(void 0, process.env); if (typeof home === "string" && home.trim() !== "") { dshHomeSource = "official"; cachedDshHome = home; return cachedDshHome; } } } catch { } dshHomeSource = "fallback"; cachedDshHome = baseDshHome(); return cachedDshHome; } var PLUGIN_VERSION = false ? "dev" : "0.5.2-local.6"; var name = "dsh-worktable"; var inject = ["webServer", "sessions"]; var HEALTH_PATH = "/api/worktable/health"; var MAX_ENTRIES = 500; var FILE_TYPES = { html: "text/html; charset=utf-8", htm: "text/html; charset=utf-8", css: "text/css; charset=utf-8", js: "text/javascript; charset=utf-8", mjs: "text/javascript; charset=utf-8", json: "application/json; charset=utf-8", map: "application/json; charset=utf-8", md: "text/markdown; charset=utf-8", markdown: "text/markdown; charset=utf-8", txt: "text/plain; charset=utf-8", log: "text/plain; charset=utf-8", pdf: "application/pdf", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", ico: "image/x-icon", avif: "image/avif", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", wasm: "application/wasm", mp3: "audio/mpeg", mp4: "video/mp4", webm: "video/webm" }; var SITE_PREFIX = "/api/worktable/site"; var TEMPLATE_PREFIX = "/api/worktable/template"; var loadProbeAttempts = 0; function loadPkg(pkg) { const starts = /* @__PURE__ */ new Set(); try { starts.add(dirname(fileURLToPath(import.meta.url))); } catch { } try { starts.add(realpathSync(dirname(fileURLToPath(import.meta.url)))); } catch { } for (const start of starts) { let dir = start; while (dir && dir !== pathResolve(dir, "..")) { loadProbeAttempts++; try { const req = createRequire(pathToFileURL(pathResolve(dir, "__wt_probe__.js")).href); return req(pkg); } catch { } dir = pathResolve(dir, ".."); } } try { const profilesDir = pathResolve(baseDshHome(), "profiles"); for (const profile of readdirSync(profilesDir, { withFileTypes: true })) { if (!profile.isDirectory() && !profile.isSymbolicLink()) continue; const nm = pathResolve(profilesDir, profile.name, "node_modules"); loadProbeAttempts++; try { const req = createRequire(pathToFileURL(pathResolve(nm, "__wt_probe__.js")).href); return req(pkg); } catch { } } } catch { } return null; } function __wtLoadProbeStats() { return { attempts: loadProbeAttempts, homeSource: dshHomeSource }; } function serverCwd(ctx, sessionId, clientCwd) { if (sessionId) { try { const headerCwd = ctx.sessions?.get?.(sessionId)?.header?.cwd; if (typeof headerCwd === "string" && headerCwd) return headerCwd; } catch { } } if (typeof clientCwd === "string" && clientCwd) return clientCwd; return process.cwd(); } function json(res, status, body) { res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }); res.end(JSON.stringify(body)); } async function readJsonBody(req) { const chunks = []; let bytes = 0; for await (const chunk of req) { const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk; bytes += data.length; if (bytes > 22 * 1024 * 1024) throw Object.assign(new Error("request too large"), { status: 413 }); chunks.push(data); } const text = Buffer.concat(chunks).toString("utf8"); if (!text) return {}; try { return JSON.parse(text); } catch { return {}; } } async function listDirectory(path) { const abs = pathResolve(path); const dirents = await readdir(abs, { withFileTypes: true }); const entries = dirents.map((d) => ({ name: d.name, path: abs + sep2 + d.name, isDir: d.isDirectory(), hidden: d.name.startsWith(".") })).sort((a, b) => { if (a.isDir !== b.isDir) return a.isDir ? -1 : 1; return a.name.localeCompare(b.name, void 0, { sensitivity: "base" }); }); const truncated = entries.length > MAX_ENTRIES; return { path: abs, entries: truncated ? entries.slice(0, MAX_ENTRIES) : entries, truncated }; } function gitExec(args, cwd) { return new Promise((resolvePromise, reject) => { execFile("git", args, { cwd, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => { if (err) reject(err); else resolvePromise(stdout); }); }); } async function gitStatus(cwd) { try { const branchRaw = await gitExec(["rev-parse", "--abbrev-ref", "HEAD"], cwd); const porcelain = await gitExec(["status", "--porcelain=v1", "-z"], cwd); const entries = porcelain.split("\0").filter((s) => s.length > 2).map((s) => ({ xy: s.slice(0, 2), path: s.slice(3) })); return { isRepo: true, branch: branchRaw.trim() || "HEAD", entries }; } catch { return { isRepo: false, branch: void 0, entries: [] }; } } function setupTerminal(webServer, ctx) { if (typeof webServer.registerUpgrade !== "function") return; const wsMod = loadPkg("ws"); const ptyMod = loadPkg("node-pty"); ctx.logger?.info?.("[dsh-worktable] term deps: ws=" + (wsMod ? "ok" : "MISSING") + " node-pty=" + (ptyMod ? "ok" : "MISSING")); if (!wsMod || !ptyMod) { ctx.logger?.warn("[dsh-worktable] \u7EC8\u7AEF\u8DEF\u7531\u672A\u6CE8\u518C\uFF1Aws/node-pty \u4E0D\u53EF\u7528"); return; } const WebSocketServer = wsMod.WebSocketServer ?? wsMod.default?.WebSocketServer; if (!WebSocketServer) return; const pty = ptyMod.default ?? ptyMod; const wss = new WebSocketServer({ noServer: true }); let disposed = false; const terminals = /* @__PURE__ */ new Set(); ctx.effect(() => () => { disposed = true; for (const ws of wss.clients ?? []) { try { ws.terminate(); } catch { } } for (const term of terminals) { try { term.kill(); } catch { } } terminals.clear(); try { wss.close(); } catch { } }, "dsh-worktable: terminal sessions"); const spawnShell = () => process.platform === "win32" ? { cmd: "powershell.exe", args: ["-NoLogo", "-NoProfile"] } : { cmd: process.env.SHELL || "/bin/bash", args: [] }; const clampDim = (v, fallback) => Math.min(1024, Math.max(2, Number.isFinite(v) ? v : fallback)); ctx.effect(() => webServer.registerUpgrade({ path: "/api/worktable/term", handler: async (req, socket, head) => { let rejected = await requestRejection(ctx, req); if (disposed) rejected = 503; if (rejected !== void 0) { socket.end(`HTTP/1.1 ${rejected} ${rejectionText(rejected)}\r Connection: close\r Content-Length: 0\r \r `); return; } wss.handleUpgrade(req, socket, head, (ws) => { const u = new URL(req.url ?? "/", "http://dsh.internal"); const cwd = serverCwd(ctx, u.searchParams.get("sessionId") || void 0, u.searchParams.get("cwd") || void 0); const cols = clampDim(Number(u.searchParams.get("cols")), 80); const rows = clampDim(Number(u.searchParams.get("rows")), 24); let term = null; try { const shell = spawnShell(); term = pty.spawn(shell.cmd, shell.args, { name: "xterm-256color", cols, rows, cwd, env: process.env }); terminals.add(term); } catch (err) { try { ws.send("\r\n[worktable] \u7EC8\u7AEF\u542F\u52A8\u5931\u8D25\uFF1A" + String(err)); } catch { } try { ws.close(); } catch { } return; } term.onData((d) => { try { ws.send(d); } catch { } }); term.onExit(() => { terminals.delete(term); try { ws.close(); } catch { } }); ws.on("message", (raw) => { const text = String(raw); try { const msg = JSON.parse(text); if (msg && msg.type === "resize" && Number.isFinite(msg.cols) && Number.isFinite(msg.rows)) { term.resize(clampDim(msg.cols, cols), clampDim(msg.rows, rows)); return; } } catch { } try { term.write(text); } catch { } }); ws.on("close", () => { terminals.delete(term); try { term.kill(); } catch { } }); }); } }), "dsh-worktable: terminal upgrade"); } function apply(ctx) { const webServer = ctx.webServer; if (!webServer) { ctx.logger?.warn("[dsh-worktable] ctx.webServer \u4E0D\u53EF\u7528\uFF08headless profile\uFF1F\uFF09\uFF0C\u8DF3\u8FC7\u670D\u52A1\u7AEF\u8DEF\u7531"); return; } const previews = createPreviewFiles(); ctx.effect(() => () => previews.clear(), "dsh-worktable: preview capabilities"); const stateLog = (line) => { void (async () => { try { const fsx = await import("node:fs/promises"); const dir = pathResolve(resolveDshHomeSafe(), "logs"); await fsx.mkdir(dir, { recursive: true }); await fsx.appendFile(pathResolve(dir, "worktable-state.log"), (/* @__PURE__ */ new Date()).toISOString() + " " + line + "\n", "utf8"); } catch { } })(); }; const register = (route, publicAsset = false) => ctx.effect(() => webServer.register({ ...route, handler: async (req, res) => { if (!publicAsset && route.path !== HEALTH_PATH) { const rejected = await requestRejection(ctx, req); if (rejected !== void 0) { if (route.path === "/api/worktable/state") stateLog("REJECT " + req.method + " -> " + rejected); json(res, rejected, { error: rejectionText(rejected) }); return; } } return route.handler(req, res); } }), "dsh-worktable: " + route.path); register({ kind: "exact", path: "/api/worktable/preview", handler: async (req, res) => { if (req.method !== "POST") { json(res, 405, { error: "method not allowed" }); return; } try { const body = await readJsonBody(req); if (typeof body.revoke === "string") { previews.revoke(body.revoke); json(res, 200, { ok: true }); return; } if (typeof body.root !== "string") { json(res, 400, { error: "missing root" }); return; } const token = await previews.mount(body.root); json(res, 200, { token, base: PREVIEW_PREFIX + "/" + token + "/" }); } catch (err) { json(res, err.status ?? 400, { error: String(err) }); } } }); register({ kind: "prefix", path: PREVIEW_PREFIX, handler: async (req, res) => { if (req.method !== "GET" && req.method !== "HEAD") { json(res, 405, { error: "method not allowed" }); return; } try { const parts = new URL(req.url ?? "/", "http://dsh.internal").pathname.slice(PREVIEW_PREFIX.length + 1).split("/").map(decodeURIComponent); const file = await previews.file(parts.shift() ?? "", parts.filter(Boolean), req.method === "HEAD"); if (!file) { json(res, 404, { error: "preview unavailable; refresh to reopen" }); return; } const ext = (file.path.split(".").pop() || "").toLowerCase(); res.writeHead(200, { "content-type": FILE_TYPES[ext] ?? "application/octet-stream", "cache-control": "no-store", "referrer-policy": "no-referrer", "content-security-policy": PREVIEW_CSP, "x-content-type-options": "nosniff", "access-control-allow-origin": "*" }); res.end(file.data); } catch (err) { json(res, err?.status === 413 ? 413 : 404, { error: err?.status === 413 ? "file too large" : "preview unavailable" }); } } }, true); register({ kind: "exact", path: HEALTH_PATH, handler: (_req, res) => { json(res, 200, { plugin: "dsh-worktable", version: PLUGIN_VERSION, ok: true }); } }); register({ kind: "exact", path: "/api/worktable/file", handler: async (req, res) => { try { const u = new URL(req.url ?? "/", "http://dsh.internal"); const p = u.searchParams.get("path") || ""; if (!p) { json(res, 400, { error: "missing path" }); return; } const abs = pathResolve(p); const stat2 = await import("node:fs/promises").then((m) => m.stat(abs)); if (stat2.size > 20 * 1024 * 1024) { json(res, 413, { error: "file too large" }); return; } const data = await readFile(abs); const ext = (abs.split(".").pop() || "").toLowerCase(); const types = { html: "text/html; charset=utf-8", htm: "text/html; charset=utf-8", css: "text/css; charset=utf-8", js: "text/javascript; charset=utf-8", mjs: "text/javascript; charset=utf-8", json: "application/json; charset=utf-8", md: "text/markdown; charset=utf-8", markdown: "text/markdown; charset=utf-8", txt: "text/plain; charset=utf-8", log: "text/plain; charset=utf-8", pdf: "application/pdf", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", ico: "image/x-icon" }; res.writeHead(200, { "content-type": FILE_TYPES[ext] ?? "application/octet-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", .../^(html?|svg)$/.test(ext) ? { "content-security-policy": PREVIEW_CSP, "referrer-policy": "no-referrer" } : {} }); res.end(data); } catch (err) { json(res, 404, { error: String(err) }); } } }); register({ kind: "prefix", path: TEMPLATE_PREFIX, handler: (req, res) => { try { if (req.method !== "GET") { res.writeHead(405); res.end(); return; } const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname; const rel = pathname.slice(TEMPLATE_PREFIX.length); if (rel === "/dshell.css") { res.writeHead(200, { "content-type": "text/css; charset=utf-8", "cache-control": "no-store" }); res.end(dshell_default); } else { res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }); res.end(dshell_default2); } } catch (err) { res.writeHead(404); res.end(String(err)); } } }); register({ kind: "prefix", path: SITE_PREFIX, handler: async (req, res) => { try { if (req.method !== "GET") { res.writeHead(405); res.end(); return; } const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname; const segs = pathname.slice(SITE_PREFIX.length).split("/").filter(Boolean); const rootToken = decodeURIComponent(segs.shift() ?? ""); const rel = segs.map((s) => { try { return decodeURIComponent(s); } catch { return s; } }).join("/"); if (!rootToken) { json(res, 400, { error: "missing root" }); return; } const root = pathResolve(rootToken); let abs = pathResolve(root, rel); if (abs !== root && !abs.startsWith(root + sep2)) { json(res, 403, { error: "outside root" }); return; } const statMod = await import("node:fs/promises"); let info = await statMod.stat(abs).catch(() => null); if (info && info.isDirectory()) { abs = pathResolve(abs, "index.html"); info = await statMod.stat(abs).catch(() => null); } if (!info || !info.isFile()) { json(res, 404, { error: "not found" }); return; } if (info.size > 40 * 1024 * 1024) { json(res, 413, { error: "file too large" }); return; } const data = await readFile(abs); const ext = (abs.split(".").pop() || "").toLowerCase(); res.writeHead(200, { "content-type": FILE_TYPES[ext] ?? "application/octet-stream", "cache-control": "no-store", "content-security-policy": PREVIEW_CSP, "referrer-policy": "no-referrer", "x-content-type-options": "nosniff" }); res.end(data); } catch (err) { json(res, 404, { error: String(err) }); } } }); register({ kind: "exact", path: "/api/worktable/fs", handler: async (req, res) => { try { const body = await readJsonBody(req); const path = typeof body.path === "string" && body.path ? body.path : serverCwd(ctx, body.sessionId, body.cwd); json(res, 200, await listDirectory(path)); } catch (err) { json(res, err.status ?? 500, { path: "", entries: [], truncated: false, error: String(err) }); } } }); register({ kind: "exact", path: "/api/worktable/workspaces", handler: async (_req, res) => { try { let registry = null; try { registry = ctx.workspaceRegistry ?? null; } catch { } if (!registry) { try { registry = ctx.get?.("workspaceRegistry") ?? null; } catch { } } if (registry && typeof registry.list === "function") { const list = registry.list() ?? []; const workspaceIds = []; const tables = {}; for (const ws of list) { const id = String(ws?.id ?? ""); if (!id) continue; workspaceIds.push(id); tables[id] = { title: typeof ws?.title === "string" ? ws.title : void 0, sessionIds: Array.isArray(ws?.sessionIds) ? ws.sessionIds.map(String) : [] }; } let archived = []; try { archived = (registry.archivedSessionIds ?? []).map(String); } catch { } json(res, 200, { unit: { name: "workspace", version: 2 }, global: { initialized: true, workspaceIds, archivedSessionIds: archived }, tables: { workspaces: tables } }); return; } const file = pathResolve(resolveDshHomeSafe(), "storages", "workspace.json"); const raw = await readFile(file, "utf8"); json(res, 200, JSON.parse(raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw)); } catch (err) { json(res, 404, { error: String(err) }); } } }); register({ kind: "exact", path: "/api/worktable/write", handler: async (req, res) => { try { if (req.method !== "POST") { res.writeHead(405); res.end(); return; } const body = await readJsonBody(req); const p = typeof body.path === "string" ? body.path : ""; const content = typeof body.content === "string" ? body.content : ""; if (!p) { json(res, 400, { error: "missing path" }); return; } if (content.length > 20 * 1024 * 1024) { json(res, 413, { error: "content too large" }); return; } const abs = pathResolve(p); await import("node:fs/promises").then((m) => m.writeFile(abs, content, "utf8")); json(res, 200, { ok: true }); } catch (err) { json(res, err.status ?? 500, { error: String(err) }); } } }); register({ kind: "exact", path: "/api/worktable/mkdir", handler: async (req, res) => { try { if (req.method !== "POST") { res.writeHead(405); res.end(); return; } const body = await readJsonBody(req); const p = typeof body.path === "string" ? body.path.trim() : ""; if (!p) { json(res, 400, { error: "missing path" }); return; } const abs = pathResolve(p); const fsx = await import("node:fs/promises"); const parent = dirname(abs); try { await fsx.access(parent); } catch { json(res, 400, { error: "parent not found" }); return; } await fsx.mkdir(abs); json(res, 200, { ok: true, path: abs }); } catch (err) { json(res, err?.code === "EEXIST" ? 200 : err.status ?? 500, err?.code === "EEXIST" ? { ok: true, exists: true } : { error: String(err) }); } } }); register({ kind: "exact", path: "/api/worktable/git", handler: async (req, res) => { try { const body = await readJsonBody(req); const cwd = serverCwd(ctx, body.sessionId, body.cwd); json(res, 200, await gitStatus(cwd)); } catch (err) { json(res, err.status ?? 500, { error: String(err) }); } } }); const statePath = () => pathResolve(resolveDshHomeSafe(), "worktable-state.json"); register({ kind: "exact", path: "/api/worktable/state", handler: async (req, res) => { const file = statePath(); if (req.method === "GET" || req.method === "HEAD") { try { const raw = await readFile(file, "utf8"); const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw; const parsed = JSON.parse(text); json(res, 200, { ok: true, path: file, state: parsed }); } catch (err) { if (err?.code === "ENOENT") { stateLog("GET empty (no file)"); json(res, 200, { ok: true, path: file, state: null }); return; } stateLog("GET error " + String(err)); json(res, 500, { error: String(err) }); } return; } if (req.method !== "POST") { json(res, 405, { error: "method not allowed" }); return; } try { const body = await readJsonBody(req); const incoming = body && typeof body.state === "object" && body.state !== null ? body.state : null; if (!incoming) { stateLog("POST 400 missing state"); json(res, 400, { error: "missing state" }); return; } let previous = null; try { const raw = await readFile(file, "utf8"); previous = JSON.parse(raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw); } catch { } const state = previous && typeof previous === "object" && !Array.isArray(previous) ? { ...previous, ...incoming } : incoming; const text = JSON.stringify(state); const inN = incoming?.projects?.layouts?.length ?? 0; const prevN = previous?.projects?.layouts?.length ?? 0; if (inN < prevN) { stateLog("WARN projects shrink " + prevN + " -> " + inN + " (keys=" + Object.keys(incoming).join("+") + ")"); } if (text.length > 20 * 1024 * 1024) { json(res, 413, { error: "state too large" }); return; } const fsx = await import("node:fs/promises"); await fsx.mkdir(dirname(file), { recursive: true }); const tmp = file + ".tmp-" + process.pid + "-" + Date.now(); await fsx.writeFile(tmp, text, "utf8"); await fsx.rename(tmp, file); json(res, 200, { ok: true, path: file }); } catch (err) { json(res, err?.status ?? 500, { error: String(err) }); } } }); setupTerminal(webServer, ctx); } export { HEALTH_PATH, __wtLoadProbeStats, apply, inject, name };