webmux 0.16.0 → 0.17.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.
@@ -6906,6 +6906,7 @@ var require_public_api = __commonJS((exports) => {
6906
6906
 
6907
6907
  // backend/src/server.ts
6908
6908
  import { join as join6, resolve as resolve6 } from "path";
6909
+ import { mkdirSync } from "fs";
6909
6910
  import { networkInterfaces } from "os";
6910
6911
 
6911
6912
  // backend/src/lib/log.ts
@@ -7482,15 +7483,17 @@ function loadLocalProjectConfigOverlay(root) {
7482
7483
  try {
7483
7484
  const text = readLocalConfigFile(root).trim();
7484
7485
  if (!text) {
7485
- return { profiles: {}, lifecycleHooks: {} };
7486
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
7486
7487
  }
7487
7488
  const parsed = parseConfigDocument(text);
7489
+ const ws = isRecord(parsed.workspace) ? parsed.workspace : null;
7488
7490
  return {
7491
+ worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
7489
7492
  profiles: parseProfiles(parsed.profiles, false),
7490
7493
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks)
7491
7494
  };
7492
7495
  } catch {
7493
- return { profiles: {}, lifecycleHooks: {} };
7496
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
7494
7497
  }
7495
7498
  }
7496
7499
  function mergeHookCommand(projectCommand, localCommand) {
@@ -7534,6 +7537,9 @@ function loadConfig(dir, options = {}) {
7534
7537
  const localOverlay = loadLocalProjectConfigOverlay(root);
7535
7538
  return {
7536
7539
  ...projectConfig,
7540
+ ...localOverlay.worktreeRoot !== null ? {
7541
+ workspace: { ...projectConfig.workspace, worktreeRoot: localOverlay.worktreeRoot }
7542
+ } : {},
7537
7543
  profiles: {
7538
7544
  ...cloneProfiles(projectConfig.profiles),
7539
7545
  ...cloneProfiles(localOverlay.profiles)
@@ -8610,6 +8616,14 @@ class BunGitGateway {
8610
8616
  currentBranch(repoRoot) {
8611
8617
  return runGit(["branch", "--show-current"], repoRoot);
8612
8618
  }
8619
+ readDiff(cwd) {
8620
+ const result = tryRunGit(["diff", "HEAD", "--no-color"], cwd);
8621
+ return result.ok ? result.stdout : "";
8622
+ }
8623
+ readUnpushedDiff(cwd) {
8624
+ const result = tryRunGit(["diff", "@{upstream}..HEAD", "--no-color"], cwd);
8625
+ return result.ok ? result.stdout : "";
8626
+ }
8613
8627
  }
8614
8628
 
8615
8629
  // backend/src/domain/model.ts
@@ -10999,6 +11013,27 @@ async function apiGetLinearIssues() {
10999
11013
  return errorResponse(result.error, 502);
11000
11014
  return jsonResponse(result.data);
11001
11015
  }
11016
+ var MAX_DIFF_BYTES = 200 * 1024;
11017
+ async function apiGetWorktreeDiff(name) {
11018
+ await reconciliationService.reconcile(PROJECT_DIR);
11019
+ const state = projectRuntime.getWorktreeByBranch(name);
11020
+ if (!state)
11021
+ return errorResponse(`Worktree not found: ${name}`, 404);
11022
+ const uncommitted = git.readDiff(state.path);
11023
+ const unpushed = git.readUnpushedDiff(state.path);
11024
+ function cap(raw) {
11025
+ const truncated = raw.length > MAX_DIFF_BYTES;
11026
+ return { diff: truncated ? raw.slice(0, MAX_DIFF_BYTES) : raw, truncated };
11027
+ }
11028
+ const u = cap(uncommitted);
11029
+ const p = cap(unpushed);
11030
+ return jsonResponse({
11031
+ uncommitted: u.diff,
11032
+ uncommittedTruncated: u.truncated,
11033
+ unpushed: p.diff,
11034
+ unpushedTruncated: p.truncated
11035
+ });
11036
+ }
11002
11037
  async function apiCiLogs(runId) {
11003
11038
  if (!/^\d+$/.test(runId))
11004
11039
  return errorResponse("Invalid run ID", 400);
@@ -11014,6 +11049,53 @@ async function apiCiLogs(runId) {
11014
11049
  const stderr = (await new Response(proc.stderr).text()).trim();
11015
11050
  return errorResponse(`Failed to fetch logs: ${stderr || "unknown error"}`, 502);
11016
11051
  }
11052
+ var ALLOWED_IMAGE_TYPES = new Set([
11053
+ "image/png",
11054
+ "image/jpeg",
11055
+ "image/gif",
11056
+ "image/webp"
11057
+ ]);
11058
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
11059
+ function sanitizeFilename(name) {
11060
+ const base = name.split("/").pop()?.split("\\").pop() ?? "upload";
11061
+ return base.replace(/[^a-zA-Z0-9._-]/g, "_") || "upload";
11062
+ }
11063
+ async function apiUploadFiles(name, req) {
11064
+ const state = projectRuntime.getWorktreeByBranch(name);
11065
+ if (!state)
11066
+ return errorResponse(`Worktree not found: ${name}`, 404);
11067
+ let formData;
11068
+ try {
11069
+ formData = await req.formData();
11070
+ } catch {
11071
+ return errorResponse("Invalid multipart form data", 400);
11072
+ }
11073
+ const entries = formData.getAll("files");
11074
+ if (entries.length === 0)
11075
+ return errorResponse("No files provided", 400);
11076
+ const uploadDir = `/tmp/webmux-uploads/${sanitizeFilename(name)}`;
11077
+ mkdirSync(uploadDir, { recursive: true });
11078
+ const results = [];
11079
+ for (const entry of entries) {
11080
+ if (!(entry instanceof File))
11081
+ continue;
11082
+ if (!ALLOWED_IMAGE_TYPES.has(entry.type)) {
11083
+ return errorResponse(`Unsupported file type: ${entry.type}`, 400);
11084
+ }
11085
+ if (entry.size > MAX_FILE_SIZE) {
11086
+ return errorResponse(`File too large: ${entry.name} (max 10MB)`, 400);
11087
+ }
11088
+ const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
11089
+ const destPath = join6(uploadDir, safeName);
11090
+ if (!resolve6(destPath).startsWith(uploadDir + "/")) {
11091
+ return errorResponse("Invalid filename", 400);
11092
+ }
11093
+ await Bun.write(destPath, entry);
11094
+ results.push({ path: destPath });
11095
+ }
11096
+ log.info(`[upload] branch=${name} files=${results.length}`);
11097
+ return jsonResponse({ files: results });
11098
+ }
11017
11099
  Bun.serve({
11018
11100
  port: PORT,
11019
11101
  idleTimeout: 255,
@@ -11069,6 +11151,14 @@ Bun.serve({
11069
11151
  return catching(`POST /api/worktrees/${name}/send`, () => apiSendPrompt(name, req));
11070
11152
  }
11071
11153
  },
11154
+ "/api/worktrees/:name/upload": {
11155
+ POST: (req) => {
11156
+ const name = decodeURIComponent(req.params.name);
11157
+ if (!isValidWorktreeName(name))
11158
+ return errorResponse("Invalid worktree name", 400);
11159
+ return catching(`POST /api/worktrees/${name}/upload`, () => apiUploadFiles(name, req));
11160
+ }
11161
+ },
11072
11162
  "/api/worktrees/:name/merge": {
11073
11163
  POST: (req) => {
11074
11164
  const name = decodeURIComponent(req.params.name);
@@ -11077,6 +11167,14 @@ Bun.serve({
11077
11167
  return catching(`POST /api/worktrees/${name}/merge`, () => apiMergeWorktree(name));
11078
11168
  }
11079
11169
  },
11170
+ "/api/worktrees/:name/diff": {
11171
+ GET: (req) => {
11172
+ const name = decodeURIComponent(req.params.name);
11173
+ if (!isValidWorktreeName(name))
11174
+ return errorResponse("Invalid worktree name", 400);
11175
+ return catching(`GET /api/worktrees/${name}/diff`, () => apiGetWorktreeDiff(name));
11176
+ }
11177
+ },
11080
11178
  "/api/linear/issues": {
11081
11179
  GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
11082
11180
  },
package/bin/webmux.js CHANGED
@@ -267,6 +267,14 @@ class BunGitGateway {
267
267
  currentBranch(repoRoot) {
268
268
  return runGit(["branch", "--show-current"], repoRoot);
269
269
  }
270
+ readDiff(cwd) {
271
+ const result = tryRunGit(["diff", "HEAD", "--no-color"], cwd);
272
+ return result.ok ? result.stdout : "";
273
+ }
274
+ readUnpushedDiff(cwd) {
275
+ const result = tryRunGit(["diff", "@{upstream}..HEAD", "--no-color"], cwd);
276
+ return result.ok ? result.stdout : "";
277
+ }
270
278
  }
271
279
  var init_git = () => {};
272
280
 
@@ -9901,15 +9909,17 @@ function loadLocalProjectConfigOverlay(root) {
9901
9909
  try {
9902
9910
  const text = readLocalConfigFile(root).trim();
9903
9911
  if (!text) {
9904
- return { profiles: {}, lifecycleHooks: {} };
9912
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
9905
9913
  }
9906
9914
  const parsed = parseConfigDocument(text);
9915
+ const ws = isRecord3(parsed.workspace) ? parsed.workspace : null;
9907
9916
  return {
9917
+ worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
9908
9918
  profiles: parseProfiles(parsed.profiles, false),
9909
9919
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks)
9910
9920
  };
9911
9921
  } catch {
9912
- return { profiles: {}, lifecycleHooks: {} };
9922
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
9913
9923
  }
9914
9924
  }
9915
9925
  function mergeHookCommand(projectCommand, localCommand) {
@@ -9953,6 +9963,9 @@ function loadConfig(dir, options = {}) {
9953
9963
  const localOverlay = loadLocalProjectConfigOverlay(root);
9954
9964
  return {
9955
9965
  ...projectConfig,
9966
+ ...localOverlay.worktreeRoot !== null ? {
9967
+ workspace: { ...projectConfig.workspace, worktreeRoot: localOverlay.worktreeRoot }
9968
+ } : {},
9956
9969
  profiles: {
9957
9970
  ...cloneProfiles(projectConfig.profiles),
9958
9971
  ...cloneProfiles(localOverlay.profiles)
@@ -12444,7 +12457,7 @@ import { fileURLToPath } from "url";
12444
12457
  // package.json
12445
12458
  var package_default = {
12446
12459
  name: "webmux",
12447
- version: "0.16.0",
12460
+ version: "0.17.0",
12448
12461
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
12449
12462
  type: "module",
12450
12463
  repository: {