happy-stacks 0.3.0 → 0.5.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 (165) hide show
  1. package/README.md +93 -40
  2. package/bin/happys.mjs +158 -16
  3. package/docs/codex-mcp-resume.md +130 -0
  4. package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
  5. package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
  6. package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
  7. package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
  8. package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
  9. package/docs/happy-development.md +3 -4
  10. package/docs/isolated-linux-vm.md +82 -0
  11. package/docs/mobile-ios.md +112 -54
  12. package/docs/monorepo-migration.md +286 -0
  13. package/docs/server-flavors.md +19 -3
  14. package/docs/stacks.md +35 -0
  15. package/package.json +5 -1
  16. package/scripts/auth.mjs +32 -10
  17. package/scripts/build.mjs +55 -8
  18. package/scripts/daemon.mjs +166 -10
  19. package/scripts/dev.mjs +198 -50
  20. package/scripts/doctor.mjs +0 -4
  21. package/scripts/edison.mjs +6 -4
  22. package/scripts/env.mjs +150 -0
  23. package/scripts/env_cmd.test.mjs +128 -0
  24. package/scripts/init.mjs +8 -3
  25. package/scripts/install.mjs +207 -69
  26. package/scripts/lint.mjs +24 -4
  27. package/scripts/migrate.mjs +3 -12
  28. package/scripts/mobile.mjs +88 -104
  29. package/scripts/mobile_dev_client.mjs +83 -0
  30. package/scripts/monorepo.mjs +1096 -0
  31. package/scripts/monorepo_port.test.mjs +1470 -0
  32. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  33. package/scripts/review.mjs +908 -0
  34. package/scripts/review_pr.mjs +353 -0
  35. package/scripts/run.mjs +101 -21
  36. package/scripts/service.mjs +2 -2
  37. package/scripts/setup.mjs +189 -68
  38. package/scripts/setup_pr.mjs +586 -38
  39. package/scripts/stack.mjs +990 -196
  40. package/scripts/stack_archive_cmd.test.mjs +91 -0
  41. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
  42. package/scripts/stack_env_cmd.test.mjs +87 -0
  43. package/scripts/stack_happy_cmd.test.mjs +126 -0
  44. package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
  45. package/scripts/stack_monorepo_defaults.test.mjs +62 -0
  46. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
  47. package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
  48. package/scripts/stack_shorthand_cmd.test.mjs +55 -0
  49. package/scripts/stack_wt_list.test.mjs +128 -0
  50. package/scripts/tailscale.mjs +37 -1
  51. package/scripts/test.mjs +45 -8
  52. package/scripts/tui.mjs +395 -39
  53. package/scripts/typecheck.mjs +24 -4
  54. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  55. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  56. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  57. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  58. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  59. package/scripts/utils/auth/login_ux.mjs +32 -13
  60. package/scripts/utils/auth/sources.mjs +26 -0
  61. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  62. package/scripts/utils/cli/cli_registry.mjs +43 -4
  63. package/scripts/utils/cli/cwd_scope.mjs +136 -0
  64. package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
  65. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  66. package/scripts/utils/cli/prereqs.mjs +75 -0
  67. package/scripts/utils/cli/prereqs.test.mjs +34 -0
  68. package/scripts/utils/cli/progress.mjs +126 -0
  69. package/scripts/utils/cli/verbosity.mjs +12 -0
  70. package/scripts/utils/cli/wizard.mjs +17 -9
  71. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
  72. package/scripts/utils/dev/daemon.mjs +61 -4
  73. package/scripts/utils/dev/expo_dev.mjs +430 -0
  74. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  75. package/scripts/utils/dev/server.mjs +36 -42
  76. package/scripts/utils/dev_auth_key.mjs +169 -0
  77. package/scripts/utils/edison/git_roots.mjs +29 -0
  78. package/scripts/utils/edison/git_roots.test.mjs +36 -0
  79. package/scripts/utils/env/env.mjs +7 -3
  80. package/scripts/utils/env/env_file.mjs +4 -2
  81. package/scripts/utils/env/env_file.test.mjs +44 -0
  82. package/scripts/utils/expo/command.mjs +52 -0
  83. package/scripts/utils/expo/expo.mjs +20 -1
  84. package/scripts/utils/expo/metro_ports.mjs +114 -0
  85. package/scripts/utils/git/git.mjs +67 -0
  86. package/scripts/utils/git/worktrees.mjs +80 -25
  87. package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
  88. package/scripts/utils/handy_master_secret.mjs +94 -0
  89. package/scripts/utils/mobile/config.mjs +31 -0
  90. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  91. package/scripts/utils/mobile/identifiers.mjs +47 -0
  92. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  93. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  94. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  95. package/scripts/utils/net/lan_ip.mjs +24 -0
  96. package/scripts/utils/net/ports.mjs +9 -1
  97. package/scripts/utils/net/tcp_forward.mjs +162 -0
  98. package/scripts/utils/net/url.mjs +30 -0
  99. package/scripts/utils/net/url.test.mjs +20 -0
  100. package/scripts/utils/paths/localhost_host.mjs +50 -3
  101. package/scripts/utils/paths/paths.mjs +159 -40
  102. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  103. package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
  104. package/scripts/utils/proc/commands.mjs +2 -3
  105. package/scripts/utils/proc/parallel.mjs +25 -0
  106. package/scripts/utils/proc/pm.mjs +176 -22
  107. package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
  108. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
  109. package/scripts/utils/proc/proc.mjs +136 -4
  110. package/scripts/utils/proc/proc.test.mjs +77 -0
  111. package/scripts/utils/review/base_ref.mjs +74 -0
  112. package/scripts/utils/review/base_ref.test.mjs +54 -0
  113. package/scripts/utils/review/chunks.mjs +55 -0
  114. package/scripts/utils/review/chunks.test.mjs +51 -0
  115. package/scripts/utils/review/findings.mjs +165 -0
  116. package/scripts/utils/review/findings.test.mjs +85 -0
  117. package/scripts/utils/review/head_slice.mjs +153 -0
  118. package/scripts/utils/review/head_slice.test.mjs +91 -0
  119. package/scripts/utils/review/instructions/deep.md +20 -0
  120. package/scripts/utils/review/runners/coderabbit.mjs +61 -0
  121. package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
  122. package/scripts/utils/review/runners/codex.mjs +61 -0
  123. package/scripts/utils/review/runners/codex.test.mjs +35 -0
  124. package/scripts/utils/review/slices.mjs +140 -0
  125. package/scripts/utils/review/slices.test.mjs +32 -0
  126. package/scripts/utils/review/targets.mjs +24 -0
  127. package/scripts/utils/review/targets.test.mjs +36 -0
  128. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  129. package/scripts/utils/server/flavor_scripts.mjs +98 -0
  130. package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
  131. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  132. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  133. package/scripts/utils/server/prisma_import.mjs +37 -0
  134. package/scripts/utils/server/prisma_import.test.mjs +70 -0
  135. package/scripts/utils/server/ui_env.mjs +14 -0
  136. package/scripts/utils/server/ui_env.test.mjs +46 -0
  137. package/scripts/utils/server/urls.mjs +14 -4
  138. package/scripts/utils/server/validate.mjs +53 -16
  139. package/scripts/utils/server/validate.test.mjs +89 -0
  140. package/scripts/utils/service/autostart_darwin.mjs +42 -2
  141. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  142. package/scripts/utils/stack/context.mjs +2 -2
  143. package/scripts/utils/stack/editor_workspace.mjs +6 -6
  144. package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
  145. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  146. package/scripts/utils/stack/runtime_state.mjs +2 -1
  147. package/scripts/utils/stack/startup.mjs +120 -13
  148. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
  149. package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
  150. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
  151. package/scripts/utils/stack/stop.mjs +15 -4
  152. package/scripts/utils/stack_context.mjs +23 -0
  153. package/scripts/utils/stack_runtime_state.mjs +104 -0
  154. package/scripts/utils/stacks.mjs +38 -0
  155. package/scripts/utils/tailscale/ip.mjs +116 -0
  156. package/scripts/utils/ui/ansi.mjs +39 -0
  157. package/scripts/utils/ui/qr.mjs +17 -0
  158. package/scripts/utils/validate.mjs +88 -0
  159. package/scripts/where.mjs +2 -2
  160. package/scripts/worktrees.mjs +755 -179
  161. package/scripts/worktrees_archive_cmd.test.mjs +245 -0
  162. package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
  163. package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
  164. package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
  165. package/scripts/utils/dev/expo_web.mjs +0 -112
