happy-stacks 0.1.0 → 0.2.0

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 (95) hide show
  1. package/README.md +130 -74
  2. package/bin/happys.mjs +140 -9
  3. package/docs/edison.md +381 -0
  4. package/docs/happy-development.md +733 -0
  5. package/docs/menubar.md +54 -0
  6. package/docs/paths-and-env.md +141 -0
  7. package/docs/server-flavors.md +61 -2
  8. package/docs/stacks.md +55 -4
  9. package/extras/swiftbar/auth-login.sh +10 -7
  10. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +175 -83
  12. package/extras/swiftbar/happys-term.sh +128 -0
  13. package/extras/swiftbar/happys.sh +35 -0
  14. package/extras/swiftbar/install.sh +99 -13
  15. package/extras/swiftbar/lib/git.sh +309 -1
  16. package/extras/swiftbar/lib/icons.sh +2 -2
  17. package/extras/swiftbar/lib/render.sh +279 -132
  18. package/extras/swiftbar/lib/system.sh +64 -10
  19. package/extras/swiftbar/lib/utils.sh +469 -10
  20. package/extras/swiftbar/pnpm-term.sh +2 -122
  21. package/extras/swiftbar/pnpm.sh +4 -14
  22. package/extras/swiftbar/set-interval.sh +10 -5
  23. package/extras/swiftbar/set-server-flavor.sh +19 -10
  24. package/extras/swiftbar/wt-pr.sh +10 -3
  25. package/package.json +2 -1
  26. package/scripts/auth.mjs +833 -14
  27. package/scripts/build.mjs +24 -4
  28. package/scripts/cli-link.mjs +3 -3
  29. package/scripts/completion.mjs +15 -8
  30. package/scripts/daemon.mjs +200 -23
  31. package/scripts/dev.mjs +230 -57
  32. package/scripts/doctor.mjs +26 -21
  33. package/scripts/edison.mjs +1828 -0
  34. package/scripts/happy.mjs +3 -7
  35. package/scripts/init.mjs +275 -46
  36. package/scripts/install.mjs +14 -8
  37. package/scripts/lint.mjs +145 -0
  38. package/scripts/menubar.mjs +81 -8
  39. package/scripts/migrate.mjs +302 -0
  40. package/scripts/mobile.mjs +59 -21
  41. package/scripts/run.mjs +222 -43
  42. package/scripts/self.mjs +3 -7
  43. package/scripts/server_flavor.mjs +3 -3
  44. package/scripts/service.mjs +190 -38
  45. package/scripts/setup.mjs +790 -0
  46. package/scripts/setup_pr.mjs +182 -0
  47. package/scripts/stack.mjs +2273 -92
  48. package/scripts/stop.mjs +160 -0
  49. package/scripts/tailscale.mjs +164 -23
  50. package/scripts/test.mjs +144 -0
  51. package/scripts/tui.mjs +556 -0
  52. package/scripts/typecheck.mjs +145 -0
  53. package/scripts/ui_gateway.mjs +248 -0
  54. package/scripts/uninstall.mjs +21 -13
  55. package/scripts/utils/auth_files.mjs +58 -0
  56. package/scripts/utils/auth_login_ux.mjs +76 -0
  57. package/scripts/utils/auth_sources.mjs +12 -0
  58. package/scripts/utils/browser.mjs +22 -0
  59. package/scripts/utils/canonical_home.mjs +20 -0
  60. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
  61. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  62. package/scripts/utils/config.mjs +13 -1
  63. package/scripts/utils/dev_auth_key.mjs +169 -0
  64. package/scripts/utils/dev_daemon.mjs +104 -0
  65. package/scripts/utils/dev_expo_web.mjs +112 -0
  66. package/scripts/utils/dev_server.mjs +183 -0
  67. package/scripts/utils/env.mjs +94 -23
  68. package/scripts/utils/env_file.mjs +36 -0
  69. package/scripts/utils/expo.mjs +96 -0
  70. package/scripts/utils/handy_master_secret.mjs +94 -0
  71. package/scripts/utils/happy_server_infra.mjs +484 -0
  72. package/scripts/utils/localhost_host.mjs +17 -0
  73. package/scripts/utils/ownership.mjs +135 -0
  74. package/scripts/utils/paths.mjs +5 -2
  75. package/scripts/utils/pm.mjs +132 -22
  76. package/scripts/utils/ports.mjs +51 -13
  77. package/scripts/utils/proc.mjs +75 -7
  78. package/scripts/utils/runtime.mjs +1 -3
  79. package/scripts/utils/sandbox.mjs +14 -0
  80. package/scripts/utils/server.mjs +61 -0
  81. package/scripts/utils/server_port.mjs +9 -0
  82. package/scripts/utils/server_urls.mjs +54 -0
  83. package/scripts/utils/stack_context.mjs +23 -0
  84. package/scripts/utils/stack_runtime_state.mjs +104 -0
  85. package/scripts/utils/stack_startup.mjs +208 -0
  86. package/scripts/utils/stack_stop.mjs +255 -0
  87. package/scripts/utils/stacks.mjs +38 -0
  88. package/scripts/utils/validate.mjs +42 -1
  89. package/scripts/utils/watch.mjs +63 -0
  90. package/scripts/utils/worktrees.mjs +57 -1
  91. package/scripts/where.mjs +14 -7
  92. package/scripts/worktrees.mjs +135 -15
  93. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  94. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  95. /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
@@ -17,7 +17,46 @@
17
17
  # Configuration
18
18
  # ============================================================================
19
19
 
