@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,260 @@
1
+ import { detectInstalledLlmTools } from './tools.mjs';
2
+ import { buildCodexExecScript, CODEX_PERMISSION_MODES, runCodexExecHere } from './codex_exec.mjs';
3
+ import { canLaunchNewTerminal, launchScriptInNewTerminal } from '../ui/terminal_launcher.mjs';
4
+ import { clipboardAvailable, copyTextToClipboard } from '../ui/clipboard.mjs';
5
+ import { isTty, promptSelect, withRl } from '../cli/wizard.mjs';
6
+ import { banner, bullets, cmd as cmdFmt, sectionTitle } from '../ui/layout.mjs';
7
+ import { bold, cyan, dim, green, red, yellow } from '../ui/ansi.mjs';
8
+ import { run } from '../proc/proc.mjs';
9
+
10
+ function codexPermissionOptions() {
11
+ return [
12
+ { value: 'full-auto', label: `${green('recommended')} — full-auto (approvals on request + workspace sandbox)` },
13
+ { value: 'safe', label: `safe — always ask on risky actions (workspace sandbox)` },
14
+ { value: 'yolo', label: `${red('danger')} — run without approvals/sandbox (YOLO)` },
15
+ ];
16
+ }
17
+
18
+ function shouldAsk(options) {
19
+ return isTty() && options.length > 1;
20
+ }
21
+
22
+ function buildInteractiveLaunchScript({ toolCmd, cd, title, promptText }) {
23
+ const t = String(title ?? 'hstack (LLM)').trim();
24
+ const cwd = String(cd ?? '').trim();
25
+ const cmd = String(toolCmd ?? '').trim();
26
+ const prompt = String(promptText ?? '').trimEnd();
27
+ if (!cwd) throw new Error('[llm] launch: missing cwd');
28
+ if (!cmd) throw new Error('[llm] launch: missing toolCmd');
29
+
30
+ const execLine = (() => {
31
+ // Prefer providing the prompt directly when the CLI supports it.
32
+ // - Claude Code: `claude "query"` starts an interactive session with an initial prompt.
33
+ // - OpenCode: `opencode --prompt "..."` starts the TUI with an initial prompt.
34
+ if (cmd === 'claude') return 'exec command claude "$HS_PROMPT"';
35
+ if (cmd === 'opencode') return 'exec command opencode --prompt "$HS_PROMPT"';
36
+ // Fallback: start the tool and ask the user to paste.
37
+ return `exec command ${JSON.stringify(cmd)}`;
38
+ })();
39
+
40
+ return [
41
+ '#!/usr/bin/env bash',
42
+ 'set -euo pipefail',
43
+ '',
44
+ `cd ${JSON.stringify(cwd)}`,
45
+ '',
46
+ `echo ${JSON.stringify(t)}`,
47
+ 'echo',
48
+ 'echo "Prompt:"',
49
+ 'echo "------------------------------------------------------------"',
50
+ 'HS_PROMPT="$(cat <<\'HS_PROMPT_EOF\'',
51
+ prompt,
52
+ 'HS_PROMPT_EOF',
53
+ ')"',
54
+ 'echo "$HS_PROMPT"',
55
+ 'echo "------------------------------------------------------------"',
56
+ 'echo',
57
+ 'if command -v pbcopy >/dev/null 2>&1; then',
58
+ ' printf "%s" "$HS_PROMPT" | pbcopy',
59
+ ' echo "Copied prompt to clipboard (pbcopy)."',
60
+ 'fi',
61
+ 'echo',
62
+ 'echo "Press Enter to start now, or Ctrl+C to cancel."',
63
+ 'read -r _',
64
+ execLine,
65
+ '',
66
+ ].join('\n');
67
+ }
68
+
69
+ export async function launchLlmAssistant({
70
+ rl: providedRl = null,
71
+ title,
72
+ subtitle,
73
+ promptText,
74
+ cwd,
75
+ preferredToolId = '',
76
+ env = process.env,
77
+ allowRunHere = true,
78
+ allowCopyOnly = true,
79
+ defaultPermissionMode = 'full-auto',
80
+ }) {
81
+ const prompt = String(promptText ?? '').trimEnd();
82
+ const cd = String(cwd ?? '').trim();
83
+ if (!prompt) return { ok: false, reason: 'empty prompt' };
84
+ if (!cd) return { ok: false, reason: 'missing cwd' };
85
+
86
+ const tools = await detectInstalledLlmTools();
87
+ const terminalSupport = await canLaunchNewTerminal({ env });
88
+
89
+ if (tools.length === 0) {
90
+ return { ok: false, reason: 'no supported LLM CLI detected', terminalSupport };
91
+ }
92
+
93
+ const withMaybeRl = async (fn) => {
94
+ if (providedRl) return await fn(providedRl);
95
+ return await withRl(fn);
96
+ };
97
+
98
+ const chosenTool =
99
+ tools.length === 1
100
+ ? tools[0]
101
+ : tools.find((t) => t.id === preferredToolId) ||
102
+ (!isTty()
103
+ ? tools.find((t) => t.id === 'codex') || tools[0]
104
+ : await withMaybeRl(async (rl) => {
105
+ const defaultIndex = Math.max(0, tools.findIndex((t) => t.id === 'codex'));
106
+ const picked = await promptSelect(rl, {
107
+ title:
108
+ `${bold('Pick an LLM CLI')}\n` +
109
+ `${dim('We will launch it with a pre-filled migration prompt so it can run the port and resolve conflicts.')}`,
110
+ options: tools.map((t) => ({
111
+ value: t.id,
112
+ label: `${cyan(t.id)} — ${t.label}${t.note ? ` ${dim(`— ${t.note}`)}` : ''}`,
113
+ })),
114
+ defaultIndex,
115
+ });
116
+ return tools.find((t) => t.id === picked) || tools[0];
117
+ }));
118
+
119
+ const launchOptions = [];
120
+ if (terminalSupport.ok) {
121
+ launchOptions.push({ value: 'new-terminal', label: `${green('recommended')} — launch in a new terminal window` });
122
+ }
123
+ if (allowRunHere) {
124
+ launchOptions.push({ value: 'here', label: `run in this terminal ${dim('(will take over this session)')}` });
125
+ }
126
+ if (allowCopyOnly) {
127
+ launchOptions.push({ value: 'copy', label: `copy the prompt and run it yourself` });
128
+ }
129
+
130
+ const launchMode =
131
+ launchOptions.length === 1
132
+ ? launchOptions[0].value
133
+ : !isTty()
134
+ ? launchOptions[0].value
135
+ : await withMaybeRl(async (rl) => {
136
+ return await promptSelect(rl, {
137
+ title: `${bold('How do you want to run the migration assistant?')}`,
138
+ options: launchOptions,
139
+ defaultIndex: 0,
140
+ });
141
+ });
142
+
143
+ const permissionMode =
144
+ chosenTool.id !== 'codex'
145
+ ? null
146
+ : CODEX_PERMISSION_MODES.length === 1 || !isTty()
147
+ ? defaultPermissionMode
148
+ : await withMaybeRl(async (rl) => {
149
+ const opts = codexPermissionOptions();
150
+ const v = await promptSelect(rl, {
151
+ title:
152
+ `${bold('LLM permissions')}\n` +
153
+ `${dim('Choose how much autonomy the LLM should have while running commands to migrate + resolve conflicts.')}`,
154
+ options: opts,
155
+ defaultIndex: Math.max(0, opts.findIndex((o) => o.value === defaultPermissionMode)),
156
+ });
157
+ return String(v || defaultPermissionMode);
158
+ });
159
+
160
+ if (launchMode === 'copy') {
161
+ return { ok: true, launched: false, mode: 'copy', tool: chosenTool.id, permissionMode, terminalSupport };
162
+ }
163
+
164
+ // Auto-exec path (Codex only for now).
165
+ if (chosenTool.id === 'codex') {
166
+ if (launchMode === 'here') {
167
+ await runCodexExecHere({ cd, permissionMode: permissionMode || defaultPermissionMode, promptText: prompt, env });
168
+ return { ok: true, launched: true, mode: 'here', tool: chosenTool.id, permissionMode, terminalSupport };
169
+ }
170
+
171
+ // new-terminal
172
+ const script = buildCodexExecScript({ cd, permissionMode: permissionMode || defaultPermissionMode, promptText: prompt });
173
+ const res = await launchScriptInNewTerminal({ scriptText: script, title: title || 'hstack migration (LLM)' });
174
+ if (!res.ok) {
175
+ return { ok: false, reason: res.reason || 'failed to launch terminal', terminalSupport };
176
+ }
177
+ return { ok: true, launched: true, mode: 'new-terminal', tool: chosenTool.id, permissionMode, terminalSupport };
178
+ }
179
+
180
+ // Interactive launch path (Claude/OpenCode/Aider/etc).
181
+ // Best-effort: copy prompt to clipboard now; the terminal script will print it too.
182
+ if (await clipboardAvailable()) {
183
+ await copyTextToClipboard(prompt).catch(() => {});
184
+ }
185
+
186
+ if (launchMode === 'here') {
187
+ // Print prompt and then start the CLI (interactive).
188
+ // eslint-disable-next-line no-console
189
+ console.log('');
190
+ // eslint-disable-next-line no-console
191
+ console.log(banner(title || 'LLM prompt', { subtitle: subtitle || 'Paste this into your LLM CLI to run the migration.' }));
192
+ // eslint-disable-next-line no-console
193
+ console.log(prompt);
194
+ // eslint-disable-next-line no-console
195
+ console.log('');
196
+ // eslint-disable-next-line no-console
197
+ console.log(dim(`Starting: ${chosenTool.cmd}`));
198
+
199
+ if (chosenTool.cmd === 'claude') {
200
+ // Claude Code supports starting interactive mode with an initial prompt:
201
+ // `claude "query"`.
202
+ await run('claude', [prompt], { cwd: cd, env: env ?? process.env, stdio: 'inherit' });
203
+ } else if (chosenTool.cmd === 'opencode') {
204
+ // OpenCode supports starting the TUI with an initial prompt:
205
+ // `opencode --prompt "..."`.
206
+ await run('opencode', ['--prompt', prompt], { cwd: cd, env: env ?? process.env, stdio: 'inherit' });
207
+ } else {
208
+ await run(chosenTool.cmd, [], { cwd: cd, env: env ?? process.env, stdio: 'inherit' });
209
+ }
210
+ return { ok: true, launched: true, mode: 'here', tool: chosenTool.id, permissionMode, terminalSupport };
211
+ }
212
+
213
+ // new-terminal
214
+ const script = buildInteractiveLaunchScript({
215
+ toolCmd: chosenTool.cmd,
216
+ cd,
217
+ title: title || `hstack (${chosenTool.id})`,
218
+ promptText: prompt,
219
+ });
220
+ const res = await launchScriptInNewTerminal({ scriptText: script, title: title || `hstack (${chosenTool.id})` });
221
+ if (!res.ok) {
222
+ return { ok: false, reason: res.reason || 'failed to launch terminal', terminalSupport };
223
+ }
224
+ return { ok: true, launched: true, mode: 'new-terminal', tool: chosenTool.id, permissionMode, terminalSupport };
225
+ }
226
+
227
+ export async function printAndMaybeCopyPrompt({ promptText, copy = false }) {
228
+ const prompt = String(promptText ?? '').trimEnd();
229
+ // eslint-disable-next-line no-console
230
+ console.log(prompt);
231
+ if (!copy) return { ok: true, copied: false };
232
+ if (!(await clipboardAvailable())) return { ok: true, copied: false };
233
+ const res = await copyTextToClipboard(prompt);
234
+ return { ok: true, copied: Boolean(res.ok) };
235
+ }
236
+
237
+ export function renderLlmHelpBlock({ title, subtitle, promptText, detectedTools, terminalSupport }) {
238
+ const tools = Array.isArray(detectedTools) ? detectedTools : [];
239
+ const lines = [];
240
+ lines.push('');
241
+ lines.push(banner(title || 'LLM help', { subtitle: subtitle || 'Copy/paste this into your LLM to drive the migration.' }));
242
+ lines.push(promptText);
243
+ if (tools.length) {
244
+ lines.push('');
245
+ lines.push(sectionTitle('Detected LLM CLIs'));
246
+ lines.push(bullets(tools.map((t) => `- ${dim(t.id)}: ${t.label}${t.note ? ` ${dim(`— ${t.note}`)}` : ''}`)));
247
+ } else {
248
+ lines.push('');
249
+ lines.push(dim('No supported LLM CLI detected. You can still paste the prompt into any LLM UI.'));
250
+ }
251
+ if (terminalSupport?.ok) {
252
+ lines.push('');
253
+ lines.push(dim(`Terminal launch: ${green('supported')}`));
254
+ } else if (terminalSupport) {
255
+ lines.push('');
256
+ lines.push(dim(`Terminal launch: ${yellow('not available')} ${dim(`(${terminalSupport.reason || 'unknown'})`)}`));
257
+ }
258
+ lines.push('');
259
+ return lines.join('\n');
260
+ }
@@ -0,0 +1,61 @@
1
+ import { run } from '../proc/proc.mjs';
2
+
3
+ export const CODEX_PERMISSION_MODES = /** @type {const} */ (['safe', 'full-auto', 'yolo']);
4
+
5
+ export function buildCodexExecArgs({ cd, permissionMode }) {
6
+ const cwd = String(cd ?? '').trim();
7
+ if (!cwd) throw new Error('[llm] codex: missing cd');
8
+
9
+ const mode = String(permissionMode ?? '').trim() || 'full-auto';
10
+ if (!CODEX_PERMISSION_MODES.includes(mode)) {
11
+ throw new Error(`[llm] codex: invalid permission mode: ${mode}`);
12
+ }
13
+
14
+ const args = ['exec', '--cd', cwd];
15
+ if (mode === 'safe') {
16
+ args.push('--sandbox', 'workspace-write', '--ask-for-approval', 'on-request');
17
+ } else if (mode === 'full-auto') {
18
+ args.push('--full-auto');
19
+ } else if (mode === 'yolo') {
20
+ args.push('--dangerously-bypass-approvals-and-sandbox');
21
+ }
22
+
23
+ // Read the prompt from stdin.
24
+ args.push('-');
25
+ return args;
26
+ }
27
+
28
+ function pickHereDocMarker(text) {
29
+ const s = String(text ?? '');
30
+ for (let i = 0; i < 50; i++) {
31
+ const marker = `HS_CODEX_PROMPT_${Math.random().toString(16).slice(2)}_${Date.now()}`;
32
+ if (!s.includes(marker)) return marker;
33
+ }
34
+ return `HS_CODEX_PROMPT_${Date.now()}`;
35
+ }
36
+
37
+ export function buildCodexExecScript({ cd, permissionMode, promptText }) {
38
+ const args = buildCodexExecArgs({ cd, permissionMode });
39
+ const marker = pickHereDocMarker(promptText);
40
+ const prompt = String(promptText ?? '').trimEnd();
41
+
42
+ // Use `command` to avoid shell aliases and ensure the expected binary is used.
43
+ const codexCmd = ['command', 'codex', ...args.map((a) => JSON.stringify(String(a)))].join(' ');
44
+
45
+ return [
46
+ '#!/usr/bin/env bash',
47
+ 'set -euo pipefail',
48
+ '',
49
+ `cat <<'${marker}' | ${codexCmd}`,
50
+ prompt,
51
+ marker,
52
+ '',
53
+ ].join('\n');
54
+ }
55
+
56
+ export async function runCodexExecHere({ cd, permissionMode, promptText, env }) {
57
+ const args = buildCodexExecArgs({ cd, permissionMode });
58
+ const input = String(promptText ?? '');
59
+ await run('codex', args, { cwd: cd, env: env ?? process.env, input });
60
+ }
61
+
@@ -0,0 +1,46 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { buildCodexExecArgs, buildCodexExecScript } from './codex_exec.mjs';
5
+
6
+ test('buildCodexExecArgs builds stdin-prompt args for each permission mode', () => {
7
+ const cd = '/tmp/repo';
8
+
9
+ const safe = buildCodexExecArgs({ cd, permissionMode: 'safe' });
10
+ assert.deepEqual(safe, ['exec', '--cd', cd, '--sandbox', 'workspace-write', '--ask-for-approval', 'on-request', '-']);
11
+
12
+ const full = buildCodexExecArgs({ cd, permissionMode: 'full-auto' });
13
+ assert.deepEqual(full, ['exec', '--cd', cd, '--full-auto', '-']);
14
+
15
+ const yolo = buildCodexExecArgs({ cd, permissionMode: 'yolo' });
16
+ assert.deepEqual(yolo, ['exec', '--cd', cd, '--dangerously-bypass-approvals-and-sandbox', '-']);
17
+ });
18
+
19
+ test('buildCodexExecScript embeds prompt via heredoc', () => {
20
+ const script = buildCodexExecScript({ cd: '/tmp/repo', permissionMode: 'full-auto', promptText: 'hello\nworld\n' });
21
+ assert.ok(script.includes('cat <<'));
22
+ assert.ok(script.includes('hello'));
23
+ assert.ok(script.includes('world'));
24
+ assert.ok(script.includes('codex'));
25
+ assert.ok(script.includes('exec'));
26
+ });
27
+
28
+ test('buildCodexExecArgs defaults to full-auto when permission mode is blank', () => {
29
+ const args = buildCodexExecArgs({ cd: '/tmp/repo', permissionMode: ' ' });
30
+ assert.deepEqual(args, ['exec', '--cd', '/tmp/repo', '--full-auto', '-']);
31
+ });
32
+
33
+ test('buildCodexExecArgs rejects missing cd and invalid permission modes', () => {
34
+ assert.throws(() => buildCodexExecArgs({ cd: '', permissionMode: 'safe' }), /missing cd/i);
35
+ assert.throws(() => buildCodexExecArgs({ cd: '/tmp/repo', permissionMode: 'unsafe' }), /invalid permission mode/i);
36
+ });
37
+
38
+ test('buildCodexExecScript preserves quoted prompt content in heredoc body', () => {
39
+ const prompt = `Line "one"\nLine 'two'\n$HOME`;
40
+ const script = buildCodexExecScript({ cd: '/tmp/repo', permissionMode: 'safe', promptText: prompt });
41
+ assert.match(script, /cat <<'HS_CODEX_PROMPT_/);
42
+ assert.match(script, /Line "one"/);
43
+ assert.match(script, /Line 'two'/);
44
+ assert.match(script, /\$HOME/);
45
+ assert.match(script, /"--ask-for-approval" "on-request"/);
46
+ });
@@ -0,0 +1,59 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ /**
6
+ * Returns an absolute path to this package's `bin/hstack.mjs` if present.
7
+ * This is the most reliable way to re-run hstack commands from an LLM prompt
8
+ * when `npx` is unreliable (e.g. npm cache permission issues).
9
+ */
10
+ export function resolveLocalhstackBinPath() {
11
+ try {
12
+ const here = dirname(fileURLToPath(import.meta.url)); // scripts/utils/llm
13
+ const root = resolve(here, '../../..'); // package root (contains bin/ and scripts/)
14
+ const p = join(root, 'bin', 'hstack.mjs');
15
+ return existsSync(p) ? p : '';
16
+ } catch {
17
+ return '';
18
+ }
19
+ }
20
+
21
+ export function buildhstackRunnerShellSnippet({ preferLocalBin = true } = {}) {
22
+ const localBin = preferLocalBin ? resolveLocalhstackBinPath() : '';
23
+ const localClause = localBin
24
+ ? [
25
+ `hstack_LOCAL_BIN=${JSON.stringify(localBin)}`,
26
+ ' if [ -f "$hstack_LOCAL_BIN" ]; then',
27
+ ' node "$hstack_LOCAL_BIN" "$@"',
28
+ ' return $?',
29
+ ' fi',
30
+ ].join('\n')
31
+ : '';
32
+
33
+ return [
34
+ 'hstack (Happier Stack) command runner:',
35
+ '- In the commands below, run `hstack ...`.',
36
+ '- This avoids `npx` flakiness by preferring a local `bin/hstack.mjs` when available.',
37
+ '',
38
+ '```bash',
39
+ 'hstack() {',
40
+ ' # Prefer an installed `hstack` if present.',
41
+ ' if command -v hstack >/dev/null 2>&1; then',
42
+ ' command hstack "$@"',
43
+ ' return $?',
44
+ ' fi',
45
+ localClause,
46
+ ' # Fallback: npx. Work around broken ~/.npm perms by using a fresh writable cache dir.',
47
+ ' if command -v npx >/dev/null 2>&1; then',
48
+ ' local cache_dir',
49
+ ' cache_dir="${HAPPIER_STACK_NPX_CACHE_DIR:-$(mktemp -d)}"',
50
+ ' npm_config_cache="$cache_dir" npm_config_update_notifier=false npx --yes -p @happier-dev/stack@latest hstack "$@"',
51
+ ' return $?',
52
+ ' fi',
53
+ ' echo "Missing hstack and npx. Install Node/npm or install @happier-dev/stack."',
54
+ ' return 1',
55
+ '}',
56
+ '```',
57
+ '',
58
+ ].join('\n');
59
+ }
@@ -0,0 +1,56 @@
1
+ import { commandExists } from '../proc/commands.mjs';
2
+
3
+ /**
4
+ * We keep this list intentionally small and capability-driven.
5
+ * The important distinction is whether we can run an agent with a pre-filled prompt reliably.
6
+ */
7
+ const KNOWN_LLM_TOOLS = [
8
+ {
9
+ id: 'codex',
10
+ cmd: 'codex',
11
+ label: 'Codex CLI',
12
+ note: 'Supports non-interactive runs with a prompt.',
13
+ supportsPromptStdin: true,
14
+ supportsAutoExec: true,
15
+ },
16
+ {
17
+ id: 'claude',
18
+ cmd: 'claude',
19
+ label: 'Claude CLI',
20
+ note: 'Supports starting interactive mode with an initial prompt.',
21
+ supportsPromptStdin: false,
22
+ supportsAutoExec: false,
23
+ },
24
+ {
25
+ id: 'opencode',
26
+ cmd: 'opencode',
27
+ label: 'OpenCode',
28
+ note: 'Supports starting TUI with an initial prompt.',
29
+ supportsPromptStdin: false,
30
+ supportsAutoExec: false,
31
+ },
32
+ {
33
+ id: 'aider',
34
+ cmd: 'aider',
35
+ label: 'Aider',
36
+ note: 'Prompt injection varies by mode; copy/paste fallback supported.',
37
+ supportsPromptStdin: false,
38
+ supportsAutoExec: false,
39
+ },
40
+ ];
41
+
42
+ export function getKnownLlmTools() {
43
+ return [...KNOWN_LLM_TOOLS];
44
+ }
45
+
46
+ export async function detectInstalledLlmTools({ onlyAutoExec = false } = {}) {
47
+ const installed = [];
48
+ for (const t of KNOWN_LLM_TOOLS) {
49
+ if (onlyAutoExec && !t.supportsAutoExec) continue;
50
+ // eslint-disable-next-line no-await-in-loop
51
+ const ok = await commandExists(t.cmd);
52
+ if (ok) installed.push(t);
53
+ }
54
+ return installed;
55
+ }
56
+
@@ -0,0 +1,67 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { detectInstalledLlmTools } from './tools.mjs';
8
+
9
+ async function writeStubCmd(binDir, name) {
10
+ const p = join(binDir, name);
11
+ await writeFile(p, ['#!/usr/bin/env bash', 'exit 0'].join('\n') + '\n', 'utf-8');
12
+ await chmod(p, 0o755);
13
+ return p;
14
+ }
15
+
16
+ function withPatchedPath(t, value) {
17
+ const prevPath = process.env.PATH;
18
+ t.after(() => {
19
+ process.env.PATH = prevPath;
20
+ });
21
+ process.env.PATH = value;
22
+ }
23
+
24
+ test('detectInstalledLlmTools finds tools on PATH and filters onlyAutoExec', async (t) => {
25
+ const root = await mkdtemp(join(tmpdir(), 'hs-llm-tools-'));
26
+ t.after(async () => {
27
+ await rm(root, { recursive: true, force: true });
28
+ });
29
+
30
+ const binDir = join(root, 'bin');
31
+ await mkdir(binDir, { recursive: true });
32
+
33
+ await writeStubCmd(binDir, 'codex');
34
+ await writeStubCmd(binDir, 'claude');
35
+
36
+ withPatchedPath(t, `${binDir}:${process.env.PATH ?? ''}`);
37
+
38
+ const all = await detectInstalledLlmTools();
39
+ assert.ok(all.some((t) => t.id === 'codex'));
40
+ assert.ok(all.some((t) => t.id === 'claude'));
41
+
42
+ const auto = await detectInstalledLlmTools({ onlyAutoExec: true });
43
+ assert.deepEqual(
44
+ auto.map((t) => t.id),
45
+ ['codex']
46
+ );
47
+ });
48
+
49
+ test('detectInstalledLlmTools returns empty list when PATH is blank', async (t) => {
50
+ withPatchedPath(t, '');
51
+ const all = await detectInstalledLlmTools();
52
+ assert.deepEqual(all, []);
53
+ });
54
+
55
+ test('detectInstalledLlmTools onlyAutoExec excludes non-auto tools even when present', async (t) => {
56
+ const root = await mkdtemp(join(tmpdir(), 'hs-llm-tools-'));
57
+ t.after(async () => {
58
+ await rm(root, { recursive: true, force: true });
59
+ });
60
+ const binDir = join(root, 'bin');
61
+ await mkdir(binDir, { recursive: true });
62
+ await writeStubCmd(binDir, 'claude');
63
+
64
+ withPatchedPath(t, binDir);
65
+ const auto = await detectInstalledLlmTools({ onlyAutoExec: true });
66
+ assert.deepEqual(auto, []);
67
+ });
@@ -0,0 +1,121 @@
1
+ import { runCapture } from '../proc/proc.mjs';
2
+ import { readdir, stat, unlink } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ function expandHomeQuick(p) {
7
+ const s = String(p ?? '').trim();
8
+ if (!s) return '';
9
+ if (s === '~') return homedir();
10
+ if (s.startsWith('~/')) return join(homedir(), s.slice(2));
11
+ return s;
12
+ }
13
+
14
+ async function isDir(p) {
15
+ const s = String(p ?? '').trim();
16
+ if (!s) return false;
17
+ try {
18
+ const st = await stat(s);
19
+ return st.isDirectory();
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ function globToRegExp(glob) {
26
+ // Minimal glob support for our SwiftBar plugin filenames:
27
+ // - `*` matches any chars
28
+ // - `?` matches a single char
29
+ const g = String(glob ?? '').trim();
30
+ if (!g) return null;
31
+ const esc = (ch) => String(ch).replace(/[\\^$.*+?()[\]{}|/]/g, '\\$&');
32
+ let out = '^';
33
+ for (const ch of g) {
34
+ if (ch === '*') out += '.*';
35
+ else if (ch === '?') out += '.';
36
+ else out += esc(ch);
37
+ }
38
+ out += '$';
39
+ return new RegExp(out);
40
+ }
41
+
42
+ function parseBool(raw) {
43
+ const v = String(raw ?? '').trim().toLowerCase();
44
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
45
+ }
46
+
47
+ function isPluginEntry(entry) {
48
+ return Boolean(entry?.isFile?.() || entry?.isSymbolicLink?.());
49
+ }
50
+
51
+ export async function resolveSwiftbarPluginsDir({ env = process.env } = {}) {
52
+ const override = (env.HAPPIER_STACK_SWIFTBAR_PLUGINS_DIR ?? '').trim();
53
+ if (override) {
54
+ const allowNonDarwinOverride = parseBool(env.HAPPIER_STACK_SWIFTBAR_ALLOW_OVERRIDE_NON_DARWIN);
55
+ if (process.platform !== 'darwin' && !allowNonDarwinOverride) {
56
+ return null;
57
+ }
58
+ const dir = expandHomeQuick(override);
59
+ return (await isDir(dir)) ? dir : null;
60
+ }
61
+
62
+ if (process.platform !== 'darwin') return null;
63
+ try {
64
+ const dir = (await runCapture('bash', [
65
+ '-lc',
66
+ 'DIR="$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null)"; if [[ -n "$DIR" && -d "$DIR" ]]; then echo "$DIR"; exit 0; fi; D="$HOME/Library/Application Support/SwiftBar/Plugins"; if [[ -d "$D" ]]; then echo "$D"; exit 0; fi; echo ""',
67
+ ])).trim();
68
+ return dir || null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ export async function detectSwiftbarPluginInstalled({ pluginsDir, patterns = null, env = process.env } = {}) {
75
+ const dir = pluginsDir ?? (await resolveSwiftbarPluginsDir({ env }));
76
+ if (!dir) return { pluginsDir: null, installed: false };
77
+
78
+ const pats = Array.isArray(patterns) && patterns.length ? patterns : ['hstack.*.sh'];
79
+ const regs = pats.map(globToRegExp).filter(Boolean);
80
+ if (regs.length === 0) return { pluginsDir: dir, installed: false };
81
+
82
+ try {
83
+ const entries = await readdir(dir, { withFileTypes: true });
84
+ for (const e of entries) {
85
+ if (!isPluginEntry(e)) continue;
86
+ if (regs.some((r) => r.test(e.name))) {
87
+ return { pluginsDir: dir, installed: true };
88
+ }
89
+ }
90
+ return { pluginsDir: dir, installed: false };
91
+ } catch {
92
+ return { pluginsDir: dir, installed: false };
93
+ }
94
+ }
95
+
96
+ export async function removeSwiftbarPlugins({ pluginsDir, patterns = null, env = process.env } = {}) {
97
+ const dir = pluginsDir ?? (await resolveSwiftbarPluginsDir({ env }));
98
+ if (!dir) return { ok: true, removed: false, pluginsDir: null };
99
+
100
+ const pats = Array.isArray(patterns) && patterns.length ? patterns : ['hstack.*.sh'];
101
+ const regs = pats.map(globToRegExp).filter(Boolean);
102
+ if (regs.length === 0) return { ok: true, removed: false, pluginsDir: dir };
103
+
104
+ try {
105
+ const entries = await readdir(dir, { withFileTypes: true });
106
+ let removed = false;
107
+ for (const e of entries) {
108
+ if (!isPluginEntry(e)) continue;
109
+ if (!regs.some((r) => r.test(e.name))) continue;
110
+ try {
111
+ await unlink(join(dir, e.name));
112
+ removed = true;
113
+ } catch {
114
+ // ignore per-file errors (best effort)
115
+ }
116
+ }
117
+ return { ok: true, removed, pluginsDir: dir };
118
+ } catch {
119
+ return { ok: false, removed: false, pluginsDir: dir };
120
+ }
121
+ }