stim 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
  5. package/dist/{android-qx_31wJo.mjs → android-Db2eEMaf.mjs} +152 -65
  6. package/dist/android-cas-PnKfAdB2.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{app-install-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
  9. package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
  10. package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
  16. package/dist/cli-0cX5s4NM.mjs +45 -0
  17. package/dist/cli.mjs +6 -42
  18. package/dist/client-C2IS36Qf.mjs +1333 -0
  19. package/dist/collector-run.mjs +8 -7
  20. package/dist/{command-output-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
  22. package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-2Wo81np9.mjs → deps-BFlKbOIp.mjs} +3 -3
  25. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  26. package/dist/dev-client-CjzLoNs0.mjs +80 -0
  27. package/dist/{device-Cf7vl-so.mjs → device-BZ1IyS5A.mjs} +7 -7
  28. package/dist/device-capacity-DMuKC7nx.mjs +136 -0
  29. package/dist/device-host-worker.d.mts +1 -0
  30. package/dist/device-host-worker.mjs +769 -0
  31. package/dist/device-ios-DMQBUPus.mjs +381 -0
  32. package/dist/{device-lease-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
  39. package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
  41. package/dist/{guide-status-BaMYu55M.mjs → guide-status-CIoWK6mG.mjs} +3 -3
  42. package/dist/hosted-macos-DwowNU7W.mjs +447 -0
  43. package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
  44. package/dist/in-use-CndPMilN.mjs +110 -0
  45. package/dist/{ios-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
  47. package/dist/{ios-device-BW-t_Lkv.mjs → ios-device-DERmMmk-.mjs} +12 -7
  48. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  49. package/dist/{launch-verify-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
  50. package/dist/{logs-DcSRbC2t.mjs → logs-h9h6MUvE.mjs} +95 -13
  51. package/dist/machine-usage-D_YYMzfR.mjs +283 -0
  52. package/dist/machines-CYtOCqcj.mjs +259 -0
  53. package/dist/macos-BwtbUtTN.mjs +468 -0
  54. package/dist/macos-run.d.mts +5 -0
  55. package/dist/macos-run.mjs +2 -0
  56. package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
  57. package/dist/{metro-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-C5UutqpJ.mjs → ports-DddwttKM.mjs} +3 -3
  66. package/dist/prebuild-BvxDlD6C.mjs +213 -0
  67. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  68. package/dist/{project-DGeclhvO.mjs → project-CV3tDFHl.mjs} +9 -18
  69. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  70. package/dist/pull-requests.d.mts +120 -0
  71. package/dist/pull-requests.mjs +2 -0
  72. package/dist/recordings-GEXp4iBT.mjs +37 -0
  73. package/dist/{reload-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
  77. package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/slot-launch-BY0BrOLh.mjs +65 -0
  84. package/dist/{spawn-claims-PT8aQpCa.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  85. package/dist/spawn-entry-DncpJvWL.mjs +16 -0
  86. package/dist/stage-CzaiwFsI.mjs +143 -0
  87. package/dist/start-CPO4wRg7.mjs +2 -0
  88. package/dist/{start-BZhItkiL.mjs → start-CUD9nasA.mjs} +108 -61
  89. package/dist/state-B4PQFXMW.d.mts +12 -0
  90. package/dist/state-BxCgeTQu.mjs +41 -0
  91. package/dist/{state-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
  95. package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -20
  96. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  97. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  98. package/dist/stim-footprint.swift +32 -0
  99. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  100. package/dist/{stop-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +44 -12
  105. package/dist/supervisor-run.mjs +276 -33
  106. package/dist/support-CDQbDkL5.mjs +315 -0
  107. package/dist/support-WLOzVKUv.mjs +176 -0
  108. package/dist/teardown-BYuQafiA.mjs +718 -0
  109. package/dist/{xcode-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
  113. package/dist/web-run.mjs +326 -13
  114. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  115. package/dist/workspace-state-CL2V2Ons.mjs +371 -0
  116. package/dist/worktree-BT_uULFx.mjs +2 -0
  117. package/dist/{worktree-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +55 -10
  121. package/shim/bundle-response.d.cts +1 -0
  122. package/shim/expo-metro-config.cjs +10 -2
  123. package/dist/build-progress-l7d-X-ZP.mjs +0 -247
  124. package/dist/in-use-wYEaKQM9.mjs +0 -249
  125. package/dist/ownership-PUP4Dwai.mjs +0 -1138
  126. package/dist/server-expo-BeYwP_Xy.mjs +0 -2
  127. package/dist/slot-launch-BIWEW-wI.mjs +0 -19
  128. package/dist/start-Rw2GD3nV.mjs +0 -2
  129. package/dist/stats-CaSwmsSZ.mjs +0 -62
  130. package/dist/stats-D8rUTPHd.mjs +0 -368
  131. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  132. package/dist/worktree-3cBa-bQt.mjs +0 -2
  133. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,120 @@
1
+ //#region src/workspace/pull-request.d.ts
2
+ interface GhPullRequest {
3
+ number: number;
4
+ state: string;
5
+ url: string;
6
+ headRefOid: string;
7
+ mergedAt: string | null;
8
+ closedAt: string | null;
9
+ isCrossRepository?: boolean;
10
+ isDraft?: boolean;
11
+ headRepository?: {
12
+ name?: string | null;
13
+ } | null;
14
+ headRepositoryOwner?: {
15
+ login?: string | null;
16
+ } | null;
17
+ title?: string;
18
+ reviewDecision?: string | null;
19
+ commits?: {
20
+ nodes?: {
21
+ commit?: {
22
+ statusCheckRollup?: GhCheckRollup | null;
23
+ };
24
+ }[];
25
+ };
26
+ }
27
+ interface GhStateCount {
28
+ state: string;
29
+ count: number;
30
+ }
31
+ interface GhCheckRollup {
32
+ contexts?: {
33
+ checkRunCountsByState?: GhStateCount[];
34
+ statusContextCountsByState?: GhStateCount[];
35
+ };
36
+ }
37
+ interface CheckCounts {
38
+ passing: number;
39
+ failing: number;
40
+ pending: number;
41
+ }
42
+ /**
43
+ * The pull request whose head branch is the worktree's branch and whose commits include the worktree's HEAD.
44
+ * `containsHead` is false when the pull request's head is an ancestor of HEAD: HEAD has commits the pull request
45
+ * never had. `endedAt` is when it was merged or closed, in epoch milliseconds, and null while it is open.
46
+ */
47
+ interface PullRequestFact {
48
+ number: number;
49
+ state: "open" | "merged" | "closed";
50
+ draft: boolean;
51
+ url: string;
52
+ head: string;
53
+ containsHead: boolean;
54
+ endedAt: number | null;
55
+ /** Present when the lookup asked for detail. */
56
+ title?: string;
57
+ reviewDecision?: "approved" | "changes-requested" | "review-required" | null;
58
+ checks?: CheckCounts | null;
59
+ }
60
+ /** Sums a head commit's check runs and commit statuses by outcome; null when the commit has no checks. */
61
+ declare function checkCounts(rollup: GhCheckRollup | null | undefined): CheckCounts | null;
62
+ type PullRequestLookup = {
63
+ pullRequest: PullRequestFact | null;
64
+ } | {
65
+ unavailable: string;
66
+ };
67
+ /**
68
+ * Picks the pull request that describes `head` among those `gh pr list --head <branch>` returned. One whose head is
69
+ * `head` wins, then one whose head contains `head`, then one whose head `head` contains. An open one wins a tier,
70
+ * then the newest. A pull request unrelated to `head` is from an earlier use of the branch name and is ignored. A
71
+ * cross-repository pull request is kept only when its head repository is one of `ownRepos` (this checkout's own git
72
+ * remotes, lowercased "owner/name"); otherwise it only shares the branch name with a stranger's fork and is ignored.
73
+ * `isAncestor(a, b)` answers whether commit `a` is an ancestor of commit `b`, and false when git cannot tell.
74
+ */
75
+ declare function selectPullRequest(pulls: readonly GhPullRequest[], head: string, isAncestor: (ancestor: string, descendant: string) => boolean, ownRepos?: ReadonlySet<string>): PullRequestFact | null;
76
+ /**
77
+ * "owner/name", lowercased, parsed out of a git remote URL, or null when it doesn't match: https, `ssh://`, or
78
+ * scp-like (`[user@]host:owner/name`, which also covers an `~/.ssh/config` host alias in place of `user@host`).
79
+ */
80
+ declare function parseRemoteRepo(url: string): string | null;
81
+ /**
82
+ * Asks GitHub, through `gh`, which pull requests have `branch` as their head, run from `cwd` so `gh` picks the
83
+ * repository from its remotes. Each call is one fixed `gh pr list` invocation. Once `gh` is missing, signed out or
84
+ * times out, every later lookup through the same function answers `unavailable` without running it again.
85
+ */
86
+ declare function pullRequestLookup(): (cwd: string, branch: string, head: string) => PullRequestLookup;
87
+ /** A worktree whose pull request {@link pullRequestLookups} looks up: its path, branch and HEAD commit. */
88
+ interface PullRequestQuery {
89
+ cwd: string;
90
+ branch: string;
91
+ head: string;
92
+ }
93
+ /**
94
+ * {@link pullRequestLookup} for every branch of one repository in one fixed `gh api graphql` call run from `repo`,
95
+ * which asks for the same 20 newest pull requests of each head branch that `gh pr list --head` returns. `gh` fills
96
+ * `{owner}` and `{repo}` from the remotes of `repo`; branch names travel as variables. Once `gh` is missing, signed
97
+ * out or times out, every later call through the same function answers `unavailable` without running it again.
98
+ * With `detail`, it also asks for each pull request's title, review decision and head commit check counts.
99
+ */
100
+ declare function pullRequestLookups({ detail }?: {
101
+ detail?: boolean;
102
+ }): (repo: string, queries: readonly PullRequestQuery[]) => Promise<PullRequestLookup[]>;
103
+ /** The merged or closed pull request whose commits include HEAD, which makes the worktree removable. */
104
+ declare function endedPullRequest(lookup: PullRequestLookup | null): PullRequestFact | null;
105
+ /** "PR #123 merged", for reports. */
106
+ declare function describePullRequest(pr: Pick<PullRequestFact, "number" | "state">): string;
107
+ /** A linked worktree whose pull request {@link worktreePullRequests} looks up. */
108
+ interface WorktreeBranch {
109
+ path: string;
110
+ branch: string;
111
+ repository: string;
112
+ }
113
+ /**
114
+ * Looks up the pull request of each worktree's branch and HEAD, with one {@link pullRequestLookups} call per
115
+ * repository. The map holds null for a worktree without one, and leaves out a worktree whose HEAD git cannot read
116
+ * or whose lookup was unavailable.
117
+ */
118
+ declare function worktreePullRequests(): (worktrees: readonly WorktreeBranch[]) => Promise<Map<string, PullRequestFact | null>>;
119
+ //#endregion
120
+ export { CheckCounts, GhCheckRollup, GhPullRequest, PullRequestFact, PullRequestLookup, PullRequestQuery, WorktreeBranch, checkCounts, describePullRequest, endedPullRequest, parseRemoteRepo, pullRequestLookup, pullRequestLookups, selectPullRequest, worktreePullRequests };
@@ -0,0 +1,2 @@
1
+ import { a as pullRequestLookup, c as worktreePullRequests, i as parseRemoteRepo, n as describePullRequest, o as pullRequestLookups, r as endedPullRequest, s as selectPullRequest, t as checkCounts } from "./pull-request-pxnDCQzn.mjs";
2
+ export { checkCounts, describePullRequest, endedPullRequest, parseRemoteRepo, pullRequestLookup, pullRequestLookups, selectPullRequest, worktreePullRequests };
@@ -0,0 +1,37 @@
1
+ import { c as gitCommonDirOnDisk } from "./worktree-C0c5Udwa.mjs";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { loadConfig, recordingEnabled, workspaceRecordingsDir } from "@stim-cli/core/state";
4
+ //#region src/workspace/recordings.ts
5
+ /** `recording.enabled` for one registered workspace, from `STIM_RECORDING` and its workspace, repo and machine layers. */
6
+ function workspaceRecordingEnabled(path, project, config, env) {
7
+ const repo = gitCommonDirOnDisk(path);
8
+ return recordingEnabled(env, [
9
+ project.settings,
10
+ repo ? config?.repos?.[repo]?.settings : void 0,
11
+ config
12
+ ]);
13
+ }
14
+ /** Every registered workspace's `recording.enabled` from the settings files alone, without `STIM_RECORDING`. */
15
+ function recordingLayers() {
16
+ const config = loadConfig();
17
+ return new Map(Object.entries(config?.projects ?? {}).map(([path, project]) => [path, workspaceRecordingEnabled(path, project, config, {})]));
18
+ }
19
+ /**
20
+ * Deletes the recordings of every workspace a settings write turned off, comparing the settings files before
21
+ * (`before`, from {@link recordingLayers}) and after it, and returns those workspaces. stim-server stops
22
+ * recording them once `stim status` shows the change.
23
+ */
24
+ function deleteTurnedOffRecordings(before) {
25
+ return [...recordingLayers()].flatMap(([path, enabled]) => {
26
+ if (enabled || before.get(path) === false) return [];
27
+ const dir = workspaceRecordingsDir(path);
28
+ if (!existsSync(dir)) return [];
29
+ rmSync(dir, {
30
+ recursive: true,
31
+ force: true
32
+ });
33
+ return [path];
34
+ });
35
+ }
36
+ //#endregion
37
+ export { recordingLayers as n, workspaceRecordingEnabled as r, deleteTurnedOffRecordings as t };
@@ -1,14 +1,15 @@
1
- import { B as deviceSlotPlatforms, V as parseDeviceSlotKey, p as getProject } from "./config-0jCcMM0E.mjs";
2
- import { c as resolveProjectMetro, g as recordWorkspaceUse } from "./metro-BPeY1YMh.mjs";
3
- import { i as findCommandWorkspace } from "./project-DGeclhvO.mjs";
4
- import { f as readWorkspaceLaunches } from "./state-DwAU2_-D.mjs";
5
- import { f as refuseNoProject, l as phaseLine } from "./command-output-BIL7rlX7.mjs";
6
- import { q as resolveOwnedAvdSerial } from "./android-qx_31wJo.mjs";
7
- import { a as readWebRecord, l as webFacts, r as cdpEndpoint } from "./state-l1pYwOre.mjs";
8
- import { S as resolveOwnedIosSim } from "./ios-CmEzZiOj.mjs";
9
- import { h as iosAppProcess, o as androidAppProcess } from "./app-install-CDFY2nUL.mjs";
10
- import { n as nativeRunCommand } from "./slot-launch-BIWEW-wI.mjs";
11
- import { n as sendToOwnedPage, t as liveWebRecord } from "./page-BTu7I60K.mjs";
1
+ import { B as parseDeviceSlotKey, p as getProject, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
2
+ import { i as recordWorkspaceUse } from "./workspace-state-CL2V2Ons.mjs";
3
+ import { i as findCommandWorkspace } from "./project-CV3tDFHl.mjs";
4
+ import { f as readWorkspaceLaunches } from "./state-CaeaHbDA.mjs";
5
+ import { f as refuseNoProject, l as phaseLine } from "./command-output-CliHHn7a.mjs";
6
+ import { J as resolveOwnedAvdSerial } from "./android-Db2eEMaf.mjs";
7
+ import { l as resolveProjectMetro } from "./metro-Di8EN2Tb.mjs";
8
+ import { o as readWebRecord, r as cdpEndpoint, u as webFacts } from "./state-DFm77wRn.mjs";
9
+ import { w as resolveOwnedIosSim } from "./ios-3CtCRhwr.mjs";
10
+ import { p as ensureMetroReverse, s as androidAppProcess, v as iosAppProcess } from "./app-install-CAAplXAt.mjs";
11
+ import { n as nativeRunCommand } from "./slot-launch-BY0BrOLh.mjs";
12
+ import { n as sendToOwnedPage, t as liveWebRecord } from "./page-QF_Ke10s.mjs";
12
13
  import chalk from "chalk";
13
14
  import { WebSocket } from "ws";
14
15
  //#region src/engine/reload.ts
@@ -31,7 +32,11 @@ function peerMatches(query, role, appId) {
31
32
  if (role === "ios") return query.get("role")?.toLowerCase() === "ios";
32
33
  return query.get("app") === appId;
33
34
  }
34
- function reloadThroughMetro(port, { role, appId, timeoutMs = METRO_TIMEOUT_MS }) {
35
+ /**
36
+ * Asks Metro for its peers and reloads the ones matching `role` and `appId`. With `peersOnly`, it sends nothing and
37
+ * only reports the outcome a reload would have, so a caller can wait for the app to connect.
38
+ */
39
+ function reloadThroughMetro(port, { role, appId, timeoutMs = METRO_TIMEOUT_MS, peersOnly = false }) {
35
40
  const roleName = ROLE_NAMES[role];
36
41
  return new Promise((resolve) => {
37
42
  const requestId = `stim-reload-${process.pid}-${Date.now()}`;
@@ -72,6 +77,15 @@ function reloadThroughMetro(port, { role, appId, timeoutMs = METRO_TIMEOUT_MS })
72
77
  const query = peerQuery(metadata);
73
78
  return query ? peerMatches(query, role, appId) : false;
74
79
  });
80
+ if (rolePeers.length === 0 && peersOnly) {
81
+ finish({
82
+ failed: true,
83
+ noPeer: true,
84
+ peers,
85
+ reason: `No ${roleName} React Native app is connected to Metro on port ${port}.`
86
+ });
87
+ return;
88
+ }
75
89
  if (rolePeers.length === 0) {
76
90
  socket.send(JSON.stringify({
77
91
  version: 2,
@@ -86,7 +100,7 @@ function reloadThroughMetro(port, { role, appId, timeoutMs = METRO_TIMEOUT_MS })
86
100
  });
87
101
  return;
88
102
  }
89
- for (const [id] of rolePeers) socket.send(JSON.stringify({
103
+ if (!peersOnly) for (const [id] of rolePeers) socket.send(JSON.stringify({
90
104
  version: 2,
91
105
  method: "reload",
92
106
  target: id
@@ -98,7 +112,7 @@ function reloadThroughMetro(port, { role, appId, timeoutMs = METRO_TIMEOUT_MS })
98
112
  });
99
113
  return;
100
114
  }
101
- socket.send(JSON.stringify({
115
+ if (!peersOnly) socket.send(JSON.stringify({
102
116
  version: 2,
103
117
  method: "reload"
104
118
  }));
@@ -130,8 +144,16 @@ const DEFAULT_DEPS = {
130
144
  const facts = webFacts(readWebRecord(root));
131
145
  return facts?.status === "unverified" ? "unverified" : liveWebRecord(facts?.record ?? null);
132
146
  },
133
- reloadPage: (record) => sendToOwnedPage(record, "Page.reload")
147
+ reloadPage: (record) => sendToOwnedPage(record, "Page.reload"),
148
+ ensureReverse: (serial, metroPort) => ensureMetroReverse({
149
+ serial,
150
+ metroPort
151
+ }),
152
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
134
153
  };
154
+ const RECONNECT_POLL_MS = 500;
155
+ const RECONNECT_POLLS = 16;
156
+ const RECONNECT_UNOBSERVED_MS = 2500;
135
157
  function failure(code, message, remedy) {
136
158
  return {
137
159
  code,
@@ -143,8 +165,9 @@ function describe(error) {
143
165
  return error instanceof Error ? error.message : String(error);
144
166
  }
145
167
  async function reloadBrowser(browser, metroPort, d) {
168
+ let pageUrl;
146
169
  try {
147
- await d.reloadPage(browser);
170
+ pageUrl = await d.reloadPage(browser);
148
171
  } catch (error) {
149
172
  return {
150
173
  ok: false,
@@ -157,10 +180,11 @@ async function reloadBrowser(browser, metroPort, d) {
157
180
  platform: "web",
158
181
  deviceId: cdpEndpoint(browser.cdpPort),
159
182
  deviceName: browser.version ?? "Chrome",
160
- appId: browser.url,
183
+ appId: pageUrl,
161
184
  metroPort: metroPort !== null && new URL(browser.url).port === String(metroPort) ? metroPort : null,
162
185
  strategy: "cdp",
163
- targets: 1
186
+ targets: 1,
187
+ reverseRestored: []
164
188
  }
165
189
  };
166
190
  }
@@ -312,10 +336,32 @@ async function runReload({ root, platform = null, deps = {} }) {
312
336
  ok: false,
313
337
  error: stopped
314
338
  };
315
- const reloaded = await d.reloadMetro(port, {
339
+ const reverseRestored = [];
340
+ const reverseChecked = target.platform === "android" ? live.filter((c) => c.platform === "android" && !c.record.release && c.record.metroPort === port) : [];
341
+ for (const candidate of reverseChecked) {
342
+ const serial = candidate.record.deviceId;
343
+ const ensured = d.ensureReverse(serial, port);
344
+ if (ensured.failed) return {
345
+ ok: false,
346
+ error: failure("STIM_RELOAD_FAILED", `Stim could not check or restore the adb reverse for Metro port ${port} on ${serial}: ${ensured.reason}`, `Run \`adb -s ${serial} reverse tcp:${port} tcp:${port}\`, then \`stim reload android\` again.`)
347
+ };
348
+ if (ensured.restored) reverseRestored.push(serial);
349
+ }
350
+ const request = {
316
351
  role: target.platform,
317
352
  appId: target.record.appId
318
- });
353
+ };
354
+ const expectedPeers = reverseChecked.filter((c) => c.record.appId === target.record.appId).length;
355
+ for (let poll = 0; reverseRestored.length && poll < RECONNECT_POLLS; poll++) {
356
+ const peers = await d.reloadMetro(port, {
357
+ ...request,
358
+ peersOnly: true
359
+ });
360
+ if (peers.broadcast) await d.sleep(RECONNECT_UNOBSERVED_MS);
361
+ if (peers.unreachable || peers.broadcast || peers.ok && peers.targets >= expectedPeers) break;
362
+ await d.sleep(RECONNECT_POLL_MS);
363
+ }
364
+ const reloaded = await d.reloadMetro(port, request);
319
365
  if (!reloaded.ok) {
320
366
  const stoppedAfterMetro = processFailure(target.platform, target.record, d, target.slot);
321
367
  if (stoppedAfterMetro) return {
@@ -342,7 +388,8 @@ async function runReload({ root, platform = null, deps = {} }) {
342
388
  appId: target.record.appId,
343
389
  metroPort: port,
344
390
  strategy,
345
- targets: reloaded.targets ?? null
391
+ targets: reloaded.targets ?? null,
392
+ reverseRestored
346
393
  }
347
394
  };
348
395
  }
@@ -386,7 +433,8 @@ function registerReload(program, deps = {}) {
386
433
  return;
387
434
  }
388
435
  const scope = facts.strategy === "metro-broadcast" ? ` This Metro cannot name its connected apps, so the reload request was broadcast to all of them and Stim cannot confirm ${facts.appId} was one. Check the expected UI on ${facts.deviceId}; if nothing changed, reload from the app's own error screen or dev menu.` : facts.targets && facts.targets > 1 ? ` ${facts.targets} devices are running this app on that Metro and the reload request addressed all of them, not only ${facts.deviceId}.` : "";
389
- console.log(`Reload requested for ${facts.appId} on ${facts.deviceName} (${facts.deviceId}) via ${facts.strategy}; ${facts.platform} Metro port ${facts.metroPort}.${scope} Completion is not observed; verify the expected UI and run stim logs --errors.`);
436
+ const restored = facts.reverseRestored.length ? ` The adb reverse for tcp:${facts.metroPort} was missing on ${facts.reverseRestored.join(", ")}, so Stim restored it first.` : "";
437
+ console.log(`Reload requested for ${facts.appId} on ${facts.deviceName} (${facts.deviceId}) via ${facts.strategy}; ${facts.platform} Metro port ${facts.metroPort}.${restored}${scope} Completion is not observed; verify the expected UI and run stim logs --errors.`);
390
438
  }
391
439
  });
392
440
  }
@@ -1,9 +1,9 @@
1
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
2
  import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
3
- import { M as workspaceLogsDir } from "./metro-BPeY1YMh.mjs";
4
- import { f as resolvePackageJson } from "./project-DGeclhvO.mjs";
3
+ import { C as workspaceLogsDir } from "./workspace-state-CL2V2Ons.mjs";
4
+ import { f as resolvePackageJson } from "./project-CV3tDFHl.mjs";
5
5
  import { n as stripAnsi } from "./process-output-Fpl8PYi8.mjs";
6
- import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-DUCg52-K.mjs";
6
+ import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-DhtX6bom.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
@@ -0,0 +1,119 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
3
+ import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
4
+ import { i as macosRuntimeClaim, n as macosLogFile, o as updateMacosRecord, r as macosProcess } from "./state-BxCgeTQu.mjs";
5
+ import { LOG_ROTATE_BYTES } from "@stim-cli/core";
6
+ import { readMacosRecord } from "@stim-cli/core/state";
7
+ import { StringDecoder } from "node:string_decoder";
8
+ //#region src/macos/run.ts
9
+ function logLines(stream, write) {
10
+ const decoder = new StringDecoder("utf8");
11
+ let pending = "";
12
+ stream.on("data", (chunk) => {
13
+ pending += decoder.write(chunk);
14
+ while (pending.length > 65536 && !pending.includes("\n")) {
15
+ write(pending.slice(0, 65536));
16
+ pending = pending.slice(65536);
17
+ }
18
+ const lines = pending.split("\n");
19
+ pending = lines.pop() ?? "";
20
+ for (const line of lines) write(line.replace(/\r$/, ""));
21
+ });
22
+ stream.on("end", () => {
23
+ pending += decoder.end();
24
+ if (pending) write(pending);
25
+ });
26
+ }
27
+ async function runMacosSupervisor(root, launchId) {
28
+ const record = readMacosRecord(root);
29
+ if (!record || record.launchId !== launchId || record.build.state !== "ok") throw new Error("The macOS launch record changed before its supervisor started.");
30
+ const owner = macosProcess(process.pid);
31
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
32
+ root: macosRuntimeClaim(root),
33
+ mode: "exclusive",
34
+ label: "macOS app"
35
+ });
36
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
37
+ const claim = attempt.acquired;
38
+ if (!claim) throw new Error(`The macOS runtime is already held at ${attempt.held?.path ?? macosRuntimeClaim(root)}.`);
39
+ const writer = createNdjsonWriter(macosLogFile(root), { maxBytes: LOG_ROTATE_BYTES });
40
+ let app;
41
+ let stopping = false;
42
+ let spawned = false;
43
+ let childExited = false;
44
+ try {
45
+ if (!(0, process_identity_exports.sameProcessRecord)(readMacosRecord(root)?.supervisor, record.supervisor) || !updateMacosRecord(root, record.supervisor, { supervisor: owner })) throw new Error("The macOS launch owner changed.");
46
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
47
+ const child = getExecutor().spawn(record.executable, record.arguments, {
48
+ cwd: root,
49
+ stdio: [
50
+ "ignore",
51
+ "pipe",
52
+ "pipe"
53
+ ],
54
+ env: {
55
+ ...process.env,
56
+ NSUnbufferedIO: "YES"
57
+ }
58
+ });
59
+ spawned = child.pid !== void 0;
60
+ const ended = new Promise((resolve, reject) => {
61
+ child.once("error", reject);
62
+ child.once("close", () => {
63
+ childExited = true;
64
+ resolve();
65
+ });
66
+ });
67
+ if (!child.pid) {
68
+ await ended;
69
+ throw new Error("The macOS app did not start.");
70
+ }
71
+ app = macosProcess(child.pid);
72
+ (0, ownership_claim_exports.setClaimChild)(claim, app);
73
+ if (!updateMacosRecord(root, owner, { app })) {
74
+ if ((0, process_identity_exports.inspectProcessIdentity)(app) === "same") process.kill(app.pid, "SIGTERM");
75
+ throw new Error("The macOS launch owner changed.");
76
+ }
77
+ const log = (level, msg) => writer.write({
78
+ src: "client",
79
+ platform: "macos",
80
+ level,
81
+ msg
82
+ });
83
+ if (child.stdout) logLines(child.stdout, (line) => log("info", line));
84
+ if (child.stderr) logLines(child.stderr, (line) => log("warn", line));
85
+ const stop = async () => {
86
+ if (stopping) return;
87
+ stopping = true;
88
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
89
+ if (!app || (0, process_identity_exports.inspectProcessIdentity)(app) !== "same") return;
90
+ process.kill(app.pid, signal);
91
+ if (await (0, process_identity_exports.waitForProcessExit)(app, 5e3)) return;
92
+ }
93
+ };
94
+ process.on("SIGTERM", stop);
95
+ process.on("SIGINT", stop);
96
+ log("info", `Started ${record.product} (pid ${app.pid}).`);
97
+ await ended;
98
+ process.off("SIGTERM", stop);
99
+ process.off("SIGINT", stop);
100
+ log(stopping || child.exitCode === 0 ? "info" : "error", `Exited ${record.product} (${child.exitCode ?? child.signalCode ?? "unknown"}).`);
101
+ updateMacosRecord(root, owner, {
102
+ app: void 0,
103
+ supervisor: void 0
104
+ });
105
+ } finally {
106
+ if (!spawned || childExited || app && ["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(app))) {
107
+ (0, ownership_claim_exports.clearClaimChild)(claim);
108
+ (0, ownership_claim_exports.releaseClaim)(claim);
109
+ }
110
+ writer.close();
111
+ }
112
+ }
113
+ if (process.argv[1]?.endsWith("/macos-run.mjs") || process.argv[1]?.endsWith("/macos/run.ts")) {
114
+ const [root, launchId] = process.argv.slice(2);
115
+ if (!root || !launchId) throw new Error("macos-run needs a workspace and launch identity.");
116
+ await runMacosSupervisor(root, launchId);
117
+ }
118
+ //#endregion
119
+ export { runMacosSupervisor as n, logLines as t };
@@ -1,20 +1,44 @@
1
1
  import { i as __require, t as __commonJSMin } from "./rolldown-runtime-BsOwBuO_.mjs";
2
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
3
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
3
- import { w as projectMetroSharedCache } from "./settings-xB0Frkg6.mjs";
4
- import { a as metroStoreRoot, o as registerMetroStore, t as appendCacheStore } from "./metro-store-DzeWkfqW.mjs";
4
+ import { E as projectMetroSharedCache } from "./settings-CduVcoxj.mjs";
5
+ import { a as metroStoreRoot, o as registerMetroStore, t as appendCacheStore } from "./metro-store-CDDZHtPl.mjs";
5
6
  import { createRequire } from "node:module";
6
7
  import { isAbsolute, join, relative, sep } from "node:path";
7
8
  //#endregion
8
9
  //#region src/supervisor/server-bare.ts
9
10
  var import_bundle_response = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
10
11
  const { randomUUID } = __require("node:crypto");
11
- function bundleResponseMiddleware(write) {
12
+ const LOOPBACK = /* @__PURE__ */ new Set([
13
+ "127.0.0.1",
14
+ "::1",
15
+ "::ffff:127.0.0.1"
16
+ ]);
17
+ const PROGRESS_PART = /^\{"done":(\d+),"total":(\d+),"percent":(\d+)\}$/;
18
+ const PROGRESS_INTERVAL_MS = 1e3;
19
+ function clientPidFromLsof(output, clientPort, serverPort) {
20
+ const connection = new RegExp(`:${clientPort}->\\S*:${serverPort}$`);
21
+ let pid = null;
22
+ for (const line of String(output).split("\n")) if (line.startsWith("p")) pid = Number(line.slice(1));
23
+ else if (line.startsWith("n") && connection.test(line) && Number.isInteger(pid) && pid > 0) return pid;
24
+ return null;
25
+ }
26
+ function lookupClientPid(req, runLsof) {
27
+ const { remoteAddress, remotePort, localPort } = req.socket;
28
+ if (!runLsof || !LOOPBACK.has(remoteAddress)) return null;
29
+ return Promise.resolve().then(() => runLsof([
30
+ "-nP",
31
+ `-iTCP:${remotePort}`,
32
+ "-Fpn"
33
+ ])).then((output) => clientPidFromLsof(output, remotePort, localPort)).catch(() => null);
34
+ }
35
+ function bundleResponseMiddleware(write, { runLsof } = {}) {
12
36
  return (req, res, next) => {
13
37
  if (req.method === "GET" && req.url === "/_stim/metro-warmup") {
14
38
  res.end("ready");
15
39
  return;
16
40
  }
17
- if (req.headers["x-stim-metro-warmup"] === "1") return next();
41
+ const prefetch = req.headers["x-stim-metro-warmup"] === "1";
18
42
  let url;
19
43
  try {
20
44
  url = new URL(req.url, "http://localhost");
@@ -24,27 +48,50 @@ var import_bundle_response = (/* @__PURE__ */ __commonJSMin(((exports, module) =
24
48
  const platform = url.searchParams.get("platform");
25
49
  if (req.method !== "GET" || !/\.bundle\/*$/.test(url.pathname) || !["ios", "android"].includes(platform)) return next();
26
50
  const requestId = randomUUID();
27
- const emit = (event, statusCode) => {
51
+ const kind = prefetch ? "prefetch" : "response";
52
+ const clientPid = platform === "ios" && !prefetch ? lookupClientPid(req, runLsof) : null;
53
+ const send = (ts, stage, statusCode, pid, progress) => {
28
54
  try {
29
55
  write({
30
- ts: Date.now(),
56
+ ts,
31
57
  src: "metro",
32
- level: event === "bundle_response_failed" ? "error" : "debug",
33
- event,
58
+ level: stage === "failed" && !prefetch ? "error" : "debug",
59
+ event: `bundle_${kind}_${stage}`,
34
60
  platform,
35
61
  requestId,
36
- statusCode,
37
- msg: `${platform} bundle response ${event.slice(16)}`
62
+ ...progress ?? { statusCode },
63
+ msg: progress ? `${platform} bundle ${progress.percent}%` : `${platform} bundle ${kind} ${stage}`,
64
+ ...pid ? { clientPid: pid } : {}
38
65
  });
39
66
  } catch {}
40
67
  };
41
- emit("bundle_response_started");
68
+ const emit = (stage, statusCode, progress) => {
69
+ const ts = Date.now();
70
+ if (clientPid) clientPid.then((pid) => send(ts, stage, statusCode, pid, progress));
71
+ else send(ts, stage, statusCode, null, progress);
72
+ };
73
+ emit("started");
74
+ const writeChunk = res.write;
75
+ let progressAt = 0;
76
+ res.write = function(chunk, ...rest) {
77
+ const part = typeof chunk === "string" && chunk.length < 100 ? PROGRESS_PART.exec(chunk) : null;
78
+ const now = part ? Date.now() : 0;
79
+ if (part && now - progressAt >= PROGRESS_INTERVAL_MS) {
80
+ progressAt = now;
81
+ emit("progress", void 0, {
82
+ done: Number(part[1]),
83
+ total: Number(part[2]),
84
+ percent: Number(part[3])
85
+ });
86
+ }
87
+ return writeChunk.call(this, chunk, ...rest);
88
+ };
42
89
  let ended = false;
43
90
  const finish = (complete) => {
44
91
  if (ended) return;
45
92
  ended = true;
46
93
  const success = complete && (res.statusCode === 200 || res.statusCode === 304);
47
- emit(success ? "bundle_response_finished" : "bundle_response_failed", res.statusCode);
94
+ emit(success ? "finished" : "failed", res.statusCode);
48
95
  };
49
96
  res.once("finish", () => finish(true));
50
97
  res.once("close", () => finish(false));
@@ -296,7 +343,7 @@ async function startBareServer({ root, port, logsDir, writer = null, deps = null
296
343
  });
297
344
  const httpServer = await metro.runServer(config, {
298
345
  unstable_extraMiddleware: [
299
- (0, import_bundle_response.bundleResponseMiddleware)((record) => writer?.write(record)),
346
+ (0, import_bundle_response.bundleResponseMiddleware)((record) => writer?.write(record), { runLsof: process.platform === "darwin" ? (args) => getExecutor().runFileAsync("lsof", args, { timeoutMs: 2e3 }) : void 0 }),
300
347
  symbolicationReporterMiddleware(reporter),
301
348
  communityMiddleware,
302
349
  middleware
@@ -1,13 +1,13 @@
1
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
2
  import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
3
- import { f as resolvePackageJson } from "./project-DGeclhvO.mjs";
3
+ import { f as resolvePackageJson } from "./project-CV3tDFHl.mjs";
4
4
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
5
5
  import { n as stripAnsi, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
6
- import { w as projectMetroSharedCache } from "./settings-xB0Frkg6.mjs";
7
- import { a as metroStoreRoot, i as metroStoreConfirmedRoot, n as expoMetroConfigPath, o as registerMetroStore, r as expoMetroStoreEnv } from "./metro-store-DzeWkfqW.mjs";
6
+ import { E as projectMetroSharedCache } from "./settings-CduVcoxj.mjs";
7
+ import { a as metroStoreRoot, i as metroStoreConfirmedRoot, n as expoMetroConfigPath, o as registerMetroStore, r as expoMetroStoreEnv } from "./metro-store-CDDZHtPl.mjs";
8
8
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
9
- import { LOG_ROTATE_BYTES } from "@stim-cli/core";
10
9
  import { dirname, join, parse } from "node:path";
10
+ import { LOG_ROTATE_BYTES } from "@stim-cli/core";
11
11
  //#region src/supervisor/server-expo.ts
12
12
  function delay(ms) {
13
13
  return new Promise((resolve) => {
@@ -120,11 +120,7 @@ function recordFromLine(line, { stream = "stdout" } = {}) {
120
120
  if (!msg.trim()) return null;
121
121
  if (stream === "stderr" && msg.startsWith("stim-bundle-response: ")) try {
122
122
  const record = JSON.parse(msg.slice(22));
123
- if (record.src === "metro" && [
124
- "bundle_response_started",
125
- "bundle_response_finished",
126
- "bundle_response_failed"
127
- ].includes(record.event) && ["ios", "android"].includes(record.platform) && typeof record.requestId === "string" && Number.isFinite(record.ts)) return record;
123
+ if (record.src === "metro" && /^bundle_(response|prefetch)_(started|progress|finished|failed)$/.test(record.event) && ["ios", "android"].includes(record.platform) && typeof record.requestId === "string" && Number.isFinite(record.ts)) return record;
128
124
  } catch {}
129
125
  const confirmed = metroStoreConfirmedRoot(msg);
130
126
  if (confirmed) return {
@@ -0,0 +1,2 @@
1
+ import { p as startExpoServer } from "./server-expo-DhtX6bom.mjs";
2
+ export { startExpoServer };