20
- HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
20
+ # SwiftBar runs with a minimal environment, so users often won't have
21
+ # HAPPY_STACKS_HOME_DIR / HAPPY_STACKS_WORKSPACE_DIR exported.
22
+ # Treat <canonicalHomeDir>/.env as the canonical pointer file (written by `happys init`).
23
+ # Default: ~/.happy-stacks/.env
24
+ CANONICAL_HOME_DIR="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
25
+ CANONICAL_ENV_FILE="$CANONICAL_HOME_DIR/.env"
26
+
27
+ _dotenv_get_quick() {
28
+ # Usage: _dotenv_get_quick /path/to/env KEY
29
+ local file="$1"
30
+ local key="$2"
31
+ [[ -n "$file" && -n "$key" && -f "$file" ]] || return 0
32
+ local line
33
+ line="$(grep -E "^${key}=" "$file" 2>/dev/null | head -n 1 || true)"
34
+ [[ -n "$line" ]] || return 0
35
+ local v="${line#*=}"
36
+ v="${v%$'\r'}"
37
+ # Strip simple surrounding quotes.
38
+ if [[ "$v" == \"*\" && "$v" == *\" ]]; then v="${v#\"}"; v="${v%\"}"; fi
39
+ if [[ "$v" == \'*\' && "$v" == *\' ]]; then v="${v#\'}"; v="${v%\'}"; fi
40
+ echo "$v"
41
+ }
42
+
43
+ _expand_home_quick() {
44
+ local p="$1"
45
+ if [[ "$p" == "~/"* ]]; then
46
+ echo "$HOME/${p#~/}"
47
+ else
48
+ echo "$p"
49
+ fi
50
+ }
51
+
52
+ _home_from_canonical=""
53
+ if [[ -f "$CANONICAL_ENV_FILE" ]]; then
54
+ _home_from_canonical="$(_dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_HOME_DIR")"
55
+ [[ -z "$_home_from_canonical" ]] && _home_from_canonical="$(_dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_HOME_DIR")"
56
+ fi
57
+ _home_from_canonical="$(_expand_home_quick "${_home_from_canonical:-}")"
58
+
59
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-${_home_from_canonical:-$CANONICAL_HOME_DIR}}"
21
60
  HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
22
61
  HAPPY_LOCAL_PORT="${HAPPY_LOCAL_PORT:-3005}"
23
62
 
@@ -26,17 +65,6 @@ if [[ -n "${HAPPY_STACKS_WT_TERMINAL:-}" && -z "${HAPPY_LOCAL_WT_TERMINAL:-}" ]]
26
65
  if [[ -n "${HAPPY_STACKS_WT_SHELL:-}" && -z "${HAPPY_LOCAL_WT_SHELL:-}" ]]; then HAPPY_LOCAL_WT_SHELL="$HAPPY_STACKS_WT_SHELL"; fi
27
66
  if [[ -n "${HAPPY_STACKS_SWIFTBAR_ICON_PATH:-}" && -z "${HAPPY_LOCAL_SWIFTBAR_ICON_PATH:-}" ]]; then HAPPY_LOCAL_SWIFTBAR_ICON_PATH="$HAPPY_STACKS_SWIFTBAR_ICON_PATH"; fi
28
67
 
29
- # Storage root migrated from ~/.happy/local -> ~/.happy/stacks/main.
30
- if [[ -z "${HAPPY_HOME_DIR:-}" ]]; then
31
- if [[ -d "$HOME/.happy/stacks/main" ]] || [[ ! -d "$HOME/.happy/local" ]]; then
32
- HAPPY_HOME_DIR="$HOME/.happy/stacks/main"
33
- else
34
- HAPPY_HOME_DIR="$HOME/.happy/local"
35
- fi
36
- fi
37
- CLI_HOME_DIR="$HAPPY_HOME_DIR/cli"
38
- LOGS_DIR="$HAPPY_HOME_DIR/logs"
39
-
40
68
  # Colors
41
69
  GREEN="#34C759"
42
70
  RED="#FF3B30"
@@ -78,11 +106,24 @@ PNPM_BIN="$(resolve_pnpm_bin)"
78
106
  MAIN_PORT="$(resolve_main_port)"
79
107
  MAIN_SERVER_COMPONENT="$(resolve_main_server_component)"
80
108
  TAILSCALE_URL="$(get_tailscale_url)"
109
+ if swiftbar_is_sandboxed; then
110
+ # Never probe Tailscale (global machine state) when sandboxing.
111
+ TAILSCALE_URL=""
112
+ fi
81
113
  MAIN_ENV_FILE="$(resolve_main_env_file)"
114
+ MENUBAR_MODE="$(resolve_menubar_mode)"
82
115
 
83
116
  ensure_launchctl_cache
84
117
 
85
- MAIN_COLLECT="$(collect_stack_status "$MAIN_PORT" "$CLI_HOME_DIR" "com.happy.stacks" "$HAPPY_HOME_DIR")"
118
+ if [[ -z "$MAIN_ENV_FILE" ]]; then
119
+ MAIN_ENV_FILE="$(resolve_stack_env_file main)"
120
+ fi
121
+ HAPPY_HOME_DIR="$(resolve_stack_base_dir main "$MAIN_ENV_FILE")"
122
+ CLI_HOME_DIR="$(resolve_stack_cli_home_dir main "$MAIN_ENV_FILE")"
123
+ LOGS_DIR="$HAPPY_HOME_DIR/logs"
124
+ MAIN_LABEL="$(resolve_stack_label main)"
125
+
126
+ MAIN_COLLECT="$(collect_stack_status "$MAIN_PORT" "$CLI_HOME_DIR" "$MAIN_LABEL" "$HAPPY_HOME_DIR")"
86
127
  IFS=$'\t' read -r MAIN_LEVEL MAIN_SERVER_STATUS MAIN_SERVER_PID MAIN_SERVER_METRICS MAIN_DAEMON_STATUS MAIN_DAEMON_PID MAIN_DAEMON_METRICS MAIN_DAEMON_UPTIME MAIN_LAST_HEARTBEAT MAIN_LAUNCHAGENT_STATUS MAIN_AUTOSTART_PID MAIN_AUTOSTART_METRICS <<<"$MAIN_COLLECT"
87
128
  for v in MAIN_SERVER_PID MAIN_SERVER_METRICS MAIN_DAEMON_PID MAIN_DAEMON_METRICS MAIN_DAEMON_UPTIME MAIN_LAST_HEARTBEAT MAIN_AUTOSTART_PID MAIN_AUTOSTART_METRICS; do
88
129
  if [[ "${!v}" == "-" ]]; then
@@ -108,95 +149,144 @@ echo "---"
108
149
  echo "Happy Stacks | size=14 font=SF Pro Display"
109
150
  echo "---"
110
151
 
152
+ # Mode (selfhost vs dev)
153
+ if [[ "$MENUBAR_MODE" == "selfhost" ]]; then
154
+ echo "Mode: Selfhost | sfimage=house"
155
+ else
156
+ echo "Mode: Dev | sfimage=hammer"
157
+ fi
158
+ if [[ -n "$PNPM_BIN" ]]; then
159
+ if [[ "$MENUBAR_MODE" == "selfhost" ]]; then
160
+ echo "--Switch to Dev mode | bash=$PNPM_BIN param1=menubar param2=mode param3=dev dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
161
+ else
162
+ echo "--Switch to Selfhost mode | bash=$PNPM_BIN param1=menubar param2=mode param3=selfhost dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
163
+ fi
164
+ fi
165
+ echo "---"
166
+
111
167
  # Main stack (inline)
112
168
  echo "Main stack"
113
169
  echo "---"
114
170
  export MAIN_LEVEL="$MAIN_LEVEL"
115
- render_stack_info "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$HAPPY_HOME_DIR" "$CLI_HOME_DIR" "com.happy.stacks" "$MAIN_ENV_FILE" "$TAILSCALE_URL"
116
- render_component_server "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$MAIN_SERVER_STATUS" "$MAIN_SERVER_PID" "$MAIN_SERVER_METRICS" "$TAILSCALE_URL" "com.happy.stacks"
171
+ render_stack_info "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$HAPPY_HOME_DIR" "$CLI_HOME_DIR" "$MAIN_LABEL" "$MAIN_ENV_FILE" "$TAILSCALE_URL"
172
+ render_component_server "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$MAIN_SERVER_STATUS" "$MAIN_SERVER_PID" "$MAIN_SERVER_METRICS" "$TAILSCALE_URL" "$MAIN_LABEL"
117
173
  render_component_daemon "" "$MAIN_DAEMON_STATUS" "$MAIN_DAEMON_PID" "$MAIN_DAEMON_METRICS" "$MAIN_DAEMON_UPTIME" "$MAIN_LAST_HEARTBEAT" "$CLI_HOME_DIR/daemon.state.json" "main"
118
- render_component_autostart "" "main" "com.happy.stacks" "$MAIN_LAUNCHAGENT_STATUS" "$MAIN_AUTOSTART_PID" "$MAIN_AUTOSTART_METRICS" "$LOGS_DIR"
174
+ render_component_autostart "" "main" "$MAIN_LABEL" "$MAIN_LAUNCHAGENT_STATUS" "$MAIN_AUTOSTART_PID" "$MAIN_AUTOSTART_METRICS" "$LOGS_DIR"
119
175
  render_component_tailscale "" "main" "$TAILSCALE_URL"
120
176
 
121
177
  echo "---"
122
- echo "Stacks"
123
- echo "---"
178
+ if [[ "$MENUBAR_MODE" == "selfhost" ]]; then
179
+ echo "Maintenance | sfimage=wrench.and.screwdriver"
180
+ if [[ -n "$PNPM_BIN" ]]; then
181
+ UPDATE_JSON="${HAPPY_LOCAL_DIR}/cache/update.json"
182
+ update_available=""
183
+ latest=""
184
+ current=""
185
+ if [[ -f "$UPDATE_JSON" ]]; then
186
+ update_available="$(grep -oE '\"updateAvailable\"[[:space:]]*:[[:space:]]*(true|false)' "$UPDATE_JSON" 2>/dev/null | head -1 | grep -oE '(true|false)' || true)"
187
+ latest="$(grep -oE '\"latest\"[[:space:]]*:[[:space:]]*\"[^\"]+\"' "$UPDATE_JSON" 2>/dev/null | head -1 | sed -E 's/.*\"latest\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\\1/' || true)"
188
+ current="$(grep -oE '\"current\"[[:space:]]*:[[:space:]]*\"[^\"]+\"' "$UPDATE_JSON" 2>/dev/null | head -1 | sed -E 's/.*\"current\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\\1/' || true)"
189
+ fi
190
+ if [[ "$update_available" == "true" && -n "$latest" ]]; then
191
+ echo "--Update available: ${current:-current} → ${latest} | color=$YELLOW"
192
+ else
193
+ echo "--Updates: up to date | color=$GRAY"
194
+ fi
195
+ echo "--Check for updates | bash=$PNPM_BIN param1=self param2=check dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
196
+ echo "--Update happy-stacks runtime | bash=$PNPM_BIN param1=self param2=update dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
197
+ echo "--Doctor | bash=$PNPM_BIN param1=doctor dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
198
+ else
199
+ echo "--⚠️ happys not found (run: npx happy-stacks setup)"
200
+ fi
201
+ else
202
+ echo "Stacks | sfimage=server.rack"
203
+ STACKS_PREFIX="--"
124
204
 
125
- if [[ -n "$PNPM_BIN" ]]; then
126
- PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
127
- echo "New stack (interactive) | bash=$PNPM_TERM param1=stack param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
128
- echo "List stacks | bash=$PNPM_TERM param1=stack param2=list dir=$HAPPY_LOCAL_DIR terminal=false"
129
- echo "---"
130
- fi
205
+ if [[ -n "$PNPM_BIN" ]]; then
206
+ HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
207
+ echo "${STACKS_PREFIX}New stack (interactive) | bash=$HAPPYS_TERM param1=stack param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
208
+ echo "${STACKS_PREFIX}List stacks | bash=$HAPPYS_TERM param1=stack param2=list dir=$HAPPY_LOCAL_DIR terminal=false"
209
+ print_sep "$STACKS_PREFIX"
210
+ fi
131
211
 
132
- STACKS_DIR="$HOME/.happy/stacks"
133
- if [[ -d "$STACKS_DIR" ]]; then
134
- STACK_NAMES="$(ls -1 "$STACKS_DIR" 2>/dev/null || true)"
135
- if [[ -z "$STACK_NAMES" ]]; then
136
- echo "No stacks found | color=$GRAY"
212
+ STACKS_DIR="$(resolve_stacks_storage_root)"
213
+ LEGACY_STACKS_DIR="$HOME/.happy/local/stacks"
214
+ if swiftbar_is_sandboxed; then
215
+ LEGACY_STACKS_DIR=""
137
216
  fi
138
- for s in $STACK_NAMES; do
139
- env_file="$STACKS_DIR/$s/env"
140
- [[ -f "$env_file" ]] || continue
217
+ if [[ -d "$STACKS_DIR" ]] || [[ -n "$LEGACY_STACKS_DIR" && -d "$LEGACY_STACKS_DIR" ]]; then
218
+ STACK_NAMES="$(
219
+ {
220
+ ls -1 "$STACKS_DIR" 2>/dev/null || true
221
+ [[ -n "$LEGACY_STACKS_DIR" ]] && ls -1 "$LEGACY_STACKS_DIR" 2>/dev/null || true
222
+ } | sort -u
223
+ )"
224
+ if [[ -z "$STACK_NAMES" ]]; then
225
+ echo "${STACKS_PREFIX}No stacks found | color=$GRAY"
226
+ fi
227
+ for s in $STACK_NAMES; do
228
+ env_file="$(resolve_stack_env_file "$s")"
229
+ [[ -f "$env_file" ]] || continue
141
230
 
142
- port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
143
- [[ -n "$port" ]] || continue
231
+ # Ports may be ephemeral (runtime-only). Do not skip stacks if the env file does not pin a port.
232
+ port="$(resolve_stack_server_port "$s" "$env_file")"
144
233
 
145
- server_component="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_COMPONENT")"
146
- [[ -n "$server_component" ]] || server_component="happy-server-light"
234
+ server_component="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_COMPONENT")"
235
+ [[ -z "$server_component" ]] && server_component="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_COMPONENT")"
236
+ [[ -n "$server_component" ]] || server_component="happy-server-light"
147
237
 
