stim 1.25.0 → 1.26.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 (158) hide show
  1. package/README.md +1 -1
  2. package/dist/{acquire-artifact-zA4S7RHE.mjs → acquire-artifact-DVsvSwf7.mjs} +1 -1
  3. package/dist/{act-BbHS3oV8.mjs → act-C-BqJEpu.mjs} +157 -35
  4. package/dist/{activity-BpjwrQXU.mjs → activity-Lnb_EoRY.mjs} +4 -5
  5. package/dist/{agent-device-usage-output-Bnkerfod.mjs → agent-device-usage-output-DbXy5tg8.mjs} +4 -4
  6. package/dist/{android-saBeB4lr.mjs → android-B5P81yI-.mjs} +1 -1
  7. package/dist/{android-CNZgRC-4.mjs → android-CSbLRD53.mjs} +5 -5
  8. package/dist/{android-BGwuK3Mc.mjs → android-CiNQw3kq.mjs} +73 -49
  9. package/dist/{android-Cjcj8MCw.mjs → android-DTAcRwBC.mjs} +81 -34
  10. package/dist/{android-cas-BVqh2Zep.mjs → android-cas-BTVhZ0Qs.mjs} +4 -4
  11. package/dist/{android-project-CTYaETc0.d.mts → android-project-DUGMbq28.d.mts} +55 -1
  12. package/dist/api.d.mts +8 -5
  13. package/dist/api.mjs +27 -27
  14. package/dist/apk-signed-tail-B46U9hXc.mjs +142 -0
  15. package/dist/{artifact-Co8zixVx.mjs → artifact-BxREhAaV.mjs} +23 -18
  16. package/dist/{artifact-DN8JNwqS.mjs → artifact-CsAP6AGK.mjs} +35 -20
  17. package/dist/{attempt-DW-mQqM7.mjs → attempt-ClyJlZPT.mjs} +4 -2
  18. package/dist/{browser-web-CCgZsHmI.mjs → browser-web-rYPyqhTf.mjs} +11 -12
  19. package/dist/{budget-HyShrIpL.mjs → budget-k2mZGvvk.mjs} +196 -33
  20. package/dist/{build-CAiw8Ezm.mjs → build-DPhApsjV.mjs} +26 -23
  21. package/dist/{build-cache-CKCzxzxA.mjs → build-cache-DrXNCy3T.mjs} +103 -21
  22. package/dist/{build-plan-zCuHGlA8.mjs → build-plan-DrMMdayj.mjs} +5 -5
  23. package/dist/{cache-manifest-BPXRONJz.mjs → cache-manifest-BEZ9Bpl1.mjs} +1 -1
  24. package/dist/cache-manifest.mjs +1 -1
  25. package/dist/{ccache-h1FKhZeg.mjs → ccache-Bx5TbxwG.mjs} +6 -6
  26. package/dist/{cli-C4w607Pz.mjs → cli-BBKFGOFl.mjs} +19 -19
  27. package/dist/cli.mjs +1 -1
  28. package/dist/{client-BQsmyp_-.mjs → client-D9CaRks-.mjs} +36 -61
  29. package/dist/collector-run.mjs +4 -4
  30. package/dist/{command-output-BBvS1H6v.mjs → command-output-BF5AR2dL.mjs} +1 -1
  31. package/dist/{config-BZhXY15H.mjs → config-BktkY_zC.mjs} +321 -15
  32. package/dist/{launch-DscF1QfM.mjs → dependencies-Dw6wfQdv.mjs} +258 -255
  33. package/dist/{dependency-state-BwNCYG5Q.mjs → dependency-state-DFB41vfr.mjs} +1 -1
  34. package/dist/{dev-client-CHoXXZeg.mjs → dev-client-DXR_tPVY.mjs} +2 -2
  35. package/dist/{device-B9FqTBYW.mjs → device-BK-zwyd_.mjs} +13 -13
  36. package/dist/{device-F5-4z9ef.mjs → device-BcYeHSFu.mjs} +14 -14
  37. package/dist/{device-capacity-ZYaRALv5.mjs → device-capacity-DCSvUF0l.mjs} +10 -17
  38. package/dist/device-host-worker.mjs +11 -13
  39. package/dist/{device-ios-DkHBrmvH.mjs → device-ios-BLGfXQeK.mjs} +49 -17
  40. package/dist/{device-lease-run-ogEeJ62K.mjs → device-lease-run-BmvIl7bI.mjs} +4 -4
  41. package/dist/{device-pool-BIg8MU8l.mjs → device-pool-Dluix4H3.mjs} +3 -3
  42. package/dist/{doctor-k3P3PizH.mjs → doctor-Bhg1ii70.mjs} +14 -14
  43. package/dist/{doctor-A_Nr_fDe.mjs → doctor-Cp-BhzTS.mjs} +1 -1
  44. package/dist/{doctor-Bm8TVpGn.mjs → doctor-OnwP94ne.mjs} +72 -23
  45. package/dist/{doctor-fingerprint-zlujA1Gs.mjs → doctor-fingerprint-D3a04Mhu.mjs} +8 -2
  46. package/dist/doctor-fingerprint.mjs +1 -1
  47. package/dist/{doctor-watchman-BovI1y3-.mjs → doctor-watchman-BStK05nt.mjs} +4 -4
  48. package/dist/{eas-build-CyDKNTBP.mjs → eas-build-Dsj93iM9.mjs} +2 -2
  49. package/dist/{eas-fallback-gaZi-dJL.mjs → eas-fallback-FwkeqqGi.mjs} +10 -10
  50. package/dist/{eas-session-ledger-BMCA1gdO.mjs → eas-session-ledger-B5vygTiD.mjs} +5 -5
  51. package/dist/{error-diagnostics-B1Mj_-7h.mjs → error-diagnostics-BOOm9Db7.mjs} +8 -8
  52. package/dist/{gc-eaz403W6.mjs → gc-C_79zvz4.mjs} +2 -2
  53. package/dist/{gc-B9jcnmIk.mjs → gc-ldgr-hXU.mjs} +74 -43
  54. package/dist/{guide-DR0K0xW9.mjs → guide-D7K0X6KG.mjs} +272 -52
  55. package/dist/{hosted-android-Bvb2-ZxD.mjs → hosted-android-Bk54ej6n.mjs} +3 -3
  56. package/dist/{hosted-client-rsRqysJc.mjs → hosted-client-fHO8yqzc.mjs} +5 -5
  57. package/dist/{hosted-ios-RtVcJH3u.mjs → hosted-ios-D55QhYaD.mjs} +1 -1
  58. package/dist/{hosted-logs-CWKGYWNX.mjs → hosted-logs-BfbmDM4M.mjs} +17 -11
  59. package/dist/hosted-logs-sync-B0pvmOk-.mjs +195 -0
  60. package/dist/{hosted-macos-d0pv9Tth.mjs → hosted-macos-CPl0OZ9h.mjs} +5 -5
  61. package/dist/{hosted-native-BxTizGU2.mjs → hosted-native-CpDB3-PO.mjs} +6 -7
  62. package/dist/{idle-shutdown-BIyfl0Zj.mjs → idle-shutdown-FfxL1KdG.mjs} +22 -9
  63. package/dist/{ios-Dvt97HgQ.mjs → ios-B7-lToDu.mjs} +8 -8
  64. package/dist/{ios-BQzvJlzw.mjs → ios-WxpDJ5T1.mjs} +72 -5
  65. package/dist/{ios-device-CKSvk0bu.mjs → ios-device-CsyryvpT.mjs} +1 -1
  66. package/dist/{ios-BRVM8b0P.mjs → ios-ftxdQbJS.mjs} +74 -46
  67. package/dist/{ios-project-DCUDo06I.mjs → ios-project-Ty5pHl9O.mjs} +6 -6
  68. package/dist/{ios-state-CT9nV5SP.mjs → ios-state-Cw7-WQf2.mjs} +1 -1
  69. package/dist/{js-swap-Z9HbC4cj.mjs → js-swap-BMhXYVDS.mjs} +4 -4
  70. package/dist/{launch-D0lPvFFu.mjs → launch-DOp8RjDz.mjs} +31 -26
  71. package/dist/{logs-BsIRre8x.mjs → logs-dQDki6Tt.mjs} +9 -173
  72. package/dist/{logs-Crs9y34Z.mjs → logs-s1dRIG1E.mjs} +85 -36
  73. package/dist/{macos-CcUlJPMu.mjs → macos-B0VEV3Gf.mjs} +1 -1
  74. package/dist/{macos-D477nfzH.mjs → macos-DBBPNnIA.mjs} +18 -19
  75. package/dist/macos-run.mjs +1 -1
  76. package/dist/maintenance-run.mjs +28 -11
  77. package/dist/{metro-1pJgCX4U.mjs → metro-CjC10t6E.mjs} +2 -2
  78. package/dist/{metro-gateway-Bp9gJQAN.mjs → metro-gateway-B6_PgD0L.mjs} +2 -3
  79. package/dist/{named-ports-BVbn8SvC.mjs → named-ports-BG0j1X4g.mjs} +2 -2
  80. package/dist/{native-android-BvMizJn1.mjs → native-android-CL2NGiik.mjs} +17 -17
  81. package/dist/{native-gradle-artifact-inputs-BJscK4vN.mjs → native-gradle-artifact-inputs-btSUulme.mjs} +3 -3
  82. package/dist/{native-run-D5OJsGwA.mjs → native-run-BfIDFKLH.mjs} +2 -3
  83. package/dist/{native-runtime-B9w_Qyz3.mjs → native-runtime-Dq3RBcON.mjs} +6 -7
  84. package/dist/{native-xcode-inputs-Cv4l-obn.mjs → native-xcode-inputs-BW7H8JGu.mjs} +2 -2
  85. package/dist/{native-xcode-ios-DGrg90hE.mjs → native-xcode-ios-CBXJAxfC.mjs} +15 -16
  86. package/dist/offload-worker.d.mts +3 -1
  87. package/dist/offload-worker.mjs +31 -21
  88. package/dist/{ownership-jxxz-19m.mjs → ownership-BjYvMNX_.mjs} +2 -2
  89. package/dist/{phone-reverses-CYb7RRAx.mjs → phone-reverses-lmj6kcPm.mjs} +3 -3
  90. package/dist/{placement-log-DjOXJRIH.mjs → placement-log-DIFPQAXp.mjs} +1 -1
  91. package/dist/{plan-placement-BqKgKHCi.mjs → plan-placement-IiiSQhtz.mjs} +3 -3
  92. package/dist/{ports-DtS1Amb0.mjs → ports-C2_w1Z0Z.mjs} +3 -3
  93. package/dist/prebuild-gmouvmJZ.mjs +1247 -0
  94. package/dist/{project-BiC68mIp.mjs → project-BUu6Is4k.mjs} +15 -16
  95. package/dist/{react-native-android-DRYqoJc4.mjs → react-native-android-Cgg99REN.mjs} +47 -33
  96. package/dist/{react-native-build-CwZB-agu.mjs → react-native-build-oNwqF5Wb.mjs} +7 -7
  97. package/dist/{react-native-doctor-BCrytK7h.mjs → react-native-doctor-BhA3KUte.mjs} +49 -8
  98. package/dist/{react-native-ios-Cr1buNFo.mjs → react-native-ios-B-nf4rjN.mjs} +37 -23
  99. package/dist/{recordings-BaoQ0t-J.mjs → recordings-BldzVKq0.mjs} +1 -1
  100. package/dist/{reload-Beftw2X3.mjs → reload-C9OduiNc.mjs} +1 -1
  101. package/dist/{reload-FbSgMF8x.mjs → reload-CCR7KC_l.mjs} +14 -14
  102. package/dist/{remote-BuZj6AyU.mjs → remote-C8S7FlPF.mjs} +7 -7
  103. package/dist/{result-CR2vk2cj.mjs → result-7F5hEwQJ.mjs} +16 -16
  104. package/dist/{run-BtzE09Dl.mjs → run-BqB0v3pt.mjs} +1 -1
  105. package/dist/{selection-D5cxEl3t.mjs → selection-BNPBZ6T5.mjs} +155 -42
  106. package/dist/{server-bare-CH9X0RLn.mjs → server-bare-A75rDIdD.mjs} +1 -1
  107. package/dist/{server-command-CaBbDNU2.mjs → server-command-CssKFOD7.mjs} +4 -4
  108. package/dist/{server-expo-Ck8EIidM.mjs → server-expo-eQfOdydb.mjs} +2 -2
  109. package/dist/server-expo-wvcj5tiZ.mjs +2 -0
  110. package/dist/{settings-B5tCoq9N.mjs → settings-CosW5X-S.mjs} +1 -1
  111. package/dist/{settings-DzH9mGQZ.mjs → settings-OaGQF-ri.mjs} +1 -1
  112. package/dist/{settings-write-n0zwfohx.mjs → settings-write-QjYfSrjF.mjs} +5 -5
  113. package/dist/settings.schema.json +33 -6
  114. package/dist/{simslim-BjxszUAb.mjs → simslim-Bax1qcxu.mjs} +3 -2
  115. package/dist/{slot-launch-QeuJMlnJ.mjs → slot-launch-CYCEhfBY.mjs} +6 -6
  116. package/dist/{stage-D8xlRU2D.mjs → stage-jKQ7CP_B.mjs} +1 -1
  117. package/dist/{start-C1JLjkNw.mjs → start-C_wefNKB.mjs} +1 -1
  118. package/dist/{start-YMpNwLhh.mjs → start-_BdJ32cZ.mjs} +9 -10
  119. package/dist/{state-DMRNP7-4.mjs → state-C1i2nMOP.mjs} +2 -3
  120. package/dist/{state-DPaM3u5K.mjs → state-C4OdpOTF.mjs} +1 -2
  121. package/dist/{stats-CP7STzZw.mjs → stats-tEpEEH-u.mjs} +4 -4
  122. package/dist/{status-cNw6i0_t.mjs → status-B9M7x4CZ.mjs} +1 -1
  123. package/dist/{status-Fect2w9R.mjs → status-CRXz-Gtj.mjs} +1 -1
  124. package/dist/{status-CRLiJ-O_.mjs → status-DQrqodLj.mjs} +22 -23
  125. package/dist/{status-watch-Bc2J4TZn.mjs → status-watch-B5wDIH_o.mjs} +4 -4
  126. package/dist/status-worker.mjs +1 -1
  127. package/dist/{stim-desktop-DS0mzR8U.mjs → stim-desktop-6Dqp-gjX.mjs} +1 -1
  128. package/dist/{stop-Br7LKxBE.mjs → stop-CmdNhGlC.mjs} +2 -2
  129. package/dist/{stop-Db9Kh-UA.mjs → stop-DRqQVqDJ.mjs} +21 -19
  130. package/dist/{stop-COP6PQqw.mjs → stop-ySNQHXMG.mjs} +4 -5
  131. package/dist/supervisor-run.d.mts +1 -0
  132. package/dist/supervisor-run.mjs +14 -15
  133. package/dist/{support-Cxj9CmII.mjs → support-BGSCBoib.mjs} +4 -4
  134. package/dist/{support-qKcbgmI5.mjs → support-CVOCp0Ft.mjs} +9 -9
  135. package/dist/{swiftpm-artifact-inputs-iY9fsmOc.mjs → swiftpm-artifact-inputs-CfUzt_0Z.mjs} +1 -1
  136. package/dist/{swiftpm-macos-Bp_sn9P4.mjs → swiftpm-macos-x6HPpl_2.mjs} +9 -9
  137. package/dist/toolchain-DoavrvIA.mjs +2 -0
  138. package/dist/toolchain-J4J8MrAr.mjs +2107 -0
  139. package/dist/trigger-DzJu208Q.mjs +2 -0
  140. package/dist/{trigger-CBMe2YLH.mjs → trigger-KVaWWNyn.mjs} +1 -1
  141. package/dist/{warm-progress-BV3ZPmcY.mjs → warm-progress-DvhTX_eu.mjs} +3 -4
  142. package/dist/web-BFLueSh-.mjs +2 -0
  143. package/dist/{web-B4Yjyt1G.mjs → web-Bihbf7-u.mjs} +8 -9
  144. package/dist/web-run.mjs +4 -4
  145. package/dist/{workspace-state-CPSp3mav.mjs → workspace-state-pUg3_RRA.mjs} +45 -7
  146. package/dist/{workspaces-C9w4aeXF.mjs → workspaces-g2lhdBXF.mjs} +17 -17
  147. package/dist/{worktree-CclFIe1Z.mjs → worktree-BKuklFh_.mjs} +34 -31
  148. package/dist/{worktree-Ct9Gk3KL.mjs → worktree-Bm4cb-nU.mjs} +1 -1
  149. package/package.json +8 -8
  150. package/dist/created-devices-D2Aca-xu.mjs +0 -52
  151. package/dist/paths-DQtTgsBf.mjs +0 -42
  152. package/dist/prebuild-JUaqOLpc.mjs +0 -501
  153. package/dist/server-expo-C16Zf2k7.mjs +0 -2
  154. package/dist/toolchain-DB1aeqxO.mjs +0 -2
  155. package/dist/toolchain-VzLJzZ7O.mjs +0 -908
  156. package/dist/trigger-BLsHgAGR.mjs +0 -2
  157. package/dist/web-DS4UDWvf.mjs +0 -2
  158. package/dist/xcode-CiIIDnuD.mjs +0 -910
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { ambientEnv, configDir, ensurePrivateDir } from "@stim-cli/core";
5
5
  import { allMetroPorts, cannotHost, clientIdentity, configCorruptRepair, endpoint, findEnclosingWorktreeRoot, findPeer as findPeer$1, getConcurrencyLimits, getConfigPath, getConfigPath as getConfigPath$1, getProject, getProjectSettings, getRepoSettings, isJsonObject, isPathPrefix, loadConfig, loadConfig as loadConfig$1, machineEndpoint, parseMachine, parseMachine as parseMachine$1, validateAutomaticPool, withConfigLock, withConfigLock as withConfigLock$1, withConfigLockAsync, withConfigLockAsync as withConfigLockAsync$1 } from "@stim-cli/core/state";
