trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
984 lines (974 loc) • 30.6 kB
JavaScript
import {
init_resolve_runtime_theme_css,
resolveRuntimeThemeCss
} from "./chunk-WJ5T7N6J.js";
import {
init_node_adapter,
startNodeServer
} from "./chunk-KO2LIUUR.js";
import {
TrellisVcsEngine,
init_engine
} from "./chunk-O56VT7VP.js";
import "./chunk-QC5OHKIJ.js";
import {
getPromoteLockStatus,
init_promote_lock
} from "./chunk-DIHRG6LA.js";
import "./chunk-2DFWNMEW.js";
import "./chunk-G3XIHPSQ.js";
import {
init_lane,
loadLaneMeta,
saveLaneMeta
} from "./chunk-Q4FKTPX4.js";
import "./chunk-KFJMKL4Y.js";
import "./chunk-LNCBUJNO.js";
import "./chunk-PBH357QR.js";
import "./chunk-E2CFJKLU.js";
import "./chunk-MFZ22U6M.js";
import {
PROVENANCE,
init_canonical_op
} from "./chunk-RUMOVKR4.js";
import "./chunk-GRWQPKYK.js";
import "./chunk-2ESYSVXG.js";
// src/ui/lanes-dashboard.ts
init_engine();
init_lane();
init_node_adapter();
import { existsSync as existsSync2, readFileSync } from "fs";
import { join as join3, dirname as dirname2, basename } from "path";
import { fileURLToPath as fileURLToPath2 } from "url";
// src/ui/lanes-snapshot.ts
init_promote_lock();
import { join } from "path";
function buildLanesSnapshot(engine, rootPath, extras = {}) {
const trellisDir = join(rootPath, ".trellis");
const lock = getPromoteLockStatus(trellisDir);
const activeLaneId = engine.getActiveLaneId();
const lanes = engine.listLanes().map((meta) => {
const summary = safeSummarize(engine, meta.id);
const issuePlain = meta.issueId?.replace(/^issue:/, "");
const issue = issuePlain ? engine.getIssue(issuePlain) : null;
return {
id: meta.id,
status: meta.status,
issueId: meta.issueId,
issueTitle: issue?.title,
claimedSessionId: issue?.claimedSessionId,
sessionId: meta.sessionId,
agentId: meta.agentId,
opCount: engine.getLaneOpCount(meta.id),
fileCount: summary.fileCount,
worktreePath: meta.worktreePath,
baseBranch: meta.baseBranch,
targetBranch: meta.targetBranch,
isActive: activeLaneId === meta.id,
createdAt: meta.createdAt,
updatedAt: meta.updatedAt
};
});
const inProgressIssues = engine.getActiveIssues().map((issue) => ({
id: issue.id,
title: issue.title,
claimedLaneId: issue.claimedLaneId,
claimedSessionId: issue.claimedSessionId
}));
const savedLaneId = engine.getActiveLaneId();
const savedLaneLog = engine.activeLaneLog;
engine.activeLaneId = void 0;
engine.activeLaneLog = null;
engine.open();
const allIssues = engine.listIssues().map((issue) => {
const issueLanes = lanes.filter((l) => l.issueId === issue.id || l.issueId === `issue:${issue.id}`);
return {
id: issue.id,
title: issue.title,
status: issue.status,
priority: issue.priority,
labels: issue.labels || [],
createdAt: issue.createdAt,
claimedLaneId: issue.claimedLaneId,
claimedSessionId: issue.claimedSessionId,
laneCount: issueLanes.length,
laneIds: issueLanes.map((l) => l.id)
};
});
allIssues.sort((a, b) => {
const ac = a.createdAt || "";
const bc = b.createdAt || "";
if (ac && bc && ac !== bc) return bc.localeCompare(ac);
return b.id.localeCompare(a.id, void 0, { numeric: true });
});
const milestones = engine.listMilestones().map((m) => ({
id: m.id.replace(/^milestone:/, ""),
message: m.message,
createdAt: m.createdAt,
createdBy: m.createdBy,
fileCount: m.affectedFiles?.length ?? 0,
affectedFiles: m.affectedFiles ?? []
}));
milestones.sort((a, b) => {
const ac = a.createdAt || "";
const bc = b.createdAt || "";
if (ac && bc && ac !== bc) return bc.localeCompare(ac);
return b.id.localeCompare(a.id, void 0, { numeric: true });
});
engine.activeLaneId = savedLaneId;
engine.activeLaneLog = savedLaneLog;
if (savedLaneId) engine.open();
lanes.sort((a, b) => {
if (a.isActive !== b.isActive) return a.isActive ? -1 : 1;
if (a.status !== b.status) {
const order = ["active", "promoting", "promoted", "dropped"];
return order.indexOf(a.status) - order.indexOf(b.status);
}
const au = a.updatedAt || a.createdAt || "";
const bu = b.updatedAt || b.createdAt || "";
if (au !== bu) return bu.localeCompare(au);
return b.id.localeCompare(a.id, void 0, { numeric: true });
});
const activeAgentIds = new Set(
lanes.filter((l) => l.isActive || l.status === "active").map((l) => l.agentId).filter(Boolean)
);
return {
at: (/* @__PURE__ */ new Date()).toISOString(),
rootPath,
integrationBranch: engine.getCurrentBranch?.() ?? "main",
activeLaneId,
port: extras.port,
viewers: extras.viewers ?? 0,
activeAgents: activeAgentIds.size,
promoteLock: {
locked: lock.locked,
stale: lock.stale,
laneId: lock.record?.laneId,
pid: lock.record?.pid,
acquiredAt: lock.record?.acquiredAt
},
lanes,
issues: allIssues,
milestones,
inProgressIssues
};
}
function safeSummarize(engine, laneId) {
try {
const { filePaths } = engine.summarizeLane(laneId);
return { fileCount: filePaths.length };
} catch {
return { fileCount: 0 };
}
}
// src/ui/causal-graph-snapshot.ts
function shortHash(hash) {
if (!hash) return "\u2014";
const hex = hash.match(/[a-f0-9]{7,}/i);
if (hex) return hex[0].slice(0, 7);
return hash.slice(0, 7);
}
function laneIssueTitle(lane, issueTitles) {
const plain = lane.issueId?.replace(/^issue:/, "");
if (!plain) return void 0;
return issueTitles?.[plain] ?? issueTitles?.[`issue:${plain}`];
}
function assignLaneIndices(lanes) {
const map = /* @__PURE__ */ new Map();
const sorted = [...lanes].sort((a, b) => {
const ac = a.createdAt || "";
const bc = b.createdAt || "";
if (ac !== bc) return ac.localeCompare(bc);
return a.id.localeCompare(b.id);
});
let nextRoot = 1;
for (const lane of sorted) {
if (lane.parentLaneId && map.has(lane.parentLaneId)) {
const parentCol = map.get(lane.parentLaneId);
if (lane.forkKind === "child") {
map.set(lane.id, Math.min(parentCol + 1, 3));
} else {
map.set(lane.id, Math.min(nextRoot, 3));
nextRoot = Math.min(nextRoot + 1, 3);
}
} else {
map.set(lane.id, Math.min(nextRoot, 3));
nextRoot = Math.min(nextRoot + 1, 3);
}
}
return map;
}
function buildCausalGraphFromInputs(input) {
const laneCols = assignLaneIndices(input.lanes);
const events = [];
for (const m of input.milestones) {
const mid = m.id.replace(/^milestone:/, "");
events.push({
id: `milestone:${mid}`,
kind: "milestone",
lane: 0,
time: m.createdAt || "",
message: m.message || mid,
hash: shortHash(m.toOpHash),
author: m.createdBy,
tags: [mid]
});
}
for (const lane of input.lanes) {
const col = laneCols.get(lane.id) ?? 1;
const title = laneIssueTitle(lane, input.issueTitles);
const shortId = lane.id.slice(0, 13) + (lane.id.length > 13 ? "\u2026" : "");
if (lane.status === "promoted") {
events.push({
id: `fork:${lane.id}`,
kind: "fork",
lane: col,
laneId: lane.id,
time: lane.createdAt,
message: title || lane.name || `Fork ${shortId}`,
hash: shortHash(lane.baseOpHash),
author: lane.agentId
});
events.push({
id: `head:${lane.id}`,
kind: "head",
lane: col,
laneId: lane.id,
time: lane.updatedAt || lane.createdAt,
message: title || lane.name || `Lane ${shortId}`,
hash: shortHash(lane.headOpHash),
author: lane.agentId,
branches: [lane.targetBranch || shortId]
});
events.push({
id: `promote:${lane.id}`,
kind: "promote",
lane: 0,
laneId: lane.id,
forkLaneCol: col,
time: lane.updatedAt || lane.createdAt,
message: `Promote ${shortId}${title ? ` \u2014 ${title}` : ""}`,
hash: shortHash(lane.headOpHash),
author: lane.agentId,
branches: [input.integrationBranch]
});
} else if (lane.status === "active") {
events.push({
id: `head:${lane.id}`,
kind: "head",
lane: col,
laneId: lane.id,
time: lane.updatedAt || lane.createdAt,
message: title || lane.name || `Lane ${shortId}`,
hash: shortHash(lane.headOpHash),
author: lane.agentId,
branches: [lane.targetBranch || shortId]
});
} else {
events.push({
id: `fork:${lane.id}`,
kind: "fork",
lane: col,
laneId: lane.id,
time: lane.createdAt,
message: title || lane.name || `Fork ${shortId}`,
hash: shortHash(lane.baseOpHash),
author: lane.agentId
});
}
}
events.sort((a, b) => {
if (a.time !== b.time) return a.time.localeCompare(b.time);
return a.id.localeCompare(b.id);
});
let lastTrunk = null;
const lastOnLaneCol = /* @__PURE__ */ new Map();
const wired = [];
for (const ev of events) {
const parents = [];
if (ev.kind === "promote") {
if (lastTrunk) parents.push(lastTrunk);
const forkCol = ev.forkLaneCol ?? 1;
const forkHead = lastOnLaneCol.get(forkCol);
if (forkHead) parents.push(forkHead);
else if (ev.laneId) {
const forkId = `head:${ev.laneId}`;
if (wired.some((n) => n.id === forkId)) parents.push(forkId);
else {
const forkEvId = `fork:${ev.laneId}`;
if (wired.some((n) => n.id === forkEvId)) parents.push(forkEvId);
}
}
} else if (ev.lane === 0) {
if (lastTrunk) parents.push(lastTrunk);
lastTrunk = ev.id;
} else {
if (lastTrunk) parents.push(lastTrunk);
const prev = lastOnLaneCol.get(ev.lane);
if (prev && ev.kind === "head") {
parents.length = 0;
parents.push(prev);
}
lastOnLaneCol.set(ev.lane, ev.id);
}
const laneMeta = ev.laneId ? input.lanes.find((l) => l.id === ev.laneId) : void 0;
wired.push({
id: ev.id,
hash: ev.hash,
message: ev.message,
lane: ev.lane,
parents,
branches: ev.branches,
tags: ev.tags,
author: ev.author,
date: ev.time,
kind: ev.kind,
laneId: ev.laneId,
active: laneMeta?.status === "active"
});
}
const commits = wired.reverse();
const activeLanes = input.lanes.filter((l) => l.status === "active");
const activeForkCount = activeLanes.length;
return {
at: (/* @__PURE__ */ new Date()).toISOString(),
integrationBranch: input.integrationBranch,
commits,
activeLaneIds: activeLanes.map((l) => l.id),
stats: {
eventCount: commits.length,
activeForkCount
}
};
}
function buildCausalGraphSnapshot(engine) {
const savedLaneId = engine.getActiveLaneId();
const savedLaneLog = engine.activeLaneLog;
engine.activeLaneId = void 0;
engine.activeLaneLog = null;
engine.open();
const lanes = engine.listLanes();
const milestones = engine.listMilestones();
const issueTitles = {};
for (const issue of engine.listIssues()) {
if (issue.title) issueTitles[issue.id] = issue.title;
}
if (savedLaneId) {
engine.activeLaneId = savedLaneId;
engine.activeLaneLog = savedLaneLog;
engine.open();
}
const integrationBranch = engine.getCurrentBranch?.() ?? "main";
return buildCausalGraphFromInputs({
integrationBranch,
lanes,
milestones,
issueTitles
});
}
// src/ui/lanes-dashboard.ts
init_canonical_op();
init_resolve_runtime_theme_css();
// src/ui/ui-dev.ts
import { existsSync, mkdirSync, watch as fsWatch } from "fs";
import { join as join2, dirname } from "path";
import { fileURLToPath } from "url";
var UI_DEV_SUBDIR = "ui-dev";
var UI_DEV_ENTRIES = ["tml-runtime.ts", "admin-datatable.ts", "admin-shell.ts"];
var LIVE_RELOAD_CLIENT = `(() => {
const es = new EventSource('/__dev/reload');
es.addEventListener('css', () => {
for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
const href = link.getAttribute('href');
if (!href || href.startsWith('http')) continue;
const base = href.split('?')[0];
link.setAttribute('href', base + '?v=' + Date.now());
}
});
es.addEventListener('reload', () => location.reload());
es.addEventListener('html', () => location.reload());
es.onerror = () => {};
})();
`;
function liveReloadClientSource() {
return LIVE_RELOAD_CLIENT;
}
function uiDevOutDir(rootPath) {
return join2(rootPath, ".trellis", UI_DEV_SUBDIR);
}
function findUiAsset(name) {
const candidates = [];
try {
const moduleDir = dirname(fileURLToPath(import.meta.url));
candidates.push(join2(moduleDir, name));
candidates.push(join2(moduleDir, "..", "ui", name));
} catch {
}
let cwd = process.cwd();
for (let i = 0; i < 8; i++) {
candidates.push(join2(cwd, "src", "ui", name));
candidates.push(join2(cwd, "dist", "ui", name));
const parent = dirname(cwd);
if (parent === cwd) break;
cwd = parent;
}
for (const p of candidates) {
if (existsSync(p)) return p;
}
return null;
}
async function startUiDevWatch(rootPath, onReload) {
const outDir = uiDevOutDir(rootPath);
mkdirSync(outDir, { recursive: true });
const entryPoints = [];
for (const name of UI_DEV_ENTRIES) {
const tsPath = findUiAsset(name);
if (tsPath) entryPoints.push(tsPath);
}
if (entryPoints.length === 0) {
throw new Error("ui-dev: no TS entry points found under src/ui/");
}
const { context } = await import("./main-IB7F2LHH.js");
let debounce;
const notifyDebounced = (reason) => {
clearTimeout(debounce);
debounce = setTimeout(() => onReload(reason), 80);
};
const ctx = await context({
entryPoints,
bundle: true,
format: "esm",
target: "es2020",
platform: "browser",
sourcemap: "inline",
outdir: outDir,
logLevel: "warning",
plugins: [
{
name: "ui-dev-reload",
setup(build) {
build.onEnd((result) => {
if (result.errors.length === 0) notifyDebounced("reload");
});
}
}
]
});
await ctx.watch();
await ctx.rebuild();
const watchers = [];
const watchFile = (path, reason) => {
if (!existsSync(path)) return;
watchers.push(fsWatch(path, () => notifyDebounced(reason)));
};
const adminHtml = findUiAsset("admin.html");
if (adminHtml) watchFile(adminHtml, "html");
const datatableCss = findUiAsset("admin-datatable.css");
if (datatableCss) watchFile(datatableCss, "css");
const themeCss = findUiAsset("theme/runtime-theme.css");
if (themeCss) watchFile(themeCss, "css");
return {
outDir,
notify: onReload,
stop: async () => {
clearTimeout(debounce);
for (const w of watchers) w.close();
await ctx.dispose();
}
};
}
// src/ui/lanes-dashboard.ts
function findLanesHtml() {
const candidates = [];
const push = (p) => {
if (!candidates.includes(p)) candidates.push(p);
};
try {
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
push(join3(moduleDir, "lanes.html"));
push(join3(moduleDir, "..", "ui", "lanes.html"));
} catch {
}
let cwd = process.cwd();
for (let i = 0; i < 8; i++) {
push(join3(cwd, "src", "ui", "lanes.html"));
push(join3(cwd, "dist", "ui", "lanes.html"));
const parent = dirname2(cwd);
if (parent === cwd) break;
cwd = parent;
}
for (const p of candidates) {
if (existsSync2(p)) return p;
}
throw new Error("Could not find lanes.html \u2014 run from trellis-node or build dist.");
}
function findUiAsset2(name) {
const candidates = [];
try {
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
candidates.push(join3(moduleDir, name));
candidates.push(join3(moduleDir, "..", "ui", name));
} catch {
}
let cwd = process.cwd();
for (let i = 0; i < 8; i++) {
candidates.push(join3(cwd, "src", "ui", name));
candidates.push(join3(cwd, "dist", "ui", name));
const parent = dirname2(cwd);
if (parent === cwd) break;
cwd = parent;
}
for (const p of candidates) {
if (existsSync2(p)) return p;
}
return null;
}
function resolveDevMode(opts) {
if (opts.dev === true) return true;
if (opts.dev === false) return false;
const env = process.env.TRELLIS_UI_DEV?.trim().toLowerCase();
return env === "1" || env === "true" || env === "yes";
}
async function bundleUiModule(tsPath) {
const { build } = await import("./main-IB7F2LHH.js");
const out = await build({
entryPoints: [tsPath],
bundle: true,
write: false,
format: "esm",
target: "es2020",
platform: "browser",
minify: false
});
return out.outputFiles[0].text;
}
function readDevBundle(outDir, jsName) {
const p = join3(outDir, jsName);
if (!existsSync2(p)) return null;
return readFileSync(p, "utf-8");
}
function injectDevLiveReload(html) {
const tag = '<script type="module" src="/__dev/live-reload.js"></script>';
if (html.includes("/__dev/live-reload.js")) return html;
return html.replace("</body>", ` ${tag}
</body>`);
}
async function startLanesDashboard(opts) {
const engine = new TrellisVcsEngine({ rootPath: opts.rootPath, provenance: PROVENANCE.http });
engine.open();
const trellisDir = join3(opts.rootPath, ".trellis");
const uiDev = resolveDevMode(opts);
const devOutDir = uiDevOutDir(opts.rootPath);
const reloadClients = /* @__PURE__ */ new Set();
const broadcastReload = (reason) => {
for (const send of reloadClients) {
try {
send(reason);
} catch {
reloadClients.delete(send);
}
}
};
let uiDevHandle = null;
if (uiDev) {
uiDevHandle = await startUiDevWatch(opts.rootPath, broadcastReload);
}
const lanesHtmlPath = findLanesHtml();
const readHtml = () => readFileSync(lanesHtmlPath, "utf-8");
const pollMs = opts.pollMs ?? 1e3;
const requestedPort = opts.port ?? 3939;
let boundPort = requestedPort;
let viewers = 0;
const headers = {
"Access-Control-Allow-Origin": "*",
"Cache-Control": "no-store"
};
const snapshot = () => buildLanesSnapshot(engine, opts.rootPath, { port: boundPort, viewers });
const serveBundledJs = async (jsName, tsName) => {
if (uiDev) {
const cached = readDevBundle(devOutDir, jsName);
if (cached != null) {
return new Response(cached, {
headers: {
...headers,
"Content-Type": "text/javascript; charset=utf-8",
"Cache-Control": "no-cache"
}
});
}
}
const tsPath = findUiAsset2(tsName);
if (!tsPath) {
return new Response(`${tsName} not found \u2014 run from trellis-node.`, {
status: 404,
headers
});
}
try {
const text = await bundleUiModule(tsPath);
return new Response(text, {
headers: {
...headers,
"Content-Type": "text/javascript; charset=utf-8",
"Cache-Control": "no-cache"
}
});
} catch (err) {
return new Response(`${basename(tsName, ".ts")} build failed: ${String(err)}`, {
status: 500,
headers
});
}
};
const fetchHandler = async (req) => {
const url = new URL(req.url);
const path = url.pathname;
if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers });
}
if (uiDev && path === "/__dev/live-reload.js") {
return new Response(liveReloadClientSource(), {
headers: {
...headers,
"Content-Type": "text/javascript; charset=utf-8",
"Cache-Control": "no-cache"
}
});
}
if (uiDev && path === "/__dev/reload") {
const stream = new ReadableStream({
start(controller) {
const enc = new TextEncoder();
const send = (reason) => {
controller.enqueue(enc.encode(`event: ${reason}
data: ${reason}
`));
};
reloadClients.add(send);
req.signal.addEventListener("abort", () => {
reloadClients.delete(send);
try {
controller.close();
} catch {
}
});
}
});
return new Response(stream, {
headers: {
...headers,
"Content-Type": "text/event-stream",
Connection: "keep-alive",
"Cache-Control": "no-cache"
}
});
}
if (!uiDev && path.startsWith("/__dev/")) {
return new Response("Not Found", { status: 404, headers });
}
if (path === "/api/lanes") {
engine.open();
return Response.json(snapshot(), { headers });
}
if (path === "/api/causal-graph") {
engine.open();
return Response.json(buildCausalGraphSnapshot(engine), { headers });
}
const laneOpsMatch = path.match(/^\/api\/lanes\/([^/]+)\/ops$/);
if (laneOpsMatch) {
engine.open();
try {
const { ops, meta } = engine.summarizeLane(decodeURIComponent(laneOpsMatch[1]));
return Response.json(
{
laneId: meta.id,
agentId: meta.agentId,
issueId: meta.issueId,
ops: ops.map((op) => ({
hash: op.hash,
type: op.kind,
at: op.timestamp,
agentId: op.agentId,
laneId: op.laneId,
message: op.vcs?.message,
path: op.vcs?.filePath ?? op.vcs?.oldFilePath,
issueId: op.vcs?.issueId
}))
},
{ headers }
);
} catch {
return new Response("Not Found", { status: 404, headers });
}
}
if (path === "/api/lanes/stream") {
const since = url.searchParams.get("since") ?? req.headers.get("last-event-id") ?? void 0;
const wantOps = url.searchParams.get("events") !== "snapshot";
const countAsViewer = !wantOps;
const stream = new ReadableStream({
start(controller) {
const enc = new TextEncoder();
let closed = false;
let lastOpHash = since;
let sentInitial = false;
if (countAsViewer) viewers += 1;
const send = (event, data, id) => {
const idLine = id ? `id: ${id}
` : "";
controller.enqueue(
enc.encode(
`${idLine}event: ${event}
data: ${JSON.stringify(data)}
`
)
);
};
const opsSince = (all) => {
if (!lastOpHash) return all;
const idx = all.findIndex((o) => o.hash === lastOpHash);
return idx >= 0 ? all.slice(idx + 1) : all;
};
const push = () => {
if (closed) return;
try {
engine.open();
const all = engine.getOps();
const fresh = opsSince(all);
if (!fresh.length && sentInitial) return;
send("snapshot", snapshot());
if (wantOps) for (const op of fresh) send("op", op, op.hash);
if (all.length) lastOpHash = all[all.length - 1].hash;
sentInitial = true;
} catch (err) {
controller.enqueue(
enc.encode(
`event: error
data: ${JSON.stringify({ message: String(err) })}
`
)
);
}
};
push();
const timer = setInterval(push, pollMs);
req.signal.addEventListener("abort", () => {
closed = true;
if (countAsViewer) viewers = Math.max(0, viewers - 1);
clearInterval(timer);
try {
controller.close();
} catch {
}
});
}
});
return new Response(stream, {
headers: {
...headers,
"Content-Type": "text/event-stream",
Connection: "keep-alive"
}
});
}
if (path === "/lanes" || path === "/lanes.html") {
return new Response(readHtml(), {
headers: { ...headers, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" }
});
}
if (path === "/admin" || path === "/admin.html") {
return new Response(null, {
status: 302,
headers: {
...headers,
Location: `/${url.search}`
}
});
}
if (path === "/") {
const htmlPath = findUiAsset2("admin.html");
if (!htmlPath) {
return new Response("admin.html not found \u2014 run from trellis-node.", {
status: 404,
headers
});
}
let html = readFileSync(htmlPath, "utf-8");
if (uiDev) html = injectDevLiveReload(html);
return new Response(html, {
headers: { ...headers, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" }
});
}
if (path === "/logo.png") {
const logoPath = findUiAsset2("logo.png");
if (!logoPath) {
return new Response("logo.png not found", { status: 404, headers });
}
return new Response(readFileSync(logoPath), {
headers: {
...headers,
"Content-Type": "image/png",
"Cache-Control": "public, max-age=86400"
}
});
}
if (path === "/client" || path === "/client.html") {
const htmlPath = findUiAsset2("client.html");
if (!htmlPath) {
return new Response("client.html not found \u2014 run from trellis-node.", {
status: 404,
headers
});
}
return new Response(readFileSync(htmlPath, "utf-8"), {
headers: { ...headers, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" }
});
}
if (path === "/theme/runtime-theme.css") {
const cssPath = resolveRuntimeThemeCss(opts.rootPath);
if (!cssPath) {
return new Response("runtime-theme.css not found \u2014 run from trellis-node.", {
status: 404,
headers
});
}
return new Response(readFileSync(cssPath, "utf-8"), {
headers: {
...headers,
"Content-Type": "text/css; charset=utf-8",
"Cache-Control": "no-cache"
}
});
}
if (path === "/tml-lanes") {
const htmlPath = findUiAsset2("tml-lanes.html");
if (!htmlPath) {
return new Response("TML test page not found \u2014 build or run from trellis-node.", {
status: 404,
headers
});
}
return new Response(readFileSync(htmlPath, "utf-8"), {
headers: { ...headers, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" }
});
}
if (path === "/tml-runtime.js") {
return serveBundledJs("tml-runtime.js", "tml-runtime.ts");
}
if (path === "/admin-datatable.js") {
return serveBundledJs("admin-datatable.js", "admin-datatable.ts");
}
if (path === "/admin-shell.js") {
return serveBundledJs("admin-shell.js", "admin-shell.ts");
}
if (path === "/admin-causal-graph.js") {
return serveBundledJs("admin-causal-graph.js", "admin-causal-graph.ts");
}
if (path === "/admin-datatable.css") {
const cssPath = findUiAsset2("admin-datatable.css");
if (!cssPath) {
return new Response("admin-datatable.css not found \u2014 run from trellis-node.", {
status: 404,
headers
});
}
return new Response(readFileSync(cssPath, "utf-8"), {
headers: {
...headers,
"Content-Type": "text/css; charset=utf-8",
"Cache-Control": "no-cache"
}
});
}
if (path === "/api/tml-mutations" && req.method === "POST") {
let body;
try {
body = await req.json();
} catch {
return new Response(JSON.stringify({ error: "invalid json" }), {
status: 400,
headers
});
}
const { action, args } = body;
try {
if (action === "promote") {
await engine.promoteLane(String(args?.id), { dryRun: false });
} else if (action === "updateLaneMeta") {
const id = String(args?.id || "");
if (!id) {
return new Response(JSON.stringify({ error: "id required" }), {
status: 400,
headers
});
}
const meta = loadLaneMeta(trellisDir, id);
if (!meta) {
return new Response(JSON.stringify({ error: `unknown lane: ${id}` }), {
status: 400,
headers
});
}
if ("targetBranch" in (args || {})) {
const tb = args?.targetBranch;
if (typeof tb !== "string" || !tb.trim()) {
return new Response(JSON.stringify({ error: "targetBranch required" }), {
status: 400,
headers
});
}
meta.targetBranch = tb.trim();
}
if ("issueId" in (args || {})) {
const raw = args?.issueId;
if (raw == null || raw === "") {
delete meta.issueId;
} else {
const plain = String(raw).replace(/^issue:/, "").trim();
if (!/^TRL-\d+$/i.test(plain)) {
return new Response(JSON.stringify({ error: "invalid issueId" }), {
status: 400,
headers
});
}
const m = plain.match(/^trl-(\d+)$/i);
meta.issueId = m ? `issue:TRL-${m[1]}` : `issue:${plain}`;
}
}
meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
saveLaneMeta(trellisDir, meta);
} else {
return new Response(JSON.stringify({ error: `unknown action: ${action}` }), {
status: 400,
headers
});
}
return new Response(JSON.stringify({ ok: true }), { headers });
} catch (err) {
return new Response(JSON.stringify({ error: String(err) }), {
status: 400,
headers
});
}
}
return new Response("Not Found", { status: 404, headers });
};
const server = await startNodeServer({
port: requestedPort,
fetch: fetchHandler,
websocket: { open: () => {
}, message: () => {
}, close: () => {
} }
});
boundPort = server.port;
return {
port: server.port,
stop: () => {
void uiDevHandle?.stop();
server.stop();
}
};
}
export {
startLanesDashboard
};