webmux 0.26.0 → 0.27.1
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 +7 -6
- package/backend/dist/server.js +315 -71
- package/bin/webmux.js +407 -81
- 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-BthQORRm.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}`;
|
|
@@ -8618,12 +8687,16 @@ function allocateServicePorts(existingMetas, services) {
|
|
|
8618
8687
|
}
|
|
8619
8688
|
|
|
8620
8689
|
// backend/src/services/agent-service.ts
|
|
8690
|
+
var DOCKER_PATH_FALLBACK = "/root/.local/bin:/usr/local/bin:/root/.bun/bin:/root/.cargo/bin";
|
|
8621
8691
|
function quoteShell(value) {
|
|
8622
8692
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
8623
8693
|
}
|
|
8624
8694
|
function buildRuntimeBootstrap(runtimeEnvPath) {
|
|
8625
8695
|
return `set -a; . ${quoteShell(runtimeEnvPath)}; set +a`;
|
|
8626
8696
|
}
|
|
8697
|
+
function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
8698
|
+
return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
|
|
8699
|
+
}
|
|
8627
8700
|
function buildAgentInvocation(input) {
|
|
8628
8701
|
if (input.agent === "codex") {
|
|
8629
8702
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
@@ -8646,11 +8719,11 @@ function buildAgentInvocation(input) {
|
|
|
8646
8719
|
}
|
|
8647
8720
|
return `claude${yoloFlag}${promptSuffix}`;
|
|
8648
8721
|
}
|
|
8649
|
-
function buildAgentCommand(input) {
|
|
8650
|
-
return `${
|
|
8722
|
+
function buildAgentCommand(input, bootstrap = buildRuntimeBootstrap) {
|
|
8723
|
+
return `${bootstrap(input.runtimeEnvPath)}; ${buildAgentInvocation(input)}`;
|
|
8651
8724
|
}
|
|
8652
8725
|
function buildDockerExecCommand(containerName, worktreePath, command) {
|
|
8653
|
-
return `docker exec -it -w ${quoteShell(worktreePath)} ${quoteShell(containerName)}
|
|
8726
|
+
return `docker exec -it -w ${quoteShell(worktreePath)} ${quoteShell(containerName)} /bin/sh -c ${quoteShell(command)}`;
|
|
8654
8727
|
}
|
|
8655
8728
|
function buildManagedShellCommand(runtimeEnvPath, shellPath = Bun.env.SHELL || "/bin/bash") {
|
|
8656
8729
|
return `bash -lc ${quoteShell(`${buildRuntimeBootstrap(runtimeEnvPath)}; exec ${quoteShell(shellPath)} -i`)}`;
|
|
@@ -8658,15 +8731,15 @@ function buildManagedShellCommand(runtimeEnvPath, shellPath = Bun.env.SHELL || "
|
|
|
8658
8731
|
function buildAgentPaneCommand(input) {
|
|
8659
8732
|
return buildAgentCommand(input);
|
|
8660
8733
|
}
|
|
8661
|
-
function buildDockerShellCommand(containerName, worktreePath, runtimeEnvPath, shellPath =
|
|
8662
|
-
return buildDockerExecCommand(containerName, worktreePath, `${
|
|
8734
|
+
function buildDockerShellCommand(containerName, worktreePath, runtimeEnvPath, shellPath = "/bin/bash") {
|
|
8735
|
+
return buildDockerExecCommand(containerName, worktreePath, `${buildDockerRuntimeBootstrap(runtimeEnvPath)}; if [ -x ${quoteShell(shellPath)} ]; then exec ${quoteShell(shellPath)} -i; elif [ -x /bin/sh ]; then exec /bin/sh -i; else echo 'webmux: no shell found in container' >&2; exit 127; fi`);
|
|
8663
8736
|
}
|
|
8664
8737
|
function buildDockerAgentPaneCommand(input) {
|
|
8665
|
-
return
|
|
8738
|
+
return buildAgentCommand(input, buildDockerRuntimeBootstrap);
|
|
8666
8739
|
}
|
|
8667
8740
|
|
|
8668
8741
|
// backend/src/services/session-service.ts
|
|
8669
|
-
import { resolve as
|
|
8742
|
+
import { resolve as resolve4 } from "path";
|
|
8670
8743
|
function quoteShell2(value) {
|
|
8671
8744
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
8672
8745
|
}
|
|
@@ -8680,7 +8753,7 @@ function buildCommandPaneStartupCommand(template, ctx) {
|
|
|
8680
8753
|
if (!template.workingDir) {
|
|
8681
8754
|
return template.command;
|
|
8682
8755
|
}
|
|
8683
|
-
const workingDir =
|
|
8756
|
+
const workingDir = resolve4(resolvePaneCwd(template, ctx), template.workingDir);
|
|
8684
8757
|
return `cd -- ${quoteShell2(workingDir)} && ${template.command}`;
|
|
8685
8758
|
}
|
|
8686
8759
|
function resolvePaneStartupCommand(template, ctx) {
|
|
@@ -8760,7 +8833,7 @@ import { randomUUID } from "crypto";
|
|
|
8760
8833
|
|
|
8761
8834
|
// backend/src/adapters/git.ts
|
|
8762
8835
|
import { readdirSync, rmSync, statSync } from "fs";
|
|
8763
|
-
import { resolve as
|
|
8836
|
+
import { resolve as resolve5, join as join4 } from "path";
|
|
8764
8837
|
function runGit(args, cwd) {
|
|
8765
8838
|
const result = Bun.spawnSync(["git", ...args], {
|
|
8766
8839
|
cwd,
|
|
@@ -8794,8 +8867,8 @@ function errorMessage(error) {
|
|
|
8794
8867
|
return error instanceof Error ? error.message : String(error);
|
|
8795
8868
|
}
|
|
8796
8869
|
function isRegisteredWorktree(entries, worktreePath) {
|
|
8797
|
-
const resolvedPath =
|
|
8798
|
-
return entries.some((entry) =>
|
|
8870
|
+
const resolvedPath = resolve5(worktreePath);
|
|
8871
|
+
return entries.some((entry) => resolve5(entry.path) === resolvedPath);
|
|
8799
8872
|
}
|
|
8800
8873
|
function removeDirectory(path) {
|
|
8801
8874
|
rmSync(path, {
|
|
@@ -8819,7 +8892,7 @@ function currentCheckoutRef(cwd) {
|
|
|
8819
8892
|
function resolveRepoRoot(dir) {
|
|
8820
8893
|
const direct = tryRunGit(["rev-parse", "--show-toplevel"], dir);
|
|
8821
8894
|
if (direct.ok)
|
|
8822
|
-
return
|
|
8895
|
+
return resolve5(dir, direct.stdout);
|
|
8823
8896
|
let entries;
|
|
8824
8897
|
try {
|
|
8825
8898
|
entries = readdirSync(dir);
|
|
@@ -8836,17 +8909,17 @@ function resolveRepoRoot(dir) {
|
|
|
8836
8909
|
}
|
|
8837
8910
|
const childResult = tryRunGit(["rev-parse", "--show-toplevel"], child);
|
|
8838
8911
|
if (childResult.ok)
|
|
8839
|
-
return
|
|
8912
|
+
return resolve5(child, childResult.stdout);
|
|
8840
8913
|
}
|
|
8841
8914
|
return null;
|
|
8842
8915
|
}
|
|
8843
8916
|
function resolveWorktreeRoot(cwd) {
|
|
8844
8917
|
const output = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
8845
|
-
return
|
|
8918
|
+
return resolve5(cwd, output);
|
|
8846
8919
|
}
|
|
8847
8920
|
function resolveWorktreeGitDir(cwd) {
|
|
8848
8921
|
const output = runGit(["rev-parse", "--git-dir"], cwd);
|
|
8849
|
-
return
|
|
8922
|
+
return resolve5(cwd, output);
|
|
8850
8923
|
}
|
|
8851
8924
|
function parseGitWorktreePorcelain(output) {
|
|
8852
8925
|
const entries = [];
|
|
@@ -9057,9 +9130,6 @@ class BunGitGateway {
|
|
|
9057
9130
|
}
|
|
9058
9131
|
}
|
|
9059
9132
|
|
|
9060
|
-
// backend/src/domain/model.ts
|
|
9061
|
-
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
9062
|
-
|
|
9063
9133
|
// backend/src/services/worktree-service.ts
|
|
9064
9134
|
function toErrorMessage(error) {
|
|
9065
9135
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -9204,16 +9274,40 @@ function mergeManagedWorktree(opts, git = new BunGitGateway) {
|
|
|
9204
9274
|
});
|
|
9205
9275
|
}
|
|
9206
9276
|
|
|
9207
|
-
// backend/src/
|
|
9208
|
-
|
|
9277
|
+
// backend/src/lib/branch-name.ts
|
|
9278
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9279
|
+
function generateFallbackBranchName() {
|
|
9209
9280
|
return `change-${randomUUID2().slice(0, 8)}`;
|
|
9210
9281
|
}
|
|
9282
|
+
|
|
9283
|
+
// backend/src/services/lifecycle-service.ts
|
|
9284
|
+
var DOCKER_CONTROL_HOST = "host.docker.internal";
|
|
9211
9285
|
function toErrorMessage2(error) {
|
|
9212
9286
|
return error instanceof Error ? error.message : String(error);
|
|
9213
9287
|
}
|
|
9214
9288
|
function stringifyStartupEnvValue(value) {
|
|
9215
9289
|
return typeof value === "boolean" ? String(value) : value;
|
|
9216
9290
|
}
|
|
9291
|
+
function trimTrailingSlashes(value) {
|
|
9292
|
+
return value.replace(/\/+$/, "");
|
|
9293
|
+
}
|
|
9294
|
+
function isLoopbackHostname(hostname) {
|
|
9295
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
|
|
9296
|
+
}
|
|
9297
|
+
function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
|
|
9298
|
+
const trimmed = trimTrailingSlashes(controlBaseUrl);
|
|
9299
|
+
if (runtime !== "docker")
|
|
9300
|
+
return trimmed;
|
|
9301
|
+
try {
|
|
9302
|
+
const url = new URL(trimmed);
|
|
9303
|
+
if (isLoopbackHostname(url.hostname)) {
|
|
9304
|
+
url.hostname = DOCKER_CONTROL_HOST;
|
|
9305
|
+
}
|
|
9306
|
+
return trimTrailingSlashes(url.toString());
|
|
9307
|
+
} catch {
|
|
9308
|
+
return trimmed;
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9217
9311
|
function prefixAgentBranch(agent, branch) {
|
|
9218
9312
|
return `${agent}-${branch}`;
|
|
9219
9313
|
}
|
|
@@ -9311,9 +9405,8 @@ class LifecycleService {
|
|
|
9311
9405
|
}
|
|
9312
9406
|
async closeWorktree(branch) {
|
|
9313
9407
|
try {
|
|
9314
|
-
|
|
9315
|
-
this.
|
|
9316
|
-
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9408
|
+
await this.resolveExistingWorktree(branch);
|
|
9409
|
+
await this.closeBranchWindow(branch);
|
|
9317
9410
|
} catch (error) {
|
|
9318
9411
|
throw this.wrapOperationError(error);
|
|
9319
9412
|
}
|
|
@@ -9358,6 +9451,17 @@ class LifecycleService {
|
|
|
9358
9451
|
throw this.wrapOperationError(error);
|
|
9359
9452
|
}
|
|
9360
9453
|
}
|
|
9454
|
+
async setWorktreeArchived(branch, archived) {
|
|
9455
|
+
try {
|
|
9456
|
+
const resolved = await this.resolveExistingWorktree(branch);
|
|
9457
|
+
if (archived) {
|
|
9458
|
+
await this.closeBranchWindow(branch);
|
|
9459
|
+
}
|
|
9460
|
+
await this.updateWorktreeArchivedState(resolved.entry.path, archived);
|
|
9461
|
+
} catch (error) {
|
|
9462
|
+
throw this.wrapOperationError(error);
|
|
9463
|
+
}
|
|
9464
|
+
}
|
|
9361
9465
|
listAvailableBranches(options = {}) {
|
|
9362
9466
|
const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
|
|
9363
9467
|
const remoteBranches = options.includeRemote ? this.listRemoteBranches().filter((branch) => isValidBranchName(branch)) : [];
|
|
@@ -9370,7 +9474,7 @@ class LifecycleService {
|
|
|
9370
9474
|
}
|
|
9371
9475
|
async resolveBranch(rawBranch, prompt, mode) {
|
|
9372
9476
|
const explicitBranch = rawBranch?.trim();
|
|
9373
|
-
const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) ||
|
|
9477
|
+
const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) || generateFallbackBranchName();
|
|
9374
9478
|
if (!branch) {
|
|
9375
9479
|
throw new LifecycleError("Existing branch is required", 400);
|
|
9376
9480
|
}
|
|
@@ -9442,20 +9546,20 @@ class LifecycleService {
|
|
|
9442
9546
|
return allocateServicePorts(metas, this.deps.config.services);
|
|
9443
9547
|
}
|
|
9444
9548
|
resolveWorktreePath(branch) {
|
|
9445
|
-
return
|
|
9549
|
+
return resolve6(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
|
|
9446
9550
|
}
|
|
9447
9551
|
listLocalBranches() {
|
|
9448
|
-
return this.deps.git.listLocalBranches(
|
|
9552
|
+
return this.deps.git.listLocalBranches(resolve6(this.deps.projectRoot));
|
|
9449
9553
|
}
|
|
9450
9554
|
listRemoteBranches() {
|
|
9451
|
-
return this.deps.git.listRemoteBranches(
|
|
9555
|
+
return this.deps.git.listRemoteBranches(resolve6(this.deps.projectRoot));
|
|
9452
9556
|
}
|
|
9453
9557
|
listCheckedOutBranches() {
|
|
9454
|
-
return new Set(this.deps.git.listWorktrees(
|
|
9558
|
+
return new Set(this.deps.git.listWorktrees(resolve6(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
|
|
9455
9559
|
}
|
|
9456
9560
|
listProjectWorktrees() {
|
|
9457
|
-
const projectRoot2 =
|
|
9458
|
-
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare &&
|
|
9561
|
+
const projectRoot2 = resolve6(this.deps.projectRoot);
|
|
9562
|
+
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve6(entry.path) !== projectRoot2);
|
|
9459
9563
|
}
|
|
9460
9564
|
async readManagedMetas() {
|
|
9461
9565
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -9494,7 +9598,7 @@ class LifecycleService {
|
|
|
9494
9598
|
allocatedPorts: await this.allocatePorts(),
|
|
9495
9599
|
runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: resolved.entry.path },
|
|
9496
9600
|
dotenvValues,
|
|
9497
|
-
controlUrl: this.controlUrl(),
|
|
9601
|
+
controlUrl: this.controlUrl(profile.runtime),
|
|
9498
9602
|
controlToken: await this.deps.getControlToken()
|
|
9499
9603
|
});
|
|
9500
9604
|
}
|
|
@@ -9515,7 +9619,7 @@ class LifecycleService {
|
|
|
9515
9619
|
}, dotenvValues);
|
|
9516
9620
|
await writeRuntimeEnv(input.gitDir, runtimeEnv);
|
|
9517
9621
|
const controlEnv = buildControlEnvMap({
|
|
9518
|
-
controlUrl: this.controlUrl(),
|
|
9622
|
+
controlUrl: this.controlUrl(input.meta.runtime),
|
|
9519
9623
|
controlToken: await this.deps.getControlToken(),
|
|
9520
9624
|
worktreeId: input.meta.worktreeId,
|
|
9521
9625
|
branch: input.meta.branch
|
|
@@ -9528,6 +9632,13 @@ class LifecycleService {
|
|
|
9528
9632
|
controlEnv
|
|
9529
9633
|
};
|
|
9530
9634
|
}
|
|
9635
|
+
async updateWorktreeArchivedState(path, archived) {
|
|
9636
|
+
await this.deps.archiveState.setArchived(path, archived);
|
|
9637
|
+
}
|
|
9638
|
+
async closeBranchWindow(branch) {
|
|
9639
|
+
this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
|
|
9640
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9641
|
+
}
|
|
9531
9642
|
async materializeRuntimeSession(input) {
|
|
9532
9643
|
if (input.profile.runtime === "docker") {
|
|
9533
9644
|
const dockerProfile = this.requireDockerProfile(input.profile);
|
|
@@ -9570,8 +9681,6 @@ class LifecycleService {
|
|
|
9570
9681
|
paneCommands: containerName ? {
|
|
9571
9682
|
agent: buildDockerAgentPaneCommand({
|
|
9572
9683
|
agent: input.agent,
|
|
9573
|
-
containerName,
|
|
9574
|
-
worktreePath: input.worktreePath,
|
|
9575
9684
|
runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
|
|
9576
9685
|
yolo: input.profile.yolo === true,
|
|
9577
9686
|
systemPrompt,
|
|
@@ -9632,8 +9741,8 @@ class LifecycleService {
|
|
|
9632
9741
|
throw new LifecycleError(`Worktree has uncommitted changes: ${entry.branch ?? entry.path}`, 409);
|
|
9633
9742
|
}
|
|
9634
9743
|
}
|
|
9635
|
-
controlUrl() {
|
|
9636
|
-
return `${this.deps.controlBaseUrl
|
|
9744
|
+
controlUrl(runtime) {
|
|
9745
|
+
return `${buildRuntimeControlBaseUrl(this.deps.controlBaseUrl, runtime)}/api/runtime/events`;
|
|
9637
9746
|
}
|
|
9638
9747
|
async removeResolvedWorktree(resolved) {
|
|
9639
9748
|
await this.runLifecycleHook({
|
|
@@ -9655,6 +9764,7 @@ class LifecycleService {
|
|
|
9655
9764
|
deleteBranch: true,
|
|
9656
9765
|
deleteBranchForce: true
|
|
9657
9766
|
}, this.deps.git);
|
|
9767
|
+
await this.updateWorktreeArchivedState(resolved.entry.path, false);
|
|
9658
9768
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9659
9769
|
}
|
|
9660
9770
|
async runLifecycleHook(input) {
|
|
@@ -9735,7 +9845,7 @@ class LifecycleService {
|
|
|
9735
9845
|
startupEnvValues: await this.buildStartupEnvValues(input.envOverrides),
|
|
9736
9846
|
allocatedPorts: await this.allocatePorts(),
|
|
9737
9847
|
runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
|
|
9738
|
-
controlUrl: this.controlUrl(),
|
|
9848
|
+
controlUrl: this.controlUrl(profile.runtime),
|
|
9739
9849
|
controlToken: await this.deps.getControlToken(),
|
|
9740
9850
|
deleteBranchOnRollback
|
|
9741
9851
|
}, {
|
|
@@ -10440,12 +10550,13 @@ function mapCreationSnapshot(creating) {
|
|
|
10440
10550
|
phase: creating.phase
|
|
10441
10551
|
} : null;
|
|
10442
10552
|
}
|
|
10443
|
-
function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
|
|
10553
|
+
function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue) {
|
|
10444
10554
|
return {
|
|
10445
10555
|
branch: state.branch,
|
|
10446
10556
|
...state.baseBranch ? { baseBranch: state.baseBranch } : {},
|
|
10447
10557
|
path: state.path,
|
|
10448
10558
|
dir: state.path,
|
|
10559
|
+
archived: isArchived(state.path),
|
|
10449
10560
|
profile: state.profile,
|
|
10450
10561
|
agentName: state.agentName,
|
|
10451
10562
|
mux: state.session.exists,
|
|
@@ -10460,12 +10571,13 @@ function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
|
|
|
10460
10571
|
creation: mapCreationSnapshot(creating)
|
|
10461
10572
|
};
|
|
10462
10573
|
}
|
|
10463
|
-
function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
|
|
10574
|
+
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
|
|
10464
10575
|
return {
|
|
10465
10576
|
branch: creating.branch,
|
|
10466
10577
|
...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
|
|
10467
10578
|
path: creating.path,
|
|
10468
10579
|
dir: creating.path,
|
|
10580
|
+
archived: isArchived(creating.path),
|
|
10469
10581
|
profile: creating.profile,
|
|
10470
10582
|
agentName: creating.agentName,
|
|
10471
10583
|
mux: false,
|
|
@@ -10482,14 +10594,15 @@ function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
|
|
|
10482
10594
|
}
|
|
10483
10595
|
function buildProjectSnapshot(input) {
|
|
10484
10596
|
const now = input.now ?? (() => new Date);
|
|
10597
|
+
const isArchived = input.isArchived ?? (() => false);
|
|
10485
10598
|
const creatingWorktrees = input.creatingWorktrees ?? [];
|
|
10486
10599
|
const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
|
|
10487
10600
|
const runtimeWorktrees = input.runtime.listWorktrees();
|
|
10488
10601
|
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));
|
|
10602
|
+
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue));
|
|
10490
10603
|
for (const creating of creatingWorktrees) {
|
|
10491
10604
|
if (!runtimeBranches.has(creating.branch)) {
|
|
10492
|
-
worktrees.push(mapCreatingWorktreeSnapshot(creating, input.findLinearIssue));
|
|
10605
|
+
worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue));
|
|
10493
10606
|
}
|
|
10494
10607
|
}
|
|
10495
10608
|
worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
|
|
@@ -10867,7 +10980,7 @@ class BunPortProbe {
|
|
|
10867
10980
|
this.hostnames = hostnames;
|
|
10868
10981
|
}
|
|
10869
10982
|
isListening(port) {
|
|
10870
|
-
return new Promise((
|
|
10983
|
+
return new Promise((resolve7) => {
|
|
10871
10984
|
let settled = false;
|
|
10872
10985
|
let pending = this.hostnames.length;
|
|
10873
10986
|
const settle = (result) => {
|
|
@@ -10876,20 +10989,20 @@ class BunPortProbe {
|
|
|
10876
10989
|
if (result) {
|
|
10877
10990
|
settled = true;
|
|
10878
10991
|
clearTimeout(timer);
|
|
10879
|
-
|
|
10992
|
+
resolve7(true);
|
|
10880
10993
|
return;
|
|
10881
10994
|
}
|
|
10882
10995
|
pending--;
|
|
10883
10996
|
if (pending === 0) {
|
|
10884
10997
|
settled = true;
|
|
10885
10998
|
clearTimeout(timer);
|
|
10886
|
-
|
|
10999
|
+
resolve7(false);
|
|
10887
11000
|
}
|
|
10888
11001
|
};
|
|
10889
11002
|
const timer = setTimeout(() => {
|
|
10890
11003
|
if (!settled) {
|
|
10891
11004
|
settled = true;
|
|
10892
|
-
|
|
11005
|
+
resolve7(false);
|
|
10893
11006
|
}
|
|
10894
11007
|
}, this.timeoutMs);
|
|
10895
11008
|
for (const hostname of this.hostnames) {
|
|
@@ -10915,6 +11028,7 @@ class BunPortProbe {
|
|
|
10915
11028
|
// backend/src/services/auto-name-service.ts
|
|
10916
11029
|
var MAX_BRANCH_LENGTH = 40;
|
|
10917
11030
|
var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
|
|
11031
|
+
var AUTO_NAME_TIMEOUT_MS = 1e4;
|
|
10918
11032
|
var DEFAULT_SYSTEM_PROMPT = [
|
|
10919
11033
|
"Generate a concise git branch name from the task description.",
|
|
10920
11034
|
"Return only the branch name.",
|
|
@@ -10945,17 +11059,52 @@ function normalizeGeneratedBranchName(raw) {
|
|
|
10945
11059
|
function getSystemPrompt(config) {
|
|
10946
11060
|
return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
10947
11061
|
}
|
|
10948
|
-
|
|
11062
|
+
|
|
11063
|
+
class AutoNameTimeoutError extends Error {
|
|
11064
|
+
timeoutMs;
|
|
11065
|
+
constructor(timeoutMs) {
|
|
11066
|
+
super(`Auto-name timed out after ${timeoutMs}ms`);
|
|
11067
|
+
this.timeoutMs = timeoutMs;
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
11070
|
+
async function defaultSpawn(args, options = {}) {
|
|
10949
11071
|
const proc = Bun.spawn(args, {
|
|
10950
11072
|
stdout: "pipe",
|
|
10951
11073
|
stderr: "pipe"
|
|
10952
11074
|
});
|
|
10953
|
-
const
|
|
11075
|
+
const resultPromise = Promise.all([
|
|
10954
11076
|
new Response(proc.stdout).text(),
|
|
10955
11077
|
new Response(proc.stderr).text(),
|
|
10956
11078
|
proc.exited
|
|
10957
|
-
]);
|
|
10958
|
-
|
|
11079
|
+
]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
|
|
11080
|
+
if (options.timeoutMs === undefined) {
|
|
11081
|
+
return await resultPromise;
|
|
11082
|
+
}
|
|
11083
|
+
return await new Promise((resolve7, reject) => {
|
|
11084
|
+
let settled = false;
|
|
11085
|
+
const timeoutId = setTimeout(() => {
|
|
11086
|
+
if (settled)
|
|
11087
|
+
return;
|
|
11088
|
+
settled = true;
|
|
11089
|
+
try {
|
|
11090
|
+
proc.kill("SIGKILL");
|
|
11091
|
+
} catch {}
|
|
11092
|
+
reject(new AutoNameTimeoutError(options.timeoutMs));
|
|
11093
|
+
}, options.timeoutMs);
|
|
11094
|
+
resultPromise.then((result) => {
|
|
11095
|
+
if (settled)
|
|
11096
|
+
return;
|
|
11097
|
+
settled = true;
|
|
11098
|
+
clearTimeout(timeoutId);
|
|
11099
|
+
resolve7(result);
|
|
11100
|
+
}, (error) => {
|
|
11101
|
+
if (settled)
|
|
11102
|
+
return;
|
|
11103
|
+
settled = true;
|
|
11104
|
+
clearTimeout(timeoutId);
|
|
11105
|
+
reject(error);
|
|
11106
|
+
});
|
|
11107
|
+
});
|
|
10959
11108
|
}
|
|
10960
11109
|
function buildClaudeArgs(model, systemPrompt, prompt) {
|
|
10961
11110
|
const args = [
|
|
@@ -10997,8 +11146,10 @@ function buildCodexArgs(model, systemPrompt, prompt) {
|
|
|
10997
11146
|
|
|
10998
11147
|
class AutoNameService {
|
|
10999
11148
|
spawnImpl;
|
|
11149
|
+
timeoutMs;
|
|
11000
11150
|
constructor(deps = {}) {
|
|
11001
11151
|
this.spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
11152
|
+
this.timeoutMs = deps.timeoutMs ?? AUTO_NAME_TIMEOUT_MS;
|
|
11002
11153
|
}
|
|
11003
11154
|
async generateBranchName(config, task) {
|
|
11004
11155
|
const prompt = task.trim();
|
|
@@ -11011,8 +11162,13 @@ class AutoNameService {
|
|
|
11011
11162
|
const cli = config.provider === "claude" ? "claude" : "codex";
|
|
11012
11163
|
let result;
|
|
11013
11164
|
try {
|
|
11014
|
-
result = await this.spawnImpl(args);
|
|
11015
|
-
} catch {
|
|
11165
|
+
result = await this.spawnImpl(args, { timeoutMs: this.timeoutMs });
|
|
11166
|
+
} catch (error) {
|
|
11167
|
+
if (error instanceof AutoNameTimeoutError) {
|
|
11168
|
+
const fallback = generateFallbackBranchName();
|
|
11169
|
+
log.warn(`[auto-name] ${cli} timed out after ${this.timeoutMs}ms; using fallback branch ${fallback}`);
|
|
11170
|
+
return fallback;
|
|
11171
|
+
}
|
|
11016
11172
|
throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
|
|
11017
11173
|
}
|
|
11018
11174
|
if (result.exitCode !== 0) {
|
|
@@ -11030,6 +11186,66 @@ class AutoNameService {
|
|
|
11030
11186
|
}
|
|
11031
11187
|
}
|
|
11032
11188
|
|
|
11189
|
+
// backend/src/services/archive-state-service.ts
|
|
11190
|
+
function archiveStatesEqual(left, right) {
|
|
11191
|
+
if (left.schemaVersion !== right.schemaVersion)
|
|
11192
|
+
return false;
|
|
11193
|
+
if (left.entries.length !== right.entries.length)
|
|
11194
|
+
return false;
|
|
11195
|
+
return left.entries.every((entry, index) => entry.path === right.entries[index]?.path && entry.archivedAt === right.entries[index]?.archivedAt);
|
|
11196
|
+
}
|
|
11197
|
+
|
|
11198
|
+
class ArchiveStateService {
|
|
11199
|
+
gitDir;
|
|
11200
|
+
mutationQueue = Promise.resolve();
|
|
11201
|
+
readState;
|
|
11202
|
+
writeState;
|
|
11203
|
+
constructor(gitDir, deps = {}) {
|
|
11204
|
+
this.gitDir = gitDir;
|
|
11205
|
+
this.readState = deps.readState ?? readWorktreeArchiveState;
|
|
11206
|
+
this.writeState = deps.writeState ?? writeWorktreeArchiveState;
|
|
11207
|
+
}
|
|
11208
|
+
async read() {
|
|
11209
|
+
return await this.readState(this.gitDir);
|
|
11210
|
+
}
|
|
11211
|
+
async setArchived(path, archived) {
|
|
11212
|
+
return await this.mutate((state) => setArchivedWorktreeState({
|
|
11213
|
+
state,
|
|
11214
|
+
path,
|
|
11215
|
+
archived
|
|
11216
|
+
}));
|
|
11217
|
+
}
|
|
11218
|
+
async prune(paths) {
|
|
11219
|
+
return await this.mutate((state) => pruneArchivedWorktreeState({
|
|
11220
|
+
state,
|
|
11221
|
+
paths
|
|
11222
|
+
}));
|
|
11223
|
+
}
|
|
11224
|
+
async mutate(transform) {
|
|
11225
|
+
return await this.withMutationLock(async () => {
|
|
11226
|
+
const state = await this.readState(this.gitDir);
|
|
11227
|
+
const nextState = await transform(state);
|
|
11228
|
+
if (!archiveStatesEqual(state, nextState)) {
|
|
11229
|
+
await this.writeState(this.gitDir, nextState);
|
|
11230
|
+
}
|
|
11231
|
+
return nextState;
|
|
11232
|
+
});
|
|
11233
|
+
}
|
|
11234
|
+
async withMutationLock(operation) {
|
|
11235
|
+
const previous = this.mutationQueue;
|
|
11236
|
+
let release = () => {};
|
|
11237
|
+
this.mutationQueue = new Promise((resolve7) => {
|
|
11238
|
+
release = resolve7;
|
|
11239
|
+
});
|
|
11240
|
+
await previous.catch(() => {});
|
|
11241
|
+
try {
|
|
11242
|
+
return await operation();
|
|
11243
|
+
} finally {
|
|
11244
|
+
release();
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
}
|
|
11248
|
+
|
|
11033
11249
|
// backend/src/services/notification-service.ts
|
|
11034
11250
|
function eventToNotificationInput(event) {
|
|
11035
11251
|
switch (event.type) {
|
|
@@ -11294,9 +11510,9 @@ class ProjectRuntime {
|
|
|
11294
11510
|
}
|
|
11295
11511
|
|
|
11296
11512
|
// backend/src/services/reconciliation-service.ts
|
|
11297
|
-
import { basename as basename2, resolve as
|
|
11513
|
+
import { basename as basename2, resolve as resolve7 } from "path";
|
|
11298
11514
|
function makeUnmanagedWorktreeId(path) {
|
|
11299
|
-
return `unmanaged:${
|
|
11515
|
+
return `unmanaged:${resolve7(path)}`;
|
|
11300
11516
|
}
|
|
11301
11517
|
function isValidPort2(port) {
|
|
11302
11518
|
return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
@@ -11353,7 +11569,7 @@ class ReconciliationService {
|
|
|
11353
11569
|
if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
|
|
11354
11570
|
return;
|
|
11355
11571
|
}
|
|
11356
|
-
const normalizedRepoRoot =
|
|
11572
|
+
const normalizedRepoRoot = resolve7(repoRoot);
|
|
11357
11573
|
const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
|
|
11358
11574
|
this.lastReconciledAt = this.now();
|
|
11359
11575
|
});
|
|
@@ -11372,7 +11588,7 @@ class ReconciliationService {
|
|
|
11372
11588
|
windows = [];
|
|
11373
11589
|
}
|
|
11374
11590
|
const seenWorktreeIds = new Set;
|
|
11375
|
-
const candidateEntries = worktrees.filter((entry) => !entry.bare &&
|
|
11591
|
+
const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve7(entry.path) !== normalizedRepoRoot);
|
|
11376
11592
|
const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
|
|
11377
11593
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
11378
11594
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -11475,6 +11691,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11475
11691
|
const projectDir = projectRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
|
|
11476
11692
|
const config = loadConfig(projectDir, { resolvedRoot: true });
|
|
11477
11693
|
const git = new BunGitGateway;
|
|
11694
|
+
const archiveStateService = new ArchiveStateService(git.resolveWorktreeGitDir(projectDir));
|
|
11478
11695
|
const portProbe = new BunPortProbe;
|
|
11479
11696
|
const tmux = new BunTmuxGateway;
|
|
11480
11697
|
const docker = new BunDockerGateway;
|
|
@@ -11495,6 +11712,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11495
11712
|
controlBaseUrl: `http://127.0.0.1:${port}`,
|
|
11496
11713
|
getControlToken: loadControlToken,
|
|
11497
11714
|
config,
|
|
11715
|
+
archiveState: archiveStateService,
|
|
11498
11716
|
git,
|
|
11499
11717
|
tmux,
|
|
11500
11718
|
docker,
|
|
@@ -11513,6 +11731,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11513
11731
|
port,
|
|
11514
11732
|
projectDir,
|
|
11515
11733
|
config,
|
|
11734
|
+
archiveStateService,
|
|
11516
11735
|
git,
|
|
11517
11736
|
portProbe,
|
|
11518
11737
|
tmux,
|
|
@@ -11537,6 +11756,7 @@ var runtime = createWebmuxRuntime({
|
|
|
11537
11756
|
var PROJECT_DIR = runtime.projectDir;
|
|
11538
11757
|
var config = runtime.config;
|
|
11539
11758
|
var git = runtime.git;
|
|
11759
|
+
var archiveStateService = runtime.archiveStateService;
|
|
11540
11760
|
var tmux = runtime.tmux;
|
|
11541
11761
|
var projectRuntime = runtime.projectRuntime;
|
|
11542
11762
|
var worktreeCreationTracker = runtime.worktreeCreationTracker;
|
|
@@ -11594,7 +11814,7 @@ function getFrontendConfig() {
|
|
|
11594
11814
|
startupEnvs: config.startupEnvs,
|
|
11595
11815
|
linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
|
|
11596
11816
|
alias: lr.alias,
|
|
11597
|
-
...lr.dir ? { dir:
|
|
11817
|
+
...lr.dir ? { dir: resolve8(PROJECT_DIR, lr.dir) } : {}
|
|
11598
11818
|
})),
|
|
11599
11819
|
linearAutoCreateWorktrees: linearAutoCreateEnabled,
|
|
11600
11820
|
autoRemoveOnMerge: autoRemoveOnMergeEnabled,
|
|
@@ -11725,9 +11945,9 @@ async function hasValidControlToken(req) {
|
|
|
11725
11945
|
}
|
|
11726
11946
|
async function getWorktreeGitDirs() {
|
|
11727
11947
|
const gitDirs = new Map;
|
|
11728
|
-
const projectRoot2 =
|
|
11948
|
+
const projectRoot2 = resolve8(PROJECT_DIR);
|
|
11729
11949
|
for (const entry of git.listWorktrees(projectRoot2)) {
|
|
11730
|
-
if (entry.bare ||
|
|
11950
|
+
if (entry.bare || resolve8(entry.path) === projectRoot2 || !entry.branch)
|
|
11731
11951
|
continue;
|
|
11732
11952
|
gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
|
|
11733
11953
|
}
|
|
@@ -11749,10 +11969,10 @@ async function apiGetProject() {
|
|
|
11749
11969
|
touchDashboardActivity();
|
|
11750
11970
|
const linearApiKey = Bun.env.LINEAR_API_KEY;
|
|
11751
11971
|
const linearIssuesPromise = config.integrations.linear.enabled && linearApiKey?.trim() ? fetchAssignedIssues() : Promise.resolve({ ok: true, data: [] });
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11972
|
+
await reconciliationService.reconcile(PROJECT_DIR);
|
|
11973
|
+
const archiveState = await archiveStateService.prune(projectRuntime.listWorktrees().map((worktree) => worktree.path));
|
|
11974
|
+
const linearResult = await linearIssuesPromise;
|
|
11975
|
+
const archivedPaths = buildArchivedWorktreePathSet(archiveState);
|
|
11756
11976
|
const linearIssues = linearResult.ok ? linearResult.data : [];
|
|
11757
11977
|
return jsonResponse(buildProjectSnapshot({
|
|
11758
11978
|
projectName: config.name,
|
|
@@ -11760,6 +11980,7 @@ async function apiGetProject() {
|
|
|
11760
11980
|
runtime: projectRuntime,
|
|
11761
11981
|
creatingWorktrees: worktreeCreationTracker.list(),
|
|
11762
11982
|
notifications: runtimeNotifications.list(),
|
|
11983
|
+
isArchived: (path) => archivedPaths.has(normalizeArchivePath(path)),
|
|
11763
11984
|
findLinearIssue: (branch) => {
|
|
11764
11985
|
const match = linearIssues.find((issue) => branchMatchesIssue(branch, issue.branchName));
|
|
11765
11986
|
return match ? {
|
|
@@ -11937,6 +12158,21 @@ async function apiCloseWorktree(name) {
|
|
|
11937
12158
|
log.debug(`[worktree:close] done name=${name}`);
|
|
11938
12159
|
return jsonResponse({ ok: true });
|
|
11939
12160
|
}
|
|
12161
|
+
async function apiSetWorktreeArchived(name, req) {
|
|
12162
|
+
ensureBranchNotBusy(name);
|
|
12163
|
+
const raw = await req.json();
|
|
12164
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12165
|
+
return errorResponse("Invalid request body", 400);
|
|
12166
|
+
}
|
|
12167
|
+
const body = raw;
|
|
12168
|
+
if (typeof body.archived !== "boolean") {
|
|
12169
|
+
return errorResponse("Missing boolean 'archived' field", 400);
|
|
12170
|
+
}
|
|
12171
|
+
log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
|
|
12172
|
+
await lifecycleService.setWorktreeArchived(name, body.archived);
|
|
12173
|
+
log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
|
|
12174
|
+
return jsonResponse({ ok: true, archived: body.archived });
|
|
12175
|
+
}
|
|
11940
12176
|
async function apiSendPrompt(name, req) {
|
|
11941
12177
|
ensureBranchNotBusy(name);
|
|
11942
12178
|
const raw = await req.json();
|
|
@@ -12009,7 +12245,7 @@ async function apiPullMain(req) {
|
|
|
12009
12245
|
return errorResponse(`Unknown linked repo: ${repo}`, 404);
|
|
12010
12246
|
if (!linkedRepo.dir)
|
|
12011
12247
|
return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
|
|
12012
|
-
const resolvedDir =
|
|
12248
|
+
const resolvedDir = resolve8(PROJECT_DIR, linkedRepo.dir);
|
|
12013
12249
|
const repoRoot = git.resolveRepoRoot(resolvedDir);
|
|
12014
12250
|
if (!repoRoot)
|
|
12015
12251
|
return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
|
|
@@ -12102,7 +12338,7 @@ async function apiUploadFiles(name, req) {
|
|
|
12102
12338
|
}
|
|
12103
12339
|
const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
|
|
12104
12340
|
const destPath = join7(uploadDir, safeName);
|
|
12105
|
-
if (!
|
|
12341
|
+
if (!resolve8(destPath).startsWith(uploadDir + "/")) {
|
|
12106
12342
|
return errorResponse("Invalid filename", 400);
|
|
12107
12343
|
}
|
|
12108
12344
|
await Bun.write(destPath, entry);
|
|
@@ -12169,6 +12405,14 @@ Bun.serve({
|
|
|
12169
12405
|
return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
|
|
12170
12406
|
}
|
|
12171
12407
|
},
|
|
12408
|
+
"/api/worktrees/:name/archive": {
|
|
12409
|
+
PUT: (req) => {
|
|
12410
|
+
const name = decodeURIComponent(req.params.name);
|
|
12411
|
+
if (!isValidWorktreeName(name))
|
|
12412
|
+
return errorResponse("Invalid worktree name", 400);
|
|
12413
|
+
return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
|
|
12414
|
+
}
|
|
12415
|
+
},
|
|
12172
12416
|
"/api/worktrees/:name/send": {
|
|
12173
12417
|
POST: (req) => {
|
|
12174
12418
|
const name = decodeURIComponent(req.params.name);
|
|
@@ -12235,8 +12479,8 @@ Bun.serve({
|
|
|
12235
12479
|
const url = new URL(req.url);
|
|
12236
12480
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
12237
12481
|
const filePath = join7(STATIC_DIR, rawPath);
|
|
12238
|
-
const staticRoot =
|
|
12239
|
-
if (!
|
|
12482
|
+
const staticRoot = resolve8(STATIC_DIR);
|
|
12483
|
+
if (!resolve8(filePath).startsWith(staticRoot + "/")) {
|
|
12240
12484
|
return new Response("Forbidden", { status: 403 });
|
|
12241
12485
|
}
|
|
12242
12486
|
const file = Bun.file(filePath);
|