happy-stacks 0.0.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 (67) hide show
  1. package/README.md +314 -0
  2. package/bin/happys.mjs +168 -0
  3. package/docs/menubar.md +186 -0
  4. package/docs/mobile-ios.md +134 -0
  5. package/docs/remote-access.md +43 -0
  6. package/docs/server-flavors.md +79 -0
  7. package/docs/stacks.md +218 -0
  8. package/docs/tauri.md +62 -0
  9. package/docs/worktrees-and-forks.md +395 -0
  10. package/extras/swiftbar/auth-login.sh +31 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +218 -0
  12. package/extras/swiftbar/icons/happy-green.png +0 -0
  13. package/extras/swiftbar/icons/happy-orange.png +0 -0
  14. package/extras/swiftbar/icons/happy-red.png +0 -0
  15. package/extras/swiftbar/icons/logo-white.png +0 -0
  16. package/extras/swiftbar/install.sh +191 -0
  17. package/extras/swiftbar/lib/git.sh +330 -0
  18. package/extras/swiftbar/lib/icons.sh +105 -0
  19. package/extras/swiftbar/lib/render.sh +774 -0
  20. package/extras/swiftbar/lib/system.sh +190 -0
  21. package/extras/swiftbar/lib/utils.sh +205 -0
  22. package/extras/swiftbar/pnpm-term.sh +125 -0
  23. package/extras/swiftbar/pnpm.sh +21 -0
  24. package/extras/swiftbar/set-interval.sh +62 -0
  25. package/extras/swiftbar/set-server-flavor.sh +57 -0
  26. package/extras/swiftbar/wt-pr.sh +95 -0
  27. package/package.json +58 -0
  28. package/scripts/auth.mjs +272 -0
  29. package/scripts/build.mjs +204 -0
  30. package/scripts/cli-link.mjs +58 -0
  31. package/scripts/completion.mjs +364 -0
  32. package/scripts/daemon.mjs +349 -0
  33. package/scripts/dev.mjs +181 -0
  34. package/scripts/doctor.mjs +342 -0
  35. package/scripts/happy.mjs +79 -0
  36. package/scripts/init.mjs +232 -0
  37. package/scripts/install.mjs +379 -0
  38. package/scripts/menubar.mjs +107 -0
  39. package/scripts/mobile.mjs +305 -0
  40. package/scripts/run.mjs +236 -0
  41. package/scripts/self.mjs +298 -0
  42. package/scripts/server_flavor.mjs +125 -0
  43. package/scripts/service.mjs +526 -0
  44. package/scripts/stack.mjs +815 -0
  45. package/scripts/tailscale.mjs +278 -0
  46. package/scripts/uninstall.mjs +190 -0
  47. package/scripts/utils/args.mjs +17 -0
  48. package/scripts/utils/cli.mjs +24 -0
  49. package/scripts/utils/cli_registry.mjs +262 -0
  50. package/scripts/utils/config.mjs +40 -0
  51. package/scripts/utils/dotenv.mjs +30 -0
  52. package/scripts/utils/env.mjs +138 -0
  53. package/scripts/utils/env_file.mjs +59 -0
  54. package/scripts/utils/env_local.mjs +25 -0
  55. package/scripts/utils/fs.mjs +11 -0
  56. package/scripts/utils/paths.mjs +184 -0
  57. package/scripts/utils/pm.mjs +294 -0
  58. package/scripts/utils/ports.mjs +66 -0
  59. package/scripts/utils/proc.mjs +66 -0
  60. package/scripts/utils/runtime.mjs +30 -0
  61. package/scripts/utils/server.mjs +41 -0
  62. package/scripts/utils/smoke_help.mjs +45 -0
  63. package/scripts/utils/validate.mjs +47 -0
  64. package/scripts/utils/wizard.mjs +69 -0
  65. package/scripts/utils/worktrees.mjs +78 -0
  66. package/scripts/where.mjs +105 -0
  67. package/scripts/worktrees.mjs +1721 -0
