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/run.mjs
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
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
|
-
import { killProcessTree, runCapture } from './utils/proc.mjs';
|
|
4
|
+
import { killProcessTree, runCapture, spawnProc } from './utils/proc.mjs';
|
|
5
5
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
6
6
|
import { killPortListeners } from './utils/ports.mjs';
|
|
7
|
-
import { getServerComponentName, waitForServerReady } from './utils/server.mjs';
|
|
8
|
-
import { pmSpawnScript, requireDir } from './utils/pm.mjs';
|
|
7
|
+
import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server.mjs';
|
|
8
|
+
import { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/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
|
|
13
|
-
import { startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
14
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
15
|
-
import { assertServerComponentDirMatches } from './utils/validate.mjs';
|
|
12
|
+
import { maybeResetTailscaleServe } from './tailscale.mjs';
|
|
13
|
+
import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
14
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
15
|
+
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
16
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/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/localhost_host.mjs';
|
|
22
|
+
import { openUrlInBrowser } from './utils/browser.mjs';
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Run the local stack in "production-like" mode:
|
|
@@ -30,10 +37,10 @@ async function main() {
|
|
|
30
37
|
if (wantsHelp(argv, { flags })) {
|
|
31
38
|
printResult({
|
|
32
39
|
json,
|
|
33
|
-
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon'], json: true },
|
|
40
|
+
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser'], json: true },
|
|
34
41
|
text: [
|
|
35
42
|
'[start] usage:',
|
|
36
|
-
' happys start [--server=happy-server|happy-server-light] [--json]',
|
|
43
|
+
' happys start [--server=happy-server|happy-server-light] [--restart] [--json]',
|
|
37
44
|
' (legacy in a cloned repo): pnpm start [-- --server=happy-server|happy-server-light] [--json]',
|
|
38
45
|
' note: --json prints the resolved config (dry-run) and exits.',
|
|
39
46
|
].join('\n'),
|
|
@@ -43,17 +50,14 @@ async function main() {
|
|
|
43
50
|
|
|
44
51
|
const rootDir = getRootDir(import.meta.url);
|
|
45
52
|
|
|
46
|
-
const serverPort =
|
|
47
|
-
? parseInt(process.env.HAPPY_LOCAL_SERVER_PORT, 10)
|
|
48
|
-
: 3005;
|
|
53
|
+
const serverPort = resolveServerPortFromEnv({ defaultPort: 3005 });
|
|
49
54
|
|
|
50
55
|
// Internal URL used by local processes on this machine.
|
|
51
56
|
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
52
57
|
// Public URL is what you might share/open (e.g. https://<machine>.<tailnet>.ts.net).
|
|
53
58
|
// We auto-prefer the Tailscale HTTPS URL when available, unless explicitly overridden.
|
|
54
|
-
const defaultPublicUrl =
|
|
55
|
-
|
|
56
|
-
let publicServerUrl = envPublicUrl || defaultPublicUrl;
|
|
59
|
+
const { defaultPublicUrl, envPublicUrl, publicServerUrl: publicServerUrlPreview } = getPublicServerUrlEnvOverride({ serverPort });
|
|
60
|
+
let publicServerUrl = publicServerUrlPreview;
|
|
57
61
|
|
|
58
62
|
const serverComponentName = getServerComponentName({ kv });
|
|
59
63
|
if (serverComponentName === 'both') {
|
|
@@ -62,11 +66,13 @@ async function main() {
|
|
|
62
66
|
|
|
63
67
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
64
68
|
const serveUiWanted = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
|
|
65
|
-
const serveUi = serveUiWanted
|
|
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';
|
|
66
71
|
const uiPrefix = process.env.HAPPY_LOCAL_UI_PREFIX?.trim() ? process.env.HAPPY_LOCAL_UI_PREFIX.trim() : '/';
|
|
72
|
+
const autostart = getDefaultAutostartPaths();
|
|
67
73
|
const uiBuildDir = process.env.HAPPY_LOCAL_UI_BUILD_DIR?.trim()
|
|
68
74
|
? process.env.HAPPY_LOCAL_UI_BUILD_DIR.trim()
|
|
69
|
-
: join(
|
|
75
|
+
: join(autostart.baseDir, 'ui');
|
|
70
76
|
|
|
71
77
|
const enableTailscaleServe = (process.env.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0') === '1';
|
|
72
78
|
|
|
@@ -74,6 +80,7 @@ async function main() {
|
|
|
74
80
|
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
75
81
|
|
|
76
82
|
assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
|
|
83
|
+
assertServerPrismaProviderMatches({ serverComponentName, serverDir });
|
|
77
84
|
|
|
78
85
|
await requireDir(serverComponentName, serverDir);
|
|
79
86
|
await requireDir('happy-cli', cliDir);
|
|
@@ -82,7 +89,8 @@ async function main() {
|
|
|
82
89
|
|
|
83
90
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
84
91
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
85
|
-
: join(
|
|
92
|
+
: join(autostart.baseDir, 'cli');
|
|
93
|
+
const restart = flags.has('--restart');
|
|
86
94
|
|
|
87
95
|
if (json) {
|
|
88
96
|
printResult({
|
|
@@ -105,30 +113,65 @@ async function main() {
|
|
|
105
113
|
return;
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
if (serveUiWanted && !serveUi) {
|
|
109
|
-
console.log(`[local] ui serving disabled (requires happy-server-light; you are using ${serverComponentName})`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
116
|
if (serveUi && !(await pathExists(uiBuildDir))) {
|
|
113
|
-
|
|
117
|
+
if (serverComponentName === 'happy-server-light') {
|
|
118
|
+
throw new Error(`[local] UI build directory not found at ${uiBuildDir}. Run: happys build (legacy in a cloned repo: pnpm build)`);
|
|
119
|
+
}
|
|
120
|
+
// For happy-server, UI serving is optional via the UI gateway.
|
|
121
|
+
console.log(`[local] UI build directory not found at ${uiBuildDir}; UI gateway will be disabled`);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
const children = [];
|
|
117
125
|
let shuttingDown = false;
|
|
118
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 });
|
|
133
|
+
|
|
134
|
+
// Ensure server deps exist before any Prisma/docker work.
|
|
135
|
+
await ensureDepsInstalled(serverDir, serverComponentName);
|
|
136
|
+
|
|
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
|
+
}
|
|
119
150
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
|
|
152
|
+
const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
|
|
153
|
+
if (!restart && serverAlreadyRunning && (!startDaemon || daemonAlreadyRunning)) {
|
|
154
|
+
console.log(`[local] start: stack already running (server=${internalServerUrl}${startDaemon ? ` daemon=${daemonAlreadyRunning ? 'running' : 'stopped'}` : ''})`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
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
|
+
}
|
|
128
168
|
|
|
129
169
|
// Server
|
|
130
170
|
// If a previous run left a server behind, free the port first (prevents false "ready" checks).
|
|
131
|
-
|
|
171
|
+
// NOTE: In stack mode we avoid killing arbitrary port listeners (fail-closed instead).
|
|
172
|
+
if ((!serverAlreadyRunning || restart) && !stackMode) {
|
|
173
|
+
await killPortListeners(serverPort, { label: 'server' });
|
|
174
|
+
}
|
|
132
175
|
|
|
133
176
|
const serverEnv = {
|
|
134
177
|
...baseEnv,
|
|
@@ -138,19 +181,120 @@ async function main() {
|
|
|
138
181
|
// Avoid noisy failures if a previous run left the metrics port busy.
|
|
139
182
|
// You can override with METRICS_ENABLED=true if you want it.
|
|
140
183
|
METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
|
|
141
|
-
...(serveUi
|
|
184
|
+
...(serveUi && serverComponentName === 'happy-server-light'
|
|
142
185
|
? {
|
|
143
186
|
HAPPY_SERVER_LIGHT_UI_DIR: uiBuildDir,
|
|
144
187
|
HAPPY_SERVER_LIGHT_UI_PREFIX: uiPrefix,
|
|
145
188
|
}
|
|
146
189
|
: {}),
|
|
147
190
|
};
|
|
191
|
+
let serverLightAccountCount = null;
|
|
192
|
+
let happyServerAccountCount = null;
|
|
193
|
+
if (serverComponentName === 'happy-server-light') {
|
|
194
|
+
const dataDir = baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR?.trim()
|
|
195
|
+
? baseEnv.HAPPY_SERVER_LIGHT_DATA_DIR.trim()
|
|
196
|
+
: join(autostart.baseDir, 'server-light');
|
|
197
|
+
serverEnv.HAPPY_SERVER_LIGHT_DATA_DIR = dataDir;
|
|
198
|
+
serverEnv.HAPPY_SERVER_LIGHT_FILES_DIR = baseEnv.HAPPY_SERVER_LIGHT_FILES_DIR?.trim()
|
|
199
|
+
? baseEnv.HAPPY_SERVER_LIGHT_FILES_DIR.trim()
|
|
200
|
+
: join(dataDir, 'files');
|
|
201
|
+
serverEnv.DATABASE_URL = baseEnv.DATABASE_URL?.trim()
|
|
202
|
+
? baseEnv.DATABASE_URL.trim()
|
|
203
|
+
: `file:${join(dataDir, 'happy-server-light.sqlite')}`;
|
|
204
|
+
|
|
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;
|
|
213
|
+
}
|
|
214
|
+
let effectiveInternalServerUrl = internalServerUrl;
|
|
215
|
+
if (serverComponentName === 'happy-server') {
|
|
216
|
+
const managed = (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0';
|
|
217
|
+
if (managed) {
|
|
218
|
+
const envPath = baseEnv.HAPPY_STACKS_ENV_FILE ?? baseEnv.HAPPY_LOCAL_ENV_FILE ?? '';
|
|
219
|
+
const infra = await ensureHappyServerManagedInfra({
|
|
220
|
+
stackName: autostart.stackName,
|
|
221
|
+
baseDir: autostart.baseDir,
|
|
222
|
+
serverPort,
|
|
223
|
+
publicServerUrl,
|
|
224
|
+
envPath,
|
|
225
|
+
env: baseEnv,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Backend runs on a separate port; gateway owns the public port.
|
|
229
|
+
const backendPortRaw = (baseEnv.HAPPY_STACKS_HAPPY_SERVER_BACKEND_PORT ?? baseEnv.HAPPY_LOCAL_HAPPY_SERVER_BACKEND_PORT ?? '').trim();
|
|
230
|
+
const backendPort = backendPortRaw ? Number(backendPortRaw) : serverPort + 10;
|
|
231
|
+
const backendUrl = `http://127.0.0.1:${backendPort}`;
|
|
232
|
+
if (!stackMode) {
|
|
233
|
+
await killPortListeners(backendPort, { label: 'happy-server-backend' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const backendEnv = { ...serverEnv, ...infra.env, PORT: String(backendPort) };
|
|
237
|
+
const autoMigrate = (baseEnv.HAPPY_STACKS_PRISMA_MIGRATE ?? baseEnv.HAPPY_LOCAL_PRISMA_MIGRATE ?? '1') !== '0';
|
|
238
|
+
if (autoMigrate) {
|
|
239
|
+
await applyHappyServerMigrations({ serverDir, env: backendEnv });
|
|
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;
|
|
249
|
+
|
|
250
|
+
const backend = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: backendEnv });
|
|
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
|
+
}
|
|
258
|
+
await waitForServerReady(backendUrl);
|
|
259
|
+
|
|
260
|
+
const gatewayArgs = [
|
|
261
|
+
join(rootDir, 'scripts', 'ui_gateway.mjs'),
|
|
262
|
+
`--port=${serverPort}`,
|
|
263
|
+
`--backend-url=${backendUrl}`,
|
|
264
|
+
`--minio-port=${infra.env.S3_PORT}`,
|
|
265
|
+
`--bucket=${infra.env.S3_BUCKET}`,
|
|
266
|
+
];
|
|
267
|
+
if (serveUi && (await pathExists(uiBuildDir))) {
|
|
268
|
+
gatewayArgs.push(`--ui-dir=${uiBuildDir}`);
|
|
269
|
+
} else {
|
|
270
|
+
gatewayArgs.push('--no-ui');
|
|
271
|
+
}
|
|
148
272
|
|
|
149
|
-
|
|
150
|
-
|
|
273
|
+
const gateway = spawnProc('ui', process.execPath, gatewayArgs, { ...backendEnv, PORT: String(serverPort) }, { cwd: rootDir });
|
|
274
|
+
children.push(gateway);
|
|
275
|
+
if (stackMode && runtimeStatePath) {
|
|
276
|
+
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { uiGatewayPid: gateway.pid } }).catch(() => {});
|
|
277
|
+
}
|
|
278
|
+
await waitForServerReady(internalServerUrl);
|
|
279
|
+
effectiveInternalServerUrl = internalServerUrl;
|
|
151
280
|
|
|
152
|
-
|
|
153
|
-
|
|
281
|
+
// Skip default server spawn below
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Default server start (happy-server-light, or happy-server without managed infra).
|
|
286
|
+
if (!(serverComponentName === 'happy-server' && (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0')) {
|
|
287
|
+
if (!serverAlreadyRunning || restart) {
|
|
288
|
+
const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: serverEnv });
|
|
289
|
+
children.push(server);
|
|
290
|
+
if (stackMode && runtimeStatePath) {
|
|
291
|
+
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { serverPid: server.pid } }).catch(() => {});
|
|
292
|
+
}
|
|
293
|
+
await waitForServerReady(internalServerUrl);
|
|
294
|
+
} else {
|
|
295
|
+
console.log(`[local] server already running at ${internalServerUrl}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
154
298
|
|
|
155
299
|
if (enableTailscaleServe) {
|
|
156
300
|
try {
|
|
@@ -167,9 +311,9 @@ async function main() {
|
|
|
167
311
|
}
|
|
168
312
|
|
|
169
313
|
if (serveUi) {
|
|
170
|
-
const localUi =
|
|
314
|
+
const localUi = effectiveInternalServerUrl.replace(/\/+$/, '') + '/';
|
|
171
315
|
console.log(`[local] ui served locally at ${localUi}`);
|
|
172
|
-
if (publicServerUrl && publicServerUrl !==
|
|
316
|
+
if (publicServerUrl && publicServerUrl !== effectiveInternalServerUrl && publicServerUrl !== localUi && publicServerUrl !== defaultPublicUrl) {
|
|
173
317
|
const pubUi = publicServerUrl.replace(/\/+$/, '') + '/';
|
|
174
318
|
console.log(`[local] public url: ${pubUi}`);
|
|
175
319
|
}
|
|
@@ -179,20 +323,55 @@ async function main() {
|
|
|
179
323
|
|
|
180
324
|
console.log(
|
|
181
325
|
`[local] tip: to run 'happy' from your terminal *against this local server* (and have sessions show up in the UI), use:\n` +
|
|
182
|
-
`export HAPPY_SERVER_URL=\"${
|
|
326
|
+
`export HAPPY_SERVER_URL=\"${effectiveInternalServerUrl}\"\n` +
|
|
183
327
|
`export HAPPY_HOME_DIR=\"${cliHomeDir}\"\n` +
|
|
184
328
|
`export HAPPY_WEBAPP_URL=\"${publicServerUrl}\"\n`
|
|
185
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
|
+
}
|
|
186
342
|
}
|
|
187
343
|
|
|
188
344
|
// Daemon
|
|
189
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
|
+
});
|
|
190
368
|
await startLocalDaemonWithAuth({
|
|
191
369
|
cliBin,
|
|
192
370
|
cliHomeDir,
|
|
193
|
-
internalServerUrl,
|
|
371
|
+
internalServerUrl: effectiveInternalServerUrl,
|
|
194
372
|
publicServerUrl,
|
|
195
373
|
isShuttingDown: () => shuttingDown,
|
|
374
|
+
forceRestart: restart,
|
|
196
375
|
});
|
|
197
376
|
}
|
|
198
377
|
|
|
@@ -204,7 +383,7 @@ async function main() {
|
|
|
204
383
|
console.log('\n[local] shutting down...');
|
|
205
384
|
|
|
206
385
|
if (startDaemon) {
|
|
207
|
-
|
|
386
|
+
await stopLocalDaemon({ cliBin, internalServerUrl: effectiveInternalServerUrl, cliHomeDir });
|
|
208
387
|
}
|
|
209
388
|
|
|
210
389
|
for (const child of children) {
|
package/scripts/self.mjs
CHANGED
|
@@ -2,20 +2,16 @@ import './utils/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';
|
|
7
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
8
|
import { pathExists } from './utils/fs.mjs';
|
|
10
9
|
import { run, runCapture } from './utils/proc.mjs';
|
|
10
|
+
import { expandHome } from './utils/canonical_home.mjs';
|
|
11
11
|
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
|
|
12
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
12
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
13
13
|
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
14
14
|
|
|
15
|
-
function expandHome(p) {
|
|
16
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
function cachePaths() {
|
|
20
16
|
const home = getHappyStacksHomeDir();
|
|
21
17
|
return {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { getRootDir } from './utils/paths.mjs';
|
|
4
4
|
import { ensureEnvFileUpdated } from './utils/env_file.mjs';
|
|
5
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';
|
|
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' },
|