@happier-dev/stack 0.1.0-preview.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +501 -0
  2. package/bin/hstack.mjs +348 -0
  3. package/docs/codex-mcp-resume.md +129 -0
  4. package/docs/edison.md +74 -0
  5. package/docs/forking-and-branding.md +189 -0
  6. package/docs/happy-development.md +22 -0
  7. package/docs/isolated-linux-vm.md +243 -0
  8. package/docs/menubar.md +244 -0
  9. package/docs/mobile-ios.md +322 -0
  10. package/docs/monorepo-migration.md +20 -0
  11. package/docs/paths-and-env.md +154 -0
  12. package/docs/remote-access.md +43 -0
  13. package/docs/server-flavors.md +147 -0
  14. package/docs/stacks.md +330 -0
  15. package/docs/tauri.md +60 -0
  16. package/docs/worktrees-and-forks.md +133 -0
  17. package/extras/swiftbar/auth-login.sh +29 -0
  18. package/extras/swiftbar/git-cache-refresh.sh +122 -0
  19. package/extras/swiftbar/hstack-term.sh +133 -0
  20. package/extras/swiftbar/hstack.5s.sh +296 -0
  21. package/extras/swiftbar/hstack.sh +35 -0
  22. package/extras/swiftbar/icons/happy-green.png +0 -0
  23. package/extras/swiftbar/icons/happy-orange.png +0 -0
  24. package/extras/swiftbar/icons/happy-red.png +0 -0
  25. package/extras/swiftbar/icons/logo-white.png +0 -0
  26. package/extras/swiftbar/install.sh +265 -0
  27. package/extras/swiftbar/lib/git.sh +629 -0
  28. package/extras/swiftbar/lib/icons.sh +92 -0
  29. package/extras/swiftbar/lib/render.sh +999 -0
  30. package/extras/swiftbar/lib/system.sh +244 -0
  31. package/extras/swiftbar/lib/utils.sh +717 -0
  32. package/extras/swiftbar/set-interval.sh +65 -0
  33. package/extras/swiftbar/set-server-flavor.sh +61 -0
  34. package/extras/swiftbar/wt-pr.sh +140 -0
  35. package/node_modules/@happier-dev/cli-common/README.md +6 -0
  36. package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
  37. package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
  38. package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
  39. package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
  40. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
  41. package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
  42. package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
  43. package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
  44. package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
  45. package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
  46. package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
  47. package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
  48. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
  49. package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
  50. package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
  51. package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
  52. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
  53. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
  54. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
  55. package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
  56. package/node_modules/@happier-dev/cli-common/package.json +26 -0
  57. package/package.json +77 -0
  58. package/scripts/auth.mjs +1829 -0
  59. package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
  60. package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
  61. package/scripts/auth_help_cmd.test.mjs +28 -0
  62. package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
  63. package/scripts/auth_login_force_default.test.mjs +66 -0
  64. package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
  65. package/scripts/auth_login_method_override.test.mjs +67 -0
  66. package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
  67. package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
  68. package/scripts/build.mjs +266 -0
  69. package/scripts/bundleWorkspaceDeps.mjs +38 -0
  70. package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
  71. package/scripts/ci.mjs +135 -0
  72. package/scripts/ci.test.mjs +50 -0
  73. package/scripts/cli-link.mjs +57 -0
  74. package/scripts/completion.mjs +395 -0
  75. package/scripts/contrib.mjs +333 -0
  76. package/scripts/daemon.mjs +1160 -0
  77. package/scripts/daemon.status_scope.test.mjs +51 -0
  78. package/scripts/daemon_cmd.mjs +26 -0
  79. package/scripts/daemon_dist_guard.test.mjs +171 -0
  80. package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
  81. package/scripts/daemon_server_scoped_state.test.mjs +49 -0
  82. package/scripts/daemon_start_verification.integration.test.mjs +296 -0
  83. package/scripts/dev.mjs +545 -0
  84. package/scripts/doctor.mjs +340 -0
  85. package/scripts/doctor_cmd.test.mjs +22 -0
  86. package/scripts/doctor_ui_index_missing.test.mjs +37 -0
  87. package/scripts/eas.mjs +367 -0
  88. package/scripts/eas_platform_parsing.test.mjs +63 -0
  89. package/scripts/edison.mjs +1848 -0
  90. package/scripts/env.mjs +149 -0
  91. package/scripts/env_cmd.test.mjs +118 -0
  92. package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
  93. package/scripts/happier.mjs +82 -0
  94. package/scripts/import.mjs +1327 -0
  95. package/scripts/init.mjs +464 -0
  96. package/scripts/install.mjs +550 -0
  97. package/scripts/lint.mjs +177 -0
  98. package/scripts/menubar.mjs +202 -0
  99. package/scripts/migrate.mjs +318 -0
  100. package/scripts/mobile.mjs +353 -0
  101. package/scripts/mobile_dev_client.mjs +87 -0
  102. package/scripts/monorepo.mjs +2234 -0
  103. package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
  104. package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
  105. package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
  106. package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
  107. package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
  108. package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
  109. package/scripts/pack.mjs +257 -0
  110. package/scripts/pack.test.mjs +68 -0
  111. package/scripts/pglite_lock.integration.test.mjs +152 -0
  112. package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
  113. package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
  114. package/scripts/provision/macos-lima-happy-vm.sh +192 -0
  115. package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
  116. package/scripts/release.mjs +53 -0
  117. package/scripts/release_binary_smoke.integration.test.mjs +159 -0
  118. package/scripts/review.mjs +1752 -0
  119. package/scripts/review_pr.mjs +435 -0
  120. package/scripts/run.mjs +561 -0
  121. package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
  122. package/scripts/self.mjs +465 -0
  123. package/scripts/self_host.mjs +9 -0
  124. package/scripts/self_host_binary_smoke.integration.test.mjs +94 -0
  125. package/scripts/self_host_runtime.mjs +883 -0
  126. package/scripts/self_host_runtime.test.mjs +82 -0
  127. package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
  128. package/scripts/server_flavor.mjs +148 -0
  129. package/scripts/service.mjs +868 -0
  130. package/scripts/service_mode_help.test.mjs +27 -0
  131. package/scripts/setup.mjs +1324 -0
  132. package/scripts/setup_non_interactive_flag.test.mjs +60 -0
  133. package/scripts/setup_pr.mjs +605 -0
  134. package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
  135. package/scripts/stack/command_arguments.mjs +91 -0
  136. package/scripts/stack/copy_auth_from_stack.mjs +111 -0
  137. package/scripts/stack/delegated_script_commands.mjs +92 -0
  138. package/scripts/stack/help_text.mjs +110 -0
  139. package/scripts/stack/port_reservation.mjs +74 -0
  140. package/scripts/stack/repo_checkout_resolution.mjs +31 -0
  141. package/scripts/stack/run_script_with_stack_env.mjs +634 -0
  142. package/scripts/stack/stack_daemon_command.mjs +219 -0
  143. package/scripts/stack/stack_delegated_help.mjs +81 -0
  144. package/scripts/stack/stack_environment.mjs +151 -0
  145. package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
  146. package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
  147. package/scripts/stack/stack_info_snapshot.mjs +167 -0
  148. package/scripts/stack/stack_mobile_install_command.mjs +61 -0
  149. package/scripts/stack/stack_resume_command.mjs +76 -0
  150. package/scripts/stack/stack_stop_command.mjs +34 -0
  151. package/scripts/stack/stack_workspace_command.mjs +83 -0
  152. package/scripts/stack/transient_repo_overrides.mjs +29 -0
  153. package/scripts/stack.mjs +2388 -0
  154. package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
  155. package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
  156. package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
  157. package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
  158. package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
  159. package/scripts/stack_eas_help.test.mjs +72 -0
  160. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
  161. package/scripts/stack_env_cmd.test.mjs +107 -0
  162. package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
  163. package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
  164. package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
  165. package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
  166. package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
  167. package/scripts/stack_monorepo_defaults.test.mjs +31 -0
  168. package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
  169. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
  170. package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
  171. package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
  172. package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
  173. package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
  174. package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
  175. package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
  176. package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
  177. package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
  178. package/scripts/stack_wt_list.test.mjs +117 -0
  179. package/scripts/start_ui_required_default.test.mjs +63 -0
  180. package/scripts/stop.mjs +190 -0
  181. package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
  182. package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
  183. package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
  184. package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
  185. package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
  186. package/scripts/systemd_unit_info.test.mjs +24 -0
  187. package/scripts/tailscale.mjs +490 -0
  188. package/scripts/test_ci.mjs +36 -0
  189. package/scripts/test_cmd.mjs +274 -0
  190. package/scripts/test_cmd.test.mjs +133 -0
  191. package/scripts/test_integration.mjs +33 -0
  192. package/scripts/testkit/auth_testkit.mjs +121 -0
  193. package/scripts/testkit/doctor_testkit.mjs +68 -0
  194. package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
  195. package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
  196. package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
  197. package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
  198. package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
  199. package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
  200. package/scripts/tools.mjs +70 -0
  201. package/scripts/tui.mjs +914 -0
  202. package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
  203. package/scripts/typecheck.mjs +178 -0
  204. package/scripts/ui_gateway.mjs +247 -0
  205. package/scripts/uninstall.mjs +179 -0
  206. package/scripts/utils/auth/credentials_paths.mjs +181 -0
  207. package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
  208. package/scripts/utils/auth/daemon_gate.mjs +66 -0
  209. package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
  210. package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
  211. package/scripts/utils/auth/dev_key.mjs +163 -0
  212. package/scripts/utils/auth/files.mjs +56 -0
  213. package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
  214. package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
  215. package/scripts/utils/auth/handy_master_secret.mjs +42 -0
  216. package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
  217. package/scripts/utils/auth/login_ux.mjs +105 -0
  218. package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
  219. package/scripts/utils/auth/sources.mjs +28 -0
  220. package/scripts/utils/auth/stable_scope_id.mjs +91 -0
  221. package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
  222. package/scripts/utils/auth/stack_guided_login.mjs +438 -0
  223. package/scripts/utils/cli/arg_values.mjs +23 -0
  224. package/scripts/utils/cli/arg_values.test.mjs +43 -0
  225. package/scripts/utils/cli/args.mjs +17 -0
  226. package/scripts/utils/cli/cli.mjs +24 -0
  227. package/scripts/utils/cli/cli_registry.mjs +440 -0
  228. package/scripts/utils/cli/cwd_scope.mjs +158 -0
  229. package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
  230. package/scripts/utils/cli/flags.mjs +17 -0
  231. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  232. package/scripts/utils/cli/normalize.mjs +16 -0
  233. package/scripts/utils/cli/prereqs.mjs +103 -0
  234. package/scripts/utils/cli/prereqs.test.mjs +33 -0
  235. package/scripts/utils/cli/progress.mjs +141 -0
  236. package/scripts/utils/cli/smoke_help.mjs +44 -0
  237. package/scripts/utils/cli/verbosity.mjs +11 -0
  238. package/scripts/utils/cli/wizard.mjs +139 -0
  239. package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
  240. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
  241. package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
  242. package/scripts/utils/crypto/tokens.mjs +14 -0
  243. package/scripts/utils/dev/daemon.mjs +232 -0
  244. package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
  245. package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
  246. package/scripts/utils/dev/expo_dev.mjs +478 -0
  247. package/scripts/utils/dev/expo_dev.test.mjs +89 -0
  248. package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
  249. package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
  250. package/scripts/utils/dev/server.mjs +180 -0
  251. package/scripts/utils/dev_auth_key.mjs +7 -0
  252. package/scripts/utils/edison/git_roots.mjs +30 -0
  253. package/scripts/utils/edison/git_roots.test.mjs +49 -0
  254. package/scripts/utils/env/config.mjs +52 -0
  255. package/scripts/utils/env/dotenv.mjs +32 -0
  256. package/scripts/utils/env/dotenv.test.mjs +32 -0
  257. package/scripts/utils/env/env.mjs +130 -0
  258. package/scripts/utils/env/env_file.mjs +98 -0
  259. package/scripts/utils/env/env_file.test.mjs +49 -0
  260. package/scripts/utils/env/env_local.mjs +25 -0
  261. package/scripts/utils/env/load_env_file.mjs +34 -0
  262. package/scripts/utils/env/read.mjs +30 -0
  263. package/scripts/utils/env/sandbox.mjs +13 -0
  264. package/scripts/utils/env/scrub_env.mjs +69 -0
  265. package/scripts/utils/env/scrub_env.test.mjs +102 -0
  266. package/scripts/utils/env/values.mjs +13 -0
  267. package/scripts/utils/expo/command.mjs +65 -0
  268. package/scripts/utils/expo/expo.mjs +139 -0
  269. package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
  270. package/scripts/utils/expo/metro_ports.mjs +101 -0
  271. package/scripts/utils/expo/metro_ports.test.mjs +35 -0
  272. package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
  273. package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
  274. package/scripts/utils/fs/file_has_content.mjs +10 -0
  275. package/scripts/utils/fs/fs.mjs +11 -0
  276. package/scripts/utils/fs/json.mjs +25 -0
  277. package/scripts/utils/fs/ops.mjs +29 -0
  278. package/scripts/utils/fs/package_json.mjs +8 -0
  279. package/scripts/utils/fs/tail.mjs +12 -0
  280. package/scripts/utils/git/dev_checkout.mjs +127 -0
  281. package/scripts/utils/git/dev_checkout.test.mjs +115 -0
  282. package/scripts/utils/git/git.mjs +67 -0
  283. package/scripts/utils/git/parse_name_status_z.mjs +21 -0
  284. package/scripts/utils/git/refs.mjs +26 -0
  285. package/scripts/utils/git/worktrees.mjs +323 -0
  286. package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
  287. package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
  288. package/scripts/utils/llm/assist.mjs +260 -0
  289. package/scripts/utils/llm/codex_exec.mjs +61 -0
  290. package/scripts/utils/llm/codex_exec.test.mjs +46 -0
  291. package/scripts/utils/llm/hstack_runner.mjs +59 -0
  292. package/scripts/utils/llm/tools.mjs +56 -0
  293. package/scripts/utils/llm/tools.test.mjs +67 -0
  294. package/scripts/utils/menubar/swiftbar.mjs +121 -0
  295. package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
  296. package/scripts/utils/mobile/config.mjs +35 -0
  297. package/scripts/utils/mobile/dev_client_links.mjs +59 -0
  298. package/scripts/utils/mobile/identifiers.mjs +46 -0
  299. package/scripts/utils/mobile/identifiers.test.mjs +41 -0
  300. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  301. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
  302. package/scripts/utils/net/bind_mode.mjs +39 -0
  303. package/scripts/utils/net/dns.mjs +10 -0
  304. package/scripts/utils/net/lan_ip.mjs +24 -0
  305. package/scripts/utils/net/ports.mjs +110 -0
  306. package/scripts/utils/net/tcp_forward.mjs +162 -0
  307. package/scripts/utils/net/url.mjs +30 -0
  308. package/scripts/utils/net/url.test.mjs +29 -0
  309. package/scripts/utils/paths/canonical_home.mjs +15 -0
  310. package/scripts/utils/paths/canonical_home.test.mjs +28 -0
  311. package/scripts/utils/paths/localhost_host.mjs +112 -0
  312. package/scripts/utils/paths/localhost_host.test.mjs +58 -0
  313. package/scripts/utils/paths/paths.mjs +302 -0
  314. package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
  315. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  316. package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
  317. package/scripts/utils/paths/runtime.mjs +41 -0
  318. package/scripts/utils/pglite_lock.mjs +107 -0
  319. package/scripts/utils/proc/commands.mjs +33 -0
  320. package/scripts/utils/proc/exit_cleanup.mjs +57 -0
  321. package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
  322. package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
  323. package/scripts/utils/proc/ownership.mjs +217 -0
  324. package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
  325. package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
  326. package/scripts/utils/proc/package_scripts.mjs +38 -0
  327. package/scripts/utils/proc/package_scripts.test.mjs +58 -0
  328. package/scripts/utils/proc/parallel.mjs +25 -0
  329. package/scripts/utils/proc/pids.mjs +11 -0
  330. package/scripts/utils/proc/pm.mjs +478 -0
  331. package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
  332. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
  333. package/scripts/utils/proc/proc.mjs +331 -0
  334. package/scripts/utils/proc/proc.test.mjs +85 -0
  335. package/scripts/utils/proc/terminate.mjs +69 -0
  336. package/scripts/utils/proc/terminate.test.mjs +54 -0
  337. package/scripts/utils/proc/watch.mjs +63 -0
  338. package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
  339. package/scripts/utils/review/base_ref.mjs +82 -0
  340. package/scripts/utils/review/base_ref.test.mjs +89 -0
  341. package/scripts/utils/review/chunks.mjs +55 -0
  342. package/scripts/utils/review/chunks.test.mjs +107 -0
  343. package/scripts/utils/review/detached_worktree.mjs +61 -0
  344. package/scripts/utils/review/detached_worktree.test.mjs +61 -0
  345. package/scripts/utils/review/findings.mjs +278 -0
  346. package/scripts/utils/review/findings.test.mjs +203 -0
  347. package/scripts/utils/review/head_slice.mjs +132 -0
  348. package/scripts/utils/review/head_slice.test.mjs +117 -0
  349. package/scripts/utils/review/instructions/deep.md +20 -0
  350. package/scripts/utils/review/prompts.mjs +279 -0
  351. package/scripts/utils/review/prompts.test.mjs +77 -0
  352. package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
  353. package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
  354. package/scripts/utils/review/runners/augment.mjs +91 -0
  355. package/scripts/utils/review/runners/augment.test.mjs +64 -0
  356. package/scripts/utils/review/runners/claude.mjs +92 -0
  357. package/scripts/utils/review/runners/claude.test.mjs +47 -0
  358. package/scripts/utils/review/runners/coderabbit.mjs +105 -0
  359. package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
  360. package/scripts/utils/review/runners/codex.mjs +129 -0
  361. package/scripts/utils/review/runners/codex.test.mjs +115 -0
  362. package/scripts/utils/review/slice_mode.mjs +20 -0
  363. package/scripts/utils/review/slice_mode.test.mjs +69 -0
  364. package/scripts/utils/review/sliced_runner.mjs +39 -0
  365. package/scripts/utils/review/sliced_runner.test.mjs +57 -0
  366. package/scripts/utils/review/slices.mjs +140 -0
  367. package/scripts/utils/review/slices.test.mjs +41 -0
  368. package/scripts/utils/review/targets.mjs +23 -0
  369. package/scripts/utils/review/targets.test.mjs +31 -0
  370. package/scripts/utils/review/tool_home_seed.mjs +106 -0
  371. package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
  372. package/scripts/utils/review/uncommitted_ops.mjs +77 -0
  373. package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
  374. package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
  375. package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
  376. package/scripts/utils/server/flavor_scripts.mjs +138 -0
  377. package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
  378. package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
  379. package/scripts/utils/server/mobile_api_url.mjs +60 -0
  380. package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
  381. package/scripts/utils/server/port.mjs +55 -0
  382. package/scripts/utils/server/prisma_import.mjs +36 -0
  383. package/scripts/utils/server/prisma_import.test.mjs +78 -0
  384. package/scripts/utils/server/server.mjs +109 -0
  385. package/scripts/utils/server/ui_build_check.mjs +37 -0
  386. package/scripts/utils/server/ui_build_check.test.mjs +70 -0
  387. package/scripts/utils/server/ui_env.mjs +13 -0
  388. package/scripts/utils/server/ui_env.test.mjs +57 -0
  389. package/scripts/utils/server/urls.mjs +100 -0
  390. package/scripts/utils/server/validate.mjs +60 -0
  391. package/scripts/utils/server/validate.test.mjs +76 -0
  392. package/scripts/utils/service/autostart_darwin.mjs +198 -0
  393. package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
  394. package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
  395. package/scripts/utils/stack/cli_identities.mjs +29 -0
  396. package/scripts/utils/stack/context.mjs +19 -0
  397. package/scripts/utils/stack/dirs.mjs +26 -0
  398. package/scripts/utils/stack/editor_workspace.mjs +126 -0
  399. package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
  400. package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
  401. package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
  402. package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
  403. package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
  404. package/scripts/utils/stack/names.mjs +27 -0
  405. package/scripts/utils/stack/names.test.mjs +26 -0
  406. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  407. package/scripts/utils/stack/runtime_state.mjs +88 -0
  408. package/scripts/utils/stack/stacks.mjs +40 -0
  409. package/scripts/utils/stack/startup.mjs +370 -0
  410. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
  411. package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
  412. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
  413. package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
  414. package/scripts/utils/stack/stop.mjs +284 -0
  415. package/scripts/utils/stack_context.mjs +1 -0
  416. package/scripts/utils/stack_runtime_state.mjs +1 -0
  417. package/scripts/utils/stacks.mjs +1 -0
  418. package/scripts/utils/tailscale/ip.mjs +116 -0
  419. package/scripts/utils/tauri/stack_overrides.mjs +22 -0
  420. package/scripts/utils/test/collect_test_files.mjs +29 -0
  421. package/scripts/utils/time/get_today_ymd.mjs +7 -0
  422. package/scripts/utils/tui/cleanup.mjs +38 -0
  423. package/scripts/utils/ui/ansi.mjs +47 -0
  424. package/scripts/utils/ui/browser.mjs +31 -0
  425. package/scripts/utils/ui/browser.test.mjs +56 -0
  426. package/scripts/utils/ui/clipboard.mjs +38 -0
  427. package/scripts/utils/ui/layout.mjs +44 -0
  428. package/scripts/utils/ui/qr.mjs +17 -0
  429. package/scripts/utils/ui/terminal_launcher.mjs +129 -0
  430. package/scripts/utils/ui/text.mjs +16 -0
  431. package/scripts/utils/update/auto_update_notice.mjs +93 -0
  432. package/scripts/utils/validate.mjs +5 -0
  433. package/scripts/where.mjs +138 -0
  434. package/scripts/worktrees.mjs +2174 -0
  435. package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
  436. package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
  437. package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
  438. package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
  439. package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
