@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,1327 @@
1
+ import './utils/env/env.mjs';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+
6
+ import { parseArgs } from './utils/cli/args.mjs';
7
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
8
+ import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
9
+ import { run, runCapture } from './utils/proc/proc.mjs';
10
+ import { banner, bullets, cmd as cmdFmt, kv as kvFmt, sectionTitle } from './utils/ui/layout.mjs';
11
+ import { bold, cyan, dim, green, yellow } from './utils/ui/ansi.mjs';
12
+ import { coerceHappyMonorepoRootFromPath, getComponentRepoDir, getRootDir, getWorkspaceDir, isHappyMonorepoRoot, resolveStackEnvPath } from './utils/paths/paths.mjs';
13
+ import { ensureEnvFileUpdated } from './utils/env/env_file.mjs';
14
+ import { listAllStackNames, stackExistsSync } from './utils/stack/stacks.mjs';
15
+ import { sanitizeStackName } from './utils/stack/names.mjs';
16
+ import { sanitizeSlugPart } from './utils/git/refs.mjs';
17
+ import { readEnvObjectFromFile } from './utils/env/read.mjs';
18
+ import { clipboardAvailable, copyTextToClipboard } from './utils/ui/clipboard.mjs';
19
+ import { detectInstalledLlmTools } from './utils/llm/tools.mjs';
20
+ import { launchLlmAssistant } from './utils/llm/assist.mjs';
21
+ import { buildhstackRunnerShellSnippet } from './utils/llm/hstack_runner.mjs';
22
+
23
+ function usage() {
24
+ return [
25
+ '[import] usage:',
26
+ ' hstack import',
27
+ ' hstack import inspect [--happy=<path|url>] [--happy-cli=<path|url>] [--happy-server=<path|url>] [--happy-server-light=<path|url>] [--yes] [--json]',
28
+ ' hstack import apply --stack=<name> [--server=happy-server|happy-server-light] [--happy=<path|url>] [--happy-ref=<ref>] [--happy-cli=<path|url>] [--happy-cli-ref=<ref>] [--happy-server=<path|url>] [--happy-server-ref=<ref>] [--happy-server-light=<path|url>] [--happy-server-light-ref=<ref>] [--yes] [--json]',
29
+ ' hstack import migrate [--stack=<name>]',
30
+ ' hstack import llm [--mode=import|migrate] [--stack=<name>] [--copy] [--launch]',
31
+ ' hstack import [--json]',
32
+ '',
33
+ 'What it does:',
34
+ '- imports legacy split repos (happy / happy-cli / happy-server) into hstack by pinning stack component paths',
35
+ '- optionally ports commits into the Happier monorepo layout via `hstack monorepo port`',
36
+ '',
37
+ 'Notes:',
38
+ '- This is for users who still have split repos/branches/PRs (pre-monorepo).',
39
+ '- Migration uses `git format-patch` + `git am` and may require conflict resolution.',
40
+ ].join('\n');
41
+ }
42
+
43
+ async function gitRoot(dir) {
44
+ const d = resolve(String(dir ?? '').trim());
45
+ if (!d) return '';
46
+ try {
47
+ return (await runCapture('git', ['rev-parse', '--show-toplevel'], { cwd: d })).trim();
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ async function gitBranch(dir) {
54
+ try {
55
+ const b = (await runCapture('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir })).trim();
56
+ return b && b !== 'HEAD' ? b : 'detached';
57
+ } catch {
58
+ return 'unknown';
59
+ }
60
+ }
61
+
62
+ async function gitDirty(dir) {
63
+ try {
64
+ return Boolean((await runCapture('git', ['status', '--porcelain'], { cwd: dir })).trim());
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async function gitOk(cwd, args) {
71
+ try {
72
+ await runCapture('git', args, { cwd });
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ async function listGitWorktrees(repoRoot) {
80
+ // `git worktree list --porcelain` includes the current worktree plus any additional worktrees.
81
+ const out = await runCapture('git', ['worktree', 'list', '--porcelain'], { cwd: repoRoot });
82
+ const lines = out.split(/\r?\n/);
83
+ const entries = [];
84
+ let cur = null;
85
+ for (const line of lines) {
86
+ if (!line.trim()) continue;
87
+ if (line.startsWith('worktree ')) {
88
+ if (cur) entries.push(cur);
89
+ cur = { path: line.slice('worktree '.length).trim(), branch: '', head: '' };
90
+ continue;
91
+ }
92
+ if (!cur) continue;
93
+ if (line.startsWith('branch ')) {
94
+ const ref = line.slice('branch '.length).trim();
95
+ cur.branch = ref.replace(/^refs\/heads\//, '').replace(/^refs\/remotes\//, '');
96
+ continue;
97
+ }
98
+ if (line.startsWith('HEAD ')) {
99
+ cur.head = line.slice('HEAD '.length).trim();
100
+ continue;
101
+ }
102
+ if (line.startsWith('detached')) {
103
+ cur.branch = 'detached';
104
+ continue;
105
+ }
106
+ }
107
+ if (cur) entries.push(cur);
108
+
109
+ // Normalize: ensure current worktree comes first and paths are absolute.
110
+ const normalized = entries
111
+ .map((e) => ({ ...e, path: resolve(e.path) }))
112
+ .filter((e) => e.path);
113
+ return normalized;
114
+ }
115
+
116
+ async function listLocalBranches(repoRoot) {
117
+ try {
118
+ const out = await runCapture('git', ['for-each-ref', 'refs/heads', '--format=%(refname:short)'], { cwd: repoRoot });
119
+ return out
120
+ .split(/\r?\n/)
121
+ .map((s) => s.trim())
122
+ .filter(Boolean)
123
+ .sort();
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ function looksLikeGitUrl(raw) {
130
+ const s = String(raw ?? '').trim();
131
+ if (!s) return false;
132
+ if (s.startsWith('git@')) return true;
133
+ if (s.startsWith('https://') || s.startsWith('http://')) return true;
134
+ if (s.endsWith('.git')) return true;
135
+ return false;
136
+ }
137
+
138
+ function repoNameFromGitUrl(raw) {
139
+ const s = String(raw ?? '').trim();
140
+ if (!s) return 'repo';
141
+ // Examples:
142
+ // - https://github.com/org/name.git
143
+ // - git@github.com:org/name.git
144
+ const m = s.match(/[:/]+([^/]+)\/([^/]+?)(?:\.git)?$/);
145
+ if (m?.[2]) return m[2];
146
+ const tail = s.split('/').filter(Boolean).pop() ?? 'repo';
147
+ return tail.replace(/\.git$/, '') || 'repo';
148
+ }
149
+
150
+ async function resolveRepoRootFromPathOrUrl({ rootDir, label, raw, rl }) {
151
+ const input = String(raw ?? '').trim();
152
+ if (!input) return '';
153
+
154
+ if (!looksLikeGitUrl(input)) {
155
+ const r = await gitRoot(input);
156
+ if (!r) throw new Error(`[import] ${label}: not a git repo: ${input}`);
157
+ return r;
158
+ }
159
+
160
+ // Git URL: clone into the hstack workspace so it can be pinned reliably.
161
+ const workspaceDir = getWorkspaceDir(rootDir);
162
+ const repoName = repoNameFromGitUrl(input);
163
+ const targetDir = join(workspaceDir, 'imports', 'repos', label, sanitizeSlugPart(repoName));
164
+
165
+ if (existsSync(join(targetDir, '.git'))) {
166
+ const reuse = await promptSelect(rl, {
167
+ title: `${bold(label)}\n${dim(`Repo already cloned at ${targetDir}.`)}`,
168
+ options: [
169
+ { label: `reuse existing clone (default)`, value: 'reuse' },
170
+ { label: `fetch latest (${dim('git fetch --all')})`, value: 'fetch' },
171
+ ],
172
+ defaultIndex: 0,
173
+ });
174
+ if (reuse === 'fetch') {
175
+ await run('git', ['fetch', '--all', '--prune'], { cwd: targetDir }).catch(() => {});
176
+ }
177
+ const r = await gitRoot(targetDir);
178
+ if (!r) throw new Error(`[import] ${label}: expected git repo at ${targetDir} (missing)`);
179
+ return r;
180
+ }
181
+
182
+ await mkdir(join(targetDir, '..'), { recursive: true });
183
+ // eslint-disable-next-line no-console
184
+ console.log(dim(`Cloning ${label}: ${input} -> ${targetDir}`));
185
+ await run('git', ['clone', input, targetDir], { cwd: workspaceDir });
186
+ const r = await gitRoot(targetDir);
187
+ if (!r) throw new Error(`[import] ${label}: clone succeeded but repo root not found: ${targetDir}`);
188
+ return r;
189
+ }
190
+
191
+ async function resolveRepoRootFromPathOrUrlNonInteractive({ rootDir, label, raw, yes, fetch }) {
192
+ const input = String(raw ?? '').trim();
193
+ if (!input) return '';
194
+
195
+ if (!looksLikeGitUrl(input)) {
196
+ const r = await gitRoot(input);
197
+ if (!r) throw new Error(`[import] ${label}: not a git repo: ${input}`);
198
+ return r;
199
+ }
200
+
201
+ if (!yes) {
202
+ throw new Error(
203
+ `[import] ${label}: got a git URL but non-interactive mode cannot prompt.\n` +
204
+ `[import] re-run with --yes to allow cloning into the hstack workspace.\n` +
205
+ `[import] url: ${input}`
206
+ );
207
+ }
208
+
209
+ const workspaceDir = getWorkspaceDir(rootDir);
210
+ const repoName = repoNameFromGitUrl(input);
211
+ const targetDir = join(workspaceDir, 'imports', 'repos', label, sanitizeSlugPart(repoName));
212
+
213
+ if (existsSync(join(targetDir, '.git'))) {
214
+ if (fetch) {
215
+ await run('git', ['fetch', '--all', '--prune'], { cwd: targetDir }).catch(() => {});
216
+ }
217
+ const r = await gitRoot(targetDir);
218
+ if (!r) throw new Error(`[import] ${label}: expected git repo at ${targetDir} (missing)`);
219
+ return r;
220
+ }
221
+
222
+ await mkdir(join(targetDir, '..'), { recursive: true });
223
+ // eslint-disable-next-line no-console
224
+ console.log(dim(`Cloning ${label}: ${input} -> ${targetDir}`));
225
+ await run('git', ['clone', input, targetDir], { cwd: workspaceDir });
226
+ const r = await gitRoot(targetDir);
227
+ if (!r) throw new Error(`[import] ${label}: clone succeeded but repo root not found: ${targetDir}`);
228
+ return r;
229
+ }
230
+
231
+ async function ensureWorktreeForRef({ rootDir, componentLabel, repoRoot, ref }) {
232
+ const r = String(ref ?? '').trim();
233
+ if (!r) return '';
234
+
235
+ const workspaceDir = getWorkspaceDir(rootDir);
236
+ const safeRef = sanitizeSlugPart(r);
237
+ const componentSlug = String(componentLabel ?? '')
238
+ .trim()
239
+ .toLowerCase()
240
+ .replace(/[^a-z0-9]+/g, '-');
241
+ const targetDir = join(workspaceDir, 'imports', 'worktrees', componentSlug, safeRef);
242
+
243
+ await mkdir(join(targetDir, '..'), { recursive: true });
244
+ if (existsSync(targetDir)) return targetDir;
245
+
246
+ if (!(await gitOk(repoRoot, ['rev-parse', '--verify', '--quiet', r]))) {
247
+ await run('git', ['fetch', '--all', '--prune'], { cwd: repoRoot }).catch(() => {});
248
+ }
249
+
250
+ // eslint-disable-next-line no-console
251
+ console.log(dim(`Creating worktree: ${repoRoot} -> ${targetDir} (${r})`));
252
+
253
+ // Important:
254
+ // A normal clone has a branch checked out in its "main worktree" already.
255
+ // `git worktree add <dir> <branch>` fails if `<branch>` is currently checked out anywhere.
256
+ //
257
+ // To make `hstack import apply` robust for typical contributor setups,
258
+ // create a dedicated, uniquely named branch under the source repo when the ref is a local branch.
259
+ const isLocalBranch = await gitOk(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${r}`]);
260
+ if (isLocalBranch) {
261
+ const importPrefix = `hs-import/${componentSlug}/${safeRef}`;
262
+ let importBranch = importPrefix;
263
+ let i = 0;
264
+ // eslint-disable-next-line no-constant-condition
265
+ while (true) {
266
+ // eslint-disable-next-line no-await-in-loop
267
+ const exists = await gitOk(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${importBranch}`]);
268
+ if (!exists) break;
269
+ i += 1;
270
+ importBranch = `${importPrefix}-${i}`;
271
+ if (i > 50) {
272
+ throw new Error(`[import] could not find a free import branch name for ${r}`);
273
+ }
274
+ }
275
+ await run('git', ['worktree', 'add', '-b', importBranch, targetDir, r], { cwd: repoRoot });
276
+ } else {
277
+ // Commit SHA / tag / remote ref: keep it detached to avoid consuming/locking branches.
278
+ await run('git', ['worktree', 'add', '--detach', targetDir, r], { cwd: repoRoot });
279
+ }
280
+ return targetDir;
281
+ }
282
+
283
+ async function resolveDefaultTargetBaseRef(repoRoot) {
284
+ // Prefer refs/remotes/origin/HEAD when available.
285
+ try {
286
+ const sym = (await runCapture('git', ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { cwd: repoRoot })).trim();
287
+ const m = /^refs\/remotes\/origin\/(.+)$/.exec(sym);
288
+ if (m?.[1]) {
289
+ const ref = `origin/${m[1]}`;
290
+ if (await gitOk(repoRoot, ['rev-parse', '--verify', '--quiet', ref])) {
291
+ return ref;
292
+ }
293
+ }
294
+ } catch {
295
+ // ignore
296
+ }
297
+
298
+ // Fallback candidates.
299
+ for (const c of ['upstream/main', 'origin/main', 'main', 'master']) {
300
+ // eslint-disable-next-line no-await-in-loop
301
+ if (await gitOk(repoRoot, ['rev-parse', '--verify', '--quiet', c])) return c;
302
+ }
303
+ return '';
304
+ }
305
+
306
+ async function createMonorepoPortWorktree({ rootDir, monorepoRepoRoot, slug, baseRef }) {
307
+ const workspaceDir = getWorkspaceDir(rootDir);
308
+ const safe = sanitizeSlugPart(slug || 'port');
309
+ const dir = join(workspaceDir, 'imports', 'monorepo-worktrees', safe);
310
+
311
+ await mkdir(join(dir, '..'), { recursive: true });
312
+ if (existsSync(dir)) {
313
+ throw new Error(
314
+ `[import] monorepo worktree path already exists: ${dir}\n` +
315
+ `[import] fix: delete it, or pick a different port branch/slug`
316
+ );
317
+ }
318
+
319
+ const ref = String(baseRef ?? '').trim() || (await resolveDefaultTargetBaseRef(monorepoRepoRoot)) || 'main';
320
+ // eslint-disable-next-line no-console
321
+ console.log(dim(`Creating monorepo worktree: ${monorepoRepoRoot} -> ${dir} (${ref})`));
322
+ await run('git', ['worktree', 'add', dir, ref], { cwd: monorepoRepoRoot });
323
+ return dir;
324
+ }
325
+
326
+ async function chooseCheckoutPathForRepo({ rl, rootDir, componentLabel, repoRoot, repoHintLabel }) {
327
+ const worktrees = await listGitWorktrees(repoRoot);
328
+ const branches = await listLocalBranches(repoRoot);
329
+
330
+ const currentBranch = await gitBranch(repoRoot);
331
+ const currentDirty = await gitDirty(repoRoot);
332
+ const current = worktrees.find((w) => resolve(w.path) === resolve(repoRoot));
333
+
334
+ const options = [];
335
+ if (current) {
336
+ options.push({
337
+ value: { kind: 'path', path: current.path },
338
+ label: `${cyan('current')} — ${dim(current.branch || currentBranch)}${currentDirty ? ` ${yellow('(dirty)')}` : ''} ${dim(current.path)}`,
339
+ });
340
+ } else {
341
+ options.push({
342
+ value: { kind: 'path', path: repoRoot },
343
+ label: `${cyan('current')} — ${dim(currentBranch)}${currentDirty ? ` ${yellow('(dirty)')}` : ''} ${dim(repoRoot)}`,
344
+ });
345
+ }
346
+
347
+ const others = worktrees.filter((w) => resolve(w.path) !== resolve(repoRoot));
348
+ for (const w of others.slice(0, 25)) {
349
+ options.push({
350
+ value: { kind: 'path', path: w.path },
351
+ label: `${cyan('worktree')} — ${dim(w.branch || 'detached')} ${dim(w.path)}`,
352
+ });
353
+ }
354
+ if (branches.length) {
355
+ options.push({ value: { kind: 'branch' }, label: `${cyan('other branch')} — create a new worktree under your hstack workspace` });
356
+ }
357
+
358
+ const picked = await promptSelect(rl, {
359
+ title:
360
+ `${bold(componentLabel)}\n` +
361
+ `${dim(`Pick which checkout to import from this repo${repoHintLabel ? ` (${repoHintLabel})` : ''}.`)}`,
362
+ options,
363
+ defaultIndex: 0,
364
+ });
365
+
366
+ if (picked?.kind === 'path') {
367
+ return { path: picked.path, branch: await gitBranch(picked.path) };
368
+ }
369
+
370
+ // Branch -> create worktree under workspace (recommended).
371
+ const branch = await promptSelect(rl, {
372
+ title: `${bold(componentLabel)}\n${dim('Pick a branch to import (we will create a dedicated worktree for it).')}`,
373
+ options: branches.slice(0, 80).map((b) => ({ label: b, value: b })),
374
+ defaultIndex: 0,
375
+ });
376
+ const workspaceDir = getWorkspaceDir(rootDir);
377
+ const safe = sanitizeSlugPart(String(branch ?? 'branch'));
378
+ const componentSlug = componentLabel.toLowerCase().replace(/[^a-z0-9]+/g, '-');
379
+ const targetDir = join(workspaceDir, 'imports', 'worktrees', componentSlug, safe);
380
+
381
+ // eslint-disable-next-line no-console
382
+ console.log(dim(`Creating worktree: ${repoRoot} -> ${targetDir} (${branch})`));
383
+
384
+ // Create only the parent directory; git worktree add expects the target dir to NOT exist.
385
+ await mkdir(join(targetDir, '..'), { recursive: true });
386
+ if (existsSync(targetDir)) {
387
+ throw new Error(`[import] worktree path already exists: ${targetDir}\n[import] fix: delete it or pick a different branch name/slug`);
388
+ }
389
+
390
+ // Same reasoning as ensureWorktreeForRef(): avoid trying to check out the exact same branch in 2 worktrees.
391
+ const importPrefix = `hs-import/${componentSlug}/${safe}`;
392
+ let importBranch = importPrefix;
393
+ let i = 0;
394
+ // eslint-disable-next-line no-constant-condition
395
+ while (true) {
396
+ // eslint-disable-next-line no-await-in-loop
397
+ const exists = await gitOk(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${importBranch}`]);
398
+ if (!exists) break;
399
+ i += 1;
400
+ importBranch = `${importPrefix}-${i}`;
401
+ if (i > 50) throw new Error(`[import] could not find a free import branch name for ${branch}`);
402
+ }
403
+ await run('git', ['worktree', 'add', '-b', importBranch, targetDir, branch], { cwd: repoRoot });
404
+
405
+ return { path: targetDir, branch: importBranch };
406
+ }
407
+
408
+ async function ensureStackExists({ rootDir, stackName, serverComponent }) {
409
+ const name = sanitizeStackName(stackName);
410
+ if (!name) throw new Error('[import] invalid stack name');
411
+ if (stackExistsSync(name)) return name;
412
+ await run(process.execPath, [join(rootDir, 'scripts/stack.mjs'), 'new', name, `--server=${serverComponent}`], { cwd: rootDir });
413
+ return name;
414
+ }
415
+
416
+ async function pinStackComponentDirs({ stackName, pins }) {
417
+ const envPath = resolveStackEnvPath(stackName).envPath;
418
+ const roots = new Set();
419
+ for (const [, path] of Object.entries(pins)) {
420
+ const p = String(path ?? '').trim();
421
+ if (!p) continue;
422
+ const root = coerceHappyMonorepoRootFromPath(p) ?? p;
423
+ roots.add(root);
424
+ }
425
+ const unique = Array.from(roots).filter(Boolean);
426
+ if (!unique.length) return envPath;
427
+ if (unique.length > 1) {
428
+ throw new Error(
429
+ `[import] multiple repo roots detected; hstack is monorepo-only.\n` +
430
+ unique.map((r) => `- ${r}`).join('\n') +
431
+ `\nFix: pass paths/URLs that all resolve to the same Happier monorepo checkout/worktree.`
432
+ );
433
+ }
434
+ await ensureEnvFileUpdated({ envPath, updates: [{ key: 'HAPPIER_STACK_REPO_DIR', value: unique[0] }] });
435
+ return envPath;
436
+ }
437
+
438
+ async function resolveDefaultMonorepoRoot({ rootDir }) {
439
+ const repoDir = getComponentRepoDir(rootDir, 'happy');
440
+ if (repoDir && existsSync(repoDir) && isHappyMonorepoRoot(repoDir)) return repoDir;
441
+ return '';
442
+ }
443
+
444
+ async function runMonorepoPort({ rootDir, targetMonorepoRoot, sources, branch, dryRun }) {
445
+ // Use guided mode only when we expect conflicts (or when the user wants it).
446
+ const args = [
447
+ 'port',
448
+ ...(dryRun ? [] : ['guide']),
449
+ `--target=${targetMonorepoRoot}`,
450
+ `--branch=${branch}`,
451
+ '--3way',
452
+ ...(dryRun ? ['--dry-run'] : []),
453
+ ];
454
+ if (sources.happy) args.push(`--from-happy=${sources.happy}`);
455
+ if (sources['happy-cli']) args.push(`--from-happy-cli=${sources['happy-cli']}`);
456
+ if (sources['happy-server']) args.push(`--from-happy-server=${sources['happy-server']}`);
457
+ await run(process.execPath, [join(rootDir, 'scripts/monorepo.mjs'), ...args], { cwd: rootDir });
458
+ }
459
+
460
+ async function runMonorepoPortRun({ rootDir, targetMonorepoRoot, sources, branch }) {
461
+ const args = ['port', `--target=${targetMonorepoRoot}`, `--branch=${branch}`, '--3way', '--json'];
462
+ if (sources.happy) args.push(`--from-happy=${sources.happy}`);
463
+ if (sources['happy-cli']) args.push(`--from-happy-cli=${sources['happy-cli']}`);
464
+ if (sources['happy-server']) args.push(`--from-happy-server=${sources['happy-server']}`);
465
+ const out = await runCapture(process.execPath, [join(rootDir, 'scripts/monorepo.mjs'), ...args], { cwd: rootDir });
466
+ return JSON.parse(String(out ?? '').trim() || '{}');
467
+ }
468
+
469
+ async function runMonorepoPortPreflight({ rootDir, targetMonorepoRoot, sources }) {
470
+ const args = ['port', 'preflight', `--target=${targetMonorepoRoot}`, '--3way', '--json'];
471
+ if (sources.happy) args.push(`--from-happy=${sources.happy}`);
472
+ if (sources['happy-cli']) args.push(`--from-happy-cli=${sources['happy-cli']}`);
473
+ if (sources['happy-server']) args.push(`--from-happy-server=${sources['happy-server']}`);
474
+ const out = await runCapture(process.execPath, [join(rootDir, 'scripts/monorepo.mjs'), ...args], { cwd: rootDir });
475
+ return JSON.parse(String(out ?? '').trim() || '{}');
476
+ }
477
+
478
+ function summarizePreflightFailures(preflight) {
479
+ const results = Array.isArray(preflight?.results) ? preflight.results : [];
480
+ const lines = [];
481
+ for (const r of results) {
482
+ const failed = r?.report?.failed ?? [];
483
+ if (!Array.isArray(failed) || failed.length === 0) continue;
484
+ const label = String(r.label ?? '').trim() || 'source';
485
+ lines.push(`- ${cyan(label)}: ${failed.length} failed patch(es)`);
486
+ for (const f of failed.slice(0, 5)) {
487
+ const subj = String(f.subject ?? '').replace(/^\[PATCH \d+\/\d+\]\s*/, '');
488
+ const kind = f.kind ? ` (${f.kind})` : '';
489
+ const paths = (f.paths ?? []).slice(0, 3).join(', ');
490
+ lines.push(` - ${subj || f.patch}${kind}${paths ? ` → ${paths}` : ''}`);
491
+ }
492
+ if (failed.length > 5) lines.push(` - ...and ${failed.length - 5} more`);
493
+ }
494
+ return lines;
495
+ }
496
+
497
+ function summarizePins(pins) {
498
+ const lines = [];
499
+ for (const [k, v] of Object.entries(pins)) {
500
+ if (!v) continue;
501
+ lines.push(`- ${dim(k)}: ${v}`);
502
+ }
503
+ return lines;
504
+ }
505
+
506
+ function readPinnedComponentDirFromEnvObject(envObj, component) {
507
+ void component;
508
+ const raw = (envObj?.HAPPIER_STACK_REPO_DIR ?? '').toString().trim();
509
+ return raw || '';
510
+ }
511
+
512
+ function buildLlmPromptForImport() {
513
+ const hs = buildhstackRunnerShellSnippet();
514
+ return [
515
+ 'You are an assistant helping the user migrate legacy Happy split repos into hstack.',
516
+ '',
517
+ hs,
518
+ 'Goals:',
519
+ '- Import legacy split repos (happy / happy-cli / happy-server) into a stack in hstack.',
520
+ '- Optionally migrate commits into the Happier monorepo layout (packages/happy-* or legacy expo-app/cli/server).',
521
+ '',
522
+ 'How to proceed:',
523
+ '1) Run the guided import wizard:',
524
+ ' - `hs import`',
525
+ '',
526
+ 'Non-interactive (LLM-friendly) variant:',
527
+ '- Inspect candidate repos/worktrees/branches (JSON):',
528
+ ' - `hs import inspect --happy=<path|url> --happy-cli=<path|url> --happy-server=<path|url> --yes --json`',
529
+ '- Apply pins to a stack (no prompts):',
530
+ ' - `hs import apply --stack=<name> --server=happy-server-light --happy=<path|url> --happy-ref=<ref> --happy-cli=<path|url> --happy-cli-ref=<ref> --happy-server=<path|url> --happy-server-ref=<ref> --yes`',
531
+ '2) If you want to migrate an existing imported stack later:',
532
+ ' - `hs import migrate --stack=<stack>`',
533
+ '',
534
+ 'Conflict handling (monorepo port):',
535
+ '- Prefer guided mode: `hs monorepo port guide --target=<monorepo-root>`',
536
+ '- For machine-readable state, use:',
537
+ ' - `hs monorepo port status --target=<monorepo-root> --json`',
538
+ ' - `hs monorepo port continue --target=<monorepo-root>`',
539
+ '',
540
+ 'Important:',
541
+ '- A “stack” is an isolated runtime (ports + data + env) under ~/.happy/stacks/<name>.',
542
+ '- Import pins stack component paths (it does not rewrite history).',
543
+ '- Migration uses git format-patch + git am and may require resolving conflicts.',
544
+ ].join('\n');
545
+ }
546
+
547
+ function buildLlmPromptForMigrate({ stackName }) {
548
+ const hs = buildhstackRunnerShellSnippet();
549
+ return [
550
+ 'You are an assistant helping the user migrate an existing hstack stack to the monorepo.',
551
+ '',
552
+ hs,
553
+ `Target stack: ${stackName || '<stack>'}`,
554
+ '',
555
+ 'Goal:',
556
+ '- Port the stack’s pinned split-repo commits into a monorepo worktree (Happier layout).',
557
+ '- Create a new monorepo stack by default (keep the legacy stack intact).',
558
+ '',
559
+ 'Command:',
560
+ `- hs import migrate --stack=${stackName || '<stack>'}`,
561
+ '',
562
+ 'Conflict handling:',
563
+ '- This uses `hs monorepo port guide` which pauses on conflicts.',
564
+ '- To inspect machine-readably: `hs monorepo port status --target=<monorepo-root> --json`',
565
+ ].join('\n');
566
+ }
567
+
568
+ function buildMonorepoMigrationPrompt({ targetMonorepoRoot, branch, sources }) {
569
+ const args = [
570
+ `hs monorepo port --target=${targetMonorepoRoot} --branch=${branch} --3way`,
571
+ sources.happy ? `--from-happy=${sources.happy}` : '',
572
+ sources['happy-cli'] ? `--from-happy-cli=${sources['happy-cli']}` : '',
573
+ sources['happy-server'] ? `--from-happy-server=${sources['happy-server']}` : '',
574
+ ]
575
+ .filter(Boolean)
576
+ .join(' \\\n+ ');
577
+
578
+ return [
579
+ 'You are an assistant helping the user migrate split-repo commits into the Happy monorepo layout.',
580
+ '',
581
+ buildhstackRunnerShellSnippet(),
582
+ `Target monorepo worktree: ${targetMonorepoRoot}`,
583
+ `Port branch: ${branch}`,
584
+ '',
585
+ 'Goal:',
586
+ '- Run the port command.',
587
+ '- If conflicts occur, resolve them cleanly and continue until complete.',
588
+ '',
589
+ 'Start the port:',
590
+ args,
591
+ '',
592
+ 'If it stops with conflicts:',
593
+ `- Inspect: hs monorepo port status --target=${targetMonorepoRoot} --json`,
594
+ `- Resolve conflicted files (keep changes scoped to packages/happy-*/ or legacy expo-app/, cli/, server/)`,
595
+ `- Stage: git -C ${targetMonorepoRoot} add <files>`,
596
+ `- Continue: hs monorepo port continue --target=${targetMonorepoRoot}`,
597
+ '',
598
+ 'Repeat status/resolve/continue until ok.',
599
+ ].join('\n');
600
+ }
601
+
602
+ async function cmdLlm({ argv }) {
603
+ const { flags, kv } = parseArgs(argv);
604
+ const json = wantsJson(argv, { flags });
605
+ const mode = (kv.get('--mode') ?? '').trim().toLowerCase() || 'import';
606
+ const stackName = sanitizeStackName((kv.get('--stack') ?? '').trim());
607
+ const promptText = mode === 'migrate' ? buildLlmPromptForMigrate({ stackName }) : buildLlmPromptForImport();
608
+ const tools = await detectInstalledLlmTools();
609
+
610
+ if (json) {
611
+ printResult({ json, data: { mode, stack: stackName || null, prompt: promptText, detectedTools: tools.map((t) => t.id) } });
612
+ return;
613
+ }
614
+
615
+ const wantsLaunch = flags.has('--launch');
616
+ if (wantsLaunch) {
617
+ const launched = await launchLlmAssistant({
618
+ title: 'hstack import/migrate (LLM)',
619
+ subtitle: 'Guides import and/or runs the monorepo port + conflict resolution.',
620
+ promptText,
621
+ cwd: rootDir,
622
+ env: process.env,
623
+ allowRunHere: true,
624
+ allowCopyOnly: true,
625
+ });
626
+ if (launched.ok && launched.launched) return;
627
+ if (!launched.ok) {
628
+ // eslint-disable-next-line no-console
629
+ console.log(dim(`[import] LLM launch unavailable: ${launched.reason || 'unknown'}`));
630
+ }
631
+ // fall through to printing the prompt
632
+ }
633
+
634
+ // eslint-disable-next-line no-console
635
+ console.log('');
636
+ // eslint-disable-next-line no-console
637
+ console.log(banner('LLM prompt', { subtitle: 'Copy-paste this into your LLM to drive import/migration.' }));
638
+ // eslint-disable-next-line no-console
639
+ console.log(promptText);
640
+ if (tools.length) {
641
+ // eslint-disable-next-line no-console
642
+ console.log('');
643
+ // eslint-disable-next-line no-console
644
+ console.log(sectionTitle('Detected LLM CLIs'));
645
+ // eslint-disable-next-line no-console
646
+ console.log(
647
+ bullets(tools.map((t) => `- ${dim(t.id)}: ${t.label}${t.note ? ` ${dim(`— ${t.note}`)}` : ''}`))
648
+ );
649
+ }
650
+
651
+ const wantsCopy = flags.has('--copy');
652
+ if (wantsCopy && (await clipboardAvailable())) {
653
+ const res = await copyTextToClipboard(promptText);
654
+ // eslint-disable-next-line no-console
655
+ console.log(res.ok ? green('✓ Copied to clipboard') : dim(`(Clipboard copy failed: ${res.reason || 'unknown'})`));
656
+ } else if (wantsCopy) {
657
+ // eslint-disable-next-line no-console
658
+ console.log(dim('(Clipboard copy unavailable on this system)'));
659
+ }
660
+ }
661
+
662
+ async function cmdInspect({ rootDir, argv }) {
663
+ const { flags, kv } = parseArgs(argv);
664
+ const json = wantsJson(argv, { flags });
665
+ const yes = flags.has('--yes');
666
+ const fetch = flags.has('--fetch');
667
+
668
+ const inputs = {
669
+ happy: (kv.get('--happy') ?? '').toString().trim(),
670
+ 'happy-cli': (kv.get('--happy-cli') ?? '').toString().trim(),
671
+ 'happy-server': (kv.get('--happy-server') ?? '').toString().trim(),
672
+ 'happy-server-light': (kv.get('--happy-server-light') ?? '').toString().trim(),
673
+ };
674
+
675
+ const repos = {};
676
+ for (const [label, raw] of Object.entries(inputs)) {
677
+ if (!raw) continue;
678
+ // eslint-disable-next-line no-await-in-loop
679
+ const repoRoot = await resolveRepoRootFromPathOrUrlNonInteractive({ rootDir, label, raw, yes, fetch });
680
+ // eslint-disable-next-line no-await-in-loop
681
+ const branch = await gitBranch(repoRoot);
682
+ // eslint-disable-next-line no-await-in-loop
683
+ const dirty = await gitDirty(repoRoot);
684
+ // eslint-disable-next-line no-await-in-loop
685
+ const worktrees = await listGitWorktrees(repoRoot);
686
+ // eslint-disable-next-line no-await-in-loop
687
+ const branches = await listLocalBranches(repoRoot);
688
+ repos[label] = { input: raw, repoRoot, branch, dirty, worktrees, branches };
689
+ }
690
+
691
+ printResult({ json, data: { repos } });
692
+ }
693
+
694
+ async function cmdApply({ rootDir, argv }) {
695
+ const { flags, kv } = parseArgs(argv);
696
+ const json = wantsJson(argv, { flags });
697
+ const yes = flags.has('--yes');
698
+ const fetch = flags.has('--fetch');
699
+
700
+ const positionals = argv.filter((a) => !a.startsWith('--'));
701
+ const stackFromPos = positionals[1] || '';
702
+ const stackName = sanitizeStackName(((kv.get('--stack') ?? stackFromPos) || '').toString().trim());
703
+ if (!stackName) throw new Error('[import] apply: missing --stack=<name>');
704
+
705
+ const serverComponent = String(kv.get('--server') ?? 'happy-server-light').trim() || 'happy-server-light';
706
+ if (!['happy-server', 'happy-server-light'].includes(serverComponent)) {
707
+ throw new Error(`[import] apply: invalid --server=${serverComponent} (expected happy-server or happy-server-light)`);
708
+ }
709
+
710
+ const spec = {
711
+ happy: { raw: String(kv.get('--happy') ?? '').trim(), ref: String(kv.get('--happy-ref') ?? '').trim() },
712
+ 'happy-cli': { raw: String(kv.get('--happy-cli') ?? '').trim(), ref: String(kv.get('--happy-cli-ref') ?? '').trim() },
713
+ 'happy-server': { raw: String(kv.get('--happy-server') ?? '').trim(), ref: String(kv.get('--happy-server-ref') ?? '').trim() },
714
+ 'happy-server-light': {
715
+ raw: String(kv.get('--happy-server-light') ?? '').trim(),
716
+ ref: String(kv.get('--happy-server-light-ref') ?? '').trim(),
717
+ },
718
+ };
719
+
720
+ const pins = {};
721
+ for (const [label, { raw, ref }] of Object.entries(spec)) {
722
+ if (!raw) continue;
723
+ // eslint-disable-next-line no-await-in-loop
724
+ const repoRoot = await resolveRepoRootFromPathOrUrlNonInteractive({ rootDir, label, raw, yes, fetch });
725
+ // eslint-disable-next-line no-await-in-loop
726
+ const worktreePath = ref ? await ensureWorktreeForRef({ rootDir, componentLabel: label, repoRoot, ref }) : '';
727
+ pins[label] = worktreePath || repoRoot;
728
+ }
729
+
730
+ const ensured = await ensureStackExists({ rootDir, stackName, serverComponent });
731
+ const envPath = await pinStackComponentDirs({ stackName: ensured, pins });
732
+
733
+ if (json) {
734
+ printResult({ json, data: { ok: true, stackName: ensured, serverComponent, envPath, pins } });
735
+ return;
736
+ }
737
+
738
+ // eslint-disable-next-line no-console
739
+ console.log('');
740
+ // eslint-disable-next-line no-console
741
+ console.log(banner('Import applied', { subtitle: 'Pinned the provided checkouts into the target stack env file.' }));
742
+ // eslint-disable-next-line no-console
743
+ console.log(kvFmt('stack', ensured));
744
+ // eslint-disable-next-line no-console
745
+ console.log(kvFmt('server', serverComponent));
746
+ // eslint-disable-next-line no-console
747
+ console.log(kvFmt('env', envPath));
748
+ // eslint-disable-next-line no-console
749
+ console.log('');
750
+ // eslint-disable-next-line no-console
751
+ console.log(sectionTitle('Pinned components'));
752
+ // eslint-disable-next-line no-console
753
+ console.log(bullets(summarizePins(pins)));
754
+ // eslint-disable-next-line no-console
755
+ console.log('');
756
+ // eslint-disable-next-line no-console
757
+ console.log(sectionTitle('Next'));
758
+ // eslint-disable-next-line no-console
759
+ console.log(bullets([cmdFmt(`hstack stack dev ${ensured}`), cmdFmt(`hstack import migrate --stack=${ensured}`)]));
760
+ }
761
+
762
+ async function cmdMigrateStack({ rootDir, argv }) {
763
+ const { flags, kv } = parseArgs(argv);
764
+ const json = wantsJson(argv, { flags });
765
+ const interactive = isTty() && !json;
766
+ if (!interactive) {
767
+ throw new Error('[import] migrate is interactive-only (TTY required).');
768
+ }
769
+
770
+ await withRl(async (rl) => {
771
+ // eslint-disable-next-line no-console
772
+ console.log('');
773
+ // eslint-disable-next-line no-console
774
+ console.log(
775
+ banner('Migrate a stack to monorepo', {
776
+ subtitle: 'Port the stack’s split-repo commits into a monorepo worktree, then (recommended) create a new monorepo stack.',
777
+ })
778
+ );
779
+
780
+ const allStacks = await listAllStackNames();
781
+ const providedStack = (kv.get('--stack') ?? '').trim() || argv.filter((a) => !a.startsWith('--'))[1]?.trim() || '';
782
+
783
+ const stackName = providedStack
784
+ ? sanitizeStackName(providedStack)
785
+ : await promptSelect(rl, {
786
+ title: `${bold('Pick a stack to migrate')}\n${dim('We will read its component pins and port those repos into the monorepo layout.')}`,
787
+ options: allStacks.map((s) => ({ label: s, value: s })),
788
+ defaultIndex: allStacks.includes('main') ? Math.max(0, allStacks.indexOf('main') - 1) : 0,
789
+ });
790
+
791
+ if (!stackName) throw new Error('[import] missing stack name');
792
+ if (!stackExistsSync(stackName)) {
793
+ throw new Error(`[import] stack does not exist: ${stackName}`);
794
+ }
795
+
796
+ const envPath = resolveStackEnvPath(stackName).envPath;
797
+ const envObj = await readEnvObjectFromFile(envPath);
798
+ const pins = {
799
+ happy: readPinnedComponentDirFromEnvObject(envObj, 'happy'),
800
+ 'happy-cli': readPinnedComponentDirFromEnvObject(envObj, 'happy-cli'),
801
+ 'happy-server': readPinnedComponentDirFromEnvObject(envObj, 'happy-server'),
802
+ 'happy-server-light': readPinnedComponentDirFromEnvObject(envObj, 'happy-server-light'),
803
+ };
804
+
805
+ const hasAnyPins = Object.values(pins).some(Boolean);
806
+ if (!hasAnyPins) {
807
+ throw new Error(
808
+ `[import] stack ${stackName} does not have any pinned component dirs.\n` +
809
+ `[import] Fix: run ${cmdFmt('hstack import')} to create an imported stack first, then re-run migrate.`
810
+ );
811
+ }
812
+
813
+ // eslint-disable-next-line no-console
814
+ console.log('');
815
+ // eslint-disable-next-line no-console
816
+ console.log(sectionTitle('Detected pins'));
817
+ // eslint-disable-next-line no-console
818
+ console.log(bullets(summarizePins(pins)));
819
+
820
+ // Choose monorepo target repo to base the worktree on.
821
+ const defaultMonorepo = await resolveDefaultMonorepoRoot({ rootDir });
822
+ let monorepoRepoRoot = defaultMonorepo;
823
+ if (!monorepoRepoRoot) {
824
+ const raw = await prompt(rl, `Monorepo repo path or URL (Happier): `, { defaultValue: '' });
825
+ const r = raw.trim() ? await resolveRepoRootFromPathOrUrl({ rootDir, label: 'happy-monorepo', raw, rl }) : '';
826
+ if (!r || !isHappyMonorepoRoot(r)) {
827
+ throw new Error('[import] target is not a Happier monorepo root (missing apps/ui|apps/cli|apps/server).');
828
+ }
829
+ monorepoRepoRoot = r;
830
+ }
831
+
832
+ const portBranchDefault = `port/${sanitizeSlugPart(stackName)}`;
833
+ const portBranchRaw = await prompt(rl, `Monorepo port branch (default: ${portBranchDefault}): `, { defaultValue: portBranchDefault });
834
+ const branch = String(portBranchRaw ?? '').trim() || portBranchDefault;
835
+
836
+ // Recommended: do the port into a dedicated worktree so we don't disturb your main monorepo checkout.
837
+ const worktreeMode = await promptSelect(rl, {
838
+ title: `${bold('Where should the port be applied?')}\n${dim('Recommended: create a dedicated monorepo worktree for this port branch.')}`,
839
+ options: [
840
+ { label: `create a dedicated monorepo worktree (${green('recommended')})`, value: 'worktree' },
841
+ { label: `use the existing monorepo checkout ${dim('(advanced)')}`, value: 'in-place' },
842
+ ],
843
+ defaultIndex: 0,
844
+ });
845
+
846
+ const targetMonorepoRoot =
847
+ worktreeMode === 'worktree'
848
+ ? await createMonorepoPortWorktree({ rootDir, monorepoRepoRoot, slug: sanitizeSlugPart(branch), baseRef: '' })
849
+ : monorepoRepoRoot;
850
+
851
+ // Only pass sources that exist.
852
+ const sources = {};
853
+ if (pins.happy) sources.happy = pins.happy;
854
+ if (pins['happy-cli']) sources['happy-cli'] = pins['happy-cli'];
855
+ if (pins['happy-server']) sources['happy-server'] = pins['happy-server'];
856
+ // Port flow is owned by `hstack monorepo port guide` (preflight + auto-apply + conflicts + optional LLM).
857
+ // eslint-disable-next-line no-console
858
+ console.log('');
859
+ // eslint-disable-next-line no-console
860
+ console.log(banner('Migrating', { subtitle: 'Porting commits into the monorepo layout (preflight + guided conflict resolution).' }));
861
+ await runMonorepoPort({ rootDir, targetMonorepoRoot, sources, branch, dryRun: false });
862
+
863
+ // After migration: reuse or new stack.
864
+ // eslint-disable-next-line no-console
865
+ console.log('');
866
+ const stackAfter = await promptSelect(rl, {
867
+ title: `${bold('After migration')}\n${dim('Reuse this stack, or create a new monorepo stack to keep the old one intact?')}`,
868
+ options: [
869
+ { label: `create a new stack (${green('recommended')}) — keep legacy stack intact`, value: 'new' },
870
+ { label: `reuse existing stack — switch it to the monorepo checkout`, value: 'reuse' },
871
+ ],
872
+ defaultIndex: 0,
873
+ });
874
+
875
+ const migratedStackNameDefault =
876
+ sanitizeStackName(`${stackName}-mono`) || sanitizeStackName(`mono-${stackName}`) || 'mono';
877
+ const migratedStackName =
878
+ stackAfter === 'reuse'
879
+ ? stackName
880
+ : sanitizeStackName(
881
+ (
882
+ await prompt(rl, `New monorepo stack name (default: ${migratedStackNameDefault}): `, {
883
+ defaultValue: migratedStackNameDefault,
884
+ })
885
+ ).trim() || migratedStackNameDefault
886
+ );
887
+
888
+ const finalStackName =
889
+ stackAfter === 'reuse'
890
+ ? migratedStackName
891
+ : await ensureStackExists({ rootDir, stackName: migratedStackName, serverComponent: 'happy-server' });
892
+
893
+ const monoPins = {};
894
+ if (pins.happy) monoPins.happy = targetMonorepoRoot;
895
+ if (pins['happy-cli']) monoPins['happy-cli'] = targetMonorepoRoot;
896
+ if (pins['happy-server']) monoPins['happy-server'] = targetMonorepoRoot;
897
+ if (pins['happy-server-light']) monoPins['happy-server-light'] = pins['happy-server-light'];
898
+ const finalEnvPath = await pinStackComponentDirs({ stackName: finalStackName, pins: monoPins });
899
+
900
+ // eslint-disable-next-line no-console
901
+ console.log('');
902
+ // eslint-disable-next-line no-console
903
+ console.log(banner('Migrated', { subtitle: 'Your monorepo stack is ready.' }));
904
+ // eslint-disable-next-line no-console
905
+ console.log(kvFmt('Stack', cyan(finalStackName)));
906
+ // eslint-disable-next-line no-console
907
+ console.log(kvFmt('Env', finalEnvPath));
908
+ // eslint-disable-next-line no-console
909
+ console.log(sectionTitle('Next'));
910
+ // eslint-disable-next-line no-console
911
+ console.log(bullets([`Run: ${cmdFmt(`hstack stack dev ${finalStackName}`)}`]));
912
+ });
913
+ }
914
+
915
+ async function main() {
916
+ const rootDir = getRootDir(import.meta.url);
917
+ const argv = process.argv.slice(2);
918
+ const { flags, kv } = parseArgs(argv);
919
+ const json = wantsJson(argv, { flags });
920
+ const interactive = isTty() && !json;
921
+
922
+ if (wantsHelp(argv, { flags })) {
923
+ printResult({ json, data: { json: true }, text: usage() });
924
+ return;
925
+ }
926
+
927
+ const positionals = argv.filter((a) => !a.startsWith('--'));
928
+ const sub = positionals[0] || '';
929
+ if (sub === 'inspect') {
930
+ await cmdInspect({ rootDir, argv });
931
+ return;
932
+ }
933
+ if (sub === 'apply') {
934
+ await cmdApply({ rootDir, argv });
935
+ return;
936
+ }
937
+ if (sub === 'migrate') {
938
+ await cmdMigrateStack({ rootDir, argv });
939
+ return;
940
+ }
941
+ if (sub === 'llm') {
942
+ await cmdLlm({ argv });
943
+ return;
944
+ }
945
+
946
+ if (!interactive) {
947
+ printResult({
948
+ json,
949
+ data: { ok: false },
950
+ text: '[import] This command is currently interactive-only. Re-run in a TTY.',
951
+ });
952
+ return;
953
+ }
954
+
955
+ // eslint-disable-next-line no-console
956
+ console.log('');
957
+ // eslint-disable-next-line no-console
958
+ console.log(
959
+ banner('Import legacy repos', {
960
+ subtitle: 'Bring your pre-monorepo (split repo) work into hstack, then optionally migrate to monorepo.',
961
+ })
962
+ );
963
+ // eslint-disable-next-line no-console
964
+ console.log(sectionTitle('Key concepts'));
965
+ // eslint-disable-next-line no-console
966
+ console.log(
967
+ bullets([
968
+ `${bold('components')}: the main codebases (UI = ${cyan('happy')}, CLI/daemon = ${cyan('happy-cli')}, server = ${cyan('happy-server')})`,
969
+ `${bold('stack')}: an isolated runtime (ports + data + env) under ${dim('~/.happy/stacks/<name>')}`,
970
+ `${bold('import')}: pin a stack to your existing repo checkouts (so you can run your work as-is)`,
971
+ `${bold('migrate')}: port your split-repo commits into the monorepo layout via ${cyan('hstack monorepo port')}`,
972
+ ])
973
+ );
974
+
975
+ await withRl(async (rl) => {
976
+ const repos = { happy: '', 'happy-cli': '', 'happy-server': '', 'happy-server-light': '' };
977
+
978
+ const collectRepos = async () => {
979
+ // eslint-disable-next-line no-console
980
+ console.log('');
981
+ // eslint-disable-next-line no-console
982
+ console.log(sectionTitle('Your repos'));
983
+ // eslint-disable-next-line no-console
984
+ console.log(
985
+ bullets([
986
+ `Paste a ${bold('local path')} or a ${bold('git URL')} (GitHub HTTPS/SSH).`,
987
+ `If you paste a URL, we clone it into your hstack workspace under ${dim('imports/repos/...')}.`,
988
+ `Tip: if you already have a worktree checked out on the branch you want, paste that worktree path.`,
989
+ ])
990
+ );
991
+
992
+ const uiRaw = await prompt(rl, `${cyan('happy')} (UI) path or URL: `, { defaultValue: repos.happy ? repos.happy : '' });
993
+ const cliRaw = await prompt(rl, `${cyan('happy-cli')} (CLI/daemon) path or URL: `, {
994
+ defaultValue: repos['happy-cli'] ? repos['happy-cli'] : '',
995
+ });
996
+ const serverRaw = await prompt(rl, `${cyan('happy-server')} (server) path or URL: `, {
997
+ defaultValue: repos['happy-server'] ? repos['happy-server'] : '',
998
+ });
999
+ const serverLightRaw = await prompt(rl, `${cyan('happy-server-light')} (optional) path or URL: `, {
1000
+ defaultValue: repos['happy-server-light'] ? repos['happy-server-light'] : '',
1001
+ });
1002
+
1003
+ repos.happy = uiRaw.trim() ? await resolveRepoRootFromPathOrUrl({ rootDir, label: 'happy', raw: uiRaw, rl }) : '';
1004
+ repos['happy-cli'] = cliRaw.trim()
1005
+ ? await resolveRepoRootFromPathOrUrl({ rootDir, label: 'happy-cli', raw: cliRaw, rl })
1006
+ : '';
1007
+ repos['happy-server'] = serverRaw.trim()
1008
+ ? await resolveRepoRootFromPathOrUrl({ rootDir, label: 'happy-server', raw: serverRaw, rl })
1009
+ : '';
1010
+ repos['happy-server-light'] = serverLightRaw.trim()
1011
+ ? await resolveRepoRootFromPathOrUrl({ rootDir, label: 'happy-server-light', raw: serverLightRaw, rl })
1012
+ : '';
1013
+
1014
+ if (!repos.happy && !repos['happy-cli'] && !repos['happy-server'] && !repos['happy-server-light']) {
1015
+ throw new Error('[import] no repos provided. Provide at least one path/URL.');
1016
+ }
1017
+ };
1018
+
1019
+ await collectRepos();
1020
+
1021
+ // eslint-disable-next-line no-console
1022
+ console.log('');
1023
+ // eslint-disable-next-line no-console
1024
+ console.log(sectionTitle('Import plan'));
1025
+ // eslint-disable-next-line no-console
1026
+ console.log(dim('We can import multiple branches/stacks in one run. Start with your first one.'));
1027
+
1028
+ while (true) {
1029
+ // Step: choose checkouts (branch/worktree) for this stack
1030
+ // eslint-disable-next-line no-console
1031
+ console.log('');
1032
+ // eslint-disable-next-line no-console
1033
+ console.log(sectionTitle('Choose what to import'));
1034
+ // eslint-disable-next-line no-console
1035
+ console.log(dim('We will pin the stack to these exact checkouts so you can run your work as-is.'));
1036
+
1037
+ const selected = {};
1038
+ const selectedBranches = [];
1039
+
1040
+ if (repos.happy) {
1041
+ const r = await chooseCheckoutPathForRepo({
1042
+ rl,
1043
+ rootDir,
1044
+ componentLabel: 'happy',
1045
+ repoRoot: repos.happy,
1046
+ repoHintLabel: 'UI',
1047
+ });
1048
+ selected.happy = r.path;
1049
+ if (r.branch && r.branch !== 'unknown' && r.branch !== 'detached') selectedBranches.push(r.branch);
1050
+ }
1051
+ if (repos['happy-cli']) {
1052
+ const r = await chooseCheckoutPathForRepo({
1053
+ rl,
1054
+ rootDir,
1055
+ componentLabel: 'happy-cli',
1056
+ repoRoot: repos['happy-cli'],
1057
+ repoHintLabel: 'CLI/daemon',
1058
+ });
1059
+ selected['happy-cli'] = r.path;
1060
+ if (r.branch && r.branch !== 'unknown' && r.branch !== 'detached') selectedBranches.push(r.branch);
1061
+ }
1062
+ if (repos['happy-server']) {
1063
+ const r = await chooseCheckoutPathForRepo({
1064
+ rl,
1065
+ rootDir,
1066
+ componentLabel: 'happy-server',
1067
+ repoRoot: repos['happy-server'],
1068
+ repoHintLabel: 'server',
1069
+ });
1070
+ selected['happy-server'] = r.path;
1071
+ if (r.branch && r.branch !== 'unknown' && r.branch !== 'detached') selectedBranches.push(r.branch);
1072
+ }
1073
+ if (repos['happy-server-light']) {
1074
+ const includeServerLight = await promptSelect(rl, {
1075
+ title: `${bold('happy-server-light')}\n${dim('Optional: also pin server-light for this stack?')}`,
1076
+ options: [
1077
+ { label: 'no (default)', value: false },
1078
+ { label: 'yes', value: true },
1079
+ ],
1080
+ defaultIndex: 0,
1081
+ });
1082
+ if (includeServerLight) {
1083
+ const r = await chooseCheckoutPathForRepo({
1084
+ rl,
1085
+ rootDir,
1086
+ componentLabel: 'happy-server-light',
1087
+ repoRoot: repos['happy-server-light'],
1088
+ repoHintLabel: 'server-light (light flavor)',
1089
+ });
1090
+ selected['happy-server-light'] = r.path;
1091
+ if (r.branch && r.branch !== 'unknown' && r.branch !== 'detached') selectedBranches.push(r.branch);
1092
+ }
1093
+ }
1094
+
1095
+ // Step: stack selection
1096
+ // eslint-disable-next-line no-console
1097
+ console.log('');
1098
+ // eslint-disable-next-line no-console
1099
+ console.log(sectionTitle('Stack'));
1100
+ // eslint-disable-next-line no-console
1101
+ console.log(dim('A stack is an isolated runtime. Create a new stack per feature/branch (recommended).'));
1102
+
1103
+ const existing = await listAllStackNames();
1104
+ const canReuse = existing.length > 0;
1105
+ const stackMode = await promptSelect(rl, {
1106
+ title: `${bold('Where should this imported work run?')}`,
1107
+ options: [
1108
+ { label: `create a new stack (${green('recommended')})`, value: 'new' },
1109
+ ...(canReuse ? [{ label: `reuse an existing stack ${dim('(advanced)')}`, value: 'reuse' }] : []),
1110
+ ],
1111
+ defaultIndex: 0,
1112
+ });
1113
+
1114
+ const inferredBase =
1115
+ selectedBranches.length && selectedBranches.every((b) => b === selectedBranches[0]) ? selectedBranches[0] : 'import';
1116
+ const defaultStackName = sanitizeStackName(inferredBase) || 'import';
1117
+
1118
+ let stackName = '';
1119
+ if (stackMode === 'reuse') {
1120
+ stackName = await promptSelect(rl, {
1121
+ title: `${bold('Pick a stack to reuse')}\n${dim('We will update its component pins to point at your selected checkouts.')}`,
1122
+ options: existing.map((s) => ({ label: s, value: s })),
1123
+ defaultIndex: 0,
1124
+ });
1125
+ } else {
1126
+ const raw = await prompt(rl, `New stack name (default: ${defaultStackName}): `, { defaultValue: defaultStackName });
1127
+ stackName = sanitizeStackName(raw.trim() || defaultStackName);
1128
+ }
1129
+ if (!stackName) throw new Error('[import] missing stack name');
1130
+
1131
+ const serverComponentDefault = selected['happy-server-light'] ? 0 : 1;
1132
+ const serverComponent = await promptSelect(rl, {
1133
+ title: `${bold('Server flavor for this stack')}\n${dim('Pick how you want to run the server for this imported work.')}`,
1134
+ options: [
1135
+ { label: `${cyan('happy-server-light')} (${green('recommended')}) — easier local dev`, value: 'happy-server-light' },
1136
+ { label: `${cyan('happy-server')} — full server (Docker-managed infra)`, value: 'happy-server' },
1137
+ ],
1138
+ defaultIndex: serverComponentDefault,
1139
+ });
1140
+
1141
+ // Apply pins
1142
+ const ensuredStack = await ensureStackExists({ rootDir, stackName, serverComponent });
1143
+ const envPath = await pinStackComponentDirs({ stackName: ensuredStack, pins: selected });
1144
+
1145
+ // eslint-disable-next-line no-console
1146
+ console.log('');
1147
+ // eslint-disable-next-line no-console
1148
+ console.log(banner('Imported', { subtitle: 'Your stack is now pinned to your existing repo checkouts.' }));
1149
+ // eslint-disable-next-line no-console
1150
+ console.log(kvFmt('Stack', cyan(ensuredStack)));
1151
+ // eslint-disable-next-line no-console
1152
+ console.log(kvFmt('Env', envPath));
1153
+ // eslint-disable-next-line no-console
1154
+ console.log(sectionTitle('Pinned components'));
1155
+ // eslint-disable-next-line no-console
1156
+ console.log(bullets(summarizePins(selected)));
1157
+ // eslint-disable-next-line no-console
1158
+ console.log('');
1159
+ // eslint-disable-next-line no-console
1160
+ console.log(dim(`Tip: run it with ${cmdFmt(`hstack stack dev ${ensuredStack}`)} (or ${cmdFmt(`hstack stack start ${ensuredStack}`)}).`));
1161
+
1162
+ // Optional migration
1163
+ // eslint-disable-next-line no-console
1164
+ console.log('');
1165
+ const migrateWanted = await promptSelect(rl, {
1166
+ title: `${bold('Migrate to monorepo?')}\n${dim(
1167
+ 'Optional: port split-repo commits into the monorepo layout (packages/happy-* or legacy expo-app/cli/server).'
1168
+ )}`,
1169
+ options: [
1170
+ { label: `no (default) — keep running from split repos`, value: 'no' },
1171
+ { label: `yes (${green('recommended')}) — port commits into a monorepo branch`, value: 'yes' },
1172
+ { label: `dry run — preview what would be ported`, value: 'dry-run' },
1173
+ ],
1174
+ defaultIndex: 0,
1175
+ });
1176
+
1177
+ if (migrateWanted !== 'no') {
1178
+ const defaultTarget = await resolveDefaultMonorepoRoot({ rootDir });
1179
+ let monorepoRepoRoot = defaultTarget;
1180
+ if (!monorepoRepoRoot) {
1181
+ // eslint-disable-next-line no-console
1182
+ console.log(
1183
+ `${yellow('!')} No monorepo checkout detected in your hstack workspace yet.\n` +
1184
+ dim(`Fix: run ${cmdFmt('hstack setup --profile=dev')} (or ${cmdFmt('hstack bootstrap')}) first, then re-run import.`)
1185
+ );
1186
+ const raw = await prompt(rl, `Monorepo target path (Happier monorepo root): `, { defaultValue: '' });
1187
+ monorepoRepoRoot = raw.trim() ? await gitRoot(raw.trim()) : '';
1188
+ if (!monorepoRepoRoot || !isHappyMonorepoRoot(monorepoRepoRoot)) {
1189
+ throw new Error('[import] target is not a Happier monorepo root (missing apps/ui|apps/cli|apps/server).');
1190
+ }
1191
+ }
1192
+
1193
+ const portBranchDefault = `port/${sanitizeSlugPart(ensuredStack || 'import')}`;
1194
+ const portBranch = await prompt(rl, `Monorepo port branch (default: ${portBranchDefault}): `, {
1195
+ defaultValue: portBranchDefault,
1196
+ });
1197
+ const branch = String(portBranch ?? '').trim() || portBranchDefault;
1198
+
1199
+ if (migrateWanted === 'dry-run') {
1200
+ // Also show the "what would be ported" preview (patch count only).
1201
+ // eslint-disable-next-line no-console
1202
+ console.log('');
1203
+ // eslint-disable-next-line no-console
1204
+ console.log(banner('Dry run', { subtitle: 'Previewing what would be ported (does not apply patches).' }));
1205
+ await runMonorepoPort({ rootDir, targetMonorepoRoot: monorepoRepoRoot, sources: selected, branch, dryRun: true });
1206
+ // eslint-disable-next-line no-console
1207
+ console.log(green('✓ Dry run complete'));
1208
+ } else {
1209
+ // Choose where to apply the real port (only needed when we actually run it).
1210
+ const worktreeMode = await promptSelect(rl, {
1211
+ title: `${bold('Where should the port be applied?')}\n${dim('Recommended: create a dedicated monorepo worktree for this port branch.')}`,
1212
+ options: [
1213
+ { label: `create a dedicated monorepo worktree (${green('recommended')})`, value: 'worktree' },
1214
+ { label: `use the existing monorepo checkout ${dim('(advanced)')}`, value: 'in-place' },
1215
+ ],
1216
+ defaultIndex: 0,
1217
+ });
1218
+
1219
+ const targetMonorepoRoot =
1220
+ worktreeMode === 'worktree'
1221
+ ? await createMonorepoPortWorktree({ rootDir, monorepoRepoRoot, slug: sanitizeSlugPart(branch), baseRef: '' })
1222
+ : monorepoRepoRoot;
1223
+
1224
+ let migrationCompleted = false;
1225
+ try {
1226
+ // This delegates all port logic to `hstack monorepo port guide` (preflight + auto-apply + conflicts + optional LLM).
1227
+ // eslint-disable-next-line no-console
1228
+ console.log('');
1229
+ // eslint-disable-next-line no-console
1230
+ console.log(banner('Migrating', { subtitle: 'Porting commits into the monorepo layout (guided).' }));
1231
+ await runMonorepoPort({ rootDir, targetMonorepoRoot, sources: selected, branch, dryRun: false });
1232
+ migrationCompleted = true;
1233
+ } catch (e) {
1234
+ // eslint-disable-next-line no-console
1235
+ console.log('');
1236
+ // eslint-disable-next-line no-console
1237
+ console.log(`${yellow('!')} Migration stopped ${dim(`(${String(e?.message ?? e ?? 'unknown')})`)}`);
1238
+ // eslint-disable-next-line no-console
1239
+ console.log(dim('You can retry later by re-running import and choosing migration again.'));
1240
+ migrationCompleted = false;
1241
+ }
1242
+
1243
+ if (migrationCompleted) {
1244
+ // eslint-disable-next-line no-console
1245
+ console.log('');
1246
+ const stackAfter = await promptSelect(rl, {
1247
+ title: `${bold('After migration')}\n${dim('Do you want to reuse the same stack or create a new stack for the monorepo branch?')}`,
1248
+ options: [
1249
+ { label: `create a new stack (${green('recommended')}) — keep legacy stack intact`, value: 'new' },
1250
+ { label: `reuse existing stack — switch it to the monorepo checkout`, value: 'reuse' },
1251
+ ],
1252
+ defaultIndex: 0,
1253
+ });
1254
+
1255
+ const migratedStackNameDefault =
1256
+ sanitizeStackName(`${ensuredStack}-mono`) || sanitizeStackName(`mono-${ensuredStack}`) || 'mono';
1257
+ const migratedStackName =
1258
+ stackAfter === 'reuse'
1259
+ ? ensuredStack
1260
+ : sanitizeStackName(
1261
+ (
1262
+ await prompt(rl, `New monorepo stack name (default: ${migratedStackNameDefault}): `, {
1263
+ defaultValue: migratedStackNameDefault,
1264
+ })
1265
+ ).trim() || migratedStackNameDefault
1266
+ );
1267
+
1268
+ const migratedServerComponent = selected['happy-server'] ? 'happy-server' : serverComponent;
1269
+ const finalStackName =
1270
+ stackAfter === 'reuse'
1271
+ ? migratedStackName
1272
+ : await ensureStackExists({ rootDir, stackName: migratedStackName, serverComponent: migratedServerComponent });
1273
+
1274
+ const monoPins = {};
1275
+ if (selected.happy) monoPins.happy = targetMonorepoRoot;
1276
+ if (selected['happy-cli']) monoPins['happy-cli'] = targetMonorepoRoot;
1277
+ if (selected['happy-server']) monoPins['happy-server'] = targetMonorepoRoot;
1278
+ // Keep server-light pinned if user opted into it (server-light is not ported by monorepo port today).
1279
+ if (selected['happy-server-light']) monoPins['happy-server-light'] = selected['happy-server-light'];
1280
+
1281
+ const finalEnvPath = await pinStackComponentDirs({ stackName: finalStackName, pins: monoPins });
1282
+
1283
+ // eslint-disable-next-line no-console
1284
+ console.log('');
1285
+ // eslint-disable-next-line no-console
1286
+ console.log(banner('Migrated', { subtitle: 'Your monorepo stack is ready.' }));
1287
+ // eslint-disable-next-line no-console
1288
+ console.log(kvFmt('Stack', cyan(finalStackName)));
1289
+ // eslint-disable-next-line no-console
1290
+ console.log(kvFmt('Env', finalEnvPath));
1291
+ // eslint-disable-next-line no-console
1292
+ console.log(sectionTitle('Next'));
1293
+ // eslint-disable-next-line no-console
1294
+ console.log(
1295
+ bullets([
1296
+ `Run: ${cmdFmt(`hstack stack dev ${finalStackName}`)}`,
1297
+ `If you need to import more branches later, re-run: ${cmdFmt('hstack import')}`,
1298
+ ])
1299
+ );
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ // Loop
1305
+ // eslint-disable-next-line no-console
1306
+ console.log('');
1307
+ const again = await promptSelect(rl, {
1308
+ title: `${bold('Import another branch/stack?')}`,
1309
+ options: [
1310
+ { label: 'no (default)', value: 'no' },
1311
+ { label: 'yes — import another branch into another stack', value: 'yes' },
1312
+ { label: `yes — change repo inputs first ${dim('(advanced)')}`, value: 'change-repos' },
1313
+ ],
1314
+ defaultIndex: 0,
1315
+ });
1316
+ if (again === 'no') break;
1317
+ if (again === 'change-repos') {
1318
+ await collectRepos();
1319
+ }
1320
+ }
1321
+ });
1322
+ }
1323
+
1324
+ main().catch((err) => {
1325
+ process.stderr.write(String(err?.message ?? err) + '\n');
1326
+ process.exit(1);
1327
+ });