happy-stacks 0.2.0 → 0.3.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 (94) hide show
  1. package/README.md +59 -22
  2. package/bin/happys.mjs +2 -2
  3. package/package.json +1 -1
  4. package/scripts/auth.mjs +49 -202
  5. package/scripts/build.mjs +5 -6
  6. package/scripts/cli-link.mjs +3 -3
  7. package/scripts/completion.mjs +5 -5
  8. package/scripts/daemon.mjs +9 -17
  9. package/scripts/dev.mjs +18 -27
  10. package/scripts/doctor.mjs +20 -36
  11. package/scripts/edison.mjs +102 -77
  12. package/scripts/happy.mjs +8 -19
  13. package/scripts/init.mjs +5 -13
  14. package/scripts/install.mjs +8 -8
  15. package/scripts/lint.mjs +8 -29
  16. package/scripts/menubar.mjs +6 -13
  17. package/scripts/migrate.mjs +11 -21
  18. package/scripts/mobile.mjs +13 -12
  19. package/scripts/run.mjs +15 -15
  20. package/scripts/self.mjs +11 -29
  21. package/scripts/server_flavor.mjs +4 -4
  22. package/scripts/service.mjs +18 -28
  23. package/scripts/setup.mjs +26 -122
  24. package/scripts/setup_pr.mjs +11 -28
  25. package/scripts/stack.mjs +111 -161
  26. package/scripts/stop.mjs +3 -3
  27. package/scripts/tailscale.mjs +7 -10
  28. package/scripts/test.mjs +8 -29
  29. package/scripts/tui.mjs +8 -38
  30. package/scripts/typecheck.mjs +8 -29
  31. package/scripts/ui_gateway.mjs +1 -1
  32. package/scripts/uninstall.mjs +6 -6
  33. package/scripts/utils/{dev_auth_key.mjs → auth/dev_key.mjs} +2 -8
  34. package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
  35. package/scripts/utils/{handy_master_secret.mjs → auth/handy_master_secret.mjs} +6 -32
  36. package/scripts/utils/cli/flags.mjs +17 -0
  37. package/scripts/utils/cli/normalize.mjs +16 -0
  38. package/scripts/utils/cli/smoke_help.mjs +2 -2
  39. package/scripts/utils/cli/wizard.mjs +1 -1
  40. package/scripts/utils/crypto/tokens.mjs +14 -0
  41. package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +4 -4
  42. package/scripts/utils/{dev_expo_web.mjs → dev/expo_web.mjs} +5 -5
  43. package/scripts/utils/{dev_server.mjs → dev/server.mjs} +7 -7
  44. package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
  45. package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
  46. package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
  47. package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
  48. package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
  49. package/scripts/utils/env/read.mjs +30 -0
  50. package/scripts/utils/env/values.mjs +13 -0
  51. package/scripts/utils/{expo.mjs → expo/expo.mjs} +3 -9
  52. package/scripts/utils/fs/json.mjs +25 -0
  53. package/scripts/utils/fs/ops.mjs +29 -0
  54. package/scripts/utils/fs/package_json.mjs +8 -0
  55. package/scripts/utils/fs/tail.mjs +12 -0
  56. package/scripts/utils/git/refs.mjs +26 -0
  57. package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +3 -3
  58. package/scripts/utils/net/dns.mjs +10 -0
  59. package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
  60. package/scripts/utils/{localhost_host.mjs → paths/localhost_host.mjs} +2 -10
  61. package/scripts/utils/{paths.mjs → paths/paths.mjs} +10 -7
  62. package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
  63. package/scripts/utils/proc/commands.mjs +34 -0
  64. package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
  65. package/scripts/utils/proc/package_scripts.mjs +31 -0
  66. package/scripts/utils/proc/pids.mjs +11 -0
  67. package/scripts/utils/{pm.mjs → proc/pm.mjs} +65 -152
  68. package/scripts/utils/{proc.mjs → proc/proc.mjs} +1 -0
  69. package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
  70. package/scripts/utils/server/port.mjs +68 -0
  71. package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
  72. package/scripts/utils/server/urls.mjs +91 -0
  73. package/scripts/utils/{validate.mjs → server/validate.mjs} +1 -1
  74. package/scripts/utils/service/autostart_darwin.mjs +142 -0
  75. package/scripts/utils/{stack_context.mjs → stack/context.mjs} +2 -2
  76. package/scripts/utils/stack/dirs.mjs +27 -0
  77. package/scripts/utils/stack/editor_workspace.mjs +152 -0
  78. package/scripts/utils/stack/names.mjs +12 -0
  79. package/scripts/utils/{stack_runtime_state.mjs → stack/runtime_state.mjs} +10 -27
  80. package/scripts/utils/{stacks.mjs → stack/stacks.mjs} +9 -2
  81. package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +2 -2
  82. package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +9 -15
  83. package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
  84. package/scripts/utils/ui/text.mjs +16 -0
  85. package/scripts/where.mjs +6 -6
  86. package/scripts/worktrees.mjs +30 -58
  87. package/scripts/utils/server_port.mjs +0 -9
  88. package/scripts/utils/server_urls.mjs +0 -54
  89. /package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +0 -0
  90. /package/scripts/utils/{auth_sources.mjs → auth/sources.mjs} +0 -0
  91. /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
  92. /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
  93. /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
  94. /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/README.md CHANGED
