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/daemon.mjs
CHANGED
|
@@ -61,6 +61,52 @@ export function cleanupStaleDaemonState(homeDir) {
|
|
|
61
61
|
try { unlinkSync(statePath); } catch { /* ignore */ }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export function checkDaemonState(cliHomeDir) {
|
|
65
|
+
const statePath = join(cliHomeDir, 'daemon.state.json');
|
|
66
|
+
const lockPath = join(cliHomeDir, 'daemon.state.json.lock');
|
|
67
|
+
|
|
68
|
+
const alive = (pid) => {
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (existsSync(statePath)) {
|
|
78
|
+
try {
|
|
79
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
80
|
+
const pid = Number(state?.pid);
|
|
81
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
82
|
+
return alive(pid) ? { status: 'running', pid } : { status: 'stale_state', pid };
|
|
83
|
+
}
|
|
84
|
+
return { status: 'bad_state', pid: null };
|
|
85
|
+
} catch {
|
|
86
|
+
return { status: 'bad_state', pid: null };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (existsSync(lockPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const pid = Number(readFileSync(lockPath, 'utf-8').trim());
|
|
93
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
94
|
+
return alive(pid) ? { status: 'starting', pid } : { status: 'stale_lock', pid };
|
|
95
|
+
}
|
|
96
|
+
return { status: 'bad_lock', pid: null };
|
|
97
|
+
} catch {
|
|
98
|
+
return { status: 'bad_lock', pid: null };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { status: 'stopped', pid: null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isDaemonRunning(cliHomeDir) {
|
|
106
|
+
const s = checkDaemonState(cliHomeDir);
|
|
107
|
+
return s.status === 'running' || s.status === 'starting';
|
|
108
|
+
}
|
|
109
|
+
|
|
64
110
|
function getLatestDaemonLogPath(homeDir) {
|
|
65
111
|
try {
|
|
66
112
|
const logsDir = join(homeDir, 'logs');
|
|
@@ -95,6 +141,11 @@ function authLoginHint() {
|
|
|
95
141
|
return stackName === 'main' ? 'happys auth login' : `happys stack auth ${stackName} login`;
|
|
96
142
|
}
|
|
97
143
|
|
|
144
|
+
function authCopyFromMainHint() {
|
|
145
|
+
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
146
|
+
return stackName === 'main' ? null : `happys stack auth ${stackName} copy-from main`;
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
async function seedCredentialsIfMissing({ cliHomeDir }) {
|
|
99
150
|
const sources = [
|
|
100
151
|
// Legacy happy-local storage root (most common for existing users).
|
|
@@ -247,7 +298,9 @@ export async function startLocalDaemonWithAuth({
|
|
|
247
298
|
internalServerUrl,
|
|
248
299
|
publicServerUrl,
|
|
249
300
|
isShuttingDown,
|
|
301
|
+
forceRestart = false,
|
|
250
302
|
}) {
|
|
303
|
+
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
251
304
|
const baseEnv = { ...process.env };
|
|
252
305
|
const daemonEnv = getDaemonEnv({ baseEnv, cliHomeDir, internalServerUrl, publicServerUrl });
|
|
253
306
|
|
|
@@ -255,16 +308,14 @@ export async function startLocalDaemonWithAuth({
|
|
|
255
308
|
// to avoid requiring an interactive auth flow under launchd.
|
|
256
309
|
await seedCredentialsIfMissing({ cliHomeDir });
|
|
257
310
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
proc.on('exit', () => resolve());
|
|
264
|
-
});
|
|
265
|
-
} catch {
|
|
266
|
-
// ignore
|
|
311
|
+
const existing = checkDaemonState(cliHomeDir);
|
|
312
|
+
if (!forceRestart && (existing.status === 'running' || existing.status === 'starting')) {
|
|
313
|
+
// eslint-disable-next-line no-console
|
|
314
|
+
console.log(`[local] daemon already running for stack home (pid=${existing.pid})`);
|
|
315
|
+
return;
|
|
267
316
|
}
|
|
317
|
+
|
|
318
|
+
// Stop any existing daemon for THIS stack home dir.
|
|
268
319
|
try {
|
|
269
320
|
await new Promise((resolve) => {
|
|
270
321
|
const proc = spawnProc('daemon', cliBin, ['daemon', 'stop'], daemonEnv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -274,12 +325,26 @@ export async function startLocalDaemonWithAuth({
|
|
|
274
325
|
// ignore
|
|
275
326
|
}
|
|
276
327
|
|
|
328
|
+
// Best-effort: for the main stack, also stop the legacy global daemon home (~/.happy) to prevent legacy overlap.
|
|
329
|
+
if (stackName === 'main') {
|
|
330
|
+
const legacyEnv = { ...daemonEnv, HAPPY_HOME_DIR: join(homedir(), '.happy') };
|
|
331
|
+
try {
|
|
332
|
+
await new Promise((resolve) => {
|
|
333
|
+
const proc = spawnProc('daemon', cliBin, ['daemon', 'stop'], legacyEnv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
334
|
+
proc.on('exit', () => resolve());
|
|
335
|
+
});
|
|
336
|
+
} catch {
|
|
337
|
+
// ignore
|
|
338
|
+
}
|
|
339
|
+
// If state is missing and stop couldn't find it, force-stop the lock PID (otherwise repeated restarts accumulate daemons).
|
|
340
|
+
await killDaemonFromLockFile({ cliHomeDir: join(homedir(), '.happy') });
|
|
341
|
+
cleanupStaleDaemonState(join(homedir(), '.happy'));
|
|
342
|
+
}
|
|
343
|
+
|
|
277
344
|
// If state is missing and stop couldn't find it, force-stop the lock PID (otherwise repeated restarts accumulate daemons).
|
|
278
|
-
await killDaemonFromLockFile({ cliHomeDir: join(homedir(), '.happy') });
|
|
279
345
|
await killDaemonFromLockFile({ cliHomeDir });
|
|
280
346
|
|
|
281
347
|
// Clean up stale lock/state files that can block daemon start.
|
|
282
|
-
cleanupStaleDaemonState(join(homedir(), '.happy'));
|
|
283
348
|
cleanupStaleDaemonState(cliHomeDir);
|
|
284
349
|
|
|
285
350
|
const credentialsPath = join(cliHomeDir, 'access.key');
|
|
@@ -308,11 +373,13 @@ export async function startLocalDaemonWithAuth({
|
|
|
308
373
|
}
|
|
309
374
|
|
|
310
375
|
if (excerptIndicatesMissingAuth(first.excerpt)) {
|
|
376
|
+
const copyHint = authCopyFromMainHint();
|
|
311
377
|
console.error(
|
|
312
378
|
`[local] daemon is not authenticated yet (expected on first run).\n` +
|
|
313
379
|
`[local] Keeping the server running so you can login.\n` +
|
|
314
380
|
`[local] In another terminal, run:\n` +
|
|
315
381
|
`${authLoginHint()}\n` +
|
|
382
|
+
(copyHint ? `[local] Or (recommended if main is already logged in):\n${copyHint}\n` : '') +
|
|
316
383
|
`[local] Waiting for credentials at ${credentialsPath}...`
|
|
317
384
|
);
|
|
318
385
|
|
package/scripts/dev.mjs
CHANGED
|
@@ -2,16 +2,18 @@ import './utils/env.mjs';
|
|
|
2
2
|
import { parseArgs } from './utils/args.mjs';
|
|
3
3
|
import { killProcessTree } from './utils/proc.mjs';
|
|
4
4
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { killPortListeners } from './utils/ports.mjs';
|
|
6
|
-
import { getServerComponentName, waitForServerReady } from './utils/server.mjs';
|
|
7
|
-
import { ensureDepsInstalled, pmSpawnScript, requireDir } from './utils/pm.mjs';
|
|
5
|
+
import { killPortListeners, pickNextFreeTcpPort } from './utils/ports.mjs';
|
|
6
|
+
import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server.mjs';
|
|
7
|
+
import { ensureDepsInstalled, pmSpawnBin, pmSpawnScript, requireDir } from './utils/pm.mjs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
-
import { startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
11
|
+
import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
12
12
|
import { resolvePublicServerUrl } from './tailscale.mjs';
|
|
13
13
|
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
14
|
-
import { assertServerComponentDirMatches } from './utils/validate.mjs';
|
|
14
|
+
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
15
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/happy_server_infra.mjs';
|
|
16
|
+
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo.mjs';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Dev mode stack:
|
|
@@ -27,10 +29,10 @@ async function main() {
|
|
|
27
29
|
if (wantsHelp(argv, { flags })) {
|
|
28
30
|
printResult({
|
|
29
31
|
json,
|
|
30
|
-
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon'], json: true },
|
|
32
|
+
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart'], json: true },
|
|
31
33
|
text: [
|
|
32
34
|
'[dev] usage:',
|
|
33
|
-
' happys dev [--server=happy-server|happy-server-light] [--json]',
|
|
35
|
+
' happys dev [--server=happy-server|happy-server-light] [--restart] [--json]',
|
|
34
36
|
' note: --json prints the resolved config (dry-run) and exits.',
|
|
35
37
|
].join('\n'),
|
|
36
38
|
});
|
|
@@ -60,21 +62,24 @@ async function main() {
|
|
|
60
62
|
|
|
61
63
|
const startUi = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_UI ?? '1') !== '0';
|
|
62
64
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
65
|
+
const restart = flags.has('--restart');
|
|
63
66
|
|
|
64
67
|
const serverDir = getComponentDir(rootDir, serverComponentName);
|
|
65
68
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
66
69
|
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
67
70
|
|
|
68
71
|
assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
|
|
72
|
+
assertServerPrismaProviderMatches({ serverComponentName, serverDir });
|
|
69
73
|
|
|
70
74
|
await requireDir(serverComponentName, serverDir);
|
|
71
75
|
await requireDir('happy', uiDir);
|
|
72
76
|
await requireDir('happy-cli', cliDir);
|
|
73
77
|
|
|
74
78
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
79
|
+
const autostart = getDefaultAutostartPaths();
|
|
75
80
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
76
81
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
77
|
-
: join(
|
|
82
|
+
: join(autostart.baseDir, 'cli');
|
|
78
83
|
|
|
79
84
|
if (json) {
|
|
80
85
|
printResult({
|
|
@@ -100,8 +105,23 @@ async function main() {
|
|
|
100
105
|
let shuttingDown = false;
|
|
101
106
|
const baseEnv = { ...process.env };
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
|
|
109
|
+
const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
|
|
110
|
+
|
|
111
|
+
// UI dev server state (worktree-scoped)
|
|
112
|
+
const uiPaths = getExpoStatePaths({ baseDir: autostart.baseDir, kind: 'ui-dev', projectDir: uiDir, stateFileName: 'ui.state.json' });
|
|
113
|
+
const uiRunning = startUi ? await isStateProcessRunning(uiPaths.statePath) : { running: false, state: null };
|
|
114
|
+
let uiAlreadyRunning = Boolean(uiRunning.running);
|
|
115
|
+
|
|
116
|
+
if (!restart && serverAlreadyRunning && (!startDaemon || daemonAlreadyRunning) && (!startUi || uiAlreadyRunning)) {
|
|
117
|
+
console.log(`[local] dev: stack already running (server=${internalServerUrl}${startDaemon ? ` daemon=${daemonAlreadyRunning ? 'running' : 'stopped'}` : ''}${startUi ? ` ui=${uiAlreadyRunning ? 'running' : 'stopped'}` : ''})`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Start server (only if not already healthy)
|
|
122
|
+
if (!serverAlreadyRunning || restart) {
|
|
123
|
+
await killPortListeners(serverPort, { label: 'server' });
|
|
124
|
+
}
|
|
105
125
|
const serverEnv = {
|
|
106
126
|
...baseEnv,
|
|
107
127
|
PORT: String(serverPort),
|
|
@@ -109,12 +129,56 @@ async function main() {
|
|
|
109
129
|
// Avoid noisy failures if a previous run left the metrics port busy.
|
|
110
130
|
METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
|
|
111
131
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
if (serverComponentName === 'happy-server-light') {
|
|
133
|
+
const dataDir = baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR?.trim()
|
|
134
|
+
? baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR.trim()
|
|
135
|
+
: join(autostart.baseDir, 'server-light');
|
|
136
|
+
serverEnv.HAPPY_SERVER_LIGHT_DATA_DIR = dataDir;
|
|
137
|
+
serverEnv.HAPPY_SERVER_LIGHT_FILES_DIR = baseEnv.HAPPY_SERVER_LIGHT_FILES_DIR?.trim()
|
|
138
|
+
? baseEnv.HAPPY_SERVER_LIGHT_FILES_DIR.trim()
|
|
139
|
+
: join(dataDir, 'files');
|
|
140
|
+
}
|
|
141
|
+
if (serverComponentName === 'happy-server') {
|
|
142
|
+
const managed = (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0';
|
|
143
|
+
if (managed) {
|
|
144
|
+
const envPath = baseEnv.HAPPY_STACKS_ENV_FILE ?? baseEnv.HAPPY_LOCAL_ENV_FILE ?? '';
|
|
145
|
+
const infra = await ensureHappyServerManagedInfra({
|
|
146
|
+
stackName: autostart.stackName,
|
|
147
|
+
baseDir: autostart.baseDir,
|
|
148
|
+
serverPort,
|
|
149
|
+
publicServerUrl,
|
|
150
|
+
envPath,
|
|
151
|
+
env: baseEnv,
|
|
152
|
+
});
|
|
153
|
+
Object.assign(serverEnv, infra.env);
|
|
154
|
+
}
|
|
115
155
|
|
|
116
|
-
|
|
117
|
-
|
|
156
|
+
const autoMigrate = (baseEnv.HAPPY_STACKS_PRISMA_MIGRATE ?? baseEnv.HAPPY_LOCAL_PRISMA_MIGRATE ?? '1') !== '0';
|
|
157
|
+
if (autoMigrate) {
|
|
158
|
+
await applyHappyServerMigrations({ serverDir, env: serverEnv });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await ensureDepsInstalled(serverDir, serverComponentName);
|
|
162
|
+
// For happy-server: the upstream `dev` script is not stack-safe (kills fixed ports, reads .env.dev).
|
|
163
|
+
// Use `start` and rely on stack-scoped env + optional migrations above.
|
|
164
|
+
//
|
|
165
|
+
// For happy-server-light: the upstream `dev` script runs `prisma db push` automatically. If you want to skip
|
|
166
|
+
// it (e.g. big sqlite DB), set HAPPY_STACKS_PRISMA_PUSH=0 to use `start` even in dev mode.
|
|
167
|
+
const prismaPush = (baseEnv.HAPPY_STACKS_PRISMA_PUSH ?? baseEnv.HAPPY_LOCAL_PRISMA_PUSH ?? '1').trim() !== '0';
|
|
168
|
+
const serverScript =
|
|
169
|
+
serverComponentName === 'happy-server'
|
|
170
|
+
? 'start'
|
|
171
|
+
: serverComponentName === 'happy-server-light' && !prismaPush
|
|
172
|
+
? 'start'
|
|
173
|
+
: 'dev';
|
|
174
|
+
if (!serverAlreadyRunning || restart) {
|
|
175
|
+
const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: serverScript, env: serverEnv });
|
|
176
|
+
children.push(server);
|
|
177
|
+
await waitForServerReady(internalServerUrl);
|
|
178
|
+
console.log(`[local] server ready at ${internalServerUrl}`);
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`[local] server already running at ${internalServerUrl}`);
|
|
181
|
+
}
|
|
118
182
|
console.log(
|
|
119
183
|
`[local] tip: to run 'happy' from your terminal *against this local server* (and have sessions show up in the UI), use:\n` +
|
|
120
184
|
`export HAPPY_SERVER_URL=\"${internalServerUrl}\"\n` +
|
|
@@ -130,6 +194,7 @@ async function main() {
|
|
|
130
194
|
internalServerUrl,
|
|
131
195
|
publicServerUrl,
|
|
132
196
|
isShuttingDown: () => shuttingDown,
|
|
197
|
+
forceRestart: restart,
|
|
133
198
|
});
|
|
134
199
|
}
|
|
135
200
|
|
|
@@ -140,8 +205,48 @@ async function main() {
|
|
|
140
205
|
delete uiEnv.CI;
|
|
141
206
|
uiEnv.EXPO_PUBLIC_HAPPY_SERVER_URL = publicServerUrl;
|
|
142
207
|
uiEnv.EXPO_PUBLIC_DEBUG = uiEnv.EXPO_PUBLIC_DEBUG ?? '1';
|
|
143
|
-
|
|
144
|
-
|
|
208
|
+
|
|
209
|
+
await ensureExpoIsolationEnv({
|
|
210
|
+
env: uiEnv,
|
|
211
|
+
stateDir: uiPaths.stateDir,
|
|
212
|
+
expoHomeDir: uiPaths.expoHomeDir,
|
|
213
|
+
tmpDir: uiPaths.tmpDir,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Expo uses Metro (default 8081). If it's already used by another worktree/stack,
|
|
217
|
+
// Expo prompts to pick another port, which fails in non-interactive mode.
|
|
218
|
+
// Pick a free port up-front to make LLM/CI/service runs reliable.
|
|
219
|
+
const defaultMetroPort = 8081;
|
|
220
|
+
const metroPort = await pickNextFreeTcpPort(defaultMetroPort);
|
|
221
|
+
uiEnv.RCT_METRO_PORT = String(metroPort);
|
|
222
|
+
// eslint-disable-next-line no-console
|
|
223
|
+
console.log(`[local] ui: starting Expo web (metro port=${metroPort})`);
|
|
224
|
+
|
|
225
|
+
const uiArgs = ['start', '--web', '--port', String(metroPort)];
|
|
226
|
+
if (wantsExpoClearCache({ env: baseEnv })) {
|
|
227
|
+
uiArgs.push('--clear');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!uiAlreadyRunning || restart) {
|
|
231
|
+
if (restart && uiRunning.state?.pid) {
|
|
232
|
+
const prevPid = Number(uiRunning.state.pid);
|
|
233
|
+
const prevPort = Number(uiRunning.state.port);
|
|
234
|
+
if (Number.isFinite(prevPort) && prevPort > 0) {
|
|
235
|
+
await killPortListeners(prevPort, { label: 'ui' });
|
|
236
|
+
}
|
|
237
|
+
await killPid(prevPid);
|
|
238
|
+
uiAlreadyRunning = false;
|
|
239
|
+
}
|
|
240
|
+
const ui = await pmSpawnBin({ label: 'ui', dir: uiDir, bin: 'expo', args: uiArgs, env: uiEnv });
|
|
241
|
+
children.push(ui);
|
|
242
|
+
try {
|
|
243
|
+
await writePidState(uiPaths.statePath, { pid: ui.pid, port: metroPort, uiDir, startedAt: new Date().toISOString() });
|
|
244
|
+
} catch {
|
|
245
|
+
// ignore
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
console.log('[local] ui already running (skipping Expo start)');
|
|
249
|
+
}
|
|
145
250
|
}
|
|
146
251
|
|
|
147
252
|
const shutdown = async () => {
|