webmux 0.39.0 → 0.40.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 +91 -21
- package/bin/webmux.js +139 -19
- package/package.json +1 -1
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 randomUUID4 } from "crypto";
|
|
6962
|
-
import { join as join12, resolve as
|
|
6962
|
+
import { join as join12, resolve as resolve10 } from "path";
|
|
6963
6963
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } 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.40.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+aea371d0c1c4fd29/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
|
};
|
|
@@ -12395,6 +12395,7 @@ import { join } from "path";
|
|
|
12395
12395
|
// backend/src/domain/model.ts
|
|
12396
12396
|
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
12397
12397
|
var WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
12398
|
+
var OPEN_SESSIONS_STATE_VERSION = 1;
|
|
12398
12399
|
var ROOT_TAB_ID = "root";
|
|
12399
12400
|
function conversationSessionId(conversation) {
|
|
12400
12401
|
if (!conversation)
|
|
@@ -12456,6 +12457,9 @@ function getWorktreeStoragePaths(gitDir) {
|
|
|
12456
12457
|
function getProjectArchiveStatePath(gitDir) {
|
|
12457
12458
|
return join(gitDir, "webmux", "archive.json");
|
|
12458
12459
|
}
|
|
12460
|
+
function getProjectOpenSessionsStatePath(gitDir) {
|
|
12461
|
+
return join(gitDir, "webmux", "open-sessions.json");
|
|
12462
|
+
}
|
|
12459
12463
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
12460
12464
|
const paths = getWorktreeStoragePaths(gitDir);
|
|
12461
12465
|
await mkdir2(paths.webmuxDir, { recursive: true });
|
|
@@ -12505,6 +12509,12 @@ async function writeWorktreeArchiveState(gitDir, state) {
|
|
|
12505
12509
|
await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
|
|
12506
12510
|
`);
|
|
12507
12511
|
}
|
|
12512
|
+
async function writeOpenSessionsState(gitDir, state) {
|
|
12513
|
+
const statePath = getProjectOpenSessionsStatePath(gitDir);
|
|
12514
|
+
await ensureWorktreeStorageDirs(gitDir);
|
|
12515
|
+
await Bun.write(statePath, JSON.stringify(state, null, 2) + `
|
|
12516
|
+
`);
|
|
12517
|
+
}
|
|
12508
12518
|
function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
|
|
12509
12519
|
return {
|
|
12510
12520
|
...dotenvValues,
|
|
@@ -18304,9 +18314,13 @@ class LifecycleService {
|
|
|
18304
18314
|
async pruneWorktrees() {
|
|
18305
18315
|
try {
|
|
18306
18316
|
const resolvedWorktrees = await this.resolveAllWorktrees();
|
|
18317
|
+
const sessionName = buildProjectSessionName(this.deps.projectRoot);
|
|
18307
18318
|
const removedBranches = [];
|
|
18308
18319
|
for (const resolved of resolvedWorktrees) {
|
|
18309
18320
|
const branch = resolved.entry.branch ?? resolved.entry.path;
|
|
18321
|
+
if (this.deps.tmux.hasWindow(sessionName, buildWorktreeWindowName(branch))) {
|
|
18322
|
+
continue;
|
|
18323
|
+
}
|
|
18310
18324
|
await this.removeResolvedWorktree(resolved);
|
|
18311
18325
|
removedBranches.push(branch);
|
|
18312
18326
|
}
|
|
@@ -19601,6 +19615,58 @@ function startAutoPullMonitor(deps, intervalMs) {
|
|
|
19601
19615
|
return startSerializedInterval(run2, intervalMs);
|
|
19602
19616
|
}
|
|
19603
19617
|
|
|
19618
|
+
// backend/src/services/session-restore-service.ts
|
|
19619
|
+
import { basename as basename5, resolve as resolve8 } from "path";
|
|
19620
|
+
function entryBranch(entry) {
|
|
19621
|
+
return entry.branch ?? basename5(entry.path);
|
|
19622
|
+
}
|
|
19623
|
+
function computeOpenBranches(input) {
|
|
19624
|
+
const resolvedProjectDir = resolve8(input.projectDir);
|
|
19625
|
+
const openWindowNames = new Set(input.windows.filter((window) => window.sessionName === input.sessionName).map((window) => window.windowName));
|
|
19626
|
+
return input.worktrees.filter((entry) => !entry.bare && resolve8(entry.path) !== resolvedProjectDir).map((entry) => entryBranch(entry)).filter((branch) => openWindowNames.has(buildWorktreeWindowName(branch))).sort((left, right) => left.localeCompare(right));
|
|
19627
|
+
}
|
|
19628
|
+
function buildOpenSessionsState(branches, savedAt) {
|
|
19629
|
+
return {
|
|
19630
|
+
schemaVersion: OPEN_SESSIONS_STATE_VERSION,
|
|
19631
|
+
savedAt: savedAt.toISOString(),
|
|
19632
|
+
branches
|
|
19633
|
+
};
|
|
19634
|
+
}
|
|
19635
|
+
var DEFAULT_SESSION_SNAPSHOT_INTERVAL_MS = 30000;
|
|
19636
|
+
async function saveOpenSessionsSnapshot(deps) {
|
|
19637
|
+
const projectRoot2 = resolve8(deps.projectRoot);
|
|
19638
|
+
const sessionName = buildProjectSessionName(projectRoot2);
|
|
19639
|
+
let windows = [];
|
|
19640
|
+
try {
|
|
19641
|
+
windows = deps.tmux.listWindows();
|
|
19642
|
+
} catch {
|
|
19643
|
+
windows = [];
|
|
19644
|
+
}
|
|
19645
|
+
const worktrees = deps.git.listLiveWorktrees(projectRoot2);
|
|
19646
|
+
const branches = computeOpenBranches({ worktrees, windows, sessionName, projectDir: projectRoot2 });
|
|
19647
|
+
if (branches.length === 0)
|
|
19648
|
+
return null;
|
|
19649
|
+
const now = deps.now ?? (() => new Date);
|
|
19650
|
+
const writeState = deps.writeState ?? writeOpenSessionsState;
|
|
19651
|
+
const gitDir = deps.git.resolveWorktreeGitDir(projectRoot2);
|
|
19652
|
+
await writeState(gitDir, buildOpenSessionsState(branches, now()));
|
|
19653
|
+
return branches;
|
|
19654
|
+
}
|
|
19655
|
+
function startSessionSnapshotMonitor(deps, intervalMs = DEFAULT_SESSION_SNAPSHOT_INTERVAL_MS) {
|
|
19656
|
+
log.info(`[session-snapshot] monitor started (interval: ${intervalMs}ms)`);
|
|
19657
|
+
const run2 = async () => {
|
|
19658
|
+
try {
|
|
19659
|
+
const branches = await saveOpenSessionsSnapshot(deps);
|
|
19660
|
+
if (branches) {
|
|
19661
|
+
log.debug(`[session-snapshot] saved ${branches.length} open session(s): ${branches.join(", ")}`);
|
|
19662
|
+
}
|
|
19663
|
+
} catch (error) {
|
|
19664
|
+
log.warn(`[session-snapshot] save failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
19665
|
+
}
|
|
19666
|
+
};
|
|
19667
|
+
return startSerializedInterval(run2, intervalMs);
|
|
19668
|
+
}
|
|
19669
|
+
|
|
19604
19670
|
// backend/src/services/agents-ui-service.ts
|
|
19605
19671
|
function cloneConversationMeta(meta) {
|
|
19606
19672
|
return meta ? { ...meta } : null;
|
|
@@ -21660,7 +21726,7 @@ class BunPortProbe {
|
|
|
21660
21726
|
this.hostnames = hostnames;
|
|
21661
21727
|
}
|
|
21662
21728
|
isListening(port) {
|
|
21663
|
-
return new Promise((
|
|
21729
|
+
return new Promise((resolve9) => {
|
|
21664
21730
|
let settled = false;
|
|
21665
21731
|
let pending = this.hostnames.length;
|
|
21666
21732
|
const settle = (result) => {
|
|
@@ -21669,20 +21735,20 @@ class BunPortProbe {
|
|
|
21669
21735
|
if (result) {
|
|
21670
21736
|
settled = true;
|
|
21671
21737
|
clearTimeout(timer);
|
|
21672
|
-
|
|
21738
|
+
resolve9(true);
|
|
21673
21739
|
return;
|
|
21674
21740
|
}
|
|
21675
21741
|
pending--;
|
|
21676
21742
|
if (pending === 0) {
|
|
21677
21743
|
settled = true;
|
|
21678
21744
|
clearTimeout(timer);
|
|
21679
|
-
|
|
21745
|
+
resolve9(false);
|
|
21680
21746
|
}
|
|
21681
21747
|
};
|
|
21682
21748
|
const timer = setTimeout(() => {
|
|
21683
21749
|
if (!settled) {
|
|
21684
21750
|
settled = true;
|
|
21685
|
-
|
|
21751
|
+
resolve9(false);
|
|
21686
21752
|
}
|
|
21687
21753
|
}, this.timeoutMs);
|
|
21688
21754
|
for (const hostname of this.hostnames) {
|
|
@@ -21832,8 +21898,8 @@ class ArchiveStateService {
|
|
|
21832
21898
|
async withMutationLock(operation) {
|
|
21833
21899
|
const previous = this.mutationQueue;
|
|
21834
21900
|
let release = () => {};
|
|
21835
|
-
this.mutationQueue = new Promise((
|
|
21836
|
-
release =
|
|
21901
|
+
this.mutationQueue = new Promise((resolve9) => {
|
|
21902
|
+
release = resolve9;
|
|
21837
21903
|
});
|
|
21838
21904
|
await previous.catch(() => {});
|
|
21839
21905
|
try {
|
|
@@ -22136,9 +22202,9 @@ class ProjectRuntime {
|
|
|
22136
22202
|
}
|
|
22137
22203
|
|
|
22138
22204
|
// backend/src/services/reconciliation-service.ts
|
|
22139
|
-
import { basename as
|
|
22205
|
+
import { basename as basename6, resolve as resolve9 } from "path";
|
|
22140
22206
|
function makeUnmanagedWorktreeId(path) {
|
|
22141
|
-
return `unmanaged:${
|
|
22207
|
+
return `unmanaged:${resolve9(path)}`;
|
|
22142
22208
|
}
|
|
22143
22209
|
function isValidPort2(port) {
|
|
22144
22210
|
return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
@@ -22171,7 +22237,7 @@ function findWindow(windows, sessionName, branch) {
|
|
|
22171
22237
|
return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
|
|
22172
22238
|
}
|
|
22173
22239
|
function resolveBranch(entry, metaBranch) {
|
|
22174
|
-
const fallback =
|
|
22240
|
+
const fallback = basename6(entry.path);
|
|
22175
22241
|
return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
|
|
22176
22242
|
}
|
|
22177
22243
|
|
|
@@ -22195,7 +22261,7 @@ class ReconciliationService {
|
|
|
22195
22261
|
if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
|
|
22196
22262
|
return;
|
|
22197
22263
|
}
|
|
22198
|
-
const normalizedRepoRoot =
|
|
22264
|
+
const normalizedRepoRoot = resolve9(repoRoot);
|
|
22199
22265
|
const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
|
|
22200
22266
|
this.lastReconciledAt = this.now();
|
|
22201
22267
|
});
|
|
@@ -22214,7 +22280,7 @@ class ReconciliationService {
|
|
|
22214
22280
|
windows = [];
|
|
22215
22281
|
}
|
|
22216
22282
|
const seenWorktreeIds = new Set;
|
|
22217
|
-
const candidateEntries = worktrees.filter((entry) => !entry.bare &&
|
|
22283
|
+
const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve9(entry.path) !== normalizedRepoRoot);
|
|
22218
22284
|
const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
|
|
22219
22285
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
22220
22286
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -22689,6 +22755,7 @@ function createProjectApp(runtime, instancePrefix) {
|
|
|
22689
22755
|
let stopPrMonitor = null;
|
|
22690
22756
|
let stopOneshotWatcher = null;
|
|
22691
22757
|
let stopAutoPullMonitor = null;
|
|
22758
|
+
let stopSessionSnapshot = null;
|
|
22692
22759
|
async function runOneshotForIssue(issueId) {
|
|
22693
22760
|
const seed = await buildSeedFromLinear({ issueId }, defaultSeedFromLinearDeps);
|
|
22694
22761
|
if (!seed.ok) {
|
|
@@ -22798,7 +22865,7 @@ function createProjectApp(runtime, instancePrefix) {
|
|
|
22798
22865
|
startupEnvs: config.startupEnvs,
|
|
22799
22866
|
linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
|
|
22800
22867
|
alias: lr.alias,
|
|
22801
|
-
...lr.dir ? { dir:
|
|
22868
|
+
...lr.dir ? { dir: resolve10(PROJECT_DIR, lr.dir) } : {}
|
|
22802
22869
|
})),
|
|
22803
22870
|
linearAutoCreateWorktrees: linearAutoCreateEnabled,
|
|
22804
22871
|
autoRemoveOnMerge: autoRemoveOnMergeEnabled,
|
|
@@ -22966,9 +23033,9 @@ function createProjectApp(runtime, instancePrefix) {
|
|
|
22966
23033
|
}
|
|
22967
23034
|
async function getWorktreeGitDirs() {
|
|
22968
23035
|
const gitDirs = new Map;
|
|
22969
|
-
const projectRoot2 =
|
|
23036
|
+
const projectRoot2 = resolve10(PROJECT_DIR);
|
|
22970
23037
|
for (const entry of git.listLiveWorktrees(projectRoot2)) {
|
|
22971
|
-
if (entry.bare ||
|
|
23038
|
+
if (entry.bare || resolve10(entry.path) === projectRoot2 || !entry.branch)
|
|
22972
23039
|
continue;
|
|
22973
23040
|
gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
|
|
22974
23041
|
}
|
|
@@ -23763,7 +23830,7 @@ ${resolvedPrompt}` : conversationContext;
|
|
|
23763
23830
|
return errorResponse(`Unknown linked repo: ${repo}`, 404);
|
|
23764
23831
|
if (!linkedRepo.dir)
|
|
23765
23832
|
return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
|
|
23766
|
-
const resolvedDir =
|
|
23833
|
+
const resolvedDir = resolve10(PROJECT_DIR, linkedRepo.dir);
|
|
23767
23834
|
const repoRoot = git.resolveRepoRoot(resolvedDir);
|
|
23768
23835
|
if (!repoRoot)
|
|
23769
23836
|
return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
|
|
@@ -23928,7 +23995,7 @@ ${resolvedPrompt}` : conversationContext;
|
|
|
23928
23995
|
}
|
|
23929
23996
|
const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
|
|
23930
23997
|
const destPath = join12(uploadDir, safeName);
|
|
23931
|
-
if (!
|
|
23998
|
+
if (!resolve10(destPath).startsWith(uploadDir + "/")) {
|
|
23932
23999
|
return errorResponse("Invalid filename", 400);
|
|
23933
24000
|
}
|
|
23934
24001
|
await Bun.write(destPath, entry);
|
|
@@ -24383,6 +24450,7 @@ ${resolvedPrompt}` : conversationContext;
|
|
|
24383
24450
|
if (config.workspace.autoPull.enabled) {
|
|
24384
24451
|
stopAutoPullMonitor = startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
|
|
24385
24452
|
}
|
|
24453
|
+
stopSessionSnapshot = startSessionSnapshotMonitor({ git, tmux, projectRoot: PROJECT_DIR });
|
|
24386
24454
|
}
|
|
24387
24455
|
function stopLight() {
|
|
24388
24456
|
stopPrMonitor?.();
|
|
@@ -24392,6 +24460,8 @@ ${resolvedPrompt}` : conversationContext;
|
|
|
24392
24460
|
stopOneshotWatcher = null;
|
|
24393
24461
|
stopAutoPullMonitor?.();
|
|
24394
24462
|
stopAutoPullMonitor = null;
|
|
24463
|
+
stopSessionSnapshot?.();
|
|
24464
|
+
stopSessionSnapshot = null;
|
|
24395
24465
|
}
|
|
24396
24466
|
return { prefix: instancePrefix, routes, wsHandlers, startLight, stopLight };
|
|
24397
24467
|
}
|
|
@@ -24565,8 +24635,8 @@ async function globalFetch(req) {
|
|
|
24565
24635
|
if (STATIC_DIR) {
|
|
24566
24636
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
24567
24637
|
const filePath = join12(STATIC_DIR, rawPath);
|
|
24568
|
-
const staticRoot =
|
|
24569
|
-
if (!
|
|
24638
|
+
const staticRoot = resolve10(STATIC_DIR);
|
|
24639
|
+
if (!resolve10(filePath).startsWith(staticRoot + "/")) {
|
|
24570
24640
|
return new Response("Forbidden", { status: 403 });
|
|
24571
24641
|
}
|
|
24572
24642
|
const file = Bun.file(filePath);
|
package/bin/webmux.js
CHANGED
|
@@ -473,7 +473,8 @@ _webmux() {
|
|
|
473
473
|
'remove:Remove a worktree'
|
|
474
474
|
'merge:Merge a worktree into main'
|
|
475
475
|
'send:Send a prompt to a running worktree agent'
|
|
476
|
-
'prune:Remove all worktrees in the current project'
|
|
476
|
+
'prune:Remove all closed (not open) worktrees in the current project'
|
|
477
|
+
'restore:Re-open all worktree sessions that were open before'
|
|
477
478
|
'linear:Post a worktree conversation to a Linear issue/team'
|
|
478
479
|
'project:List, add, or remove projects served by the dashboard'
|
|
479
480
|
'completion:Generate shell completion script'
|
|
@@ -543,7 +544,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
543
544
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
544
545
|
|
|
545
546
|
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
546
|
-
COMPREPLY=($(compgen -W "serve init service update add oneshot list open close refresh archive unarchive label remove merge send prune linear project completion" -- "\${cur}"))
|
|
547
|
+
COMPREPLY=($(compgen -W "serve init service update add oneshot list open close refresh archive unarchive label remove merge send prune restore linear project completion" -- "\${cur}"))
|
|
547
548
|
return
|
|
548
549
|
fi
|
|
549
550
|
|
|
@@ -5996,7 +5997,7 @@ var init_zod = __esm(() => {
|
|
|
5996
5997
|
init_external();
|
|
5997
5998
|
});
|
|
5998
5999
|
|
|
5999
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
6000
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+aea371d0c1c4fd29/node_modules/@ts-rest/core/index.esm.mjs
|
|
6000
6001
|
var isZodType = (obj) => {
|
|
6001
6002
|
return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
|
|
6002
6003
|
}, isZodObjectStrict = (obj) => {
|
|
@@ -11317,7 +11318,7 @@ function conversationSessionId(conversation) {
|
|
|
11317
11318
|
return null;
|
|
11318
11319
|
return conversation.provider === "codexAppServer" ? conversation.threadId : conversation.sessionId;
|
|
11319
11320
|
}
|
|
11320
|
-
var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1, ROOT_TAB_ID = "root";
|
|
11321
|
+
var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1, OPEN_SESSIONS_STATE_VERSION = 1, ROOT_TAB_ID = "root";
|
|
11321
11322
|
|
|
11322
11323
|
// backend/src/adapters/fs.ts
|
|
11323
11324
|
import { mkdir } from "fs/promises";
|
|
@@ -11373,6 +11374,9 @@ function getWorktreeStoragePaths(gitDir) {
|
|
|
11373
11374
|
function getProjectArchiveStatePath(gitDir) {
|
|
11374
11375
|
return join8(gitDir, "webmux", "archive.json");
|
|
11375
11376
|
}
|
|
11377
|
+
function getProjectOpenSessionsStatePath(gitDir) {
|
|
11378
|
+
return join8(gitDir, "webmux", "open-sessions.json");
|
|
11379
|
+
}
|
|
11376
11380
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
11377
11381
|
const paths = getWorktreeStoragePaths(gitDir);
|
|
11378
11382
|
await mkdir(paths.webmuxDir, { recursive: true });
|
|
@@ -11422,6 +11426,29 @@ async function writeWorktreeArchiveState(gitDir, state) {
|
|
|
11422
11426
|
await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
|
|
11423
11427
|
`);
|
|
11424
11428
|
}
|
|
11429
|
+
function emptyOpenSessionsState() {
|
|
11430
|
+
return {
|
|
11431
|
+
schemaVersion: OPEN_SESSIONS_STATE_VERSION,
|
|
11432
|
+
savedAt: "",
|
|
11433
|
+
branches: []
|
|
11434
|
+
};
|
|
11435
|
+
}
|
|
11436
|
+
function isOpenSessionsState(raw) {
|
|
11437
|
+
return isRecord3(raw) && typeof raw.schemaVersion === "number" && typeof raw.savedAt === "string" && Array.isArray(raw.branches) && raw.branches.every((branch) => typeof branch === "string");
|
|
11438
|
+
}
|
|
11439
|
+
async function readOpenSessionsState(gitDir) {
|
|
11440
|
+
const statePath = getProjectOpenSessionsStatePath(gitDir);
|
|
11441
|
+
try {
|
|
11442
|
+
const raw = await Bun.file(statePath).json();
|
|
11443
|
+
return isOpenSessionsState(raw) ? {
|
|
11444
|
+
schemaVersion: raw.schemaVersion,
|
|
11445
|
+
savedAt: raw.savedAt,
|
|
11446
|
+
branches: [...raw.branches]
|
|
11447
|
+
} : emptyOpenSessionsState();
|
|
11448
|
+
} catch {
|
|
11449
|
+
return emptyOpenSessionsState();
|
|
11450
|
+
}
|
|
11451
|
+
}
|
|
11425
11452
|
function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
|
|
11426
11453
|
return {
|
|
11427
11454
|
...dotenvValues,
|
|
@@ -21289,9 +21316,13 @@ class LifecycleService {
|
|
|
21289
21316
|
async pruneWorktrees() {
|
|
21290
21317
|
try {
|
|
21291
21318
|
const resolvedWorktrees = await this.resolveAllWorktrees();
|
|
21319
|
+
const sessionName = buildProjectSessionName(this.deps.projectRoot);
|
|
21292
21320
|
const removedBranches = [];
|
|
21293
21321
|
for (const resolved of resolvedWorktrees) {
|
|
21294
21322
|
const branch = resolved.entry.branch ?? resolved.entry.path;
|
|
21323
|
+
if (this.deps.tmux.hasWindow(sessionName, buildWorktreeWindowName(branch))) {
|
|
21324
|
+
continue;
|
|
21325
|
+
}
|
|
21295
21326
|
await this.removeResolvedWorktree(resolved);
|
|
21296
21327
|
removedBranches.push(branch);
|
|
21297
21328
|
}
|
|
@@ -22544,6 +22575,11 @@ function getWorktreeCommandUsage(command) {
|
|
|
22544
22575
|
case "prune":
|
|
22545
22576
|
return `Usage:
|
|
22546
22577
|
webmux prune`;
|
|
22578
|
+
case "restore":
|
|
22579
|
+
return `Usage:
|
|
22580
|
+
webmux restore
|
|
22581
|
+
|
|
22582
|
+
Re-open every worktree session that was open the last time sessions were saved.`;
|
|
22547
22583
|
case "tab":
|
|
22548
22584
|
return [
|
|
22549
22585
|
"Usage:",
|
|
@@ -22844,6 +22880,18 @@ function parsePruneCommandArgs(args) {
|
|
|
22844
22880
|
}
|
|
22845
22881
|
return true;
|
|
22846
22882
|
}
|
|
22883
|
+
function parseRestoreCommandArgs(args) {
|
|
22884
|
+
for (const arg of args) {
|
|
22885
|
+
if (arg === "--help" || arg === "-h") {
|
|
22886
|
+
return false;
|
|
22887
|
+
}
|
|
22888
|
+
if (arg.startsWith("-")) {
|
|
22889
|
+
throw new CommandUsageError(`Unknown option: ${arg}`);
|
|
22890
|
+
}
|
|
22891
|
+
throw new CommandUsageError(`Unexpected argument: ${arg}`);
|
|
22892
|
+
}
|
|
22893
|
+
return true;
|
|
22894
|
+
}
|
|
22847
22895
|
function parseListCommandArgs(args) {
|
|
22848
22896
|
let mode = "active";
|
|
22849
22897
|
let search = "";
|
|
@@ -22882,9 +22930,19 @@ function listProjectWorktrees(runtime) {
|
|
|
22882
22930
|
const projectDir = resolve12(runtime.projectDir);
|
|
22883
22931
|
return runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve12(entry.path) !== projectDir);
|
|
22884
22932
|
}
|
|
22933
|
+
function buildOpenWorktreeWindowSet(runtime) {
|
|
22934
|
+
const sessionName = buildProjectSessionName(resolve12(runtime.projectDir));
|
|
22935
|
+
let windows = [];
|
|
22936
|
+
try {
|
|
22937
|
+
windows = runtime.tmux.listWindows();
|
|
22938
|
+
} catch {
|
|
22939
|
+
windows = [];
|
|
22940
|
+
}
|
|
22941
|
+
return new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
|
|
22942
|
+
}
|
|
22885
22943
|
async function defaultConfirmPrune(worktreeCount) {
|
|
22886
22944
|
const response = await confirm({
|
|
22887
|
-
message: `Prune
|
|
22945
|
+
message: `Prune ${worktreeCount} closed worktree${worktreeCount === 1 ? "" : "s"}? This action cannot be undone.`,
|
|
22888
22946
|
initialValue: false
|
|
22889
22947
|
});
|
|
22890
22948
|
return !isCancel(response) && response;
|
|
@@ -22928,14 +22986,7 @@ async function listWorktrees(runtime, stdout2, options) {
|
|
|
22928
22986
|
stdout2("No worktrees found.");
|
|
22929
22987
|
return;
|
|
22930
22988
|
}
|
|
22931
|
-
const
|
|
22932
|
-
let windows = [];
|
|
22933
|
-
try {
|
|
22934
|
-
windows = runtime.tmux.listWindows();
|
|
22935
|
-
} catch {
|
|
22936
|
-
windows = [];
|
|
22937
|
-
}
|
|
22938
|
-
const openWindows = new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
|
|
22989
|
+
const openWindows = buildOpenWorktreeWindowSet(runtime);
|
|
22939
22990
|
const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
|
|
22940
22991
|
const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
|
|
22941
22992
|
const rows = await Promise.all(entries.map(async (entry) => {
|
|
@@ -23000,6 +23051,7 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
23000
23051
|
const resolveBaseUrl = deps2.resolveBaseUrl ?? resolveProjectBaseUrl;
|
|
23001
23052
|
const switchToTmuxWindow = deps2.switchToTmuxWindow ?? defaultSwitchToTmuxWindow;
|
|
23002
23053
|
const confirmPrune = deps2.confirmPrune ?? defaultConfirmPrune;
|
|
23054
|
+
const readOpenSessions = deps2.readOpenSessions ?? readOpenSessionsState;
|
|
23003
23055
|
try {
|
|
23004
23056
|
if (context.command === "add") {
|
|
23005
23057
|
const parsed = parseAddCommandArgs(context.args);
|
|
@@ -23078,18 +23130,85 @@ ${parsed.input.prompt}` : seed.data.conversationMarkdown;
|
|
|
23078
23130
|
stdout2("No worktrees found.");
|
|
23079
23131
|
return 0;
|
|
23080
23132
|
}
|
|
23081
|
-
|
|
23133
|
+
const openWindows = buildOpenWorktreeWindowSet(runtime2);
|
|
23134
|
+
const closedWorktrees = worktrees.filter((entry) => !openWindows.has(buildWorktreeWindowName(entry.branch ?? basename7(entry.path))));
|
|
23135
|
+
if (closedWorktrees.length === 0) {
|
|
23136
|
+
stdout2("No closed worktrees to prune.");
|
|
23137
|
+
return 0;
|
|
23138
|
+
}
|
|
23139
|
+
if (!await confirmPrune(closedWorktrees.length)) {
|
|
23082
23140
|
stdout2("Aborted.");
|
|
23083
23141
|
return 0;
|
|
23084
23142
|
}
|
|
23085
23143
|
const result = await runtime2.lifecycleService.pruneWorktrees();
|
|
23086
23144
|
if (result.removedBranches.length === 0) {
|
|
23087
|
-
stdout2("No worktrees
|
|
23145
|
+
stdout2("No closed worktrees to prune.");
|
|
23088
23146
|
return 0;
|
|
23089
23147
|
}
|
|
23090
23148
|
stdout2(`Pruned ${result.removedBranches.length} worktree${result.removedBranches.length === 1 ? "" : "s"}: ${result.removedBranches.join(", ")}`);
|
|
23091
23149
|
return 0;
|
|
23092
23150
|
}
|
|
23151
|
+
if (context.command === "restore") {
|
|
23152
|
+
if (!parseRestoreCommandArgs(context.args)) {
|
|
23153
|
+
stdout2(getWorktreeCommandUsage("restore"));
|
|
23154
|
+
return 0;
|
|
23155
|
+
}
|
|
23156
|
+
const runtime2 = createRuntime({
|
|
23157
|
+
projectDir: context.projectDir,
|
|
23158
|
+
port: context.port
|
|
23159
|
+
});
|
|
23160
|
+
const projectDir = resolve12(runtime2.projectDir);
|
|
23161
|
+
const gitDir = runtime2.git.resolveWorktreeGitDir(projectDir);
|
|
23162
|
+
const state = await readOpenSessions(gitDir);
|
|
23163
|
+
if (state.branches.length === 0) {
|
|
23164
|
+
stdout2("No saved sessions to restore.");
|
|
23165
|
+
return 0;
|
|
23166
|
+
}
|
|
23167
|
+
const sessionName = buildProjectSessionName(projectDir);
|
|
23168
|
+
let openWindows = new Set;
|
|
23169
|
+
try {
|
|
23170
|
+
openWindows = new Set(runtime2.tmux.listWindows().filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
|
|
23171
|
+
} catch {
|
|
23172
|
+
openWindows = new Set;
|
|
23173
|
+
}
|
|
23174
|
+
const existingBranches = new Set(listProjectWorktrees(runtime2).map((entry) => entry.branch ?? basename7(entry.path)));
|
|
23175
|
+
let restored = 0;
|
|
23176
|
+
let skipped = 0;
|
|
23177
|
+
let failed = 0;
|
|
23178
|
+
let firstRestored = null;
|
|
23179
|
+
for (const branch2 of state.branches) {
|
|
23180
|
+
if (openWindows.has(buildWorktreeWindowName(branch2))) {
|
|
23181
|
+
stdout2(`Already open: ${branch2}`);
|
|
23182
|
+
skipped++;
|
|
23183
|
+
continue;
|
|
23184
|
+
}
|
|
23185
|
+
if (!existingBranches.has(branch2)) {
|
|
23186
|
+
stderr(`Skipping ${branch2}: worktree no longer exists`);
|
|
23187
|
+
skipped++;
|
|
23188
|
+
continue;
|
|
23189
|
+
}
|
|
23190
|
+
try {
|
|
23191
|
+
await runtime2.lifecycleService.openWorktree(branch2);
|
|
23192
|
+
stdout2(`Restored ${branch2}`);
|
|
23193
|
+
restored++;
|
|
23194
|
+
if (!firstRestored)
|
|
23195
|
+
firstRestored = branch2;
|
|
23196
|
+
} catch (error) {
|
|
23197
|
+
stderr(`Failed to restore ${branch2}: ${error instanceof Error ? error.message : String(error)}`);
|
|
23198
|
+
failed++;
|
|
23199
|
+
}
|
|
23200
|
+
}
|
|
23201
|
+
const summaryParts = [`Restored ${restored} session${restored === 1 ? "" : "s"}`];
|
|
23202
|
+
if (skipped > 0)
|
|
23203
|
+
summaryParts.push(`skipped ${skipped}`);
|
|
23204
|
+
if (failed > 0)
|
|
23205
|
+
summaryParts.push(`${failed} failed`);
|
|
23206
|
+
stdout2(`${summaryParts.join(", ")}.`);
|
|
23207
|
+
if (firstRestored) {
|
|
23208
|
+
switchToTmuxWindow(runtime2.projectDir, firstRestored);
|
|
23209
|
+
}
|
|
23210
|
+
return failed > 0 ? 1 : 0;
|
|
23211
|
+
}
|
|
23093
23212
|
if (context.command === "send") {
|
|
23094
23213
|
const parsed = parseSendCommandArgs(context.args);
|
|
23095
23214
|
if (!parsed) {
|
|
@@ -23233,7 +23352,7 @@ import { fileURLToPath } from "url";
|
|
|
23233
23352
|
// package.json
|
|
23234
23353
|
var package_default = {
|
|
23235
23354
|
name: "webmux",
|
|
23236
|
-
version: "0.
|
|
23355
|
+
version: "0.40.0",
|
|
23237
23356
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
23238
23357
|
type: "module",
|
|
23239
23358
|
repository: {
|
|
@@ -23313,7 +23432,8 @@ Usage:
|
|
|
23313
23432
|
webmux merge Merge a worktree into the main branch and remove it
|
|
23314
23433
|
webmux send Send a prompt to a running worktree agent
|
|
23315
23434
|
webmux tab List, create, switch, or close agent tabs in a worktree
|
|
23316
|
-
webmux prune Remove all worktrees in the current project
|
|
23435
|
+
webmux prune Remove all closed (not open) worktrees in the current project
|
|
23436
|
+
webmux restore Re-open all worktree sessions that were open before
|
|
23317
23437
|
webmux linear Post a worktree conversation to a Linear issue/team
|
|
23318
23438
|
webmux project List, add, or remove projects served by the dashboard
|
|
23319
23439
|
webmux completion Generate shell completion script (bash, zsh)
|
|
@@ -23331,7 +23451,7 @@ Environment:
|
|
|
23331
23451
|
`);
|
|
23332
23452
|
}
|
|
23333
23453
|
function isRootCommand(value) {
|
|
23334
|
-
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "refresh" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "tab" || value === "prune" || value === "linear" || value === "project" || value === "completion";
|
|
23454
|
+
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "refresh" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "tab" || value === "prune" || value === "restore" || value === "linear" || value === "project" || value === "completion";
|
|
23335
23455
|
}
|
|
23336
23456
|
function isServeRootOption(value) {
|
|
23337
23457
|
return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
@@ -23398,7 +23518,7 @@ Run webmux --help for usage.`);
|
|
|
23398
23518
|
};
|
|
23399
23519
|
}
|
|
23400
23520
|
function isWorktreeCommand(command) {
|
|
23401
|
-
return command === "add" || command === "list" || command === "open" || command === "close" || command === "refresh" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "tab" || command === "prune";
|
|
23521
|
+
return command === "add" || command === "list" || command === "open" || command === "close" || command === "refresh" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "tab" || command === "prune" || command === "restore";
|
|
23402
23522
|
}
|
|
23403
23523
|
async function loadEnvFile(path) {
|
|
23404
23524
|
if (!existsSync6(path))
|