@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.
Files changed (52) hide show
  1. package/README.md +257 -0
  2. package/dist/cli.d.ts +18 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +1326 -0
  5. package/dist/config.d.ts +10 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +20 -0
  8. package/dist/discovery.d.ts +44 -0
  9. package/dist/discovery.d.ts.map +1 -0
  10. package/dist/discovery.js +151 -0
  11. package/dist/drivers/apple.d.ts +68 -0
  12. package/dist/drivers/apple.d.ts.map +1 -0
  13. package/dist/drivers/apple.js +360 -0
  14. package/dist/drivers/expo.d.ts +14 -0
  15. package/dist/drivers/expo.d.ts.map +1 -0
  16. package/dist/drivers/expo.js +42 -0
  17. package/dist/drivers/idb.d.ts +38 -0
  18. package/dist/drivers/idb.d.ts.map +1 -0
  19. package/dist/drivers/idb.js +52 -0
  20. package/dist/drivers/maestro.d.ts +37 -0
  21. package/dist/drivers/maestro.d.ts.map +1 -0
  22. package/dist/drivers/maestro.js +64 -0
  23. package/dist/drivers/types.d.ts +23 -0
  24. package/dist/drivers/types.d.ts.map +1 -0
  25. package/dist/drivers/types.js +1 -0
  26. package/dist/errors.d.ts +31 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +59 -0
  29. package/dist/events.d.ts +33 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +26 -0
  32. package/dist/executor.d.ts +11 -0
  33. package/dist/executor.d.ts.map +1 -0
  34. package/dist/executor.js +17 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +13 -0
  38. package/dist/planning.d.ts +18 -0
  39. package/dist/planning.d.ts.map +1 -0
  40. package/dist/planning.js +75 -0
  41. package/dist/runtime.d.ts +157 -0
  42. package/dist/runtime.d.ts.map +1 -0
  43. package/dist/runtime.js +650 -0
  44. package/dist/safety.d.ts +8 -0
  45. package/dist/safety.d.ts.map +1 -0
  46. package/dist/safety.js +84 -0
  47. package/dist/spawn.d.ts +30 -0
  48. package/dist/spawn.d.ts.map +1 -0
  49. package/dist/spawn.js +178 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -0
  51. package/package.json +64 -0
  52. package/sophy.png +0 -0
