@happier-dev/stack 0.1.0-preview.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +159 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +94 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,82 @@
1
+ import { inferRemoteNameForOwner, parseGithubOwner } from '../git/worktrees.mjs';
2
+ import { gitCapture, gitOk, normalizeRemoteName, resolveRemoteDefaultBranch, ensureRemoteRefAvailable } from '../git/git.mjs';
3
+
4
+ async function currentBranchName({ cwd }) {
5
+ const branch = (await gitCapture({ cwd, args: ['branch', '--show-current'] }).catch(() => '')).trim();
6
+ return branch;
7
+ }
8
+
9
+ function branchOwnerPrefix(branch) {
10
+ const b = String(branch ?? '').trim();
11
+ if (!b || !b.includes('/')) return '';
12
+ return b.split('/')[0] ?? '';
13
+ }
14
+
15
+ async function inferRemoteFromBranchOwner({ cwd }) {
16
+ const branch = await currentBranchName({ cwd });
17
+ const owner = branchOwnerPrefix(branch);
18
+ if (!owner) return '';
19
+
20
+ // Confirm this "owner" is plausible (matches at least one remote's GitHub owner).
21
+ for (const remoteName of ['upstream', 'origin', 'fork']) {
22
+ try {
23
+ const url = (await gitCapture({ cwd, args: ['remote', 'get-url', remoteName] })).trim();
24
+ const parsedOwner = parseGithubOwner(url);
25
+ if (parsedOwner && parsedOwner === owner) {
26
+ return remoteName;
27
+ }
28
+ } catch {
29
+ // ignore
30
+ }
31
+ }
32
+
33
+ // Fall back to the generic inference helper (it checks remotes in priority order).
34
+ return await inferRemoteNameForOwner({ repoDir: cwd, owner });
35
+ }
36
+
37
+ export async function resolveBaseRef({
38
+ cwd,
39
+ baseRefOverride = '',
40
+ baseRemoteOverride = '',
41
+ baseBranchOverride = '',
42
+ stackRemoteFallback = '',
43
+ } = {}) {
44
+ const repoDir = String(cwd ?? '').trim();
45
+ if (!repoDir) {
46
+ throw new Error('[review] missing cwd for base resolution');
47
+ }
48
+
49
+ if (!(await gitOk({ cwd: repoDir, args: ['rev-parse', '--is-inside-work-tree'] }))) {
50
+ throw new Error(`[review] not a git repository: ${repoDir}`);
51
+ }
52
+
53
+ const explicitRef = String(baseRefOverride ?? '').trim();
54
+ if (explicitRef) {
55
+ return { baseRef: explicitRef, remote: '', branch: '' };
56
+ }
57
+
58
+ const stackFallback = String(stackRemoteFallback ?? '').trim();
59
+ const inferredRemote = await inferRemoteFromBranchOwner({ cwd: repoDir });
60
+ const remoteCandidates = [];
61
+ for (const name of [String(baseRemoteOverride ?? '').trim(), inferredRemote, stackFallback, 'upstream', 'origin', 'fork']) {
62
+ if (!name || remoteCandidates.includes(name)) continue;
63
+ remoteCandidates.push(name);
64
+ }
65
+
66
+ for (const candidate of remoteCandidates) {
67
+ const remote = await normalizeRemoteName({ cwd: repoDir, remote: candidate });
68
+ const branch = String(baseBranchOverride ?? '').trim() || (await resolveRemoteDefaultBranch({ cwd: repoDir, remote }));
69
+ const ok = await ensureRemoteRefAvailable({ cwd: repoDir, remote, branch });
70
+ if (ok) {
71
+ return { baseRef: `${remote}/${branch}`, remote, branch };
72
+ }
73
+ }
74
+
75
+ const first = remoteCandidates[0] || 'upstream';
76
+ const remote = await normalizeRemoteName({ cwd: repoDir, remote: first });
77
+ const branch = String(baseBranchOverride ?? '').trim() || (await resolveRemoteDefaultBranch({ cwd: repoDir, remote }));
78
+ throw new Error(
79
+ `[review] unable to resolve base ref refs/remotes/${remote}/${branch} in ${repoDir}\n` +
80
+ `[review] hint: ensure remote "${remote}" exists and has a configured HEAD/default branch (or pass --base-ref).`
81
+ );
82
+ }
@@ -0,0 +1,89 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { runCapture } from '../proc/proc.mjs';
7
+ import { resolveBaseRef } from './base_ref.mjs';
8
+
9
+ async function runGit(cwd, args) {
10
+ await runCapture('git', args, { cwd });
11
+ }
12
+
13
+ async function makeRepoWithRemoteHead(remoteName = 'upstream') {
14
+ const root = await mkdtemp(join(tmpdir(), 'hs-review-base-ref-'));
15
+ const remote = join(root, 'remote.git');
16
+ const local = join(root, 'local');
17
+
18
+ await runGit(root, ['init', '--bare', remote]);
19
+ await runGit(root, ['init', '-b', 'main', local]);
20
+ await runGit(local, ['config', 'user.email', 'test@example.com']);
21
+ await runGit(local, ['config', 'user.name', 'Test User']);
22
+ await writeFile(join(local, 'file.txt'), 'hello\n', 'utf-8');
23
+ await runGit(local, ['add', '.']);
24
+ await runGit(local, ['commit', '-m', 'initial']);
25
+ await runGit(local, ['remote', 'add', remoteName, remote]);
26
+ await runGit(local, ['push', '-u', remoteName, 'main']);
27
+ // In CI, `git init --bare` may default HEAD to `master`, and `remote set-head --auto` fails
28
+ // with "Cannot determine remote HEAD" if that branch doesn't exist. Make it deterministic.
29
+ await runGit(remote, ['symbolic-ref', 'HEAD', 'refs/heads/main']);
30
+ // Ensure refs/remotes/<remote>/HEAD exists.
31
+ await runGit(local, ['remote', 'set-head', remoteName, '--auto']);
32
+
33
+ return { root, local };
34
+ }
35
+
36
+ test('resolveBaseRef uses explicit --base-ref override', async () => {
37
+ const { root, local } = await makeRepoWithRemoteHead();
38
+ try {
39
+ const res = await resolveBaseRef({ cwd: local, baseRefOverride: 'upstream/main' });
40
+ assert.equal(res.baseRef, 'upstream/main');
41
+ } finally {
42
+ await rm(root, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ test('resolveBaseRef infers default branch from refs/remotes/<remote>/HEAD', async () => {
47
+ const { root, local } = await makeRepoWithRemoteHead();
48
+ try {
49
+ const res = await resolveBaseRef({ cwd: local, baseRemoteOverride: 'upstream' });
50
+ assert.equal(res.baseRef, 'upstream/main');
51
+ assert.equal(res.remote, 'upstream');
52
+ assert.equal(res.branch, 'main');
53
+ } finally {
54
+ await rm(root, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ test('resolveBaseRef uses stack remote fallback when no override is passed', async () => {
59
+ const { root, local } = await makeRepoWithRemoteHead('origin');
60
+ try {
61
+ const res = await resolveBaseRef({ cwd: local, stackRemoteFallback: 'origin' });
62
+ assert.equal(res.baseRef, 'origin/main');
63
+ assert.equal(res.remote, 'origin');
64
+ assert.equal(res.branch, 'main');
65
+ } finally {
66
+ await rm(root, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ test('resolveBaseRef falls back to origin when upstream is unavailable', async () => {
71
+ const { root, local } = await makeRepoWithRemoteHead('origin');
72
+ try {
73
+ const res = await resolveBaseRef({ cwd: local });
74
+ assert.equal(res.baseRef, 'origin/main');
75
+ assert.equal(res.remote, 'origin');
76
+ assert.equal(res.branch, 'main');
77
+ } finally {
78
+ await rm(root, { recursive: true, force: true });
79
+ }
80
+ });
81
+
82
+ test('resolveBaseRef throws for non-git directory', async () => {
83
+ const dir = await mkdtemp(join(tmpdir(), 'hs-review-base-ref-non-git-'));
84
+ try {
85
+ await assert.rejects(async () => await resolveBaseRef({ cwd: dir }), /not a git repository/);
86
+ } finally {
87
+ await rm(dir, { recursive: true, force: true });
88
+ }
89
+ });
@@ -0,0 +1,55 @@
1
+ export async function planCommitChunks({ baseCommit, commits, maxFiles, countFilesBetween }) {
2
+ if (!Array.isArray(commits)) throw new Error('[review] planCommitChunks: commits must be an array');
3
+ const max = Number(maxFiles);
4
+ if (!Number.isFinite(max) || max <= 0) throw new Error('[review] planCommitChunks: maxFiles must be a positive number');
5
+ if (typeof countFilesBetween !== 'function') throw new Error('[review] planCommitChunks: countFilesBetween must be a function');
6
+
7
+ const list = commits.map((c) => String(c ?? '').trim()).filter(Boolean);
8
+ if (!list.length) return [];
9
+
10
+ const chunks = [];
11
+ let base = String(baseCommit ?? '').trim();
12
+ if (!base) throw new Error('[review] planCommitChunks: baseCommit is required');
13
+ let startIndex = 0;
14
+
15
+ while (startIndex < list.length) {
16
+ let lo = startIndex;
17
+ let hi = list.length - 1;
18
+ let bestIndex = -1;
19
+ let bestCount = -1;
20
+
21
+ while (lo <= hi) {
22
+ const mid = Math.floor((lo + hi) / 2);
23
+ const head = list[mid];
24
+ // eslint-disable-next-line no-await-in-loop
25
+ const n = await countFilesBetween({ base, head });
26
+ if (!Number.isFinite(n) || n < 0) throw new Error('[review] planCommitChunks: countFilesBetween returned invalid count');
27
+
28
+ if (n <= max) {
29
+ bestIndex = mid;
30
+ bestCount = n;
31
+ lo = mid + 1;
32
+ } else {
33
+ hi = mid - 1;
34
+ }
35
+ }
36
+
37
+ // If even the smallest chunk exceeds the limit, emit a single over-limit chunk so the caller can decide what to do.
38
+ if (bestIndex === -1) {
39
+ const head = list[startIndex];
40
+ // eslint-disable-next-line no-await-in-loop
41
+ const n = await countFilesBetween({ base, head });
42
+ chunks.push({ base, head, fileCount: n, overLimit: true });
43
+ base = head;
44
+ startIndex += 1;
45
+ continue;
46
+ }
47
+
48
+ const head = list[bestIndex];
49
+ chunks.push({ base, head, fileCount: bestCount, overLimit: false });
50
+ base = head;
51
+ startIndex = bestIndex + 1;
52
+ }
53
+
54
+ return chunks;
55
+ }
@@ -0,0 +1,107 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { planCommitChunks } from './chunks.mjs';
4
+
5
+ test('planCommitChunks greedily selects the largest end commit within maxFiles', async () => {
6
+ const commits = ['c1', 'c2', 'c3', 'c4'];
7
+
8
+ const counts = new Map([
9
+ ['BASE->c1', 1],
10
+ ['BASE->c2', 3],
11
+ ['BASE->c3', 4],
12
+ ['BASE->c4', 7],
13
+ ['c2->c3', 2],
14
+ ['c2->c4', 5],
15
+ ['c3->c4', 2],
16
+ ]);
17
+
18
+ const chunks = await planCommitChunks({
19
+ baseCommit: 'BASE',
20
+ commits,
21
+ maxFiles: 3,
22
+ countFilesBetween: async ({ base, head }) => counts.get(`${base}->${head}`),
23
+ });
24
+
25
+ assert.deepEqual(chunks, [
26
+ { base: 'BASE', head: 'c2', fileCount: 3, overLimit: false },
27
+ { base: 'c2', head: 'c3', fileCount: 2, overLimit: false },
28
+ { base: 'c3', head: 'c4', fileCount: 2, overLimit: false },
29
+ ]);
30
+ });
31
+
32
+ test('planCommitChunks marks overLimit when a single step exceeds maxFiles', async () => {
33
+ const commits = ['c1', 'c2'];
34
+
35
+ const counts = new Map([
36
+ ['BASE->c1', 10],
37
+ ['c1->c2', 2],
38
+ ]);
39
+
40
+ const chunks = await planCommitChunks({
41
+ baseCommit: 'BASE',
42
+ commits,
43
+ maxFiles: 3,
44
+ countFilesBetween: async ({ base, head }) => counts.get(`${base}->${head}`),
45
+ });
46
+
47
+ assert.deepEqual(chunks, [
48
+ { base: 'BASE', head: 'c1', fileCount: 10, overLimit: true },
49
+ { base: 'c1', head: 'c2', fileCount: 2, overLimit: false },
50
+ ]);
51
+ });
52
+
53
+ test('planCommitChunks validates options and handles empty commit input', async () => {
54
+ await assert.rejects(
55
+ async () =>
56
+ await planCommitChunks({
57
+ baseCommit: 'BASE',
58
+ commits: 'not-an-array',
59
+ maxFiles: 3,
60
+ countFilesBetween: async () => 1,
61
+ }),
62
+ /commits must be an array/
63
+ );
64
+
65
+ await assert.rejects(
66
+ async () =>
67
+ await planCommitChunks({
68
+ baseCommit: 'BASE',
69
+ commits: ['c1'],
70
+ maxFiles: 0,
71
+ countFilesBetween: async () => 1,
72
+ }),
73
+ /maxFiles must be a positive number/
74
+ );
75
+
76
+ await assert.rejects(
77
+ async () =>
78
+ await planCommitChunks({
79
+ baseCommit: 'BASE',
80
+ commits: ['c1'],
81
+ maxFiles: 3,
82
+ countFilesBetween: null,
83
+ }),
84
+ /countFilesBetween must be a function/
85
+ );
86
+
87
+ const chunks = await planCommitChunks({
88
+ baseCommit: 'BASE',
89
+ commits: [' ', ''],
90
+ maxFiles: 3,
91
+ countFilesBetween: async () => 1,
92
+ });
93
+ assert.deepEqual(chunks, []);
94
+ });
95
+
96
+ test('planCommitChunks rejects invalid countFilesBetween results', async () => {
97
+ await assert.rejects(
98
+ async () =>
99
+ await planCommitChunks({
100
+ baseCommit: 'BASE',
101
+ commits: ['c1'],
102
+ maxFiles: 3,
103
+ countFilesBetween: async () => Number.NaN,
104
+ }),
105
+ /returned invalid count/
106
+ );
107
+ });
@@ -0,0 +1,61 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { ensureDir } from '../fs/ops.mjs';
4
+ import { runCapture } from '../proc/proc.mjs';
5
+
6
+ function sanitizeLabel(raw) {
7
+ return String(raw ?? 'worktree')
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9._-]+/g, '-')
10
+ .replace(/^-+|-+$/g, '');
11
+ }
12
+
13
+ function defaultNonce() {
14
+ const rand = Math.random().toString(16).slice(2, 10);
15
+ return `${process.pid}-${Date.now()}-${rand}`;
16
+ }
17
+
18
+ export function computeDetachedWorktreeDir({ repoRootDir, label, headCommit, nonce } = {}) {
19
+ const root = String(repoRootDir ?? '').trim();
20
+ if (!root) throw new Error('[review] computeDetachedWorktreeDir: missing repoRootDir');
21
+
22
+ const safeLabel = sanitizeLabel(label);
23
+ const short = String(headCommit ?? '').slice(0, 12);
24
+ const n = String(nonce ?? defaultNonce()).trim();
25
+ return join(root, '.project', 'review-worktrees', `${safeLabel}-${short}-${n}`);
26
+ }
27
+
28
+ /**
29
+ * Create a detached git worktree for `headCommit`, run `fn(worktreeDir)`, then clean up.
30
+ *
31
+ * Notes:
32
+ * - The worktree directory name includes a nonce to avoid collisions when a prior run crashed
33
+ * and left behind a directory, or when multiple review runs happen in parallel.
34
+ * - We do best-effort cleanup even if `fn` throws.
35
+ */
36
+ export async function withDetachedWorktree({ repoDir, headCommit, label, env, nonce }, fn) {
37
+ const root = (await runCapture('git', ['rev-parse', '--show-toplevel'], { cwd: repoDir, env })).toString().trim();
38
+ if (!root) throw new Error('[review] failed to resolve git toplevel');
39
+
40
+ const worktreesRoot = join(root, '.project', 'review-worktrees');
41
+ await ensureDir(worktreesRoot);
42
+ const dir = computeDetachedWorktreeDir({ repoRootDir: root, label, headCommit, nonce });
43
+
44
+ // Extremely defensive: should not happen with nonced dirs, but avoid surprising errors.
45
+ if (existsSync(dir)) {
46
+ throw new Error(`[review] detached worktree dir already exists: ${dir}`);
47
+ }
48
+
49
+ try {
50
+ await runCapture('git', ['worktree', 'add', '--detach', dir, headCommit], { cwd: repoDir, env });
51
+ return await fn(dir);
52
+ } finally {
53
+ try {
54
+ await runCapture('git', ['worktree', 'remove', '--force', dir], { cwd: repoDir, env });
55
+ await runCapture('git', ['worktree', 'prune'], { cwd: repoDir, env });
56
+ } catch {
57
+ // best-effort cleanup; leave an orphaned worktree if needed
58
+ }
59
+ }
60
+ }
61
+
@@ -0,0 +1,61 @@
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
+ import { run, runCapture } from '../proc/proc.mjs';
7
+ import { computeDetachedWorktreeDir, withDetachedWorktree } from './detached_worktree.mjs';
8
+
9
+ function gitEnv() {
10
+ const clean = {};
11
+ for (const [k, v] of Object.entries(process.env)) {
12
+ if (k.startsWith('HAPPIER_STACK_')) continue;
13
+ clean[k] = v;
14
+ }
15
+ return {
16
+ ...clean,
17
+ GIT_AUTHOR_NAME: 'Test',
18
+ GIT_AUTHOR_EMAIL: 'test@example.com',
19
+ GIT_COMMITTER_NAME: 'Test',
20
+ GIT_COMMITTER_EMAIL: 'test@example.com',
21
+ };
22
+ }
23
+
24
+ test('computeDetachedWorktreeDir includes nonce to avoid collisions', () => {
25
+ const dir1 = computeDetachedWorktreeDir({ repoRootDir: '/repo', label: 'coderabbit-1-of-21', headCommit: 'abcdef0123456789', nonce: 'n1' });
26
+ const dir2 = computeDetachedWorktreeDir({ repoRootDir: '/repo', label: 'coderabbit-1-of-21', headCommit: 'abcdef0123456789', nonce: 'n2' });
27
+ assert.notEqual(dir1, dir2);
28
+ assert.ok(dir1.includes('coderabbit-1-of-21-abcdef012345-n1'));
29
+ assert.ok(dir2.includes('coderabbit-1-of-21-abcdef012345-n2'));
30
+ });
31
+
32
+ test('withDetachedWorktree can be called repeatedly without directory collisions', async () => {
33
+ const repo = await mkdtemp(join(tmpdir(), 'happy-review-wt-'));
34
+ const env = gitEnv();
35
+
36
+ try {
37
+ await run('git', ['init', '-q'], { cwd: repo, env });
38
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: repo, env });
39
+ await mkdir(join(repo, 'x'), { recursive: true });
40
+ await writeFile(join(repo, 'x', 'a.txt'), 'a\n', 'utf-8');
41
+ await run('git', ['add', '.'], { cwd: repo, env });
42
+ await run('git', ['commit', '-q', '-m', 'base'], { cwd: repo, env });
43
+
44
+ const head = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: repo, env })).trim();
45
+
46
+ const seen = [];
47
+ await withDetachedWorktree({ repoDir: repo, headCommit: head, label: 'test', env, nonce: 'one' }, async (dir) => {
48
+ seen.push(dir);
49
+ assert.equal((await runCapture('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, env })).trim(), 'true');
50
+ });
51
+ await withDetachedWorktree({ repoDir: repo, headCommit: head, label: 'test', env, nonce: 'two' }, async (dir) => {
52
+ seen.push(dir);
53
+ assert.equal((await runCapture('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, env })).trim(), 'true');
54
+ });
55
+
56
+ assert.equal(seen.length, 2);
57
+ assert.notEqual(seen[0], seen[1]);
58
+ } finally {
59
+ await rm(repo, { recursive: true, force: true });
60
+ }
61
+ });