@@ -0,0 +1,286 @@
1
+ # Monorepo migration (split repos → `slopus/happy`)
2
+
3
+ This doc explains the **recommended, safe, step-by-step** flow to port commits from the legacy split repos (`happy`, `happy-cli`, `happy-server`) into the new `slopus/happy` monorepo layout:
4
+
5
+ - old `happy` (UI) → `expo-app/`
6
+ - old `happy-cli` → `cli/`
7
+ - old `happy-server` → `server/`
8
+
9
+ The tooling used here is `happys monorepo port` (from this repo / Happy Stacks). It ports commits by generating patches (`git format-patch`) and applying them with `git am` into the target monorepo branch.
10
+
11
+ ## Quick start (if you don’t already use Happy Stacks)
12
+
13
+ If you’re an external collaborator and you **don’t** have `happys` installed yet, this is the fastest “migration environment” setup:
14
+
15
+ ```bash
16
+ npx happy-stacks init --install-path
17
+ happys bootstrap --interactive
18
+ happys stack new monorepo-merge --interactive
19
+ ```
20
+
21
+ ### “No install” option (npx-only, port command only)
22
+
23
+ If you *only* want to run the port helper (and you already have local clones of the source repos + target monorepo), you can run it directly via `npx` without installing anything globally:
24
+
25
+ ```bash
26
+ npx --yes happy-stacks monorepo port --help
27
+ ```
28
+
29
+ Example:
30
+
31
+ ```bash
32
+ npx --yes happy-stacks monorepo port \
33
+ --target=/abs/path/to/slopus-happy-monorepo \
34
+ --branch=your-port-branch \
35
+ --base=origin/main \
36
+ --3way \
37
+ --from-happy=/abs/path/to/old-happy \
38
+ --from-happy-base=origin/main
39
+ ```
40
+
41
+ Notes:
42
+ - This “npx-only” mode is great for **one-off ports**, but it won’t manage stacks/worktrees for you.
43
+ - For the full guided flow (stacks + worktrees + repeatable commands), use the installed setup above.
44
+
45
+ ### Port helpers (guide / status / continue)
46
+
47
+ Happy Stacks also provides small helpers that make conflict resolution less error-prone:
48
+
49
+ ```bash
50
+ happys monorepo port guide
51
+ happys monorepo port status --target=/abs/path/to/monorepo
52
+ happys monorepo port continue --target=/abs/path/to/monorepo
53
+ ```
54
+
55
+ - `port guide` is interactive (TTY required) and helps you build the initial port command safely.
56
+ - `port guide` can also **pause on conflicts**, let you resolve them, and then resume until the port completes.
57
+ - `port status` shows whether a `git am` session is in progress, the current patch subject, and conflicted files.
58
+ - `port continue` runs `git am --continue` for the target repo (after you staged resolved files). If you started from `port guide`, it can also resume the remaining port automatically after each continue.
59
+
60
+ How to bring “the changes you want to port” into this environment:
61
+
62
+ - **If you already have local checkouts** of your legacy repos (recommended for forks/branches that aren’t PRs yet):
63
+ - keep them wherever they are
64
+ - you’ll pass their absolute paths to `happys monorepo port` via `--from-happy=...`, `--from-happy-cli=...`, etc.
65
+
66
+ - **If your changes exist as GitHub PRs**, you can let Happy Stacks create a clean worktree from the PR directly:
67
+ - `happys wt pr happy-cli <pr-url-or-number> --use`
68
+ - `happys wt pr happy <pr-url-or-number> --use`
69
+ - (repeat for whichever repos your changes live in)
70
+
71
+ Once you have:
72
+ - a **target monorepo worktree** (from upstream), and
73
+ - one or more **source repos** (paths or worktrees)
74
+
75
+ …continue with the rest of this doc.
76
+
77
+ ## Prereqs
78
+
79
+ - You have a **clean** source worktree/branch for each legacy repo you want to port.
80
+ - No uncommitted changes (unless you intentionally want to include them as new commits first).
81
+ - Ideally based on `upstream/main` (or you know the correct base ref).
82
+ - You have a **clean** target monorepo checkout (a worktree of `slopus/happy`).
83
+ - You are ready to resolve conflicts with `git am` (this is normal for large refactors, i18n changes, renamed files, etc).
84
+
85
+ ## Recommended workflow (interactive, safest)
86
+
87
+ ### 1) Create an isolated stack (don’t touch `main`)
88
+
89
+ Pick a new stack name (example: `monorepo-merge`):
90
+
91
+ ```bash
92
+ happys stack new monorepo-merge --interactive
93
+ ```
94
+
95
+ ### 2) Create a clean monorepo worktree from upstream
96
+
97
+ Create a worktree based on `upstream/main` for the **monorepo** (`slopus/happy`):
98
+
99
+ ```bash
100
+ happys wt new happy tmp/monorepo-port --from=upstream
101
+ ```
102
+
103
+ Point your stack at that worktree (this keeps `main` stable):
104
+
105
+ ```bash
106
+ happys stack wt monorepo-merge -- use happy /absolute/path/to/components/.worktrees/happy/slopus/tmp/monorepo-port
107
+ ```
108
+
109
+ Notes:
110
+ - In monorepo mode, `happy`, `happy-cli`, and `happy-server` are **one git repo**; the stack overrides should point at the same monorepo root.
111
+ - `happys ... wt use happy <monorepo-root>` automatically updates all three component dir overrides together (prevents UI/CLI/server version skew).
112
+ - If you pass a monorepo root, Happy Stacks normalizes component dirs to:
113
+ - `happy` → `.../expo-app`
114
+ - `happy-cli` → `.../cli`
115
+ - `happy-server` → `.../server`
116
+
117
+ ### 3) Create a target branch
118
+
119
+ In the monorepo worktree, create your migration branch from `upstream/main`:
120
+
121
+ ```bash
122
+ happys wt git happy slopus/tmp/monorepo-port -- checkout -b <your-branch-name> upstream/main
123
+ ```
124
+
125
+ Example:
126
+
127
+ ```bash
128
+ happys wt git happy slopus/tmp/monorepo-port -- checkout -b leeroy-wip upstream/main
129
+ ```
130
+
131
+ ### 4) Port the UI commits (old `happy` → `expo-app/`)
132
+
133
+ Run the port in **interactive mode**:
134
+
135
+ - use `--onto-current` to apply onto the branch you already checked out
136
+ - use `--3way` so git can do a 3-way merge and produce conflict markers instead of failing immediately
137
+ - **do not** use `--continue-on-failure` (you want it to stop at the first conflict)
138
+
139
+ ```bash
140
+ happys monorepo port \
141
+ --target=/abs/path/to/monorepo-root \
142
+ --onto-current \
143
+ --3way \
144
+ --from-happy=/abs/path/to/old-happy-repo \
145
+ --from-happy-base=upstream/main
146
+ ```
147
+
148
+ What `--from-happy-base` means:
149
+ - It’s the ref used to compute the patch range (`merge-base(base, HEAD)..HEAD`).
150
+ - If your branch is based on `upstream/main`, this should be `upstream/main`.
151
+
152
+ ### 5) Resolve conflicts (when the port stops)
153
+
154
+ If the port stops, you are now in a normal `git am` session.
155
+
156
+ Helpful commands:
157
+
158
+ ```bash
159
+ git am --show-current-patch=diff
160
+ git status
161
+ ```
162
+
163
+ Fix conflicts in the files with conflict markers:
164
+ - look for `<<<<<<<`, `=======`, `>>>>>>>`
165
+ - edit to the desired final content
166
+ - then stage the resolved files:
167
+
168
+ ```bash
169
+ git add <file...>
170
+ git am --continue
171
+ ```
172
+
173
+ If you decide a specific patch should not be ported:
174
+
175
+ ```bash
176
+ git am --skip
177
+ ```
178
+
179
+ If you want to fully abort the current patch application:
180
+
181
+ ```bash
182
+ git am --abort
183
+ ```
184
+
185
+ Then rerun the `happys monorepo port ... --onto-current ...` command.
186
+
187
+ ### 6) Port the CLI commits (old `happy-cli` → `cli/`)
188
+
189
+ After the UI port completes, port CLI commits onto the same branch:
190
+
191
+ ```bash
192
+ happys monorepo port \
193
+ --target=/abs/path/to/monorepo-root \
194
+ --onto-current \
195
+ --3way \
196
+ --from-happy-cli=/abs/path/to/old-happy-cli-repo \
197
+ --from-happy-cli-base=upstream/main
198
+ ```
199
+
200
+ Resolve conflicts the same way (`git am --continue`).
201
+
202
+ ### 7) Verify what landed
203
+
204
+ Quick sanity checks:
205
+
206
+ ```bash
207
+ # ensure upstream/main is an ancestor of your branch
208
+ git merge-base --is-ancestor upstream/main HEAD
209
+
210
+ # list commits introduced by the port
211
+ git log --oneline upstream/main..HEAD
212
+ ```
213
+
214
+ If you are porting multiple legacy branches, it’s often useful to compare counts:
215
+
216
+ ```bash
217
+ git rev-list --count upstream/main..HEAD
218
+ ```
219
+
220
+ ### 8) Push and open a PR
221
+
222
+ Push to the remote you intend to PR against (example uses `upstream`):
223
+
224
+ ```bash
225
+ git push upstream HEAD:<branch-name>
226
+ ```
227
+
228
+ ## Common failures (expected) and what to do
229
+
230
+ ### “target repo is not clean”
231
+
232
+ `happys monorepo port` refuses to run when the target has local changes.
233
+
234
+ Fix: commit, stash, or reset your target worktree, then re-run.
235
+
236
+ ### “a git am operation is already in progress”
237
+
238
+ You have an unfinished `git am` session from a previous attempt.
239
+
240
+ Fix it first:
241
+
242
+ ```bash
243
+ git am --continue # after resolving conflicts
244
+ # or
245
+ git am --abort
246
+ ```
247
+
248
+ Then re-run `happys monorepo port ...`.
249
+
250
+ ### “patch does not apply” / i18n churn / renamed files
251
+
252
+ This usually means upstream moved and the patch context no longer matches.
253
+
254
+ Recommended: rerun with `--3way` (3-way merge) so you get conflict markers to resolve.
255
+
256
+ ### “already exists in working directory”
257
+
258
+ This often happens when a commit (especially “new file”) was already folded into the monorepo history.
259
+
260
+ The tool auto-skips:
261
+ - patches that are already present (exact-match reverse apply check)
262
+ - pure new-file patches when the target already contains identical content
263
+
264
+ If you still hit this manually during a stopped `git am`, decide whether to:
265
+ - keep the existing file and `git am --skip`, or
266
+ - reconcile content and continue.
267
+
268
+ ### Missing file path errors
269
+
270
+ If the patch references files that no longer exist (or moved) in the monorepo, you’ll need to:
271
+ - map the change to the new file location, or
272
+ - skip that patch if it’s obsolete.
273
+
274
+ Use `git am --show-current-patch=diff` to understand intent, then implement the equivalent change in the monorepo layout and continue.
275
+
276
+ ## Optional: audit mode (best-effort report)
277
+
278
+ If you want a full report of what would apply vs fail (without stopping at the first conflict), you can run:
279
+
280
+ ```bash
281
+ happys monorepo port ... --continue-on-failure --json
282
+ ```
283
+
284
+ This is **not** the recommended way to produce a final clean branch, but it can be useful to:
285
+ - discover the full set of expected conflicts
286
+ - share a machine-readable report for assistance
@@ -4,7 +4,21 @@ Happy Stacks supports two server “flavors”. You can switch between them glob
4
4
 
