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.
- package/backend/dist/server.js +642 -372
- package/bin/webmux.js +598 -143
- package/frontend/dist/assets/{DiffDialog-CtwnOqjo.js → DiffDialog-Dv77nDf4.js} +1 -1
- package/frontend/dist/assets/index-BgvCuf9J.js +34 -0
- package/frontend/dist/assets/index-EO_hEDxL.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-CvURkZrd.css +0 -1
- package/frontend/dist/assets/index-EqF9CRFa.js +0 -34
package/backend/dist/server.js
CHANGED
|
@@ -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
|
|
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.
|
|
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+
|
|
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
|
-
|
|
13311
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
19936
|
-
|
|
19937
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
20381
|
-
|
|
20382
|
-
|
|
20383
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
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
|
-
|
|
20421
|
-
const
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
}
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
}
|
|
20435
|
-
|
|
20436
|
-
|
|
20437
|
-
|
|
20438
|
-
|
|
20439
|
-
|
|
20440
|
-
|
|
20441
|
-
|
|
20442
|
-
|
|
20443
|
-
|
|
20444
|
-
|
|
20445
|
-
|
|
20446
|
-
|
|
20447
|
-
|
|
20448
|
-
|
|
20449
|
-
|
|
20450
|
-
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
20461
|
-
|
|
20462
|
-
|
|
20463
|
-
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
|
|
20467
|
-
|
|
20468
|
-
|
|
20469
|
-
|
|
20470
|
-
|
|
20471
|
-
|
|
20472
|
-
|
|
20473
|
-
|
|
20474
|
-
return
|
|
20475
|
-
|
|
20476
|
-
|
|
20477
|
-
|
|
20478
|
-
|
|
20479
|
-
|
|
20480
|
-
|
|
20481
|
-
|
|
20482
|
-
|
|
20483
|
-
return
|
|
20484
|
-
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
20488
|
-
|
|
20489
|
-
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
return
|
|
20493
|
-
|
|
20494
|
-
|
|
20495
|
-
|
|
20496
|
-
|
|
20497
|
-
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
|
|
20508
|
-
|
|
20509
|
-
|
|
20510
|
-
|
|
20511
|
-
|
|
20512
|
-
|
|
20513
|
-
|
|
20514
|
-
|
|
20515
|
-
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20527
|
-
|
|
20528
|
-
|
|
20529
|
-
|
|
20530
|
-
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
|
|
20534
|
-
|
|
20535
|
-
|
|
20536
|
-
|
|
20537
|
-
|
|
20538
|
-
|
|
20539
|
-
|
|
20540
|
-
|
|
20541
|
-
|
|
20542
|
-
|
|
20543
|
-
|
|
20544
|
-
|
|
20545
|
-
|
|
20546
|
-
|
|
20547
|
-
|
|
20548
|
-
|
|
20549
|
-
|
|
20550
|
-
|
|
20551
|
-
|
|
20552
|
-
|
|
20553
|
-
|
|
20554
|
-
|
|
20555
|
-
|
|
20556
|
-
|
|
20557
|
-
|
|
20558
|
-
|
|
20559
|
-
|
|
20560
|
-
|
|
20561
|
-
|
|
20562
|
-
|
|
20563
|
-
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20569
|
-
|
|
20570
|
-
|
|
20571
|
-
|
|
20572
|
-
|
|
20573
|
-
|
|
20574
|
-
|
|
20575
|
-
|
|
20576
|
-
|
|
20577
|
-
|
|
20578
|
-
|
|
20579
|
-
|
|
20580
|
-
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20587
|
-
|
|
20588
|
-
|
|
20589
|
-
|
|
20590
|
-
|
|
20591
|
-
|
|
20592
|
-
|
|
20593
|
-
|
|
20594
|
-
|
|
20595
|
-
|
|
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
|
-
|
|
20599
|
-
|
|
20600
|
-
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
|
|
20605
|
-
|
|
20606
|
-
const
|
|
20607
|
-
if (!
|
|
20608
|
-
return
|
|
20609
|
-
|
|
20610
|
-
|
|
20611
|
-
|
|
20612
|
-
|
|
20613
|
-
|
|
20614
|
-
|
|
20615
|
-
|
|
20616
|
-
|
|
20617
|
-
|
|
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
|
-
|
|
20644
|
-
openAgentsSocket(ws, data);
|
|
20837
|
+
return new Response("Not Found", { status: 404 });
|
|
20645
20838
|
},
|
|
20646
|
-
|
|
20647
|
-
|
|
20648
|
-
|
|
20649
|
-
|
|
20650
|
-
|
|
20651
|
-
|
|
20652
|
-
|
|
20653
|
-
|
|
20654
|
-
|
|
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
|
-
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
|
|
20675
|
-
}
|
|
20676
|
-
|
|
20677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
|
|
20732
|
-
|
|
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
|
-
|
|
20735
|
-
|
|
20736
|
-
|
|
20737
|
-
|
|
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:${
|
|
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}:${
|
|
21043
|
+
log.info(` Network: http://${a.address}:${BOUND_PORT}`);
|
|
20774
21044
|
}
|
|
20775
21045
|
}
|
|
20776
21046
|
}
|