@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 +48 -3
- package/dist/auto-sync.d.ts +10 -7
- package/dist/auto-sync.js +9 -11
- package/dist/launch.d.ts +5 -0
- package/dist/launch.js +14 -9
- package/dist/mount.d.ts +7 -1
- package/dist/mount.js +98 -38
- package/package.json +1 -1
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
|
|
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`,
|
|
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: ['
|
|
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.
|
package/dist/auto-sync.d.ts
CHANGED
|
@@ -3,14 +3,17 @@ export interface AutoSyncContext {
|
|
|
3
3
|
realProjectDir: string;
|
|
4
4
|
isExcluded: (relPosix: string) => boolean;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
// -
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// -
|
|
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.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
+
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,
|
|
53
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
173
|
+
const mountPath = path.join(currentMountDir, entry.name);
|
|
143
174
|
if (entry.isDirectory()) {
|
|
144
|
-
|
|
175
|
+
const safeMountDir = ensureDirectoryWithinRoot(mountDir, mountPath);
|
|
176
|
+
if (!safeMountDir) {
|
|
145
177
|
continue;
|
|
146
178
|
}
|
|
147
|
-
walkProjectTree(projectDir, absolutePath, mountDir,
|
|
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
|
|
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
|
|
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 (
|
|
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,
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
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.
|
|
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",
|