happy-stacks 0.1.2 → 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.
- package/README.md +121 -83
- package/bin/happys.mjs +70 -10
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/stacks.md +39 -0
- package/extras/swiftbar/auth-login.sh +5 -2
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +131 -81
- package/extras/swiftbar/happys-term.sh +15 -38
- package/extras/swiftbar/happys.sh +15 -32
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +209 -80
- package/extras/swiftbar/lib/system.sh +27 -4
- package/extras/swiftbar/lib/utils.sh +311 -28
- package/extras/swiftbar/pnpm.sh +2 -1
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +11 -2
- package/extras/swiftbar/wt-pr.sh +9 -2
- package/package.json +2 -1
- package/scripts/auth.mjs +560 -112
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +130 -20
- package/scripts/dev.mjs +201 -133
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +43 -20
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +25 -15
- package/scripts/mobile.mjs +13 -7
- package/scripts/run.mjs +114 -27
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +15 -2
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +1792 -254
- package/scripts/stop.mjs +6 -3
- package/scripts/tailscale.mjs +17 -2
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +2 -2
- package/scripts/ui_gateway.mjs +2 -2
- package/scripts/uninstall.mjs +18 -10
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +6 -2
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/dev_daemon.mjs +104 -0
- package/scripts/utils/dev_expo_web.mjs +112 -0
- package/scripts/utils/dev_server.mjs +183 -0
- package/scripts/utils/env.mjs +60 -11
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +4 -2
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +100 -46
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +121 -20
- package/scripts/utils/proc.mjs +29 -2
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +24 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +79 -30
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +82 -8
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
|
@@ -11,46 +11,23 @@ set -euo pipefail
|
|
|
11
11
|
# - iTerm / Terminal: we run the command automatically via AppleScript.
|
|
12
12
|
# - Ghostty: best-effort; if we can't run the command, we open Ghostty in the dir and copy the command to clipboard.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
echo "$v"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
expand_home_quick() {
|
|
31
|
-
local p="$1"
|
|
32
|
-
if [[ "$p" == "~/"* ]]; then
|
|
33
|
-
echo "$HOME/${p#~/}"
|
|
34
|
-
else
|
|
35
|
-
echo "$p"
|
|
36
|
-
fi
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
home_from_canonical=""
|
|
40
|
-
ws_from_canonical=""
|
|
41
|
-
if [[ -f "$CANONICAL_ENV_FILE" ]]; then
|
|
42
|
-
home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_HOME_DIR")"
|
|
43
|
-
[[ -z "$home_from_canonical" ]] && home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_HOME_DIR")"
|
|
44
|
-
ws_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_WORKSPACE_DIR")"
|
|
45
|
-
[[ -z "$ws_from_canonical" ]] && ws_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_WORKSPACE_DIR")"
|
|
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"
|
|
46
27
|
fi
|
|
47
|
-
home_from_canonical="$(expand_home_quick "${home_from_canonical:-}")"
|
|
48
|
-
ws_from_canonical="$(expand_home_quick "${ws_from_canonical:-}")"
|
|
49
|
-
|
|
50
|
-
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-${home_from_canonical:-$HOME/.happy-stacks}}"
|
|
51
|
-
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
52
28
|
|
|
53
|
-
WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$
|
|
29
|
+
WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$(resolve_workspace_dir 2>/dev/null || true)}"
|
|
30
|
+
[[ -z "$WORKDIR" ]] && WORKDIR="$HAPPY_STACKS_HOME_DIR/workspace"
|
|
54
31
|
if [[ ! -d "$WORKDIR" ]]; then
|
|
55
32
|
WORKDIR="$HOME"
|
|
56
33
|
fi
|
|
@@ -2,45 +2,28 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
# SwiftBar menu action wrapper.
|
|
5
|
-
# Runs `happys` using the stable shim installed under
|
|
5
|
+
# Runs `happys` using the stable shim installed under <homeDir>/bin.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
DEFAULT_HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[[ -n "$file" && -n "$key" && -f "$file" ]] || return 0
|
|
13
|
-
local line
|
|
14
|
-
line="$(grep -E "^${key}=" "$file" 2>/dev/null | head -n 1 || true)"
|
|
15
|
-
[[ -n "$line" ]] || return 0
|
|
16
|
-
local v="${line#*=}"
|
|
17
|
-
v="${v%$'\r'}"
|
|
18
|
-
if [[ "$v" == \"*\" && "$v" == *\" ]]; then v="${v#\"}"; v="${v%\"}"; fi
|
|
19
|
-
if [[ "$v" == \'*\' && "$v" == *\' ]]; then v="${v#\'}"; v="${v%\'}"; fi
|
|
20
|
-
echo "$v"
|
|
10
|
+
# Treat presence of HAPPY_STACKS_SANDBOX_DIR as sandbox mode.
|
|
11
|
+
is_sandboxed() {
|
|
12
|
+
[[ -n "${HAPPY_STACKS_SANDBOX_DIR:-}" ]]
|
|
21
13
|
}
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
else
|
|
28
|
-
echo "$p"
|
|
29
|
-
fi
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
home_from_canonical=""
|
|
33
|
-
if [[ -f "$CANONICAL_ENV_FILE" ]]; then
|
|
34
|
-
home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_HOME_DIR")"
|
|
35
|
-
[[ -z "$home_from_canonical" ]] && home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_HOME_DIR")"
|
|
36
|
-
fi
|
|
37
|
-
home_from_canonical="$(expand_home_quick "${home_from_canonical:-}")"
|
|
38
|
-
|
|
39
|
-
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-${home_from_canonical:-$HOME/.happy-stacks}}"
|
|
40
|
-
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
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}"
|
|
41
19
|
|
|
42
20
|
HAPPYS_BIN="$HAPPY_LOCAL_DIR/bin/happys"
|
|
43
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
|
|
44
27
|
HAPPYS_BIN="$(command -v happys 2>/dev/null || true)"
|
|
45
28
|
fi
|
|
46
29
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
|
@@ -8,13 +8,321 @@ is_git_repo() {
|
|
|
8
8
|
[[ -n "$dir" && -d "$dir" && ( -d "$dir/.git" || -f "$dir/.git" ) ]]
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
git_cache_dir() {
|
|
12
|
+
local canonical="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
|
|
13
|
+
local home="${HAPPY_STACKS_HOME_DIR:-${HAPPY_LOCAL_DIR:-$canonical}}"
|
|
14
|
+
local dir="${home}/cache/swiftbar/git"
|
|
15
|
+
mkdir -p "$dir" 2>/dev/null || true
|
|
16
|
+
echo "$dir"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
git_cache_ttl_sec() {
|
|
20
|
+
# Default: 6 hours.
|
|
21
|
+
local v="${HAPPY_STACKS_SWIFTBAR_GIT_TTL_SEC:-${HAPPY_LOCAL_SWIFTBAR_GIT_TTL_SEC:-21600}}"
|
|
22
|
+
[[ "$v" =~ ^[0-9]+$ ]] || v=21600
|
|
23
|
+
echo "$v"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
git_cache_refresh_on_stale() {
|
|
27
|
+
[[ "${HAPPY_STACKS_SWIFTBAR_GIT_REFRESH_ON_STALE:-${HAPPY_LOCAL_SWIFTBAR_GIT_REFRESH_ON_STALE:-0}}" == "1" ]]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
git_cache_auto_refresh_scope() {
|
|
31
|
+
# off | main | all
|
|
32
|
+
local s="${HAPPY_STACKS_SWIFTBAR_GIT_AUTO_REFRESH_SCOPE:-${HAPPY_LOCAL_SWIFTBAR_GIT_AUTO_REFRESH_SCOPE:-main}}"
|
|
33
|
+
s="$(echo "$s" | tr '[:upper:]' '[:lower:]')"
|
|
34
|
+
case "$s" in
|
|
35
|
+
off|none|0) echo "off" ;;
|
|
36
|
+
all) echo "all" ;;
|
|
37
|
+
*) echo "main" ;;
|
|
38
|
+
esac
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
git_cache_last_refresh_file() {
|
|
42
|
+
local scope="${1:-main}" # main|all|stack:<name>
|
|
43
|
+
local dir
|
|
44
|
+
dir="$(git_cache_dir)"
|
|
45
|
+
local key="last_refresh|${scope}"
|
|
46
|
+
echo "${dir}/$(swiftbar_cache_hash12 "$key").last"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
git_cache_background_refresh_lockdir() {
|
|
50
|
+
local scope="${1:-main}"
|
|
51
|
+
local dir
|
|
52
|
+
dir="$(git_cache_dir)"
|
|
53
|
+
local key="bg_refresh_lock|${scope}"
|
|
54
|
+
echo "${dir}/$(swiftbar_cache_hash12 "$key").lock"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
git_cache_touch_last_refresh() {
|
|
58
|
+
local scope="${1:-main}"
|
|
59
|
+
local f
|
|
60
|
+
f="$(git_cache_last_refresh_file "$scope")"
|
|
61
|
+
mkdir -p "$(dirname "$f")" 2>/dev/null || true
|
|
62
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null >"$f" || true
|
|
63
|
+
touch "$f" 2>/dev/null || true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
git_cache_age_since_last_refresh_sec() {
|
|
67
|
+
local scope="${1:-main}"
|
|
68
|
+
local f
|
|
69
|
+
f="$(git_cache_last_refresh_file "$scope")"
|
|
70
|
+
[[ -f "$f" ]] || { echo ""; return; }
|
|
71
|
+
local mtime now
|
|
72
|
+
mtime="$(stat -f %m "$f" 2>/dev/null || echo 0)"
|
|
73
|
+
now="$(date +%s 2>/dev/null || echo 0)"
|
|
74
|
+
if [[ "$mtime" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ && "$now" -ge "$mtime" ]]; then
|
|
75
|
+
echo $((now - mtime))
|
|
76
|
+
else
|
|
77
|
+
echo ""
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
git_cache_maybe_refresh_async() {
|
|
82
|
+
# Non-blocking cache refresh.
|
|
83
|
+
# Usage: git_cache_maybe_refresh_async <scope> <refresh_cmd...>
|
|
84
|
+
local scope="$1"
|
|
85
|
+
shift
|
|
86
|
+
|
|
87
|
+
local ttl age
|
|
88
|
+
ttl="$(git_cache_ttl_sec)"
|
|
89
|
+
age="$(git_cache_age_since_last_refresh_sec "$scope")"
|
|
90
|
+
|
|
91
|
+
# If never refreshed, treat as stale and allow.
|
|
92
|
+
if [[ -n "$age" && "$age" =~ ^[0-9]+$ && "$age" -le "$ttl" ]]; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
local lockdir
|
|
97
|
+
lockdir="$(git_cache_background_refresh_lockdir "$scope")"
|
|
98
|
+
if [[ -d "$lockdir" ]]; then
|
|
99
|
+
# If lock is too old, break it (e.g. crashed refresh).
|
|
100
|
+
local lock_age
|
|
101
|
+
lock_age="$(git_cache_age_sec "$lockdir" 2>/dev/null || true)"
|
|
102
|
+
if [[ -n "$lock_age" && "$lock_age" =~ ^[0-9]+$ && "$lock_age" -gt 3600 ]]; then
|
|
103
|
+
rm -rf "$lockdir" 2>/dev/null || true
|
|
104
|
+
else
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
mkdir "$lockdir" 2>/dev/null || return 0
|
|
110
|
+
echo "$$" >"${lockdir}/pid" 2>/dev/null || true
|
|
111
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null >"${lockdir}/started_at" || true
|
|
112
|
+
|
|
113
|
+
# Run in the background; on success, update last-refresh marker.
|
|
114
|
+
(
|
|
115
|
+
"$@" >/dev/null 2>&1 || true
|
|
116
|
+
git_cache_touch_last_refresh "$scope"
|
|
117
|
+
rm -rf "$lockdir" >/dev/null 2>&1 || true
|
|
118
|
+
) >/dev/null 2>&1 &
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
git_cache_mode() {
|
|
122
|
+
# cached (default) | live
|
|
123
|
+
local m="${HAPPY_STACKS_SWIFTBAR_GIT_MODE:-${HAPPY_LOCAL_SWIFTBAR_GIT_MODE:-cached}}"
|
|
124
|
+
m="$(echo "$m" | tr '[:upper:]' '[:lower:]')"
|
|
125
|
+
[[ "$m" == "live" ]] && echo "live" || echo "cached"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
git_cache_key() {
|
|
129
|
+
# Include context+stack because stacks can point components at different worktrees/dirs.
|
|
130
|
+
local context="$1"
|
|
131
|
+
local stack="$2"
|
|
132
|
+
local component="$3"
|
|
133
|
+
local active_dir="$4"
|
|
134
|
+
echo "ctx=${context}|stack=${stack}|comp=${component}|dir=${active_dir}"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
git_cache_paths() {
|
|
138
|
+
# Usage: git_cache_paths <key>
|
|
139
|
+
# Output: meta<TAB>info<TAB>worktrees
|
|
140
|
+
local key="$1"
|
|
141
|
+
local dir
|
|
142
|
+
dir="$(git_cache_dir)"
|
|
143
|
+
local h
|
|
144
|
+
h="$(swiftbar_hash "$key")"
|
|
145
|
+
echo -e "${dir}/${h}.meta\t${dir}/${h}.info.tsv\t${dir}/${h}.worktrees.tsv"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
git_cache_age_sec() {
|
|
149
|
+
local meta="$1"
|
|
150
|
+
[[ -f "$meta" ]] || { echo ""; return; }
|
|
151
|
+
local mtime now
|
|
152
|
+
mtime="$(stat -f %m "$meta" 2>/dev/null || echo 0)"
|
|
153
|
+
now="$(date +%s 2>/dev/null || echo 0)"
|
|
154
|
+
if [[ "$mtime" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ && "$now" -ge "$mtime" ]]; then
|
|
155
|
+
echo $((now - mtime))
|
|
156
|
+
else
|
|
157
|
+
echo ""
|
|
158
|
+
fi
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
git_cache_is_fresh() {
|
|
162
|
+
local meta="$1"
|
|
163
|
+
local ttl
|
|
164
|
+
ttl="$(git_cache_ttl_sec)"
|
|
165
|
+
local age
|
|
166
|
+
age="$(git_cache_age_sec "$meta")"
|
|
167
|
+
[[ -n "$age" && "$age" =~ ^[0-9]+$ && "$age" -le "$ttl" ]]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
git_cache_write_meta() {
|
|
171
|
+
local meta="$1"
|
|
172
|
+
local key="$2"
|
|
173
|
+
mkdir -p "$(dirname "$meta")" 2>/dev/null || true
|
|
174
|
+
{
|
|
175
|
+
echo "key=$key"
|
|
176
|
+
echo "updated_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date)"
|
|
177
|
+
} >"$meta" 2>/dev/null || true
|
|
178
|
+
# touch to update mtime (age calculation uses mtime).
|
|
179
|
+
touch "$meta" 2>/dev/null || true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
git_cache_refresh_one() {
|
|
183
|
+
# Computes and writes cached snapshot for one component context.
|
|
184
|
+
# Usage: git_cache_refresh_one <context> <stack> <component> <active_dir>
|
|
185
|
+
local context="$1"
|
|
186
|
+
local stack="$2"
|
|
187
|
+
local component="$3"
|
|
188
|
+
local active_dir="$4"
|
|
189
|
+
|
|
190
|
+
local key
|
|
191
|
+
key="$(git_cache_key "$context" "$stack" "$component" "$active_dir")"
|
|
192
|
+
local meta info wts
|
|
193
|
+
IFS=$'\t' read -r meta info wts <<<"$(git_cache_paths "$key")"
|
|
194
|
+
|
|
195
|
+
# Missing/non-repo: still write meta so we don't thrash.
|
|
196
|
+
if ! is_git_repo "$active_dir"; then
|
|
197
|
+
echo -e "missing\t${active_dir}\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-" >"$info" 2>/dev/null || true
|
|
198
|
+
: >"$wts" 2>/dev/null || true
|
|
199
|
+
git_cache_write_meta "$meta" "$key"
|
|
200
|
+
return 0
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# Collect snapshot.
|
|
204
|
+
local branch head upstream dirty ab ahead behind
|
|
205
|
+
branch="$(git_head_branch "$active_dir")"
|
|
206
|
+
head="$(git_head_short "$active_dir")"
|
|
207
|
+
upstream="$(git_upstream_short "$active_dir")"
|
|
208
|
+
dirty="$(git_dirty_flag "$active_dir")"
|
|
209
|
+
ab="$(git_ahead_behind "$active_dir")"
|
|
210
|
+
ahead=""
|
|
211
|
+
behind=""
|
|
212
|
+
if [[ -n "$ab" ]]; then
|
|
213
|
+
ahead="$(echo "$ab" | cut -d'|' -f1)"
|
|
214
|
+
behind="$(echo "$ab" | cut -d'|' -f2)"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
local main_branch main_upstream main_ab main_ahead main_behind
|
|
218
|
+
main_branch="$(git_main_branch_name "$active_dir")"
|
|
219
|
+
main_upstream=""
|
|
220
|
+
main_ahead=""
|
|
221
|
+
main_behind=""
|
|
222
|
+
if [[ -n "$main_branch" ]]; then
|
|
223
|
+
main_upstream="$(git_branch_upstream_short "$active_dir" "$main_branch")"
|
|
224
|
+
main_ab="$(git_branch_ahead_behind "$active_dir" "$main_branch")"
|
|
225
|
+
if [[ -n "$main_ab" ]]; then
|
|
226
|
+
main_ahead="$(echo "$main_ab" | cut -d'|' -f1)"
|
|
227
|
+
main_behind="$(echo "$main_ab" | cut -d'|' -f2)"
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
local oref uref oab o_ahead o_behind uab u_ahead u_behind
|
|
232
|
+
oref="$(git_remote_main_ref "$active_dir" "origin")"
|
|
233
|
+
uref="$(git_remote_main_ref "$active_dir" "upstream")"
|
|
234
|
+
o_ahead=""; o_behind=""; u_ahead=""; u_behind=""
|
|
235
|
+
if [[ -n "$main_branch" && -n "$oref" ]]; then
|
|
236
|
+
oab="$(git_ahead_behind_refs "$active_dir" "$oref" "$main_branch")"
|
|
237
|
+
if [[ -n "$oab" ]]; then
|
|
238
|
+
o_ahead="$(echo "$oab" | cut -d'|' -f1)"
|
|
239
|
+
o_behind="$(echo "$oab" | cut -d'|' -f2)"
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
if [[ -n "$main_branch" && -n "$uref" ]]; then
|
|
243
|
+
uab="$(git_ahead_behind_refs "$active_dir" "$uref" "$main_branch")"
|
|
244
|
+
if [[ -n "$uab" ]]; then
|
|
245
|
+
u_ahead="$(echo "$uab" | cut -d'|' -f1)"
|
|
246
|
+
u_behind="$(echo "$uab" | cut -d'|' -f2)"
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
local wt_count
|
|
251
|
+
wt_count="$(git_worktree_count "$active_dir")"
|
|
252
|
+
git_worktrees_tsv "$active_dir" >"$wts" 2>/dev/null || true
|
|
253
|
+
|
|
254
|
+
# status active_dir branch head upstream dirty ahead behind main_branch main_upstream main_ahead main_behind oref o_ahead o_behind uref u_ahead u_behind wt_count
|
|
255
|
+
echo -e "ok\t${active_dir}\t${branch}\t${head}\t${upstream}\t${dirty}\t${ahead}\t${behind}\t${main_branch}\t${main_upstream}\t${main_ahead}\t${main_behind}\t${oref}\t${o_ahead}\t${o_behind}\t${uref}\t${u_ahead}\t${u_behind}\t${wt_count}" >"$info" 2>/dev/null || true
|
|
256
|
+
git_cache_write_meta "$meta" "$key"
|
|
257
|
+
return 0
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
git_cache_load_or_refresh() {
|
|
261
|
+
# Usage: git_cache_load_or_refresh <context> <stack> <component> <active_dir> <allow_refresh_on_miss:0|1>
|
|
262
|
+
# Output: meta<TAB>info<TAB>worktrees<TAB>stale(0|1)
|
|
263
|
+
local context="$1"
|
|
264
|
+
local stack="$2"
|
|
265
|
+
local component="$3"
|
|
266
|
+
local active_dir="$4"
|
|
267
|
+
local allow_refresh_on_miss="${5:-0}"
|
|
268
|
+
|
|
269
|
+
local key
|
|
270
|
+
key="$(git_cache_key "$context" "$stack" "$component" "$active_dir")"
|
|
271
|
+
local meta info wts
|
|
272
|
+
IFS=$'\t' read -r meta info wts <<<"$(git_cache_paths "$key")"
|
|
273
|
+
|
|
274
|
+
# If cache exists and is fresh, use it.
|
|
275
|
+
if [[ -f "$meta" && -f "$info" ]]; then
|
|
276
|
+
if git_cache_is_fresh "$meta"; then
|
|
277
|
+
echo -e "${meta}\t${info}\t${wts}\t0"
|
|
278
|
+
return 0
|
|
279
|
+
fi
|
|
280
|
+
# Stale: do not refresh synchronously during menu render. Background refresh is handled elsewhere.
|
|
281
|
+
echo -e "${meta}\t${info}\t${wts}\t1"
|
|
282
|
+
return 0
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# Missing: only refresh synchronously when allowed by caller.
|
|
286
|
+
if [[ "$allow_refresh_on_miss" == "1" ]]; then
|
|
287
|
+
git_cache_refresh_one "$context" "$stack" "$component" "$active_dir" >/dev/null 2>&1 || true
|
|
288
|
+
if [[ -f "$info" ]]; then
|
|
289
|
+
echo -e "${meta}\t${info}\t${wts}\t0"
|
|
290
|
+
return 0
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# Still missing; report missing and stale=1 so callers can show "refresh" action.
|
|
295
|
+
echo -e "${meta}\t${info}\t${wts}\t1"
|
|
296
|
+
return 0
|
|
297
|
+
}
|
|
298
|
+
|
|
11
299
|
git_try() {
|
|
12
300
|
local dir="$1"
|
|
13
301
|
shift
|
|
14
302
|
if ! command -v git >/dev/null 2>&1; then
|
|
15
303
|
return 1
|
|
16
304
|
fi
|
|
17
|
-
|
|
305
|
+
local subcmd="${1:-}"
|
|
306
|
+
|
|
307
|
+
# Run-cache: many stacks render the same component git info; cache by repo path + args for this SwiftBar run.
|
|
308
|
+
local cache_key="git|${dir}|$*"
|
|
309
|
+
swiftbar_cache_get "$cache_key"
|
|
310
|
+
local cached_rc=$?
|
|
311
|
+
if [[ $cached_rc -ne 111 ]]; then
|
|
312
|
+
# Cache hit: swiftbar_cache_get already printed stdout. Preserve rc.
|
|
313
|
+
return $cached_rc
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
local t0 t1 rc out
|
|
317
|
+
t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
|
|
318
|
+
out="$(git -C "$dir" "$@" 2>/dev/null)"
|
|
319
|
+
rc=$?
|
|
320
|
+
t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
|
|
321
|
+
swiftbar_cache_set "$cache_key" "$rc" "$out"
|
|
322
|
+
# Keep label short; include subcommand for aggregation.
|
|
323
|
+
swiftbar_profile_log "time" "label=git" "subcmd=$subcmd" "ms=$((t1 - t0))" "rc=${rc}"
|
|
324
|
+
printf '%s\n' "$out"
|
|
325
|
+
return $rc
|
|
18
326
|
}
|
|
19
327
|
|
|
20
328
|
git_head_branch() {
|
|
@@ -10,8 +10,8 @@ get_menu_icon_b64() {
|
|
|
10
10
|
fi
|
|
11
11
|
|
|
12
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"
|
|
13
|
+
if [[ -z "$source" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/icons/logo-white.png" ]]; then
|
|
14
|
+
source="$HAPPY_LOCAL_DIR/extras/swiftbar/icons/logo-white.png"
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
17
|
# Fallback: use Happy's favicon if present.
|