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
package/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # Happy Stacks
2
+
3
+ Run the **Happy** stack locally (or many stacks in parallel) and access it remotely and securely (using Tailscale).
4
+
5
+ `happy-stacks` is a CLI (`happys`) that orchestrate the real upstream repos
6
+ cloned under your configured workspace (default: `~/.happy-stacks/workspace/components/*`).
7
+
8
+ ## What is Happy?
9
+
10
+ Happy is an UI/CLI stack (server + web UI + CLI + daemon) who let you monitor and interact with Claude Code, Codex and Gemini sessions from your mobile, a web UI and/or a desktop app.
11
+
12
+ ## What is Happy Stacks?
13
+
14
+ happy-stacks is a “launcher + workflow toolkit” to:
15
+
16
+ - run Happy fully on your own machine (no hosted dependency)
17
+ - safely access it remotely (HTTPS secure context) via Tailscale
18
+ - manage **worktrees** for clean upstream PRs while keeping a patched fork
19
+ - run **multiple isolated stacks** (ports + dirs + component overrides)
20
+ - optionally manage autostart (macOS LaunchAgent) and a SwiftBar menu bar control panel
21
+
22
+ ## Quickstart
23
+
24
+ ### Step 1: Install / bootstrap
25
+
26
+ Recommended:
27
+
28
+ ```bash
29
+ npx happy-stacks init
30
+ export PATH="$HOME/.happy-stacks/bin:$PATH"
31
+ ```
32
+
33
+ Alternative (global install):
34
+
35
+ ```bash
36
+ npm install -g happy-stacks
37
+ happys init
38
+ export PATH="$HOME/.happy-stacks/bin:$PATH"
39
+ ```
40
+
41
+ (`init` will run `bootstrap` automatically. Use `--no-bootstrap` if you only want to write config and shims.)
42
+
43
+ Developer mode (clone this repo):
44
+
45
+ ```bash
46
+ git clone https://github.com/leeroybrun/happy-stacks.git
47
+ cd happy-stacks
48
+
49
+ node ./bin/happys.mjs bootstrap --interactive
50
+ # legacy:
51
+ # pnpm bootstrap -- --interactive
52
+ ```
53
+
54
+ Notes:
55
+
56
+ - In a cloned repo, `pnpm <script>` still works, but `happys <command>` is now the recommended UX (same underlying scripts).
57
+
58
+ ### Step 2: Run the main stack
59
+
60
+ Starts the local server, CLI daemon, and serves the pre-built UI.
61
+
62
+ ```bash
63
+ happys start
64
+ ```
65
+
66
+ ### Step 2b (first run only): authenticate the daemon
67
+
68
+ On a **fresh machine** (or any new stack), the daemon needs to authenticate once before it can register a “machine”.
69
+
70
+ ```bash
71
+ happys auth login
72
+ ```
73
+
74
+ #### Troubleshooting: “no machine” on first run (daemon auth)
75
+
76
+ If `.../new` shows “no machine” check whether this is **auth** vs a **daemon/runtime** issue:
77
+
78
+ ```bash
79
+ happys auth status
80
+ ```
81
+
82
+ If it says **auth is required**, run:
83
+
84
+ ```bash
85
+ happys auth login
86
+ ```
87
+
88
+ If auth is OK but the daemon isn’t running, run:
89
+
90
+ ```bash
91
+ happys doctor
92
+ ```
93
+
94
+ ### Step 3: Enable Tailscale Serve (recommended for remote devices)
95
+
96
+ ```bash
97
+ happys tailscale enable
98
+ happys tailscale url
99
+ ```
100
+
101
+ ### Step 4: Mobile access
102
+
103
+ Make sure Tailscale is [installed and running]
104
+ ([https://tailscale.com/kb/1347/installation](https://tailscale.com/kb/1347/installation)) on your
105
+ phone, then either:
106
+
107
+ - Open the URL from `happys tailscale url` on your phone and “Add to Home Screen”, or
108
+ - [Download the Happy mobile app]
109
+ ([https://happy.engineering/](https://happy.engineering/)) and [configure it to use
110
+ your local server](docs/remote-access.md).
111
+
112
+ Details (secure context, phone instructions, automation knobs): `[docs/remote-access.md](docs/remote-access.md)`.
113
+
114
+ ## Why this exists
115
+
116
+ - **Automated setup**: `happys init` + `happys start` gets the whole stack up and running.
117
+ - **No hosted dependency**: run the full stack on your own computer.
118
+ - **Lower latency**: localhost/LAN is typically much faster than remote hosted servers.
119
+ - **Custom forks**: easily use forks of the Happy UI + CLI (e.g. `leeroybrun/*`) while still contributing upstream to `slopus/*`.
120
+ - **Worktrees**: clean upstream PR branches without mixing fork-only patches.
121
+ - **Stacks**: run multiple isolated instances in parallel (ports + dirs + component overrides).
122
+ - **Remote access**: `happys tailscale ...` helps you get an HTTPS URL for mobile/remote devices.
123
+
124
+ ## How Happy Stacks wires “local” URLs
125
+
126
+ There are two “URLs” to understand:
127
+
128
+ - **Internal URL**: used by local processes on this machine (server/daemon/CLI)
129
+ - typically `http://127.0.0.1:<port>`
130
+ - **Public URL**: used by other devices (phone/laptop) and embedded links/QR codes
131
+ - recommended: `https://<machine>.<tailnet>.ts.net` via Tailscale Serve
132
+
133
+ Diagram:
134
+
135
+ ```text
136
+ other device (phone/laptop)
137
+ |
138
+ | HTTPS (secure context)
139
+ v
140
+ https://<machine>.<tailnet>.ts.net
141
+ |
142
+ | (tailscale serve)
143
+ v
144
+ local machine (this repo)
145
+ +--------------------------------+
146
+ | happy-server(-light) |
147
+ | - listens on :PORT |
148
+ | - serves UI (server-light) |
149
+ +--------------------------------+
150
+ ^
151
+ | internal loopback
152
+ |
153
+ http://127.0.0.1:<port>
154
+ (daemon / CLI)
155
+ ```
156
+
157
+ More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
158
+
159
+ ## How it’s organized
160
+
161
+ - **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
162
+ - **Components**: `components/*` (each is its own Git repo)
163
+ - **Worktrees**: `components/.worktrees/<component>/<owner>/<branch...>`
164
+
165
+ Components:
166
+
167
+ - `happy` (UI)
168
+ - `happy-cli` (CLI + daemon)
169
+ - `happy-server-light` (light server, can serve built UI)
170
+ - `happy-server` (full server)
171
+
172
+ ## Quickstarts (feature-focused)
173
+
174
+ ### Remote access (Tailscale Serve)
175
+
176
+ ```bash
177
+ happys tailscale enable
178
+ happys tailscale url
179
+ ```
180
+
181
+ Details: `[docs/remote-access.md](docs/remote-access.md)`.
182
+
183
+ ### Worktrees + forks (clean upstream PRs)
184
+
185
+ Create a clean upstream PR worktree:
186
+
187
+ ```bash
188
+ happys wt new happy pr/my-feature --from=upstream --use
189
+ happys wt push happy active --remote=upstream
190
+ ```
191
+
192
+ Test an upstream PR locally:
193
+
194
+ ```bash
195
+ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
196
+ happys wt pr happy 123 --update --stash
197
+ ```
198
+
199
+ Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
200
+
201
+ ### Server flavor (server-light vs full server)
202
+
203
+ - Use `happy-server-light` for a light local stack (no Redis, no Postgres, no Docker), and UI serving via server-light.
204
+ - Use `happy-server` when you need some production-ready server (eg. to distribute and host multiple users) or develop server changes for upstream.
205
+
206
+ Switch globally:
207
+
208
+ ```bash
209
+ happys srv status
210
+ happys srv use --interactive
211
+ ```
212
+
213
+ Switch per-stack:
214
+
215
+ ```bash
216
+ happys stack srv exp1 -- use --interactive
217
+ ```
218
+
219
+ Details: `[docs/server-flavors.md](docs/server-flavors.md)`.
220
+
221
+ ### Stacks (multiple isolated instances)
222
+
223
+ ```bash
224
+ happys stack new exp1 --interactive
225
+ happys stack dev exp1
226
+ ```
227
+
228
+ Point a stack at a PR worktree:
229
+
230
+ ```bash
231
+ happys wt pr happy 123 --use
232
+ happys stack wt exp1 -- use happy slopus/pr/123-fix-thing
233
+ happys stack dev exp1
234
+ ```
235
+
236
+ Details: `[docs/stacks.md](docs/stacks.md)`.
237
+
238
+ ### Menu bar (SwiftBar)
239
+
240
+ ```bash
241
+ happys menubar install
242
+ happys menubar open
243
+ ```
244
+
245
+ Details: `[docs/menubar.md](docs/menubar.md)`.
246
+
247
+ ### Mobile iOS dev (optional)
248
+
249
+ ```bash
250
+ happys mobile --help
251
+ happys mobile --json
252
+ ```
253
+
254
+ Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
255
+
256
+ ### Tauri desktop app (optional)
257
+
258
+ ```bash
259
+ happys build --tauri
260
+ ```
261
+
262
+ Details: `[docs/tauri.md](docs/tauri.md)`.
263
+
264
+ ## Commands (high-signal)
265
+
266
+ - **Setup**:
267
+ - `happys init`
268
+ - `happys bootstrap --interactive` (wizard)
269
+ - `happys bootstrap --forks|--upstream`
270
+ - `happys bootstrap --server=happy-server|happy-server-light|both`
271
+ - **Run**:
272
+ - `happys start` (production-like; serves built UI via server-light)
273
+ - `happys dev` (dev; Expo web dev server for UI)
274
+ - **Server flavor**:
275
+ - `happys srv status`
276
+ - `happys srv use --interactive`
277
+ - **Worktrees**:
278
+ - `happys wt use --interactive`
279
+ - `happys wt pr <component> <pr-url|number> --use [--update] [--stash] [--force]`
280
+ - `happys wt sync-all`
281
+ - `happys wt update-all --dry-run` / `happys wt update-all --stash`
282
+ - **Stacks**:
283
+ - `happys stack new --interactive`
284
+ - `happys stack dev <name>` / `happys stack start <name>`
285
+ - `happys stack edit <name> --interactive`
286
+ - `happys stack wt <name> -- use --interactive`
287
+ - `happys stack migrate`
288
+ - **Menu bar (SwiftBar)**:
289
+ - `happys menubar install`
290
+
291
+ ## Docs (deep dives)
292
+
293
+ - **Remote access (Tailscale + phone)**: `[docs/remote-access.md](docs/remote-access.md)`
294
+ - **Server flavors (server-light vs server)**: `[docs/server-flavors.md](docs/server-flavors.md)`
295
+ - **Worktrees + forks workflow**: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`
296
+ - **Stacks (multiple instances)**: `[docs/stacks.md](docs/stacks.md)`
297
+ - **Menu bar (SwiftBar)**: `[docs/menubar.md](docs/menubar.md)`
298
+ - **Mobile iOS dev**: `[docs/mobile-ios.md](docs/mobile-ios.md)`
299
+ - **Tauri desktop app**: `[docs/tauri.md](docs/tauri.md)`
300
+
301
+ ## Configuration
302
+
303
+ Where config lives by default:
304
+
305
+ - `~/.happy-stacks/.env`: stable “pointer” file (home/workspace/runtime)
306
+ - `~/.happy-stacks/env.local`: optional global overrides
307
+ - `~/.happy/stacks/main/env`: main stack config (port, server flavor, component overrides)
308
+
309
+ Notes:
310
+
311
+ - Canonical env prefix is `HAPPY_STACKS_*` (legacy `HAPPY_LOCAL_*` still works).
312
+ - Canonical stack storage is `~/.happy/stacks` (legacy `~/.happy/local` is still supported).
313
+
314
+ For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
package/bin/happys.mjs ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { spawn } from 'node:child_process';
5
+ import { existsSync } from 'node:fs';
6
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli_registry.mjs';
11
+
12
+ function getCliRootDir() {
13
+ return dirname(dirname(fileURLToPath(import.meta.url)));
14
+ }
15
+
16
+ function resolveHomeDir() {
17
+ const fromEnv = (process.env.HAPPY_STACKS_HOME_DIR ?? '').trim();
18
+ if (fromEnv) {
19
+ return fromEnv.replace(/^~(?=\/)/, homedir());
20
+ }
21
+ return join(homedir(), '.happy-stacks');
22
+ }
23
+
24
+ function maybeAutoUpdateNotice(cliRootDir, cmd) {
25
+ // Non-blocking, cached update checks:
26
+ // - never run network calls in-process
27
+ // - optionally print a notice (TTY only) if cache says an update is available
28
+ // - periodically kick off a background check that refreshes the cache
29
+ const enabled = (process.env.HAPPY_STACKS_UPDATE_CHECK ?? '1') !== '0';
30
+ if (!enabled) return;
31
+ // Never do background checks for non-interactive invocations (CI, LaunchAgents, scripts).
32
+ if (!process.stdout.isTTY) return;
33
+ if (process.env.HAPPY_STACKS_UPDATE_CHECK_SPAWNED === '1') return;
34
+ if (cmd === 'self' || cmd === 'help' || cmd === '--help' || cmd === '-h') return;
35
+
36
+ const homeDir = resolveHomeDir();
37
+ const cacheDir = join(homeDir, 'cache');
38
+ const cachePath = join(cacheDir, 'update.json');
39
+
40
+ const intervalMsRaw = (process.env.HAPPY_STACKS_UPDATE_CHECK_INTERVAL_MS ?? '').trim();
41
+ const intervalMs = intervalMsRaw ? Number(intervalMsRaw) : 24 * 60 * 60 * 1000;
42
+ const notifyIntervalMsRaw = (process.env.HAPPY_STACKS_UPDATE_NOTIFY_INTERVAL_MS ?? '').trim();
43
+ const notifyIntervalMs = notifyIntervalMsRaw ? Number(notifyIntervalMsRaw) : 24 * 60 * 60 * 1000;
44
+
45
+ let cached = null;
46
+ try {
47
+ if (existsSync(cachePath)) {
48
+ cached = JSON.parse(readFileSync(cachePath, 'utf-8'));
49
+ }
50
+ } catch {
51
+ cached = null;
52
+ }
53
+
54
+ const now = Date.now();
55
+ const checkedAt = typeof cached?.checkedAt === 'number' ? cached.checkedAt : 0;
56
+ const shouldCheck = !checkedAt || (Number.isFinite(intervalMs) && now - checkedAt > intervalMs);
57
+
58
+ const updateAvailable = Boolean(cached?.updateAvailable);
59
+ const latest = typeof cached?.latest === 'string' ? cached.latest : '';
60
+ const current = typeof cached?.current === 'string' ? cached.current : '';
61
+ const notifiedAt = typeof cached?.notifiedAt === 'number' ? cached.notifiedAt : 0;
62
+ const shouldNotify =
63
+ Boolean(updateAvailable && latest) &&
64
+ Boolean(process.stdout.isTTY) &&
65
+ (!notifiedAt || (Number.isFinite(notifyIntervalMs) && now - notifiedAt > notifyIntervalMs));
66
+
67
+ if (shouldNotify) {
68
+ const from = current ? current : 'current';
69
+ // Keep it short; no network calls here.
70
+ console.error(`[happys] update available: ${from} -> ${latest} (run: happys self update)`);
71
+ try {
72
+ mkdirSync(cacheDir, { recursive: true });
73
+ writeFileSync(
74
+ cachePath,
75
+ JSON.stringify(
76
+ {
77
+ ...(cached ?? {}),
78
+ notifiedAt: now,
79
+ },
80
+ null,
81
+ 2
82
+ ) + '\n',
83
+ 'utf-8'
84
+ );
85
+ } catch {
86
+ // ignore
87
+ }
88
+ }
89
+
90
+ if (!shouldCheck) return;
91
+
92
+ // Kick off a background refresh (best-effort, no logs).
93
+ try {
94
+ const child = spawn(process.execPath, [join(cliRootDir, 'scripts', 'self.mjs'), 'check', '--quiet'], {
95
+ stdio: 'ignore',
96
+ cwd: cliRootDir,
97
+ env: { ...process.env, HAPPY_STACKS_UPDATE_CHECK_SPAWNED: '1' },
98
+ detached: true,
99
+ });
100
+ child.unref();
101
+ } catch {
102
+ // ignore
103
+ }
104
+ }
105
+
106
+ function usage() {
107
+ return renderHappysRootHelp();
108
+ }
109
+
110
+ function runNodeScript(cliRootDir, scriptRelPath, args) {
111
+ const scriptPath = join(cliRootDir, scriptRelPath);
112
+ if (!existsSync(scriptPath)) {
113
+ console.error(`[happys] missing script: ${scriptPath}`);
114
+ process.exit(1);
115
+ }
116
+ const res = spawnSync(process.execPath, [scriptPath, ...args], {
117
+ stdio: 'inherit',
118
+ env: process.env,
119
+ cwd: cliRootDir,
120
+ });
121
+ process.exit(res.status ?? 1);
122
+ }
123
+
124
+ function main() {
125
+ const cliRootDir = getCliRootDir();
126
+ const argv = process.argv.slice(2);
127
+
128
+ const cmd = argv.find((a) => !a.startsWith('--')) ?? 'help';
129
+ const rest = cmd === 'help' ? [] : argv.slice(argv.indexOf(cmd) + 1);
130
+
131
+ maybeAutoUpdateNotice(cliRootDir, cmd);
132
+
133
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
134
+ const target = argv[argv.indexOf(cmd) + 1];
135
+ if (!target) {
136
+ console.log(usage());
137
+ return;
138
+ }
139
+ const targetCmd = resolveHappysCommand(target);
140
+ if (!targetCmd || targetCmd.kind !== 'node') {
141
+ console.error(`[happys] unknown command: ${target}`);
142
+ console.error('');
143
+ console.log(usage());
144
+ process.exit(1);
145
+ }
146
+ const helpArgs = commandHelpArgs(target) ?? ['--help'];
147
+ return runNodeScript(cliRootDir, targetCmd.scriptRelPath, helpArgs);
148
+ }
149
+
150
+ const resolved = resolveHappysCommand(cmd);
151
+ if (!resolved) {
152
+ console.error(`[happys] unknown command: ${cmd}`);
153
+ console.error('');
154
+ console.error(usage());
155
+ process.exit(1);
156
+ }
157
+
158
+ if (resolved.kind === 'external') {
159
+ const args = resolved.external?.argsFromRest ? resolved.external.argsFromRest(rest) : rest;
160
+ const res = spawnSync(resolved.external.cmd, args, { stdio: 'inherit', env: process.env });
161
+ process.exit(res.status ?? 1);
162
+ }
163
+
164
+ const args = resolved.argsFromRest ? resolved.argsFromRest(rest) : rest;
165
+ return runNodeScript(cliRootDir, resolved.scriptRelPath, args);
166
+ }
167
+
168
+ main();
@@ -0,0 +1,186 @@
1
+ # Menu bar (SwiftBar)
2
+
3
+ `happy-stacks` ships a macOS menu bar plugin powered by [SwiftBar](https://swiftbar.app/).
4
+
5
+ SwiftBar runs a script on an interval and renders its output as native macOS menu items.
6
+
7
+ ## Features
8
+
9
+ - **Status at a glance** with dynamic icons (green/orange/red)
10
+ - Server health
11
+ - Daemon status (PID + optional control server probe)
12
+ - Autostart LaunchAgent status
13
+ - Tailscale Serve status / URL (if configured)
14
+ - **Quick controls**
15
+ - Start / stop / restart the stack
16
+ - Install / enable / disable / uninstall autostart
17
+ - Enable / disable Tailscale Serve
18
+ - **Details**
19
+ - PID, CPU %, RAM MB, uptime (where available)
20
+ - Useful URLs and file paths
21
+ - Open logs in Console.app
22
+ - **Refresh control**
23
+ - Manual refresh
24
+ - In-menu refresh interval toggles (includes slower intervals like 10m/15m/30m/1h/6h/12h/1d)
25
+ - Uses a small helper script (`extras/swiftbar/set-interval.sh`) to avoid SwiftBar quoting issues
26
+ - **Stacks + components layout**
27
+ - Main stack is shown directly (no extra nesting level)
28
+ - Each stack shows component rows (Server/Daemon/Autostart/Tailscale) with per-component submenus
29
+ - **Components (git/worktrees)**
30
+ - Available under a top-level **Components** submenu (to keep the main menu clean)
31
+ - Shows repo/worktree status for each component under `components/`
32
+ - Each component includes a **Worktrees** submenu listing all worktrees, with actions to switch/open
33
+ - Quick actions: `wt status/sync/update`, PR worktree prompt, open shells/editors (`wt shell/code/cursor`)
34
+ - Shows **origin** and **upstream** comparisons for the component repo’s main branch (based on your last `git fetch`)
35
+
36
+ ## Stacks (multiple instances)
37
+
38
+ If you create additional stacks (see `docs/stacks.md`), the plugin shows:
39
+
40
+ - **Main stack** (the default, stack name `main`)
41
+ - **Stacks** section listing each stack found under `~/.happy/stacks/<name>/env` (legacy: `~/.happy/local/stacks/<name>/env`)
42
+
43
+ Each stack row renders the same “mini control panel” (server/daemon/autostart/logs + a few actions) with stack-specific ports, dirs, and LaunchAgent label.
44
+
45
+ The menu also includes:
46
+
47
+ - `stack new --interactive` (create stacks)
48
+ - `stack edit <name> --interactive` (edit stack port/server flavor/worktrees)
49
+ - `stack wt <name> -- use --interactive` (switch component worktrees inside a stack)
50
+ - “PR worktree into this stack (prompt)” (creates `wt pr ... --use` scoped to the stack env)
51
+
52
+ ## Worktrees (quick entry points)
53
+
54
+ The menu also provides “jump off” actions for the worktree tooling:
55
+
56
+ - `happys wt use --interactive`
57
+ - `happys wt new --interactive`
58
+ - `happys wt sync-all`
59
+ - `happys wt update-all --dry-run` / `happys wt update-all`
60
+ - `happys wt pr ...` (via an in-menu prompt)
61
+
62
+ For stack-specific worktree selection (which components a stack uses), use:
63
+
64
+ - `happys stack edit <name> --interactive`
65
+ - or `happys stack wt <name> -- use --interactive`
66
+
67
+ ## Implementation notes
68
+
69
+ - **Entry script**: `extras/swiftbar/happy-stacks.5s.sh` (installed into SwiftBar as `happy-stacks.<interval>.sh`)
70
+ - **Shared functions**: `extras/swiftbar/lib/*.sh` (sourced by the entry script)
71
+ - **Helper scripts**:
72
+ - `extras/swiftbar/set-interval.sh`
73
+ - `extras/swiftbar/set-server-flavor.sh`
74
+
75
+ ## Install
76
+
77
+ ### 1) Install SwiftBar
78
+
79
+ ```bash
80
+ brew install --cask swiftbar
81
+ ```
82
+
83
+ ### 2) Install the plugin
84
+
85
+ From the `happy-stacks` repo:
86
+
87
+ ```bash
88
+ happys menubar install
89
+ ```
90
+
91
+ If you want a different default refresh interval at install time:
92
+
93
+ ```bash
94
+ HAPPY_STACKS_SWIFTBAR_INTERVAL=15m happys menubar install
95
+ # legacy: HAPPY_LOCAL_SWIFTBAR_INTERVAL=15m happys menubar install
96
+ ```
97
+
98
+ ### 3) Open the active SwiftBar plugin folder
99
+
100
+ SwiftBar can be configured to use a custom plugin directory. To open the *active* one:
101
+
102
+ ```bash
103
+ happys menubar open
104
+ ```
105
+
106
+ ## Uninstall
107
+
108
+ Remove the installed SwiftBar plugin files (does not delete your stacks/workspace):
109
+
110
+ ```bash
111
+ happys menubar uninstall
112
+ ```
113
+
114
+ ## How refresh works (important)
115
+
116
+ SwiftBar’s refresh interval is controlled by the **filename** suffix:
117
+
118
+ - `happy-stacks.30s.sh` → every 30 seconds
119
+ - `happy-stacks.5m.sh` → every 5 minutes
120
+ - `happy-stacks.1h.sh` → every 1 hour
121
+
122
+ The plugin defaults to a slower interval (recommended), and also sets:
123
+
124
+ - `refreshOnOpen=false` (recommended) to avoid surprise refreshes while you’re navigating the menu.
125
+
126
+ You can also change the interval directly from the menu via **Refresh interval** (it renames the plugin file and restarts SwiftBar).
127
+
128
+ ## Terminal preference for interactive actions
129
+
130
+ Many menu actions open a terminal (interactive wizards, long-running dev servers, etc).
131
+ The plugin uses helper scripts so these run in your preferred terminal, using the same env var as `wt shell`:
132
+
133
+ - `HAPPY_STACKS_WT_TERMINAL=auto|ghostty|iterm|terminal|current` (legacy: `HAPPY_LOCAL_WT_TERMINAL`)
134
+
135
+ Notes:
136
+ - `auto` tries ghostty → iTerm → Terminal → current.
137
+ - Ghostty is best-effort; if your Ghostty build can’t execute the command automatically, the command is copied to your clipboard and Ghostty is opened in the repo directory.
138
+
139
+ ## Start SwiftBar at login (optional)
140
+
141
+ SwiftBar is independent from the Happy Stacks LaunchAgent.
142
+
143
+ - In SwiftBar Preferences, enable **Launch at Login**, or
144
+ - Add SwiftBar to macOS **Login Items**.
145
+
146
+ ## Troubleshooting
147
+
148
+ ### Plugin doesn’t show up
149
+
150
+ - Ensure SwiftBar is running.
151
+ - Check which plugin folder SwiftBar is using:
152
+ - SwiftBar → Preferences → Plugin Folder
153
+ - Open the active folder:
154
+ - `happys menubar open`
155
+
156
+ ### Daemon shows “auth required” / “no machine”
157
+
158
+ This happens on a **fresh machine** (or any new stack) when the daemon does not yet have credentials in the
159
+ stack-specific CLI home directory.
160
+
161
+ **What’s going on**
162
+
163
+ - The daemon stores credentials in `access.key` under the CLI home directory.
164
+ - For stacks (including main), that’s typically:
165
+ - `~/.happy/stacks/<name>/cli/access.key`
166
+ - When `access.key` is missing, `happy-cli daemon start` enters an interactive auth flow and won’t become a “machine” until it completes.
167
+ - Under `launchd` (autostart), this shows up as **no machine** and the daemon may appear “stopped”.
168
+
169
+ **If it still needs auth**
170
+
171
+ - In SwiftBar, open the **Daemon** section:
172
+ - If it shows `auth_required`, click **Auth login (opens browser)**
173
+ - Or run manually:
174
+
175
+ ```bash
176
+ happys auth login
177
+ ```
178
+
179
+ ### “Daemon stale” even though it’s running
180
+
181
+ The plugin checks:
182
+
183
+ - `daemon.state.json` **PID is alive**, and
184
+ - (optionally) the daemon control server responds.
185
+
186
+ If the daemon is running but the menu is stale, refresh and check the **PID** line under “Daemon”.