5
5
  ## What’s the difference?
6
6
 
7
- Both are forks/flavors of the same upstream server repo (`slopus/happy-server`), but optimized for different use cases:
7
+ Both are forks/flavors of the same upstream server repo (`slopus/happy-server`), but optimized for different use cases.
8
+
9
+ ### Unified codebase (recommended)
10
+
11
+ When your `happy-server` checkout includes the light flavor artifacts (notably `prisma/sqlite/schema.prisma` — legacy: `prisma/schema.sqlite.prisma`), Happy Stacks treats it as a **single unified server codebase** that supports both:
12
+
13
+ - `happy-server` (full / Postgres+Redis+S3)
14
+ - `happy-server-light` (light / SQLite+local files, can serve UI)
15
+
16
+ In that setup:
17
+
18
+ - there is **no server code duplication**
19
+ - `happy-server-light` can point at the **same checkout/worktree** as `happy-server`
20
+ - `happys stack new` will default to pinning **both** server component dirs to the same path
21
+ - `happys start/dev --server=happy-server-light` will run `start:light` / `dev:light` when available
8
22
 
9
23
  - **`happy-server-light`** (recommended default)
10
24
  - optimized for local usage
@@ -73,7 +87,7 @@ Notes:
73
87
  - **`happys start`** is “production-like”. It avoids running heavyweight schema sync loops under launchd KeepAlive.
