@ramarivera/chofi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1326 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +20 -0
- package/dist/discovery.d.ts +44 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +151 -0
- package/dist/drivers/apple.d.ts +68 -0
- package/dist/drivers/apple.d.ts.map +1 -0
- package/dist/drivers/apple.js +360 -0
- package/dist/drivers/expo.d.ts +14 -0
- package/dist/drivers/expo.d.ts.map +1 -0
- package/dist/drivers/expo.js +42 -0
- package/dist/drivers/idb.d.ts +38 -0
- package/dist/drivers/idb.d.ts.map +1 -0
- package/dist/drivers/idb.js +52 -0
- package/dist/drivers/maestro.d.ts +37 -0
- package/dist/drivers/maestro.d.ts.map +1 -0
- package/dist/drivers/maestro.js +64 -0
- package/dist/drivers/types.d.ts +23 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/events.d.ts +33 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +26 -0
- package/dist/executor.d.ts +11 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +17 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/planning.d.ts +18 -0
- package/dist/planning.d.ts.map +1 -0
- package/dist/planning.js +75 -0
- package/dist/runtime.d.ts +157 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +650 -0
- package/dist/safety.d.ts +8 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +84 -0
- package/dist/spawn.d.ts +30 -0
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +178 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +64 -0
- package/sophy.png +0 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { defaultSpawner } from "./spawn.js";
|
|
2
|
+
import { AppleDriver } from "./drivers/apple.js";
|
|
3
|
+
import { MaestroDriver } from "./drivers/maestro.js";
|
|
4
|
+
import { ExpoDriver } from "./drivers/expo.js";
|
|
5
|
+
import { IdbDriver } from "./drivers/idb.js";
|
|
6
|
+
import { SimulatorStateError, AppNotInstalledError, AppNotRunningError, BuildFailedError, TestsFailedError, ToolOutputError, classifyError } from "./errors.js";
|
|
7
|
+
export class RuntimeController {
|
|
8
|
+
apple;
|
|
9
|
+
maestro;
|
|
10
|
+
expo;
|
|
11
|
+
idb;
|
|
12
|
+
spawner;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.spawner = options.spawner ?? defaultSpawner;
|
|
15
|
+
this.apple = new AppleDriver(this.spawner);
|
|
16
|
+
this.maestro = new MaestroDriver(this.spawner);
|
|
17
|
+
this.expo = new ExpoDriver(this.spawner);
|
|
18
|
+
this.idb = new IdbDriver(this.spawner);
|
|
19
|
+
}
|
|
20
|
+
// === Simulator ===
|
|
21
|
+
async simList() {
|
|
22
|
+
return this.apple.listSimulators();
|
|
23
|
+
}
|
|
24
|
+
async simBoot(target) {
|
|
25
|
+
const device = await this.apple.getSimulatorState(target);
|
|
26
|
+
if (device?.state === "Booted") {
|
|
27
|
+
return; // Already booted, no-op
|
|
28
|
+
}
|
|
29
|
+
const result = await this.apple.bootSimulator(target);
|
|
30
|
+
if (result.exitCode !== 0) {
|
|
31
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to boot ${target}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async simShutdown(target) {
|
|
35
|
+
const device = await this.apple.getSimulatorState(target);
|
|
36
|
+
if (device?.state === "Shutdown") {
|
|
37
|
+
return; // Already shutdown, no-op
|
|
38
|
+
}
|
|
39
|
+
const result = await this.apple.shutdownSimulator(target);
|
|
40
|
+
if (result.exitCode !== 0) {
|
|
41
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to shutdown ${target}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async simOpen(target) {
|
|
45
|
+
const result = await this.apple.openSimulator(target);
|
|
46
|
+
if (result.exitCode !== 0) {
|
|
47
|
+
throw new ToolOutputError("open", result.stderr || `Failed to open ${target}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async simErase(target) {
|
|
51
|
+
const device = await this.apple.getSimulatorState(target);
|
|
52
|
+
if (device?.state === "Booted") {
|
|
53
|
+
await this.simShutdown(target);
|
|
54
|
+
}
|
|
55
|
+
const result = await this.apple.eraseSimulator(target);
|
|
56
|
+
if (result.exitCode !== 0) {
|
|
57
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to erase ${target}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async simScreenshot(target, outputPath) {
|
|
61
|
+
const device = await this.apple.getSimulatorState(target);
|
|
62
|
+
if (!device) {
|
|
63
|
+
throw new Error(`Simulator not found: ${target}`);
|
|
64
|
+
}
|
|
65
|
+
if (device.state !== "Booted") {
|
|
66
|
+
throw new SimulatorStateError(device.udid, "Booted", device.state);
|
|
67
|
+
}
|
|
68
|
+
const result = await this.apple.screenshot(device.udid, outputPath);
|
|
69
|
+
if (result.exitCode !== 0) {
|
|
70
|
+
throw new ToolOutputError("simctl", result.stderr || `Screenshot failed for ${target}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async simCreate(name, deviceType, runtime) {
|
|
74
|
+
const result = await this.apple.createSimulator(name, deviceType, runtime);
|
|
75
|
+
if (result.exitCode !== 0) {
|
|
76
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to create simulator ${name}`);
|
|
77
|
+
}
|
|
78
|
+
return result.stdout.trim();
|
|
79
|
+
}
|
|
80
|
+
async simDelete(target) {
|
|
81
|
+
const result = await this.apple.deleteSimulator(target);
|
|
82
|
+
if (result.exitCode !== 0) {
|
|
83
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to delete ${target}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async simRuntimeList() {
|
|
87
|
+
return this.apple.listRuntimes();
|
|
88
|
+
}
|
|
89
|
+
async simDeviceTypeList() {
|
|
90
|
+
return this.apple.listDeviceTypes();
|
|
91
|
+
}
|
|
92
|
+
async simClone(source, name) {
|
|
93
|
+
const result = await this.apple.cloneSimulator(source, name);
|
|
94
|
+
if (result.exitCode !== 0) {
|
|
95
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to clone ${source}`);
|
|
96
|
+
}
|
|
97
|
+
return result.stdout.trim();
|
|
98
|
+
}
|
|
99
|
+
async simClearCache(target) {
|
|
100
|
+
const result = await this.apple.clearSimulatorCache(target);
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to clear cache for ${target}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async simSetAppearance(target, appearance) {
|
|
106
|
+
const result = await this.apple.setAppearance(target, appearance);
|
|
107
|
+
if (result.exitCode !== 0) {
|
|
108
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to set appearance for ${target}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async simStartRecording(udid, outputPath) {
|
|
112
|
+
const result = await this.apple.startVideoRecording(udid, outputPath);
|
|
113
|
+
if (result.exitCode !== 0) {
|
|
114
|
+
throw new ToolOutputError("simctl", result.stderr || "Failed to start recording");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async simStopRecording(udid) {
|
|
118
|
+
const result = await this.apple.stopVideoRecording(udid);
|
|
119
|
+
if (result.exitCode !== 0) {
|
|
120
|
+
throw new ToolOutputError("simctl", result.stderr || "Failed to stop recording");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async simAddMedia(udid, mediaPaths) {
|
|
124
|
+
const result = await this.apple.addMedia(udid, mediaPaths);
|
|
125
|
+
if (result.exitCode !== 0) {
|
|
126
|
+
throw new ToolOutputError("simctl", result.stderr || "Failed to add media");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async simRunningApps(udid) {
|
|
130
|
+
return this.apple.listRunningApps(udid);
|
|
131
|
+
}
|
|
132
|
+
// === Device ===
|
|
133
|
+
async deviceList(platform) {
|
|
134
|
+
const devices = await this.apple.listDevices();
|
|
135
|
+
if (platform) {
|
|
136
|
+
return devices.filter((d) => d.status.toLowerCase() === platform.toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
return devices;
|
|
139
|
+
}
|
|
140
|
+
// === App Lifecycle ===
|
|
141
|
+
async appInstall(udid, appPath) {
|
|
142
|
+
const result = await this.apple.installApp(udid, appPath);
|
|
143
|
+
if (result.exitCode !== 0) {
|
|
144
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to install ${appPath}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async appLaunch(udid, bundleId) {
|
|
148
|
+
const installed = await this.apple.isAppInstalled(udid, bundleId);
|
|
149
|
+
if (!installed) {
|
|
150
|
+
throw new AppNotInstalledError(bundleId, udid);
|
|
151
|
+
}
|
|
152
|
+
const result = await this.apple.launchApp(udid, bundleId);
|
|
153
|
+
if (result.exitCode !== 0) {
|
|
154
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to launch ${bundleId}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async appTerminate(udid, bundleId, force = false) {
|
|
158
|
+
if (force) {
|
|
159
|
+
const result = await this.spawner.run("xcrun", ["simctl", "spawn", udid, "launchctl", "kill", "9", bundleId]);
|
|
160
|
+
if (result.exitCode !== 0) {
|
|
161
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to force-terminate ${bundleId}`);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const apps = await this.apple.listRunningApps(udid);
|
|
166
|
+
const running = apps.some((a) => a.bundleId === bundleId);
|
|
167
|
+
if (!running) {
|
|
168
|
+
throw new AppNotRunningError(bundleId, udid);
|
|
169
|
+
}
|
|
170
|
+
const result = await this.apple.terminateApp(udid, bundleId);
|
|
171
|
+
if (result.exitCode !== 0) {
|
|
172
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to terminate ${bundleId}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async appTerminateAll(udid, force = false) {
|
|
176
|
+
const apps = await this.apple.listRunningApps(udid);
|
|
177
|
+
for (const app of apps) {
|
|
178
|
+
await this.appTerminate(udid, app.bundleId, force);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async appUninstall(udid, bundleId) {
|
|
182
|
+
const result = await this.apple.uninstallApp(udid, bundleId);
|
|
183
|
+
if (result.exitCode !== 0) {
|
|
184
|
+
throw new ToolOutputError("simctl", result.stderr || `Failed to uninstall ${bundleId}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async appListRunning(udid) {
|
|
188
|
+
return this.apple.listRunningApps(udid);
|
|
189
|
+
}
|
|
190
|
+
async simPrune() {
|
|
191
|
+
const result = await this.apple.pruneUnavailableSimulators();
|
|
192
|
+
if (result.exitCode !== 0) {
|
|
193
|
+
throw new ToolOutputError("simctl", result.stderr || "Failed to prune simulators");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// === Logs ===
|
|
197
|
+
async streamLogs(udid, timeoutMs = 30_000) {
|
|
198
|
+
const result = await this.apple.streamLogs(udid, timeoutMs);
|
|
199
|
+
return result.stdout;
|
|
200
|
+
}
|
|
201
|
+
// === Build / Test / Clean ===
|
|
202
|
+
async build(cwd, scheme, options = {}) {
|
|
203
|
+
const args = buildArgs(scheme, options);
|
|
204
|
+
args.push("build");
|
|
205
|
+
const result = await this.apple.xcodebuild(args, cwd);
|
|
206
|
+
if (result.exitCode !== 0) {
|
|
207
|
+
throw new BuildFailedError(result.stderr);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async buildWithProgress(cwd, scheme, onPhase, options = {}) {
|
|
211
|
+
const args = buildArgs(scheme, options);
|
|
212
|
+
args.push("build");
|
|
213
|
+
const result = await this.apple.xcodebuildStream(args, (line) => {
|
|
214
|
+
const phase = parseBuildPhase(line);
|
|
215
|
+
if (phase) {
|
|
216
|
+
onPhase(phase);
|
|
217
|
+
}
|
|
218
|
+
}, cwd);
|
|
219
|
+
if (result.exitCode !== 0) {
|
|
220
|
+
throw new BuildFailedError("Build failed");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async test(cwd, scheme, options = {}) {
|
|
224
|
+
const args = buildArgs(scheme, options);
|
|
225
|
+
args.push("test");
|
|
226
|
+
if (options.only && options.only.length > 0) {
|
|
227
|
+
for (const test of options.only) {
|
|
228
|
+
args.push("-only-testing", test);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (options.skip && options.skip.length > 0) {
|
|
232
|
+
for (const test of options.skip) {
|
|
233
|
+
args.push("-skip-testing", test);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (options.retry) {
|
|
237
|
+
args.push("-retry-tests-on-failure");
|
|
238
|
+
}
|
|
239
|
+
const result = await this.apple.xcodebuild(args, cwd);
|
|
240
|
+
if (result.exitCode !== 0) {
|
|
241
|
+
throw new TestsFailedError(result.stderr);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async testWithResults(cwd, scheme, options = {}) {
|
|
245
|
+
const args = buildArgs(scheme, options);
|
|
246
|
+
args.push("test");
|
|
247
|
+
const result = await this.apple.xcodebuild(args, cwd);
|
|
248
|
+
// Attempt to find and parse .xcresult bundle
|
|
249
|
+
const xcresultPath = findXcresultPath(result.stdout, result.stderr);
|
|
250
|
+
if (xcresultPath) {
|
|
251
|
+
try {
|
|
252
|
+
const testsResult = await this.apple.xcresultListTests(xcresultPath);
|
|
253
|
+
if (testsResult.exitCode === 0) {
|
|
254
|
+
return parseXcresultTests(testsResult.stdout);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Fallback to empty results if xcresulttool fails
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (result.exitCode !== 0) {
|
|
262
|
+
throw new TestsFailedError(result.stderr);
|
|
263
|
+
}
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
async getTestResults(xcresultPath) {
|
|
267
|
+
const result = await this.apple.xcresultListTests(xcresultPath);
|
|
268
|
+
if (result.exitCode !== 0) {
|
|
269
|
+
throw new ToolOutputError("xcresulttool", result.stderr || "Failed to read test results");
|
|
270
|
+
}
|
|
271
|
+
return parseXcresultTests(result.stdout);
|
|
272
|
+
}
|
|
273
|
+
async clean(cwd, scheme, options = {}) {
|
|
274
|
+
if (options.derivedData || options.xcodeDerivedData || options.xcodeCache) {
|
|
275
|
+
// Custom clean operations
|
|
276
|
+
if (options.derivedData) {
|
|
277
|
+
await this.spawner.run("rm", ["-rf", "~/Library/Developer/Xcode/DerivedData"], { cwd });
|
|
278
|
+
}
|
|
279
|
+
if (options.xcodeDerivedData) {
|
|
280
|
+
await this.spawner.run("rm", ["-rf", "~/Library/Developer/Xcode/DerivedData"]);
|
|
281
|
+
}
|
|
282
|
+
if (options.xcodeCache) {
|
|
283
|
+
await this.spawner.run("rm", ["-rf", "~/Library/Caches/com.apple.dt.Xcode"]);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const args = buildArgs(scheme, options);
|
|
288
|
+
args.push("clean");
|
|
289
|
+
const result = await this.apple.xcodebuild(args, cwd);
|
|
290
|
+
if (result.exitCode !== 0) {
|
|
291
|
+
throw new ToolOutputError("xcodebuild", result.stderr || "Clean failed");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// === Expo ===
|
|
295
|
+
async prebuild(cwd, platform) {
|
|
296
|
+
const result = await this.expo.prebuild(cwd, platform);
|
|
297
|
+
if (result.exitCode !== 0) {
|
|
298
|
+
throw new ToolOutputError("expo", result.stderr || "Expo prebuild failed");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async runExpoIos(cwd, device) {
|
|
302
|
+
const result = await this.expo.runIos(cwd, device);
|
|
303
|
+
if (result.exitCode !== 0) {
|
|
304
|
+
throw new ToolOutputError("expo", result.stderr || "Expo iOS run failed");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async runExpoAndroid(cwd, device) {
|
|
308
|
+
const result = await this.expo.runAndroid(cwd, device);
|
|
309
|
+
if (result.exitCode !== 0) {
|
|
310
|
+
throw new ToolOutputError("expo", result.stderr || "Expo Android run failed");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// === Maestro ===
|
|
314
|
+
async runMaestroTest(flowPath, options) {
|
|
315
|
+
const result = await this.maestro.test(flowPath, options);
|
|
316
|
+
if (result.exitCode !== 0) {
|
|
317
|
+
throw new ToolOutputError("maestro", result.stderr || `Maestro test failed for ${flowPath}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async runMaestroContinuous(flowPath, options) {
|
|
321
|
+
const result = await this.maestro.testContinuous({ ...options, flowPath });
|
|
322
|
+
if (result.exitCode !== 0) {
|
|
323
|
+
throw new ToolOutputError("maestro", result.stderr || `Maestro continuous test failed for ${flowPath}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async runMaestroHierarchy(options) {
|
|
327
|
+
const result = await this.maestro.hierarchy(options);
|
|
328
|
+
if (result.exitCode !== 0) {
|
|
329
|
+
throw new ToolOutputError("maestro", result.stderr || "Maestro hierarchy failed");
|
|
330
|
+
}
|
|
331
|
+
return result.stdout;
|
|
332
|
+
}
|
|
333
|
+
async runMaestroRecord(flowPath, options) {
|
|
334
|
+
const result = await this.maestro.record({ ...options, flowPath });
|
|
335
|
+
if (result.exitCode !== 0) {
|
|
336
|
+
throw new ToolOutputError("maestro", result.stderr || `Maestro record failed for ${flowPath}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async maestroDriverSetup(cwd) {
|
|
340
|
+
const result = await this.maestro.driverSetup(cwd);
|
|
341
|
+
if (result.exitCode !== 0) {
|
|
342
|
+
throw new ToolOutputError("maestro", result.stderr || "Maestro driver setup failed");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async maestroStartDevice(options) {
|
|
346
|
+
const result = await this.maestro.startDevice(options);
|
|
347
|
+
if (result.exitCode !== 0) {
|
|
348
|
+
throw new ToolOutputError("maestro", result.stderr || "Maestro start-device failed");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// === Test Discovery ===
|
|
352
|
+
async testWithProgress(cwd, scheme, onProgress, options = {}) {
|
|
353
|
+
const args = buildArgs(scheme, options);
|
|
354
|
+
args.push("test");
|
|
355
|
+
if (options.only && options.only.length > 0) {
|
|
356
|
+
for (const test of options.only) {
|
|
357
|
+
args.push("-only-testing", test);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (options.skip && options.skip.length > 0) {
|
|
361
|
+
for (const test of options.skip) {
|
|
362
|
+
args.push("-skip-testing", test);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (options.retry) {
|
|
366
|
+
args.push("-retry-tests-on-failure");
|
|
367
|
+
}
|
|
368
|
+
const result = await this.apple.xcodebuildStream(args, (line) => {
|
|
369
|
+
const progress = parseTestProgress(line);
|
|
370
|
+
if (progress) {
|
|
371
|
+
onProgress(progress);
|
|
372
|
+
}
|
|
373
|
+
}, cwd);
|
|
374
|
+
if (result.exitCode !== 0) {
|
|
375
|
+
throw new TestsFailedError("Tests failed");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async projectBuildConfigs(cwd, options = {}) {
|
|
379
|
+
const result = await this.apple.listBuildConfigurations(cwd, options);
|
|
380
|
+
if (result.exitCode !== 0) {
|
|
381
|
+
throw new ToolOutputError("xcodebuild", result.stderr || "Failed to list build configurations");
|
|
382
|
+
}
|
|
383
|
+
return parseBuildConfigs(result.stdout);
|
|
384
|
+
}
|
|
385
|
+
async detectSchemes(cwd, options = {}) {
|
|
386
|
+
const result = await this.apple.listSchemes(cwd, options);
|
|
387
|
+
if (result.exitCode !== 0) {
|
|
388
|
+
throw new ToolOutputError("xcodebuild", result.stderr || "Failed to list schemes");
|
|
389
|
+
}
|
|
390
|
+
return parseSchemes(result.stdout);
|
|
391
|
+
}
|
|
392
|
+
async discoverTests(cwd, scheme, options = {}) {
|
|
393
|
+
const args = buildArgs(scheme, options);
|
|
394
|
+
args.push("-quiet", "test-without-building", "-list-tests");
|
|
395
|
+
const result = await this.apple.xcodebuild(args, cwd);
|
|
396
|
+
if (result.exitCode !== 0) {
|
|
397
|
+
throw new ToolOutputError("xcodebuild", result.stderr || "Test discovery failed");
|
|
398
|
+
}
|
|
399
|
+
return parseTestList(result.stdout);
|
|
400
|
+
}
|
|
401
|
+
// === idb: Accessibility ===
|
|
402
|
+
async idbDescribeAll(udid) {
|
|
403
|
+
const result = await this.idb.describeAll(udid);
|
|
404
|
+
if (result.exitCode !== 0) {
|
|
405
|
+
throw new ToolOutputError("idb", result.stderr || "Failed to describe accessibility");
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
return JSON.parse(result.stdout);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return result.stdout;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async idbDescribePoint(udid, x, y) {
|
|
415
|
+
const result = await this.idb.describePoint(udid, x, y);
|
|
416
|
+
if (result.exitCode !== 0) {
|
|
417
|
+
throw new ToolOutputError("idb", result.stderr || "Failed to describe point");
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
return JSON.parse(result.stdout);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return result.stdout;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// === idb: HID / UI ===
|
|
427
|
+
async idbTap(udid, x, y) {
|
|
428
|
+
const result = await this.idb.tap(udid, x, y);
|
|
429
|
+
if (result.exitCode !== 0) {
|
|
430
|
+
throw new ToolOutputError("idb", result.stderr || "Tap failed");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async idbSwipe(udid, x1, y1, x2, y2) {
|
|
434
|
+
const result = await this.idb.swipe(udid, x1, y1, x2, y2);
|
|
435
|
+
if (result.exitCode !== 0) {
|
|
436
|
+
throw new ToolOutputError("idb", result.stderr || "Swipe failed");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async idbPressButton(udid, button) {
|
|
440
|
+
const result = await this.idb.pressButton(udid, button);
|
|
441
|
+
if (result.exitCode !== 0) {
|
|
442
|
+
throw new ToolOutputError("idb", result.stderr || `Button ${button} failed`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async idbInputText(udid, text) {
|
|
446
|
+
const result = await this.idb.inputText(udid, text);
|
|
447
|
+
if (result.exitCode !== 0) {
|
|
448
|
+
throw new ToolOutputError("idb", result.stderr || "Text input failed");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// === idb: Crash Logs ===
|
|
452
|
+
async idbListCrashes(udid) {
|
|
453
|
+
const result = await this.idb.listCrashes(udid);
|
|
454
|
+
if (result.exitCode !== 0) {
|
|
455
|
+
throw new ToolOutputError("idb", result.stderr || "Failed to list crashes");
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(result.stdout);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async idbShowCrash(udid, name) {
|
|
465
|
+
const result = await this.idb.showCrash(udid, name);
|
|
466
|
+
if (result.exitCode !== 0) {
|
|
467
|
+
throw new ToolOutputError("idb", result.stderr || `Failed to show crash ${name}`);
|
|
468
|
+
}
|
|
469
|
+
return result.stdout;
|
|
470
|
+
}
|
|
471
|
+
async idbDeleteCrash(udid, name) {
|
|
472
|
+
const result = await this.idb.deleteCrash(udid, name);
|
|
473
|
+
if (result.exitCode !== 0) {
|
|
474
|
+
throw new ToolOutputError("idb", result.stderr || `Failed to delete crash ${name}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// === idb: Settings / Permissions ===
|
|
478
|
+
async idbApprove(udid, bundleId, permissions) {
|
|
479
|
+
const result = await this.idb.approve(udid, bundleId, permissions);
|
|
480
|
+
if (result.exitCode !== 0) {
|
|
481
|
+
throw new ToolOutputError("idb", result.stderr || "Failed to approve permissions");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// === idb: Location ===
|
|
485
|
+
async idbSetLocation(udid, latitude, longitude) {
|
|
486
|
+
const result = await this.idb.setLocation(udid, latitude, longitude);
|
|
487
|
+
if (result.exitCode !== 0) {
|
|
488
|
+
throw new ToolOutputError("idb", result.stderr || "Failed to set location");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// === idb: Focus ===
|
|
492
|
+
async idbFocus(udid) {
|
|
493
|
+
const result = await this.idb.focus(udid);
|
|
494
|
+
if (result.exitCode !== 0) {
|
|
495
|
+
throw new ToolOutputError("idb", result.stderr || "Failed to focus simulator");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function buildArgs(scheme, options) {
|
|
500
|
+
const args = [];
|
|
501
|
+
if (options.workspace) {
|
|
502
|
+
args.push("-workspace", options.workspace);
|
|
503
|
+
}
|
|
504
|
+
else if (options.project) {
|
|
505
|
+
args.push("-project", options.project);
|
|
506
|
+
}
|
|
507
|
+
args.push("-scheme", scheme);
|
|
508
|
+
if (options.destination) {
|
|
509
|
+
args.push("-destination", options.destination);
|
|
510
|
+
}
|
|
511
|
+
return args;
|
|
512
|
+
}
|
|
513
|
+
function parseTestList(stdout) {
|
|
514
|
+
const tests = [];
|
|
515
|
+
for (const line of stdout.split("\n")) {
|
|
516
|
+
const trimmed = line.trim();
|
|
517
|
+
if (trimmed && trimmed.includes("/") && !trimmed.startsWith(" ")) {
|
|
518
|
+
tests.push(trimmed);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return tests;
|
|
522
|
+
}
|
|
523
|
+
function parseBuildPhase(line) {
|
|
524
|
+
// CompileSwift / CompileC / CompileSwiftSources
|
|
525
|
+
const compileMatch = /Compile\w+\s+(?:\[.*\]\s+)?(.+)\s+\((in target '([^']+)'|in target \`([^']+)\`)/.exec(line);
|
|
526
|
+
if (compileMatch) {
|
|
527
|
+
return { phase: "compile", target: compileMatch[3] || compileMatch[4], file: compileMatch[1]?.trim() };
|
|
528
|
+
}
|
|
529
|
+
// Link
|
|
530
|
+
const linkMatch = /Ld\s+(.+)\s+\(in target '([^']+)'/.exec(line);
|
|
531
|
+
if (linkMatch) {
|
|
532
|
+
return { phase: "link", target: linkMatch[2], file: linkMatch[1]?.trim() };
|
|
533
|
+
}
|
|
534
|
+
// CodeSign
|
|
535
|
+
const signMatch = /CodeSign\s+(.+)\s+\(in target '([^']+)'/.exec(line);
|
|
536
|
+
if (signMatch) {
|
|
537
|
+
return { phase: "codesign", target: signMatch[2], file: signMatch[1]?.trim() };
|
|
538
|
+
}
|
|
539
|
+
// Copy
|
|
540
|
+
const copyMatch = /CpResource\s+(.+)\s+\(in target '([^']+)'/.exec(line);
|
|
541
|
+
if (copyMatch) {
|
|
542
|
+
return { phase: "copy", target: copyMatch[2], file: copyMatch[1]?.trim() };
|
|
543
|
+
}
|
|
544
|
+
// ProcessInfoPlist
|
|
545
|
+
const plistMatch = /ProcessInfoPlistFile\s+(.+)\s+\(in target '([^']+)'/.exec(line);
|
|
546
|
+
if (plistMatch) {
|
|
547
|
+
return { phase: "process-plist", target: plistMatch[2], file: plistMatch[1]?.trim() };
|
|
548
|
+
}
|
|
549
|
+
// Build success/failure
|
|
550
|
+
if (line.includes("** BUILD SUCCEEDED **")) {
|
|
551
|
+
return { phase: "succeeded" };
|
|
552
|
+
}
|
|
553
|
+
if (line.includes("** BUILD FAILED **")) {
|
|
554
|
+
return { phase: "failed" };
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
function findXcresultPath(stdout, stderr) {
|
|
559
|
+
const combined = stdout + "\n" + stderr;
|
|
560
|
+
const match = /Test results saved to:\s+(.+\.xcresult)/.exec(combined);
|
|
561
|
+
if (match && match[1]) {
|
|
562
|
+
return match[1].trim();
|
|
563
|
+
}
|
|
564
|
+
// Also check for derived data path
|
|
565
|
+
const derivedMatch = /-resultBundlePath\s+(.+\.xcresult)/.exec(combined);
|
|
566
|
+
if (derivedMatch && derivedMatch[1]) {
|
|
567
|
+
return derivedMatch[1].trim();
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
function parseTestProgress(line) {
|
|
572
|
+
// Test suite started
|
|
573
|
+
if (line.includes("Test Suite") && line.includes("started")) {
|
|
574
|
+
return { status: "suite-started" };
|
|
575
|
+
}
|
|
576
|
+
// Test suite finished
|
|
577
|
+
if (line.includes("Test Suite") && line.includes("finished")) {
|
|
578
|
+
return { status: "suite-finished" };
|
|
579
|
+
}
|
|
580
|
+
// Test case started
|
|
581
|
+
const startedMatch = /Test Case '-\[([^\]]+)\]' started/.exec(line);
|
|
582
|
+
if (startedMatch) {
|
|
583
|
+
return { testName: startedMatch[1], status: "started" };
|
|
584
|
+
}
|
|
585
|
+
// Test case passed
|
|
586
|
+
const passedMatch = /Test Case '-\[([^\]]+)\]' passed \(([^)]+)\)/.exec(line);
|
|
587
|
+
if (passedMatch) {
|
|
588
|
+
const durationMs = parseDuration(passedMatch[2]);
|
|
589
|
+
return { testName: passedMatch[1], status: "passed", durationMs };
|
|
590
|
+
}
|
|
591
|
+
// Test case failed
|
|
592
|
+
const failedMatch = /Test Case '-\[([^\]]+)\]' failed \(([^)]+)\)/.exec(line);
|
|
593
|
+
if (failedMatch) {
|
|
594
|
+
const durationMs = parseDuration(failedMatch[2]);
|
|
595
|
+
return { testName: failedMatch[1], status: "failed", durationMs };
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
function parseDuration(durationStr) {
|
|
600
|
+
const seconds = Number.parseFloat(durationStr.replace(" seconds", ""));
|
|
601
|
+
return Number.isNaN(seconds) ? undefined : Math.round(seconds * 1000);
|
|
602
|
+
}
|
|
603
|
+
function parseBuildConfigs(stdout) {
|
|
604
|
+
const configs = [];
|
|
605
|
+
const match = /Build Configurations:\s+(.+)/.exec(stdout);
|
|
606
|
+
if (match && match[1]) {
|
|
607
|
+
configs.push(...match[1].split(", ").map((c) => c.trim()));
|
|
608
|
+
}
|
|
609
|
+
return configs;
|
|
610
|
+
}
|
|
611
|
+
function parseSchemes(stdout) {
|
|
612
|
+
const schemes = [];
|
|
613
|
+
const lines = stdout.split("\n");
|
|
614
|
+
let inSchemes = false;
|
|
615
|
+
for (const line of lines) {
|
|
616
|
+
const trimmed = line.trim();
|
|
617
|
+
if (trimmed === "Schemes:") {
|
|
618
|
+
inSchemes = true;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (inSchemes) {
|
|
622
|
+
if (trimmed === "" || trimmed.endsWith(":")) {
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
schemes.push(trimmed);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return schemes;
|
|
629
|
+
}
|
|
630
|
+
function parseXcresultTests(stdout) {
|
|
631
|
+
const results = [];
|
|
632
|
+
try {
|
|
633
|
+
const parsed = JSON.parse(stdout);
|
|
634
|
+
for (const test of parsed.tests ?? []) {
|
|
635
|
+
const name = test.testName || test.identifier || "unknown";
|
|
636
|
+
const passed = test.testStatus === "Success" || test.testStatus === "success";
|
|
637
|
+
const failureMessage = test.failureSummaries?.[0]?.message;
|
|
638
|
+
results.push({
|
|
639
|
+
testName: name,
|
|
640
|
+
passed,
|
|
641
|
+
durationMs: test.duration ? Math.round(test.duration * 1000) : undefined,
|
|
642
|
+
failureMessage
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// If xcresulttool returns non-JSON, return empty
|
|
648
|
+
}
|
|
649
|
+
return results;
|
|
650
|
+
}
|
package/dist/safety.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SafetyGate {
|
|
2
|
+
readonly requiresConfirmation: boolean;
|
|
3
|
+
readonly description: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const safetyGates: Record<string, SafetyGate>;
|
|
6
|
+
export declare function isConfirmed(phase: string, flags: readonly string[]): boolean;
|
|
7
|
+
export declare function requireConfirmation(phase: string): string;
|
|
8
|
+
//# sourceMappingURL=safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../src/safety.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAuElD,CAAC;AAEF,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAMT;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIzD"}
|