6
6
  import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, normalize, parse, sep as sep$1 } from "path";
7
+ import { request } from "node:https";
7
8
  import { WebSocket } from "ws";
8
9
  import { HandshakeRefusal, MAX_SERVER_MESSAGE_BYTES, TRANSPORT_OVERHEAD_BYTES, decodeSecret, initiateNoise, newSecret } from "@stim-cli/core/noise-session";
9
10
  import { existsSync, lstatSync, readdirSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -23,19 +24,246 @@ function readHostPermissions(value) {
23
24
  } : void 0;
24
25
  }
25
26
  //#endregion
27
+ //#region src/offload/diagnosis.ts
28
+ const SILENT_ERRORS = /* @__PURE__ */ new Set([
29
+ "ETIMEDOUT",
30
+ "EHOSTUNREACH",
31
+ "ENETUNREACH",
32
+ "EHOSTDOWN"
33
+ ]);
34
+ function classifyTransport(error, status) {
35
+ if (status !== void 0) return {
36
+ kind: "http",
37
+ status
38
+ };
39
+ const code = error.code ?? "";
40
+ if (code === "ECONNREFUSED") return { kind: "refused" };
41
+ if (SILENT_ERRORS.has(code) || /handshake has timed out/i.test(error.message)) return { kind: "silent" };
42
+ if (/^(ERR_TLS_|CERT_|UNABLE_TO_|DEPTH_ZERO|SELF_SIGNED|ERR_SSL_)/.test(code) || /certificate|SSL|TLS/i.test(error.message)) return {
43
+ kind: "tls",
44
+ message: error.message
45
+ };
46
+ return {
47
+ kind: "other",
48
+ message: error.message
49
+ };
50
+ }
51
+ /** The `startup` a 503 `/health` body from a peer carries, when it is the shape stim-server sends. */
52
+ function parsePeerStartup(body) {
53
+ const startup = isJsonObject(body) && isJsonObject(body.startup) ? body.startup : null;
54
+ if (!startup || startup.state !== "pending" && startup.state !== "degraded") return null;
55
+ const cause = startup.cause === "home-unreadable" || startup.cause === "start-failed" ? startup.cause : void 0;
56
+ return {
57
+ state: startup.state,
58
+ ...cause ? { cause } : {}
59
+ };
60
+ }
61
+ function readTailscale(status) {
62
+ if (!isJsonObject(status)) return {
63
+ running: false,
64
+ code: "tailscale-unavailable"
65
+ };
66
+ switch (status.BackendState) {
67
+ case "Running": return { running: true };
68
+ case "NeedsLogin":
69
+ case "NeedsMachineAuth": return {
70
+ running: false,
71
+ code: "tailscale-logged-out"
72
+ };
73
+ default: return {
74
+ running: false,
75
+ code: "tailscale-stopped"
76
+ };
77
+ }
78
+ }
79
+ function tailscaleDiagnosis(code) {
80
+ switch (code) {
81
+ case "tailscale-logged-out": return {
82
+ layer: "tailscale",
83
+ code,
84
+ title: "Tailscale signed out",
85
+ message: "Tailscale on this Mac is signed out, so it cannot reach any remote machine.",
86
+ remedy: "Sign in to Tailscale on this Mac (its menu bar app, or `tailscale up`), then run `stim doctor` again."
87
+ };
88
+ case "tailscale-stopped": return {
89
+ layer: "tailscale",
90
+ code,
91
+ title: "Tailscale is off",
92
+ message: "Tailscale is turned off on this Mac, so it cannot reach any remote machine.",
93
+ remedy: "Turn on Tailscale on this Mac (its menu bar app, or `tailscale up`), then run `stim doctor` again."
94
+ };
95
+ case "tailscale-unavailable": return {
96
+ layer: "tailscale",
97
+ code,
98
+ title: "Tailscale is off",
99
+ message: "Tailscale is not running on this Mac or is not installed, so it cannot reach any remote machine.",
100
+ remedy: "Install Tailscale or open it on this Mac and sign in, then run `stim doctor` again."
101
+ };
102
+ }
103
+ }
104
+ function notOnTailnetDiagnosis(entry, ambiguous) {
105
+ return {
106
+ layer: "peer",
107
+ code: ambiguous ? "peer-ambiguous" : "peer-not-found",
108
+ title: ambiguous ? "Name is ambiguous" : "Not on the tailnet",
109
+ message: ambiguous ? `Several peers on this tailnet match ${entry}.` : `No peer on this tailnet is named ${entry}.`,
110
+ remedy: ambiguous ? `Use the full MagicDNS name in remote.machines (see \`tailscale status\`).` : `Check the name in \`tailscale status\`, or sign in to Tailscale on ${entry} with the same account.`
111
+ };
112
+ }
113
+ function ago(iso, now) {
114
+ const then = iso ? Date.parse(iso) : NaN;
115
+ if (!Number.isFinite(then) || then <= 0) return null;
116
+ const minutes = Math.max(0, Math.round((now - then) / 6e4));
117
+ if (minutes < 1) return "less than a minute ago";
118
+ if (minutes < 60) return `${minutes} min ago`;
119
+ const hours = Math.round(minutes / 60);
120
+ if (hours < 48) return `${hours} h ago`;
121
+ return `${Math.round(hours / 24)} days ago`;
122
+ }
123
+ const sentence = (text) => text.replace(/\s+$/, "").replace(/([^.!?])$/, "$1.");
124
+ /**
125
+ * Tailscale's `Online` lags a reachable peer, so `peer-offline` needs a silent connection; any other outcome,
126
+ * including one past the transport (no `transport`), shows the peer answered.
127
+ */
128
+ function diagnoseUnreachable(input) {
129
+ const { entry, peer, port, transport, now = Date.now() } = input;
130
+ const route = `https port ${port}`;
131
+ if (transport?.kind === "silent" && input.pinged === true) return {
132
+ layer: "server",
133
+ code: "server-no-answer",
134
+ title: "Server not answering",
135
+ message: `${entry} answers on Tailscale, but its serve route and stim-server sent no reply in time. stim-server may be starting, stalled or restarting.`,
136
+ remedy: `On ${entry}, run \`stim-server service status\`; it reports a restart loop or a Stim home it is still reading, and the log file to read.`
137
+ };
138
+ if (peer.online === false && transport?.kind === "silent") {
139
+ const seen = ago(peer.lastSeen, now);
140
+ return {
141
+ layer: "peer",
142
+ code: "peer-offline",
143
+ title: "Mac is offline",
144
+ message: `${entry} is offline on Tailscale${seen ? ` (last seen ${seen})` : ""}.`,
145
+ remedy: `Wake ${entry} or check that it is powered on, connected to a network and signed in to Tailscale, then run \`stim doctor\` again.`
146
+ };
147
+ }
148
+ if (transport?.kind === "refused") return {
149
+ layer: "route",
150
+ code: "route-missing",
151
+ title: "No serve route",
152
+ message: `${entry} answers on the tailnet, but nothing accepts connections on ${route}, so it has no tailscale serve route for stim-server.`,
153
+ remedy: `On ${entry}, run \`stim-server service status\`; \`stim-server service install --serve\` creates the route.`
154
+ };
155
+ if (transport?.kind === "silent") return {
156
+ layer: "peer",
157
+ code: "peer-no-answer",
158
+ title: "Mac not answering",
159
+ message: `${entry} did not answer the connection, and Tailscale does not list it as offline.`,
160
+ remedy: `Check that ${entry} is awake and its network is up; \`tailscale ping ${entry}\` on this Mac tests the path.`
161
+ };
162
+ if (transport?.kind === "tls") return {
163
+ layer: "route",
164
+ code: "route-tls",
165
+ title: "Route certificate",
166
+ message: `The tailscale serve route on ${entry} (${route}) did not present a certificate this Mac accepts: ${sentence(transport.message)}`,
167
+ remedy: `On ${entry}, run \`tailscale serve status\`, then \`stim-server service install --serve\` to recreate the route.`
168
+ };
169
+ if (transport?.kind === "http") {
170
+ const { status } = transport;
171
+ if (status === 502) return {
172
+ layer: "server",
173
+ code: "server-not-running",
174
+ title: "Server not running",
175
+ message: `Tailscale and the serve route on ${entry} work, but nothing listens behind the route: stim-server is not running there (HTTP 502).`,
176
+ remedy: `On ${entry}, run \`stim-server service status\`; \`stim-server service install --serve\` starts it and keeps it running.`
177
+ };
178
+ if (status === 503) {
179
+ const startup = input.startup;
180
+ const why = startup?.cause === "home-unreadable" ? " It cannot read its Stim home, usually because the disk holding ~/.stim is disconnected or not responding." : startup?.cause === "start-failed" ? " It failed while starting." : startup?.state === "pending" ? " It is still starting." : "";
181
+ const check = startup?.cause === "home-unreadable" ? `Check the disk that holds the Stim home on ${entry} (reconnect or wake an external drive); stim-server serves again by itself once it answers. \`stim-server service status\` there shows the exact reason.` : startup?.state === "pending" ? `Wait a few seconds and run \`stim doctor\` again.` : `On ${entry}, run \`stim-server service status\` for the reason; the log file it prints has the rest.`;
182
+ return {
183
+ layer: "server",
184
+ code: "server-not-ready",
185
+ title: "Server not ready",
186
+ message: `stim-server on ${entry} is running but is not serving clients (HTTP 503).${why}`,
187
+ remedy: check
188
+ };
189
+ }
190
+ return {
191
+ layer: "route",
192
+ code: "route-http-error",
193
+ title: `Route answers ${status}`,
194
+ message: `The tailscale serve route on ${entry} (${route}) answered HTTP ${status}, which stim-server does not send for a connection.`,
195
+ remedy: `On ${entry}, run \`tailscale serve status\` and check what the route on port ${port} points to.`
196
+ };
197
+ }
198
+ return {
199
+ layer: "server",
200
+ code: "connect-failed",
201
+ title: "Not answering",
202
+ message: `Could not connect to stim-server on ${entry}: ${sentence(input.reason)}`,
203
+ remedy: `On ${entry}, run \`stim-server service status\`. To keep it running there, run \`stim-server service install --serve\`.`
204
+ };
205
+ }
206
+ function diagnosisFinding(code, entry, diagnosis) {
207
+ return {
208
+ code,
209
+ level: "note",
210
+ title: `Remote machine ${entry}: ${diagnosis.title}`,
211
+ detail: diagnosis.message,
212
+ fix: diagnosis.remedy
213
+ };
214
+ }
215
+ //#endregion
26
216
  //#region src/offload/tailnet.ts
