@limrun/api 0.22.2 → 0.23.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/exec-client.d.mts.map +1 -1
  3. package/exec-client.d.ts.map +1 -1
  4. package/exec-client.js +3 -5
  5. package/exec-client.js.map +1 -1
  6. package/exec-client.mjs +3 -5
  7. package/exec-client.mjs.map +1 -1
  8. package/folder-sync-ignore.d.mts +7 -0
  9. package/folder-sync-ignore.d.mts.map +1 -0
  10. package/folder-sync-ignore.d.ts +7 -0
  11. package/folder-sync-ignore.d.ts.map +1 -0
  12. package/folder-sync-ignore.js +56 -0
  13. package/folder-sync-ignore.js.map +1 -0
  14. package/folder-sync-ignore.mjs +52 -0
  15. package/folder-sync-ignore.mjs.map +1 -0
  16. package/folder-sync-watcher.d.mts +2 -0
  17. package/folder-sync-watcher.d.mts.map +1 -1
  18. package/folder-sync-watcher.d.ts +2 -0
  19. package/folder-sync-watcher.d.ts.map +1 -1
  20. package/folder-sync-watcher.js +10 -62
  21. package/folder-sync-watcher.js.map +1 -1
  22. package/folder-sync-watcher.mjs +10 -62
  23. package/folder-sync-watcher.mjs.map +1 -1
  24. package/folder-sync.d.mts +16 -16
  25. package/folder-sync.d.mts.map +1 -1
  26. package/folder-sync.d.ts +16 -16
  27. package/folder-sync.d.ts.map +1 -1
  28. package/folder-sync.js +22 -65
  29. package/folder-sync.js.map +1 -1
  30. package/folder-sync.mjs +21 -64
  31. package/folder-sync.mjs.map +1 -1
  32. package/ios-client.d.mts +24 -0
  33. package/ios-client.d.mts.map +1 -1
  34. package/ios-client.d.ts +24 -0
  35. package/ios-client.d.ts.map +1 -1
  36. package/ios-client.js +108 -8
  37. package/ios-client.js.map +1 -1
  38. package/ios-client.mjs +109 -9
  39. package/ios-client.mjs.map +1 -1
  40. package/package.json +11 -1
  41. package/sandbox-client.d.mts +20 -15
  42. package/sandbox-client.d.mts.map +1 -1
  43. package/sandbox-client.d.ts +20 -15
  44. package/sandbox-client.d.ts.map +1 -1
  45. package/sandbox-client.js +49 -40
  46. package/sandbox-client.js.map +1 -1
  47. package/sandbox-client.mjs +48 -40
  48. package/sandbox-client.mjs.map +1 -1
  49. package/src/exec-client.ts +3 -5
  50. package/src/folder-sync-ignore.ts +65 -0
  51. package/src/folder-sync-watcher.ts +11 -66
  52. package/src/folder-sync.ts +39 -89
  53. package/src/ios-client.ts +138 -9
  54. package/src/sandbox-client.ts +72 -62
  55. package/src/version.ts +1 -1
  56. package/version.d.mts +1 -1
  57. package/version.d.ts +1 -1
  58. package/version.js +1 -1
  59. package/version.mjs +1 -1
@@ -1,5 +1,9 @@
1
- import { syncApp as syncFolderImpl } from "./folder-sync.mjs";
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { syncFolder as syncFolderImpl } from "./folder-sync.mjs";
2
4
  import { exec } from "./exec-client.mjs";
