happy-stacks 0.2.0 → 0.4.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 +84 -25
- package/bin/happys.mjs +116 -17
- package/docs/happy-development.md +2 -2
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/package.json +5 -1
- package/scripts/auth.mjs +59 -208
- package/scripts/build.mjs +58 -12
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +168 -20
- package/scripts/dev.mjs +196 -70
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +105 -78
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +8 -14
- package/scripts/install.mjs +119 -23
- package/scripts/lint.mjs +31 -32
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +93 -108
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +217 -0
- package/scripts/review_pr.mjs +368 -0
- package/scripts/run.mjs +95 -21
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +19 -29
- package/scripts/setup.mjs +63 -160
- package/scripts/setup_pr.mjs +592 -52
- package/scripts/stack.mjs +608 -200
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +44 -11
- package/scripts/test.mjs +52 -36
- package/scripts/tui.mjs +314 -74
- package/scripts/typecheck.mjs +31 -32
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- 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/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
- package/scripts/utils/auth/sources.mjs +38 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +24 -0
- package/scripts/utils/cli/cwd_scope.mjs +82 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
- package/scripts/utils/dev/expo_dev.mjs +246 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
- package/scripts/utils/dev_auth_key.mjs +1 -1
- package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
- package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
- package/scripts/utils/handy_master_secret.mjs +2 -2
- 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/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +56 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/runners/coderabbit.mjs +19 -0
- package/scripts/utils/review/runners/codex.mjs +51 -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/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- 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/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +101 -0
- package/scripts/utils/server/validate.mjs +88 -0
- package/scripts/utils/service/autostart_darwin.mjs +182 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +23 -0
- package/scripts/utils/stack/dirs.mjs +27 -0
- package/scripts/utils/stack/editor_workspace.mjs +152 -0
- package/scripts/utils/stack/names.mjs +12 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
- package/scripts/utils/stack_context.mjs +3 -3
- package/scripts/utils/stack_runtime_state.mjs +1 -1
- package/scripts/utils/stacks.mjs +2 -2
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/validate.mjs +1 -1
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +171 -113
- package/scripts/utils/auth_sources.mjs +0 -12
- package/scripts/utils/dev_expo_web.mjs +0 -112
- package/scripts/utils/localhost_host.mjs +0 -17
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
- /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
- /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/docs/mobile-ios.md
CHANGED
|
@@ -8,101 +8,166 @@ see the “Using Happy from your phone” section in the main README.
|
|
|
8
8
|
- Xcode installed
|
|
9
9
|
- CocoaPods installed (`brew install cocoapods`)
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Two supported modes
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- **Shared dev-client app** (recommended for development):
|
|
14
|
+
- Install *one* “Happy Stacks Dev” app on your phone.
|
|
15
|
+
- Run any stack with `--mobile`; scan the QR to open that stack inside the dev-client.
|
|
16
|
+
- Per-stack auth/storage is isolated via `EXPO_PUBLIC_HAPPY_STORAGE_SCOPE` (set automatically in stack mode).
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
- **Per-stack “release” app** (recommended for demos / strict isolation):
|
|
19
|
+
- Install a separate iOS app per stack (unique bundle id + scheme).
|
|
20
|
+
- Each stack app is isolated by iOS app container (no token collisions).
|
|
21
|
+
|
|
22
|
+
## Shared dev-client app (install once)
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
Install the dedicated Happy Stacks dev-client app on your iPhone (USB).
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
This command **runs a prebuild** (generates `ios/` + runs CocoaPods) and then installs a Debug build
|
|
27
|
+
without starting Metro:
|
|
22
28
|
|
|
23
29
|
```bash
|
|
24
|
-
happys mobile
|
|
30
|
+
happys mobile-dev-client --install
|
|
25
31
|
```
|
|
26
32
|
|
|
27
|
-
-
|
|
33
|
+
If you want to ensure the dev-client is built from a specific stack’s active `happy` worktree
|
|
34
|
+
(e.g. to include upstream changes that aren’t merged into your default checkout yet), run:
|
|
28
35
|
|
|
29
36
|
```bash
|
|
30
|
-
happys mobile
|
|
37
|
+
happys stack mobile-dev-client <stack> --install
|
|
31
38
|
```
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
Optional:
|
|
34
41
|
|
|
35
42
|
```bash
|
|
36
|
-
happys mobile --
|
|
43
|
+
happys mobile-dev-client --install --device="Your iPhone"
|
|
44
|
+
happys mobile-dev-client --install --clean
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
Then run any stack with mobile enabled:
|
|
40
48
|
|
|
41
49
|
```bash
|
|
42
|
-
happys mobile
|
|
50
|
+
happys stack dev <stack> --mobile
|
|
51
|
+
# or:
|
|
52
|
+
happys dev --mobile
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
Notes:
|
|
56
|
+
|
|
57
|
+
- **LAN requirement**: for physical iPhones, Metro must be reachable over LAN.
|
|
58
|
+
- Happy Stacks defaults to `lan` for mobile, and will print a QR code + deep link.
|
|
59
|
+
- For simulators you can usually use `localhost` (see `HAPPY_STACKS_MOBILE_HOST` below).
|
|
60
|
+
- **If Expo is already running in web-only mode**: re-run with `--restart` and include `--mobile`.
|
|
61
|
+
|
|
62
|
+
## Per-stack app install (isolated)
|
|
63
|
+
|
|
64
|
+
Install an isolated app for a specific stack (unique bundle id + scheme, Release config, no Metro):
|
|
46
65
|
|
|
47
66
|
```bash
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
happys stack mobile:install <stack> --name="Happy (<stack>)"
|
|
68
|
+
happys stack mobile:install <stack> --name="Happy PR 272" --device="Your iPhone"
|
|
50
69
|
```
|
|
51
70
|
|
|
52
|
-
|
|
71
|
+
The chosen app name is persisted in the stack env so you can re-run installs without re-typing it.
|
|
72
|
+
|
|
73
|
+
## Native iOS regeneration / “prebuild” (critical)
|
|
74
|
+
|
|
75
|
+
You’ll need to regenerate the iOS native project + Pods when:
|
|
76
|
+
|
|
77
|
+
- you pull changes that affect native deps / Expo config
|
|
78
|
+
- `components/happy/ios/` was deleted
|
|
79
|
+
- you hit CocoaPods / deployment-target mismatches after a dependency bump
|
|
53
80
|
|
|
54
|
-
|
|
81
|
+
Run:
|
|
55
82
|
|
|
56
83
|
```bash
|
|
57
|
-
happys mobile
|
|
84
|
+
happys mobile --prebuild
|
|
85
|
+
# (optional) fully regenerate ios/:
|
|
86
|
+
happys mobile --prebuild --clean
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
|
|
89
|
+
What this does today:
|
|
61
90
|
|
|
62
|
-
-
|
|
91
|
+
- runs `expo prebuild --no-install` (so we can patch before CocoaPods runs)
|
|
92
|
+
- patches `ios/Podfile.properties.json` to:
|
|
93
|
+
- set `ios.deploymentTarget` to `16.0`
|
|
94
|
+
- set `ios.buildReactNativeFromSource` to `true`
|
|
95
|
+
- patches the generated Xcode project deployment target (where applicable)
|
|
96
|
+
- runs `pod install`
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
|
|
100
|
+
- **You usually don’t need to run this manually** because both:
|
|
101
|
+
- `happys mobile-dev-client --install`
|
|
102
|
+
- `happys stack mobile:install <stack>`
|
|
103
|
+
already include `--prebuild`.
|
|
104
|
+
- Legacy alias: `happys mobile:prebuild` exists (hidden), but prefer `happys mobile --prebuild`.
|
|
105
|
+
|
|
106
|
+
## Manual `happys mobile` usage (advanced)
|
|
107
|
+
|
|
108
|
+
If you want to work on the embedded Expo app directly (outside `happys dev --mobile`), `happys mobile` supports:
|
|
63
109
|
|
|
64
110
|
```bash
|
|
65
|
-
|
|
111
|
+
# Start Metro (keeps running):
|
|
112
|
+
happys mobile --host=lan
|
|
113
|
+
|
|
114
|
+
# Build + install on iOS (and exit). If you omit --device, it will try to auto-pick a connected iPhone over USB:
|
|
115
|
+
happys mobile --prebuild --run-ios --device="Your iPhone"
|
|
116
|
+
happys mobile --prebuild --run-ios --configuration=Release --no-metro
|
|
66
117
|
```
|
|
67
118
|
|
|
68
|
-
|
|
119
|
+
## Notes / troubleshooting
|
|
120
|
+
|
|
121
|
+
- **QR opens the wrong app**:
|
|
122
|
+
- The dev-client QR uses the `HAPPY_STACKS_DEV_CLIENT_SCHEME` (default: `happystacks-dev`).
|
|
123
|
+
- Per-stack installs use a different per-stack scheme, so they should not intercept dev-client QR scans.
|
|
124
|
+
|
|
125
|
+
- **List connected devices** (for `--device=`):
|
|
69
126
|
|
|
70
127
|
```bash
|
|
71
|
-
happys mobile
|
|
128
|
+
happys mobile:devices
|
|
72
129
|
```
|
|
73
130
|
|
|
74
|
-
|
|
131
|
+
- **Code signing weirdness on a real iPhone**:
|
|
132
|
+
- Happy Stacks will try to “un-pin” signing fields in the generated `.pbxproj` so Expo/Xcode can reconfigure signing
|
|
133
|
+
(this avoids failures where automatic signing is disabled because `DEVELOPMENT_TEAM`/profiles were pinned).
|
|
134
|
+
- If you want to manage signing manually, pass `--no-signing-fix` to `happys mobile ...` / `happys stack mobile <stack> ...`.
|
|
75
135
|
|
|
76
136
|
## Bake the default server URL into the app (optional)
|
|
77
137
|
|
|
78
138
|
If you want the built app to default to your happy-stacks server URL, set this **when building**:
|
|
79
139
|
|
|
80
140
|
```bash
|
|
81
|
-
HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net" happys mobile
|
|
141
|
+
HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net" happys mobile-dev-client --install
|
|
82
142
|
```
|
|
83
143
|
|
|
84
|
-
Note: changing `HAPPY_STACKS_SERVER_URL` requires rebuilding/reinstalling the
|
|
144
|
+
Note: changing `HAPPY_STACKS_SERVER_URL` requires rebuilding/reinstalling the app you care about.
|
|
85
145
|
|
|
86
|
-
|
|
146
|
+
Important:
|
|
87
147
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
- For **non-main stacks**, `HAPPY_STACKS_SERVER_URL` is only respected if it’s set **in that stack’s env file**
|
|
149
|
+
(safety: we ignore “global” URLs for non-main stacks to avoid accidentally repointing other stacks).
|
|
150
|
+
|
|
151
|
+
## Customizing the app identity (optional / advanced)
|
|
152
|
+
|
|
153
|
+
Happy Stacks uses these identities:
|
|
91
154
|
|
|
92
|
-
|
|
155
|
+
- **Dev-client**: defaults to `Happy Stacks Dev` + bundle id `com.happystacks.dev.<user>`
|
|
156
|
+
- **Per-stack release**: defaults to `Happy (<stack>)` + bundle id `com.happystacks.stack.<user>.<stack>`
|
|
157
|
+
|
|
158
|
+
If you want to build/install *manually* (instead of `mobile-dev-client` / `stack mobile:install`), you can override:
|
|
93
159
|
|
|
94
160
|
- **Bundle identifier (recommended for real iPhones)**:
|
|
95
|
-
- You may
|
|
161
|
+
- You may need this if the bundle id you’re using isn’t available/owned by your Apple team.
|
|
96
162
|
|
|
97
163
|
```bash
|
|
98
|
-
HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile --run-ios
|
|
99
|
-
HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile:install
|
|
164
|
+
HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile --prebuild --run-ios --no-metro
|
|
100
165
|
```
|
|
101
166
|
|
|
102
167
|
- **App name (what shows on the home screen)**:
|
|
103
168
|
|
|
104
169
|
```bash
|
|
105
|
-
HAPPY_STACKS_IOS_APP_NAME="Happy Local" happys mobile
|
|
170
|
+
HAPPY_STACKS_IOS_APP_NAME="Happy Local" happys mobile --prebuild --run-ios --no-metro
|
|
106
171
|
```
|
|
107
172
|
|
|
108
173
|
## Suggested env (recommended)
|
|
@@ -110,25 +175,18 @@ HAPPY_STACKS_IOS_APP_NAME="Happy Local" happys mobile:install
|
|
|
110
175
|
Add these to your main stack env file (`~/.happy/stacks/main/env`) (or `~/.happy-stacks/env.local` for global overrides) so you don’t have to prefix every command:
|
|
111
176
|
|
|
112
177
|
```bash
|
|
113
|
-
#
|
|
114
|
-
|
|
178
|
+
# How the phone reaches Metro:
|
|
179
|
+
# - lan: recommended for real devices
|
|
180
|
+
# - localhost: OK for simulators
|
|
181
|
+
HAPPY_STACKS_MOBILE_HOST="lan"
|
|
115
182
|
|
|
116
|
-
#
|
|
117
|
-
|
|
183
|
+
# (optional) default scheme used in the dev-client QR / deep link
|
|
184
|
+
# (must match your installed dev-client app):
|
|
185
|
+
HAPPY_STACKS_DEV_CLIENT_SCHEME="happystacks-dev"
|
|
186
|
+
|
|
187
|
+
# Default public server URL for the stack (baked into the Expo app config):
|
|
188
|
+
HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net"
|
|
118
189
|
|
|
119
190
|
# Optional: home screen name:
|
|
120
191
|
HAPPY_STACKS_IOS_APP_NAME="Happy Local"
|
|
121
192
|
```
|
|
122
|
-
|
|
123
|
-
## Personal build on iPhone (EAS internal distribution)
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
cd "$HOME/.happy-stacks/workspace/components/happy"
|
|
127
|
-
eas build --profile development --platform ios
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Then keep Metro running from `happy-stacks`:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
happys mobile --host=lan
|
|
134
|
-
```
|
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.4.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
|
@@ -1,170 +1,68 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir, getStackName, resolveStackEnvPath } from './utils/paths.mjs';
|
|
5
|
-
import { listAllStackNames } from './utils/stacks.mjs';
|
|
4
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir, getStackName, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
5
|
+
import { listAllStackNames } from './utils/stack/stacks.mjs';
|
|
6
6
|
import { resolvePublicServerUrl } from './tailscale.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { getInternalServerUrl, getPublicServerUrlEnvOverride, getWebappUrlEnvOverride } from './utils/server/urls.mjs';
|
|
8
|
+
import { fetchHappyHealth } from './utils/server/server.mjs';
|
|
8
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
12
|
import { spawn } from 'node:child_process';
|
|
12
|
-
import { mkdir,
|
|
13
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
13
14
|
import { dirname } from 'node:path';
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
-
import { ensureDepsInstalled, pmExecBin } from './utils/pm.mjs';
|
|
17
|
-
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/happy_server_infra.mjs';
|
|
18
|
-
import { clearDevAuthKey, readDevAuthKey, writeDevAuthKey } from './utils/
|
|
19
|
-
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
|
|
20
|
-
import { resolveAuthSeedFromEnv } from './utils/
|
|
21
|
-
import { printAuthLoginInstructions } from './utils/
|
|
22
|
-
import { copyFileIfMissing, linkFileIfMissing, removeFileOrSymlinkIfExists, writeSecretFileIfMissing } from './utils/
|
|
23
|
-
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './utils/
|
|
24
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
25
|
-
import { resolveHandyMasterSecretFromStack } from './utils/handy_master_secret.mjs';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// For non-main stacks, do NOT inherit a global/server URL override from ~/.happy-stacks/env.local
|
|
41
|
-
// (which often points at main). Only use a public URL override if it is explicitly present in the
|
|
42
|
-
// stack env file itself.
|
|
43
|
-
const envPath =
|
|
44
|
-
(process.env.HAPPY_STACKS_ENV_FILE ?? '').trim() ||
|
|
45
|
-
(process.env.HAPPY_LOCAL_ENV_FILE ?? '').trim() ||
|
|
46
|
-
getStackEnvPath(stackName);
|
|
47
|
-
try {
|
|
48
|
-
if (!envPath || !existsSync(envPath)) return '';
|
|
49
|
-
const raw = readFileSync(envPath, 'utf-8');
|
|
50
|
-
const env = raw ? parseEnvToObject(raw) : {};
|
|
51
|
-
return (env.HAPPY_LOCAL_SERVER_URL ?? env.HAPPY_STACKS_SERVER_URL ?? '').toString().trim();
|
|
52
|
-
} catch {
|
|
53
|
-
return '';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function expandTilde(p) {
|
|
58
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolveEnvWebappUrlForStack({ stackName }) {
|
|
62
|
-
const candidate = (process.env.HAPPY_WEBAPP_URL ?? '').trim();
|
|
63
|
-
|
|
64
|
-
// For main, allow the user's global override.
|
|
65
|
-
if (stackName === 'main') {
|
|
66
|
-
return candidate;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// For non-main stacks, only respect HAPPY_WEBAPP_URL if it is explicitly present in the stack env file.
|
|
70
|
-
const envPath =
|
|
71
|
-
(process.env.HAPPY_STACKS_ENV_FILE ?? '').trim() ||
|
|
72
|
-
(process.env.HAPPY_LOCAL_ENV_FILE ?? '').trim() ||
|
|
73
|
-
getStackEnvPath(stackName);
|
|
74
|
-
try {
|
|
75
|
-
if (!envPath || !existsSync(envPath)) return '';
|
|
76
|
-
const raw = readFileSync(envPath, 'utf-8');
|
|
77
|
-
const env = raw ? parseEnvToObject(raw) : {};
|
|
78
|
-
return (env.HAPPY_WEBAPP_URL ?? '').toString().trim();
|
|
79
|
-
} catch {
|
|
80
|
-
return '';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function sanitizeDnsLabel(raw, { fallback = 'stack' } = {}) {
|
|
85
|
-
const s = String(raw ?? '')
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
88
|
-
.replace(/-+/g, '-')
|
|
89
|
-
.replace(/^-+/, '')
|
|
90
|
-
.replace(/-+$/, '');
|
|
91
|
-
return s || fallback;
|
|
16
|
+
import { parseEnvToObject } from './utils/env/dotenv.mjs';
|
|
17
|
+
import { ensureDepsInstalled, pmExecBin } from './utils/proc/pm.mjs';
|
|
18
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
|
|
19
|
+
import { clearDevAuthKey, readDevAuthKey, writeDevAuthKey } from './utils/auth/dev_key.mjs';
|
|
20
|
+
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo/expo.mjs';
|
|
21
|
+
import { resolveAuthSeedFromEnv } from './utils/stack/startup.mjs';
|
|
22
|
+
import { printAuthLoginInstructions } from './utils/auth/login_ux.mjs';
|
|
23
|
+
import { copyFileIfMissing, linkFileIfMissing, removeFileOrSymlinkIfExists, writeSecretFileIfMissing } from './utils/auth/files.mjs';
|
|
24
|
+
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './utils/auth/sources.mjs';
|
|
25
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
26
|
+
import { resolveHandyMasterSecretFromStack } from './utils/auth/handy_master_secret.mjs';
|
|
27
|
+
import { ensureDir, readTextIfExists } from './utils/fs/ops.mjs';
|
|
28
|
+
import { stackExistsSync } from './utils/stack/stacks.mjs';
|
|
29
|
+
import { checkDaemonState } from './daemon.mjs';
|
|
30
|
+
import {
|
|
31
|
+
getCliHomeDirFromEnvOrDefault,
|
|
32
|
+
getServerLightDataDirFromEnvOrDefault,
|
|
33
|
+
resolveCliHomeDir,
|
|
34
|
+
} from './utils/stack/dirs.mjs';
|
|
35
|
+
import { resolveLocalhostHost, preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
|
|
36
|
+
|
|
37
|
+
function getInternalServerUrlCompat() {
|
|
38
|
+
const { port, internalServerUrl } = getInternalServerUrl({ env: process.env, defaultPort: 3005 });
|
|
39
|
+
return { port, url: internalServerUrl };
|
|
92
40
|
}
|
|
93
41
|
|
|
94
42
|
async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
|
|
95
43
|
try {
|
|
96
|
-
const baseDir =
|
|
44
|
+
const baseDir = resolveStackEnvPath(stackName).baseDir;
|
|
97
45
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
98
46
|
const uiPaths = getExpoStatePaths({
|
|
99
47
|
baseDir,
|
|
100
|
-
kind: '
|
|
48
|
+
kind: 'expo-dev',
|
|
101
49
|
projectDir: uiDir,
|
|
102
|
-
stateFileName: '
|
|
50
|
+
stateFileName: 'expo.state.json',
|
|
103
51
|
});
|
|
104
52
|
const uiRunning = await isStateProcessRunning(uiPaths.statePath);
|
|
105
53
|
if (!uiRunning.running) return null;
|
|
106
54
|
const port = Number(uiRunning.state?.port);
|
|
107
55
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
108
|
-
const host =
|
|
56
|
+
const host = resolveLocalhostHost({ stackMode: stackName !== 'main', stackName });
|
|
109
57
|
return `http://${host}:${port}`;
|
|
110
58
|
} catch {
|
|
111
59
|
return null;
|
|
112
60
|
}
|
|
113
61
|
}
|
|
114
62
|
|
|
115
|
-
|
|
116
|
-
await mkdir(p, { recursive: true });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function readTextIfExists(path) {
|
|
120
|
-
try {
|
|
121
|
-
if (!existsSync(path)) return null;
|
|
122
|
-
const raw = await readFile(path, 'utf-8');
|
|
123
|
-
const t = raw.trim();
|
|
124
|
-
return t ? t : null;
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// (auth file copy/link helpers live in scripts/utils/auth_files.mjs)
|
|
131
|
-
|
|
132
|
-
function parseEnvToObject(raw) {
|
|
133
|
-
const parsed = parseDotenv(raw);
|
|
134
|
-
return Object.fromEntries(parsed.entries());
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getStackDir(stackName) {
|
|
138
|
-
return resolveStackEnvPath(stackName).baseDir;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getStackEnvPath(stackName) {
|
|
142
|
-
return resolveStackEnvPath(stackName).envPath;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function stackExistsSync(stackName) {
|
|
146
|
-
if (stackName === 'main') return true;
|
|
147
|
-
const envPath = getStackEnvPath(stackName);
|
|
148
|
-
return existsSync(envPath);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function getCliHomeDirFromEnvOrDefault({ stackBaseDir, env }) {
|
|
152
|
-
const fromEnv = (env.HAPPY_STACKS_CLI_HOME_DIR ?? env.HAPPY_LOCAL_CLI_HOME_DIR ?? '').trim();
|
|
153
|
-
return fromEnv || join(stackBaseDir, 'cli');
|
|
154
|
-
}
|
|
63
|
+
// NOTE: common fs helpers live in scripts/utils/fs/ops.mjs
|
|
155
64
|
|
|
156
|
-
|
|
157
|
-
const fromEnv = (env.HAPPY_SERVER_LIGHT_DATA_DIR ?? '').trim();
|
|
158
|
-
return fromEnv || join(stackBaseDir, 'server-light');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveCliHomeDir() {
|
|
162
|
-
const fromEnv = (process.env.HAPPY_LOCAL_CLI_HOME_DIR ?? process.env.HAPPY_STACKS_CLI_HOME_DIR ?? '').trim();
|
|
163
|
-
if (fromEnv) {
|
|
164
|
-
return expandTilde(fromEnv);
|
|
165
|
-
}
|
|
166
|
-
return join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
167
|
-
}
|
|
65
|
+
// (auth file copy/link helpers live in scripts/utils/auth/files.mjs)
|
|
168
66
|
|
|
169
67
|
function fileHasContent(path) {
|
|
170
68
|
try {
|
|
@@ -175,60 +73,6 @@ function fileHasContent(path) {
|
|
|
175
73
|
}
|
|
176
74
|
}
|
|
177
75
|
|
|
178
|
-
function checkDaemonState(cliHomeDir) {
|
|
179
|
-
const statePath = join(cliHomeDir, 'daemon.state.json');
|
|
180
|
-
const lockPath = join(cliHomeDir, 'daemon.state.json.lock');
|
|
181
|
-
|
|
182
|
-
const alive = (pid) => {
|
|
183
|
-
try {
|
|
184
|
-
process.kill(pid, 0);
|
|
185
|
-
return true;
|
|
186
|
-
} catch {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
if (existsSync(statePath)) {
|
|
192
|
-
try {
|
|
193
|
-
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
194
|
-
const pid = Number(state?.pid);
|
|
195
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
196
|
-
return alive(pid) ? { status: 'running', pid } : { status: 'stale_state', pid };
|
|
197
|
-
}
|
|
198
|
-
return { status: 'bad_state' };
|
|
199
|
-
} catch {
|
|
200
|
-
return { status: 'bad_state' };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (existsSync(lockPath)) {
|
|
205
|
-
try {
|
|
206
|
-
const pid = Number(readFileSync(lockPath, 'utf-8').trim());
|
|
207
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
208
|
-
return alive(pid) ? { status: 'starting', pid } : { status: 'stale_lock', pid };
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
// ignore
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { status: 'stopped' };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function fetchHealth(internalServerUrl) {
|
|
219
|
-
const ctl = new AbortController();
|
|
220
|
-
const t = setTimeout(() => ctl.abort(), 1500);
|
|
221
|
-
try {
|
|
222
|
-
const res = await fetch(`${internalServerUrl}/health`, { method: 'GET', signal: ctl.signal });
|
|
223
|
-
const body = (await res.text()).trim();
|
|
224
|
-
return { ok: res.ok, status: res.status, body };
|
|
225
|
-
} catch {
|
|
226
|
-
return { ok: false, status: null, body: null };
|
|
227
|
-
} finally {
|
|
228
|
-
clearTimeout(t);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
76
|
function authLoginSuggestion(stackName) {
|
|
233
77
|
return stackName === 'main' ? 'happys auth login' : `happys stack auth ${stackName} login`;
|
|
234
78
|
}
|
|
@@ -678,8 +522,8 @@ async function cmdCopyFrom({ argv, json }) {
|
|
|
678
522
|
}
|
|
679
523
|
}
|
|
680
524
|
|
|
681
|
-
const sourceBaseDir = isLegacySource ? getLegacyHappyBaseDir() :
|
|
682
|
-
const sourceEnvRaw = isLegacySource ? '' : await readTextIfExists(
|
|
525
|
+
const sourceBaseDir = isLegacySource ? getLegacyHappyBaseDir() : resolveStackEnvPath(fromStackName).baseDir;
|
|
526
|
+
const sourceEnvRaw = isLegacySource ? '' : await readTextIfExists(resolveStackEnvPath(fromStackName).envPath);
|
|
683
527
|
const sourceEnv = sourceEnvRaw ? parseEnvToObject(sourceEnvRaw) : {};
|
|
684
528
|
const sourceCli = isLegacySource
|
|
685
529
|
? join(sourceBaseDir, 'cli')
|
|
@@ -735,9 +579,9 @@ async function cmdCopyFrom({ argv, json }) {
|
|
|
735
579
|
// so we can seed DB accounts reliably.
|
|
736
580
|
const managed = (targetEnv.HAPPY_STACKS_MANAGED_INFRA ?? targetEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1').toString().trim() !== '0';
|
|
737
581
|
if (targetServerComponent === 'happy-server' && withInfra && managed) {
|
|
738
|
-
const { port } =
|
|
739
|
-
const publicServerUrl = `http://localhost:${port}
|
|
740
|
-
const envPath =
|
|
582
|
+
const { port } = getInternalServerUrlCompat();
|
|
583
|
+
const publicServerUrl = await preferStackLocalhostUrl(`http://localhost:${port}`, { stackName });
|
|
584
|
+
const envPath = resolveStackEnvPath(stackName).envPath;
|
|
741
585
|
const infra = await ensureHappyServerManagedInfra({
|
|
742
586
|
stackName,
|
|
743
587
|
baseDir: targetBaseDir,
|
|
@@ -838,14 +682,14 @@ async function cmdStatus({ json }) {
|
|
|
838
682
|
const rootDir = getRootDir(import.meta.url);
|
|
839
683
|
const stackName = getStackName();
|
|
840
684
|
|
|
841
|
-
const { port, url: internalServerUrl } =
|
|
842
|
-
const defaultPublicUrl =
|
|
843
|
-
const envPublicUrl = resolveEnvPublicUrlForStack({ stackName });
|
|
685
|
+
const { port, url: internalServerUrl } = getInternalServerUrlCompat();
|
|
686
|
+
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
|
|
844
687
|
const { publicServerUrl } = await resolvePublicServerUrl({
|
|
845
688
|
internalServerUrl,
|
|
846
689
|
defaultPublicUrl,
|
|
847
690
|
envPublicUrl,
|
|
848
691
|
allowEnable: false,
|
|
692
|
+
stackName,
|
|
849
693
|
});
|
|
850
694
|
|
|
851
695
|
const cliHomeDir = resolveCliHomeDir();
|
|
@@ -861,7 +705,12 @@ async function cmdStatus({ json }) {
|
|
|
861
705
|
};
|
|
862
706
|
|
|
863
707
|
const daemon = checkDaemonState(cliHomeDir);
|
|
864
|
-
const
|
|
708
|
+
const healthRaw = await fetchHappyHealth(internalServerUrl);
|
|
709
|
+
const health = {
|
|
710
|
+
ok: Boolean(healthRaw.ok),
|
|
711
|
+
status: healthRaw.status,
|
|
712
|
+
body: healthRaw.text ? healthRaw.text.trim() : null,
|
|
713
|
+
};
|
|
865
714
|
|
|
866
715
|
const out = {
|
|
867
716
|
stackName,
|
|
@@ -922,20 +771,21 @@ async function cmdStatus({ json }) {
|
|
|
922
771
|
async function cmdLogin({ argv, json }) {
|
|
923
772
|
const rootDir = getRootDir(import.meta.url);
|
|
924
773
|
const stackName = getStackName();
|
|
925
|
-
const { kv } = parseArgs(argv);
|
|
774
|
+
const { flags, kv } = parseArgs(argv);
|
|
926
775
|
|
|
927
|
-
const { port, url: internalServerUrl } =
|
|
928
|
-
const defaultPublicUrl =
|
|
929
|
-
const envPublicUrl = resolveEnvPublicUrlForStack({ stackName });
|
|
776
|
+
const { port, url: internalServerUrl } = getInternalServerUrlCompat();
|
|
777
|
+
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
|
|
930
778
|
const { publicServerUrl } = await resolvePublicServerUrl({
|
|
931
779
|
internalServerUrl,
|
|
932
780
|
defaultPublicUrl,
|
|
933
781
|
envPublicUrl,
|
|
934
782
|
allowEnable: false,
|
|
783
|
+
stackName,
|
|
935
784
|
});
|
|
936
|
-
const envWebappUrl =
|
|
785
|
+
const { envWebappUrl } = getWebappUrlEnvOverride({ env: process.env, stackName });
|
|
937
786
|
const expoWebappUrl = await resolveWebappUrlFromRunningExpo({ rootDir, stackName });
|
|
938
|
-
const
|
|
787
|
+
const webappUrlRaw = envWebappUrl || expoWebappUrl || publicServerUrl;
|
|
788
|
+
const webappUrl = await preferStackLocalhostUrl(webappUrlRaw, { stackName });
|
|
939
789
|
const webappUrlSource = expoWebappUrl ? 'expo' : envWebappUrl ? 'stack env override' : 'server';
|
|
940
790
|
|
|
941
791
|
const cliHomeDir = resolveCliHomeDir();
|
|
@@ -971,7 +821,8 @@ async function cmdLogin({ argv, json }) {
|
|
|
971
821
|
return;
|
|
972
822
|
}
|
|
973
823
|
|
|
974
|
-
|
|
824
|
+
const quietUx = flags.has('--quiet') || flags.has('--no-ux');
|
|
825
|
+
if (!json && !quietUx) {
|
|
975
826
|
printAuthLoginInstructions({
|
|
976
827
|
stackName,
|
|
977
828
|
context,
|