27
217
  const MAC_APP_TAILSCALE = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
28
218
  const HELLO_TIMEOUT_MS = 1e4;
219
+ const HEALTH_TIMEOUT_MS = 3e3;
29
220
  /** How long a setup ticket `stim doctor --fix` generates stays valid; `stim-server setup` takes at most 2 hours. */
30
221
  const GENERATED_TICKET_MS = 714e4;
31
- function parseTailscaleStatus(output) {
222
+ function parseJson(output) {
32
223
  try {
33
- const status = JSON.parse(output);
34
- return isJsonObject(status) && status.BackendState === "Running" ? status : null;
224
+ return JSON.parse(output);
35
225
  } catch {
36
226
  return null;
37
227
  }
38
228
  }
229
+ function parseTailscaleStatus(output) {
230
+ const status = parseJson(output);
231
+ return isJsonObject(status) && status.BackendState === "Running" ? status : null;
232
+ }
233
+ function rawTailscaleStatus() {
234
+ for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
235
+ const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
236
+ if (output !== null) return parseJson(output);
237
+ }
238
+ return null;
239
+ }
240
+ /** Reads the `startup` of a peer's `/health`, which stim-server answers with 503 while it is not serving clients. */
241
+ function peerStartup({ url, servername, host }) {
242
+ return new Promise((resolve) => {
243
+ const base = new URL(url);
244
+ const request$1 = request({
245
+ host: base.hostname.replace(/^\[|\]$/g, ""),
246
+ port: base.port || 443,
247
+ path: "/health",
248
+ servername,
249
+ headers: { Host: host },
250
+ timeout: HEALTH_TIMEOUT_MS
251
+ }, (response) => {
252
+ const chunks = [];
253
+ let size = 0;
254
+ response.on("data", (chunk) => {
255
+ size += chunk.length;
256
+ if (size > 4096) request$1.destroy();
257
+ else chunks.push(chunk);
258
+ });
259
+ response.on("end", () => resolve(parsePeerStartup(parseJson(Buffer.concat(chunks).toString("utf8")))));
260
+ response.on("error", () => resolve(null));
261
+ });
262
+ request$1.on("timeout", () => request$1.destroy());
263
+ request$1.on("error", () => resolve(null));
264
+ request$1.end();
265
+ });
266
+ }
39
267
  function tailscaleStatus() {
40
268
  for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
41
269
  const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
@@ -43,7 +271,7 @@ function tailscaleStatus() {
43
271
  }
44
272
  return null;
45
273
  }
46
- async function tailscaleStatusAsync(signal) {
274
+ async function tailscaleOutputAsync(signal) {
47
275
  for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
48
276
  signal?.throwIfAborted();
49
277
  let output;
@@ -57,10 +285,42 @@ async function tailscaleStatusAsync(signal) {
57
285
  continue;
58
286
  }
59
287
  signal?.throwIfAborted();
60
- return parseTailscaleStatus(output);
288
+ return output;
61
289
  }
62
290
  return null;
63
291
  }
292
+ async function tailscaleStatusAsync(signal) {
293
+ const output = await tailscaleOutputAsync(signal);
294
+ return output === null ? null : parseTailscaleStatus(output);
295
+ }
296
+ async function pingPeer(address, signal) {
297
+ for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
298
+ signal?.throwIfAborted();
299
+ try {
300
+ await getExecutor().runFileAsync(binary, [
301
+ "ping",
302
+ "--c",
303
+ "1",
304
+ "--until-direct=false",
305
+ "--timeout",
306
+ "4s",
307
+ address
308
+ ], {
309
+ timeoutMs: 6e3,
310
+ signal
311
+ });
312
+ return true;
313
+ } catch (error) {
314
+ signal?.throwIfAborted();
315
+ if (!/ENOENT|not found/i.test(String(error))) return false;
316
+ }
317
+ }
318
+ return false;
319
+ }
320
+ async function rawTailscaleStatusAsync(signal) {
321
+ const output = await tailscaleOutputAsync(signal);
322
+ return output === null ? null : parseJson(output);
323
+ }
64
324
  const TURNED_AWAY = /* @__PURE__ */ new Set([
65
325
  "unauthorized",
66
326
  "forbidden",
@@ -106,6 +366,7 @@ function openAuthenticated(target, auth, timeoutMs, signal, operation) {
106
366
  socket.removeAllListeners("open");
107
367
  socket.removeAllListeners("message");
108
368
  socket.removeAllListeners("error");
369
+ socket.removeAllListeners("unexpected-response");
109
370
  socket.on("error", () => {});
110
371
  socket.terminate();
111
372
  }
@@ -116,18 +377,33 @@ function openAuthenticated(target, auth, timeoutMs, signal, operation) {
116
377
  refused: false,
117
378
  code: "STIM_CANCELLED"
118
379
  });
380
+ let opened = false;
119
381
  const timer = setTimeout(() => finish({
120
382
  failure: "no reply in time",
121
- refused: false
383
+ refused: false,
384
+ transport: opened ? {
385
+ kind: "other",
386
+ message: "the handshake got no reply"
387
+ } : { kind: "silent" }
122
388
  }), timeoutMs);
123
389
  signal?.addEventListener("abort", cancel, { once: true });
124
390
  if (signal?.aborted) return cancel();
125
- const failed = (error) => finish({
126
- failure: /Unexpected server response: 502/.test(error.message) ? "no stim-server answers behind its tailscale serve route (HTTP 502)" : error.message,
127
- refused: false
391
+ const failed = (error, status) => finish({
392
+ failure: status === 502 ? "no stim-server answers behind its tailscale serve route (HTTP 502)" : status === 503 ? "stim-server answers behind its tailscale serve route but is not serving clients (HTTP 503)" : status !== void 0 ? `its tailscale serve route answered HTTP ${status}` : error.message,
393
+ refused: false,
394
+ transport: opened && status === void 0 ? {
395
+ kind: "other",
396
+ message: error.message
397
+ } : classifyTransport(error, status)
398
+ });
399
+ socket.once("unexpected-response", (request, response) => {
400
+ response.resume();
401
+ request.destroy();
402
+ failed(/* @__PURE__ */ new Error(`Unexpected server response: ${response.statusCode}`), response.statusCode);
128
403
  });
129
404
  socket.once("error", failed);
130
405
  socket.once("open", () => {
406
+ opened = true;
131
407
  const secret = "ticket" in auth ? decodeSecret(auth.ticket) : null;
132
408
  let keyPair;
133
409
  try {
@@ -215,7 +491,10 @@ async function hello(target, auth, operation) {
215
491
  if (!("send" in socket)) return socket.code ? { error: {
216
492
  code: socket.code,
217
493
  message: socket.failure
218
- } } : { failed: socket.failure };
494
+ } } : {
495
+ failed: socket.failure,
496
+ ...socket.transport ? { transport: socket.transport } : {}
497
+ };
219
498
  try {
220
499
  if ("onServerKey" in auth) await auth.onServerKey?.(socket.remoteKey.toString("base64url"));
221
500
  } catch (error) {
@@ -241,14 +520,39 @@ async function hello(target, auth, operation) {
241
520
  }
242
521
  const realIo = {
243
522
  status: tailscaleStatus,
244
- hello
523
+ hello,
524
+ rawStatus: rawTailscaleStatus,
525
+ startup: peerStartup,
526
+ ping: (address) => pingPeer(address)
245
527
  };
246
528
  function doctorIo(operation) {
247
529
  return {
248
530
  status: () => tailscaleStatusAsync(operation?.signal),
249
- hello: (target, auth) => hello(target, auth, operation)
531
+ hello: (target, auth) => hello(target, auth, operation),
532
+ rawStatus: () => rawTailscaleStatusAsync(operation?.signal),
533
+ startup: peerStartup,
534
+ ping: (address) => pingPeer(address, operation?.signal)
250
535
  };
251
536
  }
537
+ /**
538
+ * Names the layer an unreachable `entry` stopped at. A 503 reads the peer's `/health` for why the server is not
539
+ * serving; an older stim-server answers without it, and the diagnosis then says only that it is not ready.
540
+ */
541
+ async function diagnoseOutcome(io, input) {
542
+ const { target, ...rest } = input;
543
+ const startup = rest.transport?.kind === "http" && rest.transport.status === 503 ? await io.startup?.(target) : null;
544
+ const pinged = rest.transport?.kind === "silent" ? await io.ping?.(rest.peer.address) : void 0;
545
+ return diagnoseUnreachable({
546
+ ...rest,
547
+ startup,
548
+ pinged
549
+ });
550
+ }
551
+ /** Why `status` is null, as a diagnosis of the Tailscale layer; falls back to the daemon being unavailable. */
552
+ async function diagnoseTailscale(io) {
553
+ const reading = readTailscale(io.rawStatus ? await io.rawStatus() : null);
554
+ return tailscaleDiagnosis(reading.running ? "tailscale-unavailable" : reading.code);
555
+ }
252
556
  /** Where to reach a paired machine now, or why Stim cannot; the handshake then checks its pinned key. */
253
557
  async function resolveEndpoint(credential, status = () => tailscaleStatusAsync(signal), signal) {
254
558
  return machineEndpoint(credential.machine, parseMachine(credential.machine) ? await status() : null);
@@ -357,7 +661,8 @@ async function reachMachine(input) {
357
661
  return {
358
662
  state: "unreachable",
359
663
  credential: pending,
360
- reason: "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "it did not grant this access"
664
+ reason: "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "it did not grant this access",
665
+ ..."failed" in reply && reply.transport ? { transport: reply.transport } : {}
361
666
  };
362
667
  };
363
668
  if (!credential) return input.fix ? ask() : { state: "not-asked" };
@@ -389,7 +694,8 @@ async function reachMachine(input) {
389
694
  return {
390
695
  state: "unreachable",
391
696
  credential,
392
- reason: "failed" in reply ? reply.failed : reply.error.message
697
+ reason: "failed" in reply ? reply.failed : reply.error.message,
698
+ ..."failed" in reply && reply.transport ? { transport: reply.transport } : {}
393
699
  };
394
700
  }
395
701
  if (input.fix && given && given !== credential.ticket) return ask();
@@ -952,4 +1258,4 @@ function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
952
1258
  return result;
953
1259
  }
954
1260
  //#endregion
955
- export { outdatedServer as $, writeConfigSettingAsync as A, deviceSlotFileKey as B, saveConfig as C, withConfigLock$1 as D, upsertProject as E, isOnMountedVolume as F, projectDeviceSlots as G, deviceSlotPlatforms as H, listMountedVolumes as I, SERVER_OUTDATED as J, removeSlotDevice as K, measuredDirectorySizeAsync as L, directorySize as M, directorySizeAsync as N, withConfigLockAsync$1 as O, formatBytes as P, openAuthenticated as Q, volumeRootFor as R, removeProject as S, setSupervisor as T, parseDeviceSlotKey as U, deviceSlotKey as V, parseDeviceSlotOption as W, endpoint as X, doctorIo as Y, findPeer$1 as Z, loadConfig$1 as _, clearDevice as a, setupRemedy as at, releaseAndroidConsolePort as b, ensureConfig as c, getConfigDir as d, parseMachine$1 as et, getConfigPath$1 as f, isPathPrefix as g, getRepoSettings as h, clearAndroidBootPending as i, sayHello as it, acquireAvdClaim as j, writeConfigSetting as k, findEnclosingWorktreeRoot as l, getProjectSettings as m, allMetroPorts as n, realIo as nt, clearSupervisor as o, tailscaleStatusAsync as ot, getProject as p, validateDeviceSlot as q, claimMetroPort as r, resolveEndpoint as rt, configCorruptRepair as s, readHostPermissions as st, allConsolePortsAndSerials as t, reachMachine as tt, getConcurrencyLimits as u, recordIosSchemeApprovals as v, setDevice as w, releaseMetroPort as x, refuseRelativeStimPaths as y, assignSlotDevice as z };
1261
+ export { findPeer$1 as $, writeConfigSettingAsync as A, deviceSlotFileKey as B, saveConfig as C, withConfigLock$1 as D, upsertProject as E, isOnMountedVolume as F, projectDeviceSlots as G, deviceSlotPlatforms as H, listMountedVolumes as I, SERVER_OUTDATED as J, removeSlotDevice as K, measuredDirectorySizeAsync as L, directorySize as M, directorySizeAsync as N, withConfigLockAsync$1 as O, formatBytes as P, endpoint as Q, volumeRootFor as R, removeProject as S, setSupervisor as T, parseDeviceSlotKey as U, deviceSlotKey as V, parseDeviceSlotOption as W, diagnoseTailscale as X, diagnoseOutcome as Y, doctorIo as Z, loadConfig$1 as _, clearDevice as a, resolveEndpoint as at, releaseAndroidConsolePort as b, ensureConfig as c, tailscaleStatusAsync as ct, getConfigDir as d, readHostPermissions as dt, openAuthenticated as et, getConfigPath$1 as f, isPathPrefix as g, getRepoSettings as h, clearAndroidBootPending as i, realIo as it, acquireAvdClaim as j, writeConfigSetting as k, findEnclosingWorktreeRoot as l, diagnosisFinding as lt, getProjectSettings as m, allMetroPorts as n, parseMachine$1 as nt, clearSupervisor as o, sayHello as ot, getProject as p, validateDeviceSlot as q, claimMetroPort as r, reachMachine as rt, configCorruptRepair as s, setupRemedy as st, allConsolePortsAndSerials as t, outdatedServer as tt, getConcurrencyLimits as u, notOnTailnetDiagnosis as ut, recordIosSchemeApprovals as v, setDevice as w, releaseMetroPort as x, refuseRelativeStimPaths as y, assignSlotDevice as z };