@happier-dev/stack 0.1.0-preview.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +138 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +74 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,219 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ import { findExistingStackCredentialPath } from '../utils/auth/credentials_paths.mjs';
5
+ import { parseArgs } from '../utils/cli/args.mjs';
6
+ import { printResult, wantsHelp } from '../utils/cli/cli.mjs';
7
+ import { isTty, promptSelect, withRl } from '../utils/cli/wizard.mjs';
8
+ import { daemonStatusSummary, startLocalDaemonWithAuth, stopLocalDaemon } from '../daemon.mjs';
9
+ import { getComponentDir, resolveStackEnvPath } from '../utils/paths/paths.mjs';
10
+ import { run } from '../utils/proc/proc.mjs';
11
+ import { resolveServerPortFromEnv, resolveServerUrls } from '../utils/server/urls.mjs';
12
+ import { parseCliIdentityOrThrow, resolveCliHomeDirForIdentity } from '../utils/stack/cli_identities.mjs';
13
+ import { readStackRuntimeStateFile, isPidAlive } from '../utils/stack/runtime_state.mjs';
14
+ import { withStackEnv } from './stack_environment.mjs';
15
+ import { banner, cmd as cmdFmt, sectionTitle } from '../utils/ui/layout.mjs';
16
+ import { cyan, green } from '../utils/ui/ansi.mjs';
17
+
18
+ export async function runStackDaemonCommand({ rootDir, stackName, argv, json }) {
19
+ const { flags, kv } = parseArgs(argv);
20
+ const wantsHelpFlag = wantsHelp(argv, { flags });
21
+
22
+ const positionals = argv.filter((a) => a && a !== '--' && !a.startsWith('--'));
23
+ const action = (positionals[0] ?? 'status').toString().trim();
24
+ const identity = parseCliIdentityOrThrow((kv.get('--identity') ?? '').trim());
25
+ const noOpen = flags.has('--no-open') || flags.has('--no-browser') || flags.has('--no-browser-open');
26
+
27
+ if (wantsHelpFlag || !action || action === 'help') {
28
+ printResult({
29
+ json,
30
+ data: { ok: true, stackName, commands: ['start', 'stop', 'restart', 'status'], flags: ['--identity=<name>'] },
31
+ text: [
32
+ banner('stack daemon', { subtitle: `Manage the happier-cli daemon for stack ${cyan(stackName || 'main')}.` }),
33
+ '',
34
+ sectionTitle('usage:'),
35
+ ` ${cyan('hstack stack daemon')} <name> status [--identity=<name>] [--json]`,
36
+ ` ${cyan('hstack stack daemon')} <name> start [--identity=<name>] [--json]`,
37
+ ` ${cyan('hstack stack daemon')} <name> stop [--identity=<name>] [--json]`,
38
+ ` ${cyan('hstack stack daemon')} <name> restart [--identity=<name>] [--json]`,
39
+ '',
40
+ sectionTitle('example:'),
41
+ ` ${cmdFmt(`hstack stack daemon ${stackName || 'main'} restart`)}`,
42
+ ` ${cmdFmt(`hstack stack daemon ${stackName || 'main'} start --identity=account-b`)}`,
43
+ ].join('\n'),
44
+ });
45
+ return;
46
+ }
47
+
48
+ if (!['start', 'stop', 'restart', 'status'].includes(action)) {
49
+ printResult({
50
+ json,
51
+ data: { ok: false, error: 'invalid_daemon_subcommand', stackName, action },
52
+ text: [
53
+ `[stack] invalid daemon subcommand: ${action}`,
54
+ '',
55
+ 'usage:',
56
+ ' hstack stack daemon <name> start|stop|restart|status [--json]',
57
+ ].join('\n'),
58
+ });
59
+ process.exit(1);
60
+ }
61
+
62
+ const res = await withStackEnv({
63
+ stackName,
64
+ fn: async ({ env }) => {
65
+ const cliDir = getComponentDir(rootDir, 'happier-cli', env);
66
+ const cliBin = join(cliDir, 'bin', 'happier.mjs');
67
+ const baseCliHomeDir = (env.HAPPIER_STACK_CLI_HOME_DIR ?? join(resolveStackEnvPath(stackName).baseDir, 'cli')).toString();
68
+ const cliHomeDir = resolveCliHomeDirForIdentity({ cliHomeDir: baseCliHomeDir, identity });
69
+
70
+ // Stack env files don't always include a server port; for running stacks, prefer runtime state.
71
+ // This avoids accidentally targeting the main stack default (3005) for stacks on other ports.
72
+ let runtimePort = null;
73
+ const runtimePath = (env.HAPPIER_STACK_RUNTIME_STATE_PATH ?? '').toString().trim();
74
+ if (runtimePath) {
75
+ const state = await readStackRuntimeStateFile(runtimePath).catch(() => null);
76
+ const candidate = Number(state?.ports?.server);
77
+ const serverPid = Number(state?.processes?.serverPid);
78
+ if (Number.isFinite(candidate) && candidate > 0 && isPidAlive(serverPid)) {
79
+ runtimePort = candidate;
80
+ }
81
+ }
82
+
83
+ const serverPort = runtimePort ?? resolveServerPortFromEnv({ env, defaultPort: 3005 });
84
+ const urls = await resolveServerUrls({ env, serverPort, allowEnable: false });
85
+ const internalServerUrl = urls.internalServerUrl;
86
+ const publicServerUrl = urls.publicServerUrl;
87
+ const envForIdentity = {
88
+ ...env,
89
+ HAPPIER_STACK_CLI_IDENTITY: identity,
90
+ ...(identity !== 'default'
91
+ ? {
92
+ HAPPIER_STACK_MIGRATE_CREDENTIALS: '0',
93
+ HAPPIER_STACK_AUTO_AUTH_SEED: '0',
94
+ }
95
+ : {}),
96
+ };
97
+ await mkdir(cliHomeDir, { recursive: true }).catch(() => {});
98
+
99
+ if (action === 'start' || action === 'restart') {
100
+ // UX: if this identity is not authenticated yet and we're in a real TTY, offer to run the
101
+ // guided login flow inline (instead of failing or asking for a second terminal).
102
+ //
103
+ // Important: never prompt in --json mode (automation must not hang).
104
+ const hasCreds = Boolean(findExistingStackCredentialPath({
105
+ cliHomeDir,
106
+ serverUrl: internalServerUrl,
107
+ env: envForIdentity,
108
+ }));
109
+
110
+ if (!hasCreds) {
111
+ if (json) {
112
+ const loginCmd = `hstack stack auth ${stackName} login${identity !== 'default' ? ` --identity=${identity} --no-open` : ''}`;
113
+ return { ok: false, action, error: 'auth_required', cliIdentity: identity, cliHomeDir, loginCmd };
114
+ }
115
+
116
+ if (isTty()) {
117
+ const choice = await withRl(async (rl) => {
118
+ return await promptSelect(rl, {
119
+ title:
120
+ `Daemon identity "${identity}" is not authenticated yet.\n` +
121
+ `Authenticate now? (recommended)\n`,
122
+ options: [
123
+ { label: 'yes (run guided login now)', value: 'yes' },
124
+ { label: 'no (show command and exit)', value: 'no' },
125
+ ],
126
+ defaultIndex: 0,
127
+ });
128
+ });
129
+
130
+ if (choice === 'yes') {
131
+ const authArgs = [
132
+ 'login',
133
+ ...(identity !== 'default' ? [`--identity=${identity}`] : []),
134
+ ...(identity !== 'default' || noOpen ? ['--no-open'] : []),
135
+ ];
136
+ await run(process.execPath, [join(rootDir, 'scripts', 'auth.mjs'), ...authArgs], {
137
+ cwd: rootDir,
138
+ env: envForIdentity,
139
+ stdio: 'inherit',
140
+ });
141
+ } else {
142
+ const loginCmd = `hstack stack auth ${stackName} login${identity !== 'default' ? ` --identity=${identity} --no-open` : ''}`;
143
+ throw new Error(`[stack] daemon auth required. Run:\n${loginCmd}`);
144
+ }
145
+ }
146
+ }
147
+
148
+ await startLocalDaemonWithAuth({
149
+ cliBin,
150
+ cliHomeDir,
151
+ internalServerUrl,
152
+ publicServerUrl,
153
+ isShuttingDown: () => false,
154
+ forceRestart: action === 'restart',
155
+ env: envForIdentity,
156
+ stackName,
157
+ cliIdentity: identity,
158
+ });
159
+ const status = await daemonStatusSummary({
160
+ cliBin,
161
+ cliHomeDir,
162
+ internalServerUrl,
163
+ publicServerUrl,
164
+ env: envForIdentity,
165
+ stackName,
166
+ cliIdentity: identity,
167
+ });
168
+ return { ok: true, action, cliIdentity: identity, cliHomeDir, status: status.trim() };
169
+ }
170
+
171
+ if (action === 'stop') {
172
+ await stopLocalDaemon({
173
+ cliBin,
174
+ internalServerUrl,
175
+ publicServerUrl,
176
+ cliHomeDir,
177
+ env: envForIdentity,
178
+ stackName,
179
+ cliIdentity: identity,
180
+ });
181
+ const status = await daemonStatusSummary({
182
+ cliBin,
183
+ cliHomeDir,
184
+ internalServerUrl,
185
+ publicServerUrl,
186
+ env: envForIdentity,
187
+ stackName,
188
+ cliIdentity: identity,
189
+ }).catch(() => '');
190
+ return { ok: true, action, cliIdentity: identity, cliHomeDir, status: status.trim() || null };
191
+ }
192
+
193
+ const status = await daemonStatusSummary({
194
+ cliBin,
195
+ cliHomeDir,
196
+ internalServerUrl,
197
+ publicServerUrl,
198
+ env: envForIdentity,
199
+ stackName,
200
+ cliIdentity: identity,
201
+ });
202
+ return { ok: true, action, cliIdentity: identity, cliHomeDir, status: status.trim() };
203
+ },
204
+ });
205
+
206
+ if (json) {
207
+ printResult({ json, data: { stackName, ...res } });
208
+ return;
209
+ }
210
+
211
+ if (res?.status) {
212
+ console.log('');
213
+ console.log(sectionTitle('Daemon'));
214
+ console.log(res.status);
215
+ return;
216
+ }
217
+
218
+ console.log(`${green('✓')} daemon command completed`);
219
+ }
@@ -0,0 +1,81 @@
1
+ import { join } from 'node:path';
2
+
3
+ import { run } from '../utils/proc/proc.mjs';
4
+ import { printResult } from '../utils/cli/cli.mjs';
5
+ import { resolveTopLevelNodeScriptFile } from './command_arguments.mjs';
6
+ import { getStackHelpUsageLine } from './help_text.mjs';
7
+ import { withStackEnv } from './stack_environment.mjs';
8
+ import { stackExistsSync } from '../utils/stack/stacks.mjs';
9
+
10
+ const DELEGATED_STACK_COMMANDS = new Set(['dev', 'start', 'build', 'typecheck', 'lint', 'test', 'review', 'doctor', 'mobile', 'mobile-dev-client']);
11
+
12
+ function resolveStackOnlyFlags(command) {
13
+ if (command === 'build' || command === 'typecheck' || command === 'lint' || command === 'test' || command === 'review') {
14
+ return ['--repo=default|main|active|dev|<owner/...>|<path>', '--repo-dir=<path>'];
15
+ }
16
+ if (command === 'dev' || command === 'start') {
17
+ return ['--background', '--bg'];
18
+ }
19
+ return [];
20
+ }
21
+
22
+ export async function printDelegatedStackHelpIfAvailable({ rootDir, command, stackName, json }) {
23
+ const normalizedCommand = (command ?? '').toString().trim();
24
+ if (!normalizedCommand) return false;
25
+ if (!DELEGATED_STACK_COMMANDS.has(normalizedCommand)) return false;
26
+
27
+ const delegatedScript = resolveTopLevelNodeScriptFile(normalizedCommand);
28
+ if (!delegatedScript) return false;
29
+
30
+ const usageLine = getStackHelpUsageLine(normalizedCommand);
31
+ const stackOnlyFlags = resolveStackOnlyFlags(normalizedCommand);
32
+
33
+ if (json) {
34
+ printResult({
35
+ json,
36
+ data: {
37
+ ok: true,
38
+ cmd: normalizedCommand,
39
+ stackName: stackName || null,
40
+ usage: usageLine,
41
+ stackFlags: stackOnlyFlags,
42
+ delegatedHelp: delegatedScript,
43
+ },
44
+ text: null,
45
+ });
46
+ return true;
47
+ }
48
+
49
+ const headerLines = [
50
+ `[stack ${normalizedCommand}] usage:`,
51
+ ...(usageLine ? [` ${usageLine}`] : []),
52
+ ...(stackOnlyFlags.length
53
+ ? [
54
+ '',
55
+ 'stack-only flags:',
56
+ ...stackOnlyFlags.map((flag) => ` ${flag}`),
57
+ ]
58
+ : []),
59
+ '',
60
+ `${normalizedCommand} flags (delegated):`,
61
+ '',
62
+ ];
63
+ process.stdout.write(headerLines.join('\n') + '\n');
64
+
65
+ const runDelegatedHelp = async (env) => {
66
+ await run(process.execPath, [join(rootDir, 'scripts', delegatedScript), '--help'], { cwd: rootDir, env });
67
+ };
68
+
69
+ if (stackName && stackExistsSync(stackName)) {
70
+ await withStackEnv({
71
+ stackName,
72
+ fn: async ({ env }) => {
73
+ await runDelegatedHelp(env);
74
+ },
75
+ });
76
+ return true;
77
+ }
78
+
79
+ await runDelegatedHelp(process.env);
80
+ return true;
81
+ }
@@ -0,0 +1,151 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { ensureDir, readTextOrEmpty } from '../utils/fs/ops.mjs';
3
+ import { parseEnvToObject } from '../utils/env/dotenv.mjs';
4
+ import { getRepoDir, resolveStackEnvPath } from '../utils/paths/paths.mjs';
5
+ import { stackExistsSync } from '../utils/stack/stacks.mjs';
6
+ import { STACK_WRAPPER_PRESERVE_KEYS, scrubHappierStackEnv } from '../utils/env/scrub_env.mjs';
7
+ import { applyStackActiveServerScopeEnv } from '../utils/auth/stable_scope_id.mjs';
8
+ import { getStackRuntimeStatePath, isPidAlive, readStackRuntimeStateFile } from '../utils/stack/runtime_state.mjs';
9
+
10
+ const readExistingEnv = readTextOrEmpty;
11
+ const STACK_WRAPPER_CLEAR_UNPREFIXED_KEYS = [
12
+ 'HAPPIER_SERVER_URL',
13
+ 'HAPPIER_PUBLIC_SERVER_URL',
14
+ 'HAPPIER_WEBAPP_URL',
15
+ 'HAPPIER_HOME_DIR',
16
+ ];
17
+
18
+ function stringifyEnv(env) {
19
+ const lines = [];
20
+ for (const [k, v] of Object.entries(env)) {
21
+ if (v == null) continue;
22
+ const s = String(v);
23
+ if (!s.trim()) continue;
24
+ // Keep it simple: no quoting/escaping beyond this.
25
+ lines.push(`${k}=${s}`);
26
+ }
27
+ return lines.join('\n') + '\n';
28
+ }
29
+
30
+ export function resolveDefaultRepoEnv({ rootDir }) {
31
+ // Stacks are pinned to an explicit repo checkout/worktree.
32
+ //
33
+ // Default: use the workspace clone (<workspace>/happier), regardless of any current
34
+ // one-off repo/worktree selection in the user's environment.
35
+ const repoDir = getRepoDir(rootDir, { ...process.env, HAPPIER_STACK_REPO_DIR: '' });
36
+ return { HAPPIER_STACK_REPO_DIR: repoDir };
37
+ }
38
+
39
+ export async function writeStackEnv({ stackName, env }) {
40
+ const stackDir = resolveStackEnvPath(stackName).baseDir;
41
+ await ensureDir(stackDir);
42
+ const envPath = resolveStackEnvPath(stackName).envPath;
43
+ const next = stringifyEnv(env);
44
+ const existing = await readExistingEnv(envPath);
45
+ if (existing !== next) {
46
+ await writeFile(envPath, next, 'utf-8');
47
+ }
48
+ return envPath;
49
+ }
50
+
51
+ export async function withStackEnv({ stackName, fn, extraEnv = {} }) {
52
+ const envPath = resolveStackEnvPath(stackName).envPath;
53
+ if (!stackExistsSync(stackName)) {
54
+ throw new Error(
55
+ `[stack] stack "${stackName}" does not exist yet.\n` +
56
+ `[stack] Create it first:\n` +
57
+ ` hstack stack new ${stackName}\n` +
58
+ ` # or:\n` +
59
+ ` hstack stack new ${stackName} --interactive\n`
60
+ );
61
+ }
62
+ // IMPORTANT: stack env file should be authoritative. If the user has HAPPIER_STACK_*
63
+ // exported in their shell, it would otherwise "win" because utils/env.mjs only sets
64
+ // env vars if they are missing/empty.
65
+ const cleaned = scrubHappierStackEnv(process.env, {
66
+ keepHappierStackKeys: STACK_WRAPPER_PRESERVE_KEYS,
67
+ clearUnprefixedKeys: STACK_WRAPPER_CLEAR_UNPREFIXED_KEYS,
68
+ });
69
+ const raw = await readExistingEnv(envPath);
70
+ const stackEnv = parseEnvToObject(raw);
71
+
72
+ const runtimeStatePath = getStackRuntimeStatePath(stackName);
73
+ const runtimeState = await readStackRuntimeStateFile(runtimeStatePath);
74
+
75
+ let env = {
76
+ ...cleaned,
77
+ HAPPIER_STACK_STACK: stackName,
78
+ HAPPIER_STACK_ENV_FILE: envPath,
79
+ // Expose runtime state path so scripts can find it if needed.
80
+ HAPPIER_STACK_RUNTIME_STATE_PATH: runtimeStatePath,
81
+ // Stack env is authoritative by default.
82
+ ...stackEnv,
83
+ // One-shot overrides (e.g. --repo=...) win over stack env file.
84
+ ...extraEnv,
85
+ };
86
+ env = applyStackActiveServerScopeEnv({
87
+ env,
88
+ stackName,
89
+ cliIdentity: (env.HAPPIER_STACK_CLI_IDENTITY ?? '').toString().trim() || 'default',
90
+ });
91
+
92
+ // Runtime-only port overlay (ephemeral stacks): prefer stack.runtime.json ports when the stack
93
+ // is still running, even if the original "owner" process is gone (common during dev restarts).
94
+ const ownerPid = Number(runtimeState?.ownerPid);
95
+ const processes = runtimeState?.processes && typeof runtimeState.processes === 'object' ? runtimeState.processes : {};
96
+ const serverPid = Number(processes.serverPid);
97
+ const expoPid = Number(processes.expoPid);
98
+ const daemonPid = Number(processes.daemonPid);
99
+ const shouldTrustRuntimePorts =
100
+ isPidAlive(ownerPid) ||
101
+ isPidAlive(serverPid) ||
102
+ isPidAlive(expoPid) ||
103
+ isPidAlive(daemonPid);
104
+
105
+ if (shouldTrustRuntimePorts) {
106
+ const ports = runtimeState?.ports && typeof runtimeState.ports === 'object' ? runtimeState.ports : {};
107
+ const applyPort = (suffix, value) => {
108
+ const n = Number(value);
109
+ if (!Number.isFinite(n) || n <= 0) return;
110
+ env[`HAPPIER_STACK_${suffix}`] = String(n);
111
+ };
112
+ applyPort('SERVER_PORT', ports.server);
113
+ applyPort('SERVER_BACKEND_PORT', ports.backend);
114
+ applyPort('PG_PORT', ports.pg);
115
+ applyPort('REDIS_PORT', ports.redis);
116
+ applyPort('MINIO_PORT', ports.minio);
117
+ applyPort('MINIO_CONSOLE_PORT', ports.minioConsole);
118
+
119
+ // Mark ephemeral mode for downstream helpers (e.g. infra should not persist ports).
120
+ if (runtimeState?.ephemeral) {
121
+ env.HAPPIER_STACK_EPHEMERAL_PORTS = '1';
122
+ }
123
+ }
124
+
125
+ return await fn({ env, envPath, stackEnv, runtimeStatePath, runtimeState });
126
+ }
127
+
128
+ export function parseServerComponentFromEnv(env) {
129
+ const v = (env.HAPPIER_STACK_SERVER_COMPONENT ?? '').toString().trim() || 'happier-server-light';
130
+ return v === 'happier-server' ? 'happier-server' : 'happier-server-light';
131
+ }
132
+
133
+ export async function readStackEnvObject(stackName) {
134
+ const envPath = resolveStackEnvPath(stackName).envPath;
135
+ const raw = await readExistingEnv(envPath);
136
+ const env = raw ? parseEnvToObject(raw) : {};
137
+ return { envPath, env };
138
+ }
139
+
140
+ export async function getRuntimePortExtraEnv(stackName) {
141
+ const runtimeStatePath = getStackRuntimeStatePath(stackName);
142
+ const runtimeState = await readStackRuntimeStateFile(runtimeStatePath);
143
+ const runtimePort = Number(runtimeState?.ports?.server);
144
+ return Number.isFinite(runtimePort) && runtimePort > 0
145
+ ? {
146
+ // Ephemeral stacks (PR stacks) store their chosen ports in stack.runtime.json, not the env file.
147
+ // Ensure stack-scoped commands that compute URLs don't fall back to 3005 (main default).
148
+ HAPPIER_STACK_SERVER_PORT: String(runtimePort),
149
+ }
150
+ : null;
151
+ }
@@ -0,0 +1,75 @@
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 { withStackEnv } from './stack_environment.mjs';
8
+
9
+ async function withTempStackEnvFixture(fn) {
10
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-stack-env-sanitize-'));
11
+ const storageDir = join(tmp, 'storage');
12
+ const stackName = 'sanitize';
13
+ const stackDir = join(storageDir, stackName);
14
+
15
+ await mkdir(stackDir, { recursive: true });
16
+ await writeFile(
17
+ join(stackDir, 'env'),
18
+ [
19
+ 'HAPPIER_STACK_REPO_DIR=/tmp/happier',
20
+ `HAPPIER_STACK_CLI_HOME_DIR=${join(storageDir, stackName, 'cli')}`,
21
+ 'HAPPIER_STACK_SERVER_PORT=3555',
22
+ '',
23
+ ].join('\n'),
24
+ 'utf-8',
25
+ );
26
+
27
+ const previousStorageDir = process.env.HAPPIER_STACK_STORAGE_DIR;
28
+ process.env.HAPPIER_STACK_STORAGE_DIR = storageDir;
29
+
30
+ try {
31
+ await fn({ stackName, storageDir });
32
+ } finally {
33
+ if (typeof previousStorageDir === 'undefined') {
34
+ delete process.env.HAPPIER_STACK_STORAGE_DIR;
35
+ } else {
36
+ process.env.HAPPIER_STACK_STORAGE_DIR = previousStorageDir;
37
+ }
38
+ await rm(tmp, { recursive: true, force: true });
39
+ }
40
+ }
41
+
42
+ test('withStackEnv clears leaked unprefixed server/home env vars from caller scope', async () => {
43
+ await withTempStackEnvFixture(async ({ stackName }) => {
44
+ const previousServerUrl = process.env.HAPPIER_SERVER_URL;
45
+ const previousPublicServerUrl = process.env.HAPPIER_PUBLIC_SERVER_URL;
46
+ const previousWebappUrl = process.env.HAPPIER_WEBAPP_URL;
47
+ const previousHomeDir = process.env.HAPPIER_HOME_DIR;
48
+
49
+ process.env.HAPPIER_SERVER_URL = 'http://stale.localhost:9999';
50
+ process.env.HAPPIER_PUBLIC_SERVER_URL = 'http://stale.localhost:9999';
51
+ process.env.HAPPIER_WEBAPP_URL = 'http://stale.localhost:9999';
52
+ process.env.HAPPIER_HOME_DIR = '/tmp/stale-home';
53
+
54
+ try {
55
+ await withStackEnv({
56
+ stackName,
57
+ fn: async ({ env }) => {
58
+ assert.equal(env.HAPPIER_SERVER_URL, undefined);
59
+ assert.equal(env.HAPPIER_PUBLIC_SERVER_URL, undefined);
60
+ assert.equal(env.HAPPIER_WEBAPP_URL, undefined);
61
+ assert.equal(env.HAPPIER_HOME_DIR, undefined);
62
+ },
63
+ });
64
+ } finally {
65
+ if (typeof previousServerUrl === 'undefined') delete process.env.HAPPIER_SERVER_URL;
66
+ else process.env.HAPPIER_SERVER_URL = previousServerUrl;
67
+ if (typeof previousPublicServerUrl === 'undefined') delete process.env.HAPPIER_PUBLIC_SERVER_URL;
68
+ else process.env.HAPPIER_PUBLIC_SERVER_URL = previousPublicServerUrl;
69
+ if (typeof previousWebappUrl === 'undefined') delete process.env.HAPPIER_WEBAPP_URL;
70
+ else process.env.HAPPIER_WEBAPP_URL = previousWebappUrl;
71
+ if (typeof previousHomeDir === 'undefined') delete process.env.HAPPIER_HOME_DIR;
72
+ else process.env.HAPPIER_HOME_DIR = previousHomeDir;
73
+ }
74
+ });
75
+ });
@@ -0,0 +1,63 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+
4
+ import { parseArgs } from '../utils/cli/args.mjs';
5
+ import { applyStackActiveServerScopeEnv } from '../utils/auth/stable_scope_id.mjs';
6
+ import { resolveStackEnvPath } from '../utils/paths/paths.mjs';
7
+ import { parseCliIdentityOrThrow, resolveCliHomeDirForIdentity } from '../utils/stack/cli_identities.mjs';
8
+
9
+ import { withStackEnv } from './stack_environment.mjs';
10
+
11
+ export async function runStackHappierPassthroughCommand({ rootDir, stackName, passthrough }) {
12
+ const sepIdx = passthrough.indexOf('--');
13
+ const wrapperArgs = sepIdx === -1 ? passthrough : passthrough.slice(0, sepIdx);
14
+ const forwardedArgsRaw = sepIdx === -1 ? passthrough : passthrough.slice(sepIdx + 1);
15
+
16
+ const { kv } = parseArgs(wrapperArgs);
17
+ const identityRaw = (kv.get('--identity') ?? '').toString().trim();
18
+ const identity = identityRaw ? parseCliIdentityOrThrow(identityRaw) : null;
19
+
20
+ const forwardedArgs =
21
+ sepIdx === -1
22
+ ? forwardedArgsRaw.filter((arg) => !(identity && typeof arg === 'string' && arg.trim().startsWith('--identity=')))
23
+ : forwardedArgsRaw;
24
+
25
+ await withStackEnv({
26
+ stackName,
27
+ fn: async ({ env }) => {
28
+ const baseCliHomeDir = (env.HAPPIER_STACK_CLI_HOME_DIR ?? join(resolveStackEnvPath(stackName).baseDir, 'cli')).toString();
29
+ const cliHomeDirForIdentity = identity
30
+ ? resolveCliHomeDirForIdentity({ cliHomeDir: baseCliHomeDir, identity })
31
+ : baseCliHomeDir;
32
+
33
+ let envForHappy = identity
34
+ ? {
35
+ ...env,
36
+ HAPPIER_STACK_CLI_IDENTITY: identity,
37
+ HAPPIER_HOME_DIR: cliHomeDirForIdentity,
38
+ HAPPIER_STACK_CLI_HOME_DIR: cliHomeDirForIdentity,
39
+ }
40
+ : env;
41
+
42
+ envForHappy = applyStackActiveServerScopeEnv({
43
+ env: envForHappy,
44
+ stackName,
45
+ cliIdentity: identity || (envForHappy.HAPPIER_STACK_CLI_IDENTITY ?? '').toString().trim() || 'default',
46
+ });
47
+
48
+ const child = spawn(process.execPath, [join(rootDir, 'scripts', 'happier.mjs'), ...forwardedArgs], {
49
+ cwd: rootDir,
50
+ env: envForHappy,
51
+ stdio: 'inherit',
52
+ shell: false,
53
+ });
54
+
55
+ const exitCode = await new Promise((resolvePromise) => {
56
+ child.on('error', () => resolvePromise(1));
57
+ child.on('exit', (code) => resolvePromise(code ?? 1));
58
+ });
59
+
60
+ process.exit(exitCode);
61
+ },
62
+ });
63
+ }