mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
feat: add native devtools iOS — view hierarchy, network inspection, passive simulator watcher
- Adds native-devtools-ios package with pre-built dylibs (libNativeDevtoolsIos, libKeyboardPatch, libInjectionBootstrap) - Passive simulator watcher injects dylibs into all booted simulators on tool-server startup, eliminating manual restart requirement - Lazy network activation: NSURLProtocol interception is opt-in per bundleId via Control socket command, re-activated on reconnect - New tools: native-find-views, native-full-hierarchy, native-network-logs, native-devtools-status - dev script: non-fatal argent-private submodule (uses pre-built dylibs when source unavailable) - Adds Prettier config and format script - Updates CONTRIBUTING.md with npm run dev workflow and project structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"quoteProps": "consistent",
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
+30
-1
@@ -38,6 +38,22 @@ Thank you for your interest in contributing to Argent! This guide covers everyth
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start the dev environment:**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This builds the native devtools dylibs (if the private submodule is available, otherwise uses pre-built binaries), compiles the MCP TypeScript, patches `~/.claude.json` to point at the local MCP, and starts the tool-server from source via `ts-node`. Press `Ctrl+C` to stop — the script automatically restores your global Argent configuration.
|
||||
|
||||
To use a different port:
|
||||
|
||||
```bash
|
||||
PORT=4000 npm run dev
|
||||
```
|
||||
|
||||
> **Note:** `packages/argent-private` is a private git submodule that holds the ObjC source for the native devtools dylibs. If you don't have SSH access to it, `npm run dev` will use the pre-built dylibs committed to the repository — everything else works normally.
|
||||
|
||||
That's it — no separate install steps per package are needed.
|
||||
|
||||
---
|
||||
@@ -52,6 +68,7 @@ This is an npm workspaces monorepo. All packages live under `packages/`:
|
||||
| `@argent/tool-server` | `packages/tool-server` | HTTP API over the registry (port 3001). Registers all blueprints and tools |
|
||||
| `@software-mansion/argent` | `packages/mcp` | MCP bridge — exposes tools to AI assistants via Model Context Protocol |
|
||||
| `@argent/skills` | `packages/skills` | Markdown skill files (prefixed `argent-*`) that instruct AI agents how to use Argent tools |
|
||||
| `@argent/native-devtools-ios` | `packages/native-devtools-ios` | Pre-built dylibs for iOS simulator injection (view hierarchy, network inspection). ObjC source lives in `packages/argent-private` _(private submodule)_ |
|
||||
|
||||
The `tsconfig.json` at the root uses TypeScript project references; `tsconfig.base.json` holds shared compiler options (`strict`, `ES2022`, etc.).
|
||||
|
||||
@@ -59,6 +76,16 @@ The `tsconfig.json` at the root uses TypeScript project references; `tsconfig.ba
|
||||
|
||||
## Building
|
||||
|
||||
### Local development (recommended)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Builds everything and starts the tool-server from source. See [Setting up the dev environment](#setting-up-the-dev-environment) for details.
|
||||
|
||||
### Full build
|
||||
|
||||
Build all packages at once using TypeScript project references:
|
||||
|
||||
```bash
|
||||
@@ -84,7 +111,9 @@ npm run pack:mcp
|
||||
|
||||
## Running the project
|
||||
|
||||
**Start the tools server:**
|
||||
For day-to-day development, use `npm run dev` (see [Setting up the dev environment](#setting-up-the-dev-environment)).
|
||||
|
||||
**Production-like start** (builds bundles first, then starts from compiled output):
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
|
||||
Generated
+36
-4
@@ -7,7 +7,14 @@
|
||||
"name": "argent-workspace",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@argent/native-devtools-ios": {
|
||||
"resolved": "packages/native-devtools-ios",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@argent/registry": {
|
||||
"resolved": "packages/registry",
|
||||
@@ -2258,6 +2265,22 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"license": "MIT",
|
||||
@@ -4231,9 +4254,17 @@
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
},
|
||||
"packages/native-devtools-ios": {
|
||||
"name": "@argent/native-devtools-ios",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
},
|
||||
"packages/registry": {
|
||||
"name": "@argent/registry",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.3",
|
||||
"dependencies": {
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
@@ -4945,15 +4976,16 @@
|
||||
},
|
||||
"packages/skills": {
|
||||
"name": "@argent/skills",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.3",
|
||||
"bin": {
|
||||
"argent-skills": "scripts/install.js"
|
||||
}
|
||||
},
|
||||
"packages/tool-server": {
|
||||
"name": "@argent/tool-server",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.3",
|
||||
"dependencies": {
|
||||
"@argent/native-devtools-ios": "file:../native-devtools-ios",
|
||||
"@argent/registry": "file:../registry",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"express": "^4.19.2",
|
||||
|
||||
+5
-1
@@ -7,6 +7,10 @@
|
||||
"start": "npm run build -w @argent/registry && npm run dev -w @argent/tool-server",
|
||||
"start:tool-server": "npm run build -w @argent/registry && npm run dev -w @argent/tool-server",
|
||||
"pack:mcp": "npm run build -w @software-mansion/argent && npm pack -w @software-mansion/argent --pack-destination .",
|
||||
"dev": "node scripts/dev.cjs"
|
||||
"dev": "node scripts/dev.cjs",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@argent/native-devtools-ios",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc",
|
||||
"build:dylibs": "bash scripts/build.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
// When bundled by esbuild, __dirname points into dist/.
|
||||
// ARGENT_NATIVE_DEVTOOLS_DIR lets the launcher override the dylib directory,
|
||||
// matching the same pattern used by ARGENT_SIMULATOR_SERVER_DIR.
|
||||
const DYLIB_DIR =
|
||||
process.env.ARGENT_NATIVE_DEVTOOLS_DIR ??
|
||||
path.join(__dirname, "..", "dylibs");
|
||||
|
||||
function requireDylib(name: string): string {
|
||||
const p = path.join(DYLIB_DIR, name);
|
||||
if (!fs.existsSync(p)) {
|
||||
throw new Error(`Native devtools dylib not found: ${p}`);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
export const bootstrapDylibPath = () =>
|
||||
requireDylib("libInjectionBootstrap.dylib");
|
||||
export const nativeDevtoolsDylibPath = () =>
|
||||
requireDylib("libNativeDevtoolsIos.dylib");
|
||||
export const keyboardPatchDylibPath = () =>
|
||||
requireDylib("libKeyboardPatch.dylib");
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@argent/registry": "file:../registry",
|
||||
"@argent/native-devtools-ios": "file:../native-devtools-ios",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"express": "^4.19.2",
|
||||
"source-map-js": "^1.2.1",
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import * as net from "node:net";
|
||||
import * as fs from "node:fs";
|
||||
import * as readline from "node:readline";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
TypedEventEmitter,
|
||||
type ServiceBlueprint,
|
||||
type ServiceEvents,
|
||||
} from "@argent/registry";
|
||||
import { bootstrapDylibPath } from "@argent/native-devtools-ios";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const NATIVE_DEVTOOLS_NAMESPACE = "NativeDevtools";
|
||||
|
||||
export interface NetworkEvent {
|
||||
method: string;
|
||||
params: unknown;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type ViewInspectorMethod =
|
||||
| "ViewHierarchy.getFullHierarchy"
|
||||
| "ViewHierarchy.findViews"
|
||||
| "ViewHierarchy.viewAtPoint"
|
||||
| "ViewHierarchy.userInteractableViewAtPoint"
|
||||
| "ViewHierarchy.describeScreen";
|
||||
|
||||
export interface NativeDevtoolsApi {
|
||||
// Simulator-level
|
||||
isEnvSetup(): boolean;
|
||||
readonly socketPath: string;
|
||||
|
||||
// App-level — all keyed by bundleId
|
||||
isConnected(bundleId: string): boolean;
|
||||
/**
|
||||
* Returns true if the app needs to be restarted before native features are available.
|
||||
* Async because when not connected it re-verifies and re-sets the launchd env —
|
||||
* this handles the simulator-reboot case where DYLD_INSERT_LIBRARIES was silently cleared.
|
||||
*/
|
||||
requiresAppRestart(bundleId: string): Promise<boolean>;
|
||||
/**
|
||||
* Activates NSURLProtocol network interception for a specific app.
|
||||
* Idempotent — safe to call multiple times. Sticky: if the app is killed
|
||||
* and relaunched, network inspection is automatically re-enabled on reconnect.
|
||||
*/
|
||||
activateNetworkInspection(bundleId: string): void;
|
||||
getNetworkLog(bundleId: string): NetworkEvent[];
|
||||
clearNetworkLog(bundleId: string): void;
|
||||
queryViewHierarchy(
|
||||
bundleId: string,
|
||||
method: ViewInspectorMethod,
|
||||
params?: Record<string, unknown>
|
||||
): Promise<unknown>;
|
||||
}
|
||||
|
||||
interface AppConnection {
|
||||
socket: net.Socket;
|
||||
networkLog: NetworkEvent[];
|
||||
}
|
||||
|
||||
function getNativeDevtoolsSocketPath(udid: string): string {
|
||||
// Deterministic, short — well under the 104-char macOS Unix socket limit
|
||||
// /tmp/argent-nd-XXXXXXXX.sock = 28 chars
|
||||
return `/tmp/argent-nd-${udid.slice(0, 8)}.sock`;
|
||||
}
|
||||
|
||||
async function ensureEnv(udid: string, socketPath: string): Promise<void> {
|
||||
const bootstrapPath = bootstrapDylibPath();
|
||||
|
||||
// xcrun simctl getenv exits non-zero when the var is unset — suppress rejection
|
||||
const result = await execFileAsync(
|
||||
"xcrun",
|
||||
["simctl", "getenv", udid, "DYLD_INSERT_LIBRARIES"],
|
||||
{ encoding: "utf8" }
|
||||
).catch((e) => ({ stdout: (e as NodeJS.ErrnoException & { stdout?: string }).stdout ?? "" }));
|
||||
|
||||
const existing = (result.stdout ?? "").trim();
|
||||
const entries = existing ? existing.split(":") : [];
|
||||
|
||||
if (!entries.includes(bootstrapPath)) {
|
||||
const updated = [...entries, bootstrapPath].join(":");
|
||||
await execFileAsync("xcrun", [
|
||||
"simctl",
|
||||
"spawn",
|
||||
udid,
|
||||
"launchctl",
|
||||
"setenv",
|
||||
"DYLD_INSERT_LIBRARIES",
|
||||
updated,
|
||||
]);
|
||||
}
|
||||
|
||||
// Always re-set the socket path — deterministic value, cheap no-op if already correct,
|
||||
// ensures correctness after tool-server restarts.
|
||||
await execFileAsync("xcrun", [
|
||||
"simctl",
|
||||
"spawn",
|
||||
udid,
|
||||
"launchctl",
|
||||
"setenv",
|
||||
"NATIVE_DEVTOOLS_IOS_CDP_SOCKET",
|
||||
socketPath,
|
||||
]);
|
||||
}
|
||||
|
||||
export const nativeDevtoolsBlueprint: ServiceBlueprint<
|
||||
NativeDevtoolsApi,
|
||||
string
|
||||
> = {
|
||||
namespace: NATIVE_DEVTOOLS_NAMESPACE,
|
||||
|
||||
getURN(udid: string) {
|
||||
return `${NATIVE_DEVTOOLS_NAMESPACE}:${udid}`;
|
||||
},
|
||||
|
||||
async factory(_deps, udid) {
|
||||
const socketPath = getNativeDevtoolsSocketPath(udid);
|
||||
const MAX_LOG_ENTRIES = 1000;
|
||||
const connections = new Map<string, AppConnection>();
|
||||
const pendingRpc = new Map<
|
||||
number,
|
||||
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
|
||||
>();
|
||||
let nextRpcId = 1;
|
||||
let envSetup = false;
|
||||
|
||||
const activatedBundleIds = new Set<string>();
|
||||
const events = new TypedEventEmitter<ServiceEvents>();
|
||||
|
||||
// Remove stale socket file from a crashed previous run
|
||||
try {
|
||||
fs.unlinkSync(socketPath);
|
||||
} catch {}
|
||||
|
||||
// ── Socket server ─────────────────────────────────────────────────────────
|
||||
const server = net.createServer((socket) => {
|
||||
let bundleId: string | null = null;
|
||||
const rl = readline.createInterface({ input: socket });
|
||||
|
||||
rl.on("line", (raw) => {
|
||||
let msg: { type: string; payload: any };
|
||||
try {
|
||||
msg = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Handshake (must be first message) ──
|
||||
if (bundleId === null) {
|
||||
if (msg.type !== "Control") return;
|
||||
bundleId = msg.payload.bundleId as string;
|
||||
|
||||
// If the same app reconnects (e.g. fast restart), close the old socket
|
||||
const existing = connections.get(bundleId);
|
||||
if (existing) {
|
||||
existing.socket.destroy();
|
||||
}
|
||||
|
||||
connections.set(bundleId, { socket, networkLog: [] });
|
||||
|
||||
// Re-activate network inspection if it was previously enabled for this app
|
||||
if (activatedBundleIds.has(bundleId)) {
|
||||
socket.write(
|
||||
JSON.stringify({
|
||||
type: "Control",
|
||||
payload: { command: "activateNetworkInspection" },
|
||||
}) + "\n"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── CDP Network.* events ──
|
||||
if (msg.type === "CDP") {
|
||||
const p = msg.payload;
|
||||
// Unsolicited events have method but no id
|
||||
if (p.method && p.id === undefined) {
|
||||
const conn = connections.get(bundleId);
|
||||
if (conn) {
|
||||
if (conn.networkLog.length >= MAX_LOG_ENTRIES) {
|
||||
conn.networkLog.shift();
|
||||
}
|
||||
conn.networkLog.push({
|
||||
method: p.method,
|
||||
params: p.params,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ViewInspector RPC responses ──
|
||||
if (msg.type === "ViewInspector") {
|
||||
const p = msg.payload;
|
||||
const pending = pendingRpc.get(p.id);
|
||||
if (!pending) return;
|
||||
pendingRpc.delete(p.id);
|
||||
if (p.error) pending.reject(new Error(p.error.message));
|
||||
else pending.resolve(p.result);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
rl.close();
|
||||
if (bundleId !== null) {
|
||||
// Only delete if this socket is still the active one —
|
||||
// a fast reconnect may have already replaced it
|
||||
if (connections.get(bundleId)?.socket === socket) {
|
||||
connections.delete(bundleId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("error", () => {
|
||||
// errors are handled via the close event
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(socketPath);
|
||||
|
||||
// ── ensureEnv — runs once at factory init ─────────────────────────────────
|
||||
await ensureEnv(udid, socketPath);
|
||||
envSetup = true;
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
const api: NativeDevtoolsApi = {
|
||||
isEnvSetup: () => envSetup,
|
||||
socketPath,
|
||||
|
||||
isConnected: (bundleId) => connections.has(bundleId),
|
||||
|
||||
async requiresAppRestart(bundleId) {
|
||||
if (connections.has(bundleId)) return false;
|
||||
// Re-verify and re-set env — handles the case where the simulator was
|
||||
// rebooted and launchd cleared DYLD_INSERT_LIBRARIES
|
||||
await ensureEnv(udid, socketPath);
|
||||
return true;
|
||||
},
|
||||
|
||||
activateNetworkInspection(bundleId) {
|
||||
activatedBundleIds.add(bundleId);
|
||||
const conn = connections.get(bundleId);
|
||||
if (conn) {
|
||||
conn.socket.write(
|
||||
JSON.stringify({
|
||||
type: "Control",
|
||||
payload: { command: "activateNetworkInspection" },
|
||||
}) + "\n"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getNetworkLog: (bundleId) => [
|
||||
...(connections.get(bundleId)?.networkLog ?? []),
|
||||
],
|
||||
|
||||
clearNetworkLog: (bundleId) => {
|
||||
const conn = connections.get(bundleId);
|
||||
if (conn) conn.networkLog.length = 0;
|
||||
},
|
||||
|
||||
queryViewHierarchy(bundleId, method, params = {}) {
|
||||
const conn = connections.get(bundleId);
|
||||
if (!conn) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Native devtools not connected for bundleId: " + bundleId
|
||||
)
|
||||
);
|
||||
}
|
||||
const id = nextRpcId++;
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRpc.set(id, { resolve, reject });
|
||||
conn.socket.write(
|
||||
JSON.stringify({
|
||||
type: "ViewInspector",
|
||||
payload: { id, method, params },
|
||||
}) + "\n"
|
||||
);
|
||||
setTimeout(() => {
|
||||
if (pendingRpc.has(id)) {
|
||||
pendingRpc.delete(id);
|
||||
reject(new Error(`ViewInspector RPC timed out: ${method}`));
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
api,
|
||||
dispose: async () => {
|
||||
for (const { socket } of connections.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
connections.clear();
|
||||
activatedBundleIds.clear();
|
||||
server.close();
|
||||
try {
|
||||
fs.unlinkSync(socketPath);
|
||||
} catch {}
|
||||
for (const { reject } of pendingRpc.values()) {
|
||||
reject(new Error("NativeDevtools service disposed"));
|
||||
}
|
||||
pendingRpc.clear();
|
||||
},
|
||||
events,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { attachRegistryLogger } from "@argent/registry";
|
||||
import { createHttpApp } from "./http";
|
||||
import { createRegistry } from "./utils/setup-registry";
|
||||
import { startSimulatorWatcher } from "./utils/simulator-watcher";
|
||||
import { DEFAULT_IDLE_TIMEOUT_MINUTES } from "./utils/idle-timer";
|
||||
import { startUpdateChecker } from "./utils/update-checker";
|
||||
|
||||
@@ -19,13 +20,18 @@ const registry = createRegistry();
|
||||
attachRegistryLogger(registry);
|
||||
const updateChecker = startUpdateChecker();
|
||||
|
||||
// `shutdown` captures `httpHandle` and `server` by closure; safe because it is
|
||||
// only invoked asynchronously after both are initialized.
|
||||
const { stop: stopWatcher, ready: watcherReady } = startSimulatorWatcher(registry);
|
||||
|
||||
let server: ReturnType<typeof httpHandle.app.listen> | null = null;
|
||||
|
||||
// `shutdown` closes over `server` by reference — reads the current value when
|
||||
// called, so it works correctly whether server has started yet or not.
|
||||
const shutdown = async () => {
|
||||
updateChecker.dispose();
|
||||
stopWatcher();
|
||||
httpHandle.dispose();
|
||||
await registry.dispose();
|
||||
server.close();
|
||||
server?.close();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
@@ -35,15 +41,20 @@ const httpHandle = createHttpApp(registry, {
|
||||
onShutdown: shutdown,
|
||||
});
|
||||
|
||||
const server = httpHandle.app.listen(PORT, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
const boundPort = typeof addr === "object" && addr ? addr.port : PORT;
|
||||
process.stdout.write(`Tools server listening on http://127.0.0.1:${boundPort}\n`);
|
||||
process.stderr.write(` GET http://127.0.0.1:${boundPort}/tools\n`);
|
||||
process.stderr.write(` POST http://127.0.0.1:${boundPort}/tools/:name\n`);
|
||||
if (idleTimeoutMs > 0) {
|
||||
process.stderr.write(` Idle timeout: ${idleMinutes}min\n`);
|
||||
}
|
||||
// Block advertising readiness until the first watcher poll completes — this
|
||||
// guarantees DYLD_INSERT_LIBRARIES is set in launchd for all currently-booted
|
||||
// simulators before any agent tool call (e.g. launch-app) can arrive.
|
||||
watcherReady.then(() => {
|
||||
server = httpHandle.app.listen(PORT, "127.0.0.1", () => {
|
||||
const addr = server!.address();
|
||||
const boundPort = typeof addr === "object" && addr ? addr.port : PORT;
|
||||
process.stdout.write(`Tools server listening on http://127.0.0.1:${boundPort}\n`);
|
||||
process.stderr.write(` GET http://127.0.0.1:${boundPort}/tools\n`);
|
||||
process.stderr.write(` POST http://127.0.0.1:${boundPort}/tools/:name\n`);
|
||||
if (idleTimeoutMs > 0) {
|
||||
process.stderr.write(` Idle timeout: ${idleMinutes}min\n`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { z } from "zod";
|
||||
import type { ToolDefinition } from "@argent/registry";
|
||||
import type { NativeDevtoolsApi } from "../../blueprints/native-devtools";
|
||||
import { NATIVE_DEVTOOLS_NAMESPACE } from "../../blueprints/native-devtools";
|
||||
|
||||
const zodSchema = z.object({
|
||||
udid: z.string().describe("Simulator UDID"),
|
||||
bundleId: z
|
||||
.string()
|
||||
.describe("Bundle ID of the app to check (e.g. com.example.MyApp)"),
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof zodSchema>;
|
||||
type Result = {
|
||||
envSetup: boolean;
|
||||
connected: boolean;
|
||||
requiresRestart: boolean;
|
||||
};
|
||||
|
||||
export const nativeDevtoolsStatusTool: ToolDefinition<Params, Result> = {
|
||||
id: "native-devtools-status",
|
||||
description: `Check whether native devtools dylibs are injected into a specific running app on the simulator.
|
||||
|
||||
Returns:
|
||||
- envSetup: DYLD_INSERT_LIBRARIES is configured in the simulator's launchd environment
|
||||
- connected: the dylib is active in the current running process for this bundleId
|
||||
- requiresRestart: the app must be restarted before native devtools features are available
|
||||
|
||||
Call this before using native-view-hierarchy or native-network-logs.
|
||||
If requiresRestart is true: call restart-app, then proceed with the native feature.`,
|
||||
zodSchema,
|
||||
services: (params) => ({
|
||||
nativeDevtools: `${NATIVE_DEVTOOLS_NAMESPACE}:${params.udid}`,
|
||||
}),
|
||||
async execute(services, params) {
|
||||
const api = services.nativeDevtools as NativeDevtoolsApi;
|
||||
const requiresRestart = await api.requiresAppRestart(params.bundleId);
|
||||
return {
|
||||
envSetup: api.isEnvSetup(),
|
||||
connected: api.isConnected(params.bundleId),
|
||||
requiresRestart,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { z } from "zod";
|
||||
import type { ToolDefinition } from "@argent/registry";
|
||||
import type { NativeDevtoolsApi } from "../../blueprints/native-devtools";
|
||||
import { NATIVE_DEVTOOLS_NAMESPACE } from "../../blueprints/native-devtools";
|
||||
|
||||
const zodSchema = z.object({
|
||||
udid: z.string().describe("Simulator UDID"),
|
||||
bundleId: z.string().describe("Bundle ID of the app"),
|
||||
className: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("UIView class name to match (exact, e.g. UIButton)"),
|
||||
identifier: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Accessibility identifier to match (exact)"),
|
||||
label: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Accessibility label to match (exact)"),
|
||||
tag: z.number().int().optional().describe("UIView tag integer to match"),
|
||||
nativeID: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("React Native nativeID prop to match (exact)"),
|
||||
includeAncestors: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Include ancestor chain for each matched view (default true)"),
|
||||
includeChildren: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Include child views for each matched view (default true)"),
|
||||
fields: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"View fields to include. Defaults: className, frame, hidden, alpha, " +
|
||||
"identifier, label, nativeID, userInteractionEnabled, depth. " +
|
||||
"Additional: pointer, tag, windowFrame, bounds, center, opaque, " +
|
||||
"clipsToBounds, transform, contentMode, backgroundColor, tintColor, layerName"
|
||||
),
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof zodSchema>;
|
||||
type Result =
|
||||
| { status: "restart_required"; message: string }
|
||||
| { status: "ok"; matches: unknown[] };
|
||||
|
||||
export const nativeFindViewsTool: ToolDefinition<Params, Result> = {
|
||||
id: "native-find-views",
|
||||
description: `Search for specific UIViews in the running app by class name, accessibility identifier, label, tag, or React Native nativeID.
|
||||
|
||||
Returns matching views with their frames, properties, optional ancestors, and optional children. Much more targeted than native-full-hierarchy — use this when you know what you're looking for.
|
||||
|
||||
At least one of className, identifier, label, tag, or nativeID must be provided.
|
||||
|
||||
If status is restart_required: call restart-app then retry.`,
|
||||
zodSchema,
|
||||
services: (params) => ({
|
||||
nativeDevtools: `${NATIVE_DEVTOOLS_NAMESPACE}:${params.udid}`,
|
||||
}),
|
||||
async execute(services, params) {
|
||||
const api = services.nativeDevtools as NativeDevtoolsApi;
|
||||
|
||||
if (await api.requiresAppRestart(params.bundleId)) {
|
||||
return {
|
||||
status: "restart_required",
|
||||
message:
|
||||
"Native devtools are not injected into the running app. " +
|
||||
"Call restart-app then retry.",
|
||||
};
|
||||
}
|
||||
|
||||
const rpcParams: Record<string, unknown> = {};
|
||||
if (params.className !== undefined) rpcParams.className = params.className;
|
||||
if (params.identifier !== undefined)
|
||||
rpcParams.identifier = params.identifier;
|
||||
if (params.label !== undefined) rpcParams.label = params.label;
|
||||
if (params.tag !== undefined) rpcParams.tag = params.tag;
|
||||
if (params.nativeID !== undefined) rpcParams.nativeID = params.nativeID;
|
||||
if (params.includeAncestors !== undefined)
|
||||
rpcParams.includeAncestors = params.includeAncestors;
|
||||
if (params.includeChildren !== undefined)
|
||||
rpcParams.includeChildren = params.includeChildren;
|
||||
if (params.fields !== undefined) rpcParams.fields = params.fields;
|
||||
|
||||
const result = (await api.queryViewHierarchy(
|
||||
params.bundleId,
|
||||
"ViewHierarchy.findViews",
|
||||
rpcParams
|
||||
)) as { matches?: unknown[]; error?: string };
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return { status: "ok", matches: result.matches ?? [] };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { z } from "zod";
|
||||
import type { ToolDefinition } from "@argent/registry";
|
||||
import type { NativeDevtoolsApi } from "../../blueprints/native-devtools";
|
||||
import { NATIVE_DEVTOOLS_NAMESPACE } from "../../blueprints/native-devtools";
|
||||
|
||||
const zodSchema = z.object({
|
||||
udid: z.string().describe("Simulator UDID"),
|
||||
bundleId: z.string().describe("Bundle ID of the app"),
|
||||
fields: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"View fields to include. Use EXACT names: " +
|
||||
"className, frame, hidden, alpha, identifier, label, nativeID, " +
|
||||
"userInteractionEnabled, depth, pointer, tag, windowFrame, bounds, " +
|
||||
"center, opaque, clipsToBounds, transform, contentMode, " +
|
||||
"backgroundColor, tintColor, layerName. " +
|
||||
"Defaults to all of the first group when omitted."
|
||||
),
|
||||
skipClasses: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Exact UIView class names whose entire subtree should be pruned " +
|
||||
"(e.g. [\"UIImageView\"] to drop image leaf nodes)"
|
||||
),
|
||||
skipClassPrefixes: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Class name prefixes to prune entire subtrees. " +
|
||||
"For SwiftUI apps use [\"_TtGC7SwiftUI\"] to drop mangled SwiftUI " +
|
||||
"generic type subtrees while keeping _UIHostingView and UIKit bridges. " +
|
||||
"Avoid broad prefixes like \"_UI\" — they prune useful system views."
|
||||
),
|
||||
maxDepth: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe(
|
||||
"Maximum recursion depth (default 8). Increase for deeper inspection, " +
|
||||
"decrease to reduce output size."
|
||||
),
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof zodSchema>;
|
||||
type Result =
|
||||
| { status: "restart_required"; message: string }
|
||||
| { status: "ok"; windows: unknown[] };
|
||||
|
||||
export const nativeFullHierarchyTool: ToolDefinition<Params, Result> = {
|
||||
id: "native-full-hierarchy",
|
||||
description: `Get the complete UIKit view tree for the running app.
|
||||
|
||||
WARNING: Output can be extremely large (100KB–500KB+) for complex apps, especially
|
||||
those built with SwiftUI. Prefer native-find-views for targeted queries.
|
||||
|
||||
Use skipClasses / skipClassPrefixes to prune SwiftUI internal subtrees and reduce
|
||||
output size. Use the fields param to request only the properties you need.
|
||||
|
||||
Useful for: deep layout debugging, finding views with no accessibility labels,
|
||||
verifying view structure not exposed through the accessibility tree.
|
||||
|
||||
If status is restart_required: call restart-app then retry.`,
|
||||
zodSchema,
|
||||
services: (params) => ({
|
||||
nativeDevtools: `${NATIVE_DEVTOOLS_NAMESPACE}:${params.udid}`,
|
||||
}),
|
||||
async execute(services, params) {
|
||||
const api = services.nativeDevtools as NativeDevtoolsApi;
|
||||
|
||||
if (await api.requiresAppRestart(params.bundleId)) {
|
||||
return {
|
||||
status: "restart_required",
|
||||
message:
|
||||
"Native devtools are not injected into the running app. " +
|
||||
"Call restart-app then retry.",
|
||||
};
|
||||
}
|
||||
|
||||
const rpcParams: Record<string, unknown> = {};
|
||||
if (params.fields !== undefined) rpcParams.fields = params.fields;
|
||||
if (params.skipClasses !== undefined)
|
||||
rpcParams.skipClasses = params.skipClasses;
|
||||
if (params.skipClassPrefixes !== undefined)
|
||||
rpcParams.skipClassPrefixes = params.skipClassPrefixes;
|
||||
if (params.maxDepth !== undefined) rpcParams.maxDepth = params.maxDepth;
|
||||
|
||||
const result = (await api.queryViewHierarchy(
|
||||
params.bundleId,
|
||||
"ViewHierarchy.getFullHierarchy",
|
||||
rpcParams
|
||||
)) as { windows?: unknown[] };
|
||||
|
||||
return { status: "ok", windows: result.windows ?? [] };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
import type { ToolDefinition } from "@argent/registry";
|
||||
import type { NativeDevtoolsApi, NetworkEvent } from "../../blueprints/native-devtools";
|
||||
import { NATIVE_DEVTOOLS_NAMESPACE } from "../../blueprints/native-devtools";
|
||||
|
||||
const zodSchema = z.object({
|
||||
udid: z.string().describe("Simulator UDID"),
|
||||
bundleId: z.string().describe("Bundle ID of the app"),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.default(50)
|
||||
.describe("Maximum number of events to return (most recent first)"),
|
||||
clear: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe("Clear the log after reading"),
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof zodSchema>;
|
||||
type Result =
|
||||
| { status: "restart_required"; message: string }
|
||||
| { status: "ok"; count: number; events: NetworkEvent[] };
|
||||
|
||||
export const nativeNetworkLogsTool: ToolDefinition<Params, Result> = {
|
||||
id: "native-network-logs",
|
||||
description: `View network requests captured at the native NSURLProtocol level.
|
||||
|
||||
Unlike the JS-level network inspector (view-network-logs), this captures ALL network
|
||||
traffic from the app including native modules, Swift/Objective-C networking, and
|
||||
background transfers that bypass JS fetch.
|
||||
|
||||
If requiresRestart is returned: call restart-app then retry.`,
|
||||
zodSchema,
|
||||
services: (params) => ({
|
||||
nativeDevtools: `${NATIVE_DEVTOOLS_NAMESPACE}:${params.udid}`,
|
||||
}),
|
||||
async execute(services, params) {
|
||||
const api = services.nativeDevtools as NativeDevtoolsApi;
|
||||
|
||||
if (await api.requiresAppRestart(params.bundleId)) {
|
||||
return {
|
||||
status: "restart_required",
|
||||
message:
|
||||
"Native devtools are not injected into the running app. " +
|
||||
"Call restart-app then retry.",
|
||||
};
|
||||
}
|
||||
|
||||
api.activateNetworkInspection(params.bundleId);
|
||||
|
||||
const events = api.getNetworkLog(params.bundleId).slice(-params.limit);
|
||||
if (params.clear) api.clearNetworkLog(params.bundleId);
|
||||
return { status: "ok", count: events.length, events };
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ServiceState } from "@argent/registry";
|
||||
import type { Registry, ToolDefinition } from "@argent/registry";
|
||||
import { SIMULATOR_SERVER_NAMESPACE } from "../../blueprints/simulator-server";
|
||||
import { NATIVE_DEVTOOLS_NAMESPACE } from "../../blueprints/native-devtools";
|
||||
|
||||
const PREFIX = `${SIMULATOR_SERVER_NAMESPACE}:`;
|
||||
const PREFIXES = [
|
||||
`${SIMULATOR_SERVER_NAMESPACE}:`,
|
||||
`${NATIVE_DEVTOOLS_NAMESPACE}:`,
|
||||
];
|
||||
|
||||
export function createStopAllSimulatorServersTool(
|
||||
registry: Registry,
|
||||
@@ -10,14 +14,17 @@ export function createStopAllSimulatorServersTool(
|
||||
return {
|
||||
id: "stop-all-simulator-servers",
|
||||
description:
|
||||
"Stop all running simulator-server processes. " +
|
||||
"Stop all running simulator-server processes and native devtools services. " +
|
||||
"Call this when your session ends or the user says they are done, to free resources.",
|
||||
services: () => ({}),
|
||||
async execute() {
|
||||
const snapshot = registry.getSnapshot();
|
||||
const stopped: string[] = [];
|
||||
for (const [urn, entry] of snapshot.services) {
|
||||
if (urn.startsWith(PREFIX) && entry.state !== ServiceState.IDLE) {
|
||||
if (
|
||||
PREFIXES.some((p) => urn.startsWith(p)) &&
|
||||
entry.state !== ServiceState.IDLE
|
||||
) {
|
||||
await registry.disposeService(urn);
|
||||
stopped.push(urn);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Registry } from "@argent/registry";
|
||||
import { simulatorServerBlueprint } from "../blueprints/simulator-server";
|
||||
import { nativeDevtoolsBlueprint } from "../blueprints/native-devtools";
|
||||
import { nativeDevtoolsStatusTool } from "../tools/native-devtools/native-devtools-status";
|
||||
import { nativeNetworkLogsTool } from "../tools/native-devtools/native-network-logs";
|
||||
import { nativeFindViewsTool } from "../tools/native-devtools/native-find-views";
|
||||
import { nativeFullHierarchyTool } from "../tools/native-devtools/native-full-hierarchy";
|
||||
import { jsRuntimeDebuggerBlueprint } from "../blueprints/js-runtime-debugger";
|
||||
import { networkInspectorBlueprint } from "../blueprints/network-inspector";
|
||||
import { reactProfilerSessionBlueprint } from "../blueprints/react-profiler-session";
|
||||
@@ -65,6 +70,7 @@ export function createRegistry(): Registry {
|
||||
registry.registerBlueprint(networkInspectorBlueprint);
|
||||
registry.registerBlueprint(reactProfilerSessionBlueprint);
|
||||
registry.registerBlueprint(iosInstrumentsSessionBlueprint);
|
||||
registry.registerBlueprint(nativeDevtoolsBlueprint);
|
||||
|
||||
registry.registerTool(listSimulatorsTool);
|
||||
registry.registerTool(bootSimulatorTool);
|
||||
@@ -109,6 +115,10 @@ export function createRegistry(): Registry {
|
||||
registry.registerTool(profilerCombinedReportTool);
|
||||
registry.registerTool(profilerLoadTool);
|
||||
registry.registerTool(gatherWorkspaceDataTool);
|
||||
registry.registerTool(nativeDevtoolsStatusTool);
|
||||
registry.registerTool(nativeNetworkLogsTool);
|
||||
registry.registerTool(nativeFindViewsTool);
|
||||
registry.registerTool(nativeFullHierarchyTool);
|
||||
|
||||
// Cleanup tools (close over registry for direct service disposal)
|
||||
registry.registerTool(createStopSimulatorServerTool(registry));
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import type { Registry } from "@argent/registry";
|
||||
import { NATIVE_DEVTOOLS_NAMESPACE } from "../blueprints/native-devtools";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const POLL_INTERVAL_MS = 10_000;
|
||||
|
||||
async function getBootedUdids(): Promise<Set<string>> {
|
||||
const { stdout } = await execFileAsync("xcrun", [
|
||||
"simctl",
|
||||
"list",
|
||||
"devices",
|
||||
"--json",
|
||||
]);
|
||||
const data = JSON.parse(stdout) as {
|
||||
devices: Record<string, Array<{ udid: string; state: string }>>;
|
||||
};
|
||||
const udids = new Set<string>();
|
||||
for (const devices of Object.values(data.devices)) {
|
||||
for (const device of devices) {
|
||||
if (device.state === "Booted") udids.add(device.udid);
|
||||
}
|
||||
}
|
||||
return udids;
|
||||
}
|
||||
|
||||
async function initSimulator(
|
||||
registry: Registry,
|
||||
watchedUdids: Set<string>,
|
||||
udid: string
|
||||
): Promise<void> {
|
||||
watchedUdids.add(udid);
|
||||
try {
|
||||
await registry.resolveService(`${NATIVE_DEVTOOLS_NAMESPACE}:${udid}`);
|
||||
} catch {
|
||||
// Service failed to start (e.g. simulator shut down mid-init); retry next tick
|
||||
watchedUdids.delete(udid);
|
||||
}
|
||||
}
|
||||
|
||||
export function startSimulatorWatcher(
|
||||
registry: Registry
|
||||
): { stop: () => void; ready: Promise<void> } {
|
||||
const watchedUdids = new Set<string>();
|
||||
|
||||
async function poll(awaitInit: boolean): Promise<void> {
|
||||
let booted: Set<string>;
|
||||
try {
|
||||
booted = await getBootedUdids();
|
||||
} catch {
|
||||
// xcrun unavailable or transient error — skip this tick
|
||||
return;
|
||||
}
|
||||
|
||||
// New simulators: start NativeDevtools service (sets launchd env + opens socket)
|
||||
const newUdids = [...booted].filter((udid) => !watchedUdids.has(udid));
|
||||
if (awaitInit) {
|
||||
// First poll: await all ensureEnv completions so the server is only marked
|
||||
// ready after injection is guaranteed for all currently-booted simulators.
|
||||
await Promise.all(
|
||||
newUdids.map((udid) => initSimulator(registry, watchedUdids, udid))
|
||||
);
|
||||
} else {
|
||||
// Subsequent polls: fire-and-forget to avoid blocking the interval tick.
|
||||
newUdids.forEach((udid) => initSimulator(registry, watchedUdids, udid));
|
||||
}
|
||||
|
||||
// Simulators that shut down: dispose service and clean up
|
||||
for (const udid of watchedUdids) {
|
||||
if (!booted.has(udid)) {
|
||||
watchedUdids.delete(udid);
|
||||
registry
|
||||
.disposeService(`${NATIVE_DEVTOOLS_NAMESPACE}:${udid}`)
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First poll is awaited — server startup blocks until ensureEnv completes for
|
||||
// all booted simulators, eliminating the race with launch-app.
|
||||
const ready = poll(true);
|
||||
|
||||
// Subsequent polls are fire-and-forget.
|
||||
const interval = setInterval(() => poll(false), POLL_INTERVAL_MS);
|
||||
|
||||
return { stop: () => clearInterval(interval), ready };
|
||||
}
|
||||
+31
-12
@@ -63,19 +63,38 @@ async function waitForHttp(url, timeoutMs = 20_000) {
|
||||
|
||||
// ── Step 1: Build native devtools dylibs ─────────────────────────────────────
|
||||
|
||||
console.log("Initialising argent-private submodule...");
|
||||
execSync("git submodule update --init packages/argent-private", {
|
||||
cwd: ROOT,
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log("✓ Submodule ready\n");
|
||||
const DYLIBS_DIR = path.join(NATIVE_DEVTOOLS_PKG, "dylibs");
|
||||
const DYLIBS_EXIST = fs.existsSync(path.join(DYLIBS_DIR, "libNativeDevtoolsIos.dylib"));
|
||||
|
||||
console.log("Building native devtools dylibs...");
|
||||
execSync("bash scripts/build.sh dev", {
|
||||
cwd: NATIVE_DEVTOOLS_PKG,
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log("✓ Native devtools dylibs built\n");
|
||||
// Try to init the submodule and rebuild. Failure is non-fatal if pre-built
|
||||
// dylibs are already present — developers without argent-private access can
|
||||
// still work on Argent using the committed binaries.
|
||||
let submoduleReady = false;
|
||||
try {
|
||||
execSync("git submodule update --init packages/argent-private", {
|
||||
cwd: ROOT,
|
||||
stdio: "pipe",
|
||||
});
|
||||
submoduleReady = true;
|
||||
} catch {
|
||||
if (DYLIBS_EXIST) {
|
||||
console.warn("⚠ argent-private submodule unavailable — using pre-built dylibs\n");
|
||||
} else {
|
||||
console.error("✗ argent-private submodule unavailable and no pre-built dylibs found.");
|
||||
console.error(" Grant SSH access to github.com/software-mansion-labs/argent-private");
|
||||
console.error(" or obtain pre-built dylibs and place them in packages/native-devtools-ios/dylibs/");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (submoduleReady) {
|
||||
console.log("Building native devtools dylibs...");
|
||||
execSync("bash scripts/build.sh dev", {
|
||||
cwd: NATIVE_DEVTOOLS_PKG,
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log("✓ Native devtools dylibs built\n");
|
||||
}
|
||||
|
||||
// ── Step 2: Build MCP TypeScript ─────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user