webmux 0.18.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
@@ -9910,7 +9910,10 @@ function parseProjectConfig(parsed) {
9910
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) : []
9911
9911
  },
9912
9912
  linear: {
9913
- 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() } : {}
9914
9917
  }
9915
9918
  },
9916
9919
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
@@ -9920,21 +9923,39 @@ function parseProjectConfig(parsed) {
9920
9923
  function defaultConfig() {
9921
9924
  return parseProjectConfig({});
9922
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
+ }
9923
9943
  function loadLocalProjectConfigOverlay(root) {
9924
9944
  try {
9925
9945
  const text = readLocalConfigFile(root).trim();
9926
9946
  if (!text) {
9927
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
9947
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null };
9928
9948
  }
9929
9949
  const parsed = parseConfigDocument(text);
9930
9950
  const ws = isRecord3(parsed.workspace) ? parsed.workspace : null;
9931
9951
  return {
9932
9952
  worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
9933
9953
  profiles: parseProfiles(parsed.profiles, false),
9934
- lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks)
9954
+ lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
9955
+ linear: parseLocalLinearOverlay(parsed)
9935
9956
  };
9936
9957
  } catch {
9937
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
9958
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null };
9938
9959
  }
9939
9960
  }
9940
9961
  function mergeHookCommand(projectCommand, localCommand) {
@@ -9985,7 +10006,13 @@ function loadConfig(dir, options = {}) {
9985
10006
  ...cloneProfiles(projectConfig.profiles),
9986
10007
  ...cloneProfiles(localOverlay.profiles)
9987
10008
  },
9988
- 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
+ } : {}
9989
10016
  };
9990
10017
  }
9991
10018
  function expandTemplate(template, env) {
@@ -10016,7 +10043,7 @@ var init_config = __esm(() => {
10016
10043
  startupEnvs: {},
10017
10044
  integrations: {
10018
10045
  github: { linkedRepos: [] },
10019
- linear: { enabled: true }
10046
+ linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
10020
10047
  },
10021
10048
  lifecycleHooks: {},
10022
10049
  autoName: null
@@ -10360,8 +10387,8 @@ class BunLifecycleHookRunner {
10360
10387
  }
10361
10388
  async run(input) {
10362
10389
  const cmd = await this.buildCommand(input.cwd, input.command);
10363
- console.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
10364
- 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}`);
10365
10392
  const proc = Bun.spawn(cmd, {
10366
10393
  cwd: input.cwd,
10367
10394
  env: {
@@ -10376,17 +10403,19 @@ class BunLifecycleHookRunner {
10376
10403
  new Response(proc.stdout).text(),
10377
10404
  new Response(proc.stderr).text()
10378
10405
  ]);
10379
- console.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
10406
+ log.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
10380
10407
  if (stdout.trim())
10381
- console.debug(`[hook-runner] stdout: ${stdout.trim()}`);
10408
+ log.debug(`[hook-runner] stdout: ${stdout.trim()}`);
10382
10409
  if (stderr.trim())
10383
- console.debug(`[hook-runner] stderr: ${stderr.trim()}`);
10410
+ log.debug(`[hook-runner] stderr: ${stderr.trim()}`);
10384
10411
  if (exitCode !== 0) {
10385
10412
  throw new Error(buildErrorMessage(input.name, exitCode, stdout, stderr));
10386
10413
  }
10387
10414
  }
10388
10415
  }
10389
- var init_hooks = () => {};
10416
+ var init_hooks = __esm(() => {
10417
+ init_log();
10418
+ });
10390
10419
 
10391
10420
  // backend/src/adapters/port-probe.ts
10392
10421
  class BunPortProbe {
@@ -11229,7 +11258,7 @@ class LifecycleService {
11229
11258
  agent,
11230
11259
  phase: "reconciling"
11231
11260
  });
11232
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11261
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11233
11262
  return {
11234
11263
  branch,
11235
11264
  worktreeId: initialized.meta.worktreeId
@@ -11264,7 +11293,7 @@ class LifecycleService {
11264
11293
  worktreePath: resolved.entry.path,
11265
11294
  launchMode
11266
11295
  });
11267
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11296
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11268
11297
  return {
11269
11298
  branch,
11270
11299
  worktreeId: initialized.meta.worktreeId
@@ -11277,7 +11306,7 @@ class LifecycleService {
11277
11306
  try {
11278
11307
  const resolved = await this.resolveExistingWorktree(branch);
11279
11308
  this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
11280
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11309
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11281
11310
  } catch (error) {
11282
11311
  throw this.wrapOperationError(error);
11283
11312
  }
@@ -11603,15 +11632,15 @@ class LifecycleService {
11603
11632
  deleteBranch: true,
11604
11633
  deleteBranchForce: true
11605
11634
  }, this.deps.git);
11606
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11635
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
11607
11636
  }
11608
11637
  async runLifecycleHook(input) {
11609
- 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}`);
11610
11639
  if (!input.command || !input.meta) {
11611
- 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}`);
11612
11641
  return;
11613
11642
  }
11614
- 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}`);
11615
11644
  const dotenvValues = await loadDotenvLocal(input.worktreePath);
11616
11645
  await this.deps.hooks.run({
11617
11646
  name: input.name,
@@ -11621,7 +11650,7 @@ class LifecycleService {
11621
11650
  WEBMUX_WORKTREE_PATH: input.worktreePath
11622
11651
  }, dotenvValues)
11623
11652
  });
