harnery 0.41.0 → 0.43.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 (269) hide show
  1. package/dist/commander.d.ts +16 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +21 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +63 -5
  6. package/dist/commands/artifacts.d.ts.map +1 -1
  7. package/dist/commands/artifacts.js +113 -12
  8. package/dist/commands/browse.d.ts +164 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +263 -51
  11. package/dist/commands/claude-desktop.d.ts.map +1 -1
  12. package/dist/commands/claude-desktop.js +52 -0
  13. package/dist/commands/disk.d.ts +6 -0
  14. package/dist/commands/disk.d.ts.map +1 -0
  15. package/dist/commands/disk.js +80 -0
  16. package/dist/commands/doctor.d.ts +8 -0
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +48 -1
  19. package/dist/commands/files.d.ts +16 -0
  20. package/dist/commands/files.d.ts.map +1 -1
  21. package/dist/commands/files.js +54 -2
  22. package/dist/commands/init.d.ts +2 -2
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +43 -9
  25. package/dist/commands/rm.d.ts +4 -0
  26. package/dist/commands/rm.d.ts.map +1 -0
  27. package/dist/commands/rm.js +87 -0
  28. package/dist/commands/servers.d.ts +7 -0
  29. package/dist/commands/servers.d.ts.map +1 -0
  30. package/dist/commands/servers.js +394 -0
  31. package/dist/commands/tunnel.d.ts +65 -0
  32. package/dist/commands/tunnel.d.ts.map +1 -1
  33. package/dist/commands/tunnel.js +372 -103
  34. package/dist/commands/web-fetch.d.ts +3 -2
  35. package/dist/commands/web-fetch.d.ts.map +1 -1
  36. package/dist/commands/web-fetch.js +25 -19
  37. package/dist/commands/web.d.ts +7 -1
  38. package/dist/commands/web.d.ts.map +1 -1
  39. package/dist/commands/web.js +9 -3
  40. package/dist/core/adapters/profiles.d.ts +9 -0
  41. package/dist/core/adapters/profiles.d.ts.map +1 -1
  42. package/dist/core/adapters/profiles.js +22 -1
  43. package/dist/core/adapters/types.d.ts +9 -0
  44. package/dist/core/adapters/types.d.ts.map +1 -1
  45. package/dist/core/agents/cli.js +13 -6
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +11 -5
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +59 -13
  50. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  51. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  52. package/dist/core/agents/state/live-coordination-view.js +60 -0
  53. package/dist/core/agents/state/names.d.ts +43 -3
  54. package/dist/core/agents/state/names.d.ts.map +1 -1
  55. package/dist/core/agents/state/names.js +87 -3
  56. package/dist/core/artifacts/delivery-card.js +9 -2
  57. package/dist/core/artifacts/index.d.ts +140 -4
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +522 -49
  60. package/dist/core/config.d.ts +68 -11
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +128 -3
  63. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  64. package/dist/core/diagnostics/bundle.js +2 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  66. package/dist/core/events/v3/producers/recorder.js +16 -0
  67. package/dist/core/events/v3/projection.js +9 -1
  68. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  69. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  70. package/dist/core/hooks/adapter/events.d.ts +17 -0
  71. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  72. package/dist/core/hooks/adapter/events.js +5 -0
  73. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  74. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +62 -4
  77. package/dist/core/hooks/cli.js +131 -23
  78. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  79. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  80. package/dist/core/hooks/fork-lineage.js +62 -0
  81. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  82. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  83. package/dist/core/hooks/resolve/transcript.js +84 -0
  84. package/dist/core/hooks/run-markers.d.ts +37 -0
  85. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  86. package/dist/core/hooks/run-markers.js +109 -0
  87. package/dist/core/hooks/shell-rm.d.ts +2 -0
  88. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  89. package/dist/core/hooks/shell-rm.js +105 -0
  90. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  91. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  92. package/dist/core/hooks/shell-tokens.js +97 -0
  93. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  94. package/dist/core/hooks/shell-waiter.js +3 -93
  95. package/dist/core/semantic/contract.d.ts +5 -5
  96. package/dist/core/servers/index.d.ts +211 -0
  97. package/dist/core/servers/index.d.ts.map +1 -0
  98. package/dist/core/servers/index.js +560 -0
  99. package/dist/core/servers/net.d.ts +58 -0
  100. package/dist/core/servers/net.d.ts.map +1 -0
  101. package/dist/core/servers/net.js +270 -0
  102. package/dist/core/servers/tunnels.d.ts +14 -0
  103. package/dist/core/servers/tunnels.d.ts.map +1 -0
  104. package/dist/core/servers/tunnels.js +33 -0
  105. package/dist/core/storage/builtins.js +1 -0
  106. package/dist/core/storage/process-log.d.ts +12 -0
  107. package/dist/core/storage/process-log.d.ts.map +1 -1
  108. package/dist/core/storage/process-log.js +10 -2
  109. package/dist/core/workflow/engine.d.ts.map +1 -1
  110. package/dist/core/workflow/engine.js +30 -1
  111. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  112. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  113. package/dist/core/workflow/sandbox-projection.js +23 -0
  114. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  115. package/dist/core/workflow/spawn-claude.js +6 -1
  116. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  117. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-codex.js +20 -1
  119. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-cursor.js +6 -1
  121. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  122. package/dist/core/workflow/spawn-opencode.js +4 -1
  123. package/dist/core/workflow/types.d.ts +47 -2
  124. package/dist/core/workflow/types.d.ts.map +1 -1
  125. package/dist/core/workflow/worker-access.d.ts +92 -0
  126. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  127. package/dist/core/workflow/worker-access.js +450 -0
  128. package/dist/lib/browser/batch-steps.d.ts +18 -0
  129. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  130. package/dist/lib/browser/batch-steps.js +82 -0
  131. package/dist/lib/browser/collect/collector.d.ts +99 -0
  132. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  133. package/dist/lib/browser/collect/collector.js +202 -0
  134. package/dist/lib/browser/collect/fields.d.ts +31 -0
  135. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  136. package/dist/lib/browser/collect/fields.js +75 -0
  137. package/dist/lib/browser/collect/index.d.ts +6 -0
  138. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  139. package/dist/lib/browser/collect/index.js +5 -0
  140. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  141. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  142. package/dist/lib/browser/collect/page-driver.js +306 -0
  143. package/dist/lib/browser/collect/presets.d.ts +52 -0
  144. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  145. package/dist/lib/browser/collect/presets.js +176 -0
  146. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  147. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  148. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  149. package/dist/lib/browser/index.d.ts +1 -1
  150. package/dist/lib/browser/index.d.ts.map +1 -1
  151. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  152. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  153. package/dist/lib/browser/page-review-judge.js +13 -0
  154. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  155. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  156. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  157. package/dist/lib/browser/qa-run.js +1 -0
  158. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  159. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  160. package/dist/lib/browser/windows-chrome.js +179 -0
  161. package/dist/lib/claude-desktop-share.d.ts +136 -0
  162. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  163. package/dist/lib/claude-desktop-share.js +394 -0
  164. package/dist/lib/disk-usage.d.ts +50 -0
  165. package/dist/lib/disk-usage.d.ts.map +1 -0
  166. package/dist/lib/disk-usage.js +303 -0
  167. package/dist/lib/docs-lint.d.ts +6 -0
  168. package/dist/lib/docs-lint.d.ts.map +1 -1
  169. package/dist/lib/docs-lint.js +8 -3
  170. package/dist/lib/guarded-remove.d.ts +34 -0
  171. package/dist/lib/guarded-remove.d.ts.map +1 -0
  172. package/dist/lib/guarded-remove.js +310 -0
  173. package/dist/lib/instructions/templates.js +8 -8
  174. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  175. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  176. package/dist/lib/tunnel/allowlist.js +204 -0
  177. package/dist/lib/tunnel/current-address.d.ts +41 -0
  178. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  179. package/dist/lib/tunnel/current-address.js +123 -0
  180. package/dist/lib/tunnel/error-page.d.ts +1 -1
  181. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  182. package/dist/lib/tunnel/error-page.js +19 -8
  183. package/dist/lib/tunnel/gate.js +33 -7
  184. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  185. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  186. package/dist/lib/tunnel/path-scope.js +62 -0
  187. package/dist/lib/tunnel/processes.d.ts +39 -0
  188. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  189. package/dist/lib/tunnel/processes.js +121 -0
  190. package/dist/lib/tunnel/state.d.ts +33 -3
  191. package/dist/lib/tunnel/state.d.ts.map +1 -1
  192. package/dist/lib/tunnel/state.js +115 -30
  193. package/package.json +7 -1
  194. package/schemas/config.schema.json +91 -0
  195. package/src/commander.ts +51 -0
  196. package/src/commands/agents.ts +111 -11
  197. package/src/commands/artifacts.ts +171 -19
  198. package/src/commands/browse.ts +400 -57
  199. package/src/commands/claude-desktop.ts +61 -0
  200. package/src/commands/disk.ts +114 -0
  201. package/src/commands/doctor.ts +52 -1
  202. package/src/commands/files.ts +93 -2
  203. package/src/commands/init.ts +55 -5
  204. package/src/commands/rm.ts +115 -0
  205. package/src/commands/servers.ts +506 -0
  206. package/src/commands/tunnel.ts +494 -116
  207. package/src/commands/web-fetch.ts +30 -19
  208. package/src/commands/web.ts +9 -3
  209. package/src/core/adapters/profiles.ts +22 -1
  210. package/src/core/adapters/types.ts +10 -0
  211. package/src/core/agents/cli.ts +14 -6
  212. package/src/core/agents/render/prompt-context.ts +25 -8
  213. package/src/core/agents/render/session-context.ts +84 -16
  214. package/src/core/agents/state/live-coordination-view.ts +73 -0
  215. package/src/core/agents/state/names.ts +117 -6
  216. package/src/core/artifacts/delivery-card.ts +10 -2
  217. package/src/core/artifacts/index.ts +697 -44
  218. package/src/core/config.ts +183 -4
  219. package/src/core/diagnostics/bundle.ts +2 -1
  220. package/src/core/events/v3/producers/recorder.ts +16 -0
  221. package/src/core/events/v3/projection.ts +11 -1
  222. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  223. package/src/core/hooks/adapter/events.ts +19 -0
  224. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  225. package/src/core/hooks/adapter/wiring.ts +116 -8
  226. package/src/core/hooks/cli.ts +152 -22
  227. package/src/core/hooks/fork-lineage.ts +82 -0
  228. package/src/core/hooks/resolve/transcript.ts +88 -0
  229. package/src/core/hooks/run-markers.ts +132 -0
  230. package/src/core/hooks/shell-rm.ts +103 -0
  231. package/src/core/hooks/shell-tokens.ts +96 -0
  232. package/src/core/hooks/shell-waiter.ts +4 -91
  233. package/src/core/servers/index.ts +712 -0
  234. package/src/core/servers/net.ts +283 -0
  235. package/src/core/servers/tunnels.ts +39 -0
  236. package/src/core/storage/builtins.ts +1 -0
  237. package/src/core/storage/process-log.ts +17 -2
  238. package/src/core/workflow/engine.ts +38 -1
  239. package/src/core/workflow/sandbox-projection.ts +38 -2
  240. package/src/core/workflow/spawn-claude.ts +5 -1
  241. package/src/core/workflow/spawn-codex.ts +18 -1
  242. package/src/core/workflow/spawn-cursor.ts +5 -1
  243. package/src/core/workflow/spawn-opencode.ts +4 -1
  244. package/src/core/workflow/types.ts +62 -2
  245. package/src/core/workflow/worker-access.ts +525 -0
  246. package/src/lib/browser/batch-steps.ts +79 -0
  247. package/src/lib/browser/collect/collector.ts +306 -0
  248. package/src/lib/browser/collect/fields.ts +87 -0
  249. package/src/lib/browser/collect/index.ts +47 -0
  250. package/src/lib/browser/collect/page-driver.ts +327 -0
  251. package/src/lib/browser/collect/presets.ts +237 -0
  252. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  253. package/src/lib/browser/index.ts +1 -0
  254. package/src/lib/browser/page-review-judge.ts +16 -0
  255. package/src/lib/browser/qa-run-contracts.ts +11 -0
  256. package/src/lib/browser/qa-run.ts +1 -0
  257. package/src/lib/browser/windows-chrome.ts +227 -0
  258. package/src/lib/claude-desktop-share.ts +511 -0
  259. package/src/lib/disk-usage.ts +347 -0
  260. package/src/lib/docs-lint.ts +8 -3
  261. package/src/lib/guarded-remove.ts +376 -0
  262. package/src/lib/instructions/templates.ts +8 -8
  263. package/src/lib/tunnel/allowlist.ts +208 -0
  264. package/src/lib/tunnel/current-address.ts +158 -0
  265. package/src/lib/tunnel/error-page.ts +25 -9
  266. package/src/lib/tunnel/gate.ts +38 -9
  267. package/src/lib/tunnel/path-scope.ts +73 -0
  268. package/src/lib/tunnel/processes.ts +149 -0
  269. package/src/lib/tunnel/state.ts +143 -30
