@relayfile/local-mount 0.6.15 → 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,6 +27,8 @@ 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`
package/dist/launch.js CHANGED
@@ -7,14 +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
- includeDefaultExcludeDirs: opts.includeDefaultExcludeDirs,
17
- });
10
+ let handle;
18
11
  let syncedCount = 0;
19
12
  let finalized = false;
20
13
  let autoSync;
@@ -29,6 +22,8 @@ export async function launchOnMount(opts) {
29
22
  autoSyncChanges = autoSync.totalChanges();
30
23
  autoSync = undefined;
31
24
  }
25
+ if (!handle)
26
+ return;
32
27
  const finalSynced = await handle.syncBack({ signal: opts.shutdownSignal });
33
28
  syncedCount = autoSyncChanges + finalSynced;
34
29
  if (opts.onAfterSync) {
@@ -36,10 +31,18 @@ export async function launchOnMount(opts) {
36
31
  }
37
32
  }
38
33
  finally {
39
- handle.cleanup();
34
+ handle?.cleanup();
40
35
  }
41
36
  };
42
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
+ });
43
46
  if (opts.onBeforeLaunch) {
44
47
  await opts.onBeforeLaunch(handle.mountDir);
45
48
  }
@@ -51,9 +54,10 @@ export async function launchOnMount(opts) {
51
54
  ...process.env,
52
55
  ...(opts.env ?? {}),
53
56
  };
57
+ const mountCwd = handle.mountDir;
54
58
  const exitCode = await new Promise((resolve, reject) => {
55
59
  const child = spawn(opts.cli, opts.args, {
56
- cwd: handle.mountDir,
60
+ cwd: mountCwd,
57
61
  stdio: 'inherit',
58
62
  env: envVars,
59
63
  });
package/dist/mount.d.ts CHANGED
@@ -41,4 +41,4 @@ export interface MountHandle {
41
41
  startAutoSync(opts?: AutoSyncOptions): AutoSyncHandle;
42
42
  cleanup(): void;
43
43
  }
44
- 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
@@ -30,7 +30,7 @@ const DEFAULT_ROOT_EXCLUDES = [
30
30
  const MOUNT_README_FILENAME = '_MOUNT_README.md';
31
31
  const MOUNT_MARKER_FILENAME = '.relayfile-local-mount';
32
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';
33
- export function createMount(projectDir, mountDir, options) {
33
+ export async function createMount(projectDir, mountDir, options) {
34
34
  const resolvedProjectDir = realpathSync(projectDir);
35
35
  const resolvedMountDir = path.resolve(mountDir);
36
36
  const readonlyPatterns = [...options.readonlyPatterns];
@@ -60,7 +60,7 @@ export function createMount(projectDir, mountDir, options) {
60
60
  mkdirSync(resolvedMountDir, { recursive: true });
61
61
  const realMountDir = realpathSync(resolvedMountDir);
62
62
  writeFileSync(path.join(realMountDir, MOUNT_MARKER_FILENAME), MOUNT_MARKER_CONTENT, 'utf8');
63
- walkProjectTree(resolvedProjectDir, resolvedProjectDir, realMountDir, realMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
63
+ await walkProjectTree(resolvedProjectDir, resolvedProjectDir, realMountDir, realMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
64
64
  const readmePath = resolveSafeCopyTarget(realMountDir, path.join(realMountDir, MOUNT_README_FILENAME));
65
65
  if (!readmePath) {
66
66
  throw new Error('Failed to create mount readme inside mountDir');
@@ -143,9 +143,19 @@ function assertMountDirSafeToRemove(mountDir, projectDir) {
143
143
  `Only directories previously created by createMount can be reused as mountDir.`);
144
144
  }
145
145
  }
146
- function walkProjectTree(projectDir, currentDir, mountDir, currentMountDir, excludeRules, 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();
147
152
  const entries = readdirSync(currentDir, { withFileTypes: true });
153
+ let processed = 0;
148
154
  for (const entry of entries) {
155
+ if (processed > 0 && processed % WALK_YIELD_EVERY === 0) {
156
+ await yieldToEventLoop();
157
+ }
158
+ processed += 1;
149
159
  const absolutePath = path.join(currentDir, entry.name);
150
160
  const relativePath = normalizeRelativePosix(path.relative(projectDir, absolutePath));
151
161
  if (!relativePath || relativePath.startsWith('..')) {
@@ -166,7 +176,7 @@ function walkProjectTree(projectDir, currentDir, mountDir, currentMountDir, excl
166
176
  if (!safeMountDir) {
167
177
  continue;
168
178
  }
169
- walkProjectTree(projectDir, absolutePath, mountDir, safeMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
179
+ await walkProjectTree(projectDir, absolutePath, mountDir, safeMountDir, excludeRules, readonlyMatcher, ignoredMatcher);
170
180
  continue;
171
181
  }
172
182
  if (entry.isSymbolicLink()) {
@@ -179,6 +189,9 @@ function walkProjectTree(projectDir, currentDir, mountDir, currentMountDir, excl
179
189
  copyMountedFile(projectDir, mountDir, absolutePath, mountPath, relativePath, readonlyMatcher);
180
190
  }
181
191
  }
192
+ function yieldToEventLoop() {
193
+ return new Promise((resolve) => setImmediate(resolve));
194
+ }
182
195
  function copySymlinkedFile(projectDir, mountDir, sourcePath, mountPath, relativePath, readonlyMatcher) {
183
196
  let realSource;
184
197
  let resolvedStat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/local-mount",
3
- "version": "0.6.15",
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",