happy-stacks 0.2.0 → 0.4.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 +84 -25
- package/bin/happys.mjs +116 -17
- package/docs/happy-development.md +2 -2
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/package.json +5 -1
- package/scripts/auth.mjs +59 -208
- package/scripts/build.mjs +58 -12
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +168 -20
- package/scripts/dev.mjs +196 -70
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +105 -78
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +8 -14
- package/scripts/install.mjs +119 -23
- package/scripts/lint.mjs +31 -32
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +93 -108
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +217 -0
- package/scripts/review_pr.mjs +368 -0
- package/scripts/run.mjs +95 -21
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +19 -29
- package/scripts/setup.mjs +63 -160
- package/scripts/setup_pr.mjs +592 -52
- package/scripts/stack.mjs +608 -200
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +44 -11
- package/scripts/test.mjs +52 -36
- package/scripts/tui.mjs +314 -74
- package/scripts/typecheck.mjs +31 -32
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
- package/scripts/utils/auth/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
- package/scripts/utils/auth/sources.mjs +38 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +24 -0
- package/scripts/utils/cli/cwd_scope.mjs +82 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
- package/scripts/utils/dev/expo_dev.mjs +246 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
- package/scripts/utils/dev_auth_key.mjs +1 -1
- package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -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/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- 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/git.mjs +67 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
- package/scripts/utils/handy_master_secret.mjs +2 -2
- package/scripts/utils/mobile/config.mjs +31 -0
- package/scripts/utils/mobile/dev_client_links.mjs +60 -0
- package/scripts/utils/mobile/identifiers.mjs +47 -0
- package/scripts/utils/mobile/identifiers.test.mjs +42 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +56 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/runners/coderabbit.mjs +19 -0
- package/scripts/utils/review/runners/codex.mjs +51 -0
- package/scripts/utils/review/targets.mjs +24 -0
- package/scripts/utils/review/targets.test.mjs +36 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- package/scripts/utils/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +101 -0
- package/scripts/utils/server/validate.mjs +88 -0
- package/scripts/utils/service/autostart_darwin.mjs +182 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -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/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
- package/scripts/utils/stack_context.mjs +3 -3
- package/scripts/utils/stack_runtime_state.mjs +1 -1
- package/scripts/utils/stacks.mjs +2 -2
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/validate.mjs +1 -1
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +171 -113
- package/scripts/utils/auth_sources.mjs +0 -12
- package/scripts/utils/dev_expo_web.mjs +0 -112
- package/scripts/utils/localhost_host.mjs +0 -17
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
- /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
- /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/scripts/dev.mjs
CHANGED
|
@@ -1,36 +1,31 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { killProcessTree } from './utils/proc.mjs';
|
|
4
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { killPortListeners } from './utils/ports.mjs';
|
|
6
|
-
import { getServerComponentName, isHappyServerRunning } from './utils/server.mjs';
|
|
7
|
-
import { requireDir } from './utils/pm.mjs';
|
|
3
|
+
import { killProcessTree } from './utils/proc/proc.mjs';
|
|
4
|
+
import { componentDirEnvKey, 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
11
|
import { isDaemonRunning, stopLocalDaemon } from './daemon.mjs';
|
|
12
12
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
13
|
-
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
14
|
-
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
|
|
15
|
-
import { isPidAlive, readStackRuntimeStateFile, recordStackRuntimeStart } from './utils/
|
|
16
|
-
import { resolveStackContext } from './utils/
|
|
17
|
-
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/
|
|
18
|
-
import { ensureDevCliReady, prepareDaemonAuthSeed, startDevDaemon, watchHappyCliAndRestartDaemon } from './utils/
|
|
19
|
-
import { startDevServer, watchDevServerAndRestart } from './utils/
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { openUrlInBrowser } from './utils/browser.mjs';
|
|
23
|
-
import { waitForHttpOk } from './utils/server.mjs';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.replace(/-+/g, '-')
|
|
30
|
-
.replace(/^-+/, '')
|
|
31
|
-
.replace(/-+$/, '');
|
|
32
|
-
return s || fallback;
|
|
33
|
-
}
|
|
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 { ensureDevExpoServer } from './utils/dev/expo_dev.mjs';
|
|
21
|
+
import { preferStackLocalhostUrl } 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';
|
|
25
|
+
import { getAccountCountForServerComponent, resolveAutoCopyFromMainEnabled } from './utils/stack/startup.mjs';
|
|
26
|
+
import { maybeRunInteractiveStackAuthSetup } from './utils/auth/interactive_stack_auth.mjs';
|
|
27
|
+
import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
|
|
28
|
+
import { daemonStartGate, formatDaemonAuthRequiredError } from './utils/auth/daemon_gate.mjs';
|
|
34
29
|
|
|
35
30
|
/**
|
|
36
31
|
* Dev mode stack:
|
|
@@ -46,20 +41,38 @@ async function main() {
|
|
|
46
41
|
if (wantsHelp(argv, { flags })) {
|
|
47
42
|
printResult({
|
|
48
43
|
json,
|
|
49
|
-
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--watch', '--no-watch', '--no-browser'], json: true },
|
|
44
|
+
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--watch', '--no-watch', '--no-browser', '--mobile'], json: true },
|
|
50
45
|
text: [
|
|
51
46
|
'[dev] usage:',
|
|
52
47
|
' happys dev [--server=happy-server|happy-server-light] [--restart] [--json]',
|
|
53
48
|
' happys dev --watch # rebuild/restart happy-cli daemon on file changes (TTY default)',
|
|
54
49
|
' happys dev --no-watch # disable watch mode (always disabled in non-interactive mode)',
|
|
55
50
|
' happys dev --no-browser # do not open the UI in your browser automatically',
|
|
51
|
+
' happys dev --mobile # also start Expo dev-client Metro for mobile',
|
|
56
52
|
' note: --json prints the resolved config (dry-run) and exits.',
|
|
53
|
+
'',
|
|
54
|
+
'note:',
|
|
55
|
+
' If run from inside a component checkout/worktree, that checkout is used for this run (without requiring `happys wt use`).',
|
|
57
56
|
].join('\n'),
|
|
58
57
|
});
|
|
59
58
|
return;
|
|
60
59
|
}
|
|
61
60
|
const rootDir = getRootDir(import.meta.url);
|
|
62
61
|
|
|
62
|
+
const inferred = inferComponentFromCwd({
|
|
63
|
+
rootDir,
|
|
64
|
+
invokedCwd: getInvokedCwd(process.env),
|
|
65
|
+
components: ['happy', 'happy-cli', 'happy-server-light', 'happy-server'],
|
|
66
|
+
});
|
|
67
|
+
if (inferred) {
|
|
68
|
+
const stacksKey = componentDirEnvKey(inferred.component);
|
|
69
|
+
const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
|
|
70
|
+
// Stack env should win. Only infer from CWD when the component dir isn't already configured.
|
|
71
|
+
if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
|
|
72
|
+
process.env[stacksKey] = inferred.repoDir;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
const serverComponentName = getServerComponentName({ kv });
|
|
64
77
|
if (serverComponentName === 'both') {
|
|
65
78
|
throw new Error(`[local] --server=both is not supported for dev (pick one: happy-server-light or happy-server)`);
|
|
@@ -67,6 +80,7 @@ async function main() {
|
|
|
67
80
|
|
|
68
81
|
const startUi = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_UI ?? '1') !== '0';
|
|
69
82
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
83
|
+
const startMobile = flags.has('--mobile') || flags.has('--with-mobile');
|
|
70
84
|
const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
|
|
71
85
|
|
|
72
86
|
const serverDir = getComponentDir(rootDir, serverComponentName);
|
|
@@ -91,7 +105,10 @@ async function main() {
|
|
|
91
105
|
// - Only the main stack should ever auto-enable (or prefer) Tailscale Serve by default.
|
|
92
106
|
// - Non-main stacks should default to localhost URLs unless the user explicitly configured a public URL
|
|
93
107
|
// OR Tailscale Serve is already configured for this stack's internal URL (status matches).
|
|
94
|
-
const allowEnableTailscale =
|
|
108
|
+
const allowEnableTailscale =
|
|
109
|
+
!stackMode ||
|
|
110
|
+
stackName === 'main' ||
|
|
111
|
+
(baseEnv.HAPPY_STACKS_TAILSCALE_SERVE ?? baseEnv.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0').toString().trim() === '1';
|
|
95
112
|
const resolvedUrls = await resolveServerUrls({ env: baseEnv, serverPort, allowEnable: allowEnableTailscale });
|
|
96
113
|
const internalServerUrl = resolvedUrls.internalServerUrl;
|
|
97
114
|
let publicServerUrl = resolvedUrls.publicServerUrl;
|
|
@@ -102,6 +119,8 @@ async function main() {
|
|
|
102
119
|
publicServerUrl = resolvedUrls.defaultPublicUrl;
|
|
103
120
|
}
|
|
104
121
|
}
|
|
122
|
+
// Expo app config: this is what both web + native app use to reach the Happy server.
|
|
123
|
+
// LAN rewrite (for dev-client) is centralized in ensureDevExpoServer.
|
|
105
124
|
const uiApiUrl = resolvedUrls.defaultPublicUrl;
|
|
106
125
|
const restart = flags.has('--restart');
|
|
107
126
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
@@ -121,6 +140,7 @@ async function main() {
|
|
|
121
140
|
internalServerUrl,
|
|
122
141
|
publicServerUrl,
|
|
123
142
|
startUi,
|
|
143
|
+
startMobile,
|
|
124
144
|
startDaemon,
|
|
125
145
|
cliHomeDir,
|
|
126
146
|
},
|
|
@@ -144,13 +164,25 @@ async function main() {
|
|
|
144
164
|
const serverAlreadyRunning = await isHappyServerRunning(internalServerUrl);
|
|
145
165
|
const daemonAlreadyRunning = startDaemon ? isDaemonRunning(cliHomeDir) : false;
|
|
146
166
|
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
// Expo dev server state (worktree-scoped): single Expo process per stack/worktree.
|
|
168
|
+
const startExpo = startUi || startMobile;
|
|
169
|
+
const expoPaths = getExpoStatePaths({
|
|
170
|
+
baseDir: autostart.baseDir,
|
|
171
|
+
kind: 'expo-dev',
|
|
172
|
+
projectDir: uiDir,
|
|
173
|
+
stateFileName: 'expo.state.json',
|
|
174
|
+
});
|
|
175
|
+
const expoRunning = startExpo ? await isStateProcessRunning(expoPaths.statePath) : { running: false, state: null };
|
|
176
|
+
let expoAlreadyRunning = Boolean(expoRunning.running);
|
|
177
|
+
|
|
178
|
+
if (!restart && serverAlreadyRunning && (!startDaemon || daemonAlreadyRunning) && (!startExpo || expoAlreadyRunning)) {
|
|
179
|
+
console.log(
|
|
180
|
+
`[local] dev: stack already running (server=${internalServerUrl}` +
|
|
181
|
+
`${startDaemon ? ` daemon=${daemonAlreadyRunning ? 'running' : 'stopped'}` : ''}` +
|
|
182
|
+
`${startUi ? ` ui=${expoAlreadyRunning ? 'running' : 'stopped'}` : ''}` +
|
|
183
|
+
`${startMobile ? ` mobile=${expoAlreadyRunning ? 'running' : 'stopped'}` : ''}` +
|
|
184
|
+
`)`
|
|
185
|
+
);
|
|
154
186
|
return;
|
|
155
187
|
}
|
|
156
188
|
|
|
@@ -202,6 +234,75 @@ async function main() {
|
|
|
202
234
|
// - Ensure schema exists (server-light: db push; happy-server: migrate deploy if tables missing)
|
|
203
235
|
// - Auto-seed from main only when needed (non-main + non-interactive default, and only if missing creds or 0 accounts)
|
|
204
236
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
237
|
+
const accountProbe = await getAccountCountForServerComponent({
|
|
238
|
+
serverComponentName,
|
|
239
|
+
serverDir,
|
|
240
|
+
env: serverEnv,
|
|
241
|
+
bestEffort: true,
|
|
242
|
+
});
|
|
243
|
+
const accountCount = typeof accountProbe.accountCount === 'number' ? accountProbe.accountCount : null;
|
|
244
|
+
const autoSeedEnabled = resolveAutoCopyFromMainEnabled({ env: baseEnv, stackName, isInteractive });
|
|
245
|
+
|
|
246
|
+
let expoResEarly = null;
|
|
247
|
+
const wantsAuthFlow =
|
|
248
|
+
(baseEnv.HAPPY_STACKS_AUTH_FLOW ?? baseEnv.HAPPY_LOCAL_AUTH_FLOW ?? '').toString().trim() === '1' ||
|
|
249
|
+
(baseEnv.HAPPY_STACKS_DAEMON_WAIT_FOR_AUTH ?? baseEnv.HAPPY_LOCAL_DAEMON_WAIT_FOR_AUTH ?? '').toString().trim() === '1';
|
|
250
|
+
|
|
251
|
+
// CRITICAL (review-pr / setup-pr guided login):
|
|
252
|
+
// In background/non-interactive runs, the daemon may block on auth. If we wait to start Expo web
|
|
253
|
+
// until after the daemon is authenticated, guided login will have no UI origin and will fall back
|
|
254
|
+
// to the server port (wrong). Start Expo web UI early when running an auth flow.
|
|
255
|
+
if (wantsAuthFlow && startUi && !expoResEarly) {
|
|
256
|
+
expoResEarly = await ensureDevExpoServer({
|
|
257
|
+
startUi,
|
|
258
|
+
startMobile,
|
|
259
|
+
uiDir,
|
|
260
|
+
autostart,
|
|
261
|
+
baseEnv,
|
|
262
|
+
apiServerUrl: uiApiUrl,
|
|
263
|
+
restart,
|
|
264
|
+
stackMode,
|
|
265
|
+
runtimeStatePath,
|
|
266
|
+
stackName,
|
|
267
|
+
envPath,
|
|
268
|
+
children,
|
|
269
|
+
spawnOptions: { stdio: ['ignore', 'ignore', 'ignore'] },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
await maybeRunInteractiveStackAuthSetup({
|
|
273
|
+
rootDir,
|
|
274
|
+
// In dev mode, guided login must target the Expo web UI origin (not the server port).
|
|
275
|
+
// Mark this as an auth-flow so URL resolution fails closed if Expo isn't ready.
|
|
276
|
+
env: startUi ? { ...baseEnv, HAPPY_STACKS_AUTH_FLOW: '1', HAPPY_LOCAL_AUTH_FLOW: '1' } : baseEnv,
|
|
277
|
+
stackName,
|
|
278
|
+
cliHomeDir,
|
|
279
|
+
accountCount,
|
|
280
|
+
isInteractive,
|
|
281
|
+
autoSeedEnabled,
|
|
282
|
+
beforeLogin: async () => {
|
|
283
|
+
if (!startUi) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`[local] auth: interactive login requires the web UI.\n` +
|
|
286
|
+
`Re-run without --no-ui, or set HAPPY_WEBAPP_URL to a reachable Happy UI for this stack.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (expoResEarly) return;
|
|
290
|
+
expoResEarly = await ensureDevExpoServer({
|
|
291
|
+
startUi,
|
|
292
|
+
startMobile,
|
|
293
|
+
uiDir,
|
|
294
|
+
autostart,
|
|
295
|
+
baseEnv,
|
|
296
|
+
apiServerUrl: uiApiUrl,
|
|
297
|
+
restart,
|
|
298
|
+
stackMode,
|
|
299
|
+
runtimeStatePath,
|
|
300
|
+
stackName,
|
|
301
|
+
envPath,
|
|
302
|
+
children,
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
});
|
|
205
306
|
await prepareDaemonAuthSeed({
|
|
206
307
|
rootDir,
|
|
207
308
|
env: baseEnv,
|
|
@@ -215,19 +316,35 @@ async function main() {
|
|
|
215
316
|
quiet: false,
|
|
216
317
|
});
|
|
217
318
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
319
|
+
if (startDaemon) {
|
|
320
|
+
const gate = daemonStartGate({ env: baseEnv, cliHomeDir });
|
|
321
|
+
if (!gate.ok) {
|
|
322
|
+
// In orchestrated auth flows (setup-pr/review-pr), we intentionally keep server/UI up
|
|
323
|
+
// for guided login and start daemon post-auth from the orchestrator.
|
|
324
|
+
if (gate.reason === 'auth_flow_missing_credentials') {
|
|
325
|
+
console.log('[local] auth flow: skipping daemon start until credentials exist');
|
|
326
|
+
} else {
|
|
327
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
328
|
+
if (!isInteractive) {
|
|
329
|
+
throw new Error(formatDaemonAuthRequiredError({ stackName, cliHomeDir }));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
await startDevDaemon({
|
|
334
|
+
startDaemon,
|
|
335
|
+
cliBin,
|
|
336
|
+
cliHomeDir,
|
|
337
|
+
internalServerUrl,
|
|
338
|
+
publicServerUrl,
|
|
339
|
+
restart,
|
|
340
|
+
isShuttingDown: () => shuttingDown,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
227
344
|
|
|
228
345
|
const cliWatcher = watchHappyCliAndRestartDaemon({
|
|
229
346
|
enabled: watchEnabled,
|
|
230
|
-
startDaemon,
|
|
347
|
+
startDaemon: startDaemon && daemonStartGate({ env: baseEnv, cliHomeDir }).ok,
|
|
231
348
|
buildCli,
|
|
232
349
|
cliDir,
|
|
233
350
|
cliBin,
|
|
@@ -272,43 +389,52 @@ async function main() {
|
|
|
272
389
|
);
|
|
273
390
|
}
|
|
274
391
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
392
|
+
const expoRes =
|
|
393
|
+
expoResEarly ??
|
|
394
|
+
(await ensureDevExpoServer({
|
|
395
|
+
startUi,
|
|
396
|
+
startMobile,
|
|
397
|
+
uiDir,
|
|
398
|
+
autostart,
|
|
399
|
+
baseEnv,
|
|
400
|
+
apiServerUrl: uiApiUrl,
|
|
401
|
+
restart,
|
|
402
|
+
stackMode,
|
|
403
|
+
runtimeStatePath,
|
|
404
|
+
stackName,
|
|
405
|
+
envPath,
|
|
406
|
+
children,
|
|
407
|
+
}));
|
|
288
408
|
if (startUi) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
console.log(`[local] ui: open
|
|
295
|
-
} else if (
|
|
409
|
+
const uiPort = expoRes?.port;
|
|
410
|
+
const uiUrlRaw = uiPort ? `http://localhost:${uiPort}` : '';
|
|
411
|
+
const uiUrl = uiUrlRaw ? await preferStackLocalhostUrl(uiUrlRaw, { stackName }) : '';
|
|
412
|
+
if (expoRes?.reason === 'already_running' && expoRes.port) {
|
|
413
|
+
console.log(`[local] ui already running (pid=${expoRes.pid}, port=${expoRes.port})`);
|
|
414
|
+
if (uiUrl) console.log(`[local] ui: open ${uiUrl}`);
|
|
415
|
+
} else if (expoRes?.skipped === false && expoRes.port) {
|
|
416
|
+
if (uiUrl) console.log(`[local] ui: open ${uiUrl}`);
|
|
417
|
+
} else if (expoRes?.skipped && expoRes?.reason === 'already_running') {
|
|
296
418
|
console.log('[local] ui already running (skipping Expo start)');
|
|
297
419
|
}
|
|
298
420
|
|
|
299
421
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
300
|
-
const shouldOpen = isInteractive && !noBrowser && Boolean(
|
|
422
|
+
const shouldOpen = isInteractive && !noBrowser && Boolean(expoRes?.port);
|
|
301
423
|
if (shouldOpen) {
|
|
302
|
-
const url = `http://${host}:${uiRes.port}`;
|
|
303
424
|
// Prefer localhost for readiness checks (faster/more reliable), but open the stack-scoped hostname.
|
|
304
|
-
await waitForHttpOk(`http://localhost:${
|
|
305
|
-
const res = await openUrlInBrowser(
|
|
425
|
+
await waitForHttpOk(`http://localhost:${expoRes.port}`, { timeoutMs: 30_000 }).catch(() => {});
|
|
426
|
+
const res = await openUrlInBrowser(uiUrl);
|
|
306
427
|
if (!res.ok) {
|
|
307
428
|
console.warn(`[local] ui: failed to open browser automatically (${res.error}).`);
|
|
308
429
|
}
|
|
309
430
|
}
|
|
310
431
|
}
|
|
311
432
|
|
|
433
|
+
if (startMobile && expoRes?.port) {
|
|
434
|
+
const metroUrl = await preferStackLocalhostUrl(`http://localhost:${expoRes.port}`, { stackName });
|
|
435
|
+
console.log(`[local] mobile: metro ${metroUrl}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
312
438
|
const shutdown = async () => {
|
|
313
439
|
if (shuttingDown) {
|
|
314
440
|
return;
|
package/scripts/doctor.mjs
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { pathExists } from './utils/fs.mjs';
|
|
4
|
-
import { runCapture } from './utils/proc.mjs';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
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 { readFile } from 'node:fs/promises';
|
|
14
15
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
15
|
-
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
16
|
-
import { assertServerComponentDirMatches } from './utils/validate.mjs';
|
|
17
|
-
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/
|
|
18
|
-
import { resolveStackContext } from './utils/
|
|
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';
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Doctor script for common happy-stacks failure modes.
|
|
@@ -43,7 +46,8 @@ async function fetchHealth(url) {
|
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
// Prefer /health when available, but fall back to / (matches waitForServerReady).
|
|
46
|
-
const
|
|
49
|
+
const healthRaw = await fetchHappyHealth(url);
|
|
50
|
+
const health = { ok: healthRaw.ok, status: healthRaw.status, body: healthRaw.text ? healthRaw.text.trim() : null };
|
|
47
51
|
if (health.ok) {
|
|
48
52
|
return health;
|
|
49
53
|
}
|
|
@@ -54,26 +58,6 @@ async function fetchHealth(url) {
|
|
|
54
58
|
return health.ok ? health : root;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
async function readJsonSafe(path) {
|
|
58
|
-
try {
|
|
59
|
-
const raw = await readFile(path, 'utf-8');
|
|
60
|
-
return JSON.parse(raw);
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function readPkgVersion(path) {
|
|
67
|
-
try {
|
|
68
|
-
const raw = await readFile(path, 'utf-8');
|
|
69
|
-
const pkg = JSON.parse(raw);
|
|
70
|
-
const v = String(pkg.version ?? '').trim();
|
|
71
|
-
return v || null;
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
61
|
async function resolveSwiftbarPluginsDir() {
|
|
78
62
|
if (process.platform !== 'darwin') {
|
|
79
63
|
return null;
|
|
@@ -114,8 +98,8 @@ async function main() {
|
|
|
114
98
|
const workspaceDir = getWorkspaceDir(rootDir);
|
|
115
99
|
const updateCachePath = join(homeDir, 'cache', 'update.json');
|
|
116
100
|
const runtimePkgJson = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
|
|
117
|
-
const runtimeVersion = await
|
|
118
|
-
const updateCache =
|
|
101
|
+
const runtimeVersion = await readPackageJsonVersion(runtimePkgJson);
|
|
102
|
+
const updateCache = await readJsonIfExists(updateCachePath, { defaultValue: null });
|
|
119
103
|
|
|
120
104
|
const autostart = getDefaultAutostartPaths();
|
|
121
105
|
const stackCtx = resolveStackContext({ env: process.env, autostart });
|
|
@@ -302,7 +286,7 @@ async function main() {
|
|
|
302
286
|
|
|
303
287
|
// happy wrapper
|
|
304
288
|
try {
|
|
305
|
-
const happyPath =
|
|
289
|
+
const happyPath = await resolveCommandPath('happy');
|
|
306
290
|
if (happyPath) {
|
|
307
291
|
report.checks.happyOnPath = { ok: true, path: happyPath };
|
|
308
292
|
if (!json) console.log(`✅ happy on PATH: ${happyPath}`);
|
|
@@ -314,7 +298,7 @@ async function main() {
|
|
|
314
298
|
|
|
315
299
|
// happys on PATH
|
|
316
300
|
try {
|
|
317
|
-
const happysPath =
|
|
301
|
+
const happysPath = await resolveCommandPath('happys');
|
|
318
302
|
if (happysPath) {
|
|
319
303
|
report.checks.happysOnPath = { ok: true, path: happysPath };
|
|
320
304
|
if (!json) console.log(`✅ happys on PATH: ${happysPath}`);
|