webmux 0.17.0 → 0.19.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/bin/webmux.js CHANGED
@@ -172,7 +172,10 @@ function listLocalGitBranches(cwd) {
172
172
  function readGitWorktreeStatus(cwd) {
173
173
  const dirtyOutput = runGit(["status", "--porcelain"], cwd);
174
174
  const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
175
- const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
175
+ let ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
176
+ if (!ahead.ok) {
177
+ ahead = tryRunGit(["rev-list", "--count", "HEAD", "--not", "--remotes=origin"], cwd);
178
+ }
176
179
  return {
177
180
  dirty: dirtyOutput.length > 0,
178
181
  aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
@@ -271,9 +274,21 @@ class BunGitGateway {
271
274
  const result = tryRunGit(["diff", "HEAD", "--no-color"], cwd);
272
275
  return result.ok ? result.stdout : "";
273
276
  }
274
- readUnpushedDiff(cwd) {
275
- const result = tryRunGit(["diff", "@{upstream}..HEAD", "--no-color"], cwd);
276
- return result.ok ? result.stdout : "";
277
+ listUnpushedCommits(cwd) {
278
+ let result = tryRunGit(["log", "--oneline", "@{upstream}..HEAD"], cwd);
279
+ if (!result.ok) {
280
+ result = tryRunGit(["log", "--oneline", "HEAD", "--not", "--remotes=origin"], cwd);
281
+ }
282
+ if (!result.ok || !result.stdout)
283
+ return [];
284
+ return result.stdout.split(`
285
+ `).filter((line) => line.length > 0).map((line) => {
286
+ const spaceIdx = line.indexOf(" ");
287
+ return {
288
+ hash: line.slice(0, spaceIdx),
289
+ message: line.slice(spaceIdx + 1)
290
+ };
291
+ });
277
292
  }
278
293
  }
279
294
  var init_git = () => {};
@@ -9895,7 +9910,10 @@ function parseProjectConfig(parsed) {
9895
9910
  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) : []
9896
9911
  },
9897
9912
  linear: {
9898
- enabled: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
9913
+ enabled: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled,
9914
+ autoCreateWorktrees: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.autoCreateWorktrees === "boolean" ? parsed.integrations.linear.autoCreateWorktrees : DEFAULT_CONFIG.integrations.linear.autoCreateWorktrees,
9915
+ createTicketOption: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.createTicketOption === "boolean" ? parsed.integrations.linear.createTicketOption : DEFAULT_CONFIG.integrations.linear.createTicketOption,
9916
+ ...isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.teamId === "string" && parsed.integrations.linear.teamId.trim() ? { teamId: parsed.integrations.linear.teamId.trim() } : {}
9899
9917
  }
9900
9918
  },
9901
9919
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
@@ -9905,21 +9923,39 @@ function parseProjectConfig(parsed) {
9905
9923
  function defaultConfig() {
9906
9924
  return parseProjectConfig({});
9907
9925
  }
9926
+ function parseLocalLinearOverlay(parsed) {
9927
+ if (!isRecord3(parsed.integrations))
9928
+ return null;
9929
+ const linear = parsed.integrations.linear;
9930
+ if (!isRecord3(linear))
9931
+ return null;
9932
+ const overlay = {};
9933
+ if (typeof linear.enabled === "boolean")
9934
+ overlay.enabled = linear.enabled;
9935
+ if (typeof linear.autoCreateWorktrees === "boolean")
9936
+ overlay.autoCreateWorktrees = linear.autoCreateWorktrees;
9937
+ if (typeof linear.createTicketOption === "boolean")
9938
+ overlay.createTicketOption = linear.createTicketOption;
9939
+ if (typeof linear.teamId === "string" && linear.teamId.trim())
9940
+ overlay.teamId = linear.teamId.trim();
9941
+ return Object.keys(overlay).length > 0 ? overlay : null;
9942
+ }
9908
9943
  function loadLocalProjectConfigOverlay(root) {
9909
9944
  try {
9910
9945
  const text = readLocalConfigFile(root).trim();
9911
9946
  if (!text) {
9912
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
9947
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null };
9913
9948
  }
9914
9949
  const parsed = parseConfigDocument(text);
9915
9950
  const ws = isRecord3(parsed.workspace) ? parsed.workspace : null;
9916
9951
  return {
9917
9952
  worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
9918
9953
  profiles: parseProfiles(parsed.profiles, false),
9919
- lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks)
9954
+ lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
9955
+ linear: parseLocalLinearOverlay(parsed)
9920
9956
  };
9921
9957
  } catch {
9922
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
9958
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null };
9923
9959
  }
