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,190 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
get_process_metrics() {
|
|
4
|
+
local pid="$1"
|
|
5
|
+
if [[ -z "$pid" ]]; then
|
|
6
|
+
echo ""
|
|
7
|
+
return
|
|
8
|
+
fi
|
|
9
|
+
# Output: cpu|mem_mb|etime
|
|
10
|
+
local line
|
|
11
|
+
line="$(ps -p "$pid" -o %cpu= -o rss= -o etime= 2>/dev/null | head -1 | tr -s ' ' | sed 's/^ //')"
|
|
12
|
+
if [[ -z "$line" ]]; then
|
|
13
|
+
echo ""
|
|
14
|
+
return
|
|
15
|
+
fi
|
|
16
|
+
local cpu rss etime
|
|
17
|
+
cpu="$(echo "$line" | awk '{print $1}')"
|
|
18
|
+
rss="$(echo "$line" | awk '{print $2}')" # KB
|
|
19
|
+
etime="$(echo "$line" | awk '{print $3}')"
|
|
20
|
+
local mem_mb
|
|
21
|
+
mem_mb="$(awk -v rss="$rss" 'BEGIN { printf "%.0f", (rss/1024.0) }')"
|
|
22
|
+
echo "$cpu|$mem_mb|$etime"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get_port_listener_pid() {
|
|
26
|
+
local port="$1"
|
|
27
|
+
if [[ -z "$port" ]]; then
|
|
28
|
+
echo ""
|
|
29
|
+
return
|
|
30
|
+
fi
|
|
31
|
+
if ! command -v lsof >/dev/null 2>&1; then
|
|
32
|
+
echo ""
|
|
33
|
+
return
|
|
34
|
+
fi
|
|
35
|
+
lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -1 || true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Cached launchctl output for speed across many stacks.
|
|
39
|
+
LAUNCHCTL_LIST_CACHE=""
|
|
40
|
+
|
|
41
|
+
ensure_launchctl_cache() {
|
|
42
|
+
if [[ -n "$LAUNCHCTL_LIST_CACHE" ]]; then
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
if command -v launchctl >/dev/null 2>&1; then
|
|
46
|
+
LAUNCHCTL_LIST_CACHE="$(launchctl list 2>/dev/null || true)"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
check_launchagent_status() {
|
|
51
|
+
local label="${1:-com.happy.stacks}"
|
|
52
|
+
local plist="${2:-$HOME/Library/LaunchAgents/${label}.plist}"
|
|
53
|
+
if [[ ! -f "$plist" ]]; then
|
|
54
|
+
echo "not_installed"
|
|
55
|
+
return
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
ensure_launchctl_cache
|
|
59
|
+
if echo "$LAUNCHCTL_LIST_CACHE" | grep -q "$label"; then
|
|
60
|
+
echo "loaded"
|
|
61
|
+
return
|
|
62
|
+
fi
|
|
63
|
+
echo "unloaded"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
launchagent_pid_for_label() {
|
|
67
|
+
local label="$1"
|
|
68
|
+
if [[ -z "$label" ]]; then
|
|
69
|
+
return
|
|
70
|
+
fi
|
|
71
|
+
ensure_launchctl_cache
|
|
72
|
+
if [[ -z "$LAUNCHCTL_LIST_CACHE" ]]; then
|
|
73
|
+
return
|
|
74
|
+
fi
|
|
75
|
+
local pid
|
|
76
|
+
pid="$(echo "$LAUNCHCTL_LIST_CACHE" | awk -v lbl="$label" '$3==lbl{print $1}' | head -1)"
|
|
77
|
+
if [[ "$pid" == "-" ]]; then
|
|
78
|
+
return
|
|
79
|
+
fi
|
|
80
|
+
echo "$pid"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
check_server_health() {
|
|
84
|
+
local port="$1"
|
|
85
|
+
if [[ -z "$port" ]]; then
|
|
86
|
+
echo "stopped"
|
|
87
|
+
return
|
|
88
|
+
fi
|
|
89
|
+
local response
|
|
90
|
+
# Tight timeouts to keep menus snappy even with many stacks.
|
|
91
|
+
response="$(curl -s --connect-timeout 0.2 --max-time 0.6 "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
|
92
|
+
if [[ "$response" == *"ok"* ]] || [[ "$response" == *"Welcome"* ]]; then
|
|
93
|
+
echo "running"
|
|
94
|
+
return
|
|
95
|
+
fi
|
|
96
|
+
echo "stopped"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
check_daemon_status() {
|
|
100
|
+
local cli_home_dir="$1"
|
|
101
|
+
local state_file="$cli_home_dir/daemon.state.json"
|
|
102
|
+
if [[ -z "$cli_home_dir" ]] || [[ ! -f "$state_file" ]]; then
|
|
103
|
+
# If the daemon is starting but hasn't written daemon.state.json yet, we can still detect it
|
|
104
|
+
# via the lock file PID.
|
|
105
|
+
local lock_file="$cli_home_dir/daemon.state.json.lock"
|
|
106
|
+
if [[ -f "$lock_file" ]]; then
|
|
107
|
+
local lock_pid
|
|
108
|
+
lock_pid="$(cat "$lock_file" 2>/dev/null | tr -d '[:space:]')"
|
|
109
|
+
if [[ -n "$lock_pid" ]] && [[ "$lock_pid" =~ ^[0-9]+$ ]]; then
|
|
110
|
+
if kill -0 "$lock_pid" 2>/dev/null; then
|
|
111
|
+
# Best-effort: classify "auth required" by inspecting the latest daemon log.
|
|
112
|
+
local latest_log
|
|
113
|
+
latest_log="$(ls -1t "$cli_home_dir"/logs/*-daemon.log 2>/dev/null | head -1 || true)"
|
|
114
|
+
if [[ -n "$latest_log" ]]; then
|
|
115
|
+
if tail -n 120 "$latest_log" 2>/dev/null | rg -q "No credentials found|starting authentication flow|Waiting for credentials"; then
|
|
116
|
+
echo "auth_required:$lock_pid"
|
|
117
|
+
return
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
echo "starting:$lock_pid"
|
|
121
|
+
return
|
|
122
|
+
fi
|
|
123
|
+
echo "stale"
|
|
124
|
+
return
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
echo "stopped"
|
|
129
|
+
return
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
local pid httpPort
|
|
133
|
+
pid="$(node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(s.pid ?? ""));' "$state_file" 2>/dev/null || true)"
|
|
134
|
+
httpPort="$(node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(s.httpPort ?? ""));' "$state_file" 2>/dev/null || true)"
|
|
135
|
+
|
|
136
|
+
if [[ -z "$pid" ]] || ! [[ "$pid" =~ ^[0-9]+$ ]]; then
|
|
137
|
+
echo "unknown"
|
|
138
|
+
return
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
142
|
+
echo "stale"
|
|
143
|
+
return
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Best-effort: confirm the control server is responding (tight timeouts).
|
|
147
|
+
if [[ -n "$httpPort" ]] && [[ "$httpPort" =~ ^[0-9]+$ ]]; then
|
|
148
|
+
if curl -s --connect-timeout 0.2 --max-time 0.5 -X POST -H 'content-type: application/json' -d '{}' "http://127.0.0.1:${httpPort}/list" >/dev/null 2>&1; then
|
|
149
|
+
echo "running:$pid"
|
|
150
|
+
return
|
|
151
|
+
fi
|
|
152
|
+
echo "running-no-http:$pid"
|
|
153
|
+
return
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
echo "running:$pid"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get_daemon_uptime() {
|
|
160
|
+
local cli_home_dir="$1"
|
|
161
|
+
local state_file="$cli_home_dir/daemon.state.json"
|
|
162
|
+
if [[ -z "$cli_home_dir" ]] || [[ ! -f "$state_file" ]]; then
|
|
163
|
+
return
|
|
164
|
+
fi
|
|
165
|
+
node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (s.startTime) process.stdout.write(String(s.startTime));' "$state_file" 2>/dev/null || true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get_last_heartbeat() {
|
|
169
|
+
local cli_home_dir="$1"
|
|
170
|
+
local state_file="$cli_home_dir/daemon.state.json"
|
|
171
|
+
if [[ -z "$cli_home_dir" ]] || [[ ! -f "$state_file" ]]; then
|
|
172
|
+
return
|
|
173
|
+
fi
|
|
174
|
+
node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (s.lastHeartbeat) process.stdout.write(String(s.lastHeartbeat));' "$state_file" 2>/dev/null || true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get_tailscale_url() {
|
|
178
|
+
# Try multiple methods to get the Tailscale URL (best-effort).
|
|
179
|
+
local url=""
|
|
180
|
+
|
|
181
|
+
if command -v tailscale &>/dev/null; then
|
|
182
|
+
url="$(tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"
|
|
183
|
+
fi
|
|
184
|
+
if [[ -z "$url" ]] && [[ -x "/Applications/Tailscale.app/Contents/MacOS/Tailscale" ]]; then
|
|
185
|
+
url="$(/Applications/Tailscale.app/Contents/MacOS/Tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
echo "$url"
|
|
189
|
+
}
|
|
190
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
shorten_text() {
|
|
4
|
+
local s="$1"
|
|
5
|
+
local max="${2:-44}"
|
|
6
|
+
if [[ ${#s} -le $max ]]; then
|
|
7
|
+
echo "$s"
|
|
8
|
+
return
|
|
9
|
+
fi
|
|
10
|
+
echo "${s:0:$((max - 3))}..."
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
shorten_path() {
|
|
14
|
+
local p="$1"
|
|
15
|
+
local max="${2:-44}"
|
|
16
|
+
local pretty="${p/#$HOME/~}"
|
|
17
|
+
shorten_text "$pretty" "$max"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dotenv_get() {
|
|
21
|
+
# Usage: dotenv_get /path/to/env KEY
|
|
22
|
+
# Notes:
|
|
23
|
+
# - ignores blank lines and comments
|
|
24
|
+
# - does not expand variables
|
|
25
|
+
# - strips simple surrounding quotes
|
|
26
|
+
local file="$1"
|
|
27
|
+
local key="$2"
|
|
28
|
+
if [[ -z "$file" ]] || [[ -z "$key" ]] || [[ ! -f "$file" ]]; then
|
|
29
|
+
return
|
|
30
|
+
fi
|
|
31
|
+
awk -v k="$key" '
|
|
32
|
+
BEGIN { FS="=" }
|
|
33
|
+
/^[[:space:]]*$/ { next }
|
|
34
|
+
/^[[:space:]]*#/ { next }
|
|
35
|
+
{
|
|
36
|
+
kk=$1
|
|
37
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", kk)
|
|
38
|
+
if (kk != k) next
|
|
39
|
+
|
|
40
|
+
vv=$0
|
|
41
|
+
sub(/^[^=]*=/, "", vv)
|
|
42
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", vv)
|
|
43
|
+
|
|
44
|
+
if (vv ~ /^".*"$/) { sub(/^"/, "", vv); sub(/"$/, "", vv) }
|
|
45
|
+
if (vv ~ /^'\''.*'\''$/) { sub(/^'\''/, "", vv); sub(/'\''$/, "", vv) }
|
|
46
|
+
|
|
47
|
+
print vv
|
|
48
|
+
exit
|
|
49
|
+
}
|
|
50
|
+
' "$file" 2>/dev/null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resolve_happy_local_dir() {
|
|
54
|
+
local home="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
|
|
55
|
+
|
|
56
|
+
# If user provided a valid directory, keep it.
|
|
57
|
+
if [[ -n "${HAPPY_LOCAL_DIR:-}" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/lib/utils.sh" ]]; then
|
|
58
|
+
echo "$HAPPY_LOCAL_DIR"
|
|
59
|
+
return
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Canonical install location.
|
|
63
|
+
if [[ -f "$home/extras/swiftbar/lib/utils.sh" ]]; then
|
|
64
|
+
echo "$home"
|
|
65
|
+
return
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Fall back to home even if missing so the menu can show actionable errors.
|
|
69
|
+
echo "$home"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolve_pnpm_bin() {
|
|
73
|
+
# Back-compat: historically this was "pnpm", but the plugin now runs `happys` via a wrapper script.
|
|
74
|
+
local wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
|
|
75
|
+
if [[ -x "$wrapper" ]]; then
|
|
76
|
+
echo "$wrapper"
|
|
77
|
+
return
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
local global_happys
|
|
81
|
+
global_happys="$(command -v happys 2>/dev/null || true)"
|
|
82
|
+
if [[ -n "$global_happys" ]]; then
|
|
83
|
+
echo "$global_happys"
|
|
84
|
+
return
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
echo ""
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
resolve_workspace_dir() {
|
|
91
|
+
if [[ -n "${HAPPY_STACKS_WORKSPACE_DIR:-}" ]]; then
|
|
92
|
+
echo "$HAPPY_STACKS_WORKSPACE_DIR"
|
|
93
|
+
return
|
|
94
|
+
fi
|
|
95
|
+
local p
|
|
96
|
+
p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_STACKS_WORKSPACE_DIR")"
|
|
97
|
+
[[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_STACKS_WORKSPACE_DIR")"
|
|
98
|
+
if [[ -n "$p" ]]; then
|
|
99
|
+
echo "$p"
|
|
100
|
+
return
|
|
101
|
+
fi
|
|
102
|
+
echo "$HAPPY_LOCAL_DIR/workspace"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
resolve_components_dir() {
|
|
106
|
+
echo "$(resolve_workspace_dir)/components"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve_main_env_file() {
|
|
110
|
+
local explicit="${HAPPY_STACKS_ENV_FILE:-${HAPPY_LOCAL_ENV_FILE:-}}"
|
|
111
|
+
if [[ -n "$explicit" ]] && [[ -f "$explicit" ]]; then
|
|
112
|
+
echo "$explicit"
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
local main="$HOME/.happy/stacks/main/env"
|
|
116
|
+
if [[ -f "$main" ]]; then
|
|
117
|
+
echo "$main"
|
|
118
|
+
return
|
|
119
|
+
fi
|
|
120
|
+
echo ""
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resolve_main_port() {
|
|
124
|
+
# Priority:
|
|
125
|
+
# 1) explicit env var
|
|
126
|
+
# 2) main stack env (~/.happy/stacks/main/env)
|
|
127
|
+
# 3) home env.local
|
|
128
|
+
# 4) home .env
|
|
129
|
+
# 4) fallback to HAPPY_LOCAL_PORT / 3005
|
|
130
|
+
if [[ -n "${HAPPY_LOCAL_SERVER_PORT:-}" ]]; then
|
|
131
|
+
echo "$HAPPY_LOCAL_SERVER_PORT"
|
|
132
|
+
return
|
|
133
|
+
fi
|
|
134
|
+
if [[ -n "${HAPPY_STACKS_SERVER_PORT:-}" ]]; then
|
|
135
|
+
echo "$HAPPY_STACKS_SERVER_PORT"
|
|
136
|
+
return
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
local p
|
|
140
|
+
local env_file
|
|
141
|
+
env_file="$(resolve_main_env_file)"
|
|
142
|
+
if [[ -n "$env_file" ]]; then
|
|
143
|
+
p="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
|
|
144
|
+
[[ -z "$p" ]] && p="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_PORT")"
|
|
145
|
+
fi
|
|
146
|
+
if [[ -n "$p" ]]; then
|
|
147
|
+
echo "$p"
|
|
148
|
+
return
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_LOCAL_SERVER_PORT")"
|
|
152
|
+
[[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_STACKS_SERVER_PORT")"
|
|
153
|
+
if [[ -n "$p" ]]; then
|
|
154
|
+
echo "$p"
|
|
155
|
+
return
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_LOCAL_SERVER_PORT")"
|
|
159
|
+
[[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_STACKS_SERVER_PORT")"
|
|
160
|
+
if [[ -n "$p" ]]; then
|
|
161
|
+
echo "$p"
|
|
162
|
+
return
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
echo "${HAPPY_LOCAL_PORT:-3005}"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
resolve_main_server_component() {
|
|
169
|
+
if [[ -n "${HAPPY_LOCAL_SERVER_COMPONENT:-}" ]]; then
|
|
170
|
+
echo "$HAPPY_LOCAL_SERVER_COMPONENT"
|
|
171
|
+
return
|
|
172
|
+
fi
|
|
173
|
+
if [[ -n "${HAPPY_STACKS_SERVER_COMPONENT:-}" ]]; then
|
|
174
|
+
echo "$HAPPY_STACKS_SERVER_COMPONENT"
|
|
175
|
+
return
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
local c
|
|
179
|
+
local env_file
|
|
180
|
+
env_file="$(resolve_main_env_file)"
|
|
181
|
+
if [[ -n "$env_file" ]]; then
|
|
182
|
+
c="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_COMPONENT")"
|
|
183
|
+
[[ -z "$c" ]] && c="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_COMPONENT")"
|
|
184
|
+
fi
|
|
185
|
+
if [[ -n "$c" ]]; then
|
|
186
|
+
echo "$c"
|
|
187
|
+
return
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
c="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_LOCAL_SERVER_COMPONENT")"
|
|
191
|
+
[[ -z "$c" ]] && c="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_STACKS_SERVER_COMPONENT")"
|
|
192
|
+
if [[ -n "$c" ]]; then
|
|
193
|
+
echo "$c"
|
|
194
|
+
return
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
c="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_LOCAL_SERVER_COMPONENT")"
|
|
198
|
+
[[ -z "$c" ]] && c="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_STACKS_SERVER_COMPONENT")"
|
|
199
|
+
if [[ -n "$c" ]]; then
|
|
200
|
+
echo "$c"
|
|
201
|
+
return
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
echo "happy-server-light"
|
|
205
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
|
|
15
|
+
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
16
|
+
|
|
17
|
+
WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$HAPPY_STACKS_HOME_DIR/workspace}"
|
|
18
|
+
if [[ ! -d "$WORKDIR" ]]; then
|
|
19
|
+
WORKDIR="$HOME"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
PNPM_SH="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
|
|
23
|
+
if [[ ! -x "$PNPM_SH" ]]; then
|
|
24
|
+
echo "missing happys wrapper: $PNPM_SH" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
pref_raw="$(echo "${HAPPY_STACKS_WT_TERMINAL:-${HAPPY_LOCAL_WT_TERMINAL:-auto}}" | tr '[:upper:]' '[:lower:]')"
|
|
29
|
+
pref="$pref_raw"
|
|
30
|
+
if [[ "$pref" == "" ]]; then pref="auto"; fi
|
|
31
|
+
|
|
32
|
+
cmd=( "$PNPM_SH" "$@" )
|
|
33
|
+
|
|
34
|
+
escape_for_osascript_string() {
|
|
35
|
+
# Escape for inclusion inside an AppleScript string literal.
|
|
36
|
+
# (We generate: write text "<cmd>")
|
|
37
|
+
local s="$1"
|
|
38
|
+
s="${s//\\/\\\\}"
|
|
39
|
+
s="${s//\"/\\\"}"
|
|
40
|
+
echo "$s"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
shell_cmd() {
|
|
44
|
+
# Build a zsh command that cds and runs happys (via wrapper), leaving the shell open.
|
|
45
|
+
local joined=""
|
|
46
|
+
local q
|
|
47
|
+
joined="cd \"${WORKDIR//\"/\\\"}\"; "
|
|
48
|
+
for q in "${cmd[@]}"; do
|
|
49
|
+
# Basic shell quoting
|
|
50
|
+
if [[ "$q" =~ [[:space:]\\"\'\$\`\!\&\|\;\<\>\(\)\[\]\{\}] ]]; then
|
|
51
|
+
joined+="'${q//\'/\'\\\'\'}' "
|
|
52
|
+
else
|
|
53
|
+
joined+="$q "
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
joined+="; echo; echo \"[happy-stacks] done\"; exec /bin/zsh -i"
|
|
57
|
+
echo "$joined"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run_iterm() {
|
|
61
|
+
if ! command -v osascript >/dev/null 2>&1; then
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
local s
|
|
65
|
+
s="$(shell_cmd)"
|
|
66
|
+
s="$(escape_for_osascript_string "$s")"
|
|
67
|
+
osascript \
|
|
68
|
+
-e 'tell application "iTerm" to activate' \
|
|
69
|
+
-e 'tell application "iTerm" to create window with default profile' \
|
|
70
|
+
-e "tell application \"iTerm\" to tell current session of current window to write text \"${s}\"" >/dev/null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
run_terminal_app() {
|
|
74
|
+
if ! command -v osascript >/dev/null 2>&1; then
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
local s
|
|
78
|
+
s="$(shell_cmd)"
|
|
79
|
+
# Terminal.app uses do script.
|
|
80
|
+
s="$(escape_for_osascript_string "$s")"
|
|
81
|
+
osascript \
|
|
82
|
+
-e 'tell application "Terminal" to activate' \
|
|
83
|
+
-e "tell application \"Terminal\" to do script \"${s}\"" >/dev/null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
run_ghostty() {
|
|
87
|
+
if ! command -v ghostty >/dev/null 2>&1; then
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Best-effort: try to run the command. If ghostty doesn't support -e on this system,
|
|
92
|
+
# fall back to opening the dir and copying the command.
|
|
93
|
+
local s
|
|
94
|
+
s="$(shell_cmd)"
|
|
95
|
+
if ghostty --working-directory "$WORKDIR" -e /bin/zsh -lc "$s" >/dev/null 2>&1; then
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Fallback: open in dir and copy command for manual paste.
|
|
100
|
+
echo -n "$s" | pbcopy 2>/dev/null || true
|
|
101
|
+
ghostty --working-directory "$WORKDIR" >/dev/null 2>&1 || true
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try_one() {
|
|
106
|
+
local t="$1"
|
|
107
|
+
case "$t" in
|
|
108
|
+
ghostty) run_ghostty ;;
|
|
109
|
+
iterm) run_iterm ;;
|
|
110
|
+
terminal) run_terminal_app ;;
|
|
111
|
+
current) ( cd "$WORKDIR"; exec "${cmd[@]}" ) ;;
|
|
112
|
+
*) return 1 ;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if [[ "$pref" == "auto" ]]; then
|
|
117
|
+
for t in ghostty iterm terminal current; do
|
|
118
|
+
if try_one "$t"; then
|
|
119
|
+
exit 0
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
try_one "$pref"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Back-compat wrapper for SwiftBar menu actions.
|
|
5
|
+
# Historically this executed `pnpm` in the cloned repo; it now executes `happys`.
|
|
6
|
+
|
|
7
|
+
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
|
|
8
|
+
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
9
|
+
|
|
10
|
+
HAPPYS_BIN="$HAPPY_LOCAL_DIR/bin/happys"
|
|
11
|
+
if [[ ! -x "$HAPPYS_BIN" ]]; then
|
|
12
|
+
HAPPYS_BIN="$(command -v happys 2>/dev/null || true)"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [[ -z "${HAPPYS_BIN:-}" ]]; then
|
|
16
|
+
echo "happys not found (run: npx happy-stacks init, or npm i -g happy-stacks)" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
exec "$HAPPYS_BIN" "$@"
|
|
21
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./set-interval.sh 10s|30s|1m|5m|10m|15m|30m|1h|2h|6h|12h
|
|
6
|
+
#
|
|
7
|
+
# Runs non-interactively from SwiftBar:
|
|
8
|
+
# - figures out SwiftBar's active plugin directory
|
|
9
|
+
# - renames (or installs) happy-stacks.<interval>.sh
|
|
10
|
+
# - restarts SwiftBar so the new schedule takes effect
|
|
11
|
+
|
|
12
|
+
INTERVAL="${1:-}"
|
|
13
|
+
if [[ -z "$INTERVAL" ]]; then
|
|
14
|
+
echo "missing interval (example: 5m)" >&2
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if ! [[ "$INTERVAL" =~ ^[0-9]+[smhd]$ ]]; then
|
|
19
|
+
echo "invalid interval: $INTERVAL (expected like 10s, 5m, 1h, 1d)" >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
PLUGIN_DIR="$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null || true)"
|
|
24
|
+
if [[ -z "$PLUGIN_DIR" ]]; then
|
|
25
|
+
PLUGIN_DIR="$HOME/Library/Application Support/SwiftBar/Plugins"
|
|
26
|
+
fi
|
|
27
|
+
mkdir -p "$PLUGIN_DIR"
|
|
28
|
+
|
|
29
|
+
TARGET="$PLUGIN_DIR/happy-stacks.${INTERVAL}.sh"
|
|
30
|
+
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
|
|
31
|
+
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
32
|
+
SOURCE="${HAPPY_LOCAL_DIR}/extras/swiftbar/happy-stacks.5s.sh"
|
|
33
|
+
|
|
34
|
+
# If a happy-stacks plugin already exists, rename it into place; otherwise copy from repo source.
|
|
35
|
+
EXISTING="$(ls "$PLUGIN_DIR"/happy-stacks.*.sh 2>/dev/null | head -1 || true)"
|
|
36
|
+
if [[ -n "$EXISTING" ]]; then
|
|
37
|
+
if [[ "$EXISTING" != "$TARGET" ]]; then
|
|
38
|
+
rm -f "$TARGET"
|
|
39
|
+
mv "$EXISTING" "$TARGET"
|
|
40
|
+
fi
|
|
41
|
+
else
|
|
42
|
+
if [[ ! -f "$SOURCE" ]]; then
|
|
43
|
+
echo "cannot find plugin source at: $SOURCE" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
cp "$SOURCE" "$TARGET"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Remove any other intervals to avoid duplicates in SwiftBar.
|
|
50
|
+
for f in "$PLUGIN_DIR"/happy-stacks.*.sh; do
|
|
51
|
+
[[ "$f" == "$TARGET" ]] && continue
|
|
52
|
+
rm -f "$f" || true
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
chmod +x "$TARGET"
|
|
56
|
+
touch "$TARGET"
|
|
57
|
+
|
|
58
|
+
# Restart SwiftBar so the new filename interval is picked up reliably.
|
|
59
|
+
killall SwiftBar 2>/dev/null || true
|
|
60
|
+
open -a SwiftBar
|
|
61
|
+
|
|
62
|
+
echo "ok: $TARGET"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./set-server-flavor.sh main|<stackName> happy-server|happy-server-light
|
|
6
|
+
#
|
|
7
|
+
# For main:
|
|
8
|
+
# - updates env.local via `happys srv use ...`
|
|
9
|
+
# - restarts the LaunchAgent service if installed (best-effort)
|
|
10
|
+
#
|
|
11
|
+
# For stacks:
|
|
12
|
+
# - updates the stack env via `happys stack srv <name> -- use ...`
|
|
13
|
+
# - restarts the stack LaunchAgent service if installed (best-effort)
|
|
14
|
+
|
|
15
|
+
STACK="${1:-}"
|
|
16
|
+
FLAVOR="${2:-}"
|
|
17
|
+
|
|
18
|
+
if [[ -z "$STACK" ]] || [[ -z "$FLAVOR" ]]; then
|
|
19
|
+
echo "usage: $0 <main|stackName> <happy-server|happy-server-light>" >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
if [[ "$FLAVOR" != "happy-server" && "$FLAVOR" != "happy-server-light" ]]; then
|
|
23
|
+
echo "invalid flavor: $FLAVOR" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
|
|
28
|
+
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
29
|
+
|
|
30
|
+
PNPM_BIN="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
|
|
31
|
+
if [[ ! -x "$PNPM_BIN" ]]; then
|
|
32
|
+
echo "happys wrapper not found (run: happys menubar install)" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
restart_main_service_best_effort() {
|
|
37
|
+
"$PNPM_BIN" service:restart >/dev/null 2>&1 || true
|
|
38
|
+
# If the installed LaunchAgent is still legacy/baked, reinstall so it persists only env-file pointer.
|
|
39
|
+
"$PNPM_BIN" service:install >/dev/null 2>&1 || true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
restart_stack_service_best_effort() {
|
|
43
|
+
local name="$1"
|
|
44
|
+
"$PNPM_BIN" stack service:restart "$name" >/dev/null 2>&1 || true
|
|
45
|
+
"$PNPM_BIN" stack service:install "$name" >/dev/null 2>&1 || true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if [[ "$STACK" == "main" ]]; then
|
|
49
|
+
"$PNPM_BIN" srv -- use "$FLAVOR"
|
|
50
|
+
restart_main_service_best_effort
|
|
51
|
+
echo "ok: main -> $FLAVOR"
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
"$PNPM_BIN" stack srv "$STACK" -- use "$FLAVOR"
|
|
56
|
+
restart_stack_service_best_effort "$STACK"
|
|
57
|
+
echo "ok: $STACK -> $FLAVOR"
|