@nexrall/code-core 1.4.59 → 1.4.60

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.
@@ -0,0 +1,502 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports._worktreeInternal = void 0;
37
+ exports.randomWorktreeName = randomWorktreeName;
38
+ exports.createWorktree = createWorktree;
39
+ exports.worktreeHasWork = worktreeHasWork;
40
+ exports.removeWorktree = removeWorktree;
41
+ exports.enterWorktree = enterWorktree;
42
+ exports.pathEscapesWorktree = pathEscapesWorktree;
43
+ exports.listWorktrees = listWorktrees;
44
+ exports.findWorktreeByName = findWorktreeByName;
45
+ /**
46
+ * Worktree isolation — lets one session confine ALL its file mutations to a
47
+ * separate directory, so a SECOND session working the same repo can never be
48
+ * physically overwritten by the first. This is the primitive Claude Code's
49
+ * docs describe under "Isolate sessions with worktrees": messaging (see
50
+ * peer messaging, once that lands) is a SIGNAL channel — it tells another
51
+ * session what happened. Worktree isolation is a PHYSICAL guarantee — it
52
+ * makes "what happened" impossible to be "you overwrote my file", because
53
+ * the two sessions were never touching the same bytes on disk in the first
54
+ * place.
55
+ *
56
+ * Two backends, chosen automatically per workDir:
57
+ *
58
+ * 1. GIT-NATIVE (`git worktree add`) — used when workDir is inside a git
59
+ * repository. A real git worktree: its own checkout, its own branch,
60
+ * full history/remote sharing with the main checkout. Cleanup is
61
+ * `git worktree remove`. This mirrors Claude Code's own `--worktree`
62
+ * flag almost exactly (`.nexrall/worktrees/<name>/` instead of
63
+ * `.claude/worktrees/<name>/`, `worktree-<name>` branch naming kept the
64
+ * same since there's no reason to diverge on a convention this legible).
65
+ *
66
+ * 2. COPY-BASED (non-git) — used when workDir has no git ancestor at all
67
+ * (Perforce, SVN, no VCS, or a git repo the user explicitly opted out
68
+ * of). Claude Code solves this with a `WorktreeCreate` hook the PROJECT
69
+ * must supply; we go one step further and ALSO ship a zero-config
70
+ * default (a plain recursive copy, respecting .gitignore-style ignore
71
+ * patterns) so isolation works out of the box even in a project that
72
+ * hasn't written a hook. A project CAN still override this default with
73
+ * its own `.nexrall/hooks/worktree-create|enter|exit` command for a
74
+ * VCS-aware equivalent (Perforce `p4 client`, SVN working copies, etc.)
75
+ * — see `runWorktreeHook` below — exactly the escape hatch Claude Code's
76
+ * docs describe for "Non-git version control".
77
+ *
78
+ * STATE
79
+ * -----
80
+ * A worktree session's active isolation state lives in-memory in the caller
81
+ * (loop.ts / the client), passed as `WorktreeState` through AgentLoopOptions.
82
+ * This module only creates/removes/enforces — it does not own "which
83
+ * worktree is this session in right now", because that already has an
84
+ * owner in every client (ChatPanel's session state, the CLI's chat loop
85
+ * locals, TaskHost) and a second source of truth would drift from it.
86
+ */
87
+ const fs = __importStar(require("fs"));
88
+ const path = __importStar(require("path"));
89
+ const child_process_1 = require("child_process");
90
+ const crypto = __importStar(require("crypto"));
91
+ const WORKTREE_ROOT_DIRNAME = '.nexrall/worktrees';
92
+ // Directories that must never be copied into a copy-based worktree: they are
93
+ // either huge/regenerable (node_modules, dist, build caches) or themselves a
94
+ // VCS metadata directory that would confuse the copy semantics. This list is
95
+ // intentionally conservative and copy-specific — NOT the same list search/glob
96
+ // tools use, because those exist to avoid noisy search results, while this one
97
+ // exists to keep worktree creation fast and avoid duplicating gigabytes of
98
+ // reproducible artifacts. A project can still get everything copied verbatim
99
+ // by using its OWN hook instead of the built-in copy fallback.
100
+ const COPY_SKIP_DIRS = new Set([
101
+ '.git', 'node_modules', 'dist', 'build', '.next', '__pycache__',
102
+ '.venv', 'venv', '.nexrall', '.svn', '.hg',
103
+ ]);
104
+ function isGitRepo(dir) {
105
+ try {
106
+ const r = (0, child_process_1.spawnSync)('git', ['rev-parse', '--show-toplevel'], {
107
+ cwd: dir, encoding: 'utf-8', timeout: 5000,
108
+ });
109
+ if (r.status === 0 && r.stdout)
110
+ return { ok: true, root: r.stdout.trim() };
111
+ }
112
+ catch { /* git not installed or not a repo */ }
113
+ return { ok: false };
114
+ }
115
+ /** Generate a readable random name, same shape Claude Code uses ("bright-running-fox"). */
116
+ function randomWorktreeName() {
117
+ const adjectives = ['bright', 'quiet', 'brave', 'calm', 'swift', 'keen', 'bold', 'clear', 'gentle', 'sharp'];
118
+ const nouns = ['fox', 'river', 'falcon', 'harbor', 'meadow', 'ember', 'summit', 'cedar', 'otter', 'quartz'];
119
+ const a = adjectives[crypto.randomInt(adjectives.length)];
120
+ const n = nouns[crypto.randomInt(nouns.length)];
121
+ const suffix = crypto.randomBytes(2).toString('hex');
122
+ return `${a}-${n}-${suffix}`;
123
+ }
124
+ function safeName(name) {
125
+ // Same constraint a git branch name / directory basename both need: no
126
+ // path separators, no leading dot, nothing that could escape
127
+ // .nexrall/worktrees/ via `..`.
128
+ const cleaned = name.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^\.+/, '');
129
+ return cleaned || randomWorktreeName();
130
+ }
131
+ /**
132
+ * Look for a project-supplied override hook: `.nexrall/hooks/worktree-<phase>`
133
+ * (an executable script/command), same escape hatch Claude Code's docs
134
+ * describe for non-git version control. Returns the command string to run,
135
+ * or null if the project hasn't defined one for this phase.
136
+ */
137
+ function findWorktreeHook(workDir, phase) {
138
+ try {
139
+ const settingsPath = path.join(workDir, '.nexrall', 'settings.json');
140
+ if (!fs.existsSync(settingsPath))
141
+ return null;
142
+ const raw = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
143
+ const hooks = (raw.worktreeHooks ?? {});
144
+ const cmd = hooks[phase];
145
+ return typeof cmd === 'string' && cmd.trim() ? cmd.trim() : null;
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ function runWorktreeHook(workDir, phase, env) {
152
+ const cmd = findWorktreeHook(workDir, phase);
153
+ if (!cmd)
154
+ return { ran: false, ok: false };
155
+ try {
156
+ const r = (0, child_process_1.spawnSync)(cmd, {
157
+ shell: true,
158
+ cwd: workDir,
159
+ timeout: 60000,
160
+ encoding: 'utf-8',
161
+ env: { ...process.env, ...env },
162
+ });
163
+ return { ran: true, ok: r.status === 0, output: (r.stdout ?? '') + (r.stderr ?? '') };
164
+ }
165
+ catch (err) {
166
+ return { ran: true, ok: false, output: err.message };
167
+ }
168
+ }
169
+ function copyRecursive(src, dest) {
170
+ fs.mkdirSync(dest, { recursive: true });
171
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
172
+ if (entry.isDirectory() && COPY_SKIP_DIRS.has(entry.name))
173
+ continue;
174
+ const s = path.join(src, entry.name);
175
+ const d = path.join(dest, entry.name);
176
+ if (entry.isSymbolicLink()) {
177
+ try {
178
+ fs.symlinkSync(fs.readlinkSync(s), d);
179
+ }
180
+ catch { /* best-effort */ }
181
+ }
182
+ else if (entry.isDirectory()) {
183
+ copyRecursive(s, d);
184
+ }
185
+ else if (entry.isFile()) {
186
+ fs.copyFileSync(s, d);
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * Create an isolated worktree for `workDir`, choosing the best backend
192
+ * automatically:
193
+ * 1. A project-supplied `.nexrall/hooks` override for 'create', if defined
194
+ * (works for ANY VCS, or a fully custom isolation scheme).
195
+ * 2. Git-native `git worktree add`, if workDir is inside a git repo.
196
+ * 3. Plain recursive copy (skips COPY_SKIP_DIRS), as a zero-config
197
+ * fallback that always works.
198
+ */
199
+ function createWorktree(workDir, requestedName) {
200
+ const name = safeName(requestedName?.trim() || randomWorktreeName());
201
+ const root = path.join(workDir, WORKTREE_ROOT_DIRNAME);
202
+ const worktreePath = path.join(root, name);
203
+ if (fs.existsSync(worktreePath)) {
204
+ return { ok: false, error: `A worktree named "${name}" already exists at ${worktreePath}.` };
205
+ }
206
+ // 1. Project hook takes priority — even in a git repo, a project may want
207
+ // VCS-specific behaviour (e.g. Perforce) instead of the git-native path.
208
+ const hookEnv = { NEXRALL_WORKTREE_NAME: name, NEXRALL_WORKTREE_PATH: worktreePath, NEXRALL_MAIN_CHECKOUT: workDir };
209
+ const hook = runWorktreeHook(workDir, 'create', hookEnv);
210
+ if (hook.ran) {
211
+ if (!hook.ok)
212
+ return { ok: false, error: `worktree-create hook failed: ${hook.output ?? 'unknown error'}` };
213
+ if (!fs.existsSync(worktreePath)) {
214
+ return { ok: false, error: `worktree-create hook exited 0 but did not create ${worktreePath}.` };
215
+ }
216
+ const state = { worktreePath, mainCheckout: workDir, backend: 'hook', name };
217
+ writeMetadata(state);
218
+ return { ok: true, state };
219
+ }
220
+ // 2. Git-native.
221
+ const git = isGitRepo(workDir);
222
+ if (git.ok && git.root) {
223
+ fs.mkdirSync(root, { recursive: true });
224
+ const branch = `worktree-${name}`;
225
+ const r = (0, child_process_1.spawnSync)('git', ['worktree', 'add', '-b', branch, worktreePath], { cwd: git.root, encoding: 'utf-8', timeout: 30000 });
226
+ if (r.status !== 0) {
227
+ return { ok: false, error: `git worktree add failed: ${(r.stderr ?? r.stdout ?? '').trim() || 'unknown error'}` };
228
+ }
229
+ const state = { worktreePath, mainCheckout: workDir, backend: 'git', branch, name };
230
+ writeMetadata(state);
231
+ return { ok: true, state };
232
+ }
233
+ // 3. Zero-config copy fallback — always works, no git and no hook required.
234
+ try {
235
+ copyRecursive(workDir, worktreePath);
236
+ const state = { worktreePath, mainCheckout: workDir, backend: 'copy', name };
237
+ writeMetadata(state);
238
+ return { ok: true, state };
239
+ }
240
+ catch (err) {
241
+ return { ok: false, error: `Failed to create copy-based worktree: ${err.message}` };
242
+ }
243
+ }
244
+ /** Whether a worktree has anything in it worth not silently discarding. */
245
+ function worktreeHasWork(state) {
246
+ if (state.backend === 'git') {
247
+ try {
248
+ const status = (0, child_process_1.spawnSync)('git', ['status', '--porcelain'], {
249
+ cwd: state.worktreePath, encoding: 'utf-8', timeout: 10000,
250
+ });
251
+ // Filter our own bookkeeping file out of the porcelain output before
252
+ // deciding — see writeMetadata's comment on why this is filtered here
253
+ // rather than via `.git/info/exclude` (which is shared repo-wide and
254
+ // would leak into the main checkout's status).
255
+ const meaningfulLines = (status.stdout ?? '')
256
+ .split('\n')
257
+ .filter((line) => line.trim().length > 0 && !line.includes(METADATA_FILENAME));
258
+ if (meaningfulLines.length > 0)
259
+ return true;
260
+ // Also check for unpushed commits on the worktree's own branch.
261
+ const unpushed = (0, child_process_1.spawnSync)('git', ['log', '@{u}..HEAD', '--oneline'], { cwd: state.worktreePath, encoding: 'utf-8', timeout: 10000 });
262
+ if (unpushed.status === 0 && unpushed.stdout && unpushed.stdout.trim().length > 0)
263
+ return true;
264
+ return false;
265
+ }
266
+ catch {
267
+ return true; // can't verify — be conservative like Claude Code's own docs describe
268
+ }
269
+ }
270
+ // Copy-based / hook-based: can't diff against an origin, so any content at
271
+ // all counts as "has work" — conservative on purpose (see Claude Code's
272
+ // "worktree's state can't be verified" case: it prompts rather than assumes clean).
273
+ // Our own bookkeeping file doesn't count — a freshly created worktree with
274
+ // nothing done in it yet must still read as clean.
275
+ try {
276
+ return fs.readdirSync(state.worktreePath).some((f) => f !== METADATA_FILENAME);
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ }
282
+ function removeWorktree(state, opts = {}) {
283
+ if (!opts.force && worktreeHasWork(state)) {
284
+ return { ok: false, hadWork: true };
285
+ }
286
+ const hookEnv = { NEXRALL_WORKTREE_NAME: state.name, NEXRALL_WORKTREE_PATH: state.worktreePath, NEXRALL_MAIN_CHECKOUT: state.mainCheckout };
287
+ if (state.backend === 'hook') {
288
+ const hook = runWorktreeHook(state.mainCheckout, 'exit', hookEnv);
289
+ if (hook.ran && !hook.ok)
290
+ return { ok: false, error: `worktree-exit hook failed: ${hook.output ?? 'unknown error'}` };
291
+ // A hook backend is responsible for its own directory removal during 'exit'.
292
+ // If it left the directory in place, that's the hook's choice to make (it
293
+ // may want to keep worktrees around for its own reasons) — don't second-guess it.
294
+ return { ok: true };
295
+ }
296
+ if (state.backend === 'git') {
297
+ // worktreeHasWork (checked above) already treats our own bookkeeping file
298
+ // as invisible, but `git worktree remove` itself does its OWN untracked-
299
+ // file check and does not know about that exception — it would refuse a
300
+ // "clean" (per worktreeHasWork) worktree purely because our metadata
301
+ // file is untracked. Remove it first: by this point it has served its
302
+ // only purpose (letting a fresh process reconstruct this WorktreeState),
303
+ // and the worktree itself is one syscall from being deleted anyway.
304
+ try {
305
+ fs.unlinkSync(metadataPath(state.worktreePath));
306
+ }
307
+ catch { /* fine if already gone */ }
308
+ const r = (0, child_process_1.spawnSync)('git', ['worktree', 'remove', state.worktreePath, ...(opts.force ? ['--force'] : [])], { cwd: state.mainCheckout, encoding: 'utf-8', timeout: 30000 });
309
+ if (r.status !== 0) {
310
+ return { ok: false, error: `git worktree remove failed: ${(r.stderr ?? r.stdout ?? '').trim() || 'unknown error'}` };
311
+ }
312
+ // Also drop the branch — an unnamed worktree's branch is disposable once
313
+ // its worktree is gone (mirrors Claude Code's automatic cleanup for
314
+ // unnamed sessions). Best-effort: a shared/renamed branch failing to
315
+ // delete is not worth surfacing as an error.
316
+ if (state.branch) {
317
+ (0, child_process_1.spawnSync)('git', ['branch', '-D', state.branch], { cwd: state.mainCheckout, encoding: 'utf-8', timeout: 10000 });
318
+ }
319
+ return { ok: true };
320
+ }
321
+ // Copy-based.
322
+ try {
323
+ fs.rmSync(state.worktreePath, { recursive: true, force: true });
324
+ return { ok: true };
325
+ }
326
+ catch (err) {
327
+ return { ok: false, error: err.message };
328
+ }
329
+ }
330
+ /**
331
+ * Called when a session ENTERS an already-created worktree (including
332
+ * re-entering across a resume). Fires the project's 'enter' hook if defined
333
+ * — informational only for the git/copy backends (nothing to actually do:
334
+ * the directory already exists and is already the session's cwd), but
335
+ * REQUIRED for the 'hook' backend, which may need to check out/refresh
336
+ * VCS-specific state each time.
337
+ */
338
+ function enterWorktree(state) {
339
+ if (state.backend !== 'hook')
340
+ return { ok: true };
341
+ const hookEnv = { NEXRALL_WORKTREE_NAME: state.name, NEXRALL_WORKTREE_PATH: state.worktreePath, NEXRALL_MAIN_CHECKOUT: state.mainCheckout };
342
+ const hook = runWorktreeHook(state.mainCheckout, 'enter', hookEnv);
343
+ if (hook.ran && !hook.ok)
344
+ return { ok: false, error: `worktree-enter hook failed: ${hook.output ?? 'unknown error'}` };
345
+ return { ok: true };
346
+ }
347
+ function isWithin(real, ancestorReal) {
348
+ if (real === ancestorReal)
349
+ return true;
350
+ const rel = path.relative(ancestorReal, real);
351
+ // Within the ancestor iff the relative path does NOT start with '..' and is
352
+ // NOT itself absolute (path.relative returns an absolute path on Windows
353
+ // when the two inputs are on different drives, which correctly means "not
354
+ // within").
355
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
356
+ }
357
+ /**
358
+ * Resolve `p` to a real, symlink-free absolute path, even when `p` itself
359
+ * does not exist yet (e.g. a `write_file` target about to be created).
360
+ *
361
+ * A plain `fs.realpathSync` fails outright on a non-existent path, and a
362
+ * plain `path.resolve` fallback for that case is not enough on its own: on
363
+ * macOS, `/tmp` is itself a symlink to `/private/tmp`, so a target
364
+ * `/tmp/x/new-file.txt` (lexical, unresolved) and its EXISTING sibling
365
+ * directory `/tmp/x` (which realpathSync resolves to `/private/tmp/x`) would
366
+ * compare as two different roots even though they are the same directory —
367
+ * silently defeating `isWithin` for any not-yet-created path under a
368
+ * symlinked ancestor. The fix: walk up to the NEAREST ancestor that DOES
369
+ * exist, realpath just that ancestor, then re-append the non-existent
370
+ * remainder lexically (a component that doesn't exist cannot itself be a
371
+ * symlink to resolve).
372
+ */
373
+ function realOrLexical(p) {
374
+ try {
375
+ return fs.realpathSync(p);
376
+ }
377
+ catch { /* fall through — p (or a piece of it) doesn't exist yet */ }
378
+ let dir = path.dirname(p);
379
+ const tail = [path.basename(p)];
380
+ for (;;) {
381
+ try {
382
+ const realDir = fs.realpathSync(dir);
383
+ return path.join(realDir, ...tail.reverse());
384
+ }
385
+ catch {
386
+ const parent = path.dirname(dir);
387
+ if (parent === dir)
388
+ return path.resolve(p); // reached filesystem root without finding an existing ancestor
389
+ tail.push(path.basename(dir));
390
+ dir = parent;
391
+ }
392
+ }
393
+ }
394
+ /**
395
+ * True when `absPath` resolves to somewhere inside the MAIN CHECKOUT that
396
+ * isolation is meant to protect, while NOT being inside the currently active
397
+ * worktree — the check `blockOutsideWorktree` (in executor.ts) uses to
398
+ * refuse a write/bash-cwd that targets the wrong tree. This is exactly the
399
+ * check Claude Code's own worktree docs describe: "blocks an Edit, Write, or
400
+ * NotebookEdit that targets a path in the main checkout" — NOT "any path
401
+ * outside the worktree" (a write to some unrelated directory elsewhere on
402
+ * disk isn't the hazard this specific check exists to prevent; ordinary
403
+ * sandbox/permission rules still govern that separately).
404
+ *
405
+ * The git/copy backends nest the worktree INSIDE the main checkout
406
+ * (`<mainCheckout>/.nexrall/worktrees/<name>`), so "inside mainCheckout" on
407
+ * its own would wrongly flag every path inside a perfectly legitimate
408
+ * worktree — this function excludes the active worktree's own subtree first.
409
+ *
410
+ * Deliberately resolves symlinks (fs.realpathSync) so a symlink planted
411
+ * INSIDE the worktree that points back at the main checkout can't be used to
412
+ * defeat the check — the same class of bypass Claude Code's own worktree
413
+ * docs call out for git redirects.
414
+ */
415
+ function pathEscapesWorktree(absPath, state) {
416
+ const real = realOrLexical(absPath);
417
+ const worktreeReal = realOrLexical(state.worktreePath);
418
+ if (isWithin(real, worktreeReal))
419
+ return false; // inside the active worktree — never escaping
420
+ const mainReal = realOrLexical(state.mainCheckout);
421
+ return isWithin(real, mainReal); // outside the worktree AND inside the main checkout — this IS the hazard
422
+ }
423
+ /**
424
+ * Small metadata file written next to a worktree's own contents
425
+ * (`.nexrall-worktree.json` at its root) so a LATER process invocation —
426
+ * `nex --worktree <name>` in a brand-new CLI process, VS Code re-opening the
427
+ * same project, a resumed session — can reconstruct the WorktreeState
428
+ * without re-deriving `backend`/`branch` from scratch. This is what makes
429
+ * "resume into an existing worktree" possible at all: the git/copy/hook
430
+ * distinction is a property of HOW it was created, not something a fresh
431
+ * process can re-detect reliably from the directory alone (a copy-based
432
+ * worktree contains no marker of its own; nothing stops a git worktree's
433
+ * branch from being renamed after the fact).
434
+ */
435
+ const METADATA_FILENAME = '.nexrall-worktree.json';
436
+ function metadataPath(worktreePath) {
437
+ return path.join(worktreePath, METADATA_FILENAME);
438
+ }
439
+ function writeMetadata(state) {
440
+ try {
441
+ fs.writeFileSync(metadataPath(state.worktreePath), JSON.stringify(state));
442
+ // NOTE: deliberately NOT using `.git/info/exclude` to hide this file from
443
+ // git status. `info/exclude` is a REPO-WIDE file shared by every linked
444
+ // worktree (`git rev-parse --git-path info/exclude` resolves to the SAME
445
+ // path from the main checkout and from any worktree) — writing to it
446
+ // would leak this exclusion into the main checkout's own `git status`
447
+ // too, which is exactly the cross-contamination worktree isolation
448
+ // exists to prevent. Instead, worktreeHasWork (below) filters this one
449
+ // filename out of `git status --porcelain`'s output directly — no shared
450
+ // state touched, no risk to the main checkout.
451
+ }
452
+ catch { /* best-effort */ }
453
+ }
454
+ function readMetadata(worktreePath) {
455
+ try {
456
+ const raw = JSON.parse(fs.readFileSync(metadataPath(worktreePath), 'utf-8'));
457
+ if (raw && typeof raw.worktreePath === 'string' && typeof raw.mainCheckout === 'string')
458
+ return raw;
459
+ return null;
460
+ }
461
+ catch {
462
+ return null;
463
+ }
464
+ }
465
+ /**
466
+ * List every worktree registered under `workDir`'s `.nexrall/worktrees/`,
467
+ * reconstructed from each one's metadata file. Skips (rather than throws on)
468
+ * a directory with no readable metadata — e.g. one a user created by hand,
469
+ * or debris from an older version — since one bad entry must not hide every
470
+ * legitimate one from a session picker.
471
+ */
472
+ function listWorktrees(workDir) {
473
+ const root = path.join(workDir, WORKTREE_ROOT_DIRNAME);
474
+ let entries;
475
+ try {
476
+ entries = fs.readdirSync(root);
477
+ }
478
+ catch {
479
+ return [];
480
+ }
481
+ const out = [];
482
+ for (const name of entries) {
483
+ const wt = path.join(root, name);
484
+ try {
485
+ if (!fs.statSync(wt).isDirectory())
486
+ continue;
487
+ }
488
+ catch {
489
+ continue;
490
+ }
491
+ const meta = readMetadata(wt);
492
+ if (meta)
493
+ out.push(meta);
494
+ }
495
+ return out;
496
+ }
497
+ /** Find a previously created worktree by name, for resuming into it in a fresh process. */
498
+ function findWorktreeByName(workDir, name) {
499
+ return listWorktrees(workDir).find((w) => w.name === name) ?? null;
500
+ }
501
+ exports._worktreeInternal = { isGitRepo, safeName, copyRecursive, findWorktreeHook, COPY_SKIP_DIRS, WORKTREE_ROOT_DIRNAME, metadataPath };
502
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1,29 @@
1
+ import type { WorktreeState } from './worktree';
2
+ export interface WorktreeRefusal {
3
+ reason: 'edit-outside-worktree' | 'bash-cwd-outside-worktree' | 'git-redirect-outside-worktree' | 'bash-shape-unverifiable';
4
+ message: string;
5
+ }
6
+ /**
7
+ * Check ONE tool call against active worktree isolation. Returns null when
8
+ * the call is fine (either no isolation is active, or the call stays inside
9
+ * the worktree). Call this the same way checkPlanMode is called — BEFORE
10
+ * requestPermission, since no user answer at that prompt can lawfully unlock
11
+ * a session-wide isolation boundary.
12
+ */
13
+ export declare function checkWorktreeIsolation(tool: string, input: Record<string, unknown>, state: WorktreeState | undefined, workDir?: string): WorktreeRefusal | null;
14
+ declare function resolvesIntoMainCheckout(candidate: string, state: WorktreeState): boolean;
15
+ declare function checkBashAgainstWorktree(command: string, state: WorktreeState): WorktreeRefusal | null;
16
+ /**
17
+ * Prepended to nexrall.md when worktree isolation is active — same rationale
18
+ * as PLAN_MODE_INSTRUCTIONS in planMode.ts: telling the model up front is not
19
+ * the enforcement (checkWorktreeIsolation at the call site is), it exists so
20
+ * the model plans its work AROUND the boundary instead of discovering it one
21
+ * refused tool call at a time.
22
+ */
23
+ export declare function worktreeInstructions(state: WorktreeState): string;
24
+ export declare const _worktreeEnforcementInternal: {
25
+ checkBashAgainstWorktree: typeof checkBashAgainstWorktree;
26
+ resolvesIntoMainCheckout: typeof resolvesIntoMainCheckout;
27
+ };
28
+ export {};
29
+ //# sourceMappingURL=worktreeEnforcement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktreeEnforcement.d.ts","sourceRoot":"","sources":["../../src/agent/worktreeEnforcement.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,uBAAuB,GAAG,2BAA2B,GAAG,+BAA+B,GAAG,yBAAyB,CAAC;IAC5H,OAAO,EAAE,MAAM,CAAC;CACjB;AAwBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,aAAa,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,MAAM,GACf,eAAe,GAAG,IAAI,CAuBxB;AA2BD,iBAAS,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAIlF;AAED,iBAAS,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,eAAe,GAAG,IAAI,CAyC/F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAcjE;AAED,eAAO,MAAM,4BAA4B;;;CAAyD,CAAC"}