webmux 0.25.0 → 0.27.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 +5 -6
- package/backend/dist/server.js +279 -58
- package/bin/webmux.js +1469 -908
- package/frontend/dist/assets/index-CBdl44Z_.css +32 -0
- package/frontend/dist/assets/index-kg2aNK8N.js +151 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-BRoz1T_W.js +0 -151
- package/frontend/dist/assets/index-D9R5ycW2.css +0 -32
package/backend/dist/server.js
CHANGED
|
@@ -6919,7 +6919,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6919
6919
|
|
|
6920
6920
|
// backend/src/server.ts
|
|
6921
6921
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6922
|
-
import { join as join7, resolve as
|
|
6922
|
+
import { join as join7, resolve as resolve8 } from "path";
|
|
6923
6923
|
import { mkdirSync } from "fs";
|
|
6924
6924
|
import { networkInterfaces } from "os";
|
|
6925
6925
|
|
|
@@ -7713,6 +7713,43 @@ function hasRecentDashboardActivity() {
|
|
|
7713
7713
|
return Date.now() - lastActivityAt < ACTIVITY_TIMEOUT_MS;
|
|
7714
7714
|
}
|
|
7715
7715
|
|
|
7716
|
+
// backend/src/services/archive-service.ts
|
|
7717
|
+
import { resolve as resolve2 } from "path";
|
|
7718
|
+
|
|
7719
|
+
// backend/src/domain/model.ts
|
|
7720
|
+
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
7721
|
+
var WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
7722
|
+
|
|
7723
|
+
// backend/src/services/archive-service.ts
|
|
7724
|
+
function createArchiveState(entries) {
|
|
7725
|
+
return {
|
|
7726
|
+
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
7727
|
+
entries: [...entries].sort((left, right) => left.path.localeCompare(right.path))
|
|
7728
|
+
};
|
|
7729
|
+
}
|
|
7730
|
+
function normalizeArchivePath(path) {
|
|
7731
|
+
return resolve2(path);
|
|
7732
|
+
}
|
|
7733
|
+
function buildArchivedWorktreePathSet(state) {
|
|
7734
|
+
return new Set(state.entries.map((entry) => normalizeArchivePath(entry.path)));
|
|
7735
|
+
}
|
|
7736
|
+
function setArchivedWorktreeState(input) {
|
|
7737
|
+
const normalizedPath = normalizeArchivePath(input.path);
|
|
7738
|
+
const entries = input.state.entries.filter((entry) => normalizeArchivePath(entry.path) !== normalizedPath);
|
|
7739
|
+
if (!input.archived) {
|
|
7740
|
+
return createArchiveState(entries);
|
|
7741
|
+
}
|
|
7742
|
+
entries.push({
|
|
7743
|
+
path: normalizedPath,
|
|
7744
|
+
archivedAt: (input.now ?? (() => new Date))().toISOString()
|
|
7745
|
+
});
|
|
7746
|
+
return createArchiveState(entries);
|
|
7747
|
+
}
|
|
7748
|
+
function pruneArchivedWorktreeState(input) {
|
|
7749
|
+
const validPaths = new Set(input.paths.map((path) => normalizeArchivePath(path)));
|
|
7750
|
+
return createArchiveState(input.state.entries.filter((entry) => validPaths.has(normalizeArchivePath(entry.path))));
|
|
7751
|
+
}
|
|
7752
|
+
|
|
7716
7753
|
// backend/src/services/linear-service.ts
|
|
7717
7754
|
var ASSIGNED_ISSUES_QUERY = `
|
|
7718
7755
|
query AssignedIssues {
|
|
@@ -8041,9 +8078,8 @@ async function createLinearIssue(input) {
|
|
|
8041
8078
|
}
|
|
8042
8079
|
|
|
8043
8080
|
// backend/src/services/lifecycle-service.ts
|
|
8044
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
8045
8081
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
8046
|
-
import { dirname as dirname4, resolve as
|
|
8082
|
+
import { dirname as dirname4, resolve as resolve6 } from "path";
|
|
8047
8083
|
|
|
8048
8084
|
// backend/src/adapters/agent-runtime.ts
|
|
8049
8085
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
@@ -8102,6 +8138,9 @@ function getWorktreeStoragePaths(gitDir) {
|
|
|
8102
8138
|
prsPath: join2(webmuxDir, "prs.json")
|
|
8103
8139
|
};
|
|
8104
8140
|
}
|
|
8141
|
+
function getProjectArchiveStatePath(gitDir) {
|
|
8142
|
+
return join2(gitDir, "webmux", "archive.json");
|
|
8143
|
+
}
|
|
8105
8144
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
8106
8145
|
const paths = getWorktreeStoragePaths(gitDir);
|
|
8107
8146
|
await mkdir2(paths.webmuxDir, { recursive: true });
|
|
@@ -8120,6 +8159,36 @@ async function writeWorktreeMeta(gitDir, meta) {
|
|
|
8120
8159
|
await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
|
|
8121
8160
|
`);
|
|
8122
8161
|
}
|
|
8162
|
+
function isArchivedWorktreeEntry(raw) {
|
|
8163
|
+
return isRecord2(raw) && typeof raw.path === "string" && typeof raw.archivedAt === "string";
|
|
8164
|
+
}
|
|
8165
|
+
function emptyWorktreeArchiveState() {
|
|
8166
|
+
return {
|
|
8167
|
+
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
8168
|
+
entries: []
|
|
8169
|
+
};
|
|
8170
|
+
}
|
|
8171
|
+
function isWorktreeArchiveState(raw) {
|
|
8172
|
+
return isRecord2(raw) && typeof raw.schemaVersion === "number" && Array.isArray(raw.entries) && raw.entries.every((entry) => isArchivedWorktreeEntry(entry));
|
|
8173
|
+
}
|
|
8174
|
+
async function readWorktreeArchiveState(gitDir) {
|
|
8175
|
+
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
8176
|
+
try {
|
|
8177
|
+
const raw = await Bun.file(archivePath).json();
|
|
8178
|
+
return isWorktreeArchiveState(raw) ? {
|
|
8179
|
+
schemaVersion: raw.schemaVersion,
|
|
8180
|
+
entries: raw.entries.map((entry) => ({ ...entry }))
|
|
8181
|
+
} : emptyWorktreeArchiveState();
|
|
8182
|
+
} catch {
|
|
8183
|
+
return emptyWorktreeArchiveState();
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
async function writeWorktreeArchiveState(gitDir, state) {
|
|
8187
|
+
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
8188
|
+
await ensureWorktreeStorageDirs(gitDir);
|
|
8189
|
+
await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
|
|
8190
|
+
`);
|
|
8191
|
+
}
|
|
8123
8192
|
function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
|
|
8124
8193
|
return {
|
|
8125
8194
|
...dotenvValues,
|
|
@@ -8469,7 +8538,7 @@ async function ensureAgentRuntimeArtifacts(input) {
|
|
|
8469
8538
|
|
|
8470
8539
|
// backend/src/adapters/tmux.ts
|
|
8471
8540
|
import { createHash } from "crypto";
|
|
8472
|
-
import { basename, resolve as
|
|
8541
|
+
import { basename, resolve as resolve3 } from "path";
|
|
8473
8542
|
function runTmux(args) {
|
|
8474
8543
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
8475
8544
|
stdout: "pipe",
|
|
@@ -8497,7 +8566,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
|
8497
8566
|
return trimmed || "x";
|
|
8498
8567
|
}
|
|
8499
8568
|
function buildProjectSessionName(projectRoot2) {
|
|
8500
|
-
const resolved =
|
|
8569
|
+
const resolved = resolve3(projectRoot2);
|
|
8501
8570
|
const base = sanitizeTmuxNameSegment(basename(resolved), 18);
|
|
8502
8571
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
8503
8572
|
return `wm-${base}-${hash}`;
|
|
@@ -8666,7 +8735,7 @@ function buildDockerAgentPaneCommand(input) {
|
|
|
8666
8735
|
}
|
|
8667
8736
|
|
|
8668
8737
|
// backend/src/services/session-service.ts
|
|
8669
|
-
import { resolve as
|
|
8738
|
+
import { resolve as resolve4 } from "path";
|
|
8670
8739
|
function quoteShell2(value) {
|
|
8671
8740
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
8672
8741
|
}
|
|
@@ -8680,7 +8749,7 @@ function buildCommandPaneStartupCommand(template, ctx) {
|
|
|
8680
8749
|
if (!template.workingDir) {
|
|
8681
8750
|
return template.command;
|
|
8682
8751
|
}
|
|
8683
|
-
const workingDir =
|
|
8752
|
+
const workingDir = resolve4(resolvePaneCwd(template, ctx), template.workingDir);
|
|
8684
8753
|
return `cd -- ${quoteShell2(workingDir)} && ${template.command}`;
|
|
8685
8754
|
}
|
|
8686
8755
|
function resolvePaneStartupCommand(template, ctx) {
|
|
@@ -8760,7 +8829,7 @@ import { randomUUID } from "crypto";
|
|
|
8760
8829
|
|
|
8761
8830
|
// backend/src/adapters/git.ts
|
|
8762
8831
|
import { readdirSync, rmSync, statSync } from "fs";
|
|
8763
|
-
import { resolve as
|
|
8832
|
+
import { resolve as resolve5, join as join4 } from "path";
|
|
8764
8833
|
function runGit(args, cwd) {
|
|
8765
8834
|
const result = Bun.spawnSync(["git", ...args], {
|
|
8766
8835
|
cwd,
|
|
@@ -8794,8 +8863,8 @@ function errorMessage(error) {
|
|
|
8794
8863
|
return error instanceof Error ? error.message : String(error);
|
|
8795
8864
|
}
|
|
8796
8865
|
function isRegisteredWorktree(entries, worktreePath) {
|
|
8797
|
-
const resolvedPath =
|
|
8798
|
-
return entries.some((entry) =>
|
|
8866
|
+
const resolvedPath = resolve5(worktreePath);
|
|
8867
|
+
return entries.some((entry) => resolve5(entry.path) === resolvedPath);
|
|
8799
8868
|
}
|
|
8800
8869
|
function removeDirectory(path) {
|
|
8801
8870
|
rmSync(path, {
|
|
@@ -8819,7 +8888,7 @@ function currentCheckoutRef(cwd) {
|
|
|
8819
8888
|
function resolveRepoRoot(dir) {
|
|
8820
8889
|
const direct = tryRunGit(["rev-parse", "--show-toplevel"], dir);
|
|
8821
8890
|
if (direct.ok)
|
|
8822
|
-
return
|
|
8891
|
+
return resolve5(dir, direct.stdout);
|
|
8823
8892
|
let entries;
|
|
8824
8893
|
try {
|
|
8825
8894
|
entries = readdirSync(dir);
|
|
@@ -8836,17 +8905,17 @@ function resolveRepoRoot(dir) {
|
|
|
8836
8905
|
}
|
|
8837
8906
|
const childResult = tryRunGit(["rev-parse", "--show-toplevel"], child);
|
|
8838
8907
|
if (childResult.ok)
|
|
8839
|
-
return
|
|
8908
|
+
return resolve5(child, childResult.stdout);
|
|
8840
8909
|
}
|
|
8841
8910
|
return null;
|
|
8842
8911
|
}
|
|
8843
8912
|
function resolveWorktreeRoot(cwd) {
|
|
8844
8913
|
const output = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
8845
|
-
return
|
|
8914
|
+
return resolve5(cwd, output);
|
|
8846
8915
|
}
|
|
8847
8916
|
function resolveWorktreeGitDir(cwd) {
|
|
8848
8917
|
const output = runGit(["rev-parse", "--git-dir"], cwd);
|
|
8849
|
-
return
|
|
8918
|
+
return resolve5(cwd, output);
|
|
8850
8919
|
}
|
|
8851
8920
|
function parseGitWorktreePorcelain(output) {
|
|
8852
8921
|
const entries = [];
|
|
@@ -9057,9 +9126,6 @@ class BunGitGateway {
|
|
|
9057
9126
|
}
|
|
9058
9127
|
}
|
|
9059
9128
|
|
|
9060
|
-
// backend/src/domain/model.ts
|
|
9061
|
-
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
9062
|
-
|
|
9063
9129
|
// backend/src/services/worktree-service.ts
|
|
9064
9130
|
function toErrorMessage(error) {
|
|
9065
9131
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -9204,10 +9270,13 @@ function mergeManagedWorktree(opts, git = new BunGitGateway) {
|
|
|
9204
9270
|
});
|
|
9205
9271
|
}
|
|
9206
9272
|
|
|
9207
|
-
// backend/src/
|
|
9208
|
-
|
|
9273
|
+
// backend/src/lib/branch-name.ts
|
|
9274
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9275
|
+
function generateFallbackBranchName() {
|
|
9209
9276
|
return `change-${randomUUID2().slice(0, 8)}`;
|
|
9210
9277
|
}
|
|
9278
|
+
|
|
9279
|
+
// backend/src/services/lifecycle-service.ts
|
|
9211
9280
|
function toErrorMessage2(error) {
|
|
9212
9281
|
return error instanceof Error ? error.message : String(error);
|
|
9213
9282
|
}
|
|
@@ -9311,9 +9380,8 @@ class LifecycleService {
|
|
|
9311
9380
|
}
|
|
9312
9381
|
async closeWorktree(branch) {
|
|
9313
9382
|
try {
|
|
9314
|
-
|
|
9315
|
-
this.
|
|
9316
|
-
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9383
|
+
await this.resolveExistingWorktree(branch);
|
|
9384
|
+
await this.closeBranchWindow(branch);
|
|
9317
9385
|
} catch (error) {
|
|
9318
9386
|
throw this.wrapOperationError(error);
|
|
9319
9387
|
}
|
|
@@ -9358,6 +9426,17 @@ class LifecycleService {
|
|
|
9358
9426
|
throw this.wrapOperationError(error);
|
|
9359
9427
|
}
|
|
9360
9428
|
}
|
|
9429
|
+
async setWorktreeArchived(branch, archived) {
|
|
9430
|
+
try {
|
|
9431
|
+
const resolved = await this.resolveExistingWorktree(branch);
|
|
9432
|
+
if (archived) {
|
|
9433
|
+
await this.closeBranchWindow(branch);
|
|
9434
|
+
}
|
|
9435
|
+
await this.updateWorktreeArchivedState(resolved.entry.path, archived);
|
|
9436
|
+
} catch (error) {
|
|
9437
|
+
throw this.wrapOperationError(error);
|
|
9438
|
+
}
|
|
9439
|
+
}
|
|
9361
9440
|
listAvailableBranches(options = {}) {
|
|
9362
9441
|
const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
|
|
9363
9442
|
const remoteBranches = options.includeRemote ? this.listRemoteBranches().filter((branch) => isValidBranchName(branch)) : [];
|
|
@@ -9370,7 +9449,7 @@ class LifecycleService {
|
|
|
9370
9449
|
}
|
|
9371
9450
|
async resolveBranch(rawBranch, prompt, mode) {
|
|
9372
9451
|
const explicitBranch = rawBranch?.trim();
|
|
9373
|
-
const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) ||
|
|
9452
|
+
const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) || generateFallbackBranchName();
|
|
9374
9453
|
if (!branch) {
|
|
9375
9454
|
throw new LifecycleError("Existing branch is required", 400);
|
|
9376
9455
|
}
|
|
@@ -9442,20 +9521,20 @@ class LifecycleService {
|
|
|
9442
9521
|
return allocateServicePorts(metas, this.deps.config.services);
|
|
9443
9522
|
}
|
|
9444
9523
|
resolveWorktreePath(branch) {
|
|
9445
|
-
return
|
|
9524
|
+
return resolve6(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
|
|
9446
9525
|
}
|
|
9447
9526
|
listLocalBranches() {
|
|
9448
|
-
return this.deps.git.listLocalBranches(
|
|
9527
|
+
return this.deps.git.listLocalBranches(resolve6(this.deps.projectRoot));
|
|
9449
9528
|
}
|
|
9450
9529
|
listRemoteBranches() {
|
|
9451
|
-
return this.deps.git.listRemoteBranches(
|
|
9530
|
+
return this.deps.git.listRemoteBranches(resolve6(this.deps.projectRoot));
|
|
9452
9531
|
}
|
|
9453
9532
|
listCheckedOutBranches() {
|
|
9454
|
-
return new Set(this.deps.git.listWorktrees(
|
|
9533
|
+
return new Set(this.deps.git.listWorktrees(resolve6(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
|
|
9455
9534
|
}
|
|
9456
9535
|
listProjectWorktrees() {
|
|
9457
|
-
const projectRoot2 =
|
|
9458
|
-
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare &&
|
|
9536
|
+
const projectRoot2 = resolve6(this.deps.projectRoot);
|
|
9537
|
+
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve6(entry.path) !== projectRoot2);
|
|
9459
9538
|
}
|
|
9460
9539
|
async readManagedMetas() {
|
|
9461
9540
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -9528,6 +9607,13 @@ class LifecycleService {
|
|
|
9528
9607
|
controlEnv
|
|
9529
9608
|
};
|
|
9530
9609
|
}
|
|
9610
|
+
async updateWorktreeArchivedState(path, archived) {
|
|
9611
|
+
await this.deps.archiveState.setArchived(path, archived);
|
|
9612
|
+
}
|
|
9613
|
+
async closeBranchWindow(branch) {
|
|
9614
|
+
this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
|
|
9615
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9616
|
+
}
|
|
9531
9617
|
async materializeRuntimeSession(input) {
|
|
9532
9618
|
if (input.profile.runtime === "docker") {
|
|
9533
9619
|
const dockerProfile = this.requireDockerProfile(input.profile);
|
|
@@ -9655,6 +9741,7 @@ class LifecycleService {
|
|
|
9655
9741
|
deleteBranch: true,
|
|
9656
9742
|
deleteBranchForce: true
|
|
9657
9743
|
}, this.deps.git);
|
|
9744
|
+
await this.updateWorktreeArchivedState(resolved.entry.path, false);
|
|
9658
9745
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9659
9746
|
}
|
|
9660
9747
|
async runLifecycleHook(input) {
|
|
@@ -10440,12 +10527,13 @@ function mapCreationSnapshot(creating) {
|
|
|
10440
10527
|
phase: creating.phase
|
|
10441
10528
|
} : null;
|
|
10442
10529
|
}
|
|
10443
|
-
function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
|
|
10530
|
+
function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue) {
|
|
10444
10531
|
return {
|
|
10445
10532
|
branch: state.branch,
|
|
10446
10533
|
...state.baseBranch ? { baseBranch: state.baseBranch } : {},
|
|
10447
10534
|
path: state.path,
|
|
10448
10535
|
dir: state.path,
|
|
10536
|
+
archived: isArchived(state.path),
|
|
10449
10537
|
profile: state.profile,
|
|
10450
10538
|
agentName: state.agentName,
|
|
10451
10539
|
mux: state.session.exists,
|
|
@@ -10460,12 +10548,13 @@ function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
|
|
|
10460
10548
|
creation: mapCreationSnapshot(creating)
|
|
10461
10549
|
};
|
|
10462
10550
|
}
|
|
10463
|
-
function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
|
|
10551
|
+
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
|
|
10464
10552
|
return {
|
|
10465
10553
|
branch: creating.branch,
|
|
10466
10554
|
...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
|
|
10467
10555
|
path: creating.path,
|
|
10468
10556
|
dir: creating.path,
|
|
10557
|
+
archived: isArchived(creating.path),
|
|
10469
10558
|
profile: creating.profile,
|
|
10470
10559
|
agentName: creating.agentName,
|
|
10471
10560
|
mux: false,
|
|
@@ -10482,14 +10571,15 @@ function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
|
|
|
10482
10571
|
}
|
|
10483
10572
|
function buildProjectSnapshot(input) {
|
|
10484
10573
|
const now = input.now ?? (() => new Date);
|
|
10574
|
+
const isArchived = input.isArchived ?? (() => false);
|
|
10485
10575
|
const creatingWorktrees = input.creatingWorktrees ?? [];
|
|
10486
10576
|
const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
|
|
10487
10577
|
const runtimeWorktrees = input.runtime.listWorktrees();
|
|
10488
10578
|
const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
|
|
10489
|
-
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, input.findLinearIssue));
|
|
10579
|
+
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue));
|
|
10490
10580
|
for (const creating of creatingWorktrees) {
|
|
10491
10581
|
if (!runtimeBranches.has(creating.branch)) {
|
|
10492
|
-
worktrees.push(mapCreatingWorktreeSnapshot(creating, input.findLinearIssue));
|
|
10582
|
+
worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue));
|
|
10493
10583
|
}
|
|
10494
10584
|
}
|
|
10495
10585
|
worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
|
|
@@ -10867,7 +10957,7 @@ class BunPortProbe {
|
|
|
10867
10957
|
this.hostnames = hostnames;
|
|
10868
10958
|
}
|
|
10869
10959
|
isListening(port) {
|
|
10870
|
-
return new Promise((
|
|
10960
|
+
return new Promise((resolve7) => {
|
|
10871
10961
|
let settled = false;
|
|
10872
10962
|
let pending = this.hostnames.length;
|
|
10873
10963
|
const settle = (result) => {
|
|
@@ -10876,20 +10966,20 @@ class BunPortProbe {
|
|
|
10876
10966
|
if (result) {
|
|
10877
10967
|
settled = true;
|
|
10878
10968
|
clearTimeout(timer);
|
|
10879
|
-
|
|
10969
|
+
resolve7(true);
|
|
10880
10970
|
return;
|
|
10881
10971
|
}
|
|
10882
10972
|
pending--;
|
|
10883
10973
|
if (pending === 0) {
|
|
10884
10974
|
settled = true;
|
|
10885
10975
|
clearTimeout(timer);
|
|
10886
|
-
|
|
10976
|
+
resolve7(false);
|
|
10887
10977
|
}
|
|
10888
10978
|
};
|
|
10889
10979
|
const timer = setTimeout(() => {
|
|
10890
10980
|
if (!settled) {
|
|
10891
10981
|
settled = true;
|
|
10892
|
-
|
|
10982
|
+
resolve7(false);
|
|
10893
10983
|
}
|
|
10894
10984
|
}, this.timeoutMs);
|
|
10895
10985
|
for (const hostname of this.hostnames) {
|
|
@@ -10915,6 +11005,7 @@ class BunPortProbe {
|
|
|
10915
11005
|
// backend/src/services/auto-name-service.ts
|
|
10916
11006
|
var MAX_BRANCH_LENGTH = 40;
|
|
10917
11007
|
var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
|
|
11008
|
+
var AUTO_NAME_TIMEOUT_MS = 1e4;
|
|
10918
11009
|
var DEFAULT_SYSTEM_PROMPT = [
|
|
10919
11010
|
"Generate a concise git branch name from the task description.",
|
|
10920
11011
|
"Return only the branch name.",
|
|
@@ -10945,17 +11036,52 @@ function normalizeGeneratedBranchName(raw) {
|
|
|
10945
11036
|
function getSystemPrompt(config) {
|
|
10946
11037
|
return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
10947
11038
|
}
|
|
10948
|
-
|
|
11039
|
+
|
|
11040
|
+
class AutoNameTimeoutError extends Error {
|
|
11041
|
+
timeoutMs;
|
|
11042
|
+
constructor(timeoutMs) {
|
|
11043
|
+
super(`Auto-name timed out after ${timeoutMs}ms`);
|
|
11044
|
+
this.timeoutMs = timeoutMs;
|
|
11045
|
+
}
|
|
11046
|
+
}
|
|
11047
|
+
async function defaultSpawn(args, options = {}) {
|
|
10949
11048
|
const proc = Bun.spawn(args, {
|
|
10950
11049
|
stdout: "pipe",
|
|
10951
11050
|
stderr: "pipe"
|
|
10952
11051
|
});
|
|
10953
|
-
const
|
|
11052
|
+
const resultPromise = Promise.all([
|
|
10954
11053
|
new Response(proc.stdout).text(),
|
|
10955
11054
|
new Response(proc.stderr).text(),
|
|
10956
11055
|
proc.exited
|
|
10957
|
-
]);
|
|
10958
|
-
|
|
11056
|
+
]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
|
|
11057
|
+
if (options.timeoutMs === undefined) {
|
|
11058
|
+
return await resultPromise;
|
|
11059
|
+
}
|
|
11060
|
+
return await new Promise((resolve7, reject) => {
|
|
11061
|
+
let settled = false;
|
|
11062
|
+
const timeoutId = setTimeout(() => {
|
|
11063
|
+
if (settled)
|
|
11064
|
+
return;
|
|
11065
|
+
settled = true;
|
|
11066
|
+
try {
|
|
11067
|
+
proc.kill("SIGKILL");
|
|
11068
|
+
} catch {}
|
|
11069
|
+
reject(new AutoNameTimeoutError(options.timeoutMs));
|
|
11070
|
+
}, options.timeoutMs);
|
|
11071
|
+
resultPromise.then((result) => {
|
|
11072
|
+
if (settled)
|
|
11073
|
+
return;
|
|
11074
|
+
settled = true;
|
|
11075
|
+
clearTimeout(timeoutId);
|
|
11076
|
+
resolve7(result);
|
|
11077
|
+
}, (error) => {
|
|
11078
|
+
if (settled)
|
|
11079
|
+
return;
|
|
11080
|
+
settled = true;
|
|
11081
|
+
clearTimeout(timeoutId);
|
|
11082
|
+
reject(error);
|
|
11083
|
+
});
|
|
11084
|
+
});
|
|
10959
11085
|
}
|
|
10960
11086
|
function buildClaudeArgs(model, systemPrompt, prompt) {
|
|
10961
11087
|
const args = [
|
|
@@ -10997,8 +11123,10 @@ function buildCodexArgs(model, systemPrompt, prompt) {
|
|
|
10997
11123
|
|
|
10998
11124
|
class AutoNameService {
|
|
10999
11125
|
spawnImpl;
|
|
11126
|
+
timeoutMs;
|
|
11000
11127
|
constructor(deps = {}) {
|
|
11001
11128
|
this.spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
11129
|
+
this.timeoutMs = deps.timeoutMs ?? AUTO_NAME_TIMEOUT_MS;
|
|
11002
11130
|
}
|
|
11003
11131
|
async generateBranchName(config, task) {
|
|
11004
11132
|
const prompt = task.trim();
|
|
@@ -11011,8 +11139,13 @@ class AutoNameService {
|
|
|
11011
11139
|
const cli = config.provider === "claude" ? "claude" : "codex";
|
|
11012
11140
|
let result;
|
|
11013
11141
|
try {
|
|
11014
|
-
result = await this.spawnImpl(args);
|
|
11015
|
-
} catch {
|
|
11142
|
+
result = await this.spawnImpl(args, { timeoutMs: this.timeoutMs });
|
|
11143
|
+
} catch (error) {
|
|
11144
|
+
if (error instanceof AutoNameTimeoutError) {
|
|
11145
|
+
const fallback = generateFallbackBranchName();
|
|
11146
|
+
log.warn(`[auto-name] ${cli} timed out after ${this.timeoutMs}ms; using fallback branch ${fallback}`);
|
|
11147
|
+
return fallback;
|
|
11148
|
+
}
|
|
11016
11149
|
throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
|
|
11017
11150
|
}
|
|
11018
11151
|
if (result.exitCode !== 0) {
|
|
@@ -11030,6 +11163,66 @@ class AutoNameService {
|
|
|
11030
11163
|
}
|
|
11031
11164
|
}
|
|
11032
11165
|
|
|
11166
|
+
// backend/src/services/archive-state-service.ts
|
|
11167
|
+
function archiveStatesEqual(left, right) {
|
|
11168
|
+
if (left.schemaVersion !== right.schemaVersion)
|
|
11169
|
+
return false;
|
|
11170
|
+
if (left.entries.length !== right.entries.length)
|
|
11171
|
+
return false;
|
|
11172
|
+
return left.entries.every((entry, index) => entry.path === right.entries[index]?.path && entry.archivedAt === right.entries[index]?.archivedAt);
|
|
11173
|
+
}
|
|
11174
|
+
|
|
11175
|
+
class ArchiveStateService {
|
|
11176
|
+
gitDir;
|
|
11177
|
+
mutationQueue = Promise.resolve();
|
|
11178
|
+
readState;
|
|
11179
|
+
writeState;
|
|
11180
|
+
constructor(gitDir, deps = {}) {
|
|
11181
|
+
this.gitDir = gitDir;
|
|
11182
|
+
this.readState = deps.readState ?? readWorktreeArchiveState;
|
|
11183
|
+
this.writeState = deps.writeState ?? writeWorktreeArchiveState;
|
|
11184
|
+
}
|
|
11185
|
+
async read() {
|
|
11186
|
+
return await this.readState(this.gitDir);
|
|
11187
|
+
}
|
|
11188
|
+
async setArchived(path, archived) {
|
|
11189
|
+
return await this.mutate((state) => setArchivedWorktreeState({
|
|
11190
|
+
state,
|
|
11191
|
+
path,
|
|
11192
|
+
archived
|
|
11193
|
+
}));
|
|
11194
|
+
}
|
|
11195
|
+
async prune(paths) {
|
|
11196
|
+
return await this.mutate((state) => pruneArchivedWorktreeState({
|
|
11197
|
+
state,
|
|
11198
|
+
paths
|
|
11199
|
+
}));
|
|
11200
|
+
}
|
|
11201
|
+
async mutate(transform) {
|
|
11202
|
+
return await this.withMutationLock(async () => {
|
|
11203
|
+
const state = await this.readState(this.gitDir);
|
|
11204
|
+
const nextState = await transform(state);
|
|
11205
|
+
if (!archiveStatesEqual(state, nextState)) {
|
|
11206
|
+
await this.writeState(this.gitDir, nextState);
|
|
11207
|
+
}
|
|
11208
|
+
return nextState;
|
|
11209
|
+
});
|
|
11210
|
+
}
|
|
11211
|
+
async withMutationLock(operation) {
|
|
11212
|
+
const previous = this.mutationQueue;
|
|
11213
|
+
let release = () => {};
|
|
11214
|
+
this.mutationQueue = new Promise((resolve7) => {
|
|
11215
|
+
release = resolve7;
|
|
11216
|
+
});
|
|
11217
|
+
await previous.catch(() => {});
|
|
11218
|
+
try {
|
|
11219
|
+
return await operation();
|
|
11220
|
+
} finally {
|
|
11221
|
+
release();
|
|
11222
|
+
}
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
|
|
11033
11226
|
// backend/src/services/notification-service.ts
|
|
11034
11227
|
function eventToNotificationInput(event) {
|
|
11035
11228
|
switch (event.type) {
|
|
@@ -11294,9 +11487,9 @@ class ProjectRuntime {
|
|
|
11294
11487
|
}
|
|
11295
11488
|
|
|
11296
11489
|
// backend/src/services/reconciliation-service.ts
|
|
11297
|
-
import { basename as basename2, resolve as
|
|
11490
|
+
import { basename as basename2, resolve as resolve7 } from "path";
|
|
11298
11491
|
function makeUnmanagedWorktreeId(path) {
|
|
11299
|
-
return `unmanaged:${
|
|
11492
|
+
return `unmanaged:${resolve7(path)}`;
|
|
11300
11493
|
}
|
|
11301
11494
|
function isValidPort2(port) {
|
|
11302
11495
|
return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
@@ -11353,7 +11546,7 @@ class ReconciliationService {
|
|
|
11353
11546
|
if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
|
|
11354
11547
|
return;
|
|
11355
11548
|
}
|
|
11356
|
-
const normalizedRepoRoot =
|
|
11549
|
+
const normalizedRepoRoot = resolve7(repoRoot);
|
|
11357
11550
|
const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
|
|
11358
11551
|
this.lastReconciledAt = this.now();
|
|
11359
11552
|
});
|
|
@@ -11372,7 +11565,7 @@ class ReconciliationService {
|
|
|
11372
11565
|
windows = [];
|
|
11373
11566
|
}
|
|
11374
11567
|
const seenWorktreeIds = new Set;
|
|
11375
|
-
const candidateEntries = worktrees.filter((entry) => !entry.bare &&
|
|
11568
|
+
const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve7(entry.path) !== normalizedRepoRoot);
|
|
11376
11569
|
const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
|
|
11377
11570
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
11378
11571
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -11475,6 +11668,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11475
11668
|
const projectDir = projectRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
|
|
11476
11669
|
const config = loadConfig(projectDir, { resolvedRoot: true });
|
|
11477
11670
|
const git = new BunGitGateway;
|
|
11671
|
+
const archiveStateService = new ArchiveStateService(git.resolveWorktreeGitDir(projectDir));
|
|
11478
11672
|
const portProbe = new BunPortProbe;
|
|
11479
11673
|
const tmux = new BunTmuxGateway;
|
|
11480
11674
|
const docker = new BunDockerGateway;
|
|
@@ -11495,6 +11689,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11495
11689
|
controlBaseUrl: `http://127.0.0.1:${port}`,
|
|
11496
11690
|
getControlToken: loadControlToken,
|
|
11497
11691
|
config,
|
|
11692
|
+
archiveState: archiveStateService,
|
|
11498
11693
|
git,
|
|
11499
11694
|
tmux,
|
|
11500
11695
|
docker,
|
|
@@ -11513,6 +11708,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11513
11708
|
port,
|
|
11514
11709
|
projectDir,
|
|
11515
11710
|
config,
|
|
11711
|
+
archiveStateService,
|
|
11516
11712
|
git,
|
|
11517
11713
|
portProbe,
|
|
11518
11714
|
tmux,
|
|
@@ -11537,6 +11733,7 @@ var runtime = createWebmuxRuntime({
|
|
|
11537
11733
|
var PROJECT_DIR = runtime.projectDir;
|
|
11538
11734
|
var config = runtime.config;
|
|
11539
11735
|
var git = runtime.git;
|
|
11736
|
+
var archiveStateService = runtime.archiveStateService;
|
|
11540
11737
|
var tmux = runtime.tmux;
|
|
11541
11738
|
var projectRuntime = runtime.projectRuntime;
|
|
11542
11739
|
var worktreeCreationTracker = runtime.worktreeCreationTracker;
|
|
@@ -11594,7 +11791,7 @@ function getFrontendConfig() {
|
|
|
11594
11791
|
startupEnvs: config.startupEnvs,
|
|
11595
11792
|
linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
|
|
11596
11793
|
alias: lr.alias,
|
|
11597
|
-
...lr.dir ? { dir:
|
|
11794
|
+
...lr.dir ? { dir: resolve8(PROJECT_DIR, lr.dir) } : {}
|
|
11598
11795
|
})),
|
|
11599
11796
|
linearAutoCreateWorktrees: linearAutoCreateEnabled,
|
|
11600
11797
|
autoRemoveOnMerge: autoRemoveOnMergeEnabled,
|
|
@@ -11725,9 +11922,9 @@ async function hasValidControlToken(req) {
|
|
|
11725
11922
|
}
|
|
11726
11923
|
async function getWorktreeGitDirs() {
|
|
11727
11924
|
const gitDirs = new Map;
|
|
11728
|
-
const projectRoot2 =
|
|
11925
|
+
const projectRoot2 = resolve8(PROJECT_DIR);
|
|
11729
11926
|
for (const entry of git.listWorktrees(projectRoot2)) {
|
|
11730
|
-
if (entry.bare ||
|
|
11927
|
+
if (entry.bare || resolve8(entry.path) === projectRoot2 || !entry.branch)
|
|
11731
11928
|
continue;
|
|
11732
11929
|
gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
|
|
11733
11930
|
}
|
|
@@ -11749,10 +11946,10 @@ async function apiGetProject() {
|
|
|
11749
11946
|
touchDashboardActivity();
|
|
11750
11947
|
const linearApiKey = Bun.env.LINEAR_API_KEY;
|
|
11751
11948
|
const linearIssuesPromise = config.integrations.linear.enabled && linearApiKey?.trim() ? fetchAssignedIssues() : Promise.resolve({ ok: true, data: [] });
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11949
|
+
await reconciliationService.reconcile(PROJECT_DIR);
|
|
11950
|
+
const archiveState = await archiveStateService.prune(projectRuntime.listWorktrees().map((worktree) => worktree.path));
|
|
11951
|
+
const linearResult = await linearIssuesPromise;
|
|
11952
|
+
const archivedPaths = buildArchivedWorktreePathSet(archiveState);
|
|
11756
11953
|
const linearIssues = linearResult.ok ? linearResult.data : [];
|
|
11757
11954
|
return jsonResponse(buildProjectSnapshot({
|
|
11758
11955
|
projectName: config.name,
|
|
@@ -11760,6 +11957,7 @@ async function apiGetProject() {
|
|
|
11760
11957
|
runtime: projectRuntime,
|
|
11761
11958
|
creatingWorktrees: worktreeCreationTracker.list(),
|
|
11762
11959
|
notifications: runtimeNotifications.list(),
|
|
11960
|
+
isArchived: (path) => archivedPaths.has(normalizeArchivePath(path)),
|
|
11763
11961
|
findLinearIssue: (branch) => {
|
|
11764
11962
|
const match = linearIssues.find((issue) => branchMatchesIssue(branch, issue.branchName));
|
|
11765
11963
|
return match ? {
|
|
@@ -11937,6 +12135,21 @@ async function apiCloseWorktree(name) {
|
|
|
11937
12135
|
log.debug(`[worktree:close] done name=${name}`);
|
|
11938
12136
|
return jsonResponse({ ok: true });
|
|
11939
12137
|
}
|
|
12138
|
+
async function apiSetWorktreeArchived(name, req) {
|
|
12139
|
+
ensureBranchNotBusy(name);
|
|
12140
|
+
const raw = await req.json();
|
|
12141
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12142
|
+
return errorResponse("Invalid request body", 400);
|
|
12143
|
+
}
|
|
12144
|
+
const body = raw;
|
|
12145
|
+
if (typeof body.archived !== "boolean") {
|
|
12146
|
+
return errorResponse("Missing boolean 'archived' field", 400);
|
|
12147
|
+
}
|
|
12148
|
+
log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
|
|
12149
|
+
await lifecycleService.setWorktreeArchived(name, body.archived);
|
|
12150
|
+
log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
|
|
12151
|
+
return jsonResponse({ ok: true, archived: body.archived });
|
|
12152
|
+
}
|
|
11940
12153
|
async function apiSendPrompt(name, req) {
|
|
11941
12154
|
ensureBranchNotBusy(name);
|
|
11942
12155
|
const raw = await req.json();
|
|
@@ -12009,7 +12222,7 @@ async function apiPullMain(req) {
|
|
|
12009
12222
|
return errorResponse(`Unknown linked repo: ${repo}`, 404);
|
|
12010
12223
|
if (!linkedRepo.dir)
|
|
12011
12224
|
return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
|
|
12012
|
-
const resolvedDir =
|
|
12225
|
+
const resolvedDir = resolve8(PROJECT_DIR, linkedRepo.dir);
|
|
12013
12226
|
const repoRoot = git.resolveRepoRoot(resolvedDir);
|
|
12014
12227
|
if (!repoRoot)
|
|
12015
12228
|
return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
|
|
@@ -12102,7 +12315,7 @@ async function apiUploadFiles(name, req) {
|
|
|
12102
12315
|
}
|
|
12103
12316
|
const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
|
|
12104
12317
|
const destPath = join7(uploadDir, safeName);
|
|
12105
|
-
if (!
|
|
12318
|
+
if (!resolve8(destPath).startsWith(uploadDir + "/")) {
|
|
12106
12319
|
return errorResponse("Invalid filename", 400);
|
|
12107
12320
|
}
|
|
12108
12321
|
await Bun.write(destPath, entry);
|
|
@@ -12169,6 +12382,14 @@ Bun.serve({
|
|
|
12169
12382
|
return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
|
|
12170
12383
|
}
|
|
12171
12384
|
},
|
|
12385
|
+
"/api/worktrees/:name/archive": {
|
|
12386
|
+
PUT: (req) => {
|
|
12387
|
+
const name = decodeURIComponent(req.params.name);
|
|
12388
|
+
if (!isValidWorktreeName(name))
|
|
12389
|
+
return errorResponse("Invalid worktree name", 400);
|
|
12390
|
+
return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
|
|
12391
|
+
}
|
|
12392
|
+
},
|
|
12172
12393
|
"/api/worktrees/:name/send": {
|
|
12173
12394
|
POST: (req) => {
|
|
12174
12395
|
const name = decodeURIComponent(req.params.name);
|
|
@@ -12235,8 +12456,8 @@ Bun.serve({
|
|
|
12235
12456
|
const url = new URL(req.url);
|
|
12236
12457
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
12237
12458
|
const filePath = join7(STATIC_DIR, rawPath);
|
|
12238
|
-
const staticRoot =
|
|
12239
|
-
if (!
|
|
12459
|
+
const staticRoot = resolve8(STATIC_DIR);
|
|
12460
|
+
if (!resolve8(filePath).startsWith(staticRoot + "/")) {
|
|
12240
12461
|
return new Response("Forbidden", { status: 403 });
|
|
12241
12462
|
}
|
|
12242
12463
|
const file = Bun.file(filePath);
|