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,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"