@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,64 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { buildAugmentReviewArgs, detectAugmentAuthError } from './augment.mjs';
5
+
6
+ test('detectAugmentAuthError matches 401 login guidance', () => {
7
+ const stdout = "❌ Authentication failed: HTTP error: 401 Unauthorized\n Run 'auggie login' to authenticate first.";
8
+ assert.equal(detectAugmentAuthError({ stdout, stderr: '' }), true);
9
+ });
10
+
11
+ test('detectAugmentAuthError returns false when required markers are incomplete', () => {
12
+ assert.equal(detectAugmentAuthError({ stdout: 'Authentication failed', stderr: '' }), false);
13
+ assert.equal(detectAugmentAuthError({ stdout: "Run 'auggie login' to authenticate first.", stderr: '' }), false);
14
+ });
15
+
16
+ test('buildAugmentReviewArgs builds auggie --print args with optional settings', () => {
17
+ const args = buildAugmentReviewArgs({
18
+ prompt: 'Review the code',
19
+ workspaceRoot: '/repo',
20
+ cacheDir: '/cache',
21
+ model: 'gpt-5.2',
22
+ rulesFiles: ['/rules/a.md', '/rules/b.md'],
23
+ retryTimeoutSec: 123,
24
+ maxTurns: 9,
25
+ });
26
+
27
+ assert.equal(args[0], '--print');
28
+ assert.ok(args.includes('--quiet'));
29
+ assert.ok(args.includes('--dont-save-session'));
30
+ assert.ok(args.includes('--ask'));
31
+ assert.ok(args.includes('--workspace-root'));
32
+ assert.ok(args.includes('/repo'));
33
+ assert.ok(args.includes('--augment-cache-dir'));
34
+ assert.ok(args.includes('/cache'));
35
+ assert.ok(args.includes('--model'));
36
+ assert.ok(args.includes('gpt-5.2'));
37
+ assert.ok(args.includes('--retry-timeout'));
38
+ assert.ok(args.includes('123'));
39
+ assert.ok(args.includes('--max-turns'));
40
+ assert.ok(args.includes('9'));
41
+
42
+ const joined = args.join(' ');
43
+ assert.match(joined, /--rules \/rules\/a\.md/);
44
+ assert.match(joined, /--rules \/rules\/b\.md/);
45
+ assert.equal(args.at(-1), 'Review the code');
46
+ });
47
+
48
+ test('buildAugmentReviewArgs keeps required args and ignores blank option values', () => {
49
+ const args = buildAugmentReviewArgs({
50
+ prompt: 'Review now',
51
+ workspaceRoot: ' ',
52
+ cacheDir: '',
53
+ model: null,
54
+ rulesFiles: 'not-an-array',
55
+ retryTimeoutSec: undefined,
56
+ maxTurns: '',
57
+ });
58
+
59
+ assert.deepEqual(args, ['--print', '--quiet', '--dont-save-session', '--ask', '--output-format', 'text', 'Review now']);
60
+ });
61
+
62
+ test('buildAugmentReviewArgs requires a non-empty prompt', () => {
63
+ assert.throws(() => buildAugmentReviewArgs({ prompt: ' ' }), /missing prompt/);
64
+ });
@@ -0,0 +1,92 @@
1
+ import { runCaptureResult } from '../../proc/proc.mjs';
2
+
3
+ export function detectClaudeAuthError({ stdout, stderr }) {
4
+ const combined = `${stdout ?? ''}\n${stderr ?? ''}`.toLowerCase();
5
+ const hasAuthHint =
6
+ combined.includes('authentication required') ||
7
+ combined.includes('authentication failed') ||
8
+ combined.includes('invalid api key') ||
9
+ combined.includes('permission denied') ||
10
+ combined.includes('claude auth login') ||
11
+ combined.includes('claude login');
12
+ const hasRateLimitHint =
13
+ combined.includes('status code: 429') ||
14
+ combined.includes('http 429') ||
15
+ combined.includes('429 too many requests') ||
16
+ combined.includes('rate_limit_exceeded') ||
17
+ combined.includes('ratelimiterror');
18
+ return hasAuthHint || hasRateLimitHint;
19
+ }
20
+
21
+ const DEFAULT_ALLOWED_TOOLS = [
22
+ 'Bash(git:*)',
23
+ 'Bash(rg:*)',
24
+ 'Bash(cat:*)',
25
+ 'Bash(sed:*)',
26
+ 'Bash(ls:*)',
27
+ 'Bash(wc:*)',
28
+ 'Bash(head:*)',
29
+ 'Bash(tail:*)',
30
+ ].join(',');
31
+
32
+ function parsePositiveInt(raw) {
33
+ const n = Number(String(raw ?? '').trim());
34
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
35
+ }
36
+
37
+ function resolveClaudeKeepaliveMs(env) {
38
+ const specific = parsePositiveInt(env?.HAPPIER_STACK_REVIEW_CLAUDE_KEEPALIVE_MS);
39
+ if (specific !== null) return specific;
40
+ const global = parsePositiveInt(env?.HAPPIER_STACK_REVIEW_KEEPALIVE_MS);
41
+ if (global !== null) return global;
42
+ return 30_000;
43
+ }
44
+
45
+ export function buildClaudeReviewArgs({ model, allowedTools } = {}) {
46
+ const args = [
47
+ '--print',
48
+ '--input-format',
49
+ 'text',
50
+ '--output-format',
51
+ 'text',
52
+ '--no-session-persistence',
53
+ '--disable-slash-commands',
54
+ '--permission-mode',
55
+ 'bypassPermissions',
56
+ ];
57
+
58
+ const m = String(model ?? '').trim();
59
+ if (m) args.push('--model', m);
60
+
61
+ const tools =
62
+ allowedTools === undefined ? DEFAULT_ALLOWED_TOOLS : String(allowedTools ?? '').trim();
63
+ if (tools) args.push('--allowed-tools', tools);
64
+ return args;
65
+ }
66
+
67
+ export async function runClaudeReview({
68
+ repoDir,
69
+ prompt,
70
+ env,
71
+ streamLabel,
72
+ teeFile,
73
+ teeLabel,
74
+ model,
75
+ allowedTools = DEFAULT_ALLOWED_TOOLS,
76
+ } = {}) {
77
+ const p = String(prompt ?? '').trim();
78
+ if (!p) throw new Error('[review] claude: missing prompt');
79
+
80
+ const args = buildClaudeReviewArgs({ model, allowedTools });
81
+ const heartbeatMs = resolveClaudeKeepaliveMs(env ?? {});
82
+ const res = await runCaptureResult('claude', args, {
83
+ cwd: repoDir,
84
+ env: env ?? {},
85
+ streamLabel,
86
+ teeFile,
87
+ teeLabel,
88
+ input: p,
89
+ heartbeatMs,
90
+ });
91
+ return { ...res, stdout: res.out, stderr: res.err };
92
+ }
@@ -0,0 +1,47 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { buildClaudeReviewArgs, detectClaudeAuthError } from './claude.mjs';
5
+
6
+ test('buildClaudeReviewArgs includes non-interactive flags', () => {
7
+ const args = buildClaudeReviewArgs();
8
+ assert.deepEqual(args, [
9
+ '--print',
10
+ '--input-format',
11
+ 'text',
12
+ '--output-format',
13
+ 'text',
14
+ '--no-session-persistence',
15
+ '--disable-slash-commands',
16
+ '--permission-mode',
17
+ 'bypassPermissions',
18
+ '--allowed-tools',
19
+ 'Bash(git:*),Bash(rg:*),Bash(cat:*),Bash(sed:*),Bash(ls:*),Bash(wc:*),Bash(head:*),Bash(tail:*)',
20
+ ]);
21
+ });
22
+
23
+ test('buildClaudeReviewArgs includes --model when provided', () => {
24
+ const args = buildClaudeReviewArgs({ model: 'opus' });
25
+ assert.equal(args.includes('--model'), true);
26
+ const idx = args.indexOf('--model');
27
+ assert.equal(args[idx + 1], 'opus');
28
+ });
29
+
30
+ test('detectClaudeAuthError matches login/auth failures', () => {
31
+ const stdout = 'Authentication required. Run `claude login` to continue.';
32
+ assert.equal(detectClaudeAuthError({ stdout, stderr: '' }), true);
33
+ });
34
+
35
+ test('detectClaudeAuthError matches rate-limit failures', () => {
36
+ const stderr = 'HTTP 429: rate limit exceeded';
37
+ assert.equal(detectClaudeAuthError({ stdout: '', stderr }), true);
38
+ });
39
+
40
+ test('detectClaudeAuthError returns false for normal output', () => {
41
+ assert.equal(detectClaudeAuthError({ stdout: 'Review completed', stderr: '' }), false);
42
+ });
43
+
44
+ test('detectClaudeAuthError does not treat reviewer prose as a runtime rate-limit failure', () => {
45
+ const stdout = 'If gh release upload fails due to a transient network issue or rate limit, the job fails.';
46
+ assert.equal(detectClaudeAuthError({ stdout, stderr: '' }), false);
47
+ });
@@ -0,0 +1,105 @@
1
+ import { runCaptureResult } from '../../proc/proc.mjs';
2
+ import { join } from 'node:path';
3
+ import { appendFile } from 'node:fs/promises';
4
+
5
+ function normalizeType(raw) {
6
+ const t = String(raw ?? '').trim().toLowerCase();
7
+ if (!t) return 'committed';
8
+ if (t === 'all' || t === 'committed' || t === 'uncommitted') return t;
9
+ throw new Error(`[review] invalid coderabbit type: ${raw} (expected: all|committed|uncommitted)`);
10
+ }
11
+
12
+ export function parseCodeRabbitRateLimitRetryMs(text) {
13
+ const s = String(text ?? '');
14
+ const m = s.match(/Rate limit exceeded,\s*please try after\s+(\d+)\s+minutes?\s+and\s+(\d+)\s+seconds?/i);
15
+ if (!m) return null;
16
+ const minutes = Number(m[1]);
17
+ const seconds = Number(m[2]);
18
+ if (!Number.isFinite(minutes) || minutes < 0) return null;
19
+ if (!Number.isFinite(seconds) || seconds < 0) return null;
20
+ // Add +1s padding to avoid retrying too early.
21
+ const totalSeconds = minutes * 60 + seconds + 1;
22
+ return Math.max(1000, totalSeconds * 1000);
23
+ }
24
+
25
+ export function buildCodeRabbitReviewArgs({ repoDir, baseRef, baseCommit, type, configFiles }) {
26
+ const args = ['review', '--plain', '--no-color', '--type', normalizeType(type), '--cwd', repoDir];
27
+ const base = String(baseRef ?? '').trim();
28
+ const bc = String(baseCommit ?? '').trim();
29
+ if (base && bc) {
30
+ throw new Error('[review] coderabbit: baseRef and baseCommit are mutually exclusive');
31
+ }
32
+ if (base) args.push('--base', base);
33
+ if (bc) args.push('--base-commit', bc);
34
+ const files = Array.isArray(configFiles) ? configFiles.filter(Boolean) : [];
35
+ if (files.length) args.push('--config', ...files);
36
+ return args;
37
+ }
38
+
39
+ export function buildCodeRabbitEnv({ env, homeDir }) {
40
+ const merged = { ...(env ?? {}) };
41
+ const dir = String(homeDir ?? '').trim();
42
+ if (!dir) return merged;
43
+
44
+ // IMPORTANT:
45
+ // Do not override HOME/USERPROFILE here.
46
+ //
47
+ // CodeRabbit uses OS credential storage (e.g. macOS Keychain). If HOME is pointed at
48
+ // an isolated directory (like .project/coderabbit-home), the underlying keychain
49
+ // lookup can fail with "Keychain Not Found" and auth will not work in the wrapper.
50
+ //
51
+ // We still isolate CodeRabbit's on-disk config/cache under the provided homeDir via
52
+ // CODERABBIT_HOME + XDG dirs.
53
+ merged.CODERABBIT_HOME = join(dir, '.coderabbit');
54
+ merged.XDG_CONFIG_HOME = join(dir, '.config');
55
+ merged.XDG_CACHE_HOME = join(dir, '.cache');
56
+ merged.XDG_STATE_HOME = join(dir, '.local', 'state');
57
+ merged.XDG_DATA_HOME = join(dir, '.local', 'share');
58
+ return merged;
59
+ }
60
+
61
+ export async function runCodeRabbitReview({
62
+ repoDir,
63
+ baseRef,
64
+ baseCommit,
65
+ env,
66
+ type = 'committed',
67
+ configFiles = [],
68
+ streamLabel,
69
+ teeFile,
70
+ teeLabel,
71
+ }) {
72
+ const homeDir = (env?.HAPPIER_STACK_CODERABBIT_HOME_DIR ?? '').toString().trim();
73
+ const args = buildCodeRabbitReviewArgs({ repoDir, baseRef, baseCommit, type, configFiles });
74
+ const maxAttempts = 50;
75
+ let last = null;
76
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
77
+ // eslint-disable-next-line no-await-in-loop
78
+ const res = await runCaptureResult('coderabbit', args, {
79
+ cwd: repoDir,
80
+ env: buildCodeRabbitEnv({ env, homeDir }),
81
+ streamLabel,
82
+ teeFile,
83
+ teeLabel,
84
+ });
85
+ last = res;
86
+ if (res.ok) return { ...res, stdout: res.out, stderr: res.err };
87
+
88
+ const retryMs = parseCodeRabbitRateLimitRetryMs(`${res.out ?? ''}\n${res.err ?? ''}`);
89
+ if (!retryMs) return { ...res, stdout: res.out, stderr: res.err };
90
+
91
+ const seconds = Math.ceil(retryMs / 1000);
92
+ const msg = `[review] coderabbit rate limited; retrying in ${seconds}s (attempt ${attempt}/${maxAttempts})\n`;
93
+ try {
94
+ if (teeFile) await appendFile(teeFile, msg);
95
+ } catch {
96
+ // ignore
97
+ }
98
+ // eslint-disable-next-line no-console
99
+ console.warn(msg.trimEnd());
100
+ // eslint-disable-next-line no-await-in-loop
101
+ await new Promise((r) => setTimeout(r, retryMs));
102
+ }
103
+
104
+ return { ...last, stdout: last?.out ?? '', stderr: last?.err ?? '' };
105
+ }
@@ -0,0 +1,32 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { parseCodeRabbitRateLimitRetryMs } from './coderabbit.mjs';
5
+
6
+ test('parseCodeRabbitRateLimitRetryMs returns null when no rate limit message is present', () => {
7
+ assert.equal(parseCodeRabbitRateLimitRetryMs('Review completed ✔'), null);
8
+ });
9
+
10
+ test('parseCodeRabbitRateLimitRetryMs parses the suggested retry delay', () => {
11
+ const ms = parseCodeRabbitRateLimitRetryMs(
12
+ '[2026-01-25T22:29:41.623Z] ❌ ERROR: Error: Rate limit exceeded, please try after 3 minutes and 2 seconds'
13
+ );
14
+ assert.ok(ms);
15
+ // Allow +1s padding.
16
+ assert.equal(ms, (3 * 60 + 2 + 1) * 1000);
17
+ });
18
+
19
+ test('parseCodeRabbitRateLimitRetryMs supports seconds-only windows', () => {
20
+ const ms = parseCodeRabbitRateLimitRetryMs(
21
+ '[2026-01-26T00:27:23.067Z] ❌ ERROR: Error: Rate limit exceeded, please try after 0 minutes and 31 seconds'
22
+ );
23
+ assert.ok(ms);
24
+ assert.equal(ms, (31 + 1) * 1000);
25
+ });
26
+
27
+ test('parseCodeRabbitRateLimitRetryMs enforces a minimum 1s retry window', () => {
28
+ const ms = parseCodeRabbitRateLimitRetryMs(
29
+ '[2026-01-26T00:27:23.067Z] ERROR: Rate limit exceeded, please try after 0 minutes and 0 seconds'
30
+ );
31
+ assert.equal(ms, 1000);
32
+ });
@@ -0,0 +1,129 @@
1
+ import { runCaptureResult } from '../../proc/proc.mjs';
2
+
3
+ const unsupportedModels = new Set();
4
+
5
+ function parsePositiveInt(raw) {
6
+ const n = Number(String(raw ?? '').trim());
7
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
8
+ }
9
+
10
+ function resolveCodexKeepaliveMs(env) {
11
+ const specific = parsePositiveInt(env?.HAPPIER_STACK_REVIEW_CODEX_KEEPALIVE_MS);
12
+ if (specific !== null) return specific;
13
+ const global = parsePositiveInt(env?.HAPPIER_STACK_REVIEW_KEEPALIVE_MS);
14
+ if (global !== null) return global;
15
+ return 30_000;
16
+ }
17
+
18
+ export function detectCodexUnsupportedModelError({ stdout, stderr }) {
19
+ const combined = `${stdout ?? ''}\n${stderr ?? ''}`;
20
+ return combined.includes('model is not supported when using Codex with a ChatGPT account');
21
+ }
22
+
23
+ export function markCodexModelUnsupported(model) {
24
+ const m = String(model ?? '').trim();
25
+ if (m) unsupportedModels.add(m);
26
+ }
27
+
28
+ export function isCodexModelKnownUnsupported(model) {
29
+ const m = String(model ?? '').trim();
30
+ if (!m) return false;
31
+ return unsupportedModels.has(m);
32
+ }
33
+
34
+ export function extractCodexReviewFromJsonl(jsonlText) {
35
+ const lines = String(jsonlText ?? '')
36
+ .split('\n')
37
+ .map((l) => l.trim())
38
+ .filter(Boolean);
39
+
40
+ // JSONL events typically look like: { "type": "...", "payload": {...} } or similar.
41
+ // We keep this resilient by searching for keys matching the exec output format.
42
+ for (const line of lines) {
43
+ let obj = null;
44
+ try {
45
+ obj = JSON.parse(line);
46
+ } catch {
47
+ continue;
48
+ }
49
+ const candidates = [obj, obj?.msg, obj?.payload, obj?.event, obj?.data, obj?.result].filter(Boolean);
50
+ for (const c of candidates) {
51
+ const exited =
52
+ c?.ExitedReviewMode ??
53
+ (c?.type === 'ExitedReviewMode' ? c : null) ??
54
+ (c?.event?.type === 'ExitedReviewMode' ? c.event : null) ??
55
+ (c?.payload?.type === 'ExitedReviewMode' ? c.payload : null);
56
+
57
+ const reviewOutput = exited?.review_output ?? exited?.reviewOutput ?? null;
58
+ if (reviewOutput) return reviewOutput;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ export function buildCodexReviewArgs({ baseRef, jsonMode, prompt, model }) {
65
+ const args = ['exec', 'review', '--dangerously-bypass-approvals-and-sandbox'];
66
+ // Review runs should be deterministic and lightweight; disable user-configured MCP servers.
67
+ args.push('-c', 'mcp_servers={}');
68
+
69
+ // Codex review targets are mutually exclusive:
70
+ // - --base / --commit / --uncommitted are distinct "targets"
71
+ // - Providing a PROMPT switches to the "custom instructions" target and cannot be combined with the above.
72
+ // Therefore, when reviewing a target (base/commit/uncommitted), we do not pass a prompt.
73
+ if (baseRef) args.push('--base', baseRef);
74
+
75
+ if (jsonMode) {
76
+ args.push('--json');
77
+ }
78
+
79
+ const m = String(model ?? '').trim();
80
+ if (m) args.push('--model', m);
81
+
82
+ const p = String(prompt ?? '').trim();
83
+ if (!baseRef && p) args.push(p);
84
+ if (!baseRef && !p) args.push('--uncommitted');
85
+ return args;
86
+ }
87
+
88
+ export async function runCodexReview({ repoDir, baseRef, env, jsonMode, streamLabel, teeFile, teeLabel, prompt, model }) {
89
+ const merged = { ...(env ?? {}) };
90
+ const codexHome =
91
+ (merged.HAPPIER_STACK_CODEX_HOME_DIR ?? merged.CODEX_HOME ?? '').toString().trim();
92
+ if (codexHome) merged.CODEX_HOME = codexHome;
93
+
94
+ const effectiveModel = isCodexModelKnownUnsupported(model) ? '' : model;
95
+ const args = buildCodexReviewArgs({ baseRef, jsonMode, prompt, model: effectiveModel });
96
+ const heartbeatMs = resolveCodexKeepaliveMs(merged);
97
+ const res = await runCaptureResult('codex', args, {
98
+ cwd: repoDir,
99
+ env: merged,
100
+ streamLabel,
101
+ teeFile,
102
+ teeLabel,
103
+ heartbeatMs,
104
+ });
105
+
106
+ const out = { ...res, stdout: res.out, stderr: res.err };
107
+ if (out.ok) return out;
108
+
109
+ const m = String(model ?? '').trim();
110
+ if (m && detectCodexUnsupportedModelError({ stdout: out.stdout, stderr: out.stderr })) {
111
+ markCodexModelUnsupported(m);
112
+ // eslint-disable-next-line no-console
113
+ console.warn(`[review] codex model '${m}' not supported; retrying without --model`);
114
+ // In some environments, Codex running under a ChatGPT account rejects certain model IDs.
115
+ // If that happens, fall back to the user's configured default model by omitting --model.
116
+ const retryArgs = buildCodexReviewArgs({ baseRef, jsonMode, prompt, model: '' });
117
+ const retry = await runCaptureResult('codex', retryArgs, {
118
+ cwd: repoDir,
119
+ env: merged,
120
+ streamLabel,
121
+ teeFile,
122
+ teeLabel,
123
+ heartbeatMs,
124
+ });
125
+ return { ...retry, stdout: retry.out, stderr: retry.err };
126
+ }
127
+
128
+ return out;
129
+ }
@@ -0,0 +1,115 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ buildCodexReviewArgs,
6
+ detectCodexUnsupportedModelError,
7
+ extractCodexReviewFromJsonl,
8
+ isCodexModelKnownUnsupported,
9
+ markCodexModelUnsupported,
10
+ } from './codex.mjs';
11
+
12
+ test('buildCodexReviewArgs uses --base and avoids --cd', () => {
13
+ const args = buildCodexReviewArgs({ baseRef: 'upstream/main', jsonMode: false });
14
+ assert.equal(args.includes('--cd'), false);
15
+ assert.deepEqual(args, [
16
+ 'exec',
17
+ 'review',
18
+ '--dangerously-bypass-approvals-and-sandbox',
19
+ '-c',
20
+ 'mcp_servers={}',
21
+ '--base',
22
+ 'upstream/main',
23
+ ]);
24
+ });
25
+
26
+ test('buildCodexReviewArgs uses --experimental-json when jsonMode is true', () => {
27
+ const args = buildCodexReviewArgs({ baseRef: 'upstream/main', jsonMode: true });
28
+ assert.deepEqual(args, [
29
+ 'exec',
30
+ 'review',
31
+ '--dangerously-bypass-approvals-and-sandbox',
32
+ '-c',
33
+ 'mcp_servers={}',
34
+ '--base',
35
+ 'upstream/main',
36
+ '--json',
37
+ ]);
38
+ });
39
+
40
+ test('buildCodexReviewArgs appends a prompt when provided', () => {
41
+ const args = buildCodexReviewArgs({ baseRef: null, jsonMode: false, prompt: 'be thorough' });
42
+ assert.deepEqual(args, ['exec', 'review', '--dangerously-bypass-approvals-and-sandbox', '-c', 'mcp_servers={}', 'be thorough']);
43
+ });
44
+
45
+ test('buildCodexReviewArgs includes --model when provided', () => {
46
+ const args = buildCodexReviewArgs({ baseRef: 'upstream/main', jsonMode: false, model: 'codex-5.3' });
47
+ assert.deepEqual(args, [
48
+ 'exec',
49
+ 'review',
50
+ '--dangerously-bypass-approvals-and-sandbox',
51
+ '-c',
52
+ 'mcp_servers={}',
53
+ '--base',
54
+ 'upstream/main',
55
+ '--model',
56
+ 'codex-5.3',
57
+ ]);
58
+ });
59
+
60
+ test('buildCodexReviewArgs defaults to --uncommitted for targetless review', () => {
61
+ const args = buildCodexReviewArgs({ baseRef: '', jsonMode: false, prompt: ' ' });
62
+ assert.deepEqual(args, [
63
+ 'exec',
64
+ 'review',
65
+ '--dangerously-bypass-approvals-and-sandbox',
66
+ '-c',
67
+ 'mcp_servers={}',
68
+ '--uncommitted',
69
+ ]);
70
+ });
71
+
72
+ test('buildCodexReviewArgs ignores prompt when baseRef target is provided', () => {
73
+ const args = buildCodexReviewArgs({ baseRef: 'upstream/main', jsonMode: false, prompt: 'be thorough' });
74
+ assert.equal(args.includes('be thorough'), false);
75
+ assert.equal(args.includes('--uncommitted'), false);
76
+ });
77
+
78
+ test('extractCodexReviewFromJsonl finds review_output in multiple event shapes', () => {
79
+ const out1 = extractCodexReviewFromJsonl(
80
+ JSON.stringify({ msg: { ExitedReviewMode: { review_output: { a: 1 } } } }) + '\n'
81
+ );
82
+ assert.deepEqual(out1, { a: 1 });
83
+
84
+ const out2 = extractCodexReviewFromJsonl(JSON.stringify({ type: 'ExitedReviewMode', review_output: { b: 2 } }) + '\n');
85
+ assert.deepEqual(out2, { b: 2 });
86
+
87
+ const out3 = extractCodexReviewFromJsonl(
88
+ JSON.stringify({ event: { type: 'ExitedReviewMode', reviewOutput: { c: 3 } } }) + '\n'
89
+ );
90
+ assert.deepEqual(out3, { c: 3 });
91
+ });
92
+
93
+ test('extractCodexReviewFromJsonl returns null for invalid/no-match lines', () => {
94
+ const result = extractCodexReviewFromJsonl('not-json\n{"type":"Progress","payload":{"x":1}}\n');
95
+ assert.equal(result, null);
96
+ });
97
+
98
+ test('detectCodexUnsupportedModelError detects the ChatGPT-account unsupported-model failure', () => {
99
+ assert.equal(
100
+ detectCodexUnsupportedModelError({
101
+ stdout: '',
102
+ stderr: `ERROR: {"detail":"The 'codex-5.3' model is not supported when using Codex with a ChatGPT account."}`,
103
+ }),
104
+ true,
105
+ );
106
+ assert.equal(detectCodexUnsupportedModelError({ stdout: 'ok', stderr: '' }), false);
107
+ });
108
+
109
+ test('markCodexModelUnsupported / isCodexModelKnownUnsupported track unsupported models', () => {
110
+ const model = `test-model-${process.hrtime.bigint()}`;
111
+ assert.equal(isCodexModelKnownUnsupported(model), false);
112
+ markCodexModelUnsupported(model);
113
+ assert.equal(isCodexModelKnownUnsupported(model), true);
114
+ assert.equal(isCodexModelKnownUnsupported(` ${model} `), true);
115
+ });
@@ -0,0 +1,20 @@
1
+ const UNCOMMITTED_PATH_SLICE_REVIEWERS = new Set(['coderabbit', 'codex', 'claude']);
2
+
3
+ export function reviewerSupportsUncommittedPathSlices(reviewer) {
4
+ return UNCOMMITTED_PATH_SLICE_REVIEWERS.has(String(reviewer ?? '').trim().toLowerCase());
5
+ }
6
+
7
+ export function shouldUseUncommittedPathSlices({
8
+ reviewer,
9
+ changeType,
10
+ fileCount,
11
+ maxFiles,
12
+ chunksPreference = null,
13
+ } = {}) {
14
+ if (String(changeType ?? '').trim().toLowerCase() !== 'uncommitted') return false;
15
+ if (!reviewerSupportsUncommittedPathSlices(reviewer)) return false;
16
+ if (chunksPreference !== null && chunksPreference !== undefined) return Boolean(chunksPreference);
17
+ const parsedMaxFiles = Number(maxFiles);
18
+ if (!Number.isFinite(parsedMaxFiles) || parsedMaxFiles <= 0) return false;
19
+ return Number(fileCount ?? 0) > parsedMaxFiles;
20
+ }
@@ -0,0 +1,69 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { shouldUseUncommittedPathSlices } from './slice_mode.mjs';
5
+
6
+ test('shouldUseUncommittedPathSlices enables codex when file count exceeds max', () => {
7
+ const out = shouldUseUncommittedPathSlices({
8
+ reviewer: 'codex',
9
+ changeType: 'uncommitted',
10
+ fileCount: 120,
11
+ maxFiles: 100,
12
+ chunksPreference: null,
13
+ });
14
+ assert.equal(out, true);
15
+ });
16
+
17
+ test('shouldUseUncommittedPathSlices disables claude when under max and no explicit chunk flag', () => {
18
+ const out = shouldUseUncommittedPathSlices({
19
+ reviewer: 'claude',
20
+ changeType: 'uncommitted',
21
+ fileCount: 20,
22
+ maxFiles: 100,
23
+ chunksPreference: null,
24
+ });
25
+ assert.equal(out, false);
26
+ });
27
+
28
+ test('shouldUseUncommittedPathSlices honors explicit chunk override', () => {
29
+ const out = shouldUseUncommittedPathSlices({
30
+ reviewer: 'claude',
31
+ changeType: 'uncommitted',
32
+ fileCount: 20,
33
+ maxFiles: 100,
34
+ chunksPreference: true,
35
+ });
36
+ assert.equal(out, true);
37
+ });
38
+
39
+ test('shouldUseUncommittedPathSlices returns false for unsupported reviewers', () => {
40
+ const out = shouldUseUncommittedPathSlices({
41
+ reviewer: 'augment',
42
+ changeType: 'uncommitted',
43
+ fileCount: 120,
44
+ maxFiles: 100,
45
+ chunksPreference: true,
46
+ });
47
+ assert.equal(out, false);
48
+ });
49
+
50
+ test('shouldUseUncommittedPathSlices returns false for non-uncommitted change types', () => {
51
+ const out = shouldUseUncommittedPathSlices({
52
+ reviewer: 'codex',
53
+ changeType: 'committed',
54
+ fileCount: 120,
55
+ maxFiles: 100,
56
+ chunksPreference: true,
57
+ });
58
+ assert.equal(out, false);
59
+ });
60
+
61
+ test('shouldUseUncommittedPathSlices returns false when maxFiles is missing and no override is set', () => {
62
+ const out = shouldUseUncommittedPathSlices({
63
+ reviewer: 'codex',
64
+ changeType: 'uncommitted',
65
+ fileCount: 1,
66
+ chunksPreference: null,
67
+ });
68
+ assert.equal(out, false);
69
+ });