@limrun/api 0.19.2 → 0.20.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 +19 -0
- package/client.d.mts +1 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +1 -0
- package/client.d.ts.map +1 -1
- package/client.js +10 -2
- package/client.js.map +1 -1
- package/client.mjs +10 -2
- package/client.mjs.map +1 -1
- package/exec-client.d.mts +101 -0
- package/exec-client.d.mts.map +1 -0
- package/exec-client.d.ts +101 -0
- package/exec-client.d.ts.map +1 -0
- package/exec-client.js +265 -0
- package/exec-client.js.map +1 -0
- package/exec-client.mjs +259 -0
- package/exec-client.mjs.map +1 -0
- package/folder-sync.d.mts +16 -2
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts +16 -2
- package/folder-sync.d.ts.map +1 -1
- package/folder-sync.js +43 -14
- package/folder-sync.js.map +1 -1
- package/folder-sync.mjs +43 -13
- package/folder-sync.mjs.map +1 -1
- package/index.d.mts +2 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/index.mjs +2 -0
- package/index.mjs.map +1 -1
- package/internal/parse.d.mts.map +1 -1
- package/internal/parse.d.ts.map +1 -1
- package/internal/parse.js +5 -0
- package/internal/parse.js.map +1 -1
- package/internal/parse.mjs +5 -0
- package/internal/parse.mjs.map +1 -1
- package/ios-client.d.mts +60 -1
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +60 -1
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +131 -2
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +129 -1
- package/ios-client.mjs.map +1 -1
- package/package.json +23 -1
- package/sandbox-client.d.mts +124 -0
- package/sandbox-client.d.mts.map +1 -0
- package/sandbox-client.d.ts +124 -0
- package/sandbox-client.d.ts.map +1 -0
- package/sandbox-client.js +149 -0
- package/sandbox-client.js.map +1 -0
- package/sandbox-client.mjs +146 -0
- package/sandbox-client.mjs.map +1 -0
- package/src/client.ts +10 -2
- package/src/exec-client.ts +333 -0
- package/src/folder-sync.ts +66 -18
- package/src/index.ts +16 -0
- package/src/internal/parse.ts +6 -0
- package/src/ios-client.ts +207 -2
- package/src/sandbox-client.ts +267 -0
- 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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createXCodeSandboxClient = createXCodeSandboxClient;
|
|
4
|
+
const folder_sync_1 = require("./folder-sync.js");
|
|
5
|
+
const exec_client_1 = require("./exec-client.js");
|
|
6
|
+
/**
|
|
7
|
+
* Creates a client for interacting with a sandboxed Xcode build service.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // When using an iOS instance (simulator already configured):
|
|
11
|
+
* const client = await createXCodeSandboxClient({
|
|
12
|
+
* apiUrl: instance.status.sandbox.xcode.url,
|
|
13
|
+
* token: apiKey,
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // When using a standalone sandbox (need to configure simulator):
|
|
17
|
+
* const client = await createXCodeSandboxClient({
|
|
18
|
+
* apiUrl: 'https://sandbox.example.com',
|
|
19
|
+
* token: 'xxx',
|
|
20
|
+
* simulator: {
|
|
21
|
+
* apiUrl: 'https://limulator.example.com',
|
|
22
|
+
* token: 'yyy', // optional, defaults to sandbox token
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Sync code and build
|
|
27
|
+
* await client.sync('./my-ios-app', { watch: true });
|
|
28
|
+
* const build = client.xcodebuild();
|
|
29
|
+
* build.stdout.on('data', (line) => console.log('[build]', line));
|
|
30
|
+
* const { exitCode } = await build;
|
|
31
|
+
*/
|
|
32
|
+
async function createXCodeSandboxClient(options) {
|
|
33
|
+
const logLevel = options.logLevel ?? 'info';
|
|
34
|
+
const logger = {
|
|
35
|
+
debug: (...args) => {
|
|
36
|
+
if (logLevel === 'debug')
|
|
37
|
+
console.log('[XCodeSandbox]', ...args);
|
|
38
|
+
},
|
|
39
|
+
info: (...args) => {
|
|
40
|
+
if (logLevel === 'info' || logLevel === 'debug')
|
|
41
|
+
console.log('[XCodeSandbox]', ...args);
|
|
42
|
+
},
|
|
43
|
+
warn: (...args) => {
|
|
44
|
+
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
|
|
45
|
+
console.warn('[XCodeSandbox]', ...args);
|
|
46
|
+
},
|
|
47
|
+
error: (...args) => {
|
|
48
|
+
if (logLevel !== 'none')
|
|
49
|
+
console.error('[XCodeSandbox]', ...args);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const logFn = (level, msg) => {
|
|
53
|
+
switch (level) {
|
|
54
|
+
case 'debug':
|
|
55
|
+
logger.debug(msg);
|
|
56
|
+
break;
|
|
57
|
+
case 'info':
|
|
58
|
+
logger.info(msg);
|
|
59
|
+
break;
|
|
60
|
+
case 'warn':
|
|
61
|
+
logger.warn(msg);
|
|
62
|
+
break;
|
|
63
|
+
case 'error':
|
|
64
|
+
logger.error(msg);
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
logger.info(msg);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// Configure the simulator connection if provided
|
|
72
|
+
if (options.simulator) {
|
|
73
|
+
const cfg = {
|
|
74
|
+
simulatorApiUrl: options.simulator.apiUrl,
|
|
75
|
+
simulatorToken: options.simulator.token ?? options.token,
|
|
76
|
+
};
|
|
77
|
+
const res = await fetch(`${options.apiUrl}/simulator`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
Authorization: `Bearer ${options.token}`,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(cfg),
|
|
84
|
+
});
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`POST /simulator failed: ${res.status} ${text}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
async sync(localCodePath, opts) {
|
|
92
|
+
const codeSyncOpts = {
|
|
93
|
+
apiUrl: options.apiUrl,
|
|
94
|
+
token: options.token,
|
|
95
|
+
udid: opts?.cacheKey ?? 'xcode-sandbox',
|
|
96
|
+
install: false,
|
|
97
|
+
filter: (relativePath) => {
|
|
98
|
+
if (relativePath.startsWith('build/') ||
|
|
99
|
+
relativePath.startsWith('.build/') ||
|
|
100
|
+
relativePath.startsWith('DerivedData/') ||
|
|
101
|
+
relativePath.startsWith('Index.noindex/') ||
|
|
102
|
+
relativePath.startsWith('ModuleCache.noindex/') ||
|
|
103
|
+
relativePath.startsWith('.index-build/')) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (relativePath.startsWith('.swiftpm/') ||
|
|
107
|
+
relativePath.startsWith('Pods/') ||
|
|
108
|
+
relativePath.startsWith('Carthage/Build/')) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (relativePath.startsWith('.git/') ||
|
|
112
|
+
relativePath.startsWith('.limsync-cache/') ||
|
|
113
|
+
relativePath === '.DS_Store' ||
|
|
114
|
+
relativePath.endsWith('/.DS_Store')) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (relativePath.includes('/xcuserdata/')) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (relativePath.includes('.dSYM/')) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// User-provided filter
|
|
124
|
+
if (opts?.filter && !opts.filter(relativePath)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
...(opts?.basisCacheDir ? { basisCacheDir: opts.basisCacheDir } : {}),
|
|
130
|
+
...(opts?.maxPatchBytes !== undefined ? { maxPatchBytes: opts.maxPatchBytes } : {}),
|
|
131
|
+
...(opts?.watch !== undefined ? { watch: opts.watch } : {}),
|
|
132
|
+
log: opts?.log ?? logFn,
|
|
133
|
+
};
|
|
134
|
+
const result = await (0, folder_sync_1.syncApp)(localCodePath, codeSyncOpts);
|
|
135
|
+
if (result.stopWatching) {
|
|
136
|
+
return { stopWatching: result.stopWatching };
|
|
137
|
+
}
|
|
138
|
+
return {};
|
|
139
|
+
},
|
|
140
|
+
xcodebuild(opts) {
|
|
141
|
+
return (0, exec_client_1.exec)({ command: 'xcodebuild', ...(opts && { xcodebuild: opts }) }, {
|
|
142
|
+
apiUrl: options.apiUrl,
|
|
143
|
+
token: options.token,
|
|
144
|
+
log: logFn,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=sandbox-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-client.js","sourceRoot":"","sources":["src/sandbox-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { syncApp as syncFolderImpl } from "./folder-sync.mjs";
|
|
2
|
+
import { exec } from "./exec-client.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a client for interacting with a sandboxed Xcode build service.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // When using an iOS instance (simulator already configured):
|
|
8
|
+
* const client = await createXCodeSandboxClient({
|
|
9
|
+
* apiUrl: instance.status.sandbox.xcode.url,
|
|
10
|
+
* token: apiKey,
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* // When using a standalone sandbox (need to configure simulator):
|
|
14
|
+
* const client = await createXCodeSandboxClient({
|
|
15
|
+
* apiUrl: 'https://sandbox.example.com',
|
|
16
|
+
* token: 'xxx',
|
|
17
|
+
* simulator: {
|
|
18
|
+
* apiUrl: 'https://limulator.example.com',
|
|
19
|
+
* token: 'yyy', // optional, defaults to sandbox token
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // Sync code and build
|
|
24
|
+
* await client.sync('./my-ios-app', { watch: true });
|
|
25
|
+
* const build = client.xcodebuild();
|
|
26
|
+
* build.stdout.on('data', (line) => console.log('[build]', line));
|
|
27
|
+
* const { exitCode } = await build;
|
|
28
|
+
*/
|
|
29
|
+
export async function createXCodeSandboxClient(options) {
|
|
30
|
+
const logLevel = options.logLevel ?? 'info';
|
|
31
|
+
const logger = {
|
|
32
|
+
debug: (...args) => {
|
|
33
|
+
if (logLevel === 'debug')
|
|
34
|
+
console.log('[XCodeSandbox]', ...args);
|
|
35
|
+
},
|
|
36
|
+
info: (...args) => {
|
|
37
|
+
if (logLevel === 'info' || logLevel === 'debug')
|
|
38
|
+
console.log('[XCodeSandbox]', ...args);
|
|
39
|
+
},
|
|
40
|
+
warn: (...args) => {
|
|
41
|
+
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
|
|
42
|
+
console.warn('[XCodeSandbox]', ...args);
|
|
43
|
+
},
|
|
44
|
+
error: (...args) => {
|
|
45
|
+
if (logLevel !== 'none')
|
|
46
|
+
console.error('[XCodeSandbox]', ...args);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const logFn = (level, msg) => {
|
|
50
|
+
switch (level) {
|
|
51
|
+
case 'debug':
|
|
52
|
+
logger.debug(msg);
|
|
53
|
+
break;
|
|
54
|
+
case 'info':
|
|
55
|
+
logger.info(msg);
|
|
56
|
+
break;
|
|
57
|
+
case 'warn':
|
|
58
|
+
logger.warn(msg);
|
|
59
|
+
break;
|
|
60
|
+
case 'error':
|
|
61
|
+
logger.error(msg);
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
logger.info(msg);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Configure the simulator connection if provided
|
|
69
|
+
if (options.simulator) {
|
|
70
|
+
const cfg = {
|
|
71
|
+
simulatorApiUrl: options.simulator.apiUrl,
|
|
72
|
+
simulatorToken: options.simulator.token ?? options.token,
|
|
73
|
+
};
|
|
74
|
+
const res = await fetch(`${options.apiUrl}/simulator`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
Authorization: `Bearer ${options.token}`,
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify(cfg),
|
|
81
|
+
});
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(`POST /simulator failed: ${res.status} ${text}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
async sync(localCodePath, opts) {
|
|
89
|
+
const codeSyncOpts = {
|
|
90
|
+
apiUrl: options.apiUrl,
|
|
91
|
+
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/')) {
|
|
101
|
+
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,
|
|
130
|
+
};
|
|
131
|
+
const result = await syncFolderImpl(localCodePath, codeSyncOpts);
|
|
132
|
+
if (result.stopWatching) {
|
|
133
|
+
return { stopWatching: result.stopWatching };
|
|
134
|
+
}
|
|
135
|
+
return {};
|
|
136
|
+
},
|
|
137
|
+
xcodebuild(opts) {
|
|
138
|
+
return exec({ command: 'xcodebuild', ...(opts && { xcodebuild: opts }) }, {
|
|
139
|
+
apiUrl: options.apiUrl,
|
|
140
|
+
token: options.token,
|
|
141
|
+
log: logFn,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=sandbox-client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-client.mjs","sourceRoot":"","sources":["src/sandbox-client.ts"],"names":[],"mappings":"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"}
|
package/src/client.ts
CHANGED
|
@@ -537,9 +537,10 @@ export class Limrun {
|
|
|
537
537
|
controller: AbortController,
|
|
538
538
|
): Promise<Response> {
|
|
539
539
|
const { signal, method, ...options } = init || {};
|
|
540
|
-
|
|
540
|
+
const abort = this._makeAbort(controller);
|
|
541
|
+
if (signal) signal.addEventListener('abort', abort, { once: true });
|
|
541
542
|
|
|
542
|
-
const timeout = setTimeout(
|
|
543
|
+
const timeout = setTimeout(abort, ms);
|
|
543
544
|
|
|
544
545
|
const isReadableBody =
|
|
545
546
|
((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) ||
|
|
@@ -562,6 +563,7 @@ export class Limrun {
|
|
|
562
563
|
return await this.fetch.call(undefined, url, fetchOptions);
|
|
563
564
|
} finally {
|
|
564
565
|
clearTimeout(timeout);
|
|
566
|
+
if (signal) signal.removeEventListener('abort', abort);
|
|
565
567
|
}
|
|
566
568
|
}
|
|
567
569
|
|
|
@@ -706,6 +708,12 @@ export class Limrun {
|
|
|
706
708
|
return headers.values;
|
|
707
709
|
}
|
|
708
710
|
|
|
711
|
+
private _makeAbort(controller: AbortController) {
|
|
712
|
+
// note: we can't just inline this method inside `fetchWithTimeout()` because then the closure
|
|
713
|
+
// would capture all request options, and cause a memory leak.
|
|
714
|
+
return () => controller.abort();
|
|
715
|
+
}
|
|
716
|
+
|
|
709
717
|
private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): {
|
|
710
718
|
bodyHeaders: HeadersLike;
|
|
711
719
|
body: BodyInit | undefined;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for executing commands on limbuild server with streaming output.
|
|
3
|
+
*
|
|
4
|
+
* The interface is designed to be similar to Node.js's child_process.spawn()
|
|
5
|
+
* for familiarity and ease of extension.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createEventSource, type EventSourceClient, type EventSourceMessage } from 'eventsource-client';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export type ExecRequest = {
|
|
15
|
+
command: 'xcodebuild';
|
|
16
|
+
xcodebuild?: {
|
|
17
|
+
workspace?: string;
|
|
18
|
+
project?: string;
|
|
19
|
+
scheme?: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ExecOptions = {
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
token: string;
|
|
26
|
+
log?: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ExecResult = {
|
|
30
|
+
exitCode: number;
|
|
31
|
+
execId: string;
|
|
32
|
+
status: 'SUCCEEDED' | 'FAILED' | 'CANCELLED';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type DataListener = (chunk: string) => void;
|
|
36
|
+
type CloseListener = () => void;
|
|
37
|
+
type ExitListener = (code: number) => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A Readable-like stream interface, similar to Node.js stream.Readable.
|
|
41
|
+
* Emits 'data' for each chunk and 'close' when the stream ends.
|
|
42
|
+
*/
|
|
43
|
+
export class ReadableStream {
|
|
44
|
+
private dataListeners: DataListener[] = [];
|
|
45
|
+
private closeListeners: CloseListener[] = [];
|
|
46
|
+
private closed = false;
|
|
47
|
+
|
|
48
|
+
on(event: 'data', listener: DataListener): this;
|
|
49
|
+
on(event: 'close', listener: CloseListener): this;
|
|
50
|
+
on(event: 'data' | 'close', listener: DataListener | CloseListener): this {
|
|
51
|
+
if (event === 'data') {
|
|
52
|
+
this.dataListeners.push(listener as DataListener);
|
|
53
|
+
} else if (event === 'close') {
|
|
54
|
+
this.closeListeners.push(listener as CloseListener);
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @internal */
|
|
60
|
+
emit(event: 'data', chunk: string): void;
|
|
61
|
+
emit(event: 'close'): void;
|
|
62
|
+
emit(event: 'data' | 'close', arg?: string): void {
|
|
63
|
+
if (event === 'data' && typeof arg === 'string') {
|
|
64
|
+
for (const l of this.dataListeners) l(arg);
|
|
65
|
+
} else if (event === 'close' && !this.closed) {
|
|
66
|
+
this.closed = true;
|
|
67
|
+
for (const l of this.closeListeners) l();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A ChildProcess-like object similar to Node.js's ChildProcess.
|
|
74
|
+
*
|
|
75
|
+
* Implements PromiseLike so it can be awaited directly.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Stream-based (like Node.js spawn)
|
|
79
|
+
* const proc = exec({ command: 'xcodebuild' }, options);
|
|
80
|
+
* proc.stdout.on('data', (chunk) => process.stdout.write(chunk));
|
|
81
|
+
* proc.stderr.on('data', (chunk) => process.stderr.write(chunk));
|
|
82
|
+
* proc.on('exit', (code) => console.log(`Exited with code ${code}`));
|
|
83
|
+
*
|
|
84
|
+
* // Promise-based (can be awaited)
|
|
85
|
+
* const { exitCode, status } = await proc;
|
|
86
|
+
*/
|
|
87
|
+
export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
88
|
+
/** Stdout stream - emits 'data' and 'close' events */
|
|
89
|
+
readonly stdout = new ReadableStream();
|
|
90
|
+
|
|
91
|
+
/** Stderr stream - emits 'data' and 'close' events */
|
|
92
|
+
readonly stderr = new ReadableStream();
|
|
93
|
+
|
|
94
|
+
/** The remote process/build identifier (similar to pid in Node.js) */
|
|
95
|
+
execId: string | undefined;
|
|
96
|
+
|
|
97
|
+
private readonly resultPromise: Promise<ExecResult>;
|
|
98
|
+
private readonly exitListeners: ExitListener[] = [];
|
|
99
|
+
private abortController: AbortController | null = null;
|
|
100
|
+
private sseConnection: EventSourceClient | null = null;
|
|
101
|
+
private killed = false;
|
|
102
|
+
private readonly options: ExecOptions;
|
|
103
|
+
private readonly log: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
|
|
104
|
+
|
|
105
|
+
constructor(request: ExecRequest, options: ExecOptions) {
|
|
106
|
+
this.options = options;
|
|
107
|
+
this.log = options.log ?? (() => {});
|
|
108
|
+
this.resultPromise = this.run(request);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Implement PromiseLike so this object can be awaited */
|
|
112
|
+
then<TResult1 = ExecResult, TResult2 = never>(
|
|
113
|
+
onfulfilled?: ((value: ExecResult) => TResult1 | PromiseLike<TResult1>) | null,
|
|
114
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
115
|
+
): Promise<TResult1 | TResult2> {
|
|
116
|
+
return this.resultPromise.then(onfulfilled, onrejected);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Catch errors */
|
|
120
|
+
catch<TResult = never>(
|
|
121
|
+
onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
|
|
122
|
+
): Promise<ExecResult | TResult> {
|
|
123
|
+
return this.resultPromise.catch(onrejected);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Finally handler */
|
|
127
|
+
finally(onfinally?: (() => void) | null): Promise<ExecResult> {
|
|
128
|
+
return this.resultPromise.finally(onfinally);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Listen for process events */
|
|
132
|
+
on(event: 'exit', listener: ExitListener): this {
|
|
133
|
+
if (event === 'exit') {
|
|
134
|
+
this.exitListeners.push(listener);
|
|
135
|
+
}
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Send a signal to terminate the process */
|
|
140
|
+
async kill(): Promise<void> {
|
|
141
|
+
this.killed = true;
|
|
142
|
+
if (this.abortController) {
|
|
143
|
+
this.abortController.abort();
|
|
144
|
+
}
|
|
145
|
+
if (this.sseConnection) {
|
|
146
|
+
this.sseConnection.close();
|
|
147
|
+
this.sseConnection = null;
|
|
148
|
+
}
|
|
149
|
+
if (!this.execId) {
|
|
150
|
+
this.log('warn', 'Failed to cancel build: execId is not set');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await fetch(`${this.options.apiUrl}/exec/${this.execId}/cancel`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: `Bearer ${this.options.token}`,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
this.log('info', 'Build cancelled');
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.log('warn', `Failed to cancel build: ${err}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async run(request: ExecRequest): Promise<ExecResult> {
|
|
167
|
+
const { log } = this;
|
|
168
|
+
const { apiUrl, token } = this.options;
|
|
169
|
+
|
|
170
|
+
// 1. Trigger the build via POST /exec
|
|
171
|
+
log('debug', `POST ${apiUrl}/exec`);
|
|
172
|
+
const execRes = await fetch(`${apiUrl}/exec`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
Authorization: `Bearer ${token}`,
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify(request),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!execRes.ok) {
|
|
182
|
+
const text = await execRes.text();
|
|
183
|
+
throw new Error(`exec failed: ${execRes.status} ${text}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const execData = (await execRes.json()) as { execId: string };
|
|
187
|
+
this.execId = execData.execId;
|
|
188
|
+
log('info', `Build started: ${this.execId}`);
|
|
189
|
+
|
|
190
|
+
// 2. Connect to SSE for log streaming and completion detection
|
|
191
|
+
this.abortController = new AbortController();
|
|
192
|
+
const eventsUrl = `${apiUrl}/exec/${this.execId}/events`;
|
|
193
|
+
log('debug', `GET ${eventsUrl} (SSE)`);
|
|
194
|
+
|
|
195
|
+
// Promise that resolves when build completes (via exitCode event)
|
|
196
|
+
let sseCompletionResolve: ((exitCode: number) => void) | null = null;
|
|
197
|
+
const sseCompletionPromise = new Promise<number>((resolve) => {
|
|
198
|
+
sseCompletionResolve = resolve;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const ssePromise = this.connectSSE(eventsUrl, sseCompletionResolve);
|
|
202
|
+
|
|
203
|
+
// Wait for SSE to signal completion (with timeout fallback)
|
|
204
|
+
const timeoutMs = 3600 * 1000; // 1 hour max
|
|
205
|
+
let exitCode: number;
|
|
206
|
+
try {
|
|
207
|
+
exitCode = await Promise.race([
|
|
208
|
+
sseCompletionPromise,
|
|
209
|
+
new Promise<number>((_, reject) => setTimeout(() => reject(new Error('SSE timeout')), timeoutMs)),
|
|
210
|
+
]);
|
|
211
|
+
} catch {
|
|
212
|
+
log('warn', 'SSE completion timeout');
|
|
213
|
+
exitCode = 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Cleanup SSE connection
|
|
217
|
+
if (this.abortController) {
|
|
218
|
+
this.abortController.abort();
|
|
219
|
+
}
|
|
220
|
+
await ssePromise.catch(() => {});
|
|
221
|
+
|
|
222
|
+
// Emit close events on streams
|
|
223
|
+
this.stdout.emit('close');
|
|
224
|
+
this.stderr.emit('close');
|
|
225
|
+
|
|
226
|
+
// Emit exit event
|
|
227
|
+
for (const listener of this.exitListeners) {
|
|
228
|
+
listener(exitCode);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Determine status from exit code
|
|
232
|
+
const status: 'SUCCEEDED' | 'FAILED' | 'CANCELLED' =
|
|
233
|
+
exitCode === 0 ? 'SUCCEEDED'
|
|
234
|
+
: exitCode === -1 ? 'CANCELLED'
|
|
235
|
+
: 'FAILED';
|
|
236
|
+
|
|
237
|
+
const result: ExecResult = {
|
|
238
|
+
exitCode,
|
|
239
|
+
execId: this.execId!,
|
|
240
|
+
status,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this.log('info', `Build finished: ${result.status} (exit ${result.exitCode})`);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async connectSSE(
|
|
248
|
+
eventsUrl: string,
|
|
249
|
+
onComplete: ((exitCode: number) => void) | null,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
const authHeader = `Bearer ${this.options.token}`;
|
|
253
|
+
|
|
254
|
+
let resolved = false;
|
|
255
|
+
const resolveOnce = () => {
|
|
256
|
+
if (resolved) return;
|
|
257
|
+
resolved = true;
|
|
258
|
+
this.sseConnection?.close();
|
|
259
|
+
this.sseConnection = null;
|
|
260
|
+
resolve();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const eventSource = createEventSource({
|
|
265
|
+
url: eventsUrl,
|
|
266
|
+
headers: { Authorization: authHeader },
|
|
267
|
+
onMessage: (message: EventSourceMessage) => {
|
|
268
|
+
const data = typeof message.data === 'string' ? message.data : String(message.data ?? '');
|
|
269
|
+
const eventType = message.event;
|
|
270
|
+
if (eventType === 'stdout') {
|
|
271
|
+
this.stdout.emit('data', data);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (eventType === 'stderr') {
|
|
275
|
+
this.stderr.emit('data', data);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (eventType === 'exitCode') {
|
|
279
|
+
const exitCode = parseInt(data, 10);
|
|
280
|
+
if (Number.isNaN(exitCode)) {
|
|
281
|
+
this.log('warn', `SSE exitCode event has invalid data: ${data}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.log('debug', `Build completed via SSE: exitCode=${exitCode}`);
|
|
285
|
+
onComplete?.(exitCode);
|
|
286
|
+
resolveOnce();
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
onDisconnect: () => {
|
|
290
|
+
if (!this.killed) {
|
|
291
|
+
this.log('warn', 'SSE disconnected');
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
this.sseConnection = eventSource;
|
|
296
|
+
|
|
297
|
+
const abortSignal = this.abortController?.signal;
|
|
298
|
+
if (abortSignal) {
|
|
299
|
+
abortSignal.addEventListener(
|
|
300
|
+
'abort',
|
|
301
|
+
() => {
|
|
302
|
+
resolveOnce();
|
|
303
|
+
},
|
|
304
|
+
{ once: true },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (!this.killed) {
|
|
309
|
+
this.log('warn', `SSE setup failed: ${err}`);
|
|
310
|
+
}
|
|
311
|
+
resolveOnce();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Execute a command on the limbuild server.
|
|
319
|
+
* Returns a ChildProcess-like object with stdout/stderr streams.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* const proc = exec({ command: 'xcodebuild' }, { apiUrl: '...', token: '...' });
|
|
323
|
+
*
|
|
324
|
+
* // Stream output
|
|
325
|
+
* proc.stdout.on('data', (chunk) => console.log('[stdout]', chunk));
|
|
326
|
+
* proc.stderr.on('data', (chunk) => console.error('[stderr]', chunk));
|
|
327
|
+
*
|
|
328
|
+
* // Wait for completion
|
|
329
|
+
* const { exitCode, status } = await proc;
|
|
330
|
+
*/
|
|
331
|
+
export function exec(request: ExecRequest, options: ExecOptions): ExecChildProcess {
|
|
332
|
+
return new ExecChildProcess(request, options);
|
|
333
|
+
}
|