@@ -23,13 +23,6 @@ Recommended:
23
23
  npx happy-stacks setup --profile=selfhost
24
24
  ```
25
25
 
26
- Alternative (global install):
27
-
28
- ```bash
29
- npm install -g happy-stacks
30
- happys setup --profile=selfhost
31
- ```
32
-
33
26
  `setup` can optionally start Happy and guide you through authentication.
34
27
 
35
28
  ### Step 2: Start Happy
@@ -192,7 +185,10 @@ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
192
185
  happys wt pr happy 123 --update --stash
193
186
  ```
194
187
 
195
- Create a fully isolated PR stack (creates stack + PR worktrees + optional auth seeding + starts dev):
188
+ ##### Developer quickstart: create a PR stack (isolated ports/dirs; idempotent updates)
189
+
190
+ This creates (or reuses) a named stack, checks out PR worktrees for the selected components, optionally seeds auth, and starts the stack.
191
+ Re-run with `--reuse` to update the existing worktrees when the PR changes.
196
192
 
197
193
  ```bash
198
194
  happys stack pr pr123 \
@@ -202,15 +198,27 @@ happys stack pr pr123 \
202
198
  --dev
203
199
  ```
204
200
 
205
- One-shot “install + run PR stack” (best for maintainers who don’t have Happy Stacks set up yet):
201
+ Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
206
202
 
207
203
  ```bash
208
- npx happy-stacks setup pr \
209
- --happy=https://github.com/slopus/happy/pull/123 \
210
- --happy-cli=https://github.com/slopus/happy-cli/pull/456
204
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
205
+ happys --sandbox-dir "$SANDBOX" stack pr pr123 --happy=123 --happy-cli=456 --dev
206
+ rm -rf "$SANDBOX"
211
207
  ```
212
208
 
213
- You can also run it as:
209
+ Update when the PR changes:
210
+
211
+ - Re-run with `--reuse` to fast-forward worktrees when possible.
212
+ - If the PR was force-pushed, add `--force`.
213
+
214
+ ```bash
215
+ happys stack pr pr123 --happy=123 --happy-cli=456 --reuse
216
+ happys stack pr pr123 --happy=123 --happy-cli=456 --reuse --force
217
+ ```
218
+
219
+ ##### Maintainer quickstart: one-shot “install + run PR stack” (idempotent)
220
+
221
+ This is the maintainer-friendly entrypoint. It is safe to re-run and will keep the PR stack wiring intact.
214
222
 
215
223
  ```bash
216
224
  npx happy-stacks setup-pr \
@@ -218,11 +226,36 @@ npx happy-stacks setup-pr \
218
226
  --happy-cli=https://github.com/slopus/happy-cli/pull/456
219
227
  ```
220
228
 
221
- Updating when the PR changes:
229
+ Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
230
+
231
+ ```bash
232
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
233
+ npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
234
+ rm -rf "$SANDBOX"
235
+ ```
236
+
237
+ Short form (PR numbers):
238
+
239
+ ```bash
240
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456
241
+ ```
242
+
243
+ Override stack name (optional):
244
+
245
+ ```bash
246
+ npx happy-stacks setup-pr --name=pr123 --happy=123 --happy-cli=456
247
+ ```
248
+
249
+ Update when the PR changes:
222
250
 
