Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions packages/argent-cli/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
formatToolsServerUrl,
formatLinkUrl,
generateAuthToken,
IOS_DEVICE_SET_ENV,
resolveIosDeviceSetPath,
type ToolsServerPaths,
type ToolsServerState,
} from "@argent/tools-client";
Expand Down Expand Up @@ -64,6 +66,9 @@ async function statusCmd(json: boolean): Promise<void> {
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.`);
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -115,6 +122,7 @@ export function parseStartFlags(argv: string[]): StartFlags {
detach: false,
force: false,
noAuth: false,
iosDeviceSetPath: null,
help: false,
};

Expand Down Expand Up @@ -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}`);
}

Expand All @@ -191,6 +207,14 @@ export function parseIdle(raw: string): number {
return Number(raw);
}

export function parseIosDeviceSetPath(raw: string): string {
const resolved = resolveIosDeviceSetPath(raw);
if (!resolved) {
throw new StartFlagError(`--ios-device-set requires a non-empty path`);
}
return resolved;
}

function printStartHelp(): void {
console.log(`Usage: argent server start [flags]

Expand All @@ -203,6 +227,8 @@ Flags:
Use 0.0.0.0 to expose on every interface.
--idle-timeout <m> Auto-shutdown after <m> idle minutes (0 disables).
Default: 0 (never auto-shutdown).
--ios-device-set <path> 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
Expand Down Expand Up @@ -331,6 +357,8 @@ async function startCmd(argv: string[], paths: ToolsServerPaths | undefined): Pr
}

const port = await resolvePort(flags.port);
const iosDeviceSetPath =
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).
Expand All @@ -352,23 +380,25 @@ 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(
paths: ToolsServerPaths,
port: number,
host: string,
idleTimeoutMinutes: number,
iosDeviceSetPath: string | null,
token?: string
): Promise<void> {
const { port: actualPort, pid } = await spawnToolsServer(paths, port, {
host,
idleTimeoutMinutes,
iosDeviceSetPath,
token,
});
await writeToolsServerState({
Expand All @@ -377,6 +407,7 @@ async function runDetached(
startedAt: new Date().toISOString(),
bundlePath: paths.bundlePath,
host,
...(iosDeviceSetPath ? { iosDeviceSetPath } : {}),
...(token ? { token } : {}),
});
const url = formatToolsServerUrl(host, actualPort);
Expand All @@ -393,11 +424,13 @@ async function runForeground(
port: number,
host: string,
idleTimeoutMinutes: number,
iosDeviceSetPath: string | null,
token?: string
): Promise<void> {
const env = buildToolsServerEnv(paths, port, process.env, {
host,
idleTimeoutMinutes,
iosDeviceSetPath,
token,
});

Expand Down Expand Up @@ -432,6 +465,7 @@ async function runForeground(
startedAt: new Date().toISOString(),
bundlePath: paths.bundlePath,
host,
...(iosDeviceSetPath ? { iosDeviceSetPath } : {}),
...(token ? { token } : {}),
});
stateWritten = true;
Expand Down
18 changes: 18 additions & 0 deletions packages/argent-cli/test/parse-start-flags.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -10,6 +11,7 @@ describe("parseStartFlags", () => {
detach: false,
force: false,
noAuth: false,
iosDeviceSetPath: null,
help: false,
});
});
Expand All @@ -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);
Expand All @@ -61,6 +72,7 @@ describe("parseStartFlags", () => {
detach: true,
force: true,
noAuth: false,
iosDeviceSetPath: null,
help: false,
});
});
Expand All @@ -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", () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/argent-tools-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export {
formatToolsServerUrl,
generateAuthToken,
AUTH_TOKEN_ENV,
IOS_DEVICE_SET_ENV,
STATE_PATHS,
resolveIosDeviceSetPath,
type ToolsServerPaths,
type ToolsServerState,
type ToolsServerHandle,
Expand Down
29 changes: 26 additions & 3 deletions packages/argent-tools-client/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,6 +46,13 @@ 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;
}

export function resolveIosDeviceSetPath(raw: string | null | undefined): string | null {
if (!raw?.trim()) return null;
return path.resolve(raw.trim());
}

export function buildToolsServerEnv(
Expand All @@ -53,12 +61,17 @@ export function buildToolsServerEnv(
baseEnv: NodeJS.ProcessEnv = process.env,
options: BuildToolsServerEnvOptions = {}
): NodeJS.ProcessEnv {
const iosDeviceSetPath = resolveIosDeviceSetPath(
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);
Expand All @@ -81,6 +94,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. */
Expand Down Expand Up @@ -358,19 +373,25 @@ export async function killToolServer(): Promise<void> {
}

export async function ensureToolsServer(paths: ToolsServerPaths): Promise<ToolsServerHandle> {
const requestedIosDeviceSetPath = resolveIosDeviceSetPath(process.env[IOS_DEVICE_SET_ENV]);
const state = await readState();

if (state) {
const alive = isProcessAlive(state.pid);
if (alive) {
const host = state.host ?? "127.0.0.1";
const healthy = await isToolsServerHealthy(state.port, host, 2000, state.token);
if (healthy) {
const sameIosDeviceSet =
(state.iosDeviceSetPath ?? null) === (requestedIosDeviceSetPath ?? null);
if (healthy && sameIosDeviceSet) {
return {
url: formatUrl(healthCheckHost(host), state.port),
token: state.token ?? "",
};
}
if (healthy && !sameIosDeviceSet) {
await killToolServer();
}
}
await clearState();
}
Expand All @@ -382,6 +403,7 @@ export async function ensureToolsServer(paths: ToolsServerPaths): Promise<ToolsS
const { port: actualPort, pid } = await spawnToolsServer(paths, port, {
token,
idleTimeoutMinutes: AUTOSPAWN_IDLE_TIMEOUT_MINUTES,
iosDeviceSetPath: requestedIosDeviceSetPath,
});

await writeState({
Expand All @@ -391,6 +413,7 @@ export async function ensureToolsServer(paths: ToolsServerPaths): Promise<ToolsS
bundlePath: paths.bundlePath,
host: "127.0.0.1",
token,
...(requestedIosDeviceSetPath ? { iosDeviceSetPath: requestedIosDeviceSetPath } : {}),
});

return { url: formatUrl("127.0.0.1", actualPort), token };
Expand Down
29 changes: 29 additions & 0 deletions packages/argent-tools-client/test/launcher-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { describe, expect, it } from "vitest";
import * as net from "node:net";
import * as path from "node:path";
import {
buildToolsServerEnv,
findFreePort,
formatToolsServerUrl,
IOS_DEVICE_SET_ENV,
isToolsServerHealthy,
isToolsServerProcessAlive,
resolveIosDeviceSetPath,
} from "../src/launcher.js";

const paths = {
Expand Down Expand Up @@ -40,6 +43,32 @@ describe("buildToolsServerEnv — host and idle options", () => {
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("resolveIosDeviceSetPath", () => {
it("returns null for empty values", () => {
expect(resolveIosDeviceSetPath(undefined)).toBeNull();
expect(resolveIosDeviceSetPath(" ")).toBeNull();
});

it("resolves relative paths", () => {
expect(resolveIosDeviceSetPath("tmp/device-set")).toBe(path.resolve("tmp/device-set"));
});
});

describe("formatToolsServerUrl", () => {
Expand Down
Loading
Loading