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
@@ -1,12 +1,13 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
- import { pickNextFreeTcpPort } from './utils/ports.mjs';
4
- import { run, runCapture, spawnProc } from './utils/proc.mjs';
5
- import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
6
- import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/pm.mjs';
3
+ import { pickNextFreeTcpPort } from './utils/net/ports.mjs';
4
+ import { run, runCapture, spawnProc } from './utils/proc/proc.mjs';
5
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
6
+ import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/proc/pm.mjs';
7
7
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
8
- import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo.mjs';
9
- import { killProcessGroupOwnedByStack } from './utils/ownership.mjs';
8
+ import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo/expo.mjs';
9
+ import { killProcessGroupOwnedByStack } from './utils/proc/ownership.mjs';
10
+ import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv } from './utils/server/urls.mjs';
10
11
 
11
12
  /**
12
13
  * Mobile dev helper for the embedded `components/happy` Expo app.
@@ -58,7 +59,7 @@ async function main() {
58
59
  '',
59
60
  'Notes:',
60
61
  '- This script is designed to avoid editing upstream `components/happy` config in-place.',
61
- '- It sets EXPO_PUBLIC_HAPPY_SERVER_URL from HAPPY_STACKS_SERVER_URL (legacy: HAPPY_LOCAL_SERVER_URL) if provided.',
62
+ '- If you explicitly set HAPPY_STACKS_SERVER_URL (legacy: HAPPY_LOCAL_SERVER_URL), it bakes that URL into the app via EXPO_PUBLIC_HAPPY_SERVER_URL.',
62
63
  ].join('\n'),
63
64
  });
64
65
  return;
@@ -141,10 +142,10 @@ async function main() {
141
142
 
142
143
  // Allow happy-stacks to define the default server URL baked into the app bundle.
143
144
  // This is read by the app via `process.env.EXPO_PUBLIC_HAPPY_SERVER_URL`.
144
- const stacksServerUrl =
145
- process.env.HAPPY_STACKS_SERVER_URL?.trim() || process.env.HAPPY_LOCAL_SERVER_URL?.trim() || '';
146
- if (stacksServerUrl && !env.EXPO_PUBLIC_HAPPY_SERVER_URL) {
147
- env.EXPO_PUBLIC_HAPPY_SERVER_URL = stacksServerUrl;
145
+ const serverPort = resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
146
+ const { envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort });
147
+ if (envPublicUrl && !env.EXPO_PUBLIC_HAPPY_SERVER_URL) {
148
+ env.EXPO_PUBLIC_HAPPY_SERVER_URL = envPublicUrl;
148
149
  }
149
150
 
150
151
  if (json) {
package/scripts/run.mjs CHANGED
@@ -1,25 +1,25 @@
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 { 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 } 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/paths/localhost_host.mjs';
22
+ import { openUrlInBrowser } from './utils/ui/browser.mjs';
23
23
 
24
24
  /**
25
25
  * Run the local stack in "production-like" mode:
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 { 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,16 +264,19 @@ 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
281
  const cliDir = join(rootDir, 'components', 'happy-cli');
282
282
  const cliBin = join(cliDir, 'bin', 'happy.mjs');
@@ -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' });
package/scripts/setup.mjs CHANGED
@@ -1,137 +1,41 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { parseArgs } from './utils/cli/args.mjs';
6
6
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
7
- import { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
7
+ import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
8
8
  import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
9
- import { getCanonicalHomeDir } from './utils/config.mjs';
10
- import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
11
- import { run, runCapture } from './utils/proc.mjs';
12
- import { fetchHappyHealth } from './utils/server.mjs';
9
+ import { getCanonicalHomeDir } from './utils/env/config.mjs';
10
+ import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
11
+ import { run, runCapture } from './utils/proc/proc.mjs';
12
+ import { waitForHappyHealthOk } from './utils/server/server.mjs';
13
13
  import { tailscaleServeEnable, tailscaleServeHttpsUrlForInternalServerUrl } from './tailscale.mjs';
14
- import { getRuntimeDir } from './utils/runtime.mjs';
14
+ import { getRuntimeDir } from './utils/paths/runtime.mjs';
15
15
  import { readFile } from 'node:fs/promises';
16
16
  import { homedir } from 'node:os';
17
- import { parseDotenv } from './utils/dotenv.mjs';
18
17
  import { installService } from './service.mjs';
19
- import { getDevAuthKeyPath } from './utils/dev_auth_key.mjs';
20
- import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
21
-
22
- function boolFromFlagsOrKv({ flags, kv, onFlag, offFlag, key, defaultValue }) {
23
- if (flags.has(offFlag)) return false;
24
- if (flags.has(onFlag)) return true;
25
- if (key && kv.has(key)) {
26
- const raw = String(kv.get(key) ?? '').trim().toLowerCase();
27
- if (raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y') return true;
28
- if (raw === '0' || raw === 'false' || raw === 'no' || raw === 'n') return false;
29
- }
30
- return defaultValue;
31
- }
32
-
33
- function normalizeProfile(raw) {
34
- const v = (raw ?? '').trim().toLowerCase();
35
- if (!v) return '';
36
- if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
37
- if (v === 'dev' || v === 'developer' || v === 'develop') return 'dev';
38
- return '';
39
- }
40
-
41
- function normalizeServer(raw) {
42
- const v = (raw ?? '').trim().toLowerCase();
43
- if (!v) return '';
44
- if (v === 'light' || v === 'server-light' || v === 'happy-server-light') return 'happy-server-light';
45
- if (v === 'server' || v === 'full' || v === 'happy-server') return 'happy-server';
46
- return '';
47
- }
48
-
49
- function boolFromFlags({ flags, onFlag, offFlag, defaultValue }) {
50
- if (flags.has(offFlag)) return false;
51
- if (flags.has(onFlag)) return true;
52
- return defaultValue;
53
- }
54
-
55
- async function commandExists(cmd) {
56
- try {
57
- const out = (await runCapture('sh', ['-lc', `command -v ${cmd} >/dev/null 2>&1 && echo yes || echo no`])).trim();
58
- return out === 'yes';
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- async function openUrl(url) {
65
- const u = String(url ?? '').trim();
66
- if (!u) return false;
67
- if (process.platform === 'darwin') {
68
- await run('open', [u]).catch(() => {});
69
- return true;
70
- }
71
- if (process.platform === 'linux') {
72
- if (await commandExists('xdg-open')) {
73
- await run('xdg-open', [u]).catch(() => {});
74
- return true;
75
- }
76
- return false;
77
- }
78
- return false;
79
- }
80
-
81
- async function waitForHealthOk(internalServerUrl, { timeoutMs = 60_000 } = {}) {
82
- const deadline = Date.now() + timeoutMs;
83
- while (Date.now() < deadline) {
84
- // eslint-disable-next-line no-await-in-loop
85
- const health = await fetchHappyHealth(internalServerUrl);
86
- if (health.ok) {
87
- return true;
88
- }
89
- // eslint-disable-next-line no-await-in-loop
90
- await new Promise((r) => setTimeout(r, 300));
91
- }
92
- return false;
93
- }
94
-
95
- function parseEnvFileText(text) {
96
- try {
97
- return parseDotenv(text ?? '');
98
- } catch {
99
- return new Map();
100
- }
101
- }
102
-
103
- async function readEnvValueFromFile(envPath, key) {
104
- try {
105
- if (!envPath || !existsSync(envPath)) return '';
106
- const raw = await readFile(envPath, 'utf-8');
107
- const parsed = parseEnvFileText(raw);
108
- return (parsed.get(key) ?? '').trim();
109
- } catch {
110
- return '';
111
- }
112
- }
18
+ import { getDevAuthKeyPath } from './utils/auth/dev_key.mjs';
19
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
20
+ import { boolFromFlags, boolFromFlagsOrKv } from './utils/cli/flags.mjs';
21
+ import { normalizeProfile, normalizeServerComponent } from './utils/cli/normalize.mjs';
22
+ import { openUrlInBrowser } from './utils/ui/browser.mjs';
23
+ import { commandExists } from './utils/proc/commands.mjs';
24
+ import { readEnvValueFromFile } from './utils/env/read.mjs';
25
+ import { readServerPortFromEnvFile, resolveServerPortFromEnv } from './utils/server/port.mjs';
113
26
 
114
27
  async function resolveMainServerPort() {
115
28
  // Priority:
116
29
  // - explicit env var
117
30
  // - main stack env file (preferred)
118
31
  // - default
119
- const fromEnv =
120
- (process.env.HAPPY_LOCAL_SERVER_PORT ?? process.env.HAPPY_STACKS_SERVER_PORT ?? '').toString().trim();
121
- if (fromEnv) {
122
- const n = Number(fromEnv);
123
- return Number.isFinite(n) && n > 0 ? n : 3005;
32
+ const hasEnvOverride =
33
+ (process.env.HAPPY_STACKS_SERVER_PORT ?? process.env.HAPPY_LOCAL_SERVER_PORT ?? '').toString().trim() !== '';
34
+ if (hasEnvOverride) {
35
+ return resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
124
36
  }
125
37
  const envPath = resolveStackEnvPath('main').envPath;
126
- const v =
127
- (await readEnvValueFromFile(envPath, 'HAPPY_LOCAL_SERVER_PORT')) ||
128
- (await readEnvValueFromFile(envPath, 'HAPPY_STACKS_SERVER_PORT')) ||
129
- '';
130
- if (v) {
131
- const n = Number(v);
132
- return Number.isFinite(n) && n > 0 ? n : 3005;
133
- }
134
- return 3005;
38
+ return await readServerPortFromEnvFile(envPath, { defaultPort: 3005 });
135
39
  }
136
40
 
137
41
  async function ensureSetupConfigPersisted({ rootDir, profile, serverComponent, tailscaleWanted, menubarMode }) {
@@ -393,8 +297,8 @@ async function cmdSetup({ rootDir, argv }) {
393
297
  const supportsAutostart = platform === 'darwin' || platform === 'linux';
394
298
  const supportsMenubar = platform === 'darwin';
395
299
 
396
- const serverFromArg = normalizeServer(kv.get('--server'));
397
- let serverComponent = serverFromArg || normalizeServer(process.env.HAPPY_STACKS_SERVER_COMPONENT) || 'happy-server-light';
300
+ const serverFromArg = normalizeServerComponent(kv.get('--server'));
301
+ let serverComponent = serverFromArg || normalizeServerComponent(process.env.HAPPY_STACKS_SERVER_COMPONENT) || 'happy-server-light';
398
302
  if (profile === 'selfhost' && interactive && !serverFromArg) {
399
303
  serverComponent = await withRl(async (rl) => {
400
304
  const picked = await promptSelect(rl, {
@@ -645,12 +549,12 @@ async function cmdSetup({ rootDir, argv }) {
645
549
  // eslint-disable-next-line no-console
646
550
  console.log(res.enableUrl);
647
551
  // Best-effort open
648
- await openUrl(res.enableUrl);
552
+ await openUrlInBrowser(res.enableUrl).catch(() => {});
649
553
  }
650
554
  } catch (e) {
651
555
  // eslint-disable-next-line no-console
652
556
  console.log('[setup] tailscale not available. Install it from: https://tailscale.com/download');
653
- await openUrl('https://tailscale.com/download');
557
+ await openUrlInBrowser('https://tailscale.com/download').catch(() => {});
654
558
  }
655
559
  }
656
560
 
@@ -664,7 +568,7 @@ async function cmdSetup({ rootDir, argv }) {
664
568
  await spawnDetachedNodeScript({ rootDir, rel: 'scripts/run.mjs', args: [] });
665
569
  }
666
570
 
667
- const ready = await waitForHealthOk(internalServerUrl, { timeoutMs: 90_000 });
571
+ const ready = await waitForHappyHealthOk(internalServerUrl, { timeoutMs: 90_000 });
668
572
  if (!ready) {
669
573
  // eslint-disable-next-line no-console
670
574
  console.log(`[setup] started, but server did not become healthy yet: ${internalServerUrl}`);
@@ -742,7 +646,7 @@ async function cmdSetup({ rootDir, argv }) {
742
646
  console.log('[setup] tip: when you are ready, authenticate with: happys auth login');
743
647
  }
744
648
 
745
- await openUrl(openTarget);
649
+ await openUrlInBrowser(openTarget).catch(() => {});
746
650
  // eslint-disable-next-line no-console
747
651
  console.log(`[setup] open: ${openTarget}`);
748
652
  }
@@ -1,43 +1,26 @@
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 { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
5
- import { run } from './utils/proc.mjs';
6
- import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
4
+ import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
5
+ import { run } from './utils/proc/proc.mjs';
6
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
7
+ import { parseGithubPullRequest } from './utils/git/refs.mjs';
8
+ import { sanitizeStackName } from './utils/stack/names.mjs';
7
9
  import { existsSync } from 'node:fs';
8
10
  import { homedir } from 'node:os';
9
11
  import { join } from 'node:path';
10
12
 
11
- function sanitizeStackName(raw) {
12
- const s = String(raw ?? '')
13
- .trim()
14
- .toLowerCase()
15
- .replace(/[^a-z0-9-]+/g, '-')
16
- .replace(/-+/g, '-')
17
- .replace(/^-+/, '')
18
- .replace(/-+$/, '');
19
- return s || 'pr';
20
- }
21
-
22
- function parseGithubPullRequestNumber(input) {
23
- const raw = String(input ?? '').trim();
24
- if (!raw) return null;
25
- if (/^\d+$/.test(raw)) return Number(raw);
26
- const m = raw.match(/github\.com\/[^/]+\/[^/]+\/pull\/(?<num>\d+)/);
27
- return m?.groups?.num ? Number(m.groups.num) : null;
28
- }
29
-
30
13
  function inferStackNameFromPrArgs({ happy, happyCli, server, serverLight }) {
31
14
  const parts = [];
32
- const hn = parseGithubPullRequestNumber(happy);
33
- const cn = parseGithubPullRequestNumber(happyCli);
34
- const sn = parseGithubPullRequestNumber(server);
35
- const sln = parseGithubPullRequestNumber(serverLight);
15
+ const hn = parseGithubPullRequest(happy)?.number ?? null;
16
+ const cn = parseGithubPullRequest(happyCli)?.number ?? null;
17
+ const sn = parseGithubPullRequest(server)?.number ?? null;
18
+ const sln = parseGithubPullRequest(serverLight)?.number ?? null;
36
19
  if (hn) parts.push(`happy${hn}`);
37
20
  if (cn) parts.push(`cli${cn}`);
38
21
  if (sn) parts.push(`server${sn}`);
39
22
  if (sln) parts.push(`light${sln}`);
40
- return sanitizeStackName(parts.length ? `pr-${parts.join('-')}` : 'pr');
23
+ return sanitizeStackName(parts.length ? `pr-${parts.join('-')}` : 'pr', { fallback: 'pr', maxLen: 64 });
41
24
  }
42
25
 
43
26
  function detectBestAuthSource() {