148
- cli_home_dir="$(dotenv_get "$env_file" "HAPPY_LOCAL_CLI_HOME_DIR")"
149
- [[ -n "$cli_home_dir" ]] || cli_home_dir="$STACKS_DIR/$s/cli"
238
+ base_dir="$(resolve_stack_base_dir "$s" "$env_file")"
239
+ cli_home_dir="$(resolve_stack_cli_home_dir "$s" "$env_file")"
240
+ label="$(resolve_stack_label "$s")"
150
241
 
151
- base_dir="$STACKS_DIR/$s"
152
- label="com.happy.stacks.$s"
242
+ COLLECT="$(collect_stack_status "$port" "$cli_home_dir" "$label" "$base_dir")"
243
+ IFS=$'\t' read -r LEVEL SERVER_STATUS SERVER_PID SERVER_METRICS DAEMON_STATUS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT LAUNCHAGENT_STATUS AUTOSTART_PID AUTOSTART_METRICS <<<"$COLLECT"
244
+ for v in SERVER_PID SERVER_METRICS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT AUTOSTART_PID AUTOSTART_METRICS; do
245
+ if [[ "${!v}" == "-" ]]; then
246
+ printf -v "$v" '%s' ""
247
+ fi
248
+ done
153
249
 
154
- COLLECT="$(collect_stack_status "$port" "$cli_home_dir" "$label" "$base_dir")"
155
- IFS=$'\t' read -r LEVEL SERVER_STATUS SERVER_PID SERVER_METRICS DAEMON_STATUS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT LAUNCHAGENT_STATUS AUTOSTART_PID AUTOSTART_METRICS <<<"$COLLECT"
156
- for v in SERVER_PID SERVER_METRICS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT AUTOSTART_PID AUTOSTART_METRICS; do
157
- if [[ "${!v}" == "-" ]]; then
158
- printf -v "$v" '%s' ""
159
- fi
250
+ render_stack_overview_item "Stack: $s" "$LEVEL" "$STACKS_PREFIX"
251
+ export STACK_LEVEL="$LEVEL"
252
+ render_stack_info "${STACKS_PREFIX}--" "$s" "$port" "$server_component" "$base_dir" "$cli_home_dir" "$label" "$env_file" ""
253
+ render_component_server "${STACKS_PREFIX}--" "$s" "$port" "$server_component" "$SERVER_STATUS" "$SERVER_PID" "$SERVER_METRICS" "" "$label"
254
+ render_component_daemon "${STACKS_PREFIX}--" "$DAEMON_STATUS" "$DAEMON_PID" "$DAEMON_METRICS" "$DAEMON_UPTIME" "$LAST_HEARTBEAT" "$cli_home_dir/daemon.state.json" "$s"
255
+ render_component_autostart "${STACKS_PREFIX}--" "$s" "$label" "$LAUNCHAGENT_STATUS" "$AUTOSTART_PID" "$AUTOSTART_METRICS" "$base_dir/logs"
256
+ render_component_tailscale "${STACKS_PREFIX}--" "$s" ""
257
+ render_components_menu "${STACKS_PREFIX}--" "stack" "$s" "$env_file"
160
258
  done
