happy-stacks 0.1.0 → 0.1.2

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 (42) hide show
  1. package/README.md +22 -4
  2. package/bin/happys.mjs +76 -5
  3. package/docs/server-flavors.md +61 -2
  4. package/docs/stacks.md +16 -4
  5. package/extras/swiftbar/auth-login.sh +5 -5
  6. package/extras/swiftbar/happy-stacks.5s.sh +83 -41
  7. package/extras/swiftbar/happys-term.sh +151 -0
  8. package/extras/swiftbar/happys.sh +52 -0
  9. package/extras/swiftbar/lib/render.sh +74 -56
  10. package/extras/swiftbar/lib/system.sh +37 -6
  11. package/extras/swiftbar/lib/utils.sh +180 -4
  12. package/extras/swiftbar/pnpm-term.sh +2 -122
  13. package/extras/swiftbar/pnpm.sh +2 -13
  14. package/extras/swiftbar/set-server-flavor.sh +8 -8
  15. package/extras/swiftbar/wt-pr.sh +1 -1
  16. package/package.json +1 -1
  17. package/scripts/auth.mjs +374 -3
  18. package/scripts/daemon.mjs +78 -11
  19. package/scripts/dev.mjs +122 -17
  20. package/scripts/init.mjs +238 -32
  21. package/scripts/migrate.mjs +292 -0
  22. package/scripts/mobile.mjs +51 -19
  23. package/scripts/run.mjs +118 -26
  24. package/scripts/service.mjs +176 -37
  25. package/scripts/stack.mjs +665 -22
  26. package/scripts/stop.mjs +157 -0
  27. package/scripts/tailscale.mjs +147 -21
  28. package/scripts/typecheck.mjs +145 -0
  29. package/scripts/ui_gateway.mjs +248 -0
  30. package/scripts/uninstall.mjs +3 -3
  31. package/scripts/utils/cli_registry.mjs +23 -0
  32. package/scripts/utils/config.mjs +9 -1
  33. package/scripts/utils/env.mjs +37 -15
  34. package/scripts/utils/expo.mjs +94 -0
  35. package/scripts/utils/happy_server_infra.mjs +430 -0
  36. package/scripts/utils/pm.mjs +11 -2
  37. package/scripts/utils/ports.mjs +51 -13
  38. package/scripts/utils/proc.mjs +46 -5
  39. package/scripts/utils/server.mjs +37 -0
  40. package/scripts/utils/stack_stop.mjs +206 -0
  41. package/scripts/utils/validate.mjs +42 -1
  42. package/scripts/worktrees.mjs +53 -7
@@ -1,10 +1,11 @@
1
1
  import './utils/env.mjs';
2
2
  import { parseArgs } from './utils/args.mjs';
3
- import { killPortListeners } from './utils/ports.mjs';
3
+ import { killPortListeners, pickNextFreeTcpPort } from './utils/ports.mjs';
4
4
  import { run, runCapture, spawnProc } from './utils/proc.mjs';
5
- import { getComponentDir, getRootDir } from './utils/paths.mjs';
6
- import { ensureDepsInstalled, requireDir } from './utils/pm.mjs';
5
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
6
+ import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/pm.mjs';
7
7
  import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
8
+ import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo.mjs';
8
9
 
