@agentproto/worktree 0.10.0 → 0.13.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 +61 -3
- package/dist/bin/worktree-agent.mjs +3 -3
- package/dist/{chunk-IKCBL2UN.mjs → chunk-3FZWPMLF.mjs} +10 -5
- package/dist/chunk-3FZWPMLF.mjs.map +1 -0
- package/dist/{chunk-KRQFVJPK.mjs → chunk-MYH5XOX6.mjs} +307 -33
- package/dist/chunk-MYH5XOX6.mjs.map +1 -0
- package/dist/{chunk-6EV7WP2U.mjs → chunk-YMDOSMMD.mjs} +4 -4
- package/dist/{chunk-6EV7WP2U.mjs.map → chunk-YMDOSMMD.mjs.map} +1 -1
- package/dist/index.d.ts +260 -9
- package/dist/index.mjs +17 -7
- package/dist/index.mjs.map +1 -1
- package/dist/provider/index.mjs +2 -2
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-IKCBL2UN.mjs.map +0 -1
- package/dist/chunk-KRQFVJPK.mjs.map +0 -1
|
@@ -1,12 +1,13 @@
|
|
|
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, readdir, stat
|
|
5
|
-
import { resolve, join, dirname,
|
|
4
|
+
import { lstat, mkdir, symlink, appendFile, writeFile, copyFile, readFile, rename, readdir, stat } from 'fs/promises';
|
|
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
9
|
import { homedir } from 'os';
|
|
10
|
+
import { existsSync, mkdirSync, realpathSync, readdirSync } from 'fs';
|
|
10
11
|
import { createServer } from 'net';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -105,6 +106,53 @@ async function expandGlob(root, pattern) {
|
|
|
105
106
|
const candidates = await walk(root, base);
|
|
106
107
|
return candidates.filter((rel) => regex.test(rel));
|
|
107
108
|
}
|
|
109
|
+
var GlobTraversalError = class extends Error {
|
|
110
|
+
constructor(pattern) {
|
|
111
|
+
super(`glob pattern '${pattern}' escapes the repo root (path traversal)`);
|
|
112
|
+
this.name = "GlobTraversalError";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function assertNoTraversal(pattern) {
|
|
116
|
+
if (pattern.startsWith("/")) throw new GlobTraversalError(pattern);
|
|
117
|
+
if (pattern.split("/").some((seg) => seg === "..")) throw new GlobTraversalError(pattern);
|
|
118
|
+
}
|
|
119
|
+
async function expandCloneGlob(root, pattern) {
|
|
120
|
+
assertNoTraversal(pattern);
|
|
121
|
+
const segments = pattern.split("/").filter((seg) => seg.length > 0);
|
|
122
|
+
if (segments.length === 0) return [];
|
|
123
|
+
if (segments.includes("**")) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`cloneGlobs pattern '${pattern}' uses '**', which is not supported \u2014 name each path segment explicitly so a directory match clones as one unit.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
async function walkMatch(dirRel, idx) {
|
|
129
|
+
const seg = segments[idx];
|
|
130
|
+
const isLast = idx === segments.length - 1;
|
|
131
|
+
const abs = dirRel ? join(root, dirRel) : root;
|
|
132
|
+
let entries;
|
|
133
|
+
try {
|
|
134
|
+
entries = await readdir(abs, { withFileTypes: true });
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const regex = new RegExp(`^${escapeSegment(seg)}$`);
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.name === ".git") continue;
|
|
142
|
+
if (!regex.test(entry.name)) continue;
|
|
143
|
+
const rel = dirRel ? posix.join(dirRel, entry.name) : entry.name;
|
|
144
|
+
if (isLast) {
|
|
145
|
+
out.push(rel);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
out.push(...await walkMatch(rel, idx + 1));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
return walkMatch("", 0);
|
|
155
|
+
}
|
|
108
156
|
var CONFIG_FILENAME = "agentproto.json";
|
|
109
157
|
var hookSchema = z.union([z.string(), z.array(z.string())]);
|
|
110
158
|
var scriptSchema = z.object({
|
|
@@ -181,6 +229,86 @@ async function loadConfigFromBase(repoRoot, base = "origin/main") {
|
|
|
181
229
|
}
|
|
182
230
|
return parseConfig(result.stdout);
|
|
183
231
|
}
|
|
232
|
+
var LOCAL_WORKTREE_CONFIG_REL = ".agentproto/worktree.json";
|
|
233
|
+
var localWriteFileEntrySchema = z.object({
|
|
234
|
+
path: z.string().min(1, "writeFiles[].path must be non-empty"),
|
|
235
|
+
content: z.string(),
|
|
236
|
+
mode: z.enum(["create", "append"]).optional()
|
|
237
|
+
});
|
|
238
|
+
var localWorktreeConfigSchema = z.object({
|
|
239
|
+
copyGlobs: z.array(z.string()).optional(),
|
|
240
|
+
/** Gitignored dirs/files cloned (copy-on-write where the filesystem
|
|
241
|
+
* supports it) from the source checkout into the worktree before
|
|
242
|
+
* `depsCmd` runs — see `clone.ts`. */
|
|
243
|
+
cloneGlobs: z.array(z.string()).optional(),
|
|
244
|
+
writeFiles: z.array(localWriteFileEntrySchema).optional(),
|
|
245
|
+
depsCmd: z.string().optional(),
|
|
246
|
+
linkPaths: z.array(z.string()).optional()
|
|
247
|
+
});
|
|
248
|
+
function parseLocalWorktreeConfig(raw) {
|
|
249
|
+
let json;
|
|
250
|
+
try {
|
|
251
|
+
json = JSON.parse(raw);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
throw new ConfigError(
|
|
254
|
+
`${LOCAL_WORKTREE_CONFIG_REL} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const result = localWorktreeConfigSchema.safeParse(json);
|
|
258
|
+
if (!result.success) {
|
|
259
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
260
|
+
throw new ConfigError(`${LOCAL_WORKTREE_CONFIG_REL} failed validation: ${issues}`);
|
|
261
|
+
}
|
|
262
|
+
return result.data;
|
|
263
|
+
}
|
|
264
|
+
async function loadLocalWorktreeConfig(workspace) {
|
|
265
|
+
const path = resolve(workspace, LOCAL_WORKTREE_CONFIG_REL);
|
|
266
|
+
let raw;
|
|
267
|
+
try {
|
|
268
|
+
raw = await readFile(path, "utf8");
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (err.code === "ENOENT") return null;
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
return parseLocalWorktreeConfig(raw);
|
|
274
|
+
}
|
|
275
|
+
var SLUG_PLACEHOLDER = /\{slug\}/g;
|
|
276
|
+
function applySlugPlaceholder(text, slug) {
|
|
277
|
+
return text.replace(SLUG_PLACEHOLDER, slug);
|
|
278
|
+
}
|
|
279
|
+
function resolveLocalWriteFiles(files, slug) {
|
|
280
|
+
return (files ?? []).map((file) => ({
|
|
281
|
+
...file,
|
|
282
|
+
path: applySlugPlaceholder(file.path, slug),
|
|
283
|
+
content: applySlugPlaceholder(file.content, slug)
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
async function cloneEntry(src, dest) {
|
|
287
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
288
|
+
const platform = process.platform;
|
|
289
|
+
const cloneArgs = platform === "darwin" ? ["-Rc", src, dest] : platform === "linux" ? ["-r", "--reflink=auto", src, dest] : null;
|
|
290
|
+
if (cloneArgs) {
|
|
291
|
+
const result = await execArgv("cp", cloneArgs, dirname(dest));
|
|
292
|
+
if (result.exitCode === 0) return;
|
|
293
|
+
}
|
|
294
|
+
const fallback = await execArgv("cp", ["-R", src, dest], dirname(dest));
|
|
295
|
+
if (fallback.exitCode !== 0) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`clone of '${src}' into '${dest}' failed: ${fallback.stderr || fallback.stdout}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function cloneEntries(repoRoot, cwd, patterns) {
|
|
302
|
+
for (const pattern of patterns) {
|
|
303
|
+
const matches = await expandCloneGlob(repoRoot, pattern);
|
|
304
|
+
for (const rel of matches) {
|
|
305
|
+
const dest = join(cwd, rel);
|
|
306
|
+
const existing = await lstat(dest).catch(() => null);
|
|
307
|
+
if (existing) continue;
|
|
308
|
+
await cloneEntry(join(repoRoot, rel), dest);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
184
312
|
var ENV_VARS = {
|
|
185
313
|
/** Absolute path to the original repo checkout the worktree was cut from. */
|
|
186
314
|
sourceCheckoutPath: "AGENTPROTO_SOURCE_CHECKOUT_PATH",
|
|
@@ -232,38 +360,78 @@ function resolveWorktreesTurboCacheDir() {
|
|
|
232
360
|
}
|
|
233
361
|
|
|
234
362
|
// src/lifecycle.ts
|
|
363
|
+
var NOISE_LINE_RES = [
|
|
364
|
+
/Failed to replace env in config: \$\{NODE_AUTH_TOKEN\}/,
|
|
365
|
+
/^\s*WARN\s+Issue while reading ".*\.npmrc"\.?\s*$/
|
|
366
|
+
];
|
|
367
|
+
function filteredTail(result) {
|
|
368
|
+
const combined = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
369
|
+
const lines = combined.split("\n").filter((line) => !NOISE_LINE_RES.some((re) => re.test(line)));
|
|
370
|
+
const tail = lines.slice(-60).join("\n").trim();
|
|
371
|
+
return tail || "(no output captured)";
|
|
372
|
+
}
|
|
235
373
|
var HookError = class extends Error {
|
|
236
374
|
command;
|
|
237
375
|
result;
|
|
238
|
-
|
|
376
|
+
/** Path to the full, unfiltered stdout+stderr log for this and every
|
|
377
|
+
* other hook command in the same run, when the caller asked for one to
|
|
378
|
+
* be persisted (see {@link runSetup}'s `opts.logPath`). */
|
|
379
|
+
logPath;
|
|
380
|
+
constructor(phase, run, logPath) {
|
|
239
381
|
super(
|
|
240
382
|
`worktree ${phase} hook failed (exit ${run.result.exitCode}): ${run.command}
|
|
241
|
-
` + (run.result
|
|
383
|
+
` + filteredTail(run.result) + (logPath ? `
|
|
384
|
+
|
|
385
|
+
(full hook output: ${logPath})` : "")
|
|
242
386
|
);
|
|
243
387
|
this.name = "HookError";
|
|
244
388
|
this.command = run.command;
|
|
245
389
|
this.result = run.result;
|
|
390
|
+
if (logPath !== void 0) this.logPath = logPath;
|
|
246
391
|
}
|
|
247
392
|
};
|
|
248
|
-
async function
|
|
393
|
+
async function appendHookLog(logPath, command, result) {
|
|
394
|
+
if (!logPath) return;
|
|
395
|
+
try {
|
|
396
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
397
|
+
const parts = [
|
|
398
|
+
`$ ${command}`,
|
|
399
|
+
result.stdout,
|
|
400
|
+
result.stderr,
|
|
401
|
+
`(exit ${result.exitCode})`,
|
|
402
|
+
""
|
|
403
|
+
].filter(Boolean);
|
|
404
|
+
await appendFile(logPath, parts.join("\n") + "\n", "utf8");
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function runSetup(config, ctx, opts = {}) {
|
|
249
409
|
const commands = normalizeHook(config.worktree?.setup);
|
|
250
410
|
const runs = [];
|
|
251
411
|
const env = { ...hookEnv(ctx), TURBO_CACHE_DIR: resolveWorktreesTurboCacheDir() };
|
|
252
412
|
for (const command of commands) {
|
|
253
|
-
|
|
254
|
-
|
|
413
|
+
let result = await execShell(command, ctx.worktreePath, { env });
|
|
414
|
+
let run = { command, result };
|
|
255
415
|
runs.push(run);
|
|
256
|
-
|
|
416
|
+
await appendHookLog(opts.logPath, command, result);
|
|
417
|
+
if (result.exitCode !== 0 && opts.retryOnFailure) {
|
|
418
|
+
result = await execShell(command, ctx.worktreePath, { env });
|
|
419
|
+
run = { command, result };
|
|
420
|
+
runs.push(run);
|
|
421
|
+
await appendHookLog(opts.logPath, `${command} (retry 1/1)`, result);
|
|
422
|
+
}
|
|
423
|
+
if (result.exitCode !== 0) throw new HookError("setup", run, opts.logPath);
|
|
257
424
|
}
|
|
258
425
|
return runs;
|
|
259
426
|
}
|
|
260
|
-
async function runTeardown(config, ctx) {
|
|
427
|
+
async function runTeardown(config, ctx, opts = {}) {
|
|
261
428
|
const commands = normalizeHook(config.worktree?.teardown);
|
|
262
429
|
const runs = [];
|
|
263
430
|
const env = hookEnv(ctx);
|
|
264
431
|
for (const command of commands) {
|
|
265
432
|
const result = await execShell(command, ctx.worktreePath, { env });
|
|
266
433
|
runs.push({ command, result });
|
|
434
|
+
await appendHookLog(opts.logPath, command, result);
|
|
267
435
|
}
|
|
268
436
|
return runs;
|
|
269
437
|
}
|
|
@@ -414,8 +582,12 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
414
582
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
415
583
|
});
|
|
416
584
|
const config = input.runSetup !== false ? await loadConfigFromBase(input.repoRoot, base) : null;
|
|
417
|
-
const
|
|
418
|
-
const
|
|
585
|
+
const localConfig = input.runSetup !== false ? await loadLocalWorktreeConfig(input.repoRoot) : null;
|
|
586
|
+
const linkPaths = input.linkPaths ?? localConfig?.linkPaths ?? config?.worktree?.linkPaths ?? [];
|
|
587
|
+
const depsCmd = input.depsCmd ?? localConfig?.depsCmd ?? config?.worktree?.depsCmd;
|
|
588
|
+
const copyGlobs = input.copyGlobs ?? localConfig?.copyGlobs ?? [];
|
|
589
|
+
const cloneGlobs = input.cloneGlobs ?? localConfig?.cloneGlobs ?? [];
|
|
590
|
+
const writeFiles = input.writeFiles ?? resolveLocalWriteFiles(localConfig?.writeFiles, input.slug);
|
|
419
591
|
for (const rel of linkPaths) {
|
|
420
592
|
const target = resolve(input.repoRoot, rel);
|
|
421
593
|
const dest = join(cwd, rel);
|
|
@@ -424,7 +596,7 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
424
596
|
await mkdir(dirname(dest), { recursive: true });
|
|
425
597
|
await symlink(target, dest, "dir");
|
|
426
598
|
}
|
|
427
|
-
for (const file of
|
|
599
|
+
for (const file of writeFiles) {
|
|
428
600
|
const dest = join(cwd, file.path);
|
|
429
601
|
await mkdir(dirname(dest), { recursive: true });
|
|
430
602
|
if (file.mode === "append") {
|
|
@@ -437,6 +609,16 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
437
609
|
await writeFile(dest, file.content);
|
|
438
610
|
}
|
|
439
611
|
}
|
|
612
|
+
if (cloneGlobs.length > 0) {
|
|
613
|
+
try {
|
|
614
|
+
await cloneEntries(input.repoRoot, cwd, cloneGlobs);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
throw new ToolError({
|
|
617
|
+
code: "execution_failed",
|
|
618
|
+
message: `cloneGlobs failed: ${err instanceof Error ? err.message : String(err)}`
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
440
622
|
if (depsCmd) {
|
|
441
623
|
const result = await execShell(depsCmd, cwd);
|
|
442
624
|
if (result.exitCode !== 0) {
|
|
@@ -446,7 +628,7 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
446
628
|
});
|
|
447
629
|
}
|
|
448
630
|
}
|
|
449
|
-
for (const pattern of
|
|
631
|
+
for (const pattern of copyGlobs) {
|
|
450
632
|
const matches = await expandGlob(input.repoRoot, pattern);
|
|
451
633
|
for (const rel of matches) {
|
|
452
634
|
const dest = join(cwd, rel);
|
|
@@ -456,11 +638,18 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
456
638
|
}
|
|
457
639
|
if (config) {
|
|
458
640
|
try {
|
|
459
|
-
await runSetup(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
641
|
+
await runSetup(
|
|
642
|
+
config,
|
|
643
|
+
{
|
|
644
|
+
sourceCheckoutPath: input.repoRoot,
|
|
645
|
+
worktreePath: cwd,
|
|
646
|
+
branchName: branch
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
...input.setupLogPath ? { logPath: input.setupLogPath } : {},
|
|
650
|
+
...input.retrySetupOnFailure ? { retryOnFailure: true } : {}
|
|
651
|
+
}
|
|
652
|
+
);
|
|
464
653
|
} catch (err) {
|
|
465
654
|
if (err instanceof HookError) {
|
|
466
655
|
throw new ToolError({ code: "execution_failed", message: err.message });
|
|
@@ -471,25 +660,105 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
471
660
|
return { cwd, branch };
|
|
472
661
|
}
|
|
473
662
|
);
|
|
663
|
+
var WORKTREE_TRASH_DIRNAME = ".trash";
|
|
664
|
+
function realOrResolved(path) {
|
|
665
|
+
try {
|
|
666
|
+
return realpathSync(path);
|
|
667
|
+
} catch {
|
|
668
|
+
return path;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function removeWorktreeFast(repoRoot, path, options = {}) {
|
|
672
|
+
const force = options.force === true;
|
|
673
|
+
if (!force) {
|
|
674
|
+
const status = await execArgv(
|
|
675
|
+
"git",
|
|
676
|
+
["-C", path, "status", "--porcelain=v2", "--ignored=matching"],
|
|
677
|
+
repoRoot
|
|
678
|
+
);
|
|
679
|
+
if (status.exitCode !== 0) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`git status failed in ${path} (exit ${status.exitCode}): ${status.stderr.trim() || status.stdout.trim()}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (status.stdout.trim().length > 0) {
|
|
685
|
+
const probe = await execArgv("git", ["-C", repoRoot, "worktree", "remove", path], repoRoot);
|
|
686
|
+
if (probe.exitCode !== 0) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`git worktree remove failed (exit ${probe.exitCode}): ${probe.stderr.trim() || probe.stdout.trim()}`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
await execGit(repoRoot, ["worktree", "prune"]);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const list = await execArgv("git", ["-C", repoRoot, "worktree", "list", "--porcelain"], repoRoot);
|
|
696
|
+
if (list.exitCode !== 0) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`git worktree list failed (exit ${list.exitCode}): ${list.stderr.trim() || list.stdout.trim()}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const registered = new Set(
|
|
702
|
+
list.stdout.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice("worktree ".length).trim())
|
|
703
|
+
);
|
|
704
|
+
const registeredReal = new Set([...registered].map(realOrResolved));
|
|
705
|
+
if (!registeredReal.has(realOrResolved(path))) {
|
|
706
|
+
throw new Error(`not a linked worktree of ${repoRoot}: ${path}`);
|
|
707
|
+
}
|
|
708
|
+
const poolDir = join(path, "..");
|
|
709
|
+
const stamp = (options.now ?? Date.now)();
|
|
710
|
+
const trashDir = join(poolDir, WORKTREE_TRASH_DIRNAME, `${basename(path)}-${stamp}`);
|
|
711
|
+
try {
|
|
712
|
+
if (!existsSync(join(poolDir, WORKTREE_TRASH_DIRNAME))) {
|
|
713
|
+
mkdirSync(join(poolDir, WORKTREE_TRASH_DIRNAME));
|
|
714
|
+
}
|
|
715
|
+
await rename(path, trashDir);
|
|
716
|
+
} catch {
|
|
717
|
+
await execGit(repoRoot, force ? ["worktree", "remove", "--force", path] : ["worktree", "remove", path]);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
await execGit(repoRoot, ["worktree", "prune"]);
|
|
721
|
+
if (options.spawnRemoval) {
|
|
722
|
+
options.spawnRemoval(trashDir);
|
|
723
|
+
} else {
|
|
724
|
+
spawn("rm", ["-rf", trashDir], { detached: true, stdio: "ignore" }).unref();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function sweepWorktreeTrash(root, options = {}) {
|
|
728
|
+
let names;
|
|
729
|
+
try {
|
|
730
|
+
names = readdirSync(join(root, WORKTREE_TRASH_DIRNAME));
|
|
731
|
+
} catch {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const name of names) {
|
|
735
|
+
const trashDir = join(root, WORKTREE_TRASH_DIRNAME, name);
|
|
736
|
+
if (options.spawnRemoval) {
|
|
737
|
+
options.spawnRemoval(trashDir);
|
|
738
|
+
} else {
|
|
739
|
+
spawn("rm", ["-rf", trashDir], { detached: true, stdio: "ignore" }).unref();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
474
743
|
function isPortFree(port) {
|
|
475
|
-
return new Promise((
|
|
744
|
+
return new Promise((resolve6) => {
|
|
476
745
|
const server = createServer();
|
|
477
|
-
server.once("error", () =>
|
|
746
|
+
server.once("error", () => resolve6(false));
|
|
478
747
|
server.once("listening", () => {
|
|
479
|
-
server.close(() =>
|
|
748
|
+
server.close(() => resolve6(true));
|
|
480
749
|
});
|
|
481
750
|
server.listen(port, "127.0.0.1");
|
|
482
751
|
});
|
|
483
752
|
}
|
|
484
753
|
function ephemeralPort() {
|
|
485
|
-
return new Promise((
|
|
754
|
+
return new Promise((resolve6, reject) => {
|
|
486
755
|
const server = createServer();
|
|
487
756
|
server.once("error", reject);
|
|
488
757
|
server.listen(0, "127.0.0.1", () => {
|
|
489
758
|
const address = server.address();
|
|
490
759
|
if (address && typeof address === "object") {
|
|
491
760
|
const port = address.port;
|
|
492
|
-
server.close(() =>
|
|
761
|
+
server.close(() => resolve6(port));
|
|
493
762
|
} else {
|
|
494
763
|
server.close(() => reject(new Error("could not determine an ephemeral port")));
|
|
495
764
|
}
|
|
@@ -624,10 +893,10 @@ var ServiceSupervisor = class _ServiceSupervisor {
|
|
|
624
893
|
this.opts.proxyTable?.delete(t.runtime.hostname);
|
|
625
894
|
const child = t.child;
|
|
626
895
|
if (!child || t.status !== "running") return false;
|
|
627
|
-
return new Promise((
|
|
896
|
+
return new Promise((resolve6) => {
|
|
628
897
|
const done = () => {
|
|
629
898
|
t.status = "exited";
|
|
630
|
-
|
|
899
|
+
resolve6(true);
|
|
631
900
|
};
|
|
632
901
|
child.once("exit", done);
|
|
633
902
|
child.kill("SIGTERM");
|
|
@@ -769,6 +1038,7 @@ async function computeTreeState(repoRoot, worktreePath, options = {}) {
|
|
|
769
1038
|
);
|
|
770
1039
|
}
|
|
771
1040
|
const noise = [];
|
|
1041
|
+
const dirtyPaths = [];
|
|
772
1042
|
let modified = 0;
|
|
773
1043
|
let staged = 0;
|
|
774
1044
|
let untracked = 0;
|
|
@@ -779,6 +1049,7 @@ async function computeTreeState(repoRoot, worktreePath, options = {}) {
|
|
|
779
1049
|
noise.push(path);
|
|
780
1050
|
continue;
|
|
781
1051
|
}
|
|
1052
|
+
if (path !== null) dirtyPaths.push(path);
|
|
782
1053
|
if (line.startsWith("? ")) {
|
|
783
1054
|
untracked++;
|
|
784
1055
|
continue;
|
|
@@ -790,12 +1061,14 @@ async function computeTreeState(repoRoot, worktreePath, options = {}) {
|
|
|
790
1061
|
}
|
|
791
1062
|
}
|
|
792
1063
|
if (modified === 0 && staged === 0 && untracked === 0) return noise.length > 0 ? { state: "clean", noise } : { state: "clean" };
|
|
1064
|
+
const onlyUnder = dirtyPaths.length > 0 && dirtyPaths.every((p) => p.startsWith(".plans/")) ? ".plans/" : void 0;
|
|
793
1065
|
return {
|
|
794
1066
|
state: "dirty",
|
|
795
1067
|
modified,
|
|
796
1068
|
staged,
|
|
797
1069
|
untracked,
|
|
798
|
-
newestMtimeMs: await newestDirtyMtimeMs(repoRoot, worktreePath)
|
|
1070
|
+
newestMtimeMs: await newestDirtyMtimeMs(repoRoot, worktreePath),
|
|
1071
|
+
...onlyUnder ? { onlyUnder } : {}
|
|
799
1072
|
};
|
|
800
1073
|
}
|
|
801
1074
|
async function newestDirtyMtimeMs(repoRoot, worktreePath) {
|
|
@@ -1062,6 +1335,7 @@ function classify(tree, integration, liveness, nowMs = Date.now()) {
|
|
|
1062
1335
|
const idleOrUnreachable = liveness.state === "idle" || liveness.state === "daemon-unreachable";
|
|
1063
1336
|
if ((merged || fresh) && clean && idleOrUnreachable) return { reclaimable: true, class: "reclaim" };
|
|
1064
1337
|
if (merged && !clean) {
|
|
1338
|
+
if (!idleOrUnreachable) return { reclaimable: false, class: "hold" };
|
|
1065
1339
|
const writtenRecently = tree.state === "dirty" && typeof tree.newestMtimeMs === "number" && nowMs - tree.newestMtimeMs < RECENT_WRITE_HOLD_WINDOW_MS;
|
|
1066
1340
|
if (writtenRecently) return { reclaimable: false, class: "hold" };
|
|
1067
1341
|
return { reclaimable: false, class: "salvage" };
|
|
@@ -1189,13 +1463,13 @@ var cleanupWorktreeBuiltin = implementTool(
|
|
|
1189
1463
|
if (tree.untracked > 0 && !discardUntracked) blocked.push("untracked");
|
|
1190
1464
|
if ((tree.modified > 0 || tree.staged > 0) && !discardModified) blocked.push("modified");
|
|
1191
1465
|
if (blocked.length > 0) throw new WorktreeNotRemovableError(input.cwd, blocked);
|
|
1192
|
-
await
|
|
1466
|
+
await removeWorktreeFast(input.repoRoot, input.cwd, { force: true });
|
|
1193
1467
|
} else {
|
|
1194
|
-
await
|
|
1468
|
+
await removeWorktreeFast(input.repoRoot, input.cwd);
|
|
1195
1469
|
}
|
|
1196
1470
|
} else {
|
|
1197
1471
|
try {
|
|
1198
|
-
await
|
|
1472
|
+
await removeWorktreeFast(input.repoRoot, input.cwd);
|
|
1199
1473
|
} catch (err) {
|
|
1200
1474
|
throw new WorktreeNotRemovableError(input.cwd, [], err instanceof Error ? err.message : String(err));
|
|
1201
1475
|
}
|
|
@@ -1287,6 +1561,6 @@ var worktreeProvider = defineDriver({
|
|
|
1287
1561
|
]
|
|
1288
1562
|
});
|
|
1289
1563
|
|
|
1290
|
-
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, 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, repoLabel, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, worktreeProvider, writeWorktreeMarker };
|
|
1291
|
-
//# sourceMappingURL=chunk-
|
|
1292
|
-
//# sourceMappingURL=chunk-
|
|
1564
|
+
export { CONFIG_FILENAME, ConfigError, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, GlobTraversalError, HookError, InMemoryVerdictMemoStore, LOCAL_WORKTREE_CONFIG_REL, 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, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandCloneGlob, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, loadLocalWorktreeConfig, normalizeHook, parseConfig, parseLocalWorktreeConfig, peerEnv, peerEnvKey, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, removeWorktreeFast, repoLabel, resolveLocalWriteFiles, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, sweepWorktreeTrash, worktreeProvider, writeWorktreeMarker };
|
|
1565
|
+
//# sourceMappingURL=chunk-MYH5XOX6.mjs.map
|
|
1566
|
+
//# sourceMappingURL=chunk-MYH5XOX6.mjs.map
|