74
88
  - **`happys dev`** is for rapid iteration:
75
89
  - for `happy-server`: Happy Stacks runs `prisma migrate deploy` by default (configurable via `HAPPY_STACKS_PRISMA_MIGRATE`).
76
- - for `happy-server-light`: the upstream dev script runs `prisma db push` by default (configurable via `HAPPY_STACKS_PRISMA_PUSH`).
90
+ - for `happy-server-light`: Happy Stacks runs `prisma migrate deploy` (SQLite migrations) using the unified schema under `prisma/sqlite/schema.prisma`.
77
91
 
78
92
  Important: for a given run (`happys start` / `happys dev`) you choose **one** flavor.
79
93
 
@@ -125,7 +139,9 @@ There are two separate concepts:
125
139
  - controlled by `HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT` and `HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER`
126
140
  - easiest via `happys wt use happy-server-light ...` / `happys wt use happy-server ...`
127
141
 
128
- If you set `HAPPY_STACKS_SERVER_COMPONENT=happy-server-light` but accidentally point the *server-light component dir* at a `happy-server` worktree (or vice versa), `happys start/dev/doctor` will refuse to run and print a fix hint.
142
+ If you set `HAPPY_STACKS_SERVER_COMPONENT=happy-server-light` but accidentally point the *server-light component dir* at a postgres-only `happy-server` checkout (or vice versa), `happys start/dev/doctor` will refuse to run and print a fix hint.
143
+
144
+ In unified mode (same repo supports both flavors), it is valid (and recommended) for both component dirs to point at the same checkout.
129
145
 
