From 63074e6168af93c8d2f52c8adb5b770a88d8e88d Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 24 Jun 2026 12:13:06 +0200 Subject: [PATCH 1/2] Add iOS device set support to Argent CLI --- packages/argent-cli/src/server.ts | 38 ++++++++++++++- .../argent-cli/test/parse-start-flags.test.ts | 18 ++++++++ packages/argent-tools-client/src/index.ts | 2 + packages/argent-tools-client/src/launcher.ts | 36 +++++++++++++-- .../test/launcher-helpers.test.ts | 29 ++++++++++++ packages/argent/src/cli.ts | 46 +++++++++++++++++-- .../tool-server/src/blueprints/ax-service.ts | 17 +++---- .../src/blueprints/native-devtools.ts | 30 ++++++------ .../src/blueprints/simulator-server.ts | 3 ++ .../src/tools/devices/boot-device.ts | 23 ++++++---- .../src/tools/launch-app/platforms/ios.ts | 3 +- .../src/tools/open-url/platforms/ios.ts | 3 +- .../profiler/native-profiler/platforms/ios.ts | 12 +++-- .../src/tools/reinstall-app/platforms/ios.ts | 5 +- .../src/tools/restart-app/platforms/ios.ts | 5 +- packages/tool-server/src/utils/ios-devices.ts | 3 +- packages/tool-server/src/utils/simctl.ts | 33 +++++++++++++ .../src/utils/simulator-watcher.ts | 3 +- packages/tool-server/test/boot-device.test.ts | 40 ++++++++++++++++ packages/tool-server/test/simctl.test.ts | 37 +++++++++++++++ .../test/simulator-server-blueprint.test.ts | 27 +++++++++++ 21 files changed, 360 insertions(+), 53 deletions(-) create mode 100644 packages/tool-server/src/utils/simctl.ts create mode 100644 packages/tool-server/test/simctl.test.ts diff --git a/packages/argent-cli/src/server.ts b/packages/argent-cli/src/server.ts index 16a1db082..15ad8c395 100644 --- a/packages/argent-cli/src/server.ts +++ b/packages/argent-cli/src/server.ts @@ -16,6 +16,8 @@ import { formatToolsServerUrl, formatLinkUrl, generateAuthToken, + IOS_DEVICE_SET_ENV, + normalizeIosDeviceSetPath, type ToolsServerPaths, type ToolsServerState, } from "@argent/tools-client"; @@ -64,6 +66,9 @@ async function statusCmd(json: boolean): Promise { console.log(` startedAt: ${state.startedAt}`); console.log(` process: ${alive ? "alive" : "dead"}`); console.log(` health: ${healthy ? "ok" : "unreachable"}`); + if (state.iosDeviceSetPath) { + console.log(` iOS set: ${state.iosDeviceSetPath}`); + } if (!alive || !healthy) { console.log(`\nState file is stale; next \`argent\` invocation will respawn the server.`); } @@ -102,6 +107,8 @@ export interface StartFlags { force: boolean; /** Disable auth (no token minted). Server accepts unauthenticated requests. */ noAuth: boolean; + /** Optional CoreSimulator device set path for iOS-only operations. */ + iosDeviceSetPath: string | null; help: boolean; } @@ -115,6 +122,7 @@ export function parseStartFlags(argv: string[]): StartFlags { detach: false, force: false, noAuth: false, + iosDeviceSetPath: null, help: false, }; @@ -166,6 +174,14 @@ export function parseStartFlags(argv: string[]): StartFlags { flags.idleTimeoutMinutes = parseIdle(tok.slice("--idle-timeout=".length)); continue; } + if (tok === "--ios-device-set") { + flags.iosDeviceSetPath = parseIosDeviceSetPath(takeValue("--ios-device-set")); + continue; + } + if (tok.startsWith("--ios-device-set=")) { + flags.iosDeviceSetPath = parseIosDeviceSetPath(tok.slice("--ios-device-set=".length)); + continue; + } throw new StartFlagError(`Unknown flag: ${tok}`); } @@ -191,6 +207,14 @@ export function parseIdle(raw: string): number { return Number(raw); } +export function parseIosDeviceSetPath(raw: string): string { + const normalized = normalizeIosDeviceSetPath(raw); + if (!normalized) { + throw new StartFlagError(`--ios-device-set requires a non-empty path`); + } + return normalized; +} + function printStartHelp(): void { console.log(`Usage: argent server start [flags] @@ -203,6 +227,8 @@ Flags: Use 0.0.0.0 to expose on every interface. --idle-timeout Auto-shutdown after idle minutes (0 disables). Default: 0 (never auto-shutdown). + --ios-device-set Use this CoreSimulator device set for iOS operations. + Also configurable via ${IOS_DEVICE_SET_ENV}. --detach, -d Run as a detached background process and return. --force If a tool-server is already running, kill it first. --no-auth Disable authentication (no token). Anyone who can @@ -331,6 +357,8 @@ async function startCmd(argv: string[], paths: ToolsServerPaths | undefined): Pr } const port = await resolvePort(flags.port); + const iosDeviceSetPath = + flags.iosDeviceSetPath ?? normalizeIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]); // Auth on by default; --no-auth opts out (token stays undefined → the // tool-server runs unauthenticated and prints its own warning). @@ -352,11 +380,11 @@ async function startCmd(argv: string[], paths: ToolsServerPaths | undefined): Pr } if (flags.detach) { - await runDetached(paths, port, flags.host, flags.idleTimeoutMinutes, token); + await runDetached(paths, port, flags.host, flags.idleTimeoutMinutes, iosDeviceSetPath, token); return; } - await runForeground(paths, port, flags.host, flags.idleTimeoutMinutes, token); + await runForeground(paths, port, flags.host, flags.idleTimeoutMinutes, iosDeviceSetPath, token); } async function runDetached( @@ -364,11 +392,13 @@ async function runDetached( port: number, host: string, idleTimeoutMinutes: number, + iosDeviceSetPath: string | null, token?: string ): Promise { const { port: actualPort, pid } = await spawnToolsServer(paths, port, { host, idleTimeoutMinutes, + iosDeviceSetPath, token, }); await writeToolsServerState({ @@ -377,6 +407,7 @@ async function runDetached( startedAt: new Date().toISOString(), bundlePath: paths.bundlePath, host, + ...(iosDeviceSetPath ? { iosDeviceSetPath } : {}), ...(token ? { token } : {}), }); const url = formatToolsServerUrl(host, actualPort); @@ -393,11 +424,13 @@ async function runForeground( port: number, host: string, idleTimeoutMinutes: number, + iosDeviceSetPath: string | null, token?: string ): Promise { const env = buildToolsServerEnv(paths, port, process.env, { host, idleTimeoutMinutes, + iosDeviceSetPath, token, }); @@ -432,6 +465,7 @@ async function runForeground( startedAt: new Date().toISOString(), bundlePath: paths.bundlePath, host, + ...(iosDeviceSetPath ? { iosDeviceSetPath } : {}), ...(token ? { token } : {}), }); stateWritten = true; diff --git a/packages/argent-cli/test/parse-start-flags.test.ts b/packages/argent-cli/test/parse-start-flags.test.ts index 759eae221..0b3e93b09 100644 --- a/packages/argent-cli/test/parse-start-flags.test.ts +++ b/packages/argent-cli/test/parse-start-flags.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import * as path from "node:path"; import { parseStartFlags, parsePort, parseIdle, StartFlagError } from "../src/server.js"; describe("parseStartFlags", () => { @@ -10,6 +11,7 @@ describe("parseStartFlags", () => { detach: false, force: false, noAuth: false, + iosDeviceSetPath: null, help: false, }); }); @@ -35,6 +37,15 @@ describe("parseStartFlags", () => { expect(parseStartFlags(["--idle-timeout=0"]).idleTimeoutMinutes).toBe(0); }); + it("parses --ios-device-set in space and equals form", () => { + expect(parseStartFlags(["--ios-device-set", "tmp/device-set"]).iosDeviceSetPath).toBe( + path.resolve("tmp/device-set") + ); + expect(parseStartFlags(["--ios-device-set=/tmp/argent-set"]).iosDeviceSetPath).toBe( + "/tmp/argent-set" + ); + }); + it("parses boolean flags --detach/-d, --force, --no-auth, --help/-h", () => { expect(parseStartFlags(["--detach"]).detach).toBe(true); expect(parseStartFlags(["-d"]).detach).toBe(true); @@ -61,6 +72,7 @@ describe("parseStartFlags", () => { detach: true, force: true, noAuth: false, + iosDeviceSetPath: null, help: false, }); }); @@ -74,6 +86,12 @@ describe("parseStartFlags", () => { expect(() => parseStartFlags(["--port"])).toThrow(/--port requires a value/); expect(() => parseStartFlags(["--host"])).toThrow(/--host requires a value/); expect(() => parseStartFlags(["--idle-timeout"])).toThrow(/--idle-timeout requires a value/); + expect(() => parseStartFlags(["--ios-device-set"])).toThrow( + /--ios-device-set requires a value/ + ); + expect(() => parseStartFlags(["--ios-device-set="])).toThrow( + /--ios-device-set requires a non-empty path/ + ); }); it("does not consume the next token as a value for boolean flags", () => { diff --git a/packages/argent-tools-client/src/index.ts b/packages/argent-tools-client/src/index.ts index d9d542de3..689a22022 100644 --- a/packages/argent-tools-client/src/index.ts +++ b/packages/argent-tools-client/src/index.ts @@ -13,7 +13,9 @@ export { formatToolsServerUrl, generateAuthToken, AUTH_TOKEN_ENV, + IOS_DEVICE_SET_ENV, STATE_PATHS, + normalizeIosDeviceSetPath, type ToolsServerPaths, type ToolsServerState, type ToolsServerHandle, diff --git a/packages/argent-tools-client/src/launcher.ts b/packages/argent-tools-client/src/launcher.ts index 8f1a0335f..879e09364 100644 --- a/packages/argent-tools-client/src/launcher.ts +++ b/packages/argent-tools-client/src/launcher.ts @@ -2,17 +2,18 @@ import * as net from "node:net"; import * as fs from "node:fs"; import * as path from "node:path"; import * as readline from "node:readline"; -import { homedir } from "node:os"; +import * as os from "node:os"; import { spawn } from "node:child_process"; import { randomBytes } from "node:crypto"; import { mkdir, writeFile, readFile, unlink, rename, chmod } from "node:fs/promises"; -const STATE_DIR = path.join(homedir(), ".argent"); +const STATE_DIR = path.join(os.homedir(), ".argent"); const STATE_FILE = path.join(STATE_DIR, "tool-server.json"); const LOG_FILE = path.join(STATE_DIR, "tool-server.log"); const AUTH_TOKEN_BYTES = 32; export const AUTH_TOKEN_ENV = "ARGENT_AUTH_TOKEN"; +export const IOS_DEVICE_SET_ENV = "ARGENT_IOS_DEVICE_SET_PATH"; // Idle-shutdown policy for auto-spawned servers (MCP / `argent run` path). The // CLI's `argent server start` overrides this; manual launches default to no @@ -45,6 +46,20 @@ export interface BuildToolsServerEnvOptions { * `argent server start` path, which prints its own no-auth warning. */ token?: string; + /** Optional CoreSimulator device set path for iOS-only operations. */ + iosDeviceSetPath?: string | null; +} + +function normalizePath(raw: string): string { + const trimmed = raw.trim(); + if (trimmed === "~") return os.homedir(); + if (trimmed.startsWith("~/")) return path.join(os.homedir(), trimmed.slice(2)); + return path.resolve(trimmed); +} + +export function normalizeIosDeviceSetPath(raw: string | null | undefined): string | null { + if (!raw?.trim()) return null; + return normalizePath(raw); } export function buildToolsServerEnv( @@ -53,12 +68,17 @@ export function buildToolsServerEnv( baseEnv: NodeJS.ProcessEnv = process.env, options: BuildToolsServerEnvOptions = {} ): NodeJS.ProcessEnv { + const iosDeviceSetPath = normalizeIosDeviceSetPath( + options.iosDeviceSetPath ?? baseEnv[IOS_DEVICE_SET_ENV] + ); const env: NodeJS.ProcessEnv = { ...baseEnv, ARGENT_PORT: String(port), ARGENT_SIMULATOR_SERVER_DIR: paths.simulatorServerDir, ARGENT_NATIVE_DEVTOOLS_DIR: paths.nativeDevtoolsDir, }; + if (iosDeviceSetPath) env[IOS_DEVICE_SET_ENV] = iosDeviceSetPath; + else delete env[IOS_DEVICE_SET_ENV]; if (options.host !== undefined) env.ARGENT_HOST = options.host; if (options.idleTimeoutMinutes !== undefined) { env.ARGENT_IDLE_TIMEOUT_MINUTES = String(options.idleTimeoutMinutes); @@ -81,6 +101,8 @@ export interface ToolsServerState { * `argent server start` writes tokenless (auth-disabled) state. */ token?: string; + /** CoreSimulator device set path this tool-server was started with, if any. */ + iosDeviceSetPath?: string; } /** Handle returned to clients: the base URL plus the matching auth token. */ @@ -358,6 +380,7 @@ export async function killToolServer(): Promise { } export async function ensureToolsServer(paths: ToolsServerPaths): Promise { + const requestedIosDeviceSetPath = normalizeIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]); const state = await readState(); if (state) { @@ -365,12 +388,17 @@ export async function ensureToolsServer(paths: ToolsServerPaths): Promise { expect(env.FOO).toBe("bar"); expect(env.PATH).toBe("/usr/bin"); }); + + it("normalizes and propagates the iOS device set from options", () => { + const env = buildToolsServerEnv(paths, 3001, {}, { iosDeviceSetPath: "tmp/device-set" }); + expect(env[IOS_DEVICE_SET_ENV]).toBe(path.resolve("tmp/device-set")); + }); + + it("falls back to the iOS device set from the base env", () => { + const env = buildToolsServerEnv(paths, 3001, { [IOS_DEVICE_SET_ENV]: "/tmp/argent-set" }); + expect(env[IOS_DEVICE_SET_ENV]).toBe("/tmp/argent-set"); + }); + + it("removes a blank iOS device set from the spawned env", () => { + const env = buildToolsServerEnv(paths, 3001, { [IOS_DEVICE_SET_ENV]: " " }); + expect(env[IOS_DEVICE_SET_ENV]).toBeUndefined(); + }); +}); + +describe("normalizeIosDeviceSetPath", () => { + it("returns null for empty values", () => { + expect(normalizeIosDeviceSetPath(undefined)).toBeNull(); + expect(normalizeIosDeviceSetPath(" ")).toBeNull(); + }); + + it("resolves relative paths", () => { + expect(normalizeIosDeviceSetPath("tmp/device-set")).toBe(path.resolve("tmp/device-set")); + }); }); describe("formatToolsServerUrl", () => { diff --git a/packages/argent/src/cli.ts b/packages/argent/src/cli.ts index 050bad3e5..7766a5e8a 100644 --- a/packages/argent/src/cli.ts +++ b/packages/argent/src/cli.ts @@ -27,6 +27,7 @@ */ import * as fs from "node:fs"; +import * as os from "node:os"; import * as path from "node:path"; import type * as Installer from "@argent/installer"; import type * as Mcp from "@argent/mcp"; @@ -35,6 +36,7 @@ import { BUNDLED_RUNTIME_PATHS } from "./bundled-paths.js"; import { installFatalHandlers } from "./fatal-handlers.js"; const PACKAGE_NAME = "@swmansion/argent"; +const IOS_DEVICE_SET_ENV = "ARGENT_IOS_DEVICE_SET_PATH"; function getInstalledVersion(): string | null { try { @@ -48,7 +50,44 @@ function getInstalledVersion(): string | null { } } -const [, , command, ...rest] = process.argv; +function normalizePath(raw: string): string { + const trimmed = raw.trim(); + if (trimmed === "~") return os.homedir(); + if (trimmed.startsWith("~/")) return path.join(os.homedir(), trimmed.slice(2)); + return path.resolve(trimmed); +} + +function parseTopLevelArgs(argv: string[]): { command: string | undefined; rest: string[] } { + const pending = [...argv]; + while (pending.length > 0) { + const tok = pending[0]!; + if (tok === "--ios-device-set") { + const value = pending[1]; + if (!value?.trim()) { + console.error("Error: --ios-device-set requires a non-empty path"); + process.exit(2); + } + process.env[IOS_DEVICE_SET_ENV] = normalizePath(value); + pending.splice(0, 2); + continue; + } + if (tok.startsWith("--ios-device-set=")) { + const value = tok.slice("--ios-device-set=".length); + if (!value.trim()) { + console.error("Error: --ios-device-set requires a non-empty path"); + process.exit(2); + } + process.env[IOS_DEVICE_SET_ENV] = normalizePath(value); + pending.shift(); + continue; + } + break; + } + const [command, ...rest] = pending; + return { command, rest }; +} + +const { command, rest } = parseTopLevelArgs(process.argv.slice(2)); const isMcpServer = command === "mcp"; installFatalHandlers({ isMcpServer }); @@ -77,8 +116,9 @@ Commands: flags Show current feature-flag state Options: - --help, -h Show this help message - --version, -v Show version + --ios-device-set Use this CoreSimulator device set for iOS operations + --help, -h Show this help message + --version, -v Show version Run \`argent --help\` for command-specific help. diff --git a/packages/tool-server/src/blueprints/ax-service.ts b/packages/tool-server/src/blueprints/ax-service.ts index 64aff4b55..ead87cdf4 100644 --- a/packages/tool-server/src/blueprints/ax-service.ts +++ b/packages/tool-server/src/blueprints/ax-service.ts @@ -1,7 +1,6 @@ import * as net from "node:net"; import * as fs from "node:fs"; import * as fsAsync from "node:fs/promises"; -import * as os from "node:os"; import * as path from "node:path"; import * as readline from "node:readline"; import { promisify } from "node:util"; @@ -15,6 +14,7 @@ import { } from "@argent/registry"; import { axServiceBinaryPath, axServiceBinaryPathTcp } from "@argent/native-devtools-ios"; import { SIMCTL_SPAWN_TIMEOUT_MS } from "../utils/simctl-config"; +import { activeIosDeviceSetPath, simctlArgs } from "../utils/simctl"; const execFileAsync = promisify(execFile); @@ -86,8 +86,7 @@ interface PendingRpc { export async function ensureAutomationEnabled(udid: string): Promise { await execFileAsync( "xcrun", - [ - "simctl", + simctlArgs([ "spawn", udid, "defaults", @@ -96,7 +95,7 @@ export async function ensureAutomationEnabled(udid: string): Promise { "AutomationEnabled", "-bool", "true", - ], + ]), { timeout: SIMCTL_SPAWN_TIMEOUT_MS } ); } @@ -115,15 +114,14 @@ export async function ensureAutomationEnabled(udid: string): Promise { export async function isEntitlementBypassActive(udid: string): Promise { return execFileAsync( "xcrun", - [ - "simctl", + simctlArgs([ "spawn", udid, "defaults", "read", "com.apple.Accessibility", "IgnoreAXServerEntitlements", - ], + ]), { timeout: SIMCTL_SPAWN_TIMEOUT_MS } ) .then(({ stdout }) => stdout.trim() === "1") @@ -136,8 +134,7 @@ export async function isEntitlementBypassActive(udid: string): Promise */ function accessibilityPlistPath(udid: string): string { return path.join( - os.homedir(), - "Library/Developer/CoreSimulator/Devices", + activeIosDeviceSetPath(), udid, "data/Library/Preferences/com.apple.Accessibility.plist" ); @@ -222,7 +219,7 @@ function spawnDaemon(udid: string, endpoint: AXEndpoint): ChildProcess { const proc = execFile( "xcrun", - ["simctl", "spawn", udid, binaryPath, ...endpointArgs, "--timeout", "3600"], + simctlArgs(["spawn", udid, binaryPath, ...endpointArgs, "--timeout", "3600"]), { encoding: "utf8" } ) as ChildProcess; diff --git a/packages/tool-server/src/blueprints/native-devtools.ts b/packages/tool-server/src/blueprints/native-devtools.ts index 0f02fad2a..c9cd7f8a8 100644 --- a/packages/tool-server/src/blueprints/native-devtools.ts +++ b/packages/tool-server/src/blueprints/native-devtools.ts @@ -12,6 +12,7 @@ import { } from "@argent/registry"; import { bootstrapDylibPath, bootstrapDylibPathTcp } from "@argent/native-devtools-ios"; import { SIMCTL_SPAWN_TIMEOUT_MS } from "../utils/simctl-config"; +import { simctlArgs } from "../utils/simctl"; export type NativeDevtoolsTransport = "unix" | "tcp"; @@ -248,8 +249,7 @@ async function ensureAccessibilityEnabled(udid: string): Promise { flags.map((flag) => execFileAsync( "xcrun", - [ - "simctl", + simctlArgs([ "spawn", udid, "defaults", @@ -258,7 +258,7 @@ async function ensureAccessibilityEnabled(udid: string): Promise { flag, "-bool", "true", - ], + ]), { timeout: SIMCTL_SPAWN_TIMEOUT_MS } ) ) @@ -278,7 +278,7 @@ async function ensureEnv( // accumulate on every ensureEnv() cycle. const result = await execFileAsync( "xcrun", - ["simctl", "spawn", udid, "launchctl", "getenv", "DYLD_INSERT_LIBRARIES"], + simctlArgs(["spawn", udid, "launchctl", "getenv", "DYLD_INSERT_LIBRARIES"]), { encoding: "utf8", timeout: SIMCTL_SPAWN_TIMEOUT_MS } ).catch((e) => ({ stdout: (e as NodeJS.ErrnoException & { stdout?: string }).stdout ?? "" })); @@ -288,7 +288,7 @@ async function ensureEnv( if (updated !== existing) { await execFileAsync( "xcrun", - ["simctl", "spawn", udid, "launchctl", "setenv", "DYLD_INSERT_LIBRARIES", updated], + simctlArgs(["spawn", udid, "launchctl", "setenv", "DYLD_INSERT_LIBRARIES", updated]), { timeout: SIMCTL_SPAWN_TIMEOUT_MS } ); } @@ -298,29 +298,27 @@ async function ensureEnv( if (endpoint.transport === "tcp") { await execFileAsync( "xcrun", - [ - "simctl", + simctlArgs([ "spawn", udid, "launchctl", "setenv", "NATIVE_DEVTOOLS_IOS_CDP_PORT", String(endpoint.port), - ], + ]), { timeout: SIMCTL_SPAWN_TIMEOUT_MS } ); } else { await execFileAsync( "xcrun", - [ - "simctl", + simctlArgs([ "spawn", udid, "launchctl", "setenv", "NATIVE_DEVTOOLS_IOS_CDP_SOCKET", endpoint.socketPath, - ], + ]), { timeout: SIMCTL_SPAWN_TIMEOUT_MS } ); } @@ -330,9 +328,13 @@ async function ensureEnv( } async function listRunningUIKitApplicationBundleIds(udid: string): Promise> { - const { stdout } = await execFileAsync("xcrun", ["simctl", "spawn", udid, "launchctl", "list"], { - encoding: "utf8", - }); + const { stdout } = await execFileAsync( + "xcrun", + simctlArgs(["spawn", udid, "launchctl", "list"]), + { + encoding: "utf8", + } + ); const bundleIds = new Set(); for (const line of stdout.split("\n")) { diff --git a/packages/tool-server/src/blueprints/simulator-server.ts b/packages/tool-server/src/blueprints/simulator-server.ts index 899b542b5..cbf3c0552 100644 --- a/packages/tool-server/src/blueprints/simulator-server.ts +++ b/packages/tool-server/src/blueprints/simulator-server.ts @@ -10,6 +10,7 @@ import { import { simulatorServerBinaryPath, simulatorServerBinaryDir } from "@argent/native-devtools-ios"; import { ensureAutomationEnabled } from "./ax-service"; import { ensureDep } from "../utils/check-deps"; +import { iosDeviceSetPath } from "../utils/simctl"; export const SIMULATOR_SERVER_NAMESPACE = "SimulatorServer"; @@ -85,6 +86,8 @@ function spawnSimulatorServerProcess( const { BINARY_PATH, BINARY_DIR } = getPaths(); return new Promise((resolve, reject) => { const args = [subcommand, "--id", udid]; + const deviceSet = subcommand === "ios" ? iosDeviceSetPath() : null; + if (deviceSet) args.push("--device-set", deviceSet); const proc = spawn(BINARY_PATH, args, { cwd: BINARY_DIR, diff --git a/packages/tool-server/src/tools/devices/boot-device.ts b/packages/tool-server/src/tools/devices/boot-device.ts index bce8b514c..a2bd63212 100644 --- a/packages/tool-server/src/tools/devices/boot-device.ts +++ b/packages/tool-server/src/tools/devices/boot-device.ts @@ -23,6 +23,7 @@ import { import { ensureDep } from "../../utils/check-deps"; import { linuxBootDiagnostics } from "../../utils/linux-preflight"; import { listIosSimulators } from "../../utils/ios-devices"; +import { iosDeviceSetPath, simctlArgs } from "../../utils/simctl"; import { bootElectronApp, type ElectronBootResult } from "./boot-electron"; const execFileAsync = promisify(execFile); @@ -414,7 +415,7 @@ async function bootIos( // force=true on a running sim: shut it down so we can pre-write AX prefs. if (force && simState === "Booted") { - await execFileAsync("xcrun", ["simctl", "shutdown", udid]); + await execFileAsync("xcrun", simctlArgs(["shutdown", udid])); } const needsPreBoot = simState === "Shutdown" || (force && simState === "Booted"); @@ -428,13 +429,13 @@ async function bootIos( }); } - await execFileAsync("xcrun", ["simctl", "boot", udid]).catch((err: unknown) => { + await execFileAsync("xcrun", simctlArgs(["boot", udid])).catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err); if (!message.includes("Unable to boot device in current state: Booted")) { throw err; } }); - await execFileAsync("xcrun", ["simctl", "bootstatus", udid, "-b"]); + await execFileAsync("xcrun", simctlArgs(["bootstatus", udid, "-b"])); // Best-effort fallback: no-op on the happy path (pref already cached from // pre-boot write). When the sim was already Booted without force, writes @@ -455,13 +456,15 @@ async function bootIos( if (initFailure?.givenUp) { return buildInitFailedResult(udid, initFailure); } - await execFileAsync("defaults", [ - "write", - "com.apple.iphonesimulator", - "CurrentDeviceUDID", - udid, - ]); - await execFileAsync("open", ["-a", "Simulator.app"]); + if (!iosDeviceSetPath()) { + await execFileAsync("defaults", [ + "write", + "com.apple.iphonesimulator", + "CurrentDeviceUDID", + udid, + ]); + await execFileAsync("open", ["-a", "Simulator.app"]); + } return { platform: "ios", udid, booted: true }; } diff --git a/packages/tool-server/src/tools/launch-app/platforms/ios.ts b/packages/tool-server/src/tools/launch-app/platforms/ios.ts index ddc5814ce..ff8fea5d4 100644 --- a/packages/tool-server/src/tools/launch-app/platforms/ios.ts +++ b/packages/tool-server/src/tools/launch-app/platforms/ios.ts @@ -2,6 +2,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { precheckNativeDevtools } from "../../../blueprints/native-devtools"; import type { PlatformImpl } from "../../../utils/cross-platform-tool"; +import { simctlArgs } from "../../../utils/simctl"; import type { LaunchAppIosServices, LaunchAppParams, LaunchAppResult } from "../types"; const execFileAsync = promisify(execFile); @@ -11,7 +12,7 @@ export const iosImpl: PlatformImpl { const blocked = await precheckNativeDevtools(services.nativeDevtools, params.udid); if (blocked) return blocked; - await execFileAsync("xcrun", ["simctl", "launch", params.udid, params.bundleId]); + await execFileAsync("xcrun", simctlArgs(["launch", params.udid, params.bundleId])); return { launched: true, bundleId: params.bundleId }; }, }; diff --git a/packages/tool-server/src/tools/open-url/platforms/ios.ts b/packages/tool-server/src/tools/open-url/platforms/ios.ts index 00fc99ad1..69c288f3f 100644 --- a/packages/tool-server/src/tools/open-url/platforms/ios.ts +++ b/packages/tool-server/src/tools/open-url/platforms/ios.ts @@ -1,6 +1,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import type { PlatformImpl } from "../../../utils/cross-platform-tool"; +import { simctlArgs } from "../../../utils/simctl"; import type { OpenUrlParams, OpenUrlResult, OpenUrlServices } from "../types"; const execFileAsync = promisify(execFile); @@ -8,7 +9,7 @@ const execFileAsync = promisify(execFile); export const iosImpl: PlatformImpl = { requires: ["xcrun"], handler: async (_services, params) => { - await execFileAsync("xcrun", ["simctl", "openurl", params.udid, params.url]); + await execFileAsync("xcrun", simctlArgs(["openurl", params.udid, params.url])); return { opened: true, url: params.url }; }, }; diff --git a/packages/tool-server/src/tools/profiler/native-profiler/platforms/ios.ts b/packages/tool-server/src/tools/profiler/native-profiler/platforms/ios.ts index 4386cb0cf..10704bf6a 100644 --- a/packages/tool-server/src/tools/profiler/native-profiler/platforms/ios.ts +++ b/packages/tool-server/src/tools/profiler/native-profiler/platforms/ios.ts @@ -1,4 +1,4 @@ -import { spawn, execSync, type ChildProcess } from "child_process"; +import { spawn, execFileSync, type ChildProcess } from "child_process"; import { promises as fs } from "fs"; import { existsSync } from "node:fs"; import * as path from "path"; @@ -16,6 +16,7 @@ import { runIosProfilerPipeline } from "../../../../utils/ios-profiler/pipeline/ import type { NativeProfilerAnalyzeResult } from "../../../../utils/ios-profiler/types"; import { renderNativeProfilerReport } from "../../../../utils/ios-profiler/render"; import { RECORDING_CAP_MS } from "../../../../utils/profiler-shared/types"; +import { simctlArgs } from "../../../../utils/simctl"; // Two candidates because __dirname differs by runtime: bundled it's argent/dist/ // (template in argent/assets/); in dev it's tool-server/dist/tools/profiler/ @@ -84,7 +85,7 @@ interface DetectedApp { function enumerateRunningUserApps(udid: string): { info: AppInfo; pid: number }[] { let launchctlOutput: string; try { - launchctlOutput = execSync(`xcrun simctl spawn ${udid} launchctl list`, { + launchctlOutput = execFileSync("xcrun", simctlArgs(["spawn", udid, "launchctl", "list"]), { encoding: "utf-8", timeout: DETECT_RUNNING_APP_TIMEOUT_MS, }); @@ -115,7 +116,12 @@ function enumerateRunningUserApps(udid: string): { info: AppInfo; pid: number }[ let listAppsOutput: string; try { - listAppsOutput = execSync(`xcrun simctl listapps ${udid} | plutil -convert json -o - -`, { + const listAppsPlist = execFileSync("xcrun", simctlArgs(["listapps", udid]), { + encoding: "utf-8", + timeout: DETECT_RUNNING_APP_TIMEOUT_MS, + }); + listAppsOutput = execFileSync("plutil", ["-convert", "json", "-o", "-", "-"], { + input: listAppsPlist, encoding: "utf-8", timeout: DETECT_RUNNING_APP_TIMEOUT_MS, }); diff --git a/packages/tool-server/src/tools/reinstall-app/platforms/ios.ts b/packages/tool-server/src/tools/reinstall-app/platforms/ios.ts index 9cd143eee..e66701497 100644 --- a/packages/tool-server/src/tools/reinstall-app/platforms/ios.ts +++ b/packages/tool-server/src/tools/reinstall-app/platforms/ios.ts @@ -2,6 +2,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { resolve as resolvePath } from "node:path"; import type { PlatformImpl } from "../../../utils/cross-platform-tool"; +import { simctlArgs } from "../../../utils/simctl"; import type { ReinstallAppParams, ReinstallAppResult, ReinstallAppServices } from "../types"; const execFileAsync = promisify(execFile); @@ -12,11 +13,11 @@ export const iosImpl: PlatformImpl { try { - const { stdout } = await execFileAsync("xcrun", ["simctl", "list", "devices", "--json"], { + const { stdout } = await execFileAsync("xcrun", simctlArgs(["list", "devices", "--json"]), { timeout: 10_000, }); const data: SimctlOutput = JSON.parse(stdout); diff --git a/packages/tool-server/src/utils/simctl.ts b/packages/tool-server/src/utils/simctl.ts new file mode 100644 index 000000000..8ab2311fa --- /dev/null +++ b/packages/tool-server/src/utils/simctl.ts @@ -0,0 +1,33 @@ +import * as os from "node:os"; +import * as path from "node:path"; + +export const ARGENT_IOS_DEVICE_SET_ENV = "ARGENT_IOS_DEVICE_SET_PATH"; + +function normalizePath(raw: string): string { + const trimmed = raw.trim(); + if (trimmed === "~") return os.homedir(); + if (trimmed.startsWith("~/")) return path.join(os.homedir(), trimmed.slice(2)); + return path.resolve(trimmed); +} + +export function iosDeviceSetPath(env: NodeJS.ProcessEnv = process.env): string | null { + const raw = env[ARGENT_IOS_DEVICE_SET_ENV]; + if (!raw?.trim()) return null; + return normalizePath(raw); +} + +export function defaultIosDeviceSetPath(): string { + return path.join(os.homedir(), "Library/Developer/CoreSimulator/Devices"); +} + +export function activeIosDeviceSetPath(env: NodeJS.ProcessEnv = process.env): string { + return iosDeviceSetPath(env) ?? defaultIosDeviceSetPath(); +} + +export function simctlArgs( + args: readonly string[], + env: NodeJS.ProcessEnv = process.env +): string[] { + const deviceSet = iosDeviceSetPath(env); + return deviceSet ? ["simctl", "--set", deviceSet, ...args] : ["simctl", ...args]; +} diff --git a/packages/tool-server/src/utils/simulator-watcher.ts b/packages/tool-server/src/utils/simulator-watcher.ts index d0da33d6f..5f4bd1c44 100644 --- a/packages/tool-server/src/utils/simulator-watcher.ts +++ b/packages/tool-server/src/utils/simulator-watcher.ts @@ -6,13 +6,14 @@ import { nativeDevtoolsRef, type NativeDevtoolsApi, } from "../blueprints/native-devtools"; +import { simctlArgs } from "./simctl"; const execFileAsync = promisify(execFile); const POLL_INTERVAL_MS = 10_000; async function getBootedUdids(): Promise> { - const { stdout } = await execFileAsync("xcrun", ["simctl", "list", "devices", "--json"]); + const { stdout } = await execFileAsync("xcrun", simctlArgs(["list", "devices", "--json"])); const data = JSON.parse(stdout) as { devices: Record>; }; diff --git a/packages/tool-server/test/boot-device.test.ts b/packages/tool-server/test/boot-device.test.ts index 23f944859..769fb3a0c 100644 --- a/packages/tool-server/test/boot-device.test.ts +++ b/packages/tool-server/test/boot-device.test.ts @@ -51,9 +51,11 @@ describe("boot-device — iOS path", () => { // duration of the suite — restored after each test to avoid leaking the // override into other test files run in the same vitest worker. const originalPlatform = process.platform; + const originalIosDeviceSetPath = process.env.ARGENT_IOS_DEVICE_SET_PATH; beforeEach(() => { Object.defineProperty(process, "platform", { value: "darwin", configurable: true }); + delete process.env.ARGENT_IOS_DEVICE_SET_PATH; vi.clearAllMocks(); // Pre-warm the dep cache so `ensureDep('xcrun')` doesn't probe PATH and // add an extra first `command -v xcrun` call to mockExecFile. @@ -77,6 +79,8 @@ describe("boot-device — iOS path", () => { afterEach(() => { Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }); + if (originalIosDeviceSetPath === undefined) delete process.env.ARGENT_IOS_DEVICE_SET_PATH; + else process.env.ARGENT_IOS_DEVICE_SET_PATH = originalIosDeviceSetPath; }); it("pre-boots AX prefs on a Shutdown sim then waits for boot completion and native-devtools init", async () => { @@ -147,6 +151,42 @@ describe("boot-device — iOS path", () => { expect(reverifyEnv).toHaveBeenCalledOnce(); }); + it("uses a custom CoreSimulator device set for boot commands without touching global Simulator.app defaults", async () => { + process.env.ARGENT_IOS_DEVICE_SET_PATH = "/tmp/argent-ios-set"; + const resolveService = vi.fn(async () => ({ + getInitFailure: () => null, + reverifyEnv: async () => {}, + })); + const registry = { resolveService } as unknown as Registry; + const tool = createBootDeviceTool(registry); + + await expect( + tool.execute!({}, { udid: "11111111-1111-1111-1111-111111111111" }) + ).resolves.toEqual({ + platform: "ios", + udid: "11111111-1111-1111-1111-111111111111", + booted: true, + }); + + expect(mockExecFile.mock.calls.map(([file, args]) => [file, args])).toEqual([ + [ + "xcrun", + ["simctl", "--set", "/tmp/argent-ios-set", "boot", "11111111-1111-1111-1111-111111111111"], + ], + [ + "xcrun", + [ + "simctl", + "--set", + "/tmp/argent-ios-set", + "bootstatus", + "11111111-1111-1111-1111-111111111111", + "-b", + ], + ], + ]); + }); + it("skips pre-boot plist write when the sim is already Booted and falls back to ensureAutomationEnabled", async () => { const resolveService = vi.fn(async () => ({ getInitFailure: () => null, diff --git a/packages/tool-server/test/simctl.test.ts b/packages/tool-server/test/simctl.test.ts new file mode 100644 index 000000000..6cc6447a1 --- /dev/null +++ b/packages/tool-server/test/simctl.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import * as os from "node:os"; +import * as path from "node:path"; +import { + activeIosDeviceSetPath, + defaultIosDeviceSetPath, + iosDeviceSetPath, + simctlArgs, +} from "../src/utils/simctl"; + +describe("simctl device-set helpers", () => { + it("uses plain simctl args when no custom device set is configured", () => { + expect(simctlArgs(["list", "devices"], {})).toEqual(["simctl", "list", "devices"]); + }); + + it("injects --set with a normalized custom device set path", () => { + expect( + simctlArgs(["list", "devices"], { ARGENT_IOS_DEVICE_SET_PATH: "tmp/device-set" }) + ).toEqual(["simctl", "--set", path.resolve("tmp/device-set"), "list", "devices"]); + }); + + it("expands home-relative paths", () => { + const expected = path.join(os.homedir(), "Library/Developer/CoreSimulator/CustomDevices"); + expect( + iosDeviceSetPath({ + ARGENT_IOS_DEVICE_SET_PATH: "~/Library/Developer/CoreSimulator/CustomDevices", + }) + ).toBe(expected); + }); + + it("exposes the default device set path when no custom set is configured", () => { + expect(defaultIosDeviceSetPath()).toBe( + path.join(os.homedir(), "Library/Developer/CoreSimulator/Devices") + ); + expect(activeIosDeviceSetPath({})).toBe(defaultIosDeviceSetPath()); + }); +}); diff --git a/packages/tool-server/test/simulator-server-blueprint.test.ts b/packages/tool-server/test/simulator-server-blueprint.test.ts index be890ad49..102685deb 100644 --- a/packages/tool-server/test/simulator-server-blueprint.test.ts +++ b/packages/tool-server/test/simulator-server-blueprint.test.ts @@ -13,6 +13,7 @@ import type { DeviceInfo } from "@argent/registry"; const spawnMock = vi.fn(); const ensureAutomationEnabledMock = vi.fn(); +const originalIosDeviceSetPath = process.env.ARGENT_IOS_DEVICE_SET_PATH; vi.mock("node:child_process", async () => { const actual = await vi.importActual("node:child_process"); @@ -75,6 +76,7 @@ function androidDevice(serial: string): DeviceInfo { describe("simulatorServerBlueprint.factory — receives a pre-resolved DeviceInfo", () => { beforeEach(async () => { + delete process.env.ARGENT_IOS_DEVICE_SET_PATH; spawnMock.mockReset(); ensureAutomationEnabledMock.mockReset().mockResolvedValue(undefined); // Pre-warm the dep cache so the Android branch's `ensureDep('adb')` doesn't @@ -89,6 +91,8 @@ describe("simulatorServerBlueprint.factory — receives a pre-resolved DeviceInf }); afterEach(() => { + if (originalIosDeviceSetPath === undefined) delete process.env.ARGENT_IOS_DEVICE_SET_PATH; + else process.env.ARGENT_IOS_DEVICE_SET_PATH = originalIosDeviceSetPath; vi.clearAllMocks(); }); @@ -122,6 +126,29 @@ describe("simulatorServerBlueprint.factory — receives a pre-resolved DeviceInf expect(fakeProc.kill).toHaveBeenCalledTimes(1); }); + it("passes the configured CoreSimulator device set to the iOS simulator-server", async () => { + process.env.ARGENT_IOS_DEVICE_SET_PATH = "/tmp/argent-ios-set"; + const fakeProc = makeFakeProc(); + spawnMock.mockReturnValue(fakeProc); + + const { simulatorServerBlueprint } = await import("../src/blueprints/simulator-server"); + + const udid = "11111111-2222-3333-4444-555555555555"; + const device = iosDevice(udid); + const factoryPromise = simulatorServerBlueprint.factory({}, device, { device }); + signalReady(fakeProc, 55560); + await factoryPromise; + + expect(spawnMock).toHaveBeenCalledTimes(1); + expect(spawnMock.mock.calls[0]![1]).toEqual([ + "ios", + "--id", + udid, + "--device-set", + "/tmp/argent-ios-set", + ]); + }); + it("spawns the `android` subcommand for an Android device", async () => { const fakeProc = makeFakeProc(); spawnMock.mockReturnValue(fakeProc); From 9ce22f9376851416c0cacc071c0b3f4eb183c533 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 24 Jun 2026 12:29:28 +0200 Subject: [PATCH 2/2] Use path.resolve for iOS device set paths --- packages/argent-cli/src/server.ts | 10 +++++----- packages/argent-tools-client/src/index.ts | 2 +- packages/argent-tools-client/src/launcher.ts | 15 ++++----------- .../test/launcher-helpers.test.ts | 10 +++++----- packages/argent/src/cli.ts | 12 ++---------- packages/tool-server/src/utils/simctl.ts | 9 +-------- packages/tool-server/test/simctl.test.ts | 16 +--------------- 7 files changed, 19 insertions(+), 55 deletions(-) diff --git a/packages/argent-cli/src/server.ts b/packages/argent-cli/src/server.ts index 15ad8c395..2c412b9b3 100644 --- a/packages/argent-cli/src/server.ts +++ b/packages/argent-cli/src/server.ts @@ -17,7 +17,7 @@ import { formatLinkUrl, generateAuthToken, IOS_DEVICE_SET_ENV, - normalizeIosDeviceSetPath, + resolveIosDeviceSetPath, type ToolsServerPaths, type ToolsServerState, } from "@argent/tools-client"; @@ -208,11 +208,11 @@ export function parseIdle(raw: string): number { } export function parseIosDeviceSetPath(raw: string): string { - const normalized = normalizeIosDeviceSetPath(raw); - if (!normalized) { + const resolved = resolveIosDeviceSetPath(raw); + if (!resolved) { throw new StartFlagError(`--ios-device-set requires a non-empty path`); } - return normalized; + return resolved; } function printStartHelp(): void { @@ -358,7 +358,7 @@ async function startCmd(argv: string[], paths: ToolsServerPaths | undefined): Pr const port = await resolvePort(flags.port); const iosDeviceSetPath = - flags.iosDeviceSetPath ?? normalizeIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]); + flags.iosDeviceSetPath ?? resolveIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]); // Auth on by default; --no-auth opts out (token stays undefined → the // tool-server runs unauthenticated and prints its own warning). diff --git a/packages/argent-tools-client/src/index.ts b/packages/argent-tools-client/src/index.ts index 689a22022..ff290b709 100644 --- a/packages/argent-tools-client/src/index.ts +++ b/packages/argent-tools-client/src/index.ts @@ -15,7 +15,7 @@ export { AUTH_TOKEN_ENV, IOS_DEVICE_SET_ENV, STATE_PATHS, - normalizeIosDeviceSetPath, + resolveIosDeviceSetPath, type ToolsServerPaths, type ToolsServerState, type ToolsServerHandle, diff --git a/packages/argent-tools-client/src/launcher.ts b/packages/argent-tools-client/src/launcher.ts index 879e09364..0e4476277 100644 --- a/packages/argent-tools-client/src/launcher.ts +++ b/packages/argent-tools-client/src/launcher.ts @@ -50,16 +50,9 @@ export interface BuildToolsServerEnvOptions { iosDeviceSetPath?: string | null; } -function normalizePath(raw: string): string { - const trimmed = raw.trim(); - if (trimmed === "~") return os.homedir(); - if (trimmed.startsWith("~/")) return path.join(os.homedir(), trimmed.slice(2)); - return path.resolve(trimmed); -} - -export function normalizeIosDeviceSetPath(raw: string | null | undefined): string | null { +export function resolveIosDeviceSetPath(raw: string | null | undefined): string | null { if (!raw?.trim()) return null; - return normalizePath(raw); + return path.resolve(raw.trim()); } export function buildToolsServerEnv( @@ -68,7 +61,7 @@ export function buildToolsServerEnv( baseEnv: NodeJS.ProcessEnv = process.env, options: BuildToolsServerEnvOptions = {} ): NodeJS.ProcessEnv { - const iosDeviceSetPath = normalizeIosDeviceSetPath( + const iosDeviceSetPath = resolveIosDeviceSetPath( options.iosDeviceSetPath ?? baseEnv[IOS_DEVICE_SET_ENV] ); const env: NodeJS.ProcessEnv = { @@ -380,7 +373,7 @@ export async function killToolServer(): Promise { } export async function ensureToolsServer(paths: ToolsServerPaths): Promise { - const requestedIosDeviceSetPath = normalizeIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]); + const requestedIosDeviceSetPath = resolveIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]); const state = await readState(); if (state) { diff --git a/packages/argent-tools-client/test/launcher-helpers.test.ts b/packages/argent-tools-client/test/launcher-helpers.test.ts index 1f0c0554f..88b989d02 100644 --- a/packages/argent-tools-client/test/launcher-helpers.test.ts +++ b/packages/argent-tools-client/test/launcher-helpers.test.ts @@ -8,7 +8,7 @@ import { IOS_DEVICE_SET_ENV, isToolsServerHealthy, isToolsServerProcessAlive, - normalizeIosDeviceSetPath, + resolveIosDeviceSetPath, } from "../src/launcher.js"; const paths = { @@ -60,14 +60,14 @@ describe("buildToolsServerEnv — host and idle options", () => { }); }); -describe("normalizeIosDeviceSetPath", () => { +describe("resolveIosDeviceSetPath", () => { it("returns null for empty values", () => { - expect(normalizeIosDeviceSetPath(undefined)).toBeNull(); - expect(normalizeIosDeviceSetPath(" ")).toBeNull(); + expect(resolveIosDeviceSetPath(undefined)).toBeNull(); + expect(resolveIosDeviceSetPath(" ")).toBeNull(); }); it("resolves relative paths", () => { - expect(normalizeIosDeviceSetPath("tmp/device-set")).toBe(path.resolve("tmp/device-set")); + expect(resolveIosDeviceSetPath("tmp/device-set")).toBe(path.resolve("tmp/device-set")); }); }); diff --git a/packages/argent/src/cli.ts b/packages/argent/src/cli.ts index 1edb9e5e0..9d966a7a8 100644 --- a/packages/argent/src/cli.ts +++ b/packages/argent/src/cli.ts @@ -27,7 +27,6 @@ */ import * as fs from "node:fs"; -import * as os from "node:os"; import * as path from "node:path"; import type * as Installer from "@argent/installer"; import type * as Mcp from "@argent/mcp"; @@ -50,13 +49,6 @@ function getInstalledVersion(): string | null { } } -function normalizePath(raw: string): string { - const trimmed = raw.trim(); - if (trimmed === "~") return os.homedir(); - if (trimmed.startsWith("~/")) return path.join(os.homedir(), trimmed.slice(2)); - return path.resolve(trimmed); -} - function parseTopLevelArgs(argv: string[]): { command: string | undefined; rest: string[] } { const pending = [...argv]; while (pending.length > 0) { @@ -67,7 +59,7 @@ function parseTopLevelArgs(argv: string[]): { command: string | undefined; rest: console.error("Error: --ios-device-set requires a non-empty path"); process.exit(2); } - process.env[IOS_DEVICE_SET_ENV] = normalizePath(value); + process.env[IOS_DEVICE_SET_ENV] = path.resolve(value.trim()); pending.splice(0, 2); continue; } @@ -77,7 +69,7 @@ function parseTopLevelArgs(argv: string[]): { command: string | undefined; rest: console.error("Error: --ios-device-set requires a non-empty path"); process.exit(2); } - process.env[IOS_DEVICE_SET_ENV] = normalizePath(value); + process.env[IOS_DEVICE_SET_ENV] = path.resolve(value.trim()); pending.shift(); continue; } diff --git a/packages/tool-server/src/utils/simctl.ts b/packages/tool-server/src/utils/simctl.ts index 8ab2311fa..5b6cae639 100644 --- a/packages/tool-server/src/utils/simctl.ts +++ b/packages/tool-server/src/utils/simctl.ts @@ -3,17 +3,10 @@ import * as path from "node:path"; export const ARGENT_IOS_DEVICE_SET_ENV = "ARGENT_IOS_DEVICE_SET_PATH"; -function normalizePath(raw: string): string { - const trimmed = raw.trim(); - if (trimmed === "~") return os.homedir(); - if (trimmed.startsWith("~/")) return path.join(os.homedir(), trimmed.slice(2)); - return path.resolve(trimmed); -} - export function iosDeviceSetPath(env: NodeJS.ProcessEnv = process.env): string | null { const raw = env[ARGENT_IOS_DEVICE_SET_ENV]; if (!raw?.trim()) return null; - return normalizePath(raw); + return path.resolve(raw.trim()); } export function defaultIosDeviceSetPath(): string { diff --git a/packages/tool-server/test/simctl.test.ts b/packages/tool-server/test/simctl.test.ts index 6cc6447a1..324736280 100644 --- a/packages/tool-server/test/simctl.test.ts +++ b/packages/tool-server/test/simctl.test.ts @@ -1,12 +1,7 @@ import { describe, expect, it } from "vitest"; import * as os from "node:os"; import * as path from "node:path"; -import { - activeIosDeviceSetPath, - defaultIosDeviceSetPath, - iosDeviceSetPath, - simctlArgs, -} from "../src/utils/simctl"; +import { activeIosDeviceSetPath, defaultIosDeviceSetPath, simctlArgs } from "../src/utils/simctl"; describe("simctl device-set helpers", () => { it("uses plain simctl args when no custom device set is configured", () => { @@ -19,15 +14,6 @@ describe("simctl device-set helpers", () => { ).toEqual(["simctl", "--set", path.resolve("tmp/device-set"), "list", "devices"]); }); - it("expands home-relative paths", () => { - const expected = path.join(os.homedir(), "Library/Developer/CoreSimulator/CustomDevices"); - expect( - iosDeviceSetPath({ - ARGENT_IOS_DEVICE_SET_PATH: "~/Library/Developer/CoreSimulator/CustomDevices", - }) - ).toBe(expected); - }); - it("exposes the default device set path when no custom set is configured", () => { expect(defaultIosDeviceSetPath()).toBe( path.join(os.homedir(), "Library/Developer/CoreSimulator/Devices")