@relayfile/local-mount 0.6.13 → 0.6.15
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 +43 -2
- package/dist/auto-sync.d.ts +12 -0
- package/dist/auto-sync.js +23 -15
- package/dist/launch.d.ts +5 -0
- package/dist/launch.js +1 -0
- package/dist/mount.d.ts +6 -0
- package/dist/mount.js +84 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Behavior:
|
|
|
29
29
|
- Copies regular files into the mount
|
|
30
30
|
- Applies ignore rules from `ignoredPatterns`
|
|
31
31
|
- Marks read-only matches as mode `0o444`
|
|
32
|
-
- Excludes `.git` and
|
|
32
|
+
- 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
33
|
- Writes `_MOUNT_README.md` and `.relayfile-local-mount` into the mount
|
|
34
34
|
- Skips syncing `_MOUNT_README.md`, `.relayfile-local-mount`, ignored files, read-only files, and symlinks back to the source project
|
|
35
35
|
|
|
@@ -110,6 +110,47 @@ Conflict and delete rules:
|
|
|
110
110
|
- readonly paths never flow mount→project; project-side edits still flow into the mount (the mount copy is re-chmodded `0o444`)
|
|
111
111
|
- `_MOUNT_README.md`, `.relayfile-local-mount`, ignored paths, and excluded directories never cross
|
|
112
112
|
|
|
113
|
+
## Default Excludes
|
|
114
|
+
|
|
115
|
+
By default, mounts skip directories and files that are usually large generated output
|
|
116
|
+
or local caches. These names match at any path depth:
|
|
117
|
+
|
|
118
|
+
```txt
|
|
119
|
+
.git
|
|
120
|
+
node_modules
|
|
121
|
+
.npm-cache
|
|
122
|
+
__pycache__
|
|
123
|
+
.pytest_cache
|
|
124
|
+
.mypy_cache
|
|
125
|
+
.ruff_cache
|
|
126
|
+
.gradle
|
|
127
|
+
.nyc_output
|
|
128
|
+
.turbo
|
|
129
|
+
.cache
|
|
130
|
+
.DS_Store
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
These more generic names match only at the project root so source paths such as
|
|
134
|
+
`src/build/` or `packages/env/` are still mounted:
|
|
135
|
+
|
|
136
|
+
```txt
|
|
137
|
+
target
|
|
138
|
+
.next
|
|
139
|
+
dist
|
|
140
|
+
build
|
|
141
|
+
out
|
|
142
|
+
.venv
|
|
143
|
+
venv
|
|
144
|
+
env
|
|
145
|
+
coverage
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Pass `includeDefaultExcludeDirs: false` to opt out of the broad build/cache list.
|
|
149
|
+
For safety, `.git` stays excluded unless you also pass `includeGit: true`.
|
|
150
|
+
`excludeDirs` still appends to whichever default set is active; bare caller entries
|
|
151
|
+
retain the historical any-depth behavior, while path-style entries are root-relative
|
|
152
|
+
prefixes.
|
|
153
|
+
|
|
113
154
|
## Including `.git`
|
|
114
155
|
|
|
115
156
|
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 +233,7 @@ const result = await launchOnMount({
|
|
|
192
233
|
mountDir,
|
|
193
234
|
ignoredPatterns,
|
|
194
235
|
readonlyPatterns,
|
|
195
|
-
excludeDirs: ['
|
|
236
|
+
excludeDirs: ['vendor-cache'],
|
|
196
237
|
agentName: 'reviewer',
|
|
197
238
|
onBeforeLaunch: async (dir) => {
|
|
198
239
|
// Add extra instructions or scratch files inside the mount if needed.
|
package/dist/auto-sync.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ export interface AutoSyncContext {
|
|
|
2
2
|
realMountDir: string;
|
|
3
3
|
realProjectDir: string;
|
|
4
4
|
isExcluded: (relPosix: string) => boolean;
|
|
5
|
+
/**
|
|
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.
|
|
9
|
+
*/
|
|
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[];
|
|
5
17
|
/**
|
|
6
18
|
* Directory-only ignore patterns (ending in `/`) must only match when the
|
|
7
19
|
* path is a directory. Callers that know the path's type pass `isDirectory`;
|
package/dist/auto-sync.js
CHANGED
|
@@ -80,7 +80,6 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
80
80
|
}, debounceMs);
|
|
81
81
|
pendingDebounces.set(absPath, t);
|
|
82
82
|
};
|
|
83
|
-
const ignoreGlobs = buildIgnoreGlobs(ctx);
|
|
84
83
|
const subscribeTo = (root) => watcher.subscribe(root, (err, events) => {
|
|
85
84
|
if (err) {
|
|
86
85
|
onError(err);
|
|
@@ -89,7 +88,7 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
89
88
|
for (const ev of events) {
|
|
90
89
|
schedulePathSync(root, ev.path);
|
|
91
90
|
}
|
|
92
|
-
}, { ignore:
|
|
91
|
+
}, { ignore: buildIgnoreGlobs(ctx, root) });
|
|
93
92
|
let mountSub;
|
|
94
93
|
let projectSub;
|
|
95
94
|
// Subscribe in parallel but track each outcome independently. With
|
|
@@ -163,20 +162,29 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
163
162
|
},
|
|
164
163
|
};
|
|
165
164
|
}
|
|
166
|
-
function buildIgnoreGlobs(ctx) {
|
|
167
|
-
// @parcel/watcher
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
165
|
+
function buildIgnoreGlobs(ctx, watchRoot) {
|
|
166
|
+
// @parcel/watcher's wrapper splits each ignore entry by is-glob: globs are
|
|
167
|
+
// compiled by picomatch and matched as regexes against absolute event paths;
|
|
168
|
+
// non-globs are resolved as literal absolute paths. For each excluded entry
|
|
169
|
+
// (library defaults + user-supplied excludeDirs) we emit shapes that mirror
|
|
170
|
+
// `isExcludedPath`'s semantics, so a watcher-suppressed event never differs
|
|
171
|
+
// from what the in-handler filter would have rejected.
|
|
172
|
+
//
|
|
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
|
+
// in `isExcludedPath` — they only match `<root>/build/cache`, NOT
|
|
178
|
+
// `<root>/src/build/cache`. Emit absolute patterns rooted at the
|
|
179
|
+
// watch dir so the watcher hides the same set: a literal absolute
|
|
180
|
+
// path (which the wrapper routes to ignorePaths) plus an anchored
|
|
181
|
+
// descendant glob.
|
|
174
182
|
const globs = [];
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
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}/**`);
|
|
180
188
|
}
|
|
181
189
|
return globs;
|
|
182
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
|
@@ -13,6 +13,7 @@ export async function launchOnMount(opts) {
|
|
|
13
13
|
excludeDirs: opts.excludeDirs ?? [],
|
|
14
14
|
agentName: opts.agentName,
|
|
15
15
|
includeGit: opts.includeGit,
|
|
16
|
+
includeDefaultExcludeDirs: opts.includeDefaultExcludeDirs,
|
|
16
17
|
});
|
|
17
18
|
let syncedCount = 0;
|
|
18
19
|
let finalized = false;
|
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;
|
package/dist/mount.js
CHANGED
|
@@ -2,7 +2,31 @@ 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
|
|
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';
|
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
63
|
+
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,7 +69,9 @@ export function createMount(projectDir, mountDir, options) {
|
|
|
49
69
|
const autoSyncContext = {
|
|
50
70
|
realMountDir,
|
|
51
71
|
realProjectDir: resolvedProjectDir,
|
|
52
|
-
isExcluded: (relPosix) => isExcludedPath(relPosix,
|
|
72
|
+
isExcluded: (relPosix) => isExcludedPath(relPosix, excludeRules),
|
|
73
|
+
excludedAnyDepthNames: [...excludeRules.anyDepthNames],
|
|
74
|
+
excludedRootPrefixes: [...excludeRules.rootPrefixes],
|
|
53
75
|
isIgnored: (relPosix, isDir) => isPathMatched(relPosix, ignoredMatcher, isDir),
|
|
54
76
|
isReadonly: (relPosix) => isPathMatched(relPosix, readonlyMatcher),
|
|
55
77
|
isNoSyncBack: (relPosix) => isPathMatched(relPosix, noSyncBackMatcher),
|
|
@@ -67,7 +89,7 @@ export function createMount(projectDir, mountDir, options) {
|
|
|
67
89
|
if (signal?.aborted) {
|
|
68
90
|
break;
|
|
69
91
|
}
|
|
70
|
-
const syncedForFile = syncMountedFileBack(sourceFile, realMountDir, realProjectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher);
|
|
92
|
+
const syncedForFile = syncMountedFileBack(sourceFile, realMountDir, realProjectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, (relPosix) => isExcludedPath(relPosix, excludeRules));
|
|
71
93
|
synced += syncedForFile;
|
|
72
94
|
if (signal && syncedForFile > 0 && !signal.aborted) {
|
|
73
95
|
await new Promise((resolve) => setImmediate(resolve));
|
|
@@ -121,7 +143,7 @@ function assertMountDirSafeToRemove(mountDir, projectDir) {
|
|
|
121
143
|
`Only directories previously created by createMount can be reused as mountDir.`);
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
|
-
function walkProjectTree(projectDir, currentDir, mountDir,
|
|
146
|
+
function walkProjectTree(projectDir, currentDir, mountDir, currentMountDir, excludeRules, readonlyMatcher, ignoredMatcher) {
|
|
125
147
|
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
126
148
|
for (const entry of entries) {
|
|
127
149
|
const absolutePath = path.join(currentDir, entry.name);
|
|
@@ -132,18 +154,19 @@ function walkProjectTree(projectDir, currentDir, mountDir, excludeSet, readonlyM
|
|
|
132
154
|
if (isPathWithinRoot(absolutePath, mountDir)) {
|
|
133
155
|
continue;
|
|
134
156
|
}
|
|
135
|
-
if (isExcludedPath(relativePath,
|
|
157
|
+
if (isExcludedPath(relativePath, excludeRules)) {
|
|
136
158
|
continue;
|
|
137
159
|
}
|
|
138
160
|
if (isPathMatched(relativePath, ignoredMatcher, entry.isDirectory())) {
|
|
139
161
|
continue;
|
|
140
162
|
}
|
|
141
|
-
const mountPath = path.join(
|
|
163
|
+
const mountPath = path.join(currentMountDir, entry.name);
|
|
142
164
|
if (entry.isDirectory()) {
|
|
143
|
-
|
|
165
|
+
const safeMountDir = ensureDirectoryWithinRoot(mountDir, mountPath);
|
|
166
|
+
if (!safeMountDir) {
|
|
144
167
|
continue;
|
|
145
168
|
}
|
|
146
|
-
walkProjectTree(projectDir, absolutePath, mountDir,
|
|
169
|
+
walkProjectTree(projectDir, absolutePath, mountDir, safeMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
|
|
147
170
|
continue;
|
|
148
171
|
}
|
|
149
172
|
if (entry.isSymbolicLink()) {
|
|
@@ -193,15 +216,15 @@ function ensureDirectory(pathValue) {
|
|
|
193
216
|
}
|
|
194
217
|
function ensureDirectoryWithinRoot(rootPath, dirPath) {
|
|
195
218
|
if (!isPathWithinRoot(dirPath, rootPath)) {
|
|
196
|
-
return
|
|
219
|
+
return null;
|
|
197
220
|
}
|
|
198
221
|
try {
|
|
199
222
|
ensureDirectory(dirPath);
|
|
200
223
|
const realDir = realpathSync(dirPath);
|
|
201
|
-
return isPathWithinRoot(realDir, rootPath);
|
|
224
|
+
return isPathWithinRoot(realDir, rootPath) ? realDir : null;
|
|
202
225
|
}
|
|
203
226
|
catch {
|
|
204
|
-
return
|
|
227
|
+
return null;
|
|
205
228
|
}
|
|
206
229
|
}
|
|
207
230
|
function listFiles(baseDir) {
|
|
@@ -230,6 +253,38 @@ function normalizeRelativePosix(filePath) {
|
|
|
230
253
|
function createPathMatcher(patterns) {
|
|
231
254
|
return ignore().add(patterns.map((pattern) => pattern.trim()).filter((pattern) => pattern !== '' && !pattern.startsWith('#')));
|
|
232
255
|
}
|
|
256
|
+
function createExcludeRules(excludeDirs, includeGit, includeDefaultExcludeDirs) {
|
|
257
|
+
const anyDepthNames = new Set();
|
|
258
|
+
const rootPrefixes = new Set();
|
|
259
|
+
if (includeDefaultExcludeDirs) {
|
|
260
|
+
addExcludeEntries(anyDepthNames, rootPrefixes, DEFAULT_ANY_DEPTH_EXCLUDES, 'any-depth');
|
|
261
|
+
addExcludeEntries(anyDepthNames, rootPrefixes, DEFAULT_ROOT_EXCLUDES, 'root-prefix');
|
|
262
|
+
}
|
|
263
|
+
else if (!includeGit) {
|
|
264
|
+
addExcludeEntries(anyDepthNames, rootPrefixes, ['.git'], 'any-depth');
|
|
265
|
+
}
|
|
266
|
+
if (includeGit) {
|
|
267
|
+
anyDepthNames.delete('.git');
|
|
268
|
+
}
|
|
269
|
+
// Preserve caller-supplied excludeDirs semantics: bare names match at any
|
|
270
|
+
// depth, while path-style entries are root-anchored prefixes.
|
|
271
|
+
addExcludeEntries(anyDepthNames, rootPrefixes, excludeDirs, 'legacy');
|
|
272
|
+
return { anyDepthNames, rootPrefixes };
|
|
273
|
+
}
|
|
274
|
+
function addExcludeEntries(anyDepthNames, rootPrefixes, entries, mode) {
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
const normalized = normalizeRelativePosix(entry).replace(/^\/+|\/+$/g, '');
|
|
277
|
+
if (!normalized) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (mode === 'root-prefix' || (mode === 'legacy' && normalized.includes('/'))) {
|
|
281
|
+
rootPrefixes.add(normalized);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
anyDepthNames.add(normalized);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
233
288
|
function isPathMatched(relPath, matcher, isDirectory = false) {
|
|
234
289
|
const normalized = normalizeRelativePosix(relPath);
|
|
235
290
|
return matcher.ignores(normalized) || (isDirectory && matcher.ignores(`${normalized}/`));
|
|
@@ -253,8 +308,8 @@ function hasSameContent(left, right) {
|
|
|
253
308
|
return false;
|
|
254
309
|
}
|
|
255
310
|
}
|
|
256
|
-
function syncMountedFileBack(sourceFile, mountDir, projectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher) {
|
|
257
|
-
const relative = resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher);
|
|
311
|
+
function syncMountedFileBack(sourceFile, mountDir, projectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, isExcluded) {
|
|
312
|
+
const relative = resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, isExcluded);
|
|
258
313
|
if (!relative)
|
|
259
314
|
return 0;
|
|
260
315
|
const safeTargetPath = resolveVerifiedSyncTarget(projectDir, relative);
|
|
@@ -266,7 +321,7 @@ function syncMountedFileBack(sourceFile, mountDir, projectDir, readonlyMatcher,
|
|
|
266
321
|
copyFileSync(sourceFile, safeTargetPath);
|
|
267
322
|
return 1;
|
|
268
323
|
}
|
|
269
|
-
function resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher) {
|
|
324
|
+
function resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, isExcluded) {
|
|
270
325
|
const relative = path.relative(mountDir, sourceFile);
|
|
271
326
|
if (relative === '' || relative.startsWith('..'))
|
|
272
327
|
return null;
|
|
@@ -275,7 +330,8 @@ function resolveSyncRelativePath(sourceFile, mountDir, readonlyMatcher, ignoredM
|
|
|
275
330
|
return null;
|
|
276
331
|
if (relativePosix === MOUNT_MARKER_FILENAME)
|
|
277
332
|
return null;
|
|
278
|
-
if (
|
|
333
|
+
if (isExcluded(relativePosix) ||
|
|
334
|
+
isPathMatched(relative, readonlyMatcher) ||
|
|
279
335
|
isPathMatched(relative, ignoredMatcher) ||
|
|
280
336
|
isPathMatched(relative, noSyncBackMatcher))
|
|
281
337
|
return null;
|
|
@@ -310,14 +366,14 @@ function resolveVerifiedSyncTarget(projectDir, relativePath) {
|
|
|
310
366
|
return null;
|
|
311
367
|
}
|
|
312
368
|
}
|
|
313
|
-
function isExcludedPath(relativePath,
|
|
369
|
+
function isExcludedPath(relativePath, excludeRules) {
|
|
314
370
|
const normalized = normalizeRelativePosix(relativePath).replace(/^\/+|\/+$/g, '');
|
|
315
371
|
if (!normalized)
|
|
316
372
|
return false;
|
|
317
373
|
const segments = normalized.split('/');
|
|
318
374
|
return segments.some((segment, index) => {
|
|
319
375
|
const prefix = segments.slice(0, index + 1).join('/');
|
|
320
|
-
return
|
|
376
|
+
return excludeRules.anyDepthNames.has(segment) || excludeRules.rootPrefixes.has(prefix);
|
|
321
377
|
});
|
|
322
378
|
}
|
|
323
379
|
function isPathWithinRoot(candidatePath, rootPath) {
|
|
@@ -330,19 +386,11 @@ function resolveSafeCopyTarget(rootPath, candidatePath) {
|
|
|
330
386
|
return null;
|
|
331
387
|
}
|
|
332
388
|
const parentPath = path.dirname(candidatePath);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
const realParent = realpathSync(parentPath);
|
|
338
|
-
if (!isPathWithinRoot(realParent, rootPath)) {
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
return path.join(realParent, path.basename(candidatePath));
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
389
|
+
const realParent = ensureDirectoryWithinRoot(rootPath, parentPath);
|
|
390
|
+
if (!realParent) {
|
|
344
391
|
return null;
|
|
345
392
|
}
|
|
393
|
+
return path.join(realParent, path.basename(candidatePath));
|
|
346
394
|
}
|
|
347
395
|
function resolveVerifiedFilePath(rootPath, candidatePath) {
|
|
348
396
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/local-mount",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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",
|