@@ -1,21 +1,43 @@
1
1
  // Tunnel config + state persistence + provider helpers. Commands default to
2
2
  // <cwd>/.cache/tunnel/; gitignored, so the allowlist is per-machine.
3
3
 
4
- import { execSync } from "node:child_process";
4
+ import { spawnSync } from "node:child_process";
5
5
  import {
6
+ accessSync,
7
+ chmodSync,
8
+ constants,
6
9
  existsSync,
7
10
  mkdirSync,
8
11
  readdirSync,
9
12
  readFileSync,
13
+ statSync,
10
14
  unlinkSync,
11
15
  writeFileSync,
12
16
  } from "node:fs";
13
- import { resolve } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { delimiter, join, resolve } from "node:path";
19
+ import { isPathAllowed } from "./path-scope.ts";
14
20
 
15
- // Tunnel state lives under <root>/.cache/tunnel/. Root defaults to cwd for the
16
- // command surface and can be supplied by callers that already resolved a repo.
17
- function cachePath(tool: string, filename: string, root: string = process.cwd()): string {
18
- const dir = resolve(root, ".cache", tool);
21
+ /** Overrides the directory that holds tunnel state and config (a host keeps it out of `.cache/`). */
22
+ export const TUNNEL_DIR_ENV = "HARNERY_TUNNEL_DIR";
23
+ /** Explicit cloudflared binary, for a host that ships a managed copy. */
24
+ export const CLOUDFLARED_ENV = "HARNERY_CLOUDFLARED";
25
+
26
+ /**
27
+ * Where tunnel state lives: `$HARNERY_TUNNEL_DIR` when set, else
28
+ * `<root>/.cache/tunnel/`. Root defaults to cwd for the command surface and can
29
+ * be supplied by callers that already resolved a repo.
30
+ */
31
+ export function tunnelDir(
32
+ root: string = process.cwd(),
33
+ env: NodeJS.ProcessEnv = process.env,
34
+ ): string {
35
+ const override = env[TUNNEL_DIR_ENV]?.trim();
36
+ return override ? resolve(override) : resolve(root, ".cache", "tunnel");
37
+ }
38
+
39
+ function cachePath(filename: string, root: string = process.cwd()): string {
40
+ const dir = tunnelDir(root);
19
41
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
20
42
  return resolve(dir, filename);
21
43
  }
@@ -62,7 +84,16 @@ const DEFAULT_CONFIG: TunnelConfig = {
62
84
  };
63
85
 
64
86
  export interface TunnelConfig {
87
+ /** Exact addresses and CIDR ranges the gate admits (Cloudflare provider). */
65
88
  allowed_ips: string[];
89
+ /**
90
+ * Entries `allow add --current` added for this machine's own public
91
+ * addresses. They are a subset of `allowed_ips`; the next refresh replaces
92
+ * exactly these and never touches an entry added by hand.
93
+ */
94
+ auto_allowed?: string[];
95
+ /** Explicit cloudflared binary; `$HARNERY_CLOUDFLARED` wins over this. */
96
+ cloudflared_path?: string;
66
97
  }
67
98
 
68
99
  export type TunnelProvider = "cloudflare" | "tailscale";
@@ -81,38 +112,53 @@ export interface TunnelState {
81
112
  target: string;
82
113
  vhost: string;
83
114
  gate_port: number;
115
+ /** URL path prefixes the gate forwards; every other path is refused. */
116
+ allow_paths: string[];
84
117
  tailscale_mode?: TailscaleMode;
85
118
  tailscale_path?: string;
86
119
  tailscale_https_port?: number;
87
120
  }
88
121
 
89
- /** Normalize a parsed state blob; supply `name` for pre-multi-instance files. */
122
+ /**
123
+ * Normalize a parsed state blob; supply `name` for pre-multi-instance files.
124
+ * A state written before path scopes existed reads as an empty scope, which
125
+ * the gate treats as "refuse every path".
126
+ */
90
127
  function normalizeState(raw: TunnelState, fallbackName: string): TunnelState {
91
- return { ...raw, name: raw.name ?? fallbackName, provider: raw.provider ?? "cloudflare" };
128
+ return {
129
+ ...raw,
130
+ name: raw.name ?? fallbackName,
131
+ provider: raw.provider ?? "cloudflare",
132
+ allow_paths: Array.isArray(raw.allow_paths) ? raw.allow_paths : [],
133
+ };
92
134
  }
93
135
 
94
136
  export function readConfig(): TunnelConfig {
95
- const p = cachePath("tunnel", CONFIG_FILE);
137
+ const p = cachePath(CONFIG_FILE);
96
138
  if (!existsSync(p)) {
97
139
  writeConfig(DEFAULT_CONFIG);
98
140
  return { ...DEFAULT_CONFIG, allowed_ips: [...DEFAULT_CONFIG.allowed_ips] };
99
141
  }
100
142
  try {
101
- return JSON.parse(readFileSync(p, "utf-8")) as TunnelConfig;
143
+ const parsed = JSON.parse(readFileSync(p, "utf-8")) as Partial<TunnelConfig>;
144
+ return {
145
+ ...parsed,
146
+ allowed_ips: Array.isArray(parsed.allowed_ips) ? parsed.allowed_ips : [],
147
+ };
102
148
  } catch {
103
149
  return { ...DEFAULT_CONFIG, allowed_ips: [...DEFAULT_CONFIG.allowed_ips] };
104
150
  }
105
151
  }
106
152
 
107
153
  export function writeConfig(cfg: TunnelConfig): void {
108
- writeFileSync(cachePath("tunnel", CONFIG_FILE), JSON.stringify(cfg, null, 2));
154
+ writeFileSync(cachePath(CONFIG_FILE), JSON.stringify(cfg, null, 2));
109
155
  }
110
156
 
111
157
  export function readState(
112
158
  name: string = DEFAULT_INSTANCE,
113
159
  root: string = process.cwd(),
114
160
  ): TunnelState | null {
115
- const p = cachePath("tunnel", stateFile(name), root);
161
+ const p = cachePath(stateFile(name), root);
116
162
  if (!existsSync(p)) return null;
117
163
  try {
118
164
  return normalizeState(JSON.parse(readFileSync(p, "utf-8")) as TunnelState, name);
@@ -122,11 +168,11 @@ export function readState(
122
168
  }
123
169
 
124
170
  export function writeState(state: TunnelState, root: string = process.cwd()): void {
125
- writeFileSync(cachePath("tunnel", stateFile(state.name), root), JSON.stringify(state, null, 2));
171
+ writeFileSync(cachePath(stateFile(state.name), root), JSON.stringify(state, null, 2));
126
172
  }
127
173
 
128
174
  export function clearState(name: string = DEFAULT_INSTANCE, root: string = process.cwd()): void {
129
- const p = cachePath("tunnel", stateFile(name), root);
175
+ const p = cachePath(stateFile(name), root);
130
176
  if (existsSync(p)) unlinkSync(p);
131
177
  }
132
178
 
@@ -135,7 +181,7 @@ export function clearState(name: string = DEFAULT_INSTANCE, root: string = proce
135
181
  * `state*.json` under `.cache/tunnel/`; tolerates missing/corrupt files.
136
182
  */
137
183
  export function listStates(root: string = process.cwd()): TunnelState[] {
138
- const dir = resolve(root, ".cache", "tunnel");
184
+ const dir = tunnelDir(root);
139
185
  if (!existsSync(dir)) return [];
140
186
  const out: TunnelState[] = [];
141
187
  for (const file of readdirSync(dir)) {
@@ -169,7 +215,7 @@ export function isTunnelStateLive(
169
215
  return typeof providerPid === "number" && processAlive(providerPid);
170
216
  }
171
217
 
172
- function tunnelTargetPort(state: TunnelState): number | null {
218
+ export function tunnelTargetPort(state: TunnelState): number | null {
173
219
  const value = /^[a-z][a-z\d+.-]*:\/\//i.test(state.target)
174
220
  ? state.target
175
221
  : `http://${state.target}`;
@@ -209,31 +255,98 @@ export function findLiveTunnelForOrigin(
209
255
  );
210
256
  }
211
257
 
212
- /**
213
- * Ensure cloudflared is on PATH or installed at ~/.local/bin/cloudflared.
214
- * Auto-downloads on Linux; throws on other platforms with brew hint.
215
- */
216
- export function ensureCloudflared(): string {
258
+ /** True when the tunnel's path scope forwards every one of `paths`. */
259
+ export function tunnelServesPaths(state: TunnelState, paths: readonly string[]): boolean {
260
+ return paths.every((path) => isPathAllowed(path, state.allow_paths));
261
+ }
262
+
263
+ function isRunnableFile(path: string): boolean {
217
264
  try {
218
- execSync("command -v cloudflared", { stdio: "ignore" });
219
- return "cloudflared";
265
+ if (!statSync(path).isFile()) return false;
266
+ if (process.platform === "win32") return true;
267
+ accessSync(path, constants.X_OK);
268
+ return true;
220
269
  } catch {
221
- /* not on PATH; fall through */
270
+ return false;
271
+ }
272
+ }
273
+
274
+ /** First `name` on PATH (honoring `.exe` on Windows), or null. Needs no shell. */
275
+ function findOnPath(name: string, env: NodeJS.ProcessEnv = process.env): string | null {
276
+ const names = process.platform === "win32" ? [`${name}.exe`, name] : [name];
277
+ for (const dir of (env.PATH ?? env.Path ?? "").split(delimiter)) {
278
+ if (!dir) continue;
279
+ for (const candidate of names) {
280
+ const full = join(dir, candidate);
281
+ if (isRunnableFile(full)) return full;
282
+ }
283
+ }
284
+ return null;
285
+ }
286
+
287
+ /**
288
+ * Resolve the cloudflared binary:
289
+ * 1. `$HARNERY_CLOUDFLARED`, then `cloudflared_path` in the tunnel config: an
290
+ * explicit choice, so a missing or non-runnable file is an error, never a
291
+ * silent fall-through to some other copy.
292
+ * 2. `cloudflared` on PATH.
293
+ * 3. `~/.local/bin/cloudflared`.
294
+ * 4. Linux x86-64 and arm64: download the latest release to (3).
295
+ * Elsewhere it throws with install guidance.
296
+ */
297
+ export function ensureCloudflared(
298
+ env: NodeJS.ProcessEnv = process.env,
299
+ configuredPath: string | undefined = readConfig().cloudflared_path,
300
+ ): string {
301
+ const explicit = env[CLOUDFLARED_ENV]?.trim();
302
+ const fromConfig = configuredPath?.trim();
303
+ for (const [source, path] of [
304
+ [CLOUDFLARED_ENV, explicit],
305
+ ["cloudflared_path in the tunnel config", fromConfig],
306
+ ] as const) {
307
+ if (!path) continue;
308
+ if (isRunnableFile(path)) return path;
309
+ throw new Error(
310
+ `cloudflared is set by ${source} to ${path}, but that file is missing or not runnable.`,
311
+ );
222
312
  }
223
- const local = `${process.env.HOME}/.local/bin/cloudflared`;
224
- if (existsSync(local)) return local;
225
313
 
226
- if (process.platform !== "linux") {
314
+ const onPath = findOnPath("cloudflared", env);
315
+ if (onPath) return onPath;
316
+ const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
317
+ const local = join(homedir(), ".local", "bin", exe);
318
+ if (isRunnableFile(local)) return local;
319
+
320
+ const asset =
321
+ process.platform === "linux"
322
+ ? (
323
+ { x64: "cloudflared-linux-amd64", arm64: "cloudflared-linux-arm64" } as Record<
324
+ string,
325
+ string
326
+ >
327
+ )[process.arch]
328
+ : undefined;
329
+ if (!asset) {
227
330
  throw new Error(
228
- "cloudflared not installed. On macOS: `brew install cloudflared`. " +
331
+ "cloudflared not installed. Install it (macOS: `brew install cloudflared`; Windows: " +
332
+ "`winget install Cloudflare.cloudflared`), or point `$HARNERY_CLOUDFLARED` at a binary. " +
229
333
  "On Linux it auto-installs to ~/.local/bin/cloudflared.",
230
334
  );
231
335
  }
232
336
 
233
337
  process.stderr.write("Installing cloudflared to ~/.local/bin/...\n"); // lint-ok-emission: sync setup phase before structured output; pairs with the inherited stdio of the curl below
234
- execSync(
235
- `curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o ${local} && chmod +x ${local}`,
338
+ mkdirSync(join(homedir(), ".local", "bin"), { recursive: true });
339
+ const download = spawnSync(
340
+ "curl",
341
+ [
342
+ "-fsSL",
343
+ `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
344
+ "-o",
345
+ local,
346
+ ],
236
347
  { stdio: "inherit" },
237
348
  );
349
+ if (download.status !== 0) throw new Error("Downloading cloudflared failed; install it by hand.");
350
+ chmodSync(local, 0o755);
238
351
  return local;
239
352
  }