@@ -0,0 +1,10 @@
1
+ export interface ChofiConfig {
2
+ readonly defaultPlatform?: "ios" | "android";
3
+ readonly defaultSimulator?: string;
4
+ readonly appBundleId?: string;
5
+ readonly maestroFlowDir?: string;
6
+ readonly outputDir?: string;
7
+ }
8
+ export declare function readConfig(repoRoot: string): ChofiConfig;
9
+ export declare function writeConfig(repoRoot: string, config: ChofiConfig): void;
10
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAUxD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAGvE"}
package/dist/config.js ADDED
@@ -0,0 +1,20 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ConfigParseError } from "./errors.js";
4
+ const configFileName = ".chofi.json";
5
+ export function readConfig(repoRoot) {
6
+ const path = join(repoRoot, configFileName);
7
+ if (!existsSync(path)) {
8
+ return {};
9
+ }
10
+ try {
11
+ return JSON.parse(readFileSync(path, "utf8"));
12
+ }
13
+ catch (cause) {
14
+ throw new ConfigParseError(path, String(cause));
15
+ }
16
+ }
17
+ export function writeConfig(repoRoot, config) {
18
+ const path = join(repoRoot, configFileName);
19
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
20
+ }
@@ -0,0 +1,44 @@
1
+ export interface PackageJson {
2
+ readonly dependencies?: Record<string, string>;
3
+ readonly devDependencies?: Record<string, string>;
4
+ readonly main?: string;
5
+ readonly name?: string;
6
+ readonly packageManager?: string;
7
+ readonly scripts?: Record<string, string>;
8
+ readonly version?: string;
9
+ }
10
+ export interface ToolAvailability {
11
+ readonly command: string;
12
+ readonly detail?: string | undefined;
13
+ readonly status: "available" | "missing";
14
+ }
15
+ export interface MobileProjectContext {
16
+ readonly detectedPlatforms: readonly string[];
17
+ readonly expoVersion?: string | undefined;
18
+ readonly flowdeck: ToolAvailability;
19
+ readonly maestro: ToolAvailability;
20
+ readonly maestroFlows: readonly string[];
21
+ readonly mobilePackageName: string;
22
+ readonly mobilePath: string;
23
+ readonly mobileScripts: Record<string, string>;
24
+ readonly packageManager: string;
25
+ readonly reactNativeVersion?: string | undefined;
26
+ readonly repoRoot: string;
27
+ readonly xcodeDeckReferenceExists: boolean;
28
+ }
29
+ export interface ToolChecker {
30
+ check(command: string): ToolAvailability;
31
+ }
32
+ export declare class PathToolChecker implements ToolChecker {
33
+ check(command: string): ToolAvailability;
34
+ }
35
+ export declare function discoverMobileProjectContext(input?: {
36
+ readonly cwd?: string | undefined;
37
+ readonly pathEnv?: string | undefined;
38
+ readonly toolChecker?: ToolChecker | undefined;
39
+ }): MobileProjectContext;
40
+ export declare function findProjectRoot(startDirectory: string): string;
41
+ export declare function discoverMaestroFlows(repoRoot: string): readonly string[];
42
+ export declare function checkCommandOnPath(command: string, pathEnv?: string): ToolAvailability;
43
+ export declare function pathExistsAsFile(path: string): boolean;
44
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAAC;CAC1C;AAED,qBAAa,eAAgB,YAAW,WAAW;IACjD,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB;CAiCzC;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,GAAE;IACL,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C,GACL,oBAAoB,CA2BtB;AAED,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAoB9D;AAoBD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAUxE;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,SAAyB,GAC/B,gBAAgB,CAoBlB;AAmCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD"}
@@ -0,0 +1,151 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { delimiter } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ export class PathToolChecker {
6
+ check(command) {
7
+ const version = spawnSync(command, ["--version"], {
8
+ encoding: "utf8",
9
+ stdio: ["ignore", "pipe", "pipe"]
10
+ });
11
+ if (version.status === 0) {
12
+ return {
13
+ command,
14
+ detail: firstLine(version.stdout) ?? firstLine(version.stderr),
15
+ status: "available"
16
+ };
17
+ }
18
+ const help = spawnSync(command, ["--help"], {
19
+ encoding: "utf8",
20
+ stdio: ["ignore", "pipe", "pipe"]
21
+ });
22
+ if (help.status === 0) {
23
+ return {
24
+ command,
25
+ detail: firstLine(help.stdout) ?? firstLine(help.stderr),
26
+ status: "available"
27
+ };
28
+ }
29
+ return {
30
+ command,
31
+ detail: version.error?.message ?? help.error?.message,
32
+ status: "missing"
33
+ };
34
+ }
35
+ }
36
+ export function discoverMobileProjectContext(input = {}) {
37
+ const repoRoot = findProjectRoot(input.cwd ?? process.cwd());
38
+ const mobilePath = findMobilePath(repoRoot);
39
+ const rootPackage = readPackageJson(join(repoRoot, "package.json"));
40
+ const mobilePackage = readPackageJson(join(mobilePath, "package.json"));
41
+ const dependencies = {
42
+ ...mobilePackage.dependencies,
43
+ ...mobilePackage.devDependencies
44
+ };
45
+ const checker = input.toolChecker ?? new PathToolChecker();
46
+ return {
47
+ repoRoot,
48
+ mobilePath,
49
+ packageManager: rootPackage.packageManager ?? "pnpm",
50
+ mobilePackageName: mobilePackage.name ?? "mobile",
51
+ mobileScripts: mobilePackage.scripts ?? {},
52
+ expoVersion: dependencies.expo,
53
+ reactNativeVersion: dependencies["react-native"],
54
+ detectedPlatforms: detectPlatforms(mobilePath),
55
+ maestro: checker.check("maestro"),
56
+ flowdeck: checkCommandOnPath("flowdeck", input.pathEnv),
57
+ xcodeDeckReferenceExists: existsSync(join(repoRoot, ".context", "XcodeDeck")),
58
+ maestroFlows: discoverMaestroFlows(repoRoot)
59
+ };
60
+ }
61
+ export function findProjectRoot(startDirectory) {
62
+ let current = resolve(startDirectory);
63
+ // Find the outermost directory with a package.json that is a workspace root
64
+ // or contains a mobile app subdirectory
65
+ let outermost;
66
+ while (true) {
67
+ if (existsSync(join(current, "package.json"))) {
68
+ outermost = current;
69
+ }
70
+ const parent = dirname(current);
71
+ if (parent === current) {
72
+ break;
73
+ }
74
+ current = parent;
75
+ }
76
+ return outermost ?? resolve(startDirectory);
77
+ }
78
+ function findMobilePath(repoRoot) {
79
+ const monoreboMobilePath = join(repoRoot, "apps", "mobile");
80
+ if (existsSync(join(monoreboMobilePath, "package.json"))) {
81
+ return monoreboMobilePath;
82
+ }
83
+ const rootPackage = readPackageJson(join(repoRoot, "package.json"));
84
+ const rootDeps = {
85
+ ...rootPackage.dependencies,
86
+ ...rootPackage.devDependencies
87
+ };
88
+ if (rootDeps["react-native"] || rootDeps.expo) {
89
+ return repoRoot;
90
+ }
91
+ return monoreboMobilePath;
92
+ }
93
+ export function discoverMaestroFlows(repoRoot) {
94
+ const maestroDirectory = join(repoRoot, ".maestro");
95
+ if (!existsSync(maestroDirectory)) {
96
+ return [];
97
+ }
98
+ return readdirSync(maestroDirectory)
99
+ .filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml"))
100
+ .map((entry) => join(".maestro", entry))
101
+ .sort();
102
+ }
103
+ export function checkCommandOnPath(command, pathEnv = process.env.PATH ?? "") {
104
+ for (const directory of pathEnv.split(delimiter)) {
105
+ if (directory.length === 0) {
106
+ continue;
107
+ }
108
+ const candidate = join(directory, command);
109
+ if (pathExistsAsFile(candidate)) {
110
+ return {
111
+ command,
112
+ detail: candidate,
113
+ status: "available"
114
+ };
115
+ }
116
+ }
117
+ return {
118
+ command,
119
+ status: "missing"
120
+ };
121
+ }
122
+ function detectPlatforms(mobilePath) {
123
+ const platforms = new Set();
124
+ if (existsSync(join(mobilePath, "app.json"))) {
125
+ platforms.add("expo");
126
+ }
127
+ if (existsSync(join(mobilePath, "ios"))) {
128
+ platforms.add("ios");
129
+ }
130
+ if (existsSync(join(mobilePath, "android"))) {
131
+ platforms.add("android");
132
+ }
133
+ return [...platforms].sort();
134
+ }
135
+ function readPackageJson(path) {
136
+ if (!existsSync(path)) {
137
+ return {};
138
+ }
139
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
140
+ return parsed;
141
+ }
142
+ function firstLine(value) {
143
+ const line = value
144
+ .split(/\r?\n/u)
145
+ .map((entry) => entry.trim())
146
+ .find(Boolean);
147
+ return line === undefined || line.length === 0 ? undefined : line;
148
+ }
149
+ export function pathExistsAsFile(path) {
150
+ return existsSync(path) && statSync(path).isFile();
151
+ }
@@ -0,0 +1,68 @@
1
+ import type { ProcessSpawner, ProcessResult, LineHandler } from "../spawn.js";
2
+ import type { Driver, SimDevice, ToolStatus } from "./types.js";
3
+ export interface PhysicalDevice {
4
+ readonly name: string;
5
+ readonly udid: string;
6
+ readonly status: "connected" | "disconnected" | string;
7
+ }
8
+ export interface SimRuntime {
9
+ readonly identifier: string;
10
+ readonly version: string;
11
+ readonly name: string;
12
+ readonly buildversion: string;
13
+ }
14
+ export interface RunningApp {
15
+ readonly bundleId: string;
16
+ readonly pid: number;
17
+ readonly status: string;
18
+ }
19
+ export interface DeviceType {
20
+ readonly name: string;
21
+ readonly identifier: string;
22
+ }
23
+ export declare class AppleDriver implements Driver {
24
+ private readonly spawner;
25
+ readonly name = "apple";
26
+ constructor(spawner?: ProcessSpawner);
27
+ check(): Promise<ToolStatus>;
28
+ listSimulators(): Promise<SimDevice[]>;
29
+ listDevices(): Promise<PhysicalDevice[]>;
30
+ listRuntimes(): Promise<SimRuntime[]>;
31
+ listDeviceTypes(): Promise<DeviceType[]>;
32
+ getSimulatorState(nameOrUdid: string): Promise<SimDevice | undefined>;
33
+ isAppInstalled(udid: string, bundleId: string): Promise<boolean>;
34
+ bootSimulator(nameOrUdid: string): Promise<ProcessResult>;
35
+ shutdownSimulator(nameOrUdid: string): Promise<ProcessResult>;
36
+ openSimulator(nameOrUdid: string): Promise<ProcessResult>;
37
+ eraseSimulator(nameOrUdid: string): Promise<ProcessResult>;
38
+ createSimulator(name: string, deviceType: string, runtime: string): Promise<ProcessResult>;
39
+ deleteSimulator(nameOrUdid: string): Promise<ProcessResult>;
40
+ installApp(udid: string, appPath: string): Promise<ProcessResult>;
41
+ launchApp(udid: string, bundleId: string): Promise<ProcessResult>;
42
+ terminateApp(udid: string, bundleId: string): Promise<ProcessResult>;
43
+ uninstallApp(udid: string, bundleId: string): Promise<ProcessResult>;
44
+ pruneUnavailableSimulators(): Promise<ProcessResult>;
45
+ streamLogs(udid: string, timeoutMs?: number): Promise<ProcessResult>;
46
+ screenshot(udid: string, outputPath: string): Promise<ProcessResult>;
47
+ xcodebuild(args: readonly string[], cwd?: string): Promise<ProcessResult>;
48
+ xcodebuildStream(args: readonly string[], onLine: LineHandler, cwd?: string): Promise<ProcessResult>;
49
+ xcresultTests(xcresultPath: string): Promise<ProcessResult>;
50
+ xcresultListTests(xcresultPath: string): Promise<ProcessResult>;
51
+ cloneSimulator(source: string, name: string): Promise<ProcessResult>;
52
+ clearSimulatorCache(nameOrUdid: string): Promise<ProcessResult>;
53
+ setAppearance(nameOrUdid: string, appearance: "dark" | "light"): Promise<ProcessResult>;
54
+ startVideoRecording(udid: string, outputPath: string): Promise<ProcessResult>;
55
+ stopVideoRecording(udid: string): Promise<ProcessResult>;
56
+ addMedia(udid: string, mediaPaths: readonly string[]): Promise<ProcessResult>;
57
+ listBuildConfigurations(cwd: string, options?: {
58
+ workspace?: string | undefined;
59
+ project?: string | undefined;
60
+ }): Promise<ProcessResult>;
61
+ listSchemes(cwd: string, options?: {
62
+ workspace?: string | undefined;
63
+ project?: string | undefined;
64
+ }): Promise<ProcessResult>;
65
+ screenshotToBuffer(udid: string, format?: "png" | "jpeg"): Promise<ProcessResult>;
66
+ listRunningApps(udid: string): Promise<RunningApp[]>;
67
+ }
68
+ //# sourceMappingURL=apple.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple.d.ts","sourceRoot":"","sources":["../../src/drivers/apple.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,360 @@
1
+ import { defaultSpawner } from "../spawn.js";
2
+ import { SimulatorNotFoundError, ToolOutputError } from "../errors.js";
3
+ function levenshteinDistance(a, b) {
4
+ const matrix = [];
5
+ for (let i = 0; i <= b.length; i++) {
6
+ matrix[i] = [i];
7
+ }
8
+ for (let j = 0; j <= a.length; j++) {
9
+ matrix[0][j] = j;
10
+ }
11
+ for (let i = 1; i <= b.length; i++) {
12
+ for (let j = 1; j <= a.length; j++) {
13
+ const cost = b[i - 1] === a[j - 1] ? 0 : 1;
14
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
15
+ }
16
+ }
17
+ return matrix[b.length][a.length];
18
+ }
19
+ export class AppleDriver {
20
+ spawner;
21
+ name = "apple";
22
+ constructor(spawner = defaultSpawner) {
23
+ this.spawner = spawner;
24
+ }
25
+ async check() {
26
+ const result = await this.spawner.run("xcrun", ["--version"]);
27
+ if (result.exitCode !== 0) {
28
+ return { status: "missing", reason: "xcrun not available" };
29
+ }
30
+ return {
31
+ status: "available",
32
+ version: result.stdout.trim().split("\n")[0]
33
+ };
34
+ }
35
+ async listSimulators() {
36
+ const result = await this.spawner.run("xcrun", [
37
+ "simctl",
38
+ "list",
39
+ "devices",
40
+ "available",
41
+ "--json"
42
+ ]);
43
+ if (result.exitCode !== 0) {
44
+ throw new ToolOutputError("simctl", result.stderr || result.stdout);
45
+ }
46
+ return parseSimDevices(result.stdout);
47
+ }
48
+ async listDevices() {
49
+ const result = await this.spawner.run("xcrun", [
50
+ "devicectl",
51
+ "list",
52
+ "devices",
53
+ "--json-output",
54
+ "-"
55
+ ]);
56
+ if (result.exitCode !== 0) {
57
+ return [];
58
+ }
59
+ return parsePhysicalDevices(result.stdout);
60
+ }
61
+ async listRuntimes() {
62
+ const result = await this.spawner.run("xcrun", [
63
+ "simctl",
64
+ "list",
65
+ "runtimes",
66
+ "available",
67
+ "--json"
68
+ ]);
69
+ if (result.exitCode !== 0) {
70
+ throw new ToolOutputError("simctl", result.stderr || result.stdout);
71
+ }
72
+ return parseRuntimes(result.stdout);
73
+ }
74
+ async listDeviceTypes() {
75
+ const result = await this.spawner.run("xcrun", [
76
+ "simctl",
77
+ "list",
78
+ "devicetypes",
79
+ "--json"
80
+ ]);
81
+ if (result.exitCode !== 0) {
82
+ throw new ToolOutputError("simctl", result.stderr || result.stdout);
83
+ }
84
+ return parseDeviceTypes(result.stdout);
85
+ }
86
+ async getSimulatorState(nameOrUdid) {
87
+ const devices = await this.listSimulators();
88
+ return devices.find((d) => d.name.toLowerCase() === nameOrUdid.toLowerCase() ||
89
+ d.udid.toLowerCase() === nameOrUdid.toLowerCase());
90
+ }
91
+ async isAppInstalled(udid, bundleId) {
92
+ const result = await this.spawner.run("xcrun", [
93
+ "simctl",
94
+ "get_app_container",
95
+ udid,
96
+ bundleId
97
+ ]);
98
+ return result.exitCode === 0;
99
+ }
100
+ async bootSimulator(nameOrUdid) {
101
+ const udid = await resolveUdid(this, nameOrUdid);
102
+ return this.spawner.run("xcrun", ["simctl", "boot", udid]);
103
+ }
104
+ async shutdownSimulator(nameOrUdid) {
105
+ const udid = await resolveUdid(this, nameOrUdid);
106
+ return this.spawner.run("xcrun", ["simctl", "shutdown", udid]);
107
+ }
108
+ async openSimulator(nameOrUdid) {
109
+ const udid = await resolveUdid(this, nameOrUdid);
110
+ return this.spawner.run("open", [
111
+ "-a",
112
+ "Simulator",
113
+ "--args",
114
+ "-CurrentDeviceUDID",
115
+ udid
116
+ ]);
117
+ }
118
+ async eraseSimulator(nameOrUdid) {
119
+ const udid = await resolveUdid(this, nameOrUdid);
120
+ return this.spawner.run("xcrun", ["simctl", "erase", udid]);
121
+ }
122
+ async createSimulator(name, deviceType, runtime) {
123
+ return this.spawner.run("xcrun", [
124
+ "simctl",
125
+ "create",
126
+ name,
127
+ deviceType,
128
+ runtime
129
+ ]);
130
+ }
131
+ async deleteSimulator(nameOrUdid) {
132
+ const udid = await resolveUdid(this, nameOrUdid);
133
+ return this.spawner.run("xcrun", ["simctl", "delete", udid]);
134
+ }
135
+ async installApp(udid, appPath) {
136
+ return this.spawner.run("xcrun", ["simctl", "install", udid, appPath]);
137
+ }
138
+ async launchApp(udid, bundleId) {
139
+ return this.spawner.run("xcrun", ["simctl", "launch", udid, bundleId]);
140
+ }
141
+ async terminateApp(udid, bundleId) {
142
+ return this.spawner.run("xcrun", ["simctl", "terminate", udid, bundleId]);
143
+ }
144
+ async uninstallApp(udid, bundleId) {
145
+ return this.spawner.run("xcrun", ["simctl", "uninstall", udid, bundleId]);
146
+ }
147
+ async pruneUnavailableSimulators() {
148
+ return this.spawner.run("xcrun", ["simctl", "delete", "unavailable"]);
149
+ }
150
+ async streamLogs(udid, timeoutMs = 30_000) {
151
+ return this.spawner.run("xcrun", ["simctl", "spawn", udid, "log", "stream", "--style", "compact"], { timeoutMs });
152
+ }
153
+ async screenshot(udid, outputPath) {
154
+ return this.spawner.run("xcrun", [
155
+ "simctl",
156
+ "io",
157
+ udid,
158
+ "screenshot",
159
+ outputPath
160
+ ]);
161
+ }
162
+ async xcodebuild(args, cwd) {
163
+ return this.spawner.run("xcrun", ["xcodebuild", ...args], { cwd });
164
+ }
165
+ async xcodebuildStream(args, onLine, cwd) {
166
+ return this.spawner.runStream("xcrun", ["xcodebuild", ...args], onLine, { cwd });
167
+ }
168
+ async xcresultTests(xcresultPath) {
169
+ return this.spawner.run("xcrun", [
170
+ "xcresulttool",
171
+ "get",
172
+ "test-results",
173
+ "summary",
174
+ "--path",
175
+ xcresultPath
176
+ ]);
177
+ }
178
+ async xcresultListTests(xcresultPath) {
179
+ return this.spawner.run("xcrun", [
180
+ "xcresulttool",
181
+ "get",
182
+ "test-results",
183
+ "tests",
184
+ "--path",
185
+ xcresultPath
186
+ ]);
187
+ }
188
+ async cloneSimulator(source, name) {
189
+ const udid = await resolveUdid(this, source);
190
+ return this.spawner.run("xcrun", ["simctl", "clone", udid, name]);
191
+ }
192
+ async clearSimulatorCache(nameOrUdid) {
193
+ const udid = await resolveUdid(this, nameOrUdid);
194
+ return this.spawner.run("xcrun", ["simctl", "spawn", udid, "rm", "-rf", "/Users/admin/Library/Caches"]);
195
+ }
196
+ async setAppearance(nameOrUdid, appearance) {
197
+ const udid = await resolveUdid(this, nameOrUdid);
198
+ const style = appearance === "dark" ? "Dark" : "Light";
199
+ return this.spawner.run("xcrun", ["simctl", "spawn", udid, "defaults", "write", "-g", "AppleInterfaceStyle", style]);
200
+ }
201
+ async startVideoRecording(udid, outputPath) {
202
+ return this.spawner.run("xcrun", ["simctl", "io", udid, "recordVideo", outputPath], { timeoutMs: 0 });
203
+ }
204
+ async stopVideoRecording(udid) {
205
+ return this.spawner.run("xcrun", ["simctl", "io", udid, "recordVideo", "--stop"]);
206
+ }
207
+ async addMedia(udid, mediaPaths) {
208
+ return this.spawner.run("xcrun", ["simctl", "addmedia", udid, ...mediaPaths]);
209
+ }
210
+ async listBuildConfigurations(cwd, options = {}) {
211
+ const args = ["-list"];
212
+ if (options.workspace) {
213
+ args.push("-workspace", options.workspace);
214
+ }
215
+ else if (options.project) {
216
+ args.push("-project", options.project);
217
+ }
218
+ return this.spawner.run("xcrun", ["xcodebuild", ...args], { cwd });
219
+ }
220
+ async listSchemes(cwd, options = {}) {
221
+ const args = ["-list"];
222
+ if (options.workspace) {
223
+ args.push("-workspace", options.workspace);
224
+ }
225
+ else if (options.project) {
226
+ args.push("-project", options.project);
227
+ }
228
+ return this.spawner.run("xcrun", ["xcodebuild", ...args], { cwd });
229
+ }
230
+ async screenshotToBuffer(udid, format = "png") {
231
+ const type = format === "jpeg" ? "jpeg" : "png";
232
+ return this.spawner.run("xcrun", ["simctl", "io", udid, "screenshot", "-", "--type", type]);
233
+ }
234
+ async listRunningApps(udid) {
235
+ const result = await this.spawner.run("xcrun", [
236
+ "simctl",
237
+ "spawn",
238
+ udid,
239
+ "launchctl",
240
+ "list"
241
+ ]);
242
+ if (result.exitCode !== 0) {
243
+ return [];
244
+ }
245
+ return parseRunningApps(result.stdout);
246
+ }
247
+ }
248
+ async function resolveUdid(driver, nameOrUdid) {
249
+ if (/^[0-9A-F-]{36,}$/iu.test(nameOrUdid)) {
250
+ return nameOrUdid;
251
+ }
252
+ const devices = await driver.listSimulators();
253
+ const lowerQuery = nameOrUdid.toLowerCase();
254
+ // Exact match first
255
+ const exactMatch = devices.find((d) => d.name.toLowerCase() === lowerQuery || d.udid.toLowerCase() === lowerQuery);
256
+ if (exactMatch) {
257
+ return exactMatch.udid;
258
+ }
259
+ // Partial name match
260
+ const partialMatches = devices.filter((d) => d.name.toLowerCase().includes(lowerQuery));
261
+ if (partialMatches.length === 1) {
262
+ return partialMatches[0].udid;
263
+ }
264
+ if (partialMatches.length > 1) {
265
+ throw new SimulatorNotFoundError(nameOrUdid, `Multiple simulators match "${nameOrUdid}": ${partialMatches.map((d) => d.name).join(", ")}`);
266
+ }
267
+ // Fuzzy match with Levenshtein distance
268
+ const fuzzyMatches = devices
269
+ .map((d) => ({ device: d, distance: levenshteinDistance(lowerQuery, d.name.toLowerCase()) }))
270
+ .filter((m) => m.distance <= 3)
271
+ .sort((a, b) => a.distance - b.distance);
272
+ if (fuzzyMatches.length === 1) {
273
+ return fuzzyMatches[0].device.udid;
274
+ }
275
+ if (fuzzyMatches.length > 1) {
276
+ throw new SimulatorNotFoundError(nameOrUdid, `Multiple simulators fuzzy-match "${nameOrUdid}": ${fuzzyMatches.map((m) => m.device.name).join(", ")}`);
277
+ }
278
+ throw new SimulatorNotFoundError(nameOrUdid);
279
+ }
280
+ function parseSimDevices(jsonOutput) {
281
+ try {
282
+ const parsed = JSON.parse(jsonOutput);
283
+ const devices = [];
284
+ for (const runtime of Object.values(parsed.devices ?? {})) {
285
+ for (const device of runtime) {
286
+ devices.push({
287
+ name: device.name,
288
+ udid: device.udid,
289
+ state: device.state
290
+ });
291
+ }
292
+ }
293
+ return devices;
294
+ }
295
+ catch (cause) {
296
+ throw new ToolOutputError("simctl", `Invalid JSON from simctl: ${String(cause)}`);
297
+ }
298
+ }
299
+ function parsePhysicalDevices(jsonOutput) {
300
+ try {
301
+ const parsed = JSON.parse(jsonOutput);
302
+ const devices = [];
303
+ for (const device of parsed.result?.devices ?? []) {
304
+ devices.push({
305
+ name: device.name,
306
+ udid: device.identifier,
307
+ status: device.connectionProperties?.transportType ? "connected" : "disconnected"
308
+ });
309
+ }
310
+ return devices;
311
+ }
312
+ catch {
313
+ return [];
314
+ }
315
+ }
316
+ function parseRuntimes(jsonOutput) {
317
+ try {
318
+ const parsed = JSON.parse(jsonOutput);
319
+ return (parsed.runtimes ?? []).map((r) => ({
320
+ identifier: r.identifier,
321
+ version: r.version,
322
+ name: r.name,
323
+ buildversion: r.buildversion
324
+ }));
325
+ }
326
+ catch (cause) {
327
+ throw new ToolOutputError("simctl", `Invalid JSON from simctl: ${String(cause)}`);
328
+ }
329
+ }
330
+ function parseDeviceTypes(jsonOutput) {
331
+ try {
332
+ const parsed = JSON.parse(jsonOutput);
333
+ return (parsed.devicetypes ?? []).map((d) => ({
334
+ name: d.name,
335
+ identifier: d.identifier
336
+ }));
337
+ }
338
+ catch (cause) {
339
+ throw new ToolOutputError("simctl", `Invalid JSON from simctl: ${String(cause)}`);
340
+ }
341
+ }
342
+ function parseRunningApps(stdout) {
343
+ const apps = [];
344
+ for (const line of stdout.split("\n")) {
345
+ const trimmed = line.trim();
346
+ if (!trimmed || trimmed.startsWith("-"))
347
+ continue;
348
+ const parts = trimmed.split(/\s+/);
349
+ if (parts.length >= 3) {
350
+ const pidStr = parts[0];
351
+ const status = parts[1];
352
+ const bundleId = parts[2];
353
+ const pid = Number.parseInt(pidStr, 10);
354
+ if (!Number.isNaN(pid) && bundleId.includes(".")) {
355
+ apps.push({ bundleId, pid, status });
356
+ }
357
+ }
358
+ }
359
+ return apps;
360
+ }