259
+ else
260
+ echo "${STACKS_PREFIX}No stacks dir found at: $(shorten_path "$STACKS_DIR" 52) | color=$GRAY"
261
+ fi
161
262
 
162
- render_stack_overview_item "Stack: $s" "$LEVEL" ""
163
- export STACK_LEVEL="$LEVEL"
164
- render_stack_info "--" "$s" "$port" "$server_component" "$base_dir" "$cli_home_dir" "$label" "$env_file" ""
165
- render_component_server "--" "$s" "$port" "$server_component" "$SERVER_STATUS" "$SERVER_PID" "$SERVER_METRICS" "" "$label"
166
- render_component_daemon "--" "$DAEMON_STATUS" "$DAEMON_PID" "$DAEMON_METRICS" "$DAEMON_UPTIME" "$LAST_HEARTBEAT" "$cli_home_dir/daemon.state.json" "$s"
167
- render_component_autostart "--" "$s" "$label" "$LAUNCHAGENT_STATUS" "$AUTOSTART_PID" "$AUTOSTART_METRICS" "$base_dir/logs"
168
- render_component_tailscale "--" "$s" ""
169
- render_components_menu "--" "stack" "$s" "$env_file"
170
- done
171
- else
172
- echo "No stacks dir found at ~/.happy/stacks | color=$GRAY"
173
- fi
174
-
175
- echo "---"
176
- render_components_menu "" "main" "main" ""
263
+ echo "---"
264
+ render_components_menu "" "main" "main" "$MAIN_ENV_FILE"
177
265
 