9
10
  /**
10
11
  * Mobile dev helper for the embedded `components/happy` Expo app.
@@ -25,6 +26,7 @@ async function main() {
25
26
  const argv = process.argv.slice(2);
26
27
  const { flags, kv } = parseArgs(argv);
27
28
  const json = wantsJson(argv, { flags });
29
+ const restart = flags.has('--restart');
28
30
 
29
31
  if (wantsHelp(argv, { flags })) {
30
32
  printResult({
@@ -40,6 +42,7 @@ async function main() {
40
42
  '--prebuild [--platform=ios|all] [--clean]',
41
43
  '--run-ios [--device=<id-or-name>] [--configuration=Debug|Release]',
42
44
  '--metro / --no-metro',
45
+ '--restart',
43
46
  '--no-signing-fix',
44
47
  ],
45
48
  json: true,
@@ -47,6 +50,7 @@ async function main() {
47
50
  text: [
48
51
  '[mobile] usage:',
49
52
  ' happys mobile [--host=lan|localhost|tunnel] [--port=8081] [--scheme=...] [--json]',
53
+ ' happys mobile --restart # force-restart Metro for this stack/worktree',
50
54
  ' happys mobile --run-ios [--device=...] [--configuration=Debug|Release]',
51
55
  ' happys mobile --prebuild [--platform=ios|all] [--clean]',
52
56
  ' happys mobile --no-metro # just build/install (if --run-ios) without starting Metro',
@@ -107,7 +111,7 @@ async function main() {
107
111
  process.env.HAPPY_LOCAL_MOBILE_SCHEME ??
108
112
  iosBundleId;
109
113
  const host = kv.get('--host') ?? process.env.HAPPY_STACKS_MOBILE_HOST ?? process.env.HAPPY_LOCAL_MOBILE_HOST ?? 'lan';
110
- const port = kv.get('--port') ?? process.env.HAPPY_STACKS_MOBILE_PORT ?? process.env.HAPPY_LOCAL_MOBILE_PORT ?? '8081';
114
+ const portRaw = kv.get('--port') ?? process.env.HAPPY_STACKS_MOBILE_PORT ?? process.env.HAPPY_LOCAL_MOBILE_PORT ?? '8081';
111
115
  // Default behavior:
112
116
  // - `happys mobile` starts Metro and keeps running.
113
117
  // - `happys mobile --run-ios` / `happys mobile:ios` just builds/installs and exits (unless --metro is provided).
@@ -120,6 +124,20 @@ async function main() {
120
124
  APP_ENV: appEnv,
121
125
  };
122
126
 
127
+ const autostart = getDefaultAutostartPaths();
128
+ const mobilePaths = getExpoStatePaths({
129
+ baseDir: autostart.baseDir,
130
+ kind: 'mobile-dev',
131
+ projectDir: uiDir,
132
+ stateFileName: 'mobile.state.json',
133
+ });
134
+ await ensureExpoIsolationEnv({
135
+ env,
136
+ stateDir: mobilePaths.stateDir,
137
+ expoHomeDir: mobilePaths.expoHomeDir,
138
+ tmpDir: mobilePaths.tmpDir,
139
+ });
140
+
123
141
  // Allow happy-stacks to define the default server URL baked into the app bundle.
124
142
  // This is read by the app via `process.env.EXPO_PUBLIC_HAPPY_SERVER_URL`.
125
143
  const stacksServerUrl =
@@ -139,7 +157,7 @@ async function main() {
139
157
  iosBundleId,
140
158
  scheme,
141
159
  host,
142
- port,
160
+ port: portRaw,
143
161
  shouldPrebuild: flags.has('--prebuild'),
144
162
  shouldRunIos: flags.has('--run-ios'),
145
163
  shouldStartMetro,
@@ -155,11 +173,11 @@ async function main() {
155
173
  const shouldClean = flags.has('--clean');
156
174
  // Prebuild can fail during `pod install` if deployment target mismatches.
157
175
  // We skip installs, patch deployment target + RN build mode, then run `pod install` ourselves.
158
- const prebuildArgs = ['expo', 'prebuild', '--no-install', '--platform', platform];
176
+ const prebuildArgs = ['prebuild', '--no-install', '--platform', platform];
159
177
  if (shouldClean) {
160
178
  prebuildArgs.push('--clean');
161
179
  }
162
- await run('npx', prebuildArgs, { cwd: uiDir, env });
180
+ await pmExecBin({ dir: uiDir, bin: 'expo', args: prebuildArgs, env });
163
181
 
164
182
  // Always patch iOS props if iOS was generated.
165
183
  if (platform === 'ios' || platform === 'all') {
@@ -266,35 +284,49 @@ async function main() {
266
284
  }
267
285
 
268
286
  const configuration = kv.get('--configuration') ?? 'Debug';
269
- const args = ['expo', 'run:ios', '--no-bundler', '--no-build-cache', '--configuration', configuration];
287
+ const args = ['run:ios', '--no-bundler', '--no-build-cache', '--configuration', configuration];
270
288
  if (device) {
271
289
  args.push('-d', device);
272
290
  }
273
291
  // Ensure CocoaPods doesn't crash due to locale issues.
274
292
  env.LANG = env.LANG ?? 'en_US.UTF-8';
275
293
  env.LC_ALL = env.LC_ALL ?? 'en_US.UTF-8';
276
- await run('npx', args, { cwd: uiDir, env });
294
+ await pmExecBin({ dir: uiDir, bin: 'expo', args, env });
277
295
  }
278
296
 
279
297
  if (!shouldStartMetro) {
280
298
  return;
281
299
  }
282
300
 
283
- const portNumber = Number.parseInt(port, 10);
284
- if (Number.isFinite(portNumber) && portNumber > 0) {
285
- await killPortListeners(portNumber, { label: 'expo' });
301
+ const running = await isStateProcessRunning(mobilePaths.statePath);
302
+ if (!restart && running.running) {
303
+ // eslint-disable-next-line no-console
304
+ console.log(`[mobile] Metro already running for this stack/worktree (pid=${running.state.pid}, port=${running.state.port})`);
305
+ return;
306
+ }
307
+ if (restart && running.state?.pid) {
308
+ const prevPid = Number(running.state.pid);
309
+ const prevPort = Number(running.state.port);
310
+ if (Number.isFinite(prevPort) && prevPort > 0) {
311
+ await killPortListeners(prevPort, { label: 'expo' });
312
+ }
313
+ await killPid(prevPid);
286
314
  }
287
315
 
316
+ const requestedPort = Number.parseInt(String(portRaw), 10);
317
+ const startPort = Number.isFinite(requestedPort) && requestedPort > 0 ? requestedPort : 8081;
318
+ const portNumber = await pickNextFreeTcpPort(startPort);
319
+ env.RCT_METRO_PORT = String(portNumber);
320
+
288
321
  // Start Metro for a dev client.
289
322
  // The critical part is --scheme: without it, Expo defaults to `exp+<slug>` (here `exp+happy`)
290
323
  // which the App Store app also registers, so iOS can open the wrong app.
291
- spawnProc(
292
- 'mobile',
293
- 'npx',
294
- ['expo', 'start', '--dev-client', '--host', host, '--port', port, '--scheme', scheme],
295
- env,
296
- { cwd: uiDir }
297
- );
324
+ const args = ['start', '--dev-client', '--host', host, '--port', String(portNumber), '--scheme', scheme];
325
+ if (wantsExpoClearCache({ env })) {
326
+ args.push('--clear');
327
+ }
328
+ const child = await pmSpawnBin({ label: 'mobile', dir: uiDir, bin: 'expo', args, env });
329
+ await writePidState(mobilePaths.statePath, { pid: child.pid, port: portNumber, uiDir, startedAt: new Date().toISOString() });
298
330
 
299
331
  await new Promise(() => {});
300
332
  }
package/scripts/run.mjs CHANGED
@@ -1,18 +1,19 @@
1
1
  import './utils/env.mjs';
2
2
  import { parseArgs } from './utils/args.mjs';
3
3
  import { pathExists } from './utils/fs.mjs';
4
- import { killProcessTree, runCapture } from './utils/proc.mjs';
4
+ import { killProcessTree, runCapture, spawnProc } from './utils/proc.mjs';
5
5
  import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
6
6
  import { killPortListeners } from './utils/ports.mjs';
7
- import { getServerComponentName, waitForServerReady } from './utils/server.mjs';
8
- import { pmSpawnScript, requireDir } from './utils/pm.mjs';
7
+ import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server.mjs';
8
+ import { ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/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, resolvePublicServerUrl } from './tailscale.mjs';
13
- import { startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
13
+ import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
14
14
  import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
15
- import { assertServerComponentDirMatches } from './utils/validate.mjs';
15
+ import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
16
+ import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/happy_server_infra.mjs';
16
17
 
17
18
  /**
18
19
  * Run the local stack in "production-like" mode:
@@ -30,10 +31,10 @@ async function main() {
30
31
  if (wantsHelp(argv, { flags })) {
31
32
  printResult({
32
33
  json,
33
- data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon'], json: true },
34
+ data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart'], json: true },
34
35
  text: [
35
36
  '[start] usage:',
36
- ' happys start [--server=happy-server|happy-server-light] [--json]',
37
+ ' happys start [--server=happy-server|happy-server-light] [--restart] [--json]',
37
38
  ' (legacy in a cloned repo): pnpm start [-- --server=happy-server|happy-server-light] [--json]',
38
39
  ' note: --json prints the resolved config (dry-run) and exits.',
39
40
  ].join('\n'),
@@ -62,11 +63,12 @@ async function main() {
62
63
 
63
64
  const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
64
65
  const serveUiWanted = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
65
- const serveUi = serveUiWanted && serverComponentName === 'happy-server-light';
66
+ const serveUi = serveUiWanted;
66
67
  const uiPrefix = process.env.HAPPY_LOCAL_UI_PREFIX?.trim() ? process.env.HAPPY_LOCAL_UI_PREFIX.trim() : '/';
68
+ const autostart = getDefaultAutostartPaths();
67
69
  const uiBuildDir = process.env.HAPPY_LOCAL_UI_BUILD_DIR?.trim()
68
70
  ? process.env.HAPPY_LOCAL_UI_BUILD_DIR.trim()
69
- : join(getDefaultAutostartPaths().baseDir, 'ui');
71
+ : join(autostart.baseDir, 'ui');
70
72
 
71
73
  const enableTailscaleServe = (process.env.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0') === '1';
72
74
 
@@ -74,6 +76,7 @@ async function main() {
74
76
  const cliDir = getComponentDir(rootDir, 'happy-cli');
75
77
 
76
78
  assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
79
+ assertServerPrismaProviderMatches({ serverComponentName, serverDir });
77
80
 
78
81
  await requireDir(serverComponentName, serverDir);
79
82
  await requireDir('happy-cli', cliDir);
@@ -82,7 +85,8 @@ async function main() {
82
85
 
83
86
  const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
84
87
  ? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
85
- : join(getDefaultAutostartPaths().baseDir, 'cli');
88
+ : join(autostart.baseDir, 'cli');
89
+ const restart = flags.has('--restart');
86
90
 
87
91
  if (json) {
88
92
  printResult({
@@ -105,18 +109,21 @@ async function main() {
105
109
  return;
106
110
  }
107
111
 
108
- if (serveUiWanted && !serveUi) {
109
- console.log(`[local] ui serving disabled (requires happy-server-light; you are using ${serverComponentName})`);
110
- }
111
-
112
112
  if (serveUi && !(await pathExists(uiBuildDir))) {
113
- throw new Error(`[local] UI build directory not found at ${uiBuildDir}. Run: happys build (legacy in a cloned repo: pnpm build)`);
113
+ if (serverComponentName === 'happy-server-light') {
114
+ throw new Error(`[local] UI build directory not found at ${uiBuildDir}. Run: happys build (legacy in a cloned repo: pnpm build)`);
115
+ }
116
+ // For happy-server, UI serving is optional via the UI gateway.
117
+ console.log(`[local] UI build directory not found at ${uiBuildDir}; UI gateway will be disabled`);
114
118
  }
115
119
 
116
120
  const children = [];
117
121
  let shuttingDown = false;
118
122
  const baseEnv = { ...process.env };
119
123
 
124
+ // Ensure server deps exist before any Prisma/docker work.
125
+ await ensureDepsInstalled(serverDir, serverComponentName);
126
+
120
127
  // Public URL automation: auto-prefer https://*.ts.net on every start.
121
128
  const resolved = await resolvePublicServerUrl({
122
129
  internalServerUrl,
@@ -126,9 +133,18 @@ async function main() {
126
133
  });
127
134
  publicServerUrl = resolved.publicServerUrl;
128
135
 
136
+ const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
137
+ const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
138
+ if (!restart && serverAlreadyRunning && (!startDaemon || daemonAlreadyRunning)) {
139
+ console.log(`[local] start: stack already running (server=${internalServerUrl}${startDaemon ? ` daemon=${daemonAlreadyRunning ? 'running' : 'stopped'}` : ''})`);
140
+ return;
141
+ }
142
+
129
143
  // Server
130
144
  // If a previous run left a server behind, free the port first (prevents false "ready" checks).
131
- await killPortListeners(serverPort, { label: 'server' });
145
+ if (!serverAlreadyRunning || restart) {
146
+ await killPortListeners(serverPort, { label: 'server' });
147
+ }
132
148
 
133
149
  const serverEnv = {
134
150
  ...baseEnv,
@@ -138,19 +154,94 @@ async function main() {
138
154
  // Avoid noisy failures if a previous run left the metrics port busy.
139
155
  // You can override with METRICS_ENABLED=true if you want it.
140
156
  METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
141
- ...(serveUi
157
+ ...(serveUi && serverComponentName === 'happy-server-light'
142
158
  ? {
143
159
  HAPPY_SERVER_LIGHT_UI_DIR: uiBuildDir,
144
160
  HAPPY_SERVER_LIGHT_UI_PREFIX: uiPrefix,
145
161
  }
146
162
  : {}),
147
163
  };
164
+ if (serverComponentName === 'happy-server-light') {
165
+ const dataDir = baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR?.trim()
166
+ ? baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR.trim()
167
+ : join(autostart.baseDir, 'server-light');
168
+ serverEnv.HAPPY_SERVER_LIGHT_DATA_DIR = dataDir;
169
+ serverEnv.HAPPY_SERVER_LIGHT_FILES_DIR = baseEnv.HAPPY_SERVER_LIGHT_FILES_DIR?.trim()
170
+ ? baseEnv.HAPPY_SERVER_LIGHT_FILES_DIR.trim()
171
+ : join(dataDir, 'files');
172
+ serverEnv.DATABASE_URL = baseEnv.DATABASE_URL?.trim()
173
+ ? baseEnv.DATABASE_URL.trim()
174
+ : `file:${join(dataDir, 'happy-server-light.sqlite')}`;
175
+
176
+ // Optional: update SQLite schema on interactive start only.
177
+ // We intentionally do NOT run this under launchd KeepAlive (no TTY) to avoid restart loops.
178
+ const prismaPushOnStart = (baseEnv.HAPPY_STACKS_PRISMA_PUSH ?? baseEnv.HAPPY_LOCAL_PRISMA_PUSH ?? '').trim() === '1';
179
+ if (prismaPushOnStart && process.stdout.isTTY) {
180
+ await pmExecBin({ dir: serverDir, bin: 'prisma', args: ['db', 'push'], env: serverEnv });
181
+ }
182
+ }
183
+ let effectiveInternalServerUrl = internalServerUrl;
184
+ if (serverComponentName === 'happy-server') {
185
+ const managed = (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0';
186
+ if (managed) {
187
+ const envPath = baseEnv.HAPPY_STACKS_ENV_FILE ?? baseEnv.HAPPY_LOCAL_ENV_FILE ?? '';
188
+ const infra = await ensureHappyServerManagedInfra({
189
+ stackName: autostart.stackName,
190
+ baseDir: autostart.baseDir,
191
+ serverPort,
192
+ publicServerUrl,
193
+ envPath,
194
+ env: baseEnv,
195
+ });
196
+
197
+ // Backend runs on a separate port; gateway owns the public port.
198
+ const backendPortRaw = (baseEnv.HAPPY_STACKS_HAPPY_SERVER_BACKEND_PORT ?? baseEnv.HAPPY_LOCAL_HAPPY_SERVER_BACKEND_PORT ?? '').trim();
199
+ const backendPort = backendPortRaw ? Number(backendPortRaw) : serverPort + 10;
200
+ const backendUrl = `http://127.0.0.1:${backendPort}`;
201
+ await killPortListeners(backendPort, { label: 'happy-server-backend' });
202
+
203
+ const backendEnv = { ...serverEnv, ...infra.env, PORT: String(backendPort) };
204
+ const autoMigrate = (baseEnv.HAPPY_STACKS_PRISMA_MIGRATE ?? baseEnv.HAPPY_LOCAL_PRISMA_MIGRATE ?? '1') !== '0';
205
+ if (autoMigrate) {
206
+ await applyHappyServerMigrations({ serverDir, env: backendEnv });
207
+ }
208
+
209
+ const backend = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: backendEnv });
210
+ children.push(backend);
211
+ await waitForServerReady(backendUrl);
212
+
213
+ const gatewayArgs = [
214
+ join(rootDir, 'scripts', 'ui_gateway.mjs'),
215
+ `--port=${serverPort}`,
216
+ `--backend-url=${backendUrl}`,
217
+ `--minio-port=${infra.env.S3_PORT}`,
218
+ `--bucket=${infra.env.S3_BUCKET}`,
219
+ ];
220
+ if (serveUi && (await pathExists(uiBuildDir))) {
221
+ gatewayArgs.push(`--ui-dir=${uiBuildDir}`);
222
+ } else {
223
+ gatewayArgs.push('--no-ui');
224
+ }
148
225
 
149
- const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'dev', env: serverEnv });
150
- children.push(server);
226
+ const gateway = spawnProc('ui', process.execPath, gatewayArgs, { ...backendEnv, PORT: String(serverPort) }, { cwd: rootDir });
227
+ children.push(gateway);
228
+ await waitForServerReady(internalServerUrl);
229
+ effectiveInternalServerUrl = internalServerUrl;
151
230
 
152
- await waitForServerReady(internalServerUrl);
153
- console.log(`[local] server ready at ${internalServerUrl}`);
231
+ // Skip default server spawn below
232
+ }
233
+ }
234
+
235
+ // Default server start (happy-server-light, or happy-server without managed infra).
236
+ if (!(serverComponentName === 'happy-server' && (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0')) {
237
+ if (!serverAlreadyRunning || restart) {
238
+ const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: serverEnv });
239
+ children.push(server);
240
+ await waitForServerReady(internalServerUrl);
241
+ } else {
242
+ console.log(`[local] server already running at ${internalServerUrl}`);
243
+ }
244
+ }
154
245
 
155
246
  if (enableTailscaleServe) {
156
247
  try {
@@ -167,9 +258,9 @@ async function main() {
167
258
  }
168
259
 
169
260
  if (serveUi) {
170
- const localUi = internalServerUrl.replace(/\/+$/, '') + '/';
261
+ const localUi = effectiveInternalServerUrl.replace(/\/+$/, '') + '/';
171
262
  console.log(`[local] ui served locally at ${localUi}`);
172
- if (publicServerUrl && publicServerUrl !== internalServerUrl && publicServerUrl !== localUi && publicServerUrl !== defaultPublicUrl) {
263
+ if (publicServerUrl && publicServerUrl !== effectiveInternalServerUrl && publicServerUrl !== localUi && publicServerUrl !== defaultPublicUrl) {
173
264
  const pubUi = publicServerUrl.replace(/\/+$/, '') + '/';
174
265
  console.log(`[local] public url: ${pubUi}`);
175
266
  }
@@ -179,7 +270,7 @@ async function main() {
179
270
 
180
271
  console.log(
181
272
  `[local] tip: to run 'happy' from your terminal *against this local server* (and have sessions show up in the UI), use:\n` +
182
- `export HAPPY_SERVER_URL=\"${internalServerUrl}\"\n` +
273
+ `export HAPPY_SERVER_URL=\"${effectiveInternalServerUrl}\"\n` +
183
274
  `export HAPPY_HOME_DIR=\"${cliHomeDir}\"\n` +
184
275
  `export HAPPY_WEBAPP_URL=\"${publicServerUrl}\"\n`
185
276
  );
@@ -190,9 +281,10 @@ async function main() {
190
281
  await startLocalDaemonWithAuth({
191
282
  cliBin,
192
283
  cliHomeDir,
193
- internalServerUrl,
284
+ internalServerUrl: effectiveInternalServerUrl,
194
285
  publicServerUrl,
195
286
  isShuttingDown: () => shuttingDown,
287
+ forceRestart: restart,
196
288
  });
197
289
  }
198
290
 
@@ -204,7 +296,7 @@ async function main() {
204
296
  console.log('\n[local] shutting down...');
205
297
 
206
298
  if (startDaemon) {
207
- await stopLocalDaemon({ cliBin, internalServerUrl, cliHomeDir });
299
+ await stopLocalDaemon({ cliBin, internalServerUrl: effectiveInternalServerUrl, cliHomeDir });
208
300
  }
209
301
 
210
302
  for (const child of children) {