@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,999 @@
1
+ #!/bin/bash
2
+
3
+ # Requires:
4
+ # - utils.sh
5
+ # - icons.sh
6
+ # - system.sh
7
+
8
+ # This file is sourced/executed under strict bash modes in some contexts.
9
+ # Ensure these env vars are always defined to avoid `set -u` crashes in tests.
10
+ hstack_BIN="${hstack_BIN:-}"
11
+ hstack_TERM="${hstack_TERM:-$hstack_BIN}"
12
+ hstack_ROOT_DIR="${hstack_ROOT_DIR:-}"
13
+
14
+ level_from_server_daemon() {
15
+ local server_status="$1"
16
+ local daemon_status="$2"
17
+ if [[ "$server_status" == "running" && "$daemon_status" == "running" ]]; then
18
+ echo "green"
19
+ return
20
+ fi
21
+ if [[ "$server_status" == "running" || "$daemon_status" == "running" ]]; then
22
+ echo "orange"
23
+ return
24
+ fi
25
+ echo "red"
26
+ }
27
+
28
+ color_for_level() {
29
+ local level="$1"
30
+ if [[ "$level" == "green" ]]; then echo "#16a34a"; return; fi
31
+ if [[ "$level" == "orange" ]]; then echo "#f59e0b"; return; fi
32
+ echo "#e74c3c"
33
+ }
34
+
35
+ sfconfig_for_level() {
36
+ local level="$1"
37
+ local color="$(color_for_level "$level")"
38
+ # sfconfig SFSymbol configuration Configures Rendering Mode for sfimage. Accepts a json encoded as base64, example json {"renderingMode":"Palette", "colors":["red","blue"], "scale": "large", "weight": "bold"}. Original issue #354
39
+ echo "{\"colors\":[\"$color\"], \"scale\": \"small\"}" | base64 -b 0
40
+ }
41
+
42
+ sf_for_level() {
43
+ local level="$1"
44
+ if [[ "$level" == "green" ]]; then echo "checkmark.circle.fill"; return; fi
45
+ if [[ "$level" == "orange" ]]; then echo "exclamationmark.triangle.fill"; return; fi
46
+ echo "xmark.circle.fill"
47
+ }
48
+
49
+ sf_suffix_for_level() {
50
+ local level="$1"
51
+ if [[ "$level" == "green" ]]; then echo "badge.checkmark"; return; fi
52
+ if [[ "$level" == "orange" ]]; then echo "trianglebadge.exclamationmark"; return; fi
53
+ echo "badge.xmark"
54
+ }
55
+
56
+ print_item() {
57
+ local prefix="$1"
58
+ shift
59
+ echo "${prefix}$*"
60
+ }
61
+
62
+ print_sep() {
63
+ local prefix="$1"
64
+ print_item "$prefix" "---"
65
+ }
66
+
67
+ render_component_server() {
68
+ local prefix="$1" # "" for top-level, "--" for stack submenu
69
+ local stack_name="$2" # main | <name>
70
+ local port="$3"
71
+ local server_component="$4"
72
+ local server_status="$5"
73
+ local server_pid="$6"
74
+ local server_metrics="$7"
75
+ local tailscale_url="$8" # main only (optional)
76
+ local launch_label="${9:-}" # optional (com.happier.stacks[.<stack>])
77
+
78
+ local level="red"
79
+ [[ "$server_status" == "running" ]] && level="green"
80
+
81
+ local label="Server (${server_component})"
82
+ local sf="$(sf_for_level "$level")"
83
+ local sfconfig="$(sfconfig_for_level "$level")"
84
+ print_item "$prefix" "$label | sfimage=$sf sfconfig=$sfconfig"
85
+
86
+ local p2="${prefix}--"
87
+ print_item "$p2" "Status: $server_status"
88
+ if [[ -n "$port" ]]; then
89
+ print_item "$p2" "Internal: http://127.0.0.1:${port}"
90
+ else
91
+ print_item "$p2" "Port: ephemeral (allocated at start time)"
92
+ fi
93
+ if [[ -n "$server_pid" ]]; then
94
+ if [[ -n "$server_metrics" ]]; then
95
+ local cpu mem etime
96
+ cpu="$(echo "$server_metrics" | cut -d'|' -f1)"
97
+ mem="$(echo "$server_metrics" | cut -d'|' -f2)"
98
+ etime="$(echo "$server_metrics" | cut -d'|' -f3)"
99
+ print_item "$p2" "PID: ${server_pid}, CPU: ${cpu}%, RAM: ${mem}MB, Uptime: ${etime}"
100
+ else
101
+ print_item "$p2" "PID: ${server_pid}"
102
+ fi
103
+ fi
104
+ if [[ -n "$port" ]]; then
105
+ print_item "$p2" "Open UI (local) | href=http://localhost:${port}/"
106
+ print_item "$p2" "Open Health | href=http://127.0.0.1:${port}/health"
107
+ fi
108
+ if [[ -n "$tailscale_url" ]]; then
109
+ print_item "$p2" "Open UI (Tailscale) | href=$tailscale_url"
110
+ fi
111
+
112
+ # Start/stop shortcuts (so you can control from the Server submenu too).
113
+ if [[ -n "$hstack_BIN" ]]; then
114
+ local plist=""
115
+ local svc_installed="0"
116
+ if ! swiftbar_is_sandboxed; then
117
+ if [[ -n "$launch_label" ]]; then
118
+ plist="$HOME/Library/LaunchAgents/${launch_label}.plist"
119
+ [[ -f "$plist" ]] && svc_installed="1"
120
+ fi
121
+ fi
122
+
123
+ print_sep "$p2"
124
+ if [[ "$stack_name" == "main" ]]; then
125
+ if [[ "$svc_installed" == "1" ]]; then
126
+ if [[ "$server_status" == "running" ]]; then
127
+ print_item "$p2" "Stop stack (service) | bash=$hstack_BIN param1=service:stop dir=$hstack_ROOT_DIR terminal=false refresh=true"
128
+ else
129
+ print_item "$p2" "Start stack (service) | bash=$hstack_BIN param1=service:start dir=$hstack_ROOT_DIR terminal=false refresh=true"
130
+ fi
131
+ print_item "$p2" "Restart stack (service) | bash=$hstack_BIN param1=service:restart dir=$hstack_ROOT_DIR terminal=false refresh=true"
132
+ else
133
+ if [[ "$server_status" == "running" ]]; then
134
+ print_item "$p2" "Stop stack | bash=$hstack_BIN param1=stack param2=stop param3=main dir=$hstack_ROOT_DIR terminal=false refresh=true"
135
+ else
136
+ print_item "$p2" "Start stack (foreground) | bash=$hstack_TERM param1=start dir=$hstack_ROOT_DIR terminal=false refresh=true"
137
+ fi
138
+ fi
139
+ else
140
+ if [[ "$svc_installed" == "1" ]]; then
141
+ if [[ "$server_status" == "running" ]]; then
142
+ print_item "$p2" "Stop stack (service) | bash=$hstack_BIN param1=stack param2=service:stop param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
143
+ else
144
+ print_item "$p2" "Start stack (service) | bash=$hstack_BIN param1=stack param2=service:start param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
145
+ fi
146
+ print_item "$p2" "Restart stack (service) | bash=$hstack_BIN param1=stack param2=service:restart param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
147
+ else
148
+ if [[ "$server_status" == "running" ]]; then
149
+ print_item "$p2" "Stop stack | bash=$hstack_BIN param1=stack param2=stop param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
150
+ else
151
+ print_item "$p2" "Start stack (foreground) | bash=$hstack_TERM param1=stack param2=start param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
152
+ fi
153
+ fi
154
+ fi
155
+ fi
156
+
157
+ # Flavor switching (status-aware: only show switching to the other option).
158
+ local helper="$hstack_ROOT_DIR/extras/swiftbar/set-server-flavor.sh"
159
+ if [[ -n "$hstack_BIN" ]]; then
160
+ print_sep "$p2"
161
+ if [[ "$server_component" == "happier-server" ]]; then
162
+ print_item "$p2" "Switch to happier-server-light (restart if service installed) | bash=$helper param1=$stack_name param2=happier-server-light dir=$hstack_ROOT_DIR terminal=false refresh=true"
163
+ else
164
+ print_item "$p2" "Switch to happier-server (restart if service installed) | bash=$helper param1=$stack_name param2=happier-server dir=$hstack_ROOT_DIR terminal=false refresh=true"
165
+ fi
166
+ if [[ "$stack_name" == "main" ]]; then
167
+ print_item "$p2" "Show flavor status | bash=$hstack_TERM param1=srv param2=-- param3=status dir=$hstack_ROOT_DIR terminal=false refresh=true"
168
+ else
169
+ print_item "$p2" "Show flavor status | bash=$hstack_TERM param1=stack param2=srv param3=$stack_name param4=-- param5=status dir=$hstack_ROOT_DIR terminal=false refresh=true"
170
+ fi
171
+ fi
172
+ }
173
+
174
+ render_component_daemon() {
175
+ local prefix="$1"
176
+ local daemon_status="$2" # running|stale|stopped|unknown|running-no-http|auth_required|starting
177
+ local daemon_pid="$3"
178
+ local daemon_metrics="$4"
179
+ local daemon_uptime="$5"
180
+ local last_heartbeat="$6"
181
+ local state_file="$7"
182
+ local stack_name="$8"
183
+
184
+ local level="red"
185
+ if [[ "$daemon_status" == "running" ]]; then level="green"; fi
186
+ if [[ "$daemon_status" == "running-no-http" || "$daemon_status" == "stale" || "$daemon_status" == "auth_required" || "$daemon_status" == "starting" ]]; then level="orange"; fi
187
+
188
+ local sfconfig="$(sfconfig_for_level "$level")"
189
+
190
+ local sf="$(sf_for_level "$level")"
191
+ print_item "$prefix" "Daemon | sfimage=$sf sfconfig=$sfconfig"
192
+
193
+ local p2="${prefix}--"
194
+ print_item "$p2" "Status: $daemon_status"
195
+ if [[ -n "$daemon_pid" ]]; then
196
+ if [[ -n "$daemon_metrics" ]]; then
197
+ local cpu mem etime
198
+ cpu="$(echo "$daemon_metrics" | cut -d'|' -f1)"
199
+ mem="$(echo "$daemon_metrics" | cut -d'|' -f2)"
200
+ etime="$(echo "$daemon_metrics" | cut -d'|' -f3)"
201
+ print_item "$p2" "PID: ${daemon_pid}, CPU: ${cpu}%, RAM: ${mem}MB, Uptime: ${etime}"
202
+ else
203
+ print_item "$p2" "PID: ${daemon_pid}"
204
+ fi
205
+ fi
206
+ [[ -n "$daemon_uptime" ]] && print_item "$p2" "Started: $(shorten_text "$daemon_uptime" 52)"
207
+ [[ -n "$last_heartbeat" ]] && print_item "$p2" "Last heartbeat: $(shorten_text "$last_heartbeat" 52)"
208
+ # State file may not exist yet (e.g. daemon is waiting for auth).
209
+ print_item "$p2" "State file: $(shorten_path "$state_file" 52)"
210
+
211
+ if [[ -n "$hstack_BIN" ]]; then
212
+ print_sep "$p2"
213
+ if [[ "$daemon_status" == "auth_required" ]]; then
214
+ # Provide a direct "fix" action for the common first-run problem under launchd.
215
+ local auth_helper="$hstack_ROOT_DIR/extras/swiftbar/auth-login.sh"
216
+ if [[ "$stack_name" == "main" ]]; then
217
+ print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=main dir=$hstack_ROOT_DIR terminal=false refresh=false"
218
+ else
219
+ # For stacks, best-effort use the stack's configured port if available (fallback to main port).
220
+ local env_file
221
+ env_file="$(resolve_stack_env_file "$stack_name")"
222
+ local port
223
+ port="$(dotenv_get "$env_file" "HAPPIER_STACK_SERVER_PORT")"
224
+ [[ -z "$port" ]] && port="$(resolve_main_port)"
225
+ print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=false"
226
+ fi
227
+ print_sep "$p2"
228
+ fi
229
+
230
+ print_item "$p2" "Restart daemon | bash=$hstack_BIN param1=stack param2=daemon param3=$stack_name param4=restart dir=$hstack_ROOT_DIR terminal=false refresh=true"
231
+ print_item "$p2" "Show daemon status (CLI) | bash=$hstack_TERM param1=stack param2=daemon param3=$stack_name param4=status dir=$hstack_ROOT_DIR terminal=false refresh=true"
232
+
233
+ if ! swiftbar_is_sandboxed; then
234
+ if [[ "$stack_name" == "main" ]]; then
235
+ print_item "$p2" "Restart stack (service) | bash=$hstack_BIN param1=service:restart dir=$hstack_ROOT_DIR terminal=false refresh=true"
236
+ else
237
+ print_item "$p2" "Restart stack (service) | bash=$hstack_BIN param1=stack param2=service:restart param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
238
+ fi
239
+ fi
240
+ fi
241
+ }
242
+
243
+ render_component_autostart() {
244
+ local prefix="$1"
245
+ local stack_name="$2"
246
+ local label="$3"
247
+ local launchagent_status="$4" # loaded|unloaded|not_installed
248
+ local autostart_pid="$5"
249
+ local autostart_metrics="$6"
250
+ local logs_dir="$7"
251
+
252
+ if swiftbar_is_sandboxed; then
253
+ print_item "$prefix" "Autostart | sfimage=exclamationmark.triangle sfconfig=light"
254
+ local p2="${prefix}--"
255
+ print_item "$p2" "Status: disabled in sandbox"
256
+ return
257
+ fi
258
+
259
+ local level="red"
260
+ if [[ "$launchagent_status" == "loaded" ]]; then level="green"; fi
261
+ if [[ "$launchagent_status" == "unloaded" ]]; then level="orange"; fi
262
+
263
+ local sf="$(sf_for_level "$level")"
264
+ local sfconfig="$(sfconfig_for_level "$level")"
265
+ print_item "$prefix" "Autostart | sfimage=$sf sfconfig=$sfconfig"
266
+
267
+ local p2="${prefix}--"
268
+ print_item "$p2" "Status: $launchagent_status"
269
+ print_item "$p2" "Plist: $(shorten_path "$HOME/Library/LaunchAgents/${label}.plist" 52)"
270
+ if [[ -n "$autostart_pid" ]]; then
271
+ if [[ -n "$autostart_metrics" ]]; then
272
+ local cpu mem etime
273
+ cpu="$(echo "$autostart_metrics" | cut -d'|' -f1)"
274
+ mem="$(echo "$autostart_metrics" | cut -d'|' -f2)"
275
+ etime="$(echo "$autostart_metrics" | cut -d'|' -f3)"
276
+ print_item "$p2" "PID: ${autostart_pid}, CPU: ${cpu}%, RAM: ${mem}MB, Uptime: ${etime}"
277
+ else
278
+ print_item "$p2" "PID: ${autostart_pid}"
279
+ fi
280
+ fi
281
+ local stdout_file="hstack.out.log"
282
+ local stderr_file="hstack.err.log"
283
+ print_item "$p2" "Open logs (stdout) | bash=/usr/bin/open param1=-a param2=Console param3='${logs_dir}/${stdout_file}' terminal=false"
284
+ print_item "$p2" "Open logs (stderr) | bash=/usr/bin/open param1=-a param2=Console param3='${logs_dir}/${stderr_file}' terminal=false"
285
+
286
+ if [[ -z "$hstack_BIN" ]]; then
287
+ return
288
+ fi
289
+ print_sep "$p2"
290
+ if [[ "$stack_name" == "main" ]]; then
291
+ if [[ "$launchagent_status" == "not_installed" ]]; then
292
+ print_item "$p2" "Install Autostart | bash=$hstack_BIN param1=service:install dir=$hstack_ROOT_DIR terminal=false refresh=true"
293
+ return
294
+ fi
295
+ # Status-aware: show only the relevant toggle (enable vs disable).
296
+ if [[ "$launchagent_status" == "loaded" ]]; then
297
+ print_item "$p2" "Disable Autostart | bash=$hstack_BIN param1=service:disable dir=$hstack_ROOT_DIR terminal=false refresh=true"
298
+ else
299
+ print_item "$p2" "Enable Autostart | bash=$hstack_BIN param1=service:enable dir=$hstack_ROOT_DIR terminal=false refresh=true"
300
+ fi
301
+ print_item "$p2" "Uninstall Autostart | bash=$hstack_BIN param1=service:uninstall dir=$hstack_ROOT_DIR terminal=false refresh=true"
302
+ return
303
+ fi
304
+
305
+ if [[ "$launchagent_status" == "not_installed" ]]; then
306
+ print_item "$p2" "Install Autostart | bash=$hstack_BIN param1=stack param2=service:install param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
307
+ return
308
+ fi
309
+ # Status-aware: show only the relevant toggle (enable vs disable).
310
+ if [[ "$launchagent_status" == "loaded" ]]; then
311
+ print_item "$p2" "Disable Autostart | bash=$hstack_BIN param1=stack param2=service:disable param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
312
+ else
313
+ print_item "$p2" "Enable Autostart | bash=$hstack_BIN param1=stack param2=service:enable param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
314
+ fi
315
+ print_item "$p2" "Uninstall Autostart | bash=$hstack_BIN param1=stack param2=service:uninstall param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
316
+ }
317
+
318
+ render_component_tailscale() {
319
+ local prefix="$1"
320
+ local stack_name="$2"
321
+ local tailscale_url="$3"
322
+
323
+ local level="red"
324
+ [[ -n "$tailscale_url" ]] && level="green"
325
+
326
+ local sf="$(sf_for_level "$level")"
327
+ local sfconfig="$(sfconfig_for_level "$level")"
328
+ print_item "$prefix" "Tailscale | sfimage=$sf sfconfig=$sfconfig"
329
+
330
+ local p2="${prefix}--"
331
+ if [[ -n "$tailscale_url" ]]; then
332
+ local display="$tailscale_url"
333
+ [[ ${#display} -gt 48 ]] && display="${display:0:48}..."
334
+ print_item "$p2" "URL: $display"
335
+ print_item "$p2" "Copy URL | bash=/bin/bash param1=-c param2='echo -n \"$tailscale_url\" | pbcopy' terminal=false"
336
+ print_item "$p2" "Open URL | href=$tailscale_url"
337
+ else
338
+ print_item "$p2" "Status: not configured / unknown"
339
+ fi
340
+
341
+ # Tailscale Serve is global machine state; never offer enable/disable actions in sandbox mode.
342
+ if swiftbar_is_sandboxed; then
343
+ return
344
+ fi
345
+
346
+ if [[ -z "$hstack_BIN" ]]; then
347
+ return
348
+ fi
349
+ print_sep "$p2"
350
+
351
+ if [[ "$stack_name" == "main" ]]; then
352
+ print_item "$p2" "Tailscale status | bash=$hstack_TERM param1=tailscale:status dir=$hstack_ROOT_DIR terminal=false refresh=true"
353
+ if [[ -n "$tailscale_url" ]]; then
354
+ print_item "$p2" "Disable Tailscale Serve | bash=$hstack_BIN param1=tailscale:disable dir=$hstack_ROOT_DIR terminal=false refresh=true"
355
+ else
356
+ print_item "$p2" "Enable Tailscale Serve | bash=$hstack_TERM param1=tailscale:enable dir=$hstack_ROOT_DIR terminal=false refresh=true"
357
+ fi
358
+ print_item "$p2" "Print URL | bash=$hstack_TERM param1=tailscale:url dir=$hstack_ROOT_DIR terminal=false refresh=true"
359
+ return
360
+ fi
361
+
362
+ print_item "$p2" "Tailscale status | bash=$hstack_TERM param1=stack param2=tailscale:status param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
363
+ if [[ -n "$tailscale_url" ]]; then
364
+ print_item "$p2" "Disable Tailscale Serve | bash=$hstack_BIN param1=stack param2=tailscale:disable param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
365
+ else
366
+ print_item "$p2" "Enable Tailscale Serve | bash=$hstack_TERM param1=stack param2=tailscale:enable param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
367
+ fi
368
+ print_item "$p2" "Print URL | bash=$hstack_TERM param1=stack param2=tailscale:url param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
369
+ }
370
+
371
+ render_component_repo() {
372
+ # Git/worktree component view (unified UI).
373
+ # Usage:
374
+ # render_component_repo <prefix> <component> <context> <stack_name> <env_file> <shared_repo_root?>
375
+ # context: main|stack
376
+ local prefix="$1"
377
+ local component="$2"
378
+ local context="$3"
379
+ local stack_name="$4"
380
+ local env_file="$5"
381
+ local shared_repo_root="${6:-}"
382
+
383
+ local t0 t1
384
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
385
+
386
+ local active_dir=""
387
+ # If we have an env file for the current context, prefer it (stack env is authoritative).
388
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
389
+ active_dir="$(resolve_component_dir_from_env_file "$env_file" "$component")"
390
+ else
391
+ active_dir="$(resolve_component_dir_from_env "$component")"
392
+ fi
393
+
394
+ local level="red"
395
+ local detail="missing"
396
+
397
+ local git_mode
398
+ git_mode="$(git_cache_mode)"
399
+
400
+ local stale="0"
401
+ local meta="" info="" wts=""
402
+ if [[ "$git_mode" == "cached" ]]; then
403
+ # Never refresh synchronously during menu render.
404
+ IFS=$'\t' read -r meta info wts stale <<<"$(git_cache_load_or_refresh "$context" "$stack_name" "$component" "$active_dir" "0")"
405
+ fi
406
+
407
+ local status="missing"
408
+ local dirty="" ahead="" behind="" wt_count=""
409
+ local branch="" head="" upstream=""
410
+ local main_branch="" main_upstream="" main_ahead="" main_behind=""
411
+ local oref="" o_ahead="" o_behind="" uref="" u_ahead="" u_behind=""
412
+
413
+ if [[ "$git_mode" == "cached" && -f "$info" ]]; then
414
+ IFS=$'\t' read -r status _ad 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 <"$info" || true
415
+ elif [[ "$git_mode" == "live" ]]; then
416
+ # live mode only: do git work on every refresh
417
+ if is_git_repo "$active_dir"; then
418
+ status="ok"
419
+ dirty="$(git_dirty_flag "$active_dir")"
420
+ local ab
421
+ ab="$(git_ahead_behind "$active_dir")"
422
+ if [[ -n "$ab" ]]; then
423
+ ahead="$(echo "$ab" | cut -d'|' -f1)"
424
+ behind="$(echo "$ab" | cut -d'|' -f2)"
425
+ fi
426
+ fi
427
+ fi
428
+
429
+ if [[ "$status" == "ok" ]]; then
430
+ detail="ok"
431
+ if [[ "$dirty" == "dirty" ]] || [[ -n "$behind" && "$behind" != "0" ]]; then
432
+ level="orange"
433
+ else
434
+ level="green"
435
+ fi
436
+ fi
437
+
438
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
439
+ swiftbar_profile_log "time" "label=render_component_repo" "component=${component}" "context=${context}" "ms=$((t1 - t0))" "detail=${detail}"
440
+
441
+ local sf color
442
+ sf="$(sf_for_level "$level")"
443
+ color="$(color_for_level "$level")"
444
+ print_item "$prefix" "${component} | sfimage=$sf color=$color"
445
+
446
+ local p2="${prefix}--"
447
+ local repo_root=""
448
+ local rel_dir=""
449
+ repo_root="$(swiftbar_find_git_root_upwards "$active_dir" 2>/dev/null || true)"
450
+ local repo_key=""
451
+ repo_key="$(swiftbar_repo_key_from_path "$active_dir" 2>/dev/null || true)"
452
+ [[ -n "$repo_key" ]] || repo_key="$component"
453
+ local mono_repo="0"
454
+ if [[ -n "$shared_repo_root" && -n "$repo_root" && "$shared_repo_root" == "$repo_root" ]]; then
455
+ mono_repo="1"
456
+ fi
457
+
458
+ if [[ -n "$repo_root" && "$repo_root" != "$active_dir" && "$active_dir" == "$repo_root/"* ]]; then
459
+ rel_dir="${active_dir#"$repo_root"/}"
460
+ if [[ -n "$shared_repo_root" && "$shared_repo_root" == "$repo_root" ]]; then
461
+ print_item "$p2" "Dir: $(shorten_text "$rel_dir" 52)"
462
+ else
463
+ print_item "$p2" "Repo: $(shorten_path "$repo_root" 52)"
464
+ print_item "$p2" "Dir: $(shorten_text "$rel_dir" 52)"
465
+ fi
466
+ else
467
+ print_item "$p2" "Dir: $(shorten_path "$active_dir" 52)"
468
+ repo_root=""
469
+ rel_dir=""
470
+ fi
471
+ if [[ "$detail" != "ok" ]]; then
472
+ if [[ "$git_mode" == "cached" ]]; then
473
+ print_item "$p2" "Status: git cache missing (or not a git repo)"
474
+ local refresh="$hstack_ROOT_DIR/extras/swiftbar/git-cache-refresh.sh"
475
+ if [[ -x "$refresh" ]]; then
476
+ print_sep "$p2"
477
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
478
+ print_item "$p2" "Refresh Git cache (this stack) | bash=$refresh param1=stack param2=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
479
+ else
480
+ print_item "$p2" "Refresh Git cache (main) | bash=$refresh param1=main dir=$hstack_ROOT_DIR terminal=false refresh=true"
481
+ fi
482
+ fi
483
+ else
484
+ print_item "$p2" "Status: not a git repo / missing"
485
+ fi
486
+ if [[ -n "$hstack_BIN" ]]; then
487
+ print_sep "$p2"
488
+ print_item "$p2" "Bootstrap (clone missing repos) | bash=$hstack_TERM param1=bootstrap dir=$hstack_ROOT_DIR terminal=false refresh=true"
489
+ fi
490
+ return
491
+ fi
492
+
493
+ # Cache status + refresh actions
494
+ if [[ "$git_mode" == "cached" ]]; then
495
+ local age=""
496
+ age="$(git_cache_age_sec "$meta")"
497
+ if [[ -n "$age" ]]; then
498
+ if [[ "$stale" == "1" ]]; then
499
+ print_item "$p2" "Git cache: stale (${age}s old) | color=$YELLOW"
500
+ else
501
+ print_item "$p2" "Git cache: fresh (${age}s old) | color=$GRAY"
502
+ fi
503
+ fi
504
+ local refresh="$hstack_ROOT_DIR/extras/swiftbar/git-cache-refresh.sh"
505
+ if [[ -x "$refresh" ]]; then
506
+ print_sep "$p2"
507
+ print_item "$p2" "Refresh Git cache (this component) | bash=$refresh param1=component param2=$context param3=$stack_name param4=$component dir=$hstack_ROOT_DIR terminal=false refresh=true"
508
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
509
+ print_item "$p2" "Refresh Git cache (this stack) | bash=$refresh param1=stack param2=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
510
+ fi
511
+ fi
512
+ fi
513
+
514
+ print_sep "$p2"
515
+ print_item "$p2" "HEAD: ${branch:-"(unknown)"} ${head:+($head)}"
516
+ print_item "$p2" "Upstream: ${upstream:-"(none)"}"
517
+ if [[ -n "$ahead" && -n "$behind" ]]; then
518
+ print_item "$p2" "Ahead/Behind: ${ahead}/${behind}"
519
+ fi
520
+ print_item "$p2" "Working tree: ${dirty}"
521
+
522
+ if [[ -n "$main_branch" ]]; then
523
+ if [[ -n "$main_upstream" ]]; then
524
+ print_item "$p2" "Main: ${main_branch} → ${main_upstream}"
525
+ else
526
+ print_item "$p2" "Main: ${main_branch} → (no upstream)"
527
+ fi
528
+ if [[ -n "$main_ahead" && -n "$main_behind" ]]; then
529
+ print_item "$p2" "Main ahead/behind: ${main_ahead}/${main_behind}"
530
+ fi
531
+
532
+ # Always show comparisons against origin/* and upstream/* when those remote refs exist.
533
+ # (These reflect your last fetch; we do not auto-fetch in the menu.)
534
+ if [[ -n "$oref" ]]; then
535
+ local oref_short="${oref#refs/remotes/}"
536
+ if [[ -n "$o_ahead" && -n "$o_behind" ]]; then
537
+ print_item "$p2" "Origin: ${oref_short} ahead/behind: ${o_ahead}/${o_behind}"
538
+ else
539
+ print_item "$p2" "Origin: ${oref_short}"
540
+ fi
541
+ else
542
+ print_item "$p2" "Origin: (no origin/main|master ref)"
543
+ fi
544
+ if [[ -n "$uref" ]]; then
545
+ local uref_short="${uref#refs/remotes/}"
546
+ if [[ -n "$u_ahead" && -n "$u_behind" ]]; then
547
+ print_item "$p2" "Upstream: ${uref_short} ahead/behind: ${u_ahead}/${u_behind}"
548
+ else
549
+ print_item "$p2" "Upstream: ${uref_short}"
550
+ fi
551
+ else
552
+ print_item "$p2" "Upstream: (no upstream/main|master ref)"
553
+ fi
554
+ fi
555
+
556
+ local wt_count
557
+ # If cache didn't populate wt_count, fall back to empty string.
558
+ wt_count="${wt_count:-}"
559
+
560
+ # Quick actions
561
+ print_sep "$p2"
562
+ if [[ -n "$repo_root" ]]; then
563
+ print_item "$p2" "Open package folder | bash=/usr/bin/open param1='$active_dir' terminal=false"
564
+ print_item "$p2" "Open repo root | bash=/usr/bin/open param1='$repo_root' terminal=false"
565
+ else
566
+ print_item "$p2" "Open folder | bash=/usr/bin/open param1='$active_dir' terminal=false"
567
+ fi
568
+
569
+ if [[ -n "${hstack_BIN:-}" ]]; then
570
+ # Run via stack wrappers when in a stack context so env-file stays authoritative.
571
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
572
+ print_item "$p2" "Status (active) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=status param6=active dir=$hstack_ROOT_DIR terminal=false"
573
+ print_item "$p2" "Sync mirror (upstream/main) | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=sync dir=$hstack_ROOT_DIR terminal=false refresh=true"
574
+ print_item "$p2" "Update (dry-run) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=update param6=active param7=--dry-run dir=$hstack_ROOT_DIR terminal=false"
575
+ print_item "$p2" "Update (apply) | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=update param6=active dir=$hstack_ROOT_DIR terminal=false refresh=true"
576
+ print_item "$p2" "Update (apply + stash) | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=update param6=active param7=--stash dir=$hstack_ROOT_DIR terminal=false refresh=true"
577
+ else
578
+ print_item "$p2" "Status (active) | bash=$hstack_TERM param1=wt param2=status param3=active dir=$hstack_ROOT_DIR terminal=false"
579
+ print_item "$p2" "Sync mirror (upstream/main) | bash=$hstack_BIN param1=wt param2=sync dir=$hstack_ROOT_DIR terminal=false refresh=true"
580
+ print_item "$p2" "Update (dry-run) | bash=$hstack_TERM param1=wt param2=update param3=active param4=--dry-run dir=$hstack_ROOT_DIR terminal=false"
581
+ print_item "$p2" "Update (apply) | bash=$hstack_BIN param1=wt param2=update param3=active dir=$hstack_ROOT_DIR terminal=false refresh=true"
582
+ print_item "$p2" "Update (apply + stash) | bash=$hstack_BIN param1=wt param2=update param3=active param4=--stash dir=$hstack_ROOT_DIR terminal=false refresh=true"
583
+ fi
584
+
585
+ print_sep "$p2"
586
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
587
+ print_item "$p2" "Use worktree in stack (interactive) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=use param6=--interactive dir=$hstack_ROOT_DIR terminal=false refresh=true"
588
+ print_item "$p2" "New worktree (interactive) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=new param6=--interactive dir=$hstack_ROOT_DIR terminal=false refresh=true"
589
+ print_item "$p2" "List worktrees (terminal) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=list dir=$hstack_ROOT_DIR terminal=false"
590
+ else
591
+ print_item "$p2" "Use worktree (interactive) | bash=$hstack_TERM param1=wt param2=use param3=--interactive dir=$hstack_ROOT_DIR terminal=false refresh=true"
592
+ print_item "$p2" "New worktree (interactive) | bash=$hstack_TERM param1=wt param2=new param3=--interactive dir=$hstack_ROOT_DIR terminal=false refresh=true"
593
+ print_item "$p2" "List worktrees (terminal) | bash=$hstack_TERM param1=wt param2=list dir=$hstack_ROOT_DIR terminal=false"
594
+ fi
595
+
596
+ # PR worktree (prompt)
597
+ local pr_helper="$hstack_ROOT_DIR/extras/swiftbar/wt-pr.sh"
598
+ if [[ -x "$pr_helper" ]]; then
599
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
600
+ print_item "$p2" "PR worktree (prompt) | bash=$pr_helper param1=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
601
+ else
602
+ print_item "$p2" "PR worktree (prompt) | bash=$pr_helper dir=$hstack_ROOT_DIR terminal=false refresh=true"
603
+ fi
604
+ fi
605
+
606
+ print_sep "$p2"
607
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
608
+ print_item "$p2" "Shell (active, new window) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=shell param6=active param7=--new-window dir=$hstack_ROOT_DIR terminal=false"
609
+ print_item "$p2" "Open in VS Code (active) | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=code param6=active dir=$hstack_ROOT_DIR terminal=false"
610
+ print_item "$p2" "Open in Cursor (active) | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=cursor param6=active dir=$hstack_ROOT_DIR terminal=false"
611
+ else
612
+ print_item "$p2" "Shell (active, new window) | bash=$hstack_TERM param1=wt param2=shell param3=active param4=--new-window dir=$hstack_ROOT_DIR terminal=false"
613
+ print_item "$p2" "Open in VS Code (active) | bash=$hstack_BIN param1=wt param2=code param3=active dir=$hstack_ROOT_DIR terminal=false"
614
+ print_item "$p2" "Open in Cursor (active) | bash=$hstack_BIN param1=wt param2=cursor param3=active dir=$hstack_ROOT_DIR terminal=false"
615
+ fi
616
+
617
+ # Worktrees listing (inline in SwiftBar, plus stack-aware switch).
618
+ local wt_label="Worktrees: ${wt_count:-0} | sfimage=arrow.triangle.branch"
619
+ print_item "$p2" "$wt_label"
620
+ local p3="${p2}--"
621
+ local tsv
622
+ if [[ "$git_mode" == "cached" && -f "$wts" ]]; then
623
+ tsv="$(cat "$wts" 2>/dev/null || true)"
624
+ else
625
+ tsv="$(git_worktrees_tsv "$active_dir" 2>/dev/null || true)"
626
+ fi
627
+ if [[ -z "$tsv" ]]; then
628
+ print_item "$p3" "No worktrees found | color=$GRAY"
629
+ else
630
+ # Map worktree paths back to hstack specs (main/dev or workspace/{pr,local,tmp}/...).
631
+ local repo_key
632
+ repo_key="$(swiftbar_repo_key_from_path "$active_dir" 2>/dev/null || true)"
633
+ local root=""
634
+ if [[ -n "$repo_key" ]]; then
635
+ root="$(resolve_workspace_dir)/"
636
+ fi
637
+ local shown=0
638
+ while IFS=$'\t' read -r wt_path wt_branchref; do
639
+ [[ -n "$wt_path" ]] || continue
640
+ shown=$((shown + 1))
641
+ if [[ $shown -gt 30 ]]; then
642
+ if [[ -n "$root" ]]; then
643
+ print_item "$p3" "More… (open folder) | bash=/usr/bin/open param1='$root' terminal=false"
644
+ fi
645
+ break
646
+ fi
647
+
648
+ local label=""
649
+ local spec=""
650
+ if [[ -n "$repo_key" ]]; then
651
+ spec="$(swiftbar_worktree_spec_from_path "$wt_path" "$repo_key" 2>/dev/null || true)"
652
+ fi
653
+ if [[ -n "$spec" ]]; then
654
+ label="$spec"
655
+ else
656
+ label="$(shorten_path "$wt_path" 52)"
657
+ fi
658
+
659
+ if [[ -n "$wt_branchref" && "$wt_branchref" == refs/heads/* ]]; then
660
+ label="$label ($(basename "$wt_branchref"))"
661
+ fi
662
+ # Active worktree: for monorepo packages, active_dir is under the worktree root.
663
+ local wt_component_dir="$wt_path"
664
+ if [[ -n "$rel_dir" ]]; then
665
+ wt_component_dir="$wt_path/$rel_dir"
666
+ fi
667
+ if [[ "$wt_component_dir" == "$active_dir" ]]; then
668
+ label="(active) $label"
669
+ fi
670
+
671
+ print_item "$p3" "$label"
672
+
673
+ # Only show "use" actions when we can express the worktree as a spec (main/dev or under pr/local/tmp).
674
+ # Some git worktrees can exist outside our managed tree; for those we only offer open/shell actions.
675
+ if [[ -n "$spec" ]]; then
676
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
677
+ print_item "${p3}--" "Use in stack | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=use param6=$spec dir=$hstack_ROOT_DIR terminal=false refresh=true"
678
+ print_item "${p3}--" "Shell (new window) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=shell param6=$spec param7=--new-window dir=$hstack_ROOT_DIR terminal=false"
679
+ print_item "${p3}--" "Open in VS Code | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=code param6=$spec dir=$hstack_ROOT_DIR terminal=false"
680
+ print_item "${p3}--" "Open in Cursor | bash=$hstack_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=cursor param6=$spec dir=$hstack_ROOT_DIR terminal=false"
681
+ else
682
+ print_item "${p3}--" "Use | bash=$hstack_BIN param1=wt param2=use param3=$spec dir=$hstack_ROOT_DIR terminal=false refresh=true"
683
+ print_item "${p3}--" "Shell (new window) | bash=$hstack_TERM param1=wt param2=shell param3=$spec param4=--new-window dir=$hstack_ROOT_DIR terminal=false"
684
+ print_item "${p3}--" "Open in VS Code | bash=$hstack_BIN param1=wt param2=code param3=$spec dir=$hstack_ROOT_DIR terminal=false"
685
+ print_item "${p3}--" "Open in Cursor | bash=$hstack_BIN param1=wt param2=cursor param3=$spec dir=$hstack_ROOT_DIR terminal=false"
686
+ fi
687
+ else
688
+ print_item "${p3}--" "Open folder | bash=/usr/bin/open param1='$wt_path' terminal=false"
689
+ fi
690
+ done <<<"$tsv"
691
+ fi
692
+ fi
693
+ }
694
+
695
+ render_components_menu() {
696
+ # Usage: render_components_menu <prefix> <context> <stack_name> <env_file>
697
+ local prefix="$1" # "" for main menu, "--" for inside a stack
698
+ local context="$2" # main|stack
699
+ local stack_name="$3"
700
+ local env_file="$4"
701
+
702
+ local t0 t1
703
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
704
+
705
+ print_item "$prefix" "Components | sfimage=cube"
706
+ local p2="${prefix}--"
707
+
708
+ # Background auto-refresh: keep menu refresh snappy but update git cache when TTL expires.
709
+ if [[ "$(git_cache_mode)" == "cached" ]]; then
710
+ local scope
711
+ scope="$(git_cache_auto_refresh_scope)"
712
+ local refresh="$hstack_ROOT_DIR/extras/swiftbar/git-cache-refresh.sh"
713
+ if [[ -x "$refresh" ]]; then
714
+ if [[ "$scope" == "all" ]]; then
715
+ git_cache_maybe_refresh_async "all" "$refresh" all
716
+ elif [[ "$scope" == "main" && "$context" == "main" ]]; then
717
+ git_cache_maybe_refresh_async "main" "$refresh" main
718
+ fi
719
+ fi
720
+ fi
721
+
722
+ # Git cache controls (to keep the menu refresh fast while retaining rich inline worktrees UI).
723
+ local refresh="$hstack_ROOT_DIR/extras/swiftbar/git-cache-refresh.sh"
724
+ if [[ -f "$refresh" ]]; then
725
+ local mode ttl
726
+ mode="$(git_cache_mode)"
727
+ ttl="$(git_cache_ttl_sec)"
728
+ print_item "$p2" "Git cache | sfimage=arrow.triangle.2.circlepath"
729
+ local p3="${p2}--"
730
+ print_item "$p3" "Mode: ${mode} (default: cached)"
731
+ print_item "$p3" "TTL: ${ttl}s (set HAPPIER_STACK_SWIFTBAR_GIT_TTL_SEC)"
732
+ print_sep "$p3"
733
+ if [[ "$context" == "main" ]]; then
734
+ print_item "$p3" "Refresh now (main components) | bash=$refresh param1=main dir=$hstack_ROOT_DIR terminal=false refresh=true"
735
+ print_item "$p3" "Refresh now (all stacks/components) | bash=$refresh param1=all dir=$hstack_ROOT_DIR terminal=false refresh=true"
736
+ else
737
+ print_item "$p3" "Refresh now (this stack) | bash=$refresh param1=stack param2=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
738
+ fi
739
+ print_sep "$p2"
740
+ fi
741
+
742
+ # Always render the known components using the resolved component dirs (env file → env.local/.env → fallback),
743
+ # instead of assuming they live under a fixed workspace path.
744
+ local shared_repo_root=""
745
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
746
+ local dir_h dir_c dir_s
747
+ dir_h="$(resolve_component_dir_from_env_file "$env_file" "happier-ui")"
748
+ dir_c="$(resolve_component_dir_from_env_file "$env_file" "happier-cli")"
749
+ dir_s="$(resolve_component_dir_from_env_file "$env_file" "happier-server")"
750
+ local root_h root_c root_s
751
+ root_h="$(swiftbar_find_git_root_upwards "$dir_h" 2>/dev/null || true)"
752
+ root_c="$(swiftbar_find_git_root_upwards "$dir_c" 2>/dev/null || true)"
753
+ root_s="$(swiftbar_find_git_root_upwards "$dir_s" 2>/dev/null || true)"
754
+ if [[ -n "$root_h" && "$root_h" == "$root_c" && "$root_h" == "$root_s" ]]; then
755
+ shared_repo_root="$root_h"
756
+ fi
757
+ fi
758
+ for c in happier-ui happier-cli happier-server-light happier-server; do
759
+ render_component_repo "$p2" "$c" "$context" "$stack_name" "$env_file" "$shared_repo_root"
760
+ print_sep "$p2"
761
+ done
762
+
763
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
764
+ swiftbar_profile_log "time" "label=render_components_menu" "context=${context}" "stack=${stack_name}" "ms=$((t1 - t0))"
765
+ }
766
+
767
+ render_stack_overview_item() {
768
+ local title="$1"
769
+ local level="$2" # green|orange|red
770
+ local prefix="$3"
771
+
772
+ local icon_b64
773
+ icon_b64="$(status_icon_b64 "$level" 14)"
774
+ if [[ -n "$icon_b64" ]]; then
775
+ print_item "$prefix" "$title | image=$icon_b64"
776
+ else
777
+ print_item "$prefix" "$title"
778
+ fi
779
+ }
780
+
781
+ collect_stack_status() {
782
+ # Output (tab-separated):
783
+ # level server_status server_pid server_metrics daemon_status daemon_pid daemon_metrics daemon_uptime last_heartbeat launchagent_status autostart_pid autostart_metrics
784
+ local port="$1"
785
+ local cli_home_dir="$2"
786
+ local label="$3"
787
+ local base_dir="$4"
788
+
789
+ local server_status server_pid server_metrics
790
+ server_status="$(check_server_health "$port")"
791
+ server_pid=""
792
+ server_metrics=""
793
+ if [[ "$server_status" == "running" ]]; then
794
+ server_pid="$(get_port_listener_pid "$port")"
795
+ server_metrics="$(get_process_metrics "$server_pid")"
796
+ fi
797
+
798
+ local daemon_raw daemon_status daemon_pid daemon_metrics
799
+ daemon_raw="$(check_daemon_status "$cli_home_dir")"
800
+ daemon_status="$daemon_raw"
801
+ daemon_pid=""
802
+ if [[ "$daemon_raw" == running:* ]] || [[ "$daemon_raw" == running-no-http:* ]]; then
803
+ daemon_pid="${daemon_raw#*:}"
804
+ daemon_status="${daemon_raw%%:*}"
805
+ fi
806
+ daemon_metrics=""
807
+ if [[ -n "$daemon_pid" ]]; then
808
+ daemon_metrics="$(get_process_metrics "$daemon_pid")"
809
+ fi
810
+
811
+ local daemon_uptime last_heartbeat
812
+ daemon_uptime="$(get_daemon_uptime "$cli_home_dir")"
813
+ last_heartbeat="$(get_last_heartbeat "$cli_home_dir")"
814
+
815
+ local launchagent_status autostart_pid autostart_metrics
816
+ if swiftbar_is_sandboxed; then
817
+ launchagent_status="sandbox_disabled"
818
+ autostart_pid=""
819
+ autostart_metrics=""
820
+ else
821
+ local plist_path="$HOME/Library/LaunchAgents/${label}.plist"
822
+ launchagent_status="$(check_launchagent_status "$label" "$plist_path")"
823
+ autostart_pid=""
824
+ autostart_metrics=""
825
+ if [[ "$launchagent_status" != "not_installed" ]]; then
826
+ autostart_pid="$(launchagent_pid_for_label "$label")"
827
+ autostart_metrics="$(get_process_metrics "$autostart_pid")"
828
+ fi
829
+ fi
830
+
831
+ local level
832
+ level="$(level_from_server_daemon "$server_status" "$daemon_status")"
833
+
834
+ # Important: callers use `read` with IFS=$'\t' which collapses consecutive delimiters.
835
+ # Emit "-" placeholders for optional/empty fields so parsing stays stable.
836
+ local spid="${server_pid:-"-"}"
837
+ local smet="${server_metrics:-"-"}"
838
+ local dpid="${daemon_pid:-"-"}"
839
+ local dmet="${daemon_metrics:-"-"}"
840
+ local dupt="${daemon_uptime:-"-"}"
841
+ local dhb="${last_heartbeat:-"-"}"
842
+ local apid="${autostart_pid:-"-"}"
843
+ local amet="${autostart_metrics:-"-"}"
844
+
845
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
846
+ "$level" \
847
+ "$server_status" "$spid" "$smet" \
848
+ "$daemon_status" "$dpid" "$dmet" "$dupt" "$dhb" \
849
+ "$launchagent_status" "$apid" "$amet"
850
+ }
851
+
852
+ render_stack_info() {
853
+ # Renders a single "Info" item (with actions) at the given prefix.
854
+ local prefix="$1" # "" or "--"
855
+ local stack_name="$2"
856
+ local port="$3"
857
+ local server_component="$4"
858
+ local base_dir="$5"
859
+ local cli_home_dir="$6"
860
+ local label="$7"
861
+ local env_file="$8" # optional
862
+ local tailscale_url="$9" # optional
863
+ local server_metrics="${10:-}"
864
+ local daemon_metrics="${11:-}"
865
+ local autostart_metrics="${12:-}"
866
+
867
+ # Avoid low-contrast gray in the main list; keep it readable in both light/dark.
868
+ print_item "$prefix" "Stack details | sfimage=server.rack"
869
+ local p2="${prefix}--"
870
+ print_item "$p2" "Server component: ${server_component}"
871
+ local pinned_port=""
872
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
873
+ pinned_port="$(dotenv_get "$env_file" "HAPPIER_STACK_SERVER_PORT")"
874
+ fi
875
+ local port_display="$port"
876
+ if [[ -z "$port_display" ]]; then
877
+ port_display="ephemeral (not running)"
878
+ elif [[ -z "$pinned_port" ]]; then
879
+ port_display="${port_display} (ephemeral)"
880
+ fi
881
+ print_item "$p2" "Port: ${port_display}"
882
+ print_item "$p2" "Label: ${label}"
883
+ [[ -n "$env_file" ]] && print_item "$p2" "Env: $(shorten_path "$env_file" 52)"
884
+ [[ -n "$tailscale_url" ]] && print_item "$p2" "Tailscale: $(shorten_text "$tailscale_url" 52)"
885
+
886
+ # Monorepo hint: if happier-ui + happier-cli + happier-server all share the same git root, show it once here.
887
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
888
+ local dir_h dir_c dir_s
889
+ dir_h="$(resolve_component_dir_from_env_file "$env_file" "happier-ui")"
890
+ dir_c="$(resolve_component_dir_from_env_file "$env_file" "happier-cli")"
891
+ dir_s="$(resolve_component_dir_from_env_file "$env_file" "happier-server")"
892
+ local root_h root_c root_s
893
+ root_h="$(swiftbar_find_git_root_upwards "$dir_h" 2>/dev/null || true)"
894
+ root_c="$(swiftbar_find_git_root_upwards "$dir_c" 2>/dev/null || true)"
895
+ root_s="$(swiftbar_find_git_root_upwards "$dir_s" 2>/dev/null || true)"
896
+ if [[ -n "$root_h" && "$root_h" == "$root_c" && "$root_h" == "$root_s" ]]; then
897
+ print_item "$p2" "Happier monorepo: $(shorten_path "$root_h" 52) | color=$GRAY"
898
+ fi
899
+ fi
900
+
901
+ # Aggregate metrics (best-effort): sum the per-component process snapshots.
902
+ # NOTE: CPU may exceed 100% on multi-core machines.
903
+ local totals cpu_total mem_total
904
+ totals="$(swiftbar_sum_metrics_cpu_mem "$server_metrics" "$daemon_metrics" "$autostart_metrics" 2>/dev/null || true)"
905
+ cpu_total="$(echo "$totals" | cut -d'|' -f1)"
906
+ mem_total="$(echo "$totals" | cut -d'|' -f2)"
907
+ if [[ -n "$cpu_total" && -n "$mem_total" ]]; then
908
+ # Only show when we have at least one metric (avoid "0.0|0" noise on stopped stacks).
909
+ if [[ "$cpu_total" != "0.0" || "$mem_total" != "0" ]]; then
910
+ print_item "$p2" "Usage (server+daemon+autostart): CPU ${cpu_total}%, RAM ${mem_total}MB | color=$GRAY"
911
+ fi
912
+ fi
913
+
914
+ print_sep "$p2"
915
+ print_item "$p2" "Open repo | bash=/usr/bin/open param1='$hstack_ROOT_DIR' terminal=false"
916
+ print_item "$p2" "Open data dir | bash=/usr/bin/open param1='$base_dir' terminal=false"
917
+ print_item "$p2" "Open logs dir | bash=/usr/bin/open param1='${base_dir}/logs' terminal=false"
918
+ print_item "$p2" "Open CLI home | bash=/usr/bin/open param1='$cli_home_dir' terminal=false"
919
+ if [[ "$stack_name" == "main" ]]; then
920
+ local main_env
921
+ main_env="$(resolve_main_env_file)"
922
+ if [[ -n "$main_env" ]]; then
923
+ print_item "$p2" "Edit main env | bash=/usr/bin/open param1=-a param2=TextEdit param3='$main_env' terminal=false"
924
+ else
925
+ local home
926
+ home="$(resolve_home_dir)"
927
+ print_item "$p2" "Edit env.local | bash=/usr/bin/open param1=-a param2=TextEdit param3='${home}/env.local' terminal=false"
928
+ fi
929
+ else
930
+ print_item "$p2" "Open stack env | bash=/usr/bin/open param1='$env_file' terminal=false"
931
+ fi
932
+
933
+ if [[ -z "$hstack_BIN" ]]; then
934
+ return
935
+ fi
936
+ print_sep "$p2"
937
+
938
+ local svc_installed="0"
939
+ if ! swiftbar_is_sandboxed; then
940
+ local plist="$HOME/Library/LaunchAgents/${label}.plist"
941
+ [[ -f "$plist" ]] && svc_installed="1"
942
+ fi
943
+ local menu_mode
944
+ menu_mode="$(resolve_menubar_mode)"
945
+
946
+ if [[ "$stack_name" == "main" ]]; then
947
+ if [[ "$svc_installed" == "1" ]]; then
948
+ # Status-aware: only show start/stop based on whether the stack is running.
949
+ if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
950
+ print_item "$p2" "Start (service) | bash=$hstack_BIN param1=service:start dir=$hstack_ROOT_DIR terminal=false refresh=true"
951
+ else
952
+ print_item "$p2" "Stop (service) | bash=$hstack_BIN param1=service:stop dir=$hstack_ROOT_DIR terminal=false refresh=true"
953
+ fi
954
+ print_item "$p2" "Restart (service) | bash=$hstack_BIN param1=service:restart dir=$hstack_ROOT_DIR terminal=false refresh=true"
955
+ else
956
+ if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
957
+ print_item "$p2" "Start (foreground) | bash=$hstack_TERM param1=start dir=$hstack_ROOT_DIR terminal=false refresh=true"
958
+ else
959
+ print_item "$p2" "Stop stack | bash=$hstack_BIN param1=stack param2=stop param3=main dir=$hstack_ROOT_DIR terminal=false refresh=true"
960
+ fi
961
+ fi
962
+ if [[ "$menu_mode" != "selfhost" ]]; then
963
+ print_item "$p2" "Dev mode | bash=$hstack_TERM param1=dev dir=$hstack_ROOT_DIR terminal=false refresh=true"
964
+ fi
965
+ print_item "$p2" "Build UI | bash=$hstack_TERM param1=build dir=$hstack_ROOT_DIR terminal=false refresh=true"
966
+ print_item "$p2" "Doctor | bash=$hstack_TERM param1=stack:doctor dir=$hstack_ROOT_DIR terminal=false refresh=true"
967
+ return
968
+ fi
969
+
970
+ if [[ "$svc_installed" == "1" ]]; then
971
+ # Status-aware: only show start/stop based on whether the stack is running.
972
+ if [[ "$STACK_LEVEL" == "red" ]]; then
973
+ print_item "$p2" "Start (service) | bash=$hstack_BIN param1=stack param2=service:start param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
974
+ else
975
+ print_item "$p2" "Stop (service) | bash=$hstack_BIN param1=stack param2=service:stop param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
976
+ fi
977
+ print_item "$p2" "Restart (service) | bash=$hstack_BIN param1=stack param2=service:restart param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
978
+ else
979
+ if [[ "$STACK_LEVEL" == "red" ]]; then
980
+ print_item "$p2" "Start (foreground) | bash=$hstack_TERM param1=stack param2=start param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
981
+ else
982
+ print_item "$p2" "Stop stack | bash=$hstack_BIN param1=stack param2=stop param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
983
+ fi
984
+ fi
985
+ if [[ "$menu_mode" != "selfhost" ]]; then
986
+ print_item "$p2" "Dev mode | bash=$hstack_TERM param1=stack param2=dev param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
987
+ fi
988
+ print_item "$p2" "Build UI | bash=$hstack_TERM param1=stack param2=build param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
989
+ print_item "$p2" "Doctor | bash=$hstack_TERM param1=stack param2=doctor param3=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
990
+ if [[ "$menu_mode" != "selfhost" ]]; then
991
+ print_item "$p2" "Edit stack (interactive) | bash=$hstack_TERM param1=stack param2=edit param3=$stack_name param4=--interactive dir=$hstack_ROOT_DIR terminal=false refresh=true"
992
+ print_item "$p2" "Select worktrees (interactive) | bash=$hstack_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=use param6=--interactive dir=$hstack_ROOT_DIR terminal=false refresh=true"
993
+ fi
994
+
995
+ local pr_helper="$hstack_ROOT_DIR/extras/swiftbar/wt-pr.sh"
996
+ if [[ "$menu_mode" != "selfhost" && -x "$pr_helper" ]]; then
997
+ print_item "$p2" "PR worktree into this stack (prompt) | bash=$pr_helper param1=_prompt_ param2=$stack_name dir=$hstack_ROOT_DIR terminal=false refresh=true"
998
+ fi
999
+ }