@agentproto/worktree 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -3
- package/dist/bin/worktree-agent.mjs +3 -3
- package/dist/{chunk-EK5BH2YL.mjs → chunk-3FZWPMLF.mjs} +8 -5
- package/dist/chunk-3FZWPMLF.mjs.map +1 -0
- package/dist/{chunk-5SDH2V6M.mjs → chunk-FC3I5NTS.mjs} +4 -4
- package/dist/{chunk-5SDH2V6M.mjs.map → chunk-FC3I5NTS.mjs.map} +1 -1
- package/dist/{chunk-EVHLUKC7.mjs → chunk-IEW2O3PF.mjs} +626 -136
- package/dist/chunk-IEW2O3PF.mjs.map +1 -0
- package/dist/index.d.ts +284 -13
- package/dist/index.mjs +5 -5
- package/dist/provider/index.mjs +2 -2
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.mjs +1 -1
- package/package.json +7 -7
- package/dist/chunk-EK5BH2YL.mjs.map +0 -1
- package/dist/chunk-EVHLUKC7.mjs.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { provisionWorktreeTool, cleanupWorktreeTool, runGateTool, runScriptTool, startServiceTool, stopServiceTool, listServicesTool } from './chunk-
|
|
1
|
+
import { provisionWorktreeTool, cleanupWorktreeTool, runGateTool, runScriptTool, startServiceTool, stopServiceTool, listServicesTool } from './chunk-3FZWPMLF.mjs';
|
|
2
2
|
import { implementTool, defineDriver } from '@agentproto/driver';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
|
-
import { lstat, mkdir, symlink, appendFile, writeFile, copyFile, rename, readdir, stat
|
|
4
|
+
import { lstat, mkdir, symlink, appendFile, writeFile, copyFile, readFile, rename, readdir, stat } from 'fs/promises';
|
|
5
5
|
import { resolve, join, dirname, basename, posix, sep } from 'path';
|
|
6
6
|
import { ToolError } from '@agentproto/tool';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
8
8
|
import { z } from 'zod';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
10
|
+
import { loadavg, cpus, homedir } from 'os';
|
|
11
|
+
import { existsSync, mkdirSync, realpathSync, readFileSync, writeFileSync, rmSync, readdirSync } from 'fs';
|
|
11
12
|
import { createServer } from 'net';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -17,26 +18,81 @@ import { createServer } from 'net';
|
|
|
17
18
|
function mergedEnv(extra) {
|
|
18
19
|
return extra ? { ...process.env, ...extra } : process.env;
|
|
19
20
|
}
|
|
21
|
+
function abortReason(signal) {
|
|
22
|
+
const reason = signal.reason;
|
|
23
|
+
return reason instanceof Error ? reason : new Error("aborted");
|
|
24
|
+
}
|
|
25
|
+
function signalTree(child, sig) {
|
|
26
|
+
if (child.pid === void 0) return;
|
|
27
|
+
try {
|
|
28
|
+
if (process.platform === "win32") child.kill(sig);
|
|
29
|
+
else process.kill(-child.pid, sig);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function killOnAbort(child, opts) {
|
|
34
|
+
const { signal } = opts;
|
|
35
|
+
if (!signal) return () => {
|
|
36
|
+
};
|
|
37
|
+
let escalation;
|
|
38
|
+
const onAbort = () => {
|
|
39
|
+
signalTree(child, "SIGTERM");
|
|
40
|
+
escalation = setTimeout(() => signalTree(child, "SIGKILL"), opts.killGraceMs ?? 5e3);
|
|
41
|
+
escalation.unref();
|
|
42
|
+
};
|
|
43
|
+
if (signal.aborted) onAbort();
|
|
44
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
return () => {
|
|
46
|
+
signal.removeEventListener("abort", onAbort);
|
|
47
|
+
if (escalation && !signal.aborted) clearTimeout(escalation);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
20
50
|
function execArgv(command, args, cwd, opts = {}) {
|
|
21
51
|
return new Promise((resolvePromise, reject) => {
|
|
22
|
-
|
|
52
|
+
if (opts.signal?.aborted) return reject(abortReason(opts.signal));
|
|
53
|
+
const child = spawn(command, args, {
|
|
54
|
+
cwd,
|
|
55
|
+
shell: false,
|
|
56
|
+
env: mergedEnv(opts.env),
|
|
57
|
+
detached: opts.signal !== void 0 && process.platform !== "win32"
|
|
58
|
+
});
|
|
59
|
+
const detach = killOnAbort(child, opts);
|
|
23
60
|
let stdout = "";
|
|
24
61
|
let stderr = "";
|
|
25
62
|
child.stdout?.on("data", (d) => stdout += d.toString("utf8"));
|
|
26
63
|
child.stderr?.on("data", (d) => stderr += d.toString("utf8"));
|
|
27
|
-
child.on("error",
|
|
28
|
-
|
|
64
|
+
child.on("error", (err) => {
|
|
65
|
+
detach();
|
|
66
|
+
reject(err);
|
|
67
|
+
});
|
|
68
|
+
child.on("close", (code) => {
|
|
69
|
+
detach();
|
|
70
|
+
resolvePromise({ exitCode: code ?? -1, stdout, stderr });
|
|
71
|
+
});
|
|
29
72
|
});
|
|
30
73
|
}
|
|
31
74
|
function execShell(cmd, cwd, opts = {}) {
|
|
32
75
|
return new Promise((resolvePromise, reject) => {
|
|
33
|
-
|
|
76
|
+
if (opts.signal?.aborted) return reject(abortReason(opts.signal));
|
|
77
|
+
const child = spawn(cmd, {
|
|
78
|
+
cwd,
|
|
79
|
+
shell: true,
|
|
80
|
+
env: mergedEnv(opts.env),
|
|
81
|
+
detached: opts.signal !== void 0 && process.platform !== "win32"
|
|
82
|
+
});
|
|
83
|
+
const detach = killOnAbort(child, opts);
|
|
34
84
|
let stdout = "";
|
|
35
85
|
let stderr = "";
|
|
36
86
|
child.stdout?.on("data", (d) => stdout += d.toString("utf8"));
|
|
37
87
|
child.stderr?.on("data", (d) => stderr += d.toString("utf8"));
|
|
38
|
-
child.on("error",
|
|
39
|
-
|
|
88
|
+
child.on("error", (err) => {
|
|
89
|
+
detach();
|
|
90
|
+
reject(err);
|
|
91
|
+
});
|
|
92
|
+
child.on("close", (code) => {
|
|
93
|
+
detach();
|
|
94
|
+
resolvePromise({ exitCode: code ?? -1, stdout, stderr });
|
|
95
|
+
});
|
|
40
96
|
});
|
|
41
97
|
}
|
|
42
98
|
async function execGit(repoRoot, args) {
|
|
@@ -106,6 +162,53 @@ async function expandGlob(root, pattern) {
|
|
|
106
162
|
const candidates = await walk(root, base);
|
|
107
163
|
return candidates.filter((rel) => regex.test(rel));
|
|
108
164
|
}
|
|
165
|
+
var GlobTraversalError = class extends Error {
|
|
166
|
+
constructor(pattern) {
|
|
167
|
+
super(`glob pattern '${pattern}' escapes the repo root (path traversal)`);
|
|
168
|
+
this.name = "GlobTraversalError";
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function assertNoTraversal(pattern) {
|
|
172
|
+
if (pattern.startsWith("/")) throw new GlobTraversalError(pattern);
|
|
173
|
+
if (pattern.split("/").some((seg) => seg === "..")) throw new GlobTraversalError(pattern);
|
|
174
|
+
}
|
|
175
|
+
async function expandCloneGlob(root, pattern) {
|
|
176
|
+
assertNoTraversal(pattern);
|
|
177
|
+
const segments = pattern.split("/").filter((seg) => seg.length > 0);
|
|
178
|
+
if (segments.length === 0) return [];
|
|
179
|
+
if (segments.includes("**")) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`cloneGlobs pattern '${pattern}' uses '**', which is not supported \u2014 name each path segment explicitly so a directory match clones as one unit.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
async function walkMatch(dirRel, idx) {
|
|
185
|
+
const seg = segments[idx];
|
|
186
|
+
const isLast = idx === segments.length - 1;
|
|
187
|
+
const abs = dirRel ? join(root, dirRel) : root;
|
|
188
|
+
let entries;
|
|
189
|
+
try {
|
|
190
|
+
entries = await readdir(abs, { withFileTypes: true });
|
|
191
|
+
} catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const regex = new RegExp(`^${escapeSegment(seg)}$`);
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (entry.name === ".git") continue;
|
|
198
|
+
if (!regex.test(entry.name)) continue;
|
|
199
|
+
const rel = dirRel ? posix.join(dirRel, entry.name) : entry.name;
|
|
200
|
+
if (isLast) {
|
|
201
|
+
out.push(rel);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
out.push(...await walkMatch(rel, idx + 1));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
return walkMatch("", 0);
|
|
211
|
+
}
|
|
109
212
|
var CONFIG_FILENAME = "agentproto.json";
|
|
110
213
|
var hookSchema = z.union([z.string(), z.array(z.string())]);
|
|
111
214
|
var scriptSchema = z.object({
|
|
@@ -182,6 +285,280 @@ async function loadConfigFromBase(repoRoot, base = "origin/main") {
|
|
|
182
285
|
}
|
|
183
286
|
return parseConfig(result.stdout);
|
|
184
287
|
}
|
|
288
|
+
var LOCAL_WORKTREE_CONFIG_REL = ".agentproto/worktree.json";
|
|
289
|
+
var localWriteFileEntrySchema = z.object({
|
|
290
|
+
path: z.string().min(1, "writeFiles[].path must be non-empty"),
|
|
291
|
+
content: z.string(),
|
|
292
|
+
mode: z.enum(["create", "append"]).optional()
|
|
293
|
+
});
|
|
294
|
+
var localWorktreeConfigSchema = z.object({
|
|
295
|
+
copyGlobs: z.array(z.string()).optional(),
|
|
296
|
+
/** Gitignored dirs/files cloned (copy-on-write where the filesystem
|
|
297
|
+
* supports it) from the source checkout into the worktree before
|
|
298
|
+
* `depsCmd` runs — see `clone.ts`. */
|
|
299
|
+
cloneGlobs: z.array(z.string()).optional(),
|
|
300
|
+
writeFiles: z.array(localWriteFileEntrySchema).optional(),
|
|
301
|
+
depsCmd: z.string().optional(),
|
|
302
|
+
linkPaths: z.array(z.string()).optional()
|
|
303
|
+
});
|
|
304
|
+
function parseLocalWorktreeConfig(raw) {
|
|
305
|
+
let json;
|
|
306
|
+
try {
|
|
307
|
+
json = JSON.parse(raw);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
throw new ConfigError(
|
|
310
|
+
`${LOCAL_WORKTREE_CONFIG_REL} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const result = localWorktreeConfigSchema.safeParse(json);
|
|
314
|
+
if (!result.success) {
|
|
315
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
316
|
+
throw new ConfigError(`${LOCAL_WORKTREE_CONFIG_REL} failed validation: ${issues}`);
|
|
317
|
+
}
|
|
318
|
+
return result.data;
|
|
319
|
+
}
|
|
320
|
+
async function loadLocalWorktreeConfig(workspace) {
|
|
321
|
+
const path = resolve(workspace, LOCAL_WORKTREE_CONFIG_REL);
|
|
322
|
+
let raw;
|
|
323
|
+
try {
|
|
324
|
+
raw = await readFile(path, "utf8");
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err.code === "ENOENT") return null;
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
return parseLocalWorktreeConfig(raw);
|
|
330
|
+
}
|
|
331
|
+
var SLUG_PLACEHOLDER = /\{slug\}/g;
|
|
332
|
+
function applySlugPlaceholder(text, slug) {
|
|
333
|
+
return text.replace(SLUG_PLACEHOLDER, slug);
|
|
334
|
+
}
|
|
335
|
+
function resolveLocalWriteFiles(files, slug) {
|
|
336
|
+
return (files ?? []).map((file) => ({
|
|
337
|
+
...file,
|
|
338
|
+
path: applySlugPlaceholder(file.path, slug),
|
|
339
|
+
content: applySlugPlaceholder(file.content, slug)
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
var DEFAULT_PROVISION_CONCURRENCY = 2;
|
|
343
|
+
var PROVISION_CONCURRENCY_ENV = "AGENTPROTO_WORKTREES_PROVISION_CONCURRENCY";
|
|
344
|
+
var DEFAULT_PROVISION_LIMITS = Object.freeze({
|
|
345
|
+
concurrency: DEFAULT_PROVISION_CONCURRENCY,
|
|
346
|
+
perRepo: Object.freeze({}),
|
|
347
|
+
loadFactor: 0
|
|
348
|
+
});
|
|
349
|
+
function parseCap(raw) {
|
|
350
|
+
if (typeof raw === "string" && raw.trim() !== "") raw = Number(raw);
|
|
351
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) return void 0;
|
|
352
|
+
return raw === 0 ? Infinity : raw;
|
|
353
|
+
}
|
|
354
|
+
function normalizeRepoKey(key) {
|
|
355
|
+
const trimmed = key.trim();
|
|
356
|
+
if (trimmed.startsWith("~/")) return resolve(homedir(), trimmed.slice(2));
|
|
357
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) return resolve(trimmed);
|
|
358
|
+
return trimmed;
|
|
359
|
+
}
|
|
360
|
+
function parseProvisionLimits(raw, env = process.env) {
|
|
361
|
+
const section = raw !== null && typeof raw === "object" ? raw : {};
|
|
362
|
+
const concurrency = parseCap(env[PROVISION_CONCURRENCY_ENV]) ?? parseCap(section["provisionConcurrency"]) ?? DEFAULT_PROVISION_CONCURRENCY;
|
|
363
|
+
const perRepo = {};
|
|
364
|
+
const byRepo = section["provisionConcurrencyByRepo"];
|
|
365
|
+
if (byRepo !== null && typeof byRepo === "object" && !Array.isArray(byRepo)) {
|
|
366
|
+
for (const [key, value] of Object.entries(byRepo)) {
|
|
367
|
+
const cap = parseCap(value);
|
|
368
|
+
if (cap !== void 0 && key.trim() !== "") perRepo[normalizeRepoKey(key)] = cap;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const factor = section["provisionLoadFactor"];
|
|
372
|
+
const loadFactor = typeof factor === "number" && Number.isFinite(factor) && factor > 0 ? factor : 0;
|
|
373
|
+
return { concurrency, perRepo, loadFactor };
|
|
374
|
+
}
|
|
375
|
+
var ProvisionCancelledError = class extends Error {
|
|
376
|
+
constructor(message = "worktree provisioning cancelled") {
|
|
377
|
+
super(message);
|
|
378
|
+
this.name = "ProvisionCancelledError";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
var contextStore = new AsyncLocalStorage();
|
|
382
|
+
function runWithProvisionContext(ctx, fn) {
|
|
383
|
+
return contextStore.run(ctx, fn);
|
|
384
|
+
}
|
|
385
|
+
function currentProvisionContext() {
|
|
386
|
+
return contextStore.getStore();
|
|
387
|
+
}
|
|
388
|
+
var ProvisionScheduler = class {
|
|
389
|
+
limits;
|
|
390
|
+
queue = [];
|
|
391
|
+
running = /* @__PURE__ */ new Set();
|
|
392
|
+
seq = 0;
|
|
393
|
+
loadTimer;
|
|
394
|
+
loadavg1;
|
|
395
|
+
cpuCount;
|
|
396
|
+
loadPollMs;
|
|
397
|
+
constructor(opts = {}) {
|
|
398
|
+
this.limits = opts.limits ?? DEFAULT_PROVISION_LIMITS;
|
|
399
|
+
this.loadavg1 = opts.loadavg1 ?? (() => loadavg()[0] ?? 0);
|
|
400
|
+
this.cpuCount = opts.cpuCount ?? (() => cpus().length || 1);
|
|
401
|
+
this.loadPollMs = opts.loadPollMs ?? 5e3;
|
|
402
|
+
}
|
|
403
|
+
/** Replace the limits (config is re-read per provision). Raising a cap
|
|
404
|
+
* admits waiters immediately; lowering one never interrupts a running
|
|
405
|
+
* segment, it just delays the next admission. */
|
|
406
|
+
configure(limits) {
|
|
407
|
+
this.limits = limits;
|
|
408
|
+
this.pump();
|
|
409
|
+
}
|
|
410
|
+
snapshot() {
|
|
411
|
+
return { running: this.running.size, queued: this.queue.length, limit: this.limits.concurrency };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Request a heavy-phase slot. Resolves with a lease once admitted; rejects
|
|
415
|
+
* with {@link ProvisionCancelledError} when `signal` aborts first (the
|
|
416
|
+
* queue entry is dropped, so a cancelled spawn never occupies a slot).
|
|
417
|
+
*/
|
|
418
|
+
acquire(req) {
|
|
419
|
+
if (req.signal?.aborted) return Promise.reject(new ProvisionCancelledError());
|
|
420
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
421
|
+
const waiter = {
|
|
422
|
+
seq: this.seq++,
|
|
423
|
+
repoKey: req.repoKey,
|
|
424
|
+
callerKey: req.callerKey ?? "anonymous",
|
|
425
|
+
signal: req.signal,
|
|
426
|
+
onQueued: req.onQueued,
|
|
427
|
+
lastPosition: 0,
|
|
428
|
+
grant: resolvePromise,
|
|
429
|
+
reject: rejectPromise,
|
|
430
|
+
detach: () => {
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
if (req.signal) {
|
|
434
|
+
const onAbort = () => {
|
|
435
|
+
const idx = this.queue.indexOf(waiter);
|
|
436
|
+
if (idx === -1) return;
|
|
437
|
+
this.queue.splice(idx, 1);
|
|
438
|
+
waiter.reject(new ProvisionCancelledError("worktree provisioning cancelled while queued"));
|
|
439
|
+
this.pump();
|
|
440
|
+
};
|
|
441
|
+
req.signal.addEventListener("abort", onAbort, { once: true });
|
|
442
|
+
waiter.detach = () => req.signal.removeEventListener("abort", onAbort);
|
|
443
|
+
}
|
|
444
|
+
this.queue.push(waiter);
|
|
445
|
+
this.pump();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
repoCap(repoKey) {
|
|
449
|
+
const { perRepo } = this.limits;
|
|
450
|
+
const own = (key) => Object.hasOwn(perRepo, key) ? perRepo[key] : void 0;
|
|
451
|
+
return Math.min(own(repoKey) ?? own(basename(repoKey)) ?? Infinity, this.limits.concurrency);
|
|
452
|
+
}
|
|
453
|
+
runningFor(match) {
|
|
454
|
+
let n = 0;
|
|
455
|
+
for (const slot of this.running) if (match(slot)) n++;
|
|
456
|
+
return n;
|
|
457
|
+
}
|
|
458
|
+
/** Waiters in the order they would be admitted absent per-repo caps. */
|
|
459
|
+
projectedOrder() {
|
|
460
|
+
const held = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const slot of this.running) held.set(slot.callerKey, (held.get(slot.callerKey) ?? 0) + 1);
|
|
462
|
+
const remaining = [...this.queue];
|
|
463
|
+
const ordered = [];
|
|
464
|
+
while (remaining.length > 0) {
|
|
465
|
+
let best = 0;
|
|
466
|
+
for (let i = 1; i < remaining.length; i++) {
|
|
467
|
+
if ((held.get(remaining[i].callerKey) ?? 0) < (held.get(remaining[best].callerKey) ?? 0)) best = i;
|
|
468
|
+
}
|
|
469
|
+
const [next] = remaining.splice(best, 1);
|
|
470
|
+
ordered.push(next);
|
|
471
|
+
held.set(next.callerKey, (held.get(next.callerKey) ?? 0) + 1);
|
|
472
|
+
}
|
|
473
|
+
return ordered;
|
|
474
|
+
}
|
|
475
|
+
loadGuardBlocks() {
|
|
476
|
+
const { loadFactor } = this.limits;
|
|
477
|
+
if (loadFactor <= 0) return false;
|
|
478
|
+
if (this.running.size === 0) return false;
|
|
479
|
+
return this.loadavg1() > this.cpuCount() * loadFactor;
|
|
480
|
+
}
|
|
481
|
+
pump() {
|
|
482
|
+
for (; ; ) {
|
|
483
|
+
if (this.queue.length === 0) break;
|
|
484
|
+
if (this.running.size >= this.limits.concurrency) break;
|
|
485
|
+
if (this.loadGuardBlocks()) {
|
|
486
|
+
this.armLoadTimer();
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
const next = this.projectedOrder().find(
|
|
490
|
+
(w) => this.runningFor((s) => s.repoKey === w.repoKey) < this.repoCap(w.repoKey)
|
|
491
|
+
);
|
|
492
|
+
if (!next) break;
|
|
493
|
+
this.queue.splice(this.queue.indexOf(next), 1);
|
|
494
|
+
next.detach();
|
|
495
|
+
const slot = { repoKey: next.repoKey, callerKey: next.callerKey };
|
|
496
|
+
this.running.add(slot);
|
|
497
|
+
let released = false;
|
|
498
|
+
next.grant({
|
|
499
|
+
release: () => {
|
|
500
|
+
if (released) return;
|
|
501
|
+
released = true;
|
|
502
|
+
this.running.delete(slot);
|
|
503
|
+
this.pump();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
this.notifyPositions();
|
|
508
|
+
}
|
|
509
|
+
notifyPositions() {
|
|
510
|
+
this.projectedOrder().forEach((waiter, idx) => {
|
|
511
|
+
const position = idx + 1;
|
|
512
|
+
if (position === waiter.lastPosition) return;
|
|
513
|
+
waiter.lastPosition = position;
|
|
514
|
+
try {
|
|
515
|
+
waiter.onQueued?.(position);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
armLoadTimer() {
|
|
521
|
+
if (this.loadTimer) return;
|
|
522
|
+
this.loadTimer = setTimeout(() => {
|
|
523
|
+
this.loadTimer = void 0;
|
|
524
|
+
this.pump();
|
|
525
|
+
}, this.loadPollMs);
|
|
526
|
+
this.loadTimer.unref();
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
var provisionScheduler = new ProvisionScheduler();
|
|
530
|
+
|
|
531
|
+
// src/clone.ts
|
|
532
|
+
async function cloneEntry(src, dest, signal) {
|
|
533
|
+
const execOpts = signal ? { signal } : {};
|
|
534
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
535
|
+
const platform = process.platform;
|
|
536
|
+
const cloneArgs = platform === "darwin" ? ["-Rc", src, dest] : platform === "linux" ? ["-r", "--reflink=auto", src, dest] : null;
|
|
537
|
+
if (cloneArgs) {
|
|
538
|
+
const result = await execArgv("cp", cloneArgs, dirname(dest), execOpts);
|
|
539
|
+
if (result.exitCode === 0) return;
|
|
540
|
+
if (signal?.aborted) throw new ProvisionCancelledError();
|
|
541
|
+
}
|
|
542
|
+
const fallback = await execArgv("cp", ["-R", src, dest], dirname(dest), execOpts);
|
|
543
|
+
if (signal?.aborted) throw new ProvisionCancelledError();
|
|
544
|
+
if (fallback.exitCode !== 0) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`clone of '${src}' into '${dest}' failed: ${fallback.stderr || fallback.stdout}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function cloneEntries(repoRoot, cwd, patterns, signal) {
|
|
551
|
+
for (const pattern of patterns) {
|
|
552
|
+
const matches = await expandCloneGlob(repoRoot, pattern);
|
|
553
|
+
for (const rel of matches) {
|
|
554
|
+
if (signal?.aborted) throw new ProvisionCancelledError();
|
|
555
|
+
const dest = join(cwd, rel);
|
|
556
|
+
const existing = await lstat(dest).catch(() => null);
|
|
557
|
+
if (existing) continue;
|
|
558
|
+
await cloneEntry(join(repoRoot, rel), dest, signal);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
185
562
|
var ENV_VARS = {
|
|
186
563
|
/** Absolute path to the original repo checkout the worktree was cut from. */
|
|
187
564
|
sourceCheckoutPath: "AGENTPROTO_SOURCE_CHECKOUT_PATH",
|
|
@@ -282,16 +659,20 @@ async function runSetup(config, ctx, opts = {}) {
|
|
|
282
659
|
const commands = normalizeHook(config.worktree?.setup);
|
|
283
660
|
const runs = [];
|
|
284
661
|
const env = { ...hookEnv(ctx), TURBO_CACHE_DIR: resolveWorktreesTurboCacheDir() };
|
|
662
|
+
const execOpts = { env, ...opts.signal ? { signal: opts.signal } : {} };
|
|
285
663
|
for (const command of commands) {
|
|
286
|
-
|
|
664
|
+
if (opts.signal?.aborted) throw new ProvisionCancelledError();
|
|
665
|
+
let result = await execShell(command, ctx.worktreePath, execOpts);
|
|
287
666
|
let run = { command, result };
|
|
288
667
|
runs.push(run);
|
|
289
668
|
await appendHookLog(opts.logPath, command, result);
|
|
669
|
+
if (opts.signal?.aborted) throw new ProvisionCancelledError();
|
|
290
670
|
if (result.exitCode !== 0 && opts.retryOnFailure) {
|
|
291
|
-
result = await execShell(command, ctx.worktreePath,
|
|
671
|
+
result = await execShell(command, ctx.worktreePath, execOpts);
|
|
292
672
|
run = { command, result };
|
|
293
673
|
runs.push(run);
|
|
294
674
|
await appendHookLog(opts.logPath, `${command} (retry 1/1)`, result);
|
|
675
|
+
if (opts.signal?.aborted) throw new ProvisionCancelledError();
|
|
295
676
|
}
|
|
296
677
|
if (result.exitCode !== 0) throw new HookError("setup", run, opts.logPath);
|
|
297
678
|
}
|
|
@@ -308,6 +689,127 @@ async function runTeardown(config, ctx, opts = {}) {
|
|
|
308
689
|
}
|
|
309
690
|
return runs;
|
|
310
691
|
}
|
|
692
|
+
var WORKTREE_TRASH_DIRNAME = ".trash";
|
|
693
|
+
var WORKTREE_TRASH_PIDFILE = ".deleting";
|
|
694
|
+
var DELETER_SCRIPT = `trap 'rm -f "$1/.deleting"' EXIT; [ ! -e "$1/.deleting" ] || echo $$ > "$1/.deleting"; n=1; while [ "$n" -gt 0 ]; do n=0; for d in "$1"/*; do [ -e "$d" ] || continue; rm -rf "$d"; n=1; done; done`;
|
|
695
|
+
function isPidAlive(pid) {
|
|
696
|
+
try {
|
|
697
|
+
process.kill(pid, 0);
|
|
698
|
+
return true;
|
|
699
|
+
} catch (err) {
|
|
700
|
+
return err.code === "EPERM";
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function defaultSpawnDeleter(trashParent) {
|
|
704
|
+
const child = spawn("sh", ["-c", DELETER_SCRIPT, "_", trashParent], { detached: true, stdio: "ignore" });
|
|
705
|
+
child.unref();
|
|
706
|
+
return child.pid;
|
|
707
|
+
}
|
|
708
|
+
function ensureTrashDeleter(trashParent, spawnDeleter = defaultSpawnDeleter) {
|
|
709
|
+
const pidFile = join(trashParent, WORKTREE_TRASH_PIDFILE);
|
|
710
|
+
try {
|
|
711
|
+
const pid2 = Number.parseInt(readFileSync(pidFile, "utf8").trim(), 10);
|
|
712
|
+
if (Number.isInteger(pid2) && pid2 > 0 && isPidAlive(pid2)) return;
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
let placeholder = false;
|
|
716
|
+
try {
|
|
717
|
+
writeFileSync(pidFile, String(process.pid));
|
|
718
|
+
placeholder = true;
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
let pid;
|
|
722
|
+
try {
|
|
723
|
+
pid = spawnDeleter(trashParent);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
if (placeholder) rmSync(pidFile, { force: true });
|
|
726
|
+
throw err;
|
|
727
|
+
}
|
|
728
|
+
if (pid === void 0 && placeholder) rmSync(pidFile, { force: true });
|
|
729
|
+
}
|
|
730
|
+
function realOrResolved(path) {
|
|
731
|
+
try {
|
|
732
|
+
return realpathSync(path);
|
|
733
|
+
} catch {
|
|
734
|
+
return path;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async function removeWorktreeFast(repoRoot, path, options = {}) {
|
|
738
|
+
const force = options.force === true;
|
|
739
|
+
if (!force) {
|
|
740
|
+
const status = await execArgv(
|
|
741
|
+
"git",
|
|
742
|
+
["-C", path, "status", "--porcelain", "--untracked-files=normal", "--ignore-submodules=none"],
|
|
743
|
+
repoRoot
|
|
744
|
+
);
|
|
745
|
+
if (status.exitCode !== 0) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`git status failed in ${path} (exit ${status.exitCode}): ${status.stderr.trim() || status.stdout.trim()}`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (status.stdout.trim().length > 0) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`git worktree remove failed (exit 128): '${path}' contains modified or untracked files, use --force to delete it`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const list = await execArgv("git", ["-C", repoRoot, "worktree", "list", "--porcelain"], repoRoot);
|
|
757
|
+
if (list.exitCode !== 0) {
|
|
758
|
+
throw new Error(
|
|
759
|
+
`git worktree list failed (exit ${list.exitCode}): ${list.stderr.trim() || list.stdout.trim()}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const target = realOrResolved(path);
|
|
763
|
+
let found = false;
|
|
764
|
+
let locked = false;
|
|
765
|
+
for (const block of list.stdout.split(/\n\s*\n/)) {
|
|
766
|
+
const lines = block.split("\n");
|
|
767
|
+
const head = lines.find((line) => line.startsWith("worktree "));
|
|
768
|
+
if (!head || realOrResolved(head.slice("worktree ".length).trim()) !== target) continue;
|
|
769
|
+
found = true;
|
|
770
|
+
locked = lines.some((line) => line === "locked" || line.startsWith("locked "));
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
if (!found) {
|
|
774
|
+
throw new Error(`not a linked worktree of ${repoRoot}: ${path}`);
|
|
775
|
+
}
|
|
776
|
+
if (locked) {
|
|
777
|
+
throw new Error(`git worktree remove failed (exit 128): '${path}' is locked; unlock it first`);
|
|
778
|
+
}
|
|
779
|
+
const poolDir = join(path, "..");
|
|
780
|
+
const stamp = (options.now ?? Date.now)();
|
|
781
|
+
const trashDir = join(poolDir, WORKTREE_TRASH_DIRNAME, `${basename(path)}-${stamp}`);
|
|
782
|
+
try {
|
|
783
|
+
if (!existsSync(join(poolDir, WORKTREE_TRASH_DIRNAME))) {
|
|
784
|
+
mkdirSync(join(poolDir, WORKTREE_TRASH_DIRNAME));
|
|
785
|
+
}
|
|
786
|
+
await rename(path, trashDir);
|
|
787
|
+
} catch {
|
|
788
|
+
await execGit(repoRoot, force ? ["worktree", "remove", "--force", path] : ["worktree", "remove", path]);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
await execGit(repoRoot, ["worktree", "prune"]);
|
|
792
|
+
if (options.spawnRemoval) {
|
|
793
|
+
options.spawnRemoval(trashDir);
|
|
794
|
+
} else {
|
|
795
|
+
ensureTrashDeleter(dirname(trashDir), options.spawnDeleter);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function sweepWorktreeTrash(root, options = {}) {
|
|
799
|
+
let names;
|
|
800
|
+
try {
|
|
801
|
+
names = readdirSync(join(root, WORKTREE_TRASH_DIRNAME));
|
|
802
|
+
} catch {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
names = names.filter((name) => name !== WORKTREE_TRASH_PIDFILE);
|
|
806
|
+
if (names.length === 0) return;
|
|
807
|
+
if (options.spawnRemoval) {
|
|
808
|
+
for (const name of names) options.spawnRemoval(join(root, WORKTREE_TRASH_DIRNAME, name));
|
|
809
|
+
} else {
|
|
810
|
+
ensureTrashDeleter(join(root, WORKTREE_TRASH_DIRNAME), options.spawnDeleter);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
311
813
|
var SESSIONS_FILE_PATH = () => resolve(homedir(), ".agentproto", "sessions.json");
|
|
312
814
|
var SESSIONS_BUCKETS_ROOT = () => resolve(homedir(), ".agentproto", "workspaces");
|
|
313
815
|
var sessionRefSchema = z.object({
|
|
@@ -443,6 +945,12 @@ async function computeProvenance(repoRoot, worktreePath, options = {}) {
|
|
|
443
945
|
}
|
|
444
946
|
|
|
445
947
|
// src/provider/bodies/provision-worktree.body.ts
|
|
948
|
+
async function discardCancelledWorktree(repoRoot, cwd, branch) {
|
|
949
|
+
await removeWorktreeFast(repoRoot, cwd, { force: true }).catch(() => {
|
|
950
|
+
});
|
|
951
|
+
await execGit(repoRoot, ["branch", "-D", branch]).catch(() => {
|
|
952
|
+
});
|
|
953
|
+
}
|
|
446
954
|
var provisionWorktreeBuiltin = implementTool(
|
|
447
955
|
provisionWorktreeTool,
|
|
448
956
|
async ({ input }) => {
|
|
@@ -455,8 +963,12 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
455
963
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
456
964
|
});
|
|
457
965
|
const config = input.runSetup !== false ? await loadConfigFromBase(input.repoRoot, base) : null;
|
|
458
|
-
const
|
|
459
|
-
const
|
|
966
|
+
const localConfig = input.runSetup !== false ? await loadLocalWorktreeConfig(input.repoRoot) : null;
|
|
967
|
+
const linkPaths = input.linkPaths ?? localConfig?.linkPaths ?? config?.worktree?.linkPaths ?? [];
|
|
968
|
+
const depsCmd = input.depsCmd ?? localConfig?.depsCmd ?? config?.worktree?.depsCmd;
|
|
969
|
+
const copyGlobs = input.copyGlobs ?? localConfig?.copyGlobs ?? [];
|
|
970
|
+
const cloneGlobs = input.cloneGlobs ?? localConfig?.cloneGlobs ?? [];
|
|
971
|
+
const writeFiles = input.writeFiles ?? resolveLocalWriteFiles(localConfig?.writeFiles, input.slug);
|
|
460
972
|
for (const rel of linkPaths) {
|
|
461
973
|
const target = resolve(input.repoRoot, rel);
|
|
462
974
|
const dest = join(cwd, rel);
|
|
@@ -465,7 +977,7 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
465
977
|
await mkdir(dirname(dest), { recursive: true });
|
|
466
978
|
await symlink(target, dest, "dir");
|
|
467
979
|
}
|
|
468
|
-
for (const file of
|
|
980
|
+
for (const file of writeFiles) {
|
|
469
981
|
const dest = join(cwd, file.path);
|
|
470
982
|
await mkdir(dirname(dest), { recursive: true });
|
|
471
983
|
if (file.mode === "append") {
|
|
@@ -478,146 +990,124 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
478
990
|
await writeFile(dest, file.content);
|
|
479
991
|
}
|
|
480
992
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
});
|
|
993
|
+
const ctx = currentProvisionContext();
|
|
994
|
+
const signal = ctx?.signal;
|
|
995
|
+
const report = (progress) => {
|
|
996
|
+
try {
|
|
997
|
+
ctx?.onProgress?.(progress);
|
|
998
|
+
} catch {
|
|
488
999
|
}
|
|
1000
|
+
};
|
|
1001
|
+
const hasSetup = config ? normalizeHook(config.worktree?.setup).length > 0 : false;
|
|
1002
|
+
const firstHeavy = cloneGlobs.length > 0 ? "clone" : depsCmd ? "deps" : copyGlobs.length > 0 ? "copy" : hasSetup ? "setup" : null;
|
|
1003
|
+
if (firstHeavy === null) {
|
|
1004
|
+
report({ kind: "done", outcome: "ok" });
|
|
1005
|
+
return { cwd, branch };
|
|
489
1006
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1007
|
+
let phase = firstHeavy;
|
|
1008
|
+
let lease;
|
|
1009
|
+
try {
|
|
1010
|
+
lease = await provisionScheduler.acquire({
|
|
1011
|
+
repoKey: resolve(input.repoRoot),
|
|
1012
|
+
...ctx?.callerKey ? { callerKey: ctx.callerKey } : {},
|
|
1013
|
+
...signal ? { signal } : {},
|
|
1014
|
+
onQueued: (position) => report({ kind: "queued", position, phase: firstHeavy })
|
|
1015
|
+
});
|
|
1016
|
+
if (signal?.aborted) throw new ProvisionCancelledError();
|
|
1017
|
+
report({ kind: "started", phase });
|
|
1018
|
+
if (cloneGlobs.length > 0) {
|
|
1019
|
+
phase = "clone";
|
|
1020
|
+
report({ kind: "phase", phase });
|
|
1021
|
+
try {
|
|
1022
|
+
await cloneEntries(input.repoRoot, cwd, cloneGlobs, signal);
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
if (err instanceof ProvisionCancelledError) throw err;
|
|
1025
|
+
throw new ToolError({
|
|
1026
|
+
code: "execution_failed",
|
|
1027
|
+
message: `cloneGlobs failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
496
1030
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
await
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1031
|
+
if (depsCmd) {
|
|
1032
|
+
phase = "deps";
|
|
1033
|
+
report({ kind: "phase", phase });
|
|
1034
|
+
const result = await execShell(depsCmd, cwd, signal ? { signal } : {});
|
|
1035
|
+
if (signal?.aborted) throw new ProvisionCancelledError();
|
|
1036
|
+
if (result.exitCode !== 0) {
|
|
1037
|
+
throw new ToolError({
|
|
1038
|
+
code: "execution_failed",
|
|
1039
|
+
message: `depsCmd '${depsCmd}' failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (copyGlobs.length > 0) {
|
|
1044
|
+
phase = "copy";
|
|
1045
|
+
report({ kind: "phase", phase });
|
|
1046
|
+
for (const pattern of copyGlobs) {
|
|
1047
|
+
const matches = await expandGlob(input.repoRoot, pattern);
|
|
1048
|
+
for (const rel of matches) {
|
|
1049
|
+
if (signal?.aborted) throw new ProvisionCancelledError();
|
|
1050
|
+
const dest = join(cwd, rel);
|
|
1051
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
1052
|
+
await copyFile(join(input.repoRoot, rel), dest);
|
|
510
1053
|
}
|
|
511
|
-
);
|
|
512
|
-
} catch (err) {
|
|
513
|
-
if (err instanceof HookError) {
|
|
514
|
-
throw new ToolError({ code: "execution_failed", message: err.message });
|
|
515
1054
|
}
|
|
516
|
-
throw err;
|
|
517
1055
|
}
|
|
1056
|
+
if (config && hasSetup) {
|
|
1057
|
+
phase = "setup";
|
|
1058
|
+
report({ kind: "phase", phase });
|
|
1059
|
+
try {
|
|
1060
|
+
await runSetup(
|
|
1061
|
+
config,
|
|
1062
|
+
{
|
|
1063
|
+
sourceCheckoutPath: input.repoRoot,
|
|
1064
|
+
worktreePath: cwd,
|
|
1065
|
+
branchName: branch
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
...input.setupLogPath ? { logPath: input.setupLogPath } : {},
|
|
1069
|
+
...input.retrySetupOnFailure ? { retryOnFailure: true } : {},
|
|
1070
|
+
...signal ? { signal } : {}
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
if (err instanceof HookError) {
|
|
1075
|
+
throw new ToolError({ code: "execution_failed", message: err.message });
|
|
1076
|
+
}
|
|
1077
|
+
throw err;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
const cancelled = err instanceof ProvisionCancelledError;
|
|
1082
|
+
report({ kind: "done", outcome: cancelled ? "cancelled" : "failed" });
|
|
1083
|
+
if (cancelled) await discardCancelledWorktree(input.repoRoot, cwd, branch);
|
|
1084
|
+
throw err;
|
|
1085
|
+
} finally {
|
|
1086
|
+
lease?.release();
|
|
518
1087
|
}
|
|
1088
|
+
report({ kind: "done", outcome: "ok" });
|
|
519
1089
|
return { cwd, branch };
|
|
520
1090
|
}
|
|
521
1091
|
);
|
|
522
|
-
var WORKTREE_TRASH_DIRNAME = ".trash";
|
|
523
|
-
function realOrResolved(path) {
|
|
524
|
-
try {
|
|
525
|
-
return realpathSync(path);
|
|
526
|
-
} catch {
|
|
527
|
-
return path;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
async function removeWorktreeFast(repoRoot, path, options = {}) {
|
|
531
|
-
const force = options.force === true;
|
|
532
|
-
if (!force) {
|
|
533
|
-
const status = await execArgv(
|
|
534
|
-
"git",
|
|
535
|
-
["-C", path, "status", "--porcelain=v2", "--ignored=matching"],
|
|
536
|
-
repoRoot
|
|
537
|
-
);
|
|
538
|
-
if (status.exitCode !== 0) {
|
|
539
|
-
throw new Error(
|
|
540
|
-
`git status failed in ${path} (exit ${status.exitCode}): ${status.stderr.trim() || status.stdout.trim()}`
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
if (status.stdout.trim().length > 0) {
|
|
544
|
-
const probe = await execArgv("git", ["-C", repoRoot, "worktree", "remove", path], repoRoot);
|
|
545
|
-
if (probe.exitCode !== 0) {
|
|
546
|
-
throw new Error(
|
|
547
|
-
`git worktree remove failed (exit ${probe.exitCode}): ${probe.stderr.trim() || probe.stdout.trim()}`
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
await execGit(repoRoot, ["worktree", "prune"]);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
const list = await execArgv("git", ["-C", repoRoot, "worktree", "list", "--porcelain"], repoRoot);
|
|
555
|
-
if (list.exitCode !== 0) {
|
|
556
|
-
throw new Error(
|
|
557
|
-
`git worktree list failed (exit ${list.exitCode}): ${list.stderr.trim() || list.stdout.trim()}`
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
const registered = new Set(
|
|
561
|
-
list.stdout.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice("worktree ".length).trim())
|
|
562
|
-
);
|
|
563
|
-
const registeredReal = new Set([...registered].map(realOrResolved));
|
|
564
|
-
if (!registeredReal.has(realOrResolved(path))) {
|
|
565
|
-
throw new Error(`not a linked worktree of ${repoRoot}: ${path}`);
|
|
566
|
-
}
|
|
567
|
-
const poolDir = join(path, "..");
|
|
568
|
-
const stamp = (options.now ?? Date.now)();
|
|
569
|
-
const trashDir = join(poolDir, WORKTREE_TRASH_DIRNAME, `${basename(path)}-${stamp}`);
|
|
570
|
-
try {
|
|
571
|
-
if (!existsSync(join(poolDir, WORKTREE_TRASH_DIRNAME))) {
|
|
572
|
-
mkdirSync(join(poolDir, WORKTREE_TRASH_DIRNAME));
|
|
573
|
-
}
|
|
574
|
-
await rename(path, trashDir);
|
|
575
|
-
} catch {
|
|
576
|
-
await execGit(repoRoot, force ? ["worktree", "remove", "--force", path] : ["worktree", "remove", path]);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
await execGit(repoRoot, ["worktree", "prune"]);
|
|
580
|
-
if (options.spawnRemoval) {
|
|
581
|
-
options.spawnRemoval(trashDir);
|
|
582
|
-
} else {
|
|
583
|
-
spawn("rm", ["-rf", trashDir], { detached: true, stdio: "ignore" }).unref();
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
function sweepWorktreeTrash(root, options = {}) {
|
|
587
|
-
let names;
|
|
588
|
-
try {
|
|
589
|
-
names = readdirSync(join(root, WORKTREE_TRASH_DIRNAME));
|
|
590
|
-
} catch {
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
for (const name of names) {
|
|
594
|
-
const trashDir = join(root, WORKTREE_TRASH_DIRNAME, name);
|
|
595
|
-
if (options.spawnRemoval) {
|
|
596
|
-
options.spawnRemoval(trashDir);
|
|
597
|
-
} else {
|
|
598
|
-
spawn("rm", ["-rf", trashDir], { detached: true, stdio: "ignore" }).unref();
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
1092
|
function isPortFree(port) {
|
|
603
|
-
return new Promise((
|
|
1093
|
+
return new Promise((resolve7) => {
|
|
604
1094
|
const server = createServer();
|
|
605
|
-
server.once("error", () =>
|
|
1095
|
+
server.once("error", () => resolve7(false));
|
|
606
1096
|
server.once("listening", () => {
|
|
607
|
-
server.close(() =>
|
|
1097
|
+
server.close(() => resolve7(true));
|
|
608
1098
|
});
|
|
609
1099
|
server.listen(port, "127.0.0.1");
|
|
610
1100
|
});
|
|
611
1101
|
}
|
|
612
1102
|
function ephemeralPort() {
|
|
613
|
-
return new Promise((
|
|
1103
|
+
return new Promise((resolve7, reject) => {
|
|
614
1104
|
const server = createServer();
|
|
615
1105
|
server.once("error", reject);
|
|
616
1106
|
server.listen(0, "127.0.0.1", () => {
|
|
617
1107
|
const address = server.address();
|
|
618
1108
|
if (address && typeof address === "object") {
|
|
619
1109
|
const port = address.port;
|
|
620
|
-
server.close(() =>
|
|
1110
|
+
server.close(() => resolve7(port));
|
|
621
1111
|
} else {
|
|
622
1112
|
server.close(() => reject(new Error("could not determine an ephemeral port")));
|
|
623
1113
|
}
|
|
@@ -752,10 +1242,10 @@ var ServiceSupervisor = class _ServiceSupervisor {
|
|
|
752
1242
|
this.opts.proxyTable?.delete(t.runtime.hostname);
|
|
753
1243
|
const child = t.child;
|
|
754
1244
|
if (!child || t.status !== "running") return false;
|
|
755
|
-
return new Promise((
|
|
1245
|
+
return new Promise((resolve7) => {
|
|
756
1246
|
const done = () => {
|
|
757
1247
|
t.status = "exited";
|
|
758
|
-
|
|
1248
|
+
resolve7(true);
|
|
759
1249
|
};
|
|
760
1250
|
child.once("exit", done);
|
|
761
1251
|
child.kill("SIGTERM");
|
|
@@ -1420,6 +1910,6 @@ var worktreeProvider = defineDriver({
|
|
|
1420
1910
|
]
|
|
1421
1911
|
});
|
|
1422
1912
|
|
|
1423
|
-
export { CONFIG_FILENAME, ConfigError, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, HookError, InMemoryVerdictMemoStore, ProxyTable, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, ServiceSupervisor, VERDICT_MEMO_PATH, WORKTREES_TURBO_CACHE_DIR_ENV, WORKTREE_TRASH_DIRNAME, WorktreeNotRemovableError, allocatePort, classify, computeBaseDivergence, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, normalizeHook, parseConfig, peerEnv, peerEnvKey, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, removeWorktreeFast, repoLabel, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, sweepWorktreeTrash, worktreeProvider, writeWorktreeMarker };
|
|
1424
|
-
//# sourceMappingURL=chunk-
|
|
1425
|
-
//# sourceMappingURL=chunk-
|
|
1913
|
+
export { CONFIG_FILENAME, ConfigError, DEFAULT_PROVISION_CONCURRENCY, DEFAULT_PROVISION_LIMITS, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, GlobTraversalError, HookError, InMemoryVerdictMemoStore, LOCAL_WORKTREE_CONFIG_REL, PROVISION_CONCURRENCY_ENV, ProvisionCancelledError, ProvisionScheduler, ProxyTable, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, ServiceSupervisor, VERDICT_MEMO_PATH, WORKTREES_TURBO_CACHE_DIR_ENV, WORKTREE_TRASH_DIRNAME, WorktreeNotRemovableError, allocatePort, applySlugPlaceholder, classify, cloneEntries, computeBaseDivergence, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, currentProvisionContext, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandCloneGlob, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, loadLocalWorktreeConfig, normalizeHook, parseConfig, parseLocalWorktreeConfig, parseProvisionLimits, peerEnv, peerEnvKey, provisionScheduler, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, removeWorktreeFast, repoLabel, resolveLocalWriteFiles, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, runWithProvisionContext, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, sweepWorktreeTrash, worktreeProvider, writeWorktreeMarker };
|
|
1914
|
+
//# sourceMappingURL=chunk-IEW2O3PF.mjs.map
|
|
1915
|
+
//# sourceMappingURL=chunk-IEW2O3PF.mjs.map
|