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.
- package/README.md +314 -0
- package/bin/happys.mjs +168 -0
- package/docs/menubar.md +186 -0
- package/docs/mobile-ios.md +134 -0
- package/docs/remote-access.md +43 -0
- package/docs/server-flavors.md +79 -0
- package/docs/stacks.md +218 -0
- package/docs/tauri.md +62 -0
- package/docs/worktrees-and-forks.md +395 -0
- package/extras/swiftbar/auth-login.sh +31 -0
- package/extras/swiftbar/happy-stacks.5s.sh +218 -0
- package/extras/swiftbar/icons/happy-green.png +0 -0
- package/extras/swiftbar/icons/happy-orange.png +0 -0
- package/extras/swiftbar/icons/happy-red.png +0 -0
- package/extras/swiftbar/icons/logo-white.png +0 -0
- package/extras/swiftbar/install.sh +191 -0
- package/extras/swiftbar/lib/git.sh +330 -0
- package/extras/swiftbar/lib/icons.sh +105 -0
- package/extras/swiftbar/lib/render.sh +774 -0
- package/extras/swiftbar/lib/system.sh +190 -0
- package/extras/swiftbar/lib/utils.sh +205 -0
- package/extras/swiftbar/pnpm-term.sh +125 -0
- package/extras/swiftbar/pnpm.sh +21 -0
- package/extras/swiftbar/set-interval.sh +62 -0
- package/extras/swiftbar/set-server-flavor.sh +57 -0
- package/extras/swiftbar/wt-pr.sh +95 -0
- package/package.json +58 -0
- package/scripts/auth.mjs +272 -0
- package/scripts/build.mjs +204 -0
- package/scripts/cli-link.mjs +58 -0
- package/scripts/completion.mjs +364 -0
- package/scripts/daemon.mjs +349 -0
- package/scripts/dev.mjs +181 -0
- package/scripts/doctor.mjs +342 -0
- package/scripts/happy.mjs +79 -0
- package/scripts/init.mjs +232 -0
- package/scripts/install.mjs +379 -0
- package/scripts/menubar.mjs +107 -0
- package/scripts/mobile.mjs +305 -0
- package/scripts/run.mjs +236 -0
- package/scripts/self.mjs +298 -0
- package/scripts/server_flavor.mjs +125 -0
- package/scripts/service.mjs +526 -0
- package/scripts/stack.mjs +815 -0
- package/scripts/tailscale.mjs +278 -0
- package/scripts/uninstall.mjs +190 -0
- package/scripts/utils/args.mjs +17 -0
- package/scripts/utils/cli.mjs +24 -0
- package/scripts/utils/cli_registry.mjs +262 -0
- package/scripts/utils/config.mjs +40 -0
- package/scripts/utils/dotenv.mjs +30 -0
- package/scripts/utils/env.mjs +138 -0
- package/scripts/utils/env_file.mjs +59 -0
- package/scripts/utils/env_local.mjs +25 -0
- package/scripts/utils/fs.mjs +11 -0
- package/scripts/utils/paths.mjs +184 -0
- package/scripts/utils/pm.mjs +294 -0
- package/scripts/utils/ports.mjs +66 -0
- package/scripts/utils/proc.mjs +66 -0
- package/scripts/utils/runtime.mjs +30 -0
- package/scripts/utils/server.mjs +41 -0
- package/scripts/utils/smoke_help.mjs +45 -0
- package/scripts/utils/validate.mjs +47 -0
- package/scripts/utils/wizard.mjs +69 -0
- package/scripts/utils/worktrees.mjs +78 -0
- package/scripts/where.mjs +105 -0
- 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
|
+
}
|