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.
Files changed (149) hide show
  1. package/README.md +84 -25
  2. package/bin/happys.mjs +116 -17
  3. package/docs/happy-development.md +2 -2
  4. package/docs/isolated-linux-vm.md +82 -0
  5. package/docs/mobile-ios.md +112 -54
  6. package/package.json +5 -1
  7. package/scripts/auth.mjs +59 -208
  8. package/scripts/build.mjs +58 -12
  9. package/scripts/cli-link.mjs +3 -3
  10. package/scripts/completion.mjs +5 -5
  11. package/scripts/daemon.mjs +168 -20
  12. package/scripts/dev.mjs +196 -70
  13. package/scripts/doctor.mjs +20 -36
  14. package/scripts/edison.mjs +105 -78
  15. package/scripts/happy.mjs +8 -19
  16. package/scripts/init.mjs +8 -14
  17. package/scripts/install.mjs +119 -23
  18. package/scripts/lint.mjs +31 -32
  19. package/scripts/menubar.mjs +6 -13
  20. package/scripts/migrate.mjs +11 -21
  21. package/scripts/mobile.mjs +93 -108
  22. package/scripts/mobile_dev_client.mjs +83 -0
  23. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  24. package/scripts/review.mjs +217 -0
  25. package/scripts/review_pr.mjs +368 -0
  26. package/scripts/run.mjs +95 -21
  27. package/scripts/self.mjs +11 -29
  28. package/scripts/server_flavor.mjs +4 -4
  29. package/scripts/service.mjs +19 -29
  30. package/scripts/setup.mjs +63 -160
  31. package/scripts/setup_pr.mjs +592 -52
  32. package/scripts/stack.mjs +608 -200
  33. package/scripts/stop.mjs +3 -3
  34. package/scripts/tailscale.mjs +44 -11
  35. package/scripts/test.mjs +52 -36
  36. package/scripts/tui.mjs +314 -74
  37. package/scripts/typecheck.mjs +31 -32
  38. package/scripts/ui_gateway.mjs +1 -1
  39. package/scripts/uninstall.mjs +6 -6
  40. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  41. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  42. package/scripts/utils/auth/dev_key.mjs +163 -0
  43. package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
  44. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  45. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  46. package/scripts/utils/auth/handy_master_secret.mjs +68 -0
  47. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  48. package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
  49. package/scripts/utils/auth/sources.mjs +38 -0
  50. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  51. package/scripts/utils/cli/cli_registry.mjs +24 -0
  52. package/scripts/utils/cli/cwd_scope.mjs +82 -0
  53. package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
  54. package/scripts/utils/cli/flags.mjs +17 -0
  55. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  56. package/scripts/utils/cli/normalize.mjs +16 -0
  57. package/scripts/utils/cli/prereqs.mjs +72 -0
  58. package/scripts/utils/cli/progress.mjs +126 -0
  59. package/scripts/utils/cli/smoke_help.mjs +2 -2
  60. package/scripts/utils/cli/verbosity.mjs +12 -0
  61. package/scripts/utils/cli/wizard.mjs +1 -1
  62. package/scripts/utils/crypto/tokens.mjs +14 -0
  63. package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
  64. package/scripts/utils/dev/expo_dev.mjs +246 -0
  65. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  66. package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
  67. package/scripts/utils/dev_auth_key.mjs +1 -1
  68. package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
  69. package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
  70. package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
  71. package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
  72. package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
  73. package/scripts/utils/env/read.mjs +30 -0
  74. package/scripts/utils/env/values.mjs +13 -0
  75. package/scripts/utils/expo/command.mjs +52 -0
  76. package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
  77. package/scripts/utils/expo/metro_ports.mjs +114 -0
  78. package/scripts/utils/fs/json.mjs +25 -0
  79. package/scripts/utils/fs/ops.mjs +29 -0
  80. package/scripts/utils/fs/package_json.mjs +8 -0
  81. package/scripts/utils/fs/tail.mjs +12 -0
  82. package/scripts/utils/git/git.mjs +67 -0
  83. package/scripts/utils/git/refs.mjs +26 -0
  84. package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
  85. package/scripts/utils/handy_master_secret.mjs +2 -2
  86. package/scripts/utils/mobile/config.mjs +31 -0
  87. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  88. package/scripts/utils/mobile/identifiers.mjs +47 -0
  89. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  90. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  91. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  92. package/scripts/utils/net/dns.mjs +10 -0
  93. package/scripts/utils/net/lan_ip.mjs +24 -0
  94. package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
  95. package/scripts/utils/net/url.mjs +30 -0
  96. package/scripts/utils/net/url.test.mjs +20 -0
  97. package/scripts/utils/paths/localhost_host.mjs +56 -0
  98. package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
  99. package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
  100. package/scripts/utils/proc/commands.mjs +34 -0
  101. package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
  102. package/scripts/utils/proc/package_scripts.mjs +31 -0
  103. package/scripts/utils/proc/parallel.mjs +25 -0
  104. package/scripts/utils/proc/pids.mjs +11 -0
  105. package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
  106. package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
  107. package/scripts/utils/review/base_ref.mjs +74 -0
  108. package/scripts/utils/review/base_ref.test.mjs +54 -0
  109. package/scripts/utils/review/runners/coderabbit.mjs +19 -0
  110. package/scripts/utils/review/runners/codex.mjs +51 -0
  111. package/scripts/utils/review/targets.mjs +24 -0
  112. package/scripts/utils/review/targets.test.mjs +36 -0
  113. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  114. package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
  115. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  116. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  117. package/scripts/utils/server/port.mjs +68 -0
  118. package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
  119. package/scripts/utils/server/urls.mjs +101 -0
  120. package/scripts/utils/server/validate.mjs +88 -0
  121. package/scripts/utils/service/autostart_darwin.mjs +182 -0
  122. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  123. package/scripts/utils/stack/context.mjs +23 -0
  124. package/scripts/utils/stack/dirs.mjs +27 -0
  125. package/scripts/utils/stack/editor_workspace.mjs +152 -0
  126. package/scripts/utils/stack/names.mjs +12 -0
  127. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  128. package/scripts/utils/stack/runtime_state.mjs +88 -0
  129. package/scripts/utils/stack/stacks.mjs +45 -0
  130. package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
  131. package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
  132. package/scripts/utils/stack_context.mjs +3 -3
  133. package/scripts/utils/stack_runtime_state.mjs +1 -1
  134. package/scripts/utils/stacks.mjs +2 -2
  135. package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
  136. package/scripts/utils/ui/qr.mjs +17 -0
  137. package/scripts/utils/ui/text.mjs +16 -0
  138. package/scripts/utils/validate.mjs +1 -1
  139. package/scripts/where.mjs +6 -6
  140. package/scripts/worktrees.mjs +171 -113
  141. package/scripts/utils/auth_sources.mjs +0 -12
  142. package/scripts/utils/dev_expo_web.mjs +0 -112
  143. package/scripts/utils/localhost_host.mjs +0 -17
  144. package/scripts/utils/server_port.mjs +0 -9
  145. package/scripts/utils/server_urls.mjs +0 -54
  146. /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
  147. /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
  148. /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
  149. /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/scripts/run.mjs CHANGED
