@happier-dev/stack 0.1.0-preview.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +159 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +94 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,465 @@
1
+ import './utils/env/env.mjs';
2
+
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { existsSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+
7
+ import { compareVersions, installRuntimeFromNpm, readNpmDistTagVersion, resolveNpmPackageNameOverride } from '@happier-dev/cli-common/update';
8
+
9
+ import { parseArgs } from './utils/cli/args.mjs';
10
+ import { pathExists } from './utils/fs/fs.mjs';
11
+ import { run } from './utils/proc/proc.mjs';
12
+ import { expandHome } from './utils/paths/canonical_home.mjs';
13
+ import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
14
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
15
+ import { getRuntimeDir } from './utils/paths/runtime.mjs';
16
+ import { readJsonIfExists } from './utils/fs/json.mjs';
17
+ import { readPackageJsonVersion } from './utils/fs/package_json.mjs';
18
+ import { banner, bullets, cmd, kv, sectionTitle } from './utils/ui/layout.mjs';
19
+ import { cyan, dim, green, yellow } from './utils/ui/ansi.mjs';
20
+ import { getCanonicalHomeEnvPath, getHomeEnvPath, ensureCanonicalHomeEnvUpdated, ensureHomeEnvUpdated } from './utils/env/config.mjs';
21
+ import { ensureEnvFilePruned } from './utils/env/env_file.mjs';
22
+ import { coerceHappyMonorepoRootFromPath, getDevRepoDir, getRepoDir, getWorkspaceDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
23
+ import { parseEnvToObject } from './utils/env/dotenv.mjs';
24
+
25
+ function packageJsonPathForNodeModules({ rootDir, packageName }) {
26
+ const name = String(packageName ?? '').trim();
27
+ if (!name) return null;
28
+ const parts = name.split('/').filter(Boolean);
29
+ return join(rootDir, 'node_modules', ...parts, 'package.json');
30
+ }
31
+
32
+ function cachePaths() {
33
+ const home = getHappyStacksHomeDir();
34
+ return {
35
+ home,
36
+ cacheDir: join(home, 'cache'),
37
+ updateJson: join(home, 'cache', 'update.json'),
38
+ };
39
+ }
40
+
41
+ async function writeJsonSafe(path, obj) {
42
+ try {
43
+ await mkdir(join(path, '..'), { recursive: true });
44
+ } catch {
45
+ // ignore
46
+ }
47
+ try {
48
+ await writeFile(path, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
49
+ } catch {
50
+ // ignore
51
+ }
52
+ }
53
+
54
+ async function getRuntimeInstalledVersion() {
55
+ const runtimeDir = getRuntimeDir();
56
+ const invoker = await readJsonIfExists(join(getRootDir(import.meta.url), 'package.json'), { defaultValue: null });
57
+ const pkgName = String(invoker?.name ?? '').trim();
58
+ if (!pkgName) return null;
59
+ const pkgJson = packageJsonPathForNodeModules({ rootDir: runtimeDir, packageName: pkgName });
60
+ if (!pkgJson) return null;
61
+ return await readPackageJsonVersion(pkgJson);
62
+ }
63
+
64
+ async function getInvokerVersion({ rootDir }) {
65
+ return await readPackageJsonVersion(join(rootDir, 'package.json'));
66
+ }
67
+
68
+ function parseSelfChannel({ flags, kv }) {
69
+ if (flags.has('--preview')) return 'preview';
70
+ const raw = String(kv.get('--channel') ?? '').trim();
71
+ return raw === 'preview' ? 'preview' : 'stable';
72
+ }
73
+
74
+ async function fetchLatestVersion({ packageName, distTag, cwd }) {
75
+ const pkg = String(packageName ?? '').trim();
76
+ if (!pkg) return null;
77
+ return readNpmDistTagVersion({ packageName: pkg, distTag, cwd, env: process.env });
78
+ }
79
+
80
+ async function cmdStatus({ rootDir, argv }) {
81
+ const { flags, kv: kvArgs } = parseArgs(argv);
82
+ const json = wantsJson(argv, { flags });
83
+ const doCheck = !flags.has('--no-check');
84
+ const channel = parseSelfChannel({ flags, kv: kvArgs });
85
+ const distTag = channel === 'preview' ? 'next' : 'latest';
86
+
87
+ const { updateJson, cacheDir } = cachePaths();
88
+ const invoker = await readJsonIfExists(join(rootDir, 'package.json'), { defaultValue: null });
89
+ const packageName = resolveNpmPackageNameOverride({
90
+ envValue: process.env.HAPPIER_STACK_UPDATE_PACKAGE_NAME,
91
+ fallback: String(invoker?.name ?? '').trim(),
92
+ });
93
+ const invokerVersion = await getInvokerVersion({ rootDir });
94
+ const runtimeDir = getRuntimeDir();
95
+ const runtimeVersion = await getRuntimeInstalledVersion();
96
+
97
+ const cached = await readJsonIfExists(updateJson, { defaultValue: null });
98
+
99
+ let latest = cached?.latest ?? null;
100
+ let checkedAt = cached?.checkedAt ?? null;
101
+ let updateAvailable = Boolean(cached?.updateAvailable);
102
+
103
+ if (doCheck) {
104
+ try {
105
+ latest = await fetchLatestVersion({ packageName, distTag, cwd: rootDir });
106
+ checkedAt = Date.now();
107
+ const current = runtimeVersion || invokerVersion;
108
+ updateAvailable = Boolean(current && latest && compareVersions(latest, current) > 0);
109
+ await mkdir(cacheDir, { recursive: true });
110
+ await writeJsonSafe(updateJson, {
111
+ checkedAt,
112
+ latest,
113
+ current: current || null,
114
+ runtimeVersion: runtimeVersion || null,
115
+ invokerVersion: invokerVersion || null,
116
+ updateAvailable,
117
+ notifiedAt: cached?.notifiedAt ?? null,
118
+ });
119
+ } catch {
120
+ // ignore network/npm failures; keep cached values
121
+ }
122
+ }
123
+
124
+ printResult({
125
+ json,
126
+ data: {
127
+ ok: true,
128
+ invoker: { version: invokerVersion, rootDir },
129
+ runtime: { dir: runtimeDir, installed: Boolean(runtimeVersion), version: runtimeVersion },
130
+ update: { cachedLatest: cached?.latest ?? null, latest, checkedAt, updateAvailable },
131
+ },
132
+ text: [
133
+ '',
134
+ banner('self', { subtitle: 'Runtime install + self-update.' }),
135
+ '',
136
+ sectionTitle('Versions'),
137
+ bullets([
138
+ kv('invoker:', invokerVersion ? cyan(invokerVersion) : dim('unknown')),
139
+ kv('runtime:', runtimeVersion ? cyan(runtimeVersion) : `${yellow('not installed')} ${dim(`(${runtimeDir})`)}`),
140
+ kv('latest:', latest ? cyan(latest) : dim('unknown')),
141
+ checkedAt ? kv('checked:', dim(new Date(checkedAt).toISOString())) : null,
142
+ ].filter(Boolean)),
143
+ updateAvailable ? `\n${yellow('!')} update available: ${cyan(runtimeVersion || invokerVersion || 'current')} → ${cyan(latest)}` : null,
144
+ updateAvailable ? `${dim('Run:')} ${cmd('hstack self update')}` : null,
145
+ '',
146
+ ]
147
+ .filter(Boolean)
148
+ .join('\n'),
149
+ });
150
+ }
151
+
152
+ async function cmdUpdate({ rootDir, argv }) {
153
+ const { flags, kv: kvArgs } = parseArgs(argv);
154
+ const json = wantsJson(argv, { flags });
155
+
156
+ const runtimeDir = getRuntimeDir();
157
+ const to = (kvArgs.get('--to') ?? '').trim();
158
+ const channel = parseSelfChannel({ flags, kv: kvArgs });
159
+ const invoker = await readJsonIfExists(join(rootDir, 'package.json'), { defaultValue: null });
160
+ const pkgName = resolveNpmPackageNameOverride({
161
+ envValue: process.env.HAPPIER_STACK_UPDATE_PACKAGE_NAME,
162
+ fallback: String(invoker?.name ?? '').trim(),
163
+ });
164
+ if (!pkgName) throw new Error('[self] unable to resolve package name (missing package.json name)');
165
+ const spec = to ? `${pkgName}@${to}` : `${pkgName}@${channel === 'preview' ? 'next' : 'latest'}`;
166
+
167
+ // Ensure runtime dir exists.
168
+ await mkdir(runtimeDir, { recursive: true });
169
+
170
+ // Install/update runtime package.
171
+ const installRes = installRuntimeFromNpm({ runtimeDir, spec, cwd: rootDir, env: process.env });
172
+ if (!installRes.ok) {
173
+ // Pre-publish dev fallback: allow updating runtime from the local checkout.
174
+ if (!to && !String(process.env.HAPPIER_STACK_UPDATE_PACKAGE_NAME ?? '').trim() && existsSync(join(rootDir, 'package.json'))) {
175
+ try {
176
+ const raw = await readFile(join(rootDir, 'package.json'), 'utf-8');
177
+ const pkg = JSON.parse(raw);
178
+ if (pkg?.name === pkgName) {
179
+ await run('npm', ['install', '--no-audit', '--no-fund', '--silent', '--prefix', runtimeDir, rootDir], { cwd: rootDir });
180
+ } else {
181
+ throw new Error(installRes.errorMessage);
182
+ }
183
+ } catch {
184
+ throw new Error(installRes.errorMessage);
185
+ }
186
+ } else {
187
+ throw new Error(installRes.errorMessage);
188
+ }
189
+ }
190
+
191
+ // Refresh cache best-effort.
192
+ try {
193
+ const latest = await fetchLatestVersion({ packageName: pkgName, distTag: channel === 'preview' ? 'next' : 'latest', cwd: rootDir });
194
+ const runtimeVersion = await getRuntimeInstalledVersion();
195
+ const invokerVersion = await getInvokerVersion({ rootDir });
196
+ const current = runtimeVersion || invokerVersion;
197
+ const updateAvailable = Boolean(current && latest && compareVersions(latest, current) > 0);
198
+ const { updateJson, cacheDir } = cachePaths();
199
+ await mkdir(cacheDir, { recursive: true });
200
+ await writeJsonSafe(updateJson, {
201
+ checkedAt: Date.now(),
202
+ latest,
203
+ current: current || null,
204
+ runtimeVersion: runtimeVersion || null,
205
+ invokerVersion: invokerVersion || null,
206
+ updateAvailable,
207
+ notifiedAt: null,
208
+ });
209
+ } catch {
210
+ // ignore
211
+ }
212
+
213
+ const runtimeVersionAfter = await getRuntimeInstalledVersion();
214
+ printResult({
215
+ json,
216
+ data: { ok: true, runtimeDir, version: runtimeVersionAfter ?? null, spec },
217
+ text: `${green('✓')} updated runtime in ${cyan(runtimeDir)} ${dim('(')}${cyan(runtimeVersionAfter ?? spec)}${dim(')')}`,
218
+ });
219
+ }
220
+
221
+ async function cmdCheck({ rootDir, argv }) {
222
+ const { flags, kv: kvArgs } = parseArgs(argv);
223
+ const json = wantsJson(argv, { flags });
224
+ const quiet = flags.has('--quiet');
225
+ const channel = parseSelfChannel({ flags, kv: kvArgs });
226
+ const distTag = channel === 'preview' ? 'next' : 'latest';
227
+
228
+ const { updateJson, cacheDir } = cachePaths();
229
+ const runtimeVersion = await getRuntimeInstalledVersion();
230
+ const invokerVersion = await getInvokerVersion({ rootDir });
231
+ const current = runtimeVersion || invokerVersion;
232
+ const invoker = await readJsonIfExists(join(rootDir, 'package.json'), { defaultValue: null });
233
+ const packageName = resolveNpmPackageNameOverride({
234
+ envValue: process.env.HAPPIER_STACK_UPDATE_PACKAGE_NAME,
235
+ fallback: String(invoker?.name ?? '').trim(),
236
+ });
237
+
238
+ let latest = null;
239
+ try {
240
+ latest = await fetchLatestVersion({ packageName, distTag, cwd: rootDir });
241
+ } catch {
242
+ latest = null;
243
+ }
244
+
245
+ const updateAvailable = Boolean(current && latest && compareVersions(latest, current) > 0);
246
+ await mkdir(cacheDir, { recursive: true });
247
+ await writeJsonSafe(updateJson, {
248
+ checkedAt: Date.now(),
249
+ latest,
250
+ current: current || null,
251
+ runtimeVersion: runtimeVersion || null,
252
+ invokerVersion: invokerVersion || null,
253
+ updateAvailable,
254
+ notifiedAt: null,
255
+ });
256
+
257
+ if (quiet) {
258
+ return;
259
+ }
260
+ printResult({
261
+ json,
262
+ data: { ok: true, current: current || null, latest, updateAvailable },
263
+ text: latest
264
+ ? updateAvailable
265
+ ? `${yellow('!')} update available: ${cyan(current)} → ${cyan(latest)}\n${dim('Run:')} ${cmd('hstack self update')}`
266
+ : `${green('✓')} up to date ${dim('(')}${cyan(current)}${dim(')')}`
267
+ : `${yellow('!')} unable to check latest version`,
268
+ });
269
+ }
270
+
271
+ function resolveCliRootCandidate({ rootDir, target }) {
272
+ const raw = String(target ?? '').trim();
273
+ if (!raw) return null;
274
+
275
+ const workspaceDir = getWorkspaceDir(rootDir, process.env);
276
+ if (raw === 'main') {
277
+ return join(getRepoDir(rootDir, process.env), 'apps', 'stack');
278
+ }
279
+ if (raw === 'dev') {
280
+ return join(getDevRepoDir(rootDir, process.env), 'apps', 'stack');
281
+ }
282
+
283
+ // Allow absolute/relative path targets. If the path is a monorepo root, accept apps/stack.
284
+ const expanded = expandHome(raw);
285
+ const abs = expanded.startsWith('/') ? expanded : resolve(process.cwd(), expanded);
286
+ if (existsSync(join(abs, 'bin', 'hstack.mjs'))) return abs;
287
+ if (existsSync(join(abs, 'apps', 'stack', 'bin', 'hstack.mjs'))) return join(abs, 'apps', 'stack');
288
+ if (existsSync(join(abs, 'package.json')) && existsSync(join(abs, 'bin', 'hstack.mjs'))) return abs;
289
+ // If user passed a workspace dir by mistake, allow selecting main/dev via keywords.
290
+ void workspaceDir;
291
+ return abs;
292
+ }
293
+
294
+ async function cmdUseCli({ rootDir, argv }) {
295
+ const { flags, kv } = parseArgs(argv);
296
+ const json = wantsJson(argv, { flags });
297
+
298
+ const positional = argv.filter((a) => !a.startsWith('--'));
299
+ const target = positional[1] ?? '';
300
+ const stackFlag = String(kv.get('--stack') ?? '').trim();
301
+
302
+ const key = 'HAPPIER_STACK_CLI_ROOT_DIR';
303
+ const homeEnv = getHomeEnvPath();
304
+ const canonicalEnv = getCanonicalHomeEnvPath();
305
+
306
+ if ((!target && !stackFlag) || wantsHelp(argv, { flags })) {
307
+ printResult({
308
+ json,
309
+ data: { ok: true, command: 'use-cli', targets: ['default', 'main', 'dev', '/abs/path/to/apps/stack'], flags: ['--stack=<name>'] },
310
+ text: [
311
+ banner('self use-cli', { subtitle: 'Point the hstack shim at a local checkout (or reset to runtime).' }),
312
+ '',
313
+ sectionTitle('usage:'),
314
+ ` ${cyan('hstack self')} use-cli default`,
315
+ ` ${cyan('hstack self')} use-cli main`,
316
+ ` ${cyan('hstack self')} use-cli dev`,
317
+ ` ${cyan('hstack self')} use-cli /abs/path/to/apps/stack`,
318
+ ` ${cyan('hstack self')} use-cli --stack=<name>`,
319
+ '',
320
+ sectionTitle('notes:'),
321
+ bullets([
322
+ `Writes ${cyan(homeEnv)} and ${cyan(canonicalEnv)}.`,
323
+ `Takes effect for new shells (and SwiftBar/launchd) because the ${cyan('hstack')} shim reads the canonical pointer file.`,
324
+ ]),
325
+ ].join('\n'),
326
+ });
327
+ return;
328
+ }
329
+
330
+ if (target === 'default' || target === 'runtime') {
331
+ await ensureEnvFilePruned({ envPath: homeEnv, removeKeys: [key] });
332
+ await ensureEnvFilePruned({ envPath: canonicalEnv, removeKeys: [key] });
333
+ printResult({
334
+ json,
335
+ data: { ok: true, mode: 'runtime' },
336
+ text: `${green('✓')} using runtime CLI (cleared ${cyan(key)})`,
337
+ });
338
+ return;
339
+ }
340
+
341
+ const candidate = (() => {
342
+ if (!stackFlag) return resolveCliRootCandidate({ rootDir, target });
343
+ return null;
344
+ })();
345
+
346
+ if (stackFlag) {
347
+ const { envPath } = resolveStackEnvPath(stackFlag, process.env);
348
+ const raw = existsSync(envPath) ? await readFile(envPath, 'utf-8') : '';
349
+ const parsed = raw ? parseEnvToObject(raw) : {};
350
+ const repoDirRaw = String(parsed.HAPPIER_STACK_REPO_DIR ?? '').trim();
351
+ if (!repoDirRaw) {
352
+ throw new Error(`[self use-cli] stack "${stackFlag}" has no HAPPIER_STACK_REPO_DIR in ${envPath}`);
353
+ }
354
+ const repoRoot = coerceHappyMonorepoRootFromPath(repoDirRaw) || repoDirRaw;
355
+ const fromStack = join(repoRoot, 'apps', 'stack');
356
+ const entry = join(fromStack, 'bin', 'hstack.mjs');
357
+ if (!existsSync(entry)) {
358
+ throw new Error(`[self use-cli] stack "${stackFlag}" repo does not contain apps/stack (${entry} missing)`);
359
+ }
360
+ await ensureHomeEnvUpdated({ updates: [{ key, value: fromStack }] });
361
+ await ensureCanonicalHomeEnvUpdated({ updates: [{ key, value: fromStack }] });
362
+ printResult({
363
+ json,
364
+ data: { ok: true, mode: 'local', stack: stackFlag, cliRootDir: fromStack },
365
+ text: `${green('✓')} using local CLI from stack "${stackFlag}" at ${cyan(fromStack)}`,
366
+ });
367
+ return;
368
+ }
369
+
370
+ const entry = candidate ? join(candidate, 'bin', 'hstack.mjs') : null;
371
+ if (!candidate || !entry || !existsSync(entry)) {
372
+ throw new Error(
373
+ `[self use-cli] invalid target: ${target}\n` +
374
+ `Expected one of: default|main|dev|/abs/path/to/apps/stack\n` +
375
+ `Missing: ${entry || '<path>/bin/hstack.mjs'}`
376
+ );
377
+ }
378
+
379
+ await ensureHomeEnvUpdated({ updates: [{ key, value: candidate }] });
380
+ await ensureCanonicalHomeEnvUpdated({ updates: [{ key, value: candidate }] });
381
+ printResult({
382
+ json,
383
+ data: { ok: true, mode: 'local', cliRootDir: candidate },
384
+ text: `${green('✓')} using local CLI at ${cyan(candidate)}`,
385
+ });
386
+ }
387
+
388
+ async function main() {
389
+ const rootDir = getRootDir(import.meta.url);
390
+ const argv = process.argv.slice(2);
391
+
392
+ const helpSepIdx = argv.indexOf('--');
393
+ const helpScopeArgv = helpSepIdx === -1 ? argv : argv.slice(0, helpSepIdx);
394
+ const { flags } = parseArgs(helpScopeArgv);
395
+ const cmd = helpScopeArgv.find((a) => a && a !== '--' && !a.startsWith('-')) ?? 'help';
396
+
397
+ const wantsHelpFlag = wantsHelp(helpScopeArgv, { flags });
398
+ const json = wantsJson(helpScopeArgv, { flags });
399
+ const usageByCmd = new Map([
400
+ ['status', 'hstack self status [--preview|--channel=preview] [--no-check] [--json]'],
401
+ ['update', 'hstack self update [--preview|--channel=preview] [--to=<version>] [--json]'],
402
+ ['check', 'hstack self check [--preview|--channel=preview] [--quiet] [--json]'],
403
+ ['use-cli', 'hstack self use-cli default|main|dev|/abs/path/to/apps/stack [--json]'],
404
+ ]);
405
+
406
+ if (wantsHelpFlag && cmd !== 'help') {
407
+ const usage = usageByCmd.get(cmd);
408
+ if (usage) {
409
+ printResult({
410
+ json,
411
+ data: { ok: true, cmd, usage },
412
+ text: [`[self ${cmd}] usage:`, ` ${usage}`, '', 'see also:', ' hstack self --help'].join('\n'),
413
+ });
414
+ return;
415
+ }
416
+ }
417
+
418
+ if (wantsHelpFlag || cmd === 'help') {
419
+ printResult({
420
+ json,
421
+ data: { commands: ['status', 'update', 'check', 'use-cli'], flags: ['--preview', '--channel=preview', '--no-check', '--to=<version>', '--quiet'] },
422
+ text: [
423
+ banner('self', { subtitle: 'Runtime install + self-update.' }),
424
+ '',
425
+ sectionTitle('usage:'),
426
+ ` ${cyan('hstack self')} status [--preview|--channel=preview] [--no-check] [--json]`,
427
+ ` ${cyan('hstack self')} update [--preview|--channel=preview] [--to=<version>] [--json]`,
428
+ ` ${cyan('hstack self')} check [--preview|--channel=preview] [--quiet] [--json]`,
429
+ ` ${cyan('hstack self')} use-cli default|main|dev|/abs/path/to/apps/stack [--json]`,
430
+ '',
431
+ sectionTitle('channels:'),
432
+ bullets([
433
+ kv('stable:', dim('npm dist-tag latest')),
434
+ kv('preview:', dim('npm dist-tag next')),
435
+ ]),
436
+ ].join('\n'),
437
+ });
438
+ return;
439
+ }
440
+
441
+ if (cmd === 'status') {
442
+ await cmdStatus({ rootDir, argv });
443
+ return;
444
+ }
445
+ if (cmd === 'update') {
446
+ await cmdUpdate({ rootDir, argv });
447
+ return;
448
+ }
449
+ if (cmd === 'check') {
450
+ await cmdCheck({ rootDir, argv });
451
+ return;
452
+ }
453
+ if (cmd === 'use-cli') {
454
+ await cmdUseCli({ rootDir, argv });
455
+ return;
456
+ }
457
+
458
+ throw new Error(`[self] unknown command: ${cmd}`);
459
+ }
460
+
461
+ main().catch((err) => {
462
+ const msg = err instanceof Error ? err.message : String(err);
463
+ console.error(`[self] failed: ${msg}`);
464
+ process.exit(1);
465
+ });
@@ -0,0 +1,9 @@
1
+ import './utils/env/env.mjs';
2
+
3
+ import { runSelfHostCli } from './self_host_runtime.mjs';
4
+
5
+ runSelfHostCli(process.argv.slice(2)).catch((error) => {
6
+ const msg = error instanceof Error ? error.message : String(error);
7
+ console.error(msg);
8
+ process.exit(1);
9
+ });
@@ -0,0 +1,94 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdtemp, readdir } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import test from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ function formatSpawnSyncResult(result) {
10
+ const stdout = String(result.stdout || '').trim();
11
+ const stderr = String(result.stderr || '').trim();
12
+ const error = result.error ? String(result.error.stack || result.error.message || result.error) : '';
13
+ const status = typeof result.status === 'number' ? String(result.status) : '<null>';
14
+ const signal = result.signal ? String(result.signal) : '<null>';
15
+ return [
16
+ `status=${status}`,
17
+ `signal=${signal}`,
18
+ error ? `error=${error}` : '',
19
+ stdout ? `stdout:\n${stdout}` : '',
20
+ stderr ? `stderr:\n${stderr}` : '',
21
+ ]
22
+ .filter(Boolean)
23
+ .join('\n');
24
+ }
25
+
26
+ function commandExists(cmd) {
27
+ return spawnSync('bash', ['-lc', `command -v ${cmd} >/dev/null 2>&1`], { stdio: 'ignore' }).status === 0;
28
+ }
29
+
30
+ function currentTarget() {
31
+ const os = process.platform === 'linux' ? 'linux' : process.platform === 'darwin' ? 'darwin' : '';
32
+ const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' ? 'arm64' : '';
33
+ if (!os || !arch) return '';
34
+ return `${os}-${arch}`;
35
+ }
36
+
37
+ test('compiled hstack binary runs self-host help outside repo checkout', async (t) => {
38
+ if (!commandExists('bun')) {
39
+ t.skip('bun is required for compiled binary smoke tests');
40
+ return;
41
+ }
42
+ const target = currentTarget();
43
+ if (!target) {
44
+ t.skip(`unsupported platform for smoke test: ${process.platform}-${process.arch}`);
45
+ return;
46
+ }
47
+
48
+ const repoRoot = resolve(fileURLToPath(new URL('../../..', import.meta.url)));
49
+ const version = `0.0.0-smoke.${Date.now()}`;
50
+ const build = spawnSync(
51
+ process.execPath,
52
+ [
53
+ 'scripts/release/build-hstack-binaries.mjs',
54
+ '--channel=preview',
55
+ `--version=${version}`,
56
+ `--targets=${target}`,
57
+ ],
58
+ {
59
+ cwd: repoRoot,
60
+ encoding: 'utf-8',
61
+ env: {
62
+ ...process.env,
63
+ },
64
+ // If this ever hangs on CI, fail with a clear timeout rather than blocking the entire suite.
65
+ timeout: 15 * 60 * 1000,
66
+ maxBuffer: 50 * 1024 * 1024,
67
+ }
68
+ );
69
+ assert.equal(build.status, 0, formatSpawnSyncResult(build));
70
+
71
+ const artifact = join(repoRoot, 'dist', 'release-assets', 'stack', `hstack-v${version}-${target}.tar.gz`);
72
+ const extractDir = await mkdtemp(join(tmpdir(), 'hstack-binary-smoke-'));
73
+ t.after(() => {
74
+ spawnSync('bash', ['-lc', `rm -rf "${extractDir.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
75
+ });
76
+ const untar = spawnSync('tar', ['-xzf', artifact, '-C', extractDir], { encoding: 'utf-8' });
77
+ assert.equal(untar.status, 0, untar.stderr);
78
+
79
+ const entries = await readdir(extractDir);
80
+ assert.ok(entries.length > 0, 'expected extracted artifact directory');
81
+ const binaryPath = join(extractDir, entries[0], 'hstack');
82
+
83
+ const help = spawnSync(binaryPath, ['self-host', '--help'], {
84
+ cwd: '/tmp',
85
+ encoding: 'utf-8',
86
+ env: {
87
+ ...process.env,
88
+ HAPPIER_NONINTERACTIVE: '1',
89
+ },
90
+ });
91
+ assert.equal(help.status, 0, help.stderr || help.stdout);
92
+ assert.match(help.stdout, /hstack self-host install/);
93
+ assert.match(help.stdout, /works without a repository checkout/);
94
+ });