@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,490 @@
1
+ import './utils/env/env.mjs';
2
+ import { parseArgs } from './utils/cli/args.mjs';
3
+ import { run, runCapture } from './utils/proc/proc.mjs';
4
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
5
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
6
+ import { getInternalServerUrl } from './utils/server/urls.mjs';
7
+ import { resolveCommandPath } from './utils/proc/commands.mjs';
8
+ import { constants } from 'node:fs';
9
+ import { access } from 'node:fs/promises';
10
+ import { banner, bullets, cmd as cmdFmt, kv, ok, sectionTitle } from './utils/ui/layout.mjs';
11
+ import { cyan, dim, green } from './utils/ui/ansi.mjs';
12
+
13
+ /**
14
+ * Manage Tailscale Serve for exposing the local UI/API over HTTPS (secure context).
15
+ *
16
+ * This wraps:
17
+ * - `tailscale serve --bg http://127.0.0.1:3005`
18
+ * - `tailscale serve status`
19
+ * - `tailscale serve reset`
20
+ *
21
+ * Commands:
22
+ * - status
23
+ * - enable
24
+ * - disable (alias: reset)
25
+ * - url (print the first https:// URL from status output)
26
+ */
27
+
28
+ function getServeConfig(internalServerUrl) {
29
+ const upstream = process.env.HAPPIER_STACK_TAILSCALE_UPSTREAM?.trim()
30
+ ? process.env.HAPPIER_STACK_TAILSCALE_UPSTREAM.trim()
31
+ : internalServerUrl;
32
+ const servePath = process.env.HAPPIER_STACK_TAILSCALE_SERVE_PATH?.trim()
33
+ ? process.env.HAPPIER_STACK_TAILSCALE_SERVE_PATH.trim()
34
+ : '/';
35
+ return { upstream, servePath };
36
+ }
37
+
38
+ function extractHttpsUrl(serveStatusText) {
39
+ const line = serveStatusText
40
+ .split('\n')
41
+ .map((l) => l.trim())
42
+ .find((l) => l.toLowerCase().includes('https://'));
43
+ if (!line) return null;
44
+ const m = line.match(/https:\/\/\S+/i);
45
+ if (!m) return null;
46
+ // Avoid trailing slash for base URLs (some consumers treat it as a path prefix).
47
+ return m[0].replace(/\/+$/, '');
48
+ }
49
+
50
+ function tailscaleStatusMatchesInternalServerUrl(status, internalServerUrl) {
51
+ const raw = (internalServerUrl ?? '').trim();
52
+ if (!raw) return true;
53
+
54
+ // Fast path.
55
+ if (status.includes(raw)) return true;
56
+
57
+ // Tailscale typically prints proxy targets like:
58
+ // |-- / proxy http://127.0.0.1:3005
59
+ let port = '';
60
+ try {
61
+ port = new URL(raw).port;
62
+ } catch {
63
+ port = '';
64
+ }
65
+ if (!port) return false;
66
+
67
+ const re = new RegExp(String.raw`\\bproxy\\s+https?:\\/\\/(?:127\\.0\\.0\\.1|localhost|0\\.0\\.0\\.0):${port}\\b`, 'i');
68
+ return re.test(status);
69
+ }
70
+
71
+ export async function tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl) {
72
+ try {
73
+ const status = await tailscaleServeStatus();
74
+ const https = extractHttpsUrl(status);
75
+ if (!https) return null;
76
+ return tailscaleStatusMatchesInternalServerUrl(status, internalServerUrl) ? https : null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function extractServeEnableUrl(text) {
83
+ const m = String(text ?? '').match(/https:\/\/login\.tailscale\.com\/f\/serve\?node=\S+/i);
84
+ return m ? m[0] : null;
85
+ }
86
+
87
+ function assertTailscaleAllowed(action) {
88
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
89
+ throw new Error(
90
+ `[local] tailscale ${action} is disabled in sandbox mode.\n` +
91
+ `Reason: Tailscale Serve is global machine state and sandbox runs must be isolated.\n` +
92
+ `If you really want this, set: HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1`
93
+ );
94
+ }
95
+ }
96
+
97
+ function parseTimeoutMs(raw, defaultMs) {
98
+ const s = (raw ?? '').trim();
99
+ if (!s) return defaultMs;
100
+ const n = Number(s);
101
+ // Allow 0 to disable timeouts for user-triggered commands.
102
+ if (!Number.isFinite(n)) return defaultMs;
103
+ return n > 0 ? n : 0;
104
+ }
105
+
106
+ function tailscaleProbeTimeoutMs() {
107
+ return parseTimeoutMs(process.env.HAPPIER_STACK_TAILSCALE_CMD_TIMEOUT_MS, 2500);
108
+ }
109
+
110
+ function tailscaleUserEnableTimeoutMs() {
111
+ return parseTimeoutMs(process.env.HAPPIER_STACK_TAILSCALE_ENABLE_TIMEOUT_MS, 30000);
112
+ }
113
+
114
+ function tailscaleAutoEnableTimeoutMs() {
115
+ return parseTimeoutMs(process.env.HAPPIER_STACK_TAILSCALE_ENABLE_TIMEOUT_MS_AUTO, tailscaleProbeTimeoutMs());
116
+ }
117
+
118
+ function tailscaleUserResetTimeoutMs() {
119
+ return parseTimeoutMs(process.env.HAPPIER_STACK_TAILSCALE_RESET_TIMEOUT_MS, 15000);
120
+ }
121
+
122
+ function tailscaleEnv() {
123
+ // LaunchAgents inherit `XPC_SERVICE_NAME`, which can confuse some CLI tools.
124
+ // In practice, we’ve seen Tailscale commands like `tailscale version` hang under
125
+ // this env. Strip it for any tailscale subprocesses.
126
+ const env = { ...process.env };
127
+ delete env.XPC_SERVICE_NAME;
128
+ return env;
129
+ }
130
+
131
+ async function isExecutable(path) {
132
+ try {
133
+ await access(path, constants.X_OK);
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ async function resolveTailscaleCmd() {
141
+ // Allow explicit override (useful for LaunchAgents where aliases don't exist).
142
+ if (process.env.HAPPIER_STACK_TAILSCALE_BIN?.trim()) {
143
+ return process.env.HAPPIER_STACK_TAILSCALE_BIN.trim();
144
+ }
145
+
146
+ // Try PATH first (without executing `tailscale`, which can hang in some environments).
147
+ try {
148
+ const found = await resolveCommandPath('tailscale', { env: tailscaleEnv(), timeoutMs: tailscaleProbeTimeoutMs() });
149
+ if (found) {
150
+ return found;
151
+ }
152
+ } catch {
153
+ // ignore and fall back
154
+ }
155
+
156
+ // Common macOS app install paths.
157
+ //
158
+ // IMPORTANT:
159
+ // Prefer the lowercase `tailscale` CLI inside the app bundle. The capitalized
160
+ // `Tailscale` binary can behave differently under LaunchAgents (XPC env),
161
+ // potentially hanging instead of printing a version and exiting.
162
+ const appCliPath = '/Applications/Tailscale.app/Contents/MacOS/tailscale';
163
+ if (await isExecutable(appCliPath)) {
164
+ return appCliPath;
165
+ }
166
+
167
+ const appPath = '/Applications/Tailscale.app/Contents/MacOS/Tailscale';
168
+ if (await isExecutable(appPath)) {
169
+ return appPath;
170
+ }
171
+
172
+ throw new Error(
173
+ `[local] tailscale CLI not found.\n` +
174
+ `- Install Tailscale, or\n` +
175
+ `- Put 'tailscale' on PATH, or\n` +
176
+ `- Set HAPPIER_STACK_TAILSCALE_BIN="${appCliPath}"`
177
+ );
178
+ }
179
+
180
+ export async function tailscaleServeHttpsUrl() {
181
+ try {
182
+ const status = await tailscaleServeStatus();
183
+ return extractHttpsUrl(status);
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ export async function tailscaleServeStatus() {
190
+ assertTailscaleAllowed('status');
191
+ const cmd = await resolveTailscaleCmd();
192
+ return await runCapture(cmd, ['serve', 'status'], { env: tailscaleEnv(), timeoutMs: tailscaleProbeTimeoutMs() });
193
+ }
194
+
195
+ export async function tailscaleServeEnable({ internalServerUrl, timeoutMs } = {}) {
196
+ assertTailscaleAllowed('enable');
197
+ const cmd = await resolveTailscaleCmd();
198
+ const { upstream, servePath } = getServeConfig(internalServerUrl);
199
+ const args = ['serve', '--bg'];
200
+ if (servePath && servePath !== '/' && servePath !== '') {
201
+ args.push(`--set-path=${servePath}`);
202
+ }
203
+ args.push(upstream);
204
+ const env = tailscaleEnv();
205
+ const timeout = Number.isFinite(timeoutMs) ? (timeoutMs > 0 ? timeoutMs : 0) : tailscaleUserEnableTimeoutMs();
206
+
207
+ try {
208
+ // `tailscale serve --bg` can hang in some environments (and should never block stack startup).
209
+ // Use a short, best-effort timeout; if it prints an enable URL, open it and return a helpful result.
210
+ await runCapture(cmd, args, { env, timeoutMs: timeout });
211
+ } catch (e) {
212
+ const out = e && typeof e === 'object' && 'out' in e ? e.out : '';
213
+ const err = e && typeof e === 'object' && 'err' in e ? e.err : '';
214
+ const msg = e instanceof Error ? e.message : String(e);
215
+ const combined = `${out ?? ''}\n${err ?? ''}\n${msg ?? ''}`.trim();
216
+ const enableUrl = extractServeEnableUrl(combined);
217
+ if (enableUrl) {
218
+ // User-initiated action (CLI / menubar): open the enable page.
219
+ try {
220
+ await run('open', [enableUrl]);
221
+ } catch {
222
+ // ignore (headless / restricted environment)
223
+ }
224
+ return { status: combined || String(e), httpsUrl: null, enableUrl };
225
+ }
226
+ throw e;
227
+ }
228
+
229
+ const status = await runCapture(cmd, ['serve', 'status'], { env, timeoutMs: tailscaleProbeTimeoutMs() }).catch(() => '');
230
+ return { status, httpsUrl: status ? extractHttpsUrl(status) : null };
231
+ }
232
+
233
+ export async function tailscaleServeReset({ timeoutMs } = {}) {
234
+ assertTailscaleAllowed('reset');
235
+ const cmd = await resolveTailscaleCmd();
236
+ const timeout = Number.isFinite(timeoutMs) ? (timeoutMs > 0 ? timeoutMs : 0) : tailscaleUserResetTimeoutMs();
237
+ await run(cmd, ['serve', 'reset'], { env: tailscaleEnv(), timeoutMs: timeout });
238
+ }
239
+
240
+ export async function maybeEnableTailscaleServe({ internalServerUrl }) {
241
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
242
+ return null;
243
+ }
244
+ const enabled = (process.env.HAPPIER_STACK_TAILSCALE_SERVE ?? '0') === '1';
245
+ if (!enabled) {
246
+ return null;
247
+ }
248
+ try {
249
+ // This is called from automation; it must not hang for long.
250
+ return await tailscaleServeEnable({ internalServerUrl, timeoutMs: tailscaleAutoEnableTimeoutMs() });
251
+ } catch (e) {
252
+ throw new Error(`[local] failed to enable tailscale serve (is Tailscale running/authenticated?): ${e instanceof Error ? e.message : String(e)}`);
253
+ }
254
+ }
255
+
256
+ export async function maybeResetTailscaleServe() {
257
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
258
+ return;
259
+ }
260
+ const enabled = (process.env.HAPPIER_STACK_TAILSCALE_SERVE ?? '0') === '1';
261
+ const resetOnExit = (process.env.HAPPIER_STACK_TAILSCALE_RESET_ON_EXIT ?? '0') === '1';
262
+ if (!enabled || !resetOnExit) {
263
+ return;
264
+ }
265
+ try {
266
+ // Shutdown path: never block for long.
267
+ await tailscaleServeReset({ timeoutMs: tailscaleProbeTimeoutMs() });
268
+ } catch {
269
+ // ignore
270
+ }
271
+ }
272
+
273
+ async function sleep(ms) {
274
+ await new Promise((r) => setTimeout(r, ms));
275
+ }
276
+
277
+ /**
278
+ * Resolve the best public server URL to present to users / generate links.
279
+ *
280
+ * Priority:
281
+ * 1) explicit HAPPIER_STACK_SERVER_URL override (if non-default)
282
+ * 2) if enabled, prefer existing https://*.ts.net from tailscale serve status
283
+ * 3) fallback to defaultPublicUrl
284
+ *
285
+ * If HAPPIER_STACK_TAILSCALE_SERVE=1, this can also try to enable serve and wait briefly for Tailscale to come up.
286
+ */
287
+ export async function resolvePublicServerUrl({
288
+ internalServerUrl,
289
+ defaultPublicUrl,
290
+ envPublicUrl,
291
+ allowEnable = true,
292
+ stackName = 'main',
293
+ }) {
294
+ const preferTailscalePublicUrl = (process.env.HAPPIER_STACK_TAILSCALE_PREFER_PUBLIC_URL ?? '1') !== '0';
295
+ const userExplicitlySetPublicUrl =
296
+ !!envPublicUrl && envPublicUrl !== defaultPublicUrl && envPublicUrl !== internalServerUrl;
297
+
298
+ if (userExplicitlySetPublicUrl || !preferTailscalePublicUrl) {
299
+ return { publicServerUrl: envPublicUrl || defaultPublicUrl, source: 'env' };
300
+ }
301
+
302
+ // Non-main stacks:
303
+ // - Never auto-enable (global machine state) by default.
304
+ // - If the caller explicitly allows it AND Tailscale Serve is already configured for this stack's
305
+ // internal URL, prefer the HTTPS URL (safe: status must match the internal URL).
306
+ if (stackName && stackName !== 'main') {
307
+ if (allowEnable) {
308
+ const existing = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
309
+ if (existing) {
310
+ return { publicServerUrl: existing, source: 'tailscale-status' };
311
+ }
312
+ }
313
+ return { publicServerUrl: envPublicUrl || defaultPublicUrl, source: envPublicUrl ? 'env' : 'default' };
314
+ }
315
+
316
+ // If serve is already configured, use its HTTPS URL if present.
317
+ const existing = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
318
+ if (existing) {
319
+ return { publicServerUrl: existing, source: 'tailscale-status' };
320
+ }
321
+
322
+ const enableServe = (process.env.HAPPIER_STACK_TAILSCALE_SERVE ?? '0') === '1';
323
+ if (!enableServe || !allowEnable) {
324
+ return { publicServerUrl: envPublicUrl || defaultPublicUrl, source: 'default' };
325
+ }
326
+
327
+ // Try enabling serve (best-effort); then wait a bit for Tailscale to be ready/configured.
328
+ try {
329
+ const res = await tailscaleServeEnable({ internalServerUrl, timeoutMs: tailscaleAutoEnableTimeoutMs() });
330
+ if (res?.httpsUrl) {
331
+ return { publicServerUrl: res.httpsUrl, source: 'tailscale-enable' };
332
+ }
333
+ } catch {
334
+ // ignore and fall back to waiting/polling
335
+ }
336
+
337
+ const waitMs = process.env.HAPPIER_STACK_TAILSCALE_WAIT_MS?.trim()
338
+ ? Number(process.env.HAPPIER_STACK_TAILSCALE_WAIT_MS.trim())
339
+ : 15000;
340
+ const deadline = Date.now() + (Number.isFinite(waitMs) ? waitMs : 15000);
341
+ while (Date.now() < deadline) {
342
+ const url = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
343
+ if (url) {
344
+ return { publicServerUrl: url, source: 'tailscale-wait' };
345
+ }
346
+ await sleep(500);
347
+ }
348
+
349
+ return { publicServerUrl: envPublicUrl || defaultPublicUrl, source: 'default' };
350
+ }
351
+
352
+ async function main() {
353
+ const argv = process.argv.slice(2);
354
+ const helpSepIdx = argv.indexOf('--');
355
+ const helpScopeArgv = helpSepIdx === -1 ? argv : argv.slice(0, helpSepIdx);
356
+ const { flags, kv } = parseArgs(helpScopeArgv);
357
+ const positionals = helpScopeArgv.filter((a) => a && a !== '--' && !a.startsWith('-'));
358
+ const cmd = positionals[0] ?? 'help';
359
+ const json = wantsJson(helpScopeArgv, { flags });
360
+
361
+ const wantsHelpFlag = wantsHelp(helpScopeArgv, { flags });
362
+ const usageByCmd = new Map([
363
+ ['status', 'hstack tailscale status [--json]'],
364
+ ['enable', 'hstack tailscale enable [--json]'],
365
+ ['disable', 'hstack tailscale disable [--json]'],
366
+ ['url', 'hstack tailscale url [--json]'],
367
+ ['reset', 'hstack tailscale reset [--json]'],
368
+ ]);
369
+
370
+ if (wantsHelpFlag && cmd !== 'help') {
371
+ const usage = usageByCmd.get(cmd);
372
+ if (usage) {
373
+ printResult({
374
+ json,
375
+ data: { ok: true, cmd, usage },
376
+ text: [`[tailscale ${cmd}] usage:`, ` ${usage}`, '', 'see also:', ' hstack tailscale --help'].join('\n'),
377
+ });
378
+ return;
379
+ }
380
+ }
381
+
382
+ if (wantsHelpFlag || cmd === 'help') {
383
+ printResult({
384
+ json,
385
+ data: { commands: ['status', 'enable', 'disable', 'reset', 'url'] },
386
+ text: [
387
+ '',
388
+ banner('tailscale', { subtitle: 'Tailscale Serve (HTTPS secure context)' }),
389
+ '',
390
+ sectionTitle('Usage'),
391
+ bullets([
392
+ `${dim('status:')} ${cmdFmt('hstack tailscale status')} ${dim('[--json]')}`,
393
+ `${dim('enable:')} ${cmdFmt('hstack tailscale enable')} ${dim('[--json]')}`,
394
+ `${dim('disable:')} ${cmdFmt('hstack tailscale disable')} ${dim('[--json]')}`,
395
+ `${dim('url:')} ${cmdFmt('hstack tailscale url')} ${dim('[--json]')}`,
396
+ ]),
397
+ '',
398
+ sectionTitle('Notes'),
399
+ bullets([
400
+ `${dim('what it does:')} configures \`tailscale serve\` to proxy your local server (${dim('usually')} ${cyan('http://127.0.0.1:3005')}) over HTTPS`,
401
+ `${dim('env:')} set ${cyan('HAPPIER_STACK_TAILSCALE_SERVE=1')} to allow stack runs to auto-enable serve (best-effort)`,
402
+ `${dim('sandbox:')} enable/disable are blocked unless ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`,
403
+ ]),
404
+ '',
405
+ sectionTitle('Legacy / advanced'),
406
+ bullets([
407
+ `${dim('low-level:')} ${dim('node scripts/tailscale.mjs enable --upstream=<url> --path=/ [--json]')}`,
408
+ ]),
409
+ ].join('\n'),
410
+ });
411
+ return;
412
+ }
413
+
414
+ const internalServerUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
415
+ if (flags.has('--upstream') || kv.get('--upstream')) {
416
+ process.env.HAPPIER_STACK_TAILSCALE_UPSTREAM = kv.get('--upstream') ?? internalServerUrl;
417
+ }
418
+ if (flags.has('--path') || kv.get('--path')) {
419
+ process.env.HAPPIER_STACK_TAILSCALE_SERVE_PATH = kv.get('--path') ?? '/';
420
+ }
421
+
422
+ switch (cmd) {
423
+ case 'status': {
424
+ const status = await tailscaleServeStatus();
425
+ if (json) {
426
+ printResult({ json, data: { status, httpsUrl: extractHttpsUrl(status) } });
427
+ } else {
428
+ process.stdout.write(status);
429
+ }
430
+ return;
431
+ }
432
+ case 'url': {
433
+ const status = await tailscaleServeStatus();
434
+ const url = extractHttpsUrl(status);
435
+ if (!url) {
436
+ throw new Error('[tailscale] no https:// URL found in `tailscale serve status` output');
437
+ }
438
+ printResult({ json, data: { url }, text: url });
439
+ return;
440
+ }
441
+ case 'enable': {
442
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
443
+ throw new Error(
444
+ '[tailscale] enable is disabled in sandbox mode.\n' +
445
+ 'Reason: Tailscale Serve is global machine state.\n' +
446
+ 'If you really want this, set: HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1'
447
+ );
448
+ }
449
+ const res = await tailscaleServeEnable({ internalServerUrl });
450
+ if (res?.enableUrl && !res?.httpsUrl) {
451
+ printResult({
452
+ json,
453
+ data: { ok: true, httpsUrl: null, enableUrl: res.enableUrl },
454
+ text:
455
+ `${green('✓')} tailscale serve needs one-time approval in your tailnet.\n` +
456
+ `${dim('Open:')} ${cyan(res.enableUrl)}`,
457
+ });
458
+ return;
459
+ }
460
+ printResult({
461
+ json,
462
+ data: { ok: true, httpsUrl: res.httpsUrl ?? null },
463
+ text: res.httpsUrl ? `${green('✓')} tailscale serve enabled: ${cyan(res.httpsUrl)}` : `${green('✓')} tailscale serve enabled`,
464
+ });
465
+ return;
466
+ }
467
+ case 'disable':
468
+ case 'reset': {
469
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
470
+ throw new Error(
471
+ '[tailscale] disable/reset is disabled in sandbox mode.\n' +
472
+ 'Reason: Tailscale Serve is global machine state.\n' +
473
+ 'If you really want this, set: HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1'
474
+ );
475
+ }
476
+ await tailscaleServeReset();
477
+ printResult({ json, data: { ok: true }, text: `${green('✓')} tailscale serve reset` });
478
+ return;
479
+ }
480
+ default:
481
+ throw new Error(`[tailscale] unknown command: ${cmd}`);
482
+ }
483
+ }
484
+
485
+ if (import.meta.url === `file://${process.argv[1]}`) {
486
+ main().catch((err) => {
487
+ console.error('[tailscale] failed:', err);
488
+ process.exit(1);
489
+ });
490
+ }
@@ -0,0 +1,36 @@
1
+ import { join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { collectTestFiles } from './utils/test/collect_test_files.mjs';
4
+
5
+ async function main() {
6
+ const packageRoot = fileURLToPath(new URL('..', import.meta.url));
7
+ const scriptsDir = join(packageRoot, 'scripts');
8
+ const testsDir = join(packageRoot, 'tests');
9
+
10
+ const testFiles = [];
11
+ testFiles.push(...(await collectTestFiles({
12
+ dir: scriptsDir,
13
+ includeSuffixes: ['.test.mjs'],
14
+ excludeSuffixes: ['.integration.test.mjs', '.real.integration.test.mjs'],
15
+ })));
16
+ testFiles.push(...(await collectTestFiles({
17
+ dir: testsDir,
18
+ includeSuffixes: ['.test.mjs'],
19
+ excludeSuffixes: ['.integration.test.mjs', '.real.integration.test.mjs'],
20
+ })));
21
+
22
+ if (testFiles.length === 0) {
23
+ process.stderr.write(`[stack:test] no .test.mjs files found under ${scriptsDir} or ${testsDir}\n`);
24
+ process.exit(1);
25
+ }
26
+
27
+ // Node 20 does not expand globs for `--test`, so we enumerate files.
28
+ const { spawnSync } = await import('node:child_process');
29
+ const res = spawnSync(process.execPath, ['--test', ...testFiles], { stdio: 'inherit' });
30
+ process.exit(res.status ?? 1);
31
+ }
32
+
33
+ main().catch((e) => {
34
+ process.stderr.write(`[stack:test] ${String(e?.stack ?? e)}\n`);
35
+ process.exit(1);
36
+ });