178
- echo "Worktrees | sfimage=arrow.triangle.branch"
179
- if [[ -z "$PNPM_BIN" ]]; then
180
- echo "--⚠️ happys not found (run: npx happy-stacks init, or install happy-stacks globally)"
181
- else
182
- PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
183
- echo "--Use (interactive) | bash=$PNPM_TERM param1=wt param2=use param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
184
- echo "--New (interactive) | bash=$PNPM_TERM param1=wt param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
185
- echo "--PR worktree (prompt) | bash=$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
186
- echo "--Sync mirrors (all) | bash=$PNPM_BIN param1=wt param2=sync-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
187
- echo "--Update all (dry-run) | bash=$PNPM_TERM param1=wt param2=update-all param3=--dry-run dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
188
- echo "--Update all (apply) | bash=$PNPM_BIN param1=wt param2=update-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
189
- fi
266
+ echo "Worktrees | sfimage=arrow.triangle.branch"
267
+ if [[ -z "$PNPM_BIN" ]]; then
268
+ echo "--⚠️ happys not found (run: npx happy-stacks setup)"
269
+ else
270
+ HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
271
+ echo "--Use (interactive) | bash=$HAPPYS_TERM param1=wt param2=use param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
272
+ echo "--New (interactive) | bash=$HAPPYS_TERM param1=wt param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
273
+ echo "--PR worktree (prompt) | bash=$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
274
+ echo "--Sync mirrors (all) | bash=$PNPM_BIN param1=wt param2=sync-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
275
+ echo "--Update all (dry-run) | bash=$HAPPYS_TERM param1=wt param2=update-all param3=--dry-run dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
276
+ echo "--Update all (apply) | bash=$PNPM_BIN param1=wt param2=update-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
277
+ fi
190
278
 
191
- echo "---"
192
- echo "Setup / Tools"
193
- if [[ -z "$PNPM_BIN" ]]; then
194
- echo "--⚠️ happys not found (run: npx happy-stacks init, or install happy-stacks globally)"
195
- else
196
- PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
197
- echo "--Bootstrap (clone/install) | bash=$PNPM_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
198
- echo "--CLI link (install happy wrapper) | bash=$PNPM_TERM param1=cli:link dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
199
- echo "--Mobile dev helper | bash=$PNPM_TERM param1=mobile dir=$HAPPY_LOCAL_DIR terminal=false"
279
+ echo "---"
280
+ echo "Setup / Tools"
281
+ if [[ -z "$PNPM_BIN" ]]; then
282
+ echo "--⚠️ happys not found (run: npx happy-stacks setup)"
283
+ else
284
+ HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
285
+ echo "--Setup (guided) | bash=$HAPPYS_TERM param1=setup dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
286
+ echo "--Bootstrap (clone/install) | bash=$HAPPYS_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
287
+ echo "--CLI link (install happy wrapper) | bash=$HAPPYS_TERM param1=cli:link dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
288
+ echo "--Mobile dev helper | bash=$HAPPYS_TERM param1=mobile dir=$HAPPY_LOCAL_DIR terminal=false"
289
+ fi
200
290
  fi
201
291
 
202
292
  echo "---"
@@ -215,4 +305,6 @@ echo "--1h | bash=$SET_INTERVAL param1=1h dir=$HAPPY_LOCAL_DIR terminal=false re
215
305
  echo "--2h | bash=$SET_INTERVAL param1=2h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
