@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,914 @@
1
+ import './utils/env/env.mjs';
2
+ import { spawn } from 'node:child_process';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { join, resolve, sep } from 'node:path';
5
+
6
+ import { printResult } from './utils/cli/cli.mjs';
7
+ import { readEnvObjectFromFile } from './utils/env/read.mjs';
8
+ import { getComponentDir, getRepoDir, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
9
+ import { getStackRuntimeStatePath, readStackRuntimeStateFile } from './utils/stack/runtime_state.mjs';
10
+ import { getEnvValueAny } from './utils/env/values.mjs';
11
+ import { padRight, parsePrefixedLabel, stripAnsi } from './utils/ui/text.mjs';
12
+ import { commandExists } from './utils/proc/commands.mjs';
13
+ import { renderQrAscii } from './utils/ui/qr.mjs';
14
+ import { resolveMobileQrPayload } from './utils/mobile/dev_client_links.mjs';
15
+ import { worktreeSpecFromDir } from './utils/git/worktrees.mjs';
16
+ import { stopStackForTuiExit } from './utils/tui/cleanup.mjs';
17
+ import { terminateProcessGroup } from './utils/proc/terminate.mjs';
18
+
19
+ function nowTs() {
20
+ const d = new Date();
21
+ return d.toISOString().slice(11, 19);
22
+ }
23
+
24
+ function supportsAnsi() {
25
+ if (!process.stdout.isTTY) return false;
26
+ if (process.env.NO_COLOR) return false;
27
+ if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
28
+ return true;
29
+ }
30
+
31
+ function cyan(s) {
32
+ return supportsAnsi() ? `\x1b[36m${s}\x1b[0m` : String(s);
33
+ }
34
+
35
+ function clamp(n, lo, hi) {
36
+ return Math.max(lo, Math.min(hi, n));
37
+ }
38
+
39
+ function mkPane(id, title, { visible = true, kind = 'log' } = {}) {
40
+ return { id, title, kind, visible, lines: [], scroll: 0 };
41
+ }
42
+
43
+ function pushLine(pane, line, { maxLines = 4000 } = {}) {
44
+ pane.lines.push(line);
45
+ if (pane.lines.length > maxLines) {
46
+ pane.lines.splice(0, pane.lines.length - maxLines);
47
+ }
48
+ }
49
+
50
+ function getPaneHeightForLines(lines, { min = 3, max = 16 } = {}) {
51
+ const n = Array.isArray(lines) ? lines.length : 0;
52
+ // +2 for box borders
53
+ return clamp(n + 2, min, max);
54
+ }
55
+
56
+ function drawBox({ x, y, w, h, title, lines, scroll, active = false }) {
57
+ const top = y;
58
+ const bottom = y + h - 1;
59
+ const left = x;
60
+ const horiz = '─'.repeat(Math.max(0, w - 2));
61
+ const t = title ? ` ${title} ` : '';
62
+ const titleStart = Math.max(1, Math.min(w - 2 - t.length, 2));
63
+ const topLine =
64
+ '┌' +
65
+ horiz
66
+ .split('')
67
+ .map((ch, i) => {
68
+ const pos = i + 1;
69
+ if (t && pos >= titleStart && pos < titleStart + t.length) {
70
+ return t[pos - titleStart];
71
+ }
72
+ return ch;
73
+ })
74
+ .join('') +
75
+ '┐';
76
+
77
+ const midLine = '│' + ' '.repeat(Math.max(0, w - 2)) + '│';
78
+ const botLine = '└' + horiz + '┘';
79
+
80
+ const style = (s) => (active ? cyan(s) : s);
81
+
82
+ const out = [];
83
+ out.push({ row: top, col: left, text: style(topLine) });
84
+ for (let r = top + 1; r < bottom; r++) {
85
+ out.push({ row: r, col: left, text: style(midLine) });
86
+ }
87
+ out.push({ row: bottom, col: left, text: style(botLine) });
88
+
89
+ const innerW = Math.max(0, w - 2);
90
+ const innerH = Math.max(0, h - 2);
91
+ const maxScroll = Math.max(0, lines.length - innerH);
92
+ const s = clamp(scroll, 0, maxScroll);
93
+ const start = Math.max(0, lines.length - innerH - s);
94
+ const slice = lines.slice(start, start + innerH);
95
+ for (let i = 0; i < innerH; i++) {
96
+ const line = stripAnsi(slice[i] ?? '');
97
+ out.push({ row: top + 1 + i, col: left + 1, text: padRight(line, innerW) });
98
+ }
99
+
100
+ return { out, maxScroll };
101
+ }
102
+
103
+ function isTuiHelp(argv) {
104
+ if (!argv.length) return true;
105
+ if (argv.length === 1 && (argv[0] === '--help' || argv[0] === 'help')) return true;
106
+ return false;
107
+ }
108
+
109
+ function inferStackNameFromForwardedArgs(args) {
110
+ // Primary: stack-scoped usage: `hstack tui stack <subcmd> <name> ...`
111
+ const i = args.indexOf('stack');
112
+ if (i >= 0) {
113
+ const name = args[i + 2];
114
+ if (name && !name.startsWith('-')) return name;
115
+ }
116
+ // Fallback: use current environment stack (or main).
117
+ return (process.env.HAPPIER_STACK_STACK ?? '').trim() || 'main';
118
+ }
119
+
120
+ function isStackStartLikeForwardedArgs(args) {
121
+ const i = args.indexOf('stack');
122
+ if (i < 0) return false;
123
+ const subcmd = (args[i + 1] ?? '').toString().trim();
124
+ return subcmd === 'dev' || subcmd === 'start';
125
+ }
126
+
127
+ const readEnvObject = readEnvObjectFromFile;
128
+
129
+ async function preflightCorepackYarnForStack({ envPath }) {
130
+ // Corepack caches (and therefore "download yarn?" prompts) are tied to XDG/HOME.
131
+ // In stack mode we isolate HOME/XDG caches per stack, which can cause Corepack to prompt
132
+ // the first time a stack runs Yarn.
133
+ //
134
+ // In `hstack tui`, the child runs under a pseudo-TTY (via `script`) and the TUI consumes
135
+ // all keyboard input, so Corepack's interactive prompt deadlocks.
136
+ //
137
+ // Fix: pre-download Yarn in a *non-tty* subprocess using the stack's isolated HOME/XDG,
138
+ // so later pty runs don't prompt.
139
+ if (!envPath) return;
140
+ const baseDir = resolve(join(envPath, '..'));
141
+ const stackHome = join(baseDir, 'home');
142
+ const cacheBase = join(baseDir, 'cache');
143
+ const env = {
144
+ ...process.env,
145
+ HOME: stackHome,
146
+ USERPROFILE: stackHome,
147
+ XDG_CACHE_HOME: join(cacheBase, 'xdg'),
148
+ YARN_CACHE_FOLDER: join(cacheBase, 'yarn'),
149
+ npm_config_cache: join(cacheBase, 'npm'),
150
+ // Avoid Corepack mutating package.json automatically.
151
+ COREPACK_ENABLE_AUTO_PIN: '0',
152
+ // Best-effort: disable download prompts (may not be honored by all Corepack versions).
153
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: '0',
154
+ // Treat this as non-interactive (helps some tooling).
155
+ CI: process.env.CI ?? '1',
156
+ };
157
+
158
+ await mkdir(stackHome, { recursive: true }).catch(() => {});
159
+ await mkdir(env.XDG_CACHE_HOME, { recursive: true }).catch(() => {});
160
+ await mkdir(env.YARN_CACHE_FOLDER, { recursive: true }).catch(() => {});
161
+ await mkdir(env.npm_config_cache, { recursive: true }).catch(() => {});
162
+ await mkdir(env.COREPACK_HOME, { recursive: true }).catch(() => {});
163
+
164
+ await new Promise((resolvePromise) => {
165
+ const proc = spawn('yarn', ['--version'], {
166
+ env,
167
+ cwd: baseDir,
168
+ // Non-tty stdio: Corepack typically won't prompt; if it does, we still provide "y\n".
169
+ stdio: ['pipe', 'ignore', 'ignore'],
170
+ shell: false,
171
+ });
172
+ try {
173
+ proc.stdin?.write('y\n');
174
+ proc.stdin?.end();
175
+ } catch {
176
+ // ignore
177
+ }
178
+
179
+ const t = setTimeout(() => {
180
+ try {
181
+ proc.kill('SIGKILL');
182
+ } catch {
183
+ // ignore
184
+ }
185
+ resolvePromise();
186
+ }, 60_000);
187
+
188
+ proc.on('exit', () => {
189
+ clearTimeout(t);
190
+ resolvePromise();
191
+ });
192
+ proc.on('error', () => {
193
+ clearTimeout(t);
194
+ resolvePromise();
195
+ });
196
+ });
197
+ }
198
+
199
+ function getEnvVal(env, key) {
200
+ return getEnvValueAny(env, [key]) || '';
201
+ }
202
+
203
+ function nextLineBreakIndex(s) {
204
+ const n = s.indexOf('\n');
205
+ const r = s.indexOf('\r');
206
+ if (n < 0) return r;
207
+ if (r < 0) return n;
208
+ return Math.min(n, r);
209
+ }
210
+
211
+ function consumeLineBreak(buf) {
212
+ if (buf.startsWith('\r\n')) return buf.slice(2);
213
+ if (buf.startsWith('\n') || buf.startsWith('\r')) return buf.slice(1);
214
+ return buf;
215
+ }
216
+
217
+ function formatRepoRef({ rootDir, dir }) {
218
+ const raw = String(dir ?? '').trim();
219
+ if (!raw) return '(unset)';
220
+
221
+ const abs = resolve(raw);
222
+ const defaultDir = resolve(getRepoDir(rootDir, { ...process.env, HAPPIER_STACK_REPO_DIR: '' }));
223
+ if (abs === defaultDir) return 'main';
224
+
225
+ const spec = worktreeSpecFromDir({ rootDir, component: 'happier-ui', dir: abs, env: process.env });
226
+ if (spec) return spec;
227
+ return abs;
228
+ }
229
+
230
+ async function buildStackSummaryLines({ rootDir, stackName }) {
231
+ const { envPath, baseDir } = resolveStackEnvPath(stackName);
232
+ const env = await readEnvObject(envPath);
233
+ const runtimePath = getStackRuntimeStatePath(stackName);
234
+ const runtime = await readStackRuntimeStateFile(runtimePath);
235
+
236
+ const serverComponent =
237
+ getEnvValueAny(env, ['HAPPIER_STACK_SERVER_COMPONENT']) || 'happier-server-light';
238
+
239
+ const ports = runtime?.ports && typeof runtime.ports === 'object' ? runtime.ports : {};
240
+ const expo = runtime?.expo && typeof runtime.expo === 'object' ? runtime.expo : {};
241
+ const expoPort = expo?.port ?? expo?.webPort ?? expo?.mobilePort ?? null;
242
+ const expoDevClientEnabled = Boolean(expo?.devClientEnabled);
243
+ const processes = runtime?.processes && typeof runtime.processes === 'object' ? runtime.processes : {};
244
+
245
+ const lines = [];
246
+ lines.push(`stack: ${stackName}`);
247
+ lines.push(`server: ${serverComponent}`);
248
+ lines.push(`baseDir: ${baseDir}`);
249
+ lines.push(`env: ${envPath}`);
250
+ lines.push(`runtime: ${runtimePath}${runtime ? '' : ' (missing)'}`);
251
+ if (runtime?.startedAt) lines.push(`startedAt: ${runtime.startedAt}`);
252
+ if (runtime?.updatedAt) lines.push(`updatedAt: ${runtime.updatedAt}`);
253
+ if (runtime?.ownerPid) lines.push(`ownerPid: ${runtime.ownerPid}`);
254
+
255
+ lines.push('');
256
+ lines.push('ports:');
257
+ lines.push(` server: ${ports?.server ?? '(unknown)'}`);
258
+ if (expoPort) lines.push(` expo: ${expoPort}`);
259
+ if (ports?.backend) lines.push(` backend: ${ports.backend}`);
260
+
261
+ if (expoPort && expoDevClientEnabled) {
262
+ const payload = resolveMobileQrPayload({ env: process.env, port: Number(expoPort) });
263
+ lines.push('');
264
+ lines.push('expo dev-client links:');
265
+ if (payload.metroUrl) lines.push(` metro: ${payload.metroUrl}`);
266
+ if (payload.scheme && payload.deepLink) lines.push(` link: ${payload.deepLink}`);
267
+ }
268
+
269
+ lines.push('');
270
+ lines.push('pids:');
271
+ if (processes?.serverPid) lines.push(` serverPid: ${processes.serverPid}`);
272
+ if (processes?.expoPid) lines.push(` expoPid: ${processes.expoPid}`);
273
+ if (processes?.daemonPid) lines.push(` daemonPid: ${processes.daemonPid}`);
274
+ if (processes?.uiGatewayPid) lines.push(` uiGatewayPid: ${processes.uiGatewayPid}`);
275
+
276
+ lines.push('');
277
+ lines.push('dirs:');
278
+ const repoRoot = getRepoDir(rootDir, env);
279
+ lines.push(` ${padRight('repo', 16)} ${formatRepoRef({ rootDir, dir: repoRoot })}`);
280
+ // Service subdirs (best-effort; shown relative to repo when possible).
281
+ const uiDir = getComponentDir(rootDir, 'happier-ui', env);
282
+ const cliDir = getComponentDir(rootDir, 'happier-cli', env);
283
+ const serverDir = getComponentDir(rootDir, serverComponent, env);
284
+ const relOrAbs = (absPath) => {
285
+ const p = resolve(String(absPath ?? '').trim());
286
+ const repo = resolve(String(repoRoot ?? '').trim());
287
+ if (!repo) return p;
288
+ const prefix = repo.endsWith(sep) ? repo : repo + sep;
289
+ return p === repo ? '.' : p.startsWith(prefix) ? p.slice(prefix.length) : p;
290
+ };
291
+ lines.push(` ${padRight('ui', 16)} ${relOrAbs(uiDir)}`);
292
+ lines.push(` ${padRight('cli', 16)} ${relOrAbs(cliDir)}`);
293
+ lines.push(` ${padRight('server', 16)} ${relOrAbs(serverDir)}`);
294
+
295
+ return lines;
296
+ }
297
+
298
+ async function buildExpoQrPaneLines({ stackName }) {
299
+ const runtimePath = getStackRuntimeStatePath(stackName);
300
+ const runtime = await readStackRuntimeStateFile(runtimePath);
301
+ const expo = runtime?.expo && typeof runtime.expo === 'object' ? runtime.expo : {};
302
+ const port = Number(expo?.port ?? expo?.mobilePort ?? expo?.webPort);
303
+ const enabled = Boolean(expo?.devClientEnabled);
304
+ if (!enabled || !Number.isFinite(port) || port <= 0) {
305
+ return { visible: false, lines: [] };
306
+ }
307
+
308
+ const payload = resolveMobileQrPayload({ env: process.env, port });
309
+ // Try to keep the QR compact:
310
+ // - qrcode-terminal uses a terminal-friendly pattern with adequate quiet-zone.
311
+ const qr = await renderQrAscii(payload.payload, { small: true });
312
+ const lines = [];
313
+ if (qr.ok) {
314
+ lines.push(...qr.lines);
315
+ } else {
316
+ lines.push(`(QR unavailable) ${qr.error || ''}`.trim());
317
+ }
318
+ return { visible: true, lines };
319
+ }
320
+
321
+ async function main() {
322
+ const argv = process.argv.slice(2);
323
+
324
+ if (isTuiHelp(argv)) {
325
+ printResult({
326
+ json: false,
327
+ data: { usage: 'hstack tui <hstack args...>', json: false },
328
+ text: [
329
+ '[tui] usage:',
330
+ ' hstack tui <hstack args...>',
331
+ '',
332
+ 'examples:',
333
+ ' hstack tui stack dev resume-upstream',
334
+ ' hstack tui stack start resume-upstream',
335
+ ' hstack tui stack auth dev-auth login',
336
+ '',
337
+ 'layouts:',
338
+ ' single : one pane (focused)',
339
+ ' split : two panes (left=orchestration, right=focused)',
340
+ ' columns : multiple panes stacked in two columns (toggle visibility per pane)',
341
+ '',
342
+ 'keys:',
343
+ ' tab / shift+tab : focus next/prev (visible panes only)',
344
+ ' 1..9 : jump to pane index',
345
+ ' v : cycle layout (single → split → columns)',
346
+ ' m : toggle focused pane visibility (columns layout)',
347
+ ' c : clear focused pane',
348
+ ' p : pause/resume rendering',
349
+ ' ↑/↓, PgUp/PgDn : scroll focused pane',
350
+ ' Home/End : jump bottom/top (focused pane)',
351
+ ' q / Ctrl+C : quit (sends SIGINT to child)',
352
+ '',
353
+ 'panes (default):',
354
+ ' orchestration | summary | local | server | expo | daemon | stack logs',
355
+ ].join('\n'),
356
+ });
357
+ return;
358
+ }
359
+
360
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
361
+ throw new Error('[tui] requires a TTY (interactive terminal)');
362
+ }
363
+
364
+ const rootDir = getRootDir(import.meta.url);
365
+ const happysBin = join(rootDir, 'bin', 'hstack.mjs');
366
+ const forwarded = argv;
367
+
368
+ const stackName = inferStackNameFromForwardedArgs(forwarded);
369
+ const { envPath: stackEnvPath } = resolveStackEnvPath(stackName);
370
+
371
+ const panes = [
372
+ mkPane('orch', 'orchestration', { visible: true, kind: 'log' }),
373
+ mkPane('summary', `stack summary (${stackName})`, { visible: true, kind: 'summary' }),
374
+ // Data-only pane: we render QR inside the Expo pane (no separate box).
375
+ mkPane('qr', 'expo QR', { visible: false, kind: 'qr' }),
376
+ mkPane('local', 'local', { visible: true, kind: 'log' }),
377
+ mkPane('server', 'server', { visible: false, kind: 'log' }),
378
+ mkPane('expo', 'expo', { visible: false, kind: 'log' }),
379
+ mkPane('daemon', 'daemon', { visible: false, kind: 'log' }),
380
+ mkPane('stacklog', 'stack logs', { visible: false, kind: 'log' }),
381
+ ];
382
+
383
+ const paneIndexById = new Map(panes.map((p, i) => [p.id, i]));
384
+
385
+ const routeLine = (line) => {
386
+ const label = parsePrefixedLabel(line);
387
+ const normalized = label ? label.toLowerCase() : '';
388
+
389
+ let paneId = 'local';
390
+ if (normalized.includes('server')) paneId = 'server';
391
+ else if (normalized === 'ui') paneId = 'expo';
392
+ else if (normalized === 'mobile') paneId = 'expo';
393
+ else if (normalized === 'expo') paneId = 'expo';
394
+ else if (normalized.includes('daemon')) paneId = 'daemon';
395
+ else if (normalized === 'stack') paneId = 'stacklog';
396
+ else if (normalized === 'local') paneId = 'local';
397
+
398
+ const idx = paneIndexById.get(paneId) ?? paneIndexById.get('local');
399
+ if (panes[idx] && !panes[idx].visible && panes[idx].kind === 'log') {
400
+ panes[idx].visible = true;
401
+ // If the focused pane was hidden before, keep focus stable but ensure render updates layout.
402
+ }
403
+ pushLine(panes[idx], line);
404
+ };
405
+
406
+ const logOrch = (msg) => {
407
+ pushLine(panes[paneIndexById.get('orch')], `[${nowTs()}] ${msg}`);
408
+ };
409
+
410
+ // Preflight Yarn/Corepack for this stack before spawning the pty child.
411
+ // This prevents Corepack "download yarn? [Y/n]" prompts from deadlocking the TUI.
412
+ await preflightCorepackYarnForStack({ envPath: stackEnvPath });
413
+
414
+ let layout = 'columns'; // single | split | columns
415
+ let focused = paneIndexById.get('local'); // default focus
416
+ let paused = false;
417
+ let renderScheduled = false;
418
+
419
+ const wantsPty = process.platform !== 'win32' && (await commandExists('script', { cwd: rootDir }));
420
+ // In TUI mode, we intentionally do not forward keyboard input to the child process (stdin is ignored),
421
+ // so any interactive prompts inside the child would deadlock.
422
+ // Mark the child env so dependency installers can auto-approve safe prompts (Corepack yarn downloads).
423
+ const childEnv = {
424
+ ...process.env,
425
+ HAPPIER_STACK_TUI: '1',
426
+ // Avoid Corepack mutating package.json automatically.
427
+ COREPACK_ENABLE_AUTO_PIN: '0',
428
+ };
429
+ const child = wantsPty
430
+ ? // Use a pseudo-terminal so tools like Expo print QR/status output that they hide in non-TTY mode.
431
+ // `script` is available by default on macOS (and common on Linux).
432
+ spawn('script', ['-q', '/dev/null', process.execPath, happysBin, ...forwarded], {
433
+ cwd: rootDir,
434
+ env: childEnv,
435
+ stdio: ['ignore', 'pipe', 'pipe'],
436
+ detached: process.platform !== 'win32',
437
+ })
438
+ : spawn(process.execPath, [happysBin, ...forwarded], {
439
+ cwd: rootDir,
440
+ env: childEnv,
441
+ stdio: ['ignore', 'pipe', 'pipe'],
442
+ detached: process.platform !== 'win32',
443
+ });
444
+
445
+ logOrch(
446
+ `spawned: ${wantsPty ? 'script -q /dev/null ' : ''}node ${happysBin} ${forwarded.join(' ')} (pid=${child.pid})`
447
+ );
448
+
449
+ const buf = { out: '', err: '' };
450
+ const flush = (kind) => {
451
+ const key = kind === 'stderr' ? 'err' : 'out';
452
+ let b = buf[key];
453
+ while (true) {
454
+ const idx = nextLineBreakIndex(b);
455
+ if (idx < 0) break;
456
+ const line = b.slice(0, idx);
457
+ b = consumeLineBreak(b.slice(idx));
458
+ routeLine(line);
459
+ }
460
+ buf[key] = b;
461
+ };
462
+
463
+ child.stdout?.on('data', (d) => {
464
+ buf.out += d.toString();
465
+ flush('stdout');
466
+ scheduleRender();
467
+ });
468
+ child.stderr?.on('data', (d) => {
469
+ buf.err += d.toString();
470
+ flush('stderr');
471
+ scheduleRender();
472
+ });
473
+ child.on('exit', (code, sig) => {
474
+ logOrch(`child exited (code=${code}, sig=${sig ?? 'null'})`);
475
+ scheduleRender();
476
+ });
477
+
478
+ async function refreshSummary() {
479
+ const idx = paneIndexById.get('summary');
480
+ try {
481
+ const lines = await buildStackSummaryLines({ rootDir, stackName });
482
+ panes[idx].lines = lines;
483
+ } catch (e) {
484
+ panes[idx].lines = [`summary error: ${e instanceof Error ? e.message : String(e)}`];
485
+ }
486
+
487
+ // QR pane: driven by runtime state (expo port) and rendered independently of logs.
488
+ try {
489
+ const qrIdx = paneIndexById.get('qr');
490
+ const qr = await buildExpoQrPaneLines({ stackName });
491
+ // Data-only pane (kept hidden): rendered inside the expo pane.
492
+ panes[qrIdx].visible = false;
493
+ panes[qrIdx].lines = qr.lines;
494
+ } catch {
495
+ const qrIdx = paneIndexById.get('qr');
496
+ panes[qrIdx].visible = false;
497
+ panes[qrIdx].lines = [];
498
+ }
499
+ scheduleRender();
500
+ }
501
+
502
+ const summaryTimer = setInterval(() => {
503
+ if (!paused) {
504
+ void refreshSummary();
505
+ }
506
+ }, 1000);
507
+
508
+ function scheduleRender() {
509
+ if (paused) return;
510
+ if (renderScheduled) return;
511
+ renderScheduled = true;
512
+ setTimeout(() => {
513
+ renderScheduled = false;
514
+ render();
515
+ }, 16);
516
+ }
517
+
518
+ function visiblePaneIndexes() {
519
+ return panes
520
+ .map((p, idx) => ({ p, idx }))
521
+ .filter(({ p }) => p.visible)
522
+ .map(({ idx }) => idx);
523
+ }
524
+
525
+ function focusNext(delta) {
526
+ const visible = visiblePaneIndexes();
527
+ if (!visible.length) return;
528
+ const pos = Math.max(0, visible.indexOf(focused));
529
+ const next = (pos + delta + visible.length) % visible.length;
530
+ focused = visible[next];
531
+ scheduleRender();
532
+ }
533
+
534
+ function scrollFocused(delta) {
535
+ const pane = panes[focused];
536
+ pane.scroll = Math.max(0, pane.scroll + delta);
537
+ scheduleRender();
538
+ }
539
+
540
+ function clearFocused() {
541
+ const pane = panes[focused];
542
+ if (pane.kind === 'summary') return;
543
+ pane.lines = [];
544
+ pane.scroll = 0;
545
+ scheduleRender();
546
+ }
547
+
548
+ function cycleLayout() {
549
+ layout = layout === 'single' ? 'split' : layout === 'split' ? 'columns' : 'single';
550
+ scheduleRender();
551
+ }
552
+
553
+ function toggleFocusedVisibility() {
554
+ if (layout !== 'columns') return;
555
+ const pane = panes[focused];
556
+ if (pane.id === 'orch') return; // always visible
557
+ pane.visible = !pane.visible;
558
+ if (!pane.visible) {
559
+ // Move focus to next visible pane.
560
+ focusNext(+1);
561
+ }
562
+ scheduleRender();
563
+ }
564
+
565
+ function render() {
566
+ if (paused) return;
567
+ const cols = process.stdout.columns ?? 120;
568
+ const rows = process.stdout.rows ?? 40;
569
+ process.stdout.write('\x1b[?25l');
570
+ process.stdout.write('\x1b[2J\x1b[H');
571
+
572
+ const focusPane = panes[focused];
573
+ const focusLabel = focusPane ? `${focusPane.id} (${focusPane.title})` : String(focused);
574
+ const header = `hstack tui | ${forwarded.join(' ')} | layout=${layout} | focus=${focusLabel}`;
575
+ process.stdout.write(padRight(header, cols) + '\n');
576
+
577
+ const bodyY = 1;
578
+ const bodyH = rows - 2;
579
+ const footerY = rows - 1;
580
+
581
+ const drawWrites = [];
582
+
583
+ const contentY = bodyY;
584
+ let contentH = bodyH;
585
+
586
+ if (layout === 'single') {
587
+ const pane = panes[focused];
588
+ const box = drawBox({
589
+ x: 0,
590
+ y: contentY,
591
+ w: cols,
592
+ h: contentH,
593
+ title: pane.title,
594
+ lines: pane.lines,
595
+ scroll: pane.scroll,
596
+ active: true,
597
+ });
598
+ pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
599
+ drawWrites.push(...box.out);
600
+ } else if (layout === 'split') {
601
+ const leftW = Math.floor(cols / 2);
602
+ const rightW = cols - leftW;
603
+
604
+ const leftPane = panes[paneIndexById.get('orch')];
605
+ const rightPane = panes[focused === paneIndexById.get('orch') ? paneIndexById.get('local') : focused];
606
+
607
+ const leftBox = drawBox({
608
+ x: 0,
609
+ y: contentY,
610
+ w: leftW,
611
+ h: contentH,
612
+ title: leftPane.title,
613
+ lines: leftPane.lines,
614
+ scroll: leftPane.scroll,
615
+ active: focused === paneIndexById.get('orch'),
616
+ });
617
+ leftPane.scroll = clamp(leftPane.scroll, 0, leftBox.maxScroll);
618
+ drawWrites.push(...leftBox.out);
619
+
620
+ const rightBox = drawBox({
621
+ x: leftW,
622
+ y: contentY,
623
+ w: rightW,
624
+ h: contentH,
625
+ title: rightPane.title,
626
+ lines: rightPane.lines,
627
+ scroll: rightPane.scroll,
628
+ active: focused === (paneIndexById.get(rightPane.id) ?? focused),
629
+ });
630
+ rightPane.scroll = clamp(rightPane.scroll, 0, rightBox.maxScroll);
631
+ drawWrites.push(...rightBox.out);
632
+ } else {
633
+ // columns: render a compact top row (orch + summary), then render QR alongside Expo logs.
634
+ const orchIdx = paneIndexById.get('orch');
635
+ const summaryIdx = paneIndexById.get('summary');
636
+ const qrIdx = paneIndexById.get('qr');
637
+ const qrPane = panes[qrIdx];
638
+ const qrVisible = Boolean(qrPane?.visible && qrPane.lines?.length);
639
+
640
+ const topPanes = [panes[orchIdx], panes[summaryIdx]];
641
+ const topCount = topPanes.length;
642
+ const topH = getPaneHeightForLines(panes[summaryIdx].lines, { min: 6, max: 14 });
643
+
644
+ const topY = contentY;
645
+ const belowY = contentY + topH;
646
+ const belowH = Math.max(0, contentH - topH);
647
+
648
+ const colW = Math.floor(cols / topCount);
649
+ for (let i = 0; i < topCount; i++) {
650
+ const pane = topPanes[i];
651
+ const x = i === topCount - 1 ? colW * i : colW * i;
652
+ const w = i === topCount - 1 ? cols - colW * i : colW;
653
+ const box = drawBox({
654
+ x,
655
+ y: topY,
656
+ w,
657
+ h: topH,
658
+ title: pane.title,
659
+ lines: pane.lines,
660
+ scroll: pane.scroll,
661
+ active: paneIndexById.get(pane.id) === focused,
662
+ });
663
+ pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
664
+ drawWrites.push(...box.out);
665
+ }
666
+
667
+ // Remaining panes: exclude the top-row panes. QR is rendered inside the expo pane.
668
+ const visibleAll = visiblePaneIndexes()
669
+ .filter((idx) => idx !== orchIdx && idx !== summaryIdx && idx !== qrIdx)
670
+ .map((idx) => panes[idx]);
671
+ const leftW = Math.floor(cols / 2);
672
+ const rightW = cols - leftW;
673
+
674
+ const leftPanes = [];
675
+ const rightPanes = [];
676
+ const expoPane = panes[paneIndexById.get('expo')];
677
+ const visible = visibleAll.filter((p) => p !== expoPane);
678
+ for (let i = 0; i < visible.length; i++) {
679
+ (i % 2 === 0 ? leftPanes : rightPanes).push(visible[i]);
680
+ }
681
+ if (expoPane?.visible) {
682
+ rightPanes.unshift(expoPane);
683
+ }
684
+
685
+ const layoutColumn = (colX, colW, colPanes) => {
686
+ if (!colPanes.length) return;
687
+ const n = colPanes.length;
688
+ const base = Math.max(3, Math.floor(belowH / n));
689
+ let y = belowY;
690
+ for (let i = 0; i < n; i++) {
691
+ const pane = colPanes[i];
692
+ const remaining = belowY + belowH - y;
693
+ let h = i === n - 1 ? remaining : Math.min(base, remaining);
694
+ if (h < 3) break;
695
+ if (pane.id === 'expo') {
696
+ const qrLines = Array.isArray(qrPane?.lines) ? qrPane.lines : [];
697
+ const qrHas = Boolean(qrLines.length);
698
+ const qrMinH = qrHas ? Math.max(6, qrLines.length + 2) : 0; // +2 borders
699
+ if (qrMinH && h < qrMinH) {
700
+ h = Math.min(remaining, qrMinH);
701
+ if (h < 3) break;
702
+ }
703
+
704
+ if (qrHas) {
705
+ // Split the expo pane horizontally:
706
+ // left = expo logs, right = QR. This uses width instead of extra height.
707
+ const maxLineLen = qrLines.reduce((m, l) => Math.max(m, stripAnsi(l).length), 0);
708
+ const minLogW = 24;
709
+ const minQrW = 22;
710
+ const maxQrW = Math.max(0, Math.min(80, colW - minLogW));
711
+ const fixedQrWRaw = (process.env.HAPPIER_STACK_TUI_QR_WIDTH ?? '').toString().trim();
712
+ const fixedQrW = fixedQrWRaw ? Number(fixedQrWRaw) : 44;
713
+ const qrW = clamp(Number.isFinite(fixedQrW) && fixedQrW > 0 ? fixedQrW : maxLineLen + 2, minQrW, maxQrW);
714
+ const canSplit = qrW >= minQrW && colW - qrW >= minLogW;
715
+
716
+ if (canSplit) {
717
+ const logW = colW - qrW;
718
+ const logBox = drawBox({
719
+ x: colX,
720
+ y,
721
+ w: logW,
722
+ h,
723
+ title: pane.title,
724
+ lines: pane.lines,
725
+ scroll: pane.scroll,
726
+ active: paneIndexById.get(pane.id) === focused,
727
+ });
728
+ pane.scroll = clamp(pane.scroll, 0, logBox.maxScroll);
729
+ drawWrites.push(...logBox.out);
730
+
731
+ const qrBox = drawBox({
732
+ x: colX + logW,
733
+ y,
734
+ w: qrW,
735
+ h,
736
+ title: qrPane.title,
737
+ lines: qrLines,
738
+ scroll: 0,
739
+ active: paneIndexById.get(pane.id) === focused,
740
+ });
741
+ drawWrites.push(...qrBox.out);
742
+ } else {
743
+ // Too narrow to split cleanly: fallback to single expo log box.
744
+ const box = drawBox({
745
+ x: colX,
746
+ y,
747
+ w: colW,
748
+ h,
749
+ title: pane.title,
750
+ lines: pane.lines,
751
+ scroll: pane.scroll,
752
+ active: paneIndexById.get(pane.id) === focused,
753
+ });
754
+ pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
755
+ drawWrites.push(...box.out);
756
+ }
757
+ } else {
758
+ const box = drawBox({
759
+ x: colX,
760
+ y,
761
+ w: colW,
762
+ h,
763
+ title: pane.title,
764
+ lines: pane.lines,
765
+ scroll: pane.scroll,
766
+ active: paneIndexById.get(pane.id) === focused,
767
+ });
768
+ pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
769
+ drawWrites.push(...box.out);
770
+ }
771
+ } else {
772
+ const box = drawBox({
773
+ x: colX,
774
+ y,
775
+ w: colW,
776
+ h,
777
+ title: pane.title,
778
+ lines: pane.lines,
779
+ scroll: pane.scroll,
780
+ active: paneIndexById.get(pane.id) === focused,
781
+ });
782
+ pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
783
+ drawWrites.push(...box.out);
784
+ }
785
+ y += h;
786
+ }
787
+ };
788
+
789
+ layoutColumn(0, leftW, leftPanes);
790
+ layoutColumn(leftW, rightW, rightPanes);
791
+ }
792
+
793
+ for (const w of drawWrites) {
794
+ process.stdout.write(`\x1b[${w.row + 1};${w.col + 1}H${w.text}`);
795
+ }
796
+
797
+ const footer =
798
+ 'tab:next shift+tab:prev 1..9:jump v:layout m:toggle-pane c:clear p:pause arrows:scroll q/Ctrl+C:quit';
799
+ process.stdout.write(`\x1b[${footerY + 1};1H` + padRight(footer, cols));
800
+ process.stdout.write('\x1b[?25h');
801
+ }
802
+
803
+ let exiting = false;
804
+ async function shutdownAndExit(code = 0) {
805
+ if (exiting) return;
806
+ exiting = true;
807
+
808
+ clearInterval(summaryTimer);
809
+ try {
810
+ process.stdin.setRawMode(false);
811
+ } catch {
812
+ // ignore
813
+ }
814
+ try {
815
+ process.stdin.pause();
816
+ } catch {
817
+ // ignore
818
+ }
819
+ const childPid = Number(child?.pid);
820
+ if (child.exitCode == null && Number.isFinite(childPid) && childPid > 1) {
821
+ // Ensure the child is actually gone before stack infra cleanup, otherwise a still-running
822
+ // watch process can immediately respawn server/daemon and re-lock the DB.
823
+ await terminateProcessGroup(childPid, { graceMs: 900 });
824
+ }
825
+
826
+ // Best-effort cleanup: when the TUI runs a long-lived `stack dev/start` command, ensure all
827
+ // stack-owned infra processes are stopped (server/expo/daemon) even if the child exits early.
828
+ let cleanupError = null;
829
+ if (isStackStartLikeForwardedArgs(forwarded)) {
830
+ try {
831
+ await stopStackForTuiExit({ rootDir, stackName, json: false, noDocker: false });
832
+ } catch (e) {
833
+ cleanupError = e;
834
+ logOrch(`stop failed: ${e instanceof Error ? e.message : String(e)}`);
835
+ }
836
+ }
837
+
838
+ process.stdout.write('\x1b[2J\x1b[H\x1b[?25h');
839
+ if (cleanupError) {
840
+ // eslint-disable-next-line no-console
841
+ console.error(`[tui] cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
842
+ }
843
+ process.exit(code);
844
+ }
845
+
846
+ function shutdown() {
847
+ shutdownAndExit(0).catch((err) => {
848
+ // eslint-disable-next-line no-console
849
+ console.error(`[tui] shutdown error: ${err instanceof Error ? err.message : String(err)}`);
850
+ process.exit(1);
851
+ });
852
+ }
853
+
854
+ // Ensure we still clean up if the process receives an actual signal (e.g. watch reload / external stop).
855
+ process.on('SIGINT', shutdown);
856
+ process.on('SIGTERM', shutdown);
857
+
858
+ process.stdin.setRawMode(true);
859
+ process.stdin.resume();
860
+ process.stdin.on('data', (d) => {
861
+ const s = d.toString('utf-8');
862
+ if (s === '\u0003' || s === 'q') {
863
+ shutdown();
864
+ return;
865
+ }
866
+ if (s === '\t') return focusNext(+1);
867
+ if (s === '\x1b[Z') return focusNext(-1);
868
+ if (s >= '1' && s <= '9') {
869
+ const idx = Number(s) - 1;
870
+ if (idx >= 0 && idx < panes.length) {
871
+ if (panes[idx].visible) {
872
+ focused = idx;
873
+ scheduleRender();
874
+ }
875
+ }
876
+ return;
877
+ }
878
+ if (s === 'v') return cycleLayout();
879
+ if (s === 'm') return toggleFocusedVisibility();
880
+ if (s === 'c') return clearFocused();
881
+ if (s === 'p') {
882
+ paused = !paused;
883
+ if (!paused) {
884
+ void refreshSummary();
885
+ scheduleRender();
886
+ }
887
+ return;
888
+ }
889
+
890
+ if (s === '\x1b[A') return scrollFocused(+1);
891
+ if (s === '\x1b[B') return scrollFocused(-1);
892
+ if (s === '\x1b[5~') return scrollFocused(+10);
893
+ if (s === '\x1b[6~') return scrollFocused(-10);
894
+ if (s === '\x1b[H') {
895
+ panes[focused].scroll = 1000000;
896
+ scheduleRender();
897
+ return;
898
+ }
899
+ if (s === '\x1b[F') {
900
+ panes[focused].scroll = 0;
901
+ scheduleRender();
902
+ return;
903
+ }
904
+ });
905
+
906
+ await refreshSummary();
907
+ render();
908
+ await new Promise(() => {});
909
+ }
910
+
911
+ main().catch((err) => {
912
+ console.error('[tui] failed:', err);
913
+ process.exit(1);
914
+ });