happy-stacks 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -74
- package/bin/happys.mjs +140 -9
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +55 -4
- package/extras/swiftbar/auth-login.sh +10 -7
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +175 -83
- package/extras/swiftbar/happys-term.sh +128 -0
- package/extras/swiftbar/happys.sh +35 -0
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +279 -132
- package/extras/swiftbar/lib/system.sh +64 -10
- package/extras/swiftbar/lib/utils.sh +469 -10
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +4 -14
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +19 -10
- package/extras/swiftbar/wt-pr.sh +10 -3
- package/package.json +2 -1
- package/scripts/auth.mjs +833 -14
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +200 -23
- package/scripts/dev.mjs +230 -57
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +275 -46
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +302 -0
- package/scripts/mobile.mjs +59 -21
- package/scripts/run.mjs +222 -43
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +190 -38
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +2273 -92
- package/scripts/stop.mjs +160 -0
- package/scripts/tailscale.mjs +164 -23
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +13 -1
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/dev_daemon.mjs +104 -0
- package/scripts/utils/dev_expo_web.mjs +112 -0
- package/scripts/utils/dev_server.mjs +183 -0
- package/scripts/utils/env.mjs +94 -23
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +96 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +484 -0
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +132 -22
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +75 -7
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +61 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +255 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +135 -15
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# Happy Stacks
|
|
2
2
|
|
|
3
|
-
Run
|
|
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/*`).
|
|
3
|
+
Run [**Happy**](https://happy.engineering/) locally and access it remotely and securely (using Tailscale).
|
|
7
4
|
|
|
8
5
|
## What is Happy?
|
|
9
6
|
|
|
@@ -11,51 +8,31 @@ Happy is an UI/CLI stack (server + web UI + CLI + daemon) who let you monitor an
|
|
|
11
8
|
|
|
12
9
|
## What is Happy Stacks?
|
|
13
10
|
|
|
14
|
-
happy-stacks is a
|
|
11
|
+
happy-stacks is a guided installer + local orchestration CLI for Happy.
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
13
|
+
If you only want to **self-host Happy**, start with the **Self-host** section below.
|
|
14
|
+
If you want to **develop Happy** (worktrees, multiple stacks, upstream PR workflows), see the **Development** section further down.
|
|
21
15
|
|
|
22
|
-
##
|
|
16
|
+
## Self-host Happy (install + run)
|
|
23
17
|
|
|
24
|
-
### Step 1:
|
|
18
|
+
### Step 1: Setup
|
|
25
19
|
|
|
26
20
|
Recommended:
|
|
27
21
|
|
|
28
22
|
```bash
|
|
29
|
-
npx happy-stacks
|
|
30
|
-
export PATH="$HOME/.happy-stacks/bin:$PATH"
|
|
23
|
+
npx happy-stacks setup --profile=selfhost
|
|
31
24
|
```
|
|
32
25
|
|
|
33
26
|
Alternative (global install):
|
|
34
27
|
|
|
35
28
|
```bash
|
|
36
29
|
npm install -g happy-stacks
|
|
37
|
-
happys
|
|
38
|
-
export PATH="$HOME/.happy-stacks/bin:$PATH"
|
|
30
|
+
happys setup --profile=selfhost
|
|
39
31
|
```
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Developer mode (clone this repo):
|
|
33
|
+
`setup` can optionally start Happy and guide you through authentication.
|
|
44
34
|
|
|
45
|
-
|
|
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
|
|
35
|
+
### Step 2: Start Happy
|
|
59
36
|
|
|
60
37
|
Starts the local server, CLI daemon, and serves the pre-built UI.
|
|
61
38
|
|
|
@@ -63,7 +40,7 @@ Starts the local server, CLI daemon, and serves the pre-built UI.
|
|
|
63
40
|
happys start
|
|
64
41
|
```
|
|
65
42
|
|
|
66
|
-
### Step
|
|
43
|
+
### Step 3 (first run only): authenticate
|
|
67
44
|
|
|
68
45
|
On a **fresh machine** (or any new stack), the daemon needs to authenticate once before it can register a “machine”.
|
|
69
46
|
|
|
@@ -71,34 +48,20 @@ On a **fresh machine** (or any new stack), the daemon needs to authenticate once
|
|
|
71
48
|
happys auth login
|
|
72
49
|
```
|
|
73
50
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
If `.../new` shows “no machine” check whether this is **auth** vs a **daemon/runtime** issue:
|
|
51
|
+
If you want a quick diagnosis:
|
|
77
52
|
|
|
78
53
|
```bash
|
|
79
54
|
happys auth status
|
|
80
55
|
```
|
|
81
56
|
|
|
82
|
-
|
|
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)
|
|
57
|
+
### Step 4: Enable Tailscale Serve (recommended for mobile/remote)
|
|
95
58
|
|
|
96
59
|
```bash
|
|
97
60
|
happys tailscale enable
|
|
98
61
|
happys tailscale url
|
|
99
62
|
```
|
|
100
63
|
|
|
101
|
-
### Step
|
|
64
|
+
### Step 5: Mobile access
|
|
102
65
|
|
|
103
66
|
Make sure Tailscale is [installed and running]
|
|
104
67
|
([https://tailscale.com/kb/1347/installation](https://tailscale.com/kb/1347/installation)) on your
|
|
@@ -111,9 +74,41 @@ your local server](docs/remote-access.md).
|
|
|
111
74
|
|
|
112
75
|
Details (secure context, phone instructions, automation knobs): `[docs/remote-access.md](docs/remote-access.md)`.
|
|
113
76
|
|
|
114
|
-
##
|
|
77
|
+
## Development (worktrees, stacks, contributor workflows)
|
|
78
|
+
|
|
79
|
+
### Setup (guided)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx happy-stacks setup --profile=dev
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Developing from a cloned repo
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/leeroybrun/happy-stacks.git
|
|
89
|
+
cd happy-stacks
|
|
90
|
+
|
|
91
|
+
node ./bin/happys.mjs setup --profile=dev
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Notes:
|
|
115
95
|
|
|
116
|
-
-
|
|
96
|
+
- In a cloned repo, `pnpm <script>` still works, but `happys <command>` is the recommended UX (same underlying scripts).
|
|
97
|
+
- To make the installed `~/.happy-stacks/bin/happys` shim (LaunchAgents / SwiftBar) run your local checkout without publishing to npm, set:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
echo 'HAPPY_STACKS_CLI_ROOT_DIR=/path/to/your/happy-stacks-checkout' >> ~/.happy-stacks/.env
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or (recommended) persist it via init:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
happys init --cli-root-dir=/path/to/your/happy-stacks-checkout
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Why this exists
|
|
110
|
+
|
|
111
|
+
- **Automated setup**: `happys setup` + `happys start` gets the whole stack up and running.
|
|
117
112
|
- **No hosted dependency**: run the full stack on your own computer.
|
|
118
113
|
- **Lower latency**: localhost/LAN is typically much faster than remote hosted servers.
|
|
119
114
|
- **Custom forks**: easily use forks of the Happy UI + CLI (e.g. `leeroybrun/*`) while still contributing upstream to `slopus/*`.
|
|
@@ -121,7 +116,7 @@ Details (secure context, phone instructions, automation knobs): `[docs/remote-ac
|
|
|
121
116
|
- **Stacks**: run multiple isolated instances in parallel (ports + dirs + component overrides).
|
|
122
117
|
- **Remote access**: `happys tailscale ...` helps you get an HTTPS URL for mobile/remote devices.
|
|
123
118
|
|
|
124
|
-
|
|
119
|
+
### How Happy Stacks wires “local” URLs
|
|
125
120
|
|
|
126
121
|
There are two “URLs” to understand:
|
|
127
122
|
|
|
@@ -143,9 +138,10 @@ Diagram:
|
|
|
143
138
|
v
|
|
144
139
|
local machine (this repo)
|
|
145
140
|
+--------------------------------+
|
|
146
|
-
| happy-server
|
|
141
|
+
| happy-server-light OR |
|
|
142
|
+
| happy-server (via UI gateway) |
|
|
147
143
|
| - listens on :PORT |
|
|
148
|
-
| - serves UI
|
|
144
|
+
| - serves UI at / |
|
|
149
145
|
+--------------------------------+
|
|
150
146
|
^
|
|
151
147
|
| internal loopback
|
|
@@ -156,7 +152,7 @@ Diagram:
|
|
|
156
152
|
|
|
157
153
|
More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
|
|
158
154
|
|
|
159
|
-
|
|
155
|
+
### How it’s organized
|
|
160
156
|
|
|
161
157
|
- **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
|
|
162
158
|
- **Components**: `components/*` (each is its own Git repo)
|
|
@@ -169,9 +165,9 @@ Components:
|
|
|
169
165
|
- `happy-server-light` (light server, can serve built UI)
|
|
170
166
|
- `happy-server` (full server)
|
|
171
167
|
|
|
172
|
-
|
|
168
|
+
### Quickstarts (feature-focused)
|
|
173
169
|
|
|
174
|
-
|
|
170
|
+
#### Remote access (Tailscale Serve)
|
|
175
171
|
|
|
176
172
|
```bash
|
|
177
173
|
happys tailscale enable
|
|
@@ -180,7 +176,7 @@ happys tailscale url
|
|
|
180
176
|
|
|
181
177
|
Details: `[docs/remote-access.md](docs/remote-access.md)`.
|
|
182
178
|
|
|
183
|
-
|
|
179
|
+
#### Worktrees + forks (clean upstream PRs)
|
|
184
180
|
|
|
185
181
|
Create a clean upstream PR worktree:
|
|
186
182
|
|
|
@@ -196,12 +192,44 @@ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
|
|
|
196
192
|
happys wt pr happy 123 --update --stash
|
|
197
193
|
```
|
|
198
194
|
|
|
195
|
+
Create a fully isolated PR stack (creates stack + PR worktrees + optional auth seeding + starts dev):
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
happys stack pr pr123 \
|
|
199
|
+
--happy=https://github.com/slopus/happy/pull/123 \
|
|
200
|
+
--happy-cli=https://github.com/slopus/happy-cli/pull/456 \
|
|
201
|
+
--seed-auth --copy-auth-from=dev-auth --link-auth \
|
|
202
|
+
--dev
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
One-shot “install + run PR stack” (best for maintainers who don’t have Happy Stacks set up yet):
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npx happy-stacks setup pr \
|
|
209
|
+
--happy=https://github.com/slopus/happy/pull/123 \
|
|
210
|
+
--happy-cli=https://github.com/slopus/happy-cli/pull/456
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
You can also run it as:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx happy-stacks setup-pr \
|
|
217
|
+
--happy=https://github.com/slopus/happy/pull/123 \
|
|
218
|
+
--happy-cli=https://github.com/slopus/happy-cli/pull/456
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Updating when the PR changes:
|
|
222
|
+
|
|
223
|
+
- Re-run the same command to fast-forward the PR worktrees.
|
|
224
|
+
- If the PR was force-pushed, add `--force`.
|
|
225
|
+
|
|
199
226
|
Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
|
|
200
227
|
|
|
201
|
-
|
|
228
|
+
#### Server flavor (server-light vs full server)
|
|
202
229
|
|
|
203
230
|
- 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
|
|
231
|
+
- Use `happy-server` when you need a more production-like server (Postgres + Redis + S3-compatible storage) or want to develop server changes for upstream.
|
|
232
|
+
- Happy Stacks can **manage the required infra automatically per stack** (via Docker Compose) and runs a **UI gateway** so you still get a single `https://...ts.net` URL that serves the UI + proxies API/websockets/files.
|
|
205
233
|
|
|
206
234
|
Switch globally:
|
|
207
235
|
|
|
@@ -218,7 +246,7 @@ happys stack srv exp1 -- use --interactive
|
|
|
218
246
|
|
|
219
247
|
Details: `[docs/server-flavors.md](docs/server-flavors.md)`.
|
|
220
248
|
|
|
221
|
-
|
|
249
|
+
#### Stacks (multiple isolated instances)
|
|
222
250
|
|
|
223
251
|
```bash
|
|
224
252
|
happys stack new exp1 --interactive
|
|
@@ -235,7 +263,15 @@ happys stack dev exp1
|
|
|
235
263
|
|
|
236
264
|
Details: `[docs/stacks.md](docs/stacks.md)`.
|
|
237
265
|
|
|
238
|
-
|
|
266
|
+
#### Dev stacks: browser origin isolation (IMPORTANT)
|
|
267
|
+
|
|
268
|
+
Non-main stacks use a stack-specific localhost hostname (no `/etc/hosts` changes required):
|
|
269
|
+
|
|
270
|
+
- `http://happy-<stack>.localhost:<uiPort>`
|
|
271
|
+
|
|
272
|
+
This avoids browser auth/session collisions between stacks (separate origin per stack).
|
|
273
|
+
|
|
274
|
+
#### Menu bar (SwiftBar)
|
|
239
275
|
|
|
240
276
|
```bash
|
|
241
277
|
happys menubar install
|
|
@@ -244,7 +280,7 @@ happys menubar open
|
|
|
244
280
|
|
|
245
281
|
Details: `[docs/menubar.md](docs/menubar.md)`.
|
|
246
282
|
|
|
247
|
-
|
|
283
|
+
#### Mobile iOS dev (optional)
|
|
248
284
|
|
|
249
285
|
```bash
|
|
250
286
|
happys mobile --help
|
|
@@ -253,7 +289,7 @@ happys mobile --json
|
|
|
253
289
|
|
|
254
290
|
Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
|
|
255
291
|
|
|
256
|
-
|
|
292
|
+
#### Tauri desktop app (optional)
|
|
257
293
|
|
|
258
294
|
```bash
|
|
259
295
|
happys build --tauri
|
|
@@ -261,13 +297,12 @@ happys build --tauri
|
|
|
261
297
|
|
|
262
298
|
Details: `[docs/tauri.md](docs/tauri.md)`.
|
|
263
299
|
|
|
264
|
-
|
|
300
|
+
### Commands (high-signal)
|
|
265
301
|
|
|
266
302
|
- **Setup**:
|
|
267
|
-
- `happys
|
|
268
|
-
- `happys
|
|
269
|
-
- `happys bootstrap --
|
|
270
|
-
- `happys bootstrap --server=happy-server|happy-server-light|both`
|
|
303
|
+
- `happys setup` (guided; selfhost or dev)
|
|
304
|
+
- (advanced) `happys init` (plumbing: shims/runtime/pointer env)
|
|
305
|
+
- (advanced) `happys bootstrap --interactive` (component installer wizard)
|
|
271
306
|
- **Run**:
|
|
272
307
|
- `happys start` (production-like; serves built UI via server-light)
|
|
273
308
|
- `happys dev` (dev; Expo web dev server for UI)
|
|
@@ -288,17 +323,18 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
|
|
|
288
323
|
- **Menu bar (SwiftBar)**:
|
|
289
324
|
- `happys menubar install`
|
|
290
325
|
|
|
291
|
-
|
|
326
|
+
### Docs (deep dives)
|
|
292
327
|
|
|
293
328
|
- **Remote access (Tailscale + phone)**: `[docs/remote-access.md](docs/remote-access.md)`
|
|
294
329
|
- **Server flavors (server-light vs server)**: `[docs/server-flavors.md](docs/server-flavors.md)`
|
|
295
330
|
- **Worktrees + forks workflow**: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`
|
|
296
331
|
- **Stacks (multiple instances)**: `[docs/stacks.md](docs/stacks.md)`
|
|
332
|
+
- **Paths + env precedence (home/workspace/runtime/stacks)**: `[docs/paths-and-env.md](docs/paths-and-env.md)`
|
|
297
333
|
- **Menu bar (SwiftBar)**: `[docs/menubar.md](docs/menubar.md)`
|
|
298
334
|
- **Mobile iOS dev**: `[docs/mobile-ios.md](docs/mobile-ios.md)`
|
|
299
335
|
- **Tauri desktop app**: `[docs/tauri.md](docs/tauri.md)`
|
|
300
336
|
|
|
301
|
-
|
|
337
|
+
### Configuration
|
|
302
338
|
|
|
303
339
|
Where config lives by default:
|
|
304
340
|
|
|
@@ -310,5 +346,25 @@ Notes:
|
|
|
310
346
|
|
|
311
347
|
- Canonical env prefix is `HAPPY_STACKS_*` (legacy `HAPPY_LOCAL_*` still works).
|
|
312
348
|
- Canonical stack storage is `~/.happy/stacks` (legacy `~/.happy/local` is still supported).
|
|
349
|
+
- **Repo env templates**:
|
|
350
|
+
- **Use `.env.example` as the canonical template** (copy it to `.env` if you’re running this repo directly).
|
|
351
|
+
- If an LLM tool refuses to read/edit `.env.example` due to safety restrictions, **do not create an `env.example` workaround**—instead, ask the user to apply the change manually.
|
|
352
|
+
|
|
353
|
+
### Sandbox / test installs (fully isolated)
|
|
354
|
+
|
|
355
|
+
If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run with:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npx happy-stacks --sandbox-dir /tmp/happy-stacks-sandbox setup pr --happy=123 --happy-cli=456
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
To reset completely, just delete the sandbox folder:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
rm -rf /tmp/happy-stacks-sandbox
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Sandbox mode disables global OS side effects (PATH edits, SwiftBar plugin install, LaunchAgents/systemd services) by default.
|
|
368
|
+
To explicitly allow them for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1`.
|
|
313
369
|
|
|
314
|
-
For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
|
|
370
|
+
For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
|
package/bin/happys.mjs
CHANGED
|
@@ -7,18 +7,142 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli_registry.mjs';
|
|
10
|
+
import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli/cli_registry.mjs';
|
|
11
|
+
import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/canonical_home.mjs';
|
|
11
12
|
|
|
12
13
|
function getCliRootDir() {
|
|
13
14
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// expandHome is imported from scripts/utils/canonical_home.mjs
|
|
18
|
+
|
|
19
|
+
function dotenvGetQuick(envPath, key) {
|
|
20
|
+
try {
|
|
21
|
+
if (!envPath || !existsSync(envPath)) return '';
|
|
22
|
+
const lines = readFileSync(envPath, 'utf-8').split('\n');
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
26
|
+
if (!trimmed.startsWith(`${key}=`)) continue;
|
|
27
|
+
let v = trimmed.slice(`${key}=`.length).trim();
|
|
28
|
+
if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
|
|
29
|
+
if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveCliRootDir() {
|
|
39
|
+
const fromEnv = (
|
|
40
|
+
process.env.HAPPY_STACKS_CLI_ROOT_DIR ??
|
|
41
|
+
process.env.HAPPY_LOCAL_CLI_ROOT_DIR ??
|
|
42
|
+
process.env.HAPPY_STACKS_DEV_CLI_ROOT_DIR ??
|
|
43
|
+
process.env.HAPPY_LOCAL_DEV_CLI_ROOT_DIR ??
|
|
44
|
+
''
|
|
45
|
+
).trim();
|
|
46
|
+
if (fromEnv) return expandHome(fromEnv);
|
|
47
|
+
|
|
48
|
+
// Stable pointer file: even if the real home dir is elsewhere, `happys init` writes the pointer here.
|
|
49
|
+
const canonicalEnv = getCanonicalHomeEnvPathFromEnv(process.env);
|
|
50
|
+
const v =
|
|
51
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_CLI_ROOT_DIR') ||
|
|
52
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_CLI_ROOT_DIR') ||
|
|
53
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_DEV_CLI_ROOT_DIR') ||
|
|
54
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_DEV_CLI_ROOT_DIR') ||
|
|
55
|
+
'';
|
|
56
|
+
return v ? expandHome(v) : '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function maybeReexecToCliRoot(cliRootDir) {
|
|
60
|
+
if ((process.env.HAPPY_STACKS_CLI_REEXEC ?? process.env.HAPPY_STACKS_DEV_REEXEC ?? '') === '1') return;
|
|
61
|
+
if ((process.env.HAPPY_STACKS_CLI_ROOT_DISABLE ?? process.env.HAPPY_STACKS_DEV_CLI_DISABLE ?? '') === '1') return;
|
|
62
|
+
|
|
63
|
+
const cliRoot = resolveCliRootDir();
|
|
64
|
+
if (!cliRoot) return;
|
|
65
|
+
if (cliRoot === cliRootDir) return;
|
|
66
|
+
|
|
67
|
+
const cliBin = join(cliRoot, 'bin', 'happys.mjs');
|
|
68
|
+
if (!existsSync(cliBin)) return;
|
|
69
|
+
|
|
70
|
+
const argv = process.argv.slice(2);
|
|
71
|
+
const res = spawnSync(process.execPath, [cliBin, ...argv], {
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
cwd: cliRoot,
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
HAPPY_STACKS_CLI_REEXEC: '1',
|
|
77
|
+
HAPPY_STACKS_CLI_ROOT_DIR: cliRoot,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
process.exit(res.status ?? 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
16
83
|
function resolveHomeDir() {
|
|
17
|
-
const fromEnv = (process.env.HAPPY_STACKS_HOME_DIR ?? '').trim();
|
|
18
|
-
if (fromEnv)
|
|
19
|
-
|
|
84
|
+
const fromEnv = (process.env.HAPPY_STACKS_HOME_DIR ?? process.env.HAPPY_LOCAL_HOME_DIR ?? '').trim();
|
|
85
|
+
if (fromEnv) return expandHome(fromEnv);
|
|
86
|
+
|
|
87
|
+
// Stable pointer file: even if the real home dir is elsewhere, `happys init` writes the pointer here.
|
|
88
|
+
const canonicalEnv = getCanonicalHomeEnvPathFromEnv(process.env);
|
|
89
|
+
const v = dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_HOME_DIR') || dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_HOME_DIR') || '';
|
|
90
|
+
return v ? expandHome(v) : join(homedir(), '.happy-stacks');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stripGlobalOpt(argv, { name, aliases = [] }) {
|
|
94
|
+
const names = [name, ...aliases];
|
|
95
|
+
for (const n of names) {
|
|
96
|
+
const eq = `${n}=`;
|
|
97
|
+
const iEq = argv.findIndex((a) => a.startsWith(eq));
|
|
98
|
+
if (iEq >= 0) {
|
|
99
|
+
const value = argv[iEq].slice(eq.length);
|
|
100
|
+
const next = [...argv.slice(0, iEq), ...argv.slice(iEq + 1)];
|
|
101
|
+
return { value, argv: next };
|
|
102
|
+
}
|
|
103
|
+
const i = argv.indexOf(n);
|
|
104
|
+
if (i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('-')) {
|
|
105
|
+
const value = argv[i + 1];
|
|
106
|
+
const next = [...argv.slice(0, i), ...argv.slice(i + 2)];
|
|
107
|
+
return { value, argv: next };
|
|
108
|
+
}
|
|
20
109
|
}
|
|
21
|
-
return
|
|
110
|
+
return { value: '', argv };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applySandboxDirIfRequested(argv) {
|
|
114
|
+
const explicit = (process.env.HAPPY_STACKS_SANDBOX_DIR ?? '').trim();
|
|
115
|
+
const { value, argv: nextArgv } = stripGlobalOpt(argv, { name: '--sandbox-dir', aliases: ['--sandbox'] });
|
|
116
|
+
const raw = value || explicit;
|
|
117
|
+
if (!raw) return { argv: nextArgv, enabled: false };
|
|
118
|
+
|
|
119
|
+
const sandboxDir = expandHome(raw);
|
|
120
|
+
// Keep all state under one folder that can be deleted to reset completely.
|
|
121
|
+
const canonicalHomeDir = join(sandboxDir, 'canonical');
|
|
122
|
+
const homeDir = join(sandboxDir, 'home');
|
|
123
|
+
const workspaceDir = join(sandboxDir, 'workspace');
|
|
124
|
+
const runtimeDir = join(sandboxDir, 'runtime');
|
|
125
|
+
const storageDir = join(sandboxDir, 'storage');
|
|
126
|
+
|
|
127
|
+
process.env.HAPPY_STACKS_SANDBOX_DIR = sandboxDir;
|
|
128
|
+
process.env.HAPPY_STACKS_CLI_ROOT_DISABLE = '1'; // never re-exec into a user's "real" install when sandboxing
|
|
129
|
+
|
|
130
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = process.env.HAPPY_STACKS_CANONICAL_HOME_DIR ?? canonicalHomeDir;
|
|
131
|
+
process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR = process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR ?? process.env.HAPPY_STACKS_CANONICAL_HOME_DIR;
|
|
132
|
+
|
|
133
|
+
process.env.HAPPY_STACKS_HOME_DIR = process.env.HAPPY_STACKS_HOME_DIR ?? homeDir;
|
|
134
|
+
process.env.HAPPY_LOCAL_HOME_DIR = process.env.HAPPY_LOCAL_HOME_DIR ?? process.env.HAPPY_STACKS_HOME_DIR;
|
|
135
|
+
|
|
136
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR = process.env.HAPPY_STACKS_WORKSPACE_DIR ?? workspaceDir;
|
|
137
|
+
process.env.HAPPY_LOCAL_WORKSPACE_DIR = process.env.HAPPY_LOCAL_WORKSPACE_DIR ?? process.env.HAPPY_STACKS_WORKSPACE_DIR;
|
|
138
|
+
|
|
139
|
+
process.env.HAPPY_STACKS_RUNTIME_DIR = process.env.HAPPY_STACKS_RUNTIME_DIR ?? runtimeDir;
|
|
140
|
+
process.env.HAPPY_LOCAL_RUNTIME_DIR = process.env.HAPPY_LOCAL_RUNTIME_DIR ?? process.env.HAPPY_STACKS_RUNTIME_DIR;
|
|
141
|
+
|
|
142
|
+
process.env.HAPPY_STACKS_STORAGE_DIR = process.env.HAPPY_STACKS_STORAGE_DIR ?? storageDir;
|
|
143
|
+
process.env.HAPPY_LOCAL_STORAGE_DIR = process.env.HAPPY_LOCAL_STORAGE_DIR ?? process.env.HAPPY_STACKS_STORAGE_DIR;
|
|
144
|
+
|
|
145
|
+
return { argv: nextArgv, enabled: true };
|
|
22
146
|
}
|
|
23
147
|
|
|
24
148
|
function maybeAutoUpdateNotice(cliRootDir, cmd) {
|
|
@@ -123,16 +247,23 @@ function runNodeScript(cliRootDir, scriptRelPath, args) {
|
|
|
123
247
|
|
|
124
248
|
function main() {
|
|
125
249
|
const cliRootDir = getCliRootDir();
|
|
126
|
-
const
|
|
250
|
+
const initialArgv = process.argv.slice(2);
|
|
251
|
+
const { argv, enabled: sandboxed } = applySandboxDirIfRequested(initialArgv);
|
|
252
|
+
void sandboxed;
|
|
253
|
+
maybeReexecToCliRoot(cliRootDir);
|
|
127
254
|
|
|
255
|
+
// If the user passed only flags (common via `npx happy-stacks --help`),
|
|
256
|
+
// treat it as root help rather than `help --help` (which would look like
|
|
257
|
+
// "unknown command: --help").
|
|
128
258
|
const cmd = argv.find((a) => !a.startsWith('--')) ?? 'help';
|
|
129
|
-
const
|
|
259
|
+
const cmdIndex = argv.indexOf(cmd);
|
|
260
|
+
const rest = cmdIndex >= 0 ? argv.slice(cmdIndex + 1) : [];
|
|
130
261
|
|
|
131
262
|
maybeAutoUpdateNotice(cliRootDir, cmd);
|
|
132
263
|
|
|
133
264
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
134
|
-
const target =
|
|
135
|
-
if (!target) {
|
|
265
|
+
const target = rest[0];
|
|
266
|
+
if (!target || target.startsWith('-')) {
|
|
136
267
|
console.log(usage());
|
|
137
268
|
return;
|
|
138
269
|
}
|