223
251
  - Re-run the same command to fast-forward the PR worktrees.
224
252
  - If the PR was force-pushed, add `--force`.
225
253
 
254
+ ```bash
255
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456
256
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456 --force
257
+ ```
258
+
226
259
  Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
227
260
 
228
261
  #### Server flavor (server-light vs full server)
@@ -352,19 +385,23 @@ Notes:
352
385
 
353
386
  ### Sandbox / test installs (fully isolated)
354
387
 
355
- If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run with:
388
+ If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run everything with `--sandbox-dir`.
389
+ To fully uninstall the test run, stop the sandbox stacks and delete the sandbox folder.
356
390
 
357
391
  ```bash
358
- npx happy-stacks --sandbox-dir /tmp/happy-stacks-sandbox setup pr --happy=123 --happy-cli=456
359
- ```
392
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
360
393
 
361
- To reset completely, just delete the sandbox folder:
394
+ # Run a PR stack (fully isolated install)
395
+ npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
362
396
 
363
- ```bash
364
- rm -rf /tmp/happy-stacks-sandbox
397
+ # Tear down + uninstall
398
+ npx happy-stacks --sandbox-dir "$SANDBOX" stop --yes --no-service
399
+ rm -rf "$SANDBOX"
365
400
  ```
366
401
 
367
- Sandbox mode disables global OS side effects (PATH edits, SwiftBar plugin install, LaunchAgents/systemd services) by default.
368
- To explicitly allow them for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1`.
402
+ Notes:
403
+
404
+ - Sandbox mode disables global OS side effects (**PATH edits**, **SwiftBar plugin install**, **LaunchAgents/systemd services**, **Tailscale Serve enable/disable**) by default.
405
+ - To explicitly allow those for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1` (still recommended to clean up after).
369
406
 
370
407
  For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
package/bin/happys.mjs CHANGED
@@ -8,13 +8,13 @@ import { homedir } from 'node:os';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli/cli_registry.mjs';
11
- import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/canonical_home.mjs';
11
+ import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/paths/canonical_home.mjs';
12
12
 
13
13
  function getCliRootDir() {
14
14
  return dirname(dirname(fileURLToPath(import.meta.url)));
15
15
  }
16
16
 
17
- // expandHome is imported from scripts/utils/canonical_home.mjs
17
+ // expandHome is imported from scripts/utils/paths/canonical_home.mjs
18
18
 
