@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,266 @@
1
+ import { prompt, promptSelect, promptWorktreeSource } from '../cli/wizard.mjs';
2
+ import { gitCapture, gitOk } from '../git/git.mjs';
3
+ import { parseGithubOwnerRepo } from '../git/worktrees.mjs';
4
+ import { getRepoDir } from '../paths/paths.mjs';
5
+ import { cyan, dim, green } from '../ui/ansi.mjs';
6
+ import { sectionTitle, warn } from '../ui/layout.mjs';
7
+ import { normalizeStackNameOrNull } from './names.mjs';
8
+
9
+ function wantsNo(raw) {
10
+ const v = String(raw ?? '').trim().toLowerCase();
11
+ return v === 'n' || v === 'no' || v === '0' || v === 'false';
12
+ }
13
+
14
+ async function promptStackName({ rl } = {}) {
15
+ while (true) {
16
+ // eslint-disable-next-line no-await-in-loop
17
+ const raw = (await rl.question(`${dim('Stack name')}: `)).trim();
18
+ const normalized = normalizeStackNameOrNull(raw);
19
+ if (!normalized) {
20
+ // eslint-disable-next-line no-console
21
+ console.log(warn('Invalid stack name. Use lowercase letters/numbers/hyphens (example: my-stack).'));
22
+ continue;
23
+ }
24
+ if (normalized === 'main') {
25
+ // eslint-disable-next-line no-console
26
+ console.log(warn('Stack name "main" is reserved. Use the default stack without creating it.'));
27
+ continue;
28
+ }
29
+
30
+ const trimmedLower = raw.trim().toLowerCase();
31
+ if (normalized !== trimmedLower) {
32
+ // eslint-disable-next-line no-console
33
+ console.log(warn(`Normalized stack name to ${cyan(normalized)}.`));
34
+ }
35
+ return normalized;
36
+ }
37
+ }
38
+
39
+ function parsePortOrNull(raw) {
40
+ const trimmed = String(raw ?? '').trim();
41
+ if (!trimmed) return { ok: true, kind: 'empty', port: null };
42
+ const token = trimmed.toLowerCase();
43
+ if (token === 'ephemeral') return { ok: true, kind: 'ephemeral', port: null };
44
+ if (!/^\d+$/.test(token)) return { ok: false, kind: 'invalid', port: null };
45
+ const n = Number(token);
46
+ if (!Number.isFinite(n) || n <= 0 || n > 65535) return { ok: false, kind: 'invalid', port: null };
47
+ return { ok: true, kind: 'port', port: n };
48
+ }
49
+
50
+ async function promptPort({ rl, promptFn, label, defaultValue = '' } = {}) {
51
+ while (true) {
52
+ // eslint-disable-next-line no-await-in-loop
53
+ const raw = await promptFn(rl, label, { defaultValue });
54
+ const parsed = parsePortOrNull(raw);
55
+ if (parsed.ok) return { raw, parsed };
56
+ // eslint-disable-next-line no-console
57
+ console.log(warn(`Invalid port: "${String(raw ?? '').trim()}". Enter a number (1-65535), or leave empty.`));
58
+ }
59
+ }
60
+
61
+ function normalizeRemoteNameOrNull(raw) {
62
+ const v = String(raw ?? '').trim();
63
+ if (!v) return null;
64
+ // Remote names should be safe identifiers and should not be interpreted as flags by `git remote ...`.
65
+ // Allow namespaced remotes (e.g. "team/upstream") while avoiding ambiguous path-like forms.
66
+ if (!/^(?!.*\/\/)(?!.*\/$)[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(v)) return null;
67
+ return v;
68
+ }
69
+
70
+ async function promptRemoteName({ rl, promptFn, label, defaultValue = '' } = {}) {
71
+ while (true) {
72
+ // eslint-disable-next-line no-await-in-loop
73
+ const raw = await promptFn(rl, label, { defaultValue });
74
+ const trimmed = String(raw ?? '').trim();
75
+ const fallback = String(defaultValue ?? '').trim();
76
+ const normalized = normalizeRemoteNameOrNull(trimmed === '' && fallback !== '' ? fallback : trimmed);
77
+ if (normalized) return normalized;
78
+ // eslint-disable-next-line no-console
79
+ console.log(
80
+ warn(
81
+ `Invalid git remote name: "${String(raw ?? '').trim()}". Use letters/numbers/dot/underscore/hyphen/slash (example: upstream or team/upstream).`
82
+ )
83
+ );
84
+ }
85
+ }
86
+
87
+ async function describeGitRemote({ repoDir, remote }) {
88
+ const r = String(remote ?? '').trim();
89
+ if (!repoDir || !r) return '';
90
+ try {
91
+ const url = (await gitCapture({ cwd: repoDir, args: ['remote', 'get-url', r] })).trim();
92
+ if (!url) return '';
93
+ const parsed = parseGithubOwnerRepo(url);
94
+ return parsed ? `${parsed.owner}/${parsed.repo}` : url;
95
+ } catch {
96
+ return '';
97
+ }
98
+ }
99
+
100
+ async function resolveDefaultCreateRemote({ repoDir }) {
101
+ // Prefer upstream when present (clean PR history), else fall back to origin.
102
+ if (await gitOk({ cwd: repoDir, args: ['remote', 'get-url', 'upstream'] })) return 'upstream';
103
+ if (await gitOk({ cwd: repoDir, args: ['remote', 'get-url', 'origin'] })) return 'origin';
104
+ return 'upstream';
105
+ }
106
+
107
+ export async function interactiveNew({ rootDir, rl, defaults, deps = {} }) {
108
+ const promptFn = deps.prompt ?? prompt;
109
+ const promptSelectFn = deps.promptSelect ?? promptSelect;
110
+ const promptWorktreeSourceFn = deps.promptWorktreeSource ?? promptWorktreeSource;
111
+
112
+ const out = { ...defaults };
113
+
114
+ if (!out.stackName) {
115
+ // eslint-disable-next-line no-console
116
+ console.log('');
117
+ // eslint-disable-next-line no-console
118
+ console.log(sectionTitle('Create a stack'));
119
+ // eslint-disable-next-line no-console
120
+ console.log(dim('Stacks are isolated local environments (ports + dirs + DB + CLI home).'));
121
+ out.stackName = await promptStackName({ rl });
122
+ }
123
+ if (!out.stackName) {
124
+ throw new Error('[stack] stack name is required');
125
+ }
126
+ {
127
+ const normalized = normalizeStackNameOrNull(out.stackName);
128
+ if (!normalized) {
129
+ throw new Error('[stack] stack name is invalid');
130
+ }
131
+ if (normalized === 'main') {
132
+ throw new Error('[stack] stack name "main" is reserved');
133
+ }
134
+ out.stackName = normalized;
135
+ }
136
+
137
+ if (!out.serverComponent) {
138
+ out.serverComponent = await promptSelectFn(rl, {
139
+ title: `${sectionTitle('Server flavor')}\n${dim('Pick the backend this stack should run. You can switch later with `stack srv`.')}`,
140
+ options: [
141
+ { label: `happier-server-light (${green('recommended')}) — simplest local install (PG_Light via embedded PGlite)`, value: 'happier-server-light' },
142
+ { label: `happier-server — full server (Postgres/Redis/Minio via Docker)`, value: 'happier-server' },
143
+ ],
144
+ defaultIndex: 0,
145
+ });
146
+ }
147
+
148
+ if (!out.port) {
149
+ // eslint-disable-next-line no-console
150
+ console.log('');
151
+ // eslint-disable-next-line no-console
152
+ console.log(sectionTitle('Ports'));
153
+ // eslint-disable-next-line no-console
154
+ console.log(dim('Tip: leaving this empty uses an ephemeral port (recommended for non-main stacks).'));
155
+ // Accept "ephemeral" explicitly, and reprompt on invalid input.
156
+ const { parsed } = await promptPort({
157
+ rl,
158
+ promptFn,
159
+ label: `${dim('Port')} (empty = ephemeral; type 'ephemeral' to unpin): `,
160
+ defaultValue: '',
161
+ });
162
+ out.port = parsed.port;
163
+ }
164
+
165
+ if (!out.createRemote) {
166
+ // eslint-disable-next-line no-console
167
+ console.log('');
168
+ // eslint-disable-next-line no-console
169
+ console.log(sectionTitle('Worktrees'));
170
+ const mainDir = getRepoDir(rootDir, { ...process.env, HAPPIER_STACK_REPO_DIR: '' });
171
+ const upstreamRepo = await describeGitRemote({ repoDir: mainDir, remote: 'upstream' });
172
+ const originRepo = await describeGitRemote({ repoDir: mainDir, remote: 'origin' });
173
+ const defaultRemote = await resolveDefaultCreateRemote({ repoDir: mainDir });
174
+
175
+ // eslint-disable-next-line no-console
176
+ console.log(
177
+ dim(`New worktrees are typically based on ${cyan('upstream')}${upstreamRepo ? ` (${upstreamRepo})` : ''} (clean PR history).`)
178
+ );
179
+ if (upstreamRepo || originRepo) {
180
+ // eslint-disable-next-line no-console
181
+ console.log(dim(`Remotes: ${upstreamRepo ? `upstream=${upstreamRepo}` : 'upstream=(missing)'}, ${originRepo ? `origin=${originRepo}` : 'origin=(missing)'}`));
182
+ }
183
+
184
+ out.createRemote = await promptRemoteName({
185
+ rl,
186
+ promptFn,
187
+ label: `${dim('Git remote for new worktrees')} (default: ${defaultRemote}): `,
188
+ defaultValue: defaultRemote,
189
+ });
190
+ }
191
+
192
+ if (out.repo == null) {
193
+ // NOTE: promptWorktreeSource is still component-named internally; for hstack, this is the monorepo checkout.
194
+ out.repo = await promptWorktreeSourceFn({
195
+ rl,
196
+ rootDir,
197
+ component: 'happier-ui',
198
+ stackName: out.stackName,
199
+ createRemote: out.createRemote,
200
+ });
201
+ }
202
+
203
+ return out;
204
+ }
205
+
206
+ export async function interactiveEdit({ rootDir, rl, stackName, existingEnv, defaults, deps = {} }) {
207
+ const promptFn = deps.prompt ?? prompt;
208
+ const promptSelectFn = deps.promptSelect ?? promptSelect;
209
+ const promptWorktreeSourceFn = deps.promptWorktreeSource ?? promptWorktreeSource;
210
+
211
+ const out = { ...defaults, stackName };
212
+
213
+ const currentServer = existingEnv.HAPPIER_STACK_SERVER_COMPONENT ?? '';
214
+ out.serverComponent = await promptSelectFn(rl, {
215
+ title: `${sectionTitle('Server flavor')}\n${dim('Pick the backend this stack should run. You can switch again later.')}`,
216
+ options: [
217
+ { label: `happier-server-light (${green('recommended')}) — simplest local install (PG_Light via embedded PGlite)`, value: 'happier-server-light' },
218
+ { label: `happier-server — full server (Postgres/Redis/Minio via Docker)`, value: 'happier-server' },
219
+ ],
220
+ defaultIndex: (currentServer || 'happier-server-light') === 'happier-server' ? 1 : 0,
221
+ });
222
+
223
+ const currentPort = existingEnv.HAPPIER_STACK_SERVER_PORT ?? '';
224
+ // eslint-disable-next-line no-console
225
+ console.log('');
226
+ // eslint-disable-next-line no-console
227
+ console.log(sectionTitle('Ports'));
228
+ const { parsed } = await promptPort({
229
+ rl,
230
+ promptFn,
231
+ label: `${dim(`Port`)} (empty = keep ${currentPort || 'ephemeral'}; type 'ephemeral' to unpin): `,
232
+ defaultValue: '',
233
+ });
234
+ const existingPort = currentPort ? Number(currentPort) : null;
235
+ out.port = parsed.kind === 'empty' ? (Number.isFinite(existingPort) ? existingPort : null) : parsed.port;
236
+
237
+ const currentRemote = existingEnv.HAPPIER_STACK_STACK_REMOTE ?? '';
238
+ // eslint-disable-next-line no-console
239
+ console.log('');
240
+ // eslint-disable-next-line no-console
241
+ console.log(sectionTitle('Worktrees'));
242
+ const mainDir = getRepoDir(rootDir, { ...process.env, HAPPIER_STACK_REPO_DIR: '' });
243
+ const upstreamRepo = await describeGitRemote({ repoDir: mainDir, remote: 'upstream' });
244
+ const originRepo = await describeGitRemote({ repoDir: mainDir, remote: 'origin' });
245
+ if (upstreamRepo || originRepo) {
246
+ // eslint-disable-next-line no-console
247
+ console.log(dim(`Remotes: ${upstreamRepo ? `upstream=${upstreamRepo}` : 'upstream=(missing)'}, ${originRepo ? `origin=${originRepo}` : 'origin=(missing)'}`));
248
+ }
249
+ const defaultRemote = (currentRemote || (await resolveDefaultCreateRemote({ repoDir: mainDir })) || 'upstream').trim();
250
+ out.createRemote = await promptRemoteName({
251
+ rl,
252
+ promptFn,
253
+ label: `${dim('Git remote for new worktrees')} (default: ${defaultRemote}): `,
254
+ defaultValue: defaultRemote,
255
+ });
256
+
257
+ out.repo = await promptWorktreeSourceFn({
258
+ rl,
259
+ rootDir,
260
+ component: 'happier-ui',
261
+ stackName,
262
+ createRemote: out.createRemote,
263
+ });
264
+
265
+ return out;
266
+ }
@@ -0,0 +1,93 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { interactiveEdit, interactiveNew } from './interactive_stack_config.mjs';
5
+ import { createInteractiveStackConfigDeps, mkRl } from './interactive_stack_config_testkit.mjs';
6
+
7
+ test('interactiveNew reprompts when port input is invalid', async (t) => {
8
+ t.mock.method(console, 'log', () => {});
9
+ const rootDir = '/tmp/hstack-root';
10
+ const rl = mkRl(['not-a-number', '4242']);
11
+
12
+ const out = await interactiveNew({
13
+ rootDir,
14
+ rl,
15
+ defaults: {
16
+ stackName: 'exp-port-validate',
17
+ port: null,
18
+ serverComponent: 'happier-server-light',
19
+ createRemote: 'upstream',
20
+ repo: 'default',
21
+ },
22
+ deps: createInteractiveStackConfigDeps(),
23
+ });
24
+
25
+ assert.equal(out.port, 4242);
26
+ });
27
+
28
+ test('interactiveEdit reprompts when port input is invalid and allows empty to keep current port', async (t) => {
29
+ t.mock.method(console, 'log', () => {});
30
+ const rootDir = '/tmp/hstack-root';
31
+ const rl = mkRl([]);
32
+
33
+ const prompted = [];
34
+ const out = await interactiveEdit({
35
+ rootDir,
36
+ rl,
37
+ stackName: 'exp-edit-port-validate',
38
+ existingEnv: {
39
+ HAPPIER_STACK_SERVER_COMPONENT: 'happier-server-light',
40
+ HAPPIER_STACK_SERVER_PORT: '4101',
41
+ HAPPIER_STACK_STACK_REMOTE: 'upstream',
42
+ },
43
+ defaults: {},
44
+ deps: createInteractiveStackConfigDeps({
45
+ prompt: async (_rl, question, { defaultValue } = {}) => {
46
+ prompted.push(String(question));
47
+ if (String(question).includes('Port')) {
48
+ const idx = prompted.filter((q) => q.includes('Port')).length;
49
+ return idx === 1 ? 'abc' : '';
50
+ }
51
+ return defaultValue ?? '';
52
+ },
53
+ }),
54
+ });
55
+
56
+ assert.equal(out.port, 4101);
57
+ });
58
+
59
+ test('interactiveNew accepts explicit "ephemeral" token for unpinned port', async (t) => {
60
+ t.mock.method(console, 'log', () => {});
61
+ const out = await interactiveNew({
62
+ rootDir: '/tmp/hstack-root',
63
+ rl: mkRl(['ephemeral']),
64
+ defaults: {
65
+ stackName: 'exp-port-ephemeral',
66
+ port: null,
67
+ serverComponent: 'happier-server-light',
68
+ createRemote: 'upstream',
69
+ repo: 'default',
70
+ },
71
+ deps: createInteractiveStackConfigDeps(),
72
+ });
73
+ assert.equal(out.port, null);
74
+ });
75
+
76
+ test('interactiveEdit treats malformed existing port as ephemeral when input is empty', async (t) => {
77
+ t.mock.method(console, 'log', () => {});
78
+ const out = await interactiveEdit({
79
+ rootDir: '/tmp/hstack-root',
80
+ rl: mkRl([]),
81
+ stackName: 'exp-edit-port-malformed',
82
+ existingEnv: {
83
+ HAPPIER_STACK_SERVER_COMPONENT: 'happier-server-light',
84
+ HAPPIER_STACK_SERVER_PORT: 'not-a-port',
85
+ HAPPIER_STACK_STACK_REMOTE: 'upstream',
86
+ },
87
+ defaults: {},
88
+ deps: createInteractiveStackConfigDeps({
89
+ prompt: async (_rl, _question, { defaultValue } = {}) => defaultValue ?? '',
90
+ }),
91
+ });
92
+ assert.equal(out.port, null);
93
+ });
@@ -0,0 +1,122 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { interactiveEdit, interactiveNew } from './interactive_stack_config.mjs';
5
+ import { createInteractiveStackConfigDeps, mkRl } from './interactive_stack_config_testkit.mjs';
6
+
7
+ test('interactiveNew reprompts when createRemote input is invalid', async (t) => {
8
+ t.mock.method(console, 'log', () => {});
9
+ const asked = [];
10
+ let remoteAnswers = 0;
11
+ const out = await interactiveNew({
12
+ rootDir: '/tmp/hstack-root',
13
+ rl: mkRl([]),
14
+ defaults: {
15
+ stackName: 'exp-remote-validate',
16
+ port: 4101,
17
+ serverComponent: 'happier-server-light',
18
+ createRemote: '',
19
+ repo: 'default',
20
+ },
21
+ deps: createInteractiveStackConfigDeps({
22
+ prompt: async (_rl, question, { defaultValue } = {}) => {
23
+ asked.push(String(question));
24
+ if (String(question).includes('Git remote for new worktrees')) {
25
+ remoteAnswers += 1;
26
+ return remoteAnswers === 1 ? 'bad remote' : 'upstream';
27
+ }
28
+ return defaultValue ?? '';
29
+ },
30
+ }),
31
+ });
32
+
33
+ assert.ok(asked.some((q) => q.includes('Git remote for new worktrees')));
34
+ assert.equal(out.createRemote, 'upstream');
35
+ });
36
+
37
+ test('interactiveEdit reprompts when createRemote input is invalid', async (t) => {
38
+ t.mock.method(console, 'log', () => {});
39
+ let remoteAnswers = 0;
40
+ const out = await interactiveEdit({
41
+ rootDir: '/tmp/hstack-root',
42
+ rl: mkRl([]),
43
+ stackName: 'exp-edit-remote-validate',
44
+ existingEnv: {
45
+ HAPPIER_STACK_SERVER_COMPONENT: 'happier-server-light',
46
+ HAPPIER_STACK_SERVER_PORT: '4101',
47
+ HAPPIER_STACK_STACK_REMOTE: 'origin',
48
+ },
49
+ defaults: {},
50
+ deps: createInteractiveStackConfigDeps({
51
+ prompt: async (_rl, question, { defaultValue } = {}) => {
52
+ if (String(question).includes('Git remote for new worktrees')) {
53
+ remoteAnswers += 1;
54
+ return remoteAnswers === 1 ? 'bad remote' : 'upstream';
55
+ }
56
+ if (String(question).includes('Port')) {
57
+ return '';
58
+ }
59
+ return defaultValue ?? '';
60
+ },
61
+ }),
62
+ });
63
+
64
+ assert.equal(out.createRemote, 'upstream');
65
+ });
66
+
67
+ test('interactiveEdit keeps current remote when prompt answer is empty', async (t) => {
68
+ t.mock.method(console, 'log', () => {});
69
+ const out = await interactiveEdit({
70
+ rootDir: '/tmp/hstack-root',
71
+ rl: mkRl([]),
72
+ stackName: 'exp-edit-remote-default',
73
+ existingEnv: {
74
+ HAPPIER_STACK_SERVER_COMPONENT: 'happier-server-light',
75
+ HAPPIER_STACK_SERVER_PORT: '4101',
76
+ HAPPIER_STACK_STACK_REMOTE: 'origin',
77
+ },
78
+ defaults: {},
79
+ deps: createInteractiveStackConfigDeps({
80
+ prompt: async (_rl, question, { defaultValue } = {}) => {
81
+ if (String(question).includes('Git remote for new worktrees')) {
82
+ return '';
83
+ }
84
+ if (String(question).includes('Port')) {
85
+ return '';
86
+ }
87
+ return defaultValue ?? '';
88
+ },
89
+ }),
90
+ });
91
+
92
+ assert.equal(out.createRemote, 'origin');
93
+ });
94
+
95
+ test('interactiveNew accepts slash in createRemote input', async (t) => {
96
+ t.mock.method(console, 'log', () => {});
97
+ let remoteAnswers = 0;
98
+ const out = await interactiveNew({
99
+ rootDir: '/tmp/hstack-root',
100
+ rl: mkRl([]),
101
+ defaults: {
102
+ stackName: 'exp-remote-slash',
103
+ port: 4101,
104
+ serverComponent: 'happier-server-light',
105
+ createRemote: '',
106
+ repo: 'default',
107
+ },
108
+ deps: createInteractiveStackConfigDeps({
109
+ prompt: async (_rl, question, { defaultValue } = {}) => {
110
+ if (String(question).includes('Git remote for new worktrees')) {
111
+ remoteAnswers += 1;
112
+ if (remoteAnswers > 1) throw new Error('unexpected remote reprompt');
113
+ return 'team/upstream';
114
+ }
115
+ return defaultValue ?? '';
116
+ },
117
+ }),
118
+ });
119
+
120
+ assert.equal(remoteAnswers, 1);
121
+ assert.equal(out.createRemote, 'team/upstream');
122
+ });
@@ -0,0 +1,76 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { interactiveNew } from './interactive_stack_config.mjs';
5
+ import { createInteractiveStackConfigDeps, mkRl } from './interactive_stack_config_testkit.mjs';
6
+
7
+ test('interactiveNew normalizes stackName to a DNS-safe label', async (t) => {
8
+ t.mock.method(console, 'log', () => {});
9
+ const out = await interactiveNew({
10
+ rootDir: '/tmp/hstack-root',
11
+ rl: mkRl(['My Stack']),
12
+ defaults: {
13
+ stackName: '',
14
+ port: 4101,
15
+ serverComponent: 'happier-server-light',
16
+ createRemote: 'upstream',
17
+ repo: 'default',
18
+ },
19
+ deps: createInteractiveStackConfigDeps(),
20
+ });
21
+ assert.equal(out.stackName, 'my-stack');
22
+ });
23
+
24
+ test('interactiveNew reprompts when stackName is reserved or sanitizes to empty', async (t) => {
25
+ t.mock.method(console, 'log', () => {});
26
+ const out = await interactiveNew({
27
+ rootDir: '/tmp/hstack-root',
28
+ rl: mkRl(['main', '----', 'ok-stack']),
29
+ defaults: {
30
+ stackName: '',
31
+ port: 4101,
32
+ serverComponent: 'happier-server-light',
33
+ createRemote: 'upstream',
34
+ repo: 'default',
35
+ },
36
+ deps: createInteractiveStackConfigDeps(),
37
+ });
38
+ assert.equal(out.stackName, 'ok-stack');
39
+ });
40
+
41
+ test('interactiveNew rejects prefilled reserved stackName', async (t) => {
42
+ t.mock.method(console, 'log', () => {});
43
+ await assert.rejects(
44
+ () =>
45
+ interactiveNew({
46
+ rootDir: '/tmp/hstack-root',
47
+ rl: mkRl([]),
48
+ defaults: {
49
+ stackName: 'main',
50
+ port: 4101,
51
+ serverComponent: 'happier-server-light',
52
+ createRemote: 'upstream',
53
+ repo: 'default',
54
+ },
55
+ deps: createInteractiveStackConfigDeps(),
56
+ }),
57
+ /reserved/i
58
+ );
59
+ });
60
+
61
+ test('interactiveNew reprompts when stackName exceeds max length', async (t) => {
62
+ t.mock.method(console, 'log', () => {});
63
+ const out = await interactiveNew({
64
+ rootDir: '/tmp/hstack-root',
65
+ rl: mkRl(['a'.repeat(64), 'max-len-ok']),
66
+ defaults: {
67
+ stackName: '',
68
+ port: 4101,
69
+ serverComponent: 'happier-server-light',
70
+ createRemote: 'upstream',
71
+ repo: 'default',
72
+ },
73
+ deps: createInteractiveStackConfigDeps(),
74
+ });
75
+ assert.equal(out.stackName, 'max-len-ok');
76
+ });
@@ -0,0 +1,18 @@
1
+ export function mkRl(answers = []) {
2
+ let index = 0;
3
+ return {
4
+ question: async () => {
5
+ const value = answers[index] ?? '';
6
+ index += 1;
7
+ return String(value);
8
+ },
9
+ };
10
+ }
11
+
12
+ export function createInteractiveStackConfigDeps(overrides = {}) {
13
+ return {
14
+ promptSelect: async (_rl, { options, defaultIndex = 0 }) => options?.[defaultIndex]?.value,
15
+ promptWorktreeSource: async () => 'default',
16
+ ...overrides,
17
+ };
18
+ }
@@ -0,0 +1,27 @@
1
+ export function sanitizeStackName(raw, { fallback = 'stack', maxLen = 64 } = {}) {
2
+ const s = String(raw ?? '')
3
+ .trim()
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9-]+/g, '-')
6
+ .replace(/-+/g, '-')
7
+ .replace(/^-+/, '')
8
+ .replace(/-+$/, '');
9
+ const out = s || String(fallback ?? 'stack');
10
+ return Number.isFinite(maxLen) && maxLen > 0 ? out.slice(0, maxLen) : out;
11
+ }
12
+
13
+ export function normalizeStackNameOrNull(raw, { maxLen = 63 } = {}) {
14
+ const trimmed = String(raw ?? '').trim();
15
+ if (!trimmed) return null;
16
+
17
+ const normalized = String(trimmed)
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9-]+/g, '-')
20
+ .replace(/-+/g, '-')
21
+ .replace(/^-+/, '')
22
+ .replace(/-+$/, '');
23
+
24
+ if (!normalized) return null;
25
+ if (Number.isFinite(maxLen) && maxLen > 0 && normalized.length > maxLen) return null;
26
+ return normalized;
27
+ }
@@ -0,0 +1,26 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { normalizeStackNameOrNull } from './names.mjs';
5
+
6
+ test('normalizeStackNameOrNull normalizes to a DNS-safe label', () => {
7
+ assert.equal(normalizeStackNameOrNull('My Stack'), 'my-stack');
8
+ });
9
+
10
+ test('normalizeStackNameOrNull returns null when the name sanitizes to empty', () => {
11
+ assert.equal(normalizeStackNameOrNull('----'), null);
12
+ });
13
+
14
+ test('normalizeStackNameOrNull returns null when the name exceeds maxLen', () => {
15
+ const long = 'a'.repeat(64);
16
+ assert.equal(normalizeStackNameOrNull(long), null);
17
+ });
18
+
19
+ test('normalizeStackNameOrNull accepts a 63-character DNS-safe label', () => {
20
+ const max = 'a'.repeat(63);
21
+ assert.equal(normalizeStackNameOrNull(max), max);
22
+ });
23
+
24
+ test('normalizeStackNameOrNull collapses punctuation runs into single separators', () => {
25
+ assert.equal(normalizeStackNameOrNull('My__Stack...Name'), 'my-stack-name');
26
+ });
@@ -0,0 +1,16 @@
1
+ import { parseGithubPullRequest } from '../git/refs.mjs';
2
+ import { sanitizeStackName } from './names.mjs';
3
+
4
+ export function inferPrStackBaseName({ happy, happyCli, server, serverLight, fallback = 'pr' }) {
5
+ const parts = [];
6
+ const hn = parseGithubPullRequest(happy)?.number ?? null;
7
+ const cn = parseGithubPullRequest(happyCli)?.number ?? null;
8
+ const sn = parseGithubPullRequest(server)?.number ?? null;
9
+ const sln = parseGithubPullRequest(serverLight)?.number ?? null;
10
+ if (hn) parts.push(`happy${hn}`);
11
+ if (cn) parts.push(`cli${cn}`);
12
+ if (sn) parts.push(`server${sn}`);
13
+ if (sln) parts.push(`light${sln}`);
14
+ return sanitizeStackName(parts.length ? `pr-${parts.join('-')}` : fallback, { fallback, maxLen: 64 });
15
+ }
16
+