11624
- console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11653
+ log.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11625
11654
  }
11626
11655
  async reportCreateProgress(progress) {
11627
11656
  await this.deps.onCreateProgress?.(progress);
@@ -11645,6 +11674,7 @@ var init_lifecycle_service = __esm(() => {
11645
11674
  init_policies();
11646
11675
  init_session_service();
11647
11676
  init_worktree_service();
11677
+ init_log();
11648
11678
  LifecycleError = class LifecycleError extends Error {
11649
11679
  status;
11650
11680
  constructor(message, status2) {
@@ -11925,6 +11955,21 @@ var init_project_runtime = __esm(() => {
11925
11955
  init_tmux();
11926
11956
  });
11927
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
+
11928
11973
  // backend/src/services/reconciliation-service.ts
11929
11974
  import { basename as basename4, resolve as resolve6 } from "path";
11930
11975
  function makeUnmanagedWorktreeId(path) {
@@ -11967,11 +12012,34 @@ function resolveBranch(entry, metaBranch) {
11967
12012
 
11968
12013
  class ReconciliationService {
11969
12014
  deps;
11970
- constructor(deps2) {
12015
+ freshnessMs;
12016
+ now;
12017
+ concurrency;
12018
+ inFlight = null;
12019
+ lastReconciledAt = 0;
12020
+ constructor(deps2, options = {}) {
11971
12021
  this.deps = deps2;
12022
+ this.freshnessMs = options.freshnessMs ?? 500;
12023
+ this.now = options.now ?? Date.now;
12024
+ this.concurrency = options.concurrency ?? 4;
11972
12025
  }
11973
- 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
+ }
11974
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) {
11975
12043
  const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
11976
12044
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
11977
12045
  let windows = [];
@@ -11981,40 +12049,32 @@ class ReconciliationService {
11981
12049
  windows = [];
11982
12050
  }
11983
12051
  const seenWorktreeIds = new Set;
11984
- for (const entry of worktrees) {
11985
- if (entry.bare)
11986
- continue;
11987
- if (resolve6(entry.path) === normalizedRepoRoot)
11988
- continue;
12052
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve6(entry.path) !== normalizedRepoRoot);
12053
+ const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
11989
12054
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11990
12055
  const meta = await readWorktreeMeta(gitDir);
11991
12056
  const branch = resolveBranch(entry, meta?.branch ?? null);
11992
12057
  const worktreeId = meta?.worktreeId ?? makeUnmanagedWorktreeId(entry.path);
11993
- seenWorktreeIds.add(worktreeId);
11994
- this.deps.runtime.upsertWorktree({
12058
+ const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
12059
+ const window = findWindow(windows, sessionName, branch);
12060
+ return {
11995
12061
  worktreeId,
11996
12062
  branch,
11997
12063
  path: entry.path,
11998
12064
  profile: meta?.profile ?? null,
11999
12065
  agentName: meta?.agent ?? null,
12000
- runtime: meta?.runtime ?? "host"
12001
- });
12002
- const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
12003
- this.deps.runtime.setGitState(worktreeId, {
12004
- exists: true,
12005
- branch,
12006
- dirty: gitStatus.dirty,
12007
- aheadCount: gitStatus.aheadCount,
12008
- currentCommit: gitStatus.currentCommit
12009
- });
12010
- const window = findWindow(windows, sessionName, branch);
12011
- this.deps.runtime.setSessionState(worktreeId, {
12012
- exists: window !== null,
12013
- sessionName: window?.sessionName ?? null,
12014
- paneCount: window?.paneCount ?? 0
12015
- });
12016
- if (meta) {
12017
- 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, {
12018
12078
  allocatedPorts: meta.allocatedPorts,
12019
12079
  startupEnvValues: meta.startupEnvValues,
12020
12080
  worktreeId: meta.worktreeId,
@@ -12022,11 +12082,34 @@ class ReconciliationService {
12022
12082
  profile: meta.profile,
12023
12083
  agent: meta.agent,
12024
12084
  runtime: meta.runtime
12025
- }));
12026
- } else {
12027
- this.deps.runtime.setServices(worktreeId, []);
12028
- }
12029
- 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);
12030
12113
  }
12031
12114
  for (const state of this.deps.runtime.listWorktrees()) {
12032
12115
  if (!seenWorktreeIds.has(state.worktreeId)) {
@@ -12148,13 +12231,14 @@ function getWorktreeCommandUsage(command) {
12148
12231
  case "add":
12149
12232
  return [
12150
12233
  "Usage:",
12151
- " 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]",
12152
12235
  "",
12153
12236
  "Options:",
12154
12237
  " --profile <name> Worktree profile from .webmux.yaml",
12155
12238
  " --agent <claude|codex> Agent to launch in the worktree",
12156
12239
  " --prompt <text> Initial agent prompt",
12157
12240
  " --env KEY=VALUE Runtime env override (repeatable)",
12241
+ " -d, --detach Create worktree without switching to it",
12158
12242
  " --help Show this help message"
12159
12243
  ].join(`
12160
12244
  `);
@@ -12208,6 +12292,7 @@ function parseAgent(value) {
12208
12292
  function parseAddCommandArgs(args) {
12209
12293
  const input = {};
12210
12294
  const envOverrides = {};
12295
+ let detach = false;
12211
12296
  for (let index = 0;index < args.length; index++) {
12212
12297
  const arg = args[index];
12213
12298
  if (!arg)
@@ -12215,6 +12300,10 @@ function parseAddCommandArgs(args) {
12215
12300
  if (arg === "--help" || arg === "-h") {
12216
12301
  return null;
12217
12302
  }
12303
+ if (arg === "--detach" || arg === "-d") {
12304
+ detach = true;
12305
+ continue;
12306
+ }
12218
12307
  if (arg === "--profile" || arg.startsWith("--profile=")) {
12219
12308
  const { value, nextIndex } = readOptionValue(args, index, "--profile");
12220
12309
  input.profile = value;
@@ -12254,7 +12343,7 @@ function parseAddCommandArgs(args) {
12254
12343
  if (Object.keys(envOverrides).length > 0) {
12255
12344
  input.envOverrides = envOverrides;
12256
12345
  }
12257
- return input;
12346
+ return { input, detach };
12258
12347
  }
12259
12348
  function parseBranchCommandArgs(args) {
12260
12349
  let branch = null;
@@ -12368,8 +12457,8 @@ async function runWorktreeCommand(context, deps2 = {}) {
12368
12457
  const confirmPrune = deps2.confirmPrune ?? defaultConfirmPrune;
12369
12458
  try {
12370
12459
  if (context.command === "add") {
12371
- const input = parseAddCommandArgs(context.args);
12372
- if (!input) {
12460
+ const parsed = parseAddCommandArgs(context.args);
12461
+ if (!parsed) {
12373
12462
  stdout(getWorktreeCommandUsage("add"));
12374
12463
  return 0;
12375
12464
  }
@@ -12377,9 +12466,11 @@ async function runWorktreeCommand(context, deps2 = {}) {
12377
12466
  projectDir: context.projectDir,
12378
12467
  port: context.port
12379
12468
  });
12380
- const result = await runtime2.lifecycleService.createWorktree(input);
12469
+ const result = await runtime2.lifecycleService.createWorktree(parsed.input);
12381
12470
  stdout(`Created worktree ${result.branch}`);
12382
- switchToTmuxWindow(runtime2.projectDir, result.branch);
12471
+ if (!parsed.detach) {
12472
+ switchToTmuxWindow(runtime2.projectDir, result.branch);
12473
+ }
12383
12474
  return 0;
12384
12475
  }
12385
12476
  if (context.command === "list") {
@@ -12472,7 +12563,7 @@ import { fileURLToPath } from "url";
12472
12563
  // package.json
12473
12564
  var package_default = {
12474
12565
  name: "webmux",
12475
- version: "0.18.0",
12566
+ version: "0.19.0",
12476
12567
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
12477
12568
  type: "module",
12478
12569
  repository: {
@@ -12532,7 +12623,7 @@ function usage2() {
12532
12623
  webmux \u2014 Dev dashboard for managing Git worktrees
12533
12624
 
12534
12625
  Usage:
12535
- webmux serve Start the dashboard server
12626
+ webmux serve Start the dashboard server (--app opens in app mode)
12536
12627
  webmux init Interactive project setup
12537
12628
  webmux service Manage webmux as a system service
12538
12629
  webmux update Update webmux to the latest version
@@ -12547,6 +12638,7 @@ Usage:
12547
12638
 
12548
12639
  Options:
12549
12640
  --port N Set port (default: 5111)
12641
+ --app Open dashboard in browser app mode (minimal window)
12550
12642
  --debug Show debug-level logs
12551
12643
  --version Show version number
12552
12644
  --help Show this help message
@@ -12559,11 +12651,12 @@ function isRootCommand(value) {
12559
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";
12560
12652
  }
12561
12653
  function isServeRootOption(value) {
12562
- 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";
12563
12655
  }
12564
12656
  function parseRootArgs(args) {
12565
12657
  let port = parseInt(process.env.PORT || "5111", 10);
12566
12658
  let debug = false;
12659
+ let app = false;
12567
12660
  let command = null;
12568
12661
  const commandArgs = [];
12569
12662
  for (let index = 0;index < args.length; index++) {
@@ -12587,6 +12680,9 @@ function parseRootArgs(args) {
12587
12680
  index += 1;
12588
12681
  break;
12589
12682
  }
12683
+ case "--app":
12684
+ app = true;
12685
+ break;
12590
12686
  case "--debug":
12591
12687
  debug = true;
12592
12688
  break;
@@ -12610,6 +12706,7 @@ Run webmux --help for usage.`);
12610
12706
  return {
12611
12707
  port,
12612
12708
  debug,
12709
+ app,
12613
12710
  command,
12614
12711
  commandArgs
12615
12712
  };
@@ -12636,10 +12733,44 @@ async function loadEnvFile(path) {
12636
12733
  }
12637
12734
  }
12638
12735
  }
12639
- 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) {
12640
12770
  const reader = stream.getReader();
12641
12771
  const decoder = new TextDecoder;
12642
12772
  let buffer = "";
12773
+ let fired = false;
12643
12774
  (async () => {
12644
12775
  while (true) {
12645
12776
  const { done, value } = await reader.read();
@@ -12651,6 +12782,10 @@ function pipeWithPrefix(stream, prefix) {
12651
12782
  buffer = lines.pop();
12652
12783
  for (const line of lines) {
12653
12784
  console.log(`${prefix} ${line}`);
12785
+ if (onTrigger && !fired && line.includes(onTrigger.text)) {
12786
+ fired = true;
12787
+ onTrigger.callback();
12788
+ }
12654
12789
  }
12655
12790
  }
12656
12791
  if (buffer) {
@@ -12755,7 +12890,14 @@ async function main(args = process.argv.slice(2)) {
12755
12890
  stderr: "pipe"
12756
12891
  });
12757
12892
  children.push(be);
12758
- 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
+ }
12759
12901
  pipeWithPrefix(be.stderr, "[BE]");
12760
12902
  await be.exited;
12761
12903
  }