130
146
  `happys wt use` also prevents the most common mismatch when selecting server worktrees inside `components/` / `components/.worktrees/`.
131
147
 
package/docs/stacks.md CHANGED
@@ -238,6 +238,41 @@ Cloned-repo fallback (before you run `happys init`):
238
238
  2. `<repo>/env.local` (optional overrides)
239
239
  3. `HAPPY_STACKS_ENV_FILE` (stack env)
240
240
 
241
+ ## Manage per-stack environment variables (including API keys)
242
+
243
+ To add/update environment variables in a stack env file from the CLI:
244
+
245
+ ```bash
246
+ happys stack env <stack> set KEY=VALUE [KEY2=VALUE2...]
247
+ ```
248
+
249
+ To remove keys:
250
+
251
+ ```bash
252
+ happys stack env <stack> unset KEY [KEY2...]
253
+ ```
254
+
255
+ To inspect:
256
+
257
+ ```bash
258
+ happys stack env <stack> get KEY
259
+ happys stack env <stack> list
260
+ happys stack env <stack> path
261
+ ```
262
+
263
+ Notes:
264
+
265
+ - This is the recommended place for **provider API keys** the daemon needs (example: `OPENAI_API_KEY`).
266
+ - Changes apply on the **next start** of the stack/daemon. Restart to pick them up:
267
+ - `main`: `happys start --restart`
268
+ - named stack: `happys stack start <stack> -- --restart` (or `happys stack dev <stack> -- --restart`)
269
+
270
+ Self-host shortcut (defaults to `main` when not running under a stack wrapper):
271
+
272
+ ```bash
273
+ happys env set OPENAI_API_KEY=sk-...
274
+ ```
275
+
241
276
  ## Daemon auth + “no machine” on first run
242
277
 
243
278
  On a **fresh machine** (or any new stack), the daemon may need to authenticate before it can register a “machine”.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "happy-stacks",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "packageManager": "pnpm@10.18.3",
6
6
  "bin": {
7
7
  "happys": "./bin/happys.mjs",
@@ -55,5 +55,9 @@
55
55
  "menubar:install": "node ./scripts/menubar.mjs install",
56
56
  "menubar:uninstall": "node ./scripts/menubar.mjs uninstall",
57
57
  "menubar:open": "bash -lc 'DIR=\"$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null)\"; if [[ -z \"$DIR\" ]]; then DIR=\"$HOME/Library/Application Support/SwiftBar/Plugins\"; fi; open \"$DIR\"'"
58
+ },
59
+ "dependencies": {
60
+ "qrcode": "^1.5.4",
61
+ "qrcode-terminal": "^0.12.0"
58
62
  }
59
63
  }
package/scripts/auth.mjs CHANGED
@@ -16,6 +16,7 @@ import { dirname } from 'node:path';
16
16
  import { parseEnvToObject } from './utils/env/dotenv.mjs';
17
17
  import { ensureDepsInstalled, pmExecBin } from './utils/proc/pm.mjs';
18
18
  import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
19
+ import { resolvePrismaClientImportForServerComponent, resolveServerLightPrismaMigrateDeployArgs } from './utils/server/flavor_scripts.mjs';
19
20
  import { clearDevAuthKey, readDevAuthKey, writeDevAuthKey } from './utils/auth/dev_key.mjs';
20
21
  import { getExpoStatePaths, isStateProcessRunning } from './utils/expo/expo.mjs';
21
22
  import { resolveAuthSeedFromEnv } from './utils/stack/startup.mjs';
@@ -32,7 +33,7 @@ import {
32
33
  getServerLightDataDirFromEnvOrDefault,
33
34
  resolveCliHomeDir,
34
35
  } from './utils/stack/dirs.mjs';