216
306
  echo "--6h | bash=$SET_INTERVAL param1=6h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
217
307
  echo "--12h | bash=$SET_INTERVAL param1=12h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
218
- echo "--1d | bash=$SET_INTERVAL param1=1d dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"exit 0
308
+ echo "--1d | bash=$SET_INTERVAL param1=1d dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
309
+
310
+ exit 0
@@ -0,0 +1,128 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Open preferred terminal and run a happys command.
5
+ #
6
+ # Preference order follows wt shell semantics:
7
+ # - HAPPY_LOCAL_WT_TERMINAL=ghostty|iterm|terminal|current
8
+ # (also accepts "auto" which tries ghostty->iterm->terminal->current)
9
+ #
10
+ # Notes:
11
+ # - iTerm / Terminal: we run the command automatically via AppleScript.
12
+ # - Ghostty: best-effort; if we can't run the command, we open Ghostty in the dir and copy the command to clipboard.
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ DEFAULT_HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
16
+
17
+ # Prefer explicit env vars, but default to the install location inferred from this script path.
18
+ CANONICAL_HOME_DIR="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$DEFAULT_HOME_DIR}}"
19
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-${HAPPY_STACKS_HOME_DIR:-$CANONICAL_HOME_DIR}}"
20
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HAPPY_LOCAL_DIR}"
21
+
22
+ # Use shared resolver for workspace dir (respects HAPPY_STACKS_WORKSPACE_DIR and canonical pointer env).
23
+ LIB_DIR="$HAPPY_LOCAL_DIR/extras/swiftbar/lib"
24
+ if [[ -f "$LIB_DIR/utils.sh" ]]; then
25
+ # shellcheck source=/dev/null
26
+ source "$LIB_DIR/utils.sh"
27
+ fi
28
+
29
+ WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$(resolve_workspace_dir 2>/dev/null || true)}"
30
+ [[ -z "$WORKDIR" ]] && WORKDIR="$HAPPY_STACKS_HOME_DIR/workspace"
31
+ if [[ ! -d "$WORKDIR" ]]; then
32
+ WORKDIR="$HOME"
33
+ fi
34
+
35
+ HAPPYS_SH="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
36
+ if [[ ! -x "$HAPPYS_SH" ]]; then
37
+ echo "missing happys wrapper: $HAPPYS_SH" >&2
38
+ exit 1
39
+ fi
40
+
41
+ pref_raw="$(echo "${HAPPY_STACKS_WT_TERMINAL:-${HAPPY_LOCAL_WT_TERMINAL:-auto}}" | tr '[:upper:]' '[:lower:]')"
42
+ pref="$pref_raw"
43
+ if [[ "$pref" == "" ]]; then pref="auto"; fi
44
+
45
+ cmd=( "$HAPPYS_SH" "$@" )
46
+
47
+ escape_for_osascript_string() {
48
+ local s="$1"
49
+ s="${s//\\/\\\\}"
50
+ s="${s//\"/\\\"}"
51
+ echo "$s"
52
+ }
53
+
54
+ shell_cmd() {
55
+ local joined=""
56
+ local q
57
+ joined="cd \"${WORKDIR//\"/\\\"}\"; "
58
+ for q in "${cmd[@]}"; do
59
+ local escaped
60
+ escaped="$(printf "%s" "$q" | sed "s/'/'\\\\''/g")"
61
+ joined+="'${escaped}' "
62
+ done
63
+ joined+="; echo; echo \"[happy-stacks] done\"; exec /bin/zsh -i"
64
+ echo "$joined"
65
+ }
66
+
67
+ run_iterm() {
68
+ if ! command -v osascript >/dev/null 2>&1; then
69
+ return 1
70
+ fi
71
+ local s
72
+ s="$(shell_cmd)"
73
+ s="$(escape_for_osascript_string "$s")"
74
+ osascript \
75
+ -e 'tell application "iTerm" to activate' \
76
+ -e 'tell application "iTerm" to create window with default profile' \
77
+ -e "tell application \"iTerm\" to tell current session of current window to write text \"${s}\"" >/dev/null
78
+ }
79
+
80
+ run_terminal_app() {
81
+ if ! command -v osascript >/dev/null 2>&1; then
82
+ return 1
83
+ fi
84
+ local s
85
+ s="$(shell_cmd)"
86
+ s="$(escape_for_osascript_string "$s")"
87
+ osascript \
88
+ -e 'tell application "Terminal" to activate' \
89
+ -e "tell application \"Terminal\" to do script \"${s}\"" >/dev/null
90
+ }
91
+
92
+ run_ghostty() {
93
+ if ! command -v ghostty >/dev/null 2>&1; then
94
+ return 1
95
+ fi
96
+
97
+ local s
98
+ s="$(shell_cmd)"
99
+ if ghostty --working-directory "$WORKDIR" -e /bin/zsh -lc "$s" >/dev/null 2>&1; then
100
+ return 0
101
+ fi
102
+
103
+ echo -n "$s" | pbcopy 2>/dev/null || true
104
+ ghostty --working-directory "$WORKDIR" >/dev/null 2>&1 || true
105
+ return 0
106
+ }
107
+
108
+ try_one() {
109
+ local t="$1"
110
+ case "$t" in
111
+ ghostty) run_ghostty ;;
112
+ iterm) run_iterm ;;
113
+ terminal) run_terminal_app ;;
114
+ current) ( cd "$WORKDIR"; exec "${cmd[@]}" ) ;;
115
+ *) return 1 ;;
116
+ esac
117
+ }
118
+
119
+ if [[ "$pref" == "auto" ]]; then
120
+ for t in ghostty iterm terminal current; do
121
+ if try_one "$t"; then
122
+ exit 0
123
+ fi
124
+ done
125
+ exit 1
126
+ fi
127
+
128
+ try_one "$pref"
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # SwiftBar menu action wrapper.
5
+ # Runs `happys` using the stable shim installed under <homeDir>/bin.
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ DEFAULT_HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
9
+
10
+ # Treat presence of HAPPY_STACKS_SANDBOX_DIR as sandbox mode.
11
+ is_sandboxed() {
12
+ [[ -n "${HAPPY_STACKS_SANDBOX_DIR:-}" ]]
13
+ }
14
+
15
+ # Prefer explicit env vars, but default to the install location inferred from this script path.
16
+ CANONICAL_HOME_DIR="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$DEFAULT_HOME_DIR}}"
17
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-${HAPPY_STACKS_HOME_DIR:-$CANONICAL_HOME_DIR}}"
18
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HAPPY_LOCAL_DIR}"
19
+
20
+ HAPPYS_BIN="$HAPPY_LOCAL_DIR/bin/happys"
21
+ if [[ ! -x "$HAPPYS_BIN" ]]; then
22
+ if is_sandboxed; then
23
+ echo "happys not found in sandbox home: $HAPPYS_BIN" >&2
24
+ echo "Tip: re-run: happys init (inside the sandbox) then re-install the menubar plugin." >&2
25
+ exit 1
26
+ fi
27
+ HAPPYS_BIN="$(command -v happys 2>/dev/null || true)"
28
+ fi
29
+
30
+ if [[ -z "${HAPPYS_BIN:-}" ]]; then
31
+ echo "happys not found (run: npx happy-stacks init, or npm i -g happy-stacks)" >&2
32
+ exit 1
33
+ fi
34
+
35
+ exec "$HAPPYS_BIN" "$@"
@@ -13,7 +13,19 @@ PLUGIN_SOURCE="$SCRIPT_DIR/happy-stacks.5s.sh"
13
13
  # You can override:
