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
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();
|
package/docs/menubar.md
ADDED
|
@@ -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”.
|