happy-stacks 0.0.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.
- package/README.md +22 -4
- package/bin/happys.mjs +76 -5
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +16 -4
- package/extras/swiftbar/auth-login.sh +5 -5
- package/extras/swiftbar/happy-stacks.5s.sh +83 -41
- package/extras/swiftbar/happys-term.sh +151 -0
- package/extras/swiftbar/happys.sh +52 -0
- package/extras/swiftbar/lib/render.sh +74 -56
- package/extras/swiftbar/lib/system.sh +37 -6
- package/extras/swiftbar/lib/utils.sh +180 -4
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +2 -13
- package/extras/swiftbar/set-server-flavor.sh +8 -8
- package/extras/swiftbar/wt-pr.sh +1 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +374 -3
- package/scripts/daemon.mjs +78 -11
- package/scripts/dev.mjs +122 -17
- package/scripts/init.mjs +238 -32
- package/scripts/migrate.mjs +292 -0
- package/scripts/mobile.mjs +51 -19
- package/scripts/run.mjs +118 -26
- package/scripts/service.mjs +176 -37
- package/scripts/stack.mjs +665 -22
- package/scripts/stop.mjs +157 -0
- package/scripts/tailscale.mjs +147 -21
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/utils/cli_registry.mjs +23 -0
- package/scripts/utils/config.mjs +9 -1
- package/scripts/utils/env.mjs +37 -15
- package/scripts/utils/expo.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +430 -0
- package/scripts/utils/pm.mjs +11 -2
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +46 -5
- package/scripts/utils/server.mjs +37 -0
- package/scripts/utils/stack_stop.mjs +206 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/worktrees.mjs +53 -7
package/scripts/mobile.mjs
CHANGED
|
@@ -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
|
|
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 = ['
|
|
176
|
+
const prebuildArgs = ['prebuild', '--no-install', '--platform', platform];
|
|
159
177
|
if (shouldClean) {
|
|
160
178
|
prebuildArgs.push('--clean');
|
|
161
179
|
}
|
|
162
|
-
await
|
|
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 = ['
|
|
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
|
|
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
|
|
284
|
-
if (
|
|
285
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
'
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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 =
|
|
261
|
+
const localUi = effectiveInternalServerUrl.replace(/\/+$/, '') + '/';
|
|
171
262
|
console.log(`[local] ui served locally at ${localUi}`);
|
|
172
|
-
if (publicServerUrl && publicServerUrl !==
|
|
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=\"${
|
|
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
|
-
|
|
299
|
+
await stopLocalDaemon({ cliBin, internalServerUrl: effectiveInternalServerUrl, cliHomeDir });
|
|
208
300
|
}
|
|
209
301
|
|
|
210
302
|
for (const child of children) {
|