5
+ import { createIgnoreFn } from "./folder-sync-ignore.mjs";
6
+ import crypto from 'crypto';
3
7
  /**
4
8
  * Creates a client for interacting with a sandboxed Xcode build service.
5
9
  *
@@ -46,7 +50,7 @@ export async function createXCodeSandboxClient(options) {
46
50
  console.error('[XCodeSandbox]', ...args);
47
51
  },
48
52
  };
49
- const logFn = (level, msg) => {
53
+ const log = (level, msg) => {
50
54
  switch (level) {
51
55
  case 'debug':
52
56
  logger.debug(msg);
@@ -86,47 +90,51 @@ export async function createXCodeSandboxClient(options) {
86
90
  }
87
91
  return {
88
92
  async sync(localCodePath, opts) {
93
+ // Use folder name and hash of absolute path to scope basisCacheDir uniquely for each sync root
94
+ const resolvedPath = path.resolve(localCodePath);
95
+ const folderName = path.basename(resolvedPath);
96
+ const hash = crypto.createHash('sha1').update(resolvedPath).digest('hex').slice(0, 8);
97
+ const cacheKey = `limsync-cache-${folderName}-${hash}`;
98
+ const basisCacheDir = opts?.basisCacheDir ?? path.join(os.tmpdir(), cacheKey);
89
99
  const codeSyncOpts = {
90
100
  apiUrl: options.apiUrl,
91
101
  token: options.token,
92
- udid: opts?.cacheKey ?? 'xcode-sandbox',
93
- install: false,
94
- filter: (relativePath) => {
95
- if (relativePath.startsWith('build/') ||
96
- relativePath.startsWith('.build/') ||
97
- relativePath.startsWith('DerivedData/') ||
98
- relativePath.startsWith('Index.noindex/') ||
99
- relativePath.startsWith('ModuleCache.noindex/') ||
100
- relativePath.startsWith('.index-build/')) {
102
+ udid: cacheKey,
103
+ install: opts?.install ?? true,
104
+ ignoreFn: await createIgnoreFn(localCodePath, {
105
+ basisCacheDir,
106
+ additional: (relativePath) => {
107
+ if (relativePath.startsWith('build/') ||
108
+ relativePath.startsWith('.build/') ||
109
+ relativePath.startsWith('DerivedData/') ||
110
+ relativePath.startsWith('Index.noindex/') ||
111
+ relativePath.startsWith('ModuleCache.noindex/') ||
112
+ relativePath.startsWith('.index-build/')) {
113
+ return true;
114
+ }
115
+ if (relativePath.startsWith('.swiftpm/') ||
116
+ relativePath.startsWith('Pods/') ||
117
+ relativePath.startsWith('Carthage/Build/')) {
118
+ return true;
119
+ }
120
+ if (relativePath.includes('/xcuserdata/')) {
121
+ return true;
122
+ }
123
+ if (relativePath.includes('.dSYM/')) {
124
+ return true;
125
+ }
126
+ // User-provided ignores
127
+ if (opts?.ignore?.(relativePath)) {
128
+ return true;
129
+ }
101
130
  return false;
102
- }
103
- if (relativePath.startsWith('.swiftpm/') ||
104
- relativePath.startsWith('Pods/') ||
105
- relativePath.startsWith('Carthage/Build/')) {
106
- return false;
107
- }
108
- if (relativePath.startsWith('.git/') ||
109
- relativePath.startsWith('.limsync-cache/') ||
110
- relativePath === '.DS_Store' ||
111
- relativePath.endsWith('/.DS_Store')) {
112
- return false;
113
- }
114
- if (relativePath.includes('/xcuserdata/')) {
115
- return false;
116
- }
117
- if (relativePath.includes('.dSYM/')) {
118
- return false;
119
- }
120
- // User-provided filter
121
- if (opts?.filter && !opts.filter(relativePath)) {
122
- return false;
123
- }
124
- return true;
125
- },
126
- ...(opts?.basisCacheDir ? { basisCacheDir: opts.basisCacheDir } : {}),
127
- ...(opts?.maxPatchBytes !== undefined ? { maxPatchBytes: opts.maxPatchBytes } : {}),
128
- ...(opts?.watch !== undefined ? { watch: opts.watch } : {}),
129
- log: opts?.log ?? logFn,
131
+ },
132
+ }),
133
+ basisCacheDir,
134
+ watch: opts?.watch ?? true,
135
+ maxPatchBytes: opts?.maxPatchBytes ?? 4 * 1024 * 1024,
136
+ launchMode: 'ForegroundIfRunning',
137
+ log,
130
138
  };
131
139
  const result = await syncFolderImpl(localCodePath, codeSyncOpts);
132
140
  if (result.stopWatching) {
@@ -138,7 +146,7 @@ export async function createXCodeSandboxClient(options) {
138
146
  return exec({ command: 'xcodebuild', ...(opts && { xcodebuild: opts }) }, {
139
147
  apiUrl: options.apiUrl,
140
148
  token: options.token,
141
- log: logFn,
149
+ log,
142
150
  });
143
151
  },
144
152
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-client.mjs","sourceRoot":"","sources":["src/sandbox-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sandbox-client.mjs","sourceRoot":"","sources":["src/sandbox-client.ts"],"names":[],"mappings":"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"}
@@ -166,7 +166,6 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
166
166
  const { apiUrl, token } = this.options;
167
167
 
168
168
  // 1. Trigger the build via POST /exec
169
- log('debug', `POST ${apiUrl}/exec`);
170
169
  let execRes: Response;
171
170
  try {
172
171
  execRes = await fetch(`${apiUrl}/exec`, {
@@ -197,11 +196,10 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
197
196
 
198
197
  const execData = (await execRes.json()) as { execId: string };
199
198
  this.execId = execData.execId;
200
- log('info', `Build started: ${this.execId}`);
199
+ log('debug', `Build started: ${this.execId}`);
201
200
 
202
201
  // 2. Stream logs via SSE and wait for exit code
203
202
  const eventsUrl = `${apiUrl}/exec/${this.execId}/events`;
204
- log('debug', `GET ${eventsUrl} (SSE)`);
205
203
 
206
204
  const timeoutMs = 3600 * 1000; // 1 hour max
207
205
  let exitCode: number;
@@ -215,7 +213,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
215
213
  ]);
216
214
  } catch {
217
215
  if (this.killed) {
218
- log('info', 'Build killed');
216
+ log('debug', 'Build killed');
219
217
  exitCode = -1;
220
218
  } else {
221
219
  log('warn', 'SSE completion timeout');
@@ -250,7 +248,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
250
248
  status,
251
249
  };
252
250
 
253
- this.log('info', `Build finished: ${result.status} (exit ${result.exitCode})`);
251
+ this.log('debug', `Build finished: ${result.status} (exit ${result.exitCode})`);
254
252
  return result;
255
253
  }
256
254
 
@@ -0,0 +1,65 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import ignorePkg from 'ignore';
4
+
5
+ export type IgnoreFn = (relativePath: string) => boolean;
6
+
7
+ export type IgnoreFnOptions = {
8
+ additional?: IgnoreFn;
9
+ basisCacheDir: string;
10
+ };
11
+
12
+ function normalizeRelativePath(relativePath: string): string {
13
+ return relativePath
14
+ .split(path.sep)
15
+ .join('/')
16
+ .replace(/^\.\/+/, '')
17
+ .replace(/\/+/g, '/');
18
+ }
19
+
20
+ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions): Promise<IgnoreFn> {
21
+ const rootResolved = path.resolve(rootDir);
22
+ const ig = ignorePkg();
23
+ const gitignorePath = path.join(rootResolved, '.gitignore');
24
+ try {
25
+ const content = await fs.promises.readFile(gitignorePath, 'utf-8');
26
+ ig.add(content);
27
+ } catch {
28
+ // No .gitignore file, return empty ignore instance
29
+ }
30
+ const basisCacheRelative = normalizeRelativePath(
31
+ path.relative(rootResolved, options.basisCacheDir),
32
+ ).replace(/\/+$/, '');
33
+ const shouldIgnoreBasisCache =
34
+ basisCacheRelative &&
35
+ basisCacheRelative !== '.' &&
36
+ basisCacheRelative !== '..' &&
37
+ !basisCacheRelative.startsWith('../');
38
+
39
+ return (relativePath: string) => {
40
+ const normalized = normalizeRelativePath(relativePath);
41
+ if (!normalized) return false;
42
+ const withoutTrailingSlash = normalized.replace(/\/+$/, '');
43
+
44
+ if (
45
+ withoutTrailingSlash === '.git' ||
46
+ withoutTrailingSlash.startsWith('.git/') ||
47
+ withoutTrailingSlash.endsWith('/.git') ||
48
+ withoutTrailingSlash.includes('/.git/') ||
49
+ withoutTrailingSlash === '.DS_Store' ||
50
+ withoutTrailingSlash.endsWith('/.DS_Store')
51
+ ) {
52
+ return true;
53
+ }
54
+ if (
55
+ shouldIgnoreBasisCache &&
56
+ (withoutTrailingSlash === basisCacheRelative ||
57
+ withoutTrailingSlash.startsWith(`${basisCacheRelative}/`))
58
+ ) {
59
+ return true;
60
+ }
61
+ if (ig.ignores(normalized)) return true;
62
+ if (options.additional?.(normalized)) return true;
63
+ return false;
64
+ };
65
+ }
@@ -1,9 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { IgnoreFn } from './folder-sync-ignore';
3
4
 
4
5
  export type FolderSyncWatcherOptions = {
5
6
  rootPath: string;
6
7
  log?: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
8
+ ignoreFn: IgnoreFn;
7
9
  onChange: (reason: string) => void;
8
10
  };
9
11
 
@@ -11,27 +13,6 @@ type WatcherHandle = { close: () => void };
11
13
 
12
14
  const noopLogger = (_level: 'debug' | 'info' | 'warn' | 'error', _msg: string) => {};
13
15
 
14
- async function listDirsRecursive(root: string): Promise<string[]> {
15
- const dirs: string[] = [root];
16
- const queue: string[] = [root];
17
- while (queue.length) {
18
- const dir = queue.pop()!;
19
- let entries: fs.Dirent[];
20
- try {
21
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
22
- } catch {
23
- continue;
24
- }
25
- for (const ent of entries) {
26
- if (!ent.isDirectory()) continue;
27
- const full = path.join(dir, ent.name);
28
- dirs.push(full);
29
- queue.push(full);
30
- }
31
- }
32
- return dirs;
33
- }
34
-
35
16
  /**
36
17
  * Watch a folder tree for changes. Uses recursive watch when supported (macOS),
37
18
  * otherwise falls back to watching each directory. Debounced.
@@ -42,58 +23,22 @@ export async function watchFolderTree(opts: FolderSyncWatcherOptions): Promise<W
42
23
  const log = opts.log ?? noopLogger;
43
24
  const debounceMs = 500;
44
25
  const rootPath = opts.rootPath;
45
-
46
26
  if (!fs.existsSync(rootPath)) {
47
27
  throw new Error(`watchFolderTree root does not exist: ${rootPath}`);
48
28
  }
49
-
50
29
  let timer: NodeJS.Timeout | undefined;
51
30
  const schedule = (reason: string) => {
52
31
  if (timer) clearTimeout(timer);
53
32
  timer = setTimeout(() => opts.onChange(reason), debounceMs);
54
33
  };
55
-
56
- // Preferred: recursive watch
57
- try {
58
- const watcher = fs.watch(rootPath, { recursive: true }, (_eventType, filename) => {
59
- schedule(filename ? `change:${filename.toString()}` : 'change');
60
- });
61
- log('info', `watchFolderTree(recursive): ${rootPath}`);
62
- return { close: () => watcher.close() };
63
- } catch (err) {
64
- log(
65
- 'warn',
66
- `watchFolderTree: recursive unsupported, using per-directory watches: ${(err as Error).message}`,
67
- );
68
- }
69
-
70
- // Fallback: watch every directory. Also re-scan on any event to pick up newly-created dirs.
71
- const watchers = new Map<string, fs.FSWatcher>();
72
-
73
- const ensureWatched = async () => {
74
- const dirs = await listDirsRecursive(rootPath);
75
- for (const d of dirs) {
76
- if (watchers.has(d)) continue;
77
- try {
78
- const w = fs.watch(d, (_eventType, filename) => {
79
- schedule(filename ? `change:${filename.toString()}` : 'change');
80
- void ensureWatched();
81
- });
82
- watchers.set(d, w);
83
- } catch {
84
- // ignore dirs we can't watch
85
- }
34
+ const watcher = fs.watch(rootPath, { recursive: true }, (_eventType, filename) => {
35
+ if (!filename) return;
36
+ const relativePath = filename.split(path.sep).join('/');
37
+ if (opts.ignoreFn(relativePath)) {
38
+ return;
86
39
  }
87
- };
88
-
89
- await ensureWatched();
90
- log('info', `watchFolderTree(per-dir): ${rootPath} dirs=${watchers.size}`);
91
-
92
- return {
93
- close: () => {
94
- if (timer) clearTimeout(timer);
95
- for (const w of watchers.values()) w.close();
96
- watchers.clear();
97
- },
98
- };
40
+ schedule(relativePath ? `change:${relativePath}` : 'change');
41
+ });
42
+ log('debug', `watchFolderTree(recursive): ${rootPath}`);
43
+ return { close: () => watcher.close() };
99
44
  }
@@ -4,9 +4,9 @@ import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { spawn } from 'child_process';
6
6
  import { watchFolderTree } from './folder-sync-watcher';
7
+ import { type IgnoreFn } from './folder-sync-ignore';
7
8
  import { Readable } from 'stream';
8
9
  import * as zlib from 'zlib';
9
- import ignore, { type Ignore } from 'ignore';
10
10
 
11
11
  // =============================================================================
12
12
  // Folder Sync (HTTP batch)
@@ -21,32 +21,32 @@ export type FolderSyncOptions = {
21
21
  * Used to store the last-synced “basis” copies of files (and related sync metadata) so we can compute xdelta patches
22
22
  * on subsequent syncs without re-downloading server state.
23
23
  *
24
- * Can be absolute or relative to process.cwd(). Defaults to `.limsync-cache/`.
24
+ * Defaults to a temporary directory under the OS temp directory.
25
25
  */
