happy-stacks 0.1.0 → 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 +130 -74
- package/bin/happys.mjs +140 -9
- 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/server-flavors.md +61 -2
- package/docs/stacks.md +55 -4
- package/extras/swiftbar/auth-login.sh +10 -7
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +175 -83
- package/extras/swiftbar/happys-term.sh +128 -0
- package/extras/swiftbar/happys.sh +35 -0
- 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 +279 -132
- package/extras/swiftbar/lib/system.sh +64 -10
- package/extras/swiftbar/lib/utils.sh +469 -10
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +4 -14
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +19 -10
- package/extras/swiftbar/wt-pr.sh +10 -3
- package/package.json +2 -1
- package/scripts/auth.mjs +833 -14
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +200 -23
- package/scripts/dev.mjs +230 -57
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +275 -46
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +302 -0
- package/scripts/mobile.mjs +59 -21
- package/scripts/run.mjs +222 -43
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +190 -38
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +2273 -92
- package/scripts/stop.mjs +160 -0
- package/scripts/tailscale.mjs +164 -23
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +21 -13
- 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} +71 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +13 -1
- 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 +94 -23
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +96 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +484 -0
- 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 +132 -22
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +75 -7
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +61 -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 +255 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +135 -15
- /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,17 +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
5
|
import { killPortListeners } from './utils/ports.mjs';
|
|
6
|
-
import { getServerComponentName,
|
|
7
|
-
import {
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { isDaemonRunning, stopLocalDaemon } from './daemon.mjs';
|
|
12
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
13
|
+
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
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
|
+
}
|
|
15
34
|
|
|
16
35
|
/**
|
|
17
36
|
* Dev mode stack:
|
|
@@ -27,10 +46,13 @@ async function main() {
|
|
|
27
46
|
if (wantsHelp(argv, { flags })) {
|
|
28
47
|
printResult({
|
|
29
48
|
json,
|
|
30
|
-
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon'], json: true },
|
|
49
|
+
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--watch', '--no-watch', '--no-browser'], json: true },
|
|
31
50
|
text: [
|
|
32
51
|
'[dev] usage:',
|
|
33
|
-
' happys dev [--server=happy-server|happy-server-light] [--json]',
|
|
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',
|
|
34
56
|
' note: --json prints the resolved config (dry-run) and exits.',
|
|
35
57
|
].join('\n'),
|
|
36
58
|
});
|
|
@@ -38,21 +60,6 @@ async function main() {
|
|
|
38
60
|
}
|
|
39
61
|
const rootDir = getRootDir(import.meta.url);
|
|
40
62
|
|
|
41
|
-
const serverPort = process.env.HAPPY_LOCAL_SERVER_PORT
|
|
42
|
-
? parseInt(process.env.HAPPY_LOCAL_SERVER_PORT, 10)
|
|
43
|
-
: 3005;
|
|
44
|
-
|
|
45
|
-
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
46
|
-
const defaultPublicUrl = `http://localhost:${serverPort}`;
|
|
47
|
-
const envPublicUrl = process.env.HAPPY_LOCAL_SERVER_URL?.trim() ? process.env.HAPPY_LOCAL_SERVER_URL.trim() : '';
|
|
48
|
-
const resolved = await resolvePublicServerUrl({
|
|
49
|
-
internalServerUrl,
|
|
50
|
-
defaultPublicUrl,
|
|
51
|
-
envPublicUrl,
|
|
52
|
-
allowEnable: true,
|
|
53
|
-
});
|
|
54
|
-
const publicServerUrl = resolved.publicServerUrl;
|
|
55
|
-
|
|
56
63
|
const serverComponentName = getServerComponentName({ kv });
|
|
57
64
|
if (serverComponentName === 'both') {
|
|
58
65
|
throw new Error(`[local] --server=both is not supported for dev (pick one: happy-server-light or happy-server)`);
|
|
@@ -60,21 +67,46 @@ async function main() {
|
|
|
60
67
|
|
|
61
68
|
const startUi = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_UI ?? '1') !== '0';
|
|
62
69
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
70
|
+
const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
|
|
63
71
|
|
|
64
72
|
const serverDir = getComponentDir(rootDir, serverComponentName);
|
|
65
73
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
66
74
|
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
67
75
|
|
|
68
76
|
assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
|
|
77
|
+
assertServerPrismaProviderMatches({ serverComponentName, serverDir });
|
|
69
78
|
|
|
70
79
|
await requireDir(serverComponentName, serverDir);
|
|
71
80
|
await requireDir('happy', uiDir);
|
|
72
81
|
await requireDir('happy-cli', cliDir);
|
|
73
82
|
|
|
74
83
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
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');
|
|
75
107
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
76
108
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
77
|
-
: join(
|
|
109
|
+
: join(autostart.baseDir, 'cli');
|
|
78
110
|
|
|
79
111
|
if (json) {
|
|
80
112
|
printResult({
|
|
@@ -98,23 +130,67 @@ async function main() {
|
|
|
98
130
|
|
|
99
131
|
const children = [];
|
|
100
132
|
let shuttingDown = false;
|
|
101
|
-
const baseEnv = { ...process.env };
|
|
102
133
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
PORT: String(serverPort),
|
|
108
|
-
PUBLIC_URL: publicServerUrl,
|
|
109
|
-
// Avoid noisy failures if a previous run left the metrics port busy.
|
|
110
|
-
METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
|
|
111
|
-
};
|
|
112
|
-
await ensureDepsInstalled(serverDir, serverComponentName);
|
|
113
|
-
const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'dev', env: serverEnv });
|
|
114
|
-
children.push(server);
|
|
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 });
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
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 = [];
|
|
143
|
+
|
|
144
|
+
const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
|
|
145
|
+
const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
|
|
146
|
+
|
|
147
|
+
// UI dev server state (worktree-scoped)
|
|
148
|
+
const uiPaths = getExpoStatePaths({ baseDir: autostart.baseDir, kind: 'ui-dev', projectDir: uiDir, stateFileName: 'ui.state.json' });
|
|
149
|
+
const uiRunning = startUi ? await isStateProcessRunning(uiPaths.statePath) : { running: false, state: null };
|
|
150
|
+
let uiAlreadyRunning = Boolean(uiRunning.running);
|
|
151
|
+
|
|
152
|
+
if (!restart && serverAlreadyRunning && (!startDaemon || daemonAlreadyRunning) && (!startUi || uiAlreadyRunning)) {
|
|
153
|
+
console.log(`[local] dev: stack already running (server=${internalServerUrl}${startDaemon ? ` daemon=${daemonAlreadyRunning ? 'running' : 'stopped'}` : ''}${startUi ? ` ui=${uiAlreadyRunning ? 'running' : 'stopped'}` : ''})`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
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
|
+
|
|
167
|
+
// Start server (only if not already healthy)
|
|
168
|
+
// NOTE: In stack mode we avoid killing arbitrary port listeners (fail-closed instead).
|
|
169
|
+
if ((!serverAlreadyRunning || restart) && !stackMode) {
|
|
170
|
+
await killPortListeners(serverPort, { label: 'server' });
|
|
171
|
+
}
|
|
172
|
+
|
|
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
|
+
|
|
189
|
+
if (!serverAlreadyRunning || restart) {
|
|
190
|
+
console.log(`[local] server ready at ${internalServerUrl}`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`[local] server already running at ${internalServerUrl}`);
|
|
193
|
+
}
|
|
118
194
|
console.log(
|
|
119
195
|
`[local] tip: to run 'happy' from your terminal *against this local server* (and have sessions show up in the UI), use:\n` +
|
|
120
196
|
`export HAPPY_SERVER_URL=\"${internalServerUrl}\"\n` +
|
|
@@ -122,26 +198,115 @@ async function main() {
|
|
|
122
198
|
`export HAPPY_WEBAPP_URL=\"${publicServerUrl}\"\n`
|
|
123
199
|
);
|
|
124
200
|
|
|
125
|
-
//
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
);
|
|
134
273
|
}
|
|
135
274
|
|
|
136
|
-
|
|
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
|
+
});
|
|
137
288
|
if (startUi) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)');
|
|
297
|
+
}
|
|
298
|
+
|
|
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}).`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
145
310
|
}
|
|
146
311
|
|
|
147
312
|
const shutdown = async () => {
|
|
@@ -151,6 +316,14 @@ async function main() {
|
|
|
151
316
|
shuttingDown = true;
|
|
152
317
|
console.log('\n[local] shutting down...');
|
|
153
318
|
|
|
319
|
+
for (const w of watchers) {
|
|
320
|
+
try {
|
|
321
|
+
w.close();
|
|
322
|
+
} catch {
|
|
323
|
+
// ignore
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
154
327
|
if (startDaemon) {
|
|
155
328
|
await stopLocalDaemon({ cliBin, internalServerUrl, cliHomeDir });
|
|
156
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
|
}
|