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/mobile.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
3
|
-
import {
|
|
4
|
-
import { run, runCapture, spawnProc } from './utils/proc.mjs';
|
|
5
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
6
|
-
import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/pm.mjs';
|
|
7
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
8
|
-
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { pickNextFreeTcpPort } from './utils/net/ports.mjs';
|
|
4
|
+
import { run, runCapture, spawnProc } from './utils/proc/proc.mjs';
|
|
5
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
6
|
+
import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/proc/pm.mjs';
|
|
7
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
|
+
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo/expo.mjs';
|
|
9
|
+
import { killProcessGroupOwnedByStack } from './utils/proc/ownership.mjs';
|
|
10
|
+
import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv } from './utils/server/urls.mjs';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Mobile dev helper for the embedded `components/happy` Expo app.
|
|
@@ -57,7 +59,7 @@ async function main() {
|
|
|
57
59
|
'',
|
|
58
60
|
'Notes:',
|
|
59
61
|
'- This script is designed to avoid editing upstream `components/happy` config in-place.',
|
|
60
|
-
'-
|
|
62
|
+
'- If you explicitly set HAPPY_STACKS_SERVER_URL (legacy: HAPPY_LOCAL_SERVER_URL), it bakes that URL into the app via EXPO_PUBLIC_HAPPY_SERVER_URL.',
|
|
61
63
|
].join('\n'),
|
|
62
64
|
});
|
|
63
65
|
return;
|
|
@@ -140,10 +142,10 @@ async function main() {
|
|
|
140
142
|
|
|
141
143
|
// Allow happy-stacks to define the default server URL baked into the app bundle.
|
|
142
144
|
// This is read by the app via `process.env.EXPO_PUBLIC_HAPPY_SERVER_URL`.
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
env.EXPO_PUBLIC_HAPPY_SERVER_URL =
|
|
145
|
+
const serverPort = resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
|
|
146
|
+
const { envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort });
|
|
147
|
+
if (envPublicUrl && !env.EXPO_PUBLIC_HAPPY_SERVER_URL) {
|
|
148
|
+
env.EXPO_PUBLIC_HAPPY_SERVER_URL = envPublicUrl;
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
if (json) {
|
|
@@ -306,11 +308,16 @@ async function main() {
|
|
|
306
308
|
}
|
|
307
309
|
if (restart && running.state?.pid) {
|
|
308
310
|
const prevPid = Number(running.state.pid);
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || autostart.stackName;
|
|
312
|
+
const envPath = (process.env.HAPPY_STACKS_ENV_FILE ?? process.env.HAPPY_LOCAL_ENV_FILE ?? '').toString();
|
|
313
|
+
const res = await killProcessGroupOwnedByStack(prevPid, { stackName, envPath, label: 'expo-mobile', json: true });
|
|
314
|
+
if (!res.killed) {
|
|
315
|
+
// eslint-disable-next-line no-console
|
|
316
|
+
console.warn(
|
|
317
|
+
`[mobile] not stopping existing Metro pid=${prevPid} because it does not look stack-owned.\n` +
|
|
318
|
+
`[mobile] continuing by starting a new Metro on a free port.`
|
|
319
|
+
);
|
|
312
320
|
}
|
|
313
|
-
await killPid(prevPid);
|
|
314
321
|
}
|
|
315
322
|
|
|
316
323
|
const requestedPort = Number.parseInt(String(portRaw), 10);
|
package/scripts/run.mjs
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
3
|
-
import { pathExists } from './utils/fs.mjs';
|
|
4
|
-
import { killProcessTree, runCapture, spawnProc } from './utils/proc.mjs';
|
|
5
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
6
|
-
import { killPortListeners } from './utils/ports.mjs';
|
|
7
|
-
import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server.mjs';
|
|
8
|
-
import { ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/pm.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
4
|
+
import { killProcessTree, runCapture, spawnProc } from './utils/proc/proc.mjs';
|
|
5
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
6
|
+
import { killPortListeners } from './utils/net/ports.mjs';
|
|
7
|
+
import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server/server.mjs';
|
|
8
|
+
import { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/proc/pm.mjs';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
12
|
-
import { maybeResetTailscaleServe
|
|
12
|
+
import { maybeResetTailscaleServe } from './tailscale.mjs';
|
|
13
13
|
import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
14
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
15
|
-
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
16
|
-
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/happy_server_infra.mjs';
|
|
14
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
15
|
+
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/server/validate.mjs';
|
|
16
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
|
|
17
|
+
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from './utils/stack/startup.mjs';
|
|
18
|
+
import { recordStackRuntimeStart, recordStackRuntimeUpdate } from './utils/stack/runtime_state.mjs';
|
|
19
|
+
import { resolveStackContext } from './utils/stack/context.mjs';
|
|
20
|
+
import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
|
|
21
|
+
import { resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
|
|
22
|
+
import { openUrlInBrowser } from './utils/ui/browser.mjs';
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Run the local stack in "production-like" mode:
|
|
@@ -31,7 +37,7 @@ async function main() {
|
|
|
31
37
|
if (wantsHelp(argv, { flags })) {
|
|
32
38
|
printResult({
|
|
33
39
|
json,
|
|
34
|
-
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', '--no-browser'], json: true },
|
|
35
41
|
text: [
|
|
36
42
|
'[start] usage:',
|
|
37
43
|
' happys start [--server=happy-server|happy-server-light] [--restart] [--json]',
|
|
@@ -44,17 +50,14 @@ async function main() {
|
|
|
44
50
|
|
|
45
51
|
const rootDir = getRootDir(import.meta.url);
|
|
46
52
|
|
|
47
|
-
const serverPort =
|
|
48
|
-
? parseInt(process.env.HAPPY_LOCAL_SERVER_PORT, 10)
|
|
49
|
-
: 3005;
|
|
53
|
+
const serverPort = resolveServerPortFromEnv({ defaultPort: 3005 });
|
|
50
54
|
|
|
51
55
|
// Internal URL used by local processes on this machine.
|
|
52
56
|
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
53
57
|
// Public URL is what you might share/open (e.g. https://<machine>.<tailnet>.ts.net).
|
|
54
58
|
// We auto-prefer the Tailscale HTTPS URL when available, unless explicitly overridden.
|
|
55
|
-
const defaultPublicUrl =
|
|
56
|
-
|
|
57
|
-
let publicServerUrl = envPublicUrl || defaultPublicUrl;
|
|
59
|
+
const { defaultPublicUrl, envPublicUrl, publicServerUrl: publicServerUrlPreview } = getPublicServerUrlEnvOverride({ serverPort });
|
|
60
|
+
let publicServerUrl = publicServerUrlPreview;
|
|
58
61
|
|
|
59
62
|
const serverComponentName = getServerComponentName({ kv });
|
|
60
63
|
if (serverComponentName === 'both') {
|
|
@@ -64,6 +67,7 @@ async function main() {
|
|
|
64
67
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
65
68
|
const serveUiWanted = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
|
|
66
69
|
const serveUi = serveUiWanted;
|
|
70
|
+
const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
|
|
67
71
|
const uiPrefix = process.env.HAPPY_LOCAL_UI_PREFIX?.trim() ? process.env.HAPPY_LOCAL_UI_PREFIX.trim() : '/';
|
|
68
72
|
const autostart = getDefaultAutostartPaths();
|
|
69
73
|
const uiBuildDir = process.env.HAPPY_LOCAL_UI_BUILD_DIR?.trim()
|
|
@@ -120,18 +124,29 @@ async function main() {
|
|
|
120
124
|
const children = [];
|
|
121
125
|
let shuttingDown = false;
|
|
122
126
|
const baseEnv = { ...process.env };
|
|
127
|
+
const stackCtx = resolveStackContext({ env: baseEnv, autostart });
|
|
128
|
+
const { stackMode, runtimeStatePath, stackName, ephemeral } = stackCtx;
|
|
129
|
+
|
|
130
|
+
// Ensure happy-cli is install+build ready before starting the daemon.
|
|
131
|
+
const buildCli = (baseEnv.HAPPY_STACKS_CLI_BUILD ?? baseEnv.HAPPY_LOCAL_CLI_BUILD ?? '1').toString().trim() !== '0';
|
|
132
|
+
await ensureCliBuilt(cliDir, { buildCli });
|
|
123
133
|
|
|
124
134
|
// Ensure server deps exist before any Prisma/docker work.
|
|
125
135
|
await ensureDepsInstalled(serverDir, serverComponentName);
|
|
126
136
|
|
|
127
|
-
// Public URL automation:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
// Public URL automation:
|
|
138
|
+
// - Only the main stack should ever auto-enable Tailscale Serve by default.
|
|
139
|
+
// - Non-main stacks default to localhost unless the user explicitly configured a public URL
|
|
140
|
+
// OR Tailscale Serve is already configured for this stack's internal URL (status matches).
|
|
141
|
+
const allowEnableTailscale = !stackMode || stackName === 'main';
|
|
142
|
+
const resolvedUrls = await resolveServerUrls({ env: baseEnv, serverPort, allowEnable: allowEnableTailscale });
|
|
143
|
+
if (stackMode && stackName !== 'main' && !resolvedUrls.envPublicUrl) {
|
|
144
|
+
const src = String(resolvedUrls.publicServerUrlSource ?? '');
|
|
145
|
+
const hasStackScopedTailscale = src.startsWith('tailscale-');
|
|
146
|
+
publicServerUrl = hasStackScopedTailscale ? resolvedUrls.publicServerUrl : resolvedUrls.defaultPublicUrl;
|
|
147
|
+
} else {
|
|
148
|
+
publicServerUrl = resolvedUrls.publicServerUrl;
|
|
149
|
+
}
|
|
135
150
|
|
|
136
151
|
const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
|
|
137
152
|
const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
|
|
@@ -140,9 +155,21 @@ async function main() {
|
|
|
140
155
|
return;
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
// Stack runtime state (stack-scoped commands only): record the runner PID + chosen ports so stop/restart never kills other stacks.
|
|
159
|
+
if (stackMode && runtimeStatePath) {
|
|
160
|
+
await recordStackRuntimeStart(runtimeStatePath, {
|
|
161
|
+
stackName,
|
|
162
|
+
script: 'run.mjs',
|
|
163
|
+
ephemeral,
|
|
164
|
+
ownerPid: process.pid,
|
|
165
|
+
ports: { server: serverPort },
|
|
166
|
+
}).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
|
|
143
169
|
// Server
|
|
144
170
|
// If a previous run left a server behind, free the port first (prevents false "ready" checks).
|
|
145
|
-
|
|
171
|
+
// NOTE: In stack mode we avoid killing arbitrary port listeners (fail-closed instead).
|
|
172
|
+
if ((!serverAlreadyRunning || restart) && !stackMode) {
|
|
146
173
|
await killPortListeners(serverPort, { label: 'server' });
|
|
147
174
|
}
|
|
148
175
|
|
|
@@ -161,6 +188,8 @@ async function main() {
|
|
|
161
188
|
}
|
|
162
189
|
: {}),
|
|
163
190
|
};
|
|
191
|
+
let serverLightAccountCount = null;
|
|
192
|
+
let happyServerAccountCount = null;
|
|
164
193
|
if (serverComponentName === 'happy-server-light') {
|
|
165
194
|
const dataDir = baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR?.trim()
|
|
166
195
|
? baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR.trim()
|
|
@@ -173,12 +202,14 @@ async function main() {
|
|
|
173
202
|
? baseEnv.DATABASE_URL.trim()
|
|
174
203
|
: `file:${join(dataDir, 'happy-server-light.sqlite')}`;
|
|
175
204
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
// Reliability: ensure DB schema exists before daemon hits /v1/machines (health checks don't cover DB readiness).
|
|
206
|
+
const acct = await getAccountCountForServerComponent({
|
|
207
|
+
serverComponentName,
|
|
208
|
+
serverDir,
|
|
209
|
+
env: serverEnv,
|
|
210
|
+
bestEffort: false,
|
|
211
|
+
});
|
|
212
|
+
serverLightAccountCount = typeof acct.accountCount === 'number' ? acct.accountCount : null;
|
|
182
213
|
}
|
|
183
214
|
let effectiveInternalServerUrl = internalServerUrl;
|
|
184
215
|
if (serverComponentName === 'happy-server') {
|
|
@@ -198,16 +229,32 @@ async function main() {
|
|
|
198
229
|
const backendPortRaw = (baseEnv.HAPPY_STACKS_HAPPY_SERVER_BACKEND_PORT ?? baseEnv.HAPPY_LOCAL_HAPPY_SERVER_BACKEND_PORT ?? '').trim();
|
|
199
230
|
const backendPort = backendPortRaw ? Number(backendPortRaw) : serverPort + 10;
|
|
200
231
|
const backendUrl = `http://127.0.0.1:${backendPort}`;
|
|
201
|
-
|
|
232
|
+
if (!stackMode) {
|
|
233
|
+
await killPortListeners(backendPort, { label: 'happy-server-backend' });
|
|
234
|
+
}
|
|
202
235
|
|
|
203
236
|
const backendEnv = { ...serverEnv, ...infra.env, PORT: String(backendPort) };
|
|
204
237
|
const autoMigrate = (baseEnv.HAPPY_STACKS_PRISMA_MIGRATE ?? baseEnv.HAPPY_LOCAL_PRISMA_MIGRATE ?? '1') !== '0';
|
|
205
238
|
if (autoMigrate) {
|
|
206
239
|
await applyHappyServerMigrations({ serverDir, env: backendEnv });
|
|
207
240
|
}
|
|
241
|
+
// Account probe should use the *actual* DATABASE_URL/infra env (ephemeral stacks do not persist it in env files).
|
|
242
|
+
const acct = await getAccountCountForServerComponent({
|
|
243
|
+
serverComponentName,
|
|
244
|
+
serverDir,
|
|
245
|
+
env: backendEnv,
|
|
246
|
+
bestEffort: true,
|
|
247
|
+
});
|
|
248
|
+
happyServerAccountCount = typeof acct.accountCount === 'number' ? acct.accountCount : null;
|
|
208
249
|
|
|
209
250
|
const backend = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: backendEnv });
|
|
210
251
|
children.push(backend);
|
|
252
|
+
if (stackMode && runtimeStatePath) {
|
|
253
|
+
await recordStackRuntimeUpdate(runtimeStatePath, {
|
|
254
|
+
ports: { server: serverPort, backend: backendPort },
|
|
255
|
+
processes: { happyServerBackendPid: backend.pid },
|
|
256
|
+
}).catch(() => {});
|
|
257
|
+
}
|
|
211
258
|
await waitForServerReady(backendUrl);
|
|
212
259
|
|
|
213
260
|
const gatewayArgs = [
|
|
@@ -225,6 +272,9 @@ async function main() {
|
|
|
225
272
|
|
|
226
273
|
const gateway = spawnProc('ui', process.execPath, gatewayArgs, { ...backendEnv, PORT: String(serverPort) }, { cwd: rootDir });
|
|
227
274
|
children.push(gateway);
|
|
275
|
+
if (stackMode && runtimeStatePath) {
|
|
276
|
+
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { uiGatewayPid: gateway.pid } }).catch(() => {});
|
|
277
|
+
}
|
|
228
278
|
await waitForServerReady(internalServerUrl);
|
|
229
279
|
effectiveInternalServerUrl = internalServerUrl;
|
|
230
280
|
|
|
@@ -237,6 +287,9 @@ async function main() {
|
|
|
237
287
|
if (!serverAlreadyRunning || restart) {
|
|
238
288
|
const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: serverEnv });
|
|
239
289
|
children.push(server);
|
|
290
|
+
if (stackMode && runtimeStatePath) {
|
|
291
|
+
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { serverPid: server.pid } }).catch(() => {});
|
|
292
|
+
}
|
|
240
293
|
await waitForServerReady(internalServerUrl);
|
|
241
294
|
} else {
|
|
242
295
|
console.log(`[local] server already running at ${internalServerUrl}`);
|
|
@@ -274,10 +327,44 @@ async function main() {
|
|
|
274
327
|
`export HAPPY_HOME_DIR=\"${cliHomeDir}\"\n` +
|
|
275
328
|
`export HAPPY_WEBAPP_URL=\"${publicServerUrl}\"\n`
|
|
276
329
|
);
|
|
330
|
+
|
|
331
|
+
// Auto-open UI (interactive only) using the stack-scoped hostname when applicable.
|
|
332
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
333
|
+
if (isInteractive && !noBrowser) {
|
|
334
|
+
const host = resolveLocalhostHost({ stackMode, stackName: autostart.stackName });
|
|
335
|
+
const prefix = uiPrefix.startsWith('/') ? uiPrefix : `/${uiPrefix}`;
|
|
336
|
+
const openUrl = `http://${host}:${serverPort}${prefix}`;
|
|
337
|
+
const res = await openUrlInBrowser(openUrl);
|
|
338
|
+
if (!res.ok) {
|
|
339
|
+
console.warn(`[local] ui: failed to open browser automatically (${res.error}).`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
277
342
|
}
|
|
278
343
|
|
|
279
344
|
// Daemon
|
|
280
345
|
if (startDaemon) {
|
|
346
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
347
|
+
if (serverComponentName === 'happy-server' && happyServerAccountCount == null) {
|
|
348
|
+
const acct = await getAccountCountForServerComponent({
|
|
349
|
+
serverComponentName,
|
|
350
|
+
serverDir,
|
|
351
|
+
env: serverEnv,
|
|
352
|
+
bestEffort: true,
|
|
353
|
+
});
|
|
354
|
+
happyServerAccountCount = typeof acct.accountCount === 'number' ? acct.accountCount : null;
|
|
355
|
+
}
|
|
356
|
+
const accountCount =
|
|
357
|
+
serverComponentName === 'happy-server-light' ? serverLightAccountCount : happyServerAccountCount;
|
|
358
|
+
await prepareDaemonAuthSeedIfNeeded({
|
|
359
|
+
rootDir,
|
|
360
|
+
env: baseEnv,
|
|
361
|
+
stackName: autostart.stackName,
|
|
362
|
+
cliHomeDir,
|
|
363
|
+
startDaemon,
|
|
364
|
+
isInteractive,
|
|
365
|
+
accountCount,
|
|
366
|
+
quiet: false,
|
|
367
|
+
});
|
|
281
368
|
await startLocalDaemonWithAuth({
|
|
282
369
|
cliBin,
|
|
283
370
|
cliHomeDir,
|
package/scripts/self.mjs
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import { join } from 'node:path';
|
|
7
6
|
|
|
8
|
-
import { parseArgs } from './utils/args.mjs';
|
|
9
|
-
import { pathExists } from './utils/fs.mjs';
|
|
10
|
-
import { run, runCapture } from './utils/proc.mjs';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
7
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
8
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
9
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
10
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
11
|
+
import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
|
|
12
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
13
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
14
|
+
import { readJsonIfExists } from './utils/fs/json.mjs';
|
|
15
|
+
import { readPackageJsonVersion } from './utils/fs/package_json.mjs';
|
|
18
16
|
|
|
19
17
|
function cachePaths() {
|
|
20
18
|
const home = getHappyStacksHomeDir();
|
|
@@ -25,15 +23,6 @@ function cachePaths() {
|
|
|
25
23
|
};
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
async function readJsonSafe(path) {
|
|
29
|
-
try {
|
|
30
|
-
const raw = await readFile(path, 'utf-8');
|
|
31
|
-
return JSON.parse(raw);
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
async function writeJsonSafe(path, obj) {
|
|
38
27
|
try {
|
|
39
28
|
await mkdir(join(path, '..'), { recursive: true });
|
|
@@ -47,25 +36,14 @@ async function writeJsonSafe(path, obj) {
|
|
|
47
36
|
}
|
|
48
37
|
}
|
|
49
38
|
|
|
50
|
-
async function readPkgVersion(pkgJsonPath) {
|
|
51
|
-
try {
|
|
52
|
-
const raw = await readFile(pkgJsonPath, 'utf-8');
|
|
53
|
-
const pkg = JSON.parse(raw);
|
|
54
|
-
const v = String(pkg.version ?? '').trim();
|
|
55
|
-
return v || null;
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
39
|
async function getRuntimeInstalledVersion() {
|
|
62
40
|
const runtimeDir = getRuntimeDir();
|
|
63
41
|
const pkgJson = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
|
|
64
|
-
return await
|
|
42
|
+
return await readPackageJsonVersion(pkgJson);
|
|
65
43
|
}
|
|
66
44
|
|
|
67
45
|
async function getInvokerVersion({ rootDir }) {
|
|
68
|
-
return await
|
|
46
|
+
return await readPackageJsonVersion(join(rootDir, 'package.json'));
|
|
69
47
|
}
|
|
70
48
|
|
|
71
49
|
async function fetchLatestVersion() {
|
|
@@ -106,7 +84,7 @@ async function cmdStatus({ rootDir, argv }) {
|
|
|
106
84
|
const runtimeDir = getRuntimeDir();
|
|
107
85
|
const runtimeVersion = await getRuntimeInstalledVersion();
|
|
108
86
|
|
|
109
|
-
const cached = await
|
|
87
|
+
const cached = await readJsonIfExists(updateJson, { defaultValue: null });
|
|
110
88
|
|
|
111
89
|
let latest = cached?.latest ?? null;
|
|
112
90
|
let checkedAt = cached?.checkedAt ?? null;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
3
|
-
import { getRootDir } from './utils/paths.mjs';
|
|
4
|
-
import { ensureEnvFileUpdated } from './utils/env_file.mjs';
|
|
5
|
-
import { resolveUserConfigEnvPath } from './utils/config.mjs';
|
|
6
|
-
import { isTty, promptSelect, withRl } from './utils/wizard.mjs';
|
|
7
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { getRootDir } from './utils/paths/paths.mjs';
|
|
4
|
+
import { ensureEnvFileUpdated } from './utils/env/env_file.mjs';
|
|
5
|
+
import { resolveUserConfigEnvPath } from './utils/env/config.mjs';
|
|
6
|
+
import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
7
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
8
|
|
|
9
9
|
const FLAVORS = [
|
|
10
10
|
{ label: 'happy-server-light (recommended default, serves UI)', value: 'happy-server-light' },
|
package/scripts/service.mjs
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
2
|
-
import { run, runCapture } from './utils/proc.mjs';
|
|
3
|
-
import { getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
4
|
-
import {
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
3
|
+
import { getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
4
|
+
import { getInternalServerUrl, getPublicServerUrlEnvOverride } from './utils/server/urls.mjs';
|
|
5
|
+
import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/service/autostart_darwin.mjs';
|
|
6
|
+
import { getCanonicalHomeDir } from './utils/env/config.mjs';
|
|
7
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
5
8
|
import { spawn } from 'node:child_process';
|
|
6
9
|
import { homedir } from 'node:os';
|
|
7
10
|
import { existsSync } from 'node:fs';
|
|
8
11
|
import { rm } from 'node:fs/promises';
|
|
9
12
|
import { dirname, join, resolve } from 'node:path';
|
|
10
13
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
14
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
12
15
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { readLastLines } from './utils/fs/tail.mjs';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Manage the autostart service installed by `happys bootstrap -- --autostart`.
|
|
@@ -33,11 +37,6 @@ function getUid() {
|
|
|
33
37
|
return Number.isFinite(n) ? n : null;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
function getInternalUrl() {
|
|
37
|
-
const port = process.env.HAPPY_LOCAL_SERVER_PORT?.trim() ? Number(process.env.HAPPY_LOCAL_SERVER_PORT) : 3005;
|
|
38
|
-
return `http://127.0.0.1:${port}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
40
|
function getAutostartEnv({ rootDir }) {
|
|
42
41
|
// IMPORTANT:
|
|
43
42
|
// LaunchAgents should NOT bake the entire config into the plist, because that would require
|
|
@@ -63,6 +62,13 @@ function getAutostartEnv({ rootDir }) {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export async function installService() {
|
|
65
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'[local] service install is disabled in sandbox mode.\n' +
|
|
68
|
+
'Reason: services are global OS state (launchd/systemd) and can affect your real installation.\n' +
|
|
69
|
+
'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
66
72
|
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
67
73
|
throw new Error('[local] service install is only supported on macOS (launchd) and Linux (systemd user).');
|
|
68
74
|
}
|
|
@@ -89,6 +95,10 @@ export async function installService() {
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
export async function uninstallService() {
|
|
98
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
99
|
+
// Sandbox cleanups should be safe and should not touch global services by default.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
92
102
|
if (process.platform !== 'darwin' && process.platform !== 'linux') return;
|
|
93
103
|
|
|
94
104
|
if (process.platform === 'linux') {
|
|
@@ -132,7 +142,7 @@ function systemdEnvLines(env) {
|
|
|
132
142
|
async function ensureSystemdUserServiceEnabled({ rootDir, label, env }) {
|
|
133
143
|
const unitPath = systemdUnitPath();
|
|
134
144
|
await mkdir(dirname(unitPath), { recursive: true });
|
|
135
|
-
const happysShim = join(
|
|
145
|
+
const happysShim = join(getCanonicalHomeDir(), 'bin', 'happys');
|
|
136
146
|
const entry = existsSync(happysShim) ? happysShim : join(rootDir, 'bin', 'happys.mjs');
|
|
137
147
|
const exec = existsSync(happysShim) ? entry : `${process.execPath} ${entry}`;
|
|
138
148
|
|
|
@@ -254,16 +264,19 @@ async function startLaunchAgent({ persistent }) {
|
|
|
254
264
|
|
|
255
265
|
async function postStartDiagnostics() {
|
|
256
266
|
const rootDir = getRootDir(import.meta.url);
|
|
257
|
-
const internalUrl =
|
|
267
|
+
const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
|
|
258
268
|
|
|
259
269
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
260
270
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
261
271
|
: join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
262
272
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
let port = 3005;
|
|
274
|
+
try {
|
|
275
|
+
port = Number(new URL(internalUrl).port || 0) || 3005;
|
|
276
|
+
} catch {
|
|
277
|
+
port = 3005;
|
|
278
|
+
}
|
|
279
|
+
const { publicServerUrl: publicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port });
|
|
267
280
|
|
|
268
281
|
const cliDir = join(rootDir, 'components', 'happy-cli');
|
|
269
282
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
@@ -273,16 +286,6 @@ async function postStartDiagnostics() {
|
|
|
273
286
|
const lockFile = join(cliHomeDir, 'daemon.state.json.lock');
|
|
274
287
|
const logsDir = join(cliHomeDir, 'logs');
|
|
275
288
|
|
|
276
|
-
const readLastLines = async (path, lines = 60) => {
|
|
277
|
-
try {
|
|
278
|
-
const raw = await readFile(path, 'utf-8');
|
|
279
|
-
const parts = raw.split('\n');
|
|
280
|
-
return parts.slice(Math.max(0, parts.length - lines)).join('\n');
|
|
281
|
-
} catch {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
|
|
286
289
|
const latestDaemonLog = async () => {
|
|
287
290
|
try {
|
|
288
291
|
const ls = await runCapture('bash', ['-lc', `ls -1t "${logsDir}"/*-daemon.log 2>/dev/null | head -1 || true`]);
|
|
@@ -448,7 +451,7 @@ async function waitForLaunchAgentStopped({ timeoutMs = 8000 } = {}) {
|
|
|
448
451
|
|
|
449
452
|
async function showStatus() {
|
|
450
453
|
const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
|
|
451
|
-
const internalUrl =
|
|
454
|
+
const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
|
|
452
455
|
|
|
453
456
|
console.log(`label: ${label}`);
|
|
454
457
|
console.log(`plist: ${plistPath} ${existsSync(plistPath) ? '(present)' : '(missing)'}`);
|
|
@@ -537,7 +540,7 @@ async function main() {
|
|
|
537
540
|
return;
|
|
538
541
|
case 'status':
|
|
539
542
|
if (json) {
|
|
540
|
-
const internalUrl =
|
|
543
|
+
const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
|
|
541
544
|
let health = null;
|
|
542
545
|
try {
|
|
543
546
|
const res = await fetch(`${internalUrl}/health`, { method: 'GET' });
|