35
- import { resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
36
+ import { resolveLocalhostHost, preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
36
37
 
37
38
  function getInternalServerUrlCompat() {
38
39
  const { port, internalServerUrl } = getInternalServerUrl({ env: process.env, defaultPort: 3005 });
@@ -45,9 +46,9 @@ async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
45
46
  const uiDir = getComponentDir(rootDir, 'happy');
46
47
  const uiPaths = getExpoStatePaths({
47
48
  baseDir,
48
- kind: 'ui-dev',
49
+ kind: 'expo-dev',
49
50
  projectDir: uiDir,
50
- stateFileName: 'ui.state.json',
51
+ stateFileName: 'expo.state.json',
51
52
  });
52
53
  const uiRunning = await isStateProcessRunning(uiPaths.statePath);
53
54
  if (!uiRunning.running) return null;
@@ -227,6 +228,15 @@ async function seedAccountsFromSourceDbToTargetDb({
227
228
  const sourceCwd = resolveServerComponentDir({ rootDir, serverComponent: fromServerComponent });
228
229
  const targetCwd = resolveServerComponentDir({ rootDir, serverComponent: targetServerComponent });
229
230
 
231
+ const sourceClientImport = resolvePrismaClientImportForServerComponent({
232
+ serverComponentName: fromServerComponent,
233
+ serverDir: sourceCwd,
234
+ });
235
+ const targetClientImport = resolvePrismaClientImportForServerComponent({
236
+ serverComponentName: targetServerComponent,
237
+ serverDir: targetCwd,
238
+ });
239
+
230
240
  const listScript = `
231
241
  process.on('uncaughtException', (e) => {
232
242
  console.error(e instanceof Error ? e.message : String(e));
@@ -236,7 +246,11 @@ process.on('unhandledRejection', (e) => {
236
246
  console.error(e instanceof Error ? e.message : String(e));
237
247
  process.exit(1);
238
248
  });
239
- import { PrismaClient } from '@prisma/client';
249
+ const mod = await import(${JSON.stringify(sourceClientImport)});
250
+ const PrismaClient = mod?.PrismaClient ?? mod?.default?.PrismaClient;
251
+ if (!PrismaClient) {
252
+ throw new Error('Failed to load PrismaClient for DB seed (source).');
253
+ }
240
254
  const db = new PrismaClient();
241
255
  try {
242
256
  const accounts = await db.account.findMany({ select: { id: true, publicKey: true } });
@@ -255,7 +269,11 @@ process.on('unhandledRejection', (e) => {
255
269
  console.error(e instanceof Error ? e.message : String(e));
256
270
  process.exit(1);
257
271
  });
258
- import { PrismaClient } from '@prisma/client';
272
+ const mod = await import(${JSON.stringify(targetClientImport)});
273
+ const PrismaClient = mod?.PrismaClient ?? mod?.default?.PrismaClient;
274
+ if (!PrismaClient) {
275
+ throw new Error('Failed to load PrismaClient for DB seed (target).');
276
+ }
259
277
  import fs from 'node:fs';
260
278
  const FORCE = ${force ? 'true' : 'false'};
261
279
  const raw = fs.readFileSync(0, 'utf8').trim();
@@ -580,7 +598,7 @@ async function cmdCopyFrom({ argv, json }) {
580
598
  const managed = (targetEnv.HAPPY_STACKS_MANAGED_INFRA ?? targetEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1').toString().trim() !== '0';
581
599
  if (targetServerComponent === 'happy-server' && withInfra && managed) {
582
600
  const { port } = getInternalServerUrlCompat();
583
- const publicServerUrl = `http://localhost:${port}`;
601
+ const publicServerUrl = await preferStackLocalhostUrl(`http://localhost:${port}`, { stackName });
584
602
  const envPath = resolveStackEnvPath(stackName).envPath;
585
603
  const infra = await ensureHappyServerManagedInfra({
586
604
  stackName,
@@ -633,7 +651,7 @@ async function cmdCopyFrom({ argv, json }) {
633
651
  await pmExecBin({
634
652
  dir: serverDirForPrisma,
635
653
  bin: 'prisma',
636
- args: ['db', 'push'],
654
+ args: resolveServerLightPrismaMigrateDeployArgs({ serverDir: serverDirForPrisma }),
637
655
  env: { ...process.env, DATABASE_URL: targetDatabaseUrl },
638
656
  quiet: json,
639
657
  }).catch(() => {});
@@ -689,6 +707,7 @@ async function cmdStatus({ json }) {
689
707
  defaultPublicUrl,
690
708
  envPublicUrl,
691
709
  allowEnable: false,
710
+ stackName,
692
711
  });
693
712
 
694
713
  const cliHomeDir = resolveCliHomeDir();
@@ -770,7 +789,7 @@ async function cmdStatus({ json }) {
770
789
  async function cmdLogin({ argv, json }) {
771
790
  const rootDir = getRootDir(import.meta.url);
772
791
  const stackName = getStackName();
773
- const { kv } = parseArgs(argv);
792
+ const { flags, kv } = parseArgs(argv);
774
793
 
775
794
  const { port, url: internalServerUrl } = getInternalServerUrlCompat();
776
795
  const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
@@ -779,10 +798,12 @@ async function cmdLogin({ argv, json }) {
779
798
  defaultPublicUrl,
780
799
  envPublicUrl,
781
800
  allowEnable: false,
801
+ stackName,
782
802
  });
783
803
  const { envWebappUrl } = getWebappUrlEnvOverride({ env: process.env, stackName });
784
804
  const expoWebappUrl = await resolveWebappUrlFromRunningExpo({ rootDir, stackName });
785
- const webappUrl = envWebappUrl || expoWebappUrl || publicServerUrl;
805
+ const webappUrlRaw = envWebappUrl || expoWebappUrl || publicServerUrl;
806
+ const webappUrl = await preferStackLocalhostUrl(webappUrlRaw, { stackName });
786
807
  const webappUrlSource = expoWebappUrl ? 'expo' : envWebappUrl ? 'stack env override' : 'server';
787
808
 
788
809
  const cliHomeDir = resolveCliHomeDir();
@@ -818,7 +839,8 @@ async function cmdLogin({ argv, json }) {
818
839
  return;
819
840
  }
820
841
 
821
- if (!json) {
842
+ const quietUx = flags.has('--quiet') || flags.has('--no-ux');
843
+ if (!json && !quietUx) {
822
844
  printAuthLoginInstructions({
823
845
  stackName,
824
846
  context,
package/scripts/build.mjs CHANGED
@@ -1,18 +1,21 @@
1
1
  import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
3
+ import { componentDirEnvKey, getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
4
4
  import { ensureDepsInstalled, pmExecBin, requireDir } from './utils/proc/pm.mjs';
5
5
  import { resolveServerPortFromEnv } from './utils/server/urls.mjs';
6
6
  import { dirname, join } from 'node:path';
7
7
  import { readFile, rm, mkdir, writeFile } from 'node:fs/promises';
8
8
  import { tailscaleServeHttpsUrl } from './tailscale.mjs';
9
9
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
10
+ import { ensureExpoIsolationEnv, getExpoStatePaths, wantsExpoClearCache } from './utils/expo/expo.mjs';
11
+ import { expoExec } from './utils/expo/command.mjs';
12
+ import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
10
13
 
11
14
  /**
12
15
  * Build a lightweight static web UI bundle (no Expo dev server).
13
16
  *
14
17
  * Output directory default: ~/.happy/stacks/main/ui (legacy: ~/.happy/local/ui)
15
- * Server will serve it at / when HAPPY_SERVER_LIGHT_UI_DIR is set.
18
+ * Server will serve it at / when HAPPY_SERVER_UI_DIR is set.
16
19
  * (Legacy /ui paths are redirected to /.)
17
20
  */
18
21
 
@@ -29,12 +32,29 @@ async function main() {
29
32
  ' happys build [--tauri] [--json]',
30
33
  ' (legacy in a cloned repo): pnpm build [-- --tauri] [--json]',
31
34
  ' node scripts/build.mjs [--tauri|--no-tauri] [--no-ui] [--json]',
35
+ '',
36
+ 'note:',
37
+ ' If run from inside the Happy UI checkout/worktree, the build uses that checkout.',
32
38
  ].join('\n'),
33
39
  });
34
40
  return;
35
41
  }
36
42
  const rootDir = getRootDir(import.meta.url);
37
43
 
44
+ // If invoked from inside the Happy UI checkout/worktree, prefer that directory without requiring `happys wt use ...`.
45
+ const inferred = inferComponentFromCwd({
46
+ rootDir,
47
+ invokedCwd: getInvokedCwd(process.env),
48
+ components: ['happy'],
49
+ });
50
+ if (inferred?.component === 'happy') {
51
+ const stacksKey = componentDirEnvKey('happy');
52
+ const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
53
+ if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
54
+ process.env[stacksKey] = inferred.repoDir;
55
+ }
56
+ }
57
+
38
58
  // Optional: skip building the web UI bundle.
39
59
  //
40
60
  // This is useful for evidence capture flows that validate non-UI components (e.g. `happy-cli`)
@@ -87,7 +107,17 @@ async function main() {
87
107
  };
88
108
 
89
109
  // Expo CLI is available via node_modules/.bin once dependencies are installed.
90
- await pmExecBin({ dir: uiDir, bin: 'expo', args: ['export', '--platform', 'web', '--output-dir', outDir], env });
110
+ {
111
+ const paths = getExpoStatePaths({
112
+ baseDir: getDefaultAutostartPaths().baseDir,
113
+ kind: 'ui-export',
114
+ projectDir: uiDir,
115
+ stateFileName: 'ui.export.state.json',
116
+ });
117
+ await ensureExpoIsolationEnv({ env, stateDir: paths.stateDir, expoHomeDir: paths.expoHomeDir, tmpDir: paths.tmpDir });
118
+ const args = ['export', '--platform', 'web', '--output-dir', outDir, ...(wantsExpoClearCache({ env }) ? ['-c'] : [])];
119
+ await expoExec({ dir: uiDir, args, env, ensureDepsLabel: 'happy' });
120
+ }
91
121
 
92
122
  if (json) {
93
123
  printResult({ json, data: { ok: true, outDir, tauriBuilt: false } });
@@ -146,13 +176,30 @@ async function main() {
146
176
  };
147
177
  delete tauriEnv.EXPO_PUBLIC_WEB_BASE_URL;
148
178
 
149
- await pmExecBin({
179
+ {
180
+ const paths = getExpoStatePaths({
181
+ baseDir: getDefaultAutostartPaths().baseDir,
182
+ kind: 'ui-export-tauri',
183
+ projectDir: uiDir,
184
+ stateFileName: 'ui.export.tauri.state.json',
185
+ });
186
+ await ensureExpoIsolationEnv({ env: tauriEnv, stateDir: paths.stateDir, expoHomeDir: paths.expoHomeDir, tmpDir: paths.tmpDir });
187
+ }
188
+
189
+ await expoExec({
150
190
  dir: uiDir,
151
- bin: 'expo',
152
- // Important: clear bundler cache so EXPO_PUBLIC_* inlining doesn't reuse
153
- // the previous (web) export's transform results.
154
- args: ['export', '--platform', 'web', '--output-dir', tauriDistDir, '-c'],
191
+ args: [
192
+ 'export',
193
+ '--platform',
194
+ 'web',
195
+ '--output-dir',
196
+ tauriDistDir,
197
+ // Important: clear bundler cache so EXPO_PUBLIC_* inlining doesn't reuse
198
+ // the previous (web) export's transform results.
199
+ '-c',
200
+ ],
155
201
  env: tauriEnv,
202
+ ensureDepsLabel: 'happy',
156
203
  });
157
204
 
158
205
  // Build the Tauri app using a generated config that skips upstream beforeBuildCommand (which uses yarn).