14
14
  # HAPPY_LOCAL_SWIFTBAR_INTERVAL=30s ./install.sh
15
15
  PLUGIN_INTERVAL="${HAPPY_STACKS_SWIFTBAR_INTERVAL:-${HAPPY_LOCAL_SWIFTBAR_INTERVAL:-5m}}"
16
- PLUGIN_FILE="happy-stacks.${PLUGIN_INTERVAL}.sh"
16
+ PLUGIN_BASENAME="${HAPPY_STACKS_SWIFTBAR_PLUGIN_BASENAME:-happy-stacks}"
17
+ PLUGIN_FILE="${PLUGIN_BASENAME}.${PLUGIN_INTERVAL}.sh"
18
+
19
+ # Optional: install a wrapper plugin instead of copying the source.
20
+ # This is useful for sandbox/test installs so the plugin can be pinned to a specific home/canonical dir
21
+ # even under SwiftBar's minimal environment.
22
+ WRAPPER="${HAPPY_STACKS_SWIFTBAR_PLUGIN_WRAPPER:-0}"
23
+
24
+ escape_single_quotes() {
25
+ # Escape a string so it can be safely embedded inside single quotes in a bash script.
26
+ # e.g. abc'def -> abc'"'"'def
27
+ printf "%s" "$1" | sed "s/'/'\"'\"'/g"
28
+ }
17
29
 
18
30
  FORCE=0
19
31
  for arg in "$@"; do
@@ -128,33 +140,107 @@ echo -e "${YELLOW}Step 3: Installing Happy Stacks plugin...${NC}"
128
140
 
129
141
  PLUGIN_DEST="$PLUGINS_DIR/$PLUGIN_FILE"
130
142
 
131
- # Remove any legacy happy-local plugins to avoid duplicates.
132
- rm -f "$PLUGINS_DIR"/happy-local.*.sh 2>/dev/null || true
143
+ # Remove any legacy happy-local plugins to avoid duplicates *only* for the primary plugin.
144
+ # For sandbox installs (separate basenames), never delete other plugins.
145
+ if [[ "$PLUGIN_BASENAME" == "happy-stacks" ]]; then
146
+ rm -f "$PLUGINS_DIR"/happy-local.*.sh 2>/dev/null || true
147
+ fi
133
148
 
149
+ EXISTED=0
134
150
  if [[ -f "$PLUGIN_DEST" ]]; then
151
+ EXISTED=1
152
+ fi
153
+
154
+ SHOULD_INSTALL=1
155
+ if [[ "$EXISTED" == "1" ]]; then
135
156
  echo "Plugin already exists at $PLUGIN_DEST"
136
157
  if [[ "$FORCE" == "1" ]] || [[ ! -t 0 ]]; then
137
- cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
138
- chmod +x "$PLUGIN_DEST"
139
- echo -e "${GREEN}✓ Plugin updated${NC}"
158
+ SHOULD_INSTALL=1
140
159
  else
141
160
  echo "Would you like to overwrite it? (y/n)"
142
161
  read -r OVERWRITE_CHOICE
143
-
144
162
  if [[ "$OVERWRITE_CHOICE" != "y" ]] && [[ "$OVERWRITE_CHOICE" != "Y" ]]; then
163
+ SHOULD_INSTALL=0
145
164
  echo "Skipping plugin installation."
