@happier-dev/stack 0.1.0-preview.5.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 +159 -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 +94 -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,634 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { open } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { buildConfigureServerLinks } from '@happier-dev/cli-common/links';
5
+
6
+ import { findExistingStackCredentialPath } from '../utils/auth/credentials_paths.mjs';
7
+ import { ensureDir } from '../utils/fs/ops.mjs';
8
+ import { readLastLines } from '../utils/fs/tail.mjs';
9
+ import { isTcpPortFree, listListenPids, pickNextFreeTcpPort } from '../utils/net/ports.mjs';
10
+ import { resolveStackEnvPath } from '../utils/paths/paths.mjs';
11
+ import { resolveLocalhostHost } from '../utils/paths/localhost_host.mjs';
12
+ import { killProcessGroupOwnedByStack } from '../utils/proc/ownership.mjs';
13
+ import { run } from '../utils/proc/proc.mjs';
14
+ import { coercePort } from '../utils/server/port.mjs';
15
+ import { waitForHttpOk } from '../utils/server/server.mjs';
16
+ import { getCliHomeDirFromEnvOrDefault } from '../utils/stack/dirs.mjs';
17
+ import {
18
+ deleteStackRuntimeStateFile,
19
+ getStackRuntimeStatePath,
20
+ isPidAlive,
21
+ recordStackRuntimeStart,
22
+ readStackRuntimeStateFile,
23
+ } from '../utils/stack/runtime_state.mjs';
24
+ import { listAllStackNames } from '../utils/stack/stacks.mjs';
25
+ import { stopStackWithEnv } from '../utils/stack/stop.mjs';
26
+ import { openUrlInBrowser } from '../utils/ui/browser.mjs';
27
+
28
+ import { collectReservedStackPorts, getDefaultPortStart } from './port_reservation.mjs';
29
+ import { withStackEnv } from './stack_environment.mjs';
30
+
31
+ export function hasRecordedRuntimePortsForRestart(runtimeState = null) {
32
+ const ports = runtimeState?.ports && typeof runtimeState.ports === 'object' ? runtimeState.ports : null;
33
+ return Number(ports?.server) > 0;
34
+ }
35
+
36
+ export function shouldReuseRuntimePortsOnRestart({ wantsRestart = false, runtimeState = null, wasRunning = false } = {}) {
37
+ return Boolean(wantsRestart && (wasRunning || hasRecordedRuntimePortsForRestart(runtimeState)));
38
+ }
39
+
40
+ export async function runStackScriptWithStackEnv({ rootDir, stackName, scriptPath, args, extraEnv = {}, background = false }) {
41
+ await withStackEnv({
42
+ stackName,
43
+ extraEnv,
44
+ fn: async ({ env, envPath, stackEnv, runtimeStatePath, runtimeState }) => {
45
+ const isStartLike = scriptPath === 'dev.mjs' || scriptPath === 'run.mjs';
46
+ if (!isStartLike) {
47
+ await run(process.execPath, [join(rootDir, 'scripts', scriptPath), ...args], { cwd: rootDir, env });
48
+ return;
49
+ }
50
+
51
+ const wantsRestart = args.includes('--restart');
52
+ const wantsJson = args.includes('--json');
53
+ const pinnedServerPort = Boolean((stackEnv.HAPPIER_STACK_SERVER_PORT ?? '').trim());
54
+ const serverComponent = (stackEnv.HAPPIER_STACK_SERVER_COMPONENT ?? '').toString().trim() || 'happier-server-light';
55
+ const managedInfra =
56
+ serverComponent === 'happier-server'
57
+ ? (stackEnv.HAPPIER_STACK_MANAGED_INFRA ?? '1').toString().trim() !== '0'
58
+ : false;
59
+
60
+ // If this is an ephemeral-port stack and it's already running, avoid spawning a second copy.
61
+ const existingOwnerPid = Number(runtimeState?.ownerPid);
62
+ const existingPort = Number(runtimeState?.ports?.server);
63
+ const existingUiPort = Number(runtimeState?.expo?.webPort);
64
+ const existingPorts = runtimeState?.ports && typeof runtimeState.ports === 'object' ? runtimeState.ports : null;
65
+ const infraRuntimePids = [
66
+ Number(runtimeState?.processes?.serverPid),
67
+ Number(runtimeState?.processes?.expoPid),
68
+ Number(runtimeState?.processes?.expoTailscaleForwarderPid),
69
+ ].filter((pid) => Number.isFinite(pid) && pid > 1);
70
+ const infraPidAlive = infraRuntimePids.some((pid) => isPidAlive(pid));
71
+ const serverPortOccupied =
72
+ Number.isFinite(existingPort) && existingPort > 0
73
+ ? !(await isTcpPortFree(existingPort, { host: '127.0.0.1' }).catch(() => true))
74
+ : false;
75
+ const wasRunning = isPidAlive(existingOwnerPid) || infraPidAlive || serverPortOccupied;
76
+ // True restart = there was an active runner for this stack. If the stack is not running,
77
+ // `--restart` should behave like a normal start (allocate new ephemeral ports if needed).
78
+ const isTrueRestart = shouldReuseRuntimePortsOnRestart({ wantsRestart, runtimeState, wasRunning });
79
+
80
+ // Restart semantics (stack mode):
81
+ // - Stop stack-owned processes first (runner, daemon, Expo, etc.)
82
+ // - Never kill arbitrary port listeners
83
+ // - Preserve previous runtime ports in memory so a true restart can reuse them
84
+ if (wantsRestart && !wantsJson) {
85
+ const baseDir = resolveStackEnvPath(stackName).baseDir;
86
+ try {
87
+ await stopStackWithEnv({
88
+ rootDir,
89
+ stackName,
90
+ baseDir,
91
+ env,
92
+ json: false,
93
+ noDocker: false,
94
+ aggressive: false,
95
+ sweepOwned: true,
96
+ });
97
+ } catch {
98
+ // ignore (fail-closed below on port checks)
99
+ }
100
+ await deleteStackRuntimeStateFile(runtimeStatePath).catch(() => {});
101
+ }
102
+ if (wasRunning) {
103
+ if (!wantsRestart) {
104
+ const serverPart = Number.isFinite(existingPort) && existingPort > 0 ? ` server=${existingPort}` : '';
105
+ const uiPart =
106
+ scriptPath === 'dev.mjs' && Number.isFinite(existingUiPort) && existingUiPort > 0 ? ` ui=${existingUiPort}` : '';
107
+ console.log(`[stack] ${stackName}: already running (pid=${existingOwnerPid}${serverPart}${uiPart})`);
108
+
109
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
110
+ const noBrowser = args.includes('--no-browser') || (env.HAPPIER_STACK_NO_BROWSER ?? '').toString().trim() === '1';
111
+ const openBrowser = isInteractive && !wantsJson && !noBrowser;
112
+
113
+ const host = resolveLocalhostHost({ stackMode: true, stackName });
114
+ const uiUrl =
115
+ scriptPath === 'dev.mjs'
116
+ ? Number.isFinite(existingUiPort) && existingUiPort > 0
117
+ ? `http://${host}:${existingUiPort}`
118
+ : null
119
+ : Number.isFinite(existingPort) && existingPort > 0
120
+ ? `http://${host}:${existingPort}`
121
+ : null;
122
+
123
+ if (uiUrl) {
124
+ const serverUrlForUi = Number.isFinite(existingPort) && existingPort > 0 ? `http://localhost:${existingPort}` : '';
125
+ const uiOpenUrl = serverUrlForUi ? buildConfigureServerLinks({ webappUrl: uiUrl, serverUrl: serverUrlForUi }).webUrl : uiUrl;
126
+ console.log(`[stack] ${stackName}: ui: ${uiOpenUrl}`);
127
+ if (openBrowser) {
128
+ await openUrlInBrowser(uiOpenUrl);
129
+ }
130
+ } else if (scriptPath === 'dev.mjs') {
131
+ console.log(`[stack] ${stackName}: ui: unknown (missing expo.webPort in stack.runtime.json)`);
132
+ }
133
+
134
+ // Opt-in: allow starting mobile Metro alongside an already-running stack without restarting the runner.
135
+ // This is important for workflows like re-running `setup-pr` with --mobile after the stack is already up.
136
+ const wantsMobile = args.includes('--mobile') || args.includes('--with-mobile');
137
+ if (wantsMobile) {
138
+ await run(process.execPath, [join(rootDir, 'scripts', 'mobile.mjs'), '--metro'], { cwd: rootDir, env });
139
+ }
140
+ return;
141
+ }
142
+ // Restart: already handled above (stopStackWithEnv is ownership-gated).
143
+ }
144
+
145
+ // Ephemeral ports: allocate at start time, store only in runtime state (not in stack env).
146
+ if (!pinnedServerPort) {
147
+ const reserved = await collectReservedStackPorts({ excludeStackName: stackName });
148
+
149
+ // Also avoid ports held by other *running* ephemeral stacks.
150
+ const names = await listAllStackNames();
151
+ for (const n of names) {
152
+ if (n === stackName) continue;
153
+ const p = getStackRuntimeStatePath(n);
154
+ // eslint-disable-next-line no-await-in-loop
155
+ const st = await readStackRuntimeStateFile(p);
156
+ const pid = Number(st?.ownerPid);
157
+ if (!isPidAlive(pid)) continue;
158
+ const ports = st?.ports && typeof st.ports === 'object' ? st.ports : {};
159
+ for (const v of Object.values(ports)) {
160
+ const num = Number(v);
161
+ if (Number.isFinite(num) && num > 0) reserved.add(num);
162
+ }
163
+ }
164
+
165
+ const startPort = getDefaultPortStart(stackName);
166
+ const ports = {};
167
+
168
+ const parsePortOrNull = (v) => {
169
+ const n = Number(v);
170
+ return Number.isFinite(n) && n > 0 ? n : null;
171
+ };
172
+ const candidatePorts =
173
+ isTrueRestart && existingPorts
174
+ ? {
175
+ server: parsePortOrNull(existingPorts.server),
176
+ backend: parsePortOrNull(existingPorts.backend),
177
+ pg: parsePortOrNull(existingPorts.pg),
178
+ redis: parsePortOrNull(existingPorts.redis),
179
+ minio: parsePortOrNull(existingPorts.minio),
180
+ minioConsole: parsePortOrNull(existingPorts.minioConsole),
181
+ }
182
+ : null;
183
+
184
+ const canReuse =
185
+ candidatePorts &&
186
+ candidatePorts.server &&
187
+ (serverComponent !== 'happier-server' || candidatePorts.backend) &&
188
+ (!managedInfra || (candidatePorts.pg && candidatePorts.redis && candidatePorts.minio && candidatePorts.minioConsole));
189
+
190
+ if (canReuse) {
191
+ ports.server = candidatePorts.server;
192
+ if (serverComponent === 'happier-server') {
193
+ ports.backend = candidatePorts.backend;
194
+ if (managedInfra) {
195
+ ports.pg = candidatePorts.pg;
196
+ ports.redis = candidatePorts.redis;
197
+ ports.minio = candidatePorts.minio;
198
+ ports.minioConsole = candidatePorts.minioConsole;
199
+ }
200
+ }
201
+
202
+ // Fail-closed if any of the reused ports are unexpectedly occupied (prevents cross-stack collisions).
203
+ const toCheck = Object.values(ports)
204
+ .map((n) => Number(n))
205
+ .filter((n) => Number.isFinite(n) && n > 0);
206
+ for (const p of toCheck) {
207
+ // eslint-disable-next-line no-await-in-loop
208
+ if (!(await isTcpPortFree(p))) {
209
+ if (isTrueRestart && !wantsJson) {
210
+ // Try one more safe cleanup of stack-owned processes and re-check.
211
+ const baseDir = resolveStackEnvPath(stackName).baseDir;
212
+ try {
213
+ await stopStackWithEnv({
214
+ rootDir,
215
+ stackName,
216
+ baseDir,
217
+ env,
218
+ json: false,
219
+ noDocker: false,
220
+ aggressive: false,
221
+ sweepOwned: true,
222
+ });
223
+ } catch {
224
+ // ignore
225
+ }
226
+ // eslint-disable-next-line no-await-in-loop
227
+ if (await isTcpPortFree(p)) {
228
+ continue;
229
+ }
230
+
231
+ // Last resort: if we can prove the listener is stack-owned, kill it.
232
+ // eslint-disable-next-line no-await-in-loop
233
+ const pids = await listListenPids(p);
234
+ const stackBaseDir = resolveStackEnvPath(stackName).baseDir;
235
+ const cliHomeDir = getCliHomeDirFromEnvOrDefault({ stackBaseDir, env });
236
+ for (const pid of pids) {
237
+ // eslint-disable-next-line no-await-in-loop
238
+ await killProcessGroupOwnedByStack(pid, { stackName, envPath, cliHomeDir, label: `port:${p}`, json: false });
239
+ }
240
+ // eslint-disable-next-line no-await-in-loop
241
+ if (await isTcpPortFree(p)) {
242
+ continue;
243
+ }
244
+ }
245
+ throw new Error(
246
+ `[stack] ${stackName}: cannot reuse port ${p} on restart (port is not free).\n` +
247
+ `[stack] Fix: stop the process using it, or re-run without --restart to allocate new ports.`
248
+ );
249
+ }
250
+ }
251
+ } else {
252
+ ports.server = await pickNextFreeTcpPort(startPort, { reservedPorts: reserved });
253
+ reserved.add(ports.server);
254
+
255
+ if (serverComponent === 'happier-server') {
256
+ ports.backend = await pickNextFreeTcpPort(ports.server + 10, { reservedPorts: reserved });
257
+ reserved.add(ports.backend);
258
+ if (managedInfra) {
259
+ ports.pg = await pickNextFreeTcpPort(ports.server + 1000, { reservedPorts: reserved });
260
+ reserved.add(ports.pg);
261
+ ports.redis = await pickNextFreeTcpPort(ports.pg + 1, { reservedPorts: reserved });
262
+ reserved.add(ports.redis);
263
+ ports.minio = await pickNextFreeTcpPort(ports.redis + 1, { reservedPorts: reserved });
264
+ reserved.add(ports.minio);
265
+ ports.minioConsole = await pickNextFreeTcpPort(ports.minio + 1, { reservedPorts: reserved });
266
+ reserved.add(ports.minioConsole);
267
+ }
268
+ }
269
+ }
270
+
271
+ // Sanity: if somehow the server port is now occupied, fail closed (avoids killPortListeners nuking random processes).
272
+ if (!(await isTcpPortFree(Number(ports.server)))) {
273
+ throw new Error(`[stack] ${stackName}: picked server port ${ports.server} but it is not free`);
274
+ }
275
+
276
+ const childEnv = {
277
+ ...env,
278
+ HAPPIER_STACK_EPHEMERAL_PORTS: '1',
279
+ HAPPIER_STACK_SERVER_PORT: String(ports.server),
280
+ ...(serverComponent === 'happier-server' && ports.backend
281
+ ? {
282
+ HAPPIER_STACK_SERVER_BACKEND_PORT: String(ports.backend),
283
+ }
284
+ : {}),
285
+ ...(managedInfra && ports.pg
286
+ ? {
287
+ HAPPIER_STACK_PG_PORT: String(ports.pg),
288
+ HAPPIER_STACK_REDIS_PORT: String(ports.redis),
289
+ HAPPIER_STACK_MINIO_PORT: String(ports.minio),
290
+ HAPPIER_STACK_MINIO_CONSOLE_PORT: String(ports.minioConsole),
291
+ }
292
+ : {}),
293
+ };
294
+
295
+ // Background dev auth flow (automatic):
296
+ // If we're starting `dev.mjs` in background and the stack is not authenticated yet,
297
+ // keep the stack alive for guided login by marking this as an auth-flow so URL resolution
298
+ // fails closed (never opens server port as "UI").
299
+ //
300
+ // IMPORTANT:
301
+ // We must NOT start the daemon before credentials exist in orchestrated flows (setup-pr/review-pr),
302
+ // because the daemon can enter its own auth flow and become stranded (lock held, no machine registration).
303
+ if (background && scriptPath === 'dev.mjs') {
304
+ const startUi = !args.includes('--no-ui') && (env.HAPPIER_STACK_SERVE_UI ?? '1').toString().trim() !== '0';
305
+ const startDaemon = !args.includes('--no-daemon') && (env.HAPPIER_STACK_DAEMON ?? '1').toString().trim() !== '0';
306
+ if (startUi && startDaemon) {
307
+ try {
308
+ const stackBaseDir = resolveStackEnvPath(stackName).baseDir;
309
+ const cliHomeDir = getCliHomeDirFromEnvOrDefault({ stackBaseDir, env });
310
+ const serverUrl = (childEnv.HAPPIER_SERVER_URL ?? env.HAPPIER_SERVER_URL ?? '').toString().trim();
311
+ const hasCreds = Boolean(findExistingStackCredentialPath({ cliHomeDir, serverUrl, env: childEnv }));
312
+ if (!hasCreds) {
313
+ childEnv.HAPPIER_STACK_AUTH_FLOW = '1';
314
+ }
315
+ } catch {
316
+ // If we can't resolve CLI home dir, skip auto auth-flow markers (best-effort).
317
+ }
318
+ }
319
+ }
320
+
321
+ // Background mode: send runner output to a stack-scoped log file so quiet flows can
322
+ // remain clean while still providing actionable error logs.
323
+ const stackBaseDir = resolveStackEnvPath(stackName).baseDir;
324
+ const logsDir = join(stackBaseDir, 'logs');
325
+ const logPath = join(logsDir, `${scriptPath.replace(/\.mjs$/, '')}.${Date.now()}.log`);
326
+ if (background) {
327
+ await ensureDir(logsDir);
328
+ }
329
+
330
+ let logHandle = null;
331
+ let outFd = null;
332
+ if (background) {
333
+ logHandle = await open(logPath, 'a');
334
+ outFd = logHandle.fd;
335
+ }
336
+
337
+ // Spawn the runner (long-lived) and record its pid + ports for other stack-scoped commands.
338
+ const child = spawn(process.execPath, [join(rootDir, 'scripts', scriptPath), ...args], {
339
+ cwd: rootDir,
340
+ env: childEnv,
341
+ stdio: background ? ['ignore', outFd ?? 'ignore', outFd ?? 'ignore'] : 'inherit',
342
+ shell: false,
343
+ detached: background && process.platform !== 'win32',
344
+ });
345
+ try {
346
+ await logHandle?.close();
347
+ } catch {
348
+ // ignore
349
+ }
350
+
351
+ // Record the chosen ports immediately (before the runner finishes booting), so other stack commands
352
+ // can resolve the correct endpoints and `--restart` can reliably reuse the same ports.
353
+ await recordStackRuntimeStart(runtimeStatePath, {
354
+ stackName,
355
+ script: scriptPath,
356
+ ephemeral: true,
357
+ ownerPid: child.pid,
358
+ ports,
359
+ ...(background ? { logs: { runner: logPath } } : {}),
360
+ }).catch(() => {});
361
+
362
+ if (background) {
363
+ // Keep stack.runtime.json so stack-scoped stop/restart can manage this runner.
364
+ // This mode is used by higher-level commands that want to run guided auth steps
365
+ // without mixing them into server logs.
366
+ const internalServerUrl = `http://127.0.0.1:${ports.server}`;
367
+
368
+ // Fail fast if the runner dies immediately or never exposes HTTP.
369
+ // IMPORTANT: do not treat "some process answered /health" as success unless our runner
370
+ // is still alive. Otherwise, if the chosen port is already in use, the runner can exit
371
+ // and a different stack/process could satisfy the health check (leading to confusing
372
+ // follow-on behavior like auth using the wrong port).
373
+ try {
374
+ let exited = null;
375
+ const exitPromise = new Promise((resolvePromise) => {
376
+ child.once('exit', (code, sig) => {
377
+ exited = { kind: 'exit', code: code ?? 0, sig: sig ?? null };
378
+ resolvePromise(exited);
379
+ });
380
+ child.once('error', (err) => {
381
+ exited = { kind: 'error', error: err instanceof Error ? err.message : String(err) };
382
+ resolvePromise(exited);
383
+ });
384
+ });
385
+ const readyPromise = (async () => {
386
+ const timeoutMsRaw = (process.env.HAPPIER_STACK_STACK_BACKGROUND_READY_TIMEOUT_MS ?? '180000').toString().trim();
387
+ const timeoutMs = timeoutMsRaw ? Number(timeoutMsRaw) : 180_000;
388
+ await waitForHttpOk(`${internalServerUrl}/health`, {
389
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 180_000,
390
+ intervalMs: 300,
391
+ });
392
+ return { kind: 'ready' };
393
+ })();
394
+
395
+ const first = await Promise.race([exitPromise, readyPromise]);
396
+ if (first.kind !== 'ready') {
397
+ throw new Error(`[stack] ${stackName}: runner exited before becoming ready. log: ${logPath}`);
398
+ }
399
+ // Even if /health responded, ensure our runner is still alive.
400
+ // (Prevents false positives when another process owns the port.)
401
+ if (exited && exited.kind !== 'ready') {
402
+ throw new Error(`[stack] ${stackName}: runner reported ready but exited immediately. log: ${logPath}`);
403
+ }
404
+ if (!isPidAlive(child.pid)) {
405
+ throw new Error(
406
+ `[stack] ${stackName}: runner health check passed, but runner is not running.\n` +
407
+ `[stack] This usually means the chosen port (${ports.server}) is already in use by another process.\n` +
408
+ `[stack] log: ${logPath}`
409
+ );
410
+ }
411
+ } catch (e) {
412
+ // Attach some log context so failures are debuggable even when a higher-level
413
+ // command cleans up the sandbox directory afterwards.
414
+ try {
415
+ const tail = await readLastLines(logPath, 160);
416
+ if (tail && e instanceof Error) {
417
+ e.message = `${e.message}\n\n[stack] last runner log lines:\n${tail}`;
418
+ }
419
+ } catch {
420
+ // ignore
421
+ }
422
+ // Best-effort cleanup on boot failure.
423
+ try {
424
+ // We spawned this runner process, so we can safely terminate it without relying
425
+ // on ownership heuristics (which can be unreliable on some platforms due to `ps` truncation).
426
+ if (background && process.platform !== 'win32') {
427
+ try {
428
+ process.kill(-child.pid, 'SIGTERM');
429
+ } catch {
430
+ // ignore
431
+ }
432
+ }
433
+ try {
434
+ child.kill('SIGTERM');
435
+ } catch {
436
+ // ignore
437
+ }
438
+ } catch {
439
+ // ignore
440
+ }
441
+ await deleteStackRuntimeStateFile(runtimeStatePath).catch(() => {});
442
+ throw e;
443
+ }
444
+
445
+ if (!wantsJson) {
446
+ console.log(`[stack] ${stackName}: logs: ${logPath}`);
447
+ }
448
+ try {
449
+ child.unref();
450
+ } catch {
451
+ // ignore
452
+ }
453
+ return;
454
+ }
455
+
456
+ let exit = { code: null, sig: null, ok: false };
457
+ try {
458
+ await new Promise((resolvePromise, rejectPromise) => {
459
+ child.on('error', rejectPromise);
460
+ child.on('exit', (code, sig) => {
461
+ exit = { code: code ?? null, sig: sig ?? null, ok: code === 0 };
462
+ if (code === 0) return resolvePromise();
463
+ return rejectPromise(new Error(`stack ${scriptPath} exited (code=${code ?? 'null'}, sig=${sig ?? 'null'})`));
464
+ });
465
+ });
466
+ } finally {
467
+ const cur = await readStackRuntimeStateFile(runtimeStatePath);
468
+ if (Number(cur?.ownerPid) === Number(child.pid)) {
469
+ // Only delete runtime state when we're confident no child processes are left behind.
470
+ // If the runner crashes but a child (server/expo/daemon) stays alive, keeping stack.runtime.json
471
+ // allows `hstack stack stop --aggressive` to kill the recorded PIDs safely.
472
+ const processes = cur?.processes && typeof cur.processes === 'object' ? cur.processes : {};
473
+ const anyAlive = Object.values(processes)
474
+ .map((p) => Number(p))
475
+ .some((pid) => Number.isFinite(pid) && pid > 1 && isPidAlive(pid));
476
+ const portRaw = cur?.ports && typeof cur.ports === 'object' ? cur.ports.server : null;
477
+ const port = Number(portRaw);
478
+ const portOccupied = Number.isFinite(port) && port > 0 ? !(await isTcpPortFree(port, { host: '127.0.0.1' }).catch(() => true)) : false;
479
+
480
+ if (!anyAlive && !portOccupied) {
481
+ await deleteStackRuntimeStateFile(runtimeStatePath);
482
+ } else if (!wantsJson) {
483
+ console.warn(
484
+ `[stack] ${stackName}: preserving ${runtimeStatePath} after runner exit (child processes still alive). ` +
485
+ `Run: hstack stack stop ${stackName} --yes --aggressive`
486
+ );
487
+ }
488
+ }
489
+ }
490
+ return;
491
+ }
492
+
493
+ // Pinned port stack: run normally under the pinned env.
494
+ if (background && wantsJson) {
495
+ // Background mode is meaningless for a dry-run. Run the script normally so callers
496
+ // can still use `--background --json` as a config probe.
497
+ await run(process.execPath, [join(rootDir, 'scripts', scriptPath), ...args], { cwd: rootDir, env });
498
+ return;
499
+ }
500
+ if (background) {
501
+ const pinnedPort = coercePort(env.HAPPIER_STACK_SERVER_PORT);
502
+ if (!pinnedPort) {
503
+ throw new Error(`[stack] ${stackName}: cannot start in background (missing HAPPIER_STACK_SERVER_PORT)`);
504
+ }
505
+
506
+ const stackBaseDir = resolveStackEnvPath(stackName).baseDir;
507
+ const logsDir = join(stackBaseDir, 'logs');
508
+ const logPath = join(logsDir, `${scriptPath.replace(/\.mjs$/, '')}.${Date.now()}.log`);
509
+ await ensureDir(logsDir);
510
+
511
+ const logHandle = await open(logPath, 'a');
512
+ const outFd = logHandle.fd;
513
+
514
+ const child = spawn(process.execPath, [join(rootDir, 'scripts', scriptPath), ...args], {
515
+ cwd: rootDir,
516
+ env,
517
+ stdio: ['ignore', outFd ?? 'ignore', outFd ?? 'ignore'],
518
+ shell: false,
519
+ detached: process.platform !== 'win32',
520
+ });
521
+ try {
522
+ await logHandle?.close();
523
+ } catch {
524
+ // ignore
525
+ }
526
+
527
+ await recordStackRuntimeStart(runtimeStatePath, {
528
+ stackName,
529
+ script: scriptPath,
530
+ ephemeral: false,
531
+ ownerPid: child.pid,
532
+ ports: { server: pinnedPort },
533
+ logs: { runner: logPath },
534
+ }).catch(() => {});
535
+
536
+ const internalServerUrl = `http://127.0.0.1:${pinnedPort}`;
537
+ try {
538
+ let exited = null;
539
+ const exitPromise = new Promise((resolvePromise) => {
540
+ child.once('exit', (code, sig) => {
541
+ exited = { kind: 'exit', code: code ?? 0, sig: sig ?? null };
542
+ resolvePromise(exited);
543
+ });
544
+ child.once('error', (err) => {
545
+ exited = { kind: 'error', error: err instanceof Error ? err.message : String(err) };
546
+ resolvePromise(exited);
547
+ });
548
+ });
549
+ const readyPromise = (async () => {
550
+ const timeoutMsRaw = (process.env.HAPPIER_STACK_STACK_BACKGROUND_READY_TIMEOUT_MS ?? '180000').toString().trim();
551
+ const timeoutMs = timeoutMsRaw ? Number(timeoutMsRaw) : 180_000;
552
+ await waitForHttpOk(`${internalServerUrl}/health`, {
553
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 180_000,
554
+ intervalMs: 300,
555
+ });
556
+ return { kind: 'ready' };
557
+ })();
558
+
559
+ const first = await Promise.race([exitPromise, readyPromise]);
560
+ if (first.kind !== 'ready') {
561
+ throw new Error(`[stack] ${stackName}: runner exited before becoming ready. log: ${logPath}`);
562
+ }
563
+ if (exited && exited.kind !== 'ready') {
564
+ throw new Error(`[stack] ${stackName}: runner reported ready but exited immediately. log: ${logPath}`);
565
+ }
566
+ if (!isPidAlive(child.pid)) {
567
+ throw new Error(
568
+ `[stack] ${stackName}: runner health check passed, but runner is not running.\n` +
569
+ `[stack] This usually means the chosen port (${pinnedPort}) is already in use by another process.\n` +
570
+ `[stack] log: ${logPath}`
571
+ );
572
+ }
573
+ } catch (e) {
574
+ try {
575
+ const tail = await readLastLines(logPath, 160);
576
+ if (tail && e instanceof Error) {
577
+ e.message = `${e.message}\n\n[stack] last runner log lines:\n${tail}`;
578
+ }
579
+ } catch {
580
+ // ignore
581
+ }
582
+ try {
583
+ if (process.platform !== 'win32') {
584
+ try {
585
+ process.kill(-child.pid, 'SIGTERM');
586
+ } catch {
587
+ // ignore
588
+ }
589
+ }
590
+ try {
591
+ child.kill('SIGTERM');
592
+ } catch {
593
+ // ignore
594
+ }
595
+ } catch {
596
+ // ignore
597
+ }
598
+ await deleteStackRuntimeStateFile(runtimeStatePath).catch(() => {});
599
+ throw e;
600
+ }
601
+
602
+ if (!wantsJson) {
603
+ console.log(`[stack] ${stackName}: logs: ${logPath}`);
604
+ }
605
+ try {
606
+ child.unref();
607
+ } catch {
608
+ // ignore
609
+ }
610
+ return;
611
+ }
612
+ if (wantsRestart && !wantsJson) {
613
+ const pinnedPort = coercePort(env.HAPPIER_STACK_SERVER_PORT);
614
+ if (pinnedPort && !(await isTcpPortFree(pinnedPort))) {
615
+ // Last resort: kill listener only if it is stack-owned.
616
+ const pids = await listListenPids(pinnedPort);
617
+ const stackBaseDir = resolveStackEnvPath(stackName).baseDir;
618
+ const cliHomeDir = getCliHomeDirFromEnvOrDefault({ stackBaseDir, env });
619
+ for (const pid of pids) {
620
+ // eslint-disable-next-line no-await-in-loop
621
+ await killProcessGroupOwnedByStack(pid, { stackName, envPath, cliHomeDir, label: `port:${pinnedPort}`, json: false });
622
+ }
623
+ if (!(await isTcpPortFree(pinnedPort))) {
624
+ throw new Error(
625
+ `[stack] ${stackName}: server port ${pinnedPort} is not free on restart.\n` +
626
+ `[stack] Refusing to kill unknown listeners. Stop the process using it, or change the pinned port.`
627
+ );
628
+ }
629
+ }
630
+ }
631
+ await run(process.execPath, [join(rootDir, 'scripts', scriptPath), ...args], { cwd: rootDir, env });
632
+ },
633
+ });
634
+ }