@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,274 @@
1
+ import './utils/env/env.mjs';
2
+ import { parseArgs } from './utils/cli/args.mjs';
3
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
4
+ import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
5
+ import { ensureDepsInstalled } from './utils/proc/pm.mjs';
6
+ import { ensureHappyMonorepoNestedDepsInstalled } from './utils/proc/happy_monorepo_deps.mjs';
7
+ import { pathExists } from './utils/fs/fs.mjs';
8
+ import { run, runCapture } from './utils/proc/proc.mjs';
9
+ import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
10
+ import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
11
+ import { readdir, readFile } from 'node:fs/promises';
12
+ import { dirname, join, sep } from 'node:path';
13
+
14
+ const EXTRA_COMPONENTS = ['stacks'];
15
+ const VALID_TARGETS = ['ui', 'cli', 'server'];
16
+ const VALID_COMPONENTS = [...VALID_TARGETS, ...EXTRA_COMPONENTS, 'all'];
17
+
18
+ function targetFromComponentToken(component) {
19
+ const c = String(component ?? '').trim();
20
+ if (!c) return null;
21
+ if (c === 'ui' || c === 'cli' || c === 'server' || c === 'stacks' || c === 'all') return c;
22
+
23
+ // Modern (Happier) component ids:
24
+ if (c === 'happier-ui') return 'ui';
25
+ if (c === 'happier-cli') return 'cli';
26
+ if (c === 'happier-server' || c === 'happier-server-light') return 'server';
27
+
28
+ // Legacy (Happy) ids:
29
+ if (c === 'happy') return 'ui';
30
+ if (c === 'happy-cli') return 'cli';
31
+ if (c === 'happy-server' || c === 'happy-server-light') return 'server';
32
+
33
+ return null;
34
+ }
35
+
36
+ function componentFromTarget(target) {
37
+ const t = String(target ?? '').trim();
38
+ if (t === 'ui') return 'happier-ui';
39
+ if (t === 'cli') return 'happier-cli';
40
+ if (t === 'server') return 'happier-server';
41
+ return null;
42
+ }
43
+
44
+ function normalizeTargetsOrThrow(rawTargets) {
45
+ const requested = Array.isArray(rawTargets) ? rawTargets.map((t) => String(t ?? '').trim()).filter(Boolean) : [];
46
+ if (!requested.length) return ['all'];
47
+
48
+ const mapped = requested
49
+ .map((t) => {
50
+ const lower = t.toLowerCase();
51
+ return targetFromComponentToken(lower);
52
+ })
53
+ .filter(Boolean);
54
+
55
+ if (!mapped.length) return ['all'];
56
+ return mapped;
57
+ }
58
+
59
+ async function collectTestFiles(dir) {
60
+ const entries = await readdir(dir, { withFileTypes: true });
61
+ const files = [];
62
+ for (const e of entries) {
63
+ // Avoid dot-dirs and dot-files (e.g. .DS_Store).
64
+ if (e.name.startsWith('.')) continue;
65
+ const p = join(dir, e.name);
66
+ if (e.isDirectory()) {
67
+ files.push(...(await collectTestFiles(p)));
68
+ continue;
69
+ }
70
+ if (!e.isFile()) continue;
71
+ if (!e.name.endsWith('.test.mjs')) continue;
72
+ files.push(p);
73
+ }
74
+ files.sort();
75
+ return files;
76
+ }
77
+
78
+ function pickTestScript(scripts) {
79
+ const candidates = [
80
+ 'test',
81
+ 'tst',
82
+ 'test:ci',
83
+ 'test:unit',
84
+ 'check:test',
85
+ ];
86
+ return pickFirstScript(scripts, candidates);
87
+ }
88
+
89
+ async function resolveTestDirForComponent({ component, dir }) {
90
+ // Monorepo mode:
91
+ // When the UI component dir resolves to a subdirectory (apps/ui, legacy expo-app, etc),
92
+ // we prefer running tests from the monorepo root scripts when available.
93
+ if (component !== 'happier-ui') return dir;
94
+
95
+ const abs = dir;
96
+ const isLegacyExpoApp = abs.endsWith(`${sep}expo-app`) || abs.endsWith('/expo-app');
97
+ const isAppsUi = abs.endsWith(`${sep}apps${sep}ui`) || abs.endsWith('/apps/ui');
98
+ const isLegacyPackagesApp = abs.endsWith(`${sep}packages${sep}app`) || abs.endsWith('/packages/app');
99
+ if (!isLegacyExpoApp && !isAppsUi && !isLegacyPackagesApp) return dir;
100
+
101
+ const parent = isAppsUi || isLegacyPackagesApp ? dirname(dirname(abs)) : dirname(abs);
102
+ try {
103
+ const scripts = await readPackageJsonScripts(parent);
104
+ if (!scripts) return dir;
105
+ if ((scripts?.test ?? '').toString().trim().length === 0) return dir;
106
+
107
+ // Only redirect when the parent is clearly intended as the monorepo root.
108
+ const pkg = JSON.parse(await readFile(join(parent, 'package.json'), 'utf-8'));
109
+ const name = String(pkg?.name ?? '').trim();
110
+ if (name !== 'monorepo') return dir;
111
+ return parent;
112
+ } catch {
113
+ return dir;
114
+ }
115
+ }
116
+
117
+ async function main() {
118
+ const argv = process.argv.slice(2);
119
+ const { flags } = parseArgs(argv);
120
+ const json = wantsJson(argv, { flags });
121
+
122
+ if (wantsHelp(argv, { flags })) {
123
+ printResult({
124
+ json,
125
+ data: { components: VALID_COMPONENTS, flags: ['--json'] },
126
+ text: [
127
+ '[test] usage:',
128
+ ' hstack test [ui|cli|server|all|stacks] [--json]',
129
+ '',
130
+ 'targets:',
131
+ ` ${VALID_COMPONENTS.join(' | ')}`,
132
+ '',
133
+ 'examples:',
134
+ ' hstack test',
135
+ ' hstack test stacks',
136
+ ' hstack test ui cli',
137
+ '',
138
+ 'note:',
139
+ ' If run from inside a repo checkout/worktree and no targets are provided, defaults to the inferred app (ui/cli/server).',
140
+ ].join('\n'),
141
+ });
142
+ return;
143
+ }
144
+
145
+ const rootDir = getRootDir(import.meta.url);
146
+
147
+ const positionals = argv.filter((a) => !a.startsWith('--'));
148
+ const inferred =
149
+ positionals.length === 0
150
+ ? inferComponentFromCwd({
151
+ rootDir,
152
+ invokedCwd: getInvokedCwd(process.env),
153
+ components: ['happier-ui', 'happier-cli', 'happier-server'],
154
+ })
155
+ : null;
156
+ if (inferred) {
157
+ if (!(process.env.HAPPIER_STACK_REPO_DIR ?? '').toString().trim()) {
158
+ process.env.HAPPIER_STACK_REPO_DIR = inferred.repoDir;
159
+ }
160
+ }
161
+
162
+ const inferredTarget = inferred ? targetFromComponentToken(inferred.component) : null;
163
+ const requested = normalizeTargetsOrThrow(positionals.length ? positionals : inferredTarget ? [inferredTarget] : ['all']);
164
+ const wantAll = requested.includes('all');
165
+ // Default `all` excludes "stacks" to avoid coupling to stack tests and their baselines.
166
+ const targets = wantAll ? VALID_TARGETS : requested;
167
+
168
+ const results = [];
169
+ for (const target of targets) {
170
+ if (!VALID_COMPONENTS.includes(target)) {
171
+ results.push({ target, ok: false, skipped: false, error: `unknown target (expected one of: ${VALID_COMPONENTS.join(', ')})` });
172
+ continue;
173
+ }
174
+
175
+ if (target === 'stacks') {
176
+ try {
177
+ // eslint-disable-next-line no-console
178
+ console.log('[test] stacks: running node --test (hstack unit tests)');
179
+ // Note: do not rely on shell glob expansion here.
180
+ // Node 20 does not expand globs for `--test`, and bash/sh won't expand globs inside quotes.
181
+ // Enumerate files ourselves so this works reliably in CI.
182
+ const scriptsDir = join(rootDir, 'scripts');
183
+ const testFiles = await collectTestFiles(scriptsDir);
184
+ if (testFiles.length === 0) {
185
+ throw new Error(`[test] stacks: no test files found under ${scriptsDir}`);
186
+ }
187
+ await run(process.execPath, ['--test', ...testFiles], { cwd: rootDir, env: process.env });
188
+ results.push({ target, ok: true, skipped: false, dir: rootDir, pm: 'node', script: '--test' });
189
+ } catch (e) {
190
+ results.push({ target, ok: false, skipped: false, dir: rootDir, pm: 'node', script: '--test', error: String(e?.message ?? e) });
191
+ }
192
+ continue;
193
+ }
194
+
195
+ const component = componentFromTarget(target);
196
+ const rawDir = getComponentDir(rootDir, component);
197
+ const dir = await resolveTestDirForComponent({ component, dir: rawDir });
198
+ if (!(await pathExists(dir))) {
199
+ results.push({ target, ok: false, skipped: false, dir, error: `missing target dir: ${dir}` });
200
+ continue;
201
+ }
202
+
203
+ const scripts = await readPackageJsonScripts(dir);
204
+ if (!scripts) {
205
+ results.push({ target, ok: true, skipped: true, dir, reason: 'no package.json' });
206
+ continue;
207
+ }
208
+
209
+ const script = pickTestScript(scripts);
210
+ if (!script) {
211
+ results.push({ target, ok: true, skipped: true, dir, reason: 'no test script found in package.json' });
212
+ continue;
213
+ }
214
+
215
+ if (target === 'ui') {
216
+ await ensureHappyMonorepoNestedDepsInstalled({
217
+ happyTestDir: dir,
218
+ quiet: json,
219
+ env: process.env,
220
+ ensureDepsInstalled,
221
+ });
222
+ }
223
+
224
+ await ensureDepsInstalled(dir, target, { quiet: json, env: process.env });
225
+ const pm = await detectPackageManagerCmd(dir);
226
+
227
+ try {
228
+ const line = `[test] ${target}: running ${pm.name} ${script}\n`;
229
+ if (json) {
230
+ process.stderr.write(line);
231
+ const out = await runCapture(pm.cmd, pm.argsForScript(script), { cwd: dir, env: process.env });
232
+ if (out) process.stderr.write(out);
233
+ } else {
234
+ // eslint-disable-next-line no-console
235
+ console.log(line.trimEnd());
236
+ await run(pm.cmd, pm.argsForScript(script), { cwd: dir, env: process.env });
237
+ }
238
+ results.push({ target, ok: true, skipped: false, dir, pm: pm.name, script });
239
+ } catch (e) {
240
+ results.push({ target, ok: false, skipped: false, dir, pm: pm.name, script, error: String(e?.message ?? e) });
241
+ }
242
+ }
243
+
244
+ const ok = results.every((r) => r.ok);
245
+ if (json) {
246
+ printResult({ json, data: { ok, results } });
247
+ return;
248
+ }
249
+
250
+ const lines = ['[test] results:'];
251
+ for (const r of results) {
252
+ if (r.ok && r.skipped) {
253
+ lines.push(`- ↪ ${r.target}: skipped (${r.reason})`);
254
+ } else if (r.ok) {
255
+ lines.push(`- ✅ ${r.target}: ok (${r.pm} ${r.script})`);
256
+ } else {
257
+ lines.push(`- ❌ ${r.target}: failed (${r.pm ?? 'unknown'} ${r.script ?? ''})`);
258
+ if (r.error) lines.push(` - ${r.error}`);
259
+ }
260
+ }
261
+ if (!ok) {
262
+ lines.push('');
263
+ lines.push('[test] failed');
264
+ }
265
+ printResult({ json: false, text: lines.join('\n') });
266
+ if (!ok) {
267
+ process.exit(1);
268
+ }
269
+ }
270
+
271
+ main().catch((err) => {
272
+ console.error('[test] failed:', err);
273
+ process.exit(1);
274
+ });
@@ -0,0 +1,133 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ function runNode(args, { cwd, env }) {
10
+ return new Promise((resolve, reject) => {
11
+ const proc = spawn(process.execPath, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
12
+ let stdout = '';
13
+ let stderr = '';
14
+ proc.stdout.on('data', (d) => (stdout += String(d)));
15
+ proc.stderr.on('data', (d) => (stderr += String(d)));
16
+ proc.on('error', reject);
17
+ proc.on('exit', (code, signal) => resolve({ code: code ?? (signal ? 1 : 0), signal: signal ?? null, stdout, stderr }));
18
+ });
19
+ }
20
+
21
+ async function writeYarnOkPackage({ dir, name, scriptOutput }) {
22
+ await mkdir(join(dir, 'node_modules'), { recursive: true });
23
+ await writeFile(join(dir, 'yarn.lock'), '# stub lock\n', 'utf-8');
24
+ await writeFile(join(dir, 'test-script.mjs'), `process.stdout.write(${JSON.stringify(scriptOutput)});\n`, 'utf-8');
25
+ await writeFile(
26
+ join(dir, 'package.json'),
27
+ JSON.stringify(
28
+ {
29
+ name,
30
+ private: true,
31
+ packageManager: 'yarn@1.22.22',
32
+ scripts: {
33
+ test: 'node ./test-script.mjs',
34
+ },
35
+ },
36
+ null,
37
+ 2
38
+ ),
39
+ 'utf-8'
40
+ );
41
+ // Ensure deps are considered "already installed" by hstack.
42
+ await writeFile(join(dir, 'node_modules', '.yarn-integrity'), 'ok\n', 'utf-8');
43
+ }
44
+
45
+ async function setupTestCmdFixture({ importMetaUrl, t, tmpPrefix }) {
46
+ const scriptsDir = dirname(fileURLToPath(importMetaUrl));
47
+ const rootDir = dirname(scriptsDir);
48
+
49
+ const tmp = await mkdtemp(join(tmpdir(), tmpPrefix));
50
+ t.after(async () => {
51
+ await rm(tmp, { recursive: true, force: true }).catch(() => {});
52
+ });
53
+
54
+ const monoRoot = join(tmp, 'mono');
55
+ const appDir = join(monoRoot, 'apps', 'ui');
56
+
57
+ await mkdir(appDir, { recursive: true });
58
+
59
+ await writeYarnOkPackage({ dir: monoRoot, name: 'monorepo', scriptOutput: 'ROOT_TEST_RUN' });
60
+ await writeYarnOkPackage({ dir: appDir, name: 'happy-app', scriptOutput: 'APP_TEST_RUN' });
61
+
62
+ const env = {
63
+ ...process.env,
64
+ HAPPIER_STACK_REPO_DIR: monoRoot,
65
+ // Prevent env.mjs from auto-discovering and loading a real machine stack env file,
66
+ // which would overwrite our component dir override.
67
+ HAPPIER_STACK_STACK: 'test-stack',
68
+ HAPPIER_STACK_ENV_FILE: join(tmp, 'nonexistent-env'),
69
+ };
70
+
71
+ return { rootDir, monoRoot, appDir, env };
72
+ }
73
+
74
+ test('hstack test --json keeps stdout JSON-only and runs monorepo root when happy points at apps/ui', async (t) => {
75
+ const fixture = await setupTestCmdFixture({
76
+ importMetaUrl: import.meta.url,
77
+ t,
78
+ tmpPrefix: 'happy-stacks-test-cmd-json-',
79
+ });
80
+
81
+ const res = await runNode([join(fixture.rootDir, 'scripts', 'test_cmd.mjs'), 'ui', '--json'], {
82
+ cwd: fixture.rootDir,
83
+ env: fixture.env,
84
+ });
85
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`);
86
+
87
+ // Stdout must be JSON only.
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(res.stdout);
91
+ } catch (e) {
92
+ throw new Error(
93
+ `stdout was not valid JSON.\n` +
94
+ `error: ${String(e?.message ?? e)}\n` +
95
+ `stdout:\n${res.stdout}\n` +
96
+ `stderr:\n${res.stderr}\n`
97
+ );
98
+ }
99
+ assert.equal(
100
+ parsed?.ok,
101
+ true,
102
+ `expected ok=true, got:\n${JSON.stringify(parsed, null, 2)}\n\nstderr:\n${res.stderr}\n\nstdout:\n${res.stdout}`
103
+ );
104
+ assert.equal(parsed?.results?.length, 1);
105
+ assert.equal(parsed.results[0].target, 'ui');
106
+
107
+ // Monorepo detection: when happy points at apps/ui, tests should run from the monorepo root.
108
+ assert.equal(parsed.results[0].dir, fixture.monoRoot);
109
+
110
+ // Any command output should be written to stderr (to keep stdout JSON-only).
111
+ assert.ok(res.stderr.includes('ROOT_TEST_RUN'));
112
+ assert.ok(!res.stderr.includes('APP_TEST_RUN'));
113
+ });
114
+
115
+ test('hstack test (non-json) keeps monorepo-root routing and reports human-readable summary', async (t) => {
116
+ const fixture = await setupTestCmdFixture({
117
+ importMetaUrl: import.meta.url,
118
+ t,
119
+ tmpPrefix: 'happy-stacks-test-cmd-text-',
120
+ });
121
+
122
+ const res = await runNode([join(fixture.rootDir, 'scripts', 'test_cmd.mjs'), 'ui'], {
123
+ cwd: fixture.rootDir,
124
+ env: fixture.env,
125
+ });
126
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`);
127
+
128
+ assert.ok(res.stdout.includes('[test] ui: running yarn test'), res.stdout);
129
+ assert.ok(res.stdout.includes('ROOT_TEST_RUN'), res.stdout);
130
+ assert.ok(!res.stdout.includes('APP_TEST_RUN'), res.stdout);
131
+ assert.ok(res.stdout.includes('[test] results:'), res.stdout);
132
+ assert.ok(res.stdout.includes('- ✅ ui: ok (yarn test)'), res.stdout);
133
+ });
@@ -0,0 +1,33 @@
1
+ import { join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { collectTestFiles } from './utils/test/collect_test_files.mjs';
4
+
5
+ async function main() {
6
+ const packageRoot = fileURLToPath(new URL('..', import.meta.url));
7
+ const scriptsDir = join(packageRoot, 'scripts');
8
+ const testsDir = join(packageRoot, 'tests');
9
+
10
+ const testFiles = [];
11
+ testFiles.push(...(await collectTestFiles({
12
+ dir: scriptsDir,
13
+ includeSuffixes: ['.integration.test.mjs', '.real.integration.test.mjs'],
14
+ })));
15
+ testFiles.push(...(await collectTestFiles({
16
+ dir: testsDir,
17
+ includeSuffixes: ['.integration.test.mjs', '.real.integration.test.mjs'],
18
+ })));
19
+
20
+ if (testFiles.length === 0) {
21
+ process.stdout.write('[stack:test:integration] no integration test files found; skipping\n');
22
+ process.exit(0);
23
+ }
24
+
25
+ const { spawnSync } = await import('node:child_process');
26
+ const res = spawnSync(process.execPath, ['--test', ...testFiles], { stdio: 'inherit' });
27
+ process.exit(res.status ?? 1);
28
+ }
29
+
30
+ main().catch((e) => {
31
+ process.stderr.write(`[stack:test:integration] ${String(e?.stack ?? e)}\n`);
32
+ process.exit(1);
33
+ });
@@ -0,0 +1,121 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ function sanitizeEnv(env) {
8
+ if (!env) return undefined;
9
+ const clean = {};
10
+ for (const [key, value] of Object.entries(env)) {
11
+ if (value == null) continue;
12
+ clean[key] = String(value);
13
+ }
14
+ return clean;
15
+ }
16
+
17
+ export function getStackRootFromMeta(metaUrl) {
18
+ const scriptsDir = dirname(fileURLToPath(metaUrl));
19
+ return dirname(scriptsDir);
20
+ }
21
+
22
+ export function hstackBinPath(rootDir) {
23
+ return join(rootDir, 'bin', 'hstack.mjs');
24
+ }
25
+
26
+ export function authScriptPath(rootDir) {
27
+ return join(rootDir, 'scripts', 'auth.mjs');
28
+ }
29
+
30
+ export async function runNodeCapture(args, { cwd, env, input } = {}) {
31
+ return await new Promise((resolve, reject) => {
32
+ const usePipeInput = input != null;
33
+ const proc = spawn(process.execPath, args, {
34
+ cwd,
35
+ env: sanitizeEnv(env),
36
+ stdio: [usePipeInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
37
+ });
38
+
39
+ let stdout = '';
40
+ let stderr = '';
41
+ proc.stdout.on('data', (chunk) => {
42
+ stdout += String(chunk);
43
+ });
44
+ proc.stderr.on('data', (chunk) => {
45
+ stderr += String(chunk);
46
+ });
47
+ proc.on('error', reject);
48
+ proc.on('close', (code, signal) => {
49
+ resolve({ code: code ?? (signal ? 128 : 0), signal, stdout, stderr });
50
+ });
51
+
52
+ if (usePipeInput) {
53
+ proc.stdin.write(String(input));
54
+ proc.stdin.end();
55
+ }
56
+ });
57
+ }
58
+
59
+ function wait(ms) {
60
+ return new Promise((resolve) => setTimeout(resolve, ms));
61
+ }
62
+
63
+ function isPidAlive(pid) {
64
+ if (!pid || pid <= 1) return false;
65
+ try {
66
+ process.kill(pid, 0);
67
+ return true;
68
+ } catch (error) {
69
+ return error?.code !== 'ESRCH';
70
+ }
71
+ }
72
+
73
+ export async function terminateChildProcess(child, { signal = 'SIGTERM', timeoutMs = 800 } = {}) {
74
+ if (!child) return true;
75
+ if (child.exitCode != null) return true;
76
+ const waitForExit = new Promise((resolve) => {
77
+ child.once('exit', () => resolve());
78
+ });
79
+
80
+ try {
81
+ child.kill(signal);
82
+ } catch {}
83
+ await Promise.race([waitForExit, wait(timeoutMs)]);
84
+ if (child.exitCode == null) {
85
+ try {
86
+ child.kill('SIGKILL');
87
+ } catch {}
88
+ await Promise.race([waitForExit, wait(timeoutMs)]);
89
+ }
90
+ return !isPidAlive(child.pid);
91
+ }
92
+
93
+ export async function createAuthStackFixture({
94
+ prefix,
95
+ stackName = 'main',
96
+ stackEnvLines = [],
97
+ }) {
98
+ const tmpDir = await mkdtemp(join(tmpdir(), prefix));
99
+ const storageDir = join(tmpDir, 'storage');
100
+ await mkdir(join(storageDir, stackName), { recursive: true });
101
+ const envPath = join(storageDir, stackName, 'env');
102
+ await writeFile(envPath, [...stackEnvLines, ''].join('\n'), 'utf-8');
103
+
104
+ return {
105
+ tmpDir,
106
+ storageDir,
107
+ envPath,
108
+ buildEnv(extra = {}) {
109
+ return {
110
+ ...process.env,
111
+ HAPPIER_STACK_STORAGE_DIR: storageDir,
112
+ HAPPIER_STACK_STACK: stackName,
113
+ HAPPIER_STACK_ENV_FILE: envPath,
114
+ ...extra,
115
+ };
116
+ },
117
+ async cleanup() {
118
+ await rm(tmpDir, { recursive: true, force: true });
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,68 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ export function runNode(args, { cwd, env }) {
7
+ return new Promise((resolve, reject) => {
8
+ const cleanEnv = {};
9
+ for (const [k, v] of Object.entries(env ?? {})) {
10
+ if (v == null) continue;
11
+ cleanEnv[k] = String(v);
12
+ }
13
+ const proc = spawn(process.execPath, args, { cwd, env: cleanEnv, stdio: ['ignore', 'pipe', 'pipe'] });
14
+ let stdout = '';
15
+ let stderr = '';
16
+ proc.stdout.on('data', (d) => (stdout += String(d)));
17
+ proc.stderr.on('data', (d) => (stderr += String(d)));
18
+ proc.on('error', reject);
19
+ proc.on('exit', (code, signal) => resolve({ code: code ?? (signal ? 1 : 0), signal, stdout, stderr }));
20
+ });
21
+ }
22
+
23
+ async function writeStubHappierCli({ dir }) {
24
+ await mkdir(join(dir, 'bin'), { recursive: true });
25
+ await mkdir(join(dir, 'dist'), { recursive: true });
26
+ await writeFile(
27
+ join(dir, 'bin', 'happier.mjs'),
28
+ [
29
+ `if (process.argv.includes('daemon') && process.argv.includes('status')) {`,
30
+ ` console.log('Daemon is running');`,
31
+ ` process.exit(0);`,
32
+ `}`,
33
+ `console.log('ok');`,
34
+ ].join('\n'),
35
+ 'utf-8'
36
+ );
37
+ await writeFile(join(dir, 'dist', 'index.mjs'), `export {};`, 'utf-8');
38
+ }
39
+
40
+ export async function createDoctorWorkspaceFixture(t, { tmpPrefix = 'happier-stack-doctor-' } = {}) {
41
+ const tmp = await mkdtemp(join(tmpdir(), tmpPrefix));
42
+ t.after(async () => {
43
+ await rm(tmp, { recursive: true, force: true });
44
+ });
45
+
46
+ const monoRoot = join(tmp, 'workspace', 'happier');
47
+ const stubServer = join(monoRoot, 'apps', 'server');
48
+ const stubCli = join(monoRoot, 'apps', 'cli');
49
+ const stubUi = join(monoRoot, 'apps', 'ui');
50
+ await mkdir(stubUi, { recursive: true });
51
+ await mkdir(stubServer, { recursive: true });
52
+ await mkdir(stubCli, { recursive: true });
53
+ await writeFile(join(stubUi, 'package.json'), '{}\n', 'utf-8');
54
+ await writeFile(join(stubCli, 'package.json'), '{}\n', 'utf-8');
55
+ await writeFile(join(stubServer, 'package.json'), '{}\n', 'utf-8');
56
+ await writeStubHappierCli({ dir: stubCli });
57
+
58
+ return { tmp, monoRoot };
59
+ }
60
+
61
+ export function doctorEnv({ monoRoot, extraEnv = {} } = {}) {
62
+ return {
63
+ ...process.env,
64
+ HAPPIER_STACK_REPO_DIR: monoRoot,
65
+ HAPPIER_STACK_CLI_ROOT_DISABLE: '1',
66
+ ...extraEnv,
67
+ };
68
+ }