@happier-dev/stack 0.1.0-preview.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +138 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +74 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,1829 @@
1
+ import './utils/env/env.mjs';
2
+ import { parseArgs } from './utils/cli/args.mjs';
3
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
4
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir, getStackName, resolveStackEnvPath } from './utils/paths/paths.mjs';
5
+ import { listAllStackNames } from './utils/stack/stacks.mjs';
6
+ import { resolvePublicServerUrl } from './tailscale.mjs';
7
+ import { getInternalServerUrl, getPublicServerUrlEnvOverride, getWebappUrlEnvOverride } from './utils/server/urls.mjs';
8
+ import { fetchHappierHealth, waitForHappierHealthOk } from './utils/server/server.mjs';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ import { spawn } from 'node:child_process';
13
+ import { mkdir, writeFile } from 'node:fs/promises';
14
+ import { dirname } from 'node:path';
15
+
16
+ import { parseEnvToObject } from './utils/env/dotenv.mjs';
17
+ import { run, runCapture } from './utils/proc/proc.mjs';
18
+ import { applyStackCacheEnv, ensureDepsInstalled } from './utils/proc/pm.mjs';
19
+ import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
20
+ import { resolvePrismaClientImportForDbProvider, resolvePrismaClientImportForServerComponent } from './utils/server/flavor_scripts.mjs';
21
+ import { clearDevAuthKey, readDevAuthKey, writeDevAuthKey } from './utils/auth/dev_key.mjs';
22
+ import { getExpoStatePaths, isStateProcessRunning } from './utils/expo/expo.mjs';
23
+ import { resolveAuthSeedFromEnv } from './utils/stack/startup.mjs';
24
+ import { copyFileIfMissing, linkFileIfMissing, removeFileOrSymlinkIfExists, writeSecretFileIfMissing } from './utils/auth/files.mjs';
25
+ import { resolveHandyMasterSecretFromStack } from './utils/auth/handy_master_secret.mjs';
26
+ import { ensureDir, readTextIfExists } from './utils/fs/ops.mjs';
27
+ import { stackExistsSync } from './utils/stack/stacks.mjs';
28
+ import { checkDaemonState } from './daemon.mjs';
29
+ import { isTty, prompt, withRl } from './utils/cli/wizard.mjs';
30
+ import { parseCliIdentityOrThrow, resolveCliHomeDirForIdentity } from './utils/stack/cli_identities.mjs';
31
+ import {
32
+ getCliHomeDirFromEnvOrDefault,
33
+ getServerLightDataDirFromEnvOrDefault,
34
+ resolveCliHomeDir,
35
+ } from './utils/stack/dirs.mjs';
36
+ import { resolveLocalhostHost, preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
37
+ import { banner, bullets, cmd as cmdFmt, kv, ok, sectionTitle, warn } from './utils/ui/layout.mjs';
38
+ import { bold, cyan, dim } from './utils/ui/ansi.mjs';
39
+ import { getVerbosityLevel } from './utils/cli/verbosity.mjs';
40
+ import { runOrchestratedGuidedAuthFlow, startDaemonPostAuth } from './utils/auth/orchestrated_stack_auth_flow.mjs';
41
+ import { applyStackActiveServerScopeEnv } from './utils/auth/stable_scope_id.mjs';
42
+ import {
43
+ findAnyCredentialPathInCliHome,
44
+ findExistingStackCredentialPath,
45
+ resolveStackCredentialPaths,
46
+ } from './utils/auth/credentials_paths.mjs';
47
+ import { decodeJwtPayloadUnsafe } from './utils/auth/decode_jwt_payload_unsafe.mjs';
48
+ import { fileHasContent } from './utils/fs/file_has_content.mjs';
49
+ import { buildConfigureServerLinks } from '@happier-dev/cli-common/links';
50
+ import { getStackRuntimeStatePath, isPidAlive as isRuntimePidAlive, readStackRuntimeStateFile } from './utils/stack/runtime_state.mjs';
51
+
52
+ function resolveGuidedStartAction({ healthOk = false, runtimeOwnerAlive = false, autoStart = false } = {}) {
53
+ if (healthOk) return 'proceed';
54
+ if (runtimeOwnerAlive) return 'wait';
55
+ if (autoStart) return 'start';
56
+ return 'prompt';
57
+ }
58
+
59
+ function getInternalServerUrlCompat() {
60
+ const { port, internalServerUrl } = getInternalServerUrl({ env: process.env, defaultPort: 3005 });
61
+ return { port, url: internalServerUrl };
62
+ }
63
+
64
+ async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
65
+ try {
66
+ const baseDir = resolveStackEnvPath(stackName).baseDir;
67
+ const uiDir = getComponentDir(rootDir, 'happier-ui');
68
+ const uiPaths = getExpoStatePaths({
69
+ baseDir,
70
+ kind: 'expo-dev',
71
+ projectDir: uiDir,
72
+ stateFileName: 'expo.state.json',
73
+ });
74
+ const uiRunning = await isStateProcessRunning(uiPaths.statePath);
75
+ if (!uiRunning.running) return null;
76
+ const port = Number(uiRunning.state?.port);
77
+ if (!Number.isFinite(port) || port <= 0) return null;
78
+ const host = resolveLocalhostHost({ stackMode: stackName !== 'main', stackName });
79
+ return `http://${host}:${port}`;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ // NOTE: common fs helpers live in scripts/utils/fs/ops.mjs
86
+
87
+ // (auth file copy/link helpers live in scripts/utils/auth/files.mjs)
88
+
89
+ function readAuthTokenFromCredentialPath(path) {
90
+ const p = String(path ?? '').trim();
91
+ if (!p || !existsSync(p)) return null;
92
+ try {
93
+ const raw = readFileSync(p, 'utf-8').trim();
94
+ if (!raw) return null;
95
+ try {
96
+ const parsed = JSON.parse(raw);
97
+ if (typeof parsed?.token === 'string' && parsed.token.trim()) {
98
+ return parsed.token.trim();
99
+ }
100
+ } catch {
101
+ // fall through
102
+ }
103
+ return raw;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function resolveJwtSubjectFromCredentialPath(path) {
110
+ const token = readAuthTokenFromCredentialPath(path);
111
+ if (!token) return '';
112
+ const payload = decodeJwtPayloadUnsafe(token);
113
+ const sub = payload && typeof payload.sub === 'string' ? payload.sub.trim() : '';
114
+ return sub || '';
115
+ }
116
+
117
+ async function validateAuthTokenAgainstServer({ credentialPath, internalServerUrl }) {
118
+ const token = readAuthTokenFromCredentialPath(credentialPath);
119
+ if (!token) {
120
+ return {
121
+ checked: false,
122
+ valid: null,
123
+ status: null,
124
+ code: 'missing-token',
125
+ error: null,
126
+ };
127
+ }
128
+
129
+ const ctl = new AbortController();
130
+ const timeout = setTimeout(() => ctl.abort(), 2_500);
131
+ try {
132
+ const res = await fetch(`${internalServerUrl}/v1/account/profile`, {
133
+ method: 'GET',
134
+ headers: { Authorization: `Bearer ${token}` },
135
+ signal: ctl.signal,
136
+ });
137
+ const text = await res.text();
138
+ let body = null;
139
+ try {
140
+ body = text ? JSON.parse(text) : null;
141
+ } catch {
142
+ body = null;
143
+ }
144
+
145
+ if (res.status >= 200 && res.status < 300) {
146
+ return {
147
+ checked: true,
148
+ valid: true,
149
+ status: res.status,
150
+ code: 'ok',
151
+ error: null,
152
+ };
153
+ }
154
+
155
+ if (res.status === 401) {
156
+ return {
157
+ checked: true,
158
+ valid: false,
159
+ status: res.status,
160
+ code: typeof body?.code === 'string' && body.code ? body.code : 'invalid-token',
161
+ error: typeof body?.error === 'string' ? body.error : null,
162
+ };
163
+ }
164
+
165
+ return {
166
+ checked: true,
167
+ valid: false,
168
+ status: res.status,
169
+ code: `http-${res.status}`,
170
+ error: typeof body?.error === 'string' ? body.error : null,
171
+ };
172
+ } catch (e) {
173
+ return {
174
+ checked: true,
175
+ valid: null,
176
+ status: null,
177
+ code: 'request-error',
178
+ error: e instanceof Error ? e.message : String(e),
179
+ };
180
+ } finally {
181
+ clearTimeout(timeout);
182
+ }
183
+ }
184
+
185
+ function authLoginSuggestion(stackName) {
186
+ return stackName === 'main' ? 'hstack auth login' : `hstack stack auth ${stackName} login`;
187
+ }
188
+
189
+ function authCopyFromSeedSuggestion(stackName) {
190
+ if (stackName === 'main') return null;
191
+ const from = resolveAuthSeedFromEnv(process.env);
192
+ return `hstack stack auth ${stackName} copy-from ${from}`;
193
+ }
194
+
195
+ function argvKvValue(argv, name) {
196
+ const n = String(name ?? '').trim();
197
+ if (!n) return '';
198
+ for (let i = 0; i < argv.length; i += 1) {
199
+ const a = String(argv[i] ?? '');
200
+ if (a === n) {
201
+ const next = String(argv[i + 1] ?? '');
202
+ if (next && !next.startsWith('--')) return next;
203
+ return '';
204
+ }
205
+ if (a.startsWith(`${n}=`)) {
206
+ return a.slice(`${n}=`.length);
207
+ }
208
+ }
209
+ return '';
210
+ }
211
+
212
+ function resolveGuidedServerReadyTimeoutMs(env = process.env) {
213
+ const raw = String(env.HAPPIER_STACK_AUTH_SERVER_READY_TIMEOUT_MS ?? '').trim();
214
+ if (!raw) return 30_000;
215
+ const n = Number(raw);
216
+ return Number.isFinite(n) && n >= 1_000 ? n : 30_000;
217
+ }
218
+
219
+ async function isStackRuntimeOwnerAlive(stackName) {
220
+ try {
221
+ const statePath = getStackRuntimeStatePath(stackName);
222
+ const state = await readStackRuntimeStateFile(statePath);
223
+ const ownerPid = Number(state?.ownerPid);
224
+ return Number.isFinite(ownerPid) && ownerPid > 1 && isRuntimePidAlive(ownerPid);
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ function resolveServerComponentForCurrentStack() {
231
+ return (
232
+ (process.env.HAPPIER_STACK_SERVER_COMPONENT ?? 'happier-server-light').trim() ||
233
+ 'happier-server-light'
234
+ );
235
+ }
236
+
237
+ async function cmdSeed({ argv, json }) {
238
+ const rootDir = getRootDir(import.meta.url);
239
+ const positionals = argv.filter((a) => !a.startsWith('--'));
240
+ const name = (positionals[1] ?? '').trim() || 'dev-auth';
241
+
242
+ // Forward to the stack subcommand that implements the full "seed stack" workflow.
243
+ const passthrough = argv.slice(1).filter((a) => a !== name);
244
+ const child = spawn(
245
+ process.execPath,
246
+ [join(rootDir, 'scripts', 'stack.mjs'), 'create-dev-auth-seed', name, ...passthrough],
247
+ { cwd: rootDir, env: process.env, stdio: 'inherit' }
248
+ );
249
+ await new Promise((resolve) => child.on('exit', resolve));
250
+
251
+ if (json) {
252
+ printResult({ json, data: { ok: child.exitCode === 0, exitCode: child.exitCode, name } });
253
+ } else if (child.exitCode && child.exitCode !== 0) {
254
+ process.exit(child.exitCode);
255
+ }
256
+ }
257
+
258
+ async function cmdDevKey({ argv, json }) {
259
+ const { flags, kv } = parseArgs(argv);
260
+
261
+ // parseArgs currently only supports --k=v, but UX/docs commonly use: --k "value".
262
+ // Support both forms here (without changing global parsing semantics).
263
+ const argvKvValue = (name) => {
264
+ const n = String(name ?? '').trim();
265
+ if (!n) return '';
266
+ for (let i = 0; i < argv.length; i += 1) {
267
+ const a = String(argv[i] ?? '');
268
+ if (a === n) {
269
+ const next = String(argv[i + 1] ?? '');
270
+ if (next && !next.startsWith('--')) return next;
271
+ return '';
272
+ }
273
+ if (a.startsWith(`${n}=`)) {
274
+ return a.slice(`${n}=`.length);
275
+ }
276
+ }
277
+ return '';
278
+ };
279
+
280
+ const wantPrint = flags.has('--print');
281
+ const fmtRaw = (argvKvValue('--format') || (kv.get('--format') ?? '')).trim();
282
+ // UX: the Happy UI restore screen expects the "backup" (XXXXX-...) format.
283
+ //
284
+ // IMPORTANT: the Happy restore screen treats any key containing '-' as "backup format",
285
+ // so printing a base64url key (which may contain '-') is *not reliably pasteable*.
286
+ // Default to backup always unless explicitly overridden.
287
+ const fmt = fmtRaw || 'backup'; // base64url | backup
288
+ const set = (argvKvValue('--set') || (kv.get('--set') ?? '')).trim();
289
+ const clear = flags.has('--clear');
290
+
291
+ if (set) {
292
+ const res = await writeDevAuthKey({ env: process.env, input: set });
293
+ if (json) {
294
+ printResult({ json, data: { ok: true, action: 'set', path: res.path } });
295
+ return;
296
+ }
297
+ // eslint-disable-next-line no-console
298
+ console.log('');
299
+ // eslint-disable-next-line no-console
300
+ console.log(banner('auth dev-key', { subtitle: 'Saved locally (never committed).' }));
301
+ // eslint-disable-next-line no-console
302
+ console.log(bullets([ok(kv('path:', res.path))]));
303
+ return;
304
+ }
305
+ if (clear) {
306
+ const res = await clearDevAuthKey({ env: process.env });
307
+ if (json) {
308
+ printResult({ json, data: { ok: res.ok, action: 'clear', ...res } });
309
+ return;
310
+ }
311
+ // eslint-disable-next-line no-console
312
+ console.log('');
313
+ // eslint-disable-next-line no-console
314
+ console.log(banner('auth dev-key', { subtitle: 'Local dev key state.' }));
315
+ // eslint-disable-next-line no-console
316
+ console.log(
317
+ bullets([
318
+ res.deleted ? ok(`removed ${dim(`(${res.path})`)}`) : warn(`not set ${dim(`(${res.path})`)}`),
319
+ ])
320
+ );
321
+ return;
322
+ }
323
+
324
+ const out = await readDevAuthKey({ env: process.env });
325
+ if (!out.ok) {
326
+ throw new Error(`[auth] dev-key: ${out.error ?? 'failed'}`);
327
+ }
328
+ if (!out.secretKeyBase64Url) {
329
+ if (json) {
330
+ printResult({ json, data: { ok: false, error: 'missing_dev_key', file: out.path ?? null } });
331
+ } else {
332
+ // eslint-disable-next-line no-console
333
+ console.log('');
334
+ // eslint-disable-next-line no-console
335
+ console.log(banner('auth dev-key', { subtitle: 'Not configured.' }));
336
+ // eslint-disable-next-line no-console
337
+ console.log('');
338
+ // eslint-disable-next-line no-console
339
+ console.log(sectionTitle('How to set it'));
340
+ // eslint-disable-next-line no-console
341
+ console.log(
342
+ bullets([
343
+ `${dim('save locally:')} ${cmdFmt('hstack auth dev-key --set "<base64url-secret-or-backup-format>"')}`,
344
+ `${dim('or export for this shell:')} export HAPPIER_STACK_DEV_AUTH_SECRET_KEY="<base64url-secret>"`,
345
+ ])
346
+ );
347
+ if (out.path) {
348
+ // eslint-disable-next-line no-console
349
+ console.log('');
350
+ // eslint-disable-next-line no-console
351
+ console.log(dim(`Path: ${out.path}`));
352
+ }
353
+ }
354
+ process.exit(1);
355
+ }
356
+
357
+ const value = fmt === 'backup' ? out.backup : out.secretKeyBase64Url;
358
+ if (wantPrint) {
359
+ process.stdout.write(value + '\n');
360
+ return;
361
+ }
362
+ if (json) {
363
+ printResult({ json, data: { ok: true, key: value, format: fmt, source: out.source ?? null } });
364
+ return;
365
+ }
366
+ // eslint-disable-next-line no-console
367
+ console.log('');
368
+ // eslint-disable-next-line no-console
369
+ console.log(banner('auth dev-key', { subtitle: 'Local dev key (use --print for raw output).' }));
370
+ // eslint-disable-next-line no-console
371
+ console.log(bullets([kv('format:', cyan(fmt)), kv('source:', out.source ?? 'unknown')]));
372
+ // eslint-disable-next-line no-console
373
+ console.log('');
374
+ // eslint-disable-next-line no-console
375
+ console.log(value);
376
+ }
377
+
378
+ async function runNodeCapture({ cwd, env, args, stdin }) {
379
+ return await new Promise((resolvePromise, rejectPromise) => {
380
+ const child = spawn(process.execPath, args, {
381
+ cwd,
382
+ env,
383
+ stdio: ['pipe', 'pipe', 'pipe'],
384
+ });
385
+ let stdout = '';
386
+ let stderr = '';
387
+ child.stdout.on('data', (d) => {
388
+ stdout += String(d);
389
+ });
390
+ child.stderr.on('data', (d) => {
391
+ stderr += String(d);
392
+ });
393
+ child.on('error', (err) => rejectPromise(err));
394
+ child.on('close', (code) => {
395
+ if (code === 0) {
396
+ resolvePromise({ stdout, stderr });
397
+ return;
398
+ }
399
+ rejectPromise(new Error(`node exited with ${code}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
400
+ });
401
+ if (stdin != null) {
402
+ child.stdin.write(String(stdin));
403
+ }
404
+ child.stdin.end();
405
+ });
406
+ }
407
+
408
+ function resolveServerComponentFromEnv(env) {
409
+ const v = (env.HAPPIER_STACK_SERVER_COMPONENT ?? 'happier-server-light').trim() || 'happier-server-light';
410
+ return v === 'happier-server' ? 'happier-server' : 'happier-server-light';
411
+ }
412
+
413
+ function resolvePostgresDatabaseUrlFromEnvOrThrow({ env, stackName, label }) {
414
+ const v = (env.DATABASE_URL ?? '').toString().trim();
415
+ if (!v) throw new Error(`[auth] missing DATABASE_URL for ${label || `stack "${stackName}"`}`);
416
+ const lower = v.toLowerCase();
417
+ const ok = lower.startsWith('postgresql://') || lower.startsWith('postgres://');
418
+ if (!ok) {
419
+ throw new Error(
420
+ `[auth] invalid DATABASE_URL for ${label || `stack "${stackName}"`}: expected postgresql://... (got ${JSON.stringify(v)})`
421
+ );
422
+ }
423
+ return v;
424
+ }
425
+
426
+ function resolveDatabaseUrlFromEnvOrThrow({ env, stackName, label, provider }) {
427
+ const v = (env.DATABASE_URL ?? '').toString().trim();
428
+ if (!v) throw new Error(`[auth] missing DATABASE_URL for ${label || `stack "${stackName}"`}`);
429
+ const lower = v.toLowerCase();
430
+ const p = String(provider ?? '').trim().toLowerCase();
431
+ if (p === 'mysql') {
432
+ const ok = lower.startsWith('mysql://') || lower.startsWith('mysqls://') || lower.startsWith('mariadb://');
433
+ if (!ok) {
434
+ throw new Error(
435
+ `[auth] invalid DATABASE_URL for ${label || `stack "${stackName}"`}: expected mysql://... (got ${JSON.stringify(v)})`
436
+ );
437
+ }
438
+ return v;
439
+ }
440
+ // Default: postgres (also covers pglite socket URLs).
441
+ return resolvePostgresDatabaseUrlFromEnvOrThrow({ env, stackName, label });
442
+ }
443
+
444
+ function resolveLightDirsForStack({ env, baseDir }) {
445
+ const dataDir = (env.HAPPIER_SERVER_LIGHT_DATA_DIR ?? env.HAPPY_SERVER_LIGHT_DATA_DIR ?? '').toString().trim() || join(baseDir, 'server-light');
446
+ const filesDir = (env.HAPPIER_SERVER_LIGHT_FILES_DIR ?? env.HAPPY_SERVER_LIGHT_FILES_DIR ?? '').toString().trim() || join(dataDir, 'files');
447
+ const dbDir = (env.HAPPIER_SERVER_LIGHT_DB_DIR ?? env.HAPPY_SERVER_LIGHT_DB_DIR ?? '').toString().trim() || join(dataDir, 'pglite');
448
+ return { dataDir, filesDir, dbDir };
449
+ }
450
+
451
+ function resolveDbProviderForLightFromEnv(env) {
452
+ const raw = (env.HAPPIER_DB_PROVIDER ?? env.HAPPY_DB_PROVIDER ?? '').toString().trim().toLowerCase();
453
+ if (raw === 'sqlite') return 'sqlite';
454
+ // Default for light flavor (embedded Postgres via pglite).
455
+ return 'pglite';
456
+ }
457
+
458
+ function resolveDbProviderForFullFromEnv(env) {
459
+ const raw = (env.HAPPIER_DB_PROVIDER ?? env.HAPPY_DB_PROVIDER ?? '').toString().trim().toLowerCase();
460
+ if (raw === 'mysql') return 'mysql';
461
+ return 'postgres';
462
+ }
463
+
464
+ function resolveSqliteDatabaseUrlForLight({ dataDir }) {
465
+ return `file:${join(dataDir, 'happier-server-light.sqlite')}`;
466
+ }
467
+
468
+ async function ensureLightMigrationsApplied({ serverDir, baseDir, envIn, quiet = false }) {
469
+ // IMPORTANT: envIn is often parsed from a stack env file (so it does not include PATH).
470
+ // Start from the current process env so we can spawn Yarn reliably, then overlay stack-specific vars.
471
+ const env = { ...process.env, ...(envIn && typeof envIn === 'object' ? envIn : {}) };
472
+ const { dataDir, filesDir, dbDir } = resolveLightDirsForStack({ env, baseDir });
473
+ env.HAPPIER_SERVER_LIGHT_DATA_DIR = dataDir;
474
+ env.HAPPIER_SERVER_LIGHT_FILES_DIR = filesDir;
475
+ env.HAPPIER_SERVER_LIGHT_DB_DIR = dbDir;
476
+ env.HAPPY_SERVER_LIGHT_DATA_DIR = env.HAPPY_SERVER_LIGHT_DATA_DIR ?? dataDir;
477
+ env.HAPPY_SERVER_LIGHT_FILES_DIR = env.HAPPY_SERVER_LIGHT_FILES_DIR ?? filesDir;
478
+ env.HAPPY_SERVER_LIGHT_DB_DIR = env.HAPPY_SERVER_LIGHT_DB_DIR ?? dbDir;
479
+
480
+ const provider = resolveDbProviderForLightFromEnv(env);
481
+ env.HAPPIER_DB_PROVIDER = env.HAPPIER_DB_PROVIDER ?? provider;
482
+
483
+ // Migration step:
484
+ // - pglite: spins a temporary pglite socket and runs prisma migrate deploy against prisma/schema.prisma
485
+ // - sqlite: runs migrate:sqlite:deploy against prisma/sqlite/schema.prisma
486
+ //
487
+ // Both are idempotent and safe to re-run when the light DB is not held open.
488
+ const envWithCache = await applyStackCacheEnv(env);
489
+ const migrateScript = provider === 'sqlite' ? 'migrate:sqlite:deploy' : 'migrate:light:deploy';
490
+ await run('yarn', ['-s', migrateScript], { cwd: serverDir, env: envWithCache, stdio: quiet ? 'ignore' : 'inherit' });
491
+ return { dataDir, filesDir, dbDir };
492
+ }
493
+
494
+ async function listAccountsFromPglite({ cwd, dbDir }) {
495
+ const lockModuleUrl = new URL('./utils/pglite_lock.mjs', import.meta.url).toString();
496
+ const script = `
497
+ process.on('uncaughtException', (e) => {
498
+ console.error(e instanceof Error ? e.message : String(e));
499
+ process.exit(1);
500
+ });
501
+ process.on('unhandledRejection', (e) => {
502
+ console.error(e instanceof Error ? e.message : String(e));
503
+ process.exit(1);
504
+ });
505
+ const DB_DIR = ${JSON.stringify(dbDir)};
506
+ const { acquirePgliteDirLock } = await import(${JSON.stringify(lockModuleUrl)});
507
+ const releaseLock = await acquirePgliteDirLock(DB_DIR, { purpose: 'auth:listAccountsFromPglite' });
508
+ const { PGlite } = await import('@electric-sql/pglite');
509
+ const { PGLiteSocketServer } = await import('@electric-sql/pglite-socket');
510
+ const { PrismaClient } = await import('@prisma/client');
511
+ const pglite = new PGlite(DB_DIR);
512
+ try {
513
+ await pglite.waitReady;
514
+ const server = new PGLiteSocketServer({ db: pglite, host: '127.0.0.1', port: 0 });
515
+ await server.start();
516
+ try {
517
+ const raw = server.getServerConn();
518
+ const url = (() => {
519
+ try { return new URL(raw); } catch { return new URL(\`postgresql://postgres@\${raw}/postgres?sslmode=disable\`); }
520
+ })();
521
+ url.searchParams.set('connection_limit', '1');
522
+ process.env.DATABASE_URL = url.toString();
523
+ const db = new PrismaClient();
524
+ try {
525
+ const accounts = await db.account.findMany({ select: { id: true, publicKey: true } });
526
+ console.log(JSON.stringify(accounts));
527
+ } finally {
528
+ await db.$disconnect();
529
+ }
530
+ } finally {
531
+ await server.stop();
532
+ }
533
+ } finally {
534
+ await pglite.close();
535
+ await releaseLock().catch(() => {});
536
+ }
537
+ `.trim();
538
+
539
+ const { stdout } = await runNodeCapture({
540
+ cwd,
541
+ env: process.env,
542
+ args: ['--input-type=module', '-e', script],
543
+ });
544
+ return stdout.trim() ? JSON.parse(stdout.trim()) : [];
545
+ }
546
+
547
+ async function insertAccountsIntoPglite({ cwd, dbDir, accounts, force }) {
548
+ const lockModuleUrl = new URL('./utils/pglite_lock.mjs', import.meta.url).toString();
549
+ const script = `
550
+ process.on('uncaughtException', (e) => {
551
+ console.error(e instanceof Error ? e.message : String(e));
552
+ process.exit(1);
553
+ });
554
+ process.on('unhandledRejection', (e) => {
555
+ console.error(e instanceof Error ? e.message : String(e));
556
+ process.exit(1);
557
+ });
558
+ const { PGlite } = await import('@electric-sql/pglite');
559
+ const { PGLiteSocketServer } = await import('@electric-sql/pglite-socket');
560
+ const { PrismaClient } = await import('@prisma/client');
561
+ import fs from 'node:fs';
562
+ const DB_DIR = ${JSON.stringify(dbDir)};
563
+ const { acquirePgliteDirLock } = await import(${JSON.stringify(lockModuleUrl)});
564
+ const releaseLock = await acquirePgliteDirLock(DB_DIR, { purpose: 'auth:insertAccountsIntoPglite' });
565
+ const FORCE = ${force ? 'true' : 'false'};
566
+ const raw = fs.readFileSync(0, 'utf8').trim();
567
+ const accounts = raw ? JSON.parse(raw) : [];
568
+ const pglite = new PGlite(DB_DIR);
569
+ try {
570
+ await pglite.waitReady;
571
+ const server = new PGLiteSocketServer({ db: pglite, host: '127.0.0.1', port: 0 });
572
+ await server.start();
573
+ try {
574
+ const rawConn = server.getServerConn();
575
+ const url = (() => {
576
+ try { return new URL(rawConn); } catch { return new URL(\`postgresql://postgres@\${rawConn}/postgres?sslmode=disable\`); }
577
+ })();
578
+ url.searchParams.set('connection_limit', '1');
579
+ process.env.DATABASE_URL = url.toString();
580
+ const db = new PrismaClient();
581
+ try {
582
+ let insertedCount = 0;
583
+ for (const a of accounts) {
584
+ // eslint-disable-next-line no-await-in-loop
585
+ try {
586
+ await db.account.upsert({
587
+ where: { id: a.id },
588
+ update: { publicKey: a.publicKey },
589
+ create: { id: a.id, publicKey: a.publicKey },
590
+ });
591
+ insertedCount += 1;
592
+ } catch (e) {
593
+ // Prisma unique constraint violation (most commonly: publicKey already exists on another id).
594
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'P2002') {
595
+ const existing = await db.account.findUnique({ where: { publicKey: a.publicKey }, select: { id: true } });
596
+ if (existing?.id && existing.id !== a.id) {
597
+ if (!FORCE) {
598
+ throw new Error(
599
+ \`account publicKey conflict: target already has publicKey for id=\${existing.id}, but seed wants id=\${a.id}. Re-run with --force to replace the conflicting account row.\`
600
+ );
601
+ }
602
+ await db.account.delete({ where: { publicKey: a.publicKey } });
603
+ await db.account.upsert({
604
+ where: { id: a.id },
605
+ update: { publicKey: a.publicKey },
606
+ create: { id: a.id, publicKey: a.publicKey },
607
+ });
608
+ insertedCount += 1;
609
+ continue;
610
+ }
611
+ // If we can't attribute the constraint to a publicKey conflict, treat it as "already seeded".
612
+ continue;
613
+ }
614
+ throw e;
615
+ }
616
+ }
617
+ console.log(JSON.stringify({ sourceCount: accounts.length, insertedCount }));
618
+ } finally {
619
+ await db.$disconnect();
620
+ }
621
+ } finally {
622
+ await server.stop();
623
+ }
624
+ } finally {
625
+ await pglite.close();
626
+ await releaseLock().catch(() => {});
627
+ }
628
+ `.trim();
629
+
630
+ const { stdout } = await runNodeCapture({
631
+ cwd,
632
+ env: process.env,
633
+ args: ['--input-type=module', '-e', script],
634
+ stdin: JSON.stringify(accounts),
635
+ });
636
+ const res = stdout.trim() ? JSON.parse(stdout.trim()) : { sourceCount: accounts.length, insertedCount: 0 };
637
+ return {
638
+ ok: true,
639
+ sourceCount: Number(res.sourceCount ?? accounts.length) || 0,
640
+ insertedCount: Number(res.insertedCount ?? 0) || 0,
641
+ };
642
+ }
643
+
644
+ async function listAccountsFromPostgres({ cwd, clientImport, databaseUrl }) {
645
+ const script = `
646
+ process.on('uncaughtException', (e) => {
647
+ console.error(e instanceof Error ? e.message : String(e));
648
+ process.exit(1);
649
+ });
650
+ process.on('unhandledRejection', (e) => {
651
+ console.error(e instanceof Error ? e.message : String(e));
652
+ process.exit(1);
653
+ });
654
+ const mod = await import(${JSON.stringify(clientImport)});
655
+ const PrismaClient = mod?.PrismaClient ?? mod?.default?.PrismaClient;
656
+ if (!PrismaClient) throw new Error('Failed to load PrismaClient for DB seed (source).');
657
+ const db = new PrismaClient();
658
+ try {
659
+ const accounts = await db.account.findMany({ select: { id: true, publicKey: true } });
660
+ console.log(JSON.stringify(accounts));
661
+ } finally {
662
+ await db.$disconnect();
663
+ }
664
+ `.trim();
665
+
666
+ const { stdout } = await runNodeCapture({
667
+ cwd,
668
+ env: { ...process.env, DATABASE_URL: databaseUrl },
669
+ args: ['--input-type=module', '-e', script],
670
+ });
671
+ return stdout.trim() ? JSON.parse(stdout.trim()) : [];
672
+ }
673
+
674
+ async function insertAccountsIntoPostgres({ cwd, clientImport, databaseUrl, accounts, force }) {
675
+ const script = `
676
+ process.on('uncaughtException', (e) => {
677
+ console.error(e instanceof Error ? e.message : String(e));
678
+ process.exit(1);
679
+ });
680
+ process.on('unhandledRejection', (e) => {
681
+ console.error(e instanceof Error ? e.message : String(e));
682
+ process.exit(1);
683
+ });
684
+ const mod = await import(${JSON.stringify(clientImport)});
685
+ const PrismaClient = mod?.PrismaClient ?? mod?.default?.PrismaClient;
686
+ if (!PrismaClient) throw new Error('Failed to load PrismaClient for DB seed (target).');
687
+ import fs from 'node:fs';
688
+ const FORCE = ${force ? 'true' : 'false'};
689
+ const raw = fs.readFileSync(0, 'utf8').trim();
690
+ const accounts = raw ? JSON.parse(raw) : [];
691
+ const db = new PrismaClient();
692
+ try {
693
+ let insertedCount = 0;
694
+ for (const a of accounts) {
695
+ // eslint-disable-next-line no-await-in-loop
696
+ try {
697
+ await db.account.upsert({
698
+ where: { id: a.id },
699
+ update: { publicKey: a.publicKey },
700
+ create: { id: a.id, publicKey: a.publicKey },
701
+ });
702
+ insertedCount += 1;
703
+ } catch (e) {
704
+ // Prisma unique constraint violation (most commonly: publicKey already exists on another id).
705
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'P2002') {
706
+ const existing = await db.account.findUnique({ where: { publicKey: a.publicKey }, select: { id: true } });
707
+ if (existing?.id && existing.id !== a.id) {
708
+ if (!FORCE) {
709
+ throw new Error(
710
+ \`account publicKey conflict: target already has publicKey for id=\${existing.id}, but seed wants id=\${a.id}. Re-run with --force to replace the conflicting account row.\`
711
+ );
712
+ }
713
+ await db.account.delete({ where: { publicKey: a.publicKey } });
714
+ await db.account.upsert({
715
+ where: { id: a.id },
716
+ update: { publicKey: a.publicKey },
717
+ create: { id: a.id, publicKey: a.publicKey },
718
+ });
719
+ insertedCount += 1;
720
+ continue;
721
+ }
722
+ // If we can't attribute the constraint to a publicKey conflict, treat it as "already seeded".
723
+ continue;
724
+ }
725
+ throw e;
726
+ }
727
+ }
728
+ console.log(JSON.stringify({ sourceCount: accounts.length, insertedCount }));
729
+ } finally {
730
+ await db.$disconnect();
731
+ }
732
+ `.trim();
733
+
734
+ const { stdout } = await runNodeCapture({
735
+ cwd,
736
+ env: { ...process.env, DATABASE_URL: databaseUrl },
737
+ args: ['--input-type=module', '-e', script],
738
+ stdin: JSON.stringify(accounts),
739
+ });
740
+ const res = stdout.trim() ? JSON.parse(stdout.trim()) : { sourceCount: accounts.length, insertedCount: 0 };
741
+ return {
742
+ ok: true,
743
+ sourceCount: Number(res.sourceCount ?? accounts.length) || 0,
744
+ insertedCount: Number(res.insertedCount ?? 0) || 0,
745
+ };
746
+ }
747
+
748
+ function resolveServerComponentDir({ rootDir, serverComponent }) {
749
+ return getComponentDir(rootDir, serverComponent === 'happier-server' ? 'happier-server' : 'happier-server-light');
750
+ }
751
+
752
+ async function seedAccountsFromSourceDbToTargetDb({
753
+ rootDir,
754
+ fromStackName,
755
+ fromServerComponent,
756
+ fromDatabaseUrl,
757
+ targetStackName,
758
+ targetServerComponent,
759
+ targetDatabaseUrl,
760
+ force = false,
761
+ }) {
762
+ const sourceCwd = resolveServerComponentDir({ rootDir, serverComponent: fromServerComponent });
763
+ const targetCwd = resolveServerComponentDir({ rootDir, serverComponent: targetServerComponent });
764
+
765
+ const sourceClientImport = resolvePrismaClientImportForServerComponent({
766
+ serverComponentName: fromServerComponent,
767
+ serverDir: sourceCwd,
768
+ });
769
+ const targetClientImport = resolvePrismaClientImportForServerComponent({
770
+ serverComponentName: targetServerComponent,
771
+ serverDir: targetCwd,
772
+ });
773
+
774
+ const listScript = `
775
+ process.on('uncaughtException', (e) => {
776
+ console.error(e instanceof Error ? e.message : String(e));
777
+ process.exit(1);
778
+ });
779
+ process.on('unhandledRejection', (e) => {
780
+ console.error(e instanceof Error ? e.message : String(e));
781
+ process.exit(1);
782
+ });
783
+ const mod = await import(${JSON.stringify(sourceClientImport)});
784
+ const PrismaClient = mod?.PrismaClient ?? mod?.default?.PrismaClient;
785
+ if (!PrismaClient) {
786
+ throw new Error('Failed to load PrismaClient for DB seed (source).');
787
+ }
788
+ const db = new PrismaClient();
789
+ try {
790
+ const accounts = await db.account.findMany({ select: { id: true, publicKey: true } });
791
+ console.log(JSON.stringify(accounts));
792
+ } finally {
793
+ await db.$disconnect();
794
+ }
795
+ `.trim();
796
+
797
+ const insertScript = `
798
+ process.on('uncaughtException', (e) => {
799
+ console.error(e instanceof Error ? e.message : String(e));
800
+ process.exit(1);
801
+ });
802
+ process.on('unhandledRejection', (e) => {
803
+ console.error(e instanceof Error ? e.message : String(e));
804
+ process.exit(1);
805
+ });
806
+ const mod = await import(${JSON.stringify(targetClientImport)});
807
+ const PrismaClient = mod?.PrismaClient ?? mod?.default?.PrismaClient;
808
+ if (!PrismaClient) {
809
+ throw new Error('Failed to load PrismaClient for DB seed (target).');
810
+ }
811
+ import fs from 'node:fs';
812
+ const FORCE = ${force ? 'true' : 'false'};
813
+ const raw = fs.readFileSync(0, 'utf8').trim();
814
+ const accounts = raw ? JSON.parse(raw) : [];
815
+ const db = new PrismaClient();
816
+ try {
817
+ let insertedCount = 0;
818
+ for (const a of accounts) {
819
+ // eslint-disable-next-line no-await-in-loop
820
+ try {
821
+ await db.account.create({ data: { id: a.id, publicKey: a.publicKey } });
822
+ insertedCount += 1;
823
+ } catch (e) {
824
+ // Prisma unique constraint violation
825
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'P2002') {
826
+ // Two common cases:
827
+ // - id already exists (fine)
828
+ // - publicKey already exists on a different id (auth mismatch -> machine FK failures later)
829
+ //
830
+ // For --force, we try to delete the conflicting row by publicKey and then retry insert.
831
+ // Without --force, fail-closed with a helpful error so users don't end up with "seeded" but broken stacks.
832
+ try {
833
+ const existing = await db.account.findUnique({ where: { publicKey: a.publicKey }, select: { id: true } });
834
+ if (existing?.id && existing.id !== a.id) {
835
+ if (!FORCE) {
836
+ throw new Error(
837
+ \`account publicKey conflict: target already has publicKey for id=\${existing.id}, but seed wants id=\${a.id}. Re-run with --force to replace the conflicting account row.\`
838
+ );
839
+ }
840
+ // Best-effort delete; will fail if other rows reference this account (then we fail closed).
841
+ await db.account.delete({ where: { publicKey: a.publicKey } });
842
+ await db.account.create({ data: { id: a.id, publicKey: a.publicKey } });
843
+ insertedCount += 1;
844
+ continue;
845
+ }
846
+ } catch (inner) {
847
+ throw inner;
848
+ }
849
+ continue;
850
+ }
851
+ throw e;
852
+ }
853
+ }
854
+ console.log(JSON.stringify({ sourceCount: accounts.length, insertedCount }));
855
+ } finally {
856
+ await db.$disconnect();
857
+ }
858
+ `.trim();
859
+
860
+ const { stdout: srcOut } = await runNodeCapture({
861
+ cwd: sourceCwd,
862
+ env: { ...process.env, DATABASE_URL: fromDatabaseUrl },
863
+ args: ['--input-type=module', '-e', listScript],
864
+ });
865
+ const accounts = srcOut.trim() ? JSON.parse(srcOut.trim()) : [];
866
+
867
+ const { stdout: insOut } = await runNodeCapture({
868
+ cwd: targetCwd,
869
+ env: { ...process.env, DATABASE_URL: targetDatabaseUrl },
870
+ args: ['--input-type=module', '-e', insertScript],
871
+ stdin: JSON.stringify(accounts),
872
+ });
873
+ const res = insOut.trim() ? JSON.parse(insOut.trim()) : { sourceCount: accounts.length, insertedCount: 0 };
874
+
875
+ return {
876
+ ok: true,
877
+ fromStackName,
878
+ targetStackName,
879
+ sourceCount: Number(res.sourceCount ?? accounts.length) || 0,
880
+ insertedCount: Number(res.insertedCount ?? 0) || 0,
881
+ };
882
+ }
883
+
884
+ async function cmdCopyFrom({ argv, json }) {
885
+ const rootDir = getRootDir(import.meta.url);
886
+ const stackName = getStackName();
887
+
888
+ const positionals = argv.filter((a) => !a.startsWith('--'));
889
+ const fromStackName = (positionals[1] ?? '').trim();
890
+ if (!fromStackName) {
891
+ throw new Error(
892
+ '[auth] usage: hstack stack auth <name> copy-from <sourceStack> [--force] [--with-infra] [--json] OR hstack auth copy-from <sourceStack> --all [--except=main,dev-auth] [--force] [--with-infra] [--json]\n' +
893
+ 'notes:\n' +
894
+ ' - sourceStack can be a stack name (e.g. main, dev-auth)'
895
+ );
896
+ }
897
+
898
+ const { flags, kv } = parseArgs(argv);
899
+ const all = flags.has('--all');
900
+ const force =
901
+ flags.has('--force') ||
902
+ flags.has('--overwrite') ||
903
+ (kv.get('--force') ?? '').trim() === '1' ||
904
+ (kv.get('--overwrite') ?? '').trim() === '1';
905
+ const withInfra =
906
+ flags.has('--with-infra') ||
907
+ flags.has('--ensure-infra') ||
908
+ flags.has('--infra') ||
909
+ (kv.get('--with-infra') ?? '').trim() === '1' ||
910
+ (kv.get('--ensure-infra') ?? '').trim() === '1';
911
+ const linkMode =
912
+ flags.has('--link') ||
913
+ flags.has('--symlink') ||
914
+ flags.has('--link-auth') ||
915
+ (kv.get('--link') ?? '').trim() === '1' ||
916
+ (kv.get('--symlink') ?? '').trim() === '1' ||
917
+ (kv.get('--auth-mode') ?? '').trim() === 'link' ||
918
+ (process.env.HAPPIER_STACK_AUTH_LINK ?? '').toString().trim() === '1' ||
919
+ (process.env.HAPPIER_STACK_AUTH_MODE ?? '').toString().trim() === 'link';
920
+ const allowMain = flags.has('--allow-main') || flags.has('--main-ok') || (kv.get('--allow-main') ?? '').trim() === '1';
921
+ const exceptRaw = (kv.get('--except') ?? '').trim();
922
+ const except = new Set(exceptRaw.split(',').map((s) => s.trim()).filter(Boolean));
923
+
924
+ if (all) {
925
+ // Global bulk operation (no stack context required).
926
+ const stacks = await listAllStackNames();
927
+ const results = [];
928
+ const totalTargets = stacks.filter((s) => !except.has(s) && s !== fromStackName).length;
929
+ let idx = 0;
930
+ const progress = (line) => {
931
+ // In JSON mode, never pollute stdout (reserved for final JSON).
932
+ // eslint-disable-next-line no-console
933
+ (json ? console.error : console.log)(line);
934
+ };
935
+
936
+ progress(
937
+ `[auth] copy-from --all: from=${fromStackName}${except.size ? ` (except=${[...except].join(',')})` : ''}${force ? ' (force)' : ''}${withInfra ? ' (with-infra)' : ''}`
938
+ );
939
+ for (const target of stacks) {
940
+ if (except.has(target)) {
941
+ progress(`- ↪ ${target}: skipped (excluded)`);
942
+ results.push({ stackName: target, ok: true, skipped: true, reason: 'excluded' });
943
+ continue;
944
+ }
945
+ if (target === fromStackName) {
946
+ progress(`- ↪ ${target}: skipped (source_stack)`);
947
+ results.push({ stackName: target, ok: true, skipped: true, reason: 'source_stack' });
948
+ continue;
949
+ }
950
+
951
+ idx += 1;
952
+ progress(`[auth] [${idx}/${totalTargets}] seeding stack "${target}"...`);
953
+
954
+ try {
955
+ const out = await runNodeCapture({
956
+ cwd: rootDir,
957
+ env: process.env,
958
+ args: [
959
+ join(rootDir, 'scripts', 'stack.mjs'),
960
+ 'auth',
961
+ target,
962
+ '--',
963
+ 'copy-from',
964
+ fromStackName,
965
+ '--json',
966
+ ...(force ? ['--force'] : []),
967
+ ...(withInfra ? ['--with-infra'] : []),
968
+ ...(linkMode ? ['--link'] : []),
969
+ ],
970
+ });
971
+ const parsed = out.stdout.trim() ? JSON.parse(out.stdout.trim()) : null;
972
+
973
+ const copied = parsed?.copied && typeof parsed.copied === 'object' ? parsed.copied : null;
974
+ const db = copied?.dbAccounts ? `db=${copied.dbAccounts.insertedCount}/${copied.dbAccounts.sourceCount}` : copied?.dbError ? `db=skipped` : `db=unknown`;
975
+ const secret = copied?.secret ? 'secret' : null;
976
+ const cli = copied?.accessKey || copied?.settings ? 'cli' : null;
977
+ const any = copied?.secret || copied?.accessKey || copied?.settings || copied?.db;
978
+ const summary = any ? `seeded (${[db, secret, cli].filter(Boolean).join(', ')})` : `noop (already has auth)`;
979
+ progress(`- ✅ ${target}: ${summary}`);
980
+ if (copied?.dbError) {
981
+ progress(` - db seed skipped: ${copied.dbError}`);
982
+ }
983
+
984
+ results.push({ stackName: target, ok: true, skipped: false, fromStackName, out: parsed });
985
+ } catch (e) {
986
+ const msg = e instanceof Error ? e.message : String(e);
987
+ progress(`- ❌ ${target}: failed`);
988
+ progress(` - ${msg}`);
989
+ results.push({ stackName: target, ok: false, skipped: false, fromStackName, error: msg });
990
+ }
991
+ }
992
+
993
+ const ok = results.every((r) => r.ok);
994
+ if (json) {
995
+ printResult({ json, data: { ok, fromStackName, results } });
996
+ return;
997
+ }
998
+ // (we already streamed progress above)
999
+ const failed = results.filter((r) => !r.ok).length;
1000
+ const skipped = results.filter((r) => r.ok && r.skipped).length;
1001
+ const seeded = results.filter((r) => r.ok && !r.skipped).length;
1002
+ // eslint-disable-next-line no-console
1003
+ console.log(`[auth] done: ok=${ok ? 'true' : 'false'} seeded=${seeded} skipped=${skipped} failed=${failed}`);
1004
+ if (!ok) process.exit(1);
1005
+ return;
1006
+ }
1007
+
1008
+ if (stackName === 'main' && !allowMain) {
1009
+ throw new Error(
1010
+ '[auth] copy-from is intended for stack-scoped usage (e.g. hstack stack auth <name> copy-from main), or pass --all.\n' +
1011
+ 'If you really intend to seed the main hstack install, re-run with: --allow-main'
1012
+ );
1013
+ }
1014
+
1015
+ const serverComponent = resolveServerComponentForCurrentStack();
1016
+ const serverDirForPrisma = resolveServerComponentDir({ rootDir, serverComponent });
1017
+ const targetBaseDir = getDefaultAutostartPaths().baseDir;
1018
+ const targetCli = resolveCliHomeDir();
1019
+ const targetServerLightDataDir =
1020
+ (process.env.HAPPIER_SERVER_LIGHT_DATA_DIR ?? '').trim() || join(targetBaseDir, 'server-light');
1021
+ const targetSecretFile =
1022
+ (process.env.HAPPIER_STACK_HANDY_MASTER_SECRET_FILE ?? '').trim() || join(targetBaseDir, 'happier-server', 'handy-master-secret.txt');
1023
+ const { secret, source } = await resolveHandyMasterSecretFromStack({
1024
+ stackName: fromStackName,
1025
+ requireStackExists: true,
1026
+ });
1027
+
1028
+ const copied = {
1029
+ secret: false,
1030
+ accessKey: false,
1031
+ settings: false,
1032
+ db: false,
1033
+ dbAccounts: null,
1034
+ dbError: null,
1035
+ sourceStack: fromStackName,
1036
+ stackName,
1037
+ };
1038
+
1039
+ const sourceBaseDir = resolveStackEnvPath(fromStackName).baseDir;
1040
+ const sourceEnvRaw = await readTextIfExists(resolveStackEnvPath(fromStackName).envPath);
1041
+ const sourceEnv = sourceEnvRaw ? parseEnvToObject(sourceEnvRaw) : {};
1042
+ const sourceCli = getCliHomeDirFromEnvOrDefault({ stackBaseDir: sourceBaseDir, env: sourceEnv });
1043
+
1044
+ const resolveStackInternalServerUrlForAuth = async ({ stackName: name, env, defaultPort }) => {
1045
+ const runtimeStatePath = getStackRuntimeStatePath(name);
1046
+ try {
1047
+ const st = await readStackRuntimeStateFile(runtimeStatePath);
1048
+ const ownerPid = Number(st?.ownerPid);
1049
+ const runtimeOwnerAlive = Number.isFinite(ownerPid) && ownerPid > 1 ? isRuntimePidAlive(ownerPid) : false;
1050
+ const port = Number(st?.ports?.server);
1051
+ if (runtimeOwnerAlive && Number.isFinite(port) && port > 0) {
1052
+ return `http://127.0.0.1:${port}`;
1053
+ }
1054
+ } catch {
1055
+ // ignore; fall back to env/default port
1056
+ }
1057
+ const { internalServerUrl } = getInternalServerUrl({ env, defaultPort });
1058
+ return internalServerUrl;
1059
+ };
1060
+
1061
+ // IMPORTANT:
1062
+ // Stack auth now uses stable server IDs (`HAPPIER_ACTIVE_SERVER_ID`) which are not persisted in stack env files.
1063
+ // Reconstruct the stable scope ID here so copy-from reads the same source credential path that login wrote.
1064
+ // copy-from must use stack-stable credential scope even if the caller shell leaked
1065
+ // rollback flags like HAPPIER_STACK_DISABLE_STABLE_SCOPE=1.
1066
+ const sourceScopeEnv = { ...process.env, ...sourceEnv };
1067
+ delete sourceScopeEnv.HAPPIER_STACK_DISABLE_STABLE_SCOPE;
1068
+ const sourceEnvScoped = applyStackActiveServerScopeEnv({
1069
+ env: sourceScopeEnv,
1070
+ stackName: fromStackName,
1071
+ cliIdentity: 'default',
1072
+ });
1073
+ const sourceInternalServerUrl = await resolveStackInternalServerUrlForAuth({
1074
+ stackName: fromStackName,
1075
+ env: sourceEnvScoped,
1076
+ defaultPort: 3005,
1077
+ });
1078
+ const sourceCredentialPath =
1079
+ findExistingStackCredentialPath({ cliHomeDir: sourceCli, serverUrl: sourceInternalServerUrl, env: sourceEnvScoped }) ||
1080
+ findAnyCredentialPathInCliHome({ cliHomeDir: sourceCli });
1081
+ const targetEnv = process.env;
1082
+ const targetScopeEnv = { ...targetEnv };
1083
+ delete targetScopeEnv.HAPPIER_STACK_DISABLE_STABLE_SCOPE;
1084
+ const targetEnvScoped = applyStackActiveServerScopeEnv({
1085
+ env: targetScopeEnv,
1086
+ stackName,
1087
+ cliIdentity: 'default',
1088
+ });
1089
+ const { url: targetInternalServerUrl } = getInternalServerUrlCompat();
1090
+ const targetCredentialPaths = resolveStackCredentialPaths({
1091
+ cliHomeDir: targetCli,
1092
+ serverUrl: targetInternalServerUrl,
1093
+ env: targetEnvScoped,
1094
+ });
1095
+ const fromServerComponent = resolveServerComponentFromEnv(sourceEnv);
1096
+ const targetServerComponent = resolveServerComponentFromEnv(targetEnv);
1097
+
1098
+ const sourceCwd = resolveServerComponentDir({ rootDir, serverComponent: fromServerComponent });
1099
+ const targetCwd = resolveServerComponentDir({ rootDir, serverComponent: targetServerComponent });
1100
+ const sourceDbProvider =
1101
+ fromServerComponent === 'happier-server-light'
1102
+ ? resolveDbProviderForLightFromEnv(sourceEnv)
1103
+ : resolveDbProviderForFullFromEnv(sourceEnv);
1104
+ const targetDbProvider =
1105
+ targetServerComponent === 'happier-server-light'
1106
+ ? resolveDbProviderForLightFromEnv(targetEnv)
1107
+ : resolveDbProviderForFullFromEnv(targetEnv);
1108
+
1109
+ const sourceClientImport =
1110
+ fromServerComponent === 'happier-server-light'
1111
+ ? sourceDbProvider === 'sqlite'
1112
+ ? resolvePrismaClientImportForDbProvider({ serverDir: sourceCwd, provider: 'sqlite' })
1113
+ : resolvePrismaClientImportForServerComponent({ serverComponentName: fromServerComponent, serverDir: sourceCwd })
1114
+ : resolvePrismaClientImportForDbProvider({ serverDir: sourceCwd, provider: sourceDbProvider });
1115
+ const targetClientImport =
1116
+ targetServerComponent === 'happier-server-light'
1117
+ ? targetDbProvider === 'sqlite'
1118
+ ? resolvePrismaClientImportForDbProvider({ serverDir: targetCwd, provider: 'sqlite' })
1119
+ : resolvePrismaClientImportForServerComponent({ serverComponentName: targetServerComponent, serverDir: targetCwd })
1120
+ : resolvePrismaClientImportForDbProvider({ serverDir: targetCwd, provider: targetDbProvider });
1121
+
1122
+ const readSourceAccounts = async () => {
1123
+ if (fromServerComponent === 'happier-server-light') {
1124
+ const lightProvider = resolveDbProviderForLightFromEnv(sourceEnv);
1125
+ const { dataDir, dbDir } = await ensureLightMigrationsApplied({
1126
+ serverDir: sourceCwd,
1127
+ baseDir: sourceBaseDir,
1128
+ envIn: sourceEnv,
1129
+ quiet: json,
1130
+ });
1131
+ if (lightProvider === 'sqlite') {
1132
+ const url = resolveSqliteDatabaseUrlForLight({ dataDir });
1133
+ return await listAccountsFromPostgres({ cwd: sourceCwd, clientImport: sourceClientImport, databaseUrl: url });
1134
+ }
1135
+ return await listAccountsFromPglite({ cwd: sourceCwd, dbDir });
1136
+ }
1137
+ const fromDatabaseUrl = resolveDatabaseUrlFromEnvOrThrow({
1138
+ env: sourceEnv,
1139
+ stackName: fromStackName,
1140
+ label: `source stack "${fromStackName}"`,
1141
+ provider: sourceDbProvider,
1142
+ });
1143
+ return await listAccountsFromPostgres({ cwd: sourceCwd, clientImport: sourceClientImport, databaseUrl: fromDatabaseUrl });
1144
+ };
1145
+
1146
+ let sourceAccounts = null;
1147
+ const sourceTokenSubject = resolveJwtSubjectFromCredentialPath(sourceCredentialPath);
1148
+ let sourceTokenValidation = null;
1149
+ const sourceRuntimeOwnerAlive = await isStackRuntimeOwnerAlive(fromStackName);
1150
+ if (sourceCredentialPath && sourceRuntimeOwnerAlive) {
1151
+ sourceTokenValidation = await validateAuthTokenAgainstServer({
1152
+ credentialPath: sourceCredentialPath,
1153
+ internalServerUrl: sourceInternalServerUrl,
1154
+ });
1155
+ if (sourceTokenValidation.checked && sourceTokenValidation.valid === false) {
1156
+ const status = sourceTokenValidation.status != null ? sourceTokenValidation.status : 'error';
1157
+ const code = sourceTokenValidation.code ? `/${sourceTokenValidation.code}` : '';
1158
+ throw new Error(
1159
+ `[auth] source auth appears stale: source server rejected credential for stack "${fromStackName}" (${status}${code}). Re-auth the source stack and retry.`
1160
+ );
1161
+ }
1162
+ }
1163
+ const hasSourceDatabaseUrl = Boolean(String(sourceEnv.DATABASE_URL ?? '').trim());
1164
+ const canValidateSourceTokenSubject =
1165
+ !(sourceTokenValidation && sourceTokenValidation.checked && sourceTokenValidation.valid === true) &&
1166
+ Boolean(sourceTokenSubject) &&
1167
+ (fromServerComponent === 'happier-server-light' || hasSourceDatabaseUrl);
1168
+ if (canValidateSourceTokenSubject) {
1169
+ try {
1170
+ sourceAccounts = await readSourceAccounts();
1171
+ } catch (error) {
1172
+ const detail = error instanceof Error ? error.message : String(error);
1173
+ throw new Error(
1174
+ `[auth] source auth appears stale: unable to validate token subject "${sourceTokenSubject}" against source Account rows for stack "${fromStackName}" (${detail}). Re-auth the source stack and retry.`
1175
+ );
1176
+ }
1177
+ const hasMatchingSourceAccount = sourceAccounts.some((account) => String(account?.id ?? '') === sourceTokenSubject);
1178
+ if (!hasMatchingSourceAccount) {
1179
+ throw new Error(
1180
+ `[auth] source auth appears stale: token subject "${sourceTokenSubject}" is not present in source Account rows for stack "${fromStackName}". Re-auth the source stack and retry.`
1181
+ );
1182
+ }
1183
+ }
1184
+
1185
+ if (secret) {
1186
+ if (serverComponent === 'happier-server-light') {
1187
+ const target = join(targetServerLightDataDir, 'handy-master-secret.txt');
1188
+ const sourcePath = source && !String(source).includes('(HANDY_MASTER_SECRET)') ? String(source) : '';
1189
+ if (linkMode && sourcePath && existsSync(sourcePath)) {
1190
+ copied.secret = await linkFileIfMissing({ from: sourcePath, to: target, force });
1191
+ } else {
1192
+ copied.secret = await writeSecretFileIfMissing({ path: target, secret, force });
1193
+ }
1194
+ } else if (serverComponent === 'happier-server') {
1195
+ const sourcePath = source && !String(source).includes('(HANDY_MASTER_SECRET)') ? String(source) : '';
1196
+ if (linkMode && sourcePath && existsSync(sourcePath)) {
1197
+ copied.secret = await linkFileIfMissing({ from: sourcePath, to: targetSecretFile, force });
1198
+ } else {
1199
+ copied.secret = await writeSecretFileIfMissing({ path: targetSecretFile, secret, force });
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ if (linkMode) {
1205
+ const linkedLegacy = await linkFileIfMissing({
1206
+ from: sourceCredentialPath || '',
1207
+ to: targetCredentialPaths.legacyPath,
1208
+ force,
1209
+ });
1210
+ const linkedServerScoped = await linkFileIfMissing({
1211
+ from: sourceCredentialPath || '',
1212
+ to: targetCredentialPaths.serverScopedPath,
1213
+ force,
1214
+ });
1215
+ copied.accessKey = linkedLegacy || linkedServerScoped;
1216
+ copied.settings = await linkFileIfMissing({ from: join(sourceCli, 'settings.json'), to: join(targetCli, 'settings.json'), force });
1217
+ } else {
1218
+ const copiedLegacy = await copyFileIfMissing({
1219
+ from: sourceCredentialPath || '',
1220
+ to: targetCredentialPaths.legacyPath,
1221
+ mode: 0o600,
1222
+ force,
1223
+ });
1224
+ const copiedServerScoped = await copyFileIfMissing({
1225
+ from: sourceCredentialPath || '',
1226
+ to: targetCredentialPaths.serverScopedPath,
1227
+ mode: 0o600,
1228
+ force,
1229
+ });
1230
+ copied.accessKey = copiedLegacy || copiedServerScoped;
1231
+ copied.settings = await copyFileIfMissing({
1232
+ from: join(sourceCli, 'settings.json'),
1233
+ to: join(targetCli, 'settings.json'),
1234
+ mode: 0o600,
1235
+ force,
1236
+ });
1237
+ }
1238
+
1239
+ // Best-effort DB seeding: copy Account rows from source stack DB to target stack DB.
1240
+ // This avoids FK failures (e.g., Prisma P2003) when the target DB is fresh but the copied token
1241
+ // refers to an account ID that does not exist there yet.
1242
+ try {
1243
+ // Ensure prisma is runnable (best-effort). If deps aren't installed, we'll fall back to skipping DB seeding.
1244
+ // IMPORTANT: when running with --json, keep stdout clean (no yarn/prisma chatter).
1245
+ await ensureDepsInstalled(serverDirForPrisma, serverComponent, { quiet: json }).catch(() => {});
1246
+
1247
+ // 1) Read Account rows from the source DB.
1248
+ const accounts = sourceAccounts ?? (await readSourceAccounts());
1249
+
1250
+ // 2) Insert Account rows into the target DB.
1251
+ const runInsert = async () => {
1252
+ if (targetServerComponent === 'happier-server-light') {
1253
+ const lightProvider = resolveDbProviderForLightFromEnv(targetEnv);
1254
+ const { dataDir, dbDir } = await ensureLightMigrationsApplied({ serverDir: targetCwd, baseDir: targetBaseDir, envIn: targetEnv, quiet: json });
1255
+ if (lightProvider === 'sqlite') {
1256
+ const url = resolveSqliteDatabaseUrlForLight({ dataDir });
1257
+ return await insertAccountsIntoPostgres({
1258
+ cwd: targetCwd,
1259
+ clientImport: targetClientImport,
1260
+ databaseUrl: url,
1261
+ accounts,
1262
+ force,
1263
+ });
1264
+ }
1265
+ return await insertAccountsIntoPglite({ cwd: targetCwd, dbDir, accounts, force });
1266
+ }
1267
+
1268
+ let targetDatabaseUrl;
1269
+ try {
1270
+ targetDatabaseUrl = resolveDatabaseUrlFromEnvOrThrow({
1271
+ env: targetEnv,
1272
+ stackName,
1273
+ label: `target stack "${stackName}"`,
1274
+ provider: targetDbProvider,
1275
+ });
1276
+ } catch (e) {
1277
+ // For full server stacks, allow `copy-from --with-infra` to bring up Docker infra just-in-time
1278
+ // so we can seed DB accounts reliably.
1279
+ const managed = (targetEnv.HAPPIER_STACK_MANAGED_INFRA ?? '1').toString().trim() !== '0';
1280
+ if (targetServerComponent === 'happier-server' && targetDbProvider === 'postgres' && withInfra && managed) {
1281
+ const { port } = getInternalServerUrlCompat();
1282
+ const publicServerUrl = await preferStackLocalhostUrl(`http://localhost:${port}`, { stackName });
1283
+ const envPath = resolveStackEnvPath(stackName).envPath;
1284
+ const infra = await ensureHappyServerManagedInfra({
1285
+ stackName,
1286
+ baseDir: targetBaseDir,
1287
+ serverPort: port,
1288
+ publicServerUrl,
1289
+ envPath,
1290
+ env: targetEnv,
1291
+ quiet: json,
1292
+ // Auth seeding only needs Postgres; don't block on Minio bucket init.
1293
+ skipMinioInit: true,
1294
+ });
1295
+ targetDatabaseUrl = infra?.env?.DATABASE_URL ?? '';
1296
+ } else {
1297
+ throw e;
1298
+ }
1299
+ }
1300
+ if (!targetDatabaseUrl) {
1301
+ throw new Error(
1302
+ `[auth] missing DATABASE_URL for target stack "${stackName}". ` +
1303
+ (targetServerComponent === 'happier-server' ? `If this is a managed infra stack, re-run with --with-infra.` : '')
1304
+ );
1305
+ }
1306
+
1307
+ return await insertAccountsIntoPostgres({ cwd: targetCwd, clientImport: targetClientImport, databaseUrl: targetDatabaseUrl, accounts, force });
1308
+ };
1309
+
1310
+ const res = await (async () => {
1311
+ try {
1312
+ return await runInsert();
1313
+ } catch (e) {
1314
+ const msg = e instanceof Error ? e.message : String(e);
1315
+ const looksLikeMissingTable = msg.toLowerCase().includes('does not exist') || msg.toLowerCase().includes('no such table');
1316
+ if (!looksLikeMissingTable) throw e;
1317
+
1318
+ // Best-effort: apply schema, then retry once.
1319
+ if (targetServerComponent === 'happier-server-light') {
1320
+ await ensureLightMigrationsApplied({ serverDir: targetCwd, baseDir: targetBaseDir, envIn: targetEnv, quiet: json }).catch(() => {});
1321
+ } else if (targetServerComponent === 'happier-server') {
1322
+ await applyHappyServerMigrations({ serverDir: targetCwd, env: targetEnv, quiet: json }).catch(() => {});
1323
+ }
1324
+ return await runInsert();
1325
+ }
1326
+ })();
1327
+
1328
+ copied.dbAccounts = { sourceCount: res.sourceCount, insertedCount: res.insertedCount };
1329
+ copied.db = true;
1330
+ copied.dbError = null;
1331
+ } catch (err) {
1332
+ copied.db = false;
1333
+ copied.dbAccounts = null;
1334
+ copied.dbError = err instanceof Error ? err.message : String(err);
1335
+ if (!json) {
1336
+ console.warn(`[auth] db seed skipped: ${copied.dbError}`);
1337
+ }
1338
+ }
1339
+
1340
+ if (json) {
1341
+ printResult({ json, data: { ok: true, copied } });
1342
+ return;
1343
+ }
1344
+
1345
+ const any = copied.secret || copied.accessKey || copied.settings || copied.db;
1346
+ if (!any) {
1347
+ console.log(`[auth] nothing to copy (target already has auth files)`);
1348
+ return;
1349
+ }
1350
+
1351
+ console.log(`[auth] copied auth from "${fromStackName}" into "${stackName}" (no re-login needed)`);
1352
+ if (copied.secret) console.log(` - master secret: copied (${source || 'unknown source'})`);
1353
+ if (copied.dbAccounts) {
1354
+ console.log(` - db: seeded Account rows (inserted=${copied.dbAccounts.insertedCount}/${copied.dbAccounts.sourceCount})`);
1355
+ }
1356
+ if (copied.accessKey) console.log(` - cli: copied access.key`);
1357
+ if (copied.settings) console.log(` - cli: copied settings.json`);
1358
+ }
1359
+
1360
+ async function cmdStatus({ json }) {
1361
+ const rootDir = getRootDir(import.meta.url);
1362
+ const stackName = getStackName();
1363
+ const argv = process.argv.slice(2);
1364
+ const { kv } = parseArgs(argv);
1365
+ const identity = parseCliIdentityOrThrow((kv.get('--identity') ?? '').trim());
1366
+
1367
+ const { port, url: internalServerUrl } = getInternalServerUrlCompat();
1368
+ const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
1369
+ const { publicServerUrl } = await resolvePublicServerUrl({
1370
+ internalServerUrl,
1371
+ defaultPublicUrl,
1372
+ envPublicUrl,
1373
+ allowEnable: false,
1374
+ stackName,
1375
+ });
1376
+
1377
+ const cliHomeDir = resolveCliHomeDirForIdentity({ cliHomeDir: resolveCliHomeDir(), identity });
1378
+ const credentialPaths = resolveStackCredentialPaths({ cliHomeDir, serverUrl: internalServerUrl, env: process.env });
1379
+ const existingCredentialPath = findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalServerUrl, env: process.env });
1380
+ const settingsPath = join(cliHomeDir, 'settings.json');
1381
+
1382
+ const auth = {
1383
+ ok: Boolean(existingCredentialPath),
1384
+ accessKeyPath: existingCredentialPath || credentialPaths.legacyPath,
1385
+ accessKeyPaths: credentialPaths.paths,
1386
+ hasAccessKey: Boolean(existingCredentialPath),
1387
+ settingsPath,
1388
+ hasSettings: fileHasContent(settingsPath),
1389
+ serverValidation: {
1390
+ checked: false,
1391
+ valid: null,
1392
+ status: null,
1393
+ code: 'not-checked',
1394
+ error: null,
1395
+ },
1396
+ };
1397
+
1398
+ const daemon = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl });
1399
+ const healthRaw = await fetchHappierHealth(internalServerUrl);
1400
+ const health = {
1401
+ ok: Boolean(healthRaw.ok),
1402
+ status: healthRaw.status,
1403
+ body: healthRaw.text ? healthRaw.text.trim() : null,
1404
+ };
1405
+
1406
+ if (auth.hasAccessKey && health.ok) {
1407
+ auth.serverValidation = await validateAuthTokenAgainstServer({
1408
+ credentialPath: existingCredentialPath || credentialPaths.legacyPath,
1409
+ internalServerUrl,
1410
+ });
1411
+ } else if (auth.hasAccessKey && !health.ok) {
1412
+ auth.serverValidation = {
1413
+ checked: false,
1414
+ valid: null,
1415
+ status: null,
1416
+ code: 'server-unreachable',
1417
+ error: null,
1418
+ };
1419
+ }
1420
+
1421
+ const out = {
1422
+ stackName,
1423
+ internalServerUrl,
1424
+ publicServerUrl,
1425
+ cliHomeDir,
1426
+ cliIdentity: identity,
1427
+ auth,
1428
+ daemon,
1429
+ serverHealth: health,
1430
+ cliBin: join(getComponentDir(rootDir, 'happier-cli'), 'bin', 'happier.mjs'),
1431
+ };
1432
+
1433
+ if (json) {
1434
+ printResult({ json, data: out });
1435
+ return;
1436
+ }
1437
+
1438
+ const authLine = (() => {
1439
+ if (!auth.hasAccessKey) return '❌ auth: required';
1440
+ if (auth.serverValidation.checked && auth.serverValidation.valid === true) {
1441
+ return '✅ auth: ok (server-verified)';
1442
+ }
1443
+ if (auth.serverValidation.checked && auth.serverValidation.valid === false) {
1444
+ const code = auth.serverValidation.code ? `, ${auth.serverValidation.code}` : '';
1445
+ const status = auth.serverValidation.status != null ? auth.serverValidation.status : 'error';
1446
+ return `❌ auth: invalid (${status}${code})`;
1447
+ }
1448
+ if (!health.ok) return '⚠️ auth: present (server unreachable)';
1449
+ return '⚠️ auth: present (not verified)';
1450
+ })();
1451
+ const daemonLine =
1452
+ daemon.status === 'running'
1453
+ ? `✅ daemon: running (pid=${daemon.pid})`
1454
+ : daemon.status === 'starting'
1455
+ ? `⏳ daemon: starting (pid=${daemon.pid})`
1456
+ : daemon.status === 'stale_state'
1457
+ ? `⚠️ daemon: stale state file (pid=${daemon.pid} not running)`
1458
+ : daemon.status === 'stale_lock'
1459
+ ? `⚠️ daemon: stale lock file (pid=${daemon.pid} not running)`
1460
+ : daemon.status === 'bad_state'
1461
+ ? '⚠️ daemon: unreadable state'
1462
+ : '❌ daemon: not running';
1463
+
1464
+ const serverLine = health.ok ? `✅ server: healthy (${health.status})` : `⚠️ server: unreachable (${internalServerUrl})`;
1465
+
1466
+ console.log(`[auth] stack: ${stackName}`);
1467
+ console.log(`[auth] urls: internal=${internalServerUrl} public=${publicServerUrl}`);
1468
+ console.log(`[auth] cli: ${cliHomeDir}`);
1469
+ console.log('');
1470
+ console.log(authLine);
1471
+ if (!auth.ok) {
1472
+ console.log(` ↪ run: ${authLoginSuggestion(stackName)}`);
1473
+ const copyFromSeed = authCopyFromSeedSuggestion(stackName);
1474
+ if (copyFromSeed) {
1475
+ console.log(` ↪ or (recommended if your seed stack is already logged in): ${copyFromSeed}`);
1476
+ }
1477
+ } else if (auth.serverValidation.checked && auth.serverValidation.valid === false) {
1478
+ const copyFromSeed = authCopyFromSeedSuggestion(stackName);
1479
+ console.log(` ↪ run: ${authLoginSuggestion(stackName)} --force`);
1480
+ if (copyFromSeed) {
1481
+ console.log(` ↪ or reseed explicitly: ${copyFromSeed} --force`);
1482
+ }
1483
+ }
1484
+ console.log(daemonLine);
1485
+ console.log(serverLine);
1486
+ if (!health.ok) {
1487
+ const startHint = stackName === 'main' ? 'hstack dev' : `hstack stack dev ${stackName}`;
1488
+ console.log(` ↪ this stack does not appear to be running. Start it with: ${startHint}`);
1489
+ return;
1490
+ }
1491
+ if (auth.ok && daemon.status !== 'running') {
1492
+ console.log(` ↪ daemon is not running for this stack. If you expected it to be running, try: hstack doctor`);
1493
+ }
1494
+ }
1495
+
1496
+ async function cmdLogin({ argv, json }) {
1497
+ const rootDir = getRootDir(import.meta.url);
1498
+ const stackName = getStackName();
1499
+ const { flags, kv } = parseArgs(argv);
1500
+
1501
+ const tty = isTty();
1502
+ const { port, url: internalServerUrl } = getInternalServerUrlCompat();
1503
+ const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
1504
+ const { publicServerUrl } = await resolvePublicServerUrl({
1505
+ internalServerUrl,
1506
+ defaultPublicUrl,
1507
+ envPublicUrl,
1508
+ allowEnable: false,
1509
+ stackName,
1510
+ });
1511
+
1512
+ const webappModeRaw =
1513
+ (argvKvValue(argv, '--webapp') || (kv.get('--webapp') ?? '')).toString().trim().toLowerCase();
1514
+ const webappMode = webappModeRaw || 'auto'; // auto|stack|public|expo|hosted
1515
+ const explicitWebappUrl =
1516
+ (argvKvValue(argv, '--webapp-url') || (kv.get('--webapp-url') ?? '')).toString().trim();
1517
+ const methodRaw = (argvKvValue(argv, '--method') || (kv.get('--method') ?? '')).toString().trim().toLowerCase();
1518
+ const method = methodRaw === 'mobile' ? 'mobile' : methodRaw === 'web' || methodRaw === 'browser' ? 'web' : '';
1519
+ if (methodRaw && !method) {
1520
+ throw new Error(`[auth] login: invalid --method=${methodRaw} (expected: web|browser|mobile)`);
1521
+ }
1522
+
1523
+ const { envWebappUrl } = getWebappUrlEnvOverride({ env: process.env, stackName });
1524
+ const expoWebappUrl = await resolveWebappUrlFromRunningExpo({ rootDir, stackName });
1525
+
1526
+ let webappUrlRaw = '';
1527
+ let webappUrlSource = '';
1528
+ if (explicitWebappUrl) {
1529
+ webappUrlRaw = explicitWebappUrl;
1530
+ webappUrlSource = 'webapp-url flag';
1531
+ } else if (webappMode === 'public') {
1532
+ webappUrlRaw = publicServerUrl;
1533
+ webappUrlSource = 'public server';
1534
+ } else if (webappMode === 'hosted') {
1535
+ // Use happier-cli defaults (do not force a URL here).
1536
+ webappUrlRaw = '';
1537
+ webappUrlSource = 'cli default';
1538
+ } else if (webappMode === 'expo') {
1539
+ webappUrlRaw = expoWebappUrl || '';
1540
+ webappUrlSource = 'expo';
1541
+ } else {
1542
+ // auto|stack: preserve existing ordering for now (env override wins unless explicitly forced otherwise).
1543
+ webappUrlRaw = envWebappUrl || expoWebappUrl || publicServerUrl;
1544
+ webappUrlSource = envWebappUrl ? 'stack env override' : expoWebappUrl ? 'expo' : 'server';
1545
+ }
1546
+
1547
+ const webappUrl = webappUrlRaw ? await preferStackLocalhostUrl(webappUrlRaw, { stackName }) : '';
1548
+ const flowRaw = (argvKvValue(argv, '--flow') || (kv.get('--flow') ?? '')).toString().trim();
1549
+ if (flowRaw) {
1550
+ throw new Error('[auth] login: --flow is no longer supported (stack login is always guided)');
1551
+ }
1552
+ const flow = 'guided';
1553
+
1554
+ const identity = parseCliIdentityOrThrow((kv.get('--identity') ?? '').trim());
1555
+ const cliHomeDir = resolveCliHomeDirForIdentity({ cliHomeDir: resolveCliHomeDir(), identity });
1556
+ const cliBin = join(getComponentDir(rootDir, 'happier-cli'), 'bin', 'happier.mjs');
1557
+
1558
+ const force =
1559
+ argv.includes('--force') ||
1560
+ (kv.get('--force') ?? '').toString().trim() === '1';
1561
+ const wantPrint = argv.includes('--print');
1562
+ const noOpen = flags.has('--no-open') || flags.has('--no-browser') || flags.has('--no-browser-open');
1563
+
1564
+ const nodeArgs = [cliBin, 'auth', 'login'];
1565
+ if (force || argv.includes('--force')) {
1566
+ nodeArgs.push('--force');
1567
+ }
1568
+ if (noOpen) {
1569
+ nodeArgs.push('--no-open');
1570
+ }
1571
+ if (method) {
1572
+ nodeArgs.push('--method', method);
1573
+ }
1574
+
1575
+ let env = {
1576
+ ...process.env,
1577
+ HAPPIER_HOME_DIR: cliHomeDir,
1578
+ HAPPIER_SERVER_URL: internalServerUrl,
1579
+ HAPPIER_PUBLIC_SERVER_URL: publicServerUrl,
1580
+ ...(webappUrl ? { HAPPIER_WEBAPP_URL: webappUrl } : {}),
1581
+ ...(noOpen ? { HAPPIER_NO_BROWSER_OPEN: '1' } : {}),
1582
+ ...(method ? { HAPPIER_AUTH_METHOD: method } : {}),
1583
+ ...(force ? { HAPPIER_AUTH_FORCE: '1' } : {}),
1584
+ };
1585
+ env = applyStackActiveServerScopeEnv({ env, stackName, cliIdentity: identity });
1586
+
1587
+ if (wantPrint) {
1588
+ const cmd =
1589
+ `HAPPIER_HOME_DIR="${cliHomeDir}" ` +
1590
+ `HAPPIER_SERVER_URL="${internalServerUrl}" ` +
1591
+ `HAPPIER_PUBLIC_SERVER_URL="${publicServerUrl}" ` +
1592
+ (env.HAPPIER_ACTIVE_SERVER_ID ? `HAPPIER_ACTIVE_SERVER_ID="${env.HAPPIER_ACTIVE_SERVER_ID}" ` : '') +
1593
+ (webappUrl ? `HAPPIER_WEBAPP_URL="${webappUrl}" ` : '') +
1594
+ (noOpen ? `HAPPIER_NO_BROWSER_OPEN="1" ` : '') +
1595
+ (method ? `HAPPIER_AUTH_METHOD="${method}" ` : '') +
1596
+ `node "${cliBin}" auth login` +
1597
+ (nodeArgs.includes('--force') ? ' --force' : '') +
1598
+ (noOpen ? ' --no-open' : '') +
1599
+ (method ? ` --method ${method}` : '');
1600
+
1601
+ const configureServer =
1602
+ webappUrl && publicServerUrl
1603
+ ? buildConfigureServerLinks({ webappUrl, serverUrl: publicServerUrl })
1604
+ : webappUrl
1605
+ ? buildConfigureServerLinks({ webappUrl, serverUrl: internalServerUrl })
1606
+ : null;
1607
+ if (json) {
1608
+ printResult({
1609
+ json,
1610
+ data: {
1611
+ ok: true,
1612
+ flow,
1613
+ stackName,
1614
+ cliIdentity: identity,
1615
+ internalServerUrl,
1616
+ publicServerUrl,
1617
+ webappUrl,
1618
+ webappUrlSource,
1619
+ method: method || null,
1620
+ configureServer,
1621
+ cmd,
1622
+ },
1623
+ });
1624
+ } else {
1625
+ console.log(cmd);
1626
+ }
1627
+ return;
1628
+ }
1629
+
1630
+ if (json) {
1631
+ throw new Error('[auth] login: --json is supported only with --print');
1632
+ }
1633
+
1634
+ const shouldAutoStart = flags.has('--start-if-needed');
1635
+ const guidedReadyTimeoutMs = resolveGuidedServerReadyTimeoutMs(process.env);
1636
+ const waitForGuidedServerReadyOrThrow = async (reason) => {
1637
+ const ready = await waitForHappierHealthOk(internalServerUrl, {
1638
+ timeoutMs: guidedReadyTimeoutMs,
1639
+ intervalMs: 300,
1640
+ });
1641
+ if (!ready) {
1642
+ throw new Error(
1643
+ `[auth] ${stackName}: server did not become healthy in time (${guidedReadyTimeoutMs}ms) while ${reason}.\n` +
1644
+ `[auth] Start it manually:\n` +
1645
+ ` hstack stack dev ${stackName} --background`
1646
+ );
1647
+ }
1648
+ };
1649
+
1650
+ const health = await fetchHappierHealth(internalServerUrl);
1651
+ if (!health.ok) {
1652
+ const runtimeOwnerAlive = await isStackRuntimeOwnerAlive(stackName);
1653
+ const action = resolveGuidedStartAction({
1654
+ healthOk: false,
1655
+ runtimeOwnerAlive,
1656
+ autoStart: shouldAutoStart,
1657
+ });
1658
+
1659
+ if (action === 'wait') {
1660
+ // eslint-disable-next-line no-console
1661
+ console.error(`[auth] ${stackName}: stack runtime is already starting; waiting for health before guided login...`);
1662
+ await waitForGuidedServerReadyOrThrow('already starting');
1663
+ } else {
1664
+ let startOk = action === 'start';
1665
+ if (!startOk) {
1666
+ if (!tty) {
1667
+ throw new Error(
1668
+ `[auth] ${stackName}: cannot run guided login because the stack is not running in non-interactive mode.\n` +
1669
+ `[auth] Re-run with --start-if-needed or start it manually:\n` +
1670
+ ` hstack stack dev ${stackName} --background`
1671
+ );
1672
+ }
1673
+ startOk = await withRl(async (rl) => {
1674
+ const ans = (
1675
+ await prompt(rl, `[auth] ${stackName}: server is not running. Start the stack in background now? [Y/n] `, {
1676
+ defaultValue: 'y',
1677
+ })
1678
+ ).toLowerCase();
1679
+ return ans === 'y' || ans === 'yes' || ans === '';
1680
+ });
1681
+ }
1682
+ if (!startOk) {
1683
+ throw new Error(
1684
+ `[auth] ${stackName}: cannot run guided login because the stack is not running.\n` +
1685
+ `[auth] Start it with: hstack stack dev ${stackName} --background`
1686
+ );
1687
+ }
1688
+
1689
+ await run(
1690
+ process.execPath,
1691
+ [join(rootDir, 'scripts', 'stack.mjs'), 'dev', stackName, '--background', '--no-daemon', '--no-browser'],
1692
+ {
1693
+ cwd: rootDir,
1694
+ env: { ...process.env, HAPPIER_STACK_AUTH_FLOW: '1' },
1695
+ }
1696
+ ).catch((err) => {
1697
+ const msg =
1698
+ `[auth] ${stackName}: failed to start the stack for guided login.\n` +
1699
+ `[auth] Try starting it manually:\n` +
1700
+ ` hstack stack dev ${stackName} --background\n\n` +
1701
+ `${String(err?.stack ?? err)}`;
1702
+ throw new Error(msg);
1703
+ });
1704
+
1705
+ await waitForGuidedServerReadyOrThrow('starting in background');
1706
+ }
1707
+ }
1708
+
1709
+ const verbosity = getVerbosityLevel(process.env);
1710
+ const guidedEnv = applyStackActiveServerScopeEnv({
1711
+ env: { ...env, HAPPIER_STACK_AUTH_FLOW: '1' },
1712
+ stackName,
1713
+ cliIdentity: identity,
1714
+ });
1715
+ const guided = await runOrchestratedGuidedAuthFlow({
1716
+ rootDir,
1717
+ stackName,
1718
+ env: guidedEnv,
1719
+ verbosity,
1720
+ json: false,
1721
+ });
1722
+
1723
+ const daemonStart = await startDaemonPostAuth({
1724
+ rootDir,
1725
+ stackName,
1726
+ env: guidedEnv,
1727
+ forceRestart: true,
1728
+ webappUrl: guided?.webappUrl,
1729
+ });
1730
+ if (daemonStart?.ok === false) {
1731
+ // eslint-disable-next-line no-console
1732
+ console.error(daemonStart.error ?? `[auth] ${stackName}: post-auth daemon start verification timed out`);
1733
+ }
1734
+ return;
1735
+ }
1736
+
1737
+ async function main() {
1738
+ const argv = process.argv.slice(2);
1739
+ const helpSepIdx = argv.indexOf('--');
1740
+ const helpScopeArgv = helpSepIdx === -1 ? argv : argv.slice(0, helpSepIdx);
1741
+ const { flags } = parseArgs(helpScopeArgv);
1742
+ const json = wantsJson(helpScopeArgv, { flags });
1743
+
1744
+ const wantsHelpFlag = wantsHelp(helpScopeArgv, { flags });
1745
+ const explicitCmd = helpScopeArgv.find((a) => a && a !== '--' && !a.startsWith('-')) || '';
1746
+ const cmd = explicitCmd || (wantsHelpFlag ? 'help' : 'status');
1747
+
1748
+ const usageByCmd = new Map([
1749
+ ['status', 'hstack auth status [--json]'],
1750
+ ['login', 'hstack auth login [--identity=<name>] [--no-open] [--force] [--method=web|mobile] [--print] [--webapp=auto|stack|public|expo|hosted] [--webapp-url=<url>] [--start-if-needed] [--json]'],
1751
+ ['seed', 'hstack auth seed [name=dev-auth] [--login|--no-login] [--server=...] [--skip-default-seed] [--non-interactive] [--json]'],
1752
+ ['copy-from', 'hstack auth copy-from <sourceStack|legacy> --all [--except=main,dev-auth] [--force] [--with-infra] [--link] [--json]'],
1753
+ ['dev-key', 'hstack auth dev-key [--print] [--format=base64url|backup] [--set=<secret>] [--clear] [--json]'],
1754
+ ]);
1755
+
1756
+ if (wantsHelpFlag && cmd !== 'help') {
1757
+ const usage = usageByCmd.get(cmd);
1758
+ if (usage) {
1759
+ printResult({
1760
+ json,
1761
+ data: { ok: true, cmd, usage },
1762
+ text: [`[auth ${cmd}] usage:`, ` ${usage}`, '', 'see also:', ' hstack auth --help'].join('\n'),
1763
+ });
1764
+ return;
1765
+ }
1766
+ }
1767
+
1768
+ if (wantsHelpFlag || cmd === 'help') {
1769
+ printResult({
1770
+ json,
1771
+ data: { commands: ['status', 'login', 'seed', 'copy-from', 'dev-key'], stackScoped: 'hstack stack auth <name> status|login|copy-from' },
1772
+ text: [
1773
+ '',
1774
+ banner('auth', { subtitle: 'Login and auth seeding helpers for hstack.' }),
1775
+ '',
1776
+ sectionTitle('Usage (global)'),
1777
+ bullets([
1778
+ `${dim('status:')} ${cmdFmt('hstack auth status')} ${dim('[--json]')}`,
1779
+ `${dim('login:')} ${cmdFmt('hstack auth login')} ${dim('[--identity=<name>] [--no-open] [--force] [--method=web|mobile] [--print] [--webapp=auto|stack|public|expo|hosted] [--webapp-url=<url>] [--start-if-needed] [--json]')}`,
1780
+ `${dim('seed stack:')} ${cmdFmt('hstack auth seed')} ${dim('[name=dev-auth] [--login|--no-login] [--server=...] [--skip-default-seed] [--non-interactive] [--json]')}`,
1781
+ `${dim('seed:')} ${cmdFmt('hstack auth copy-from <sourceStack|legacy> --all')} ${dim('[--except=main,dev-auth] [--force] [--with-infra] [--link] [--json]')}`,
1782
+ `${dim('dev key:')} ${cmdFmt('hstack auth dev-key')} ${dim('[--print] [--format=base64url|backup] [--set=<secret>] [--clear] [--json]')}`,
1783
+ ]),
1784
+ '',
1785
+ sectionTitle('Usage (stack-scoped)'),
1786
+ bullets([
1787
+ `${dim('status:')} ${cmdFmt('hstack stack auth <name> status')} ${dim('[--json]')}`,
1788
+ `${dim('login:')} ${cmdFmt('hstack stack auth <name> login')} ${dim('[--identity=<name>] [--no-open] [--force] [--method=web|mobile] [--print] [--webapp=auto|stack|public|expo|hosted] [--webapp-url=<url>] [--start-if-needed] [--json]')}`,
1789
+ `${dim('seed:')} ${cmdFmt('hstack stack auth <name> copy-from <sourceStack|legacy>')} ${dim('[--force] [--with-infra] [--link] [--json]')}`,
1790
+ ]),
1791
+ '',
1792
+ sectionTitle('Advanced'),
1793
+ bullets([
1794
+ `${dim('UX labels only:')} ${cmdFmt('hstack auth login --context=selfhost|dev|stack')}`,
1795
+ `${dim('import legacy creds into main:')} ${cmdFmt('hstack auth copy-from legacy --allow-main')} ${dim('[--link] [--force]')}`,
1796
+ ]),
1797
+ ].join('\n'),
1798
+ });
1799
+ return;
1800
+ }
1801
+
1802
+ if (cmd === 'status') {
1803
+ await cmdStatus({ json });
1804
+ return;
1805
+ }
1806
+ if (cmd === 'login') {
1807
+ await cmdLogin({ argv, json });
1808
+ return;
1809
+ }
1810
+ if (cmd === 'seed') {
1811
+ await cmdSeed({ argv, json });
1812
+ return;
1813
+ }
1814
+ if (cmd === 'copy-from') {
1815
+ await cmdCopyFrom({ argv, json });
1816
+ return;
1817
+ }
1818
+ if (cmd === 'dev-key') {
1819
+ await cmdDevKey({ argv, json });
1820
+ return;
1821
+ }
1822
+
1823
+ throw new Error(`[auth] unknown command: ${cmd}`);
1824
+ }
1825
+
1826
+ main().catch((err) => {
1827
+ console.error('[auth] failed:', err);
1828
+ process.exit(1);
1829
+ });