@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,107 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { runNodeCapture } from './testkit/stack_script_command_testkit.mjs';
8
+
9
+ const scriptsDir = dirname(fileURLToPath(import.meta.url));
10
+ const rootDir = dirname(scriptsDir);
11
+
12
+ async function createStackEnvFixture(t, { stackName = 'exp-test', initialEnv = 'FOO=bar\n' } = {}) {
13
+ const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-stack-env-'));
14
+ t.after(async () => {
15
+ await rm(tmp, { recursive: true, force: true });
16
+ });
17
+
18
+ const storageDir = join(tmp, 'storage');
19
+ const homeDir = join(tmp, 'home');
20
+ const envPath = join(storageDir, stackName, 'env');
21
+ await mkdir(dirname(envPath), { recursive: true });
22
+ await writeFile(envPath, initialEnv, 'utf-8');
23
+
24
+ return {
25
+ envPath,
26
+ stackName,
27
+ baseEnv: {
28
+ ...process.env,
29
+ HAPPIER_STACK_HOME_DIR: homeDir,
30
+ HAPPIER_STACK_STORAGE_DIR: storageDir,
31
+ },
32
+ };
33
+ }
34
+
35
+ test('hstack stack env set/unset writes to stack env file', async (t) => {
36
+ const fixture = await createStackEnvFixture(t);
37
+
38
+ const setRes = await runNodeCapture(
39
+ [join(rootDir, 'scripts', 'stack.mjs'), 'env', fixture.stackName, 'set', 'OPENAI_API_KEY=sk-test'],
40
+ { cwd: rootDir, env: fixture.baseEnv }
41
+ );
42
+ assert.equal(setRes.code, 0, `expected exit 0, got ${setRes.code}\nstdout:\n${setRes.stdout}\nstderr:\n${setRes.stderr}`);
43
+
44
+ const afterSet = await readFile(fixture.envPath, 'utf-8');
45
+ assert.ok(afterSet.includes('OPENAI_API_KEY=sk-test\n'), `expected env file to include OPENAI_API_KEY\n${afterSet}`);
46
+
47
+ const unsetRes = await runNodeCapture([join(rootDir, 'scripts', 'stack.mjs'), 'env', fixture.stackName, 'unset', 'FOO'], {
48
+ cwd: rootDir,
49
+ env: fixture.baseEnv,
50
+ });
51
+ assert.equal(
52
+ unsetRes.code,
53
+ 0,
54
+ `expected exit 0, got ${unsetRes.code}\nstdout:\n${unsetRes.stdout}\nstderr:\n${unsetRes.stderr}`
55
+ );
56
+
57
+ const afterUnset = await readFile(fixture.envPath, 'utf-8');
58
+ assert.ok(!afterUnset.includes('FOO=bar'), `expected env file to remove FOO\n${afterUnset}`);
59
+ });
60
+
61
+ test('hstack stack env <name> defaults to list', async (t) => {
62
+ const fixture = await createStackEnvFixture(t);
63
+
64
+ const res = await runNodeCapture([join(rootDir, 'scripts', 'stack.mjs'), 'env', fixture.stackName], {
65
+ cwd: rootDir,
66
+ env: fixture.baseEnv,
67
+ });
68
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
69
+ assert.ok(res.stdout.includes('FOO=bar'), `expected stdout to include FOO=bar\nstdout:\n${res.stdout}`);
70
+ });
71
+
72
+ test('hstack stack env set rejects invalid KEY=VALUE assignment form', async (t) => {
73
+ const fixture = await createStackEnvFixture(t);
74
+
75
+ const res = await runNodeCapture([join(rootDir, 'scripts', 'stack.mjs'), 'env', fixture.stackName, 'set', 'INVALID_ASSIGNMENT'], {
76
+ cwd: rootDir,
77
+ env: fixture.baseEnv,
78
+ });
79
+ assert.equal(res.code, 1, `expected exit 1, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
80
+ assert.match(res.stderr, /expected KEY=VALUE/i, `expected invalid assignment guidance in stderr\n${res.stderr}`);
81
+ });
82
+
83
+ test('hstack stack env list fails for unknown stack', async (t) => {
84
+ const fixture = await createStackEnvFixture(t, { stackName: 'known-stack' });
85
+ const missingStack = 'missing-stack';
86
+
87
+ const res = await runNodeCapture([join(rootDir, 'scripts', 'stack.mjs'), 'env', missingStack, 'list'], {
88
+ cwd: rootDir,
89
+ env: fixture.baseEnv,
90
+ });
91
+ assert.equal(res.code, 1, `expected exit 1, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
92
+ assert.match(res.stderr, /does not exist yet/i, `expected missing stack error\n${res.stderr}`);
93
+ });
94
+
95
+ test('hstack stack env unset missing key is a no-op that keeps existing entries', async (t) => {
96
+ const fixture = await createStackEnvFixture(t, { initialEnv: 'FOO=bar\nBAR=baz\n' });
97
+
98
+ const res = await runNodeCapture([join(rootDir, 'scripts', 'stack.mjs'), 'env', fixture.stackName, 'unset', 'MISSING_KEY'], {
99
+ cwd: rootDir,
100
+ env: fixture.baseEnv,
101
+ });
102
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
103
+
104
+ const afterUnset = await readFile(fixture.envPath, 'utf-8');
105
+ assert.ok(afterUnset.includes('FOO=bar'), `expected existing key FOO to remain\n${afterUnset}`);
106
+ assert.ok(afterUnset.includes('BAR=baz'), `expected existing key BAR to remain\n${afterUnset}`);
107
+ });
@@ -0,0 +1,20 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { parseExpoBundleErrorPayload } from './utils/auth/stack_guided_login.mjs';
5
+
6
+ test('parseExpoBundleErrorPayload extracts resolver errors from Metro JSON payloads', () => {
7
+ const payload = JSON.stringify({
8
+ type: 'UnableToResolveError',
9
+ message: 'Unable to resolve module ../ops from /tmp/taskSessionLink.ts',
10
+ });
11
+ const parsed = parseExpoBundleErrorPayload(payload);
12
+ assert.equal(parsed.type, 'UnableToResolveError');
13
+ assert.match(parsed.message, /Unable to resolve module/i);
14
+ assert.equal(parsed.isResolverError, true);
15
+ });
16
+
17
+ test('parseExpoBundleErrorPayload returns null for non-json payloads', () => {
18
+ const parsed = parseExpoBundleErrorPayload('<html>500</html>');
19
+ assert.equal(parsed, null);
20
+ });
@@ -0,0 +1,46 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { buildStackAuthLoginInvocation } from './utils/auth/stack_guided_login.mjs';
5
+ import { getStackRootFromMeta } from './testkit/auth_testkit.mjs';
6
+
7
+ test('guided stack auth login invokes core happier auth login directly', () => {
8
+ const rootDir = getStackRootFromMeta(import.meta.url);
9
+ const webappUrl = 'http://localhost:1234';
10
+ const inv = buildStackAuthLoginInvocation({ rootDir, stackName: 'main', webappUrl });
11
+ assert.ok(Array.isArray(inv?.args));
12
+ assert.match(String(inv.args[0] ?? ''), /apps\/cli\/bin\/happier\.mjs$/);
13
+ assert.equal(inv.args[1], 'auth');
14
+ assert.ok(inv.args.includes('login'));
15
+ assert.equal(inv?.env?.HAPPIER_WEBAPP_URL, webappUrl);
16
+ assert.notEqual(inv?.env?.HAPPIER_STACK_AUTH_INNER, '1');
17
+ });
18
+
19
+ test('guided stack auth login defaults stack name to main and preserves invocation ordering', () => {
20
+ const rootDir = getStackRootFromMeta(import.meta.url);
21
+ const webappUrl = 'http://localhost:4321';
22
+ const inv = buildStackAuthLoginInvocation({ rootDir, stackName: ' ', webappUrl });
23
+ assert.equal(inv.args[1], 'auth');
24
+ assert.equal(inv.args[2], 'login');
25
+ assert.equal(inv.env.HAPPIER_WEBAPP_URL, webappUrl);
26
+ });
27
+
28
+ test('guided stack auth login invocation merges caller env', () => {
29
+ const rootDir = getStackRootFromMeta(import.meta.url);
30
+ const inv = buildStackAuthLoginInvocation({
31
+ rootDir,
32
+ stackName: 'feature-1',
33
+ webappUrl: 'http://localhost:5555',
34
+ env: { CUSTOM_FLAG: 'yes', HAPPIER_STACK_AUTH_INNER: '0' },
35
+ });
36
+ assert.equal(inv.env.CUSTOM_FLAG, 'yes');
37
+ assert.equal(inv.env.HAPPIER_STACK_AUTH_INNER, '0');
38
+ });
39
+
40
+ test('guided stack auth login invocation rejects empty webappUrl', () => {
41
+ const rootDir = getStackRootFromMeta(import.meta.url);
42
+ assert.throws(
43
+ () => buildStackAuthLoginInvocation({ rootDir, stackName: 'main', webappUrl: ' ' }),
44
+ /requires a webappUrl/i
45
+ );
46
+ });
@@ -0,0 +1,263 @@
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 { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { runNodeCapture } from './testkit/stack_script_command_testkit.mjs';
8
+
9
+ const scriptsDir = dirname(fileURLToPath(import.meta.url));
10
+ const rootDir = dirname(scriptsDir);
11
+
12
+ async function ensureMinimalHappierMonorepo({ monoRoot }) {
13
+ await mkdir(join(monoRoot, 'apps', 'ui'), { recursive: true });
14
+ await mkdir(join(monoRoot, 'apps', 'cli'), { recursive: true });
15
+ await mkdir(join(monoRoot, 'apps', 'server'), { recursive: true });
16
+ await writeFile(join(monoRoot, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
17
+ await writeFile(join(monoRoot, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
18
+ await writeFile(join(monoRoot, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
19
+ }
20
+
21
+ async function writeStubHappyCli({ cliDir, message }) {
22
+ await mkdir(join(cliDir, 'dist'), { recursive: true });
23
+ await writeFile(
24
+ join(cliDir, 'dist', 'index.mjs'),
25
+ [
26
+ `console.log(JSON.stringify({`,
27
+ ` message: ${JSON.stringify(message)},`,
28
+ ` stack: process.env.HAPPIER_STACK_STACK || null,`,
29
+ ` envFile: process.env.HAPPIER_STACK_ENV_FILE || null,`,
30
+ ` homeDir: process.env.HAPPIER_HOME_DIR || null,`,
31
+ ` serverUrl: process.env.HAPPIER_SERVER_URL || null,`,
32
+ ` webappUrl: process.env.HAPPIER_WEBAPP_URL || null,`,
33
+ `}));`,
34
+ ].join('\n'),
35
+ 'utf-8'
36
+ );
37
+ }
38
+
39
+ async function writeFailingStubHappyCli({ cliDir, errorMessage }) {
40
+ await mkdir(join(cliDir, 'dist'), { recursive: true });
41
+ await writeFile(join(cliDir, 'dist', 'index.mjs'), `console.error(${JSON.stringify(errorMessage)});\nprocess.exit(1);\n`, 'utf-8');
42
+ }
43
+
44
+ async function createHappyStackFixture(
45
+ t,
46
+ {
47
+ prefix,
48
+ stackName = 'exp-test',
49
+ serverPort = 3999,
50
+ stubType = 'success',
51
+ message = 'hello',
52
+ errorMessage = 'stub failure',
53
+ includePinnedServerPortInEnvFile = true,
54
+ runtimeOwnerPid = null,
55
+ runtimeServerPid = null,
56
+ } = {}
57
+ ) {
58
+ const tmp = await mkdtemp(join(tmpdir(), prefix));
59
+ t.after(async () => {
60
+ await rm(tmp, { recursive: true, force: true });
61
+ });
62
+
63
+ const storageDir = join(tmp, 'storage');
64
+ const homeDir = join(tmp, 'home');
65
+ const workspaceDir = join(tmp, 'workspace');
66
+ const monoRoot = join(workspaceDir, 'happier');
67
+ const cliDir = join(monoRoot, 'apps', 'cli');
68
+ await ensureMinimalHappierMonorepo({ monoRoot });
69
+
70
+ if (stubType === 'failing') {
71
+ await writeFailingStubHappyCli({ cliDir, errorMessage });
72
+ } else {
73
+ await writeStubHappyCli({ cliDir, message });
74
+ }
75
+
76
+ const stackCliHome = join(storageDir, stackName, 'cli');
77
+ const envPath = join(storageDir, stackName, 'env');
78
+ await mkdir(dirname(envPath), { recursive: true });
79
+ await writeFile(
80
+ envPath,
81
+ [
82
+ `HAPPIER_STACK_REPO_DIR=${monoRoot}`,
83
+ `HAPPIER_STACK_CLI_HOME_DIR=${stackCliHome}`,
84
+ ...(includePinnedServerPortInEnvFile ? [`HAPPIER_STACK_SERVER_PORT=${serverPort}`] : []),
85
+ '',
86
+ ].join('\n'),
87
+ 'utf-8'
88
+ );
89
+
90
+ if (runtimeOwnerPid !== null || runtimeServerPid !== null) {
91
+ await writeFile(
92
+ join(storageDir, stackName, 'stack.runtime.json'),
93
+ JSON.stringify(
94
+ {
95
+ version: 1,
96
+ stackName,
97
+ ephemeral: true,
98
+ ownerPid: runtimeOwnerPid,
99
+ ports: { server: serverPort },
100
+ processes: { serverPid: runtimeServerPid },
101
+ },
102
+ null,
103
+ 2
104
+ ) + '\n',
105
+ 'utf-8'
106
+ );
107
+ }
108
+
109
+ return {
110
+ stackName,
111
+ storageDir,
112
+ baseEnv: {
113
+ ...process.env,
114
+ HAPPIER_STACK_HOME_DIR: homeDir,
115
+ HAPPIER_STACK_STORAGE_DIR: storageDir,
116
+ HAPPIER_STACK_WORKSPACE_DIR: workspaceDir,
117
+ HAPPIER_STACK_CLI_ROOT_DISABLE: '1',
118
+ },
119
+ };
120
+ }
121
+
122
+ test('hstack stack happier <name> runs CLI under that stack env', async (t) => {
123
+ const fixture = await createHappyStackFixture(t, {
124
+ prefix: 'happier-stack-stack-happy-',
125
+ message: 'hello',
126
+ serverPort: 3999,
127
+ });
128
+
129
+ const res = await runNodeCapture([join(rootDir, 'bin', 'hstack.mjs'), 'stack', 'happier', fixture.stackName], {
130
+ cwd: rootDir,
131
+ env: fixture.baseEnv,
132
+ });
133
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
134
+
135
+ const out = JSON.parse(res.stdout.trim());
136
+ assert.equal(out.message, 'hello');
137
+ assert.equal(out.stack, fixture.stackName);
138
+ assert.ok(String(out.envFile).endsWith(`/${fixture.stackName}/env`), `expected envFile to end with /${fixture.stackName}/env, got: ${out.envFile}`);
139
+ assert.equal(out.homeDir, join(fixture.storageDir, fixture.stackName, 'cli'));
140
+ assert.equal(out.serverUrl, 'http://127.0.0.1:3999');
141
+ });
142
+
143
+ test('hstack stack happier <name> overrides pre-set HAPPIER_* env vars with stack-scoped values', async (t) => {
144
+ const fixture = await createHappyStackFixture(t, {
145
+ prefix: 'happier-stack-stack-happy-override-',
146
+ message: 'override',
147
+ serverPort: 4123,
148
+ });
149
+
150
+ const res = await runNodeCapture([join(rootDir, 'bin', 'hstack.mjs'), 'stack', 'happier', fixture.stackName], {
151
+ cwd: rootDir,
152
+ env: {
153
+ ...fixture.baseEnv,
154
+ HAPPIER_HOME_DIR: join(fixture.storageDir, 'wrong', 'cli'),
155
+ HAPPIER_SERVER_URL: 'http://127.0.0.1:3005',
156
+ HAPPIER_WEBAPP_URL: 'http://wrong-webapp.example.test',
157
+ },
158
+ });
159
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
160
+
161
+ const out = JSON.parse(res.stdout.trim());
162
+ assert.equal(out.message, 'override');
163
+ assert.equal(out.stack, fixture.stackName);
164
+ assert.equal(out.homeDir, join(fixture.storageDir, fixture.stackName, 'cli'));
165
+ assert.equal(out.serverUrl, 'http://127.0.0.1:4123');
166
+ });
167
+
168
+ test('hstack stack happier <name> uses stack.runtime.json ports when env file does not pin HAPPIER_STACK_SERVER_PORT', async (t) => {
169
+ const fixture = await createHappyStackFixture(t, {
170
+ prefix: 'happier-stack-stack-happy-runtime-ports-',
171
+ message: 'runtime-ports',
172
+ serverPort: 4777,
173
+ includePinnedServerPortInEnvFile: false,
174
+ // Simulate a stale owner pid but a still-running server process.
175
+ runtimeOwnerPid: 999999,
176
+ runtimeServerPid: process.pid,
177
+ });
178
+
179
+ const res = await runNodeCapture([join(rootDir, 'bin', 'hstack.mjs'), 'stack', 'happier', fixture.stackName], {
180
+ cwd: rootDir,
181
+ env: fixture.baseEnv,
182
+ });
183
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
184
+
185
+ const out = JSON.parse(res.stdout.trim());
186
+ assert.equal(out.message, 'runtime-ports');
187
+ assert.equal(out.stack, fixture.stackName);
188
+ assert.equal(out.serverUrl, 'http://127.0.0.1:4777');
189
+ });
190
+
191
+ test('hstack stack happier <name> --identity=<name> uses identity-scoped HAPPIER_HOME_DIR', async (t) => {
192
+ const fixture = await createHappyStackFixture(t, {
193
+ prefix: 'happier-stack-stack-happy-identity-',
194
+ message: 'identity',
195
+ serverPort: 3999,
196
+ });
197
+ const identity = 'account-a';
198
+
199
+ const res = await runNodeCapture(
200
+ [join(rootDir, 'bin', 'hstack.mjs'), 'stack', 'happier', fixture.stackName, `--identity=${identity}`],
201
+ { cwd: rootDir, env: fixture.baseEnv }
202
+ );
203
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
204
+
205
+ const out = JSON.parse(res.stdout.trim());
206
+ assert.equal(out.message, 'identity');
207
+ assert.equal(out.stack, fixture.stackName);
208
+ assert.equal(out.homeDir, join(fixture.storageDir, fixture.stackName, 'cli-identities', identity));
209
+ assert.equal(out.serverUrl, 'http://127.0.0.1:3999');
210
+ });
211
+
212
+ test('hstack <stack> happier ... shorthand runs CLI under that stack env', async (t) => {
213
+ const fixture = await createHappyStackFixture(t, {
214
+ prefix: 'happy-stacks-stack-happy-',
215
+ message: 'shorthand',
216
+ serverPort: 4101,
217
+ });
218
+
219
+ const res = await runNodeCapture([join(rootDir, 'bin', 'hstack.mjs'), fixture.stackName, 'happier'], { cwd: rootDir, env: fixture.baseEnv });
220
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
221
+
222
+ const out = JSON.parse(res.stdout.trim());
223
+ assert.equal(out.message, 'shorthand');
224
+ assert.equal(out.stack, fixture.stackName);
225
+ assert.equal(out.serverUrl, 'http://127.0.0.1:4101');
226
+ });
227
+
228
+ test('hstack stack happier <name> does not print wrapper stack traces on CLI failure', async (t) => {
229
+ const fixture = await createHappyStackFixture(t, {
230
+ prefix: 'happy-stacks-stack-happy-fail-',
231
+ stubType: 'failing',
232
+ errorMessage: 'stub failure',
233
+ serverPort: 3999,
234
+ });
235
+
236
+ const res = await runNodeCapture([join(rootDir, 'bin', 'hstack.mjs'), 'stack', 'happier', fixture.stackName, 'attach', 'abc'], {
237
+ cwd: rootDir,
238
+ env: fixture.baseEnv,
239
+ });
240
+ assert.equal(res.code, 1, `expected exit 1, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
241
+ assert.ok(res.stderr.includes('stub failure'), `expected stderr to include stub failure, got:\n${res.stderr}`);
242
+ assert.ok(!res.stderr.includes('[happier] failed:'), `expected no [happier] failed stack trace, got:\n${res.stderr}`);
243
+ assert.ok(!res.stderr.includes('[stack] failed:'), `expected no [stack] failed stack trace, got:\n${res.stderr}`);
244
+ assert.ok(!res.stderr.includes('node:internal'), `expected no node:internal stack trace, got:\n${res.stderr}`);
245
+ });
246
+
247
+ test('hstack stack <name> happier ... stack-name-first shorthand works', async (t) => {
248
+ const fixture = await createHappyStackFixture(t, {
249
+ prefix: 'happier-stack-stack-happy-name-first-',
250
+ message: 'name-first',
251
+ serverPort: 3999,
252
+ });
253
+
254
+ const res = await runNodeCapture([join(rootDir, 'bin', 'hstack.mjs'), 'stack', fixture.stackName, 'happier'], {
255
+ cwd: rootDir,
256
+ env: fixture.baseEnv,
257
+ });
258
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
259
+
260
+ const out = JSON.parse(res.stdout.trim());
261
+ assert.equal(out.message, 'name-first');
262
+ assert.equal(out.stack, fixture.stackName);
263
+ });
@@ -0,0 +1,186 @@
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
+ import net from 'node:net';
7
+
8
+ import { readStackInfoSnapshot } from './stack/stack_info_snapshot.mjs';
9
+
10
+ function withStorageDir(storageDir) {
11
+ const prev = process.env.HAPPIER_STACK_STORAGE_DIR;
12
+ process.env.HAPPIER_STACK_STORAGE_DIR = storageDir;
13
+ return () => {
14
+ if (typeof prev === 'undefined') {
15
+ delete process.env.HAPPIER_STACK_STORAGE_DIR;
16
+ } else {
17
+ process.env.HAPPIER_STACK_STORAGE_DIR = prev;
18
+ }
19
+ };
20
+ }
21
+
22
+ async function withListeningServer() {
23
+ const server = net.createServer();
24
+ await new Promise((resolve, reject) => {
25
+ server.once('error', reject);
26
+ server.listen(0, '127.0.0.1', resolve);
27
+ });
28
+ const addr = server.address();
29
+ const port = typeof addr === 'object' && addr ? Number(addr.port) : 0;
30
+ return {
31
+ port,
32
+ async close() {
33
+ await new Promise((resolve) => server.close(() => resolve()));
34
+ },
35
+ };
36
+ }
37
+
38
+ async function reserveUnusedPort() {
39
+ const listener = await withListeningServer();
40
+ const port = listener.port;
41
+ await listener.close();
42
+ return port;
43
+ }
44
+
45
+ test('readStackInfoSnapshot reports running when owner pid is stale but an infra pid is alive', async () => {
46
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-info-running-process-'));
47
+ const storageDir = join(tmp, 'storage');
48
+ const stackName = 'dev-auth';
49
+ const baseDir = join(storageDir, stackName);
50
+
51
+ await mkdir(baseDir, { recursive: true });
52
+ await writeFile(join(baseDir, 'env'), 'HAPPIER_STACK_SERVER_PORT=3009\n', 'utf-8');
53
+ await writeFile(
54
+ join(baseDir, 'stack.runtime.json'),
55
+ JSON.stringify({
56
+ version: 1,
57
+ stackName,
58
+ ownerPid: 999_999_999,
59
+ processes: { serverPid: process.pid },
60
+ ports: { server: 3009 },
61
+ }) + '\n',
62
+ 'utf-8'
63
+ );
64
+
65
+ const restore = withStorageDir(storageDir);
66
+ try {
67
+ const out = await readStackInfoSnapshot({ rootDir: process.cwd(), stackName });
68
+ assert.equal(out.runtime.running, true);
69
+ assert.equal(out.runtime.runningPid, process.pid);
70
+ } finally {
71
+ restore();
72
+ await rm(tmp, { recursive: true, force: true });
73
+ }
74
+ });
75
+
76
+ test('readStackInfoSnapshot reports running when owner pid is stale but stack server port is occupied', async () => {
77
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-info-running-port-'));
78
+ const storageDir = join(tmp, 'storage');
79
+ const stackName = 'dev-auth';
80
+ const baseDir = join(storageDir, stackName);
81
+
82
+ await mkdir(baseDir, { recursive: true });
83
+
84
+ const listener = await withListeningServer();
85
+ await writeFile(join(baseDir, 'env'), `HAPPIER_STACK_SERVER_PORT=${listener.port}\n`, 'utf-8');
86
+ await writeFile(
87
+ join(baseDir, 'stack.runtime.json'),
88
+ JSON.stringify({
89
+ version: 1,
90
+ stackName,
91
+ ownerPid: 999_999_999,
92
+ processes: { serverPid: 999_999_998 },
93
+ ports: { server: listener.port },
94
+ }) + '\n',
95
+ 'utf-8'
96
+ );
97
+
98
+ const restore = withStorageDir(storageDir);
99
+ try {
100
+ const out = await readStackInfoSnapshot({ rootDir: process.cwd(), stackName });
101
+ assert.equal(out.runtime.running, true);
102
+ assert.equal(out.runtime.runningPid, null);
103
+ } finally {
104
+ restore();
105
+ await listener.close();
106
+ await rm(tmp, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ test('readStackInfoSnapshot marks UI as down when expo runtime metadata is stale', async () => {
111
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-info-ui-stale-'));
112
+ const storageDir = join(tmp, 'storage');
113
+ const stackName = 'dev-auth';
114
+ const baseDir = join(storageDir, stackName);
115
+
116
+ await mkdir(baseDir, { recursive: true });
117
+
118
+ const serverListener = await withListeningServer();
119
+ const staleUiPort = await reserveUnusedPort();
120
+ await writeFile(join(baseDir, 'env'), `HAPPIER_STACK_SERVER_PORT=${serverListener.port}\n`, 'utf-8');
121
+ await writeFile(
122
+ join(baseDir, 'stack.runtime.json'),
123
+ JSON.stringify({
124
+ version: 1,
125
+ stackName,
126
+ ownerPid: 999_999_999,
127
+ processes: { serverPid: process.pid, expoPid: 999_999_998 },
128
+ ports: { server: serverListener.port },
129
+ expo: { webPort: staleUiPort, webEnabled: true },
130
+ }) + '\n',
131
+ 'utf-8'
132
+ );
133
+
134
+ const restore = withStorageDir(storageDir);
135
+ try {
136
+ const out = await readStackInfoSnapshot({ rootDir: process.cwd(), stackName });
137
+ assert.equal(out.runtime.running, true);
138
+ assert.equal(out.runtime.components.server.running, true);
139
+ assert.equal(out.runtime.components.ui.running, false);
140
+ assert.equal(out.runtime.health.status, 'degraded');
141
+ assert.deepEqual(out.runtime.health.issues, ['ui_down']);
142
+ } finally {
143
+ restore();
144
+ await serverListener.close();
145
+ await rm(tmp, { recursive: true, force: true });
146
+ }
147
+ });
148
+
149
+ test('readStackInfoSnapshot requires UI port reachability even when expo pid is alive', async () => {
150
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-info-ui-unreachable-'));
151
+ const storageDir = join(tmp, 'storage');
152
+ const stackName = 'dev-auth';
153
+ const baseDir = join(storageDir, stackName);
154
+
155
+ await mkdir(baseDir, { recursive: true });
156
+
157
+ const serverListener = await withListeningServer();
158
+ const staleUiPort = await reserveUnusedPort();
159
+ await writeFile(join(baseDir, 'env'), `HAPPIER_STACK_SERVER_PORT=${serverListener.port}\n`, 'utf-8');
160
+ await writeFile(
161
+ join(baseDir, 'stack.runtime.json'),
162
+ JSON.stringify({
163
+ version: 1,
164
+ stackName,
165
+ ownerPid: 999_999_999,
166
+ processes: { serverPid: process.pid, expoPid: process.pid },
167
+ ports: { server: serverListener.port },
168
+ expo: { webPort: staleUiPort, webEnabled: true },
169
+ }) + '\n',
170
+ 'utf-8'
171
+ );
172
+
173
+ const restore = withStorageDir(storageDir);
174
+ try {
175
+ const out = await readStackInfoSnapshot({ rootDir: process.cwd(), stackName });
176
+ assert.equal(out.runtime.running, true);
177
+ assert.equal(out.runtime.components.ui.pidAlive, true);
178
+ assert.equal(out.runtime.components.ui.running, false);
179
+ assert.equal(out.runtime.health.status, 'degraded');
180
+ assert.deepEqual(out.runtime.health.issues, ['ui_down']);
181
+ } finally {
182
+ restore();
183
+ await serverListener.close();
184
+ await rm(tmp, { recursive: true, force: true });
185
+ }
186
+ });