@@ -1,25 +1,29 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { pathExists } from './utils/fs.mjs';
4
- import { killProcessTree, runCapture, spawnProc } from './utils/proc.mjs';
5
- import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
6
- import { killPortListeners } from './utils/ports.mjs';
7
- import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server.mjs';
8
- import { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/pm.mjs';
3
+ import { pathExists } from './utils/fs/fs.mjs';
4
+ import { killProcessTree, runCapture, spawnProc } from './utils/proc/proc.mjs';
5
+ import { componentDirEnvKey, getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
6
+ import { killPortListeners } from './utils/net/ports.mjs';
7
+ import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server/server.mjs';
8
+ import { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/proc/pm.mjs';
9
9
  import { homedir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { setTimeout as delay } from 'node:timers/promises';
12
12
  import { maybeResetTailscaleServe } from './tailscale.mjs';
13
13
  import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
14
14
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
15
- import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
16
- import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/happy_server_infra.mjs';
17
- import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from './utils/stack_startup.mjs';
18
- import { recordStackRuntimeStart, recordStackRuntimeUpdate } from './utils/stack_runtime_state.mjs';
19
- import { resolveStackContext } from './utils/stack_context.mjs';
20
- import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv, resolveServerUrls } from './utils/server_urls.mjs';
21
- import { resolveLocalhostHost } from './utils/localhost_host.mjs';
22
- import { openUrlInBrowser } from './utils/browser.mjs';
15
+ import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/server/validate.mjs';
16
+ import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
17
+ import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded, resolveAutoCopyFromMainEnabled } from './utils/stack/startup.mjs';
18
+ import { recordStackRuntimeStart, recordStackRuntimeUpdate } from './utils/stack/runtime_state.mjs';
19
+ import { resolveStackContext } from './utils/stack/context.mjs';
20
+ import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
21
+ import { preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
22
+ import { openUrlInBrowser } from './utils/ui/browser.mjs';
23
+ import { ensureDevExpoServer } from './utils/dev/expo_dev.mjs';
24
+ import { maybeRunInteractiveStackAuthSetup } from './utils/auth/interactive_stack_auth.mjs';
25
+ import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
26
+ import { daemonStartGate, formatDaemonAuthRequiredError } from './utils/auth/daemon_gate.mjs';
23
27
 
24
28
  /**
25
29
  * Run the local stack in "production-like" mode:
@@ -27,7 +31,7 @@ import { openUrlInBrowser } from './utils/browser.mjs';
27
31
  * - happy-cli daemon
28
32
  * - serve prebuilt UI via happy-server-light (/)
29
33
  *
30
- * No Expo dev server.
34
+ * Optional: Expo dev-client Metro for mobile reviewers (`--mobile`).
31
35
  */
32
36
 
33
37
  async function main() {
@@ -37,12 +41,15 @@ async function main() {
37
41
  if (wantsHelp(argv, { flags })) {
38
42
  printResult({
39
43
  json,
40
- data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser'], json: true },
44
+ data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser', '--mobile'], json: true },
41
45
  text: [
42
46
  '[start] usage:',
43
47
  ' happys start [--server=happy-server|happy-server-light] [--restart] [--json]',
44
48
  ' (legacy in a cloned repo): pnpm start [-- --server=happy-server|happy-server-light] [--json]',
45
49
  ' note: --json prints the resolved config (dry-run) and exits.',
50
+ '',
51
+ 'note:',
52
+ ' If run from inside a component checkout/worktree, that checkout is used for this run (without requiring `happys wt use`).',
46
53
  ].join('\n'),
47
54
  });
48
55
  return;
@@ -50,6 +57,20 @@ async function main() {
50
57
 
51
58
  const rootDir = getRootDir(import.meta.url);
52
59
 
60
+ const inferred = inferComponentFromCwd({
61
+ rootDir,
62
+ invokedCwd: getInvokedCwd(process.env),
63
+ components: ['happy', 'happy-cli', 'happy-server-light', 'happy-server'],
64
+ });
65
+ if (inferred) {
66
+ const stacksKey = componentDirEnvKey(inferred.component);
67
+ const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
68
+ // Stack env should win. Only infer from CWD when the component dir isn't already configured.
69
+ if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
70
+ process.env[stacksKey] = inferred.repoDir;
71
+ }
72
+ }
73
+
53
74
  const serverPort = resolveServerPortFromEnv({ defaultPort: 3005 });
54
75
 
55
76
  // Internal URL used by local processes on this machine.
@@ -67,6 +88,7 @@ async function main() {
67
88
  const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
68
89
  const serveUiWanted = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
69
90
  const serveUi = serveUiWanted;
91
+ const startMobile = flags.has('--mobile') || flags.has('--with-mobile');
70
92
  const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
71
93
  const uiPrefix = process.env.HAPPY_LOCAL_UI_PREFIX?.trim() ? process.env.HAPPY_LOCAL_UI_PREFIX.trim() : '/';
72
94
  const autostart = getDefaultAutostartPaths();
@@ -78,12 +100,16 @@ async function main() {
78
100
 
79
101
  const serverDir = getComponentDir(rootDir, serverComponentName);
80
102
  const cliDir = getComponentDir(rootDir, 'happy-cli');
103
+ const uiDir = getComponentDir(rootDir, 'happy');
81
104
 
82
105
  assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
83
106
  assertServerPrismaProviderMatches({ serverComponentName, serverDir });
84
107
 
85
108
  await requireDir(serverComponentName, serverDir);
86
109
  await requireDir('happy-cli', cliDir);
110
+ if (startMobile) {
111
+ await requireDir('happy', uiDir);
112
+ }
87
113
 
88
114
  const cliBin = join(cliDir, 'bin', 'happy.mjs');
89
115
 
@@ -99,12 +125,14 @@ async function main() {
99
125
  mode: 'start',
100
126
  serverComponentName,
101
127
  serverDir,
128
+ uiDir,
102
129
  cliDir,
103
130
  serverPort,
104
131
  internalServerUrl,
105
132
  publicServerUrl,
106
133
  startDaemon,
107
134
  serveUi,
135
+ startMobile,
108
136
  uiPrefix,
109
137
  uiBuildDir,
110
138
  cliHomeDir,
@@ -125,7 +153,7 @@ async function main() {
125
153
  let shuttingDown = false;
126
154
  const baseEnv = { ...process.env };
127
155
  const stackCtx = resolveStackContext({ env: baseEnv, autostart });
128
- const { stackMode, runtimeStatePath, stackName, ephemeral } = stackCtx;
156
+ const { stackMode, runtimeStatePath, stackName, envPath, ephemeral } = stackCtx;
129
157
 
130
158
  // Ensure happy-cli is install+build ready before starting the daemon.
131
159
  const buildCli = (baseEnv.HAPPY_STACKS_CLI_BUILD ?? baseEnv.HAPPY_LOCAL_CLI_BUILD ?? '1').toString().trim() !== '0';
@@ -133,12 +161,18 @@ async function main() {
133
161
 
134
162
  // Ensure server deps exist before any Prisma/docker work.
135
163
  await ensureDepsInstalled(serverDir, serverComponentName);
164
+ if (startMobile) {
165
+ await ensureDepsInstalled(uiDir, 'happy');
166
+ }
136
167
 
137
168
  // Public URL automation:
138
169
  // - Only the main stack should ever auto-enable Tailscale Serve by default.
139
170
  // - Non-main stacks default to localhost unless the user explicitly configured a public URL
140
171
  // OR Tailscale Serve is already configured for this stack's internal URL (status matches).
141
- const allowEnableTailscale = !stackMode || stackName === 'main';
172
+ const allowEnableTailscale =
173
+ !stackMode ||
174
+ stackName === 'main' ||
175
+ (baseEnv.HAPPY_STACKS_TAILSCALE_SERVE ?? baseEnv.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0').toString().trim() === '1';
142
176
  const resolvedUrls = await resolveServerUrls({ env: baseEnv, serverPort, allowEnable: allowEnableTailscale });
143
177
  if (stackMode && stackName !== 'main' && !resolvedUrls.envPublicUrl) {
144
178
  const src = String(resolvedUrls.publicServerUrlSource ?? '');
@@ -331,9 +365,8 @@ async function main() {
331
365
  // Auto-open UI (interactive only) using the stack-scoped hostname when applicable.
332
366
  const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
333
367
  if (isInteractive && !noBrowser) {
334
- const host = resolveLocalhostHost({ stackMode, stackName: autostart.stackName });
335
368
  const prefix = uiPrefix.startsWith('/') ? uiPrefix : `/${uiPrefix}`;
336
- const openUrl = `http://${host}:${serverPort}${prefix}`;
369
+ const openUrl = await preferStackLocalhostUrl(`http://localhost:${serverPort}${prefix}`, { stackName: autostart.stackName });
337
370
  const res = await openUrlInBrowser(openUrl);
338
371
  if (!res.ok) {
339
372
  console.warn(`[local] ui: failed to open browser automatically (${res.error}).`);
@@ -343,6 +376,16 @@ async function main() {
343
376
 
344
377
  // Daemon
345
378
  if (startDaemon) {
379
+ const gate = daemonStartGate({ env: baseEnv, cliHomeDir });
380
+ if (!gate.ok) {
381
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
382
+ // In orchestrated auth flows, keep server/UI up and let the orchestrator start daemon post-auth.
383
+ if (gate.reason === 'auth_flow_missing_credentials') {
384
+ console.log('[local] auth flow: skipping daemon start until credentials exist');
385
+ } else if (!isInteractive) {
386
+ throw new Error(formatDaemonAuthRequiredError({ stackName: autostart.stackName, cliHomeDir }));
387
+ }
388
+ } else {
346
389
  const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
347
390
  if (serverComponentName === 'happy-server' && happyServerAccountCount == null) {
348
391
  const acct = await getAccountCountForServerComponent({
@@ -355,6 +398,16 @@ async function main() {
355
398
  }
356
399
  const accountCount =
357
400
  serverComponentName === 'happy-server-light' ? serverLightAccountCount : happyServerAccountCount;
401
+ const autoSeedEnabled = resolveAutoCopyFromMainEnabled({ env: baseEnv, stackName: autostart.stackName, isInteractive });
402
+ await maybeRunInteractiveStackAuthSetup({
403
+ rootDir,
404
+ env: baseEnv,
405
+ stackName: autostart.stackName,
406
+ cliHomeDir,
407
+ accountCount,
408
+ isInteractive,
409
+ autoSeedEnabled,
410
+ });
358
411
  await prepareDaemonAuthSeedIfNeeded({
359
412
  rootDir,
360
413
  env: baseEnv,
@@ -372,6 +425,27 @@ async function main() {
372
425
  publicServerUrl,
373
426
  isShuttingDown: () => shuttingDown,
374
427
  forceRestart: restart,
428
+ env: baseEnv,
429
+ stackName: autostart.stackName,
430
+ });
431
+ }
432
+ }
433
+
434
+ // Optional: start Expo dev-client Metro for mobile reviewers.
435
+ if (startMobile) {
436
+ await ensureDevExpoServer({
437
+ startUi: false,
438
+ startMobile: true,
439
+ uiDir,
440
+ autostart,
441
+ baseEnv,
442
+ apiServerUrl: publicServerUrl,
443
+ restart,
444
+ stackMode,
445
+ runtimeStatePath,
446
+ stackName,
447
+ envPath,
448
+ children,
375
449
  });
376
450
  }
377
451
 
package/scripts/self.mjs CHANGED
@@ -1,16 +1,18 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
 
3
3
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
4
  import { existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
7
  import { parseArgs } from './utils/cli/args.mjs';
8
- import { pathExists } from './utils/fs.mjs';
9
- import { run, runCapture } from './utils/proc.mjs';
10
- import { expandHome } from './utils/canonical_home.mjs';
11
- import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
8
+ import { pathExists } from './utils/fs/fs.mjs';
9
+ import { run, runCapture } from './utils/proc/proc.mjs';
10
+ import { expandHome } from './utils/paths/canonical_home.mjs';
11
+ import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
12
12
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
13
- import { getRuntimeDir } from './utils/runtime.mjs';
13
+ import { getRuntimeDir } from './utils/paths/runtime.mjs';
14
+ import { readJsonIfExists } from './utils/fs/json.mjs';
15
+ import { readPackageJsonVersion } from './utils/fs/package_json.mjs';
14
16
 
15
17
  function cachePaths() {
16
18
  const home = getHappyStacksHomeDir();
@@ -21,15 +23,6 @@ function cachePaths() {
21
23
  };
22
24
  }
23
25
 
24
- async function readJsonSafe(path) {
25
- try {
26
- const raw = await readFile(path, 'utf-8');
27
- return JSON.parse(raw);
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
26
  async function writeJsonSafe(path, obj) {
34
27
  try {
35
28
  await mkdir(join(path, '..'), { recursive: true });
@@ -43,25 +36,14 @@ async function writeJsonSafe(path, obj) {
43
36
  }
44
37
  }
45
38
 
46
- async function readPkgVersion(pkgJsonPath) {
47
- try {
48
- const raw = await readFile(pkgJsonPath, 'utf-8');
49
- const pkg = JSON.parse(raw);
50
- const v = String(pkg.version ?? '').trim();
51
- return v || null;
52
- } catch {
53
- return null;
54
- }
55
- }
56
-
57
39
  async function getRuntimeInstalledVersion() {
58
40
  const runtimeDir = getRuntimeDir();
59
41
  const pkgJson = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
60
- return await readPkgVersion(pkgJson);
42
+ return await readPackageJsonVersion(pkgJson);
61
43
  }
62
44
 
63
45
  async function getInvokerVersion({ rootDir }) {
64
- return await readPkgVersion(join(rootDir, 'package.json'));
46
+ return await readPackageJsonVersion(join(rootDir, 'package.json'));
65
47
  }
66
48
 
67
49
  async function fetchLatestVersion() {
@@ -102,7 +84,7 @@ async function cmdStatus({ rootDir, argv }) {
102
84
  const runtimeDir = getRuntimeDir();
103
85
  const runtimeVersion = await getRuntimeInstalledVersion();
104
86
 
105
- const cached = await readJsonSafe(updateJson);
87
+ const cached = await readJsonIfExists(updateJson, { defaultValue: null });
106
88
 
107
89
  let latest = cached?.latest ?? null;
108
90
  let checkedAt = cached?.checkedAt ?? null;
@@ -1,8 +1,8 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { getRootDir } from './utils/paths.mjs';
4
- import { ensureEnvFileUpdated } from './utils/env_file.mjs';
5
- import { resolveUserConfigEnvPath } from './utils/config.mjs';
3
+ import { getRootDir } from './utils/paths/paths.mjs';
4
+ import { ensureEnvFileUpdated } from './utils/env/env_file.mjs';
5
+ import { resolveUserConfigEnvPath } from './utils/env/config.mjs';
6
6
  import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
7
7
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
8
8
 
@@ -1,9 +1,10 @@
1
- import './utils/env.mjs';
2
- import { run, runCapture } from './utils/proc.mjs';
3
- import { getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
4
- import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/pm.mjs';
5
- import { getCanonicalHomeDir } from './utils/config.mjs';
6
- import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
1
+ import './utils/env/env.mjs';
2
+ import { run, runCapture } from './utils/proc/proc.mjs';
3
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
4
+ import { getInternalServerUrl, getPublicServerUrlEnvOverride } from './utils/server/urls.mjs';
5
+ import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/service/autostart_darwin.mjs';
6
+ import { getCanonicalHomeDir } from './utils/env/config.mjs';
7
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
7
8
  import { spawn } from 'node:child_process';
8
9
  import { homedir } from 'node:os';
9
10
  import { existsSync } from 'node:fs';
@@ -12,6 +13,7 @@ import { dirname, join, resolve } from 'node:path';
12
13
  import { fileURLToPath } from 'node:url';
13
14
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
14
15
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
16
+ import { readLastLines } from './utils/fs/tail.mjs';
15
17
 
16
18
  /**
17
19
  * Manage the autostart service installed by `happys bootstrap -- --autostart`.
@@ -35,11 +37,6 @@ function getUid() {
35
37
  return Number.isFinite(n) ? n : null;
36
38
  }
37
39
 
38
- function getInternalUrl() {
39
- const port = process.env.HAPPY_LOCAL_SERVER_PORT?.trim() ? Number(process.env.HAPPY_LOCAL_SERVER_PORT) : 3005;
40
- return `http://127.0.0.1:${port}`;
41
- }
42
-
43
40
  function getAutostartEnv({ rootDir }) {
44
41
  // IMPORTANT:
45
42
  // LaunchAgents should NOT bake the entire config into the plist, because that would require
@@ -267,18 +264,21 @@ async function startLaunchAgent({ persistent }) {
267
264
 
268
265
  async function postStartDiagnostics() {
269
266
  const rootDir = getRootDir(import.meta.url);
270
- const internalUrl = getInternalUrl();
267
+ const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
271
268
 
272
269
  const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
273
270
  ? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
274
271
  : join(getDefaultAutostartPaths().baseDir, 'cli');
275
272
 
276
- const publicUrl =
277
- process.env.HAPPY_LOCAL_SERVER_URL?.trim()
278
- ? process.env.HAPPY_LOCAL_SERVER_URL.trim()
279
- : internalUrl.replace('127.0.0.1', 'localhost');
273
+ let port = 3005;
274
+ try {
275
+ port = Number(new URL(internalUrl).port || 0) || 3005;
276
+ } catch {
277
+ port = 3005;
278
+ }
279
+ const { publicServerUrl: publicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port });
280
280
 
281
- const cliDir = join(rootDir, 'components', 'happy-cli');
281
+ const cliDir = getComponentDir(rootDir, 'happy-cli');
282
282
  const cliBin = join(cliDir, 'bin', 'happy.mjs');
283
283
 
284
284
  const accessKey = join(cliHomeDir, 'access.key');
@@ -286,16 +286,6 @@ async function postStartDiagnostics() {
286
286
  const lockFile = join(cliHomeDir, 'daemon.state.json.lock');
287
287
  const logsDir = join(cliHomeDir, 'logs');
288
288
 
289
- const readLastLines = async (path, lines = 60) => {
290
- try {
291
- const raw = await readFile(path, 'utf-8');
292
- const parts = raw.split('\n');
293
- return parts.slice(Math.max(0, parts.length - lines)).join('\n');
294
- } catch {
295
- return null;
296
- }
297
- };
298
-
299
289
  const latestDaemonLog = async () => {
300
290
  try {
301
291
  const ls = await runCapture('bash', ['-lc', `ls -1t "${logsDir}"/*-daemon.log 2>/dev/null | head -1 || true`]);
@@ -461,7 +451,7 @@ async function waitForLaunchAgentStopped({ timeoutMs = 8000 } = {}) {
461
451
 
462
452
  async function showStatus() {
463
453
  const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
464
- const internalUrl = getInternalUrl();
454
+ const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
465
455
 
466
456
  console.log(`label: ${label}`);
467
457
  console.log(`plist: ${plistPath} ${existsSync(plistPath) ? '(present)' : '(missing)'}`);
@@ -550,7 +540,7 @@ async function main() {
550
540
  return;
551
541
  case 'status':
552
542
  if (json) {
553
- const internalUrl = getInternalUrl();
543
+ const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
554
544
  let health = null;
555
545
  try {
556
546
  const res = await fetch(`${internalUrl}/health`, { method: 'GET' });