agent-workbench 0.8.0 → 0.8.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 +5 -3
- package/dist/server.js +629 -138
- package/dist/web/assets/{de-CsDV7Kuv.js → de-CDXh9n3d.js} +1 -1
- package/dist/web/assets/{es-1bnGoSgS.js → es-CLg7TmrV.js} +1 -1
- package/dist/web/assets/{fr-BqyJYy32.js → fr-Pnm-P-lG.js} +1 -1
- package/dist/web/assets/{highlight-BVnJ3VdZ.js → highlight-BUWAzUc6.js} +1 -1
- package/dist/web/assets/index-Hua8TqlR.js +156 -0
- package/dist/web/assets/{ja-xHkS6kKf.js → ja-COKYooK_.js} +1 -1
- package/dist/web/assets/{ko-CC_KhzvE.js → ko-CGAnMMHl.js} +1 -1
- package/dist/web/assets/{pt-BR-Dctf7Mfd.js → pt-BR-DPgHVjMQ.js} +1 -1
- package/dist/web/assets/{ru-B7OPQhs_.js → ru-BfcWzg67.js} +1 -1
- package/dist/web/assets/{zh-CN-Llsx2Xa1.js → zh-CN-Xcl7z1Nt.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-DOfNPF9Z.js +0 -156
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// packages/server/src/index.ts
|
|
4
4
|
import { existsSync as existsSync4 } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import
|
|
6
|
+
import path75 from "node:path";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
8
|
import express from "express";
|
|
9
9
|
|
|
@@ -144,20 +144,25 @@ var WORKTREE_COPY_MAX_FILES = 2e3;
|
|
|
144
144
|
var WORKTREE_COPY_MAX_BYTES = 50 * 1024 * 1024;
|
|
145
145
|
var WORKTREE_COPY_SAMPLE = 8;
|
|
146
146
|
var WORKTREE_CREATE_MODES = ["new", "existing"];
|
|
147
|
+
var WORKTREE_HOLDERS_MAX = 8;
|
|
147
148
|
var WORKTREE_PLAN_PATHS_MAX = 20;
|
|
149
|
+
var WORKTREE_UNTRACKED_MAX_FILES = 2e4;
|
|
148
150
|
var WORKTREE_ROW_FILES_MAX = 8;
|
|
149
151
|
var WORKTREE_CHANGES_COMMITS_MAX = 50;
|
|
150
152
|
var WORKTREE_CHANGES_FILES_MAX = 2e3;
|
|
151
153
|
function asCommitId(value) {
|
|
152
154
|
return typeof value === "string" && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value) ? value : null;
|
|
153
155
|
}
|
|
156
|
+
function asUntrackedStamp(value) {
|
|
157
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value) ? value : null;
|
|
158
|
+
}
|
|
154
159
|
function parseSessionWorktree(value) {
|
|
155
160
|
const record = asRecord(value);
|
|
156
161
|
if (record === null) return null;
|
|
157
162
|
const branch = asNonEmptyString(record["branch"]);
|
|
158
|
-
const
|
|
159
|
-
if (branch === null ||
|
|
160
|
-
return { branch, exists:
|
|
163
|
+
const exists7 = asBoolean(record["exists"]);
|
|
164
|
+
if (branch === null || exists7 === null) return null;
|
|
165
|
+
return { branch, exists: exists7, reopenable: record["reopenable"] === true };
|
|
161
166
|
}
|
|
162
167
|
function parseWorktreeSource(value) {
|
|
163
168
|
const record = asRecord(value);
|
|
@@ -172,6 +177,16 @@ function parseWorktreeSource(value) {
|
|
|
172
177
|
}
|
|
173
178
|
return null;
|
|
174
179
|
}
|
|
180
|
+
var HOLDER_TEXT_MAX = 255;
|
|
181
|
+
function parseWorktreeHolder(value) {
|
|
182
|
+
const record = asRecord(value);
|
|
183
|
+
if (record === null) return null;
|
|
184
|
+
const pid = asPositiveInt(record["pid"]);
|
|
185
|
+
const name = asNonEmptyString(record["name"]);
|
|
186
|
+
if (pid === null || name === null || name.length > HOLDER_TEXT_MAX) return null;
|
|
187
|
+
const description = asString(record["description"])?.trim() ?? "";
|
|
188
|
+
return { pid, name, description: description.length > 0 && description.length <= HOLDER_TEXT_MAX ? description : null };
|
|
189
|
+
}
|
|
175
190
|
|
|
176
191
|
// packages/shared/src/models.ts
|
|
177
192
|
var SESSION_TITLE_SOURCES = [
|
|
@@ -1326,7 +1341,10 @@ function parseClientMessage(raw) {
|
|
|
1326
1341
|
const requestId = asNonEmptyString(record["requestId"]);
|
|
1327
1342
|
const worktreeId = asNonEmptyString(record["worktreeId"]);
|
|
1328
1343
|
const discardConflicts = asBoolean(record["discardConflicts"]);
|
|
1329
|
-
|
|
1344
|
+
if (requestId === null || worktreeId === null || discardConflicts === null) return null;
|
|
1345
|
+
const message = { type: "worktree.remove", requestId, worktreeId, discardConflicts };
|
|
1346
|
+
const discardUntracked = asUntrackedStamp(record["discardUntracked"]);
|
|
1347
|
+
return discardUntracked === null ? message : { ...message, discardUntracked };
|
|
1330
1348
|
}
|
|
1331
1349
|
case "worktree.reopen": {
|
|
1332
1350
|
const requestId = asNonEmptyString(record["requestId"]);
|
|
@@ -1364,8 +1382,8 @@ function parseClientMessage(raw) {
|
|
|
1364
1382
|
case "worktree.changesDiff": {
|
|
1365
1383
|
const requestId = asNonEmptyString(record["requestId"]);
|
|
1366
1384
|
const worktreeId = asNonEmptyString(record["worktreeId"]);
|
|
1367
|
-
const
|
|
1368
|
-
return requestId === null || worktreeId === null ||
|
|
1385
|
+
const path76 = asNonEmptyString(record["path"]);
|
|
1386
|
+
return requestId === null || worktreeId === null || path76 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path76 };
|
|
1369
1387
|
}
|
|
1370
1388
|
case "worktree.requestReview": {
|
|
1371
1389
|
const requestId = asNonEmptyString(record["requestId"]);
|
|
@@ -5962,18 +5980,18 @@ var BranchTracker = class {
|
|
|
5962
5980
|
}
|
|
5963
5981
|
/** La linea y todas las de las que cuelga. */
|
|
5964
5982
|
pathFrom(leaf) {
|
|
5965
|
-
const
|
|
5983
|
+
const path76 = /* @__PURE__ */ new Set();
|
|
5966
5984
|
let current = leaf;
|
|
5967
|
-
while (current !== null && !
|
|
5985
|
+
while (current !== null && !path76.has(current)) {
|
|
5968
5986
|
const node = this.nodes.get(current);
|
|
5969
5987
|
if (node === void 0) break;
|
|
5970
|
-
|
|
5988
|
+
path76.add(current);
|
|
5971
5989
|
current = node.parent;
|
|
5972
5990
|
}
|
|
5973
|
-
return
|
|
5991
|
+
return path76;
|
|
5974
5992
|
}
|
|
5975
5993
|
recompute() {
|
|
5976
|
-
const
|
|
5994
|
+
const path76 = this.pathFrom(this.leaf());
|
|
5977
5995
|
const children = /* @__PURE__ */ new Map();
|
|
5978
5996
|
for (const [uuid, node] of this.nodes) {
|
|
5979
5997
|
const key = node.parent !== null && this.nodes.has(node.parent) ? node.parent : null;
|
|
@@ -5982,9 +6000,9 @@ var BranchTracker = class {
|
|
|
5982
6000
|
else list.push(uuid);
|
|
5983
6001
|
}
|
|
5984
6002
|
const roots = [];
|
|
5985
|
-
for (const uuid of children.get(null) ?? []) if (!
|
|
5986
|
-
for (const uuid of
|
|
5987
|
-
for (const child of children.get(uuid) ?? []) if (!
|
|
6003
|
+
for (const uuid of children.get(null) ?? []) if (!path76.has(uuid)) roots.push(uuid);
|
|
6004
|
+
for (const uuid of path76) {
|
|
6005
|
+
for (const child of children.get(uuid) ?? []) if (!path76.has(child)) roots.push(child);
|
|
5988
6006
|
}
|
|
5989
6007
|
const hidden = /* @__PURE__ */ new Set();
|
|
5990
6008
|
for (const root of roots) {
|
|
@@ -19299,8 +19317,8 @@ function buildContinuationMessage(input) {
|
|
|
19299
19317
|
}
|
|
19300
19318
|
return sanitizeForPaste(lines.join("\n"));
|
|
19301
19319
|
}
|
|
19302
|
-
function transcriptReferenceFor(
|
|
19303
|
-
return style === "at-quoted" ? fileReference(
|
|
19320
|
+
function transcriptReferenceFor(path76, style) {
|
|
19321
|
+
return style === "at-quoted" ? fileReference(path76, "at-quoted") : `"${path76}"`;
|
|
19304
19322
|
}
|
|
19305
19323
|
function planTranscript(input) {
|
|
19306
19324
|
const turns = splitTurns(input.events);
|
|
@@ -21145,9 +21163,9 @@ function watchSessions(index, hub, agents, options = {}) {
|
|
|
21145
21163
|
const probe = setInterval(() => {
|
|
21146
21164
|
if (checking) return;
|
|
21147
21165
|
checking = true;
|
|
21148
|
-
void isDirectory3(root.path).then((
|
|
21166
|
+
void isDirectory3(root.path).then((exists7) => {
|
|
21149
21167
|
checking = false;
|
|
21150
|
-
if (!
|
|
21168
|
+
if (!exists7 || stopped || !probes.has(probe)) return;
|
|
21151
21169
|
clearInterval(probe);
|
|
21152
21170
|
probes.delete(probe);
|
|
21153
21171
|
watchRoot(agent, root, true);
|
|
@@ -21162,9 +21180,9 @@ function watchSessions(index, hub, agents, options = {}) {
|
|
|
21162
21180
|
subscribeRoot(adapter.id, root, root.watch);
|
|
21163
21181
|
continue;
|
|
21164
21182
|
}
|
|
21165
|
-
void isDirectory3(root.path).then((
|
|
21183
|
+
void isDirectory3(root.path).then((exists7) => {
|
|
21166
21184
|
if (stopped) return;
|
|
21167
|
-
if (
|
|
21185
|
+
if (exists7) watchRoot(adapter.id, root, false);
|
|
21168
21186
|
else probeRoot(adapter.id, root);
|
|
21169
21187
|
});
|
|
21170
21188
|
}
|
|
@@ -22915,16 +22933,19 @@ async function searchFiles(cwd, query, folders, options = {}) {
|
|
|
22915
22933
|
truncated: truncated || found.length > SEARCH_MAX_RESULTS
|
|
22916
22934
|
};
|
|
22917
22935
|
}
|
|
22918
|
-
async function
|
|
22919
|
-
const
|
|
22936
|
+
async function existingPaths(cwd, relativePaths, folders) {
|
|
22937
|
+
const files = [];
|
|
22938
|
+
const directories = [];
|
|
22920
22939
|
for (const relativePath of relativePaths) {
|
|
22921
22940
|
try {
|
|
22922
22941
|
const absolute = await resolveForFiles(cwd, relativePath, folders);
|
|
22923
|
-
|
|
22942
|
+
const info = await stat37(absolute);
|
|
22943
|
+
if (info.isFile()) files.push(relativePath);
|
|
22944
|
+
else if (info.isDirectory()) directories.push(relativePath);
|
|
22924
22945
|
} catch {
|
|
22925
22946
|
}
|
|
22926
22947
|
}
|
|
22927
|
-
return
|
|
22948
|
+
return { files, directories };
|
|
22928
22949
|
}
|
|
22929
22950
|
function contentHash(content) {
|
|
22930
22951
|
return createHash4("sha256").update(content).digest("hex");
|
|
@@ -25055,12 +25076,12 @@ var VaultService = class {
|
|
|
25055
25076
|
// packages/server/src/worktrees/service.ts
|
|
25056
25077
|
import { randomBytes as randomBytes6, randomUUID as randomUUID7 } from "node:crypto";
|
|
25057
25078
|
import { EventEmitter as EventEmitter7 } from "node:events";
|
|
25058
|
-
import { mkdir as mkdir22, rename as rename7, rm as
|
|
25059
|
-
import
|
|
25079
|
+
import { lstat as lstat5, mkdir as mkdir22, rename as rename7, rm as rm10, rmdir, stat as stat45 } from "node:fs/promises";
|
|
25080
|
+
import path72 from "node:path";
|
|
25060
25081
|
|
|
25061
25082
|
// packages/server/src/worktrees/git.ts
|
|
25062
25083
|
import { execFile as execFile6 } from "node:child_process";
|
|
25063
|
-
import { mkdtemp, rm as rm7 } from "node:fs/promises";
|
|
25084
|
+
import { mkdtemp, rm as rm7, stat as stat43 } from "node:fs/promises";
|
|
25064
25085
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
25065
25086
|
import path65 from "node:path";
|
|
25066
25087
|
var READ_TIMEOUT_MS = 15e3;
|
|
@@ -25073,7 +25094,7 @@ function run(cwd, args, options) {
|
|
|
25073
25094
|
return new Promise((resolve, reject) => {
|
|
25074
25095
|
const child = execFile6(
|
|
25075
25096
|
"git",
|
|
25076
|
-
["-c", "core.quotepath=false", ...options.write ? [] : ["--no-optional-locks"], ...args],
|
|
25097
|
+
["-c", "core.quotepath=false", "-c", "core.longpaths=true", ...options.write ? [] : ["--no-optional-locks"], ...args],
|
|
25077
25098
|
{
|
|
25078
25099
|
cwd,
|
|
25079
25100
|
timeout: options.write ? WRITE_TIMEOUT_MS : READ_TIMEOUT_MS,
|
|
@@ -25126,15 +25147,42 @@ async function localBranches(cwd) {
|
|
|
25126
25147
|
return result.stdout.split(/\r?\n/).map((line) => branchOfRef(line.trim())).filter((branch) => branch !== null);
|
|
25127
25148
|
}
|
|
25128
25149
|
async function listWorktrees(cwd) {
|
|
25150
|
+
return await readWorktreeList(cwd) ?? [];
|
|
25151
|
+
}
|
|
25152
|
+
async function readWorktreeList(cwd) {
|
|
25129
25153
|
const result = await read(cwd, ["worktree", "list", "--porcelain", "-z"]);
|
|
25130
|
-
return result.code === 0 ? parseWorktreeListZ(result.stdout) :
|
|
25154
|
+
return result.code === 0 ? parseWorktreeListZ(result.stdout) : null;
|
|
25131
25155
|
}
|
|
25132
|
-
async function
|
|
25156
|
+
async function uncommittedChanges(cwd) {
|
|
25133
25157
|
const result = await read(cwd, ["status", "--porcelain=v2", "-z", "--untracked-files=normal"]);
|
|
25134
25158
|
if (result.code !== 0) return null;
|
|
25159
|
+
const parsed = parsePorcelainV2(result.stdout);
|
|
25135
25160
|
const seen = /* @__PURE__ */ new Set();
|
|
25136
|
-
|
|
25137
|
-
|
|
25161
|
+
const untracked = [];
|
|
25162
|
+
for (const change of parsed.changes) {
|
|
25163
|
+
seen.add(change.path);
|
|
25164
|
+
if (change.kind === "untracked") untracked.push(change.path);
|
|
25165
|
+
}
|
|
25166
|
+
return { paths: [...seen], untracked, complete: !parsed.truncated };
|
|
25167
|
+
}
|
|
25168
|
+
async function uncommittedPaths(cwd) {
|
|
25169
|
+
return (await uncommittedChanges(cwd))?.paths ?? null;
|
|
25170
|
+
}
|
|
25171
|
+
async function hasLoadedSubmodule(cwd) {
|
|
25172
|
+
const [modules, staged] = await Promise.all([read(cwd, ["rev-parse", "--git-path", "modules"]), read(cwd, ["ls-files", "--stage", "-z"])]);
|
|
25173
|
+
if (modules.code !== 0 || staged.code !== 0) return null;
|
|
25174
|
+
if (await isDirectory4(path65.resolve(cwd, modules.stdout.trim()))) return true;
|
|
25175
|
+
const submodules = staged.stdout.split("\0").filter((line) => line.startsWith("160000 ")).map((line) => line.slice(line.indexOf(" ") + 1));
|
|
25176
|
+
for (const submodule of submodules) {
|
|
25177
|
+
if (await exists4(path65.join(cwd, submodule, ".git"))) return true;
|
|
25178
|
+
}
|
|
25179
|
+
return false;
|
|
25180
|
+
}
|
|
25181
|
+
async function isDirectory4(candidate) {
|
|
25182
|
+
return (await stat43(candidate).catch(() => null))?.isDirectory() === true;
|
|
25183
|
+
}
|
|
25184
|
+
async function exists4(candidate) {
|
|
25185
|
+
return await stat43(candidate).catch(() => null) !== null;
|
|
25138
25186
|
}
|
|
25139
25187
|
async function isIntegrated(cwd, branch, base) {
|
|
25140
25188
|
const result = await read(cwd, ["merge-base", "--is-ancestor", `refs/heads/${branch}`, `refs/heads/${base}`]);
|
|
@@ -25342,10 +25390,247 @@ function fastForward(baseFolder, target) {
|
|
|
25342
25390
|
return run(baseFolder, fastForwardArgs(target), { write: true });
|
|
25343
25391
|
}
|
|
25344
25392
|
|
|
25393
|
+
// packages/server/src/worktrees/holders.ts
|
|
25394
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
25395
|
+
import path66 from "node:path";
|
|
25396
|
+
var SCAN_MAX_ENTRIES = 2e5;
|
|
25397
|
+
var SCAN_MAX_MS = 4e3;
|
|
25398
|
+
var HOLDERS_TIMEOUT_MS = 15e3;
|
|
25399
|
+
var OUTPUT_MAX_CHARS = 256 * 1024;
|
|
25400
|
+
var CSHARP = String.raw`
|
|
25401
|
+
using System;
|
|
25402
|
+
using System.Collections.Generic;
|
|
25403
|
+
using System.Diagnostics;
|
|
25404
|
+
using System.IO;
|
|
25405
|
+
using System.Runtime.InteropServices;
|
|
25406
|
+
using System.Text;
|
|
25407
|
+
using Microsoft.Win32.SafeHandles;
|
|
25408
|
+
|
|
25409
|
+
public static class AwFolderHolders {
|
|
25410
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
25411
|
+
struct IoStatus { public IntPtr Status; public IntPtr Information; }
|
|
25412
|
+
|
|
25413
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
25414
|
+
struct FindData {
|
|
25415
|
+
public uint Attributes;
|
|
25416
|
+
public uint Created1, Created2, Accessed1, Accessed2, Written1, Written2;
|
|
25417
|
+
public uint SizeHigh, SizeLow, Reserved0, Reserved1;
|
|
25418
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string Name;
|
|
25419
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string ShortName;
|
|
25420
|
+
}
|
|
25421
|
+
|
|
25422
|
+
[DllImport("ntdll.dll")]
|
|
25423
|
+
static extern int NtQueryInformationFile(SafeFileHandle file, out IoStatus status, IntPtr buffer, int length, int infoClass);
|
|
25424
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
25425
|
+
static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template);
|
|
25426
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
25427
|
+
static extern IntPtr FindFirstFileExW(string name, int level, out FindData data, int search, IntPtr filter, int flags);
|
|
25428
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
25429
|
+
static extern bool FindNextFileW(IntPtr find, out FindData data);
|
|
25430
|
+
[DllImport("kernel32.dll")]
|
|
25431
|
+
static extern bool FindClose(IntPtr find);
|
|
25432
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
25433
|
+
static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
|
|
25434
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
25435
|
+
static extern bool QueryFullProcessImageNameW(IntPtr process, int flags, StringBuilder name, ref int size);
|
|
25436
|
+
[DllImport("kernel32.dll")]
|
|
25437
|
+
static extern bool CloseHandle(IntPtr handle);
|
|
25438
|
+
|
|
25439
|
+
const uint ReadData = 0x1, ReadAttributes = 0x80, Delete = 0x10000, ShareAll = 7, OpenExisting = 3;
|
|
25440
|
+
/* BACKUP_SEMANTICS abre carpetas; OPEN_REPARSE_POINT abre el enlace y no su destino. */
|
|
25441
|
+
const uint OpenFlags = 0x02000000 | 0x00200000;
|
|
25442
|
+
const int ProcessIdsUsingFile = 47, QueryLimited = 0x1000, MaxNamed = 32;
|
|
25443
|
+
static readonly IntPtr NoFind = new IntPtr(-1);
|
|
25444
|
+
|
|
25445
|
+
static bool Taken(string path, bool top) {
|
|
25446
|
+
using (SafeFileHandle file = CreateFileW(path, top ? Delete : ReadData, top ? ShareAll : 0, IntPtr.Zero, OpenExisting, OpenFlags, IntPtr.Zero)) {
|
|
25447
|
+
if (!file.IsInvalid) return false;
|
|
25448
|
+
int error = Marshal.GetLastWin32Error();
|
|
25449
|
+
return error != 2 && error != 3;
|
|
25450
|
+
}
|
|
25451
|
+
}
|
|
25452
|
+
|
|
25453
|
+
static void Collect(string path, HashSet<int> seen, List<int> found, int self) {
|
|
25454
|
+
using (SafeFileHandle file = CreateFileW(path, ReadAttributes, ShareAll, IntPtr.Zero, OpenExisting, OpenFlags, IntPtr.Zero)) {
|
|
25455
|
+
if (file.IsInvalid) return;
|
|
25456
|
+
for (int size = 4096; size <= 1 << 20; size *= 4) {
|
|
25457
|
+
IntPtr buffer = Marshal.AllocHGlobal(size);
|
|
25458
|
+
try {
|
|
25459
|
+
IoStatus status;
|
|
25460
|
+
int code = NtQueryInformationFile(file, out status, buffer, size, ProcessIdsUsingFile);
|
|
25461
|
+
if (code == unchecked((int)0xC0000004) || code == unchecked((int)0x80000005)) continue;
|
|
25462
|
+
if (code != 0) return;
|
|
25463
|
+
int count = Marshal.ReadInt32(buffer);
|
|
25464
|
+
if (IntPtr.Size * (count + 1) > size) continue;
|
|
25465
|
+
for (int i = 0; i < count; i++) {
|
|
25466
|
+
int pid = (int)Marshal.ReadIntPtr(buffer, IntPtr.Size * (i + 1)).ToInt64();
|
|
25467
|
+
if (pid != self && seen.Add(pid)) found.Add(pid);
|
|
25468
|
+
}
|
|
25469
|
+
return;
|
|
25470
|
+
} finally { Marshal.FreeHGlobal(buffer); }
|
|
25471
|
+
}
|
|
25472
|
+
}
|
|
25473
|
+
}
|
|
25474
|
+
|
|
25475
|
+
static string Json(string value) {
|
|
25476
|
+
if (value == null) return "null";
|
|
25477
|
+
StringBuilder text = new StringBuilder("\"");
|
|
25478
|
+
foreach (char c in value) {
|
|
25479
|
+
if (c == '"' || c == '\\') text.Append('\\').Append(c);
|
|
25480
|
+
else if (c < 0x20 || c > 0x7E) text.Append("\\u").Append(((int)c).ToString("x4"));
|
|
25481
|
+
else text.Append(c);
|
|
25482
|
+
}
|
|
25483
|
+
return text.Append('"').ToString();
|
|
25484
|
+
}
|
|
25485
|
+
|
|
25486
|
+
public static string Scan(string root, int maxEntries, int maxMs) {
|
|
25487
|
+
Stopwatch clock = Stopwatch.StartNew();
|
|
25488
|
+
int self = Process.GetCurrentProcess().Id;
|
|
25489
|
+
HashSet<int> seen = new HashSet<int>();
|
|
25490
|
+
List<int> found = new List<int>();
|
|
25491
|
+
List<string> files = new List<string>();
|
|
25492
|
+
Queue<string> dirs = new Queue<string>();
|
|
25493
|
+
bool complete = true;
|
|
25494
|
+
int entries = 0;
|
|
25495
|
+
if (Taken(root, true)) Collect(root, seen, found, self);
|
|
25496
|
+
dirs.Enqueue(root);
|
|
25497
|
+
while (dirs.Count > 0) {
|
|
25498
|
+
if (++entries > maxEntries || clock.ElapsedMilliseconds > maxMs) { complete = false; break; }
|
|
25499
|
+
string dir = dirs.Dequeue();
|
|
25500
|
+
if (dir != root && Taken(dir, false)) Collect(dir, seen, found, self);
|
|
25501
|
+
FindData data;
|
|
25502
|
+
IntPtr find = FindFirstFileExW(dir + "\\*", 1, out data, 0, IntPtr.Zero, 2);
|
|
25503
|
+
if (find == NoFind) continue;
|
|
25504
|
+
try {
|
|
25505
|
+
do {
|
|
25506
|
+
if (data.Name == "." || data.Name == "..") continue;
|
|
25507
|
+
string child = dir + "\\" + data.Name;
|
|
25508
|
+
if ((data.Attributes & 0x10) != 0 && (data.Attributes & 0x400) == 0) dirs.Enqueue(child);
|
|
25509
|
+
else if (files.Count < maxEntries) files.Add(child);
|
|
25510
|
+
} while (FindNextFileW(find, out data));
|
|
25511
|
+
} finally { FindClose(find); }
|
|
25512
|
+
}
|
|
25513
|
+
if (complete) {
|
|
25514
|
+
foreach (string file in files) {
|
|
25515
|
+
if (++entries > maxEntries || clock.ElapsedMilliseconds > maxMs) { complete = false; break; }
|
|
25516
|
+
if (Taken(file, false)) Collect(file, seen, found, self);
|
|
25517
|
+
}
|
|
25518
|
+
}
|
|
25519
|
+
StringBuilder json = new StringBuilder("{\"complete\":");
|
|
25520
|
+
json.Append(complete ? "true" : "false").Append(",\"ms\":").Append(clock.ElapsedMilliseconds).Append(",\"holders\":[");
|
|
25521
|
+
int named = 0;
|
|
25522
|
+
foreach (int pid in found) {
|
|
25523
|
+
if (named >= MaxNamed) break;
|
|
25524
|
+
string name = null, description = null;
|
|
25525
|
+
IntPtr process = OpenProcess(QueryLimited, false, pid);
|
|
25526
|
+
if (process != IntPtr.Zero) {
|
|
25527
|
+
try {
|
|
25528
|
+
StringBuilder image = new StringBuilder(32768);
|
|
25529
|
+
int length = image.Capacity;
|
|
25530
|
+
if (QueryFullProcessImageNameW(process, 0, image, ref length)) {
|
|
25531
|
+
string full = image.ToString(0, length);
|
|
25532
|
+
name = Path.GetFileName(full);
|
|
25533
|
+
try { description = FileVersionInfo.GetVersionInfo(full).FileDescription; } catch { }
|
|
25534
|
+
}
|
|
25535
|
+
} finally { CloseHandle(process); }
|
|
25536
|
+
}
|
|
25537
|
+
if (name == null) {
|
|
25538
|
+
try { name = pid == 4 ? "System" : Process.GetProcessById(pid).ProcessName + ".exe"; } catch { continue; }
|
|
25539
|
+
}
|
|
25540
|
+
json.Append(named++ == 0 ? "" : ",").Append("{\"pid\":").Append(pid).Append(",\"name\":").Append(Json(name))
|
|
25541
|
+
.Append(",\"description\":").Append(Json(description)).Append('}');
|
|
25542
|
+
}
|
|
25543
|
+
return json.Append("]}").ToString();
|
|
25544
|
+
}
|
|
25545
|
+
}
|
|
25546
|
+
`;
|
|
25547
|
+
function longPath(folder) {
|
|
25548
|
+
const absolute = path66.win32.resolve(folder);
|
|
25549
|
+
if (absolute.startsWith("\\\\?\\")) return absolute;
|
|
25550
|
+
return absolute.startsWith("\\\\") ? `\\\\?\\UNC\\${absolute.slice(2)}` : `\\\\?\\${absolute}`;
|
|
25551
|
+
}
|
|
25552
|
+
function holdersScript(folder) {
|
|
25553
|
+
const encoded = Buffer.from(longPath(folder), "utf8").toString("base64");
|
|
25554
|
+
return [
|
|
25555
|
+
"$ErrorActionPreference = 'Stop'",
|
|
25556
|
+
`$source = @'
|
|
25557
|
+
${CSHARP}
|
|
25558
|
+
'@`,
|
|
25559
|
+
"Add-Type -TypeDefinition $source",
|
|
25560
|
+
`$root = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,
|
|
25561
|
+
`[Console]::Out.WriteLine([AwFolderHolders]::Scan($root, ${SCAN_MAX_ENTRIES}, ${SCAN_MAX_MS}))`,
|
|
25562
|
+
// Con `-Command -`, la última línea corre recién con la línea en blanco de después.
|
|
25563
|
+
"",
|
|
25564
|
+
""
|
|
25565
|
+
].join("\n");
|
|
25566
|
+
}
|
|
25567
|
+
function parseHoldersOutput(output, selfPid) {
|
|
25568
|
+
const line = output.split(/\r?\n/).map((candidate) => candidate.trim()).find((candidate) => candidate.startsWith('{"complete":'));
|
|
25569
|
+
const record = line === void 0 ? null : asRecord(parseJson(line));
|
|
25570
|
+
if (record === null || typeof record["complete"] !== "boolean" || !Array.isArray(record["holders"])) return null;
|
|
25571
|
+
const holders = [];
|
|
25572
|
+
let self = false;
|
|
25573
|
+
for (const item of record["holders"]) {
|
|
25574
|
+
const holder = parseWorktreeHolder(item);
|
|
25575
|
+
if (holder === null || holders.some((other) => other.pid === holder.pid)) continue;
|
|
25576
|
+
if (holder.pid === selfPid) self = true;
|
|
25577
|
+
else if (holders.length < WORKTREE_HOLDERS_MAX) holders.push(holder);
|
|
25578
|
+
}
|
|
25579
|
+
return { holders, complete: record["complete"], self };
|
|
25580
|
+
}
|
|
25581
|
+
function powershellPath() {
|
|
25582
|
+
const systemRoot = process.env["SystemRoot"] ?? process.env["windir"] ?? "C:\\Windows";
|
|
25583
|
+
return path66.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
25584
|
+
}
|
|
25585
|
+
function runPowerShell(script) {
|
|
25586
|
+
return new Promise((resolve) => {
|
|
25587
|
+
const child = spawn4(powershellPath(), ["-NoProfile", "-NonInteractive", "-Command", "-"], {
|
|
25588
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
25589
|
+
windowsHide: true
|
|
25590
|
+
});
|
|
25591
|
+
let output = "";
|
|
25592
|
+
let settled = false;
|
|
25593
|
+
const finish = (value) => {
|
|
25594
|
+
if (settled) return;
|
|
25595
|
+
settled = true;
|
|
25596
|
+
clearTimeout(timer);
|
|
25597
|
+
resolve(value);
|
|
25598
|
+
};
|
|
25599
|
+
const timer = setTimeout(() => {
|
|
25600
|
+
child.kill();
|
|
25601
|
+
finish(null);
|
|
25602
|
+
}, HOLDERS_TIMEOUT_MS);
|
|
25603
|
+
timer.unref();
|
|
25604
|
+
child.stdout.setEncoding("utf8");
|
|
25605
|
+
child.stdout.on("data", (chunk) => {
|
|
25606
|
+
output += chunk;
|
|
25607
|
+
if (output.length <= OUTPUT_MAX_CHARS) return;
|
|
25608
|
+
child.kill();
|
|
25609
|
+
finish(null);
|
|
25610
|
+
});
|
|
25611
|
+
child.on("error", () => finish(null));
|
|
25612
|
+
child.on("close", () => finish(output));
|
|
25613
|
+
child.stdin.on("error", () => void 0);
|
|
25614
|
+
child.stdin.end(script);
|
|
25615
|
+
});
|
|
25616
|
+
}
|
|
25617
|
+
async function folderHolders(folder) {
|
|
25618
|
+
if (process.platform !== "win32") return [];
|
|
25619
|
+
const output = await runPowerShell(holdersScript(folder));
|
|
25620
|
+
const parsed = output === null ? null : parseHoldersOutput(output, process.pid);
|
|
25621
|
+
if (parsed === null) {
|
|
25622
|
+
console.warn(`[worktrees] couldn't tell which programs have ${folder} open`);
|
|
25623
|
+
return [];
|
|
25624
|
+
}
|
|
25625
|
+
if (parsed.self) console.warn(`[worktrees] this server has ${folder} open`);
|
|
25626
|
+
debugLog("worktrees", `holders of ${folder}: ${parsed.holders.length}, complete ${parsed.complete}`);
|
|
25627
|
+
return parsed.holders;
|
|
25628
|
+
}
|
|
25629
|
+
|
|
25345
25630
|
// packages/server/src/worktrees/include.ts
|
|
25346
25631
|
import { createHash as createHash6 } from "node:crypto";
|
|
25347
25632
|
import { lstat as lstat3, mkdir as mkdir19, readdir as readdir19, readFile as readFile28, writeFile as writeFile11 } from "node:fs/promises";
|
|
25348
|
-
import
|
|
25633
|
+
import path67 from "node:path";
|
|
25349
25634
|
|
|
25350
25635
|
// packages/server/src/worktrees/merge-notes.ts
|
|
25351
25636
|
var MERGE_MAX_EDITS = 2e3;
|
|
@@ -25521,7 +25806,7 @@ var CLI_CONFIG_DIRS = /* @__PURE__ */ new Set([".claude", ".codex", ".gemini", "
|
|
|
25521
25806
|
var CLI_CONFIG_FILES = /* @__PURE__ */ new Set([".mcp.json", "opencode.json", "opencode.jsonc"]);
|
|
25522
25807
|
var INCLUDE_FILE = ".worktreeinclude";
|
|
25523
25808
|
var INCLUDE_FILE_MAX_BYTES = 64 * 1024;
|
|
25524
|
-
var toPosix2 = (value) => value.split(
|
|
25809
|
+
var toPosix2 = (value) => value.split(path67.sep).join("/");
|
|
25525
25810
|
function hashOf(content) {
|
|
25526
25811
|
return createHash6("sha256").update(content).digest("hex");
|
|
25527
25812
|
}
|
|
@@ -25628,7 +25913,7 @@ async function copyToWorktree(mainRoot, folder, files, bases) {
|
|
|
25628
25913
|
if (!info.isFile()) continue;
|
|
25629
25914
|
content = await readFile28(source);
|
|
25630
25915
|
const target = await resolveInside(folder, file.path);
|
|
25631
|
-
await mkdir19(
|
|
25916
|
+
await mkdir19(path67.dirname(target), { recursive: true });
|
|
25632
25917
|
await writeFile11(target, content, { flag: "wx" });
|
|
25633
25918
|
copied.push({ path: file.path, hash: hashOf(content) });
|
|
25634
25919
|
} catch (error) {
|
|
@@ -25768,7 +26053,7 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
|
|
|
25768
26053
|
result.skipped.push(file);
|
|
25769
26054
|
continue;
|
|
25770
26055
|
}
|
|
25771
|
-
await mkdir19(
|
|
26056
|
+
await mkdir19(path67.dirname(target), { recursive: true });
|
|
25772
26057
|
await writeAtomically(target, outcome.content, {
|
|
25773
26058
|
beforeRename: async () => {
|
|
25774
26059
|
const now = await readInside(mainRoot, file);
|
|
@@ -25790,15 +26075,15 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
|
|
|
25790
26075
|
}
|
|
25791
26076
|
|
|
25792
26077
|
// packages/server/src/worktrees/names.ts
|
|
25793
|
-
import
|
|
25794
|
-
function worktreeFolderFor(mainRoot, branch, pathApi =
|
|
26078
|
+
import path68 from "node:path";
|
|
26079
|
+
function worktreeFolderFor(mainRoot, branch, pathApi = path68) {
|
|
25795
26080
|
const parent = pathApi.dirname(mainRoot);
|
|
25796
26081
|
return pathApi.join(parent, worktreesDirName(pathApi.basename(mainRoot)), worktreeFolderName(branch));
|
|
25797
26082
|
}
|
|
25798
|
-
function worktreesParentFor(mainRoot, pathApi =
|
|
26083
|
+
function worktreesParentFor(mainRoot, pathApi = path68) {
|
|
25799
26084
|
return pathApi.join(pathApi.dirname(mainRoot), worktreesDirName(pathApi.basename(mainRoot)));
|
|
25800
26085
|
}
|
|
25801
|
-
function mainRootFromShape(folder, pathApi =
|
|
26086
|
+
function mainRootFromShape(folder, pathApi = path68) {
|
|
25802
26087
|
const parent = pathApi.dirname(folder);
|
|
25803
26088
|
const parentName = pathApi.basename(parent);
|
|
25804
26089
|
if (parentName.length > ".worktrees".length && parentName.toLowerCase().endsWith(".worktrees")) {
|
|
@@ -25813,8 +26098,8 @@ function mainRootFromShape(folder, pathApi = path67) {
|
|
|
25813
26098
|
|
|
25814
26099
|
// packages/server/src/worktrees/note-bases.ts
|
|
25815
26100
|
import { createHash as createHash7 } from "node:crypto";
|
|
25816
|
-
import { mkdir as mkdir20, readdir as readdir20, readFile as readFile29, rm as rm8, stat as
|
|
25817
|
-
import
|
|
26101
|
+
import { mkdir as mkdir20, readdir as readdir20, readFile as readFile29, rm as rm8, stat as stat44, utimes as utimes3 } from "node:fs/promises";
|
|
26102
|
+
import path69 from "node:path";
|
|
25818
26103
|
var TEXT_NOTE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown", ".txt", ".rst", ".adoc"]);
|
|
25819
26104
|
var NOTE_BASE_MAX_BYTES = 2 * 1024 * 1024;
|
|
25820
26105
|
var PRUNE_GRACE_MS = 10 * 6e4;
|
|
@@ -25823,7 +26108,7 @@ function hashOf2(content) {
|
|
|
25823
26108
|
return createHash7("sha256").update(content).digest("hex");
|
|
25824
26109
|
}
|
|
25825
26110
|
function keepsNoteBase(relative, content) {
|
|
25826
|
-
return TEXT_NOTE_EXTENSIONS.has(
|
|
26111
|
+
return TEXT_NOTE_EXTENSIONS.has(path69.posix.extname(relative).toLowerCase()) && content.length <= NOTE_BASE_MAX_BYTES && !content.subarray(0, 8e3).includes(0);
|
|
25827
26112
|
}
|
|
25828
26113
|
var NoteBases = class {
|
|
25829
26114
|
constructor(dir = worktreeNotesDir(), options = {}) {
|
|
@@ -25843,7 +26128,7 @@ var NoteBases = class {
|
|
|
25843
26128
|
async save(relative, content) {
|
|
25844
26129
|
if (!keepsNoteBase(relative, content)) return;
|
|
25845
26130
|
const hash = hashOf2(content);
|
|
25846
|
-
const file =
|
|
26131
|
+
const file = path69.join(this.dir, hash);
|
|
25847
26132
|
const current = await readFile29(file).catch(() => null);
|
|
25848
26133
|
if (current !== null && hashOf2(current) === hash) {
|
|
25849
26134
|
const now = new Date(this.now());
|
|
@@ -25857,7 +26142,7 @@ var NoteBases = class {
|
|
|
25857
26142
|
async load(hash) {
|
|
25858
26143
|
if (!HASH.test(hash)) return null;
|
|
25859
26144
|
try {
|
|
25860
|
-
const content = await readFile29(
|
|
26145
|
+
const content = await readFile29(path69.join(this.dir, hash));
|
|
25861
26146
|
return hashOf2(content) === hash ? content : null;
|
|
25862
26147
|
} catch {
|
|
25863
26148
|
return null;
|
|
@@ -25874,9 +26159,9 @@ var NoteBases = class {
|
|
|
25874
26159
|
const now = this.now();
|
|
25875
26160
|
for (const name of names) {
|
|
25876
26161
|
if (!HASH.test(name) || keep.has(name)) continue;
|
|
25877
|
-
const file =
|
|
26162
|
+
const file = path69.join(this.dir, name);
|
|
25878
26163
|
try {
|
|
25879
|
-
const info = await
|
|
26164
|
+
const info = await stat44(file);
|
|
25880
26165
|
if (!info.isFile() || now - info.mtimeMs < this.graceMs) continue;
|
|
25881
26166
|
await rm8(file, { force: true });
|
|
25882
26167
|
} catch {
|
|
@@ -25929,7 +26214,7 @@ function catchUpPrompt(base, conflicts, uncommitted) {
|
|
|
25929
26214
|
}
|
|
25930
26215
|
function commitPrompt(files, count) {
|
|
25931
26216
|
const more = count > files.length ? ` and ${count - files.length} more` : "";
|
|
25932
|
-
return `This branch folder has uncommitted changes: ${files.join(", ")}${more}. Commit them with a clear message,
|
|
26217
|
+
return `This branch folder has uncommitted changes: ${files.join(", ")}${more}. Commit them with a clear message. If any of them shouldn't be committed, leave it as it is and tell me which and why. Don't push.`;
|
|
25933
26218
|
}
|
|
25934
26219
|
function lastReplyText(events) {
|
|
25935
26220
|
const textOf3 = (event2) => event2.parts.flatMap((part) => part.kind === "text" ? [part.text] : []).join("\n").trim();
|
|
@@ -25964,7 +26249,7 @@ function readVerdict(reply) {
|
|
|
25964
26249
|
|
|
25965
26250
|
// packages/server/src/worktrees/store.ts
|
|
25966
26251
|
import { mkdir as mkdir21, readFile as readFile30 } from "node:fs/promises";
|
|
25967
|
-
import
|
|
26252
|
+
import path70 from "node:path";
|
|
25968
26253
|
var STATE_VERSION5 = 1;
|
|
25969
26254
|
var MAX_REMOVED = 300;
|
|
25970
26255
|
var MAX_REVIEW_SESSIONS = 20;
|
|
@@ -26102,16 +26387,106 @@ var WorktreeStore = class {
|
|
|
26102
26387
|
}
|
|
26103
26388
|
const content = Buffer.from(JSON.stringify({ version: STATE_VERSION5, worktrees: this.records }, null, 2), "utf8");
|
|
26104
26389
|
this.writing = this.writing.catch(() => void 0).then(async () => {
|
|
26105
|
-
await mkdir21(
|
|
26390
|
+
await mkdir21(path70.dirname(this.file), { recursive: true });
|
|
26106
26391
|
await writeAtomically(this.file, content);
|
|
26107
26392
|
});
|
|
26108
26393
|
return this.writing;
|
|
26109
26394
|
}
|
|
26110
26395
|
};
|
|
26111
26396
|
|
|
26397
|
+
// packages/server/src/worktrees/untracked.ts
|
|
26398
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
26399
|
+
import { lstat as lstat4, readdir as readdir21, realpath as realpath7, rm as rm9 } from "node:fs/promises";
|
|
26400
|
+
import path71 from "node:path";
|
|
26401
|
+
var sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
26402
|
+
function cleanEntry(entry) {
|
|
26403
|
+
const clean = entry.replace(/\/+$/, "");
|
|
26404
|
+
if (clean.length === 0 || clean.includes("\\")) return null;
|
|
26405
|
+
const parts = clean.split("/");
|
|
26406
|
+
return parts.some((part) => part.length === 0 || part === "." || part === ".." || sameName(part, ".git")) ? null : clean;
|
|
26407
|
+
}
|
|
26408
|
+
async function scanUntracked(folder, entries, maxFiles = WORKTREE_UNTRACKED_MAX_FILES) {
|
|
26409
|
+
const walk2 = { hash: createHash8("sha256"), files: 0, bytes: 0, max: maxFiles, blocked: null };
|
|
26410
|
+
const targets = [];
|
|
26411
|
+
for (const entry of [...entries].sort()) {
|
|
26412
|
+
const clean = cleanEntry(entry);
|
|
26413
|
+
if (clean === null) return null;
|
|
26414
|
+
let target;
|
|
26415
|
+
let real;
|
|
26416
|
+
try {
|
|
26417
|
+
const parent = await resolveInside(folder, path71.posix.dirname(clean), { mustExist: true });
|
|
26418
|
+
target = path71.join(parent, path71.posix.basename(clean));
|
|
26419
|
+
real = path71.join(await realpath7(parent), path71.posix.basename(clean));
|
|
26420
|
+
} catch {
|
|
26421
|
+
return null;
|
|
26422
|
+
}
|
|
26423
|
+
targets.push(target);
|
|
26424
|
+
walk2.hash.update(`${clean}\0`);
|
|
26425
|
+
await visit(target, real, clean, walk2);
|
|
26426
|
+
if (walk2.blocked !== null) return { files: walk2.files, bytes: walk2.bytes, stamp: null, blocked: walk2.blocked, targets: [] };
|
|
26427
|
+
}
|
|
26428
|
+
return { files: walk2.files, bytes: walk2.bytes, stamp: walk2.hash.digest("hex"), blocked: null, targets };
|
|
26429
|
+
}
|
|
26430
|
+
async function visit(absolute, real, relative, walk2) {
|
|
26431
|
+
if (walk2.files >= walk2.max) {
|
|
26432
|
+
walk2.blocked = "too-many";
|
|
26433
|
+
return;
|
|
26434
|
+
}
|
|
26435
|
+
let info;
|
|
26436
|
+
try {
|
|
26437
|
+
info = await lstat4(absolute);
|
|
26438
|
+
} catch {
|
|
26439
|
+
walk2.hash.update(`${relative}\0?
|
|
26440
|
+
`);
|
|
26441
|
+
return;
|
|
26442
|
+
}
|
|
26443
|
+
const linked = !info.isDirectory() || !sameName(await realpath7(absolute).catch(() => ""), real);
|
|
26444
|
+
if (linked) {
|
|
26445
|
+
walk2.files += 1;
|
|
26446
|
+
walk2.bytes += info.size;
|
|
26447
|
+
walk2.hash.update(`${relative}\0${info.isSymbolicLink() || info.isDirectory() ? "l" : "f"}\0${info.size}\0${info.mtimeMs}
|
|
26448
|
+
`);
|
|
26449
|
+
return;
|
|
26450
|
+
}
|
|
26451
|
+
walk2.hash.update(`${relative}\0d
|
|
26452
|
+
`);
|
|
26453
|
+
let names;
|
|
26454
|
+
try {
|
|
26455
|
+
names = (await readdir21(absolute)).sort();
|
|
26456
|
+
} catch {
|
|
26457
|
+
walk2.hash.update(`${relative}\0!
|
|
26458
|
+
`);
|
|
26459
|
+
return;
|
|
26460
|
+
}
|
|
26461
|
+
if (names.some((name) => sameName(name, ".git"))) {
|
|
26462
|
+
walk2.blocked = "nested-repo";
|
|
26463
|
+
return;
|
|
26464
|
+
}
|
|
26465
|
+
for (const name of names) {
|
|
26466
|
+
await visit(path71.join(absolute, name), path71.join(real, name), `${relative}/${name}`, walk2);
|
|
26467
|
+
if (walk2.blocked !== null) return;
|
|
26468
|
+
}
|
|
26469
|
+
}
|
|
26470
|
+
async function deleteUntracked(targets) {
|
|
26471
|
+
for (const target of targets) {
|
|
26472
|
+
try {
|
|
26473
|
+
await rm9(target, { recursive: true, maxRetries: 3 });
|
|
26474
|
+
} catch (error) {
|
|
26475
|
+
if (error.code !== "ENOENT") throw error;
|
|
26476
|
+
}
|
|
26477
|
+
}
|
|
26478
|
+
}
|
|
26479
|
+
|
|
26112
26480
|
// packages/server/src/worktrees/service.ts
|
|
26113
26481
|
var WorktreeError = class extends ServerTextError {
|
|
26114
26482
|
};
|
|
26483
|
+
var WorktreeInUseError = class extends WorktreeError {
|
|
26484
|
+
constructor(plan) {
|
|
26485
|
+
super(serverText("worktreeInUse"));
|
|
26486
|
+
this.plan = plan;
|
|
26487
|
+
}
|
|
26488
|
+
plan;
|
|
26489
|
+
};
|
|
26115
26490
|
var ROWS_TTL_MS = 5e3;
|
|
26116
26491
|
var RELEASE_TIMEOUT_MS = 5e3;
|
|
26117
26492
|
var RELEASE_POLL_MS = 100;
|
|
@@ -26120,17 +26495,20 @@ var REPLY_LIMITS = { textMaxChars: 2e5, toolInputMaxChars: 200, toolResultMaxCha
|
|
|
26120
26495
|
var REPLY_EVENTS = 300;
|
|
26121
26496
|
var REPLY_CACHE_MAX = 50;
|
|
26122
26497
|
var REVIEW_ACTIVITY_DELAY_MS = 1e3;
|
|
26123
|
-
async function
|
|
26498
|
+
async function leftWithoutGit(folder) {
|
|
26499
|
+
return await exists5(folder) && await lstat5(path72.join(folder, ".git")).catch(() => null) === null;
|
|
26500
|
+
}
|
|
26501
|
+
async function exists5(candidate) {
|
|
26124
26502
|
try {
|
|
26125
|
-
await
|
|
26503
|
+
await stat45(candidate);
|
|
26126
26504
|
return true;
|
|
26127
26505
|
} catch {
|
|
26128
26506
|
return false;
|
|
26129
26507
|
}
|
|
26130
26508
|
}
|
|
26131
26509
|
function isInside4(outer, inner) {
|
|
26132
|
-
const relative =
|
|
26133
|
-
return relative.length === 0 || !relative.startsWith("..") && !
|
|
26510
|
+
const relative = path72.relative(path72.resolve(outer), path72.resolve(inner));
|
|
26511
|
+
return relative.length === 0 || !relative.startsWith("..") && !path72.isAbsolute(relative);
|
|
26134
26512
|
}
|
|
26135
26513
|
function displayCommand(args) {
|
|
26136
26514
|
return ["git", ...args].map((arg) => /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg).join(" ");
|
|
@@ -26186,7 +26564,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26186
26564
|
/** La carpeta principal del repo de un proyecto o de una pestaña. */
|
|
26187
26565
|
async mainRootOf(source) {
|
|
26188
26566
|
const cwd = source.kind === "terminal" ? this.deps.registry.get(source.terminalId)?.cwd ?? null : this.deps.projectCwd(source.projectKey);
|
|
26189
|
-
if (cwd === null || !await
|
|
26567
|
+
if (cwd === null || !await exists5(cwd)) throw new WorktreeError(serverText("worktreeProjectUnknown"));
|
|
26190
26568
|
const layout = await repoLayout(cwd);
|
|
26191
26569
|
if (layout === null || layout.mainRoot === null) throw new WorktreeError(serverText("worktreeNotRepo"));
|
|
26192
26570
|
return layout.mainRoot;
|
|
@@ -26205,7 +26583,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26205
26583
|
return {
|
|
26206
26584
|
mainRoot,
|
|
26207
26585
|
parentDir: worktreesParentFor(mainRoot),
|
|
26208
|
-
separator:
|
|
26586
|
+
separator: path72.sep,
|
|
26209
26587
|
currentBranch: current,
|
|
26210
26588
|
branches: branches.map((name) => ({ name, checkedOut: checkedOut.has(name) })),
|
|
26211
26589
|
mainChanges: changes?.length ?? 0,
|
|
@@ -26227,9 +26605,9 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26227
26605
|
throw new WorktreeError(serverText("worktreeBranchInUse", { branch }));
|
|
26228
26606
|
}
|
|
26229
26607
|
const folder = worktreeFolderFor(mainRoot, branch);
|
|
26230
|
-
if (await
|
|
26608
|
+
if (await exists5(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
|
|
26231
26609
|
const copy = await planCopy(mainRoot);
|
|
26232
|
-
await mkdir22(
|
|
26610
|
+
await mkdir22(path72.dirname(folder), { recursive: true });
|
|
26233
26611
|
const added = await addWorktree(mainRoot, folder, branch, mode === "new" ? base : null);
|
|
26234
26612
|
if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
|
|
26235
26613
|
const { copied, copiedDirs } = await this.copyInto(mainRoot, folder, copy, request.copyNotes);
|
|
@@ -26274,13 +26652,13 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26274
26652
|
/** Las filas de "Ramas aparte" para la pestaña que pregunta. Vacío fuera de git. */
|
|
26275
26653
|
async rows(terminalId) {
|
|
26276
26654
|
const descriptor = this.deps.registry.get(terminalId);
|
|
26277
|
-
if (descriptor === null || !await
|
|
26655
|
+
if (descriptor === null || !await exists5(descriptor.cwd)) return [];
|
|
26278
26656
|
const layout = await repoLayout(descriptor.cwd);
|
|
26279
26657
|
if (layout === null || layout.mainRoot === null) return [];
|
|
26280
26658
|
const mainRoot = layout.mainRoot;
|
|
26281
26659
|
const tips = await branchTips(mainRoot);
|
|
26282
26660
|
const tipsStamp = [...tips].map(([branch, commit]) => `${branch}\0${commit}`).join("\n");
|
|
26283
|
-
const cacheKey2 =
|
|
26661
|
+
const cacheKey2 = path72.resolve(mainRoot).toLowerCase();
|
|
26284
26662
|
const cached = this.rowsCache.get(cacheKey2);
|
|
26285
26663
|
const fresh = cached !== void 0 && cached.tips === tipsStamp && this.now() - cached.at < ROWS_TTL_MS;
|
|
26286
26664
|
const generation = this.rowsGeneration;
|
|
@@ -26289,7 +26667,12 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26289
26667
|
return rows.map((row) => ({ ...row, isCurrent: samePathShape(row.path, layout.toplevel) }));
|
|
26290
26668
|
}
|
|
26291
26669
|
async readRows(mainRoot, tips) {
|
|
26292
|
-
const
|
|
26670
|
+
const listed = await readWorktreeList(mainRoot);
|
|
26671
|
+
const entries = [
|
|
26672
|
+
...(listed ?? []).filter((entry) => !entry.bare),
|
|
26673
|
+
// Las que git soltó a medias: sin su fila, no había cómo terminar de quitarlas.
|
|
26674
|
+
...listed === null ? [] : await this.releasedEntries(mainRoot, listed)
|
|
26675
|
+
];
|
|
26293
26676
|
const mainBranch = entries.find((entry) => entry.isMain)?.branch ?? null;
|
|
26294
26677
|
const read2 = await Promise.all(
|
|
26295
26678
|
entries.map(async (entry) => {
|
|
@@ -26336,7 +26719,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26336
26719
|
const baseTip = tips.get(base);
|
|
26337
26720
|
const branchTip = tips.get(branch);
|
|
26338
26721
|
if (baseTip === void 0 || branchTip === void 0) return Promise.resolve(null);
|
|
26339
|
-
const key = [
|
|
26722
|
+
const key = [path72.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
|
|
26340
26723
|
const cached = this.pairs.get(key);
|
|
26341
26724
|
if (cached !== void 0) {
|
|
26342
26725
|
this.pairs.delete(key);
|
|
@@ -26381,12 +26764,12 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26381
26764
|
/** Una rama aparte que existe en disco, para lo que la lee o le deja un pedido. */
|
|
26382
26765
|
async presentRecordOrThrow(id) {
|
|
26383
26766
|
const record = this.recordOrThrow(id);
|
|
26384
|
-
if (!await
|
|
26767
|
+
if (!await exists5(record.path)) throw new WorktreeError(serverText("worktreeUnknown"));
|
|
26385
26768
|
return record;
|
|
26386
26769
|
}
|
|
26387
26770
|
/** Las pestañas y consolas abiertas dentro de la carpeta de la rama. */
|
|
26388
26771
|
tabsIn(folder) {
|
|
26389
|
-
return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label ||
|
|
26772
|
+
return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label || path72.basename(tab.cwd) }));
|
|
26390
26773
|
}
|
|
26391
26774
|
/**
|
|
26392
26775
|
* Las pestañas de una carpeta de trabajo, sin las de otra que viva adentro
|
|
@@ -26399,17 +26782,70 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26399
26782
|
return !nested.some((inner) => isInside4(inner, cwd));
|
|
26400
26783
|
});
|
|
26401
26784
|
}
|
|
26785
|
+
/**
|
|
26786
|
+
* Los archivos nuevos que se pueden perder al quitar la rama (23-10-2026):
|
|
26787
|
+
* sólo si todo lo que no está commiteado es nuevo y git lo listó entero. null
|
|
26788
|
+
* si no; si git se va a negar a quitarla igual, bloqueada o con un submódulo
|
|
26789
|
+
* cargado (no se borra nada antes de saberlo); o si una entrada no se puede
|
|
26790
|
+
* borrar sin salir de la carpeta. Con algo adentro que la app copió de la
|
|
26791
|
+
* principal y la rama no ignora, no se ofrece (`copied`): no vuelve como una
|
|
26792
|
+
* nota, y se perdería lo que la rama le cambió.
|
|
26793
|
+
*/
|
|
26794
|
+
async untrackedIn(record, changes) {
|
|
26795
|
+
const onlyNew = changes.complete && changes.paths.length > 0 && changes.untracked.length === changes.paths.length;
|
|
26796
|
+
if (!onlyNew) return null;
|
|
26797
|
+
const [entries, submodule] = await Promise.all([listWorktrees(record.mainRoot), hasLoadedSubmodule(record.path)]);
|
|
26798
|
+
if (submodule !== false || entries.some((entry) => entry.locked && samePathShape(entry.path, record.path))) return null;
|
|
26799
|
+
const entryOf = (file) => changes.untracked.some((entry) => {
|
|
26800
|
+
const clean = entry.replace(/\/+$/, "").toLowerCase();
|
|
26801
|
+
return file.toLowerCase() === clean || file.toLowerCase().startsWith(`${clean}/`);
|
|
26802
|
+
});
|
|
26803
|
+
const copied = [];
|
|
26804
|
+
for (const file of record.copied) {
|
|
26805
|
+
if (entryOf(file.path) && await exists5(path72.join(record.path, file.path))) copied.push(file.path);
|
|
26806
|
+
}
|
|
26807
|
+
if (copied.length > 0) {
|
|
26808
|
+
const ignored = await ignoredAmong(record.path, copied);
|
|
26809
|
+
if (ignored === null || copied.some((file) => !ignored.has(file))) return { files: 0, bytes: 0, stamp: null, blocked: "copied", targets: [] };
|
|
26810
|
+
}
|
|
26811
|
+
return scanUntracked(record.path, changes.untracked);
|
|
26812
|
+
}
|
|
26813
|
+
/**
|
|
26814
|
+
* Borra los archivos nuevos que el usuario eligió perder, si siguen siendo
|
|
26815
|
+
* los que vio: se mira justo antes, con las pestañas de la rama cerradas y
|
|
26816
|
+
* las notas ya de vuelta. Si desaparecieron solos, no hay nada que borrar.
|
|
26817
|
+
* Devuelve cuántos archivos se perdieron.
|
|
26818
|
+
*/
|
|
26819
|
+
async discardNew(record, stamp2) {
|
|
26820
|
+
const changes = await uncommittedChanges(record.path);
|
|
26821
|
+
if (changes === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
|
|
26822
|
+
if (changes.paths.length === 0) return 0;
|
|
26823
|
+
const scan = await this.untrackedIn(record, changes);
|
|
26824
|
+
if (scan === null || scan.stamp === null || scan.stamp !== stamp2) throw new WorktreeError(serverText("worktreeUntrackedChanged"));
|
|
26825
|
+
try {
|
|
26826
|
+
await deleteUntracked(scan.targets);
|
|
26827
|
+
} catch (error) {
|
|
26828
|
+
throw new WorktreeError(serverText("worktreeDiscardFailed", { detail: error instanceof Error ? error.message : String(error) }));
|
|
26829
|
+
}
|
|
26830
|
+
return scan.files;
|
|
26831
|
+
}
|
|
26402
26832
|
async planFor(record) {
|
|
26403
|
-
const
|
|
26404
|
-
const
|
|
26405
|
-
|
|
26833
|
+
const onDisk = await exists5(record.path);
|
|
26834
|
+
const present = onDisk && !await this.releasedByGit(record);
|
|
26835
|
+
const [changes, notes, integrated] = await Promise.all([
|
|
26836
|
+
present ? uncommittedChanges(record.path) : Promise.resolve({ paths: [], untracked: [], complete: true }),
|
|
26406
26837
|
present ? reviewNotes(record.mainRoot, record.path, record, this.loadNoteBase) : Promise.resolve({ returning: [], merging: [], resolved: [], conflicts: [] }),
|
|
26407
26838
|
record.createdBranch ? isIntegrated(record.mainRoot, record.branch, record.base) : Promise.resolve(false)
|
|
26408
26839
|
]);
|
|
26409
|
-
if (
|
|
26840
|
+
if (changes === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
|
|
26841
|
+
const uncommitted = changes.paths;
|
|
26842
|
+
const untracked = await this.untrackedIn(record, changes);
|
|
26410
26843
|
const tabs = this.tabsIn(record.path);
|
|
26411
26844
|
const deletesBranch = integrated === true;
|
|
26412
26845
|
const branchUnused = deletesBranch && stillAtStart(record, await branchCommit(record.mainRoot, record.branch) ?? void 0);
|
|
26846
|
+
let inUse = !onDisk ? false : tabs.length > 0 ? null : await folderInUse(record.path);
|
|
26847
|
+
const holders = inUse === true ? await this.holdersOf(record.path) : [];
|
|
26848
|
+
if (inUse === true && holders.length === 0) inUse = await folderInUse(record.path);
|
|
26413
26849
|
return {
|
|
26414
26850
|
id: record.id,
|
|
26415
26851
|
branch: record.branch,
|
|
@@ -26420,13 +26856,15 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26420
26856
|
],
|
|
26421
26857
|
uncommitted: uncommitted.slice(0, WORKTREE_PLAN_PATHS_MAX),
|
|
26422
26858
|
uncommittedCount: uncommitted.length,
|
|
26859
|
+
// Sin las rutas absolutas de `targets`: al cliente le basta con lo que muestra y la huella.
|
|
26860
|
+
untracked: untracked === null ? null : { files: untracked.files, bytes: untracked.bytes, stamp: untracked.stamp, blocked: untracked.blocked },
|
|
26423
26861
|
tabs,
|
|
26424
26862
|
returning: notes.returning,
|
|
26425
26863
|
merging: notes.merging,
|
|
26426
26864
|
resolved: notes.resolved,
|
|
26427
26865
|
conflicts: notes.conflicts,
|
|
26428
|
-
|
|
26429
|
-
|
|
26866
|
+
inUse,
|
|
26867
|
+
holders,
|
|
26430
26868
|
deletesBranch,
|
|
26431
26869
|
branchUnused,
|
|
26432
26870
|
createdBranch: record.createdBranch
|
|
@@ -26439,34 +26877,46 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26439
26877
|
/**
|
|
26440
26878
|
* Quita la rama aparte. Rehace el plan y se detiene en el primer "no" (ver
|
|
26441
26879
|
* el principio del archivo). `closeTab` cierra una pestaña como lo hace el
|
|
26442
|
-
* socket, soltando todo lo que la seguía.
|
|
26880
|
+
* socket, soltando todo lo que la seguía. `discardUntracked` es la huella de
|
|
26881
|
+
* los archivos nuevos que el usuario eligió perder (23-10-2026).
|
|
26443
26882
|
*/
|
|
26444
|
-
async remove(id, discardConflicts, closeTab) {
|
|
26883
|
+
async remove(id, discardConflicts, closeTab, discardUntracked = null) {
|
|
26445
26884
|
const record = this.recordOrThrow(id);
|
|
26446
26885
|
const plan = await this.planFor(record);
|
|
26447
26886
|
if (plan.uncommittedCount > 0) {
|
|
26448
|
-
|
|
26887
|
+
if (plan.untracked === null || plan.untracked.stamp === null || discardUntracked === null) {
|
|
26888
|
+
throw new WorktreeError(serverText("worktreeUncommitted", { count: plan.uncommittedCount }));
|
|
26889
|
+
}
|
|
26890
|
+
if (plan.untracked.stamp !== discardUntracked) throw new WorktreeError(serverText("worktreeUntrackedChanged"));
|
|
26449
26891
|
}
|
|
26450
26892
|
if (plan.conflicts.length > 0 && !discardConflicts) {
|
|
26451
26893
|
throw new WorktreeError(serverText("worktreeNotesConflict", { count: plan.conflicts.length }));
|
|
26452
26894
|
}
|
|
26453
|
-
if (plan.inUse === true) throw new
|
|
26895
|
+
if (plan.inUse === true) throw new WorktreeInUseError(plan);
|
|
26454
26896
|
let returned = 0;
|
|
26455
|
-
|
|
26897
|
+
let discardedFiles = 0;
|
|
26898
|
+
if (await exists5(record.path)) {
|
|
26456
26899
|
for (const tab of plan.tabs) closeTab(tab.terminalId);
|
|
26457
|
-
if (!await this.waitUntilFree(record.path))
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
|
|
26465
|
-
|
|
26466
|
-
|
|
26900
|
+
if (!await this.waitUntilFree(record.path)) {
|
|
26901
|
+
throw new WorktreeInUseError({ ...plan, tabs: [], inUse: true, holders: await this.holdersOf(record.path) });
|
|
26902
|
+
}
|
|
26903
|
+
if (!await this.releasedByGit(record)) {
|
|
26904
|
+
const bases = new Map(record.copied.map((file) => [file.path, file.hash]));
|
|
26905
|
+
try {
|
|
26906
|
+
returned = (await returnNotes(record.mainRoot, record.path, plan.returning, bases, {
|
|
26907
|
+
loadBase: this.loadNoteBase,
|
|
26908
|
+
onWritten: (file, branch) => this.noteReturned(record.id, file, branch),
|
|
26909
|
+
notesAsked: record.notesAsked
|
|
26910
|
+
})).returned.length;
|
|
26911
|
+
} catch (error) {
|
|
26912
|
+
throw new WorktreeError(serverText("worktreeNotesFailed", { detail: error instanceof Error ? error.message : String(error) }));
|
|
26913
|
+
}
|
|
26914
|
+
if (plan.uncommittedCount > 0) discardedFiles = await this.discardNew(record, discardUntracked);
|
|
26915
|
+
const removed = await removeWorktree(record.mainRoot, record.path);
|
|
26916
|
+
if (removed.code !== 0 && !await this.releasedByGit(record)) {
|
|
26917
|
+
throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(removed) }));
|
|
26918
|
+
}
|
|
26467
26919
|
}
|
|
26468
|
-
const removed = await removeWorktree(record.mainRoot, record.path);
|
|
26469
|
-
if (removed.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(removed) }));
|
|
26470
26920
|
await this.removeLeftovers(record);
|
|
26471
26921
|
}
|
|
26472
26922
|
const endCommit = await branchCommit(record.mainRoot, record.branch);
|
|
@@ -26474,7 +26924,15 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26474
26924
|
await this.deps.store.update(id, { removedAt: this.now(), endCommit: endCommit ?? record.endCommit });
|
|
26475
26925
|
await this.pruneNoteBases();
|
|
26476
26926
|
this.changed();
|
|
26477
|
-
return { branchDeleted, returned, discarded: discardConflicts ? plan.conflicts.length : 0 };
|
|
26927
|
+
return { branchDeleted, returned, discarded: discardConflicts ? plan.conflicts.length : 0, discardedFiles };
|
|
26928
|
+
}
|
|
26929
|
+
/** Los programas que tienen la carpeta, para el diálogo. Sin saberlo, ninguno: el diálogo dice lo de antes. */
|
|
26930
|
+
async holdersOf(folder) {
|
|
26931
|
+
try {
|
|
26932
|
+
return await (this.deps.folderHolders ?? folderHolders)(folder);
|
|
26933
|
+
} catch {
|
|
26934
|
+
return [];
|
|
26935
|
+
}
|
|
26478
26936
|
}
|
|
26479
26937
|
/** Espera, con tope, a que ningún proceso tenga la carpeta. */
|
|
26480
26938
|
async waitUntilFree(folder) {
|
|
@@ -26492,16 +26950,40 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26492
26950
|
* contenga, ni una carpeta que git todavía tenga registrada.
|
|
26493
26951
|
*/
|
|
26494
26952
|
async removeLeftovers(record) {
|
|
26495
|
-
const folder =
|
|
26496
|
-
if (!await
|
|
26497
|
-
if (isInside4(folder, record.mainRoot) ||
|
|
26953
|
+
const folder = path72.resolve(record.path);
|
|
26954
|
+
if (!await exists5(folder)) return this.removeEmptyParent(folder);
|
|
26955
|
+
if (isInside4(folder, record.mainRoot) || path72.dirname(folder) === folder) return;
|
|
26498
26956
|
if ((await listWorktrees(record.mainRoot)).some((entry) => samePathShape(entry.path, folder))) return;
|
|
26499
|
-
await
|
|
26957
|
+
await rm10(folder, { recursive: true, maxRetries: 3 });
|
|
26500
26958
|
await this.removeEmptyParent(folder);
|
|
26501
26959
|
}
|
|
26960
|
+
/**
|
|
26961
|
+
* true si git ya soltó la rama sin terminar de borrar su carpeta
|
|
26962
|
+
* (23-10-2026). `worktree remove` borra después de sus controles y, si algo
|
|
26963
|
+
* no se deja borrar a mitad de camino, quita su registro igual (medido con git
|
|
26964
|
+
* 2.56: una ruta de más de 260 caracteres sin `core.longpaths`, o un nombre
|
|
26965
|
+
* terminado en punto). Lo que quedó ya pasó esos controles, y lo termina de
|
|
26966
|
+
* borrar la app. Sólo sin `.git` adentro, y si git contesta que no la tiene:
|
|
26967
|
+
* una carpeta con su `.git` puede tener trabajo aunque git ya no la conozca.
|
|
26968
|
+
*/
|
|
26969
|
+
async releasedByGit(record) {
|
|
26970
|
+
if (!await leftWithoutGit(record.path)) return false;
|
|
26971
|
+
const entries = await readWorktreeList(record.mainRoot);
|
|
26972
|
+
return entries !== null && !entries.some((entry) => samePathShape(entry.path, record.path));
|
|
26973
|
+
}
|
|
26974
|
+
/** Las ramas de la app que git soltó a medias (`releasedByGit`), como si git las listara. */
|
|
26975
|
+
async releasedEntries(mainRoot, listed) {
|
|
26976
|
+
const released = [];
|
|
26977
|
+
for (const record of this.deps.store.list()) {
|
|
26978
|
+
if (record.removedAt !== null || !samePathShape(record.mainRoot, mainRoot)) continue;
|
|
26979
|
+
if (listed.some((entry) => samePathShape(entry.path, record.path)) || !await leftWithoutGit(record.path)) continue;
|
|
26980
|
+
released.push({ path: path72.resolve(record.path), branch: record.branch, isMain: false, bare: false, locked: false, prunable: true });
|
|
26981
|
+
}
|
|
26982
|
+
return released;
|
|
26983
|
+
}
|
|
26502
26984
|
/** La carpeta `<proyecto>.worktrees`, sólo si quedó vacía. */
|
|
26503
26985
|
async removeEmptyParent(folder) {
|
|
26504
|
-
await rmdir(
|
|
26986
|
+
await rmdir(path72.dirname(folder)).catch(() => void 0);
|
|
26505
26987
|
}
|
|
26506
26988
|
/**
|
|
26507
26989
|
* Vuelve a abrir la rama de una sesión cuya carpeta se quitó: la saca otra
|
|
@@ -26513,7 +26995,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26513
26995
|
const session = this.deps.findSession(agent, sessionId);
|
|
26514
26996
|
if (session === null || session.cwd.length === 0) throw new WorktreeError(serverText("worktreeUnknown"));
|
|
26515
26997
|
const record = this.removedRecordAbove(session.cwd);
|
|
26516
|
-
if (record === null || await
|
|
26998
|
+
if (record === null || await exists5(record.path) || !await exists5(record.mainRoot)) {
|
|
26517
26999
|
throw new WorktreeError(serverText("worktreeUnknown"));
|
|
26518
27000
|
}
|
|
26519
27001
|
const branchExists = await branchCommit(record.mainRoot, record.branch) !== null;
|
|
@@ -26525,7 +27007,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26525
27007
|
throw new WorktreeError(serverText("worktreeBranchInUse", { branch: record.branch }));
|
|
26526
27008
|
}
|
|
26527
27009
|
const copy = await planCopy(record.mainRoot);
|
|
26528
|
-
await mkdir22(
|
|
27010
|
+
await mkdir22(path72.dirname(record.path), { recursive: true });
|
|
26529
27011
|
const added = await addWorktree(record.mainRoot, record.path, record.branch, recreateAt);
|
|
26530
27012
|
if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
|
|
26531
27013
|
const { copied, copiedDirs } = await this.copyInto(record.mainRoot, record.path, copy, record.copyNotes);
|
|
@@ -26541,7 +27023,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26541
27023
|
*/
|
|
26542
27024
|
async keptBranches(terminalId) {
|
|
26543
27025
|
const descriptor = this.deps.registry.get(terminalId);
|
|
26544
|
-
if (descriptor === null || !await
|
|
27026
|
+
if (descriptor === null || !await exists5(descriptor.cwd)) return [];
|
|
26545
27027
|
const layout = await repoLayout(descriptor.cwd);
|
|
26546
27028
|
if (layout === null || layout.mainRoot === null) return [];
|
|
26547
27029
|
const mainRoot = layout.mainRoot;
|
|
@@ -26612,11 +27094,11 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26612
27094
|
return { branch: plan.branch };
|
|
26613
27095
|
}
|
|
26614
27096
|
removedRecordAbove(cwd) {
|
|
26615
|
-
let current =
|
|
27097
|
+
let current = path72.resolve(cwd);
|
|
26616
27098
|
for (let depth = 0; depth < 24; depth += 1) {
|
|
26617
27099
|
const record = this.deps.store.byPath(current);
|
|
26618
27100
|
if (record !== null) return record.removedAt === null ? null : record;
|
|
26619
|
-
const parent =
|
|
27101
|
+
const parent = path72.dirname(current);
|
|
26620
27102
|
if (parent === current) return null;
|
|
26621
27103
|
current = parent;
|
|
26622
27104
|
}
|
|
@@ -26728,7 +27210,7 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26728
27210
|
const open9 = this.authorTab(record);
|
|
26729
27211
|
if (open9 !== null) return { terminal: open9, opened: false };
|
|
26730
27212
|
const session = this.authorSession(record);
|
|
26731
|
-
if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await
|
|
27213
|
+
if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await exists5(session.cwd)) {
|
|
26732
27214
|
const terminal = await this.deps.registry.open({ cwd: session.cwd, agent: session.agent, resumeSessionId: session.sessionId });
|
|
26733
27215
|
return { terminal, opened: true };
|
|
26734
27216
|
}
|
|
@@ -26945,11 +27427,11 @@ var WorktreeService = class extends EventEmitter7 {
|
|
|
26945
27427
|
const baseEntry = entries.find((entry) => !entry.bare && !entry.prunable && entry.branch === record.base) ?? null;
|
|
26946
27428
|
const [baseChanges, branchChanges, target] = await Promise.all([
|
|
26947
27429
|
baseEntry === null ? Promise.resolve([]) : uncommittedPaths(baseEntry.path),
|
|
26948
|
-
await
|
|
27430
|
+
await exists5(record.path) ? uncommittedPaths(record.path) : Promise.resolve([]),
|
|
26949
27431
|
baseEntry === null ? Promise.resolve(`refs/heads/${record.branch}`) : mergeTarget(baseEntry.path, record.branch)
|
|
26950
27432
|
]);
|
|
26951
27433
|
const localConflicts = pathsTouching(baseChanges ?? [], files);
|
|
26952
|
-
const notes = baseEntry?.isMain === true && await
|
|
27434
|
+
const notes = baseEntry?.isMain === true && await exists5(record.path) ? await reviewNotes(mainRoot, record.path, record, this.loadNoteBase) : null;
|
|
26953
27435
|
const blocker = pair.ahead === 0 ? "nothing" : pair.behind > 0 ? "behind" : baseEntry === null ? "no-base-folder" : localConflicts.length > 0 ? "local-changes" : null;
|
|
26954
27436
|
const plan = {
|
|
26955
27437
|
id: record.id,
|
|
@@ -28644,17 +29126,24 @@ ${seen.join("\n-----\n")}`);
|
|
|
28644
29126
|
})();
|
|
28645
29127
|
break;
|
|
28646
29128
|
/*
|
|
28647
|
-
Cuales de esas rutas son archivos del proyecto (§6.33): lo
|
|
28648
|
-
hilo vuelve enlace. Sin error para ningun caso
|
|
28649
|
-
esta, una ruta que cae fuera
|
|
29129
|
+
Cuales de esas rutas son archivos o carpetas del proyecto (§6.33): lo
|
|
29130
|
+
que el hilo vuelve enlace. Sin error para ningun caso (una pestana que
|
|
29131
|
+
ya no esta, una ruta que cae fuera): nada existe, y el hilo queda como
|
|
28650
29132
|
texto. Un cartel por una ruta que escribio el agente no le dice nada
|
|
28651
29133
|
al usuario.
|
|
28652
29134
|
*/
|
|
28653
29135
|
case "files.check":
|
|
28654
29136
|
void (async () => {
|
|
28655
29137
|
const cwd = cwdOf(message.terminalId);
|
|
28656
|
-
const
|
|
28657
|
-
|
|
29138
|
+
const none = { files: [], directories: [] };
|
|
29139
|
+
const { files, directories } = cwd === null ? none : await existingPaths(cwd, message.paths, await folders).catch(() => none);
|
|
29140
|
+
send(socket, {
|
|
29141
|
+
type: "files.checked",
|
|
29142
|
+
terminalId: message.terminalId,
|
|
29143
|
+
requestId: message.requestId,
|
|
29144
|
+
existing: files,
|
|
29145
|
+
directories
|
|
29146
|
+
});
|
|
28658
29147
|
})();
|
|
28659
29148
|
break;
|
|
28660
29149
|
case "files.reveal":
|
|
@@ -29024,9 +29513,11 @@ ${seen.join("\n-----\n")}`);
|
|
|
29024
29513
|
}
|
|
29025
29514
|
case "worktree.remove": {
|
|
29026
29515
|
const { requestId, worktreeId, discardConflicts } = message;
|
|
29027
|
-
|
|
29516
|
+
const closeTab = (terminalId) => void dropTab(terminalId);
|
|
29517
|
+
void worktrees.remove(worktreeId, discardConflicts, closeTab, message.discardUntracked ?? null).then(
|
|
29028
29518
|
(outcome) => send(socket, { type: "worktree.removed", requestId, worktreeId, outcome }),
|
|
29029
|
-
|
|
29519
|
+
// Otro programa tiene la carpeta: el plan otra vez, que dice cuál (23-10-2026).
|
|
29520
|
+
(error) => error instanceof WorktreeInUseError ? send(socket, { type: "worktree.removePlan", requestId, plan: error.plan }) : sendWorktreeError(socket, error, requestId)
|
|
29030
29521
|
);
|
|
29031
29522
|
break;
|
|
29032
29523
|
}
|
|
@@ -29314,8 +29805,8 @@ ${seen.join("\n-----\n")}`);
|
|
|
29314
29805
|
}
|
|
29315
29806
|
|
|
29316
29807
|
// packages/server/src/vault/catalog.ts
|
|
29317
|
-
import { open as open8, readdir as
|
|
29318
|
-
import
|
|
29808
|
+
import { open as open8, readdir as readdir22, stat as stat46 } from "node:fs/promises";
|
|
29809
|
+
import path73 from "node:path";
|
|
29319
29810
|
import { createInterface as createInterface4 } from "node:readline";
|
|
29320
29811
|
var HEADER_MAX_BYTES = 64 * 1024;
|
|
29321
29812
|
var LOAD_CONCURRENCY = 16;
|
|
@@ -29327,7 +29818,7 @@ async function assetBytesOf(dir, agent, sessionId) {
|
|
|
29327
29818
|
const folder = assetsDir(dir, agent, sessionId);
|
|
29328
29819
|
let names;
|
|
29329
29820
|
try {
|
|
29330
|
-
names = await
|
|
29821
|
+
names = await readdir22(folder);
|
|
29331
29822
|
} catch {
|
|
29332
29823
|
return 0;
|
|
29333
29824
|
}
|
|
@@ -29335,7 +29826,7 @@ async function assetBytesOf(dir, agent, sessionId) {
|
|
|
29335
29826
|
for (const name of names) {
|
|
29336
29827
|
if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
|
|
29337
29828
|
try {
|
|
29338
|
-
const info = await
|
|
29829
|
+
const info = await stat46(path73.join(folder, name));
|
|
29339
29830
|
if (info.isFile()) total += info.size;
|
|
29340
29831
|
} catch {
|
|
29341
29832
|
}
|
|
@@ -29346,7 +29837,7 @@ async function readEntry2(dir, agent, sessionId) {
|
|
|
29346
29837
|
const file = sessionFile(dir, agent, sessionId);
|
|
29347
29838
|
let sizeBytes;
|
|
29348
29839
|
try {
|
|
29349
|
-
const info = await
|
|
29840
|
+
const info = await stat46(file);
|
|
29350
29841
|
if (!info.isFile()) return { kind: "absent" };
|
|
29351
29842
|
sizeBytes = info.size;
|
|
29352
29843
|
} catch {
|
|
@@ -29403,7 +29894,7 @@ var VaultCatalog = class {
|
|
|
29403
29894
|
const keys = [];
|
|
29404
29895
|
let agentFolders = [];
|
|
29405
29896
|
try {
|
|
29406
|
-
agentFolders = await
|
|
29897
|
+
agentFolders = await readdir22(sessionsRoot2(dir));
|
|
29407
29898
|
} catch {
|
|
29408
29899
|
agentFolders = [];
|
|
29409
29900
|
}
|
|
@@ -29411,7 +29902,7 @@ var VaultCatalog = class {
|
|
|
29411
29902
|
if (!isSessionAgentId2(folder)) continue;
|
|
29412
29903
|
let names;
|
|
29413
29904
|
try {
|
|
29414
|
-
names = await
|
|
29905
|
+
names = await readdir22(agentSessionsDir(dir, folder));
|
|
29415
29906
|
} catch {
|
|
29416
29907
|
continue;
|
|
29417
29908
|
}
|
|
@@ -29555,22 +30046,22 @@ var VaultCatalog = class {
|
|
|
29555
30046
|
};
|
|
29556
30047
|
|
|
29557
30048
|
// packages/server/src/worktrees/links.ts
|
|
29558
|
-
import { stat as
|
|
29559
|
-
import
|
|
29560
|
-
async function
|
|
30049
|
+
import { stat as stat47 } from "node:fs/promises";
|
|
30050
|
+
import path74 from "node:path";
|
|
30051
|
+
async function exists6(candidate) {
|
|
29561
30052
|
try {
|
|
29562
|
-
await
|
|
30053
|
+
await stat47(candidate);
|
|
29563
30054
|
return true;
|
|
29564
30055
|
} catch {
|
|
29565
30056
|
return false;
|
|
29566
30057
|
}
|
|
29567
30058
|
}
|
|
29568
30059
|
function recordAbove(registry, cwd) {
|
|
29569
|
-
let current =
|
|
30060
|
+
let current = path74.resolve(cwd);
|
|
29570
30061
|
for (let depth = 0; depth < 24; depth += 1) {
|
|
29571
30062
|
const record = registry.byPath(current);
|
|
29572
|
-
if (record !== null) return { record, inner:
|
|
29573
|
-
const parent =
|
|
30063
|
+
if (record !== null) return { record, inner: path74.relative(current, path74.resolve(cwd)) };
|
|
30064
|
+
const parent = path74.dirname(current);
|
|
29574
30065
|
if (parent === current) return null;
|
|
29575
30066
|
current = parent;
|
|
29576
30067
|
}
|
|
@@ -29584,14 +30075,14 @@ var WorktreeLinks = class {
|
|
|
29584
30075
|
/** La rama aparte de una carpeta del historial, o null si es la de un proyecto (o no es de git). */
|
|
29585
30076
|
async sessionLink(cwd) {
|
|
29586
30077
|
if (cwd.length === 0) return null;
|
|
29587
|
-
if (await
|
|
30078
|
+
if (await exists6(cwd)) {
|
|
29588
30079
|
const checkout = await readCheckout(cwd);
|
|
29589
30080
|
if (checkout === null || !checkout.linked) return null;
|
|
29590
30081
|
const record = this.registry.byPath(checkout.root);
|
|
29591
30082
|
return {
|
|
29592
30083
|
mainCwd: mainCwdFor(cwd, checkout),
|
|
29593
30084
|
worktree: {
|
|
29594
|
-
branch: checkout.branch ?? record?.branch ??
|
|
30085
|
+
branch: checkout.branch ?? record?.branch ?? path74.basename(checkout.root),
|
|
29595
30086
|
exists: true,
|
|
29596
30087
|
reopenable: false
|
|
29597
30088
|
}
|
|
@@ -29601,13 +30092,13 @@ var WorktreeLinks = class {
|
|
|
29601
30092
|
if (found !== null) {
|
|
29602
30093
|
const { record, inner } = found;
|
|
29603
30094
|
return {
|
|
29604
|
-
mainCwd: inner.length === 0 ? record.mainRoot :
|
|
30095
|
+
mainCwd: inner.length === 0 ? record.mainRoot : path74.join(record.mainRoot, inner),
|
|
29605
30096
|
worktree: { branch: record.branch, exists: false, reopenable: await this.reopenable(record) }
|
|
29606
30097
|
};
|
|
29607
30098
|
}
|
|
29608
|
-
const mainRoot = mainRootFromShape(
|
|
30099
|
+
const mainRoot = mainRootFromShape(path74.resolve(cwd));
|
|
29609
30100
|
if (mainRoot === null) return null;
|
|
29610
|
-
return { mainCwd: mainRoot, worktree: { branch:
|
|
30101
|
+
return { mainCwd: mainRoot, worktree: { branch: path74.basename(cwd), exists: false, reopenable: false } };
|
|
29611
30102
|
}
|
|
29612
30103
|
/**
|
|
29613
30104
|
* Se puede volver a abrir una rama quitada si la creó la app, su carpeta no
|
|
@@ -29616,7 +30107,7 @@ var WorktreeLinks = class {
|
|
|
29616
30107
|
*/
|
|
29617
30108
|
async reopenable(record) {
|
|
29618
30109
|
if (record.removedAt === null) return false;
|
|
29619
|
-
if (await
|
|
30110
|
+
if (await exists6(record.path) || !await exists6(record.mainRoot)) return false;
|
|
29620
30111
|
try {
|
|
29621
30112
|
if (await branchCommit(record.mainRoot, record.branch) !== null) return true;
|
|
29622
30113
|
return record.endCommit !== null && await commitExists(record.mainRoot, record.endCommit);
|
|
@@ -29637,15 +30128,15 @@ var WorktreeLinks = class {
|
|
|
29637
30128
|
};
|
|
29638
30129
|
|
|
29639
30130
|
// packages/server/src/index.ts
|
|
29640
|
-
var serverDir =
|
|
30131
|
+
var serverDir = path75.dirname(fileURLToPath2(import.meta.url));
|
|
29641
30132
|
var isPackaged = true;
|
|
29642
|
-
var webRoot = isPackaged ? serverDir :
|
|
29643
|
-
var webDist = isPackaged ?
|
|
29644
|
-
var repoRoot =
|
|
30133
|
+
var webRoot = isPackaged ? serverDir : path75.resolve(serverDir, "../../web");
|
|
30134
|
+
var webDist = isPackaged ? path75.join(serverDir, "web") : path75.join(webRoot, "dist");
|
|
30135
|
+
var repoRoot = path75.resolve(serverDir, "../../..");
|
|
29645
30136
|
var isProduction = isPackaged || process.argv.includes("--prod");
|
|
29646
30137
|
function resolveDefaultCwd() {
|
|
29647
30138
|
const override = process.env["AGENT_WORKBENCH_CWD"];
|
|
29648
|
-
if (override !== void 0 && override.length > 0) return
|
|
30139
|
+
if (override !== void 0 && override.length > 0) return path75.resolve(override);
|
|
29649
30140
|
if (isPackaged) return process.cwd();
|
|
29650
30141
|
return repoRoot;
|
|
29651
30142
|
}
|
|
@@ -29720,7 +30211,7 @@ async function mountDevUi(app, httpServer) {
|
|
|
29720
30211
|
}
|
|
29721
30212
|
var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
|
|
29722
30213
|
function mountBuiltUi(app) {
|
|
29723
|
-
const indexHtml =
|
|
30214
|
+
const indexHtml = path75.join(webDist, "index.html");
|
|
29724
30215
|
if (!existsSync4(indexHtml)) {
|
|
29725
30216
|
throw new Error(
|
|
29726
30217
|
`There's no compiled interface in ${webDist}.
|
|
@@ -29808,11 +30299,11 @@ async function main() {
|
|
|
29808
30299
|
findSession: (agent, sessionId) => index.find(agent, sessionId),
|
|
29809
30300
|
// Hito 49: las conversaciones de una rama (para dejarle un pedido a su autor) y la respuesta del revisor.
|
|
29810
30301
|
sessionsUnder: (folder) => {
|
|
29811
|
-
const root =
|
|
30302
|
+
const root = path75.resolve(folder);
|
|
29812
30303
|
return index.getProjects().flatMap((project) => project.sessions).filter((session) => {
|
|
29813
30304
|
if (session.archived || session.cwd.length === 0) return false;
|
|
29814
|
-
const relative =
|
|
29815
|
-
return relative.length === 0 || !relative.startsWith("..") && !
|
|
30305
|
+
const relative = path75.relative(root, path75.resolve(session.cwd));
|
|
30306
|
+
return relative.length === 0 || !relative.startsWith("..") && !path75.isAbsolute(relative);
|
|
29816
30307
|
});
|
|
29817
30308
|
},
|
|
29818
30309
|
history: (agent) => agents.get(agent)?.adapter.history ?? null
|