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