@relayfile/local-mount 0.6.14 → 0.7.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 CHANGED
@@ -14,9 +14,11 @@ npm install @relayfile/local-mount
14
14
 
15
15
  ### `createMount(projectDir, mountDir, options)`
16
16
 
17
- Builds a mounted copy of `projectDir` at `mountDir` and returns a handle:
17
+ Builds a mounted copy of `projectDir` at `mountDir` and resolves with a handle:
18
18
 
19
19
  ```ts
20
+ const handle = await createMount(projectDir, mountDir, options);
21
+
20
22
  interface MountHandle {
21
23
  mountDir: string;
22
24
  syncBack(opts?: { signal?: AbortSignal }): Promise<number>;
@@ -25,11 +27,13 @@ interface MountHandle {
25
27
  }
26
28
  ```
27
29
 
30
+ `createMount` returns `Promise<MountHandle>`. The walker yields the event loop between directory entries so consumer-side timers (e.g. an `ora` spinner driven by `setInterval`) keep firing while the mount is being built.
31
+
28
32
  Behavior:
29
33
  - Copies regular files into the mount
30
34
  - Applies ignore rules from `ignoredPatterns`
31
35
  - Marks read-only matches as mode `0o444`
32
- - Excludes `.git`, `node_modules`, and `.npm-cache` by default. Pass `includeGit: true` to opt the project's `.git` directory back in (see [Including `.git`](#including-git))
36
+ - Excludes `.git`, `node_modules`, `.npm-cache`, and common build/cache output directories by default. Pass `includeGit: true` to opt the project's `.git` directory back in (see [Including `.git`](#including-git))
33
37
  - Writes `_MOUNT_README.md` and `.relayfile-local-mount` into the mount
34
38
  - Skips syncing `_MOUNT_README.md`, `.relayfile-local-mount`, ignored files, read-only files, and symlinks back to the source project
35
39
 
@@ -110,6 +114,47 @@ Conflict and delete rules:
110
114
  - readonly paths never flow mount→project; project-side edits still flow into the mount (the mount copy is re-chmodded `0o444`)
111
115
  - `_MOUNT_README.md`, `.relayfile-local-mount`, ignored paths, and excluded directories never cross
112
116
 
117
+ ## Default Excludes
118
+
119
+ By default, mounts skip directories and files that are usually large generated output
120
+ or local caches. These names match at any path depth:
121
+
122
+ ```txt
123
+ .git
124
+ node_modules
125
+ .npm-cache
126
+ __pycache__
127
+ .pytest_cache
128
+ .mypy_cache
129
+ .ruff_cache
130
+ .gradle
131
+ .nyc_output
132
+ .turbo
133
+ .cache
134
+ .DS_Store
135
+ ```
136
+
137
+ These more generic names match only at the project root so source paths such as
138
+ `src/build/` or `packages/env/` are still mounted:
139
+
140
+ ```txt
141
+ target
142
+ .next
143
+ dist
144
+ build
145
+ out
146
+ .venv
147
+ venv
148
+ env
149
+ coverage
150
+ ```
151
+
152
+ Pass `includeDefaultExcludeDirs: false` to opt out of the broad build/cache list.
153
+ For safety, `.git` stays excluded unless you also pass `includeGit: true`.
154
+ `excludeDirs` still appends to whichever default set is active; bare caller entries
155
+ retain the historical any-depth behavior, while path-style entries are root-relative
156
+ prefixes.
157
+
113
158
  ## Including `.git`
114
159
 
115
160
  By default, the project's `.git` directory is excluded from the mount, which means git commands inside the mount fail with `fatal: not a git repository`. Pass `includeGit: true` (on `createMount` or `launchOnMount`) to copy `.git` into the mount with **one-way project→mount sync**:
@@ -192,7 +237,7 @@ const result = await launchOnMount({
192
237
  mountDir,
193
238
  ignoredPatterns,
194
239
  readonlyPatterns,
195
- excludeDirs: ['dist'],
240
+ excludeDirs: ['vendor-cache'],
196
241
  agentName: 'reviewer',
197
242
  onBeforeLaunch: async (dir) => {
198
243
  // Add extra instructions or scratch files inside the mount if needed.
@@ -3,14 +3,17 @@ export interface AutoSyncContext {
3
3
  realProjectDir: string;
4
4
  isExcluded: (relPosix: string) => boolean;
5
5
  /**
6
- * The exact set of normalized directory names/prefixes that drive
7
- * `isExcluded` (i.e., {@link MountOptions.excludeDirs} merged with the
8
- * library defaults). Used purely to hint `@parcel/watcher` which subtrees
9
- * to skip subscribing to. The in-handler `isSyncCandidate` filter remains
10
- * authoritative; this is just a perf hint that keeps the watcher from
11
- * recursing into heavy trees like `node_modules` or `.npm-cache`.
6
+ * Normalized directory names that drive any-depth `isExcluded` matches.
7
+ * Used purely to hint `@parcel/watcher` which subtrees to skip subscribing
8
+ * to. The in-handler `isSyncCandidate` filter remains authoritative.
12
9
  */
13
- excludedNames: readonly string[];
10
+ excludedAnyDepthNames: readonly string[];
11
+ /**
12
+ * Root-anchored excluded names/prefixes such as `build` or `packages/cache`.
13
+ * These are matched only from the watch root to avoid hiding legitimate
14
+ * nested source directories like `src/build`.
15
+ */
16
+ excludedRootPrefixes: readonly string[];
14
17
  /**
15
18
  * Directory-only ignore patterns (ending in `/`) must only match when the
16
19
  * path is a directory. Callers that know the path's type pass `isDirectory`;
package/dist/auto-sync.js CHANGED
@@ -170,23 +170,21 @@ function buildIgnoreGlobs(ctx, watchRoot) {
170
170
  // `isExcludedPath`'s semantics, so a watcher-suppressed event never differs
171
171
  // from what the in-handler filter would have rejected.
172
172
  //
173
- // - Bare directory names (e.g. `node_modules`) match at any depth, so
174
- // emit `**/<name>` plus `**/<name>/**`. picomatch turns both into
175
- // depth-agnostic regexes that catch the dir and its descendants.
176
- // - Path-style entries (e.g. `build/cache`) are root-anchored prefixes
173
+ // - Any-depth names (e.g. `node_modules`) emit `**/<name>` plus
174
+ // `**/<name>/**`. picomatch turns both into depth-agnostic regexes
175
+ // that catch the dir and its descendants.
176
+ // - Root prefixes (e.g. `build` or `build/cache`) are root-anchored
177
177
  // in `isExcludedPath` — they only match `<root>/build/cache`, NOT
178
178
  // `<root>/src/build/cache`. Emit absolute patterns rooted at the
179
179
  // watch dir so the watcher hides the same set: a literal absolute
180
180
  // path (which the wrapper routes to ignorePaths) plus an anchored
181
181
  // descendant glob.
182
182
  const globs = [];
183
- for (const name of ctx.excludedNames) {
184
- if (name.includes('/')) {
185
- globs.push(`${watchRoot}/${name}`, `${watchRoot}/${name}/**`);
186
- }
187
- else {
188
- globs.push(`**/${name}`, `**/${name}/**`);
189
- }
183
+ for (const name of ctx.excludedAnyDepthNames) {
184
+ globs.push(`**/${name}`, `**/${name}/**`);
185
+ }
186
+ for (const prefix of ctx.excludedRootPrefixes) {
187
+ globs.push(`${watchRoot}/${prefix}`, `${watchRoot}/${prefix}/**`);
190
188
  }
191
189
  return globs;
192
190
  }
package/dist/launch.d.ts CHANGED
@@ -14,6 +14,11 @@ export interface LaunchOnMountOptions {
14
14
  readonlyPatterns?: string[];
15
15
  /** Extra directory names to exclude from the mount on top of defaults. */
16
16
  excludeDirs?: string[];
17
+ /**
18
+ * Include the built-in cache/build exclusion list. Defaults to true. `.git`
19
+ * remains excluded unless `includeGit` is true.
20
+ */
21
+ includeDefaultExcludeDirs?: boolean;
17
22
  /**
18
23
  * Include the project's `.git` directory inside the mount with one-way
19
24
  * project→mount sync. Defaults to false. See {@link MountOptions.includeGit}
package/dist/launch.js CHANGED
@@ -7,13 +7,7 @@ import { createMount } from './mount.js';
7
7
  * exit code.
8
8
  */
9
9
  export async function launchOnMount(opts) {
10
- const handle = createMount(opts.projectDir, opts.mountDir, {
11
- ignoredPatterns: opts.ignoredPatterns ?? [],
12
- readonlyPatterns: opts.readonlyPatterns ?? [],
13
- excludeDirs: opts.excludeDirs ?? [],
14
- agentName: opts.agentName,
15
- includeGit: opts.includeGit,
16
- });
10
+ let handle;
17
11
  let syncedCount = 0;
18
12
  let finalized = false;
19
13
  let autoSync;
@@ -28,6 +22,8 @@ export async function launchOnMount(opts) {
28
22
  autoSyncChanges = autoSync.totalChanges();
29
23
  autoSync = undefined;
30
24
  }
25
+ if (!handle)
26
+ return;
31
27
  const finalSynced = await handle.syncBack({ signal: opts.shutdownSignal });
32
28
  syncedCount = autoSyncChanges + finalSynced;
33
29
  if (opts.onAfterSync) {
@@ -35,10 +31,18 @@ export async function launchOnMount(opts) {
35
31
  }
36
32
  }
37
33
  finally {
38
- handle.cleanup();
34
+ handle?.cleanup();
39
35
  }
40
36
  };
41
37
  try {
38
+ handle = await createMount(opts.projectDir, opts.mountDir, {
39
+ ignoredPatterns: opts.ignoredPatterns ?? [],
40
+ readonlyPatterns: opts.readonlyPatterns ?? [],
41
+ excludeDirs: opts.excludeDirs ?? [],
42
+ agentName: opts.agentName,
43
+ includeGit: opts.includeGit,
44
+ includeDefaultExcludeDirs: opts.includeDefaultExcludeDirs,
45
+ });
42
46
  if (opts.onBeforeLaunch) {
43
47
  await opts.onBeforeLaunch(handle.mountDir);
44
48
  }
@@ -50,9 +54,10 @@ export async function launchOnMount(opts) {
50
54
  ...process.env,
51
55
  ...(opts.env ?? {}),
52
56
  };
57
+ const mountCwd = handle.mountDir;
53
58
  const exitCode = await new Promise((resolve, reject) => {
54
59
  const child = spawn(opts.cli, opts.args, {
55
- cwd: handle.mountDir,
60
+ cwd: mountCwd,
56
61
  stdio: 'inherit',
57
62
  env: envVars,
58
63
  });
package/dist/mount.d.ts CHANGED
@@ -21,6 +21,12 @@ export interface MountOptions {
21
21
  * are discarded with the mount on cleanup. Push to a remote to keep them.
22
22
  */
23
23
  includeGit?: boolean;
24
+ /**
25
+ * Include the built-in list of large cache/build output directories in
26
+ * the mount exclusion set. Default: true. The `.git` directory remains
27
+ * excluded unless `includeGit` is true, even when this is false.
28
+ */
29
+ includeDefaultExcludeDirs?: boolean;
24
30
  }
25
31
  export interface MountHandle {
26
32
  mountDir: string;
@@ -35,4 +41,4 @@ export interface MountHandle {
35
41
  startAutoSync(opts?: AutoSyncOptions): AutoSyncHandle;
36
42
  cleanup(): void;
37
43
  }
38
- export declare function createMount(projectDir: string, mountDir: string, options: MountOptions): MountHandle;
44
+ export declare function createMount(projectDir: string, mountDir: string, options: MountOptions): Promise<MountHandle>;
package/dist/mount.js CHANGED
@@ -2,11 +2,35 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync,
2
2
  import ignore from 'ignore';
3
3
  import path from 'node:path';
4
4
  import { startAutoSync, } from './auto-sync.js';
5
- const DEFAULT_EXCLUDED_DIRS = ['.git', 'node_modules', '.npm-cache'];
5
+ const DEFAULT_ANY_DEPTH_EXCLUDES = [
6
+ '.git',
7
+ 'node_modules',
8
+ '.npm-cache',
9
+ '__pycache__',
10
+ '.pytest_cache',
11
+ '.mypy_cache',
12
+ '.ruff_cache',
13
+ '.gradle',
14
+ '.nyc_output',
15
+ '.turbo',
16
+ '.cache',
17
+ '.DS_Store',
18
+ ];
19
+ const DEFAULT_ROOT_EXCLUDES = [
20
+ 'target',
21
+ '.next',
22
+ 'dist',
23
+ 'build',
24
+ 'out',
25
+ '.venv',
26
+ 'venv',
27
+ 'env',
28
+ 'coverage',
29
+ ];
6
30
  const MOUNT_README_FILENAME = '_MOUNT_README.md';
7
31
  const MOUNT_MARKER_FILENAME = '.relayfile-local-mount';
8
32
  const MOUNT_MARKER_CONTENT = 'This directory is managed by @relayfile/local-mount. Do not place unrelated files here; the directory will be deleted when the mount is torn down.\n';
9
- export function createMount(projectDir, mountDir, options) {
33
+ export async function createMount(projectDir, mountDir, options) {
10
34
  const resolvedProjectDir = realpathSync(projectDir);
11
35
  const resolvedMountDir = path.resolve(mountDir);
12
36
  const readonlyPatterns = [...options.readonlyPatterns];
@@ -14,16 +38,12 @@ export function createMount(projectDir, mountDir, options) {
14
38
  const includeGit = options.includeGit === true;
15
39
  const readonlyMatcher = createPathMatcher(readonlyPatterns);
16
40
  const ignoredMatcher = createPathMatcher(ignoredPatterns);
17
- // `.git` is in DEFAULT_EXCLUDED_DIRS so the mount stays small and git
41
+ const includeDefaultExcludeDirs = options.includeDefaultExcludeDirs !== false;
42
+ // `.git` is in the default any-depth excludes so the mount stays small and git
18
43
  // operations don't accidentally cross-mutate the host repo. When the caller
19
44
  // opts in via `includeGit`, drop it from the defaults and instead route it
20
45
  // through the noSyncBack matcher below so it stays one-way.
21
- const defaultExcludes = includeGit
22
- ? DEFAULT_EXCLUDED_DIRS.filter((d) => d !== '.git')
23
- : DEFAULT_EXCLUDED_DIRS;
24
- const excludeSet = new Set([...defaultExcludes, ...options.excludeDirs]
25
- .map((entry) => normalizeRelativePosix(entry).replace(/^\/+|\/+$/g, ''))
26
- .filter(Boolean));
46
+ const excludeRules = createExcludeRules(options.excludeDirs, includeGit, includeDefaultExcludeDirs);
27
47
  const noSyncBackPatterns = includeGit ? ['.git', '.git/**'] : [];
28
48
  const noSyncBackMatcher = createPathMatcher(noSyncBackPatterns);
29
49
  // Guard against mountDir === projectDir. We compare both the realpath'd
@@ -40,7 +60,7 @@ export function createMount(projectDir, mountDir, options) {
40
60
  mkdirSync(resolvedMountDir, { recursive: true });
41
61
  const realMountDir = realpathSync(resolvedMountDir);
42
62
  writeFileSync(path.join(realMountDir, MOUNT_MARKER_FILENAME), MOUNT_MARKER_CONTENT, 'utf8');
43
- walkProjectTree(resolvedProjectDir, resolvedProjectDir, realMountDir, excludeSet, readonlyMatcher, ignoredMatcher);
63
+ await walkProjectTree(resolvedProjectDir, resolvedProjectDir, realMountDir, realMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
44
64
  const readmePath = resolveSafeCopyTarget(realMountDir, path.join(realMountDir, MOUNT_README_FILENAME));
45
65
  if (!readmePath) {
46
66
  throw new Error('Failed to create mount readme inside mountDir');
@@ -49,8 +69,9 @@ export function createMount(projectDir, mountDir, options) {
49
69
  const autoSyncContext = {
50
70
  realMountDir,
51
71
  realProjectDir: resolvedProjectDir,
52
- isExcluded: (relPosix) => isExcludedPath(relPosix, excludeSet),
53
- excludedNames: [...excludeSet],
72
+ isExcluded: (relPosix) => isExcludedPath(relPosix, excludeRules),
73
+ excludedAnyDepthNames: [...excludeRules.anyDepthNames],
74
+ excludedRootPrefixes: [...excludeRules.rootPrefixes],
54
75
  isIgnored: (relPosix, isDir) => isPathMatched(relPosix, ignoredMatcher, isDir),
55
76
  isReadonly: (relPosix) => isPathMatched(relPosix, readonlyMatcher),
56
77
  isNoSyncBack: (relPosix) => isPathMatched(relPosix, noSyncBackMatcher),
@@ -68,7 +89,7 @@ export function createMount(projectDir, mountDir, options) {
68
89
  if (signal?.aborted) {
69
90
  break;
70
91
  }
71
- const syncedForFile = syncMountedFileBack(sourceFile, realMountDir, realProjectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher);
92
+ const syncedForFile = syncMountedFileBack(sourceFile, realMountDir, realProjectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, (relPosix) => isExcludedPath(relPosix, excludeRules));
72
93
  synced += syncedForFile;
73
94
  if (signal && syncedForFile > 0 && !signal.aborted) {
74
95
  await new Promise((resolve) => setImmediate(resolve));
@@ -122,9 +143,19 @@ function assertMountDirSafeToRemove(mountDir, projectDir) {
122
143
  `Only directories previously created by createMount can be reused as mountDir.`);
123
144
  }
124
145
  }
125
- function walkProjectTree(projectDir, currentDir, mountDir, excludeSet, readonlyMatcher, ignoredMatcher) {
146
+ // Yield often enough that a consumer's setInterval (e.g. an `ora` spinner) can
147
+ // tick during init even on flat directories with thousands of entries. The
148
+ // goal is not throughput; it is keeping the parent event loop unblocked.
149
+ const WALK_YIELD_EVERY = 64;
150
+ async function walkProjectTree(projectDir, currentDir, mountDir, currentMountDir, excludeRules, readonlyMatcher, ignoredMatcher) {
151
+ await yieldToEventLoop();
126
152
  const entries = readdirSync(currentDir, { withFileTypes: true });
153
+ let processed = 0;
127
154
  for (const entry of entries) {
155
+ if (processed > 0 && processed % WALK_YIELD_EVERY === 0) {
156
+ await yieldToEventLoop();
157
+ }
158
+ processed += 1;
128
159
  const absolutePath = path.join(currentDir, entry.name);
129
160
  const relativePath = normalizeRelativePosix(path.relative(projectDir, absolutePath));
130
161
  if (!relativePath || relativePath.startsWith('..')) {
@@ -133,18 +164,19 @@ function walkProjectTree(projectDir, currentDir, mountDir, excludeSet, readonlyM
133
164
  if (isPathWithinRoot(absolutePath, mountDir)) {
134
165
  continue;
135
166
  }
136
- if (isExcludedPath(relativePath, excludeSet)) {
167
+ if (isExcludedPath(relativePath, excludeRules)) {
137
168
  continue;
138
169
  }
139
170
  if (isPathMatched(relativePath, ignoredMatcher, entry.isDirectory())) {
140
171
  continue;
141
172
  }
142
- const mountPath = path.join(mountDir, relativePath);
173
+ const mountPath = path.join(currentMountDir, entry.name);
143
174
  if (entry.isDirectory()) {
144
- if (!ensureDirectoryWithinRoot(mountDir, mountPath)) {
175
+ const safeMountDir = ensureDirectoryWithinRoot(mountDir, mountPath);
176
+ if (!safeMountDir) {
145
177
  continue;
146
178
  }
147
- walkProjectTree(projectDir, absolutePath, mountDir, excludeSet, readonlyMatcher, ignoredMatcher);
179
+ await walkProjectTree(projectDir, absolutePath, mountDir, safeMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
148
180
  continue;
149
181
  }
150
182
  if (entry.isSymbolicLink()) {
@@ -157,6 +189,9 @@ function walkProjectTree(projectDir, currentDir, mountDir, excludeSet, readonlyM
157
189
  copyMountedFile(projectDir, mountDir, absolutePath, mountPath, relativePath, readonlyMatcher);
158
190
  }
159
191
  }
192
+ function yieldToEventLoop() {
193
+ return new Promise((resolve) => setImmediate(resolve));
194
+ }
160
195
  function copySymlinkedFile(projectDir, mountDir, sourcePath, mountPath, relativePath, readonlyMatcher) {
161
196
  let realSource;
162
197
  let resolvedStat;
@@ -194,15 +229,15 @@ function ensureDirectory(pathValue) {
194
229
  }
195
230
  function ensureDirectoryWithinRoot(rootPath, dirPath) {
196
231
  if (!isPathWithinRoot(dirPath, rootPath)) {
197
- return false;
232
+ return null;
198
233
  }
199
234
  try {
200
235
  ensureDirectory(dirPath);
201
236
  const realDir = realpathSync(dirPath);
202
- return isPathWithinRoot(realDir, rootPath);
237
+ return isPathWithinRoot(realDir, rootPath) ? realDir : null;
203
238
  }
204
239
  catch {
205
- return false;
240
+ return null;
206
241
  }
207
242
  }
208
243
  function listFiles(baseDir) {
@@ -231,6 +266,38 @@ function normalizeRelativePosix(filePath) {
231
266
  function createPathMatcher(patterns) {
232
267
  return ignore().add(patterns.map((pattern) => pattern.trim()).filter((pattern) => pattern !== '' && !pattern.startsWith('#')));
233
268
  }
269
+ function createExcludeRules(excludeDirs, includeGit, includeDefaultExcludeDirs) {
270
+ const anyDepthNames = new Set();
271
+ const rootPrefixes = new Set();
272
+ if (includeDefaultExcludeDirs) {
273
+ addExcludeEntries(anyDepthNames, rootPrefixes, DEFAULT_ANY_DEPTH_EXCLUDES, 'any-depth');
274
+ addExcludeEntries(anyDepthNames, rootPrefixes, DEFAULT_ROOT_EXCLUDES, 'root-prefix');
275
+ }
276
+ else if (!includeGit) {
277
+ addExcludeEntries(anyDepthNames, rootPrefixes, ['.git'], 'any-depth');
278
+ }
279
+ if (includeGit) {
280
+ anyDepthNames.delete('.git');
281
+ }
282
+ // Preserve caller-supplied excludeDirs semantics: bare names match at any
283
+ // depth, while path-style entries are root-anchored prefixes.
284
+ addExcludeEntries(anyDepthNames, rootPrefixes, excludeDirs, 'legacy');
285
+ return { anyDepthNames, rootPrefixes };
286
+ }
287
+ function addExcludeEntries(anyDepthNames, rootPrefixes, entries, mode) {
288
+ for (const entry of entries) {
289
+ const normalized = normalizeRelativePosix(entry).replace(/^\/+|\/+$/g, '');
290
+ if (!normalized) {
291
+ continue;
292
+ }
293
+ if (mode === 'root-prefix' || (mode === 'legacy' && normalized.includes('/'))) {
294
+ rootPrefixes.add(normalized);
295
+ }
296
+ else {
297
+ anyDepthNames.add(normalized);
298
+ }
299
+ }
300
+ }
234
301
  function isPathMatched(relPath, matcher, isDirectory = false) {
235
302
  const normalized = normalizeRelativePosix(relPath);
236
303
  return matcher.ignores(normalized) || (isDirectory && matcher.ignores(`${normalized}/`));
@@ -254,8 +321,8 @@ function hasSameContent(left, right) {
254
321
  return false;
255
322
  }
256
323
  }
257
- function syncMountedFileBack(sourceFile, mountDir, projectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher) {
258
- const relative = resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher);
324
+ function syncMountedFileBack(sourceFile, mountDir, projectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, isExcluded) {
325
+ const relative = resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, isExcluded);
259
326
  if (!relative)
260
327
  return 0;
261
328
  const safeTargetPath = resolveVerifiedSyncTarget(projectDir, relative);
@@ -267,7 +334,7 @@ function syncMountedFileBack(sourceFile, mountDir, projectDir, readonlyMatcher,
267
334
  copyFileSync(sourceFile, safeTargetPath);
268
335
  return 1;
269
336
  }
270
- function resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher) {
337
+ function resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, isExcluded) {
271
338
  const relative = path.relative(mountDir, sourceFile);
272
339
  if (relative === '' || relative.startsWith('..'))
273
340
  return null;
@@ -276,7 +343,8 @@ function resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredM
276
343
  return null;
277
344
  if (relativePosix === MOUNT_MARKER_FILENAME)
278
345
  return null;
279
- if (isPathMatched(relative, readonlyMatcher) ||
346
+ if (isExcluded(relativePosix) ||
347
+ isPathMatched(relative, readonlyMatcher) ||
280
348
  isPathMatched(relative, ignoredMatcher) ||
281
349
  isPathMatched(relative, noSyncBackMatcher))
282
350
  return null;
@@ -311,14 +379,14 @@ function resolveVerifiedSyncTarget(projectDir, relativePath) {
311
379
  return null;
312
380
  }
313
381
  }
314
- function isExcludedPath(relativePath, excludeSet) {
382
+ function isExcludedPath(relativePath, excludeRules) {
315
383
  const normalized = normalizeRelativePosix(relativePath).replace(/^\/+|\/+$/g, '');
316
384
  if (!normalized)
317
385
  return false;
318
386
  const segments = normalized.split('/');
319
387
  return segments.some((segment, index) => {
320
388
  const prefix = segments.slice(0, index + 1).join('/');
321
- return excludeSet.has(segment) || excludeSet.has(prefix);
389
+ return excludeRules.anyDepthNames.has(segment) || excludeRules.rootPrefixes.has(prefix);
322
390
  });
323
391
  }
324
392
  function isPathWithinRoot(candidatePath, rootPath) {
@@ -331,19 +399,11 @@ function resolveSafeCopyTarget(rootPath, candidatePath) {
331
399
  return null;
332
400
  }
333
401
  const parentPath = path.dirname(candidatePath);
334
- if (!ensureDirectoryWithinRoot(rootPath, parentPath)) {
335
- return null;
336
- }
337
- try {
338
- const realParent = realpathSync(parentPath);
339
- if (!isPathWithinRoot(realParent, rootPath)) {
340
- return null;
341
- }
342
- return path.join(realParent, path.basename(candidatePath));
343
- }
344
- catch {
402
+ const realParent = ensureDirectoryWithinRoot(rootPath, parentPath);
403
+ if (!realParent) {
345
404
  return null;
346
405
  }
406
+ return path.join(realParent, path.basename(candidatePath));
347
407
  }
348
408
  function resolveVerifiedFilePath(rootPath, candidatePath) {
349
409
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/local-mount",
3
- "version": "0.6.14",
3
+ "version": "0.7.0",
4
4
  "description": "Create a symlink/copy mount of a project directory with .agentignore/.agentreadonly semantics, then launch a CLI inside it and sync writable changes back on exit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",