@swmansion/argent 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ax-service CHANGED
Binary file
Binary file
@@ -10392,7 +10392,7 @@ ${l}
10392
10392
  var import_picocolors2 = __toESM(require_picocolors(), 1);
10393
10393
  import { existsSync as existsSync3 } from "node:fs";
10394
10394
  import { resolve as resolve2 } from "node:path";
10395
- import { execSync as execSync2, spawn } from "node:child_process";
10395
+ import { spawn } from "node:child_process";
10396
10396
 
10397
10397
  // ../argent-installer/src/mcp-configs.ts
10398
10398
  import * as fs2 from "node:fs";
@@ -12813,6 +12813,18 @@ function getInstalledVersion() {
12813
12813
  return null;
12814
12814
  }
12815
12815
  }
12816
+ function getGloballyInstalledVersion() {
12817
+ const binaryPath = getGlobalBinaryPath();
12818
+ if (!binaryPath) return null;
12819
+ try {
12820
+ const realPath = fs.realpathSync(binaryPath);
12821
+ const pkgRoot = resolvePackageRoot(path.dirname(realPath));
12822
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
12823
+ return pkg.version ?? null;
12824
+ } catch {
12825
+ return null;
12826
+ }
12827
+ }
12816
12828
  var PROBE_TIMEOUT_MS = 3e3;
