webmux 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6959,13 +6959,13 @@ var require_public_api = __commonJS((exports) => {
6959
6959
 
6960
6960
  // backend/src/server.ts
6961
6961
  import { randomUUID as randomUUID3 } from "crypto";
6962
- import { join as join7, resolve as resolve9 } from "path";
6963
- import { mkdirSync } from "fs";
6962
+ import { join as join8, resolve as resolve9 } from "path";
6963
+ import { mkdirSync as mkdirSync2 } from "fs";
6964
6964
  import { networkInterfaces } from "os";
6965
6965
  // package.json
6966
6966
  var package_default = {
6967
6967
  name: "webmux",
6968
- version: "0.32.0",
6968
+ version: "0.34.0",
6969
6969
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
6970
6970
  type: "module",
6971
6971
  repository: {
@@ -10994,7 +10994,7 @@ var coerce = {
10994
10994
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
10995
10995
  };
10996
10996
  var NEVER = INVALID;
10997
- // node_modules/.bun/@ts-rest+core@3.52.1+596964f7fee2c930/node_modules/@ts-rest/core/index.esm.mjs
10997
+ // node_modules/.bun/@ts-rest+core@3.52.1+94e40505b11febf1/node_modules/@ts-rest/core/index.esm.mjs
10998
10998
  var isZodObjectStrict = (obj) => {
10999
10999
  return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
11000
11000
  };
@@ -11178,6 +11178,7 @@ var CreateWorktreeRequestSchema = exports_external.object({
11178
11178
  envOverrides: exports_external.record(exports_external.string()).optional(),
11179
11179
  createLinearTicket: exports_external.literal(true).optional(),
11180
11180
  linearTitle: exports_external.string().optional(),
11181
+ linearTeamKey: exports_external.string().trim().transform((value) => value.toUpperCase()).pipe(LinearTeamKeySchema).optional(),
11181
11182
  fromLinear: FromLinearInputSchema.optional(),
11182
11183
  source: WorktreeSourceSchema.optional(),
11183
11184
  oneshot: OneshotConfigSchema.optional()
@@ -11490,6 +11491,15 @@ var AgentIdParamsSchema = exports_external.object({
11490
11491
  var RunIdParamsSchema = exports_external.object({
11491
11492
  runId: NumberLikePathParamSchema
11492
11493
  });
11494
+ var InstanceSummarySchema = exports_external.object({
11495
+ prefix: exports_external.string(),
11496
+ port: exports_external.number(),
11497
+ projectDir: exports_external.string(),
11498
+ startedAt: exports_external.number()
11499
+ });
11500
+ var InstancesResponseSchema = exports_external.object({
11501
+ instances: exports_external.array(InstanceSummarySchema)
11502
+ });
11493
11503
 
11494
11504
  // packages/api-contract/src/contract.ts
11495
11505
  var c = initContract();
@@ -11525,7 +11535,8 @@ var apiPaths = {
11525
11535
  setAutoRemoveOnMerge: "/api/github/auto-remove-on-merge",
11526
11536
  pullMain: "/api/pull-main",
11527
11537
  fetchCiLogs: "/api/ci-logs/:runId",
11528
- dismissNotification: "/api/notifications/:id/dismiss"
11538
+ dismissNotification: "/api/notifications/:id/dismiss",
11539
+ fetchInstances: "/api/instances"
11529
11540
  };
11530
11541
  var commonErrorResponses = {
11531
11542
  400: ErrorResponseSchema,
@@ -11834,6 +11845,14 @@ var apiContract = c.router({
11834
11845
  400: ErrorResponseSchema,
11835
11846
  404: ErrorResponseSchema
11836
11847
  }
11848
+ },
11849
+ fetchInstances: {
11850
+ method: "GET",
11851
+ path: apiPaths.fetchInstances,
11852
+ responses: {
11853
+ 200: InstancesResponseSchema,
11854
+ 500: ErrorResponseSchema
11855
+ }
11837
11856
  }
11838
11857
  }, {
11839
11858
  strictStatusCodes: true
@@ -13279,12 +13298,7 @@ function parseProjectConfig(parsed) {
13279
13298
  linkedRepos: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord3(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : [],
13280
13299
  autoRemoveOnMerge: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) && typeof parsed.integrations.github.autoRemoveOnMerge === "boolean" ? parsed.integrations.github.autoRemoveOnMerge : DEFAULT_CONFIG.integrations.github.autoRemoveOnMerge
13281
13300
  },
13282
- linear: {
13283
- enabled: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled,
13284
- autoCreateWorktrees: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.autoCreateWorktrees === "boolean" ? parsed.integrations.linear.autoCreateWorktrees : DEFAULT_CONFIG.integrations.linear.autoCreateWorktrees,
13285
- createTicketOption: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.createTicketOption === "boolean" ? parsed.integrations.linear.createTicketOption : DEFAULT_CONFIG.integrations.linear.createTicketOption,
13286
- ...isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.teamId === "string" && parsed.integrations.linear.teamId.trim() ? { teamId: parsed.integrations.linear.teamId.trim() } : {}
13287
- }
13301
+ linear: parseLinearIntegration(parsed)
13288
13302
  },
13289
13303
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
13290
13304
  autoName: parseAutoName(parsed.auto_name),
@@ -13294,6 +13308,30 @@ function parseProjectConfig(parsed) {
13294
13308
  function defaultConfig() {
13295
13309
  return parseProjectConfig({});
13296
13310
  }
13311
+ function parseTeamKeyList(raw) {
13312
+ if (!Array.isArray(raw))
13313
+ return;
13314
+ const keys = raw.filter((entry) => typeof entry === "string").map((entry) => entry.trim().toUpperCase()).filter((entry) => entry.length > 0);
13315
+ return keys.length > 0 ? Array.from(new Set(keys)) : undefined;
13316
+ }
13317
+ var warnedLegacyLinearTeamId = false;
13318
+ function parseLinearIntegration(parsed) {
13319
+ const defaults = DEFAULT_CONFIG.integrations.linear;
13320
+ const linear = isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) ? parsed.integrations.linear : null;
13321
+ if (!linear)
13322
+ return { ...defaults };
13323
+ if (typeof linear.teamId === "string" && !warnedLegacyLinearTeamId) {
13324
+ warnedLegacyLinearTeamId = true;
13325
+ log.warn("[config] integrations.linear.teamId is no longer used \u2014 the ticket team is now picked at creation time in the dashboard");
13326
+ }
13327
+ const watchTeams = parseTeamKeyList(linear.watchTeams);
13328
+ return {
13329
+ enabled: typeof linear.enabled === "boolean" ? linear.enabled : defaults.enabled,
13330
+ autoCreateWorktrees: typeof linear.autoCreateWorktrees === "boolean" ? linear.autoCreateWorktrees : defaults.autoCreateWorktrees,
13331
+ createTicketOption: typeof linear.createTicketOption === "boolean" ? linear.createTicketOption : defaults.createTicketOption,
13332
+ ...watchTeams ? { watchTeams } : {}
13333
+ };
13334
+ }
13297
13335
  function parseLocalLinearOverlay(parsed) {
13298
13336
  if (!isRecord3(parsed.integrations))
13299
13337
  return null;
@@ -13307,8 +13345,9 @@ function parseLocalLinearOverlay(parsed) {
13307
13345
  overlay.autoCreateWorktrees = linear.autoCreateWorktrees;
13308
13346
  if (typeof linear.createTicketOption === "boolean")
13309
13347
  overlay.createTicketOption = linear.createTicketOption;
13310
- if (typeof linear.teamId === "string" && linear.teamId.trim())
13311
- overlay.teamId = linear.teamId.trim();
13348
+ const watchTeams = parseTeamKeyList(linear.watchTeams);
13349
+ if (watchTeams)
13350
+ overlay.watchTeams = watchTeams;
13312
13351
  return Object.keys(overlay).length > 0 ? overlay : null;
13313
13352
  }
13314
13353
  function parseLocalGitHubOverlay(parsed) {
@@ -15435,6 +15474,7 @@ class BunTmuxGateway {
15435
15474
  var INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
15436
15475
  var UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15437
15476
  var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
15477
+ var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/;
15438
15478
  function sanitizeBranchName(raw) {
15439
15479
  return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
15440
15480
  }
@@ -15447,6 +15487,26 @@ function isValidWorktreeName(name) {
15447
15487
  function isValidEnvKey(key) {
15448
15488
  return UNSAFE_ENV_KEY_RE.test(key);
15449
15489
  }
15490
+ var RESERVED_INSTANCE_PREFIXES = new Set(["api", "ws", "assets"]);
15491
+ function sanitizeInstancePrefix(raw) {
15492
+ return raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
15493
+ }
15494
+ function isValidInstancePrefix(value) {
15495
+ return VALID_INSTANCE_PREFIX_RE.test(value) && !RESERVED_INSTANCE_PREFIXES.has(value);
15496
+ }
15497
+ function deriveInstancePrefix(projectDir, takenPrefixes) {
15498
+ const basename3 = projectDir.replace(/\/+$/, "").split("/").pop() ?? "webmux";
15499
+ const base = sanitizeInstancePrefix(basename3) || "webmux";
15500
+ const taken = new Set([...takenPrefixes, ...RESERVED_INSTANCE_PREFIXES]);
15501
+ if (!taken.has(base))
15502
+ return base;
15503
+ for (let n = 2;n < 1000; n++) {
15504
+ const candidate = `${base}-${n}`;
15505
+ if (!taken.has(candidate))
15506
+ return candidate;
15507
+ }
15508
+ return `${base}-${Date.now()}`;
15509
+ }
15450
15510
  function allocateServicePorts(existingMetas, services) {
15451
15511
  const allocatable = services.filter((service) => service.portStart != null);
15452
15512
  if (allocatable.length === 0)
@@ -15498,7 +15558,7 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
15498
15558
  function buildBuiltInAgentInvocation(input) {
15499
15559
  const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
15500
15560
  if (input.agent === "codex") {
15501
- const hooksFlag = " --enable codex_hooks";
15561
+ const hooksFlag = " --enable hooks";
15502
15562
  const yoloFlag2 = input.yolo ? " --yolo" : "";
15503
15563
  if (input.launchMode === "resume") {
15504
15564
  return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
@@ -17336,22 +17396,29 @@ var AUTO_ONESHOT_LABEL = "webmux_oneshot";
17336
17396
  function hasLabel(issue, name) {
17337
17397
  return issue.labels.some((l) => l.name.toLowerCase() === name);
17338
17398
  }
17339
- function filterTriggerableIssues(issues, existingBranches, matchesLabelRule) {
17399
+ function matchesTeamFilter(issue, watchTeamKeys) {
17400
+ if (!watchTeamKeys || watchTeamKeys.length === 0)
17401
+ return true;
17402
+ return watchTeamKeys.includes(issue.team.key.toUpperCase());
17403
+ }
17404
+ function filterTriggerableIssues(issues, existingBranches, matchesLabelRule, watchTeamKeys) {
17340
17405
  return issues.filter((issue) => {
17341
17406
  if (issue.state.name !== "Todo")
17342
17407
  return false;
17343
17408
  if (!matchesLabelRule(issue))
17344
17409
  return false;
17410
+ if (!matchesTeamFilter(issue, watchTeamKeys))
17411
+ return false;
17345
17412
  if (processedIssueIds.has(issue.id))
17346
17413
  return false;
17347
17414
  return !existingBranches.some((branch) => branchMatchesIssue(branch, issue.branchName));
17348
17415
  });
17349
17416
  }
17350
- function filterAutoCreateIssues(issues, existingBranches) {
17351
- return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_CREATE_LABEL) && !hasLabel(issue, AUTO_ONESHOT_LABEL));
17417
+ function filterAutoCreateIssues(issues, existingBranches, watchTeamKeys) {
17418
+ return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_CREATE_LABEL) && !hasLabel(issue, AUTO_ONESHOT_LABEL), watchTeamKeys);
17352
17419
  }
17353
- function filterAutoOneshotIssues(issues, existingBranches) {
17354
- return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_ONESHOT_LABEL));
17420
+ function filterAutoOneshotIssues(issues, existingBranches, watchTeamKeys) {
17421
+ return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_ONESHOT_LABEL), watchTeamKeys);
17355
17422
  }
17356
17423
  async function runLinearAutoCreateOnce(deps) {
17357
17424
  const fetchIssues = deps.fetchIssues ?? fetchAssignedIssues;
@@ -17367,8 +17434,8 @@ async function runLinearAutoCreateOnce(deps) {
17367
17434
  }
17368
17435
  const projectRoot2 = deps.projectRoot;
17369
17436
  const existingBranches = deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch);
17370
- const oneshotIssues = deps.runOneshotForIssue ? filterAutoOneshotIssues(result.data, existingBranches) : [];
17371
- const createIssues = filterAutoCreateIssues(result.data, existingBranches);
17437
+ const oneshotIssues = deps.runOneshotForIssue ? filterAutoOneshotIssues(result.data, existingBranches, deps.watchTeamKeys) : [];
17438
+ const createIssues = filterAutoCreateIssues(result.data, existingBranches, deps.watchTeamKeys);
17372
17439
  if (oneshotIssues.length === 0 && createIssues.length === 0) {
17373
17440
  log.debug(`[linear-auto-create] no new labeled issues (${result.data.length} assigned, ${existingBranches.length} worktrees)`);
17374
17441
  return;
@@ -19280,6 +19347,125 @@ function createWebmuxRuntime(options = {}) {
19280
19347
  };
19281
19348
  }
19282
19349
 
19350
+ // backend/src/adapters/instance-registry.ts
19351
+ import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
19352
+ import { homedir } from "os";
19353
+ import { join as join7 } from "path";
19354
+ function defaultRegistryDir() {
19355
+ return join7(homedir(), ".webmux", "instances");
19356
+ }
19357
+ function isAlive(pid) {
19358
+ try {
19359
+ process.kill(pid, 0);
19360
+ return true;
19361
+ } catch (err2) {
19362
+ return err2?.code !== "ESRCH";
19363
+ }
19364
+ }
19365
+ function isInstanceEntry(value) {
19366
+ if (typeof value !== "object" || value === null)
19367
+ return false;
19368
+ const v = value;
19369
+ return typeof v.prefix === "string" && isValidInstancePrefix(v.prefix) && typeof v.port === "number" && typeof v.projectDir === "string" && typeof v.pid === "number" && typeof v.startedAt === "number";
19370
+ }
19371
+ function createInstanceRegistry(dir = defaultRegistryDir()) {
19372
+ function ensureDir() {
19373
+ mkdirSync(dir, { recursive: true });
19374
+ }
19375
+ function entryPath(port) {
19376
+ return join7(dir, `${port}.json`);
19377
+ }
19378
+ function readEntry(filename) {
19379
+ try {
19380
+ const raw = readFileSync2(join7(dir, filename), "utf8");
19381
+ const parsed = JSON.parse(raw);
19382
+ return isInstanceEntry(parsed) ? parsed : null;
19383
+ } catch {
19384
+ return null;
19385
+ }
19386
+ }
19387
+ return {
19388
+ register(entry) {
19389
+ ensureDir();
19390
+ const finalPath = entryPath(entry.port);
19391
+ const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
19392
+ const text = `${JSON.stringify(entry, null, 2)}
19393
+ `;
19394
+ writeFileSync(tmpPath, text);
19395
+ renameSync(tmpPath, finalPath);
19396
+ },
19397
+ deregister(port, expectedPid) {
19398
+ if (expectedPid !== undefined) {
19399
+ const filename = `${port}.json`;
19400
+ const entry = readEntry(filename);
19401
+ if (entry && entry.pid !== expectedPid) {
19402
+ return;
19403
+ }
19404
+ }
19405
+ try {
19406
+ unlinkSync(entryPath(port));
19407
+ } catch (err2) {
19408
+ const code = err2?.code;
19409
+ if (code !== "ENOENT") {
19410
+ log.debug(`[instance-registry] deregister(${port}) failed: ${String(err2)}`);
19411
+ }
19412
+ }
19413
+ },
19414
+ listLive() {
19415
+ let filenames;
19416
+ try {
19417
+ filenames = readdirSync2(dir).filter((name) => name.endsWith(".json"));
19418
+ } catch {
19419
+ return [];
19420
+ }
19421
+ const live = [];
19422
+ for (const filename of filenames) {
19423
+ const entry = readEntry(filename);
19424
+ if (!entry)
19425
+ continue;
19426
+ if (!isAlive(entry.pid)) {
19427
+ try {
19428
+ unlinkSync(join7(dir, filename));
19429
+ } catch {}
19430
+ continue;
19431
+ }
19432
+ live.push(entry);
19433
+ }
19434
+ return live;
19435
+ }
19436
+ };
19437
+ }
19438
+
19439
+ // backend/src/domain/peer-routing.ts
19440
+ function decidePeerRouting(pathname, peers, selfPort) {
19441
+ const firstSegment = pathname.split("/")[1];
19442
+ if (!firstSegment || !isValidInstancePrefix(firstSegment)) {
19443
+ return { kind: "passthrough" };
19444
+ }
19445
+ if (RESERVED_INSTANCE_PREFIXES.has(firstSegment)) {
19446
+ return { kind: "passthrough" };
19447
+ }
19448
+ const peer = peers.find((entry) => entry.prefix === firstSegment);
19449
+ if (!peer)
19450
+ return { kind: "passthrough" };
19451
+ const remaining = pathname.slice(firstSegment.length + 1) || "/";
19452
+ if (peer.port === selfPort) {
19453
+ return { kind: "rewrite", path: remaining };
19454
+ }
19455
+ return { kind: "redirect", port: peer.port, path: remaining };
19456
+ }
19457
+ function resolvePeerRedirect(url, peers, selfPort) {
19458
+ const decision = decidePeerRouting(url.pathname, peers, selfPort);
19459
+ if (decision.kind === "passthrough")
19460
+ return null;
19461
+ if (decision.kind === "rewrite") {
19462
+ url.pathname = decision.path;
19463
+ return null;
19464
+ }
19465
+ const location = `${url.protocol}//${url.hostname}:${decision.port}${decision.path}${url.search}`;
19466
+ return new Response(null, { status: 302, headers: { Location: location } });
19467
+ }
19468
+
19283
19469
  // backend/src/server.ts
19284
19470
  var PORT = parseInt(Bun.env.PORT || "5111", 10);
19285
19471
  var STATIC_DIR = Bun.env.WEBMUX_STATIC_DIR || "";
@@ -19339,11 +19525,13 @@ async function runOneshotForIssue(issueId) {
19339
19525
  function startLinearAutoCreate() {
19340
19526
  if (stopLinearAutoCreate)
19341
19527
  return;
19528
+ const watchTeamKeys = config.integrations.linear.watchTeams;
19342
19529
  stopLinearAutoCreate = startLinearAutoCreateMonitor({
19343
19530
  lifecycleService,
19344
19531
  git,
19345
19532
  projectRoot: PROJECT_DIR,
19346
- runOneshotForIssue
19533
+ runOneshotForIssue,
19534
+ ...watchTeamKeys && watchTeamKeys.length > 0 ? { watchTeamKeys } : {}
19347
19535
  });
19348
19536
  }
19349
19537
  function normalizeOneshotConfig(input) {
@@ -19875,6 +20063,7 @@ async function apiCreateWorktree(req) {
19875
20063
  const agents = body.agents;
19876
20064
  const createLinearTicket = body.createLinearTicket === true;
19877
20065
  const linearTitle = body.linearTitle?.trim() ? body.linearTitle.trim() : undefined;
20066
+ const linearTeamKey = body.linearTeamKey;
19878
20067
  const mode = body.mode;
19879
20068
  const selectedAgents = agents ? agents : agent ? [agent] : [config.workspace.defaultAgent];
19880
20069
  if (baseBranch && !isValidBranchName(baseBranch)) {
@@ -19932,14 +20121,17 @@ ${resolvedPrompt}` : conversationContext;
19932
20121
  if (!title) {
19933
20122
  return errorResponse("Linear ticket title could not be derived from the prompt", 400);
19934
20123
  }
19935
- const teamId = config.integrations.linear.teamId;
19936
- if (!teamId) {
19937
- return errorResponse("Linear teamId is not configured", 503);
20124
+ if (!linearTeamKey) {
20125
+ return errorResponse('Linear team is required to create a ticket. Provide `linearTeamKey` (e.g. "ENG").', 400);
20126
+ }
20127
+ const teamResult = await fetchTeamByKey(linearTeamKey);
20128
+ if (!teamResult.ok) {
20129
+ return errorResponse(teamResult.error, teamResult.status);
19938
20130
  }
19939
20131
  const linearResult = await createLinearIssue({
19940
20132
  title,
19941
20133
  description: resolvedPrompt ?? "",
19942
- teamId
20134
+ teamId: teamResult.data.id
19943
20135
  });
19944
20136
  if (!linearResult.ok) {
19945
20137
  return errorResponse(linearResult.error, 502);
@@ -20306,7 +20498,7 @@ async function apiUploadFiles(name, req) {
20306
20498
  if (entries.length === 0)
20307
20499
  return errorResponse("No files provided", 400);
20308
20500
  const uploadDir = `/tmp/webmux-uploads/${sanitizeFilename(name)}`;
20309
- mkdirSync(uploadDir, { recursive: true });
20501
+ mkdirSync2(uploadDir, { recursive: true });
20310
20502
  const results = [];
20311
20503
  for (const entry of entries) {
20312
20504
  if (!(entry instanceof File))
@@ -20318,7 +20510,7 @@ async function apiUploadFiles(name, req) {
20318
20510
  return errorResponse(`File too large: ${entry.name} (max 10MB)`, 400);
20319
20511
  }
20320
20512
  const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
20321
- const destPath = join7(uploadDir, safeName);
20513
+ const destPath = join8(uploadDir, safeName);
20322
20514
  if (!resolve9(destPath).startsWith(uploadDir + "/")) {
20323
20515
  return errorResponse("Invalid filename", 400);
20324
20516
  }
@@ -20377,368 +20569,446 @@ function parseAgentIdParam(params) {
20377
20569
  data: agentId
20378
20570
  };
20379
20571
  }
20380
- Bun.serve({
20381
- port: PORT,
20382
- idleTimeout: 255,
20383
- routes: {
20384
- [apiPaths.streamAgentsWorktreeConversation]: (req, server) => {
20385
- const branch = decodeURIComponent(req.params.name);
20386
- return server.upgrade(req, { data: { kind: "agents", branch, conversationId: null, unsubscribe: null } }) ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
20387
- },
20388
- "/ws/:worktree": (req, server) => {
20389
- const branch = decodeURIComponent(req.params.worktree);
20390
- return server.upgrade(req, {
20391
- data: { kind: "terminal", branch, worktreeId: null, attachId: null, attached: false }
20392
- }) ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
20393
- },
20394
- [apiPaths.fetchConfig]: {
20395
- GET: () => jsonResponse(getFrontendConfig())
20396
- },
20397
- [apiPaths.fetchAvailableBranches]: {
20398
- GET: (req) => catching("GET /api/branches", () => apiListBranches(req))
20399
- },
20400
- [apiPaths.fetchBaseBranches]: {
20401
- GET: () => catching("GET /api/base-branches", () => apiListBaseBranches())
20402
- },
20403
- [apiPaths.fetchProject]: {
20404
- GET: () => catching("GET /api/project", () => apiGetProject())
20405
- },
20406
- [apiPaths.fetchAgents]: {
20407
- GET: () => catching("GET /api/agents", () => apiListAgents()),
20408
- POST: (req) => catching("POST /api/agents", () => apiCreateAgent(req))
20409
- },
20410
- [apiPaths.validateAgent]: {
20411
- POST: (req) => catching("POST /api/agents/validate", () => apiValidateAgent(req))
20412
- },
20413
- [apiPaths.updateAgent]: {
20414
- PUT: (req) => {
20415
- const parsed = parseAgentIdParam(req.params);
20416
- if (!parsed.ok)
20417
- return parsed.response;
20418
- return catching("PUT /api/agents/:id", () => apiUpdateAgent(parsed.data, req));
20572
+ function startServer(port) {
20573
+ return Bun.serve({
20574
+ port,
20575
+ idleTimeout: 255,
20576
+ routes: {
20577
+ [apiPaths.streamAgentsWorktreeConversation]: (req, server) => {
20578
+ const branch = decodeURIComponent(req.params.name);
20579
+ return server.upgrade(req, { data: { kind: "agents", branch, conversationId: null, unsubscribe: null } }) ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
20419
20580
  },
20420
- DELETE: (req) => {
20421
- const parsed = parseAgentIdParam(req.params);
20422
- if (!parsed.ok)
20423
- return parsed.response;
20424
- return catching("DELETE /api/agents/:id", () => apiDeleteAgent(parsed.data));
20425
- }
20426
- },
20427
- [apiPaths.attachAgentsWorktreeConversation]: {
20428
- POST: (req) => {
20429
- const parsed = parseWorktreeNameParam(req.params);
20430
- if (!parsed.ok)
20431
- return parsed.response;
20432
- const name = parsed.data;
20433
- return catching(`POST ${apiPaths.attachAgentsWorktreeConversation}`, () => apiAttachAgentsWorktree(name));
20434
- }
20435
- },
20436
- [apiPaths.fetchAgentsWorktreeConversationHistory]: {
20437
- GET: (req) => {
20438
- const parsed = parseWorktreeNameParam(req.params);
20439
- if (!parsed.ok)
20440
- return parsed.response;
20441
- const name = parsed.data;
20442
- return catching(`GET ${apiPaths.fetchAgentsWorktreeConversationHistory}`, () => apiGetAgentsWorktreeHistory(name));
20443
- }
20444
- },
20445
- [apiPaths.sendAgentsWorktreeConversationMessage]: {
20446
- POST: (req) => {
20447
- const parsed = parseWorktreeNameParam(req.params);
20448
- if (!parsed.ok)
20449
- return parsed.response;
20450
- const name = parsed.data;
20451
- return catching(`POST ${apiPaths.sendAgentsWorktreeConversationMessage}`, () => apiSendAgentsWorktreeMessage(name, req));
20452
- }
20453
- },
20454
- [apiPaths.interruptAgentsWorktreeConversation]: {
20455
- POST: (req) => {
20456
- const parsed = parseWorktreeNameParam(req.params);
20457
- if (!parsed.ok)
20458
- return parsed.response;
20459
- const name = parsed.data;
20460
- return catching(`POST ${apiPaths.interruptAgentsWorktreeConversation}`, () => apiInterruptAgentsWorktree(name));
20461
- }
20462
- },
20463
- "/api/runtime/events": {
20464
- POST: (req) => catching("POST /api/runtime/events", () => apiRuntimeEvent(req))
20465
- },
20466
- [apiPaths.fetchWorktrees]: {
20467
- GET: () => catching("GET /api/worktrees", () => apiGetWorktrees()),
20468
- POST: (req) => catching("POST /api/worktrees", () => apiCreateWorktree(req))
20469
- },
20470
- [apiPaths.removeWorktree]: {
20471
- DELETE: (req) => {
20472
- const parsed = parseWorktreeNameParam(req.params);
20473
- if (!parsed.ok)
20474
- return parsed.response;
20475
- const name = parsed.data;
20476
- return catching(`DELETE /api/worktrees/${name}`, () => apiDeleteWorktree(name));
20477
- }
20478
- },
20479
- [apiPaths.openWorktree]: {
20480
- POST: (req) => {
20481
- const parsed = parseWorktreeNameParam(req.params);
20482
- if (!parsed.ok)
20483
- return parsed.response;
20484
- const name = parsed.data;
20485
- return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name, req));
20486
- }
20487
- },
20488
- "/api/worktrees/:name/terminal-launch": {
20489
- GET: (req) => {
20490
- const parsed = parseWorktreeNameParam(req.params);
20491
- if (!parsed.ok)
20492
- return parsed.response;
20493
- const name = parsed.data;
20494
- return catching(`GET /api/worktrees/${name}/terminal-launch`, () => apiGetNativeTerminalLaunch(name));
20495
- }
20496
- },
20497
- [apiPaths.closeWorktree]: {
20498
- POST: (req) => {
20499
- const parsed = parseWorktreeNameParam(req.params);
20500
- if (!parsed.ok)
20501
- return parsed.response;
20502
- const name = parsed.data;
20503
- return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
20504
- }
20505
- },
20506
- [apiPaths.setWorktreeArchived]: {
20507
- PUT: (req) => {
20508
- const parsed = parseWorktreeNameParam(req.params);
20509
- if (!parsed.ok)
20510
- return parsed.response;
20511
- const name = parsed.data;
20512
- return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
20513
- }
20514
- },
20515
- [apiPaths.postWorktreeToLinear]: {
20516
- POST: (req) => {
20517
- const parsed = parseWorktreeNameParam(req.params);
20518
- if (!parsed.ok)
20519
- return parsed.response;
20520
- const name = parsed.data;
20521
- return catching(`POST /api/worktrees/${name}/linear/post`, () => apiPostWorktreeToLinear(name, req));
20522
- }
20523
- },
20524
- [apiPaths.syncWorktreePrs]: {
20525
- POST: (req) => {
20526
- const parsed = parseWorktreeNameParam(req.params);
20527
- if (!parsed.ok)
20528
- return parsed.response;
20529
- const name = parsed.data;
20530
- return catching(`POST /api/worktrees/${name}/sync-prs`, () => apiSyncWorktreePrs(name));
20531
- }
20532
- },
20533
- [apiPaths.setWorktreeLabel]: {
20534
- PUT: (req) => {
20535
- const parsed = parseWorktreeNameParam(req.params);
20536
- if (!parsed.ok)
20537
- return parsed.response;
20538
- const name = parsed.data;
20539
- return catching(`PUT /api/worktrees/${name}/label`, () => apiSetWorktreeLabel(name, req));
20540
- }
20541
- },
20542
- [apiPaths.sendWorktreePrompt]: {
20543
- POST: (req) => {
20544
- const parsed = parseWorktreeNameParam(req.params);
20545
- if (!parsed.ok)
20546
- return parsed.response;
20547
- const name = parsed.data;
20548
- return catching(`POST /api/worktrees/${name}/send`, () => apiSendPrompt(name, req));
20549
- }
20550
- },
20551
- "/api/worktrees/:name/upload": {
20552
- POST: (req) => {
20553
- const parsed = parseWorktreeNameParam(req.params);
20554
- if (!parsed.ok)
20555
- return parsed.response;
20556
- const name = parsed.data;
20557
- return catching(`POST /api/worktrees/${name}/upload`, () => apiUploadFiles(name, req));
20558
- }
20559
- },
20560
- [apiPaths.mergeWorktree]: {
20561
- POST: (req) => {
20562
- const parsed = parseWorktreeNameParam(req.params);
20563
- if (!parsed.ok)
20564
- return parsed.response;
20565
- const name = parsed.data;
20566
- return catching(`POST /api/worktrees/${name}/merge`, () => apiMergeWorktree(name));
20567
- }
20568
- },
20569
- [apiPaths.fetchWorktreeDiff]: {
20570
- GET: (req) => {
20571
- const parsed = parseWorktreeNameParam(req.params);
20572
- if (!parsed.ok)
20573
- return parsed.response;
20574
- const name = parsed.data;
20575
- return catching(`GET /api/worktrees/${name}/diff`, () => apiGetWorktreeDiff(name));
20576
- }
20577
- },
20578
- [apiPaths.fetchLinearIssues]: {
20579
- GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
20580
- },
20581
- [apiPaths.setLinearAutoCreate]: {
20582
- PUT: (req) => catching("PUT /api/linear/auto-create", () => apiSetLinearAutoCreate(req))
20583
- },
20584
- [apiPaths.setAutoRemoveOnMerge]: {
20585
- PUT: (req) => catching("PUT /api/github/auto-remove-on-merge", () => apiSetAutoRemoveOnMerge(req))
20586
- },
20587
- [apiPaths.pullMain]: {
20588
- POST: (req) => catching("POST /api/pull-main", () => apiPullMain(req))
20589
- },
20590
- [apiPaths.fetchCiLogs]: {
20591
- GET: (req) => {
20592
- const parsed = parseRunIdParam(req.params);
20593
- if (!parsed.ok)
20594
- return parsed.response;
20595
- return catching(`GET /api/ci-logs/${parsed.data}`, () => apiCiLogs(parsed.data));
20581
+ "/ws/:worktree": (req, server) => {
20582
+ const branch = decodeURIComponent(req.params.worktree);
20583
+ return server.upgrade(req, {
20584
+ data: { kind: "terminal", branch, worktreeId: null, attachId: null, attached: false }
20585
+ }) ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
20586
+ },
20587
+ [apiPaths.fetchConfig]: {
20588
+ GET: () => jsonResponse(getFrontendConfig())
20589
+ },
20590
+ [apiPaths.fetchAvailableBranches]: {
20591
+ GET: (req) => catching("GET /api/branches", () => apiListBranches(req))
20592
+ },
20593
+ [apiPaths.fetchBaseBranches]: {
20594
+ GET: () => catching("GET /api/base-branches", () => apiListBaseBranches())
20595
+ },
20596
+ [apiPaths.fetchProject]: {
20597
+ GET: () => catching("GET /api/project", () => apiGetProject())
20598
+ },
20599
+ [apiPaths.fetchAgents]: {
20600
+ GET: () => catching("GET /api/agents", () => apiListAgents()),
20601
+ POST: (req) => catching("POST /api/agents", () => apiCreateAgent(req))
20602
+ },
20603
+ [apiPaths.validateAgent]: {
20604
+ POST: (req) => catching("POST /api/agents/validate", () => apiValidateAgent(req))
20605
+ },
20606
+ [apiPaths.updateAgent]: {
20607
+ PUT: (req) => {
20608
+ const parsed = parseAgentIdParam(req.params);
20609
+ if (!parsed.ok)
20610
+ return parsed.response;
20611
+ return catching("PUT /api/agents/:id", () => apiUpdateAgent(parsed.data, req));
20612
+ },
20613
+ DELETE: (req) => {
20614
+ const parsed = parseAgentIdParam(req.params);
20615
+ if (!parsed.ok)
20616
+ return parsed.response;
20617
+ return catching("DELETE /api/agents/:id", () => apiDeleteAgent(parsed.data));
20618
+ }
20619
+ },
20620
+ [apiPaths.attachAgentsWorktreeConversation]: {
20621
+ POST: (req) => {
20622
+ const parsed = parseWorktreeNameParam(req.params);
20623
+ if (!parsed.ok)
20624
+ return parsed.response;
20625
+ const name = parsed.data;
20626
+ return catching(`POST ${apiPaths.attachAgentsWorktreeConversation}`, () => apiAttachAgentsWorktree(name));
20627
+ }
20628
+ },
20629
+ [apiPaths.fetchAgentsWorktreeConversationHistory]: {
20630
+ GET: (req) => {
20631
+ const parsed = parseWorktreeNameParam(req.params);
20632
+ if (!parsed.ok)
20633
+ return parsed.response;
20634
+ const name = parsed.data;
20635
+ return catching(`GET ${apiPaths.fetchAgentsWorktreeConversationHistory}`, () => apiGetAgentsWorktreeHistory(name));
20636
+ }
20637
+ },
20638
+ [apiPaths.sendAgentsWorktreeConversationMessage]: {
20639
+ POST: (req) => {
20640
+ const parsed = parseWorktreeNameParam(req.params);
20641
+ if (!parsed.ok)
20642
+ return parsed.response;
20643
+ const name = parsed.data;
20644
+ return catching(`POST ${apiPaths.sendAgentsWorktreeConversationMessage}`, () => apiSendAgentsWorktreeMessage(name, req));
20645
+ }
20646
+ },
20647
+ [apiPaths.interruptAgentsWorktreeConversation]: {
20648
+ POST: (req) => {
20649
+ const parsed = parseWorktreeNameParam(req.params);
20650
+ if (!parsed.ok)
20651
+ return parsed.response;
20652
+ const name = parsed.data;
20653
+ return catching(`POST ${apiPaths.interruptAgentsWorktreeConversation}`, () => apiInterruptAgentsWorktree(name));
20654
+ }
20655
+ },
20656
+ "/api/runtime/events": {
20657
+ POST: (req) => catching("POST /api/runtime/events", () => apiRuntimeEvent(req))
20658
+ },
20659
+ [apiPaths.fetchWorktrees]: {
20660
+ GET: () => catching("GET /api/worktrees", () => apiGetWorktrees()),
20661
+ POST: (req) => catching("POST /api/worktrees", () => apiCreateWorktree(req))
20662
+ },
20663
+ [apiPaths.removeWorktree]: {
20664
+ DELETE: (req) => {
20665
+ const parsed = parseWorktreeNameParam(req.params);
20666
+ if (!parsed.ok)
20667
+ return parsed.response;
20668
+ const name = parsed.data;
20669
+ return catching(`DELETE /api/worktrees/${name}`, () => apiDeleteWorktree(name));
20670
+ }
20671
+ },
20672
+ [apiPaths.openWorktree]: {
20673
+ POST: (req) => {
20674
+ const parsed = parseWorktreeNameParam(req.params);
20675
+ if (!parsed.ok)
20676
+ return parsed.response;
20677
+ const name = parsed.data;
20678
+ return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name, req));
20679
+ }
20680
+ },
20681
+ "/api/worktrees/:name/terminal-launch": {
20682
+ GET: (req) => {
20683
+ const parsed = parseWorktreeNameParam(req.params);
20684
+ if (!parsed.ok)
20685
+ return parsed.response;
20686
+ const name = parsed.data;
20687
+ return catching(`GET /api/worktrees/${name}/terminal-launch`, () => apiGetNativeTerminalLaunch(name));
20688
+ }
20689
+ },
20690
+ [apiPaths.closeWorktree]: {
20691
+ POST: (req) => {
20692
+ const parsed = parseWorktreeNameParam(req.params);
20693
+ if (!parsed.ok)
20694
+ return parsed.response;
20695
+ const name = parsed.data;
20696
+ return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
20697
+ }
20698
+ },
20699
+ [apiPaths.setWorktreeArchived]: {
20700
+ PUT: (req) => {
20701
+ const parsed = parseWorktreeNameParam(req.params);
20702
+ if (!parsed.ok)
20703
+ return parsed.response;
20704
+ const name = parsed.data;
20705
+ return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
20706
+ }
20707
+ },
20708
+ [apiPaths.postWorktreeToLinear]: {
20709
+ POST: (req) => {
20710
+ const parsed = parseWorktreeNameParam(req.params);
20711
+ if (!parsed.ok)
20712
+ return parsed.response;
20713
+ const name = parsed.data;
20714
+ return catching(`POST /api/worktrees/${name}/linear/post`, () => apiPostWorktreeToLinear(name, req));
20715
+ }
20716
+ },
20717
+ [apiPaths.syncWorktreePrs]: {
20718
+ POST: (req) => {
20719
+ const parsed = parseWorktreeNameParam(req.params);
20720
+ if (!parsed.ok)
20721
+ return parsed.response;
20722
+ const name = parsed.data;
20723
+ return catching(`POST /api/worktrees/${name}/sync-prs`, () => apiSyncWorktreePrs(name));
20724
+ }
20725
+ },
20726
+ [apiPaths.setWorktreeLabel]: {
20727
+ PUT: (req) => {
20728
+ const parsed = parseWorktreeNameParam(req.params);
20729
+ if (!parsed.ok)
20730
+ return parsed.response;
20731
+ const name = parsed.data;
20732
+ return catching(`PUT /api/worktrees/${name}/label`, () => apiSetWorktreeLabel(name, req));
20733
+ }
20734
+ },
20735
+ [apiPaths.sendWorktreePrompt]: {
20736
+ POST: (req) => {
20737
+ const parsed = parseWorktreeNameParam(req.params);
20738
+ if (!parsed.ok)
20739
+ return parsed.response;
20740
+ const name = parsed.data;
20741
+ return catching(`POST /api/worktrees/${name}/send`, () => apiSendPrompt(name, req));
20742
+ }
20743
+ },
20744
+ "/api/worktrees/:name/upload": {
20745
+ POST: (req) => {
20746
+ const parsed = parseWorktreeNameParam(req.params);
20747
+ if (!parsed.ok)
20748
+ return parsed.response;
20749
+ const name = parsed.data;
20750
+ return catching(`POST /api/worktrees/${name}/upload`, () => apiUploadFiles(name, req));
20751
+ }
20752
+ },
20753
+ [apiPaths.mergeWorktree]: {
20754
+ POST: (req) => {
20755
+ const parsed = parseWorktreeNameParam(req.params);
20756
+ if (!parsed.ok)
20757
+ return parsed.response;
20758
+ const name = parsed.data;
20759
+ return catching(`POST /api/worktrees/${name}/merge`, () => apiMergeWorktree(name));
20760
+ }
20761
+ },
20762
+ [apiPaths.fetchWorktreeDiff]: {
20763
+ GET: (req) => {
20764
+ const parsed = parseWorktreeNameParam(req.params);
20765
+ if (!parsed.ok)
20766
+ return parsed.response;
20767
+ const name = parsed.data;
20768
+ return catching(`GET /api/worktrees/${name}/diff`, () => apiGetWorktreeDiff(name));
20769
+ }
20770
+ },
20771
+ [apiPaths.fetchLinearIssues]: {
20772
+ GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
20773
+ },
20774
+ [apiPaths.setLinearAutoCreate]: {
20775
+ PUT: (req) => catching("PUT /api/linear/auto-create", () => apiSetLinearAutoCreate(req))
20776
+ },
20777
+ [apiPaths.setAutoRemoveOnMerge]: {
20778
+ PUT: (req) => catching("PUT /api/github/auto-remove-on-merge", () => apiSetAutoRemoveOnMerge(req))
20779
+ },
20780
+ [apiPaths.pullMain]: {
20781
+ POST: (req) => catching("POST /api/pull-main", () => apiPullMain(req))
20782
+ },
20783
+ [apiPaths.fetchCiLogs]: {
20784
+ GET: (req) => {
20785
+ const parsed = parseRunIdParam(req.params);
20786
+ if (!parsed.ok)
20787
+ return parsed.response;
20788
+ return catching(`GET /api/ci-logs/${parsed.data}`, () => apiCiLogs(parsed.data));
20789
+ }
20790
+ },
20791
+ "/api/notifications/stream": {
20792
+ GET: () => runtimeNotifications.stream()
20793
+ },
20794
+ [apiPaths.dismissNotification]: {
20795
+ POST: (req) => {
20796
+ const parsed = parseNotificationIdParam(req.params);
20797
+ if (!parsed.ok)
20798
+ return parsed.response;
20799
+ const id = parsed.data;
20800
+ if (!runtimeNotifications.dismiss(id))
20801
+ return errorResponse("Not found", 404);
20802
+ return jsonResponse({ ok: true });
20803
+ }
20804
+ },
20805
+ [apiPaths.fetchInstances]: {
20806
+ GET: () => jsonResponse({
20807
+ instances: instanceRegistry.listLive().filter((entry) => entry.port !== BOUND_PORT).map((entry) => ({
20808
+ prefix: entry.prefix,
20809
+ port: entry.port,
20810
+ projectDir: entry.projectDir,
20811
+ startedAt: entry.startedAt
20812
+ }))
20813
+ })
20596
20814
  }
20597
20815
  },
20598
- "/api/notifications/stream": {
20599
- GET: () => runtimeNotifications.stream()
20600
- },
20601
- [apiPaths.dismissNotification]: {
20602
- POST: (req) => {
20603
- const parsed = parseNotificationIdParam(req.params);
20604
- if (!parsed.ok)
20605
- return parsed.response;
20606
- const id = parsed.data;
20607
- if (!runtimeNotifications.dismiss(id))
20608
- return errorResponse("Not found", 404);
20609
- return jsonResponse({ ok: true });
20610
- }
20611
- }
20612
- },
20613
- async fetch(req) {
20614
- const url = new URL(req.url);
20615
- if (STATIC_DIR) {
20616
- const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
20617
- const filePath = join7(STATIC_DIR, rawPath);
20618
- const staticRoot = resolve9(STATIC_DIR);
20619
- if (!resolve9(filePath).startsWith(staticRoot + "/")) {
20620
- return new Response("Forbidden", { status: 403 });
20621
- }
20622
- const file = Bun.file(filePath);
20623
- if (await file.exists()) {
20624
- const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
20625
- return new Response(file, { headers });
20626
- }
20627
- return new Response(Bun.file(join7(STATIC_DIR, "index.html")), {
20628
- headers: { "Cache-Control": "no-cache" }
20629
- });
20630
- }
20631
- return new Response("Not Found", { status: 404 });
20632
- },
20633
- websocket: {
20634
- idleTimeout: 255,
20635
- sendPings: true,
20636
- data: {},
20637
- open(ws) {
20638
- const data = ws.data;
20639
- if (data.kind === "terminal") {
20640
- log.debug(`[ws] open branch=${data.branch}`);
20641
- return;
20816
+ async fetch(req) {
20817
+ const url = new URL(req.url);
20818
+ const peerRedirect = resolvePeerRedirect(url, instanceRegistry.listLive(), BOUND_PORT);
20819
+ if (peerRedirect)
20820
+ return peerRedirect;
20821
+ if (STATIC_DIR) {
20822
+ const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
20823
+ const filePath = join8(STATIC_DIR, rawPath);
20824
+ const staticRoot = resolve9(STATIC_DIR);
20825
+ if (!resolve9(filePath).startsWith(staticRoot + "/")) {
20826
+ return new Response("Forbidden", { status: 403 });
20827
+ }
20828
+ const file = Bun.file(filePath);
20829
+ if (await file.exists()) {
20830
+ const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
20831
+ return new Response(file, { headers });
20832
+ }
20833
+ return new Response(Bun.file(join8(STATIC_DIR, "index.html")), {
20834
+ headers: { "Cache-Control": "no-cache" }
20835
+ });
20642
20836
  }
20643
- log.debug(`[ws:agents] open branch=${data.branch}`);
20644
- openAgentsSocket(ws, data);
20837
+ return new Response("Not Found", { status: 404 });
20645
20838
  },
20646
- async message(ws, message) {
20647
- const data = ws.data;
20648
- if (data.kind === "agents") {
20649
- log.debug(`[ws:agents] ignoring inbound message branch=${data.branch}`);
20650
- return;
20651
- }
20652
- const msg = parseWsMessage(message);
20653
- if (!msg) {
20654
- sendWs(ws, { type: "error", message: "malformed message" });
20655
- return;
20656
- }
20657
- const { branch } = data;
20658
- switch (msg.type) {
20659
- case "input": {
20660
- const attachId = getAttachedSessionId(data, ws);
20661
- if (!attachId)
20662
- return;
20663
- if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20664
- disarmOneshotIfArmed(branch, "terminal-ws-input");
20665
- }
20666
- write(attachId, msg.data);
20667
- break;
20839
+ websocket: {
20840
+ idleTimeout: 255,
20841
+ sendPings: true,
20842
+ data: {},
20843
+ open(ws) {
20844
+ const data = ws.data;
20845
+ if (data.kind === "terminal") {
20846
+ log.debug(`[ws] open branch=${data.branch}`);
20847
+ return;
20668
20848
  }
20669
- case "sendKeys": {
20670
- const attachId = getAttachedSessionId(data, ws);
20671
- if (!attachId)
20672
- return;
20673
- if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20674
- disarmOneshotIfArmed(branch, "terminal-ws-send-keys");
20675
- }
20676
- await sendKeys(attachId, msg.hexBytes);
20677
- break;
20849
+ log.debug(`[ws:agents] open branch=${data.branch}`);
20850
+ openAgentsSocket(ws, data);
20851
+ },
20852
+ async message(ws, message) {
20853
+ const data = ws.data;
20854
+ if (data.kind === "agents") {
20855
+ log.debug(`[ws:agents] ignoring inbound message branch=${data.branch}`);
20856
+ return;
20857
+ }
20858
+ const msg = parseWsMessage(message);
20859
+ if (!msg) {
20860
+ sendWs(ws, { type: "error", message: "malformed message" });
20861
+ return;
20678
20862
  }
20679
- case "selectPane":
20680
- {
20863
+ const { branch } = data;
20864
+ switch (msg.type) {
20865
+ case "input": {
20681
20866
  const attachId = getAttachedSessionId(data, ws);
20682
20867
  if (!attachId)
20683
20868
  return;
20684
- log.debug(`[ws] selectPane pane=${msg.pane} branch=${branch} attachId=${attachId}`);
20685
- await selectPane(attachId, msg.pane);
20686
- }
20687
- break;
20688
- case "resize":
20689
- if (!data.attached) {
20690
- data.attached = true;
20691
- log.debug(`[ws] first resize (attaching) branch=${branch} cols=${msg.cols} rows=${msg.rows}`);
20692
- try {
20693
- if (msg.initialPane !== undefined) {
20694
- log.debug(`[ws] initialPane=${msg.initialPane} branch=${branch}`);
20695
- }
20696
- const terminalWorktree = await resolveTerminalWorktree(branch);
20697
- const attachId = `${terminalWorktree.worktreeId}:${randomUUID3()}`;
20698
- data.worktreeId = terminalWorktree.worktreeId;
20699
- data.attachId = attachId;
20700
- await attach(attachId, terminalWorktree.attachTarget, msg.cols, msg.rows, msg.initialPane);
20701
- const { onData, onExit } = makeCallbacks(ws);
20702
- setCallbacks(attachId, onData, onExit);
20703
- const scrollback = getScrollback(attachId);
20704
- log.debug(`[ws] attached branch=${branch} worktreeId=${terminalWorktree.worktreeId} attachId=${attachId} scrollback=${scrollback.length} bytes`);
20705
- if (scrollback.length > 0) {
20706
- sendWs(ws, { type: "scrollback", data: scrollback });
20707
- }
20708
- } catch (err2) {
20709
- const errMsg = err2 instanceof Error ? err2.message : String(err2);
20710
- data.attached = false;
20711
- data.worktreeId = null;
20712
- data.attachId = null;
20713
- log.error(`[ws] attach failed branch=${branch}: ${errMsg}`);
20714
- sendWs(ws, { type: "error", message: errMsg });
20715
- ws.close(1011, errMsg.slice(0, 123));
20869
+ if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20870
+ disarmOneshotIfArmed(branch, "terminal-ws-input");
20716
20871
  }
20717
- } else {
20872
+ write(attachId, msg.data);
20873
+ break;
20874
+ }
20875
+ case "sendKeys": {
20718
20876
  const attachId = getAttachedSessionId(data, ws);
20719
20877
  if (!attachId)
20720
20878
  return;
20721
- await resize(attachId, msg.cols, msg.rows);
20879
+ if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20880
+ disarmOneshotIfArmed(branch, "terminal-ws-send-keys");
20881
+ }
20882
+ await sendKeys(attachId, msg.hexBytes);
20883
+ break;
20722
20884
  }
20723
- break;
20724
- }
20725
- },
20726
- async close(ws, code, reason) {
20727
- const data = ws.data;
20728
- if (data.kind === "agents") {
20729
- log.debug(`[ws:agents] close branch=${data.branch} code=${code} reason=${reason}`);
20730
- data.unsubscribe?.();
20731
- data.unsubscribe = null;
20732
- return;
20885
+ case "selectPane":
20886
+ {
20887
+ const attachId = getAttachedSessionId(data, ws);
20888
+ if (!attachId)
20889
+ return;
20890
+ log.debug(`[ws] selectPane pane=${msg.pane} branch=${branch} attachId=${attachId}`);
20891
+ await selectPane(attachId, msg.pane);
20892
+ }
20893
+ break;
20894
+ case "resize":
20895
+ if (!data.attached) {
20896
+ data.attached = true;
20897
+ log.debug(`[ws] first resize (attaching) branch=${branch} cols=${msg.cols} rows=${msg.rows}`);
20898
+ try {
20899
+ if (msg.initialPane !== undefined) {
20900
+ log.debug(`[ws] initialPane=${msg.initialPane} branch=${branch}`);
20901
+ }
20902
+ const terminalWorktree = await resolveTerminalWorktree(branch);
20903
+ const attachId = `${terminalWorktree.worktreeId}:${randomUUID3()}`;
20904
+ data.worktreeId = terminalWorktree.worktreeId;
20905
+ data.attachId = attachId;
20906
+ await attach(attachId, terminalWorktree.attachTarget, msg.cols, msg.rows, msg.initialPane);
20907
+ const { onData, onExit } = makeCallbacks(ws);
20908
+ setCallbacks(attachId, onData, onExit);
20909
+ const scrollback = getScrollback(attachId);
20910
+ log.debug(`[ws] attached branch=${branch} worktreeId=${terminalWorktree.worktreeId} attachId=${attachId} scrollback=${scrollback.length} bytes`);
20911
+ if (scrollback.length > 0) {
20912
+ sendWs(ws, { type: "scrollback", data: scrollback });
20913
+ }
20914
+ } catch (err2) {
20915
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
20916
+ data.attached = false;
20917
+ data.worktreeId = null;
20918
+ data.attachId = null;
20919
+ log.error(`[ws] attach failed branch=${branch}: ${errMsg}`);
20920
+ sendWs(ws, { type: "error", message: errMsg });
20921
+ ws.close(1011, errMsg.slice(0, 123));
20922
+ }
20923
+ } else {
20924
+ const attachId = getAttachedSessionId(data, ws);
20925
+ if (!attachId)
20926
+ return;
20927
+ await resize(attachId, msg.cols, msg.rows);
20928
+ }
20929
+ break;
20930
+ }
20931
+ },
20932
+ async close(ws, code, reason) {
20933
+ const data = ws.data;
20934
+ if (data.kind === "agents") {
20935
+ log.debug(`[ws:agents] close branch=${data.branch} code=${code} reason=${reason}`);
20936
+ data.unsubscribe?.();
20937
+ data.unsubscribe = null;
20938
+ return;
20939
+ }
20940
+ log.debug(`[ws] close branch=${data.branch} code=${code} reason=${reason} attached=${data.attached} worktreeId=${data.worktreeId} attachId=${data.attachId}`);
20941
+ if (data.attachId) {
20942
+ clearCallbacks(data.attachId);
20943
+ await detach(data.attachId);
20944
+ }
20733
20945
  }
20734
- log.debug(`[ws] close branch=${data.branch} code=${code} reason=${reason} attached=${data.attached} worktreeId=${data.worktreeId} attachId=${data.attachId}`);
20735
- if (data.attachId) {
20736
- clearCallbacks(data.attachId);
20737
- await detach(data.attachId);
20946
+ }
20947
+ });
20948
+ }
20949
+ function actualPort(server, requested) {
20950
+ return server.port ?? requested;
20951
+ }
20952
+ var MAX_INCREMENTAL_BIND_ATTEMPTS = 100;
20953
+ var PORT_STRICT = Bun.env.WEBMUX_PORT_STRICT === "1";
20954
+ function bindServer() {
20955
+ if (PORT_STRICT) {
20956
+ try {
20957
+ return actualPort(startServer(PORT), PORT);
20958
+ } catch (err2) {
20959
+ const code = err2?.code;
20960
+ if (code === "EADDRINUSE") {
20961
+ log.error(`[serve] port ${PORT} is in use and was set explicitly; drop --port / PORT to let webmux pick a free port`);
20738
20962
  }
20963
+ throw err2;
20739
20964
  }
20740
20965
  }
20966
+ let candidate = PORT;
20967
+ for (let attempt = 0;attempt < MAX_INCREMENTAL_BIND_ATTEMPTS; attempt++) {
20968
+ try {
20969
+ const server = startServer(candidate);
20970
+ if (attempt > 0)
20971
+ log.info(`[serve] port ${PORT} in use; bound to ${actualPort(server, candidate)}`);
20972
+ return actualPort(server, candidate);
20973
+ } catch (err2) {
20974
+ const code = err2?.code;
20975
+ if (code !== "EADDRINUSE")
20976
+ throw err2;
20977
+ candidate += 1;
20978
+ }
20979
+ }
20980
+ log.info(`[serve] ports ${PORT}..${PORT + MAX_INCREMENTAL_BIND_ATTEMPTS - 1} all in use; falling back to an OS-picked port`);
20981
+ return actualPort(startServer(0), 0);
20982
+ }
20983
+ var BOUND_PORT = bindServer();
20984
+ var instanceRegistry = createInstanceRegistry();
20985
+ var explicitPrefix = Bun.env.WEBMUX_PREFIX?.trim();
20986
+ var livePeers = instanceRegistry.listLive();
20987
+ var takenPrefixes = livePeers.map((peer) => peer.prefix);
20988
+ var INSTANCE_PREFIX = explicitPrefix && isValidInstancePrefix(explicitPrefix) ? explicitPrefix : deriveInstancePrefix(PROJECT_DIR, takenPrefixes);
20989
+ var selfEntry = {
20990
+ prefix: INSTANCE_PREFIX,
20991
+ port: BOUND_PORT,
20992
+ projectDir: PROJECT_DIR,
20993
+ pid: process.pid,
20994
+ startedAt: Date.now()
20995
+ };
20996
+ instanceRegistry.register(selfEntry);
20997
+ log.info(`[serve] registered instance prefix=${INSTANCE_PREFIX} port=${BOUND_PORT}`);
20998
+ function deregisterSelf() {
20999
+ try {
21000
+ instanceRegistry.deregister(BOUND_PORT, process.pid);
21001
+ } catch {}
21002
+ }
21003
+ process.on("SIGINT", () => {
21004
+ deregisterSelf();
21005
+ process.exit(0);
21006
+ });
21007
+ process.on("SIGTERM", () => {
21008
+ deregisterSelf();
21009
+ process.exit(0);
20741
21010
  });
21011
+ process.on("exit", deregisterSelf);
20742
21012
  var tmuxCheck = Bun.spawnSync(["tmux", "list-sessions"], { stdout: "pipe", stderr: "pipe" });
20743
21013
  if (tmuxCheck.exitCode !== 0) {
20744
21014
  Bun.spawnSync(["tmux", "new-session", "-d", "-s", "0"]);
@@ -20765,12 +21035,12 @@ startOneshotWatcher({
20765
21035
  if (config.workspace.autoPull.enabled) {
20766
21036
  startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
20767
21037
  }
20768
- log.info(`Dev Dashboard API running at http://localhost:${PORT}`);
21038
+ log.info(`Dev Dashboard API running at http://localhost:${BOUND_PORT}`);
20769
21039
  var nets = networkInterfaces();
20770
21040
  for (const addrs of Object.values(nets)) {
20771
21041
  for (const a of addrs ?? []) {
20772
21042
  if (a.family === "IPv4" && !a.internal) {
20773
- log.info(` Network: http://${a.address}:${PORT}`);
21043
+ log.info(` Network: http://${a.address}:${BOUND_PORT}`);
20774
21044
  }
20775
21045
  }
20776
21046
  }