26
- basisCacheDir?: string;
27
- install?: boolean;
28
- launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning';
26
+ basisCacheDir: string;
27
+ install: boolean;
28
+ launchMode: 'ForegroundIfRunning' | 'RelaunchIfRunning';
29
29
  /** If true, watch the folder and re-sync on any changes (debounced, single-flight). */
30
- watch?: boolean;
30
+ watch: boolean;
31
31
  /** Max patch size (bytes) to send as delta before falling back to full upload. */
32
- maxPatchBytes?: number;
32
+ maxPatchBytes: number;
33
33
  /** Controls logging verbosity */
34
- log?: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
34
+ log: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
35
35
  /**
36
- * Optional filter function to include/exclude files and directories.
36
+ * Predicate for ignoring files and directories during sync.
37
37
  * Called with the relative path from localFolderPath (using forward slashes).
38
38
  * For directories, the path ends with '/'.
39
- * Return true to include, false to exclude.
39
+ * Return true to ignore, false to keep.
40
40
  *
41
41
  * @example
42
- * // Exclude build folder
43
- * filter: (path) => !path.startsWith('build/')
42
+ * // Ignore build folder
43
+ * ignoreFn: (path) => path.startsWith('build/')
44
44
  *
45
45
  * @example
46
- * // Only include source files
47
- * filter: (path) => path.startsWith('src/') || path.endsWith('.json')
46
+ * // Ignore anything outside src/ and JSON files
47
+ * ignoreFn: (path) => !(path.startsWith('src/') || path.endsWith('.json'))
48
48
  */
