@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/client.d.mts +1 -0
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +1 -0
  5. package/client.d.ts.map +1 -1
  6. package/client.js +10 -2
  7. package/client.js.map +1 -1
  8. package/client.mjs +10 -2
  9. package/client.mjs.map +1 -1
  10. package/exec-client.d.mts +101 -0
  11. package/exec-client.d.mts.map +1 -0
  12. package/exec-client.d.ts +101 -0
  13. package/exec-client.d.ts.map +1 -0
  14. package/exec-client.js +265 -0
  15. package/exec-client.js.map +1 -0
  16. package/exec-client.mjs +259 -0
  17. package/exec-client.mjs.map +1 -0
  18. package/folder-sync.d.mts +16 -2
  19. package/folder-sync.d.mts.map +1 -1
  20. package/folder-sync.d.ts +16 -2
  21. package/folder-sync.d.ts.map +1 -1
  22. package/folder-sync.js +43 -14
  23. package/folder-sync.js.map +1 -1
  24. package/folder-sync.mjs +43 -13
  25. package/folder-sync.mjs.map +1 -1
  26. package/index.d.mts +2 -0
  27. package/index.d.mts.map +1 -1
  28. package/index.d.ts +2 -0
  29. package/index.d.ts.map +1 -1
  30. package/index.js +5 -1
  31. package/index.js.map +1 -1
  32. package/index.mjs +2 -0
  33. package/index.mjs.map +1 -1
  34. package/internal/parse.d.mts.map +1 -1
  35. package/internal/parse.d.ts.map +1 -1
  36. package/internal/parse.js +5 -0
  37. package/internal/parse.js.map +1 -1
  38. package/internal/parse.mjs +5 -0
  39. package/internal/parse.mjs.map +1 -1
  40. package/ios-client.d.mts +60 -1
  41. package/ios-client.d.mts.map +1 -1
  42. package/ios-client.d.ts +60 -1
  43. package/ios-client.d.ts.map +1 -1
  44. package/ios-client.js +131 -2
  45. package/ios-client.js.map +1 -1
  46. package/ios-client.mjs +129 -1
  47. package/ios-client.mjs.map +1 -1
  48. package/package.json +23 -1
  49. package/sandbox-client.d.mts +124 -0
  50. package/sandbox-client.d.mts.map +1 -0
  51. package/sandbox-client.d.ts +124 -0
  52. package/sandbox-client.d.ts.map +1 -0
  53. package/sandbox-client.js +149 -0
  54. package/sandbox-client.js.map +1 -0
  55. package/sandbox-client.mjs +146 -0
  56. package/sandbox-client.mjs.map +1 -0
  57. package/src/client.ts +10 -2
  58. package/src/exec-client.ts +333 -0
  59. package/src/folder-sync.ts +66 -18
  60. package/src/index.ts +16 -0
  61. package/src/internal/parse.ts +6 -0
  62. package/src/ios-client.ts +207 -2
  63. package/src/sandbox-client.ts +267 -0
  64. package/src/version.ts +1 -1
  65. package/version.d.mts +1 -1
  66. package/version.d.ts +1 -1
  67. package/version.js +1 -1
  68. 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":"OAAO,EAAE,OAAO,IAAI,cAAc,EAA0B;OACrD,EAAE,IAAI,EAAoB;AAyGjC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAwC;IAExC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC;IAC5C,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE;YAC5B,IAAI,QAAQ,KAAK,OAAO;gBAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,CAAC;QACnE,CAAC;QACD,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE;YAC3B,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO;gBAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1F,CAAC;QACD,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE;YAC3B,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO;gBACpE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,KAAK,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE;YAC5B,IAAI,QAAQ,KAAK,MAAM;gBAAE,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,CAAC;QACpE,CAAC;KACF,CAAC;IAEF,MAAM,KAAK,GAAG,CAAC,KAA0C,EAAE,GAAW,EAAE,EAAE;QACxE,QAAQ,KAAK,EAAE,CAAC;YACd,KAAK,OAAO;gBACV,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAClB,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACjB,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACjB,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAClB,MAAM;YACR;gBACE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACjB,MAAM;QACV,CAAC;IACH,CAAC,CAAC;IAEF,iDAAiD;IACjD,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,GAAG,GAGL;YACF,eAAe,EAAE,OAAO,CAAC,SAAS,CAAC,MAAM;YACzC,cAAc,EAAE,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK;SACzD,CAAC;QAEF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,YAAY,EAAE;YACrD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE;aACzC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;SAC1B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,aAAqB,EAAE,IAAkB;YAClD,MAAM,YAAY,GAAsB;gBACtC,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,IAAI,EAAE,IAAI,EAAE,QAAQ,IAAI,eAAe;gBACvC,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,CAAC,YAAoB,EAAE,EAAE;oBAC/B,IACE,YAAY,CAAC,UAAU,CAAC,QAAQ,CAAC;wBACjC,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC;wBAClC,YAAY,CAAC,UAAU,CAAC,cAAc,CAAC;wBACvC,YAAY,CAAC,UAAU,CAAC,gBAAgB,CAAC;wBACzC,YAAY,CAAC,UAAU,CAAC,sBAAsB,CAAC;wBAC/C,YAAY,CAAC,UAAU,CAAC,eAAe,CAAC,EACxC,CAAC;wBACD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IACE,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC;wBACpC,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC;wBAChC,YAAY,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAC1C,CAAC;wBACD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IACE,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC;wBAChC,YAAY,CAAC,UAAU,CAAC,iBAAiB,CAAC;wBAC1C,YAAY,KAAK,WAAW;wBAC5B,YAAY,CAAC,QAAQ,CAAC,YAAY,CAAC,EACnC,CAAC;wBACD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IAAI,YAAY,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;wBAC1C,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IAAI,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;wBACpC,OAAO,KAAK,CAAC;oBACf,CAAC;oBAED,uBAAuB;oBACvB,IAAI,IAAI,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;wBAC/C,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrE,GAAG,CAAC,IAAI,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnF,GAAG,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3D,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,KAAK;aACxB,CAAC;YAEF,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;YACjE,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;gBACxB,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;YAC/C,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,UAAU,CAAC,IAAuB;YAChC,OAAO,IAAI,CACT,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,EAC5D;gBACE,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,GAAG,EAAE,KAAK;aACX,CACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
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
- if (signal) signal.addEventListener('abort', () => controller.abort());
540
+ const abort = this._makeAbort(controller);
541
+ if (signal) signal.addEventListener('abort', abort, { once: true });
541
542
 
542
- const timeout = setTimeout(() => controller.abort(), ms);
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
+ }