@@ -0,0 +1,191 @@
1
+ #!/bin/bash
2
+
3
+ # ============================================================================
4
+ # Happy Stacks SwiftBar Plugin Installer
5
+ # ============================================================================
6
+
7
+ set -e
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ PLUGIN_SOURCE="$SCRIPT_DIR/happy-stacks.5s.sh"
11
+ # No legacy fallback: always install the primary happy-stacks plugin.
12
+ # Default refresh: 5 minutes (good baseline; still refreshes instantly on open).
13
+ # You can override:
14
+ # HAPPY_LOCAL_SWIFTBAR_INTERVAL=30s ./install.sh
15
+ PLUGIN_INTERVAL="${HAPPY_STACKS_SWIFTBAR_INTERVAL:-${HAPPY_LOCAL_SWIFTBAR_INTERVAL:-5m}}"
16
+ PLUGIN_FILE="happy-stacks.${PLUGIN_INTERVAL}.sh"
17
+
18
+ FORCE=0
19
+ for arg in "$@"; do
20
+ case "$arg" in
21
+ --force) FORCE=1 ;;
22
+ esac
23
+ done
24
+
25
+ # Colors
26
+ RED='\033[0;31m'
27
+ GREEN='\033[0;32m'
28
+ YELLOW='\033[1;33m'
29
+ BLUE='\033[0;34m'
30
+ NC='\033[0m' # No Color
31
+
32
+ echo ""
33
+ echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
34
+ echo -e "${BLUE}║ Happy Stacks SwiftBar Plugin Installer ║${NC}"
35
+ echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
36
+ echo ""
37
+
38
+ # Check if running on macOS
39
+ if [[ "$(uname)" != "Darwin" ]]; then
40
+ echo -e "${RED}Error: This installer only works on macOS${NC}"
41
+ exit 1
42
+ fi
43
+
44
+ # Check if SwiftBar is installed
45
+ check_swiftbar() {
46
+ if [[ -d "/Applications/SwiftBar.app" ]]; then
47
+ return 0
48
+ elif mdfind "kMDItemCFBundleIdentifier == 'com.ameba.SwiftBar'" 2>/dev/null | grep -q ".app"; then
49
+ return 0
50
+ else
51
+ return 1
52
+ fi
53
+ }
54
+
55
+ # Get SwiftBar plugins directory
56
+ get_plugins_dir() {
57
+ # Default location
58
+ local default_dir="$HOME/Library/Application Support/SwiftBar/Plugins"
59
+
60
+ # Check if SwiftBar has a custom plugins directory set
61
+ local plist_dir
62
+ plist_dir=$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null || echo "")
63
+
64
+ if [[ -n "$plist_dir" ]] && [[ -d "$plist_dir" ]]; then
65
+ echo "$plist_dir"
66
+ elif [[ -d "$default_dir" ]]; then
67
+ echo "$default_dir"
68
+ else
69
+ echo ""
70
+ fi
71
+ }
72
+
73
+ # Step 1: Check/Install SwiftBar
74
+ echo -e "${YELLOW}Step 1: Checking for SwiftBar...${NC}"
75
+
76
+ if check_swiftbar; then
77
+ echo -e "${GREEN}✓ SwiftBar is already installed${NC}"
78
+ else
79
+ echo -e "${YELLOW}SwiftBar is not installed.${NC}"
80
+ echo ""
81
+ echo "Would you like to install SwiftBar via Homebrew? (y/n)"
82
+ read -r INSTALL_CHOICE
83
+
84
+ if [[ "$INSTALL_CHOICE" == "y" ]] || [[ "$INSTALL_CHOICE" == "Y" ]]; then
85
+ if ! command -v brew &>/dev/null; then
86
+ echo -e "${RED}Error: Homebrew is not installed.${NC}"
87
+ echo "Please install Homebrew first: https://brew.sh"
88
+ echo "Or install SwiftBar manually: https://swiftbar.app"
89
+ exit 1
90
+ fi
91
+
92
+ echo "Installing SwiftBar..."
93
+ brew install --cask swiftbar
94
+
95
+ if ! check_swiftbar; then
96
+ echo -e "${RED}Error: SwiftBar installation failed${NC}"
97
+ exit 1
98
+ fi
99
+ echo -e "${GREEN}✓ SwiftBar installed successfully${NC}"
100
+ else
101
+ echo ""
102
+ echo "Please install SwiftBar manually:"
103
+ echo " - Homebrew: brew install --cask swiftbar"
104
+ echo " - Direct download: https://swiftbar.app"
105
+ echo ""
106
+ exit 1
107
+ fi
108
+ fi
109
+
110
+ echo ""
111
+
112
+ # Step 2: Get or create plugins directory
113
+ echo -e "${YELLOW}Step 2: Setting up plugins directory...${NC}"
114
+
115
+ PLUGINS_DIR=$(get_plugins_dir)
116
+
117
+ if [[ -z "$PLUGINS_DIR" ]]; then
118
+ PLUGINS_DIR="$HOME/Library/Application Support/SwiftBar/Plugins"
119
+ echo "Creating plugins directory: $PLUGINS_DIR"
120
+ mkdir -p "$PLUGINS_DIR"
121
+ fi
122
+
123
+ echo -e "${GREEN}✓ Plugins directory: $PLUGINS_DIR${NC}"
124
+ echo ""
125
+
126
+ # Step 3: Install the plugin
127
+ echo -e "${YELLOW}Step 3: Installing Happy Stacks plugin...${NC}"
128
+
129
+ PLUGIN_DEST="$PLUGINS_DIR/$PLUGIN_FILE"
130
+
131
+ # Remove any legacy happy-local plugins to avoid duplicates.
132
+ rm -f "$PLUGINS_DIR"/happy-local.*.sh 2>/dev/null || true
133
+
134
+ if [[ -f "$PLUGIN_DEST" ]]; then
135
+ echo "Plugin already exists at $PLUGIN_DEST"
136
+ if [[ "$FORCE" == "1" ]] || [[ ! -t 0 ]]; then
137
+ cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
138
+ chmod +x "$PLUGIN_DEST"
139
+ echo -e "${GREEN}✓ Plugin updated${NC}"
140
+ else
141
+ echo "Would you like to overwrite it? (y/n)"
142
+ read -r OVERWRITE_CHOICE
143
+
144
+ if [[ "$OVERWRITE_CHOICE" != "y" ]] && [[ "$OVERWRITE_CHOICE" != "Y" ]]; then
145
+ echo "Skipping plugin installation."
146
+ else
147
+ cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
148
+ chmod +x "$PLUGIN_DEST"
149
+ echo -e "${GREEN}✓ Plugin updated${NC}"
150
+ fi
151
+ fi
152
+ else
153
+ cp "$PLUGIN_SOURCE" "$PLUGIN_DEST"
154
+ chmod +x "$PLUGIN_DEST"
155
+ echo -e "${GREEN}✓ Plugin installed${NC}"
156
+ fi
157
+
158
+ echo ""
159
+
160
+ # Step 4: Launch SwiftBar if not running
161
+ echo -e "${YELLOW}Step 4: Starting SwiftBar...${NC}"
162
+
163
+ if ! pgrep -x "SwiftBar" > /dev/null; then
164
+ echo "Launching SwiftBar..."
165
+ open -a SwiftBar
166
+ sleep 2
167
+ echo -e "${GREEN}✓ SwiftBar started${NC}"
168
+ else
169
+ echo -e "${GREEN}✓ SwiftBar is already running${NC}"
170
+ echo " Refreshing plugins..."
171
+ # Trigger a refresh by touching the plugin file
172
+ touch "$PLUGIN_DEST"
173
+ fi
174
+
175
+ echo ""
176
+
177
+ # Done!
178
+ echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
179
+ echo -e "${GREEN}║ Installation Complete! ║${NC}"
180
+ echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
181
+ echo ""
182
+ echo "You should now see a 😊 (or 😢) icon in your menu bar."
183
+ echo ""
184
+ echo "The plugin refreshes every ${PLUGIN_INTERVAL}."
185
+ echo "Click it to see the full menu with controls."
186
+ echo ""
187
+ echo -e "${BLUE}Tips:${NC}"
188
+ echo " • Right-click the icon for SwiftBar options"
189
+ echo " • The plugin is located at: $PLUGIN_DEST"
190
+ echo " • Edit the script to customize behavior"
191
+ echo ""
@@ -0,0 +1,330 @@
1
+ #!/bin/bash
2
+
3
+ # Lightweight git helpers for SwiftBar.
4
+ # Keep these fast: avoid network, avoid long commands.
5
+
6
+ is_git_repo() {
7
+ local dir="$1"
8
+ [[ -n "$dir" && -d "$dir" && ( -d "$dir/.git" || -f "$dir/.git" ) ]]
9
+ }
10
+
11
+ git_try() {
12
+ local dir="$1"
13
+ shift
14
+ if ! command -v git >/dev/null 2>&1; then
15
+ return 1
16
+ fi
17
+ git -C "$dir" "$@" 2>/dev/null
18
+ }
19
+
20
+ git_head_branch() {
21
+ local dir="$1"
22
+ git_try "$dir" rev-parse --abbrev-ref HEAD | head -1
23
+ }
24
+
25
+ git_head_short() {
26
+ local dir="$1"
27
+ git_try "$dir" rev-parse --short HEAD | head -1
28
+ }
29
+
30
+ git_upstream_short() {
31
+ local dir="$1"
32
+ # Prints like "origin/main" or "upstream/main"
33
+ git_try "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' | head -1 || true
34
+ }
35
+
36
+ git_ahead_behind() {
37
+ # Output: ahead|behind (numbers). Returns empty if no upstream.
38
+ local dir="$1"
39
+ local upstream
40
+ upstream="$(git_upstream_short "$dir")"
41
+ if [[ -z "$upstream" ]]; then
42
+ echo ""
43
+ return
44
+ fi
45
+ local counts
46
+ counts="$(git_try "$dir" rev-list --left-right --count "${upstream}...HEAD" | tr -s ' ' | sed 's/^ //')" || true
47
+ if [[ -z "$counts" ]]; then
48
+ echo ""
49
+ return
50
+ fi
51
+ # counts is "behind ahead"
52
+ local behind ahead
53
+ behind="$(echo "$counts" | awk '{print $1}')"
54
+ ahead="$(echo "$counts" | awk '{print $2}')"
55
+ if [[ -n "$ahead" && -n "$behind" ]]; then
56
+ echo "${ahead}|${behind}"
57
+ else
58
+ echo ""
59
+ fi
60
+ }
61
+
62
+ git_dirty_flag() {
63
+ # "clean" | "dirty" | "unknown"
64
+ local dir="$1"
65
+ if ! is_git_repo "$dir"; then
66
+ echo "unknown"
67
+ return
68
+ fi
69
+ local out
70
+ out="$(git_try "$dir" status --porcelain | head -1 || true)"
71
+ if [[ -n "$out" ]]; then
72
+ echo "dirty"
73
+ else
74
+ echo "clean"
75
+ fi
76
+ }
77
+
78
+ git_main_branch_name() {
79
+ local dir="$1"
80
+ if git_try "$dir" show-ref --verify --quiet refs/heads/main; then
81
+ echo "main"
82
+ return
83
+ fi
84
+ if git_try "$dir" show-ref --verify --quiet refs/heads/master; then
85
+ echo "master"
86
+ return
87
+ fi
88
+ echo ""
89
+ }
90
+
91
+ git_branch_upstream_short() {
92
+ local dir="$1"
93
+ local branch="$2"
94
+ if [[ -z "$branch" ]]; then
95
+ echo ""
96
+ return
97
+ fi
98
+ git_try "$dir" rev-parse --abbrev-ref --symbolic-full-name "${branch}@{u}" | head -1 || true
99
+ }
100
+
101
+ git_branch_ahead_behind() {
102
+ # Output: ahead|behind for a branch vs its upstream.
103
+ local dir="$1"
104
+ local branch="$2"
105
+ local upstream
106
+ upstream="$(git_branch_upstream_short "$dir" "$branch")"
107
+ if [[ -z "$branch" || -z "$upstream" ]]; then
108
+ echo ""
109
+ return
110
+ fi
111
+ local counts
112
+ counts="$(git_try "$dir" rev-list --left-right --count "${upstream}...${branch}" | tr -s ' ' | sed 's/^ //')" || true
113
+ if [[ -z "$counts" ]]; then
114
+ echo ""
115
+ return
116
+ fi
117
+ local behind ahead
118
+ behind="$(echo "$counts" | awk '{print $1}')"
119
+ ahead="$(echo "$counts" | awk '{print $2}')"
120
+ if [[ -n "$ahead" && -n "$behind" ]]; then
121
+ echo "${ahead}|${behind}"
122
+ else
123
+ echo ""
124
+ fi
125
+ }
126
+
127
+ git_ref_exists() {
128
+ local dir="$1"
129
+ local ref="$2"
130
+ [[ -n "$ref" ]] || return 1
131
+ git_try "$dir" show-ref --verify --quiet "$ref"
132
+ }
133
+
134
+ git_remote_main_ref() {
135
+ # Returns a remote tracking ref like refs/remotes/origin/main or refs/remotes/upstream/master.
136
+ local dir="$1"
137
+ local remote="$2"
138
+ if git_ref_exists "$dir" "refs/remotes/${remote}/main"; then
139
+ echo "refs/remotes/${remote}/main"
140
+ return
141
+ fi
142
+ if git_ref_exists "$dir" "refs/remotes/${remote}/master"; then
143
+ echo "refs/remotes/${remote}/master"
144
+ return
145
+ fi
146
+ echo ""
147
+ }
148
+
149
+ git_ahead_behind_refs() {
150
+ # Output: ahead|behind for local_ref compared to base_ref.
151
+ # Uses: git rev-list --left-right --count base...local => "behind ahead"
152
+ local dir="$1"
153
+ local base_ref="$2"
154
+ local local_ref="$3"
155
+ if [[ -z "$base_ref" || -z "$local_ref" ]]; then
156
+ echo ""
157
+ return
158
+ fi
159
+ local counts
160
+ counts="$(git_try "$dir" rev-list --left-right --count "${base_ref}...${local_ref}" | tr -s ' ' | sed 's/^ //')" || true
161
+ if [[ -z "$counts" ]]; then
162
+ echo ""
163
+ return
164
+ fi
165
+ local behind ahead
166
+ behind="$(echo "$counts" | awk '{print $1}')"
167
+ ahead="$(echo "$counts" | awk '{print $2}')"
168
+ if [[ -n "$ahead" && -n "$behind" ]]; then
169
+ echo "${ahead}|${behind}"
170
+ else
171
+ echo ""
172
+ fi
173
+ }
174
+
175
+ git_worktree_count() {
176
+ local dir="$1"
177
+ if ! is_git_repo "$dir"; then
178
+ echo ""
179
+ return
180
+ fi
181
+ local out
182
+ out="$(git_try "$dir" worktree list --porcelain || true)"
183
+ if [[ -z "$out" ]]; then
184
+ echo ""
185
+ return
186
+ fi
187
+ # Count "worktree <path>" blocks.
188
+ echo "$out" | awk '/^worktree /{c++} END{ if (c>0) print c; }'
189
+ }
190
+
191
+ git_worktrees_tsv() {
192
+ # Output: path<TAB>branchRefOrEmpty
193
+ # Example branch line in porcelain: "branch refs/heads/slopus/pr/foo"
194
+ local dir="$1"
195
+ if ! is_git_repo "$dir"; then
196
+ return
197
+ fi
198
+ local out
199
+ out="$(git_try "$dir" worktree list --porcelain || true)"
200
+ if [[ -z "$out" ]]; then
201
+ return
202
+ fi
203
+
204
+ local wt_path="" wt_branch=""
205
+ while IFS= read -r line; do
206
+ # Block separator
207
+ if [[ -z "$line" ]]; then
208
+ if [[ -n "$wt_path" ]]; then
209
+ echo -e "${wt_path}\t${wt_branch}"
210
+ fi
211
+ wt_path=""
212
+ wt_branch=""
213
+ continue
214
+ fi
215
+
216
+ if [[ "$line" == worktree\ * ]]; then
217
+ wt_path="${line#worktree }"
218
+ continue
219
+ fi
220
+ if [[ "$line" == branch\ * ]]; then
221
+ wt_branch="${line#branch }"
222
+ continue
223
+ fi
224
+ # ignore HEAD/detached lines
225
+ done <<<"$out"
226
+
227
+ if [[ -n "$wt_path" ]]; then
228
+ echo -e "${wt_path}\t${wt_branch}"
229
+ fi
230
+ }
231
+
232
+ resolve_component_dir_from_env_file() {
233
+ # Resolve component directory based on a stack env file.
234
+ # Usage: resolve_component_dir_from_env_file <env_file> <component>
235
+ local env_file="$1"
236
+ local component="$2"
237
+ local stacks_key=""
238
+ local local_key=""
239
+ case "$component" in
240
+ happy) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY" ;;
241
+ happy-cli) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI" ;;
242
+ happy-server-light) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT" ;;
243
+ happy-server) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER" ;;
244
+ *) stacks_key="" ;;
245
+ esac
246
+ local_key="${stacks_key/HAPPY_STACKS_/HAPPY_LOCAL_}"
247
+
248
+ local fallback
249
+ fallback="$(resolve_components_dir)/$component"
250
+ if [[ -z "$env_file" || -z "$stacks_key" || ! -f "$env_file" ]]; then
251
+ echo "$fallback"
252
+ return
253
+ fi
254
+
255
+ local raw
256
+ raw="$(dotenv_get "$env_file" "$stacks_key")"
257
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$env_file" "$local_key")"
258
+ if [[ -z "$raw" ]]; then
259
+ echo "$fallback"
260
+ return
261
+ fi
262
+
263
+ if [[ "$raw" == "~/"* ]]; then
264
+ raw="$HOME/${raw#~/}"
265
+ fi
266
+ if [[ "$raw" == /* ]]; then
267
+ echo "$raw"
268
+ else
269
+ echo "$(resolve_workspace_dir)/$raw"
270
+ fi
271
+ }
272
+
273
+ resolve_component_dir_from_env() {
274
+ # Resolve active component directory based on env + env.local + .env.
275
+ # Usage: resolve_component_dir_from_env <component>
276
+ # Output: absolute path (best-effort). Falls back to $HAPPY_LOCAL_DIR/components/<component>.
277
+ local component="$1"
278
+ local stacks_key=""
279
+ local local_key=""
280
+ case "$component" in
281
+ happy) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY" ;;
282
+ happy-cli) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI" ;;
283
+ happy-server-light) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT" ;;
284
+ happy-server) stacks_key="HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER" ;;
285
+ *) stacks_key="" ;;
286
+ esac
287
+ local_key="${stacks_key/HAPPY_STACKS_/HAPPY_LOCAL_}"
288
+
289
+ local raw=""
290
+ if [[ -n "$stacks_key" && -n "${!stacks_key:-}" ]]; then
291
+ raw="${!stacks_key}"
292
+ fi
293
+ if [[ -z "$raw" && -n "$local_key" && -n "${!local_key:-}" ]]; then
294
+ raw="${!local_key}"
295
+ fi
296
+
297
+ local env_file
298
+ env_file="$(resolve_main_env_file)"
299
+ if [[ -z "$raw" && -n "$env_file" && -n "$stacks_key" ]]; then
300
+ raw="$(dotenv_get "$env_file" "$stacks_key")"
301
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$env_file" "$local_key")"
302
+ fi
303
+ if [[ -z "$raw" && -n "$stacks_key" ]]; then
304
+ raw="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "$stacks_key")"
305
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "$local_key")"
306
+ fi
307
+ if [[ -z "$raw" && -n "$stacks_key" ]]; then
308
+ raw="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "$stacks_key")"
309
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "$local_key")"
310
+ fi
311
+
312
+ local fallback
313
+ fallback="$(resolve_components_dir)/$component"
314
+ if [[ -z "$raw" ]]; then
315
+ echo "$fallback"
316
+ return
317
+ fi
318
+
319
+ # Expand ~
320
+ if [[ "$raw" == "~/"* ]]; then
321
+ raw="$HOME/${raw#~/}"
322
+ fi
323
+
324
+ # Absolute vs relative (relative is interpreted relative to the repo root).
325
+ if [[ "$raw" == /* ]]; then
326
+ echo "$raw"
327
+ else
328
+ echo "$(resolve_workspace_dir)/$raw"
329
+ fi
330
+ }
@@ -0,0 +1,105 @@
1
+ #!/bin/bash
2
+
3
+ get_menu_icon_b64() {
4
+ # User override: point at any image file (png/jpg), we'll resize + base64 it.
5
+ local user_icon="${HAPPY_LOCAL_SWIFTBAR_ICON_PATH:-}"
6
+ local source=""
7
+
8
+ if [[ -n "$user_icon" ]] && [[ -f "$user_icon" ]]; then
9
+ source="$user_icon"
10
+ fi
11
+
12
+ # Default: prefer a menu-bar friendly PNG icon (repo-local).
13
+ if [[ -z "$source" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/logo-white.png" ]]; then
14
+ source="$HAPPY_LOCAL_DIR/extras/swiftbar/logo-white.png"
15
+ fi
16
+
17
+ # Fallback: use Happy's favicon if present.
18
+ local workspace_dir
19
+ workspace_dir="$(resolve_workspace_dir)"
20
+
21
+ if [[ -z "$source" ]] && [[ -f "$workspace_dir/components/happy/dist/favicon.ico" ]]; then
22
+ source="$workspace_dir/components/happy/dist/favicon.ico"
23
+ fi
24
+
25
+ # Final fallback: Happy logo if present.
26
+ if [[ -z "$source" ]] && [[ -f "$workspace_dir/components/happy/logo.png" ]]; then
27
+ source="$workspace_dir/components/happy/logo.png"
28
+ fi
29
+
30
+ if [[ -z "$source" ]]; then
31
+ echo ""
32
+ return
33
+ fi
34
+
35
+ local cache_dir="$HAPPY_HOME_DIR/swiftbar"
36
+ local cache_png="$cache_dir/happy-stacks-icon.png"
37
+ local cache_b64="$cache_dir/happy-stacks-icon.b64"
38
+ local cache_meta="$cache_dir/happy-stacks-icon.meta"
39
+
40
+ mkdir -p "$cache_dir" 2>/dev/null || true
41
+
42
+ local src_mtime
43
+ src_mtime="$(stat -f %m "$source" 2>/dev/null || echo 0)"
44
+
45
+ local cached_mtime
46
+ cached_mtime="$(cat "$cache_meta" 2>/dev/null || echo 0)"
47
+
48
+ if [[ -f "$cache_b64" ]] && [[ "$cached_mtime" == "$src_mtime" ]]; then
49
+ cat "$cache_b64" 2>/dev/null || true
50
+ return
51
+ fi
52
+
53
+ # Resize to menu-bar-friendly size and base64 encode.
54
+ sips -Z 18 -s format png "$source" --out "$cache_png" >/dev/null 2>&1 || true
55
+ if [[ -f "$cache_png" ]]; then
56
+ /usr/bin/base64 < "$cache_png" | tr -d '\n' > "$cache_b64" 2>/dev/null || true
57
+ echo "$src_mtime" > "$cache_meta" 2>/dev/null || true
58
+ fi
59
+
60
+ cat "$cache_b64" 2>/dev/null || true
61
+ }
62
+
63
+ icon_b64_for_file() {
64
+ local source="$1"
65
+ local cache_key="$2"
66
+ local size="${3:-18}"
67
+
68
+ if [[ -z "$source" ]] || [[ ! -f "$source" ]]; then
69
+ echo ""
70
+ return
71
+ fi
72
+
73
+ local cache_dir="$HAPPY_HOME_DIR/swiftbar/icons"
74
+ local cache_png="$cache_dir/${cache_key}-${size}.png"
75
+ local cache_b64="$cache_dir/${cache_key}-${size}.b64"
76
+ local cache_meta="$cache_dir/${cache_key}-${size}.meta"
77
+
78
+ mkdir -p "$cache_dir" 2>/dev/null || true
79
+
80
+ local src_mtime
81
+ src_mtime="$(stat -f %m "$source" 2>/dev/null || echo 0)"
82
+
83
+ local cached_mtime
84
+ cached_mtime="$(cat "$cache_meta" 2>/dev/null || echo 0)"
85
+
86
+ if [[ -f "$cache_b64" ]] && [[ "$cached_mtime" == "$src_mtime" ]]; then
87
+ cat "$cache_b64" 2>/dev/null || true
88
+ return
89
+ fi
90
+
91
+ sips -Z "$size" -s format png "$source" --out "$cache_png" >/dev/null 2>&1 || true
92
+ if [[ -f "$cache_png" ]]; then
93
+ /usr/bin/base64 < "$cache_png" | tr -d '\n' > "$cache_b64" 2>/dev/null || true
94
+ echo "$src_mtime" > "$cache_meta" 2>/dev/null || true
95
+ fi
96
+
97
+ cat "$cache_b64" 2>/dev/null || true
98
+ }
99
+
100
+ status_icon_b64() {
101
+ local level="$1" # green | orange | red
102
+ local size="${2:-14}"
103
+ local path="$HAPPY_LOCAL_DIR/extras/swiftbar/icons/happy-$level.png"
104
+ icon_b64_for_file "$path" "happy-$level" "$size"
105
+ }