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.
- package/README.md +93 -40
- package/bin/happys.mjs +158 -16
- package/docs/codex-mcp-resume.md +130 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
- package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
- package/docs/happy-development.md +3 -4
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +5 -1
- package/scripts/auth.mjs +32 -10
- package/scripts/build.mjs +55 -8
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +198 -50
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +6 -4
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +8 -3
- package/scripts/install.mjs +207 -69
- package/scripts/lint.mjs +24 -4
- package/scripts/migrate.mjs +3 -12
- package/scripts/mobile.mjs +88 -104
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +908 -0
- package/scripts/review_pr.mjs +353 -0
- package/scripts/run.mjs +101 -21
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +189 -68
- package/scripts/setup_pr.mjs +586 -38
- package/scripts/stack.mjs +990 -196
- package/scripts/stack_archive_cmd.test.mjs +91 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
- package/scripts/stack_env_cmd.test.mjs +87 -0
- package/scripts/stack_happy_cmd.test.mjs +126 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
- package/scripts/stack_monorepo_defaults.test.mjs +62 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
- package/scripts/stack_shorthand_cmd.test.mjs +55 -0
- package/scripts/stack_wt_list.test.mjs +128 -0
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +395 -39
- package/scripts/typecheck.mjs +24 -4
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/auth/login_ux.mjs +32 -13
- package/scripts/utils/auth/sources.mjs +26 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +43 -4
- package/scripts/utils/cli/cwd_scope.mjs +136 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +75 -0
- package/scripts/utils/cli/prereqs.test.mjs +34 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +17 -9
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
- package/scripts/utils/dev/daemon.mjs +61 -4
- package/scripts/utils/dev/expo_dev.mjs +430 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/dev/server.mjs +36 -42
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/edison/git_roots.mjs +29 -0
- package/scripts/utils/edison/git_roots.test.mjs +36 -0
- package/scripts/utils/env/env.mjs +7 -3
- package/scripts/utils/env/env_file.mjs +4 -2
- package/scripts/utils/env/env_file.test.mjs +44 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/expo/expo.mjs +20 -1
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/worktrees.mjs +80 -25
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/mobile/config.mjs +31 -0
- package/scripts/utils/mobile/dev_client_links.mjs +60 -0
- package/scripts/utils/mobile/identifiers.mjs +47 -0
- package/scripts/utils/mobile/identifiers.test.mjs +42 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +9 -1
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +50 -3
- package/scripts/utils/paths/paths.mjs +159 -40
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
- package/scripts/utils/proc/commands.mjs +2 -3
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +176 -22
- package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
- package/scripts/utils/proc/proc.mjs +136 -4
- package/scripts/utils/proc/proc.test.mjs +77 -0
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +51 -0
- package/scripts/utils/review/findings.mjs +165 -0
- package/scripts/utils/review/findings.test.mjs +85 -0
- package/scripts/utils/review/head_slice.mjs +153 -0
- package/scripts/utils/review/head_slice.test.mjs +91 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/runners/coderabbit.mjs +61 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +61 -0
- package/scripts/utils/review/runners/codex.test.mjs +35 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +32 -0
- package/scripts/utils/review/targets.mjs +24 -0
- package/scripts/utils/review/targets.test.mjs +36 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
- package/scripts/utils/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
- package/scripts/utils/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/prisma_import.mjs +37 -0
- package/scripts/utils/server/prisma_import.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +14 -0
- package/scripts/utils/server/ui_env.test.mjs +46 -0
- package/scripts/utils/server/urls.mjs +14 -4
- package/scripts/utils/server/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/service/autostart_darwin.mjs +42 -2
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +2 -2
- package/scripts/utils/stack/editor_workspace.mjs +6 -6
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +2 -1
- package/scripts/utils/stack/startup.mjs +120 -13
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
- package/scripts/utils/stack/stop.mjs +15 -4
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +755 -179
- package/scripts/worktrees_archive_cmd.test.mjs +245 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
- 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
|
package/docs/server-flavors.md
CHANGED
|
@@ -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`:
|
|
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`
|
|
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.
|
|
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: '
|
|
49
|
+
kind: 'expo-dev',
|
|
49
50
|
projectDir: uiDir,
|
|
50
|
-
stateFileName: '
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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).
|