@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,1324 @@
1
+ import './utils/env/env.mjs';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { parseArgs } from './utils/cli/args.mjs';
6
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
7
+ import { getDefaultAutostartPaths, getHappyStacksHomeDir, getRepoDir, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
8
+ import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
9
+ import { getCanonicalHomeDir } from './utils/env/config.mjs';
10
+ import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
11
+ import { run, runCapture } from './utils/proc/proc.mjs';
12
+ import { waitForHappierHealthOk } from './utils/server/server.mjs';
13
+ import { tailscaleServeEnable, tailscaleServeHttpsUrlForInternalServerUrl } from './tailscale.mjs';
14
+ import { getRuntimeDir } from './utils/paths/runtime.mjs';
15
+ import { homedir } from 'node:os';
16
+ import { installService } from './service.mjs';
17
+ import { getDevAuthKeyPath } from './utils/auth/dev_key.mjs';
18
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
19
+ import { boolFromFlags, boolFromFlagsOrKv } from './utils/cli/flags.mjs';
20
+ import { normalizeProfile, normalizeServerComponent } from './utils/cli/normalize.mjs';
21
+ import { openUrlInBrowser } from './utils/ui/browser.mjs';
22
+ import { commandExists } from './utils/proc/commands.mjs';
23
+ import { readServerPortFromEnvFile, resolveServerPortFromEnv } from './utils/server/port.mjs';
24
+ import { runOrchestratedGuidedAuthFlow } from './utils/auth/orchestrated_stack_auth_flow.mjs';
25
+ import { getVerbosityLevel } from './utils/cli/verbosity.mjs';
26
+ import { runCommandLogged } from './utils/cli/progress.mjs';
27
+ import { bold, cyan, dim, green, yellow } from './utils/ui/ansi.mjs';
28
+ import { expandHome } from './utils/paths/canonical_home.mjs';
29
+ import { listAllStackNames } from './utils/stack/stacks.mjs';
30
+ import { detectSwiftbarPluginInstalled } from './utils/menubar/swiftbar.mjs';
31
+ import { banner, bullets, cmd as cmdFmt, kv, sectionTitle } from './utils/ui/layout.mjs';
32
+ import { applyBindModeToEnv, resolveBindModeFromArgs } from './utils/net/bind_mode.mjs';
33
+ import { ensureDevCheckout } from './utils/git/dev_checkout.mjs';
34
+ import { parseGithubOwnerRepo } from './utils/git/worktrees.mjs';
35
+ import { findAnyCredentialPathInCliHome, findExistingStackCredentialPath } from './utils/auth/credentials_paths.mjs';
36
+
37
+ function resolveWorkspaceDirDefault() {
38
+ const explicit = (process.env.HAPPIER_STACK_WORKSPACE_DIR ?? '').toString().trim();
39
+ if (explicit) return expandHome(explicit);
40
+ return join(getHappyStacksHomeDir(process.env), 'workspace');
41
+ }
42
+
43
+ function normalizeWorkspaceDirInput(raw, { homeDir }) {
44
+ const trimmed = String(raw ?? '').trim();
45
+ const expanded = expandHome(trimmed);
46
+ if (!expanded) return '';
47
+ // If relative, treat it as relative to the home dir (same rule as init.mjs).
48
+ return expanded.startsWith('/') ? expanded : join(homeDir, expanded);
49
+ }
50
+
51
+ async function resolveMainServerPort() {
52
+ // Priority:
53
+ // - explicit env var
54
+ // - main stack env file (preferred)
55
+ // - default
56
+ const hasEnvOverride =
57
+ (process.env.HAPPIER_STACK_SERVER_PORT ?? '').toString().trim() !== '';
58
+ if (hasEnvOverride) {
59
+ return resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
60
+ }
61
+ const envPath = resolveStackEnvPath('main').envPath;
62
+ return await readServerPortFromEnvFile(envPath, { defaultPort: 3005 });
63
+ }
64
+
65
+ function normalizeGithubRepoUrl(raw) {
66
+ const v = String(raw ?? '').trim();
67
+ if (!v) return '';
68
+
69
+ // Accept full URLs and ssh URLs as-is.
70
+ if (v.includes('://') || v.startsWith('git@')) return v;
71
+
72
+ // Convenience: owner/repo -> https://github.com/owner/repo.git
73
+ const m = v.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
74
+ if (m) {
75
+ const owner = m[1];
76
+ const repo = m[2];
77
+ return `https://github.com/${owner}/${repo}.git`;
78
+ }
79
+
80
+ // Fallback: let git try to interpret it (could be a local path).
81
+ return v;
82
+ }
83
+
84
+ async function parseRemoteOwnerRepo({ repoDir, remoteName }) {
85
+ try {
86
+ const url = (await runCapture('git', ['remote', 'get-url', remoteName], { cwd: repoDir })).trim();
87
+ return parseGithubOwnerRepo(url);
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async function canPushToRemote({ repoDir, remoteName }) {
94
+ try {
95
+ // This checks for push credentials + authorization without creating anything.
96
+ await runCapture('git', ['push', '--dry-run', remoteName, 'HEAD:refs/heads/hstack-permission-check'], { cwd: repoDir });
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ async function maybeConfigureForkRemoteForDevProfile({ rootDir, interactive }) {
104
+ const repoDir = getRepoDir(rootDir, process.env);
105
+ if (!existsSync(join(repoDir, '.git'))) return;
106
+
107
+ const upstream = await parseRemoteOwnerRepo({ repoDir, remoteName: 'upstream' });
108
+ const origin = await parseRemoteOwnerRepo({ repoDir, remoteName: 'origin' });
109
+ if (!upstream?.owner || !upstream?.repo || !origin?.owner || !origin?.repo) {
110
+ return;
111
+ }
112
+
113
+ const originIsUpstream = upstream.owner === origin.owner && upstream.repo === origin.repo;
114
+ if (!originIsUpstream) {
115
+ return;
116
+ }
117
+
118
+ // origin points at upstream and the user can't push: they almost certainly need a fork.
119
+ const canPushOrigin = await canPushToRemote({ repoDir, remoteName: 'origin' });
120
+ if (canPushOrigin) {
121
+ return;
122
+ }
123
+
124
+ const forkUrlFromEnv = String(process.env.HAPPIER_STACK_FORK_URL ?? '').trim();
125
+ if (!interactive) {
126
+ if (forkUrlFromEnv) {
127
+ await run('git', ['remote', 'set-url', 'origin', forkUrlFromEnv], { cwd: repoDir });
128
+ return;
129
+ }
130
+ // eslint-disable-next-line no-console
131
+ console.log(
132
+ `${yellow('!')} Dev setup: origin points to upstream (${upstream.owner}/${upstream.repo}), but you don't have push access.\n` +
133
+ `${dim('Fix:')} re-run ${cyan('hstack setup --profile=dev')} in a TTY to configure a fork, or set ${cyan('HAPPIER_STACK_FORK_URL')} to your fork repo URL.`
134
+ );
135
+ return;
136
+ }
137
+
138
+ const configureFork = await withRl(async (rl) => {
139
+ return await promptSelect(rl, {
140
+ title:
141
+ `${bold('GitHub fork')}\n` +
142
+ `${dim('To contribute, you usually need a fork to push branches. Configure your fork as `origin` now?')}`,
143
+ options: [
144
+ { label: `yes ${dim('(recommended)')}`, value: true },
145
+ { label: 'no (skip)', value: false },
146
+ ],
147
+ defaultIndex: 0,
148
+ });
149
+ });
150
+ if (!configureFork) return;
151
+
152
+ // Best-effort open the upstream repo page to make forking easy.
153
+ await openUrlInBrowser(`https://github.com/${upstream.owner}/${upstream.repo}`).catch(() => {});
154
+
155
+ const forkUrl = await withRl(async (rl) => {
156
+ return (await prompt(rl, 'Paste your fork repo URL (https or ssh): ', { defaultValue: forkUrlFromEnv })).trim();
157
+ });
158
+ if (!forkUrl) {
159
+ throw new Error('[setup] missing fork URL.');
160
+ }
161
+
162
+ await run('git', ['remote', 'set-url', 'origin', forkUrl], { cwd: repoDir });
163
+
164
+ // Optional: ensure the fork has a dev branch matching upstream/dev (best-effort).
165
+ const devBranch = String(process.env.HAPPIER_STACK_DEV_BRANCH ?? '').trim() || 'dev';
166
+ let forkHasDev = true;
167
+ try {
168
+ await runCapture('git', ['ls-remote', '--exit-code', '--heads', 'origin', devBranch], { cwd: repoDir });
169
+ } catch {
170
+ forkHasDev = false;
171
+ }
172
+ if (!forkHasDev) {
173
+ try {
174
+ await run('git', ['fetch', 'upstream', devBranch], { cwd: repoDir });
175
+ await run('git', ['push', 'origin', `refs/remotes/upstream/${devBranch}:refs/heads/${devBranch}`], { cwd: repoDir });
176
+ } catch {
177
+ // ignore: user can still contribute via feature branches
178
+ }
179
+ }
180
+ }
181
+
182
+ async function ensureSetupConfigPersisted({ rootDir, profile, serverComponent, tailscaleWanted, menubarMode, happierRepoUrl }) {
183
+ // Repo source here describes where we clone the Happier monorepo from (apps/ui + apps/cli + apps/server).
184
+ const repoSourceForProfile = profile === 'selfhost' ? 'upstream' : null;
185
+ const monoRepo = String(happierRepoUrl ?? '').trim();
186
+ const updates = [
187
+ { key: 'HAPPIER_STACK_SERVER_COMPONENT', value: serverComponent },
188
+ // Default for selfhost:
189
+ // - monorepo: upstream (Happier)
190
+ // - server-light: fork-only today (handled in bootstrap)
191
+ ...(repoSourceForProfile
192
+ ? [
193
+ { key: 'HAPPIER_STACK_REPO_SOURCE', value: repoSourceForProfile },
194
+ ]
195
+ : []),
196
+ ...(monoRepo
197
+ ? [
198
+ // Override the Happier monorepo clone source.
199
+ // This is useful for forks that keep the same monorepo layout under a different repo name.
200
+ { key: 'HAPPIER_STACK_REPO_URL', value: monoRepo },
201
+ ]
202
+ : []),
203
+ { key: 'HAPPIER_STACK_MENUBAR_MODE', value: menubarMode },
204
+ ...(tailscaleWanted
205
+ ? [
206
+ { key: 'HAPPIER_STACK_TAILSCALE_SERVE', value: '1' },
207
+ ]
208
+ : []),
209
+ ];
210
+ await ensureEnvLocalUpdated({ rootDir, updates });
211
+ }
212
+
213
+ async function ensureSystemdAvailable() {
214
+ if (process.platform !== 'linux') return true;
215
+ return (await commandExists('systemctl')) && (await commandExists('journalctl'));
216
+ }
217
+
218
+ async function detectDockerSupport() {
219
+ const installed = await commandExists('docker');
220
+ if (!installed) return { installed: false, running: false };
221
+ try {
222
+ // `docker info` returns non-zero quickly when the daemon isn't running.
223
+ await runCapture('docker', ['info'], { timeoutMs: 2500 });
224
+ return { installed: true, running: true };
225
+ } catch {
226
+ return { installed: true, running: false };
227
+ }
228
+ }
229
+
230
+ async function detectGitSupport() {
231
+ return await commandExists('git');
232
+ }
233
+
234
+ async function detectTailscaleSupport() {
235
+ const installed = await commandExists('tailscale');
236
+ return { installed };
237
+ }
238
+
239
+ function isSwiftbarAppInstalled() {
240
+ if (process.platform !== 'darwin') return false;
241
+ // Best-effort: not exhaustive, but catches the common case.
242
+ return existsSync('/Applications/SwiftBar.app');
243
+ }
244
+
245
+ async function detectIosDevTools() {
246
+ if (process.platform !== 'darwin') return { ok: false, hasXcode: false, hasCocoapods: false };
247
+ const hasXcode = await commandExists('xcodebuild');
248
+ const hasCocoapods = await commandExists('pod');
249
+ return { ok: hasXcode && hasCocoapods, hasXcode, hasCocoapods };
250
+ }
251
+
252
+ async function runSetupPreflight({ profile, serverComponent, tailscaleWanted, menubarWanted, autostartWanted }) {
253
+ // Fail-fast on the truly required bits (so we don't get halfway through and crash).
254
+ const gitOk = await detectGitSupport();
255
+ if (!gitOk) {
256
+ throw new Error(
257
+ `[setup] missing prerequisite: git\n` +
258
+ `hstack needs git to clone/update the Happier repo.\n` +
259
+ `Fix: install git, then re-run setup.`
260
+ );
261
+ }
262
+
263
+ const sandboxed = isSandboxed();
264
+ const allowGlobal = sandboxAllowsGlobalSideEffects();
265
+
266
+ const docker = profile === 'selfhost' ? await detectDockerSupport() : { installed: false, running: false };
267
+ const tailscale = tailscaleWanted ? await detectTailscaleSupport() : { installed: false };
268
+ const ios = profile === 'dev' ? await detectIosDevTools() : { ok: false, hasXcode: false, hasCocoapods: false };
269
+
270
+ const canInstallAutostart = autostartWanted && (!sandboxed || allowGlobal);
271
+ const canInstallMenubar = menubarWanted && process.platform === 'darwin' && (!sandboxed || allowGlobal);
272
+ const canEnableTailscale = tailscaleWanted && tailscale.installed && (!sandboxed || allowGlobal);
273
+
274
+ return {
275
+ gitOk,
276
+ docker,
277
+ tailscale,
278
+ ios,
279
+ sandboxed,
280
+ allowGlobal,
281
+ canInstallAutostart,
282
+ canInstallMenubar,
283
+ canEnableTailscale,
284
+ swiftbarAppInstalled: menubarWanted ? isSwiftbarAppInstalled() : null,
285
+ serverComponent,
286
+ };
287
+ }
288
+
289
+ async function runNodeScript({ rootDir, rel, args = [], env = process.env }) {
290
+ await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
291
+ }
292
+
293
+ async function spawnDetachedNodeScript({ rootDir, rel, args = [], env = process.env }) {
294
+ const child = spawn(process.execPath, [join(rootDir, rel), ...args], {
295
+ cwd: rootDir,
296
+ env,
297
+ stdio: 'ignore',
298
+ detached: process.platform !== 'win32',
299
+ });
300
+ child.unref();
301
+ return child.pid;
302
+ }
303
+
304
+ function mainCliHomeDirForEnvPath(envPath) {
305
+ const { baseDir } = resolveStackEnvPath('main');
306
+ // Prefer stack base dir; envPath is informational and can be legacy/new.
307
+ return join(baseDir, 'cli');
308
+ }
309
+
310
+ function getMainStacksAccessKeyPath() {
311
+ const cliHomeDir = mainCliHomeDirForEnvPath(resolveStackEnvPath('main').envPath);
312
+ return findAnyCredentialPathInCliHome({ cliHomeDir });
313
+ }
314
+
315
+ function getDevAuthStackAccessKeyPath(stackName = 'dev-auth') {
316
+ const { baseDir, envPath } = resolveStackEnvPath(stackName);
317
+ if (!existsSync(envPath)) return null;
318
+ return findAnyCredentialPathInCliHome({ cliHomeDir: join(baseDir, 'cli') });
319
+ }
320
+
321
+ function detectAuthSources() {
322
+ const devKeyPath = getDevAuthKeyPath();
323
+ const mainAccessKeyPath = getMainStacksAccessKeyPath();
324
+ const devAuthAccessKeyPath = getDevAuthStackAccessKeyPath('dev-auth');
325
+ return {
326
+ devKeyPath,
327
+ hasDevKey: existsSync(devKeyPath),
328
+ mainAccessKeyPath,
329
+ hasMainAccessKey: existsSync(mainAccessKeyPath),
330
+ devAuthAccessKeyPath,
331
+ hasDevAuthAccessKey: Boolean(devAuthAccessKeyPath && existsSync(devAuthAccessKeyPath)),
332
+ };
333
+ }
334
+
335
+ async function maybeConfigureAuthDefaults({ rootDir, profile, interactive }) {
336
+ if (!interactive) return;
337
+ if (profile !== 'dev') return;
338
+
339
+ const sources = detectAuthSources();
340
+ const autoSeedEnabled =
341
+ (process.env.HAPPIER_STACK_AUTO_AUTH_SEED ?? '').toString().trim() === '1';
342
+ const seedFrom = (process.env.HAPPIER_STACK_AUTH_SEED_FROM ?? '').toString().trim();
343
+ const linkMode =
344
+ (process.env.HAPPIER_STACK_AUTH_LINK ?? '').toString().trim() === '1' ||
345
+ (process.env.HAPPIER_STACK_AUTH_MODE ?? '').toString().trim().toLowerCase() === 'link';
346
+
347
+ // If we already have dev-auth seeded and configured, don't ask redundant questions.
348
+ // (User can always re-run setup or use stack/auth commands to change this.)
349
+ const alreadyConfiguredDevAuth = autoSeedEnabled && seedFrom === 'dev-auth' && sources.hasDevAuthAccessKey;
350
+ if (alreadyConfiguredDevAuth) {
351
+ // eslint-disable-next-line no-console
352
+ console.log('');
353
+ // eslint-disable-next-line no-console
354
+ console.log(bold('Authentication (development)'));
355
+ // eslint-disable-next-line no-console
356
+ console.log(`${green('✓')} dev-auth auth seeding is already configured`);
357
+ // eslint-disable-next-line no-console
358
+ console.log(`${dim('Seed from:')} ${cyan('dev-auth')}`);
359
+ // eslint-disable-next-line no-console
360
+ console.log(`${dim('Mode:')} ${linkMode ? 'symlink' : 'copy'}`);
361
+ if (sources.hasDevKey) {
362
+ // eslint-disable-next-line no-console
363
+ console.log(`${dim('Dev key:')} configured`);
364
+ }
365
+ // If a user wants to change or recreate:
366
+ // eslint-disable-next-line no-console
367
+ console.log(dim(`Tip: to recreate the seed stack, run: ${yellow('hstack stack create-dev-auth-seed')}`));
368
+ return;
369
+ }
370
+
371
+ // eslint-disable-next-line no-console
372
+ console.log('');
373
+ // eslint-disable-next-line no-console
374
+ console.log(bold('Authentication (development)'));
375
+ // eslint-disable-next-line no-console
376
+ console.log(
377
+ dim(
378
+ `Recommended: set up a dedicated ${cyan('dev-auth')} seed stack so you authenticate once, then new stacks “just work”.`
379
+ )
380
+ );
381
+ const seedChoice = 'dev-auth';
382
+ const linkChoice = 'link';
383
+
384
+ if (!sources.hasDevAuthAccessKey) {
385
+ const wantLoginNow = await withRl(async (rl) => {
386
+ return await promptSelect(rl, {
387
+ title:
388
+ `${bold('Sign in now?')}\n` +
389
+ `${dim('This will create a dedicated dev-auth seed stack and walk you through a guided login in the browser.')}\n` +
390
+ `${dim('After this, new stacks can reuse your auth automatically (recommended).')}`,
391
+ options: [
392
+ { label: `yes (${green('recommended')}) — sign in now`, value: true },
393
+ { label: `no — I will do this later`, value: false },
394
+ ],
395
+ defaultIndex: 0,
396
+ });
397
+ });
398
+
399
+ if (!wantLoginNow) {
400
+ // eslint-disable-next-line no-console
401
+ console.log(dim(`Tip: run ${yellow('hstack stack create-dev-auth-seed dev-auth --login')} anytime to sign in.`));
402
+ return;
403
+ }
404
+
405
+ // Guided wizard: creates stack, starts temporary UI/server, stores dev key (optional), logs in CLI.
406
+ await runNodeScript({
407
+ rootDir,
408
+ rel: 'scripts/stack.mjs',
409
+ args: ['create-dev-auth-seed', 'dev-auth', '--login', '--skip-default-seed'],
410
+ });
411
+ } else {
412
+ // eslint-disable-next-line no-console
413
+ console.log(dim(`Found an existing ${cyan('dev-auth')} seed stack; configuring auth reuse for new stacks.`));
414
+ }
415
+
416
+ await ensureEnvLocalUpdated({
417
+ rootDir,
418
+ updates: [
419
+ { key: 'HAPPIER_STACK_AUTO_AUTH_SEED', value: '1' },
420
+ { key: 'HAPPIER_STACK_AUTH_SEED_FROM', value: seedChoice },
421
+ { key: 'HAPPIER_STACK_AUTH_LINK', value: linkChoice === 'link' ? '1' : '0' },
422
+ ],
423
+ });
424
+
425
+ {
426
+ const envLocalPath = join(getCanonicalHomeDir(), 'env.local');
427
+ // eslint-disable-next-line no-console
428
+ console.log('');
429
+ // eslint-disable-next-line no-console
430
+ console.log(bold('Automatic sign-in for new stacks'));
431
+ // eslint-disable-next-line no-console
432
+ console.log(dim(`Enabled: when you create a new stack, hstack will reuse auth from ${cyan(seedChoice)} automatically.`));
433
+ // eslint-disable-next-line no-console
434
+ console.log(`${dim('Seed from:')} ${cyan(seedChoice)}`);
435
+ // eslint-disable-next-line no-console
436
+ console.log(`${dim('Mode:')} ${linkChoice === 'link' ? 'symlink' : 'copy'} ${dim(linkChoice === 'link' ? '(recommended)' : '')}`.trim());
437
+ // eslint-disable-next-line no-console
438
+ console.log(dim(`Config: ${envLocalPath}`));
439
+ }
440
+
441
+ // Optional: seed existing stacks now (useful if the user already has stacks).
442
+ const allStacks = await listAllStackNames().catch(() => ['main']);
443
+ const candidateTargets = allStacks.filter((s) => s !== 'main' && s !== seedChoice);
444
+ if (candidateTargets.length) {
445
+ const seedNow = await withRl(async (rl) => {
446
+ return await promptSelect(rl, {
447
+ title:
448
+ `${bold('Apply sign-in to existing stacks?')}\n` +
449
+ `${dim(`We found ${candidateTargets.length} existing stack(s) that could reuse your auth automatically.`)}\n` +
450
+ `${dim('This can fix “auth required / no machine” without re-login.')}`,
451
+ options: [
452
+ { label: `yes (${green('recommended')}) — apply to ${candidateTargets.length} stack(s) now`, value: true },
453
+ { label: 'no — leave them as-is', value: false },
454
+ ],
455
+ defaultIndex: 0,
456
+ });
457
+ });
458
+ if (seedNow) {
459
+ const except = ['main'];
460
+ if (seedChoice !== 'main') except.push(seedChoice);
461
+ const args = [
462
+ 'copy-from',
463
+ seedChoice,
464
+ '--all',
465
+ `--except=${except.join(',')}`,
466
+ ...(linkChoice === 'link' ? ['--link'] : []),
467
+ ];
468
+ await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args });
469
+ }
470
+ } else {
471
+ // eslint-disable-next-line no-console
472
+ console.log(dim('No existing stacks detected that need seeding (nothing to do).'));
473
+ }
474
+
475
+ // Dev key UX (for phone/Playwright restores).
476
+ const sourcesAfter = detectAuthSources();
477
+ if (sourcesAfter.hasDevKey) {
478
+ // eslint-disable-next-line no-console
479
+ console.log('');
480
+ // eslint-disable-next-line no-console
481
+ console.log(bold('Dev key (optional, sensitive)'));
482
+ // eslint-disable-next-line no-console
483
+ console.log(dim('This lets you restore the UI account quickly (and can help automation).'));
484
+ // eslint-disable-next-line no-console
485
+ console.log(dim(`Stored at: ${sourcesAfter.devKeyPath}`));
486
+ // eslint-disable-next-line no-console
487
+ console.log(dim(`Tip: to print it later, run: ${yellow('hstack auth dev-key --print')}`));
488
+ } else {
489
+ // eslint-disable-next-line no-console
490
+ console.log(dim(`Tip: to store a dev key later, run: ${yellow('hstack auth dev-key --set "<key>"')}`));
491
+ }
492
+ }
493
+
494
+ async function cmdSetup({ rootDir, argv }) {
495
+ // Alias: `hstack setup pr ...` (maintainer-friendly, idempotent PR setup).
496
+ // This delegates to `tools setup-pr` (implemented in setup_pr.mjs) so the logic stays centralized.
497
+ const firstPositional = argv.find((a) => !a.startsWith('--')) ?? '';
498
+ if (firstPositional === 'pr') {
499
+ const idx = argv.indexOf('pr');
500
+ const forwarded = idx >= 0 ? argv.slice(idx + 1) : [];
501
+ await run(process.execPath, [join(rootDir, 'scripts', 'setup_pr.mjs'), ...forwarded], { cwd: rootDir });
502
+ return;
503
+ }
504
+
505
+ const { flags, kv } = parseArgs(argv);
506
+ const json = wantsJson(argv, { flags });
507
+
508
+ // Optional: bind mode affects how we print URLs (loopback vs LAN).
509
+ // We apply it early so all downstream helpers inherit the same env.
510
+ const bindMode = resolveBindModeFromArgs({ flags, kv });
511
+ if (bindMode) {
512
+ applyBindModeToEnv(process.env, bindMode);
513
+ }
514
+
515
+ if (wantsHelp(argv, { flags })) {
516
+ printResult({
517
+ json,
518
+ data: {
519
+ profiles: ['selfhost', 'dev'],
520
+ flags: [
521
+ '--profile=selfhost|dev',
522
+ '--server=happier-server-light|happier-server',
523
+ '--server-flavor=light|full',
524
+ '--non-interactive',
525
+ '--happier-repo=<owner/repo|url> # override the monorepo clone source',
526
+ '--workspace-dir=/absolute/path # dev profile only',
527
+ '--no-ui-deps # bootstrap: skip UI deps',
528
+ '--no-ui-build # bootstrap: skip UI build',
529
+ '--install-path',
530
+ '--start-now',
531
+ '--bind=loopback|lan',
532
+ '--loopback',
533
+ '--lan',
534
+ '--auth|--no-auth',
535
+ '--tailscale|--no-tailscale',
536
+ '--autostart|--no-autostart',
537
+ '--menubar|--no-menubar',
538
+ '--json',
539
+ ],
540
+ },
541
+ text: [
542
+ '[setup] usage:',
543
+ ' hstack setup',
544
+ ' hstack setup --profile=selfhost',
545
+ ' hstack setup --profile=dev',
546
+ ' hstack setup --profile=dev --workspace-dir=~/Development/happier',
547
+ ' hstack setup --happier-repo=happier-dev/happier',
548
+ ' hstack tools setup-pr --repo=<pr-url|number>',
549
+ ' hstack setup --auth',
550
+ ' hstack setup --no-auth',
551
+ '',
552
+ 'notes:',
553
+ ' - selfhost profile is a guided installer for running Happier locally (optionally with Tailscale + autostart).',
554
+ ' - dev profile prepares a development workspace (bootstrap wizard + optional dev tooling).',
555
+ ' - for PR review, use `hstack tools review-pr` / `hstack tools setup-pr`.',
556
+ ' - server selection: use --server=... or the shorthand --server-flavor=light|full',
557
+ ].join('\n'),
558
+ });
559
+ return;
560
+ }
561
+
562
+ const interactive = isTty() && !flags.has('--non-interactive');
563
+ let profile = normalizeProfile(kv.get('--profile'));
564
+ if (!profile && interactive) {
565
+ profile = await withRl(async (rl) => {
566
+ return await promptSelect(rl, {
567
+ title: bold(`✨ ${cyan('hstack')} setup ✨\n\nWhat is your goal?`),
568
+ options: [
569
+ { label: `${cyan('Self-host')}: use Happier on this machine`, value: 'selfhost' },
570
+ { label: `${cyan('Development')}: worktrees + stacks + contributor workflows`, value: 'dev' },
571
+ ],
572
+ defaultIndex: 0,
573
+ });
574
+ });
575
+ }
576
+ if (!profile) {
577
+ profile = 'selfhost';
578
+ }
579
+
580
+ const verbosity = getVerbosityLevel(process.env);
581
+ const quietUi = interactive && verbosity === 0 && !json;
582
+
583
+ // Optional: override the monorepo clone source (UI + CLI + full server).
584
+ const happierRepoUrl = normalizeGithubRepoUrl(kv.get('--happier-repo'));
585
+ const bootstrapExtraArgs = [];
586
+ if (flags.has('--no-ui-deps')) bootstrapExtraArgs.push('--no-ui-deps');
587
+ if (flags.has('--no-ui-build')) bootstrapExtraArgs.push('--no-ui-build');
588
+
589
+ function isInteractiveChildCommand({ rel, args }) {
590
+ // If a child command needs to prompt the user, it must inherit stdin/stdout.
591
+ // Otherwise setup's quiet mode will break the wizard (stdin is intentionally disabled).
592
+ void rel;
593
+ return args.some((a) => String(a).trim() === '--interactive');
594
+ }
595
+
596
+ async function runNodeScriptMaybeQuiet({ label, rel, args = [], env = process.env, interactiveChild = null }) {
597
+ const childIsInteractive = interactiveChild ?? isInteractiveChildCommand({ rel, args });
598
+ if (!quietUi || childIsInteractive) {
599
+ await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
600
+ return;
601
+ }
602
+ const baseLogDir = join(getHappyStacksHomeDir(process.env), 'logs', 'setup');
603
+ const logPath = join(baseLogDir, `${label.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.${Date.now()}.log`);
604
+ try {
605
+ await runCommandLogged({
606
+ label,
607
+ cmd: process.execPath,
608
+ args: [join(rootDir, rel), ...args],
609
+ cwd: rootDir,
610
+ env,
611
+ logPath,
612
+ quiet: true,
613
+ showSteps: true,
614
+ });
615
+ } catch (e) {
616
+ const lp = e?.logPath ? String(e.logPath) : logPath;
617
+ // eslint-disable-next-line no-console
618
+ console.error(`[setup] failed: ${label}`);
619
+ // eslint-disable-next-line no-console
620
+ console.error(`${dim('log:')} ${lp}`);
621
+ throw e;
622
+ }
623
+ }
624
+
625
+ function printProfileIntro({ profile }) {
626
+ if (!process.stdout.isTTY || json) return;
627
+ const header = profile === 'selfhost' ? `${cyan('Self-host')} setup` : `${cyan('Development')} setup`;
628
+ const lines = [
629
+ '',
630
+ bold(header),
631
+ profile === 'selfhost'
632
+ ? dim('Run Happier locally (optionally with Tailscale + autostart).')
633
+ : dim('Prepare a contributor workspace (repo + worktrees + stacks).'),
634
+ '',
635
+ bold('How Happier runs locally:'),
636
+ profile === 'selfhost'
637
+ ? [
638
+ `- ${cyan('server')}: stores sessions + serves the API`,
639
+ `- ${cyan('web UI')}: where you chat + view sessions`,
640
+ `- ${cyan('daemon')}: background process that runs/streams sessions and lets terminal runs show up in the UI`,
641
+ '',
642
+ dim(`A ${cyan('stack')} is one isolated instance (dirs + ports + database). Setup configures the default stack: ${cyan('main')}.`),
643
+ ]
644
+ : [
645
+ `- ${cyan('workspace')}: your git checkouts (repo + worktrees)`,
646
+ `- ${cyan('stacks')}: isolated runtimes under ${cyan('~/.happier/stacks/<name>')}`,
647
+ `- ${cyan('daemon')}: runs sessions + connects the UI <-> terminal`,
648
+ ],
649
+ '',
650
+ bold('What will happen:'),
651
+ profile === 'selfhost'
652
+ ? [
653
+ `- ${cyan('init')}: set up hstack home + shims`,
654
+ `- ${cyan('bootstrap')}: clone/install the repo`,
655
+ `- ${cyan('start')}: start Happier now (recommended)`,
656
+ `- ${cyan('login')}: guided login (recommended)`,
657
+ '',
658
+ dim(
659
+ `Tip: ${cyan('Happier Self-Host default')} is the simplest local install (no Docker). ${cyan('Happier Self-Host full stack')} needs Docker (Postgres/Redis/Minio).`,
660
+ ),
661
+ ]
662
+ : [
663
+ `- ${cyan('workspace')}: choose where the repo + worktrees live`,
664
+ `- ${cyan('init')}: set up hstack home + shims`,
665
+ `- ${cyan('bootstrap')}: clone/install the repo + dev tooling`,
666
+ `- ${cyan('auth')}: (recommended) set up a ${cyan('dev-auth')} seed stack (login once, reuse everywhere)`,
667
+ `- ${cyan('stacks')}: (recommended) next you’ll create an isolated dev stack for day-to-day work (keeps main stable)`,
668
+ `- ${cyan('mobile')}: (optional) install the iOS dev-client (for phone testing)`,
669
+ '',
670
+ dim(`Tip: for PR work, use ${cyan('worktrees')} (isolated branches) + ${cyan('stacks')} (isolated runtime state).`),
671
+ ],
672
+ '',
673
+ ].flat();
674
+ // eslint-disable-next-line no-console
675
+ console.log(lines.join('\n'));
676
+ }
677
+
678
+ if (interactive) {
679
+ printProfileIntro({ profile });
680
+ }
681
+
682
+ const platform = process.platform;
683
+ const supportsAutostart = platform === 'darwin' || platform === 'linux';
684
+ const supportsMenubar = platform === 'darwin';
685
+
686
+ // Convenience alias: allow `--server-flavor=light|full` for parity with `stack pr` and `tools setup-pr`.
687
+ // `--server=...` always wins when both are specified.
688
+ const serverFlavorFromArg = (kv.get('--server-flavor') ?? '').trim().toLowerCase();
689
+ if (!kv.get('--server') && serverFlavorFromArg) {
690
+ if (serverFlavorFromArg === 'light') kv.set('--server', 'happier-server-light');
691
+ else if (serverFlavorFromArg === 'full') kv.set('--server', 'happier-server');
692
+ else throw new Error(`[setup] invalid --server-flavor=${serverFlavorFromArg} (expected: light|full)`);
693
+ }
694
+
695
+ const serverFromArg = normalizeServerComponent(kv.get('--server'));
696
+ let serverComponent = serverFromArg || normalizeServerComponent(process.env.HAPPIER_STACK_SERVER_COMPONENT) || 'happier-server-light';
697
+ if (profile === 'selfhost' && interactive && !serverFromArg) {
698
+ const docker = await detectDockerSupport();
699
+ if (!docker.installed) {
700
+ serverComponent = 'happier-server-light';
701
+ // eslint-disable-next-line no-console
702
+ console.log(`${green('✓')} Server: ${cyan('Happier Self-Host default')} ${dim('(Docker not detected; simplest local install)')}`);
703
+ } else if (!docker.running) {
704
+ serverComponent = 'happier-server-light';
705
+ // eslint-disable-next-line no-console
706
+ console.log(
707
+ `${green('✓')} Server: ${cyan('Happier Self-Host default')} ${dim('(Docker detected but not running; using simplest option)')}`
708
+ );
709
+ // eslint-disable-next-line no-console
710
+ console.log(dim(`Tip: start Docker Desktop, then re-run setup if you want ${cyan('Happier Self-Host full stack')}.`));
711
+ } else {
712
+ serverComponent = await withRl(async (rl) => {
713
+ const picked = await promptSelect(rl, {
714
+ title: `${bold('Server flavor')}\n${dim('Pick the backend you want to run locally. You can switch later.')}`,
715
+ options: [
716
+ { label: `Happier Self-Host default (${green('recommended')}) — simplest local install (PG_Light via embedded PGlite)`, value: 'happier-server-light' },
717
+ { label: `Happier Self-Host full stack — full server (Postgres/Redis/Minio via Docker)`, value: 'happier-server' },
718
+ ],
719
+ defaultIndex: serverComponent === 'happier-server' ? 1 : 0,
720
+ });
721
+ return picked;
722
+ });
723
+ }
724
+ }
725
+ // If the user explicitly requested full server, enforce Docker availability.
726
+ if (profile === 'selfhost' && serverFromArg === 'happier-server') {
727
+ const docker = await detectDockerSupport();
728
+ if (!docker.installed || !docker.running) {
729
+ throw new Error(
730
+ `[setup] --server=happier-server requires Docker (Postgres/Redis/Minio).\n` +
731
+ `Docker is ${!docker.installed ? 'not installed' : 'not running'}.\n` +
732
+ `Fix: use --server=happier-server-light (simplest), or start Docker and retry.`
733
+ );
734
+ }
735
+ }
736
+
737
+ // Dev profile: pick where to store the repo + worktrees.
738
+ const workspaceDirFlagRaw = (kv.get('--workspace-dir') ?? '').toString().trim();
739
+ const homeDirForWorkspace = getHappyStacksHomeDir(process.env);
740
+ let workspaceDirWanted = workspaceDirFlagRaw ? normalizeWorkspaceDirInput(workspaceDirFlagRaw, { homeDir: homeDirForWorkspace }) : '';
741
+ if (profile === 'dev' && interactive && !workspaceDirWanted) {
742
+ const defaultWorkspaceDir = resolveWorkspaceDirDefault();
743
+ const suggested = defaultWorkspaceDir;
744
+ const helpLines = [
745
+ bold('Workspace location'),
746
+ dim('This is where hstack will keep:'),
747
+ `- ${dim('main')}: ${cyan(join(suggested, 'main'))} ${dim('(stable checkout)')}`,
748
+ `- ${dim('dev')}: ${cyan(join(suggested, 'dev'))} ${dim('(development checkout)')}`,
749
+ `- ${dim('pr')}: ${cyan(join(suggested, 'pr'))}`,
750
+ `- ${dim('local')}: ${cyan(join(suggested, 'local'))}`,
751
+ `- ${dim('tmp')}: ${cyan(join(suggested, 'tmp'))}`,
752
+ '',
753
+ dim('Pick a stable folder that is easy to open in your editor (example: ~/Development/happier).'),
754
+ '',
755
+ ].join('\n');
756
+ // eslint-disable-next-line no-console
757
+ console.log(helpLines);
758
+ const raw = await withRl(async (rl) => {
759
+ return await prompt(rl, `Workspace dir (default: ${suggested}): `, { defaultValue: suggested });
760
+ });
761
+ workspaceDirWanted = normalizeWorkspaceDirInput(raw, { homeDir: homeDirForWorkspace });
762
+ }
763
+ if (profile === 'dev' && workspaceDirWanted) {
764
+ // eslint-disable-next-line no-console
765
+ console.log(`${dim('Workspace:')} ${cyan(workspaceDirWanted)}`);
766
+ }
767
+
768
+ const defaultTailscale = false;
769
+ const defaultAutostart = false;
770
+ const defaultMenubar = false;
771
+ const defaultStartNow = profile === 'selfhost';
772
+ const defaultInstallPath = false;
773
+
774
+ let tailscaleWanted = boolFromFlags({ flags, onFlag: '--tailscale', offFlag: '--no-tailscale', defaultValue: defaultTailscale });
775
+ let autostartWanted = boolFromFlags({ flags, onFlag: '--autostart', offFlag: '--no-autostart', defaultValue: defaultAutostart });
776
+ let menubarWanted = boolFromFlags({ flags, onFlag: '--menubar', offFlag: '--no-menubar', defaultValue: defaultMenubar });
777
+ let startNow = boolFromFlags({ flags, onFlag: '--start-now', offFlag: '--no-start-now', defaultValue: defaultStartNow });
778
+ let installPath = flags.has('--install-path') ? true : defaultInstallPath;
779
+ let authWanted = boolFromFlagsOrKv({
780
+ flags,
781
+ kv,
782
+ onFlag: '--auth',
783
+ offFlag: '--no-auth',
784
+ key: '--auth',
785
+ defaultValue: profile === 'selfhost',
786
+ });
787
+
788
+ if (interactive) {
789
+ if (profile === 'selfhost') {
790
+ // Avoid asking questions when we can infer an existing setup state (unless the user explicitly passed flags).
791
+ const tailscaleExplicit = flags.has('--tailscale') || flags.has('--no-tailscale');
792
+ const autostartExplicit = flags.has('--autostart') || flags.has('--no-autostart');
793
+ const menubarExplicit = flags.has('--menubar') || flags.has('--no-menubar');
794
+ const authExplicit = flags.has('--auth') || flags.has('--no-auth') || kv.has('--auth');
795
+
796
+ // Auth: skip prompt if already configured.
797
+ const mainAccessKeyPath = getMainStacksAccessKeyPath();
798
+ const authAlreadyConfigured = existsSync(mainAccessKeyPath);
799
+ if (!authExplicit && authAlreadyConfigured) {
800
+ authWanted = false;
801
+ // eslint-disable-next-line no-console
802
+ console.log(`${green('✓')} Authentication: already configured ${dim(`(${mainAccessKeyPath})`)}`);
803
+ }
804
+ if (!authExplicit && !authAlreadyConfigured) {
805
+ // Self-host onboarding default: guide login as part of setup.
806
+ authWanted = true;
807
+ // eslint-disable-next-line no-console
808
+ console.log(`${green('✓')} Authentication: will guide you through login ${dim('(recommended)')}`);
809
+ }
810
+
811
+ // Tailscale: skip prompt if already enabled for the main internal URL.
812
+ let tailscaleDetectedHttps = null;
813
+ if (!tailscaleExplicit) {
814
+ try {
815
+ const port = await resolveMainServerPort();
816
+ const internal = `http://127.0.0.1:${port}`;
817
+ tailscaleDetectedHttps = await tailscaleServeHttpsUrlForInternalServerUrl(internal);
818
+ } catch {
819
+ tailscaleDetectedHttps = null;
820
+ }
821
+ if (tailscaleDetectedHttps) {
822
+ tailscaleWanted = true;
823
+ // eslint-disable-next-line no-console
824
+ console.log(`${green('✓')} Remote access: Tailscale Serve already enabled ${dim('→')} ${cyan(tailscaleDetectedHttps)}`);
825
+ }
826
+ }
827
+
828
+ if (!tailscaleExplicit && tailscaleDetectedHttps) {
829
+ // keep tailscaleWanted=true and skip the question
830
+ } else {
831
+ tailscaleWanted = await withRl(async (rl) => {
832
+ const v = await promptSelect(rl, {
833
+ title: `${bold('Remote access')}\n${dim('Optional: use Tailscale Serve to get an HTTPS URL for Happier (secure, recommended for phone access).')}`,
834
+ options: [
835
+ { label: `yes (${green('recommended for phone')}) — enable Tailscale Serve`, value: true },
836
+ { label: 'no (default)', value: false },
837
+ ],
838
+ defaultIndex: tailscaleWanted ? 0 : 1,
839
+ });
840
+ return v;
841
+ });
842
+ }
843
+
844
+ if (supportsAutostart) {
845
+ const a = getDefaultAutostartPaths();
846
+ const autostartAlreadyInstalled =
847
+ process.platform === 'darwin'
848
+ ? Boolean(existsSync(a.plistPath))
849
+ : process.platform === 'linux'
850
+ ? Boolean(existsSync(a.systemdUnitPath))
851
+ : false;
852
+ if (!autostartExplicit && autostartAlreadyInstalled) {
853
+ autostartWanted = false;
854
+ // eslint-disable-next-line no-console
855
+ console.log(`${green('✓')} Autostart: already installed ${dim('(leaving as-is)')}`);
856
+ } else {
857
+ autostartWanted = await withRl(async (rl) => {
858
+ const detail =
859
+ process.platform === 'darwin'
860
+ ? 'macOS: launchd LaunchAgent'
861
+ : process.platform === 'linux'
862
+ ? 'Linux: systemd --user service'
863
+ : '';
864
+ const v = await promptSelect(rl, {
865
+ title:
866
+ `${bold('Autostart')}\n` +
867
+ `${dim('Optional: start Happier automatically at login.')}` +
868
+ (detail ? `\n${dim(detail)}` : ''),
869
+ options: [
870
+ { label: 'yes', value: true },
871
+ { label: 'no (default)', value: false },
872
+ ],
873
+ defaultIndex: autostartWanted ? 0 : 1,
874
+ });
875
+ return v;
876
+ });
877
+ }
878
+ } else {
879
+ autostartWanted = false;
880
+ }
881
+
882
+ if (supportsMenubar) {
883
+ let menubarInstalled = false;
884
+ if (!menubarExplicit) {
885
+ const swift = await detectSwiftbarPluginInstalled();
886
+ menubarInstalled = Boolean(swift.installed);
887
+ if (menubarInstalled) {
888
+ menubarWanted = false;
889
+ // eslint-disable-next-line no-console
890
+ console.log(`${green('✓')} Menu bar: already installed ${dim('(SwiftBar plugin)')}`);
891
+ }
892
+ }
893
+ if (!menubarExplicit && menubarInstalled) {
894
+ // skip question
895
+ } else {
896
+ menubarWanted = await withRl(async (rl) => {
897
+ const v = await promptSelect(rl, {
898
+ title: `${bold('Menu bar (macOS)')}\n${dim('Optional: install the SwiftBar menu to control stacks quickly.')}`,
899
+ options: [
900
+ { label: 'yes', value: true },
901
+ { label: 'no (default)', value: false },
902
+ ],
903
+ defaultIndex: menubarWanted ? 0 : 1,
904
+ });
905
+ return v;
906
+ });
907
+ }
908
+ } else {
909
+ menubarWanted = false;
910
+ }
911
+
912
+ // Self-host onboarding default: start now (end-to-end setup).
913
+ const startNowExplicit = flags.has('--start-now') || flags.has('--no-start-now');
914
+ if (!startNowExplicit) {
915
+ startNow = true;
916
+ }
917
+
918
+ // No interactive auth prompt here: we either detected it's already configured, or we default to guiding login.
919
+
920
+ // Auth requires the stack to be running; if you chose "authenticate now", implicitly start.
921
+ if (authWanted) {
922
+ startNow = true;
923
+ }
924
+ } else if (profile === 'dev') {
925
+ // Dev profile: auth is handled later (after bootstrap) so we can offer the recommended
926
+ // dev-auth seed stack flow (and optional mobile dev-client install).
927
+ }
928
+
929
+ installPath = await withRl(async (rl) => {
930
+ const v = await promptSelect(rl, {
931
+ title:
932
+ `${bold('Command shortcuts')}\n` +
933
+ `${dim(
934
+ `Optional: add ${cyan(join(getCanonicalHomeDir(), 'bin'))} to your shell PATH so you can run ${cyan(
935
+ 'hstack'
936
+ )} from any terminal.`
937
+ )}\n` +
938
+ `${dim(`If you skip this, you can always run commands via ${cyan('npx --yes -p @happier-dev/stack hstack ...')}.`)}`,
939
+ options: [
940
+ { label: `yes (${green('recommended')}, default) — enable ${cyan('hstack')} in your terminal`, value: true },
941
+ { label: `no — keep using ${cyan('npx --yes -p @happier-dev/stack hstack ...')}`, value: false },
942
+ ],
943
+ defaultIndex: 0,
944
+ });
945
+ return v;
946
+ });
947
+ }
948
+
949
+ // Enforce OS support gates even if flags were passed.
950
+ if (!supportsAutostart) autostartWanted = false;
951
+ if (!supportsMenubar) menubarWanted = false;
952
+
953
+ const menubarMode = profile === 'selfhost' ? 'selfhost' : 'dev';
954
+
955
+ // Preflight: warn early + decide what we can actually do this run.
956
+ const preflight = await runSetupPreflight({ profile, serverComponent, tailscaleWanted, menubarWanted, autostartWanted });
957
+ if (interactive && process.stdout.isTTY && !json) {
958
+ // eslint-disable-next-line no-console
959
+ console.log('');
960
+ // eslint-disable-next-line no-console
961
+ console.log(banner('Preflight', { subtitle: profile === 'selfhost' ? 'Check prerequisites for self-hosting.' : 'Check prerequisites for development setup.' }));
962
+
963
+ const lines = [];
964
+ if (profile === 'selfhost') {
965
+ if (serverComponent === 'happier-server') {
966
+ lines.push(
967
+ preflight.docker.installed && preflight.docker.running
968
+ ? `${green('✓')} Docker: running`
969
+ : `${yellow('!')} Docker: ${!preflight.docker.installed ? 'not installed' : 'not running'} (full server needs Docker)`
970
+ );
971
+ } else {
972
+ lines.push(
973
+ preflight.docker.installed
974
+ ? `${green('✓')} Docker: detected ${dim('(not required for server-light)')}`
975
+ : `${dim('•')} Docker: not detected ${dim('(server-light does not need it)')}`
976
+ );
977
+ }
978
+ if (tailscaleWanted) {
979
+ lines.push(
980
+ preflight.tailscale.installed
981
+ ? `${green('✓')} Tailscale: detected`
982
+ : `${yellow('!')} Tailscale: not installed ${dim('(remote HTTPS will be available after install)')}`
983
+ );
984
+ }
985
+ if (menubarWanted && process.platform === 'darwin') {
986
+ lines.push(
987
+ preflight.swiftbarAppInstalled
988
+ ? `${green('✓')} SwiftBar: installed`
989
+ : `${yellow('!')} SwiftBar: not detected ${dim('(plugin can be installed, but you need SwiftBar to use it)')}`
990
+ );
991
+ }
992
+ } else {
993
+ // dev profile: iOS tooling is only relevant if user chooses mobile-dev-client later.
994
+ if (process.platform === 'darwin') {
995
+ lines.push(
996
+ preflight.ios.ok
997
+ ? `${green('✓')} iOS tooling: Xcode + CocoaPods detected`
998
+ : `${dim('•')} iOS tooling: ${!preflight.ios.hasXcode ? 'missing Xcode' : ''}${!preflight.ios.hasXcode && !preflight.ios.hasCocoapods ? ' + ' : ''}${!preflight.ios.hasCocoapods ? 'missing CocoaPods' : ''}`.trim()
999
+ );
1000
+ }
1001
+ }
1002
+ // eslint-disable-next-line no-console
1003
+ console.log(lines.length ? lines.join('\n') : dim('(no checks)'));
1004
+ }
1005
+
1006
+ const config = {
1007
+ profile,
1008
+ platform,
1009
+ interactive,
1010
+ serverComponent,
1011
+ authWanted,
1012
+ tailscaleWanted,
1013
+ autostartWanted,
1014
+ menubarWanted,
1015
+ startNow,
1016
+ installPath,
1017
+ runtimeDir: getRuntimeDir(),
1018
+ };
1019
+ if (json) {
1020
+ printResult({ json, data: config });
1021
+ return;
1022
+ }
1023
+
1024
+ if (interactive && process.stdout.isTTY) {
1025
+ const summary = [
1026
+ '',
1027
+ bold('Ready to set up'),
1028
+ `${dim('Profile:')} ${cyan(profile)}`,
1029
+ ...(profile === 'dev' && workspaceDirWanted ? [`${dim('Workspace:')} ${cyan(workspaceDirWanted)}`] : []),
1030
+ ...(profile === 'selfhost' ? [`${dim('Server:')} ${cyan(serverComponent)}`] : []),
1031
+ '',
1032
+ bold('Press Enter to begin') + dim(' (or Ctrl+C to cancel).'),
1033
+ ].join('\n');
1034
+ // eslint-disable-next-line no-console
1035
+ console.log(summary);
1036
+ await withRl(async (rl) => {
1037
+ await prompt(rl, '', { defaultValue: '' });
1038
+ });
1039
+ }
1040
+
1041
+ // 1) Ensure plumbing exists (runtime + shims + pointer env). Avoid auto-bootstrap here; setup drives bootstrap explicitly.
1042
+ await runNodeScriptMaybeQuiet({
1043
+ label: 'init hstack home',
1044
+ rel: 'scripts/init.mjs',
1045
+ args: [
1046
+ '--no-bootstrap',
1047
+ ...(profile === 'dev' && workspaceDirWanted ? [`--workspace-dir=${workspaceDirWanted}`] : []),
1048
+ ...(installPath ? ['--install-path'] : []),
1049
+ ],
1050
+ env: { ...process.env, HAPPIER_STACK_SETUP_CHILD: '1' },
1051
+ });
1052
+
1053
+ // 2) Persist profile defaults to stack env (server flavor, repo source, tailscale preference, menubar mode).
1054
+ await ensureSetupConfigPersisted({
1055
+ rootDir,
1056
+ profile,
1057
+ serverComponent,
1058
+ tailscaleWanted,
1059
+ menubarMode,
1060
+ happierRepoUrl,
1061
+ });
1062
+
1063
+ // Apply repo override to this process too (so the immediately-following install step sees it),
1064
+ // even if env.local was already loaded earlier in this process.
1065
+ if (happierRepoUrl) {
1066
+ process.env.HAPPIER_STACK_REPO_URL = happierRepoUrl;
1067
+ }
1068
+
1069
+ // 3) Bootstrap the monorepo.
1070
+ if (profile === 'dev') {
1071
+ // Developer setup: use the bootstrap wizard when a TTY is available; otherwise run non-interactively.
1072
+ await runNodeScriptMaybeQuiet({
1073
+ label: 'bootstrap repo',
1074
+ rootDir,
1075
+ rel: 'scripts/install.mjs',
1076
+ // Dev setup: use Expo dev server, so exporting a production web bundle is wasted work.
1077
+ // Users can always run `hstack build` later if they want `hstack start` to serve a prebuilt UI.
1078
+ args: [...(interactive ? ['--interactive'] : []), '--clone', '--no-ui-build'],
1079
+ interactiveChild: interactive,
1080
+ });
1081
+
1082
+ if (interactive) {
1083
+ // Recommended: dev-auth seed stack setup (login once, reuse across stacks).
1084
+ await maybeConfigureAuthDefaults({ rootDir, profile, interactive });
1085
+
1086
+ // Optional: mobile dev-client install (macOS only).
1087
+ if (process.platform === 'darwin') {
1088
+ const installMobile = await withRl(async (rl) => {
1089
+ return await promptSelect(rl, {
1090
+ title: `${bold('Mobile (iOS)')}\n${dim('Optional: install the shared Happier dev-client app on your iPhone (install once, reuse across stacks).')}`,
1091
+ options: [
1092
+ { label: `yes — install iOS dev-client (${yellow('requires Xcode + CocoaPods')})`, value: true },
1093
+ { label: 'no (default)', value: false },
1094
+ ],
1095
+ defaultIndex: 1,
1096
+ });
1097
+ });
1098
+ if (installMobile) {
1099
+ await runNodeScriptMaybeQuiet({
1100
+ label: 'install iOS dev-client',
1101
+ rootDir,
1102
+ rel: 'scripts/mobile_dev_client.mjs',
1103
+ args: ['--install'],
1104
+ });
1105
+ // eslint-disable-next-line no-console
1106
+ console.log(dim(`Tip: run any stack with ${yellow('--mobile')} to get a QR code / deep link for your phone.`));
1107
+ }
1108
+ } else {
1109
+ // eslint-disable-next-line no-console
1110
+ console.log(dim(`Tip: iOS dev-client install is macOS-only. You can still use the web UI on mobile via Tailscale.`));
1111
+ }
1112
+ }
1113
+
1114
+ // Contributor UX: if the user can't push to upstream, offer to configure a fork as origin
1115
+ // (so later `hstack contrib extract --push` defaults to pushing feature branches to origin).
1116
+ await maybeConfigureForkRemoteForDevProfile({ rootDir, interactive });
1117
+
1118
+ // Ensure the dedicated dev checkout exists (workspace/dev on branch "dev").
1119
+ // This is the recommended place for contributors to make changes (main stays stable).
1120
+ await ensureDevCheckout({ rootDir, env: process.env });
1121
+ } else {
1122
+ // Selfhost setup: run non-interactively and keep it simple.
1123
+ await runNodeScriptMaybeQuiet({
1124
+ label: 'bootstrap repo',
1125
+ rootDir,
1126
+ rel: 'scripts/install.mjs',
1127
+ // Self-hosting: always clone the Happier monorepo from upstream.
1128
+ // Light flavor uses embedded Postgres (PGlite) and is handled by the server package itself.
1129
+ args: [`--server=${serverComponent}`, '--upstream', '--clone', ...bootstrapExtraArgs],
1130
+ });
1131
+ }
1132
+
1133
+ // 4) Optional: install autostart (macOS launchd / Linux systemd user).
1134
+ if (autostartWanted) {
1135
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
1136
+ // eslint-disable-next-line no-console
1137
+ console.log(dim(`Autostart skipped in sandbox mode. To allow: ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`));
1138
+ } else {
1139
+ if (process.platform === 'linux') {
1140
+ const ok = await ensureSystemdAvailable();
1141
+ if (!ok) {
1142
+ // eslint-disable-next-line no-console
1143
+ console.log('[setup] autostart skipped: systemd user services not available on this Linux distro.');
1144
+ } else {
1145
+ await installService();
1146
+ }
1147
+ } else {
1148
+ await installService();
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ // 5) Optional: install menubar assets (macOS only).
1154
+ if (menubarWanted && process.platform === 'darwin') {
1155
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
1156
+ // eslint-disable-next-line no-console
1157
+ console.log(dim(`Menu bar install skipped in sandbox mode. To allow: ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`));
1158
+ } else {
1159
+ await runNodeScript({ rootDir, rel: 'scripts/menubar.mjs', args: ['install'] });
1160
+ }
1161
+ }
1162
+
1163
+ // 6) Optional: enable tailscale serve (best-effort).
1164
+ if (tailscaleWanted) {
1165
+ const tailscaleOk = await commandExists('tailscale');
1166
+ if (!tailscaleOk) {
1167
+ // eslint-disable-next-line no-console
1168
+ console.log(`${yellow('!')} Tailscale not installed. To enable remote HTTPS later: ${cyan('hstack tailscale enable')}`);
1169
+ await openUrlInBrowser('https://tailscale.com/download').catch(() => {});
1170
+ } else if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
1171
+ // eslint-disable-next-line no-console
1172
+ console.log(dim(`Tailscale enable skipped in sandbox mode. To allow: ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`));
1173
+ } else {
1174
+ try {
1175
+ const internalPort = await resolveMainServerPort();
1176
+ const internalServerUrl = `http://127.0.0.1:${internalPort}`;
1177
+ const res = await tailscaleServeEnable({ internalServerUrl });
1178
+ if (res?.enableUrl && !res?.httpsUrl) {
1179
+ // eslint-disable-next-line no-console
1180
+ console.log('[setup] tailscale serve requires enabling in your tailnet. Open this URL to continue:');
1181
+ // eslint-disable-next-line no-console
1182
+ console.log(res.enableUrl);
1183
+ // Best-effort open
1184
+ await openUrlInBrowser(res.enableUrl).catch(() => {});
1185
+ }
1186
+ } catch (e) {
1187
+ // eslint-disable-next-line no-console
1188
+ console.log('[setup] tailscale not available. Install it from: https://tailscale.com/download');
1189
+ await openUrlInBrowser('https://tailscale.com/download').catch(() => {});
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ // 7) Optional: start now (without requiring setup to keep running).
1195
+ if (startNow) {
1196
+ // Self-host UX: ensure the prebuilt UI exists so the light server can serve it immediately.
1197
+ // (Without this, `hstack start` will refuse to serve UI when the build dir is missing.)
1198
+ const serveUiWanted = (process.env.HAPPIER_STACK_SERVE_UI ?? '1').toString().trim() !== '0';
1199
+ if (profile === 'selfhost' && serveUiWanted) {
1200
+ const uiBuildDir = process.env.HAPPIER_STACK_UI_BUILD_DIR?.trim()
1201
+ ? process.env.HAPPIER_STACK_UI_BUILD_DIR.trim()
1202
+ : join(getDefaultAutostartPaths().baseDir, 'ui');
1203
+ if (!existsSync(uiBuildDir)) {
1204
+ await runNodeScriptMaybeQuiet({ label: 'build-ui', rel: 'scripts/build.mjs', args: ['--no-tauri'] });
1205
+ }
1206
+ }
1207
+
1208
+ const port = await resolveMainServerPort();
1209
+ const internalServerUrl = `http://127.0.0.1:${port}`;
1210
+
1211
+ if (!autostartWanted) {
1212
+ // Detached background start.
1213
+ await spawnDetachedNodeScript({ rootDir, rel: 'scripts/run.mjs', args: [] });
1214
+ }
1215
+
1216
+ const ready = await waitForHappierHealthOk(internalServerUrl, { timeoutMs: 90_000 });
1217
+ if (!ready) {
1218
+ // eslint-disable-next-line no-console
1219
+ console.log(`[setup] started, but server did not become healthy yet: ${internalServerUrl}`);
1220
+ }
1221
+
1222
+ // Prefer tailscale HTTPS URL if available.
1223
+ let openTarget = `http://localhost:${port}/`;
1224
+ if (tailscaleWanted) {
1225
+ const https = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
1226
+ if (https) {
1227
+ openTarget = https.replace(/\/+$/, '') + '/';
1228
+ }
1229
+ }
1230
+
1231
+ // 8) Optional: auth login (runs interactive browser flow via happier-cli).
1232
+ if (authWanted) {
1233
+ const cliHomeDir = mainCliHomeDirForEnvPath(resolveStackEnvPath('main').envPath);
1234
+ const existingCredentialPath = findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalServerUrl });
1235
+ if (existingCredentialPath) {
1236
+ // eslint-disable-next-line no-console
1237
+ console.log(`[setup] auth: already configured (${existingCredentialPath} exists)`);
1238
+ } else {
1239
+ const env = {
1240
+ ...process.env,
1241
+ HAPPIER_STACK_SERVER_PORT: String(port),
1242
+ };
1243
+ if (interactive) {
1244
+ await runOrchestratedGuidedAuthFlow({
1245
+ rootDir,
1246
+ stackName: 'main',
1247
+ env,
1248
+ verbosity,
1249
+ json: false,
1250
+ });
1251
+ } else {
1252
+ await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: ['auth', 'main', '--', 'login'], env });
1253
+ }
1254
+
1255
+ const credentialAfterLogin = findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalServerUrl });
1256
+ if (!credentialAfterLogin) {
1257
+ // eslint-disable-next-line no-console
1258
+ console.log('[setup] auth: not completed yet (missing credentials). You can retry with: hstack auth login');
1259
+ } else {
1260
+ // eslint-disable-next-line no-console
1261
+ console.log('[setup] auth: complete');
1262
+ }
1263
+ }
1264
+ } else {
1265
+ // eslint-disable-next-line no-console
1266
+ console.log('[setup] tip: when you are ready, authenticate with: hstack auth login');
1267
+ }
1268
+
1269
+ await openUrlInBrowser(openTarget).catch(() => {});
1270
+ // eslint-disable-next-line no-console
1271
+ console.log(`[setup] open: ${openTarget}`);
1272
+ }
1273
+ if (profile === 'selfhost' && authWanted && !startNow) {
1274
+ // eslint-disable-next-line no-console
1275
+ console.log('[setup] auth: skipped because Happier was not started. When ready:');
1276
+ // eslint-disable-next-line no-console
1277
+ console.log(' hstack start');
1278
+ // eslint-disable-next-line no-console
1279
+ console.log(' hstack auth login');
1280
+ }
1281
+
1282
+ // Final tips (keep short).
1283
+ if (profile === 'selfhost') {
1284
+ // eslint-disable-next-line no-console
1285
+ console.log('');
1286
+ // eslint-disable-next-line no-console
1287
+ console.log(green('✓ Setup complete'));
1288
+ // Keep this minimal for first-time users. Setup already started + opened the UI.
1289
+ // eslint-disable-next-line no-console
1290
+ console.log(dim('Happier is ready. If you need help later, run:'));
1291
+ // eslint-disable-next-line no-console
1292
+ console.log(` ${yellow('hstack doctor')}`);
1293
+ // eslint-disable-next-line no-console
1294
+ console.log(` ${yellow('hstack stop --yes')}`);
1295
+ } else {
1296
+ // eslint-disable-next-line no-console
1297
+ console.log('');
1298
+ // eslint-disable-next-line no-console
1299
+ console.log(green('✓ Setup complete'));
1300
+ // eslint-disable-next-line no-console
1301
+ console.log(dim('Next steps (development):'));
1302
+ // eslint-disable-next-line no-console
1303
+ console.log(` ${yellow('hstack stack new dev --interactive')} ${dim('# create a dedicated dev stack (recommended)')}`);
1304
+ // eslint-disable-next-line no-console
1305
+ console.log(` ${yellow('hstack stack dev dev')} ${dim('# run that stack (server + daemon + Expo web)')}`);
1306
+ // eslint-disable-next-line no-console
1307
+ console.log(` ${yellow('hstack wt new ...')} ${dim('# create a worktree for a branch/PR')}`);
1308
+ // eslint-disable-next-line no-console
1309
+ console.log(` ${yellow('hstack stack new ...')} ${dim('# create an isolated runtime stack')}`);
1310
+ // eslint-disable-next-line no-console
1311
+ console.log(` ${yellow('hstack stack dev <name>')} ${dim('# run a specific stack')}`);
1312
+ }
1313
+ }
1314
+
1315
+ async function main() {
1316
+ const rootDir = getRootDir(import.meta.url);
1317
+ const argv = process.argv.slice(2);
1318
+ await cmdSetup({ rootDir, argv });
1319
+ }
1320
+
1321
+ main().catch((err) => {
1322
+ console.error('[setup] failed:', err);
1323
+ process.exit(1);
1324
+ });