49
- filter?: (relativePath: string) => boolean;
49
+ ignoreFn: IgnoreFn;
50
50
  };
51
51
 
52
52
  export type SyncFolderResult = {
@@ -246,40 +246,30 @@ async function sha256FileHex(filePath: string): Promise<string> {
246
246
  });
247
247
  }
248
248
 
249
- async function walkFiles(root: string, filter?: (relativePath: string) => boolean): Promise<FileEntry[]> {
249
+ async function walkFiles(root: string, ignoreFn: IgnoreFn): Promise<FileEntry[]> {
250
250
  const rootResolved = path.resolve(root);
251
251
 
252
- // Load .gitignore if it exists
253
- const ig = await loadGitignore(rootResolved);
254
-
255
252
  const out: FileEntry[] = [];
256
253
  const stack: string[] = [rootResolved];
257
254
  while (stack.length) {
258
255
  const dir = stack.pop()!;
259
256
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
260
257
  for (const ent of entries) {
261
- // Always skip .git folder and .DS_Store
262
- if (ent.name === '.DS_Store' || ent.name === '.git') continue;
263
-
264
258
  const abs = path.join(dir, ent.name);
265
259
  const rel = path.relative(rootResolved, abs).split(path.sep).join('/');
266
260
 
267
- // Check if ignored by .gitignore
268
- if (ig.ignores(rel)) continue;
269
-
270
261
  if (ent.isDirectory()) {
271
262
  // For directories, check with trailing slash
272
263
  const relDir = rel + '/';
273
- if (ig.ignores(relDir)) continue;
274
- // Check custom filter (directories have trailing slash)
275
- if (filter && !filter(relDir)) continue;
264
+ // Check custom ignores (directories have trailing slash)
265
+ if (ignoreFn(relDir)) continue;
276
266
  stack.push(abs);
277
267
  continue;
278
268
  }
279
269
  if (!ent.isFile()) continue;
280
270
 
281
- // Check custom filter for files
282
- if (filter && !filter(rel)) continue;
271
+ // Check custom ignores for files
272
+ if (ignoreFn(rel)) continue;
283
273
 
284
274
  const st = await fs.promises.stat(abs);
285
275
  const sha256 = await sha256FileHex(abs);
@@ -292,22 +282,6 @@ async function walkFiles(root: string, filter?: (relativePath: string) => boolea
292
282
  return out;
293
283
  }
294
284
 
295
- /**
296
- * Load and parse .gitignore file if it exists.
297
- * Returns an Ignore instance that can be used to test paths.
298
- */
299
- async function loadGitignore(rootDir: string): Promise<Ignore> {
300
- const ig = ignore();
301
- const gitignorePath = path.join(rootDir, '.gitignore');
302
- try {
303
- const content = await fs.promises.readFile(gitignorePath, 'utf-8');
304
- ig.add(content);
305
- } catch {
306
- // No .gitignore file, return empty ignore instance
307
- }
308
- return ig;
309
- }
310
-
311
285
  let xdelta3Ready: Promise<void> | null = null;
312
286
  async function ensureXdelta3(): Promise<void> {
313
287
  if (!xdelta3Ready) {
@@ -338,19 +312,6 @@ async function runXdelta3Encode(basis: string, target: string, outPatch: string)
338
312
  });
339
313
  }
340
314
 
341
- function localBasisCacheRoot(opts: FolderSyncOptions, localFolderPath: string): string {
342
- const hostKey = opts.apiUrl.replace(/[:/]+/g, '_');
343
- const resolved = path.resolve(localFolderPath);
344
- const base = path.basename(resolved);
345
- const hash = crypto.createHash('sha1').update(resolved).digest('hex').slice(0, 8);
346
- const rootOverride =
347
- opts.basisCacheDir ?
348
- path.resolve(process.cwd(), opts.basisCacheDir)
349
- : path.join(process.cwd(), '.limsync-cache');
350
- // Include folder identity to avoid collisions between different roots.
351
- return path.join(rootOverride, 'folder-sync', hostKey, opts.udid, `${base}-${hash}`);
352
- }
353
-
354
315
  async function cachePut(cacheRoot: string, relPath: string, srcFile: string): Promise<void> {
355
316
  const dst = path.join(cacheRoot, relPath.split('/').join(path.sep));
356
317
  await fs.promises.mkdir(path.dirname(dst), { recursive: true });
@@ -361,9 +322,14 @@ function cacheGet(cacheRoot: string, relPath: string): string {
361
322
  return path.join(cacheRoot, relPath.split('/').join(path.sep));
362
323
  }
363
324
 
364
- export type SyncAppResult = SyncFolderResult;
365
-
366
- export async function syncApp(localFolderPath: string, opts: FolderSyncOptions): Promise<SyncFolderResult> {
325
+ export async function syncFolder(
326
+ localFolderPath: string,
327
+ opts: FolderSyncOptions,
328
+ ): Promise<SyncFolderResult> {
329
+ const log = (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => {
330
+ (opts.log ?? noopLogger)(level, `syncFolder: ${msg}`);
331
+ };
332
+ log('debug', `setup ${localFolderPath} watch=${opts.watch} basisCacheDir=${opts.basisCacheDir}`);
367
333
  if (!opts.watch) {
368
334
  const result = await syncFolderOnce(localFolderPath, opts);
369
335
  return result;
@@ -389,13 +355,10 @@ export async function syncApp(localFolderPath: string, opts: FolderSyncOptions):
389
355
  }
390
356
  }
391
357
  };
392
-
393
- const watcherLog = (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => {
394
- (opts.log ?? noopLogger)(level, `syncApp: ${msg}`);
395
- };
396
358
  const watcher = await watchFolderTree({
397
359
  rootPath: localFolderPath,
398
- log: watcherLog,
360
+ log,
361
+ ignoreFn: opts.ignoreFn,
399
362
  onChange: (reason) => {
400
363
  void run(reason);
401
364
  },
@@ -417,24 +380,18 @@ async function syncFolderOnce(
417
380
  ): Promise<SyncFolderResult> {
418
381
  const totalStart = nowMs();
419
382
  const log = opts.log ?? noopLogger;
420
- const slog = (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => log(level, `syncApp: ${msg}`);
383
+ const slog = (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => log(level, `syncFolder: ${msg}`);
421
384
  const maxPatchBytes = opts.maxPatchBytes ?? 4 * 1024 * 1024;
422
-
423
- const tEnsureStart = nowMs();
424
385
  await ensureXdelta3();
425
- const tEnsureMs = nowMs() - tEnsureStart;
426
386
 
427
- const tWalkStart = nowMs();
428
- const files = await walkFiles(localFolderPath, opts.filter);
429
- const tWalkMs = nowMs() - tWalkStart;
387
+ const files = await walkFiles(localFolderPath, opts.ignoreFn);
430
388
  const fileMap = new Map(files.map((f) => [f.path, f]));
431
389
 
432
390
  const syncId = genId('sync');
433
391
  const rootName = path.basename(path.resolve(localFolderPath));
434
392
  const preferredCompression = (zlib as any).createZstdCompress ? 'zstd' : 'gzip';
435
393
 
436
- const cacheRoot = localBasisCacheRoot(opts, localFolderPath);
437
- await fs.promises.mkdir(cacheRoot, { recursive: true });
394
+ await fs.promises.mkdir(opts.basisCacheDir, { recursive: true });
438
395
 
439
396
  // Track how many bytes we actually transmit to the server (single HTTP request).
440
397
  let bytesSentFull = 0;
@@ -447,7 +404,7 @@ async function syncFolderOnce(
447
404
  const encodeLimit = concurrencyLimit();
448
405
  const changed: FileEntry[] = [];
449
406
  for (const f of files) {
450
- const basisPath = cacheGet(cacheRoot, f.path);
407
+ const basisPath = cacheGet(opts.basisCacheDir, f.path);
451
408
  if (!fs.existsSync(basisPath)) {
452
409
  changed.push(f);
453
410
  continue;
@@ -459,7 +416,7 @@ async function syncFolderOnce(
459
416
  }
460
417
 
461
418
  const encodedPayloads = await mapLimit(changed, encodeLimit, async (f): Promise<EncodedPayload> => {
462
- const basisPath = cacheGet(cacheRoot, f.path);
419
+ const basisPath = cacheGet(opts.basisCacheDir, f.path);
463
420
  if (fs.existsSync(basisPath)) {
464
421
  const basisSha = await sha256FileHex(basisPath);
465
422
  const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'limulator-xdelta3-'));
@@ -505,7 +462,7 @@ async function syncFolderOnce(
505
462
  const meta: FolderSyncHttpMeta = {
506
463
  id: syncId,
507
464
  rootName,
508
- install: opts.install ?? true,
465
+ install: opts.install,
509
466
  ...(opts.launchMode ? { launchMode: opts.launchMode } : {}),
510
467
  files: files.map((f) => ({ path: f.path, size: f.size, sha256: f.sha256.toLowerCase(), mode: f.mode })),
511
468
  payloads: encodedPayloads.map((p) => p.payload),
@@ -513,7 +470,7 @@ async function syncFolderOnce(
513
470
  const hasDelta = encodedPayloads.some((p) => p.payload.kind === 'delta');
514
471
  const compression: 'zstd' | 'gzip' | 'identity' = hasDelta ? 'identity' : preferredCompression;
515
472
  slog(
516
- 'info',
473
+ 'debug',
517
474
  `sync started files=${files.length}${reason ? ` reason=${reason}` : ''} compression=${compression}`,
518
475
  );
519
476
 
@@ -590,18 +547,11 @@ async function syncFolderOnce(
590
547
  const tookMs = nowMs() - totalStart;
591
548
  const totalBytes = bytesSentFull + bytesSentDelta;
592
549
  slog(
593
- 'info',
550
+ 'debug',
594
551
  `sync finished files=${files.length} sent=${fmtBytes(totalBytes)} syncWork=${fmtMs(
595
552
  syncWorkMs,
596
553
  )} total=${fmtMs(tookMs)}`,
597
554
  );
598
- slog('debug', `sync bytes full=${fmtBytes(bytesSentFull)} delta=${fmtBytes(bytesSentDelta)}`);
599
- slog(
600
- 'debug',
601
- `timing ensureXdelta3=${fmtMs(tEnsureMs)} walk=${fmtMs(tWalkMs)} httpSend=${fmtMs(
602
- httpSendMsTotal,
603
- )} deltaEncode=${fmtMs(deltaEncodeMsTotal)}`,
604
- );
605
555
  const out: SyncFolderResult = {};
606
556
  if (resp.installedAppPath) {
607
557
  out.installedAppPath = resp.installedAppPath;
@@ -611,7 +561,7 @@ async function syncFolderOnce(
611
561
  }
612
562
  // Update local cache optimistically: after a successful sync, cache reflects current local tree.
613
563
  for (const f of files) {
614
- await cachePut(cacheRoot, f.path, f.absPath);
564
+ await cachePut(opts.basisCacheDir, f.path, f.absPath);
615
565
  }
616
566
  return out;
617
567
  }