happy-stacks 0.1.2 → 0.2.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.
- package/README.md +121 -83
- package/bin/happys.mjs +70 -10
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/stacks.md +39 -0
- package/extras/swiftbar/auth-login.sh +5 -2
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +131 -81
- package/extras/swiftbar/happys-term.sh +15 -38
- package/extras/swiftbar/happys.sh +15 -32
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +209 -80
- package/extras/swiftbar/lib/system.sh +27 -4
- package/extras/swiftbar/lib/utils.sh +311 -28
- package/extras/swiftbar/pnpm.sh +2 -1
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +11 -2
- package/extras/swiftbar/wt-pr.sh +9 -2
- package/package.json +2 -1
- package/scripts/auth.mjs +560 -112
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +130 -20
- package/scripts/dev.mjs +201 -133
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +43 -20
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +25 -15
- package/scripts/mobile.mjs +13 -7
- package/scripts/run.mjs +114 -27
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +15 -2
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +1792 -254
- package/scripts/stop.mjs +6 -3
- package/scripts/tailscale.mjs +17 -2
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +2 -2
- package/scripts/ui_gateway.mjs +2 -2
- package/scripts/uninstall.mjs +18 -10
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +6 -2
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/dev_daemon.mjs +104 -0
- package/scripts/utils/dev_expo_web.mjs +112 -0
- package/scripts/utils/dev_server.mjs +183 -0
- package/scripts/utils/env.mjs +60 -11
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +4 -2
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +100 -46
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +121 -20
- package/scripts/utils/proc.mjs +29 -2
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +24 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +79 -30
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +82 -8
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
package/scripts/dev.mjs
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { killProcessTree } from './utils/proc.mjs';
|
|
4
4
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { killPortListeners
|
|
6
|
-
import { getServerComponentName, isHappyServerRunning
|
|
7
|
-
import {
|
|
5
|
+
import { killPortListeners } from './utils/ports.mjs';
|
|
6
|
+
import { getServerComponentName, isHappyServerRunning } from './utils/server.mjs';
|
|
7
|
+
import { 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 { isDaemonRunning,
|
|
12
|
-
import {
|
|
13
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
11
|
+
import { isDaemonRunning, stopLocalDaemon } from './daemon.mjs';
|
|
12
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
14
13
|
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
|
|
15
|
+
import { isPidAlive, readStackRuntimeStateFile, recordStackRuntimeStart } from './utils/stack_runtime_state.mjs';
|
|
16
|
+
import { resolveStackContext } from './utils/stack_context.mjs';
|
|
17
|
+
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server_urls.mjs';
|
|
18
|
+
import { ensureDevCliReady, prepareDaemonAuthSeed, startDevDaemon, watchHappyCliAndRestartDaemon } from './utils/dev_daemon.mjs';
|
|
19
|
+
import { startDevServer, watchDevServerAndRestart } from './utils/dev_server.mjs';
|
|
20
|
+
import { startDevExpoWebUi } from './utils/dev_expo_web.mjs';
|
|
21
|
+
import { resolveLocalhostHost } from './utils/localhost_host.mjs';
|
|
22
|
+
import { openUrlInBrowser } from './utils/browser.mjs';
|
|
23
|
+
import { waitForHttpOk } from './utils/server.mjs';
|
|
24
|
+
|
|
25
|
+
function sanitizeDnsLabel(raw, { fallback = 'stack' } = {}) {
|
|
26
|
+
const s = String(raw ?? '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
29
|
+
.replace(/-+/g, '-')
|
|
30
|
+
.replace(/^-+/, '')
|
|
31
|
+
.replace(/-+$/, '');
|
|
32
|
+
return s || fallback;
|
|
33
|
+
}
|
|
17
34
|
|
|
18
35
|
/**
|
|
19
36
|
* Dev mode stack:
|
|
@@ -29,10 +46,13 @@ async function main() {
|
|
|
29
46
|
if (wantsHelp(argv, { flags })) {
|
|
30
47
|
printResult({
|
|
31
48
|
json,
|
|
32
|
-
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart'], json: true },
|
|
49
|
+
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--watch', '--no-watch', '--no-browser'], json: true },
|
|
33
50
|
text: [
|
|
34
51
|
'[dev] usage:',
|
|
35
52
|
' happys dev [--server=happy-server|happy-server-light] [--restart] [--json]',
|
|
53
|
+
' happys dev --watch # rebuild/restart happy-cli daemon on file changes (TTY default)',
|
|
54
|
+
' happys dev --no-watch # disable watch mode (always disabled in non-interactive mode)',
|
|
55
|
+
' happys dev --no-browser # do not open the UI in your browser automatically',
|
|
36
56
|
' note: --json prints the resolved config (dry-run) and exits.',
|
|
37
57
|
].join('\n'),
|
|
38
58
|
});
|
|
@@ -40,21 +60,6 @@ async function main() {
|
|
|
40
60
|
}
|
|
41
61
|
const rootDir = getRootDir(import.meta.url);
|
|
42
62
|
|
|
43
|
-
const serverPort = process.env.HAPPY_LOCAL_SERVER_PORT
|
|
44
|
-
? parseInt(process.env.HAPPY_LOCAL_SERVER_PORT, 10)
|
|
45
|
-
: 3005;
|
|
46
|
-
|
|
47
|
-
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
48
|
-
const defaultPublicUrl = `http://localhost:${serverPort}`;
|
|
49
|
-
const envPublicUrl = process.env.HAPPY_LOCAL_SERVER_URL?.trim() ? process.env.HAPPY_LOCAL_SERVER_URL.trim() : '';
|
|
50
|
-
const resolved = await resolvePublicServerUrl({
|
|
51
|
-
internalServerUrl,
|
|
52
|
-
defaultPublicUrl,
|
|
53
|
-
envPublicUrl,
|
|
54
|
-
allowEnable: true,
|
|
55
|
-
});
|
|
56
|
-
const publicServerUrl = resolved.publicServerUrl;
|
|
57
|
-
|
|
58
63
|
const serverComponentName = getServerComponentName({ kv });
|
|
59
64
|
if (serverComponentName === 'both') {
|
|
60
65
|
throw new Error(`[local] --server=both is not supported for dev (pick one: happy-server-light or happy-server)`);
|
|
@@ -62,7 +67,7 @@ async function main() {
|
|
|
62
67
|
|
|
63
68
|
const startUi = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_UI ?? '1') !== '0';
|
|
64
69
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
65
|
-
const
|
|
70
|
+
const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
|
|
66
71
|
|
|
67
72
|
const serverDir = getComponentDir(rootDir, serverComponentName);
|
|
68
73
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
@@ -77,6 +82,28 @@ async function main() {
|
|
|
77
82
|
|
|
78
83
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
79
84
|
const autostart = getDefaultAutostartPaths();
|
|
85
|
+
const baseEnv = { ...process.env };
|
|
86
|
+
const stackCtx = resolveStackContext({ env: baseEnv, autostart });
|
|
87
|
+
const { stackMode, runtimeStatePath, stackName, envPath, ephemeral } = stackCtx;
|
|
88
|
+
|
|
89
|
+
const serverPort = resolveServerPortFromEnv({ env: baseEnv, defaultPort: 3005 });
|
|
90
|
+
// IMPORTANT:
|
|
91
|
+
// - Only the main stack should ever auto-enable (or prefer) Tailscale Serve by default.
|
|
92
|
+
// - Non-main stacks should default to localhost URLs unless the user explicitly configured a public URL
|
|
93
|
+
// OR Tailscale Serve is already configured for this stack's internal URL (status matches).
|
|
94
|
+
const allowEnableTailscale = !stackMode || stackName === 'main';
|
|
95
|
+
const resolvedUrls = await resolveServerUrls({ env: baseEnv, serverPort, allowEnable: allowEnableTailscale });
|
|
96
|
+
const internalServerUrl = resolvedUrls.internalServerUrl;
|
|
97
|
+
let publicServerUrl = resolvedUrls.publicServerUrl;
|
|
98
|
+
if (stackMode && stackName !== 'main' && !resolvedUrls.envPublicUrl) {
|
|
99
|
+
const src = String(resolvedUrls.publicServerUrlSource ?? '');
|
|
100
|
+
const hasStackScopedTailscale = src.startsWith('tailscale-');
|
|
101
|
+
if (!hasStackScopedTailscale) {
|
|
102
|
+
publicServerUrl = resolvedUrls.defaultPublicUrl;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const uiApiUrl = resolvedUrls.defaultPublicUrl;
|
|
106
|
+
const restart = flags.has('--restart');
|
|
80
107
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
81
108
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
82
109
|
: join(autostart.baseDir, 'cli');
|
|
@@ -103,7 +130,16 @@ async function main() {
|
|
|
103
130
|
|
|
104
131
|
const children = [];
|
|
105
132
|
let shuttingDown = false;
|
|
106
|
-
|
|
133
|
+
|
|
134
|
+
// Ensure happy-cli is install+build ready before starting the daemon.
|
|
135
|
+
// Worktrees often don't have dist/ built yet, which causes MODULE_NOT_FOUND on dist/index.mjs.
|
|
136
|
+
const buildCli = (baseEnv.HAPPY_STACKS_CLI_BUILD ?? baseEnv.HAPPY_LOCAL_CLI_BUILD ?? '1').toString().trim() !== '0';
|
|
137
|
+
await ensureDevCliReady({ cliDir, buildCli });
|
|
138
|
+
|
|
139
|
+
// Watch mode (interactive only by default): rebuild happy-cli and restart daemon when code changes.
|
|
140
|
+
const watchEnabled =
|
|
141
|
+
flags.has('--watch') || (!flags.has('--no-watch') && Boolean(process.stdin.isTTY && process.stdout.isTTY));
|
|
142
|
+
const watchers = [];
|
|
107
143
|
|
|
108
144
|
const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
|
|
109
145
|
const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
|
|
@@ -118,63 +154,39 @@ async function main() {
|
|
|
118
154
|
return;
|
|
119
155
|
}
|
|
120
156
|
|
|
157
|
+
if (stackMode && runtimeStatePath) {
|
|
158
|
+
await recordStackRuntimeStart(runtimeStatePath, {
|
|
159
|
+
stackName,
|
|
160
|
+
script: 'dev.mjs',
|
|
161
|
+
ephemeral,
|
|
162
|
+
ownerPid: process.pid,
|
|
163
|
+
ports: { server: serverPort },
|
|
164
|
+
}).catch(() => {});
|
|
165
|
+
}
|
|
166
|
+
|
|
121
167
|
// Start server (only if not already healthy)
|
|
122
|
-
|
|
168
|
+
// NOTE: In stack mode we avoid killing arbitrary port listeners (fail-closed instead).
|
|
169
|
+
if ((!serverAlreadyRunning || restart) && !stackMode) {
|
|
123
170
|
await killPortListeners(serverPort, { label: 'server' });
|
|
124
171
|
}
|
|
125
|
-
const serverEnv = {
|
|
126
|
-
...baseEnv,
|
|
127
|
-
PORT: String(serverPort),
|
|
128
|
-
PUBLIC_URL: publicServerUrl,
|
|
129
|
-
// Avoid noisy failures if a previous run left the metrics port busy.
|
|
130
|
-
METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
|
|
131
|
-
};
|
|
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
|
-
}
|
|
155
172
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
? 'start'
|
|
173
|
-
: 'dev';
|
|
173
|
+
const { serverEnv, serverScript, serverProc } = await startDevServer({
|
|
174
|
+
serverComponentName,
|
|
175
|
+
serverDir,
|
|
176
|
+
autostart,
|
|
177
|
+
baseEnv,
|
|
178
|
+
serverPort,
|
|
179
|
+
internalServerUrl,
|
|
180
|
+
publicServerUrl,
|
|
181
|
+
envPath,
|
|
182
|
+
stackMode,
|
|
183
|
+
runtimeStatePath,
|
|
184
|
+
serverAlreadyRunning,
|
|
185
|
+
restart,
|
|
186
|
+
children,
|
|
187
|
+
});
|
|
188
|
+
|
|
174
189
|
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
190
|
console.log(`[local] server ready at ${internalServerUrl}`);
|
|
179
191
|
} else {
|
|
180
192
|
console.log(`[local] server already running at ${internalServerUrl}`);
|
|
@@ -186,66 +198,114 @@ async function main() {
|
|
|
186
198
|
`export HAPPY_WEBAPP_URL=\"${publicServerUrl}\"\n`
|
|
187
199
|
);
|
|
188
200
|
|
|
189
|
-
//
|
|
190
|
-
if
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
// Reliability before daemon start:
|
|
202
|
+
// - Ensure schema exists (server-light: db push; happy-server: migrate deploy if tables missing)
|
|
203
|
+
// - Auto-seed from main only when needed (non-main + non-interactive default, and only if missing creds or 0 accounts)
|
|
204
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
205
|
+
await prepareDaemonAuthSeed({
|
|
206
|
+
rootDir,
|
|
207
|
+
env: baseEnv,
|
|
208
|
+
stackName,
|
|
209
|
+
cliHomeDir,
|
|
210
|
+
startDaemon,
|
|
211
|
+
isInteractive,
|
|
212
|
+
serverComponentName,
|
|
213
|
+
serverDir,
|
|
214
|
+
serverEnv,
|
|
215
|
+
quiet: false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await startDevDaemon({
|
|
219
|
+
startDaemon,
|
|
220
|
+
cliBin,
|
|
221
|
+
cliHomeDir,
|
|
222
|
+
internalServerUrl,
|
|
223
|
+
publicServerUrl,
|
|
224
|
+
restart,
|
|
225
|
+
isShuttingDown: () => shuttingDown,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const cliWatcher = watchHappyCliAndRestartDaemon({
|
|
229
|
+
enabled: watchEnabled,
|
|
230
|
+
startDaemon,
|
|
231
|
+
buildCli,
|
|
232
|
+
cliDir,
|
|
233
|
+
cliBin,
|
|
234
|
+
cliHomeDir,
|
|
235
|
+
internalServerUrl,
|
|
236
|
+
publicServerUrl,
|
|
237
|
+
isShuttingDown: () => shuttingDown,
|
|
238
|
+
});
|
|
239
|
+
if (cliWatcher) watchers.push(cliWatcher);
|
|
240
|
+
|
|
241
|
+
const serverProcRef = { current: serverProc };
|
|
242
|
+
if (stackMode && runtimeStatePath && !serverProcRef.current?.pid) {
|
|
243
|
+
// If the server was already running when we started dev, `startDevServer` won't spawn a new process
|
|
244
|
+
// (and therefore we don't have a ChildProcess handle). For safe watch/restart we need a PID.
|
|
245
|
+
const state = await readStackRuntimeStateFile(runtimeStatePath);
|
|
246
|
+
const pid = state?.processes?.serverPid;
|
|
247
|
+
if (isPidAlive(pid)) {
|
|
248
|
+
serverProcRef.current = { pid: Number(pid), exitCode: null };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const serverWatcher = watchDevServerAndRestart({
|
|
252
|
+
enabled: watchEnabled && Boolean(serverProcRef.current?.pid),
|
|
253
|
+
stackMode,
|
|
254
|
+
serverComponentName,
|
|
255
|
+
serverDir,
|
|
256
|
+
serverPort,
|
|
257
|
+
internalServerUrl,
|
|
258
|
+
serverScript,
|
|
259
|
+
serverEnv,
|
|
260
|
+
runtimeStatePath,
|
|
261
|
+
stackName,
|
|
262
|
+
envPath,
|
|
263
|
+
children,
|
|
264
|
+
serverProcRef,
|
|
265
|
+
isShuttingDown: () => shuttingDown,
|
|
266
|
+
});
|
|
267
|
+
if (serverWatcher) watchers.push(serverWatcher);
|
|
268
|
+
if (watchEnabled && stackMode && serverComponentName === 'happy-server' && !serverWatcher) {
|
|
269
|
+
console.warn(
|
|
270
|
+
`[local] watch: server restart is disabled because the running server PID is unknown.\n` +
|
|
271
|
+
`[local] watch: fix: re-run with --restart so Happy Stacks can (re)spawn the server and track its PID.`
|
|
272
|
+
);
|
|
199
273
|
}
|
|
200
274
|
|
|
201
|
-
|
|
275
|
+
const uiRes = await startDevExpoWebUi({
|
|
276
|
+
startUi,
|
|
277
|
+
uiDir,
|
|
278
|
+
autostart,
|
|
279
|
+
baseEnv,
|
|
280
|
+
apiServerUrl: uiApiUrl,
|
|
281
|
+
restart,
|
|
282
|
+
stackMode,
|
|
283
|
+
runtimeStatePath,
|
|
284
|
+
stackName,
|
|
285
|
+
envPath,
|
|
286
|
+
children,
|
|
287
|
+
});
|
|
202
288
|
if (startUi) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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');
|
|
289
|
+
const host = resolveLocalhostHost({ stackMode, stackName });
|
|
290
|
+
if (uiRes?.reason === 'already_running' && uiRes.port) {
|
|
291
|
+
console.log(`[local] ui already running (pid=${uiRes.pid}, port=${uiRes.port})`);
|
|
292
|
+
console.log(`[local] ui: open http://${host}:${uiRes.port}`);
|
|
293
|
+
} else if (uiRes?.skipped === false && uiRes.port) {
|
|
294
|
+
console.log(`[local] ui: open http://${host}:${uiRes.port}`);
|
|
295
|
+
} else if (uiRes?.skipped && uiRes?.reason === 'already_running') {
|
|
296
|
+
console.log('[local] ui already running (skipping Expo start)');
|
|
228
297
|
}
|
|
229
298
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
299
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
300
|
+
const shouldOpen = isInteractive && !noBrowser && Boolean(uiRes?.port);
|
|
301
|
+
if (shouldOpen) {
|
|
302
|
+
const url = `http://${host}:${uiRes.port}`;
|
|
303
|
+
// Prefer localhost for readiness checks (faster/more reliable), but open the stack-scoped hostname.
|
|
304
|
+
await waitForHttpOk(`http://localhost:${uiRes.port}`, { timeoutMs: 30_000 }).catch(() => {});
|
|
305
|
+
const res = await openUrlInBrowser(url);
|
|
306
|
+
if (!res.ok) {
|
|
307
|
+
console.warn(`[local] ui: failed to open browser automatically (${res.error}).`);
|
|
246
308
|
}
|
|
247
|
-
} else {
|
|
248
|
-
console.log('[local] ui already running (skipping Expo start)');
|
|
249
309
|
}
|
|
250
310
|
}
|
|
251
311
|
|
|
@@ -256,6 +316,14 @@ async function main() {
|
|
|
256
316
|
shuttingDown = true;
|
|
257
317
|
console.log('\n[local] shutting down...');
|
|
258
318
|
|
|
319
|
+
for (const w of watchers) {
|
|
320
|
+
try {
|
|
321
|
+
w.close();
|
|
322
|
+
} catch {
|
|
323
|
+
// ignore
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
259
327
|
if (startDaemon) {
|
|
260
328
|
await stopLocalDaemon({ cliBin, internalServerUrl, cliHomeDir });
|
|
261
329
|
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { pathExists } from './utils/fs.mjs';
|
|
4
4
|
import { runCapture } from './utils/proc.mjs';
|
|
5
5
|
import { getComponentDir, getDefaultAutostartPaths, getHappyStacksHomeDir, getRootDir, getWorkspaceDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
6
6
|
import { killPortListeners } from './utils/ports.mjs';
|
|
7
7
|
import { getServerComponentName } from './utils/server.mjs';
|
|
8
8
|
import { daemonStatusSummary } from './daemon.mjs';
|
|
9
|
-
import { tailscaleServeStatus
|
|
9
|
+
import { tailscaleServeStatus } from './tailscale.mjs';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { readFile } from 'node:fs/promises';
|
|
14
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
14
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
15
15
|
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
16
16
|
import { assertServerComponentDirMatches } from './utils/validate.mjs';
|
|
17
|
+
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server_urls.mjs';
|
|
18
|
+
import { resolveStackContext } from './utils/stack_context.mjs';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Doctor script for common happy-stacks failure modes.
|
|
@@ -115,27 +117,23 @@ async function main() {
|
|
|
115
117
|
const runtimeVersion = await readPkgVersion(runtimePkgJson);
|
|
116
118
|
const updateCache = existsSync(updateCachePath) ? await readJsonSafe(updateCachePath) : null;
|
|
117
119
|
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
+
const autostart = getDefaultAutostartPaths();
|
|
121
|
+
const stackCtx = resolveStackContext({ env: process.env, autostart });
|
|
122
|
+
const stackMode = stackCtx.stackMode;
|
|
120
123
|
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
defaultPublicUrl,
|
|
126
|
-
envPublicUrl,
|
|
127
|
-
allowEnable: false,
|
|
128
|
-
});
|
|
129
|
-
const publicServerUrl = resolved.publicServerUrl;
|
|
124
|
+
const serverPort = resolveServerPortFromEnv({ defaultPort: 3005 });
|
|
125
|
+
const resolvedUrls = await resolveServerUrls({ serverPort, allowEnable: false });
|
|
126
|
+
const internalServerUrl = resolvedUrls.internalServerUrl;
|
|
127
|
+
const publicServerUrl = resolvedUrls.publicServerUrl;
|
|
130
128
|
|
|
131
129
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
132
130
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
133
|
-
: join(
|
|
131
|
+
: join(autostart.baseDir, 'cli');
|
|
134
132
|
|
|
135
133
|
const serveUi = (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
|
|
136
134
|
const uiBuildDir = process.env.HAPPY_LOCAL_UI_BUILD_DIR?.trim()
|
|
137
135
|
? process.env.HAPPY_LOCAL_UI_BUILD_DIR.trim()
|
|
138
|
-
: join(
|
|
136
|
+
: join(autostart.baseDir, 'ui');
|
|
139
137
|
|
|
140
138
|
const serverComponentName = getServerComponentName({ kv });
|
|
141
139
|
if (serverComponentName === 'both') {
|
|
@@ -206,8 +204,15 @@ async function main() {
|
|
|
206
204
|
report.checks.serverHealth = { ok: false };
|
|
207
205
|
if (!json) console.log(`❌ server health: unreachable (${internalServerUrl})`);
|
|
208
206
|
if (fix) {
|
|
209
|
-
if (
|
|
210
|
-
|
|
207
|
+
if (stackMode) {
|
|
208
|
+
if (!json) {
|
|
209
|
+
console.log(`↪ fix skipped: refusing to kill unknown port listeners in stack mode.`);
|
|
210
|
+
console.log(`↪ Fix: use stack-safe controls instead: happys stack stop ${process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? 'main'} --aggressive`);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
if (!json) console.log(`↪ attempting fix: freeing tcp:${serverPort}`);
|
|
214
|
+
await killPortListeners(serverPort, { label: 'doctor' });
|
|
215
|
+
}
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -304,7 +309,7 @@ async function main() {
|
|
|
304
309
|
}
|
|
305
310
|
} catch {
|
|
306
311
|
report.checks.happyOnPath = { ok: false };
|
|
307
|
-
if (!json) console.log(
|
|
312
|
+
if (!json) console.log(`ℹ️ happy on PATH: not found (run: happys init --install-path, or add ${join(getHappyStacksHomeDir(), 'bin')} to PATH)`);
|
|
308
313
|
}
|
|
309
314
|
|
|
310
315
|
// happys on PATH
|
|
@@ -316,7 +321,7 @@ async function main() {
|
|
|
316
321
|
}
|
|
317
322
|
} catch {
|
|
318
323
|
report.checks.happysOnPath = { ok: false };
|
|
319
|
-
if (!json) console.log(
|
|
324
|
+
if (!json) console.log(`ℹ️ happys on PATH: not found (run: happys init --install-path, or add ${join(getHappyStacksHomeDir(), 'bin')} to PATH)`);
|
|
320
325
|
}
|
|
321
326
|
|
|
322
327
|
if (!json) {
|
|
@@ -326,7 +331,7 @@ async function main() {
|
|
|
326
331
|
console.log('- Install a stable runtime (recommended for SwiftBar/services): happys self update');
|
|
327
332
|
}
|
|
328
333
|
if (!report.checks.happysOnPath?.ok) {
|
|
329
|
-
console.log(
|
|
334
|
+
console.log(`- Add shims to PATH: export PATH="${join(getHappyStacksHomeDir(), 'bin')}:$PATH" (or: happys init --install-path)`);
|
|
330
335
|
}
|
|
331
336
|
console.log('');
|
|
332
337
|
}
|