@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,629 @@
1
+ #!/bin/bash
2
+
3
+ # Lightweight git helpers for SwiftBar.
4
+ # Keep these fast: avoid network, avoid long commands.
5
+
6
+ is_git_repo() {
7
+ local dir="$1"
8
+ [[ -n "$dir" && -d "$dir" ]] || return 1
9
+ # Fast-path: repo root/worktree checkout.
10
+ if [[ -d "$dir/.git" || -f "$dir/.git" ]]; then
11
+ return 0
12
+ fi
13
+ # Monorepo packages (and other nested dirs) are still "inside" a git repo even if they don't contain .git.
14
+ command -v git >/dev/null 2>&1 || return 1
15
+ local inside
16
+ inside="$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null || true)"
17
+ [[ "$inside" == "true" ]]
18
+ }
19
+
20
+ git_cache_dir() {
21
+ local home
22
+ home="$(resolve_home_dir)"
23
+ local dir="${home}/cache/swiftbar/git"
24
+ mkdir -p "$dir" 2>/dev/null || true
25
+ echo "$dir"
26
+ }
27
+
28
+ git_cache_ttl_sec() {
29
+ # Default: 6 hours.
30
+ local v="${HAPPIER_STACK_SWIFTBAR_GIT_TTL_SEC:-21600}"
31
+ [[ "$v" =~ ^[0-9]+$ ]] || v=21600
32
+ echo "$v"
33
+ }
34
+
35
+ git_cache_refresh_on_stale() {
36
+ [[ "${HAPPIER_STACK_SWIFTBAR_GIT_REFRESH_ON_STALE:-0}" == "1" ]]
37
+ }
38
+
39
+ git_cache_auto_refresh_scope() {
40
+ # off | main | all
41
+ local s="${HAPPIER_STACK_SWIFTBAR_GIT_AUTO_REFRESH_SCOPE:-main}"
42
+ s="$(echo "$s" | tr '[:upper:]' '[:lower:]')"
43
+ case "$s" in
44
+ off|none|0) echo "off" ;;
45
+ all) echo "all" ;;
46
+ *) echo "main" ;;
47
+ esac
48
+ }
49
+
50
+ git_cache_last_refresh_file() {
51
+ local scope="${1:-main}" # main|all|stack:<name>
52
+ local dir
53
+ dir="$(git_cache_dir)"
54
+ local key="last_refresh|${scope}"
55
+ echo "${dir}/$(swiftbar_cache_hash12 "$key").last"
56
+ }
57
+
58
+ git_cache_background_refresh_lockdir() {
59
+ local scope="${1:-main}"
60
+ local dir
61
+ dir="$(git_cache_dir)"
62
+ local key="bg_refresh_lock|${scope}"
63
+ echo "${dir}/$(swiftbar_cache_hash12 "$key").lock"
64
+ }
65
+
66
+ git_cache_touch_last_refresh() {
67
+ local scope="${1:-main}"
68
+ local f
69
+ f="$(git_cache_last_refresh_file "$scope")"
70
+ mkdir -p "$(dirname "$f")" 2>/dev/null || true
71
+ date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null >"$f" || true
72
+ touch "$f" 2>/dev/null || true
73
+ }
74
+
75
+ git_cache_age_since_last_refresh_sec() {
76
+ local scope="${1:-main}"
77
+ local f
78
+ f="$(git_cache_last_refresh_file "$scope")"
79
+ [[ -f "$f" ]] || { echo ""; return; }
80
+ local mtime now
81
+ mtime="$(stat -f %m "$f" 2>/dev/null || echo 0)"
82
+ now="$(date +%s 2>/dev/null || echo 0)"
83
+ if [[ "$mtime" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ && "$now" -ge "$mtime" ]]; then
84
+ echo $((now - mtime))
85
+ else
86
+ echo ""
87
+ fi
88
+ }
89
+
90
+ git_cache_maybe_refresh_async() {
91
+ # Non-blocking cache refresh.
92
+ # Usage: git_cache_maybe_refresh_async <scope> <refresh_cmd...>
93
+ local scope="$1"
94
+ shift
95
+
96
+ local ttl age
97
+ ttl="$(git_cache_ttl_sec)"
98
+ age="$(git_cache_age_since_last_refresh_sec "$scope")"
99
+
100
+ # If never refreshed, treat as stale and allow.
101
+ if [[ -n "$age" && "$age" =~ ^[0-9]+$ && "$age" -le "$ttl" ]]; then
102
+ return 0
103
+ fi
104
+
105
+ local lockdir
106
+ lockdir="$(git_cache_background_refresh_lockdir "$scope")"
107
+ if [[ -d "$lockdir" ]]; then
108
+ # If lock is too old, break it (e.g. crashed refresh).
109
+ local lock_age
110
+ lock_age="$(git_cache_age_sec "$lockdir" 2>/dev/null || true)"
111
+ if [[ -n "$lock_age" && "$lock_age" =~ ^[0-9]+$ && "$lock_age" -gt 3600 ]]; then
112
+ rm -rf "$lockdir" 2>/dev/null || true
113
+ else
114
+ return 0
115
+ fi
116
+ fi
117
+
118
+ mkdir "$lockdir" 2>/dev/null || return 0
119
+ echo "$$" >"${lockdir}/pid" 2>/dev/null || true
120
+ date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null >"${lockdir}/started_at" || true
121
+
122
+ # Run in the background; on success, update last-refresh marker.
123
+ (
124
+ "$@" >/dev/null 2>&1 || true
125
+ git_cache_touch_last_refresh "$scope"
126
+ rm -rf "$lockdir" >/dev/null 2>&1 || true
127
+ ) >/dev/null 2>&1 &
128
+ }
129
+
130
+ git_cache_mode() {
131
+ # cached (default) | live
132
+ local m="${HAPPIER_STACK_SWIFTBAR_GIT_MODE:-cached}"
133
+ m="$(echo "$m" | tr '[:upper:]' '[:lower:]')"
134
+ [[ "$m" == "live" ]] && echo "live" || echo "cached"
135
+ }
136
+
137
+ git_cache_key() {
138
+ # Include context+stack because stacks can point components at different worktrees/dirs.
139
+ local context="$1"
140
+ local stack="$2"
141
+ local component="$3"
142
+ local active_dir="$4"
143
+ echo "ctx=${context}|stack=${stack}|comp=${component}|dir=${active_dir}"
144
+ }
145
+
146
+ git_cache_paths() {
147
+ # Usage: git_cache_paths <key>
148
+ # Output: meta<TAB>info<TAB>worktrees
149
+ local key="$1"
150
+ local dir
151
+ dir="$(git_cache_dir)"
152
+ local h
153
+ h="$(swiftbar_hash "$key")"
154
+ echo -e "${dir}/${h}.meta\t${dir}/${h}.info.tsv\t${dir}/${h}.worktrees.tsv"
155
+ }
156
+
157
+ git_cache_age_sec() {
158
+ local meta="$1"
159
+ [[ -f "$meta" ]] || { echo ""; return; }
160
+ local mtime now
161
+ mtime="$(stat -f %m "$meta" 2>/dev/null || echo 0)"
162
+ now="$(date +%s 2>/dev/null || echo 0)"
163
+ if [[ "$mtime" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ && "$now" -ge "$mtime" ]]; then
164
+ echo $((now - mtime))
165
+ else
166
+ echo ""
167
+ fi
168
+ }
169
+
170
+ git_cache_is_fresh() {
171
+ local meta="$1"
172
+ local ttl
173
+ ttl="$(git_cache_ttl_sec)"
174
+ local age
175
+ age="$(git_cache_age_sec "$meta")"
176
+ [[ -n "$age" && "$age" =~ ^[0-9]+$ && "$age" -le "$ttl" ]]
177
+ }
178
+
179
+ git_cache_write_meta() {
180
+ local meta="$1"
181
+ local key="$2"
182
+ mkdir -p "$(dirname "$meta")" 2>/dev/null || true
183
+ {
184
+ echo "key=$key"
185
+ echo "updated_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date)"
186
+ } >"$meta" 2>/dev/null || true
187
+ # touch to update mtime (age calculation uses mtime).
188
+ touch "$meta" 2>/dev/null || true
189
+ }
190
+
191
+ git_cache_refresh_one() {
192
+ # Computes and writes cached snapshot for one component context.
193
+ # Usage: git_cache_refresh_one <context> <stack> <component> <active_dir>
194
+ local context="$1"
195
+ local stack="$2"
196
+ local component="$3"
197
+ local active_dir="$4"
198
+
199
+ local key
200
+ key="$(git_cache_key "$context" "$stack" "$component" "$active_dir")"
201
+ local meta info wts
202
+ IFS=$'\t' read -r meta info wts <<<"$(git_cache_paths "$key")"
203
+
204
+ # Missing/non-repo: still write meta so we don't thrash.
205
+ if ! is_git_repo "$active_dir"; then
206
+ echo -e "missing\t${active_dir}\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-" >"$info" 2>/dev/null || true
207
+ : >"$wts" 2>/dev/null || true
208
+ git_cache_write_meta "$meta" "$key"
209
+ return 0
210
+ fi
211
+
212
+ # Collect snapshot.
213
+ local branch head upstream dirty ab ahead behind
214
+ branch="$(git_head_branch "$active_dir")"
215
+ head="$(git_head_short "$active_dir")"
216
+ upstream="$(git_upstream_short "$active_dir")"
217
+ dirty="$(git_dirty_flag "$active_dir")"
218
+ ab="$(git_ahead_behind "$active_dir")"
219
+ ahead=""
220
+ behind=""
221
+ if [[ -n "$ab" ]]; then
222
+ ahead="$(echo "$ab" | cut -d'|' -f1)"
223
+ behind="$(echo "$ab" | cut -d'|' -f2)"
224
+ fi
225
+
226
+ local main_branch main_upstream main_ab main_ahead main_behind
227
+ main_branch="$(git_main_branch_name "$active_dir")"
228
+ main_upstream=""
229
+ main_ahead=""
230
+ main_behind=""
231
+ if [[ -n "$main_branch" ]]; then
232
+ main_upstream="$(git_branch_upstream_short "$active_dir" "$main_branch")"
233
+ main_ab="$(git_branch_ahead_behind "$active_dir" "$main_branch")"
234
+ if [[ -n "$main_ab" ]]; then
235
+ main_ahead="$(echo "$main_ab" | cut -d'|' -f1)"
236
+ main_behind="$(echo "$main_ab" | cut -d'|' -f2)"
237
+ fi
238
+ fi
239
+
240
+ local oref uref oab o_ahead o_behind uab u_ahead u_behind
241
+ oref="$(git_remote_main_ref "$active_dir" "origin")"
242
+ uref="$(git_remote_main_ref "$active_dir" "upstream")"
243
+ o_ahead=""; o_behind=""; u_ahead=""; u_behind=""
244
+ if [[ -n "$main_branch" && -n "$oref" ]]; then
245
+ oab="$(git_ahead_behind_refs "$active_dir" "$oref" "$main_branch")"
246
+ if [[ -n "$oab" ]]; then
247
+ o_ahead="$(echo "$oab" | cut -d'|' -f1)"
248
+ o_behind="$(echo "$oab" | cut -d'|' -f2)"
249
+ fi
250
+ fi
251
+ if [[ -n "$main_branch" && -n "$uref" ]]; then
252
+ uab="$(git_ahead_behind_refs "$active_dir" "$uref" "$main_branch")"
253
+ if [[ -n "$uab" ]]; then
254
+ u_ahead="$(echo "$uab" | cut -d'|' -f1)"
255
+ u_behind="$(echo "$uab" | cut -d'|' -f2)"
256
+ fi
257
+ fi
258
+
259
+ local wt_count
260
+ wt_count="$(git_worktree_count "$active_dir")"
261
+ git_worktrees_tsv "$active_dir" >"$wts" 2>/dev/null || true
262
+
263
+ # status active_dir branch head upstream dirty ahead behind main_branch main_upstream main_ahead main_behind oref o_ahead o_behind uref u_ahead u_behind wt_count
264
+ echo -e "ok\t${active_dir}\t${branch}\t${head}\t${upstream}\t${dirty}\t${ahead}\t${behind}\t${main_branch}\t${main_upstream}\t${main_ahead}\t${main_behind}\t${oref}\t${o_ahead}\t${o_behind}\t${uref}\t${u_ahead}\t${u_behind}\t${wt_count}" >"$info" 2>/dev/null || true
265
+ git_cache_write_meta "$meta" "$key"
266
+ return 0
267
+ }
268
+
269
+ git_cache_load_or_refresh() {
270
+ # Usage: git_cache_load_or_refresh <context> <stack> <component> <active_dir> <allow_refresh_on_miss:0|1>
271
+ # Output: meta<TAB>info<TAB>worktrees<TAB>stale(0|1)
272
+ local context="$1"
273
+ local stack="$2"
274
+ local component="$3"
275
+ local active_dir="$4"
276
+ local allow_refresh_on_miss="${5:-0}"
277
+
278
+ local key
279
+ key="$(git_cache_key "$context" "$stack" "$component" "$active_dir")"
280
+ local meta info wts
281
+ IFS=$'\t' read -r meta info wts <<<"$(git_cache_paths "$key")"
282
+
283
+ # If cache exists and is fresh, use it.
284
+ if [[ -f "$meta" && -f "$info" ]]; then
285
+ if git_cache_is_fresh "$meta"; then
286
+ echo -e "${meta}\t${info}\t${wts}\t0"
287
+ return 0
288
+ fi
289
+ # Stale: do not refresh synchronously during menu render. Background refresh is handled elsewhere.
290
+ echo -e "${meta}\t${info}\t${wts}\t1"
291
+ return 0
292
+ fi
293
+
294
+ # Missing: only refresh synchronously when allowed by caller.
295
+ if [[ "$allow_refresh_on_miss" == "1" ]]; then
296
+ git_cache_refresh_one "$context" "$stack" "$component" "$active_dir" >/dev/null 2>&1 || true
297
+ if [[ -f "$info" ]]; then
298
+ echo -e "${meta}\t${info}\t${wts}\t0"
299
+ return 0
300
+ fi
301
+ fi
302
+
303
+ # Still missing; report missing and stale=1 so callers can show "refresh" action.
304
+ echo -e "${meta}\t${info}\t${wts}\t1"
305
+ return 0
306
+ }
307
+
308
+ git_try() {
309
+ local dir="$1"
310
+ shift
311
+ if ! command -v git >/dev/null 2>&1; then
312
+ return 1
313
+ fi
314
+ local subcmd="${1:-}"
315
+
316
+ # Run-cache: many stacks render the same component git info; cache by repo path + args for this SwiftBar run.
317
+ local cache_key="git|${dir}|$*"
318
+ swiftbar_cache_get "$cache_key"
319
+ local cached_rc=$?
320
+ if [[ $cached_rc -ne 111 ]]; then
321
+ # Cache hit: swiftbar_cache_get already printed stdout. Preserve rc.
322
+ return $cached_rc
323
+ fi
324
+
325
+ local t0 t1 rc out
326
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
327
+ out="$(git -C "$dir" "$@" 2>/dev/null)"
328
+ rc=$?
329
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
330
+ swiftbar_cache_set "$cache_key" "$rc" "$out"
331
+ # Keep label short; include subcommand for aggregation.
332
+ swiftbar_profile_log "time" "label=git" "subcmd=$subcmd" "ms=$((t1 - t0))" "rc=${rc}"
333
+ printf '%s\n' "$out"
334
+ return $rc
335
+ }
336
+
337
+ git_head_branch() {
338
+ local dir="$1"
339
+ git_try "$dir" rev-parse --abbrev-ref HEAD | head -1
340
+ }
341
+
342
+ git_head_short() {
343
+ local dir="$1"
344
+ git_try "$dir" rev-parse --short HEAD | head -1
345
+ }
346
+
347
+ git_upstream_short() {
348
+ local dir="$1"
349
+ # Prints like "origin/main" or "upstream/main"
350
+ git_try "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' | head -1 || true
351
+ }
352
+
353
+ git_ahead_behind() {
354
+ # Output: ahead|behind (numbers). Returns empty if no upstream.
355
+ local dir="$1"
356
+ local upstream
357
+ upstream="$(git_upstream_short "$dir")"
358
+ if [[ -z "$upstream" ]]; then
359
+ echo ""
360
+ return
361
+ fi
362
+ local counts
363
+ counts="$(git_try "$dir" rev-list --left-right --count "${upstream}...HEAD" | tr -s ' ' | sed 's/^ //')" || true
364
+ if [[ -z "$counts" ]]; then
365
+ echo ""
366
+ return
367
+ fi
368
+ # counts is "behind ahead"
369
+ local behind ahead
370
+ behind="$(echo "$counts" | awk '{print $1}')"
371
+ ahead="$(echo "$counts" | awk '{print $2}')"
372
+ if [[ -n "$ahead" && -n "$behind" ]]; then
373
+ echo "${ahead}|${behind}"
374
+ else
375
+ echo ""
376
+ fi
377
+ }
378
+
379
+ git_dirty_flag() {
380
+ # "clean" | "dirty" | "unknown"
381
+ local dir="$1"
382
+ if ! is_git_repo "$dir"; then
383
+ echo "unknown"
384
+ return
385
+ fi
386
+ local out
387
+ out="$(git_try "$dir" status --porcelain | head -1 || true)"
388
+ if [[ -n "$out" ]]; then
389
+ echo "dirty"
390
+ else
391
+ echo "clean"
392
+ fi
393
+ }
394
+
395
+ git_main_branch_name() {
396
+ local dir="$1"
397
+ if git_try "$dir" show-ref --verify --quiet refs/heads/main; then
398
+ echo "main"
399
+ return
400
+ fi
401
+ if git_try "$dir" show-ref --verify --quiet refs/heads/master; then
402
+ echo "master"
403
+ return
404
+ fi
405
+ echo ""
406
+ }
407
+
408
+ git_branch_upstream_short() {
409
+ local dir="$1"
410
+ local branch="$2"
411
+ if [[ -z "$branch" ]]; then
412
+ echo ""
413
+ return
414
+ fi
415
+ git_try "$dir" rev-parse --abbrev-ref --symbolic-full-name "${branch}@{u}" | head -1 || true
416
+ }
417
+
418
+ git_branch_ahead_behind() {
419
+ # Output: ahead|behind for a branch vs its upstream.
420
+ local dir="$1"
421
+ local branch="$2"
422
+ local upstream
423
+ upstream="$(git_branch_upstream_short "$dir" "$branch")"
424
+ if [[ -z "$branch" || -z "$upstream" ]]; then
425
+ echo ""
426
+ return
427
+ fi
428
+ local counts
429
+ counts="$(git_try "$dir" rev-list --left-right --count "${upstream}...${branch}" | tr -s ' ' | sed 's/^ //')" || true
430
+ if [[ -z "$counts" ]]; then
431
+ echo ""
432
+ return
433
+ fi
434
+ local behind ahead
435
+ behind="$(echo "$counts" | awk '{print $1}')"
436
+ ahead="$(echo "$counts" | awk '{print $2}')"
437
+ if [[ -n "$ahead" && -n "$behind" ]]; then
438
+ echo "${ahead}|${behind}"
439
+ else
440
+ echo ""
441
+ fi
442
+ }
443
+
444
+ git_ref_exists() {
445
+ local dir="$1"
446
+ local ref="$2"
447
+ [[ -n "$ref" ]] || return 1
448
+ git_try "$dir" show-ref --verify --quiet "$ref"
449
+ }
450
+
451
+ git_remote_main_ref() {
452
+ # Returns a remote tracking ref like refs/remotes/origin/main or refs/remotes/upstream/master.
453
+ local dir="$1"
454
+ local remote="$2"
455
+ if git_ref_exists "$dir" "refs/remotes/${remote}/main"; then
456
+ echo "refs/remotes/${remote}/main"
457
+ return
458
+ fi
459
+ if git_ref_exists "$dir" "refs/remotes/${remote}/master"; then
460
+ echo "refs/remotes/${remote}/master"
461
+ return
462
+ fi
463
+ echo ""
464
+ }
465
+
466
+ git_ahead_behind_refs() {
467
+ # Output: ahead|behind for local_ref compared to base_ref.
468
+ # Uses: git rev-list --left-right --count base...local => "behind ahead"
469
+ local dir="$1"
470
+ local base_ref="$2"
471
+ local local_ref="$3"
472
+ if [[ -z "$base_ref" || -z "$local_ref" ]]; then
473
+ echo ""
474
+ return
475
+ fi
476
+ local counts
477
+ counts="$(git_try "$dir" rev-list --left-right --count "${base_ref}...${local_ref}" | tr -s ' ' | sed 's/^ //')" || true
478
+ if [[ -z "$counts" ]]; then
479
+ echo ""
480
+ return
481
+ fi
482
+ local behind ahead
483
+ behind="$(echo "$counts" | awk '{print $1}')"
484
+ ahead="$(echo "$counts" | awk '{print $2}')"
485
+ if [[ -n "$ahead" && -n "$behind" ]]; then
486
+ echo "${ahead}|${behind}"
487
+ else
488
+ echo ""
489
+ fi
490
+ }
491
+
492
+ git_worktree_count() {
493
+ local dir="$1"
494
+ if ! is_git_repo "$dir"; then
495
+ echo ""
496
+ return
497
+ fi
498
+ local out
499
+ out="$(git_try "$dir" worktree list --porcelain || true)"
500
+ if [[ -z "$out" ]]; then
501
+ echo ""
502
+ return
503
+ fi
504
+ # Count "worktree <path>" blocks.
505
+ echo "$out" | awk '/^worktree /{c++} END{ if (c>0) print c; }'
506
+ }
507
+
508
+ git_worktrees_tsv() {
509
+ # Output: path<TAB>branchRefOrEmpty
510
+ # Example branch line in porcelain: "branch refs/heads/pr/foo"
511
+ local dir="$1"
512
+ if ! is_git_repo "$dir"; then
513
+ return
514
+ fi
515
+ local out
516
+ out="$(git_try "$dir" worktree list --porcelain || true)"
517
+ if [[ -z "$out" ]]; then
518
+ return
519
+ fi
520
+
521
+ local wt_path="" wt_branch=""
522
+ while IFS= read -r line; do
523
+ # Block separator
524
+ if [[ -z "$line" ]]; then
525
+ if [[ -n "$wt_path" ]]; then
526
+ echo -e "${wt_path}\t${wt_branch}"
527
+ fi
528
+ wt_path=""
529
+ wt_branch=""
530
+ continue
531
+ fi
532
+
533
+ if [[ "$line" == worktree\ * ]]; then
534
+ wt_path="${line#worktree }"
535
+ continue
536
+ fi
537
+ if [[ "$line" == branch\ * ]]; then
538
+ wt_branch="${line#branch }"
539
+ continue
540
+ fi
541
+ # ignore HEAD/detached lines
542
+ done <<<"$out"
543
+
544
+ if [[ -n "$wt_path" ]]; then
545
+ echo -e "${wt_path}\t${wt_branch}"
546
+ fi
547
+ }
548
+
549
+ resolve_component_dir_from_env_file() {
550
+ # Resolve the active repo directory based on a stack env file (repo-only model).
551
+ # Usage: resolve_component_dir_from_env_file <env_file> <component>
552
+ # NOTE: <component> is ignored; kept for backwards compatibility with older SwiftBar renderers.
553
+ local env_file="$1"
554
+ local component="$2"
555
+ : "$component"
556
+ local stacks_key="HAPPIER_STACK_REPO_DIR"
557
+
558
+ local fallback
559
+ fallback="$(resolve_workspace_dir)/happier"
560
+ if [[ -z "$env_file" || ! -f "$env_file" ]]; then
561
+ echo "$fallback"
562
+ return
563
+ fi
564
+
565
+ local raw
566
+ raw="$(dotenv_get "$env_file" "$stacks_key")"
567
+ if [[ -z "$raw" ]]; then
568
+ echo "$fallback"
569
+ return
570
+ fi
571
+
572
+ if [[ "$raw" == "~/"* ]]; then
573
+ raw="$HOME/${raw#~/}"
574
+ fi
575
+ if [[ "$raw" == /* ]]; then
576
+ echo "$raw"
577
+ else
578
+ echo "$(resolve_workspace_dir)/$raw"
579
+ fi
580
+ }
581
+
582
+ resolve_component_dir_from_env() {
583
+ # Resolve active repo directory based on env + env.local + .env (repo-only model).
584
+ # Usage: resolve_component_dir_from_env <component>
585
+ # NOTE: <component> is ignored; kept for backwards compatibility with older SwiftBar renderers.
586
+ # Output: absolute path (best-effort). Falls back to <workspace>/happier.
587
+ local component="$1"
588
+ : "$component"
589
+ local stacks_key="HAPPIER_STACK_REPO_DIR"
590
+
591
+ local raw=""
592
+ if [[ -n "$stacks_key" && -n "${!stacks_key:-}" ]]; then
593
+ raw="${!stacks_key}"
594
+ fi
595
+
596
+ local env_file
597
+ env_file="$(resolve_main_env_file)"
598
+ if [[ -z "$raw" && -n "$env_file" && -n "$stacks_key" ]]; then
599
+ raw="$(dotenv_get "$env_file" "$stacks_key")"
600
+ fi
601
+
602
+ local home
603
+ home="$(resolve_home_dir)"
604
+ if [[ -z "$raw" && -n "$stacks_key" ]]; then
605
+ raw="$(dotenv_get "$home/env.local" "$stacks_key")"
606
+ fi
607
+ if [[ -z "$raw" && -n "$stacks_key" ]]; then
608
+ raw="$(dotenv_get "$home/.env" "$stacks_key")"
609
+ fi
610
+
611
+ local fallback
612
+ fallback="$(resolve_workspace_dir)/happier"
613
+ if [[ -z "$raw" ]]; then
614
+ echo "$fallback"
615
+ return
616
+ fi
617
+
618
+ # Expand ~
619
+ if [[ "$raw" == "~/"* ]]; then
620
+ raw="$HOME/${raw#~/}"
621
+ fi
622
+
623
+ # Absolute vs relative (relative is interpreted relative to the repo root).
624
+ if [[ "$raw" == /* ]]; then
625
+ echo "$raw"
626
+ else
627
+ echo "$(resolve_workspace_dir)/$raw"
628
+ fi
629
+ }