@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,868 @@
1
+ import './utils/env/env.mjs';
2
+ import { run, runCapture } from './utils/proc/proc.mjs';
3
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir, getSystemdUnitInfo, resolveStackEnvPath } from './utils/paths/paths.mjs';
4
+ import { getInternalServerUrl, getPublicServerUrlEnvOverride } from './utils/server/urls.mjs';
5
+ import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/service/autostart_darwin.mjs';
6
+ import { getCanonicalHomeDir } from './utils/env/config.mjs';
7
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
8
+ import { expandHome } from './utils/paths/canonical_home.mjs';
9
+ import { spawn } from 'node:child_process';
10
+ import { homedir } from 'node:os';
11
+ import { existsSync } from 'node:fs';
12
+ import { rm } from 'node:fs/promises';
13
+ import { dirname, join, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
16
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
17
+ import { readLastLines } from './utils/fs/tail.mjs';
18
+ import { banner, bullets, cmd as cmdFmt, kv, sectionTitle } from './utils/ui/layout.mjs';
19
+ import { cyan, dim, green, yellow } from './utils/ui/ansi.mjs';
20
+ import {
21
+ findExistingStackCredentialPath,
22
+ resolvePreferredStackDaemonStatePaths,
23
+ resolveStackCredentialPaths,
24
+ } from './utils/auth/credentials_paths.mjs';
25
+
26
+ /**
27
+ * Manage the autostart service installed by `hstack bootstrap -- --autostart`.
28
+ *
29
+ * - macOS: launchd LaunchAgents
30
+ * - Linux: systemd user services (default) or system services (--mode=system)
31
+ *
32
+ * Commands:
33
+ * - install | uninstall
34
+ * - status
35
+ * - start | stop | restart
36
+ * - enable | disable (same as start/stop but explicitly persistent)
37
+ * - logs (print last N lines)
38
+ * - tail (follow logs)
39
+ */
40
+
41
+ function getUid() {
42
+ // Prefer env var if present; otherwise fall back.
43
+ // (LaunchAgents run in a user context so this is fine.)
44
+ const n = Number(process.env.UID);
45
+ return Number.isFinite(n) ? n : null;
46
+ }
47
+
48
+ function getAutostartEnv({ rootDir, mode }) {
49
+ // IMPORTANT:
50
+ // LaunchAgents should NOT bake the entire config into the plist, because that would require
51
+ // reinstalling the service for any config change (server flavor, worktrees, ports, etc).
52
+ //
53
+ // Instead, persist only the env file path; `scripts/utils/env.mjs` will load it on every start.
54
+ //
55
+ // Stack installs:
56
+ // - `hstack stack service <name> ...` runs under a stack env already, so we persist that pointer.
57
+ //
58
+ // Main installs:
59
+ // - default to the main stack env (outside the repo): ~/.happier/stacks/main/env
60
+
61
+ const stacksEnvFile = process.env.HAPPIER_STACK_ENV_FILE?.trim() ? process.env.HAPPIER_STACK_ENV_FILE.trim() : '';
62
+ const envFileRaw = stacksEnvFile || resolveStackEnvPath('main').envPath;
63
+
64
+ // For system services, prefer a dynamic %h-based default when no explicit env file is set.
65
+ // This avoids accidentally pinning root's stack env when the service will run as another user.
66
+ const envFile =
67
+ mode === 'system' && !stacksEnvFile
68
+ ? '%h/.happier/stacks/main/env'
69
+ : envFileRaw;
70
+
71
+ return {
72
+ HAPPIER_STACK_ENV_FILE: envFile,
73
+ };
74
+ }
75
+
76
+ function resolveServiceMode(argv) {
77
+ if (argv.includes('--system')) return 'system';
78
+ if (argv.includes('--user')) return 'user';
79
+ const raw = argv.find((a) => a.startsWith('--mode=')) ?? '';
80
+ const v = raw ? raw.slice('--mode='.length).trim().toLowerCase() : '';
81
+ return v === 'system' ? 'system' : 'user';
82
+ }
83
+
84
+ function resolveSystemUser(argv) {
85
+ const raw = argv.find((a) => a.startsWith('--system-user=')) ?? '';
86
+ const v = raw ? raw.slice('--system-user='.length).trim() : '';
87
+ return v || null;
88
+ }
89
+
90
+ async function resolveHomeDirForUser(user) {
91
+ const u = String(user ?? '').trim();
92
+ if (!u) return null;
93
+ try {
94
+ const out = await runCapture('getent', ['passwd', u]);
95
+ const line = out.trim().split('\n')[0] ?? '';
96
+ const parts = line.split(':');
97
+ const home = parts[5] ? parts[5].trim() : '';
98
+ return home || null;
99
+ } catch {
100
+ // best-effort fallback
101
+ return `/home/${u}`;
102
+ }
103
+ }
104
+
105
+ function ensureLinuxSystemModeSupported({ mode }) {
106
+ if (mode !== 'system') return;
107
+ if (process.platform !== 'linux') {
108
+ throw new Error(`[local] --mode=system is only supported on Linux.`);
109
+ }
110
+ }
111
+
112
+ export async function installService({ mode = 'user', systemUser = null } = {}) {
113
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
114
+ throw new Error(
115
+ '[local] service install is disabled in sandbox mode.\n' +
116
+ 'Reason: services are global OS state (launchd/systemd) and can affect your real installation.\n' +
117
+ 'If you really want this, set: HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1'
118
+ );
119
+ }
120
+ ensureLinuxSystemModeSupported({ mode });
121
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
122
+ throw new Error('[local] service install is only supported on macOS (launchd) and Linux (systemd).');
123
+ }
124
+ const rootDir = getRootDir(import.meta.url);
125
+ const { label } = getDefaultAutostartPaths();
126
+ const env = getAutostartEnv({ rootDir, mode });
127
+ // Ensure the env file exists so the service never points at a missing path.
128
+ try {
129
+ const envFile = env.HAPPIER_STACK_ENV_FILE;
130
+ // systemd specifier paths like %h/... are resolved at runtime; don't try to create them here.
131
+ if (!envFile.includes('%')) {
132
+ await mkdir(dirname(envFile), { recursive: true });
133
+ if (!existsSync(envFile)) {
134
+ await writeFile(envFile, '', { flag: 'a' });
135
+ }
136
+ }
137
+ } catch {
138
+ // ignore
139
+ }
140
+ if (process.platform === 'darwin') {
141
+ await ensureMacAutostartEnabled({ rootDir, label, env });
142
+ console.log(`${green('✓')} service installed ${dim('(macOS launchd)')}`);
143
+ return;
144
+ }
145
+ if (mode === 'system') {
146
+ await ensureSystemdSystemServiceEnabled({ rootDir, label, env, systemUser });
147
+ console.log(`${green('✓')} service installed ${dim('(Linux systemd system)')}`);
148
+ return;
149
+ }
150
+ await ensureSystemdUserServiceEnabled({ rootDir, label, env });
151
+ console.log(`${green('✓')} service installed ${dim('(Linux systemd --user)')}`);
152
+ }
153
+
154
+ export async function uninstallService({ mode = 'user' } = {}) {
155
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
156
+ // Sandbox cleanups should be safe and should not touch global services by default.
157
+ return;
158
+ }
159
+ ensureLinuxSystemModeSupported({ mode });
160
+ if (process.platform !== 'darwin' && process.platform !== 'linux') return;
161
+
162
+ if (process.platform === 'linux') {
163
+ if (mode === 'system') {
164
+ await ensureSystemdSystemServiceDisabled({ remove: true });
165
+ console.log(`${green('✓')} service uninstalled ${dim('(systemd system unit removed)')}`);
166
+ return;
167
+ }
168
+ await ensureSystemdUserServiceDisabled({ remove: true });
169
+ console.log(`${green('✓')} service uninstalled ${dim('(systemd user unit removed)')}`);
170
+ return;
171
+ }
172
+ const { plistPath, label } = getDefaultAutostartPaths();
173
+
174
+ await ensureMacAutostartDisabled({ label });
175
+ try {
176
+ await rm(plistPath, { force: true });
177
+ } catch {
178
+ // ignore
179
+ }
180
+ console.log(`${green('✓')} service uninstalled ${dim('(plist removed)')}`);
181
+ }
182
+
183
+ function systemdUnitName() {
184
+ const { label } = getDefaultAutostartPaths();
185
+ return `${label}.service`;
186
+ }
187
+
188
+ function systemdUnitPath() {
189
+ return join(homedir(), '.config', 'systemd', 'user', systemdUnitName());
190
+ }
191
+
192
+ function systemdEnvLines(env) {
193
+ return Object.entries(env)
194
+ .map(([k, v]) => `Environment=${k}=${String(v)}`)
195
+ .join('\n');
196
+ }
197
+
198
+ async function ensureSystemdUserServiceEnabled({ rootDir, label, env }) {
199
+ const unitPath = systemdUnitPath();
200
+ await mkdir(dirname(unitPath), { recursive: true });
201
+ const hstackShim = join(getCanonicalHomeDir(), 'bin', 'hstack');
202
+ const entry = existsSync(hstackShim) ? hstackShim : join(rootDir, 'bin', 'hstack.mjs');
203
+ const exec = existsSync(hstackShim) ? entry : `${process.execPath} ${entry}`;
204
+
205
+ const unit = `[Unit]
206
+ Description=Happier Stack (${label})
207
+ After=network-online.target
208
+ Wants=network-online.target
209
+
210
+ [Service]
211
+ Type=simple
212
+ WorkingDirectory=%h
213
+ ${systemdEnvLines(env)}
214
+ ExecStart=${exec} start
215
+ Restart=always
216
+ RestartSec=2
217
+
218
+ [Install]
219
+ WantedBy=default.target
220
+ `;
221
+
222
+ await writeFile(unitPath, unit, 'utf-8');
223
+ await runCapture('systemctl', ['--user', 'daemon-reload']).catch(() => {});
224
+ await run('systemctl', ['--user', 'enable', '--now', systemdUnitName()]);
225
+ }
226
+
227
+ async function ensureSystemdSystemServiceEnabled({ rootDir, label, env, systemUser }) {
228
+ const { unitName, unitPath } = getSystemdUnitInfo({ mode: 'system' });
229
+ let userLine = '';
230
+ let hstackShim = join(getCanonicalHomeDir(), 'bin', 'hstack');
231
+ if (systemUser) {
232
+ const home = await resolveHomeDirForUser(systemUser);
233
+ if (home) {
234
+ hstackShim = join(home, '.happier-stack', 'bin', 'hstack');
235
+ }
236
+ userLine = `User=${systemUser}\n`;
237
+ }
238
+
239
+ const entry = existsSync(hstackShim) ? hstackShim : join(rootDir, 'bin', 'hstack.mjs');
240
+ const exec = existsSync(hstackShim) ? entry : `${process.execPath} ${entry}`;
241
+
242
+ const unit = `[Unit]
243
+ Description=Happier Stack (${label})
244
+ After=network-online.target
245
+ Wants=network-online.target
246
+
247
+ [Service]
248
+ Type=simple
249
+ ${userLine}WorkingDirectory=%h
250
+ ${systemdEnvLines(env)}
251
+ ExecStart=${exec} start
252
+ Restart=always
253
+ RestartSec=2
254
+
255
+ [Install]
256
+ WantedBy=multi-user.target
257
+ `;
258
+
259
+ try {
260
+ await writeFile(unitPath, unit, 'utf-8');
261
+ } catch (e) {
262
+ const code = e && typeof e === 'object' && 'code' in e ? String(e.code) : '';
263
+ if (code === 'EACCES' || code === 'EPERM') {
264
+ throw new Error(`[local] --mode=system requires root (run with sudo).`);
265
+ }
266
+ throw e;
267
+ }
268
+ await runCapture('systemctl', ['daemon-reload']).catch(() => {});
269
+ await run('systemctl', ['enable', '--now', unitName]);
270
+ }
271
+
272
+ async function ensureSystemdUserServiceDisabled({ remove } = {}) {
273
+ await runCapture('systemctl', ['--user', 'disable', '--now', systemdUnitName()]).catch(() => {});
274
+ await runCapture('systemctl', ['--user', 'stop', systemdUnitName()]).catch(() => {});
275
+ if (remove) {
276
+ await rm(systemdUnitPath(), { force: true }).catch(() => {});
277
+ await runCapture('systemctl', ['--user', 'daemon-reload']).catch(() => {});
278
+ }
279
+ }
280
+
281
+ async function ensureSystemdSystemServiceDisabled({ remove } = {}) {
282
+ const { unitName, unitPath } = getSystemdUnitInfo({ mode: 'system' });
283
+ await runCapture('systemctl', ['disable', '--now', unitName]).catch(() => {});
284
+ await runCapture('systemctl', ['stop', unitName]).catch(() => {});
285
+ if (remove) {
286
+ await rm(unitPath, { force: true }).catch(() => {});
287
+ await runCapture('systemctl', ['daemon-reload']).catch(() => {});
288
+ }
289
+ }
290
+
291
+ async function systemdStatus() {
292
+ await run('systemctl', ['--user', 'status', systemdUnitName(), '--no-pager']);
293
+ }
294
+
295
+ async function systemdStart({ persistent }) {
296
+ if (persistent) {
297
+ await run('systemctl', ['--user', 'enable', '--now', systemdUnitName()]);
298
+ } else {
299
+ await run('systemctl', ['--user', 'start', systemdUnitName()]);
300
+ }
301
+ }
302
+
303
+ async function systemdStop({ persistent }) {
304
+ if (persistent) {
305
+ await run('systemctl', ['--user', 'disable', '--now', systemdUnitName()]);
306
+ } else {
307
+ await run('systemctl', ['--user', 'stop', systemdUnitName()]);
308
+ }
309
+ }
310
+
311
+ async function systemdRestart() {
312
+ await run('systemctl', ['--user', 'restart', systemdUnitName()]);
313
+ }
314
+
315
+ async function systemdLogs({ lines = 120 } = {}) {
316
+ await run('journalctl', ['--user', '-u', systemdUnitName(), '-n', String(lines), '--no-pager']);
317
+ }
318
+
319
+ async function systemdTail() {
320
+ await run('journalctl', ['--user', '-u', systemdUnitName(), '-f']);
321
+ }
322
+
323
+ async function launchctlTry(args) {
324
+ try {
325
+ await runCapture('launchctl', args);
326
+ return true;
327
+ } catch {
328
+ return false;
329
+ }
330
+ }
331
+
332
+ async function restartLaunchAgentBestEffort() {
333
+ const { plistPath, label } = getDefaultAutostartPaths();
334
+ if (!existsSync(plistPath)) {
335
+ throw new Error(`[local] LaunchAgent plist not found at ${plistPath}. Run: hstack service:install (or hstack bootstrap -- --autostart)`);
336
+ }
337
+ const uid = getUid();
338
+ if (uid == null) {
339
+ return false;
340
+ }
341
+ // Prefer kickstart -k to avoid overlapping stop/start windows (which can stop a freshly started daemon).
342
+ return await launchctlTry(['kickstart', '-k', `gui/${uid}/${label}`]);
343
+ }
344
+
345
+ async function startLaunchAgent({ persistent }) {
346
+ const { plistPath } = getDefaultAutostartPaths();
347
+ if (!existsSync(plistPath)) {
348
+ throw new Error(`[local] LaunchAgent plist not found at ${plistPath}. Run: hstack service:install (or hstack bootstrap -- --autostart)`);
349
+ }
350
+
351
+ const { label } = getDefaultAutostartPaths();
352
+
353
+ // Old-style (works on many systems)
354
+ if (persistent) {
355
+ if (await launchctlTry(['load', '-w', plistPath])) {
356
+ return;
357
+ }
358
+ } else {
359
+ if (await launchctlTry(['load', plistPath])) {
360
+ return;
361
+ }
362
+ }
363
+
364
+ // Modern fallback (more reliable on newer macOS)
365
+ const uid = getUid();
366
+ if (uid == null) {
367
+ throw new Error('[local] Unable to determine UID for launchctl bootstrap.');
368
+ }
369
+
370
+ // bootstrap requires the plist
371
+ await run('launchctl', ['bootstrap', `gui/${uid}`, plistPath]);
372
+ await launchctlTry(['enable', `gui/${uid}/${label}`]);
373
+ await launchctlTry(['kickstart', '-k', `gui/${uid}/${label}`]);
374
+ }
375
+
376
+ async function postStartDiagnostics() {
377
+ const rootDir = getRootDir(import.meta.url);
378
+ const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
379
+
380
+ const cliHomeDir = process.env.HAPPIER_STACK_CLI_HOME_DIR?.trim()
381
+ ? expandHome(process.env.HAPPIER_STACK_CLI_HOME_DIR.trim())
382
+ : join(getDefaultAutostartPaths().baseDir, 'cli');
383
+
384
+ let port = 3005;
385
+ try {
386
+ port = Number(new URL(internalUrl).port || 0) || 3005;
387
+ } catch {
388
+ port = 3005;
389
+ }
390
+ const { publicServerUrl: publicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port });
391
+
392
+ const cliDir = getComponentDir(rootDir, 'happier-cli');
393
+ const cliBin = join(cliDir, 'bin', 'happier.mjs');
394
+
395
+ const credentialPaths = resolveStackCredentialPaths({ cliHomeDir, serverUrl: internalUrl });
396
+ const existingCredentialPath = findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalUrl });
397
+ const accessKey = existingCredentialPath || credentialPaths.serverScopedPath;
398
+ const daemonPaths = resolvePreferredStackDaemonStatePaths({ cliHomeDir, serverUrl: internalUrl });
399
+ const stateFile = daemonPaths.statePath;
400
+ const lockFile = daemonPaths.lockPath;
401
+ const logsDir = join(cliHomeDir, 'logs');
402
+
403
+ const latestDaemonLog = async () => {
404
+ try {
405
+ const ls = await runCapture('bash', ['-lc', `ls -1t "${logsDir}"/*-daemon.log 2>/dev/null | head -1 || true`]);
406
+ const p = ls.trim();
407
+ return p || null;
408
+ } catch {
409
+ return null;
410
+ }
411
+ };
412
+
413
+ const checkOnce = async () => {
414
+ // If state exists, trust it.
415
+ if (existsSync(stateFile)) {
416
+ try {
417
+ const raw = await readFile(stateFile, 'utf-8');
418
+ const s = JSON.parse(raw);
419
+ const pid = Number(s?.pid);
420
+ if (Number.isFinite(pid) && pid > 0) {
421
+ try {
422
+ process.kill(pid, 0);
423
+ return { ok: true, kind: 'running', pid };
424
+ } catch {
425
+ return { ok: false, kind: 'stale_state', pid };
426
+ }
427
+ }
428
+ } catch {
429
+ return { ok: false, kind: 'bad_state' };
430
+ }
431
+ }
432
+
433
+ // No state yet: check lock PID (daemon may be starting or waiting for auth).
434
+ if (existsSync(lockFile)) {
435
+ try {
436
+ const raw = (await readFile(lockFile, 'utf-8')).trim();
437
+ const pid = Number(raw);
438
+ if (Number.isFinite(pid) && pid > 0) {
439
+ try {
440
+ process.kill(pid, 0);
441
+ const logPath = await latestDaemonLog();
442
+ const tail = logPath ? await readLastLines(logPath, 120) : null;
443
+ if (tail && (tail.includes('No credentials found') || tail.includes('authentication flow') || tail.includes('Waiting for credentials'))) {
444
+ return { ok: false, kind: 'auth_required', pid, logPath };
445
+ }
446
+ return { ok: false, kind: 'starting', pid, logPath };
447
+ } catch {
448
+ return { ok: false, kind: 'stale_lock', pid };
449
+ }
450
+ }
451
+ } catch {
452
+ // ignore
453
+ }
454
+ }
455
+
456
+ return { ok: false, kind: 'stopped' };
457
+ };
458
+
459
+ // Wait briefly for the daemon to settle after a restart.
460
+ let res = await checkOnce();
461
+ for (let i = 0; i < 12 && !res.ok; i++) {
462
+ if (res.kind === 'auth_required') {
463
+ break;
464
+ }
465
+ await new Promise((r) => setTimeout(r, 650));
466
+ // eslint-disable-next-line no-await-in-loop
467
+ res = await checkOnce();
468
+ if (res.ok) {
469
+ break;
470
+ }
471
+ }
472
+
473
+ const stackName = getDefaultAutostartPaths().stackName;
474
+ console.log('');
475
+ console.log(banner('service', { subtitle: `Post-start diagnostics (${stackName})` }));
476
+ console.log('');
477
+
478
+ const authCmd = stackName === 'main' ? 'hstack auth login' : `hstack stack auth ${stackName} login`;
479
+
480
+ if (res.ok && res.kind === 'running') {
481
+ console.log(sectionTitle('Daemon'));
482
+ console.log(bullets([`${green('✓')} running ${dim(`(pid=${res.pid})`)}`, kv('server:', internalUrl)]));
483
+ return;
484
+ }
485
+
486
+ console.log(sectionTitle('Daemon'));
487
+ if (res.kind === 'starting') {
488
+ console.log(bullets([`${yellow('!')} starting ${dim(`(pid=${res.pid ?? 'unknown'})`)}`]));
489
+ } else if (!existingCredentialPath) {
490
+ console.log(bullets([`${yellow('!')} auth required ${dim(`(missing ${accessKey})`)}`]));
491
+ console.log('');
492
+ console.log(sectionTitle('Authenticate'));
493
+ console.log(bullets([`${dim('run:')} ${cmdFmt(authCmd)}`]));
494
+ } else if (res.kind === 'auth_required') {
495
+ console.log(bullets([`${yellow('!')} waiting for auth ${dim(`(pid=${res.pid ?? 'unknown'})`)}`]));
496
+ console.log('');
497
+ console.log(sectionTitle('Authenticate'));
498
+ console.log(bullets([`${dim('run:')} ${cmdFmt(authCmd)}`]));
499
+ } else {
500
+ console.log(bullets([`${yellow('!')} not running`]));
501
+ }
502
+
503
+ const logPath = res.logPath ? res.logPath : await latestDaemonLog();
504
+ console.log('');
505
+ console.log(sectionTitle('Logs'));
506
+ if (logPath) {
507
+ console.log(bullets([kv('latest:', logPath), `${dim('tail:')} ${cmdFmt(`hstack service logs`)}`]));
508
+ const tail = await readLastLines(logPath, 80);
509
+ if (tail) {
510
+ console.log('');
511
+ console.log(dim('--- last 80 daemon log lines ---'));
512
+ console.log(tail);
513
+ console.log(dim('--- end ---'));
514
+ }
515
+ } else {
516
+ console.log(bullets([kv('dir:', logsDir)]));
517
+ }
518
+ }
519
+
520
+ async function stopLaunchAgent({ persistent }) {
521
+ const { plistPath } = getDefaultAutostartPaths();
522
+ if (!existsSync(plistPath)) {
523
+ // Service isn't installed for this stack (common for ad-hoc stacks). Treat as a no-op.
524
+ return;
525
+ }
526
+
527
+ const { label } = getDefaultAutostartPaths();
528
+
529
+ // Old-style
530
+ if (persistent) {
531
+ if (await launchctlTry(['unload', '-w', plistPath])) {
532
+ return;
533
+ }
534
+ } else {
535
+ if (await launchctlTry(['unload', plistPath])) {
536
+ return;
537
+ }
538
+ }
539
+
540
+ // Modern fallback
541
+ const uid = getUid();
542
+ if (uid == null) {
543
+ return;
544
+ }
545
+ await launchctlTry(['bootout', `gui/${uid}/${label}`]);
546
+ }
547
+
548
+ async function waitForLaunchAgentStopped({ timeoutMs = 8000 } = {}) {
549
+ const { label } = getDefaultAutostartPaths();
550
+ const deadline = Date.now() + timeoutMs;
551
+ while (Date.now() < deadline) {
552
+ try {
553
+ const list = await runCapture('launchctl', ['list']);
554
+ const still = list.split('\n').some((l) => l.includes(`\t${label}`) || l.trim().endsWith(` ${label}`) || l.trim() === label);
555
+ if (!still) {
556
+ return true;
557
+ }
558
+ } catch {
559
+ // ignore
560
+ }
561
+ await new Promise((r) => setTimeout(r, 250));
562
+ }
563
+ return false;
564
+ }
565
+
566
+ async function showStatus() {
567
+ const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
568
+ const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
569
+
570
+ console.log('');
571
+ console.log(banner('service', { subtitle: 'Autostart (launchd/systemd user).' }));
572
+ console.log('');
573
+ console.log(sectionTitle('LaunchAgent (macOS)'));
574
+ console.log(
575
+ bullets([
576
+ kv('label:', cyan(label)),
577
+ kv('plist:', `${plistPath} ${existsSync(plistPath) ? green('(present)') : yellow('(missing)')}`),
578
+ kv('stdout:', stdoutPath),
579
+ kv('stderr:', stderrPath),
580
+ ])
581
+ );
582
+
583
+ try {
584
+ const list = await runCapture('launchctl', ['list']);
585
+ const line = list
586
+ .split('\n')
587
+ .map((l) => l.trim())
588
+ .find((l) => l.endsWith(` ${label}`) || l === label || l.includes(`\t${label}`));
589
+ console.log(`${dim('launchctl:')} ${line ? line : dim('(not listed)')}`);
590
+ } catch {
591
+ console.log(`${dim('launchctl:')} ${dim('(unable to query)')}`);
592
+ }
593
+
594
+ // Health can briefly be unavailable right after install/restart; retry a bit.
595
+ const deadline = Date.now() + 10_000;
596
+ while (true) {
597
+ try {
598
+ const res = await fetch(`${internalUrl}/health`, { method: 'GET' });
599
+ const body = await res.text();
600
+ console.log(`${dim('health:')} ${res.ok ? green(String(res.status)) : yellow(String(res.status))} ${dim(body.trim())}`);
601
+ break;
602
+ } catch {
603
+ if (Date.now() >= deadline) {
604
+ console.log(`${dim('health:')} ${yellow('unreachable')} ${dim('(')}${cyan(internalUrl)}${dim(')')}`);
605
+ break;
606
+ }
607
+ await new Promise((r) => setTimeout(r, 500));
608
+ }
609
+ }
610
+
611
+ console.log('');
612
+ console.log(sectionTitle('Tips'));
613
+ console.log(bullets([`${dim('Show status:')} ${cmdFmt('hstack service status')}`, `${dim('View logs:')} ${cmdFmt('hstack service logs')}`]));
614
+ }
615
+
616
+ async function showLogs(lines = 120) {
617
+ const { stdoutPath, stderrPath } = getDefaultAutostartPaths();
618
+ // Use tail if available.
619
+ await run('tail', ['-n', String(lines), stderrPath, stdoutPath]);
620
+ }
621
+
622
+ async function tailLogs() {
623
+ const { stdoutPath, stderrPath } = getDefaultAutostartPaths();
624
+ const child = spawn('tail', ['-f', stderrPath, stdoutPath], { stdio: 'inherit' });
625
+ await new Promise((resolve) => child.on('exit', resolve));
626
+ }
627
+
628
+ async function main() {
629
+ const argv = process.argv.slice(2);
630
+ const helpSepIdx = argv.indexOf('--');
631
+ const helpScopeArgv = helpSepIdx === -1 ? argv : argv.slice(0, helpSepIdx);
632
+ const mode = resolveServiceMode(helpScopeArgv);
633
+ const systemUser = resolveSystemUser(helpScopeArgv);
634
+ ensureLinuxSystemModeSupported({ mode });
635
+
636
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
637
+ throw new Error('[local] service commands are only supported on macOS (launchd) and Linux (systemd).');
638
+ }
639
+ const positionals = helpScopeArgv.filter((a) => a && a !== '--' && !a.startsWith('-'));
640
+ const cmd = positionals[0] ?? 'help';
641
+ const json = wantsJson(helpScopeArgv);
642
+
643
+ const wantsHelpFlag = wantsHelp(helpScopeArgv);
644
+ const usageByCmd = new Map([
645
+ ['install', 'hstack service install [--json]'],
646
+ ['uninstall', 'hstack service uninstall [--json]'],
647
+ ['status', 'hstack service status [--json]'],
648
+ ['start', 'hstack service start [--json]'],
649
+ ['stop', 'hstack service stop [--json]'],
650
+ ['restart', 'hstack service restart [--json]'],
651
+ ['enable', 'hstack service enable [--json]'],
652
+ ['disable', 'hstack service disable [--json]'],
653
+ ['logs', 'hstack service logs [--json]'],
654
+ ['tail', 'hstack service tail'],
655
+ ]);
656
+
657
+ if (wantsHelpFlag && cmd !== 'help') {
658
+ const usage = usageByCmd.get(cmd);
659
+ if (usage) {
660
+ printResult({
661
+ json,
662
+ data: { ok: true, cmd, usage },
663
+ text: [`[service ${cmd}] usage:`, ` ${usage}`, '', 'see also:', ' hstack service --help'].join('\n'),
664
+ });
665
+ return;
666
+ }
667
+ }
668
+
669
+ if (wantsHelpFlag || cmd === 'help') {
670
+ printResult({
671
+ json,
672
+ data: { commands: ['install', 'uninstall', 'status', 'start', 'stop', 'restart', 'enable', 'disable', 'logs', 'tail'] },
673
+ text: [
674
+ banner('service', { subtitle: 'Autostart service management (launchd/systemd).' }),
675
+ '',
676
+ sectionTitle('usage:'),
677
+ ` ${cyan('hstack service')} install|uninstall [--mode=system|user] [--system-user=<name>] [--json]`,
678
+ ` ${cyan('hstack service')} status [--mode=system|user] [--json]`,
679
+ ` ${cyan('hstack service')} start|stop|restart [--mode=system|user] [--json]`,
680
+ ` ${cyan('hstack service')} enable|disable [--mode=system|user] [--json]`,
681
+ ` ${cyan('hstack service')} logs [--mode=system|user] [--json]`,
682
+ ` ${cyan('hstack service')} tail`,
683
+ '',
684
+ sectionTitle('legacy aliases:'),
685
+ bullets([
686
+ dim('hstack service:install|uninstall|status|start|stop|restart|enable|disable'),
687
+ dim('hstack logs | hstack logs:tail'),
688
+ ]),
689
+ ].join('\n'),
690
+ });
691
+ return;
692
+ }
693
+ switch (cmd) {
694
+ case 'install':
695
+ await installService({ mode, systemUser });
696
+ if (json) printResult({ json, data: { ok: true, action: 'install' } });
697
+ return;
698
+ case 'uninstall':
699
+ await uninstallService({ mode });
700
+ if (json) printResult({ json, data: { ok: true, action: 'uninstall' } });
701
+ return;
702
+ case 'status':
703
+ if (json) {
704
+ const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
705
+ let health = null;
706
+ try {
707
+ const res = await fetch(`${internalUrl}/health`, { method: 'GET' });
708
+ const body = await res.text();
709
+ health = { ok: res.ok, status: res.status, body: body.trim() };
710
+ } catch {
711
+ health = { ok: false, status: null, body: null };
712
+ }
713
+
714
+ if (process.platform === 'darwin') {
715
+ const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
716
+ let launchctlLine = null;
717
+ try {
718
+ const list = await runCapture('launchctl', ['list']);
719
+ launchctlLine =
720
+ list
721
+ .split('\n')
722
+ .map((l) => l.trim())
723
+ .find((l) => l.endsWith(` ${label}`) || l === label || l.includes(`\t${label}`)) ?? null;
724
+ } catch {
725
+ launchctlLine = null;
726
+ }
727
+ printResult({ json, data: { label, plistPath, stdoutPath, stderrPath, internalUrl, launchctlLine, health } });
728
+ } else {
729
+ const { unitName, unitPath, systemctlArgsPrefix } = getSystemdUnitInfo({ mode });
730
+ let systemctlStatus = null;
731
+ try {
732
+ systemctlStatus = await runCapture('systemctl', [...systemctlArgsPrefix, 'status', unitName, '--no-pager']);
733
+ } catch (e) {
734
+ systemctlStatus = e && typeof e === 'object' && 'out' in e ? e.out : null;
735
+ }
736
+ printResult({ json, data: { mode, unitName, unitPath, internalUrl, systemctlStatus, health } });
737
+ }
738
+ } else {
739
+ if (process.platform === 'darwin') {
740
+ await showStatus();
741
+ } else {
742
+ if (mode === 'system') {
743
+ const { unitName, systemctlArgsPrefix } = getSystemdUnitInfo({ mode });
744
+ await run('systemctl', [...systemctlArgsPrefix, 'status', unitName, '--no-pager']);
745
+ } else {
746
+ await systemdStatus();
747
+ }
748
+ }
749
+ }
750
+ return;
751
+ case 'start':
752
+ if (process.platform === 'darwin') {
753
+ await startLaunchAgent({ persistent: false });
754
+ } else {
755
+ if (mode === 'system') {
756
+ const { unitName } = getSystemdUnitInfo({ mode });
757
+ await run('systemctl', ['start', unitName]);
758
+ } else {
759
+ await systemdStart({ persistent: false });
760
+ }
761
+ }
762
+ await postStartDiagnostics();
763
+ if (json) printResult({ json, data: { ok: true, action: 'start' } });
764
+ return;
765
+ case 'stop':
766
+ if (process.platform === 'darwin') {
767
+ await stopLaunchAgent({ persistent: false });
768
+ } else {
769
+ if (mode === 'system') {
770
+ const { unitName } = getSystemdUnitInfo({ mode });
771
+ await run('systemctl', ['stop', unitName]);
772
+ } else {
773
+ await systemdStop({ persistent: false });
774
+ }
775
+ }
776
+ if (json) printResult({ json, data: { ok: true, action: 'stop' } });
777
+ return;
778
+ case 'restart':
779
+ if (process.platform === 'darwin') {
780
+ if (!(await restartLaunchAgentBestEffort())) {
781
+ await stopLaunchAgent({ persistent: false });
782
+ await waitForLaunchAgentStopped();
783
+ await startLaunchAgent({ persistent: false });
784
+ }
785
+ } else {
786
+ if (mode === 'system') {
787
+ const { unitName } = getSystemdUnitInfo({ mode });
788
+ await run('systemctl', ['restart', unitName]);
789
+ } else {
790
+ await systemdRestart();
791
+ }
792
+ }
793
+ await postStartDiagnostics();
794
+ if (json) printResult({ json, data: { ok: true, action: 'restart' } });
795
+ return;
796
+ case 'enable':
797
+ if (process.platform === 'darwin') {
798
+ await startLaunchAgent({ persistent: true });
799
+ } else {
800
+ if (mode === 'system') {
801
+ const { unitName } = getSystemdUnitInfo({ mode });
802
+ await run('systemctl', ['enable', '--now', unitName]);
803
+ } else {
804
+ await systemdStart({ persistent: true });
805
+ }
806
+ }
807
+ await postStartDiagnostics();
808
+ if (json) printResult({ json, data: { ok: true, action: 'enable' } });
809
+ return;
810
+ case 'disable':
811
+ if (process.platform === 'darwin') {
812
+ await stopLaunchAgent({ persistent: true });
813
+ } else {
814
+ if (mode === 'system') {
815
+ const { unitName } = getSystemdUnitInfo({ mode });
816
+ await run('systemctl', ['disable', '--now', unitName]);
817
+ } else {
818
+ await systemdStop({ persistent: true });
819
+ }
820
+ }
821
+ if (json) printResult({ json, data: { ok: true, action: 'disable' } });
822
+ return;
823
+ case 'logs':
824
+ if (process.platform === 'darwin') {
825
+ await showLogs();
826
+ } else {
827
+ const lines = Number(process.env.HAPPIER_STACK_LOG_LINES ?? 120) || 120;
828
+ if (mode === 'system') {
829
+ const { unitName, journalctlArgsPrefix } = getSystemdUnitInfo({ mode });
830
+ await run('journalctl', [...journalctlArgsPrefix, '-u', unitName, '-n', String(lines), '--no-pager']);
831
+ } else {
832
+ await systemdLogs({ lines });
833
+ }
834
+ }
835
+ return;
836
+ case 'tail':
837
+ if (process.platform === 'darwin') {
838
+ await tailLogs();
839
+ } else {
840
+ if (mode === 'system') {
841
+ const { unitName, journalctlArgsPrefix } = getSystemdUnitInfo({ mode });
842
+ await run('journalctl', [...journalctlArgsPrefix, '-u', unitName, '-f']);
843
+ } else {
844
+ await systemdTail();
845
+ }
846
+ }
847
+ return;
848
+ default:
849
+ throw new Error(`[local] unknown command: ${cmd}`);
850
+ }
851
+ }
852
+
853
+ function isDirectExecution() {
854
+ try {
855
+ const selfPath = resolve(fileURLToPath(import.meta.url));
856
+ const argvPath = process.argv[1] ? resolve(process.argv[1]) : '';
857
+ return selfPath === argvPath;
858
+ } catch {
859
+ return false;
860
+ }
861
+ }
862
+
863
+ if (isDirectExecution()) {
864
+ main().catch((err) => {
865
+ console.error('[local] failed:', err);
866
+ process.exit(1);
867
+ });
868
+ }