@happier-dev/stack 0.1.0-preview.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +159 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +94 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,302 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, isAbsolute, join, resolve, win32 } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { existsSync } from 'node:fs';
5
+
6
+ import { expandHome } from './canonical_home.mjs';
7
+
8
+ const PRIMARY_APP_SLUG = 'happier-stack';
9
+ const PRIMARY_LABEL_BASE = 'dev.happier.stack';
10
+ const PRIMARY_STORAGE_ROOT = join(homedir(), '.happier', 'stacks');
11
+ const PRIMARY_HOME_DIR = join(homedir(), '.happier-stack');
12
+
13
+ // Happier monorepo layouts (historical + in-flight refactors):
14
+ //
15
+ // Newer (apps/):
16
+ // - apps/ui (Happier UI)
17
+ // - apps/cli (CLI + daemon)
18
+ // - apps/server (server)
19
+ //
20
+ // Legacy (packages/):
21
+ // - packages/app (Happier UI)
22
+ // - packages/cli (CLI + daemon)
23
+ // - packages/server (server)
24
+ const HAPPY_MONOREPO_COMPONENTS = new Set(['happier-ui', 'happier-cli', 'happier-server', 'happier-server-light']);
25
+
26
+ const HAPPY_MONOREPO_LAYOUTS = {
27
+ apps: {
28
+ id: 'apps',
29
+ // Minimum files that identify this layout.
30
+ markers: [
31
+ ['apps', 'ui', 'package.json'],
32
+ ['apps', 'cli', 'package.json'],
33
+ ['apps', 'server', 'package.json'],
34
+ ],
35
+ subdirByComponent: {
36
+ 'happier-ui': 'apps/ui',
37
+ 'happier-cli': 'apps/cli',
38
+ 'happier-server': 'apps/server',
39
+ // Server flavors share a single server package in the monorepo.
40
+ 'happier-server-light': 'apps/server',
41
+ },
42
+ },
43
+ packages: {
44
+ id: 'packages',
45
+ markers: [
46
+ ['packages', 'app', 'package.json'],
47
+ ['packages', 'cli', 'package.json'],
48
+ ['packages', 'server', 'package.json'],
49
+ ],
50
+ subdirByComponent: {
51
+ 'happier-ui': 'packages/app',
52
+ 'happier-cli': 'packages/cli',
53
+ 'happier-server': 'packages/server',
54
+ 'happier-server-light': 'packages/server',
55
+ },
56
+ },
57
+ };
58
+
59
+ export function isWin32ShapedAbsolutePath(p) {
60
+ const s = String(p ?? '').trim();
61
+ if (!s) return false;
62
+ // IMPORTANT: `path.win32.isAbsolute('/foo') === true`, so do not use it directly here.
63
+ // We only want to treat *Windows-shaped* absolute paths as win32:
64
+ // - Drive paths: C:\... or C:/...
65
+ // - UNC paths: \\server\share\...
66
+ // - Device namespace: \\?\...
67
+ // - Current-drive rooted: \foo\bar
68
+ if (/^[a-zA-Z]:[\\/]/.test(s)) return true;
69
+ if (s.startsWith('\\\\?\\')) return true;
70
+ if (s.startsWith('\\\\')) return true;
71
+ if (s.startsWith('\\')) return true;
72
+ return false;
73
+ }
74
+
75
+ function joinPath(root, ...parts) {
76
+ return isWin32ShapedAbsolutePath(root) ? win32.join(root, ...parts) : join(root, ...parts);
77
+ }
78
+
79
+ function resolvePath(p) {
80
+ const s = String(p ?? '').trim();
81
+ if (!s) return '';
82
+ return isWin32ShapedAbsolutePath(s) ? win32.resolve(s) : resolve(s);
83
+ }
84
+
85
+ function dirnamePath(p) {
86
+ const s = String(p ?? '').trim();
87
+ if (!s) return '';
88
+ return isWin32ShapedAbsolutePath(s) ? win32.dirname(s) : dirname(s);
89
+ }
90
+
91
+ function detectHappyMonorepoLayout(monorepoRoot) {
92
+ const root = String(monorepoRoot ?? '').trim();
93
+ if (!root) return '';
94
+ try {
95
+ const hasAll = (markers) => markers.every((m) => existsSync(joinPath(root, ...m)));
96
+ if (hasAll(HAPPY_MONOREPO_LAYOUTS.apps.markers)) return HAPPY_MONOREPO_LAYOUTS.apps.id;
97
+ if (hasAll(HAPPY_MONOREPO_LAYOUTS.packages.markers)) return HAPPY_MONOREPO_LAYOUTS.packages.id;
98
+ return '';
99
+ } catch {
100
+ return '';
101
+ }
102
+ }
103
+
104
+ export function getRootDir(importMetaUrl) {
105
+ return dirname(dirname(fileURLToPath(importMetaUrl)));
106
+ }
107
+
108
+ export function getHappyStacksHomeDir(env = process.env) {
109
+ const fromEnv = (env.HAPPIER_STACK_HOME_DIR ?? '').trim();
110
+ if (fromEnv) {
111
+ return expandHome(fromEnv);
112
+ }
113
+ return PRIMARY_HOME_DIR;
114
+ }
115
+
116
+ export function getWorkspaceDir(cliRootDir = null, env = process.env) {
117
+ const fromEnv = (env.HAPPIER_STACK_WORKSPACE_DIR ?? '').trim();
118
+ if (fromEnv) {
119
+ return expandHome(fromEnv);
120
+ }
121
+ const homeDir = getHappyStacksHomeDir();
122
+ return join(homeDir, 'workspace');
123
+ }
124
+
125
+ export function getRepoDir(rootDir, env = process.env) {
126
+ const fromEnv = normalizePathForEnv(rootDir, env.HAPPIER_STACK_REPO_DIR, env);
127
+ const workspaceDir = getWorkspaceDir(rootDir, env);
128
+ const fallback = join(workspaceDir, 'main');
129
+
130
+ // Prefer explicitly configured repo dir (if set).
131
+ const candidate = fromEnv || fallback;
132
+ if (!candidate) return fallback;
133
+
134
+ // Accept any nested path inside the monorepo (e.g. apps/ui) and normalize to a package dir
135
+ // for monorepo-aware components below.
136
+ const root = coerceHappyMonorepoRootFromPath(candidate);
137
+ return root || candidate;
138
+ }
139
+
140
+ export function getDevRepoDir(rootDir, env = process.env) {
141
+ // The "dev" checkout is a first-class worktree created by `hstack setup --profile=dev`.
142
+ // It is not treated as the default repo dir (that is always <workspace>/main).
143
+ const workspaceDir = getWorkspaceDir(rootDir, env);
144
+ return join(workspaceDir, 'dev');
145
+ }
146
+
147
+ function normalizePathForEnv(rootDir, raw, env = process.env) {
148
+ const trimmed = (raw ?? '').trim();
149
+ if (!trimmed) {
150
+ return '';
151
+ }
152
+ const expanded = expandHome(trimmed);
153
+ // If the path is relative, treat it as relative to the workspace root (default: repo root).
154
+ const workspaceDir = getWorkspaceDir(rootDir, env);
155
+ const abs = isAbsolute(expanded) || isWin32ShapedAbsolutePath(expanded);
156
+ return abs ? expanded : resolve(workspaceDir, expanded);
157
+ }
158
+
159
+ export function isHappyMonorepoComponentName(name) {
160
+ return HAPPY_MONOREPO_COMPONENTS.has(String(name ?? '').trim());
161
+ }
162
+
163
+ export function happyMonorepoSubdirForComponent(name, { monorepoRoot = '' } = {}) {
164
+ const n = String(name ?? '').trim();
165
+ if (!n || !isHappyMonorepoComponentName(n)) return null;
166
+
167
+ const root = String(monorepoRoot ?? '').trim();
168
+ const layout = root ? detectHappyMonorepoLayout(root) : '';
169
+ if (layout === HAPPY_MONOREPO_LAYOUTS.apps.id) {
170
+ return HAPPY_MONOREPO_LAYOUTS.apps.subdirByComponent[n] ?? null;
171
+ }
172
+ if (layout === HAPPY_MONOREPO_LAYOUTS.packages.id) {
173
+ return HAPPY_MONOREPO_LAYOUTS.packages.subdirByComponent[n] ?? null;
174
+ }
175
+ // Best-effort fallback: keep a stable mapping even when layout can't be detected.
176
+ return HAPPY_MONOREPO_LAYOUTS.apps.subdirByComponent[n] ?? HAPPY_MONOREPO_LAYOUTS.packages.subdirByComponent[n] ?? null;
177
+ }
178
+
179
+ export function isHappyMonorepoRoot(dir) {
180
+ const d = String(dir ?? '').trim();
181
+ if (!d) return false;
182
+ return Boolean(detectHappyMonorepoLayout(d));
183
+ }
184
+
185
+ export function coerceHappyMonorepoRootFromPath(path) {
186
+ const p = String(path ?? '').trim();
187
+ if (!p) return null;
188
+ let cur = resolvePath(p);
189
+ while (true) {
190
+ if (isHappyMonorepoRoot(cur)) return cur;
191
+ const parent = dirnamePath(cur);
192
+ if (parent === cur) return null;
193
+ cur = parent;
194
+ }
195
+ }
196
+
197
+ function resolveHappyMonorepoPackageDir({ monorepoRoot, component }) {
198
+ const sub = happyMonorepoSubdirForComponent(component, { monorepoRoot });
199
+ if (!sub) return null;
200
+ return joinPath(monorepoRoot, sub);
201
+ }
202
+
203
+ export function getComponentRepoDir(rootDir, name, env = process.env) {
204
+ // Happier-only: all services are inside the single monorepo repo root.
205
+ void name;
206
+ return getRepoDir(rootDir, env);
207
+ }
208
+
209
+ export function getComponentDir(rootDir, name, env = process.env) {
210
+ const n = String(name ?? '').trim();
211
+
212
+ // Monorepo-only default:
213
+ // If no explicit per-component override is set, always resolve monorepo package dirs from the repo dir
214
+ // (default: <workspace>/main).
215
+ if (isHappyMonorepoComponentName(n)) {
216
+ const repoRoot = getRepoDir(rootDir, env);
217
+ const pkg = resolveHappyMonorepoPackageDir({ monorepoRoot: repoRoot, component: n });
218
+ if (pkg) return pkg;
219
+ return repoRoot;
220
+ }
221
+ // Unknown logical component: resolve to repo root.
222
+ return getRepoDir(rootDir, env);
223
+ }
224
+
225
+ export function getStackName(env = process.env) {
226
+ return env.HAPPIER_STACK_STACK?.trim() ? env.HAPPIER_STACK_STACK.trim() : 'main';
227
+ }
228
+
229
+ export function getStackLabel(stackName = null, env = process.env) {
230
+ const name = (stackName ?? '').toString().trim() || getStackName(env);
231
+ return name === 'main' ? PRIMARY_LABEL_BASE : `${PRIMARY_LABEL_BASE}.${name}`;
232
+ }
233
+
234
+ export function getStacksStorageRoot(env = process.env) {
235
+ const fromEnv = (env.HAPPIER_STACK_STORAGE_DIR ?? '').trim();
236
+ if (fromEnv) {
237
+ return expandHome(fromEnv);
238
+ }
239
+ return PRIMARY_STORAGE_ROOT;
240
+ }
241
+
242
+ export function resolveStackBaseDir(stackName = null, env = process.env) {
243
+ const name = (stackName ?? '').toString().trim() || getStackName(env);
244
+ return { baseDir: join(getStacksStorageRoot(env), name), isLegacy: false };
245
+ }
246
+
247
+ export function resolveStackEnvPath(stackName = null, env = process.env) {
248
+ const name = (stackName ?? '').toString().trim() || getStackName(env);
249
+ const { baseDir } = resolveStackBaseDir(name, env);
250
+ return { envPath: join(baseDir, 'env'), isLegacy: false, baseDir };
251
+ }
252
+
253
+ export function getDefaultAutostartPaths(env = process.env) {
254
+ const stackName = getStackName(env);
255
+ const { baseDir, isLegacy } = resolveStackBaseDir(stackName, env);
256
+ const logsDir = join(baseDir, 'logs');
257
+
258
+ const label = getStackLabel(stackName, env);
259
+ const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
260
+ const stdoutPath = join(logsDir, `${PRIMARY_APP_SLUG}.out.log`);
261
+ const stderrPath = join(logsDir, `${PRIMARY_APP_SLUG}.err.log`);
262
+
263
+ // Linux (systemd --user) uses the same label convention as LaunchAgents.
264
+ const systemdUnitName = `${label}.service`;
265
+ const systemdUnitPath = join(homedir(), '.config', 'systemd', 'user', systemdUnitName);
266
+
267
+ return {
268
+ baseDir,
269
+ logsDir,
270
+ stackName,
271
+ isLegacy,
272
+
273
+ label,
274
+ plistPath,
275
+ systemdUnitName,
276
+ systemdUnitPath,
277
+ stdoutPath,
278
+ stderrPath,
279
+ };
280
+ }
281
+
282
+ export function getSystemdUnitInfo({ env = process.env, mode = 'user' } = {}) {
283
+ const m = String(mode ?? '').trim().toLowerCase() === 'system' ? 'system' : 'user';
284
+ const { label, systemdUnitName, systemdUnitPath } = getDefaultAutostartPaths(env);
285
+ void label;
286
+ if (m === 'system') {
287
+ return {
288
+ mode: 'system',
289
+ unitName: systemdUnitName,
290
+ unitPath: join('/etc/systemd/system', systemdUnitName),
291
+ systemctlArgsPrefix: [],
292
+ journalctlArgsPrefix: [],
293
+ };
294
+ }
295
+ return {
296
+ mode: 'user',
297
+ unitName: systemdUnitName,
298
+ unitPath: systemdUnitPath,
299
+ systemctlArgsPrefix: ['--user'],
300
+ journalctlArgsPrefix: ['--user'],
301
+ };
302
+ }
@@ -0,0 +1,36 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { getRepoDir, isWin32ShapedAbsolutePath } from './paths.mjs';
5
+
6
+ test('getRepoDir treats win32 absolute paths as absolute (does not resolve under workspace)', () => {
7
+ const rootDir = '/tmp/happier-stack-root';
8
+ const env = {
9
+ HAPPIER_STACK_WORKSPACE_DIR: '/tmp/happier-stack-workspace',
10
+ HAPPIER_STACK_REPO_DIR: 'C:\\happier\\workspace\\main',
11
+ };
12
+ assert.equal(getRepoDir(rootDir, env), 'C:\\happier\\workspace\\main');
13
+ });
14
+
15
+ test('isWin32ShapedAbsolutePath recognizes win32 absolute forms and rejects relative paths', () => {
16
+ assert.equal(isWin32ShapedAbsolutePath('C:\\foo\\bar'), true);
17
+ assert.equal(isWin32ShapedAbsolutePath('C:/foo/bar'), true);
18
+ assert.equal(isWin32ShapedAbsolutePath('\\\\server\\share\\path'), true);
19
+ assert.equal(isWin32ShapedAbsolutePath('\\\\?\\C:\\foo\\bar'), true);
20
+ assert.equal(isWin32ShapedAbsolutePath('\\foo\\bar'), true);
21
+
22
+ assert.equal(isWin32ShapedAbsolutePath('foo/bar'), false);
23
+ assert.equal(isWin32ShapedAbsolutePath('./x'), false);
24
+ assert.equal(isWin32ShapedAbsolutePath('~/x'), false);
25
+ assert.equal(isWin32ShapedAbsolutePath('/tmp/x'), false);
26
+ assert.equal(isWin32ShapedAbsolutePath(' '), false);
27
+ });
28
+
29
+ test('getRepoDir treats current-drive rooted win32 paths as absolute', () => {
30
+ const rootDir = '/tmp/happier-stack-root';
31
+ const env = {
32
+ HAPPIER_STACK_WORKSPACE_DIR: '/tmp/happier-stack-workspace',
33
+ HAPPIER_STACK_REPO_DIR: '\\happier\\workspace\\main',
34
+ };
35
+ assert.equal(getRepoDir(rootDir, env), '\\happier\\workspace\\main');
36
+ });
@@ -0,0 +1,58 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { getComponentDir, getComponentRepoDir } from './paths.mjs';
8
+
9
+ async function withTempRoot(t) {
10
+ const dir = await mkdtemp(join(tmpdir(), 'happier-stacks-paths-monorepo-'));
11
+ t.after(async () => {
12
+ await rm(dir, { recursive: true, force: true });
13
+ });
14
+ return dir;
15
+ }
16
+
17
+ async function writeHappyMonorepoStub({ rootDir }) {
18
+ const monoRoot = join(rootDir, 'main');
19
+ await mkdir(join(monoRoot, 'apps', 'ui'), { recursive: true });
20
+ await mkdir(join(monoRoot, 'apps', 'cli'), { recursive: true });
21
+ await mkdir(join(monoRoot, 'apps', 'server'), { recursive: true });
22
+ await writeFile(join(monoRoot, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
23
+ await writeFile(join(monoRoot, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
24
+ await writeFile(join(monoRoot, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
25
+ return monoRoot;
26
+ }
27
+
28
+ test('getComponentDir derives monorepo component package dirs from workspace/main', async (t) => {
29
+ const rootDir = await withTempRoot(t);
30
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
31
+
32
+ const monoRoot = await writeHappyMonorepoStub({ rootDir });
33
+ assert.equal(getComponentDir(rootDir, 'happier-ui', env), join(monoRoot, 'apps', 'ui'));
34
+ assert.equal(getComponentDir(rootDir, 'happier-cli', env), join(monoRoot, 'apps', 'cli'));
35
+ assert.equal(getComponentDir(rootDir, 'happier-server', env), join(monoRoot, 'apps', 'server'));
36
+ assert.equal(getComponentDir(rootDir, 'happier-server-light', env), join(monoRoot, 'apps', 'server'));
37
+ });
38
+
39
+ test('getComponentRepoDir returns the shared monorepo root for monorepo components', async (t) => {
40
+ const rootDir = await withTempRoot(t);
41
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
42
+
43
+ const monoRoot = await writeHappyMonorepoStub({ rootDir });
44
+ assert.equal(getComponentRepoDir(rootDir, 'happier-ui', env), monoRoot);
45
+ assert.equal(getComponentRepoDir(rootDir, 'happier-cli', env), monoRoot);
46
+ assert.equal(getComponentRepoDir(rootDir, 'happier-server', env), monoRoot);
47
+ assert.equal(getComponentRepoDir(rootDir, 'happier-server-light', env), monoRoot);
48
+ });
49
+
50
+ test('getComponentDir normalizes HAPPIER_STACK_REPO_DIR that points inside the monorepo', async (t) => {
51
+ const rootDir = await withTempRoot(t);
52
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
53
+
54
+ const monoRoot = await writeHappyMonorepoStub({ rootDir });
55
+
56
+ env.HAPPIER_STACK_REPO_DIR = join(monoRoot, 'apps', 'cli', 'src');
57
+ assert.equal(getComponentDir(rootDir, 'happier-cli', env), join(monoRoot, 'apps', 'cli'));
58
+ });
@@ -0,0 +1,50 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { getComponentDir } from './paths.mjs';
8
+
9
+ async function withTempRoot(t) {
10
+ const rootDir = await mkdtemp(join(tmpdir(), 'happier-stack-paths-server-flavors-'));
11
+ t.after(async () => {
12
+ await rm(rootDir, { recursive: true, force: true });
13
+ });
14
+ return rootDir;
15
+ }
16
+
17
+ async function createMonorepoServerFixture({ rootDir, includeSqliteSchema }) {
18
+ const repoRoot = join(rootDir, 'main');
19
+ const serverDir = join(repoRoot, 'apps', 'server');
20
+ await mkdir(serverDir, { recursive: true });
21
+
22
+ // Monorepo markers (layout detection).
23
+ await mkdir(join(repoRoot, 'apps', 'ui'), { recursive: true });
24
+ await mkdir(join(repoRoot, 'apps', 'cli'), { recursive: true });
25
+ await writeFile(join(repoRoot, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
26
+ await writeFile(join(repoRoot, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
27
+ await writeFile(join(repoRoot, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
28
+
29
+ if (includeSqliteSchema) {
30
+ await mkdir(join(serverDir, 'prisma', 'sqlite'), { recursive: true });
31
+ await writeFile(join(serverDir, 'prisma', 'sqlite', 'schema.prisma'), 'datasource db { provider = "sqlite" }\n', 'utf-8');
32
+ }
33
+
34
+ return { repoRoot, serverDir };
35
+ }
36
+
37
+ test('getComponentDir prefers happier-server for happier-server-light when unified schema exists', async (t) => {
38
+ const rootDir = await withTempRoot(t);
39
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
40
+ const { serverDir } = await createMonorepoServerFixture({ rootDir, includeSqliteSchema: true });
41
+
42
+ assert.equal(getComponentDir(rootDir, 'happier-server-light', env), serverDir);
43
+ });
44
+
45
+ test('getComponentDir resolves happier-server-light to the monorepo server package dir', async (t) => {
46
+ const rootDir = await withTempRoot(t);
47
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
48
+ const { serverDir } = await createMonorepoServerFixture({ rootDir, includeSqliteSchema: false });
49
+ assert.equal(getComponentDir(rootDir, 'happier-server-light', env), serverDir);
50
+ });
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { expandHome } from './canonical_home.mjs';
6
+
7
+ function resolveInvokerPackageName(cliRootDir) {
8
+ try {
9
+ const raw = readFileSync(join(cliRootDir, 'package.json'), 'utf-8');
10
+ const pkg = JSON.parse(raw);
11
+ const name = String(pkg?.name ?? '').trim();
12
+ return name || null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ export function getRuntimeDir() {
19
+ const fromEnv = (process.env.HAPPIER_STACK_RUNTIME_DIR ?? '').trim();
20
+ if (fromEnv) {
21
+ return expandHome(fromEnv);
22
+ }
23
+ const homeDir = (process.env.HAPPIER_STACK_HOME_DIR ?? '').trim()
24
+ ? expandHome(process.env.HAPPIER_STACK_HOME_DIR.trim())
25
+ : join(homedir(), '.happier-stack');
26
+ return join(homeDir, 'runtime');
27
+ }
28
+
29
+ export function resolveInstalledCliRoot(cliRootDir) {
30
+ const runtimeDir = getRuntimeDir();
31
+ const pkgName = resolveInvokerPackageName(cliRootDir);
32
+ const runtimePkgRoot = pkgName ? join(runtimeDir, 'node_modules', ...pkgName.split('/')) : null;
33
+ if (runtimePkgRoot && existsSync(runtimePkgRoot)) {
34
+ return runtimePkgRoot;
35
+ }
36
+ return cliRootDir;
37
+ }
38
+
39
+ export function resolveInstalledPath(cliRootDir, relativePath) {
40
+ return join(resolveInstalledCliRoot(cliRootDir), relativePath);
41
+ }
@@ -0,0 +1,107 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { open, readFile, unlink } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { isPidAlive } from './proc/pids.mjs';
5
+ import { getPidStartTime } from './proc/ownership.mjs';
6
+
7
+ function lockPathForDbDir(dbDir) {
8
+ return join(dirname(dbDir), '.happier.pglite.lock');
9
+ }
10
+
11
+ function safeParseJson(raw) {
12
+ try {
13
+ return JSON.parse(raw);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ async function readLock(lockPath) {
20
+ try {
21
+ if (!existsSync(lockPath)) return null;
22
+ const raw = await readFile(lockPath, 'utf-8');
23
+ const parsed = safeParseJson(raw);
24
+ if (!parsed || typeof parsed !== 'object') return { invalid: true };
25
+ return parsed;
26
+ } catch {
27
+ return { invalid: true };
28
+ }
29
+ }
30
+
31
+ async function removeLock(lockPath) {
32
+ try {
33
+ await unlink(lockPath);
34
+ } catch {
35
+ // ignore
36
+ }
37
+ }
38
+
39
+ export async function acquirePgliteDirLock(dbDir, { purpose = 'unknown' } = {}) {
40
+ const resolvedDbDir = resolve(String(dbDir ?? ''));
41
+ if (!resolvedDbDir) throw new Error('Missing dbDir');
42
+ const lockPath = lockPathForDbDir(resolvedDbDir);
43
+
44
+ for (let attempt = 0; attempt < 3; attempt += 1) {
45
+ try {
46
+ const handle = await open(lockPath, 'wx', 0o600);
47
+ try {
48
+ const payload = {
49
+ pid: process.pid,
50
+ createdAt: new Date().toISOString(),
51
+ purpose,
52
+ dbDir: resolvedDbDir,
53
+ pidStartTime: await getPidStartTime(process.pid),
54
+ };
55
+ await handle.writeFile(JSON.stringify(payload, null, 2) + '\n', 'utf-8');
56
+ } finally {
57
+ await handle.close();
58
+ }
59
+
60
+ return async function release() {
61
+ const existing = await readLock(lockPath);
62
+ const pid = Number(existing?.pid);
63
+ if (Number.isFinite(pid) && pid === process.pid) {
64
+ await removeLock(lockPath);
65
+ }
66
+ };
67
+ } catch (e) {
68
+ if (!e || typeof e !== 'object' || !('code' in e) || e.code !== 'EEXIST') {
69
+ throw e;
70
+ }
71
+
72
+ const existing = await readLock(lockPath);
73
+ const existingPid = Number(existing?.pid);
74
+ const existingDbDir = existing?.dbDir ? resolve(String(existing.dbDir)) : '';
75
+ const looksInvalid = Boolean(existing?.invalid);
76
+ const pidAlive = Number.isFinite(existingPid) && isPidAlive(existingPid);
77
+ const stale = await (async () => {
78
+ if (looksInvalid) return true;
79
+ if (!pidAlive) return true;
80
+ const expectedStartTime = typeof existing?.pidStartTime === 'string' ? existing.pidStartTime.trim() : '';
81
+ if (!expectedStartTime) return false;
82
+ const currentStartTime = await getPidStartTime(existingPid);
83
+ // Fail-open for fingerprint lookup errors: do not break lock acquisition.
84
+ if (!currentStartTime) return false;
85
+ return currentStartTime.trim() !== expectedStartTime;
86
+ })();
87
+
88
+ if (stale) {
89
+ await removeLock(lockPath);
90
+ continue;
91
+ }
92
+
93
+ // Fail closed: if another live process owns the lock, refuse to proceed.
94
+ // Even if dbDir mismatches, this lock still indicates the parent dir is in use.
95
+ const meta = [
96
+ existing?.purpose ? `purpose=${existing.purpose}` : '',
97
+ existing?.createdAt ? `createdAt=${existing.createdAt}` : '',
98
+ existingDbDir ? `dbDir=${existingDbDir}` : '',
99
+ ]
100
+ .filter(Boolean)
101
+ .join(' ');
102
+ throw new Error(`pglite db dir is in use by pid=${existingPid}${meta ? ` (${meta})` : ''}`);
103
+ }
104
+ }
105
+
106
+ throw new Error('Failed to acquire pglite lock after retries');
107
+ }
@@ -0,0 +1,33 @@
1
+ import { runCapture } from './proc.mjs';
2
+
3
+ export async function resolveCommandPath(cmd, { cwd, env, timeoutMs } = {}) {
4
+ const c = String(cmd ?? '').trim();
5
+ if (!c) return '';
6
+
7
+ try {
8
+ if (process.platform === 'win32') {
9
+ const out = (await runCapture('where', [c], { cwd, env, timeoutMs })).trim();
10
+ const first = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean) || '';
11
+ return first;
12
+ }
13
+ return (
14
+ await runCapture('sh', ['-lc', `command -v "${c}" 2>/dev/null || true`], { cwd, env, timeoutMs })
15
+ ).trim();
16
+ } catch {
17
+ return '';
18
+ }
19
+ }
20
+
21
+ export async function runCaptureIfCommandExists(cmd, args, { cwd, env, timeoutMs } = {}) {
22
+ const resolved = await resolveCommandPath(cmd, { cwd, env, timeoutMs });
23
+ if (!resolved) return '';
24
+ try {
25
+ return await runCapture(resolved, args, { cwd, env, timeoutMs });
26
+ } catch {
27
+ return '';
28
+ }
29
+ }
30
+
31
+ export async function commandExists(cmd, { cwd, env, timeoutMs } = {}) {
32
+ return Boolean(await resolveCommandPath(cmd, { cwd, env, timeoutMs }));
33
+ }
@@ -0,0 +1,57 @@
1
+ import { killProcessTree } from './proc.mjs';
2
+
3
+ function normalizeChildren(children) {
4
+ if (!Array.isArray(children)) return [];
5
+ return children.filter((c) => c && typeof c === 'object' && 'pid' in c && Number(c.pid) > 1);
6
+ }
7
+
8
+ export function installExitCleanup({ label = 'local', children } = {}) {
9
+ const childrenRef = children;
10
+ let cleaned = false;
11
+
12
+ const cleanupSync = () => {
13
+ if (cleaned) return;
14
+ cleaned = true;
15
+ const tracked = normalizeChildren(childrenRef);
16
+ for (const child of tracked) {
17
+ try {
18
+ killProcessTree(child, 'SIGTERM');
19
+ } catch {
20
+ // Best-effort cleanup; keep going.
21
+ }
22
+ }
23
+ // SIGKILL follows immediately as best-effort escalation.
24
+ // For graceful shutdown (with time for handlers to run), callers should invoke cleanup manually.
25
+ for (const child of tracked) {
26
+ try {
27
+ killProcessTree(child, 'SIGKILL');
28
+ } catch {
29
+ // Best-effort cleanup; keep going.
30
+ }
31
+ }
32
+ };
33
+
34
+ const fatalSync = (err) => {
35
+ try {
36
+ const msg = err instanceof Error ? err.stack || err.message : String(err);
37
+ // eslint-disable-next-line no-console
38
+ console.error(`[${label}] fatal: ${msg}`);
39
+ } catch {
40
+ // ignore
41
+ }
42
+ cleanupSync();
43
+ try {
44
+ process.exitCode = 1;
45
+ // Ensure we actually terminate; some Node configs do not exit on unhandled rejections.
46
+ process.exit(1);
47
+ } catch {
48
+ // ignore
49
+ }
50
+ };
51
+
52
+ process.once('exit', cleanupSync);
53
+ process.once('uncaughtException', fatalSync);
54
+ process.once('unhandledRejection', fatalSync);
55
+
56
+ return cleanupSync;
57
+ }