@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,26 @@
1
+ export function parseGithubPullRequest(input) {
2
+ const raw = (input ?? '').trim();
3
+ if (!raw) return null;
4
+ if (/^\d+$/.test(raw)) {
5
+ return { number: Number(raw), owner: null, repo: null };
6
+ }
7
+ // https://github.com/<owner>/<repo>/pull/<num>
8
+ const m = raw.match(/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/pull\/(?<num>\d+)/);
9
+ if (!m?.groups?.num) return null;
10
+ return {
11
+ number: Number(m.groups.num),
12
+ owner: m.groups.owner ?? null,
13
+ repo: m.groups.repo ?? null,
14
+ };
15
+ }
16
+
17
+ export function sanitizeSlugPart(s) {
18
+ return (s ?? '')
19
+ .toString()
20
+ .trim()
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9._/-]+/g, '-')
23
+ .replace(/-+/g, '-')
24
+ .replace(/^-+|-+$/g, '');
25
+ }
26
+
@@ -0,0 +1,323 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { dirname, isAbsolute, join, resolve, win32 } from 'node:path';
4
+ import { userInfo } from 'node:os';
5
+
6
+ import {
7
+ coerceHappyMonorepoRootFromPath,
8
+ getComponentRepoDir,
9
+ getDevRepoDir,
10
+ getRepoDir,
11
+ getWorkspaceDir,
12
+ happyMonorepoSubdirForComponent,
13
+ isWin32ShapedAbsolutePath,
14
+ } from '../paths/paths.mjs';
15
+ import { pathExists } from '../fs/fs.mjs';
16
+ import { runCapture } from '../proc/proc.mjs';
17
+
18
+ export const WORKTREE_CATEGORIES = Object.freeze(['pr', 'local', 'tmp']);
19
+
20
+ function normalizePathForCompare(p) {
21
+ let s = String(p ?? '').trim();
22
+ if (!s) return '';
23
+ s = s.replaceAll('\\', '/');
24
+ while (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1);
25
+ return s;
26
+ }
27
+
28
+ function resolveForCompare(rawPath) {
29
+ const raw = String(rawPath ?? '').trim();
30
+ if (!raw) return '';
31
+ return isWin32ShapedAbsolutePath(raw) ? win32.resolve(raw) : resolve(raw);
32
+ }
33
+
34
+ function getLocalOwner(env = process.env) {
35
+ const explicit = String(env.HAPPIER_STACK_OWNER ?? '').trim();
36
+ if (explicit) return explicit;
37
+ try {
38
+ const u = userInfo();
39
+ if (u?.username) return u.username;
40
+ } catch {
41
+ // ignore
42
+ }
43
+ return 'unknown';
44
+ }
45
+
46
+ export function parseGithubOwner(remoteUrl) {
47
+ const raw = (remoteUrl ?? '').trim();
48
+ if (!raw) return null;
49
+ // https://github.com/<owner>/<repo>.git
50
+ // git@github.com:<owner>/<repo>.git
51
+ const m = raw.match(/github\.com[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
52
+ return m?.groups?.owner ?? null;
53
+ }
54
+
55
+ export function parseGithubOwnerRepo(remoteUrl) {
56
+ const raw = (remoteUrl ?? '').trim();
57
+ if (!raw) return null;
58
+ const m = raw.match(/github\.com[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
59
+ const owner = m?.groups?.owner ?? null;
60
+ const repo = m?.groups?.repo ?? null;
61
+ return owner && repo ? { owner, repo } : null;
62
+ }
63
+
64
+ export function getWorktreeCategoryRoot(rootDir, category, env = process.env) {
65
+ const c = String(category ?? '').trim();
66
+ if (!WORKTREE_CATEGORIES.includes(c)) {
67
+ throw new Error(`[worktrees] invalid category: ${category}. Expected one of: ${WORKTREE_CATEGORIES.join(', ')}`);
68
+ }
69
+ return join(getWorkspaceDir(rootDir, env), c);
70
+ }
71
+
72
+ export function getWorktreeArchiveRoot(rootDir, env = process.env) {
73
+ return join(getWorkspaceDir(rootDir, env), 'archive', 'worktrees');
74
+ }
75
+
76
+ export function componentRepoDir(rootDir, component, env = process.env) {
77
+ return getComponentRepoDir(rootDir, component, env);
78
+ }
79
+
80
+ function resolveWorktreeRootFromPath({ workspaceDir, absPath }) {
81
+ // Contract (Option A): on POSIX hosts, win32-shaped strings are supported for
82
+ // prefix/spec comparisons, but filesystem walk/root inference is native-only.
83
+ // We intentionally fail closed here instead of pretending to walk a non-native path.
84
+ if (process.platform !== 'win32' && isWin32ShapedAbsolutePath(workspaceDir)) {
85
+ return null;
86
+ }
87
+
88
+ // Normalize to the actual worktree root directory (the one containing `.git`) so
89
+ // package subdirectories like `.../apps/cli` don't corrupt the computed spec.
90
+ let cur = absPath;
91
+ while (cur && cur !== workspaceDir && cur !== dirname(cur)) {
92
+ if (existsSync(join(cur, '.git'))) {
93
+ break;
94
+ }
95
+ cur = dirname(cur);
96
+ }
97
+ if (!cur || cur === workspaceDir || cur === dirname(cur)) return null;
98
+ return cur;
99
+ }
100
+
101
+ export function isWorktreePath({ rootDir, dir, env = process.env }) {
102
+ const raw = String(dir ?? '').trim();
103
+ if (!raw) return false;
104
+ const abs = normalizePathForCompare(resolveForCompare(raw));
105
+ const workspaceDir = normalizePathForCompare(resolveForCompare(getWorkspaceDir(rootDir, env)));
106
+ const prefix = `${workspaceDir}/`;
107
+ if (!abs.startsWith(prefix)) return false;
108
+
109
+ // Only count category worktrees (not main/ or dev/).
110
+ const rel = abs.slice(prefix.length).split('/').filter(Boolean);
111
+ const cat = rel[0] ?? '';
112
+ return WORKTREE_CATEGORIES.includes(cat);
113
+ }
114
+
115
+ export function worktreeSpecFromDir({ rootDir, component, dir, env = process.env }) {
116
+ const raw = String(dir ?? '').trim();
117
+ if (!raw) return null;
118
+ const absNative = resolveForCompare(raw);
119
+ const abs = normalizePathForCompare(absNative);
120
+ void component;
121
+
122
+ const workspaceDirNative = resolveForCompare(getWorkspaceDir(rootDir, env));
123
+ const workspaceDir = normalizePathForCompare(workspaceDirNative);
124
+ const mainDirNative = resolveForCompare(getRepoDir(rootDir, { ...env, HAPPIER_STACK_REPO_DIR: '' }));
125
+ const mainDir = normalizePathForCompare(mainDirNative);
126
+ const devDirNative = resolveForCompare(getDevRepoDir(rootDir, env));
127
+ const devDir = normalizePathForCompare(devDirNative);
128
+
129
+ if (abs === mainDir || abs.startsWith(`${mainDir}/`)) return 'main';
130
+ if (abs === devDir || abs.startsWith(`${devDir}/`)) return 'dev';
131
+
132
+ const prefix = `${workspaceDir}/`;
133
+ if (!abs.startsWith(prefix)) return null;
134
+
135
+ const wtRootNative = resolveWorktreeRootFromPath({ workspaceDir: workspaceDirNative, absPath: absNative });
136
+ const wtRoot = wtRootNative ? normalizePathForCompare(wtRootNative) : null;
137
+ if (!wtRoot) return null;
138
+
139
+ const rel = wtRoot.slice(prefix.length).split('/').filter(Boolean);
140
+ if (rel.length < 2) return null;
141
+ const cat = rel[0];
142
+ if (!WORKTREE_CATEGORIES.includes(cat)) return null;
143
+ return rel.join('/');
144
+ }
145
+
146
+ export function resolveComponentSpecToDir({ rootDir, component, spec, env = process.env }) {
147
+ const raw = (spec ?? '').trim();
148
+ if (!raw || raw === 'default') {
149
+ return null;
150
+ }
151
+
152
+ // Special tokens:
153
+ if (raw === 'main') {
154
+ return getRepoDir(rootDir, { ...env, HAPPIER_STACK_REPO_DIR: '' });
155
+ }
156
+ if (raw === 'dev') {
157
+ return getDevRepoDir(rootDir, env);
158
+ }
159
+
160
+ if (isAbsolute(raw) || isWin32ShapedAbsolutePath(raw)) {
161
+ const monoRoot = coerceHappyMonorepoRootFromPath(raw);
162
+ const sub = monoRoot ? happyMonorepoSubdirForComponent(component, { monorepoRoot: monoRoot }) : null;
163
+ if (monoRoot && sub) return join(monoRoot, sub);
164
+ return raw;
165
+ }
166
+
167
+ // Workspace-relative spec (Option C): pr/... | local/... | tmp/...
168
+ const workspaceDir = getWorkspaceDir(rootDir, env);
169
+ const parts = raw.split('/').filter(Boolean);
170
+ const cat = parts[0] ?? '';
171
+ const rest = parts.slice(1);
172
+ let abs = '';
173
+ if (cat === 'pr') {
174
+ abs = join(workspaceDir, 'pr', ...rest);
175
+ } else if (cat === 'local' || cat === 'tmp') {
176
+ const owner = getLocalOwner(env);
177
+ abs = join(workspaceDir, cat, owner, ...rest);
178
+ } else {
179
+ // Escape hatch: allow workspace-relative paths (e.g. "main/apps/ui").
180
+ abs = join(workspaceDir, ...parts);
181
+ }
182
+ const monoRoot = coerceHappyMonorepoRootFromPath(abs);
183
+ const sub = monoRoot ? happyMonorepoSubdirForComponent(component, { monorepoRoot: monoRoot }) : null;
184
+ return sub && monoRoot ? join(monoRoot, sub) : abs;
185
+ }
186
+
187
+ export async function listWorktreeSpecs({ rootDir, component, env = process.env }) {
188
+ void component;
189
+ const workspaceDirNative = resolveForCompare(getWorkspaceDir(rootDir, env));
190
+ const workspaceDir = normalizePathForCompare(workspaceDirNative);
191
+ const specs = [];
192
+
193
+ const walk = async (d, prefixParts) => {
194
+ const entries = await readdir(d, { withFileTypes: true });
195
+ for (const e of entries) {
196
+ if (!e.isDirectory()) continue;
197
+ const p = join(d, e.name);
198
+ const nextPrefix = [...prefixParts, e.name];
199
+ if (await pathExists(join(p, '.git'))) {
200
+ specs.push(nextPrefix.join('/'));
201
+ // Do not recurse into worktree roots (they contain full repos and can be huge).
202
+ continue;
203
+ }
204
+ await walk(p, nextPrefix);
205
+ }
206
+ };
207
+
208
+ try {
209
+ for (const cat of WORKTREE_CATEGORIES) {
210
+ const catRoot = getWorktreeCategoryRoot(rootDir, cat, env);
211
+ if (!(await pathExists(catRoot))) continue;
212
+
213
+ if (cat === 'pr') {
214
+ // PR worktrees are stored directly under <workspace>/pr/...
215
+ await walk(catRoot, [cat]);
216
+ continue;
217
+ }
218
+
219
+ // Local/tmp worktrees are namespaced by local owner:
220
+ // <workspace>/{local,tmp}/<owner>/...
221
+ // but the user-facing spec intentionally omits the owner prefix:
222
+ // local/<...>, tmp/<...>
223
+ const owner = getLocalOwner(env);
224
+ const ownerRoot = join(catRoot, owner);
225
+ if (!(await pathExists(ownerRoot))) continue;
226
+ await walk(ownerRoot, [cat]);
227
+ }
228
+ } catch {
229
+ // ignore
230
+ }
231
+
232
+ // Sort lexicographically for stable output.
233
+ return specs
234
+ .filter((s) => {
235
+ // Basic safety: ensure spec resolves under the workspace root.
236
+ try {
237
+ const abs = normalizePathForCompare(resolveForCompare(join(workspaceDirNative, s)));
238
+ return abs.startsWith(`${workspaceDir}/`);
239
+ } catch {
240
+ return false;
241
+ }
242
+ })
243
+ .sort();
244
+ }
245
+
246
+ export async function inferRemoteNameForOwner({ repoDir, owner }) {
247
+ const want = String(owner ?? '').trim();
248
+ if (!want) return 'upstream';
249
+
250
+ const candidates = ['upstream', 'origin', 'fork'];
251
+ for (const remoteName of candidates) {
252
+ try {
253
+ const url = (await runCapture('git', ['remote', 'get-url', remoteName], { cwd: repoDir })).trim();
254
+ const o = parseGithubOwner(url);
255
+ if (o && o === want) return remoteName;
256
+ } catch {
257
+ // ignore missing remote
258
+ }
259
+ }
260
+ return 'upstream';
261
+ }
262
+
263
+ export async function getRemoteOwner({ repoDir, remoteName = 'upstream' }) {
264
+ const url = (await runCapture('git', ['remote', 'get-url', remoteName], { cwd: repoDir })).trim();
265
+ const owner = parseGithubOwner(url);
266
+ if (!owner) {
267
+ throw new Error(`[worktrees] unable to parse owner for ${repoDir} remote ${remoteName} (${url})`);
268
+ }
269
+ return owner;
270
+ }
271
+
272
+ function categoryFromSlug(slug) {
273
+ const s = String(slug ?? '').trim();
274
+ if (!s) return { category: 'local', rest: '' };
275
+ const parts = s.split('/').filter(Boolean);
276
+ const first = parts[0] ?? '';
277
+ if (first === 'tmp') return { category: 'tmp', rest: parts.slice(1).join('/') };
278
+ if (first === 'local') return { category: 'local', rest: parts.slice(1).join('/') };
279
+ // `pr/` is handled by `hstack wt pr`, but allow it here as an escape hatch.
280
+ if (first === 'pr') return { category: 'pr', rest: parts.slice(1).join('/') };
281
+ return { category: 'local', rest: s };
282
+ }
283
+
284
+ export async function createWorktreeFromBaseWorktree({
285
+ rootDir,
286
+ component,
287
+ slug,
288
+ baseWorktreeSpec,
289
+ remoteName = 'upstream',
290
+ depsMode = '',
291
+ env = process.env,
292
+ }) {
293
+ const args = ['wt', 'new', component, slug, `--remote=${remoteName}`, `--base-worktree=${baseWorktreeSpec}`];
294
+ if (depsMode) args.push(`--deps=${depsMode}`);
295
+ await runCapture(process.execPath, [join(rootDir, 'bin', 'hstack.mjs'), ...args], { cwd: rootDir, env });
296
+
297
+ const { category, rest } = categoryFromSlug(slug);
298
+ const owner = getLocalOwner(env);
299
+ const wtRoot =
300
+ category === 'pr'
301
+ ? join(getWorktreeCategoryRoot(rootDir, 'pr', env), rest)
302
+ : join(getWorktreeCategoryRoot(rootDir, category, env), owner, ...rest.split('/').filter(Boolean));
303
+
304
+ const monoRoot = coerceHappyMonorepoRootFromPath(wtRoot);
305
+ const sub = monoRoot ? happyMonorepoSubdirForComponent(component, { monorepoRoot: monoRoot }) : null;
306
+ return sub && monoRoot ? join(monoRoot, sub) : wtRoot;
307
+ }
308
+
309
+ export async function createWorktree({ rootDir, component, slug, remoteName = 'upstream', env = process.env }) {
310
+ await runCapture(process.execPath, [join(rootDir, 'bin', 'hstack.mjs'), 'wt', 'new', component, slug, `--remote=${remoteName}`], {
311
+ cwd: rootDir,
312
+ env,
313
+ });
314
+ const { category, rest } = categoryFromSlug(slug);
315
+ const owner = getLocalOwner(env);
316
+ const wtRoot =
317
+ category === 'pr'
318
+ ? join(getWorktreeCategoryRoot(rootDir, 'pr', env), rest)
319
+ : join(getWorktreeCategoryRoot(rootDir, category, env), owner, ...rest.split('/').filter(Boolean));
320
+ const monoRoot = coerceHappyMonorepoRootFromPath(wtRoot);
321
+ const sub = monoRoot ? happyMonorepoSubdirForComponent(component, { monorepoRoot: monoRoot }) : null;
322
+ return sub && monoRoot ? join(monoRoot, sub) : wtRoot;
323
+ }
@@ -0,0 +1,60 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { worktreeSpecFromDir } from './worktrees.mjs';
8
+
9
+ async function withTempRoot(t) {
10
+ const dir = await mkdtemp(join(tmpdir(), 'happier-stack-worktrees-monorepo-'));
11
+ t.after(async () => {
12
+ await rm(dir, { recursive: true, force: true });
13
+ });
14
+ return dir;
15
+ }
16
+
17
+ async function writeHappyMonorepoStub({ rootDir, worktreeRoot }) {
18
+ void rootDir;
19
+ // Stub a monorepo worktree root (apps/* markers + .git) for spec parsing.
20
+ await mkdir(join(worktreeRoot, 'apps', 'ui'), { recursive: true });
21
+ await mkdir(join(worktreeRoot, 'apps', 'cli'), { recursive: true });
22
+ await mkdir(join(worktreeRoot, 'apps', 'server'), { recursive: true });
23
+ await writeFile(join(worktreeRoot, '.git'), 'gitdir: /tmp/fake\n', 'utf-8');
24
+ }
25
+
26
+ test('worktreeSpecFromDir normalizes monorepo package dirs to the worktree spec', async (t) => {
27
+ const rootDir = await withTempRoot(t);
28
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
29
+
30
+ const wtRoot = join(rootDir, 'pr', '123-fix-monorepo');
31
+ await mkdir(wtRoot, { recursive: true });
32
+ await writeHappyMonorepoStub({ rootDir, worktreeRoot: wtRoot });
33
+
34
+ assert.equal(
35
+ worktreeSpecFromDir({ rootDir, component: 'happier-ui', dir: join(wtRoot, 'apps', 'ui'), env }),
36
+ 'pr/123-fix-monorepo'
37
+ );
38
+ assert.equal(
39
+ worktreeSpecFromDir({ rootDir, component: 'happier-cli', dir: join(wtRoot, 'apps', 'cli'), env }),
40
+ 'pr/123-fix-monorepo'
41
+ );
42
+ assert.equal(
43
+ worktreeSpecFromDir({ rootDir, component: 'happier-server', dir: join(wtRoot, 'apps', 'server'), env }),
44
+ 'pr/123-fix-monorepo'
45
+ );
46
+ });
47
+
48
+ test('worktreeSpecFromDir resolves the same spec from the monorepo root and nested unknown dirs', async (t) => {
49
+ const rootDir = await withTempRoot(t);
50
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: rootDir };
51
+ const wtRoot = join(rootDir, 'pr', '456-pathstyle');
52
+ await mkdir(join(wtRoot, 'docs', 'guides'), { recursive: true });
53
+ await writeHappyMonorepoStub({ rootDir, worktreeRoot: wtRoot });
54
+
55
+ assert.equal(worktreeSpecFromDir({ rootDir, component: 'happier-ui', dir: wtRoot, env }), 'pr/456-pathstyle');
56
+ assert.equal(
57
+ worktreeSpecFromDir({ rootDir, component: 'happier-cli', dir: join(wtRoot, 'docs', 'guides'), env }),
58
+ 'pr/456-pathstyle'
59
+ );
60
+ });
@@ -0,0 +1,53 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { isWorktreePath, resolveComponentSpecToDir, worktreeSpecFromDir } from './worktrees.mjs';
5
+
6
+ test('isWorktreePath supports win32-style path separators', () => {
7
+ const rootDir = '/tmp/happier-stack-root';
8
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: 'C:\\happier\\workspace' };
9
+ assert.equal(isWorktreePath({ rootDir, dir: 'C:\\happier\\workspace\\pr\\123-fix-thing', env }), true);
10
+ });
11
+
12
+ test('isWorktreePath rejects non-category workspace paths for win32-style separators', () => {
13
+ const rootDir = '/tmp/happier-stack-root';
14
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: 'C:\\happier\\workspace' };
15
+ assert.equal(isWorktreePath({ rootDir, dir: 'C:\\happier\\workspace\\main', env }), false);
16
+ assert.equal(isWorktreePath({ rootDir, dir: 'C:\\happier\\workspace\\dev', env }), false);
17
+ });
18
+
19
+ test('resolveComponentSpecToDir treats win32-style absolute specs as absolute', () => {
20
+ const rootDir = '/tmp/happier-stack-root';
21
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: '/tmp/workspace' };
22
+ const dir = resolveComponentSpecToDir({
23
+ rootDir,
24
+ component: 'happier-cli',
25
+ spec: 'C:\\happier\\workspace\\pr\\123-fix-thing',
26
+ env,
27
+ });
28
+ assert.equal(dir, 'C:\\happier\\workspace\\pr\\123-fix-thing');
29
+ });
30
+
31
+ test('resolveComponentSpecToDir expands local/tmp workspace specs with owner', () => {
32
+ const rootDir = '/tmp/happier-stack-root';
33
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: '/tmp/workspace', HAPPIER_STACK_OWNER: 'alice' };
34
+ const localDir = resolveComponentSpecToDir({ rootDir, component: 'happier-cli', spec: 'local/branch-a', env });
35
+ const tmpDir = resolveComponentSpecToDir({ rootDir, component: 'happier-cli', spec: 'tmp/branch-b', env });
36
+ assert.equal(localDir, '/tmp/workspace/local/alice/branch-a');
37
+ assert.equal(tmpDir, '/tmp/workspace/tmp/alice/branch-b');
38
+ });
39
+
40
+ test('worktreeSpecFromDir documents native-only filesystem walk for win32 paths on posix', () => {
41
+ if (process.platform === 'win32') {
42
+ return;
43
+ }
44
+ const rootDir = '/tmp/happier-stack-root';
45
+ const env = { HAPPIER_STACK_WORKSPACE_DIR: 'C:\\happier\\workspace' };
46
+ const spec = worktreeSpecFromDir({
47
+ rootDir,
48
+ component: 'happier-cli',
49
+ dir: 'C:\\happier\\workspace\\pr\\123-fix-thing\\apps\\cli',
50
+ env,
51
+ });
52
+ assert.equal(spec, null);
53
+ });