12817
12829
  function getLatestVersion() {
12818
12830
  const result = execSync(`npm view ${PACKAGE_NAME} version --registry ${NPM_REGISTRY}`, {
@@ -12825,6 +12837,31 @@ function isNewerVersion(candidate, current) {
12825
12837
  if (!import_semver.default.valid(candidate) || !import_semver.default.valid(current)) return false;
12826
12838
  return import_semver.default.gt(candidate, current);
12827
12839
  }
12840
+ var TEMP_RUNNER_MARKERS = [
12841
+ "_npx",
12842
+ "/dlx-",
12843
+ "\\dlx-",
12844
+ "bun/install/cache",
12845
+ ".bun\\install\\cache"
12846
+ ];
12847
+ function isTempRunnerPath(binaryPath) {
12848
+ return TEMP_RUNNER_MARKERS.some((marker) => binaryPath.includes(marker));
12849
+ }
12850
+ function getGlobalBinaryPath() {
12851
+ try {
12852
+ const cmd = process.platform === "win32" ? "where" : "which -a";
12853
+ const output = execSync(`${cmd} ${MCP_BINARY_NAME}`, {
12854
+ encoding: "utf8",
12855
+ stdio: ["ignore", "pipe", "ignore"]
12856
+ });
12857
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).find((line) => !isTempRunnerPath(line)) ?? null;
12858
+ } catch {
12859
+ return null;
12860
+ }
12861
+ }
12862
+ function isGloballyInstalled() {
12863
+ return getGlobalBinaryPath() !== null;
12864
+ }
12828
12865
  function isSkillsCliAvailable() {
12829
12866
  try {
12830
12867
  execSync("npx --no-install skills --version", {
@@ -13700,25 +13737,6 @@ function formatSkillRefreshSummary(results) {
13700
13737
  }
13701
13738
 
13702
13739
  // ../argent-installer/src/init.ts
13703
- var TEMP_RUNNER_MARKERS = [
13704
- "_npx",
13705
- "/dlx-",
13706
- "\\dlx-",
13707
- "bun/install/cache",
13708
- ".bun\\install\\cache"
13709
- ];
13710
- function isGloballyInstalled() {
13711
- try {
13712
- const cmd = process.platform === "win32" ? "where" : "which";
13713
- const binaryPath = execSync2(`${cmd} ${MCP_BINARY_NAME}`, {
13714
- encoding: "utf8",
13715
- stdio: ["ignore", "pipe", "ignore"]
13716
- }).trim();
13717
- return !TEMP_RUNNER_MARKERS.some((marker) => binaryPath.includes(marker));
13718
- } catch {
13719
- return false;
13720
- }
13721
- }
13722
13740
  function runShellCommand(cmd) {
13723
13741
  return new Promise((resolve3, reject) => {
13724
13742
  const isWin = process.platform === "win32";
@@ -14246,8 +14264,9 @@ async function killToolServer() {
14246
14264
  async function update(args) {
14247
14265
  const nonInteractive = args.includes("--yes") || args.includes("-y");
14248
14266
  Wt2(import_picocolors3.default.bgCyan(import_picocolors3.default.black(" argent update ")));
14249
- const installed = getInstalledVersion();
14250
- if (!installed) {
14267
+ const globallyInstalled = isGloballyInstalled();
14268
+ const installed = globallyInstalled ? getGloballyInstalledVersion() : null;
14269
+ if (globallyInstalled && !installed) {
14251
14270
  R2.error("Could not determine installed version.");
14252
14271
  process.exit(1);
14253
14272
  }
@@ -14262,21 +14281,28 @@ async function update(args) {
14262
14281
  process.exit(1);
14263
14282
  }
14264
14283
  spinner.stop("Version check complete.");
14265
- R2.info(`Installed: ${import_picocolors3.default.cyan(`v${installed}`)}`);
14284
+ if (installed) {
14285
+ R2.info(`Installed: ${import_picocolors3.default.cyan(`v${installed}`)}`);
14286
+ } else {
14287
+ R2.warn(`${PACKAGE_NAME} is not installed globally.`);
14288
+ }
14266
14289
  R2.info(`Latest: ${import_picocolors3.default.cyan(`v${latest}`)}`);
14267
- if (isNewerVersion(latest, installed)) {
14268
- R2.warn(`Update available: ${import_picocolors3.default.yellow(`v${installed}`)} -> ${import_picocolors3.default.green(`v${latest}`)}`);
14290
+ const needsInstall = !installed || isNewerVersion(latest, installed);
14291
+ if (needsInstall) {
14292
+ if (installed) {
14293
+ R2.warn(`Update available: ${import_picocolors3.default.yellow(`v${installed}`)} -> ${import_picocolors3.default.green(`v${latest}`)}`);
14294
+ }
14269
14295
  const pm = detectPackageManager();
14270
14296
  const cmd = globalInstallCommand(pm, `${PACKAGE_NAME}@${latest}`);
14271
14297
  const cmdStr = formatShellCommand(cmd);
14272
14298
  if (!nonInteractive) {
14273
14299
  R2.message(import_picocolors3.default.dim(" Press y for yes, n for no, enter to confirm."));
14274
14300
  const proceed = await Rt({
14275
- message: `Update to v${latest}?`,
14301
+ message: installed ? `Update to v${latest}?` : `Install ${PACKAGE_NAME}@${latest} globally?`,
14276
14302
  initialValue: true
14277
14303
  });
14278
14304
  if (Ct(proceed) || !proceed) {
14279
- Nt("Update cancelled.");
14305
+ Nt(installed ? "Update cancelled." : "Install cancelled.");
14280
14306
  process.exit(0);
14281
14307
  }
14282
14308
  }
@@ -14288,7 +14314,7 @@ async function update(args) {
14288
14314
  env: { ...process.env, ARGENT_SKIP_POSTINSTALL: "1" }
14289
14315
  });
14290
14316
  } catch (err) {
14291
- R2.error(`Update failed: ${err}`);
14317
+ R2.error(`${installed ? "Update" : "Install"} failed: ${err}`);
14292
14318
  process.exit(1);
14293
14319
  }
14294
14320
  } else {
@@ -14684,6 +14710,7 @@ export {
14684
14710
  MCP_BINARY_NAME,
14685
14711
  MCP_SERVER_KEY,
14686
14712
  PACKAGE_NAME,
14713
+ getGloballyInstalledVersion,
14687
14714
  getInstalledVersion,
14688
14715
  init,
14689
14716
  uninstall,
@@ -41850,7 +41850,7 @@ var import_node_https = __toESM(require("node:https"));
41850
41850
  var import_semver = __toESM(require_semver2());
41851
41851
 
41852
41852
  // ../tool-server/package.json
41853
- var version = "0.6.0";
41853
+ var version = "0.6.1";
41854
41854
 
41855
41855
  // ../tool-server/src/utils/update-checker.ts
41856
41856
  var PACKAGE_NAME = "@swmansion/argent";
@@ -45373,7 +45373,7 @@ var gestureTapTool = {
45373
45373
  Sends a Down event followed by an Up event at the same point.
45374
45374
  Use when you need to tap a button, link, or any tappable element on the simulator screen.
45375
45375
  Returns { tapped: true, timestampMs }. Fails if the simulator server is not running for the given UDID.
45376
- Before tapping, determine the correct coordinates by using discovery tools: describe, native-describe-screen, debugger-component-tree. More information in \`simulator-interact\` skill`,
45376
+ Before tapping, determine the correct coordinates by using discovery tools: describe, native-describe-screen, debugger-component-tree. More information in \`argent-simulator-interact\` skill`,
45377
45377
  alwaysLoad: true,
45378
45378
  searchHint: "tap press button element simulator touch down up",
45379
45379
  zodSchema: zodSchema15,
@@ -47005,7 +47005,7 @@ full-screen transparent wrappers, and implementation-detail components are prune
47005
47005
  Each visible component is listed with its name, text content, and normalized
47006
47006
  tap coordinates in [0,1] space (fractions of the screen, not pixels\u2014same space as tap/swipe/gesture and simulator-server touch).
47007
47007
 
47008
- This is the preferred element discovery tool for React Native apps. More information in react-native-app-workflow skill.
47008
+ This is the preferred element discovery tool for React Native apps. More information in argent-react-native-app-workflow skill.
47009
47009
 
47010
47010
  Workflow:
47011
47011
  1. Call this tool to get the component tree.
@@ -50440,11 +50440,55 @@ Fails if the React DevTools hook is not present or no fiber roots have been comm
50440
50440
  };
50441
50441
 
50442
50442
  // ../tool-server/src/tools/profiler/ios-profiler/ios-profiler-start.ts
50443
- var import_child_process = require("child_process");
50443
+ var import_child_process2 = require("child_process");
50444
50444
  var path6 = __toESM(require("path"));
50445
50445
 
50446
+ // ../tool-server/src/utils/ios-profiler/lifecycle.ts
50447
+ function waitForChildExit(child, ms) {
50448
+ if (child.exitCode !== null || child.signalCode !== null) {
50449
+ return Promise.resolve(true);
50450
+ }
50451
+ return new Promise((resolve2) => {
50452
+ const onExit = () => {
50453
+ clearTimeout(timer);
50454
+ resolve2(true);
50455
+ };
50456
+ const timer = setTimeout(() => {
50457
+ child.removeListener("exit", onExit);
50458
+ resolve2(false);
50459
+ }, ms);
50460
+ child.once("exit", onExit);
50461
+ });
50462
+ }
50463
+ async function shutdownChild(child, t) {
50464
+ if (child.exitCode !== null || child.signalCode !== null) {
50465
+ return { clean: true, signalUsed: "SIGINT" };
50466
+ }
50467
+ try {
50468
+ child.kill("SIGINT");
50469
+ } catch {
50470
+ }
50471
+ if (await waitForChildExit(child, t.graceMs)) {
50472
+ return { clean: true, signalUsed: "SIGINT" };
50473
+ }
50474
+ try {
50475
+ child.kill("SIGTERM");
50476
+ } catch {
50477
+ }
50478
+ if (await waitForChildExit(child, t.termMs)) {
50479
+ return { clean: false, signalUsed: "SIGTERM" };
50480
+ }
50481
+ try {
50482
+ child.kill("SIGKILL");
50483
+ } catch {
50484
+ }
50485
+ await waitForChildExit(child, t.killMs);
50486
+ return { clean: false, signalUsed: "SIGKILL" };
50487
+ }
50488
+
50446
50489
  // ../tool-server/src/blueprints/ios-profiler-session.ts
50447
50490
  var IOS_PROFILER_SESSION_NAMESPACE = "IosProfilerSession";
50491
+ var DISPOSE_REAP_MS = 1e3;
50448
50492
  var iosInstrumentsSessionBlueprint = {
50449
50493
  namespace: IOS_PROFILER_SESSION_NAMESPACE,
50450
50494
  getURN(deviceId) {
@@ -50455,12 +50499,16 @@ var iosInstrumentsSessionBlueprint = {
50455
50499
  deviceId: _payload,
50456
50500
  appProcess: null,
50457
50501
  xctracePid: null,
50502
+ xctraceProcess: null,
50458
50503
  traceFile: null,
50459
50504
  exportedFiles: null,
50460
50505
  profilingActive: false,
50461
50506
  wallClockStartMs: null,
50462
50507
  parsedData: null,
50463
- recordingTimeout: null
50508
+ recordingTimeout: null,
50509
+ recordingTimedOut: false,
50510
+ recordingExitedUnexpectedly: false,
50511
+ lastExitInfo: null
50464
50512
  };
50465
50513
  const events = new TypedEventEmitter();
50466
50514
  return {
@@ -50470,12 +50518,16 @@ var iosInstrumentsSessionBlueprint = {
50470
50518
  clearTimeout(state2.recordingTimeout);
50471
50519
  state2.recordingTimeout = null;
50472
50520
  }
50473
- if (state2.profilingActive && state2.xctracePid) {
50521
+ const child = state2.xctraceProcess;
50522
+ if (state2.profilingActive && child) {
50474
50523
  try {
50475
- process.kill(state2.xctracePid, "SIGINT");
50524
+ child.kill("SIGKILL");
50476
50525
  } catch {
50477
50526
  }
50527
+ await waitForChildExit(child, DISPOSE_REAP_MS);
50478
50528
  state2.profilingActive = false;
50529
+ state2.xctracePid = null;
50530
+ state2.xctraceProcess = null;
50479
50531
  }
50480
50532
  },
50481
50533
  events
@@ -50483,8 +50535,121 @@ var iosInstrumentsSessionBlueprint = {
50483
50535
  }
50484
50536
  };
50485
50537
 
50538
+ // ../tool-server/src/utils/ios-profiler/notify.ts
50539
+ var import_child_process = require("child_process");
50540
+ var NOTIFYUTIL_PATH = "/usr/bin/notifyutil";
50541
+ var REGISTRATION_DELAY_MS = 300;
50542
+ function listenForDarwinNotification(name) {
50543
+ const proc = (0, import_child_process.spawn)(NOTIFYUTIL_PATH, ["-v", "-1", name]);
50544
+ let firedResolve = () => {
50545
+ };
50546
+ let fired = false;
50547
+ const fired$ = new Promise((r) => {
50548
+ firedResolve = r;
50549
+ });
50550
+ let readyResolve = () => {
50551
+ };
50552
+ let readyReject = () => {
50553
+ };
50554
+ let readySettled = false;
50555
+ const ready = new Promise((res, rej) => {
50556
+ readyResolve = () => {
50557
+ if (readySettled) return;
50558
+ readySettled = true;
50559
+ res();
50560
+ };
50561
+ readyReject = (e) => {
50562
+ if (readySettled) return;
50563
+ readySettled = true;
50564
+ rej(e);
50565
+ };
50566
+ });
50567
+ const registrationTimer = setTimeout(readyResolve, REGISTRATION_DELAY_MS);
50568
+ proc.on("exit", (code) => {
50569
+ if (!fired && code === 0) {
50570
+ fired = true;
50571
+ firedResolve();
50572
+ }
50573
+ });
50574
+ proc.on("error", (err) => {
50575
+ clearTimeout(registrationTimer);
50576
+ readyReject(err);
50577
+ });
50578
+ return {
50579
+ ready,
50580
+ fired: fired$,
50581
+ cancel: () => {
50582
+ clearTimeout(registrationTimer);
50583
+ try {
50584
+ proc.kill("SIGTERM");
50585
+ } catch {
50586
+ }
50587
+ }
50588
+ };
50589
+ }
50590
+
50591
+ // ../tool-server/src/utils/ios-profiler/startup.ts
50592
+ function waitForXctraceReady(child, { notify, timeoutMs }) {
50593
+ return new Promise((resolve2, reject) => {
50594
+ let settled = false;
50595
+ let stderrBuffer = "";
50596
+ const settle = (run) => {
50597
+ if (settled) return;
50598
+ settled = true;
50599
+ clearTimeout(startupTimer);
50600
+ if (notify) notify.cancel();
50601
+ run();
50602
+ };
50603
+ child.stdout?.on("data", (data) => {
50604
+ const text = data.toString();
50605
+ if (text.includes("Ctrl-C to stop") || text.includes("Starting recording")) {
50606
+ settle(() => resolve2({ stderrBuffer }));
50607
+ }
50608
+ });
50609
+ child.stderr?.on("data", (data) => {
50610
+ stderrBuffer += data.toString();
50611
+ });
50612
+ child.on("exit", (code, signal) => {
50613
+ settle(
50614
+ () => reject(
50615
+ new Error(
50616
+ `xctrace record exited before recording started (code=${code}, signal=${signal}). stderr: ${stderrBuffer.trim() || "<empty>"}`
50617
+ )
50618
+ )
50619
+ );
50620
+ });
50621
+ child.on("error", (err) => {
50622
+ settle(() => reject(new Error(`Failed to start xctrace: ${err.message}`)));
50623
+ });
50624
+ const startupTimer = setTimeout(() => {
50625
+ settle(() => {
50626
+ try {
50627
+ child.kill("SIGKILL");
50628
+ } catch {
50629
+ }
50630
+ reject(
50631
+ new Error(
50632
+ `xctrace record did not start within ${timeoutMs} ms. Last stderr: ${stderrBuffer.trim() || "<empty>"}`
50633
+ )
50634
+ );
50635
+ });
50636
+ }, timeoutMs);
50637
+ if (notify) {
50638
+ notify.fired.then(() => settle(() => resolve2({ stderrBuffer }))).catch(() => {
50639
+ });
50640
+ }
50641
+ });
50642
+ }
50643
+
50486
50644
  // ../tool-server/src/tools/profiler/ios-profiler/ios-profiler-start.ts
50487
50645
  var DEFAULT_TEMPLATE_PATH = path6.resolve(__dirname, "Argent.tracetemplate");
50646
+ var STARTUP_TIMEOUT_MS = 1e4;
50647
+ var DETECT_RUNNING_APP_TIMEOUT_MS = 1e4;
50648
+ var NOTIFY_REGISTER_TIMEOUT_MS = 2e3;
50649
+ var RECORDING_CAP_MS = 10 * 60 * 1e3;
50650
+ var MAX_START_ATTEMPTS = 2;
50651
+ var RETRY_DELAY_MS = 1200;
50652
+ var COLD_START_SIGNATURE = "Cannot find process matching name:";
50488
50653
  var zodSchema42 = external_exports.object({
50489
50654
  device_id: external_exports.string().describe("iOS Simulator or device UDID"),
50490
50655
  app_process: external_exports.string().optional().describe(
@@ -50493,9 +50658,18 @@ var zodSchema42 = external_exports.object({
50493
50658
  template_path: external_exports.string().optional().describe("Path to an Instruments .tracetemplate file (defaults to bundled Argent template)")
50494
50659
  });
50495
50660
  function detectRunningApp(udid) {
50496
- const launchctlOutput = (0, import_child_process.execSync)(`xcrun simctl spawn ${udid} launchctl list`, {
50497
- encoding: "utf-8"
50498
- });
50661
+ let launchctlOutput;
50662
+ try {
50663
+ launchctlOutput = (0, import_child_process2.execSync)(`xcrun simctl spawn ${udid} launchctl list`, {
50664
+ encoding: "utf-8",
50665
+ timeout: DETECT_RUNNING_APP_TIMEOUT_MS
50666
+ });
50667
+ } catch (err) {
50668
+ const msg = err instanceof Error ? err.message : String(err);
50669
+ throw new Error(
50670
+ `Failed to enumerate running processes on simulator ${udid} within ${DETECT_RUNNING_APP_TIMEOUT_MS} ms. Verify the simulator is booted and responsive, then retry. Underlying error: ${msg}`
50671
+ );
50672
+ }
50499
50673
  const runningBundleIds = /* @__PURE__ */ new Set();
50500
50674
  for (const line of launchctlOutput.split("\n")) {
50501
50675
  const match = line.match(/UIKitApplication:([^\[]+)/);
@@ -50508,9 +50682,18 @@ function detectRunningApp(udid) {
50508
50682
  "No running apps detected on the simulator. Launch the app first using `launch-app`, then retry."
50509
50683
  );
50510
50684
  }
50511
- const listAppsOutput = (0, import_child_process.execSync)(`xcrun simctl listapps ${udid} | plutil -convert json -o - -`, {
50512
- encoding: "utf-8"
50513
- });
50685
+ let listAppsOutput;
50686
+ try {
50687
+ listAppsOutput = (0, import_child_process2.execSync)(`xcrun simctl listapps ${udid} | plutil -convert json -o - -`, {
50688
+ encoding: "utf-8",
50689
+ timeout: DETECT_RUNNING_APP_TIMEOUT_MS
50690
+ });
50691
+ } catch (err) {
50692
+ const msg = err instanceof Error ? err.message : String(err);
50693
+ throw new Error(
50694
+ `Failed to list installed apps on simulator ${udid} within ${DETECT_RUNNING_APP_TIMEOUT_MS} ms. Verify the simulator is booted and responsive, then retry. Underlying error: ${msg}`
50695
+ );
50696
+ }
50514
50697
  const installedApps = JSON.parse(listAppsOutput);
50515
50698
  const runningUserApps = [];
50516
50699
  for (const [, appInfo] of Object.entries(installedApps)) {
@@ -50535,6 +50718,50 @@ Specify \`app_process\` with the CFBundleExecutable of the app you want to profi
50535
50718
  }
50536
50719
  return runningUserApps[0].CFBundleExecutable;
50537
50720
  }
50721
+ async function registerStartupNotify(name) {
50722
+ let handle;
50723
+ try {
50724
+ handle = listenForDarwinNotification(name);
50725
+ } catch (err) {
50726
+ const msg = err instanceof Error ? err.message : String(err);
50727
+ process.stderr.write(
50728
+ `[ios-profiler] failed to spawn notifyutil (${msg}); falling back to stdout substring match.
50729
+ `
50730
+ );
50731
+ return null;
50732
+ }
50733
+ const ready = await Promise.race([
50734
+ handle.ready.then(() => true),
50735
+ new Promise((r) => setTimeout(() => r(false), NOTIFY_REGISTER_TIMEOUT_MS))
50736
+ ]);
50737
+ if (ready) return handle;
50738
+ handle.cancel();
50739
+ process.stderr.write(
50740
+ `[ios-profiler] notifyutil did not register within ${NOTIFY_REGISTER_TIMEOUT_MS} ms; falling back to stdout substring match.
50741
+ `
50742
+ );
50743
+ return null;
50744
+ }
50745
+ function resetStartState(api) {
50746
+ api.xctracePid = null;
50747
+ api.xctraceProcess = null;
50748
+ api.traceFile = null;
50749
+ api.appProcess = null;
50750
+ }
50751
+ function handleXctraceExit(api, code, signal) {
50752
+ if (!api.profilingActive) return;
50753
+ if (api.recordingTimeout) {
50754
+ clearTimeout(api.recordingTimeout);
50755
+ api.recordingTimeout = null;
50756
+ }
50757
+ api.xctracePid = null;
50758
+ api.xctraceProcess = null;
50759
+ api.profilingActive = false;
50760
+ if (!api.recordingTimedOut) {
50761
+ api.recordingExitedUnexpectedly = true;
50762
+ }
50763
+ api.lastExitInfo = { code, signal };
50764
+ }
50538
50765
  var iosInstrumentsStartTool = {
50539
50766
  id: "ios-profiler-start",
50540
50767
  description: `Start iOS Instruments profiling via xctrace on a booted simulator or connected device.
@@ -50557,10 +50784,15 @@ Fails if no app is running on the simulator or xctrace cannot attach to the proc
50557
50784
  const debugDir = await getDebugDir();
50558
50785
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, (m) => m === "T" ? "-" : "").slice(0, 15);
50559
50786
  const outputFile = path6.join(debugDir, `ios-profiler-${timestamp}.trace`);
50560
- api.appProcess = appProcess;
50561
- api.traceFile = outputFile;
50562
- return new Promise((resolve2, reject) => {
50563
- const xctraceProcess = (0, import_child_process.spawn)("xctrace", [
50787
+ api.recordingTimedOut = false;
50788
+ api.recordingExitedUnexpectedly = false;
50789
+ api.lastExitInfo = null;
50790
+ const attemptStart = async () => {
50791
+ api.appProcess = appProcess;
50792
+ api.traceFile = outputFile;
50793
+ const notifyName = `com.argent.ios-profiler.started.${process.pid}.${Date.now()}`;
50794
+ const notify = await registerStartupNotify(notifyName);
50795
+ const xctraceArgs = [
50564
50796
  "record",
50565
50797
  "--template",
50566
50798
  templatePath,
@@ -50569,58 +50801,92 @@ Fails if no app is running on the simulator or xctrace cannot attach to the proc
50569
50801
  "--attach",
50570
50802
  appProcess,
50571
50803
  "--output",
50572
- outputFile
50573
- ]);
50574
- api.xctracePid = xctraceProcess.pid ?? null;
50575
- xctraceProcess.stdout.on("data", (data) => {
50576
- const output = data.toString();
50577
- if (output.includes("Ctrl-C to stop") || output.includes("Starting recording")) {
50578
- api.profilingActive = true;
50579
- api.wallClockStartMs = Date.now();
50580
- if (api.xctracePid) {
50581
- api.recordingTimeout = setTimeout(
50582
- () => {
50583
- xctraceProcess.kill("SIGINT");
50584
- api.profilingActive = false;
50585
- api.xctracePid = null;
50586
- api.recordingTimeout = null;
50587
- },
50588
- 10 * 60 * 1e3
50589
- );
50590
- resolve2({
50591
- status: "recording",
50592
- pid: api.xctracePid,
50593
- traceFile: outputFile
50594
- });
50595
- }
50596
- }
50804
+ outputFile,
50805
+ "--no-prompt"
50806
+ ];
50807
+ if (notify) {
50808
+ xctraceArgs.push("--notify-tracing-started", notifyName);
50809
+ }
50810
+ const xctraceProcess2 = (0, import_child_process2.spawn)("xctrace", xctraceArgs, {
50811
+ stdio: ["ignore", "pipe", "pipe"]
50597
50812
  });
50598
- xctraceProcess.stderr.on("data", (data) => {
50599
- const errorOutput = data.toString();
50600
- if (errorOutput.includes("Target failed to run") || errorOutput.includes("failed with errors")) {
50601
- api.xctracePid = null;
50602
- if (api.recordingTimeout) {
50603
- clearTimeout(api.recordingTimeout);
50604
- api.recordingTimeout = null;
50605
- }
50606
- reject(new Error(`Failed to attach to iOS process: ${errorOutput}`));
50813
+ api.xctracePid = xctraceProcess2.pid ?? null;
50814
+ api.xctraceProcess = xctraceProcess2;
50815
+ try {
50816
+ await waitForXctraceReady(xctraceProcess2, { notify, timeoutMs: STARTUP_TIMEOUT_MS });
50817
+ } catch (err) {
50818
+ resetStartState(api);
50819
+ throw err;
50820
+ }
50821
+ if (!xctraceProcess2.pid) {
50822
+ try {
50823
+ xctraceProcess2.kill("SIGKILL");
50824
+ } catch {
50607
50825
  }
50608
- });
50609
- xctraceProcess.on("error", (err) => {
50610
- api.xctracePid = null;
50611
- if (api.recordingTimeout) {
50612
- clearTimeout(api.recordingTimeout);
50613
- api.recordingTimeout = null;
50826
+ resetStartState(api);
50827
+ throw new Error("xctrace process has no pid; cannot resolve start.");
50828
+ }
50829
+ return { child: xctraceProcess2, pid: xctraceProcess2.pid };
50830
+ };
50831
+ const startMs = Date.now();
50832
+ const startWithRetry = async () => {
50833
+ for (let attempt = 1; attempt <= MAX_START_ATTEMPTS; attempt++) {
50834
+ try {
50835
+ return await attemptStart();
50836
+ } catch (err) {
50837
+ const msg = err instanceof Error ? err.message : String(err);
50838
+ const isColdStart = msg.includes(COLD_START_SIGNATURE);
50839
+ if (!isColdStart) throw err;
50840
+ if (attempt >= MAX_START_ATTEMPTS) break;
50841
+ process.stderr.write(
50842
+ `[ios-profiler] xctrace could not find "${appProcess}" on attempt ${attempt}/${MAX_START_ATTEMPTS}; waiting ${RETRY_DELAY_MS} ms for cold-start to settle, then retrying.
50843
+ `
50844
+ );
50845
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
50614
50846
  }
50615
- reject(new Error(`Failed to start xctrace: ${err.message}`));
50616
- });
50617
- });
50847
+ }
50848
+ const totalMs = Date.now() - startMs;
50849
+ throw new Error(
50850
+ `xctrace could not find process "${appProcess}" after ${MAX_START_ATTEMPTS} attempts within ${totalMs} ms. The app appears to be cold-launching \u2014 its bundle is registered with launchd, but xctrace's process resolver hasn't seen it yet. Wait 1\u20132 seconds for the app to finish launching and retry. If the wrong app is being detected, pass app_process explicitly with the CFBundleExecutable.`
50851
+ );
50852
+ };
50853
+ const { child: xctraceProcess, pid: xctracePid } = await startWithRetry();
50854
+ api.profilingActive = true;
50855
+ api.wallClockStartMs = Date.now();
50856
+ api.recordingTimeout = setTimeout(() => {
50857
+ try {
50858
+ xctraceProcess.kill("SIGINT");
50859
+ } catch {
50860
+ }
50861
+ api.profilingActive = false;
50862
+ api.xctracePid = null;
50863
+ api.xctraceProcess = null;
50864
+ api.recordingTimeout = null;
50865
+ api.recordingTimedOut = true;
50866
+ }, RECORDING_CAP_MS);
50867
+ xctraceProcess.on("exit", (code, signal) => handleXctraceExit(api, code, signal));
50868
+ return {
50869
+ status: "recording",
50870
+ pid: xctracePid,
50871
+ traceFile: outputFile
50872
+ };
50618
50873
  }
50619
50874
  };
50620
50875
 
50621
50876
  // ../tool-server/src/utils/ios-profiler/export.ts
50622
- var import_child_process2 = require("child_process");
50623
50877
  var path7 = __toESM(require("path"));
50878
+
50879
+ // ../tool-server/src/utils/ios-profiler/run-with-timeout.ts
50880
+ var import_child_process3 = require("child_process");
50881
+ var DEFAULT_EXEC_TIMEOUT_MS = 6e4;
50882
+ function execSyncWithTimeout(command, options = {}) {
50883
+ return (0, import_child_process3.execSync)(command, {
50884
+ timeout: DEFAULT_EXEC_TIMEOUT_MS,
50885
+ ...options
50886
+ });
50887
+ }
50888
+
50889
+ // ../tool-server/src/utils/ios-profiler/export.ts
50624
50890
  var CPU_SCHEMA_CANDIDATES = ["time-profile", "cpu-profile", "time-sample"];
50625
50891
  var EXPORTS = {
50626
50892
  cpu: {
@@ -50638,7 +50904,7 @@ var EXPORTS = {
50638
50904
  };
50639
50905
  function getXctraceVersion() {
50640
50906
  try {
50641
- const output = (0, import_child_process2.execSync)("xctrace version 2>&1 || true", {
50907
+ const output = execSyncWithTimeout("xctrace version 2>&1 || true", {
50642
50908
  encoding: "utf-8"
50643
50909
  });
50644
50910
  const match = output.match(/(\d+)\./);
@@ -50649,7 +50915,7 @@ function getXctraceVersion() {
50649
50915
  }
50650
50916
  function discoverTraceSchemas(traceFile) {
50651
50917
  try {
50652
- const toc = (0, import_child_process2.execSync)(`xctrace export --input "${traceFile}" --toc`, {
50918
+ const toc = execSyncWithTimeout(`xctrace export --input "${traceFile}" --toc`, {
50653
50919
  encoding: "utf-8",
50654
50920
  stdio: ["pipe", "pipe", "pipe"]
50655
50921
  });
@@ -50684,9 +50950,12 @@ function tryCpuExportFallback(traceFile, outPath, diagnostics) {
50684
50950
  for (const candidate of CPU_SCHEMA_CANDIDATES) {
50685
50951
  const xpath = `/trace-toc/run[@number="1"]/data/table[@schema="${candidate}"]`;
50686
50952
  try {
50687
- (0, import_child_process2.execSync)(`xctrace export --input "${traceFile}" --output "${outPath}" --xpath '${xpath}'`, {
50688
- stdio: "pipe"
50689
- });
50953
+ execSyncWithTimeout(
50954
+ `xctrace export --input "${traceFile}" --output "${outPath}" --xpath '${xpath}'`,
50955
+ {
50956
+ stdio: "pipe"
50957
+ }
50958
+ );
50690
50959
  diagnostics.cpuSchemaUsed = candidate;
50691
50960
  return true;
50692
50961
  } catch {
@@ -50711,7 +50980,7 @@ function exportIosTraceData(traceFile) {
50711
50980
  const resolvedXpath = resolveCpuXpath(traceFile, diagnostics);
50712
50981
  if (resolvedXpath) {
50713
50982
  try {
50714
- (0, import_child_process2.execSync)(
50983
+ execSyncWithTimeout(
50715
50984
  `xctrace export --input "${traceFile}" --output "${outPath}" --xpath '${resolvedXpath}'`,
50716
50985
  { stdio: "pipe" }
50717
50986
  );
@@ -50734,7 +51003,7 @@ function exportIosTraceData(traceFile) {
50734
51003
  const xcVersion = getXctraceVersion();
50735
51004
  const halFlag = xcVersion >= 15 ? " --hal" : "";
50736
51005
  try {
50737
- (0, import_child_process2.execSync)(
51006
+ execSyncWithTimeout(
50738
51007
  `xctrace export --input "${traceFile}" --output "${outPath}" --xpath '${config.xpath}'${halFlag}`,
50739
51008
  { stdio: "pipe" }
50740
51009
  );
@@ -50742,7 +51011,7 @@ function exportIosTraceData(traceFile) {
50742
51011
  } catch {
50743
51012
  if (halFlag) {
50744
51013
  try {
50745
- (0, import_child_process2.execSync)(
51014
+ execSyncWithTimeout(
50746
51015
  `xctrace export --input "${traceFile}" --output "${outPath}" --xpath '${config.xpath}'`,
50747
51016
  { stdio: "pipe" }
50748
51017
  );
@@ -50759,7 +51028,7 @@ function exportIosTraceData(traceFile) {
50759
51028
  continue;
50760
51029
  }
50761
51030
  try {
50762
- (0, import_child_process2.execSync)(
51031
+ execSyncWithTimeout(
50763
51032
  `xctrace export --input "${traceFile}" --output "${outPath}" --xpath '${config.xpath}'`,
50764
51033
  { stdio: "pipe" }
50765
51034
  );
@@ -50774,6 +51043,9 @@ function exportIosTraceData(traceFile) {
50774
51043
  }
50775
51044
 
50776
51045
  // ../tool-server/src/tools/profiler/ios-profiler/ios-profiler-stop.ts
51046
+ var STOP_GRACE_MS = 3e4;
51047
+ var STOP_TERM_MS = 5e3;
51048
+ var STOP_KILL_MS = 5e3;
50777
51049
  var zodSchema43 = external_exports.object({
50778
51050
  device_id: external_exports.string().describe("iOS Simulator or device UDID")
50779
51051
  });
@@ -50791,34 +51063,52 @@ Fails if no active ios-profiler-start session exists for the given device_id.`,
50791
51063
  }),
50792
51064
  async execute(services) {
50793
51065
  const api = services.session;
50794
- if (!api.profilingActive || !api.xctracePid || !api.traceFile) {
51066
+ if ((api.recordingTimedOut || api.recordingExitedUnexpectedly) && api.traceFile) {
51067
+ const traceFile = api.traceFile;
51068
+ const wasTimeout = api.recordingTimedOut;
51069
+ const exitInfo = api.lastExitInfo;
51070
+ api.recordingTimedOut = false;
51071
+ api.recordingExitedUnexpectedly = false;
51072
+ api.lastExitInfo = null;
51073
+ const { files: exportedFiles2, diagnostics: diagnostics2 } = exportIosTraceData(traceFile);
51074
+ api.exportedFiles = exportedFiles2;
51075
+ const warning2 = wasTimeout ? "Recording timed out at 10 min cap; exported the partial trace. Call ios-profiler-start again for a fresh recording." : `xctrace exited before stop was called (code=${exitInfo?.code ?? "?"}, signal=${exitInfo?.signal ?? "?"}); exported the partial trace. Common causes: attached app terminated, simulator daemon restart. Call ios-profiler-start again for a fresh recording.`;
51076
+ process.stderr.write(`[ios-profiler] ${warning2}
51077
+ `);
51078
+ return { traceFile, exportedFiles: exportedFiles2, exportDiagnostics: diagnostics2, warning: warning2 };
51079
+ }
51080
+ if (!api.profilingActive || !api.xctraceProcess || !api.traceFile) {
50795
51081
  throw new Error("No active iOS profiling session found. Call ios-profiler-start first.");
50796
51082
  }
50797
51083
  if (api.recordingTimeout) {
50798
51084
  clearTimeout(api.recordingTimeout);
50799
51085
  api.recordingTimeout = null;
50800
51086
  }
50801
- const pidToKill = api.xctracePid;
50802
- process.kill(pidToKill, "SIGINT");
50803
- await new Promise((resolve2) => {
50804
- const checkInterval = setInterval(() => {
50805
- try {
50806
- process.kill(pidToKill, 0);
50807
- } catch {
50808
- clearInterval(checkInterval);
50809
- resolve2();
50810
- }
50811
- }, 1e3);
51087
+ const result = await shutdownChild(api.xctraceProcess, {
51088
+ graceMs: STOP_GRACE_MS,
51089
+ termMs: STOP_TERM_MS,
51090
+ killMs: STOP_KILL_MS
50812
51091
  });
51092
+ let warning;
51093
+ if (!result.clean) {
51094
+ warning = `xctrace did not respond to SIGINT${result.signalUsed === "SIGKILL" ? "/SIGTERM" : ""}; ${result.signalUsed} was used. Trace bundle may be incomplete.`;
51095
+ process.stderr.write(`[ios-profiler] ${warning}
51096
+ `);
51097
+ }
50813
51098
  api.profilingActive = false;
50814
51099
  api.xctracePid = null;
51100
+ api.xctraceProcess = null;
51101
+ api.recordingExitedUnexpectedly = false;
51102
+ api.lastExitInfo = null;
50815
51103
  const { files: exportedFiles, diagnostics } = exportIosTraceData(api.traceFile);
50816
51104
  api.exportedFiles = exportedFiles;
50817
- return {
51105
+ const stopResult = {
50818
51106
  traceFile: api.traceFile,
50819
51107
  exportedFiles,
50820
51108
  exportDiagnostics: diagnostics
50821
51109
  };
51110
+ if (warning) stopResult.warning = warning;
51111
+ return stopResult;
50822
51112
  }
50823
51113
  };
50824
51114
 
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "MCP server for iOS Simulator control",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/rules/argent.md CHANGED
@@ -48,7 +48,7 @@ Before starting to interact with the app, read the `argent-simulator-interact` s
48
48
  - Interaction tools (`gesture-tap`, `gesture-swipe`, `gesture-pinch`, `gesture-rotate`, `gesture-custom`, `launch-app`, etc.) return a screenshot automatically.
49
49
  Call `screenshot` separately only for a baseline before any action or after a delay.
50
50
  - Always open apps with `launch-app` or `open-url` — never tap home screen icons.
51
- - Always use `run-sequence` when performing multiple sequential simulator actions where you don't need to observe the screen between steps. More in `simulator-interact` skill.
51
+ - Always use `run-sequence` when performing multiple sequential simulator actions where you don't need to observe the screen between steps. More in `argent-simulator-interact` skill.
52
52
  - When the session ends or the user says they are done: call `stop-all-simulator-servers`.
53
53
  If the user started Metro separately, ask whether to call `stop-metro` (specify the port if not 8081).
54
54
  - If tools provided by mcp-server are not sufficient and action can be done using `xcrun` or other commands, use the command. Examples: changing simulator options, performing simulator action such as lock, shake, etc.
@@ -1,19 +1,19 @@
1
1
  ---
2
2
  name: argent-react-native-optimization
3
- description: Optimizes a React Native app by profiling first to find real bottlenecks, then sweeping for mechanical issues. Entry-point for all performance work. Use when the app feels slow, user asks to optimize, fix re-renders, reduce jank, or improve startup. Delegates to react-native-profiler for measurement.
3
+ description: Optimizes a React Native app by profiling first to find real bottlenecks, then sweeping for mechanical issues. Entry-point for all performance work. Use when the app feels slow, user asks to optimize, fix re-renders, reduce jank, or improve startup. Delegates to argent-react-native-profiler for measurement.
4
4
  ---
5
5
 
6
6
  ## Rules
7
7
 
8
8
  - Do not apply shotgun optimizations. Measure first, define what "good enough" looks like (target metric + threshold), fix the top offender, re-measure honestly.
9
9
  - **Quick scan** — `react-profiler-renders` for a live render count table. Identifies hot components instantly.
10
- - **Deep measure** — load `react-native-profiler` skill. `react-profiler-start` → interact → `react-profiler-stop` → `react-profiler-analyze`.
10
+ - **Deep measure** — load `argent-react-native-profiler` skill. `react-profiler-start` → interact → `react-profiler-stop` → `react-profiler-analyze`.
11
11
  - **Inspect** — `react-profiler-component-source` per finding. `react-profiler-fiber-tree` to trace component ancestry and render cost.
12
12
  - **Verify correctness** - before fixing, recollect information from steps above and make a logical conclusion whether the approach is worth undertaking.
13
13
  - **Fix** — apply one fix. Validate with `debugger-evaluate` before committing.
14
14
  - **Re-measure** — report whether the target metric improved, regressed, or stayed flat. Check for regressions in other areas. If no net benefit or unacceptable tradeoffs, revert.
15
15
  - **Profile for discovery, not only verification.** Use the profiler to find issues static analysis missed, not only to confirm fixes.
16
- - **One fix per cycle for architectural changes.** Mechanical batch fixes (inline styles, index keys) can be grouped — re-profile once after the batch. When the measurement involves simulator interaction, record it as a flow (`create-flow` skill) before the first run so all subsequent cycles replay identical steps.
16
+ - **One fix per cycle for architectural changes.** Mechanical batch fixes (inline styles, index keys) can be grouped — re-profile once after the batch. When the measurement involves simulator interaction, record it as a flow (`argent-create-flow` skill) before the first run so all subsequent cycles replay identical steps.
17
17
  - **React Compiler**: if `react-profiler-analyze` reports `reactCompilerEnabled: true`, do NOT propose `useCallback`/`useMemo`/`React.memo` unless you confirmed compiler bail-out via `react-profiler-fiber-tree` (absent `useMemoCache`).
18
18
  - **Sub-agents**: Phases 1–2 dispatch sub-agents — one per file for lint results, one per checklist item for semantic. Sub-agents CANNOT touch the simulator - all profiling and E2E verification must happen in the main agent.
19
19
 
@@ -43,15 +43,15 @@ See [references/semantic-checklist.md](references/semantic-checklist.md) for ful
43
43
 
44
44
  ### Phase 3: Visual profiling
45
45
 
46
- 1. Load `react-native-profiler` skill, start dual profiling
46
+ 1. Load `argent-react-native-profiler` skill, start dual profiling
47
47
  2. Exercise key user flows (navigate screens the user specified, or all major flows)
48
48
  3. Analyze with `react-profiler-analyze` + `ios-profiler-analyze` + `profiler-combined-report`
49
49
  4. Cross-reference profiling results with Phase 1–2 findings
50
- 5. Fix highest-impact issues. Re-profile after architectural changes; batch mechanical fixes. If a recorded flow breaks after a fix (e.g., UI layout changed), follow `create-flow` skill to repair the flow rather than silently discarding it.
50
+ 5. Fix highest-impact issues. Re-profile after architectural changes; batch mechanical fixes. If a recorded flow breaks after a fix (e.g., UI layout changed), follow `argent-create-flow` skill to repair the flow rather than silently discarding it.
51
51
 
52
52
  ### Phase 4: Verify no regressions
53
53
 
54
- Navigate every screen and UI flow within scope, confirm each renders without errors. If no scope was specified, verify the entire app — cover all reachable screens via `simulator-interact`. Use `debugger-log-registry` to check for runtime errors and take screenshots to check for red/yellow error screens. Check for regressions introduced by fixes (e.g., fewer re-renders but higher CPU, or new jank in a different screen). Main agent only.
54
+ Navigate every screen and UI flow within scope, confirm each renders without errors. If no scope was specified, verify the entire app — cover all reachable screens via `argent-simulator-interact`. Use `debugger-log-registry` to check for runtime errors and take screenshots to check for red/yellow error screens. Check for regressions introduced by fixes (e.g., fewer re-renders but higher CPU, or new jank in a different screen). Main agent only.
55
55
 
56
56
  ## App-wide optimization
57
57