@relayfile/local-mount 0.7.7 → 0.7.9
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 +9 -3
- package/dist/auto-sync.d.ts +8 -0
- package/dist/auto-sync.js +120 -35
- package/dist/launch.js +33 -4
- package/dist/mount.d.ts +1 -0
- package/dist/mount.js +32 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ const handle = await createMount(projectDir, mountDir, options);
|
|
|
21
21
|
|
|
22
22
|
interface MountHandle {
|
|
23
23
|
mountDir: string;
|
|
24
|
-
syncBack(opts?: { signal?: AbortSignal }): Promise<number>;
|
|
24
|
+
syncBack(opts?: { signal?: AbortSignal; paths?: Iterable<string> }): Promise<number>;
|
|
25
25
|
startAutoSync(opts?: AutoSyncOptions): AutoSyncHandle;
|
|
26
26
|
cleanup(): void;
|
|
27
27
|
}
|
|
@@ -58,8 +58,8 @@ const { ignoredPatterns, readonlyPatterns } = readAgentDotfiles(projectDir, {
|
|
|
58
58
|
|
|
59
59
|
High-level helper that:
|
|
60
60
|
1. creates a mount,
|
|
61
|
-
2. starts bidirectional auto-sync (see below, controllable via `autoSync`),
|
|
62
|
-
3. runs a CLI inside the mount,
|
|
61
|
+
2. starts bidirectional auto-sync (see below, controllable via `autoSync`) and waits for the watchers to be ready when auto-sync is enabled,
|
|
62
|
+
3. runs `onBeforeLaunch`, then runs a CLI inside the mount,
|
|
63
63
|
4. forwards `SIGINT` and `SIGTERM`,
|
|
64
64
|
5. stops auto-sync and runs a final sync-back pass after the child exits,
|
|
65
65
|
6. cleans up the mount directory.
|
|
@@ -84,7 +84,10 @@ interface AutoSyncOptions {
|
|
|
84
84
|
|
|
85
85
|
interface AutoSyncHandle {
|
|
86
86
|
stop(opts?: { signal?: AbortSignal }): Promise<void>;
|
|
87
|
+
flushPending(opts?: { signal?: AbortSignal }): Promise<number>;
|
|
87
88
|
reconcile(opts?: { signal?: AbortSignal }): Promise<number>;
|
|
89
|
+
getDirtyPaths(): IterableIterator<string>;
|
|
90
|
+
watchersHealthy(): boolean;
|
|
88
91
|
totalChanges(): number;
|
|
89
92
|
ready(): Promise<void>;
|
|
90
93
|
}
|
|
@@ -103,6 +106,7 @@ launchOnMount({ /* ... */, autoSync: { scanIntervalMs: 5_000, debounceMs: 100 }
|
|
|
103
106
|
How it works:
|
|
104
107
|
- [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher) watches both the mount and the project tree using native FSEvents/inotify/ReadDirectoryChangesW
|
|
105
108
|
- every `scanIntervalMs`, a full reconcile walks both trees as a safety net for missed events
|
|
109
|
+
- watcher events are tracked as dirty paths, so shutdown can flush pending path-level work and make the final sync-back proportional to the number of mount-side changes when the watcher state stayed healthy
|
|
106
110
|
- `stop({ signal })` still closes watchers if aborted, but skips the final draining reconcile
|
|
107
111
|
- per-file `mtime` is tracked at the last sync, so the scan skips files that haven't changed
|
|
108
112
|
|
|
@@ -267,6 +271,8 @@ The returned number is the count of files written back to `projectDir` in that p
|
|
|
267
271
|
|
|
268
272
|
`syncBack({ signal })` checks for aborts between files. If the signal aborts during shutdown, it returns the partial count accumulated so far instead of throwing, which lets callers report a partial sync and still run cleanup.
|
|
269
273
|
|
|
274
|
+
`syncBack({ paths })` limits the pass to explicit mount-relative paths. This is used by auto-sync shutdown after `watchersHealthy()` confirms both watchers subscribed and no watcher error was observed; otherwise callers should omit `paths` to keep the full-walk safety net.
|
|
275
|
+
|
|
270
276
|
`reconcile({ signal })` and the internal tree walk also poll for aborts between file visits so an in-flight draining scan can stop cooperatively.
|
|
271
277
|
|
|
272
278
|
## Safety constraints
|
package/dist/auto-sync.d.ts
CHANGED
|
@@ -46,10 +46,18 @@ export interface AutoSyncHandle {
|
|
|
46
46
|
stop(opts?: {
|
|
47
47
|
signal?: AbortSignal;
|
|
48
48
|
}): Promise<void>;
|
|
49
|
+
/** Drain currently debounced watcher events. Falls back to reconcile if watchers are degraded. */
|
|
50
|
+
flushPending(opts?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<number>;
|
|
49
53
|
/** Force a reconcile now; returns number of files copied/deleted. */
|
|
50
54
|
reconcile(opts?: {
|
|
51
55
|
signal?: AbortSignal;
|
|
52
56
|
}): Promise<number>;
|
|
57
|
+
/** Mount-side paths that still need a final one-shot syncBack check. */
|
|
58
|
+
getDirtyPaths(): IterableIterator<string>;
|
|
59
|
+
/** True once both watchers subscribed and no watcher error has been observed. */
|
|
60
|
+
watchersHealthy(): boolean;
|
|
53
61
|
/** Cumulative files changed (copied or deleted) since autosync started. */
|
|
54
62
|
totalChanges(): number;
|
|
55
63
|
/** Resolves once both watchers have completed their initial scan. */
|
package/dist/auto-sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import watcher from '@parcel/watcher';
|
|
4
|
+
const STOP_EVENT_SETTLE_MS = 250;
|
|
4
5
|
export function startAutoSync(ctx, opts = {}) {
|
|
5
6
|
const scanIntervalMs = opts.scanIntervalMs ?? 10_000;
|
|
6
7
|
const debounceMs = opts.debounceMs ?? 50;
|
|
@@ -9,9 +10,60 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
9
10
|
primeState(state, ctx);
|
|
10
11
|
let syncing = false;
|
|
11
12
|
let pending = false;
|
|
13
|
+
let stopping = false;
|
|
12
14
|
let stopped = false;
|
|
15
|
+
let watchersReadySettled = false;
|
|
16
|
+
let watcherDegraded = false;
|
|
13
17
|
let totalChanges = 0;
|
|
18
|
+
const pendingPaths = new Set();
|
|
14
19
|
const pendingDebounces = new Map();
|
|
20
|
+
const dirtyMountPaths = new Set();
|
|
21
|
+
const watchersHealthy = () => watchersReadySettled && !watcherDegraded;
|
|
22
|
+
const markWatcherDegraded = (err) => {
|
|
23
|
+
watcherDegraded = true;
|
|
24
|
+
onError(err);
|
|
25
|
+
};
|
|
26
|
+
const clearPendingDebounces = () => {
|
|
27
|
+
for (const t of pendingDebounces.values())
|
|
28
|
+
clearTimeout(t);
|
|
29
|
+
pendingDebounces.clear();
|
|
30
|
+
};
|
|
31
|
+
const syncPath = (relPosix) => {
|
|
32
|
+
if (!isSyncCandidate(relPosix, ctx)) {
|
|
33
|
+
dirtyMountPaths.delete(relPosix);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const changed = syncOneFile(relPosix, state, ctx);
|
|
38
|
+
dirtyMountPaths.delete(relPosix);
|
|
39
|
+
return changed ? 1 : 0;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
onError(err);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const flushPendingPaths = async (opts) => {
|
|
47
|
+
const signal = opts?.signal;
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
let count = 0;
|
|
52
|
+
let processed = 0;
|
|
53
|
+
for (const relPosix of Array.from(pendingPaths)) {
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
pendingPaths.delete(relPosix);
|
|
58
|
+
count += syncPath(relPosix);
|
|
59
|
+
processed += 1;
|
|
60
|
+
if (signal && processed % 64 === 0 && !signal.aborted) {
|
|
61
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
totalChanges += count;
|
|
65
|
+
return count;
|
|
66
|
+
};
|
|
15
67
|
const runReconcile = async (opts) => {
|
|
16
68
|
const signal = opts?.signal;
|
|
17
69
|
if (signal?.aborted) {
|
|
@@ -23,8 +75,10 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
23
75
|
}
|
|
24
76
|
syncing = true;
|
|
25
77
|
let count = 0;
|
|
78
|
+
let completed = false;
|
|
26
79
|
try {
|
|
27
80
|
count = reconcile(state, ctx, onError, signal);
|
|
81
|
+
completed = !signal?.aborted;
|
|
28
82
|
}
|
|
29
83
|
catch (err) {
|
|
30
84
|
onError(err);
|
|
@@ -36,53 +90,71 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
36
90
|
pending = false;
|
|
37
91
|
try {
|
|
38
92
|
count += reconcile(state, ctx, onError, signal);
|
|
93
|
+
completed = !signal?.aborted;
|
|
39
94
|
}
|
|
40
95
|
catch (err) {
|
|
41
96
|
onError(err);
|
|
97
|
+
completed = false;
|
|
42
98
|
}
|
|
43
99
|
}
|
|
100
|
+
if (completed) {
|
|
101
|
+
pendingPaths.clear();
|
|
102
|
+
dirtyMountPaths.clear();
|
|
103
|
+
}
|
|
44
104
|
totalChanges += count;
|
|
45
105
|
return count;
|
|
46
106
|
};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const relPosix = rel.split(path.sep).join('/');
|
|
52
|
-
if (!isSyncCandidate(relPosix, ctx))
|
|
53
|
-
return;
|
|
107
|
+
const flushPending = async (opts) => {
|
|
108
|
+
if (opts?.signal?.aborted) {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
54
111
|
try {
|
|
55
|
-
|
|
56
|
-
if (changed)
|
|
57
|
-
totalChanges += 1;
|
|
112
|
+
await watchersReady;
|
|
58
113
|
}
|
|
59
|
-
catch
|
|
60
|
-
|
|
114
|
+
catch {
|
|
115
|
+
// Subscription setup failure is already marked degraded and surfaced.
|
|
116
|
+
}
|
|
117
|
+
if (opts?.signal?.aborted) {
|
|
118
|
+
return 0;
|
|
61
119
|
}
|
|
120
|
+
clearPendingDebounces();
|
|
121
|
+
if (!watchersHealthy()) {
|
|
122
|
+
return runReconcile(opts);
|
|
123
|
+
}
|
|
124
|
+
return flushPendingPaths(opts);
|
|
62
125
|
};
|
|
63
126
|
const schedulePathSync = (root, absPath) => {
|
|
64
|
-
// Once stop() has begun, refuse to schedule new timers. Watcher
|
|
65
|
-
// callbacks can still fire during the unsubscribe await (native
|
|
66
|
-
// backends deliver queued events asynchronously); without this guard
|
|
67
|
-
// those events would create timers that outlive stop() and do file
|
|
68
|
-
// work against a mount the caller may have already cleaned up.
|
|
69
127
|
if (stopped)
|
|
70
128
|
return;
|
|
129
|
+
const relPosix = root === ctx.realMountDir
|
|
130
|
+
? toRelPosix(absPath, ctx)
|
|
131
|
+
: toRelPosixFromProject(absPath, ctx);
|
|
132
|
+
if (relPosix === null || !isSyncCandidate(relPosix, ctx))
|
|
133
|
+
return;
|
|
134
|
+
pendingPaths.add(relPosix);
|
|
135
|
+
if (root === ctx.realMountDir) {
|
|
136
|
+
dirtyMountPaths.add(relPosix);
|
|
137
|
+
}
|
|
138
|
+
// During stop(), keep accepting queued watcher events so the final flush
|
|
139
|
+
// can process them, but don't create timers that could outlive teardown.
|
|
140
|
+
if (stopping)
|
|
141
|
+
return;
|
|
71
142
|
// Coalesce bursts of events for the same path. The reconcile path
|
|
72
143
|
// re-checks content via mtime+bytes, so a partial-write event that
|
|
73
144
|
// races a later write is harmless.
|
|
74
|
-
const existing = pendingDebounces.get(
|
|
145
|
+
const existing = pendingDebounces.get(relPosix);
|
|
75
146
|
if (existing)
|
|
76
147
|
clearTimeout(existing);
|
|
77
148
|
const t = setTimeout(() => {
|
|
78
|
-
pendingDebounces.delete(
|
|
79
|
-
|
|
149
|
+
pendingDebounces.delete(relPosix);
|
|
150
|
+
pendingPaths.delete(relPosix);
|
|
151
|
+
totalChanges += syncPath(relPosix);
|
|
80
152
|
}, debounceMs);
|
|
81
|
-
pendingDebounces.set(
|
|
153
|
+
pendingDebounces.set(relPosix, t);
|
|
82
154
|
};
|
|
83
155
|
const subscribeTo = (root) => watcher.subscribe(root, (err, events) => {
|
|
84
156
|
if (err) {
|
|
85
|
-
|
|
157
|
+
markWatcherDegraded(err);
|
|
86
158
|
return;
|
|
87
159
|
}
|
|
88
160
|
for (const ev of events) {
|
|
@@ -105,8 +177,11 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
105
177
|
if (projectResult.status === 'fulfilled')
|
|
106
178
|
projectSub = projectResult.value;
|
|
107
179
|
if (mountResult.status === 'fulfilled' && projectResult.status === 'fulfilled') {
|
|
180
|
+
watchersReadySettled = true;
|
|
108
181
|
return;
|
|
109
182
|
}
|
|
183
|
+
watchersReadySettled = true;
|
|
184
|
+
watcherDegraded = true;
|
|
110
185
|
await Promise.allSettled([
|
|
111
186
|
mountSub?.unsubscribe(),
|
|
112
187
|
projectSub?.unsubscribe(),
|
|
@@ -120,7 +195,7 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
120
195
|
// If subscription setup fails, surface via onError rather than an unhandled
|
|
121
196
|
// rejection. stop() still awaits the same promise and will observe the
|
|
122
197
|
// rejection after the cleanup above has already run.
|
|
123
|
-
watchersReady.catch((err) =>
|
|
198
|
+
watchersReady.catch((err) => markWatcherDegraded(err));
|
|
124
199
|
const interval = setInterval(() => {
|
|
125
200
|
void runReconcile();
|
|
126
201
|
}, scanIntervalMs);
|
|
@@ -128,10 +203,6 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
128
203
|
interval.unref?.();
|
|
129
204
|
return {
|
|
130
205
|
async stop(opts) {
|
|
131
|
-
// Flip the flag first so any watcher callbacks delivered during the
|
|
132
|
-
// awaits below refuse to schedule new timers.
|
|
133
|
-
stopped = true;
|
|
134
|
-
clearInterval(interval);
|
|
135
206
|
try {
|
|
136
207
|
await watchersReady;
|
|
137
208
|
}
|
|
@@ -139,23 +210,37 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
139
210
|
// Setup failed and already cleaned up any partial subscription;
|
|
140
211
|
// mountSub / projectSub were reset to undefined before the throw.
|
|
141
212
|
}
|
|
213
|
+
if (!opts?.signal?.aborted && watchersHealthy()) {
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, STOP_EVENT_SETTLE_MS));
|
|
215
|
+
}
|
|
216
|
+
stopping = true;
|
|
217
|
+
clearInterval(interval);
|
|
218
|
+
clearPendingDebounces();
|
|
142
219
|
await Promise.allSettled([
|
|
143
220
|
mountSub?.unsubscribe(),
|
|
144
221
|
projectSub?.unsubscribe(),
|
|
145
222
|
]);
|
|
146
|
-
|
|
147
|
-
// between stop() being called and the watcher actually quiescing is
|
|
148
|
-
// gathered here, so none fire after stop() returns.
|
|
149
|
-
for (const t of pendingDebounces.values())
|
|
150
|
-
clearTimeout(t);
|
|
151
|
-
pendingDebounces.clear();
|
|
223
|
+
clearPendingDebounces();
|
|
152
224
|
if (opts?.signal?.aborted) {
|
|
225
|
+
stopped = true;
|
|
226
|
+
stopping = false;
|
|
153
227
|
return;
|
|
154
228
|
}
|
|
155
|
-
// Drain
|
|
156
|
-
|
|
229
|
+
// Drain pending watcher work when the watcher state is trusted; otherwise
|
|
230
|
+
// keep the historical full-reconcile safety net.
|
|
231
|
+
if (watchersHealthy()) {
|
|
232
|
+
await flushPendingPaths(opts);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
await runReconcile(opts);
|
|
236
|
+
}
|
|
237
|
+
stopped = true;
|
|
238
|
+
stopping = false;
|
|
157
239
|
},
|
|
240
|
+
flushPending,
|
|
158
241
|
reconcile: runReconcile,
|
|
242
|
+
getDirtyPaths: () => new Set(dirtyMountPaths).values(),
|
|
243
|
+
watchersHealthy,
|
|
159
244
|
totalChanges: () => totalChanges,
|
|
160
245
|
ready: async () => {
|
|
161
246
|
await watchersReady;
|
package/dist/launch.js
CHANGED
|
@@ -11,20 +11,40 @@ export async function launchOnMount(opts) {
|
|
|
11
11
|
let syncedCount = 0;
|
|
12
12
|
let finalized = false;
|
|
13
13
|
let autoSync;
|
|
14
|
+
let autoSyncReadyBeforeWrites = false;
|
|
14
15
|
const finalize = async () => {
|
|
15
16
|
if (finalized)
|
|
16
17
|
return;
|
|
17
18
|
finalized = true;
|
|
18
19
|
try {
|
|
19
20
|
let autoSyncChanges = 0;
|
|
21
|
+
let finalSyncBackPaths;
|
|
20
22
|
if (autoSync) {
|
|
21
23
|
await autoSync.stop({ signal: opts.shutdownSignal });
|
|
22
24
|
autoSyncChanges = autoSync.totalChanges();
|
|
25
|
+
if (autoSyncReadyBeforeWrites && autoSync.watchersHealthy()) {
|
|
26
|
+
const dirty = Array.from(autoSync.getDirtyPaths());
|
|
27
|
+
// Only take the fast path when there's at least one dirty
|
|
28
|
+
// path to sync. An empty dirty set is ambiguous after
|
|
29
|
+
// autoSync.stop() — the healthy-path flush already drained
|
|
30
|
+
// pending events, so the set could be empty either because
|
|
31
|
+
// nothing changed or because edits raced past the watcher
|
|
32
|
+
// (short-lived runs where the watcher hadn't enqueued
|
|
33
|
+
// anything before stop). Fall through to the full walk in
|
|
34
|
+
// that case as a safety net; it's cheap on no-change runs
|
|
35
|
+
// and prevents dropped mount edits at shutdown.
|
|
36
|
+
if (dirty.length > 0) {
|
|
37
|
+
finalSyncBackPaths = dirty;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
autoSync = undefined;
|
|
24
41
|
}
|
|
25
42
|
if (!handle)
|
|
26
43
|
return;
|
|
27
|
-
const finalSynced = await handle.syncBack({
|
|
44
|
+
const finalSynced = await handle.syncBack({
|
|
45
|
+
signal: opts.shutdownSignal,
|
|
46
|
+
...(finalSyncBackPaths ? { paths: finalSyncBackPaths } : {}),
|
|
47
|
+
});
|
|
28
48
|
syncedCount = autoSyncChanges + finalSynced;
|
|
29
49
|
if (opts.onAfterSync) {
|
|
30
50
|
await opts.onAfterSync(syncedCount);
|
|
@@ -43,12 +63,21 @@ export async function launchOnMount(opts) {
|
|
|
43
63
|
includeGit: opts.includeGit,
|
|
44
64
|
includeDefaultExcludeDirs: opts.includeDefaultExcludeDirs,
|
|
45
65
|
});
|
|
46
|
-
if (opts.onBeforeLaunch) {
|
|
47
|
-
await opts.onBeforeLaunch(handle.mountDir);
|
|
48
|
-
}
|
|
49
66
|
if (opts.autoSync !== false) {
|
|
50
67
|
const autoSyncOpts = typeof opts.autoSync === 'object' ? opts.autoSync : undefined;
|
|
51
68
|
autoSync = handle.startAutoSync(autoSyncOpts);
|
|
69
|
+
try {
|
|
70
|
+
await autoSync.ready();
|
|
71
|
+
autoSyncReadyBeforeWrites = autoSync.watchersHealthy();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Keep launching with degraded auto-sync; shutdown will use the full
|
|
75
|
+
// syncBack sweep because dirty-path state was never trustworthy.
|
|
76
|
+
autoSyncReadyBeforeWrites = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (opts.onBeforeLaunch) {
|
|
80
|
+
await opts.onBeforeLaunch(handle.mountDir);
|
|
52
81
|
}
|
|
53
82
|
const envVars = {
|
|
54
83
|
...process.env,
|
package/dist/mount.d.ts
CHANGED
package/dist/mount.js
CHANGED
|
@@ -83,7 +83,9 @@ export async function createMount(projectDir, mountDir, options) {
|
|
|
83
83
|
let synced = 0;
|
|
84
84
|
const realProjectDir = realpathSync(resolvedProjectDir);
|
|
85
85
|
const realMountDir = realpathSync(resolvedMountDir);
|
|
86
|
-
const files =
|
|
86
|
+
const files = opts?.paths
|
|
87
|
+
? syncBackPathsToFiles(realMountDir, opts.paths)
|
|
88
|
+
: listFiles(realMountDir);
|
|
87
89
|
const signal = opts?.signal;
|
|
88
90
|
for (const sourceFile of files) {
|
|
89
91
|
if (signal?.aborted) {
|
|
@@ -92,7 +94,14 @@ export async function createMount(projectDir, mountDir, options) {
|
|
|
92
94
|
const syncedForFile = syncMountedFileBack(sourceFile, realMountDir, realProjectDir, readonlyMatcher, ignoredMatcher, noSyncBackMatcher, (relPosix) => isExcludedPath(relPosix, excludeRules));
|
|
93
95
|
synced += syncedForFile;
|
|
94
96
|
if (signal && syncedForFile > 0 && !signal.aborted) {
|
|
95
|
-
|
|
97
|
+
// Intentionally uses setTimeout(resolve, 0) rather than the
|
|
98
|
+
// setImmediate-based yieldToEventLoop helper below: aborts
|
|
99
|
+
// mid-walk are scheduled via setTimeout (see mount.test.ts
|
|
100
|
+
// "syncBack: returns a partial count when aborted mid-walk"),
|
|
101
|
+
// and matching the same queue makes the abort observable
|
|
102
|
+
// between file syncs. setImmediate runs after timer
|
|
103
|
+
// callbacks in a single I/O cycle and races the abort.
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
return synced;
|
|
@@ -260,6 +269,27 @@ function listFiles(baseDir) {
|
|
|
260
269
|
}
|
|
261
270
|
return files;
|
|
262
271
|
}
|
|
272
|
+
function syncBackPathsToFiles(mountDir, relPaths) {
|
|
273
|
+
const files = [];
|
|
274
|
+
const seen = new Set();
|
|
275
|
+
for (const relPath of relPaths) {
|
|
276
|
+
const sourceFile = resolveSyncBackSource(mountDir, relPath);
|
|
277
|
+
if (!sourceFile || seen.has(sourceFile)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
seen.add(sourceFile);
|
|
281
|
+
files.push(sourceFile);
|
|
282
|
+
}
|
|
283
|
+
return files;
|
|
284
|
+
}
|
|
285
|
+
function resolveSyncBackSource(mountDir, relPath) {
|
|
286
|
+
const normalized = normalizeRelativePosix(relPath);
|
|
287
|
+
if (!normalized || path.isAbsolute(normalized)) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const candidate = path.resolve(mountDir, ...normalized.split('/').filter(Boolean));
|
|
291
|
+
return isPathWithinRoot(candidate, mountDir) ? candidate : null;
|
|
292
|
+
}
|
|
263
293
|
function normalizeRelativePosix(filePath) {
|
|
264
294
|
return filePath.split(path.sep).join('/');
|
|
265
295
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/local-mount",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
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",
|