@limrun/api 0.22.1 → 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.
- package/CHANGELOG.md +8 -0
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js +3 -5
- package/exec-client.js.map +1 -1
- package/exec-client.mjs +3 -5
- package/exec-client.mjs.map +1 -1
- package/folder-sync-ignore.d.mts +7 -0
- package/folder-sync-ignore.d.mts.map +1 -0
- package/folder-sync-ignore.d.ts +7 -0
- package/folder-sync-ignore.d.ts.map +1 -0
- package/folder-sync-ignore.js +56 -0
- package/folder-sync-ignore.js.map +1 -0
- package/folder-sync-ignore.mjs +52 -0
- package/folder-sync-ignore.mjs.map +1 -0
- package/folder-sync-watcher.d.mts +2 -0
- package/folder-sync-watcher.d.mts.map +1 -1
- package/folder-sync-watcher.d.ts +2 -0
- package/folder-sync-watcher.d.ts.map +1 -1
- package/folder-sync-watcher.js +10 -62
- package/folder-sync-watcher.js.map +1 -1
- package/folder-sync-watcher.mjs +10 -62
- package/folder-sync-watcher.mjs.map +1 -1
- package/folder-sync.d.mts +16 -16
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts +16 -16
- package/folder-sync.d.ts.map +1 -1
- package/folder-sync.js +22 -65
- package/folder-sync.js.map +1 -1
- package/folder-sync.mjs +21 -64
- package/folder-sync.mjs.map +1 -1
- package/instance-client.d.mts +1 -1
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +1 -1
- package/instance-client.d.ts.map +1 -1
- package/ios-client.d.mts +24 -0
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +24 -0
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +108 -8
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +109 -9
- package/ios-client.mjs.map +1 -1
- package/package.json +11 -1
- package/sandbox-client.d.mts +20 -15
- package/sandbox-client.d.mts.map +1 -1
- package/sandbox-client.d.ts +20 -15
- package/sandbox-client.d.ts.map +1 -1
- package/sandbox-client.js +49 -40
- package/sandbox-client.js.map +1 -1
- package/sandbox-client.mjs +48 -40
- package/sandbox-client.mjs.map +1 -1
- package/src/exec-client.ts +3 -5
- package/src/folder-sync-ignore.ts +65 -0
- package/src/folder-sync-watcher.ts +11 -66
- package/src/folder-sync.ts +39 -89
- package/src/instance-client.ts +1 -1
- package/src/ios-client.ts +138 -9
- package/src/sandbox-client.ts +72 -62
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/sandbox-client.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import
|
|
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
|
|
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:
|
|
93
|
-
install:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
relativePath.startsWith('
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
149
|
+
log,
|
|
142
150
|
});
|
|
143
151
|
},
|
|
144
152
|
};
|
package/sandbox-client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-client.mjs","sourceRoot":"","sources":["src/sandbox-client.ts"],"names":[],"mappings":"OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"sandbox-client.mjs","sourceRoot":"","sources":["src/sandbox-client.ts"],"names":[],"mappings":"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"}
|
package/src/exec-client.ts
CHANGED
|
@@ -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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
}
|
package/src/folder-sync.ts
CHANGED
|
@@ -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
|
-
*
|
|
24
|
+
* Defaults to a temporary directory under the OS temp directory.
|
|
25
25
|
*/
|
|
26
|
-
basisCacheDir
|
|
27
|
-
install
|
|
28
|
-
launchMode
|
|
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
|
|
30
|
+
watch: boolean;
|
|
31
31
|
/** Max patch size (bytes) to send as delta before falling back to full upload. */
|
|
32
|
-
maxPatchBytes
|
|
32
|
+
maxPatchBytes: number;
|
|
33
33
|
/** Controls logging verbosity */
|
|
34
|
-
log
|
|
34
|
+
log: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
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
|
|
39
|
+
* Return true to ignore, false to keep.
|
|
40
40
|
*
|
|
41
41
|
* @example
|
|
42
|
-
* //
|
|
43
|
-
*
|
|
42
|
+
* // Ignore build folder
|
|
43
|
+
* ignoreFn: (path) => path.startsWith('build/')
|
|
44
44
|
*
|
|
45
45
|
* @example
|
|
46
|
-
* //
|
|
47
|
-
*
|
|
46
|
+
* // Ignore anything outside src/ and JSON files
|
|
47
|
+
* ignoreFn: (path) => !(path.startsWith('src/') || path.endsWith('.json'))
|
|
48
48
|
*/
|
|
49
|
-
|
|
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,
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
282
|
-
if (
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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, `
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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(
|
|
564
|
+
await cachePut(opts.basisCacheDir, f.path, f.absPath);
|
|
615
565
|
}
|
|
616
566
|
return out;
|
|
617
567
|
}
|
package/src/instance-client.ts
CHANGED
|
@@ -79,7 +79,7 @@ export type InstanceClient = {
|
|
|
79
79
|
* and process it (currently APK install is supported). Resolves on success,
|
|
80
80
|
* rejects with an Error on failure.
|
|
81
81
|
*/
|
|
82
|
-
sendAsset: (url: string) => Promise<void>;
|
|
82
|
+
sendAsset: (url: string, timeoutMs?: number) => Promise<void>;
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Get current connection state
|