@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,37 @@
1
+ import { join } from 'node:path';
2
+
3
+ import { pathExists } from '../fs/fs.mjs';
4
+ import { coerceHappyMonorepoRootFromPath } from '../paths/paths.mjs';
5
+
6
+ export async function ensureHappyMonorepoNestedDepsInstalled({
7
+ happyTestDir,
8
+ quiet = false,
9
+ env = process.env,
10
+ ensureDepsInstalled,
11
+ } = {}) {
12
+ const dir = String(happyTestDir ?? '').trim();
13
+ if (!dir) return { monorepoRoot: null, ensured: [] };
14
+
15
+ const monorepoRoot = coerceHappyMonorepoRootFromPath(dir);
16
+ if (!monorepoRoot || monorepoRoot !== dir) return { monorepoRoot: monorepoRoot ?? null, ensured: [] };
17
+
18
+ const ensure = ensureDepsInstalled;
19
+ if (typeof ensure !== 'function') {
20
+ throw new Error('ensureHappyMonorepoNestedDepsInstalled: missing ensureDepsInstalled implementation');
21
+ }
22
+
23
+ const candidates = [
24
+ { subdir: join('apps', 'cli'), label: 'happier-cli (monorepo)' },
25
+ { subdir: join('apps', 'server'), label: 'happier-server (monorepo)' },
26
+ ];
27
+
28
+ const ensured = [];
29
+ for (const c of candidates) {
30
+ const pkgDir = join(monorepoRoot, c.subdir);
31
+ if (!(await pathExists(join(pkgDir, 'package.json')))) continue;
32
+ await ensure(pkgDir, c.label, { quiet, env });
33
+ ensured.push(c.subdir);
34
+ }
35
+
36
+ return { monorepoRoot, ensured };
37
+ }
@@ -0,0 +1,89 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { ensureHappyMonorepoNestedDepsInstalled } from './happy_monorepo_deps.mjs';
8
+
9
+ async function mkMonorepoRoot(t) {
10
+ const root = await mkdtemp(join(tmpdir(), 'hs-mono-deps-'));
11
+ t.after(async () => {
12
+ await rm(root, { recursive: true, force: true });
13
+ });
14
+ await mkdir(join(root, 'apps', 'ui'), { recursive: true });
15
+ await mkdir(join(root, 'apps', 'cli'), { recursive: true });
16
+ await mkdir(join(root, 'apps', 'server'), { recursive: true });
17
+ await writeFile(join(root, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
18
+ await writeFile(join(root, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
19
+ await writeFile(join(root, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
20
+ return root;
21
+ }
22
+
23
+ test('ensureHappyMonorepoNestedDepsInstalled installs cli/server deps when running tests at the monorepo root', async (t) => {
24
+ const root = await mkMonorepoRoot(t);
25
+
26
+ const calls = [];
27
+ const ensureDepsInstalled = async (dir, label) => {
28
+ calls.push({ dir, label });
29
+ };
30
+
31
+ const out = await ensureHappyMonorepoNestedDepsInstalled({
32
+ happyTestDir: root,
33
+ quiet: true,
34
+ env: { ...process.env },
35
+ ensureDepsInstalled,
36
+ });
37
+
38
+ assert.equal(out.monorepoRoot, root);
39
+ assert.deepEqual(out.ensured.sort(), ['apps/cli', 'apps/server']);
40
+ assert.equal(calls.length, 2);
41
+ assert.deepEqual(
42
+ calls.map((c) => c.dir).sort(),
43
+ [join(root, 'apps', 'cli'), join(root, 'apps', 'server')].sort()
44
+ );
45
+ });
46
+
47
+ test('ensureHappyMonorepoNestedDepsInstalled is a no-op when invoked from inside a package directory', async (t) => {
48
+ const root = await mkMonorepoRoot(t);
49
+
50
+ const calls = [];
51
+ const ensureDepsInstalled = async (dir, label) => {
52
+ calls.push({ dir, label });
53
+ };
54
+
55
+ const out = await ensureHappyMonorepoNestedDepsInstalled({
56
+ happyTestDir: join(root, 'apps', 'ui'),
57
+ quiet: true,
58
+ env: { ...process.env },
59
+ ensureDepsInstalled,
60
+ });
61
+
62
+ assert.equal(out.monorepoRoot, root);
63
+ assert.deepEqual(out.ensured, []);
64
+ assert.equal(calls.length, 0);
65
+ });
66
+
67
+ test('ensureHappyMonorepoNestedDepsInstalled is a no-op outside monorepo roots', async (t) => {
68
+ const root = await mkdtemp(join(tmpdir(), 'hs-mono-deps-non-root-'));
69
+ t.after(async () => {
70
+ await rm(root, { recursive: true, force: true });
71
+ });
72
+ await mkdir(join(root, 'random', 'folder'), { recursive: true });
73
+ const out = await ensureHappyMonorepoNestedDepsInstalled({
74
+ happyTestDir: join(root, 'random', 'folder'),
75
+ quiet: true,
76
+ env: { ...process.env },
77
+ ensureDepsInstalled: async () => {},
78
+ });
79
+ assert.equal(out.monorepoRoot, null);
80
+ assert.deepEqual(out.ensured, []);
81
+ });
82
+
83
+ test('ensureHappyMonorepoNestedDepsInstalled throws when ensureDepsInstalled is missing for monorepo root', async (t) => {
84
+ const root = await mkMonorepoRoot(t);
85
+ await assert.rejects(
86
+ () => ensureHappyMonorepoNestedDepsInstalled({ happyTestDir: root, quiet: true, env: { ...process.env } }),
87
+ /missing ensureDepsInstalled implementation/i
88
+ );
89
+ });
@@ -0,0 +1,217 @@
1
+ import { runCapture } from './proc.mjs';
2
+ import { killPid } from '../expo/expo.mjs';
3
+ import { terminateProcessGroup } from './terminate.mjs';
4
+ import { readdir, readFile } from 'node:fs/promises';
5
+
6
+ function normalizeNeedles(needles) {
7
+ const raw = Array.isArray(needles) ? needles : [];
8
+ return raw.map((n) => String(n ?? '').trim()).filter(Boolean);
9
+ }
10
+
11
+ async function readLinuxProcEnviron(pid) {
12
+ try {
13
+ const raw = await readFile(`/proc/${pid}/environ`, 'utf-8');
14
+ return String(raw ?? '').replaceAll('\0', ' ').trim();
15
+ } catch {
16
+ return '';
17
+ }
18
+ }
19
+
20
+ async function readLinuxProcCmdline(pid) {
21
+ try {
22
+ const raw = await readFile(`/proc/${pid}/cmdline`, 'utf-8');
23
+ return String(raw ?? '').replaceAll('\0', ' ').trim();
24
+ } catch {
25
+ return '';
26
+ }
27
+ }
28
+
29
+ async function listLinuxProcPidsWithEnvNeedles(needles) {
30
+ if (process.platform !== 'linux') return null;
31
+ const ns = normalizeNeedles(needles);
32
+ if (ns.length === 0) return [];
33
+ try {
34
+ const entries = await readdir('/proc', { withFileTypes: true });
35
+ const pids = [];
36
+ for (const entry of entries) {
37
+ if (!entry.isDirectory()) continue;
38
+ if (!/^\d+$/.test(entry.name)) continue;
39
+ const pid = Number(entry.name);
40
+ if (!Number.isFinite(pid) || pid <= 1) continue;
41
+ // eslint-disable-next-line no-await-in-loop
42
+ const envText = await readLinuxProcEnviron(pid);
43
+ if (!envText) continue;
44
+ if (ns.every((needle) => envText.includes(needle))) {
45
+ pids.push(pid);
46
+ }
47
+ }
48
+ return Array.from(new Set(pids));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ export function parsePsPidCommandOutputForNeedles(output, needles) {
55
+ const ns = normalizeNeedles(needles);
56
+ if (ns.length === 0) return [];
57
+
58
+ const text = String(output ?? '');
59
+ const pids = [];
60
+ for (const line of text.split('\n')) {
61
+ if (!ns.every((n) => line.includes(n))) continue;
62
+ const m = line.trim().match(/^(\d+)\s+/);
63
+ if (!m) continue;
64
+ const pid = Number(m[1]);
65
+ if (Number.isFinite(pid) && pid > 1) {
66
+ pids.push(pid);
67
+ }
68
+ }
69
+ return Array.from(new Set(pids));
70
+ }
71
+
72
+ export async function getPsEnvLine(pid) {
73
+ const n = Number(pid);
74
+ if (!Number.isFinite(n) || n <= 1) return null;
75
+ if (process.platform === 'win32') return null;
76
+ if (process.platform === 'linux') {
77
+ const envText = await readLinuxProcEnviron(n);
78
+ if (envText) {
79
+ const cmdline = await readLinuxProcCmdline(n);
80
+ return `${n} ${cmdline} ${envText}`.trim();
81
+ }
82
+ }
83
+ try {
84
+ const out = await runCapture('ps', ['eww', '-p', String(n)]);
85
+ // Output usually includes a header line and then a single process line.
86
+ const lines = out.split('\n').map((l) => l.trim()).filter(Boolean);
87
+ if (lines.length >= 2) return lines[1];
88
+ if (lines.length === 1) return lines[0];
89
+ return null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export async function getPidStartTime(pid) {
96
+ const n = Number(pid);
97
+ if (!Number.isFinite(n) || n <= 1) return null;
98
+ if (process.platform === 'win32') return null;
99
+ try {
100
+ const out = await runCapture('ps', ['-o', 'lstart=', '-p', String(n)]);
101
+ const v = String(out ?? '').trim();
102
+ return v || null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ export async function listPidsWithEnvNeedle(needle) {
109
+ const n = String(needle ?? '').trim();
110
+ if (!n) return [];
111
+ if (process.platform === 'win32') return [];
112
+ const viaProc = await listLinuxProcPidsWithEnvNeedles([n]);
113
+ if (Array.isArray(viaProc)) return viaProc;
114
+ try {
115
+ // Include environment variables (eww) so we can match on HAPPIER_STACK_ENV_FILE=/.../env safely.
116
+ const out = await runCapture('ps', ['eww', '-ax', '-o', 'pid=,command=']);
117
+ return parsePsPidCommandOutputForNeedles(out, [n]);
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+
123
+ export async function listPidsWithEnvNeedles(needles) {
124
+ const ns = normalizeNeedles(needles);
125
+ if (ns.length === 0) return [];
126
+ if (process.platform === 'win32') return [];
127
+ const viaProc = await listLinuxProcPidsWithEnvNeedles(ns);
128
+ if (Array.isArray(viaProc)) return viaProc;
129
+ try {
130
+ // Include environment variables (eww) so we can match on HAPPIER_STACK_ENV_FILE=/.../env safely.
131
+ const out = await runCapture('ps', ['eww', '-ax', '-o', 'pid=,command=']);
132
+ return parsePsPidCommandOutputForNeedles(out, ns);
133
+ } catch {
134
+ return [];
135
+ }
136
+ }
137
+
138
+ export async function getProcessGroupId(pid) {
139
+ const n = Number(pid);
140
+ if (!Number.isFinite(n) || n <= 1) return null;
141
+ if (process.platform === 'win32') return null;
142
+ try {
143
+ const out = await runCapture('ps', ['-o', 'pgid=', '-p', String(n)]);
144
+ const raw = out.trim();
145
+ const pgid = raw ? Number(raw) : NaN;
146
+ return Number.isFinite(pgid) && pgid > 1 ? pgid : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ export async function isPidOwnedByStack(pid, { stackName, envPath, cliHomeDir } = {}) {
153
+ const line = await getPsEnvLine(pid);
154
+ if (!line) return false;
155
+ const sn = String(stackName ?? '').trim();
156
+ const ep = String(envPath ?? '').trim();
157
+ const ch = String(cliHomeDir ?? '').trim();
158
+
159
+ // Require at least one stack identifier.
160
+ const hasStack =
161
+ (sn && line.includes(`HAPPIER_STACK_STACK=${sn}`)) ||
162
+ (!sn && line.includes('HAPPIER_STACK_STACK='));
163
+ if (!hasStack) return false;
164
+
165
+ // Prefer env-file binding (strongest).
166
+ if (ep) {
167
+ if (line.includes(`HAPPIER_STACK_ENV_FILE=${ep}`)) {
168
+ return true;
169
+ }
170
+ }
171
+
172
+ // Fallback: CLI home dir binding (useful for daemon-related processes).
173
+ if (ch) {
174
+ if (line.includes(`HAPPIER_HOME_DIR=${ch}`) || line.includes(`HAPPIER_STACK_CLI_HOME_DIR=${ch}`)) {
175
+ return true;
176
+ }
177
+ }
178
+
179
+ return false;
180
+ }
181
+
182
+ export async function killPidOwnedByStack(pid, { stackName, envPath, cliHomeDir, label = 'process', json = false } = {}) {
183
+ const ok = await isPidOwnedByStack(pid, { stackName, envPath, cliHomeDir });
184
+ if (!ok) {
185
+ if (!json) {
186
+ // eslint-disable-next-line no-console
187
+ console.warn(`[stack] refusing to kill ${label} pid=${pid} (cannot prove it belongs to stack ${stackName ?? ''})`);
188
+ }
189
+ return { killed: false, reason: 'not_owned' };
190
+ }
191
+ await killPid(pid);
192
+ return { killed: true, reason: 'killed' };
193
+ }
194
+
195
+ export async function killProcessGroupOwnedByStack(
196
+ pid,
197
+ { stackName, envPath, cliHomeDir, label = 'process-group', json = false, signal = 'SIGTERM', graceMs = 800 } = {}
198
+ ) {
199
+ const ok = await isPidOwnedByStack(pid, { stackName, envPath, cliHomeDir });
200
+ if (!ok) {
201
+ if (!json) {
202
+ // eslint-disable-next-line no-console
203
+ console.warn(`[stack] refusing to kill ${label} pid=${pid} (cannot prove it belongs to stack ${stackName ?? ''})`);
204
+ }
205
+ return { killed: false, reason: 'not_owned' };
206
+ }
207
+ const pgid = await getProcessGroupId(pid);
208
+ if (!pgid) {
209
+ await killPid(pid);
210
+ return { killed: true, reason: 'killed_pid_only' };
211
+ }
212
+ const terminated = await terminateProcessGroup(pgid, { graceMs, signal });
213
+ if (!terminated.ok) {
214
+ return { killed: false, reason: 'kill_timeout', pgid, signal: terminated.signal ?? 'SIGKILL' };
215
+ }
216
+ return { killed: true, reason: 'killed_pgid', pgid, signal: terminated.signal ?? 'SIGKILL' };
217
+ }
@@ -0,0 +1,216 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ import { isPidAlive } from './pids.mjs';
9
+ import { killProcessGroupOwnedByStack } from './ownership.mjs';
10
+
11
+ function spawnOwnedGracefulExit({ env, exitFile, readyFile }) {
12
+ const cleanEnv = {};
13
+ for (const [k, v] of Object.entries(env ?? {})) {
14
+ if (v == null) continue;
15
+ cleanEnv[k] = String(v);
16
+ }
17
+
18
+ // Wait for SIGINT/SIGTERM, then write a marker file right before exiting.
19
+ const code = `
20
+ const fs = require('fs');
21
+ const exitFile = process.argv[1];
22
+ const readyFile = process.argv[2];
23
+ function onStop() {
24
+ setTimeout(() => {
25
+ try { fs.writeFileSync(exitFile, 'ok'); } catch {}
26
+ process.exit(0);
27
+ }, 120);
28
+ }
29
+ process.on('SIGINT', onStop);
30
+ process.on('SIGTERM', onStop);
31
+ try { fs.writeFileSync(readyFile, 'ready'); } catch {}
32
+ setInterval(() => {}, 1000);
33
+ `;
34
+
35
+ const child = spawn(process.execPath, ['-e', code, exitFile, readyFile], {
36
+ env: cleanEnv,
37
+ stdio: 'ignore',
38
+ detached: true,
39
+ });
40
+ child.unref();
41
+ return child;
42
+ }
43
+
44
+ async function waitForCondition({ description, timeoutMs, intervalMs = 30, fn }) {
45
+ const end = Date.now() + Math.max(0, Number(timeoutMs) || 0);
46
+ while (Date.now() < end) {
47
+ // eslint-disable-next-line no-await-in-loop
48
+ if (await fn()) return;
49
+ // eslint-disable-next-line no-await-in-loop
50
+ await new Promise((r) => setTimeout(r, intervalMs));
51
+ }
52
+ throw new Error(`timed out waiting for condition: ${description}`);
53
+ }
54
+
55
+ async function waitForFile(path, timeoutMs) {
56
+ await waitForCondition({
57
+ description: `file to exist at ${path}`,
58
+ timeoutMs,
59
+ fn: async () => {
60
+ try {
61
+ await stat(path);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ },
67
+ });
68
+ }
69
+
70
+ async function waitForFileContent(path, timeoutMs) {
71
+ let content = '';
72
+ await waitForCondition({
73
+ description: `file content at ${path}`,
74
+ timeoutMs,
75
+ fn: async () => {
76
+ try {
77
+ content = (await readFile(path, 'utf-8')).trim();
78
+ return Boolean(content);
79
+ } catch {
80
+ return false;
81
+ }
82
+ },
83
+ });
84
+ return content;
85
+ }
86
+
87
+ function killGroup(pid) {
88
+ try {
89
+ process.kill(-pid, 'SIGKILL');
90
+ } catch {
91
+ // ignore
92
+ }
93
+ }
94
+
95
+ test('killProcessGroupOwnedByStack allows graceful exit before SIGKILL', async (t) => {
96
+ if (process.platform === 'win32') {
97
+ t.skip('POSIX process-group signaling semantics');
98
+ return;
99
+ }
100
+
101
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-kill-grace-'));
102
+ const envPath = join(tmp, 'env');
103
+ const exitFile = join(tmp, 'exited.txt');
104
+ const readyFile = join(tmp, 'ready.txt');
105
+
106
+ const child = spawnOwnedGracefulExit({
107
+ readyFile,
108
+ exitFile,
109
+ env: {
110
+ ...process.env,
111
+ HAPPIER_STACK_STACK: 't',
112
+ HAPPIER_STACK_ENV_FILE: envPath,
113
+ HAPPIER_STACK_PROCESS_KIND: 'infra',
114
+ },
115
+ });
116
+
117
+ try {
118
+ assert.ok(Number(child.pid) > 1, 'expected child pid');
119
+ assert.ok(isPidAlive(child.pid), 'expected child alive before kill');
120
+
121
+ await waitForFile(readyFile, 1200);
122
+
123
+ const res = await killProcessGroupOwnedByStack(child.pid, {
124
+ stackName: 't',
125
+ envPath,
126
+ cliHomeDir: '',
127
+ json: true,
128
+ graceMs: 600,
129
+ });
130
+
131
+ assert.equal(res.killed, true);
132
+ assert.equal(isPidAlive(child.pid), false, 'expected child to exit');
133
+
134
+ await waitForFile(exitFile, 1200);
135
+ } finally {
136
+ killGroup(child.pid);
137
+ try {
138
+ await rm(tmp, { recursive: true, force: true });
139
+ } catch {
140
+ // ignore
141
+ }
142
+ }
143
+ });
144
+
145
+ test('killProcessGroupOwnedByStack honors requested initial signal when provided', async (t) => {
146
+ if (process.platform === 'win32') {
147
+ t.skip('POSIX process-group signaling semantics');
148
+ return;
149
+ }
150
+
151
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-kill-signal-'));
152
+ const envPath = join(tmp, 'env');
153
+ const signalFile = join(tmp, 'signal.txt');
154
+ const readyFile = join(tmp, 'ready.txt');
155
+
156
+ const child = spawn(
157
+ process.execPath,
158
+ [
159
+ '-e',
160
+ `
161
+ const fs = require('fs');
162
+ const signalFile = process.argv[1];
163
+ const readyFile = process.argv[2];
164
+ let done = false;
165
+ function onSignal(sig) {
166
+ if (done) return;
167
+ done = true;
168
+ try { fs.writeFileSync(signalFile, sig); } catch {}
169
+ setTimeout(() => process.exit(0), 30);
170
+ }
171
+ process.on('SIGINT', () => onSignal('SIGINT'));
172
+ process.on('SIGTERM', () => onSignal('SIGTERM'));
173
+ try { fs.writeFileSync(readyFile, 'ready'); } catch {}
174
+ setInterval(() => {}, 1000);
175
+ `,
176
+ signalFile,
177
+ readyFile,
178
+ ],
179
+ {
180
+ env: {
181
+ ...process.env,
182
+ HAPPIER_STACK_STACK: 't',
183
+ HAPPIER_STACK_ENV_FILE: envPath,
184
+ },
185
+ stdio: 'ignore',
186
+ detached: true,
187
+ }
188
+ );
189
+ child.unref();
190
+
191
+ try {
192
+ assert.ok(Number(child.pid) > 1, 'expected child pid');
193
+ assert.ok(isPidAlive(child.pid), 'expected child alive before kill');
194
+ await waitForFile(readyFile, 1200);
195
+
196
+ const res = await killProcessGroupOwnedByStack(child.pid, {
197
+ stackName: 't',
198
+ envPath,
199
+ signal: 'SIGTERM',
200
+ graceMs: 600,
201
+ json: true,
202
+ });
203
+ assert.equal(res.killed, true);
204
+
205
+ const firstSignal = await waitForFileContent(signalFile, 1200);
206
+ assert.ok(firstSignal, 'expected signal marker file to be written');
207
+ assert.equal(firstSignal, 'SIGTERM');
208
+ } finally {
209
+ killGroup(child.pid);
210
+ try {
211
+ await rm(tmp, { recursive: true, force: true });
212
+ } catch {
213
+ // ignore
214
+ }
215
+ }
216
+ });
@@ -0,0 +1,88 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { mkdtemp, rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ import { listPidsWithEnvNeedles, parsePsPidCommandOutputForNeedles } from './ownership.mjs';
9
+
10
+ test('parsePsPidCommandOutputForNeedles requires all needles to match', () => {
11
+ const output = [
12
+ '101 node server.js HAPPIER_STACK_ENV_FILE=/tmp/a/env HAPPIER_STACK_PROCESS_KIND=infra',
13
+ '102 node server.js HAPPIER_STACK_ENV_FILE=/tmp/a/env HAPPIER_STACK_PROCESS_KIND=session',
14
+ '103 node server.js HAPPIER_STACK_ENV_FILE=/tmp/b/env HAPPIER_STACK_PROCESS_KIND=infra',
15
+ ].join('\n');
16
+
17
+ const pids = parsePsPidCommandOutputForNeedles(output, [
18
+ 'HAPPIER_STACK_ENV_FILE=/tmp/a/env',
19
+ 'HAPPIER_STACK_PROCESS_KIND=infra',
20
+ ]);
21
+
22
+ assert.deepEqual(pids, [101]);
23
+ });
24
+
25
+ test('parsePsPidCommandOutputForNeedles deduplicates matches and ignores invalid pid lines', () => {
26
+ const output = [
27
+ '201 cmd HAPPIER_STACK_ENV_FILE=/tmp/x/env HAPPIER_STACK_PROCESS_KIND=infra',
28
+ 'not-a-pid cmd HAPPIER_STACK_ENV_FILE=/tmp/x/env HAPPIER_STACK_PROCESS_KIND=infra',
29
+ '201 cmd HAPPIER_STACK_ENV_FILE=/tmp/x/env HAPPIER_STACK_PROCESS_KIND=infra',
30
+ '1 cmd HAPPIER_STACK_ENV_FILE=/tmp/x/env HAPPIER_STACK_PROCESS_KIND=infra',
31
+ ].join('\n');
32
+
33
+ const pids = parsePsPidCommandOutputForNeedles(output, [
34
+ 'HAPPIER_STACK_ENV_FILE=/tmp/x/env',
35
+ 'HAPPIER_STACK_PROCESS_KIND=infra',
36
+ ]);
37
+
38
+ assert.deepEqual(pids, [201]);
39
+ });
40
+
41
+ test('listPidsWithEnvNeedles finds real stack-owned infra processes', async (t) => {
42
+ if (process.platform === 'win32') {
43
+ t.skip('requires posix process listing');
44
+ return;
45
+ }
46
+
47
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-needles-live-'));
48
+ const envPath = join(tmp, 'env');
49
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
50
+ env: {
51
+ PATH: process.env.PATH ?? '',
52
+ HOME: process.env.HOME ?? '',
53
+ HAPPIER_STACK_STACK: 'test-stack',
54
+ HAPPIER_STACK_ENV_FILE: envPath,
55
+ HAPPIER_STACK_PROCESS_KIND: 'infra',
56
+ },
57
+ stdio: 'ignore',
58
+ detached: true,
59
+ });
60
+ child.unref();
61
+
62
+ const childPid = Number(child.pid);
63
+ try {
64
+ assert.ok(Number.isFinite(childPid) && childPid > 1, 'expected child pid');
65
+ const needles = [
66
+ `HAPPIER_STACK_ENV_FILE=${envPath}`,
67
+ 'HAPPIER_STACK_PROCESS_KIND=infra',
68
+ ];
69
+ const timeoutMs = 5_000;
70
+ const startedAt = Date.now();
71
+ let found = [];
72
+ while (Date.now() - startedAt < timeoutMs) {
73
+ // eslint-disable-next-line no-await-in-loop
74
+ found = await listPidsWithEnvNeedles(needles);
75
+ if (found.includes(childPid)) break;
76
+ // eslint-disable-next-line no-await-in-loop
77
+ await new Promise((resolve) => setTimeout(resolve, 40));
78
+ }
79
+ assert.ok(found.includes(childPid), `expected pid ${childPid} in ${JSON.stringify(found)}`);
80
+ } finally {
81
+ try {
82
+ process.kill(-childPid, 'SIGKILL');
83
+ } catch {
84
+ // ignore
85
+ }
86
+ await rm(tmp, { recursive: true, force: true }).catch(() => {});
87
+ }
88
+ });
@@ -0,0 +1,38 @@
1
+ import { join } from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ import { pathExists } from '../fs/fs.mjs';
5
+ import { coerceHappyMonorepoRootFromPath } from '../paths/paths.mjs';
6
+
7
+ export async function detectPackageManagerCmd(dir) {
8
+ if (await pathExists(join(dir, 'yarn.lock'))) {
9
+ return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
10
+ }
11
+
12
+ // When running against the Happy monorepo, stacks/worktrees often point at a package directory
13
+ // (e.g. apps/server) instead of the monorepo root. Prefer Yarn in that case so
14
+ // workspace-only deps like `@happier-dev/agents` resolve locally instead of being fetched from npm.
15
+ const happyMonorepoRoot = coerceHappyMonorepoRootFromPath(dir);
16
+ if (happyMonorepoRoot && (await pathExists(join(happyMonorepoRoot, 'yarn.lock')))) {
17
+ return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
18
+ }
19
+ // Yarn-only: if no lockfile is found, still default to yarn to avoid mixing package managers.
20
+ return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
21
+ }
22
+
23
+ export async function readPackageJsonScripts(dir) {
24
+ try {
25
+ const raw = await readFile(join(dir, 'package.json'), 'utf-8');
26
+ const pkg = JSON.parse(raw);
27
+ const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
28
+ return scripts;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export function pickFirstScript(scripts, candidates) {
35
+ if (!scripts) return null;
36
+ const list = Array.isArray(candidates) ? candidates : [];
37
+ return list.find((k) => typeof scripts[k] === 'string' && scripts[k].trim()) ?? null;
38
+ }