19
19
  function dotenvGetQuick(envPath, key) {
20
20
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "happy-stacks",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "packageManager": "pnpm@10.18.3",
6
6
  "bin": {
7
7
  "happys": "./bin/happys.mjs",
package/scripts/auth.mjs CHANGED
@@ -1,99 +1,47 @@
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 { resolveServerPortFromEnv } from './utils/server_urls.mjs';
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, readFile, writeFile } from 'node:fs/promises';
13
+ import { mkdir, writeFile } from 'node:fs/promises';
13
14
  import { dirname } from 'node:path';
14
15
 
15
- import { parseDotenv } from './utils/dotenv.mjs';
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/dev_auth_key.mjs';
19
- import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
20
- import { resolveAuthSeedFromEnv } from './utils/stack_startup.mjs';
21
- import { printAuthLoginInstructions } from './utils/auth_login_ux.mjs';
22
- import { copyFileIfMissing, linkFileIfMissing, removeFileOrSymlinkIfExists, writeSecretFileIfMissing } from './utils/auth_files.mjs';
23
- import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './utils/auth_sources.mjs';
24
- import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
25
- import { resolveHandyMasterSecretFromStack } from './utils/handy_master_secret.mjs';
26
-
27
- function getInternalServerUrl() {
28
- const n = resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
29
- return { port: n, url: `http://127.0.0.1:${n}` };
30
- }
31
-
32
- function resolveEnvPublicUrlForStack({ stackName }) {
33
- const candidate = (process.env.HAPPY_LOCAL_SERVER_URL ?? process.env.HAPPY_STACKS_SERVER_URL ?? '').trim();
34
-
35
- // For main, allow the user's global/public URL override (commonly a Tailscale Serve URL).
36
- if (stackName === 'main') {
37
- return candidate;
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 } 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 = getStackDir(stackName);
44
+ const baseDir = resolveStackEnvPath(stackName).baseDir;
97
45
  const uiDir = getComponentDir(rootDir, 'happy');
98
46
  const uiPaths = getExpoStatePaths({
99
47
  baseDir,
@@ -105,66 +53,16 @@ async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
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 = stackName && stackName !== 'main' ? `happy-${sanitizeDnsLabel(stackName)}.localhost` : 'localhost';
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
- async function ensureDir(p) {
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
- }
63
+ // NOTE: common fs helpers live in scripts/utils/fs/ops.mjs
129
64
 
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
- }
155
-
156
- function getServerLightDataDirFromEnvOrDefault({ stackBaseDir, env }) {
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() : getStackDir(fromStackName);
682
- const sourceEnvRaw = isLegacySource ? '' : await readTextIfExists(getStackEnvPath(fromStackName));
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 } = getInternalServerUrl();
582
+ const { port } = getInternalServerUrlCompat();
739
583
  const publicServerUrl = `http://localhost:${port}`;
740
- const envPath = getStackEnvPath(stackName);
584
+ const envPath = resolveStackEnvPath(stackName).envPath;
741
585
  const infra = await ensureHappyServerManagedInfra({
742
586
  stackName,
743
587
  baseDir: targetBaseDir,
@@ -838,9 +682,8 @@ async function cmdStatus({ json }) {
838
682
  const rootDir = getRootDir(import.meta.url);
839
683
  const stackName = getStackName();
840
684
 
841
- const { port, url: internalServerUrl } = getInternalServerUrl();
842
- const defaultPublicUrl = `http://localhost:${port}`;
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,
@@ -861,7 +704,12 @@ async function cmdStatus({ json }) {
861
704
  };
862
705
 
863
706
  const daemon = checkDaemonState(cliHomeDir);
864
- const health = await fetchHealth(internalServerUrl);
707
+ const healthRaw = await fetchHappyHealth(internalServerUrl);
708
+ const health = {
709
+ ok: Boolean(healthRaw.ok),
710
+ status: healthRaw.status,
711
+ body: healthRaw.text ? healthRaw.text.trim() : null,
712
+ };
865
713
 
866
714
  const out = {
867
715
  stackName,
@@ -924,16 +772,15 @@ async function cmdLogin({ argv, json }) {
924
772
  const stackName = getStackName();
925
773
  const { kv } = parseArgs(argv);
926
774
 
927
- const { port, url: internalServerUrl } = getInternalServerUrl();
928
- const defaultPublicUrl = `http://localhost:${port}`;
929
- const envPublicUrl = resolveEnvPublicUrlForStack({ stackName });
775
+ const { port, url: internalServerUrl } = getInternalServerUrlCompat();
776
+ const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
930
777
  const { publicServerUrl } = await resolvePublicServerUrl({
931
778
  internalServerUrl,
932
779
  defaultPublicUrl,
933
780
  envPublicUrl,
934
781
  allowEnable: false,
935
782
  });
936
- const envWebappUrl = resolveEnvWebappUrlForStack({ stackName });
783
+ const { envWebappUrl } = getWebappUrlEnvOverride({ env: process.env, stackName });
937
784
  const expoWebappUrl = await resolveWebappUrlFromRunningExpo({ rootDir, stackName });
938
785
  const webappUrl = envWebappUrl || expoWebappUrl || publicServerUrl;
939
786
  const webappUrlSource = expoWebappUrl ? 'expo' : envWebappUrl ? 'stack env override' : 'server';
package/scripts/build.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
4
- import { ensureDepsInstalled, pmExecBin, requireDir } from './utils/pm.mjs';
3
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
4
+ import { ensureDepsInstalled, pmExecBin, requireDir } from './utils/proc/pm.mjs';
5
+ import { resolveServerPortFromEnv } from './utils/server/urls.mjs';
5
6
  import { dirname, join } from 'node:path';
6
7
  import { readFile, rm, mkdir, writeFile } from 'node:fs/promises';
7
8
  import { tailscaleServeHttpsUrl } from './tailscale.mjs';
@@ -43,9 +44,7 @@ async function main() {
43
44
  const uiDir = getComponentDir(rootDir, 'happy');
44
45
  await requireDir('happy', uiDir);
45
46
 
46
- const serverPort = process.env.HAPPY_LOCAL_SERVER_PORT
47
- ? parseInt(process.env.HAPPY_LOCAL_SERVER_PORT, 10)
48
- : 3005;
47
+ const serverPort = resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
49
48
 
50
49
  // For Tauri builds we embed an explicit API base URL (tauri:// origins cannot use window.location.origin).
51
50
  const internalServerUrl = `http://127.0.0.1:${serverPort}`;
@@ -1,7 +1,7 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { getComponentDir, getRootDir } from './utils/paths.mjs';
4
- import { ensureCliBuilt, ensureHappyCliLocalNpmLinked } from './utils/pm.mjs';
3
+ import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
4
+ import { ensureCliBuilt, ensureHappyCliLocalNpmLinked } from './utils/proc/pm.mjs';
5
5
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
6
6
 
7
7
  /**
@@ -1,4 +1,4 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
 
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { existsSync } from 'node:fs';
@@ -7,11 +7,11 @@ import { join } from 'node:path';
7
7
 
8
8
  import { parseArgs } from './utils/cli/args.mjs';
9
9
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
10
- import { runCapture } from './utils/proc.mjs';
10
+ import { runCapture } from './utils/proc/proc.mjs';
11
11
  import { getHappysRegistry } from './utils/cli/cli_registry.mjs';
12
- import { expandHome } from './utils/canonical_home.mjs';
13
- import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
14
- import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
12
+ import { expandHome } from './utils/paths/canonical_home.mjs';
13
+ import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
14
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
15
15
 
16
16
  function detectShell() {
17
17
  const raw = (process.env.SHELL ?? '').toLowerCase();
@@ -1,7 +1,9 @@
1
- import { spawnProc, run, runCapture } from './utils/proc.mjs';
2
- import { resolveAuthSeedFromEnv } from './utils/stack_startup.mjs';
3
- import { getStacksStorageRoot } from './utils/paths.mjs';
4
- import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
1
+ import { spawnProc, run, runCapture } from './utils/proc/proc.mjs';
2
+ import { resolveAuthSeedFromEnv } from './utils/stack/startup.mjs';
3
+ import { getStacksStorageRoot } from './utils/paths/paths.mjs';
4
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
5
+ import { runCaptureIfCommandExists } from './utils/proc/commands.mjs';
6
+ import { readLastLines } from './utils/fs/tail.mjs';
5
7
  import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
6
8
  import { chmod, copyFile, mkdir } from 'node:fs/promises';
7
9
  import { join } from 'node:path';
@@ -28,7 +30,7 @@ export async function cleanupStaleDaemonState(homeDir) {
28
30
 
29
31
  const lsofHasPath = async (pid, pathNeedle) => {
30
32
  try {
31
- const out = await runCapture('sh', ['-lc', `command -v lsof >/dev/null 2>&1 && lsof -nP -p ${pid} 2>/dev/null || true`]);
33
+ const out = await runCaptureIfCommandExists('lsof', ['-nP', '-p', String(pid)]);
32
34
  return out.includes(pathNeedle);
33
35
  } catch {
34
36
  return false;
@@ -173,16 +175,6 @@ function getLatestDaemonLogPath(homeDir) {
173
175
  }
174
176
  }
175
177
 
176
- function readLastLines(path, lines = 60) {
177
- try {
178
- const content = readFileSync(path, 'utf-8');
179
- const parts = content.split('\n');
180
- return parts.slice(Math.max(0, parts.length - lines)).join('\n');
181
- } catch {
182
- return null;
183
- }
184
- }
185
-
186
178
  function excerptIndicatesMissingAuth(excerpt) {
187
179
  if (!excerpt) return false;
188
180
  return (
@@ -297,7 +289,7 @@ async function killDaemonFromLockFile({ cliHomeDir }) {
297
289
  // We do this by checking that `lsof -p <pid>` includes the lock path (or state file path).
298
290
  let ownsLock = false;
299
291
  try {
300
- const out = await runCapture('sh', ['-lc', `command -v lsof >/dev/null 2>&1 && lsof -nP -p ${pid} 2>/dev/null || true`]);
292
+ const out = await runCaptureIfCommandExists('lsof', ['-nP', '-p', String(pid)]);
301
293
  ownsLock = out.includes(lockPath) || out.includes(join(cliHomeDir, 'daemon.state.json')) || out.includes(join(cliHomeDir, 'logs'));
302
294
  } catch {
303
295
  ownsLock = false;
@@ -463,7 +455,7 @@ export async function startLocalDaemonWithAuth({
463
455
  const logPath =
464
456
  getLatestDaemonLogPath(cliHomeDir) ||
465
457
  ((!isSandboxed() || sandboxAllowsGlobalSideEffects()) ? getLatestDaemonLogPath(join(homedir(), '.happy')) : null);
466
- const excerpt = logPath ? readLastLines(logPath, 120) : null;
458
+ const excerpt = logPath ? await readLastLines(logPath, 120) : null;
467
459
  return { ok: false, exitCode, excerpt, logPath };
468
460
  };
469
461
 
package/scripts/dev.mjs CHANGED
@@ -1,36 +1,27 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { killProcessTree } from './utils/proc.mjs';
4
- import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
5
- import { killPortListeners } from './utils/ports.mjs';
6
- import { getServerComponentName, isHappyServerRunning } from './utils/server.mjs';
7
- import { requireDir } from './utils/pm.mjs';
3
+ import { killProcessTree } from './utils/proc/proc.mjs';
4
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
5
+ import { killPortListeners } from './utils/net/ports.mjs';
6
+ import { getServerComponentName, isHappyServerRunning } from './utils/server/server.mjs';
7
+ import { requireDir } from './utils/proc/pm.mjs';
8
8
  import { join } from 'node:path';
9
9
  import { setTimeout as delay } from 'node:timers/promises';
10
10
  import { homedir } from 'node:os';
11
11
  import { isDaemonRunning, stopLocalDaemon } from './daemon.mjs';
12
12
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
13
- import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
14
- import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
15
- import { isPidAlive, readStackRuntimeStateFile, recordStackRuntimeStart } from './utils/stack_runtime_state.mjs';
16
- import { resolveStackContext } from './utils/stack_context.mjs';
17
- import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server_urls.mjs';
18
- import { ensureDevCliReady, prepareDaemonAuthSeed, startDevDaemon, watchHappyCliAndRestartDaemon } from './utils/dev_daemon.mjs';
19
- import { startDevServer, watchDevServerAndRestart } from './utils/dev_server.mjs';
20
- import { startDevExpoWebUi } from './utils/dev_expo_web.mjs';
21
- import { resolveLocalhostHost } from './utils/localhost_host.mjs';
22
- import { openUrlInBrowser } from './utils/browser.mjs';
23
- import { waitForHttpOk } from './utils/server.mjs';
24
-
25
- function sanitizeDnsLabel(raw, { fallback = 'stack' } = {}) {
26
- const s = String(raw ?? '')
27
- .toLowerCase()
28
- .replace(/[^a-z0-9-]+/g, '-')
29
- .replace(/-+/g, '-')
30
- .replace(/^-+/, '')
31
- .replace(/-+$/, '');
32
- return s || fallback;
33
- }
13
+ import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/server/validate.mjs';
14
+ import { getExpoStatePaths, isStateProcessRunning } from './utils/expo/expo.mjs';
15
+ import { isPidAlive, readStackRuntimeStateFile, recordStackRuntimeStart } from './utils/stack/runtime_state.mjs';
16
+ import { resolveStackContext } from './utils/stack/context.mjs';
17
+ import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
18
+ import { ensureDevCliReady, prepareDaemonAuthSeed, startDevDaemon, watchHappyCliAndRestartDaemon } from './utils/dev/daemon.mjs';
19
+ import { startDevServer, watchDevServerAndRestart } from './utils/dev/server.mjs';
20
+ import { startDevExpoWebUi } from './utils/dev/expo_web.mjs';
21
+ import { resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
22
+ import { openUrlInBrowser } from './utils/ui/browser.mjs';
23
+ import { waitForHttpOk } from './utils/server/server.mjs';
24
+ import { sanitizeDnsLabel } from './utils/net/dns.mjs';
34
25
 
35
26
  /**
36
27
  * Dev mode stack: