harnery 0.42.0 → 0.44.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -4,8 +4,11 @@ import { resolve } from "node:path";
4
4
  import { resolveCoordRoot } from "../core/agents/coord-client.js";
5
5
  import { resolveBinName } from "../core/config.js";
6
6
  import { tryRegisterServer, tunnelServerId, tunnelServerInput, unregisterServer, } from "../core/servers/index.js";
7
- import { processLogDestination, runRotatingProcessSync, spawnRotatingProcess, } from "../core/storage/process-log.js";
7
+ import { processLogDestination, runProcessLogWorker, runRotatingProcessSync, spawnRotatingProcess, } from "../core/storage/process-log.js";
8
+ import { normalizeAllowEntry } from "../lib/tunnel/allowlist.js";
9
+ import { detectPublicAddresses, planCurrentAddresses, } from "../lib/tunnel/current-address.js";
8
10
  import { ALLOW_PATHS_ENV, normalizeAllowPaths } from "../lib/tunnel/path-scope.js";
11
+ import { connectProbe, findPidsByCommandLine, listeningPorts, portListening, } from "../lib/tunnel/processes.js";
9
12
  import { clearState, DEFAULT_INSTANCE, ensureCloudflared, gateLogFile, isProcessAlive, isTunnelStateLive, listStates, providerLogFile, readConfig, readState, writeConfig, writeState, } from "../lib/tunnel/state.js";
10
13
  /**
11
14
  * `tunnel`: provider-backed tunnel in front of a local upstream.
@@ -17,9 +20,11 @@ import { clearState, DEFAULT_INSTANCE, ensureCloudflared, gateLogFile, isProcess
17
20
  * Every provider also passes through a path scope: `up` requires at least one
18
21
  * `--allow-path <prefix>`, and the gate refuses any request outside it.
19
22
  *
20
- * State + config persisted under `.cache/tunnel/`. cloudflared auto-installs
21
- * to ~/.local/bin/ on first run (Linux only; macOS users `brew install`);
22
- * Tailscale requires an installed/authenticated `tailscale` CLI.
23
+ * State + config persisted under `.cache/tunnel/` (or `$HARNERY_TUNNEL_DIR`).
24
+ * cloudflared comes from `$HARNERY_CLOUDFLARED` / the config's `cloudflared_path`
25
+ * when set, else PATH, else auto-installs to ~/.local/bin/ on Linux (macOS and
26
+ * Windows hosts install it themselves); Tailscale requires an installed and
27
+ * authenticated `tailscale` CLI.
23
28
  */
24
29
  const DEFAULT_TARGET = "127.0.0.1:8001";
25
30
  const DEFAULT_VHOST = "localhost";
@@ -142,35 +147,87 @@ function collectPath(value, previous) {
142
147
  function gateScriptPath() {
143
148
  return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
144
149
  }
150
+ /** The gate worker's `--name`/`--port` marker, also what the stray sweep matches. */
151
+ const GATE_SUBCOMMAND = ["tunnel", "gate"];
145
152
  /**
146
- * `harn tunnel` is the one command that hard-requires Bun: the gate worker is a
147
- * `Bun.serve` process (HTTP + WebSocket reverse proxy), spawned as `bun run
148
- * gate.ts`. Everything else in harnery runs on Node, but this can't until the
149
- * gate is ported off `Bun.serve` (node:http + `ws`). Detect Bun up front so the
150
- * failure is a clear message rather than an opaque ENOENT from the gate spawn.
153
+ * How to start the gate worker, which is a `Bun.serve` process (HTTP and
154
+ * WebSocket reverse proxy):
155
+ * 1. From a source or package checkout, `gate.ts` sits beside this module and
156
+ * runs directly.
157
+ * 2. Inside a single-file bundle there is no such file. The CLI then
158
+ * re-executes itself with the hidden `tunnel gate` task, which loads the
159
+ * gate in-process. This uses the running Bun binary, so Bun need not be
160
+ * on PATH.
161
+ * Null means neither works (a Node host with no Bun, or an unknown entry).
151
162
  */
152
- function bunAvailable() {
153
- return spawnSync("bun", ["--version"], { stdio: "ignore" }).status === 0;
163
+ export function resolveGateLaunch(i) {
164
+ if (i.gateScriptExists && (i.underBun || i.bunOnPath)) {
165
+ return {
166
+ command: i.underBun ? i.execPath : "bun",
167
+ arguments: ["run", i.gateScript],
168
+ };
169
+ }
170
+ if (i.underBun && i.entryScript && i.entryScriptExists) {
171
+ return { command: i.execPath, arguments: [i.entryScript, ...GATE_SUBCOMMAND] };
172
+ }
173
+ return null;
174
+ }
175
+ /**
176
+ * How to start the log wrapper that rotates each process's output. Beside a
177
+ * source or package checkout the default (this package's own wrapper file)
178
+ * works; inside a single-file bundle the CLI re-executes itself with the hidden
179
+ * `tunnel log-worker` task instead.
180
+ */
181
+ export function resolveWorkerLaunch(i) {
182
+ if (i.gateScriptExists)
183
+ return undefined;
184
+ if (i.underBun && i.entryScript && i.entryScriptExists) {
185
+ return { command: i.execPath, arguments: [i.entryScript, "tunnel", "log-worker"] };
186
+ }
187
+ return undefined;
154
188
  }
189
+ function currentLaunchInputs() {
190
+ const gateScript = gateScriptPath();
191
+ const entryScript = process.argv[1] ? resolve(process.argv[1]) : undefined;
192
+ const underBun = typeof process.versions.bun === "string";
193
+ const gateScriptExists = existsSync(gateScript);
194
+ return {
195
+ gateScript,
196
+ gateScriptExists,
197
+ underBun,
198
+ execPath: process.execPath,
199
+ entryScript,
200
+ entryScriptExists: entryScript ? existsSync(entryScript) : false,
201
+ bunOnPath: !underBun && gateScriptExists && bunOnPath(),
202
+ };
203
+ }
204
+ function currentGateLaunch() {
205
+ return resolveGateLaunch(currentLaunchInputs());
206
+ }
207
+ function currentWorkerLaunch() {
208
+ return resolveWorkerLaunch(currentLaunchInputs());
209
+ }
210
+ function bunOnPath() {
211
+ return spawnSync("bun", ["--version"], { stdio: "ignore", windowsHide: true }).status === 0;
212
+ }
213
+ const requiresBunMessage = () => `${resolveBinName()} tunnel requires Bun: the gate worker is a Bun.serve process. ` +
214
+ "Install Bun (https://bun.sh) and re-run. (Every other command runs on Node.)";
155
215
  function tailscaleAvailable() {
156
- return spawnSync("tailscale", ["version"], { stdio: "ignore" }).status === 0;
216
+ return spawnSync("tailscale", ["version"], { stdio: "ignore", windowsHide: true }).status === 0;
157
217
  }
158
218
  function sleep(ms) {
159
219
  return new Promise((r) => setTimeout(r, ms));
160
220
  }
161
221
  /**
162
- * Kill every process whose command line matches `pattern` (via `pgrep -f`),
163
- * skipping our own PID and any already-killed. Returns the count killed. Used
164
- * as a fallback so orphaned gate/cloudflared processes get cleaned even when
165
- * the state file was lost (which otherwise left them squatting on the port).
222
+ * Kill every process whose command line matches `pattern` (an extended regular
223
+ * expression), skipping our own PID and any already-killed. Returns the count
224
+ * killed. Used as a fallback so orphaned gate/cloudflared processes get cleaned
225
+ * even when the state file was lost (which otherwise left them squatting on the
226
+ * port).
166
227
  */
167
228
  function killByPattern(pattern, alreadyKilled) {
168
- const r = spawnSync("pgrep", ["-f", pattern], { encoding: "utf-8" });
169
- if (r.status !== 0 || typeof r.stdout !== "string")
170
- return 0;
171
229
  let killed = 0;
172
- for (const line of r.stdout.split("\n")) {
173
- const pid = Number(line.trim());
230
+ for (const pid of findPidsByCommandLine(pattern)) {
174
231
  if (!pid || pid === process.pid || alreadyKilled.has(pid))
175
232
  continue;
176
233
  try {
@@ -184,58 +241,41 @@ function killByPattern(pattern, alreadyKilled) {
184
241
  }
185
242
  return killed;
186
243
  }
244
+ /**
245
+ * Command-line pattern for ONE instance's gate, identified by its port. Matches
246
+ * both launch forms: `bun run .../gate.ts --name N --port P` and a host CLI
247
+ * re-executing itself as `<cli> tunnel gate --name N --port P`. Port boundary is
248
+ * guarded with `( |$)` so port 9001 doesn't match 90011.
249
+ */
250
+ function gatePattern(gatePort) {
251
+ return `(gate\\.ts|tunnel gate) .*--port ${gatePort}( |$)`;
252
+ }
187
253
  /**
188
254
  * Sweep stray gate + cloudflared processes for ONE instance, identified by its
189
255
  * gate port. Both signatures are port-scoped so tearing down one tunnel never
190
256
  * touches another:
191
- * - gate: `gate.ts ... --port <port>` (the port is on the gate's argv)
257
+ * - gate: see gatePattern (the port is on the gate's argv)
192
258
  * - cloudflared: `--url http://localhost:<port>` (order-independent, so it
193
259
  * matches regardless of the `--protocol http2` flag we also pass).
194
- * Port boundary is guarded with `( |$)` so port 9001 doesn't match 90011.
195
260
  */
196
261
  function sweepStrays(gatePort, alreadyKilled) {
197
- return (killByPattern(`gate\\.ts.*--port ${gatePort}( |$)`, alreadyKilled) +
262
+ return (killByPattern(gatePattern(gatePort), alreadyKilled) +
198
263
  killByPattern(`--url http://localhost:${gatePort}( |$)`, alreadyKilled));
199
264
  }
200
- /**
201
- * Ports currently bound by a LISTEN socket (best-effort, platform-dependent).
202
- *
203
- * `ss` (iproute2) is Linux-only. Without the lsof fallback this returns an empty
204
- * set on macOS/BSD. That does not fail loudly. It reports every port as free, so
205
- * `allocateGatePort` can hand out an occupied port and the reload port-release
206
- * check becomes inert.
207
- */
208
- function listeningPorts() {
209
- const ports = new Set();
210
- const ss = spawnSync("ss", ["-tlnH"], { encoding: "utf-8" });
211
- if (ss.status === 0 && typeof ss.stdout === "string") {
212
- for (const m of ss.stdout.matchAll(/:(\d+)\s/g))
213
- ports.add(Number(m[1]));
214
- return ports;
215
- }
216
- // Rows look like: `bun 123 user 4u IPv4 0x… 0t0 TCP 127.0.0.1:58055 (LISTEN)`.
217
- // Status can be nonzero while stdout still holds usable rows (lsof reports a
218
- // failure when any single process is unreadable), so judge it on the output.
219
- const lsof = spawnSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], { encoding: "utf-8" });
220
- if (typeof lsof.stdout === "string") {
221
- for (const m of lsof.stdout.matchAll(/:(\d+) \(LISTEN\)$/gm))
222
- ports.add(Number(m[1]));
223
- }
224
- return ports;
225
- }
226
265
  /**
227
266
  * Pick a gate port for a new instance. An explicit `--gate-port` is honored
228
267
  * (and rejected if it's already taken); otherwise scan upward from 9001 for the
229
268
  * first port that's neither held by a live instance nor currently listening.
230
269
  */
231
- function allocateGatePort(preferred) {
270
+ async function allocateGatePort(preferred) {
232
271
  const used = new Set(listStates()
233
272
  .filter((s) => isProcessAlive(s.gate_pid))
234
273
  .map((s) => s.gate_port));
274
+ // No discovery tool (null) is not "every port free": probe candidates by connecting.
235
275
  const listening = listeningPorts();
236
- const taken = (p) => used.has(p) || listening.has(p);
276
+ const taken = async (p) => used.has(p) || (listening ? listening.has(p) : await connectProbe(p));
237
277
  if (preferred !== undefined) {
238
- if (taken(preferred)) {
278
+ if (await taken(preferred)) {
239
279
  emit.error({
240
280
  code: "tunnel_port_taken",
241
281
  message: `Gate port ${preferred} is already in use. Omit --gate-port to auto-allocate, or pick a free one.`,
@@ -245,7 +285,7 @@ function allocateGatePort(preferred) {
245
285
  return preferred;
246
286
  }
247
287
  for (let p = DEFAULT_GATE_PORT; p <= MAX_GATE_PORT; p++) {
248
- if (!taken(p))
288
+ if (!(await taken(p)))
249
289
  return p;
250
290
  }
251
291
  emit.error({
@@ -393,13 +433,14 @@ function tunnelIsAlive(state) {
393
433
  * is passed as an env var, which the gate snapshots at module load and never
394
434
  * re-reads — that snapshot is precisely why `reload` has to exist.
395
435
  */
396
- function spawnGate(o) {
436
+ function spawnGate(o, launch) {
397
437
  // `--name`/`--port` on argv mirror the env vars; they're what makes the gate
398
- // process distinguishable per-instance in `pgrep -f` (see sweepStrays).
438
+ // process distinguishable per-instance in a command-line search (see sweepStrays).
399
439
  const gateProc = spawnRotatingProcess({
400
440
  path: o.gateLogPath,
401
- command: "bun",
402
- arguments: ["run", gateScriptPath(), "--name", o.name, "--port", String(o.gatePort)],
441
+ command: launch.command,
442
+ arguments: [...launch.arguments, "--name", o.name, "--port", String(o.gatePort)],
443
+ worker: currentWorkerLaunch(),
403
444
  env: {
404
445
  ...process.env,
405
446
  HARNERY_TUNNEL_ALLOW: o.allowedIps.join(","),
@@ -414,12 +455,9 @@ function spawnGate(o) {
414
455
  return gateProc;
415
456
  }
416
457
  async function up(opts) {
417
- if (!bunAvailable()) {
418
- emit.error({
419
- code: "tunnel_requires_bun",
420
- message: "harn tunnel requires Bun: the gate worker is a Bun.serve process. " +
421
- "Install Bun (https://bun.sh) and re-run. (Every other harn command runs on Node.)",
422
- });
458
+ const launch = currentGateLaunch();
459
+ if (!launch) {
460
+ emit.error({ code: "tunnel_requires_bun", message: requiresBunMessage() });
423
461
  process.exit(1);
424
462
  }
425
463
  const name = resolveName(opts.name);
@@ -459,7 +497,7 @@ async function up(opts) {
459
497
  downOne(name, new Set());
460
498
  // Allocate the gate port (after clearing dead state so its old port frees up
461
499
  // for reuse). Explicit --gate-port is validated; otherwise auto-scan.
462
- const gatePort = allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
500
+ const gatePort = await allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
463
501
  // Self-heal: clear any orphaned gate/cloudflared on THIS instance's port from
464
502
  // a prior crashed or state-cleared run so the gate port is free before we bind.
465
503
  if (sweepStrays(gatePort, new Set()))
@@ -468,11 +506,23 @@ async function up(opts) {
468
506
  if (provider === "cloudflare" && cfg.allowed_ips.length === 0) {
469
507
  emit.error({
470
508
  code: "tunnel_allowlist_empty",
471
- message: "Allowlist is empty; refusing to start. Add an IP first: harn tunnel allow add <ip>",
509
+ message: `Allowlist is empty; refusing to start. Add an IP first: ${resolveBinName()} tunnel allow add <ip> (or --current)`,
472
510
  });
473
511
  process.exit(1);
474
512
  }
475
- const cloudflaredBin = provider === "cloudflare" ? ensureCloudflared() : null;
513
+ let cloudflaredBin = null;
514
+ if (provider === "cloudflare") {
515
+ try {
516
+ cloudflaredBin = ensureCloudflared();
517
+ }
518
+ catch (error) {
519
+ emit.error({
520
+ code: "tunnel_cloudflared_missing",
521
+ message: error instanceof Error ? error.message : String(error),
522
+ });
523
+ process.exit(1);
524
+ }
525
+ }
476
526
  if (provider === "tailscale" && !tailscaleAvailable()) {
477
527
  emit.error({
478
528
  code: "tunnel_tailscale_missing",
@@ -492,9 +542,18 @@ async function up(opts) {
492
542
  allowedIps: cfg.allowed_ips,
493
543
  allowPaths,
494
544
  gateLogPath,
495
- });
545
+ }, launch);
496
546
  await sleep(800);
497
- if (!isProcessAlive(gateProc.pid)) {
547
+ // A host CLI that re-executes itself takes longer to load than a bare script,
548
+ // so wait for the gate to actually listen before the provider forwards to it.
549
+ if (!isProcessAlive(gateProc.pid) || !(await waitForPortBound(gatePort, 15_000))) {
550
+ try {
551
+ process.kill(gateProc.pid);
552
+ }
553
+ catch {
554
+ /* already dead */
555
+ }
556
+ sweepStrays(gatePort, new Set());
498
557
  emit.error({
499
558
  code: "tunnel_gate_failed",
500
559
  message: `Gate failed to start. Check log: ${gateLogPath}`,
@@ -514,6 +573,7 @@ async function up(opts) {
514
573
  path: providerLogPath,
515
574
  command: cloudflaredBin,
516
575
  arguments: ["tunnel", "--protocol", "http2", "--url", `http://localhost:${gatePort}`],
576
+ worker: currentWorkerLaunch(),
517
577
  });
518
578
  cfdProc.unref();
519
579
  cloudflaredPid = cfdProc.pid;
@@ -567,7 +627,8 @@ async function up(opts) {
567
627
  };
568
628
  writeState(state);
569
629
  recordTunnelServer(state);
570
- const stopHint = name === DEFAULT_INSTANCE ? "harn tunnel down" : `harn tunnel down --name ${name}`;
630
+ const bin = resolveBinName();
631
+ const stopHint = name === DEFAULT_INSTANCE ? `${bin} tunnel down` : `${bin} tunnel down --name ${name}`;
571
632
  emit.text(`\n Instance: ${name}\n`);
572
633
  emit.text(` Provider: ${provider}${tailscaleMode ? ` (${tailscaleMode})` : ""}\n`);
573
634
  emit.text(` URL: ${url}\n\n`);
@@ -584,10 +645,10 @@ async function up(opts) {
584
645
  : " Visibility: tailnet only via Tailscale Serve\n\n");
585
646
  }
586
647
  if (!registered) {
587
- emit.text(` ⚠ Edge connection didn't register within 30s (QUIC can wedge on a cold\n start). If the URL 404s or times out, bounce it: ${stopHint} && harn tunnel up\n\n`);
648
+ emit.text(` ⚠ Edge connection didn't register within 30s (QUIC can wedge on a cold\n start). If the URL 404s or times out, bounce it: ${stopHint} && ${bin} tunnel up\n\n`);
588
649
  }
589
650
  emit.text(` Stop: ${stopHint}\n`);
590
- emit.text(" Status: harn tunnel status\n");
651
+ emit.text(` Status: ${bin} tunnel status\n`);
591
652
  }
592
653
  /** The server registry lists tunnels beside every other local server. */
593
654
  function registryRoot() {
@@ -598,7 +659,10 @@ function recordTunnelServer(state) {
598
659
  tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot });
599
660
  }
600
661
  /** Tear down a single instance by name. Returns the number of processes killed. */
601
- function downOne(name, killed) {
662
+ function portHeldByOtherLiveInstance(port, name) {
663
+ return listStates().some((s) => s.name !== name && s.gate_port === port && isProcessAlive(s.gate_pid));
664
+ }
665
+ export function downOne(name, killed) {
602
666
  const before = killed.size;
603
667
  const state = readState(name);
604
668
  if (state) {
@@ -619,8 +683,12 @@ function downOne(name, killed) {
619
683
  }
620
684
  }
621
685
  // Fallback: sweep orphans on this instance's gate port, even when state was
622
- // lost; they'd otherwise squat on the port and break the next `up`.
623
- sweepStrays(state?.gate_port ?? DEFAULT_GATE_PORT, killed);
686
+ // lost; they'd otherwise squat on the port and break the next `up`. Skip it
687
+ // when another live instance now owns that port: a stale record's port is
688
+ // reused after a reboot, and sweeping it would kill the live tunnel there.
689
+ const port = state?.gate_port ?? DEFAULT_GATE_PORT;
690
+ if (!portHeldByOtherLiveInstance(port, name))
691
+ sweepStrays(port, killed);
624
692
  clearState(name);
625
693
  unregisterServer(tunnelServerId(name), { coordRoot: registryRoot() });
626
694
  return killed.size - before;
@@ -644,7 +712,7 @@ function down(opts) {
644
712
  if (name === DEFAULT_INSTANCE && !readState(DEFAULT_INSTANCE)) {
645
713
  const others = listStates();
646
714
  if (others.length > 0) {
647
- emit.text(`No default tunnel running. Other tunnels up: ${others.map((s) => s.name).join(", ")}.\nUse \`harn tunnel down --name <name>\` or \`harn tunnel down --all\`.\n`);
715
+ emit.text(`No default tunnel running. Other tunnels up: ${others.map((s) => s.name).join(", ")}.\nUse \`${resolveBinName()} tunnel down --name <name>\` or \`${resolveBinName()} tunnel down --all\`.\n`);
648
716
  return;
649
717
  }
650
718
  }
@@ -657,21 +725,21 @@ function down(opts) {
657
725
  async function waitForPortFree(port, timeoutMs) {
658
726
  const deadline = Date.now() + timeoutMs;
659
727
  while (Date.now() < deadline) {
660
- if (!listeningPorts().has(port))
728
+ if (!(await portListening(port)))
661
729
  return true;
662
730
  await sleep(100);
663
731
  }
664
- return !listeningPorts().has(port);
732
+ return !(await portListening(port));
665
733
  }
666
734
  /** Poll until `port` has a LISTEN socket (or the deadline passes). */
667
735
  async function waitForPortBound(port, timeoutMs) {
668
736
  const deadline = Date.now() + timeoutMs;
669
737
  while (Date.now() < deadline) {
670
- if (listeningPorts().has(port))
738
+ if (await portListening(port))
671
739
  return true;
672
740
  await sleep(100);
673
741
  }
674
- return listeningPorts().has(port);
742
+ return portListening(port);
675
743
  }
676
744
  /**
677
745
  * Restart ONE instance's gate in place, deliberately leaving the provider
@@ -695,8 +763,12 @@ export async function reloadOne(state) {
695
763
  `Reloading the gate can't bring it back — run \`${bin} tunnel up --name ${name}\`.`,
696
764
  };
697
765
  }
698
- if (!bunAvailable()) {
699
- return { ok: false, message: `[${name}] bun is not on PATH, so the gate can't be respawned.` };
766
+ const launch = currentGateLaunch();
767
+ if (!launch) {
768
+ return {
769
+ ok: false,
770
+ message: `[${name}] Bun is not available, so the gate can't be respawned.`,
771
+ };
700
772
  }
701
773
  if (state.allow_paths.length === 0) {
702
774
  return {
@@ -719,7 +791,7 @@ export async function reloadOne(state) {
719
791
  /* race: already gone */
720
792
  }
721
793
  }
722
- killByPattern(`gate\\.ts.*--port ${gatePort}( |$)`, killed);
794
+ killByPattern(gatePattern(gatePort), killed);
723
795
  if (!(await waitForPortFree(gatePort, 5_000))) {
724
796
  return {
725
797
  ok: false,
@@ -737,12 +809,12 @@ export async function reloadOne(state) {
737
809
  allowedIps: cfg.allowed_ips,
738
810
  allowPaths: state.allow_paths,
739
811
  gateLogPath: tunnelLogDestinations(name, state.provider).gate,
740
- });
812
+ }, launch);
741
813
  await sleep(800);
742
814
  const pid = gateProc.pid;
743
815
  if (typeof pid !== "number" ||
744
816
  !isProcessAlive(pid) ||
745
- !(await waitForPortBound(gatePort, 5_000))) {
817
+ !(await waitForPortBound(gatePort, 15_000))) {
746
818
  return {
747
819
  ok: false,
748
820
  message: `[${name}] the gate did not come back on port ${gatePort}. ` +
@@ -902,9 +974,13 @@ function allowList() {
902
974
  emit.text("(empty)\n");
903
975
  return;
904
976
  }
905
- for (const ip of cfg.allowed_ips)
906
- emit.text(`${ip}\n`);
977
+ const automatic = cfg.auto_allowed ?? [];
978
+ for (const ip of cfg.allowed_ips) {
979
+ emit.text(`${ip}${sameEntryIn(automatic, ip) ? " (automatic)" : ""}\n`);
980
+ }
907
981
  }
982
+ const canonicalEntry = (entry) => normalizeAllowEntry(entry) ?? entry;
983
+ const sameEntryIn = (list, entry) => list.some((e) => canonicalEntry(e) === canonicalEntry(entry));
908
984
  /**
909
985
  * Trailer for `allow add`/`allow rm`. Each gate snapshots the allowlist at
910
986
  * spawn, so a config edit alone changes nothing until the gates restart.
@@ -920,29 +996,122 @@ function allowChangedHint() {
920
996
  `(${live.map((s) => s.name).join(", ")}):\n` +
921
997
  ` ${resolveBinName()} tunnel reload --all\n`);
922
998
  }
923
- function allowAdd(ip) {
999
+ async function allowAdd(entry, opts = {}) {
1000
+ if (opts.current) {
1001
+ if (entry) {
1002
+ emit.error({
1003
+ code: "tunnel_allow_current_with_entry",
1004
+ message: "Pass either an address or --current, not both.",
1005
+ });
1006
+ process.exit(1);
1007
+ }
1008
+ const result = await refreshCurrentAddress({ emit, reload: true });
1009
+ if (!result.ok)
1010
+ process.exit(1);
1011
+ return;
1012
+ }
1013
+ if (!entry) {
1014
+ emit.error({
1015
+ code: "tunnel_allow_entry_missing",
1016
+ message: `Name an address or range to allow, or use --current: ${resolveBinName()} tunnel allow add <ip|cidr>`,
1017
+ });
1018
+ process.exit(1);
1019
+ }
1020
+ const canonical = normalizeAllowEntry(entry);
1021
+ if (!canonical) {
1022
+ emit.error({
1023
+ code: "tunnel_allow_entry_invalid",
1024
+ message: `"${entry}" is not an IP address or CIDR range. ` +
1025
+ "Examples: 203.0.113.8, 203.0.113.0/24, 2601:db8:1:2::/64.",
1026
+ });
1027
+ process.exit(1);
1028
+ }
924
1029
  const cfg = readConfig();
925
- if (cfg.allowed_ips.includes(ip)) {
926
- emit.text(`${ip} already in allowlist.\n`);
1030
+ if (sameEntryIn(cfg.allowed_ips, canonical)) {
1031
+ // Adding by hand an entry the refresh owns adopts it: later refreshes keep it.
1032
+ if (cfg.auto_allowed && sameEntryIn(cfg.auto_allowed, canonical)) {
1033
+ cfg.auto_allowed = cfg.auto_allowed.filter((e) => canonicalEntry(e) !== canonical);
1034
+ writeConfig(cfg);
1035
+ emit.text(`${canonical} is now a manual entry; automatic refreshes will keep it.\n`);
1036
+ return;
1037
+ }
1038
+ emit.text(`${canonical} already in allowlist.\n`);
927
1039
  return;
928
1040
  }
929
- cfg.allowed_ips.push(ip);
1041
+ cfg.allowed_ips.push(canonical);
930
1042
  writeConfig(cfg);
931
- emit.text(`Added ${ip}.\n`);
1043
+ emit.text(`Added ${canonical}.\n`);
932
1044
  allowChangedHint();
933
1045
  }
934
- function allowRm(ip) {
1046
+ function allowRm(entry) {
935
1047
  const cfg = readConfig();
936
- const idx = cfg.allowed_ips.indexOf(ip);
1048
+ const wanted = canonicalEntry(entry);
1049
+ const idx = cfg.allowed_ips.findIndex((e) => e === entry || canonicalEntry(e) === wanted);
937
1050
  if (idx === -1) {
938
- emit.text(`${ip} not in allowlist.\n`);
1051
+ emit.text(`${entry} not in allowlist.\n`);
939
1052
  return;
940
1053
  }
941
- cfg.allowed_ips.splice(idx, 1);
1054
+ const [removed] = cfg.allowed_ips.splice(idx, 1);
1055
+ if (cfg.auto_allowed) {
1056
+ cfg.auto_allowed = cfg.auto_allowed.filter((e) => canonicalEntry(e) !== canonicalEntry(removed));
1057
+ }
942
1058
  writeConfig(cfg);
943
- emit.text(`Removed ${ip}.\n`);
1059
+ emit.text(`Removed ${removed}.\n`);
944
1060
  allowChangedHint();
945
1061
  }
1062
+ /**
1063
+ * `tunnel allow add --current`: allow this machine's own public addresses, as
1064
+ * Cloudflare sees them, and drop the ones allowed by the previous refresh.
1065
+ * Exported so a host CLI can run it before `tunnel up` and from its own sync.
1066
+ */
1067
+ export async function refreshCurrentAddress(options) {
1068
+ if (options.emit)
1069
+ emit = options.emit;
1070
+ const detected = await (options.detect ?? detectPublicAddresses)(options.detectOptions);
1071
+ const cfg = readConfig();
1072
+ const plan = planCurrentAddresses(cfg, detected);
1073
+ const noAddress = !detected.v4 && !detected.v6;
1074
+ // Quiet runs (a host's sync) speak only when something changed or nothing
1075
+ // could be detected; "no IPv6 on this network" is not news every time.
1076
+ if (!options.quiet || noAddress || plan.changed) {
1077
+ for (const warning of plan.warnings)
1078
+ emit.text(` ⚠ ${warning}\n`);
1079
+ }
1080
+ const result = {
1081
+ ok: true,
1082
+ changed: plan.changed,
1083
+ added: plan.added,
1084
+ removed: plan.removed,
1085
+ warnings: plan.warnings,
1086
+ };
1087
+ if (noAddress)
1088
+ return result;
1089
+ writeConfig(plan.config);
1090
+ if (!options.quiet || plan.changed) {
1091
+ const found = [detected.v4, detected.v6].filter(Boolean).join(", ");
1092
+ emit.text(`This machine reaches Cloudflare as ${found}.\n`);
1093
+ emit.text(plan.changed
1094
+ ? `Allowlist updated${plan.added.length ? `: added ${plan.added.join(", ")}` : ""}${plan.removed.length ? `; removed ${plan.removed.join(", ")}` : ""}.\n`
1095
+ : "Allowlist already covers this machine.\n");
1096
+ }
1097
+ if (!plan.changed || !options.reload)
1098
+ return result;
1099
+ const live = listStates().filter((s) => s.provider === "cloudflare" && providerIsAlive(s));
1100
+ for (const s of live) {
1101
+ const r = await reloadOne(s);
1102
+ emit.text(`${r.ok ? "ok " : "FAIL"} ${r.message}\n`);
1103
+ if (!r.ok)
1104
+ result.ok = false;
1105
+ }
1106
+ return result;
1107
+ }
1108
+ /** Hidden task: run the gate worker in this process (the self-launch form of the gate). */
1109
+ async function runGateInProcess() {
1110
+ await import("../lib/tunnel/gate.js");
1111
+ // The gate's server keeps the process alive. Never resolve, so a host's
1112
+ // post-command exit or output finalization cannot end the worker.
1113
+ await new Promise(() => { });
1114
+ }
946
1115
  let emit;
947
1116
  let context;
948
1117
  export function registerTunnelCommand(program, emitParam, contextParam) {
@@ -994,9 +1163,29 @@ export function registerTunnelCommand(program, emitParam, contextParam) {
994
1163
  .action(logs);
995
1164
  const allow = cmd
996
1165
  .command("allow")
997
- .description("Manage the Cloudflare CF-Connecting-IP allowlist")
1166
+ .description("Manage the Cloudflare CF-Connecting-IP allowlist (addresses and CIDR ranges, IPv4 and IPv6)")
998
1167
  .action(allowList);
999
- allow.command("add <ip>").description("Add an IP to the allowlist").action(allowAdd);
1000
- allow.command("rm <ip>").description("Remove an IP from the allowlist").action(allowRm);
1001
- allow.command("list").description("List allowed IPs (default action)").action(allowList);
1168
+ allow
1169
+ .command("add [entry]")
1170
+ .description("Add an IP address or CIDR range (203.0.113.0/24, 2601:db8:1:2::/64), or --current for this machine")
1171
+ .option("--current", "allow this machine's public addresses as Cloudflare sees them (IPv4 /32 and IPv6 /64), " +
1172
+ "replace the ones the previous --current added, and reload running gates")
1173
+ .action(allowAdd);
1174
+ allow
1175
+ .command("rm <entry>")
1176
+ .description("Remove an address or range from the allowlist")
1177
+ .action(allowRm);
1178
+ allow.command("list").description("List allowed entries (default action)").action(allowList);
1179
+ cmd
1180
+ .command("log-worker <specification>", { hidden: true })
1181
+ .description("Internal: run the output-rotating wrapper for one tunnel process")
1182
+ .action(async (specification) => {
1183
+ await runProcessLogWorker(specification);
1184
+ });
1185
+ cmd
1186
+ .command("gate", { hidden: true })
1187
+ .description("Internal: run the gate worker in this process")
1188
+ .option("--name <name>", "instance name")
1189
+ .option("--port <port>", "port to listen on")
1190
+ .action(runGateInProcess);
1002
1191
  }
@@ -1,10 +1,11 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import type { EmitContext } from "../commander.js";
2
3
  /** bun if available (preferred), else npm. Both support `<runner> install` and `<runner> run <script>`. */
3
- export declare function webRunner(): "bun" | "npm";
4
+ export declare function webRunner(spawn?: typeof spawnSync): "bun" | "npm";
4
5
  /**
5
6
  * Ensure a cached harnery checkout's `web/` exists for this version and return
6
7
  * its path, fetching (clone + install) on first use. Returns null on failure
7
8
  * (after emitting an actionable error).
8
9
  */
9
- export declare function lazyFetchWebRoot(emit: EmitContext): string | null;
10
+ export declare function lazyFetchWebRoot(emit: EmitContext, spawn?: typeof spawnSync): string | null;
10
11
  //# sourceMappingURL=web-fetch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../../src/commands/web-fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAiDnD,2GAA2G;AAC3G,wBAAgB,SAAS,IAAI,KAAK,GAAG,KAAK,CAGzC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAmDjE"}
1
+ {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../../src/commands/web-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAK/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAmDnD,2GAA2G;AAC3G,wBAAgB,SAAS,CAAC,KAAK,GAAE,OAAO,SAAqB,GAAG,KAAK,GAAG,KAAK,CAG5E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EACjB,KAAK,GAAE,OAAO,SAAqB,GAClC,MAAM,GAAG,IAAI,CAyDf"}