165
+ fi
166
+ fi
167
+ fi
168
+
169
+ if [[ "$SHOULD_INSTALL" == "1" ]]; then
170
+ if [[ "$WRAPPER" == "1" ]]; then
171
+ # Generate a wrapper plugin that pins env vars and executes the real plugin source.
172
+ HOME_DIR_VAL="${HAPPY_STACKS_HOME_DIR:-${HAPPY_LOCAL_DIR:-$HOME/.happy-stacks}}"
173
+ CANONICAL_DIR_VAL="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
174
+ SANDBOX_DIR_VAL="${HAPPY_STACKS_SANDBOX_DIR:-}"
175
+ WORKSPACE_DIR_VAL="${HAPPY_STACKS_WORKSPACE_DIR:-}"
176
+ RUNTIME_DIR_VAL="${HAPPY_STACKS_RUNTIME_DIR:-}"
177
+ STORAGE_DIR_VAL="${HAPPY_STACKS_STORAGE_DIR:-}"
178
+
179
+ if [[ -n "$SANDBOX_DIR_VAL" ]]; then
180
+ [[ -z "$WORKSPACE_DIR_VAL" ]] && WORKSPACE_DIR_VAL="${SANDBOX_DIR_VAL%/}/workspace"
181
+ [[ -z "$RUNTIME_DIR_VAL" ]] && RUNTIME_DIR_VAL="${SANDBOX_DIR_VAL%/}/runtime"
182
+ [[ -z "$STORAGE_DIR_VAL" ]] && STORAGE_DIR_VAL="${SANDBOX_DIR_VAL%/}/storage"
183
+ fi
184
+ HOME_DIR_ESC="$(escape_single_quotes "$HOME_DIR_VAL")"
185
+ CANONICAL_DIR_ESC="$(escape_single_quotes "$CANONICAL_DIR_VAL")"
186
+ SANDBOX_DIR_ESC="$(escape_single_quotes "$SANDBOX_DIR_VAL")"
187
+ WORKSPACE_DIR_ESC="$(escape_single_quotes "$WORKSPACE_DIR_VAL")"
188
+ RUNTIME_DIR_ESC="$(escape_single_quotes "$RUNTIME_DIR_VAL")"
189
+ STORAGE_DIR_ESC="$(escape_single_quotes "$STORAGE_DIR_VAL")"
190
+ SRC_ESC="$(escape_single_quotes "$PLUGIN_SOURCE")"
191
+ BASENAME_ESC="$(escape_single_quotes "$PLUGIN_BASENAME")"
192
+
193
+ cat >"$PLUGIN_DEST" <<EOF
194
+ #!/bin/bash
195
+ set -euo pipefail
196
+ export HAPPY_STACKS_HOME_DIR='$HOME_DIR_ESC'
197
+ export HAPPY_LOCAL_DIR='$HOME_DIR_ESC'
198
+ export HAPPY_STACKS_CANONICAL_HOME_DIR='$CANONICAL_DIR_ESC'
199
+ export HAPPY_LOCAL_CANONICAL_HOME_DIR='$CANONICAL_DIR_ESC'
200
+ export HAPPY_STACKS_SWIFTBAR_PLUGIN_BASENAME='$BASENAME_ESC'
201
+ export HAPPY_LOCAL_SWIFTBAR_PLUGIN_BASENAME='$BASENAME_ESC'
202
+ if [[ -n '$SANDBOX_DIR_ESC' ]]; then
203
+ export HAPPY_STACKS_SANDBOX_DIR='$SANDBOX_DIR_ESC'
204
+ fi
205
+ if [[ -n '$WORKSPACE_DIR_ESC' ]]; then
206
+ export HAPPY_STACKS_WORKSPACE_DIR='$WORKSPACE_DIR_ESC'
207
+ export HAPPY_LOCAL_WORKSPACE_DIR='$WORKSPACE_DIR_ESC'
208
+ fi
209
+ if [[ -n '$RUNTIME_DIR_ESC' ]]; then
210
+ export HAPPY_STACKS_RUNTIME_DIR='$RUNTIME_DIR_ESC'
211
+ export HAPPY_LOCAL_RUNTIME_DIR='$RUNTIME_DIR_ESC'
212
+ fi
213
+ if [[ -n '$STORAGE_DIR_ESC' ]]; then
214
+ export HAPPY_STACKS_STORAGE_DIR='$STORAGE_DIR_ESC'
215
+ export HAPPY_LOCAL_STORAGE_DIR='$STORAGE_DIR_ESC'
216
+ fi
217
+ # Prevent any re-exec into a "real" install when testing.
218
+ export HAPPY_STACKS_CLI_ROOT_DISABLE="1"
219
+ exec '$SRC_ESC'
220
+ EOF
221
+ chmod +x "$PLUGIN_DEST"
222
+ if [[ "$EXISTED" == "1" ]]; then
223
+ echo -e "${GREEN}✓ Plugin updated (wrapper)${NC}"
146
224
  else
147
- cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
148
- chmod +x "$PLUGIN_DEST"
225
+ echo -e "${GREEN}✓ Plugin installed (wrapper)${NC}"
226
+ fi
227
+ else
228
+ cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
229
+ chmod +x "$PLUGIN_DEST"
230
+ if [[ "$EXISTED" == "1" ]]; then
149
231
  echo -e "${GREEN}✓ Plugin updated${NC}"
232
+ else
233
+ echo -e "${GREEN}✓ Plugin installed${NC}"
150
234
  fi
151
235
  fi
152
- else
153
- cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
154
- chmod +x "$PLUGIN_DEST"
155
- echo -e "${GREEN}✓ Plugin installed${NC}"
156
236
  fi
157
237
 
238
+ #
239
+ # Ensure helper scripts are executable (SwiftBar menu actions rely on this).
240
+ # The repo usually tracks +x, but home installs can lose mode bits depending on how assets are copied.
241
+ #
242
+ chmod +x "$SCRIPT_DIR"/*.sh 2>/dev/null || true
243
+
158
244
  echo ""
159
245
 
160
246
  # Step 4: Launch SwiftBar if not running