9924
9960
  }
9925
9961
  function mergeHookCommand(projectCommand, localCommand) {
@@ -9970,7 +10006,13 @@ function loadConfig(dir, options = {}) {
9970
10006
  ...cloneProfiles(projectConfig.profiles),
9971
10007
  ...cloneProfiles(localOverlay.profiles)
9972
10008
  },
9973
- lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks)
10009
+ lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks),
10010
+ ...localOverlay.linear ? {
10011
+ integrations: {
10012
+ ...projectConfig.integrations,
10013
+ linear: { ...projectConfig.integrations.linear, ...localOverlay.linear }
10014
+ }
10015
+ } : {}
9974
10016
  };
9975
10017
  }
9976
10018
  function expandTemplate(template, env) {
@@ -10001,7 +10043,7 @@ var init_config = __esm(() => {
10001
10043
  startupEnvs: {},
10002
10044
  integrations: {
10003
10045
  github: { linkedRepos: [] },
10004
- linear: { enabled: true }
10046
+ linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
10005
10047
  },
10006
10048
  lifecycleHooks: {},
10007
10049
  autoName: null
@@ -10345,8 +10387,8 @@ class BunLifecycleHookRunner {
10345
10387
  }
10346
10388
  async run(input) {
10347
10389
  const cmd = await this.buildCommand(input.cwd, input.command);
10348
- console.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
10349
- console.debug(`[hook-runner] Env keys: ${Object.keys(input.env).join(", ")}`);
10390
+ log.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
10391
+ log.debug(`[hook-runner] envKeys=${Object.keys(input.env).length}`);
10350
10392
  const proc = Bun.spawn(cmd, {
10351
10393
  cwd: input.cwd,
10352
10394
  env: {
@@ -10361,17 +10403,19 @@ class BunLifecycleHookRunner {
10361
10403
  new Response(proc.stdout).text(),
10362
10404
  new Response(proc.stderr).text()
10363
10405
  ]);
10364
- console.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
10406
+ log.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
10365
10407
  if (stdout.trim())
10366
- console.debug(`[hook-runner] stdout: ${stdout.trim()}`);
10408
+ log.debug(`[hook-runner] stdout: ${stdout.trim()}`);
10367
10409
  if (stderr.trim())
10368
- console.debug(`[hook-runner] stderr: ${stderr.trim()}`);
10410
+ log.debug(`[hook-runner] stderr: ${stderr.trim()}`);
10369
10411
  if (exitCode !== 0) {
10370
10412
  throw new Error(buildErrorMessage(input.name, exitCode, stdout, stderr));
10371
10413
  }
10372
10414
  }
10373
10415
  }
10374
- var init_hooks = () => {};
10416
+ var init_hooks = __esm(() => {
10417
+ init_log();
10418
+ });
10375
10419
 
10376
10420
  // backend/src/adapters/port-probe.ts
10377
10421
  class BunPortProbe {
@@ -11214,7 +11258,7 @@ class LifecycleService {
11214
11258
  agent,
11215
11259
  phase: "reconciling"
11216
11260
  });
11217
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11261
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11218
11262
  return {
11219
11263
  branch,
11220
11264
  worktreeId: initialized.meta.worktreeId
@@ -11249,7 +11293,7 @@ class LifecycleService {
11249
11293
  worktreePath: resolved.entry.path,
11250
11294
  launchMode
11251
11295
  });
11252
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11296
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11253
11297
  return {
11254
11298
  branch,
11255
11299
  worktreeId: initialized.meta.worktreeId
@@ -11262,7 +11306,7 @@ class LifecycleService {
11262
11306
  try {
11263
11307
  const resolved = await this.resolveExistingWorktree(branch);
11264
11308
  this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
11265
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11309
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11266
11310
  } catch (error) {
11267
11311
  throw this.wrapOperationError(error);
11268
11312
  }
@@ -11588,15 +11632,15 @@ class LifecycleService {
11588
11632
  deleteBranch: true,
11589
11633
  deleteBranchForce: true
11590
11634
  }, this.deps.git);
11591
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11635
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11592
11636
  }
11593
11637
  async runLifecycleHook(input) {
11594
- console.debug(`[lifecycle-hook] name=${input.name} command=${input.command ?? "UNDEFINED"} meta=${input.meta ? "present" : "NULL"} cwd=${input.worktreePath}`);
11638
+ log.debug(`[lifecycle-hook] name=${input.name} command=${input.command ?? "UNDEFINED"} meta=${input.meta ? "present" : "NULL"} cwd=${input.worktreePath}`);
11595
11639
  if (!input.command || !input.meta) {
11596
- console.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
11640
+ log.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
11597
11641
  return;
11598
11642
  }
11599
- console.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
11643
+ log.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
11600
11644
  const dotenvValues = await loadDotenvLocal(input.worktreePath);
11601
11645
  await this.deps.hooks.run({
11602
11646
  name: input.name,
@@ -11606,7 +11650,7 @@ class LifecycleService {
11606
11650
  WEBMUX_WORKTREE_PATH: input.worktreePath
11607
11651
  }, dotenvValues)
11608
11652
  });
11609
- console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11653
+ log.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11610
11654
  }
11611
11655
  async reportCreateProgress(progress) {
11612
11656
  await this.deps.onCreateProgress?.(progress);
@@ -11630,6 +11674,7 @@ var init_lifecycle_service = __esm(() => {
11630
11674
  init_policies();
11631
11675
  init_session_service();
11632
11676
  init_worktree_service();
11677
+ init_log();
11633
11678
  LifecycleError = class LifecycleError extends Error {
11634
11679
  status;
11635
11680
  constructor(message, status2) {
@@ -11910,6 +11955,21 @@ var init_project_runtime = __esm(() => {
11910
11955
  init_tmux();
11911
11956
  });
11912
11957
 
11958
+ // backend/src/lib/async.ts
11959
+ async function mapWithConcurrency(items, limit, fn) {
11960
+ const results = new Array(items.length);
11961
+ let next = 0;
11962
+ const concurrency = Math.max(1, Math.min(limit, items.length || 1));
11963
+ async function worker() {
11964
+ while (next < items.length) {
11965
+ const index = next++;
11966
+ results[index] = await fn(items[index]);
11967
+ }
11968
+ }
11969
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
11970
+ return results;
11971
+ }
11972
+
11913
11973
  // backend/src/services/reconciliation-service.ts
11914
11974
  import { basename as basename4, resolve as resolve6 } from "path";
11915
11975
  function makeUnmanagedWorktreeId(path) {
@@ -11952,11 +12012,34 @@ function resolveBranch(entry, metaBranch) {
11952
12012
 
11953
12013
  class ReconciliationService {
11954
12014
  deps;
11955
- constructor(deps2) {
12015
+ freshnessMs;
12016
+ now;
12017
+ concurrency;
12018
+ inFlight = null;
12019
+ lastReconciledAt = 0;
12020
+ constructor(deps2, options = {}) {
11956
12021
  this.deps = deps2;
12022
+ this.freshnessMs = options.freshnessMs ?? 500;
12023
+ this.now = options.now ?? Date.now;
12024
+ this.concurrency = options.concurrency ?? 4;
11957
12025
  }
11958
- async reconcile(repoRoot) {
12026
+ async reconcile(repoRoot, options = {}) {
12027
+ if (this.inFlight) {
12028
+ return await this.inFlight;
12029
+ }
12030
+ if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
12031
+ return;
12032
+ }
11959
12033
  const normalizedRepoRoot = resolve6(repoRoot);
12034
+ const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
12035
+ this.lastReconciledAt = this.now();
12036
+ });
12037
+ this.inFlight = reconcilePromise.finally(() => {
12038
+ this.inFlight = null;
12039
+ });
12040
+ return await this.inFlight;
12041
+ }
12042
+ async runReconcile(normalizedRepoRoot) {
11960
12043
  const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
11961
12044
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
11962
12045
  let windows = [];
@@ -11966,40 +12049,32 @@ class ReconciliationService {
11966
12049
  windows = [];
11967
12050
  }
11968
12051
  const seenWorktreeIds = new Set;
11969
- for (const entry of worktrees) {
11970
- if (entry.bare)
11971
- continue;
11972
- if (resolve6(entry.path) === normalizedRepoRoot)
11973
- continue;
12052
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve6(entry.path) !== normalizedRepoRoot);
12053
+ const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
11974
12054
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11975
12055
  const meta = await readWorktreeMeta(gitDir);
11976
12056
  const branch = resolveBranch(entry, meta?.branch ?? null);
11977
12057
  const worktreeId = meta?.worktreeId ?? makeUnmanagedWorktreeId(entry.path);
11978
- seenWorktreeIds.add(worktreeId);
11979
- this.deps.runtime.upsertWorktree({
12058
+ const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
12059
+ const window = findWindow(windows, sessionName, branch);
12060
+ return {
11980
12061
  worktreeId,
11981
12062
  branch,
11982
12063
  path: entry.path,
11983
12064
  profile: meta?.profile ?? null,
11984
12065
  agentName: meta?.agent ?? null,
11985
- runtime: meta?.runtime ?? "host"
11986
- });
11987
- const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
11988
- this.deps.runtime.setGitState(worktreeId, {
11989
- exists: true,
11990
- branch,
11991
- dirty: gitStatus.dirty,
11992
- aheadCount: gitStatus.aheadCount,
11993
- currentCommit: gitStatus.currentCommit
11994
- });
11995
- const window = findWindow(windows, sessionName, branch);
11996
- this.deps.runtime.setSessionState(worktreeId, {
11997
- exists: window !== null,
11998
- sessionName: window?.sessionName ?? null,
11999
- paneCount: window?.paneCount ?? 0
12000
- });
12001
- if (meta) {
12002
- this.deps.runtime.setServices(worktreeId, await buildServiceStates(this.deps, {
12066
+ runtime: meta?.runtime ?? "host",
12067
+ git: {
12068
+ dirty: gitStatus.dirty,
12069
+ aheadCount: gitStatus.aheadCount,
12070
+ currentCommit: gitStatus.currentCommit
12071
+ },
12072
+ session: {
12073
+ exists: window !== null,
12074
+ sessionName: window?.sessionName ?? null,
12075
+ paneCount: window?.paneCount ?? 0
12076
+ },
12077
+ services: meta ? await buildServiceStates(this.deps, {
12003
12078
  allocatedPorts: meta.allocatedPorts,
12004
12079
  startupEnvValues: meta.startupEnvValues,
12005
12080
  worktreeId: meta.worktreeId,
@@ -12007,11 +12082,34 @@ class ReconciliationService {
12007
12082
  profile: meta.profile,
12008
12083
  agent: meta.agent,
12009
12084
  runtime: meta.runtime
12010
- }));
12011
- } else {
12012
- this.deps.runtime.setServices(worktreeId, []);
12013
- }
12014
- this.deps.runtime.setPrs(worktreeId, await readWorktreePrs(gitDir));
12085
+ }) : [],
12086
+ prs: await readWorktreePrs(gitDir)
12087
+ };
12088
+ });
12089
+ for (const state of reconciledStates) {
12090
+ seenWorktreeIds.add(state.worktreeId);
12091
+ this.deps.runtime.upsertWorktree({
12092
+ worktreeId: state.worktreeId,
12093
+ branch: state.branch,
12094
+ path: state.path,
12095
+ profile: state.profile,
12096
+ agentName: state.agentName,
12097
+ runtime: state.runtime
12098
+ });
12099
+ this.deps.runtime.setGitState(state.worktreeId, {
12100
+ exists: true,
12101
+ branch: state.branch,
12102
+ dirty: state.git.dirty,
12103
+ aheadCount: state.git.aheadCount,
12104
+ currentCommit: state.git.currentCommit
12105
+ });
12106
+ this.deps.runtime.setSessionState(state.worktreeId, {
12107
+ exists: state.session.exists,
12108
+ sessionName: state.session.sessionName,
12109
+ paneCount: state.session.paneCount
12110
+ });
12111
+ this.deps.runtime.setServices(state.worktreeId, state.services);
12112
+ this.deps.runtime.setPrs(state.worktreeId, state.prs);
12015
12113
  }
12016
12114
  for (const state of this.deps.runtime.listWorktrees()) {
12017
12115
  if (!seenWorktreeIds.has(state.worktreeId)) {
@@ -12133,13 +12231,14 @@ function getWorktreeCommandUsage(command) {
12133
12231
  case "add":
12134
12232
  return [
12135
12233
  "Usage:",
12136
- " webmux add [branch] [--profile <name>] [--agent <claude|codex>] [--prompt <text>] [--env KEY=VALUE]",
12234
+ " webmux add [branch] [--profile <name>] [--agent <claude|codex>] [--prompt <text>] [--env KEY=VALUE] [--detach]",
12137
12235
  "",
12138
12236
  "Options:",
12139
12237
  " --profile <name> Worktree profile from .webmux.yaml",
12140
12238
  " --agent <claude|codex> Agent to launch in the worktree",
12141
12239
  " --prompt <text> Initial agent prompt",
12142
12240
  " --env KEY=VALUE Runtime env override (repeatable)",
12241
+ " -d, --detach Create worktree without switching to it",
12143
12242
  " --help Show this help message"
12144
12243
  ].join(`
12145
12244
  `);
@@ -12193,6 +12292,7 @@ function parseAgent(value) {
12193
12292
  function parseAddCommandArgs(args) {
12194
12293
  const input = {};
12195
12294
  const envOverrides = {};
12295
+ let detach = false;
12196
12296
  for (let index = 0;index < args.length; index++) {
12197
12297
  const arg = args[index];
12198
12298
  if (!arg)
@@ -12200,6 +12300,10 @@ function parseAddCommandArgs(args) {
12200
12300
  if (arg === "--help" || arg === "-h") {
12201
12301
  return null;
12202
12302
  }
12303
+ if (arg === "--detach" || arg === "-d") {
12304
+ detach = true;
12305
+ continue;
12306
+ }
12203
12307
  if (arg === "--profile" || arg.startsWith("--profile=")) {
12204
12308
  const { value, nextIndex } = readOptionValue(args, index, "--profile");
12205
12309
  input.profile = value;
@@ -12239,7 +12343,7 @@ function parseAddCommandArgs(args) {
12239
12343
  if (Object.keys(envOverrides).length > 0) {
12240
12344
  input.envOverrides = envOverrides;
12241
12345
  }
12242
- return input;
12346
+ return { input, detach };
12243
12347
  }
12244
12348
  function parseBranchCommandArgs(args) {
12245
12349
  let branch = null;
@@ -12353,8 +12457,8 @@ async function runWorktreeCommand(context, deps2 = {}) {
12353
12457
  const confirmPrune = deps2.confirmPrune ?? defaultConfirmPrune;
12354
12458
  try {
12355
12459
  if (context.command === "add") {
12356
- const input = parseAddCommandArgs(context.args);
12357
- if (!input) {
12460
+ const parsed = parseAddCommandArgs(context.args);
12461
+ if (!parsed) {
12358
12462
  stdout(getWorktreeCommandUsage("add"));
12359
12463
  return 0;
12360
12464
  }
@@ -12362,9 +12466,11 @@ async function runWorktreeCommand(context, deps2 = {}) {
12362
12466
  projectDir: context.projectDir,
12363
12467
  port: context.port
12364
12468
  });
12365
- const result = await runtime2.lifecycleService.createWorktree(input);
12469
+ const result = await runtime2.lifecycleService.createWorktree(parsed.input);
12366
12470
  stdout(`Created worktree ${result.branch}`);
12367
- switchToTmuxWindow(runtime2.projectDir, result.branch);
12471
+ if (!parsed.detach) {
12472
+ switchToTmuxWindow(runtime2.projectDir, result.branch);
12473
+ }
12368
12474
  return 0;
12369
12475
  }
12370
12476
  if (context.command === "list") {
@@ -12457,7 +12563,7 @@ import { fileURLToPath } from "url";
12457
12563
  // package.json
12458
12564
  var package_default = {
12459
12565
  name: "webmux",
12460
- version: "0.17.0",
12566
+ version: "0.19.0",
12461
12567
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
12462
12568
  type: "module",
12463
12569
  repository: {
@@ -12517,7 +12623,7 @@ function usage2() {
12517
12623
  webmux \u2014 Dev dashboard for managing Git worktrees
12518
12624
 
12519
12625
  Usage:
12520
- webmux serve Start the dashboard server
12626
+ webmux serve Start the dashboard server (--app opens in app mode)
12521
12627
  webmux init Interactive project setup
12522
12628
  webmux service Manage webmux as a system service
12523
12629
  webmux update Update webmux to the latest version
@@ -12532,6 +12638,7 @@ Usage:
12532
12638
 
12533
12639
  Options:
12534
12640
  --port N Set port (default: 5111)
12641
+ --app Open dashboard in browser app mode (minimal window)
12535
12642
  --debug Show debug-level logs
12536
12643
  --version Show version number
12537
12644
  --help Show this help message
@@ -12544,11 +12651,12 @@ function isRootCommand(value) {
12544
12651
  return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge" || value === "prune" || value === "completion";
12545
12652
  }
12546
12653
  function isServeRootOption(value) {
12547
- return value === "--port" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
12654
+ return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
12548
12655
  }
12549
12656
  function parseRootArgs(args) {
12550
12657
  let port = parseInt(process.env.PORT || "5111", 10);
12551
12658
  let debug = false;
12659
+ let app = false;
12552
12660
  let command = null;
12553
12661
  const commandArgs = [];
12554
12662
  for (let index = 0;index < args.length; index++) {
@@ -12572,6 +12680,9 @@ function parseRootArgs(args) {
12572
12680
  index += 1;
12573
12681
  break;
12574
12682
  }
12683
+ case "--app":
12684
+ app = true;
12685
+ break;
12575
12686
  case "--debug":
12576
12687
  debug = true;
12577
12688
  break;
@@ -12595,6 +12706,7 @@ Run webmux --help for usage.`);
12595
12706
  return {
12596
12707
  port,
12597
12708
  debug,
12709
+ app,
12598
12710
  command,
12599
12711
  commandArgs
12600
12712
  };
@@ -12621,10 +12733,44 @@ async function loadEnvFile(path) {
12621
12733
  }
12622
12734
  }
12623
12735
  }
12624
- function pipeWithPrefix(stream, prefix) {
12736
+ function findBrowserBinary() {
12737
+ const candidates = process.platform === "darwin" ? [
12738
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
12739
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
12740
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
12741
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
12742
+ ] : [
12743
+ "google-chrome",
12744
+ "google-chrome-stable",
12745
+ "chromium",
12746
+ "chromium-browser",
12747
+ "microsoft-edge",
12748
+ "brave-browser"
12749
+ ];
12750
+ for (const candidate of candidates) {
12751
+ const found = candidate.startsWith("/") ? existsSync5(candidate) : Bun.spawnSync(["which", candidate], { stdout: "pipe", stderr: "pipe" }).success;
12752
+ if (found)
12753
+ return candidate;
12754
+ }
12755
+ return null;
12756
+ }
12757
+ function openAppMode(url) {
12758
+ const browser = findBrowserBinary();
12759
+ if (!browser) {
12760
+ console.log(`[app] No Chromium-based browser found \u2014 open ${url} manually`);
12761
+ return;
12762
+ }
12763
+ console.log(`[app] Opening ${url} in app mode`);
12764
+ Bun.spawn([browser, `--app=${url}`], {
12765
+ stdout: "ignore",
12766
+ stderr: "ignore"
12767
+ });
12768
+ }
12769
+ function pipeWithPrefix(stream, prefix, onTrigger) {
12625
12770
  const reader = stream.getReader();
12626
12771
  const decoder = new TextDecoder;
12627
12772
  let buffer = "";
12773
+ let fired = false;
12628
12774
  (async () => {
12629
12775
  while (true) {
12630
12776
  const { done, value } = await reader.read();
@@ -12636,6 +12782,10 @@ function pipeWithPrefix(stream, prefix) {
12636
12782
  buffer = lines.pop();
12637
12783
  for (const line of lines) {
12638
12784
  console.log(`${prefix} ${line}`);
12785
+ if (onTrigger && !fired && line.includes(onTrigger.text)) {
12786
+ fired = true;
12787
+ onTrigger.callback();
12788
+ }
12639
12789
  }
12640
12790
  }
12641
12791
  if (buffer) {
@@ -12740,7 +12890,14 @@ async function main(args = process.argv.slice(2)) {
12740
12890
  stderr: "pipe"
12741
12891
  });
12742
12892
  children.push(be);
12743
- pipeWithPrefix(be.stdout, "[BE]");
12893
+ if (parsed.app) {
12894
+ pipeWithPrefix(be.stdout, "[BE]", {
12895
+ text: "Dev Dashboard API running at",
12896
+ callback: () => openAppMode(`http://localhost:${parsed.port}`)
12897
+ });
12898
+ } else {
12899
+ pipeWithPrefix(be.stdout, "[BE]");
12900
+ }
12744
12901
  pipeWithPrefix(be.stderr, "[BE]");
12745
12902
  await be.exited;
12746
12903
  }