@happier-dev/stack 0.1.0-preview.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +138 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +74 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,110 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import net from 'node:net';
3
+ import { runCaptureIfCommandExists } from '../proc/commands.mjs';
4
+
5
+ export async function listListenPids(port) {
6
+ if (!Number.isFinite(port) || port <= 0) return [];
7
+ if (process.platform === 'win32') return [];
8
+
9
+ let raw = '';
10
+ try {
11
+ // `lsof` exits non-zero if no matches; normalize to empty output.
12
+ raw = await runCaptureIfCommandExists('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t']);
13
+ if (!raw && process.platform === 'darwin') {
14
+ // Some non-interactive shells (launchd/GUI apps) have a PATH that omits /usr/sbin,
15
+ // which makes `command -v lsof` fail even though lsof exists. Fall back to absolute paths.
16
+ raw =
17
+ (await runCaptureIfCommandExists('/usr/sbin/lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'])) ||
18
+ (await runCaptureIfCommandExists('/usr/bin/lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'])) ||
19
+ '';
20
+ }
21
+ } catch {
22
+ raw = '';
23
+ }
24
+
25
+ return Array.from(
26
+ new Set(
27
+ raw
28
+ .split(/\s+/g)
29
+ .map((s) => s.trim())
30
+ .filter(Boolean)
31
+ .map((s) => Number(s))
32
+ .filter((n) => Number.isInteger(n) && n > 1)
33
+ )
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Best-effort: kill any processes LISTENing on a TCP port.
39
+ * Used to avoid EADDRINUSE when a previous run left a server behind.
40
+ */
41
+ export async function killPortListeners(port, { label = 'port' } = {}) {
42
+ if (!Number.isFinite(port) || port <= 0) {
43
+ return [];
44
+ }
45
+ if (process.platform === 'win32') {
46
+ return [];
47
+ }
48
+
49
+ const pids = await listListenPids(port);
50
+
51
+ if (!pids.length) {
52
+ return [];
53
+ }
54
+
55
+ // eslint-disable-next-line no-console
56
+ console.log(`[local] ${label}: freeing tcp:${port} (killing pids: ${pids.join(', ')})`);
57
+
58
+ for (const pid of pids) {
59
+ try {
60
+ process.kill(pid, 'SIGTERM');
61
+ } catch {
62
+ // ignore
63
+ }
64
+ }
65
+
66
+ await delay(500);
67
+
68
+ for (const pid of pids) {
69
+ try {
70
+ process.kill(pid, 0);
71
+ process.kill(pid, 'SIGKILL');
72
+ } catch {
73
+ // not running / no permission
74
+ }
75
+ }
76
+
77
+ return pids;
78
+ }
79
+
80
+ export async function isTcpPortFree(port, { host = '127.0.0.1' } = {}) {
81
+ if (!Number.isFinite(port) || port <= 0) return false;
82
+
83
+ // Prefer lsof-based detection to catch IPv6 listeners (e.g. TCP *:8081 (LISTEN))
84
+ // which can make a "bind 127.0.0.1" probe incorrectly report "free" on macOS.
85
+ const pids = await listListenPids(port);
86
+ if (pids.length) return false;
87
+
88
+ // Fallback: attempt to bind.
89
+ return await new Promise((resolvePromise) => {
90
+ const srv = net.createServer();
91
+ srv.unref();
92
+ srv.on('error', () => resolvePromise(false));
93
+ srv.listen({ port, host }, () => {
94
+ srv.close(() => resolvePromise(true));
95
+ });
96
+ });
97
+ }
98
+
99
+ export async function pickNextFreeTcpPort(startPort, { reservedPorts = new Set(), host = '127.0.0.1', tries = 200 } = {}) {
100
+ let port = startPort;
101
+ for (let i = 0; i < tries; i++) {
102
+ // eslint-disable-next-line no-await-in-loop
103
+ if (!reservedPorts.has(port) && (await isTcpPortFree(port, { host }))) {
104
+ return port;
105
+ }
106
+ port += 1;
107
+ }
108
+ throw new Error(`[local] unable to find a free TCP port starting at ${startPort}`);
109
+ }
110
+
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Lightweight TCP port forwarder using Node.js built-in `net` module.
3
+ *
4
+ * Used to expose Expo dev server (which binds to LAN IP) on the Tailscale interface,
5
+ * enabling remote device access over Tailscale without modifying Expo's binding behavior.
6
+ *
7
+ * Can be run standalone:
8
+ * node tcp_forward.mjs --listen-host=100.x.y.z --listen-port=8081 --target-host=192.168.1.50 --target-port=8081
9
+ *
10
+ * Or imported and spawned as a managed child process.
11
+ */
12
+
13
+ import net from 'node:net';
14
+
15
+ /**
16
+ * Create a TCP forwarding server.
17
+ *
18
+ * @param {Object} options
19
+ * @param {string} options.listenHost - Host/IP to listen on (e.g., Tailscale IP)
20
+ * @param {number} options.listenPort - Port to listen on
21
+ * @param {string} options.targetHost - Host/IP to forward to (e.g., LAN IP or 127.0.0.1)
22
+ * @param {number} options.targetPort - Port to forward to
23
+ * @param {string} [options.label] - Label for logging (default: 'tcp-forward')
24
+ * @returns {net.Server}
25
+ */
26
+ export function createTcpForwarder({ listenHost, listenPort, targetHost, targetPort, label = 'tcp-forward' }) {
27
+ const server = net.createServer((clientSocket) => {
28
+ const targetSocket = net.createConnection({ host: targetHost, port: targetPort }, () => {
29
+ // Connection established, pipe data both ways
30
+ clientSocket.pipe(targetSocket);
31
+ targetSocket.pipe(clientSocket);
32
+ });
33
+
34
+ // Handle errors on both sockets
35
+ clientSocket.on('error', (err) => {
36
+ if (err.code !== 'ECONNRESET') {
37
+ process.stderr.write(`[${label}] client error: ${err.message}\n`);
38
+ }
39
+ targetSocket.destroy();
40
+ });
41
+
42
+ targetSocket.on('error', (err) => {
43
+ if (err.code !== 'ECONNRESET' && err.code !== 'ECONNREFUSED') {
44
+ process.stderr.write(`[${label}] target error: ${err.message}\n`);
45
+ }
46
+ clientSocket.destroy();
47
+ });
48
+
49
+ // Clean up on close
50
+ clientSocket.on('close', () => targetSocket.destroy());
51
+ targetSocket.on('close', () => clientSocket.destroy());
52
+ });
53
+
54
+ server.on('error', (err) => {
55
+ process.stderr.write(`[${label}] server error: ${err.message}\n`);
56
+ });
57
+
58
+ return server;
59
+ }
60
+
61
+ /**
62
+ * Start a TCP forwarder and return a promise that resolves when listening.
63
+ *
64
+ * @param {Object} options - Same as createTcpForwarder
65
+ * @returns {Promise<{ server: net.Server, address: string, port: number }>}
66
+ */
67
+ export async function startTcpForwarder(options) {
68
+ const { listenHost, listenPort, label = 'tcp-forward' } = options;
69
+ const server = createTcpForwarder(options);
70
+
71
+ return new Promise((resolve, reject) => {
72
+ server.once('error', reject);
73
+ server.listen(listenPort, listenHost, () => {
74
+ server.removeListener('error', reject);
75
+ const addr = server.address();
76
+ const address = typeof addr === 'object' ? addr.address : listenHost;
77
+ const port = typeof addr === 'object' ? addr.port : listenPort;
78
+ process.stdout.write(`[${label}] forwarding ${address}:${port} -> ${options.targetHost}:${options.targetPort}\n`);
79
+ resolve({ server, address, port });
80
+ });
81
+ });
82
+ }
83
+
84
+ function trySendIpc(msg) {
85
+ try {
86
+ if (typeof process.send === 'function') {
87
+ process.send(msg);
88
+ }
89
+ } catch {
90
+ // ignore
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Gracefully stop a TCP forwarder server.
96
+ *
97
+ * @param {net.Server} server
98
+ * @param {string} [label]
99
+ * @returns {Promise<void>}
100
+ */
101
+ export async function stopTcpForwarder(server, label = 'tcp-forward') {
102
+ if (!server) return;
103
+ return new Promise((resolve) => {
104
+ server.close(() => {
105
+ process.stdout.write(`[${label}] stopped\n`);
106
+ resolve();
107
+ });
108
+ // Force-close after timeout
109
+ setTimeout(() => {
110
+ resolve();
111
+ }, 2000);
112
+ });
113
+ }
114
+
115
+ // Standalone CLI mode
116
+ if (import.meta.url === `file://${process.argv[1]}`) {
117
+ const args = process.argv.slice(2);
118
+ const kv = new Map();
119
+ for (const arg of args) {
120
+ const m = arg.match(/^--([^=]+)=(.*)$/);
121
+ if (m) kv.set(m[1], m[2]);
122
+ }
123
+
124
+ const listenHost = kv.get('listen-host') || kv.get('listenHost');
125
+ const listenPort = Number(kv.get('listen-port') || kv.get('listenPort'));
126
+ const targetHost = kv.get('target-host') || kv.get('targetHost') || '127.0.0.1';
127
+ const targetPort = Number(kv.get('target-port') || kv.get('targetPort'));
128
+ const label = kv.get('label') || 'tcp-forward';
129
+
130
+ if (!listenHost || !listenPort || !targetPort) {
131
+ console.error('Usage: node tcp_forward.mjs --listen-host=<ip> --listen-port=<port> --target-host=<ip> --target-port=<port> [--label=<label>]');
132
+ process.exit(1);
133
+ }
134
+
135
+ const shutdown = () => {
136
+ process.stdout.write(`\n[${label}] shutting down...\n`);
137
+ process.exit(0);
138
+ };
139
+
140
+ process.on('SIGINT', shutdown);
141
+ process.on('SIGTERM', shutdown);
142
+
143
+ startTcpForwarder({ listenHost, listenPort, targetHost, targetPort, label })
144
+ .then(() => {
145
+ trySendIpc({ type: 'ready', listenHost, listenPort, targetHost, targetPort, label });
146
+ // Keep running until signal
147
+ })
148
+ .catch((err) => {
149
+ trySendIpc({
150
+ type: 'error',
151
+ code: err && typeof err === 'object' ? err.code : null,
152
+ message: err instanceof Error ? err.message : String(err ?? 'unknown error'),
153
+ listenHost,
154
+ listenPort,
155
+ targetHost,
156
+ targetPort,
157
+ label,
158
+ });
159
+ console.error(`[${label}] failed to start: ${err.message}`);
160
+ process.exit(1);
161
+ });
162
+ }
@@ -0,0 +1,30 @@
1
+ export function normalizeUrlNoTrailingSlash(raw) {
2
+ const s = String(raw ?? '').trim();
3
+ if (!s) return '';
4
+
5
+ let u;
6
+ try {
7
+ u = new URL(s);
8
+ } catch {
9
+ // Best-effort: if it's a plain string with trailing slash, trim it.
10
+ return s.endsWith('/') ? s.replace(/\/+$/, '') : s;
11
+ }
12
+
13
+ // Only normalize "base" URLs without search/hash.
14
+ // If search/hash is present, removing slashes can change semantics.
15
+ if (u.search || u.hash) {
16
+ return u.toString();
17
+ }
18
+
19
+ // Normalize multiple trailing slashes down to none (root) or one-less (non-root).
20
+ const path = u.pathname || '/';
21
+ if (path === '/' || path === '') {
22
+ return u.origin;
23
+ }
24
+ if (path.endsWith('/')) {
25
+ const nextPath = path.replace(/\/+$/, '');
26
+ return `${u.origin}${nextPath}`;
27
+ }
28
+ return u.toString();
29
+ }
30
+
@@ -0,0 +1,29 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { normalizeUrlNoTrailingSlash } from './url.mjs';
5
+
6
+ test('normalizeUrlNoTrailingSlash removes trailing slash from origins', () => {
7
+ assert.equal(normalizeUrlNoTrailingSlash('https://example.com/'), 'https://example.com');
8
+ assert.equal(normalizeUrlNoTrailingSlash('http://localhost:3005/'), 'http://localhost:3005');
9
+ });
10
+
11
+ test('normalizeUrlNoTrailingSlash removes trailing slash from path-only base URLs', () => {
12
+ assert.equal(normalizeUrlNoTrailingSlash('https://example.com/api/'), 'https://example.com/api');
13
+ assert.equal(normalizeUrlNoTrailingSlash('https://example.com/api///'), 'https://example.com/api');
14
+ });
15
+
16
+ test('normalizeUrlNoTrailingSlash preserves query/hash URLs', () => {
17
+ assert.equal(normalizeUrlNoTrailingSlash('https://example.com/?q=1'), 'https://example.com/?q=1');
18
+ assert.equal(normalizeUrlNoTrailingSlash('https://example.com/api/?q=1'), 'https://example.com/api/?q=1');
19
+ });
20
+
21
+ test('normalizeUrlNoTrailingSlash handles invalid/blank values defensively', () => {
22
+ assert.equal(normalizeUrlNoTrailingSlash(''), '');
23
+ assert.equal(normalizeUrlNoTrailingSlash(' '), '');
24
+ assert.equal(normalizeUrlNoTrailingSlash('not-a-url///'), 'not-a-url');
25
+ });
26
+
27
+ test('normalizeUrlNoTrailingSlash preserves hash-only URL semantics', () => {
28
+ assert.equal(normalizeUrlNoTrailingSlash('https://example.com/path/#hash'), 'https://example.com/path/#hash');
29
+ });
@@ -0,0 +1,15 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+
4
+ export function expandHome(p) {
5
+ return String(p ?? '').replace(/^~(?=[/\\])/, homedir());
6
+ }
7
+
8
+ export function getCanonicalHomeDirFromEnv(env = process.env) {
9
+ const fromEnv = (env.HAPPIER_STACK_CANONICAL_HOME_DIR ?? '').trim();
10
+ return fromEnv ? expandHome(fromEnv) : join(homedir(), '.happier-stack');
11
+ }
12
+
13
+ export function getCanonicalHomeEnvPathFromEnv(env = process.env) {
14
+ return join(getCanonicalHomeDirFromEnv(env), '.env');
15
+ }
@@ -0,0 +1,28 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { homedir } from 'node:os';
4
+
5
+ import { expandHome, getCanonicalHomeDirFromEnv, getCanonicalHomeEnvPathFromEnv } from './canonical_home.mjs';
6
+
7
+ test('expandHome expands ~/ paths', () => {
8
+ assert.equal(expandHome('~/x'), `${homedir()}/x`);
9
+ });
10
+
11
+ test('expandHome expands ~\\ paths (Windows)', () => {
12
+ assert.equal(expandHome('~\\x'), `${homedir()}\\x`);
13
+ });
14
+
15
+ test('expandHome leaves non-home-prefixed paths unchanged', () => {
16
+ assert.equal(expandHome('~user/x'), '~user/x');
17
+ assert.equal(expandHome('/tmp/x'), '/tmp/x');
18
+ });
19
+
20
+ test('getCanonicalHomeDirFromEnv expands override and defaults to ~/.happier-stack', () => {
21
+ assert.equal(getCanonicalHomeDirFromEnv({ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' }), `${homedir()}/custom-home`);
22
+ assert.equal(getCanonicalHomeDirFromEnv({}), `${homedir()}/.happier-stack`);
23
+ });
24
+
25
+ test('getCanonicalHomeEnvPathFromEnv resolves .env under canonical home', () => {
26
+ const envPath = getCanonicalHomeEnvPathFromEnv({ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' });
27
+ assert.equal(envPath, `${homedir()}/custom-home/.env`);
28
+ });
@@ -0,0 +1,112 @@
1
+ import { getStackName } from './paths.mjs';
2
+ import { networkInterfaces } from 'node:os';
3
+ import { sanitizeDnsLabel } from '../net/dns.mjs';
4
+
5
+ function resolveBindMode(env) {
6
+ const raw = (env.HAPPIER_STACK_BIND_MODE ?? '').toString().trim().toLowerCase();
7
+ return raw === 'lan' ? 'lan' : raw === 'loopback' ? 'loopback' : '';
8
+ }
9
+
10
+ function resolveLocalhostSubdomainPrefix(env) {
11
+ const raw = (env.HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX ?? '').toString().trim().toLowerCase();
12
+ if (!raw) return 'happier';
13
+ // Keep legacy compatibility (older installs used happy-<stack>.localhost).
14
+ if (raw === 'happy') return 'happy';
15
+ if (raw === 'happier') return 'happier';
16
+ return 'happier';
17
+ }
18
+
19
+ function detectLanHost({ env = process.env } = {}) {
20
+ const override = (env.HAPPIER_STACK_LAN_HOST ?? '').toString().trim();
21
+ if (override) return override;
22
+
23
+ const nets = networkInterfaces();
24
+ const candidates = [];
25
+ for (const [ifName, addrs] of Object.entries(nets)) {
26
+ for (const a of addrs ?? []) {
27
+ if (!a || a.family !== 'IPv4' || a.internal) continue;
28
+ const ip = String(a.address ?? '').trim();
29
+ if (!ip) continue;
30
+ if (ip.startsWith('127.')) continue;
31
+ // Drop link-local IPv4 (usually not host-reachable).
32
+ if (ip.startsWith('169.254.')) continue;
33
+ let score = 0;
34
+ if (ifName === 'lima0' || ifName.startsWith('lima')) score += 50;
35
+ if (ifName.startsWith('en') || ifName.startsWith('eth')) score += 10;
36
+ candidates.push({ ip, ifName, score });
37
+ }
38
+ }
39
+
40
+ candidates.sort((a, b) => b.score - a.score);
41
+ return candidates[0]?.ip ?? '';
42
+ }
43
+
44
+ export function resolveLocalhostHost({ stackMode, stackName = null, env = process.env } = {}) {
45
+ const name = (stackName ?? '').toString().trim() || getStackName(env);
46
+ if (!stackMode) return 'localhost';
47
+ const bindMode = resolveBindMode(env);
48
+ if (bindMode === 'lan') {
49
+ const lanHost = detectLanHost({ env });
50
+ if (lanHost) return lanHost;
51
+ }
52
+ if (!name || name === 'main') return 'localhost';
53
+ const prefix = resolveLocalhostSubdomainPrefix(env);
54
+ return `${prefix}-${sanitizeDnsLabel(name)}.localhost`;
55
+ }
56
+
57
+ export async function preferStackLocalhostHost({ stackName = null, env = process.env } = {}) {
58
+ const name = (stackName ?? '').toString().trim() || getStackName(env);
59
+ if (!name || name === 'main') return 'localhost';
60
+ // IMPORTANT:
61
+ // We intentionally do NOT gate on `dns.lookup()` here.
62
+ //
63
+ // On some systems (notably macOS), Node's DNS resolver may return ENOTFOUND for `*.localhost`
64
+ // even though browsers treat `*.localhost` as loopback and will load it fine.
65
+ //
66
+ // Since this hostname is primarily used for browser-facing URLs and origin isolation, we
67
+ // prefer a stable `<prefix>-<stack>.localhost` form by default and allow opting out via env.
68
+ const modeRaw = (env.HAPPIER_STACK_LOCALHOST_SUBDOMAINS ?? '')
69
+ .toString()
70
+ .trim()
71
+ .toLowerCase();
72
+ const disabled = modeRaw === '0' || modeRaw === 'false' || modeRaw === 'no' || modeRaw === 'off';
73
+ if (disabled) return 'localhost';
74
+
75
+ const preferredHost = resolveLocalhostHost({ stackMode: true, stackName: name, env });
76
+ return preferredHost || 'localhost';
77
+ }
78
+
79
+ // Best-effort: for stacks, prefer `<prefix>-<stack>.localhost` over `localhost` when it's reachable.
80
+ // This keeps URLs stable and stack-scoped while still failing closed to plain localhost.
81
+ export async function preferStackLocalhostUrl(url, { stackName = null, env = process.env } = {}) {
82
+ const raw = String(url ?? '').trim();
83
+ if (!raw) return '';
84
+ const name = (stackName ?? '').toString().trim() || getStackName(env);
85
+ if (!name || name === 'main') return raw;
86
+
87
+ let u = null;
88
+ try {
89
+ u = new URL(raw);
90
+ } catch {
91
+ return raw;
92
+ }
93
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return raw;
94
+
95
+ const bindMode = resolveBindMode(env);
96
+ const isLoopbackHost =
97
+ u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname.toLowerCase().endsWith('.localhost');
98
+ if (!isLoopbackHost) return raw;
99
+
100
+ // In LAN bind mode, prefer an IP address that is reachable from other devices/hosts.
101
+ // This is especially useful inside VMs (e.g. Lima vzNAT) where host-local `*.localhost`
102
+ // URLs are not reachable without explicit port forwarding.
103
+ if (bindMode === 'lan') {
104
+ const lanHost = detectLanHost({ env });
105
+ if (lanHost) return raw.replace(`://${u.hostname}`, `://${lanHost}`);
106
+ return raw;
107
+ }
108
+
109
+ const preferredHost = await preferStackLocalhostHost({ stackName: name, env });
110
+ if (!preferredHost || preferredHost === 'localhost') return raw;
111
+ return raw.replace(`://${u.hostname}`, `://${preferredHost}`);
112
+ }
@@ -0,0 +1,58 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { preferStackLocalhostUrl, resolveLocalhostHost } from './localhost_host.mjs';
4
+
5
+ test('preferStackLocalhostUrl rewrites *.localhost to LAN IP when bind mode is lan', async () => {
6
+ const env = {
7
+ HAPPIER_STACK_STACK: 'dev-auth',
8
+ HAPPIER_STACK_BIND_MODE: 'lan',
9
+ // Override LAN host so test is deterministic.
10
+ HAPPIER_STACK_LAN_HOST: '192.168.5.15',
11
+ };
12
+ const url = await preferStackLocalhostUrl('http://happy-dev-auth.localhost:18137', { stackName: 'dev-auth', env });
13
+ assert.equal(url, 'http://192.168.5.15:18137');
14
+ });
15
+
16
+ test('resolveLocalhostHost returns LAN IP when bind mode is lan', () => {
17
+ const env = { HAPPIER_STACK_BIND_MODE: 'lan', HAPPIER_STACK_LAN_HOST: '192.168.5.15' };
18
+ assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), '192.168.5.15');
19
+ });
20
+
21
+ test('resolveLocalhostHost uses happier-<stack>.localhost by default', () => {
22
+ const env = {};
23
+ assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), 'happier-dev-auth.localhost');
24
+ });
25
+
26
+ test('resolveLocalhostHost supports legacy prefix via HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX=happy', () => {
27
+ const env = { HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX: 'happy' };
28
+ assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), 'happy-dev-auth.localhost');
29
+ });
30
+
31
+ test('resolveLocalhostHost falls back to happier prefix for invalid override values', () => {
32
+ const env = { HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX: 'invalid-prefix' };
33
+ assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), 'happier-dev-auth.localhost');
34
+ });
35
+
36
+ test('resolveLocalhostHost returns localhost for non-stack and main stack contexts', () => {
37
+ assert.equal(resolveLocalhostHost({ stackMode: false, stackName: 'dev-auth', env: {} }), 'localhost');
38
+ assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'main', env: {} }), 'localhost');
39
+ });
40
+
41
+ test('preferStackLocalhostUrl respects localhost subdomain disable policy', async () => {
42
+ const env = { HAPPIER_STACK_LOCALHOST_SUBDOMAINS: 'false' };
43
+ const raw = 'http://localhost:18137';
44
+ const url = await preferStackLocalhostUrl(raw, { stackName: 'dev-auth', env });
45
+ assert.equal(url, raw);
46
+ });
47
+
48
+ test('preferStackLocalhostUrl preserves non-http schemes and non-loopback hosts', async () => {
49
+ const env = { HAPPIER_STACK_BIND_MODE: 'lan', HAPPIER_STACK_LAN_HOST: '192.168.5.15' };
50
+ assert.equal(
51
+ await preferStackLocalhostUrl('ws://localhost:18137', { stackName: 'dev-auth', env }),
52
+ 'ws://localhost:18137'
53
+ );
54
+ assert.equal(
55
+ await preferStackLocalhostUrl('https://example.com:18137', { stackName: 'dev-auth', env }),
56
+ 'https://example.com:18137'
57
+ );
58
+ });