@@ -0,0 +1,680 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+
6
+ import { run, runCapture } from './utils/proc/proc.mjs';
7
+ import { withTempRoot, gitEnv, initMonorepoStub, initSplitRepoStub, spawnNodeWithCapture } from './testkit/monorepo_port_testkit.mjs';
8
+
9
+ test('monorepo port applies split-repo commits into subdirectories', async (t) => {
10
+ const root = await withTempRoot(t);
11
+ const target = join(root, 'target-mono');
12
+ const sourceCli = join(root, 'source-cli');
13
+ const env = gitEnv();
14
+
15
+ await initMonorepoStub({ dir: target, env, seed: { 'apps/cli/hello.txt': 'v1\n' } });
16
+
17
+ // Source CLI repo with one change commit
18
+ const base = await initSplitRepoStub({ dir: sourceCli, env, name: 'cli', seed: { 'hello.txt': 'v1\n' } });
19
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
20
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
21
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env });
22
+
23
+ // Run port command
24
+ const out = await runCapture(
25
+ process.execPath,
26
+ [
27
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
28
+ 'port',
29
+ `--target=${target}`,
30
+ `--branch=port/test`,
31
+ `--from-happy-cli=${sourceCli}`,
32
+ `--from-happy-cli-base=${base}`,
33
+ '--json',
34
+ ],
35
+ { cwd: process.cwd(), env }
36
+ );
37
+ const parsed = JSON.parse(out.trim());
38
+ assert.equal(parsed.ok, true);
39
+
40
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
41
+ assert.equal(content, 'v2\n');
42
+ });
43
+
44
+ test('monorepo port --skip-applied skips patches that are already present in the target', async (t) => {
45
+ const root = await withTempRoot(t);
46
+ const target = join(root, 'target-mono');
47
+ const sourceCli = join(root, 'source-cli');
48
+ const env = gitEnv();
49
+
50
+ await initMonorepoStub({ dir: target, env, seed: { 'apps/cli/hello.txt': 'v2\n' } });
51
+
52
+ // Source CLI repo with one change commit (v1 -> v2)
53
+ const base = await initSplitRepoStub({ dir: sourceCli, env, name: 'cli', seed: { 'hello.txt': 'v1\n' } });
54
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
55
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
56
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env });
57
+
58
+ // Run port command with skip-applied
59
+ const out = await runCapture(
60
+ process.execPath,
61
+ [
62
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
63
+ 'port',
64
+ `--target=${target}`,
65
+ `--branch=port/test-skip`,
66
+ `--from-happy-cli=${sourceCli}`,
67
+ `--from-happy-cli-base=${base}`,
68
+ '--skip-applied',
69
+ '--json',
70
+ ],
71
+ { cwd: process.cwd(), env }
72
+ );
73
+ const parsed = JSON.parse(out.trim());
74
+ assert.equal(parsed.ok, true);
75
+
76
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
77
+ assert.equal(content, 'v2\n');
78
+ });
79
+
80
+ test('monorepo port accepts monorepo sources without double-prefixing paths', async (t) => {
81
+ const root = await withTempRoot(t);
82
+ const target = join(root, 'target-mono');
83
+ const source = join(root, 'source-mono');
84
+ const env = gitEnv();
85
+
86
+ await initMonorepoStub({ dir: target, env, seed: { 'apps/ui/hello.txt': 'v1\n' } });
87
+
88
+ // Source monorepo repo with one change commit in apps/ui/
89
+ await initMonorepoStub({ dir: source, env, seed: { 'apps/ui/hello.txt': 'v1\n' } });
90
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: source, env })).trim();
91
+ await writeFile(join(source, 'apps', 'ui', 'hello.txt'), 'v2\n', 'utf-8');
92
+ await run('git', ['add', '.'], { cwd: source, env });
93
+ await run('git', ['commit', '-q', '-m', 'feat: update happy-app hello'], { cwd: source, env });
94
+
95
+ const out = await runCapture(
96
+ process.execPath,
97
+ [
98
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
99
+ 'port',
100
+ `--target=${target}`,
101
+ `--branch=port/test-mono-source`,
102
+ `--from-happy=${source}`,
103
+ `--from-happy-base=${base}`,
104
+ '--json',
105
+ ],
106
+ { cwd: process.cwd(), env }
107
+ );
108
+ const parsed = JSON.parse(out.trim());
109
+ assert.equal(parsed.ok, true);
110
+ const content = (await readFile(join(target, 'apps', 'ui', 'hello.txt'), 'utf-8')).toString();
111
+ assert.equal(content, 'v2\n');
112
+ });
113
+
114
+ test('monorepo port can clone the target monorepo into a new directory', async (t) => {
115
+ const root = await withTempRoot(t);
116
+ const seedMono = join(root, 'seed-mono');
117
+ const target = join(root, 'target-cloned'); // does not exist yet
118
+ const sourceCli = join(root, 'source-cli');
119
+ const env = gitEnv();
120
+
121
+ // Seed monorepo repo that will be cloned into `target`
122
+ await initMonorepoStub({
123
+ dir: seedMono,
124
+ env,
125
+ layout: 'packages',
126
+ seed: { 'apps/cli/hello.txt': 'v1\n' },
127
+ });
128
+
129
+ // Source CLI repo with one change commit (v1 -> v2)
130
+ await mkdir(sourceCli, { recursive: true });
131
+ await run('git', ['init', '-q'], { cwd: sourceCli, env });
132
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env });
133
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
134
+ await writeFile(join(sourceCli, 'hello.txt'), 'v1\n', 'utf-8');
135
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
136
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env });
137
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env })).trim();
138
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
139
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
140
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env });
141
+
142
+ const out = await runCapture(
143
+ process.execPath,
144
+ [
145
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
146
+ 'port',
147
+ `--target=${target}`,
148
+ '--clone-target',
149
+ `--target-repo=${seedMono}`,
150
+ `--branch=port/test-target-clone`,
151
+ `--from-happy-cli=${sourceCli}`,
152
+ `--from-happy-cli-base=${base}`,
153
+ '--json',
154
+ ],
155
+ { cwd: process.cwd(), env }
156
+ );
157
+ const parsed = JSON.parse(out.trim());
158
+ assert.equal(parsed.ok, true);
159
+
160
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
161
+ assert.equal(content, 'v2\n');
162
+ });
163
+
164
+ test('monorepo port guide auto-clones target when --target does not exist', async (t) => {
165
+ const root = await withTempRoot(t);
166
+ const seedMono = join(root, 'seed-mono');
167
+ const target = join(root, 'target-guide-autoclone'); // does not exist yet
168
+ const sourceCli = join(root, 'source-cli');
169
+ const env = gitEnv();
170
+
171
+ // Seed monorepo repo that will be cloned into `target`
172
+ await initMonorepoStub({
173
+ dir: seedMono,
174
+ env,
175
+ layout: 'packages',
176
+ seed: { 'apps/cli/hello.txt': 'v1\n' },
177
+ });
178
+
179
+ // Source CLI repo with one change commit (v1 -> v2)
180
+ const base = await initSplitRepoStub({ dir: sourceCli, env, name: 'cli', seed: { 'hello.txt': 'v1\n' } });
181
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
182
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
183
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env });
184
+
185
+ // Guide requires a TTY, but with all args provided it should not prompt.
186
+ // We spawn so the guide sees a TTY (required), but still feed no input.
187
+ const guide = spawnNodeWithCapture(
188
+ process.execPath,
189
+ [
190
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
191
+ 'port',
192
+ 'guide',
193
+ `--target=${target}`,
194
+ `--target-repo=${seedMono}`,
195
+ '--branch=port/test-guide-autoclone',
196
+ '--3way',
197
+ `--from-happy-cli=${sourceCli}`,
198
+ `--from-happy-cli-base=${base}`,
199
+ '--json',
200
+ ],
201
+ {
202
+ cwd: process.cwd(),
203
+ env: { ...env, HAPPIER_STACK_TEST_TTY: '1', HAPPIER_STACK_DISABLE_LLM_AUTOEXEC: '1' },
204
+ stdio: ['pipe', 'pipe', 'pipe'],
205
+ }
206
+ );
207
+ t.after(() => {
208
+ guide.kill('SIGKILL');
209
+ });
210
+ const guideResult = await guide.waitForExit(20_000);
211
+ assert.equal(
212
+ guideResult.code,
213
+ 0,
214
+ `expected guide to exit 0\nstdout:\n${guideResult.stdout}\nstderr:\n${guideResult.stderr}`
215
+ );
216
+
217
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
218
+ assert.equal(content, 'v2\n');
219
+ });
220
+
221
+ test('monorepo port accepts source repo URLs by cloning them into a temp checkout', async (t) => {
222
+ const root = await withTempRoot(t);
223
+ const target = join(root, 'target-mono');
224
+ const sourceCli = join(root, 'source-cli');
225
+ const env = gitEnv();
226
+
227
+ // Target monorepo stub (seed base file)
228
+ await initMonorepoStub({ dir: target, env, seed: { 'apps/cli/hello.txt': 'v1\n' } });
229
+
230
+ // Source CLI repo with one change commit
231
+ const base = await initSplitRepoStub({
232
+ dir: sourceCli,
233
+ env,
234
+ name: 'cli',
235
+ seed: { 'hello.txt': 'v1\n' },
236
+ });
237
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
238
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
239
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env });
240
+
241
+ const out = await runCapture(
242
+ process.execPath,
243
+ [
244
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
245
+ 'port',
246
+ `--target=${target}`,
247
+ `--branch=port/test-source-url`,
248
+ `--from-happy-cli=file://${sourceCli}`,
249
+ `--from-happy-cli-base=${base}`,
250
+ '--json',
251
+ ],
252
+ { cwd: process.cwd(), env }
253
+ );
254
+ const parsed = JSON.parse(out.trim());
255
+ assert.equal(parsed.ok, true);
256
+
257
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
258
+ assert.equal(content, 'v2\n');
259
+ });
260
+
261
+ test('monorepo port --continue-on-failure completes even when some patches do not apply', async (t) => {
262
+ const root = await withTempRoot(t);
263
+ const target = join(root, 'target-mono');
264
+ const sourceCli = join(root, 'source-cli');
265
+
266
+ // Target monorepo stub with v3 already (so v1->v2 patch won't apply, and also can't be detected as already-applied).
267
+ await mkdir(target, { recursive: true });
268
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
269
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
270
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
271
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
272
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
273
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
274
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
275
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
276
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'v3\n', 'utf-8');
277
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
278
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
279
+
280
+ // Source CLI repo with one change commit (v1 -> v2)
281
+ await mkdir(sourceCli, { recursive: true });
282
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
283
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
284
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
285
+ await writeFile(join(sourceCli, 'hello.txt'), 'v1\n', 'utf-8');
286
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
287
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
288
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
289
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
290
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
291
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env: gitEnv() });
292
+
293
+ // Run port command: patch should fail to apply, but command succeeds.
294
+ const out = await runCapture(
295
+ process.execPath,
296
+ [
297
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
298
+ 'port',
299
+ `--target=${target}`,
300
+ `--branch=port/test-continue`,
301
+ `--from-happy-cli=${sourceCli}`,
302
+ `--from-happy-cli-base=${base}`,
303
+ '--skip-applied',
304
+ '--continue-on-failure',
305
+ '--json',
306
+ ],
307
+ { cwd: process.cwd(), env: gitEnv() }
308
+ );
309
+ const parsed = JSON.parse(out.trim());
310
+ assert.equal(parsed.ok, false);
311
+ assert.equal(parsed.results[0].failedPatches, 1);
312
+ });
313
+
314
+ test('monorepo port auto-skips identical "new file" patches when the file already exists in the target', async (t) => {
315
+ const root = await withTempRoot(t);
316
+ const target = join(root, 'target-mono');
317
+ const sourceCli = join(root, 'source-cli');
318
+
319
+ // Target monorepo stub already contains cli/newfile.txt with the same content.
320
+ await mkdir(target, { recursive: true });
321
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
322
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
323
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
324
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
325
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
326
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
327
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
328
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
329
+ await writeFile(join(target, 'apps', 'cli', 'newfile.txt'), 'same\n', 'utf-8');
330
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
331
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
332
+
333
+ // Source CLI repo adds newfile.txt in a single commit.
334
+ await mkdir(sourceCli, { recursive: true });
335
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
336
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
337
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
338
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
339
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
340
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
341
+ await writeFile(join(sourceCli, 'newfile.txt'), 'same\n', 'utf-8');
342
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
343
+ await run('git', ['commit', '-q', '-m', 'feat: add newfile'], { cwd: sourceCli, env: gitEnv() });
344
+
345
+ const out = await runCapture(
346
+ process.execPath,
347
+ [
348
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
349
+ 'port',
350
+ `--target=${target}`,
351
+ `--branch=port/test-identical-newfile`,
352
+ `--from-happy-cli=${sourceCli}`,
353
+ `--from-happy-cli-base=${base}`,
354
+ '--json',
355
+ ],
356
+ { cwd: process.cwd(), env: gitEnv() }
357
+ );
358
+
359
+ const parsed = JSON.parse(out.trim());
360
+ assert.equal(parsed.ok, true);
361
+ assert.equal(parsed.results[0].failedPatches, 0);
362
+ // This commit cannot be applied (it would "create" an existing file), so the port must skip it.
363
+ assert.equal(parsed.results[0].appliedPatches, 0);
364
+ assert.equal(parsed.results[0].skippedAlreadyApplied, 0);
365
+ assert.equal(parsed.results[0].skippedAlreadyExistsIdentical, 1);
366
+ });
367
+
368
+ test('monorepo port --onto-current applies onto the currently checked-out branch', async (t) => {
369
+ const root = await withTempRoot(t);
370
+ const target = join(root, 'target-mono');
371
+ const sourceCli = join(root, 'source-cli');
372
+
373
+ // Target monorepo stub on a custom branch (so we can verify it doesn't switch).
374
+ await mkdir(target, { recursive: true });
375
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
376
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
377
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
378
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
379
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
380
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
381
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
382
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
383
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'v1\n', 'utf-8');
384
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
385
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
386
+ await run('git', ['checkout', '-q', '-b', 'existing'], { cwd: target, env: gitEnv() });
387
+
388
+ // Source CLI repo with one change commit (v1 -> v2)
389
+ await mkdir(sourceCli, { recursive: true });
390
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
391
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
392
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
393
+ await writeFile(join(sourceCli, 'hello.txt'), 'v1\n', 'utf-8');
394
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
395
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
396
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
397
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
398
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
399
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env: gitEnv() });
400
+
401
+ const out = await runCapture(
402
+ process.execPath,
403
+ [
404
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
405
+ 'port',
406
+ `--target=${target}`,
407
+ `--from-happy-cli=${sourceCli}`,
408
+ `--from-happy-cli-base=${base}`,
409
+ '--onto-current',
410
+ '--json',
411
+ ],
412
+ { cwd: process.cwd(), env: gitEnv() }
413
+ );
414
+ const parsed = JSON.parse(out.trim());
415
+ assert.equal(parsed.ok, true);
416
+
417
+ const branch = (await runCapture('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: target, env: gitEnv() })).trim();
418
+ assert.equal(branch, 'existing');
419
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
420
+ assert.equal(content, 'v2\n');
421
+ });
422
+
423
+ test('monorepo port branches from target default base (not current HEAD)', async (t) => {
424
+ const root = await withTempRoot(t);
425
+ const target = join(root, 'target-mono');
426
+ const sourceCli = join(root, 'source-cli');
427
+
428
+ // Target monorepo stub on main with cli/hello.txt=v1.
429
+ await mkdir(target, { recursive: true });
430
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
431
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
432
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
433
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
434
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
435
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
436
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
437
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
438
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'v1\n', 'utf-8');
439
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
440
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
441
+
442
+ // Create a divergent branch and leave it checked out (simulates running port from a non-base branch).
443
+ await run('git', ['checkout', '-q', '-b', 'dev'], { cwd: target, env: gitEnv() });
444
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'v3\n', 'utf-8');
445
+ await run('git', ['add', 'apps/cli/hello.txt'], { cwd: target, env: gitEnv() });
446
+ await run('git', ['commit', '-q', '-m', 'chore: dev drift'], { cwd: target, env: gitEnv() });
447
+
448
+ // Source CLI repo with one change commit (v1 -> v2).
449
+ await mkdir(sourceCli, { recursive: true });
450
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
451
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
452
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
453
+ await writeFile(join(sourceCli, 'hello.txt'), 'v1\n', 'utf-8');
454
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
455
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
456
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
457
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
458
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
459
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env: gitEnv() });
460
+
461
+ // Port should branch from target main by default (not dev), so the v1->v2 patch applies.
462
+ const out = await runCapture(
463
+ process.execPath,
464
+ [
465
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
466
+ 'port',
467
+ `--target=${target}`,
468
+ `--branch=port/test-target-base`,
469
+ `--from-happy-cli=${sourceCli}`,
470
+ `--from-happy-cli-base=${base}`,
471
+ '--json',
472
+ ],
473
+ { cwd: process.cwd(), env: gitEnv() }
474
+ );
475
+ const parsed = JSON.parse(out.trim());
476
+ assert.equal(parsed.ok, true);
477
+
478
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
479
+ assert.equal(content, 'v2\n');
480
+ });
481
+
482
+ test('monorepo port prints an actionable summary in non-json mode when patches fail', async (t) => {
483
+ const root = await withTempRoot(t);
484
+ const target = join(root, 'target-mono');
485
+ const sourceCli = join(root, 'source-cli');
486
+
487
+ // Target monorepo stub with cli/hello.txt=v3 (so v1->v2 patch fails).
488
+ await mkdir(target, { recursive: true });
489
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
490
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
491
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
492
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
493
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
494
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
495
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
496
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
497
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'v3\n', 'utf-8');
498
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
499
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
500
+
501
+ // Source CLI repo with one change commit (v1 -> v2).
502
+ await mkdir(sourceCli, { recursive: true });
503
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
504
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
505
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
506
+ await writeFile(join(sourceCli, 'hello.txt'), 'v1\n', 'utf-8');
507
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
508
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
509
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
510
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
511
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
512
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env: gitEnv() });
513
+
514
+ // Run without --json and ensure it prints a useful failure summary.
515
+ const out = await runCapture(
516
+ process.execPath,
517
+ [
518
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
519
+ 'port',
520
+ `--target=${target}`,
521
+ `--branch=port/test-nonjson`,
522
+ `--from-happy-cli=${sourceCli}`,
523
+ `--from-happy-cli-base=${base}`,
524
+ '--continue-on-failure',
525
+ ],
526
+ { cwd: process.cwd(), env: gitEnv() }
527
+ );
528
+
529
+ assert.ok(out.includes('port complete with failures'), `expected failure summary in stdout\n${out}`);
530
+ assert.ok(out.includes('feat: update hello'), `expected failed patch subject in stdout\n${out}`);
531
+ });
532
+
533
+ test('monorepo port works via bin/hstack.mjs entrypoint (CLI registry end-to-end)', async (t) => {
534
+ const root = await withTempRoot(t);
535
+ const target = join(root, 'target-mono');
536
+ const sourceCli = join(root, 'source-cli');
537
+
538
+ // Target monorepo stub
539
+ await mkdir(target, { recursive: true });
540
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
541
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
542
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
543
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
544
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
545
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
546
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
547
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
548
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'v1\n', 'utf-8');
549
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
550
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
551
+
552
+ // Source CLI repo with one change commit
553
+ await mkdir(sourceCli, { recursive: true });
554
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
555
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
556
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
557
+ await writeFile(join(sourceCli, 'hello.txt'), 'v1\n', 'utf-8');
558
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
559
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
560
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
561
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
562
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
563
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env: gitEnv() });
564
+
565
+ const env = { ...gitEnv(), HAPPIER_STACK_HOME_DIR: join(root, 'home') };
566
+ const out = await runCapture(
567
+ process.execPath,
568
+ [
569
+ join(process.cwd(), 'bin', 'hstack.mjs'),
570
+ 'monorepo',
571
+ 'port',
572
+ `--target=${target}`,
573
+ `--branch=port/test-hstack`,
574
+ '--base=main',
575
+ `--from-happy-cli=${sourceCli}`,
576
+ `--from-happy-cli-base=${base}`,
577
+ '--json',
578
+ ],
579
+ { cwd: process.cwd(), env }
580
+ );
581
+
582
+ const parsed = JSON.parse(out.trim());
583
+ assert.equal(parsed.ok, true);
584
+
585
+ const content = (await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8')).toString();
586
+ assert.equal(content, 'v2\n');
587
+ });
588
+
589
+ test('monorepo port can port multiple split repos into the same monorepo branch (including renames)', async (t) => {
590
+ const root = await withTempRoot(t);
591
+ const target = join(root, 'target-mono');
592
+ const sourceUi = join(root, 'source-happy');
593
+ const sourceCli = join(root, 'source-happy-cli');
594
+ const sourceServer = join(root, 'source-happy-server');
595
+
596
+ // Target monorepo stub seeded with base files for all three subdirs.
597
+ await mkdir(target, { recursive: true });
598
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
599
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
600
+ await mkdir(join(target, 'apps', 'ui'), { recursive: true });
601
+ await mkdir(join(target, 'apps', 'cli'), { recursive: true });
602
+ await mkdir(join(target, 'apps', 'server'), { recursive: true });
603
+ await writeFile(join(target, 'apps', 'ui', 'package.json'), '{}\n', 'utf-8');
604
+ await writeFile(join(target, 'apps', 'cli', 'package.json'), '{}\n', 'utf-8');
605
+ await writeFile(join(target, 'apps', 'server', 'package.json'), '{}\n', 'utf-8');
606
+ await writeFile(join(target, 'apps', 'ui', 'hello.txt'), 'ui-v1\n', 'utf-8');
607
+ await writeFile(join(target, 'apps', 'cli', 'hello.txt'), 'cli-v1\n', 'utf-8');
608
+ await writeFile(join(target, 'apps', 'server', 'hello.txt'), 'srv-v1\n', 'utf-8');
609
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
610
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
611
+
612
+ // UI repo: update hello + add extra
613
+ await mkdir(sourceUi, { recursive: true });
614
+ await run('git', ['init', '-q'], { cwd: sourceUi, env: gitEnv() });
615
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceUi, env: gitEnv() });
616
+ await writeFile(join(sourceUi, 'package.json'), '{}\n', 'utf-8');
617
+ await writeFile(join(sourceUi, 'hello.txt'), 'ui-v1\n', 'utf-8');
618
+ await run('git', ['add', '.'], { cwd: sourceUi, env: gitEnv() });
619
+ await run('git', ['commit', '-q', '-m', 'chore: init ui'], { cwd: sourceUi, env: gitEnv() });
620
+ const uiBase = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceUi, env: gitEnv() })).trim();
621
+ await writeFile(join(sourceUi, 'hello.txt'), 'ui-v2\n', 'utf-8');
622
+ await writeFile(join(sourceUi, 'extra.txt'), 'extra-ui\n', 'utf-8');
623
+ await run('git', ['add', '.'], { cwd: sourceUi, env: gitEnv() });
624
+ await run('git', ['commit', '-q', '-m', 'feat: update ui + add extra'], { cwd: sourceUi, env: gitEnv() });
625
+
626
+ // CLI repo: rename hello -> greeting
627
+ await mkdir(sourceCli, { recursive: true });
628
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
629
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
630
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
631
+ await writeFile(join(sourceCli, 'hello.txt'), 'cli-v1\n', 'utf-8');
632
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
633
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
634
+ const cliBase = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
635
+ await run('git', ['mv', 'hello.txt', 'greeting.txt'], { cwd: sourceCli, env: gitEnv() });
636
+ await writeFile(join(sourceCli, 'greeting.txt'), 'cli-v2\n', 'utf-8');
637
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
638
+ await run('git', ['commit', '-q', '-m', 'feat: rename hello to greeting'], { cwd: sourceCli, env: gitEnv() });
639
+
640
+ // Server repo: add routes.txt
641
+ await mkdir(sourceServer, { recursive: true });
642
+ await run('git', ['init', '-q'], { cwd: sourceServer, env: gitEnv() });
643
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceServer, env: gitEnv() });
644
+ await writeFile(join(sourceServer, 'package.json'), '{}\n', 'utf-8');
645
+ await writeFile(join(sourceServer, 'hello.txt'), 'srv-v1\n', 'utf-8');
646
+ await run('git', ['add', '.'], { cwd: sourceServer, env: gitEnv() });
647
+ await run('git', ['commit', '-q', '-m', 'chore: init server'], { cwd: sourceServer, env: gitEnv() });
648
+ const serverBase = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceServer, env: gitEnv() })).trim();
649
+ await writeFile(join(sourceServer, 'routes.txt'), 'routes\n', 'utf-8');
650
+ await run('git', ['add', '.'], { cwd: sourceServer, env: gitEnv() });
651
+ await run('git', ['commit', '-q', '-m', 'feat: add routes'], { cwd: sourceServer, env: gitEnv() });
652
+
653
+ const out = await runCapture(
654
+ process.execPath,
655
+ [
656
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
657
+ 'port',
658
+ `--target=${target}`,
659
+ `--branch=port/test-multi`,
660
+ '--base=main',
661
+ '--3way',
662
+ '--json',
663
+ `--from-happy=${sourceUi}`,
664
+ `--from-happy-base=${uiBase}`,
665
+ `--from-happy-cli=${sourceCli}`,
666
+ `--from-happy-cli-base=${cliBase}`,
667
+ `--from-happy-server=${sourceServer}`,
668
+ `--from-happy-server-base=${serverBase}`,
669
+ ],
670
+ { cwd: process.cwd(), env: gitEnv() }
671
+ );
672
+ const parsed = JSON.parse(out.trim());
673
+ assert.equal(parsed.ok, true);
674
+
675
+ assert.equal((await readFile(join(target, 'apps', 'ui', 'hello.txt'), 'utf-8')).toString(), 'ui-v2\n');
676
+ assert.equal((await readFile(join(target, 'apps', 'ui', 'extra.txt'), 'utf-8')).toString(), 'extra-ui\n');
677
+ assert.equal((await readFile(join(target, 'apps', 'cli', 'greeting.txt'), 'utf-8')).toString(), 'cli-v2\n');
678
+ await assert.rejects(async () => await readFile(join(target, 'apps', 'cli', 'hello.txt'), 'utf-8'));
679
+ assert.equal((await readFile(join(target, 'apps', 'server', 'routes.txt'), 'utf-8')).toString(), 'routes\n');
680
+ });