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/README.md +1 -1
- package/backend/dist/server.js +785 -201
- package/bin/webmux.js +224 -67
- package/frontend/dist/assets/index-CKvK6kRL.js +148 -0
- package/frontend/dist/assets/index-N_4ol4EP.css +32 -0
- package/frontend/dist/icon.svg +1 -0
- package/frontend/dist/index.html +5 -7
- package/frontend/dist/manifest.webmanifest +16 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-CMdCoXme.js +0 -148
- package/frontend/dist/assets/index-nt-8CG4j.css +0 -32
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
10349
|
-
|
|
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
|
-
|
|
10406
|
+
log.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
|
|
10365
10407
|
if (stdout.trim())
|
|
10366
|
-
|
|
10408
|
+
log.debug(`[hook-runner] stdout: ${stdout.trim()}`);
|
|
10367
10409
|
if (stderr.trim())
|
|
10368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11640
|
+
log.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
|
|
11597
11641
|
return;
|
|
11598
11642
|
}
|
|
11599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11970
|
-
|
|
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
|
-
|
|
11979
|
-
|
|
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
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
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
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
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
|
|
12357
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|