@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,49 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { ensureEnvFilePruned, ensureEnvFileUpdated } from './env_file.mjs';
8
+
9
+ async function withTempRoot(t) {
10
+ const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-env-file-'));
11
+ t.after(async () => {
12
+ await rm(dir, { recursive: true, force: true });
13
+ });
14
+ return dir;
15
+ }
16
+
17
+ test('ensureEnvFileUpdated appends new key and ensures trailing newline', async (t) => {
18
+ const dir = await withTempRoot(t);
19
+ const envPath = join(dir, 'env');
20
+
21
+ await ensureEnvFileUpdated({ envPath, updates: [{ key: 'OPENAI_API_KEY', value: 'sk-test' }] });
22
+ const next = await readFile(envPath, 'utf-8');
23
+ assert.equal(next, 'OPENAI_API_KEY=sk-test\n');
24
+ });
25
+
26
+ test('ensureEnvFileUpdated does not touch file when no content changes', async (t) => {
27
+ const dir = await withTempRoot(t);
28
+ const envPath = join(dir, 'env');
29
+
30
+ await writeFile(envPath, 'FOO=bar\n', 'utf-8');
31
+ const oldTime = new Date('2001-01-01T00:00:00.000Z');
32
+ await utimes(envPath, oldTime, oldTime);
33
+ const before = await stat(envPath);
34
+
35
+ await ensureEnvFileUpdated({ envPath, updates: [{ key: 'FOO', value: 'bar' }] });
36
+ const after = await stat(envPath);
37
+ assert.equal(after.mtimeMs, before.mtimeMs);
38
+ });
39
+
40
+ test('ensureEnvFilePruned removes a key but keeps comments/blank lines', async (t) => {
41
+ const dir = await withTempRoot(t);
42
+ const envPath = join(dir, 'env');
43
+
44
+ await writeFile(envPath, '# header\nFOO=bar\n\nBAZ=qux\n', 'utf-8');
45
+ await ensureEnvFilePruned({ envPath, removeKeys: ['FOO'] });
46
+
47
+ const next = await readFile(envPath, 'utf-8');
48
+ assert.equal(next, '# header\n\nBAZ=qux\n');
49
+ });
@@ -0,0 +1,25 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { ensureEnvFileUpdated } from './env_file.mjs';
5
+ import { ensureUserConfigEnvUpdated, getHomeEnvLocalPath, getHomeEnvPath } from './config.mjs';
6
+
7
+ export async function ensureEnvLocalUpdated({ rootDir, updates }) {
8
+ // Behavior:
9
+ // - If a stack env file is explicitly set, write there (stack-scoped).
10
+ // - If the user has run `hstack init` (home config exists), write to the main stack env file (user config).
11
+ // - If no home config exists (legacy cloned-repo usage), write to <repo>/env.local for repo-local behavior.
12
+ const explicit = (process.env.HAPPIER_STACK_ENV_FILE ?? '').trim();
13
+ if (explicit) {
14
+ await ensureEnvFileUpdated({ envPath: explicit, updates });
15
+ return;
16
+ }
17
+
18
+ const hasHomeConfig = existsSync(getHomeEnvPath()) || existsSync(getHomeEnvLocalPath());
19
+ if (hasHomeConfig) {
20
+ await ensureUserConfigEnvUpdated({ cliRootDir: rootDir, updates });
21
+ return;
22
+ }
23
+
24
+ await ensureEnvFileUpdated({ envPath: join(rootDir, 'env.local'), updates });
25
+ }
@@ -0,0 +1,34 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parseDotenv } from './dotenv.mjs';
3
+
4
+ export async function loadEnvFile(path, { override = false, overridePrefix = null } = {}) {
5
+ try {
6
+ const contents = await readFile(path, 'utf-8');
7
+ const parsed = parseDotenv(contents);
8
+ for (const [k, v] of parsed.entries()) {
9
+ const allowOverride = override && (!overridePrefix || k.startsWith(overridePrefix));
10
+ if (allowOverride || process.env[k] == null || process.env[k] === '') {
11
+ process.env[k] = v;
12
+ }
13
+ }
14
+ } catch {
15
+ // ignore missing/invalid env file
16
+ }
17
+ }
18
+
19
+ export async function loadEnvFileIgnoringPrefixes(path, { ignorePrefixes = [] } = {}) {
20
+ try {
21
+ const contents = await readFile(path, 'utf-8');
22
+ const parsed = parseDotenv(contents);
23
+ for (const [k, v] of parsed.entries()) {
24
+ if (ignorePrefixes.some((p) => k.startsWith(p))) {
25
+ continue;
26
+ }
27
+ if (process.env[k] == null || process.env[k] === '') {
28
+ process.env[k] = v;
29
+ }
30
+ }
31
+ } catch {
32
+ // ignore missing/invalid env file
33
+ }
34
+ }
@@ -0,0 +1,30 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ import { parseDotenv } from './dotenv.mjs';
5
+
6
+ export async function readEnvValueFromFile(envPath, key, { defaultValue = '' } = {}) {
7
+ try {
8
+ const p = String(envPath ?? '').trim();
9
+ const k = String(key ?? '').trim();
10
+ if (!p || !k) return defaultValue;
11
+ if (!existsSync(p)) return defaultValue;
12
+ const raw = await readFile(p, 'utf-8');
13
+ const parsed = parseDotenv(raw ?? '');
14
+ return String(parsed.get(k) ?? '').trim();
15
+ } catch {
16
+ return defaultValue;
17
+ }
18
+ }
19
+
20
+ export async function readEnvObjectFromFile(envPath) {
21
+ try {
22
+ const p = String(envPath ?? '').trim();
23
+ if (!p || !existsSync(p)) return {};
24
+ const raw = await readFile(p, 'utf-8');
25
+ return Object.fromEntries(parseDotenv(raw ?? '').entries());
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
@@ -0,0 +1,13 @@
1
+ export function getSandboxDir() {
2
+ const v = (process.env.HAPPIER_STACK_SANDBOX_DIR ?? '').trim();
3
+ return v || '';
4
+ }
5
+
6
+ export function isSandboxed() {
7
+ return Boolean(getSandboxDir());
8
+ }
9
+
10
+ export function sandboxAllowsGlobalSideEffects() {
11
+ const raw = (process.env.HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL ?? '').trim().toLowerCase();
12
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y';
13
+ }
@@ -0,0 +1,69 @@
1
+ export const SANDBOX_PRESERVE_KEYS = [
2
+ 'HAPPIER_STACK_VERBOSE',
3
+ 'HAPPIER_STACK_INVOKED_CWD',
4
+ 'HAPPIER_STACK_SANDBOX_DIR',
5
+ 'HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL',
6
+ 'HAPPIER_STACK_UPDATE_CHECK',
7
+ 'HAPPIER_STACK_UPDATE_CHECK_INTERVAL_MS',
8
+ 'HAPPIER_STACK_UPDATE_NOTIFY_INTERVAL_MS',
9
+ ];
10
+
11
+ export const STACK_WRAPPER_PRESERVE_KEYS = [
12
+ // Stack/env pointers:
13
+ 'HAPPIER_STACK_ENV_FILE',
14
+ 'HAPPIER_STACK_STACK',
15
+
16
+ // Sandbox detection + policy.
17
+ 'HAPPIER_STACK_SANDBOX_DIR',
18
+ 'HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL',
19
+
20
+ // Sandbox-enforced dirs.
21
+ 'HAPPIER_STACK_CLI_ROOT_DISABLE',
22
+ 'HAPPIER_STACK_CANONICAL_HOME_DIR',
23
+ 'HAPPIER_STACK_HOME_DIR',
24
+ 'HAPPIER_STACK_WORKSPACE_DIR',
25
+ 'HAPPIER_STACK_RUNTIME_DIR',
26
+ 'HAPPIER_STACK_STORAGE_DIR',
27
+
28
+ // UX knobs.
29
+ 'HAPPIER_STACK_VERBOSE',
30
+ // TUI marker (set by `hstack tui` and must survive stack env scrubbing).
31
+ 'HAPPIER_STACK_TUI',
32
+ 'HAPPIER_STACK_UPDATE_CHECK',
33
+ 'HAPPIER_STACK_UPDATE_CHECK_INTERVAL_MS',
34
+ 'HAPPIER_STACK_UPDATE_NOTIFY_INTERVAL_MS',
35
+
36
+ // Guided auth flow coordination across wrappers.
37
+ 'HAPPIER_STACK_DAEMON_WAIT_FOR_AUTH',
38
+ 'HAPPIER_STACK_AUTH_FLOW',
39
+
40
+ // Safe global defaults.
41
+ 'HAPPIER_STACK_STACK_PORT_START',
42
+
43
+ 'HAPPIER_STACK_BIND_MODE',
44
+ 'HAPPIER_STACK_EXPO_HOST',
45
+
46
+ // Expo dev-server port strategy.
47
+ 'HAPPIER_STACK_EXPO_DEV_PORT',
48
+ 'HAPPIER_STACK_EXPO_DEV_PORT_STRATEGY',
49
+ 'HAPPIER_STACK_EXPO_DEV_PORT_BASE',
50
+ 'HAPPIER_STACK_EXPO_DEV_PORT_RANGE',
51
+ ];
52
+
53
+ export function scrubHappierStackEnv(
54
+ env,
55
+ { keepHappierStackKeys = [], clearUnprefixedKeys = [] } = {},
56
+ ) {
57
+ const input = env && typeof env === 'object' ? env : {};
58
+ const out = { ...input };
59
+ const keep = new Set((keepHappierStackKeys ?? []).map((k) => String(k).trim()).filter(Boolean));
60
+ for (const k of Object.keys(out)) {
61
+ if (k.startsWith('HAPPIER_STACK_') && !keep.has(k)) {
62
+ delete out[k];
63
+ }
64
+ }
65
+ for (const k of (clearUnprefixedKeys ?? []).map((x) => String(x).trim()).filter(Boolean)) {
66
+ delete out[k];
67
+ }
68
+ return out;
69
+ }
@@ -0,0 +1,102 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ SANDBOX_PRESERVE_KEYS,
6
+ STACK_WRAPPER_PRESERVE_KEYS,
7
+ scrubHappierStackEnv,
8
+ } from './scrub_env.mjs';
9
+
10
+ test('scrubHappierStackEnv removes non-preserved HAPPIER_STACK_* vars and clears selected unprefixed keys', () => {
11
+ const env = {
12
+ PATH: '/bin',
13
+ HAPPIER_STACK_VERBOSE: '1',
14
+ HAPPIER_STACK_FOO: 'bar',
15
+ HAPPIER_HOME_DIR: '/tmp/happier-home',
16
+ HAPPIER_SERVER_URL: 'http://example.com',
17
+ };
18
+
19
+ const scrubbed = scrubHappierStackEnv(env, {
20
+ keepHappierStackKeys: SANDBOX_PRESERVE_KEYS,
21
+ clearUnprefixedKeys: ['HAPPIER_HOME_DIR', 'HAPPIER_SERVER_URL'],
22
+ });
23
+
24
+ assert.equal(scrubbed.PATH, '/bin');
25
+ assert.equal(scrubbed.HAPPIER_STACK_VERBOSE, '1');
26
+ assert.equal(scrubbed.HAPPIER_STACK_FOO, undefined);
27
+ assert.equal(scrubbed.HAPPIER_HOME_DIR, undefined);
28
+ assert.equal(scrubbed.HAPPIER_SERVER_URL, undefined);
29
+ });
30
+
31
+ test('scrubHappierStackEnv keeps runtime-critical non-HAPPIER env keys', () => {
32
+ const env = {
33
+ PATH: '/bin:/usr/bin',
34
+ HOME: '/tmp/home',
35
+ TMPDIR: '/tmp',
36
+ SHELL: '/bin/zsh',
37
+ HAPPIER_STACK_SECRET: 'drop-me',
38
+ };
39
+
40
+ const scrubbed = scrubHappierStackEnv(env, {
41
+ keepHappierStackKeys: [],
42
+ clearUnprefixedKeys: [],
43
+ });
44
+
45
+ assert.equal(scrubbed.PATH, '/bin:/usr/bin');
46
+ assert.equal(scrubbed.HOME, '/tmp/home');
47
+ assert.equal(scrubbed.TMPDIR, '/tmp');
48
+ assert.equal(scrubbed.SHELL, '/bin/zsh');
49
+ assert.equal(scrubbed.HAPPIER_STACK_SECRET, undefined);
50
+ });
51
+
52
+ test('scrubHappierStackEnv preserves only explicitly kept HAPPIER_STACK keys', () => {
53
+ const env = {
54
+ HAPPIER_STACK_VERBOSE: '1',
55
+ HAPPIER_STACK_SANDBOX_DIR: '/tmp/sandbox',
56
+ HAPPIER_STACK_SECRET: 'drop-me',
57
+ };
58
+ const scrubbed = scrubHappierStackEnv(env, {
59
+ keepHappierStackKeys: [' HAPPIER_STACK_SANDBOX_DIR ', ''],
60
+ clearUnprefixedKeys: [],
61
+ });
62
+
63
+ assert.equal(scrubbed.HAPPIER_STACK_SANDBOX_DIR, '/tmp/sandbox');
64
+ assert.equal(scrubbed.HAPPIER_STACK_VERBOSE, undefined);
65
+ assert.equal(scrubbed.HAPPIER_STACK_SECRET, undefined);
66
+ });
67
+
68
+ test('scrubHappierStackEnv trims and de-duplicates clearUnprefixedKeys', () => {
69
+ const env = {
70
+ PATH: '/bin',
71
+ HAPPIER_HOME_DIR: '/tmp/home',
72
+ HAPPIER_SERVER_URL: 'http://localhost:3000',
73
+ HAPPIER_STACK_KEEP: 'keep',
74
+ };
75
+ const scrubbed = scrubHappierStackEnv(env, {
76
+ keepHappierStackKeys: ['HAPPIER_STACK_KEEP'],
77
+ clearUnprefixedKeys: [' HAPPIER_HOME_DIR ', 'HAPPIER_SERVER_URL', 'HAPPIER_SERVER_URL'],
78
+ });
79
+
80
+ assert.equal(scrubbed.PATH, '/bin');
81
+ assert.equal(scrubbed.HAPPIER_HOME_DIR, undefined);
82
+ assert.equal(scrubbed.HAPPIER_SERVER_URL, undefined);
83
+ assert.equal(scrubbed.HAPPIER_STACK_KEEP, 'keep');
84
+ });
85
+
86
+ test('scrubHappierStackEnv preserves HAPPIER_STACK_TUI in stack wrapper mode', () => {
87
+ const env = {
88
+ PATH: '/bin',
89
+ HAPPIER_STACK_TUI: '1',
90
+ HAPPIER_STACK_VERBOSE: '1',
91
+ HAPPIER_STACK_SECRET: 'drop-me',
92
+ };
93
+ const scrubbed = scrubHappierStackEnv(env, {
94
+ keepHappierStackKeys: STACK_WRAPPER_PRESERVE_KEYS,
95
+ clearUnprefixedKeys: [],
96
+ });
97
+
98
+ assert.equal(scrubbed.PATH, '/bin');
99
+ assert.equal(scrubbed.HAPPIER_STACK_TUI, '1');
100
+ assert.equal(scrubbed.HAPPIER_STACK_VERBOSE, '1');
101
+ assert.equal(scrubbed.HAPPIER_STACK_SECRET, undefined);
102
+ });
@@ -0,0 +1,13 @@
1
+ export function getEnvValue(obj, key) {
2
+ const v = (obj?.[key] ?? '').toString().trim();
3
+ return v || '';
4
+ }
5
+
6
+ export function getEnvValueAny(obj, keys) {
7
+ for (const k of keys) {
8
+ const v = getEnvValue(obj, k);
9
+ if (v) return v;
10
+ }
11
+ return '';
12
+ }
13
+
@@ -0,0 +1,65 @@
1
+ import { join } from 'node:path';
2
+
3
+ import { ensureDepsInstalled } from '../proc/pm.mjs';
4
+ import { run } from '../proc/proc.mjs';
5
+ import { spawnProc } from '../proc/proc.mjs';
6
+ import { ensureExpoIsolationEnv, getExpoStatePaths, wantsExpoClearCache } from './expo.mjs';
7
+
8
+ export async function prepareExpoCommandEnv({
9
+ baseDir,
10
+ kind,
11
+ projectDir,
12
+ baseEnv,
13
+ stateFileName,
14
+ }) {
15
+ const env = { ...(baseEnv ?? process.env) };
16
+ const paths = getExpoStatePaths({ baseDir, kind, projectDir, stateFileName });
17
+ await ensureExpoIsolationEnv({ env, stateDir: paths.stateDir, expoHomeDir: paths.expoHomeDir, tmpDir: paths.tmpDir });
18
+ return { env, paths };
19
+ }
20
+
21
+ export function maybeAddExpoClear({ args, env }) {
22
+ const next = [...(args ?? [])];
23
+ if (wantsExpoClearCache({ env: env ?? process.env })) {
24
+ // Expo supports `--clear` for start, and `-c` for export.
25
+ // Callers should pass the right flag for their subcommand; we only add when missing.
26
+ if (!next.includes('--clear') && !next.includes('-c')) {
27
+ // Prefer `--clear` as a safe default; callers can override per-command.
28
+ next.push('--clear');
29
+ }
30
+ }
31
+ return next;
32
+ }
33
+
34
+ export async function expoExec({
35
+ dir,
36
+ projectDir,
37
+ args,
38
+ env,
39
+ ensureDepsLabel = 'happy',
40
+ quiet = false,
41
+ }) {
42
+ const runnerDir = dir;
43
+ const cwd = projectDir ?? runnerDir;
44
+ await ensureDepsInstalled(runnerDir, ensureDepsLabel, { quiet, env });
45
+ const expoBin = join(runnerDir, 'node_modules', '.bin', 'expo');
46
+ await run(expoBin, args, { cwd, env, stdio: quiet ? 'ignore' : 'inherit' });
47
+ }
48
+
49
+ export async function expoSpawn({
50
+ label,
51
+ dir,
52
+ projectDir,
53
+ args,
54
+ env,
55
+ ensureDepsLabel = 'happy',
56
+ quiet = false,
57
+ options,
58
+ }) {
59
+ const runnerDir = dir;
60
+ const cwd = projectDir ?? runnerDir;
61
+ await ensureDepsInstalled(runnerDir, ensureDepsLabel, { quiet, env });
62
+ const expoBin = join(runnerDir, 'node_modules', '.bin', 'expo');
63
+ return spawnProc(label, expoBin, args, env, { cwd, ...(options ?? {}) });
64
+ }
65
+
@@ -0,0 +1,139 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { isPidAlive } from '../proc/pids.mjs';
7
+ import { isTcpPortFree } from '../net/ports.mjs';
8
+
9
+ export { isPidAlive };
10
+
11
+ function resolveMetroStatusTimeoutMsFromEnv(env = process.env) {
12
+ const raw = (env.HAPPIER_STACK_EXPO_METRO_STATUS_TIMEOUT_MS ?? '').toString().trim();
13
+ const n = raw ? Number(raw) : null;
14
+ if (Number.isFinite(n) && n > 0) return n;
15
+ return 800;
16
+ }
17
+
18
+ export async function looksLikeExpoMetro({ port, timeoutMs = null } = {}) {
19
+ const p = Number(port);
20
+ if (!Number.isFinite(p) || p <= 0) return false;
21
+ const ms = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0 ? Number(timeoutMs) : resolveMetroStatusTimeoutMsFromEnv();
22
+ const url = `http://127.0.0.1:${p}/status`;
23
+ try {
24
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
25
+ const timeout = setTimeout(() => controller?.abort(), ms);
26
+ try {
27
+ const res = await fetch(url, { signal: controller?.signal });
28
+ const txt = await res.text().catch(() => '');
29
+ return res.ok && String(txt).toLowerCase().includes('packager-status:running');
30
+ } finally {
31
+ clearTimeout(timeout);
32
+ }
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function hashDir(dir) {
39
+ return createHash('sha1').update(String(dir ?? '')).digest('hex').slice(0, 12);
40
+ }
41
+
42
+ export function getExpoStatePaths({ baseDir, kind, projectDir, stateFileName = 'expo.state.json' }) {
43
+ const key = hashDir(projectDir);
44
+ const stateDir = join(baseDir, kind, key);
45
+ return {
46
+ key,
47
+ stateDir,
48
+ statePath: join(stateDir, stateFileName),
49
+ expoHomeDir: join(stateDir, 'expo-home'),
50
+ tmpDir: join(stateDir, 'tmp'),
51
+ };
52
+ }
53
+
54
+ export async function ensureExpoIsolationEnv({ env, stateDir, expoHomeDir, tmpDir }) {
55
+ await mkdir(stateDir, { recursive: true });
56
+ await mkdir(expoHomeDir, { recursive: true });
57
+ await mkdir(tmpDir, { recursive: true });
58
+
59
+ // Expo CLI uses this to override ~/.expo.
60
+ // Always override: stack/worktree isolation must not fall back to the user's global ~/.expo.
61
+ env.__UNSAFE_EXPO_HOME_DIRECTORY = expoHomeDir;
62
+
63
+ // Metro default cache root is `path.join(os.tmpdir(), 'metro-cache')`, so TMPDIR isolates it.
64
+ // Always override: macOS sets TMPDIR by default, so a "set-if-missing" guard would not isolate Metro.
65
+ env.TMPDIR = tmpDir;
66
+ }
67
+
68
+ export function wantsExpoClearCache({ env }) {
69
+ const raw = (env.HAPPIER_STACK_EXPO_CLEAR_CACHE ?? '').trim();
70
+ if (raw) {
71
+ return raw !== '0';
72
+ }
73
+ // Default: clear cache when non-interactive (LLMs/services), keep fast iteration in TTY shells.
74
+ return !(process.stdin.isTTY && process.stdout.isTTY);
75
+ }
76
+
77
+ export async function readPidState(statePath) {
78
+ try {
79
+ if (!existsSync(statePath)) return null;
80
+ const raw = await readFile(statePath, 'utf-8');
81
+ const state = JSON.parse(raw);
82
+ const pid = Number(state?.pid);
83
+ if (!Number.isFinite(pid) || pid <= 0) return null;
84
+ return state;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ export async function isStateProcessRunning(statePath) {
91
+ const state = await readPidState(statePath);
92
+ if (!state) return { running: false, state: null };
93
+ const pid = Number(state.pid);
94
+ if (isPidAlive(pid)) {
95
+ return { running: true, state, reason: 'pid' };
96
+ }
97
+
98
+ // Expo/Metro can sometimes be “up” even if the original wrapper pid exited (pm/yarn layers).
99
+ // If we have a port and something is listening on it, treat it as running only if it looks like Metro.
100
+ const port = Number(state?.port);
101
+ if (Number.isFinite(port) && port > 0) {
102
+ try {
103
+ const free = await isTcpPortFree(port, { host: '127.0.0.1' });
104
+ if (!free) {
105
+ const ok = await looksLikeExpoMetro({ port });
106
+ if (ok) {
107
+ return { running: true, state, reason: 'port' };
108
+ }
109
+ return { running: false, state };
110
+ }
111
+ } catch {
112
+ // ignore
113
+ }
114
+ }
115
+
116
+ return { running: false, state };
117
+ }
118
+
119
+ export async function writePidState(statePath, state) {
120
+ await mkdir(dirname(statePath), { recursive: true }).catch(() => {});
121
+ await writeFile(statePath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
122
+ }
123
+
124
+ export async function killPid(pid) {
125
+ const n = Number(pid);
126
+ if (!Number.isFinite(n) || n <= 1) return;
127
+ try {
128
+ process.kill(n, 'SIGTERM');
129
+ } catch {
130
+ return;
131
+ }
132
+ await delay(500);
133
+ try {
134
+ process.kill(n, 0);
135
+ process.kill(n, 'SIGKILL');
136
+ } catch {
137
+ // exited
138
+ }
139
+ }
@@ -0,0 +1,48 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import http from 'node:http';
4
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ import { isStateProcessRunning } from './expo.mjs';
9
+
10
+ function listen(server) {
11
+ return new Promise((resolve, reject) => {
12
+ server.on('error', reject);
13
+ server.listen(0, '127.0.0.1', () => resolve());
14
+ });
15
+ }
16
+
17
+ function close(server) {
18
+ return new Promise((resolve) => server.close(() => resolve()));
19
+ }
20
+
21
+ test('isStateProcessRunning does not treat occupied port as running when /status is not Metro', async () => {
22
+ const tmp = await mkdtemp(join(tmpdir(), 'hstack-expo-state-running-'));
23
+ const srv = http.createServer((req, res) => {
24
+ if (req.url === '/status') {
25
+ res.writeHead(200, { 'content-type': 'text/plain' });
26
+ res.end('not-metro');
27
+ return;
28
+ }
29
+ res.writeHead(200, { 'content-type': 'text/plain' });
30
+ res.end('ok');
31
+ });
32
+ try {
33
+ await listen(srv);
34
+ const addr = srv.address();
35
+ assert.ok(addr && typeof addr === 'object' && typeof addr.port === 'number', 'expected server to be listening');
36
+ const port = addr.port;
37
+
38
+ const statePath = join(tmp, 'expo.state.json');
39
+ await writeFile(statePath, JSON.stringify({ pid: 999999, port }, null, 2) + '\n', 'utf-8');
40
+
41
+ const res = await isStateProcessRunning(statePath);
42
+ assert.equal(res.running, false);
43
+ } finally {
44
+ await close(srv).catch(() => {});
45
+ await rm(tmp, { recursive: true, force: true });
46
+ }
47
+ });
48
+