happy-stacks 0.2.0 → 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 +59 -22
- package/bin/happys.mjs +2 -2
- package/package.json +1 -1
- package/scripts/auth.mjs +49 -202
- package/scripts/build.mjs +5 -6
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +9 -17
- package/scripts/dev.mjs +18 -27
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +102 -77
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +5 -13
- package/scripts/install.mjs +8 -8
- package/scripts/lint.mjs +8 -29
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +13 -12
- package/scripts/run.mjs +15 -15
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +18 -28
- package/scripts/setup.mjs +26 -122
- package/scripts/setup_pr.mjs +11 -28
- package/scripts/stack.mjs +111 -161
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +7 -10
- package/scripts/test.mjs +8 -29
- package/scripts/tui.mjs +8 -38
- package/scripts/typecheck.mjs +8 -29
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/{dev_auth_key.mjs → auth/dev_key.mjs} +2 -8
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- package/scripts/utils/{handy_master_secret.mjs → auth/handy_master_secret.mjs} +6 -32
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- 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} +4 -4
- package/scripts/utils/{dev_expo_web.mjs → dev/expo_web.mjs} +5 -5
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +7 -7
- 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.mjs → expo/expo.mjs} +3 -9
- 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} +3 -3
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
- package/scripts/utils/{localhost_host.mjs → paths/localhost_host.mjs} +2 -10
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +10 -7
- 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/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +65 -152
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +1 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -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 → stack/context.mjs} +2 -2
- 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 → stack/runtime_state.mjs} +10 -27
- package/scripts/utils/{stacks.mjs → stack/stacks.mjs} +9 -2
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +2 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +9 -15
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +30 -58
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +0 -0
- /package/scripts/utils/{auth_sources.mjs → auth/sources.mjs} +0 -0
- /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/mobile.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { pickNextFreeTcpPort } from './utils/ports.mjs';
|
|
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';
|
|
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
7
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
|
-
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo.mjs';
|
|
9
|
-
import { killProcessGroupOwnedByStack } from './utils/ownership.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';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Mobile dev helper for the embedded `components/happy` Expo app.
|
|
@@ -58,7 +59,7 @@ async function main() {
|
|
|
58
59
|
'',
|
|
59
60
|
'Notes:',
|
|
60
61
|
'- This script is designed to avoid editing upstream `components/happy` config in-place.',
|
|
61
|
-
'-
|
|
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.',
|
|
62
63
|
].join('\n'),
|
|
63
64
|
});
|
|
64
65
|
return;
|
|
@@ -141,10 +142,10 @@ async function main() {
|
|
|
141
142
|
|
|
142
143
|
// Allow happy-stacks to define the default server URL baked into the app bundle.
|
|
143
144
|
// This is read by the app via `process.env.EXPO_PUBLIC_HAPPY_SERVER_URL`.
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
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;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
if (json) {
|
package/scripts/run.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
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 { 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 { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/pm.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
12
|
import { maybeResetTailscaleServe } from './tailscale.mjs';
|
|
13
13
|
import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
14
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/
|
|
18
|
-
import { recordStackRuntimeStart, recordStackRuntimeUpdate } from './utils/
|
|
19
|
-
import { resolveStackContext } from './utils/
|
|
20
|
-
import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv, resolveServerUrls } from './utils/
|
|
21
|
-
import { resolveLocalhostHost } from './utils/localhost_host.mjs';
|
|
22
|
-
import { openUrlInBrowser } from './utils/browser.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';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Run the local stack in "production-like" mode:
|
package/scripts/self.mjs
CHANGED
|
@@ -1,16 +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
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
7
7
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
8
|
-
import { pathExists } from './utils/fs.mjs';
|
|
9
|
-
import { run, runCapture } from './utils/proc.mjs';
|
|
10
|
-
import { expandHome } from './utils/canonical_home.mjs';
|
|
11
|
-
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.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
12
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
13
|
-
import { getRuntimeDir } from './utils/runtime.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';
|
|
14
16
|
|
|
15
17
|
function cachePaths() {
|
|
16
18
|
const home = getHappyStacksHomeDir();
|
|
@@ -21,15 +23,6 @@ function cachePaths() {
|
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
async function readJsonSafe(path) {
|
|
25
|
-
try {
|
|
26
|
-
const raw = await readFile(path, 'utf-8');
|
|
27
|
-
return JSON.parse(raw);
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
26
|
async function writeJsonSafe(path, obj) {
|
|
34
27
|
try {
|
|
35
28
|
await mkdir(join(path, '..'), { recursive: true });
|
|
@@ -43,25 +36,14 @@ async function writeJsonSafe(path, obj) {
|
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
async function readPkgVersion(pkgJsonPath) {
|
|
47
|
-
try {
|
|
48
|
-
const raw = await readFile(pkgJsonPath, 'utf-8');
|
|
49
|
-
const pkg = JSON.parse(raw);
|
|
50
|
-
const v = String(pkg.version ?? '').trim();
|
|
51
|
-
return v || null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
39
|
async function getRuntimeInstalledVersion() {
|
|
58
40
|
const runtimeDir = getRuntimeDir();
|
|
59
41
|
const pkgJson = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
|
|
60
|
-
return await
|
|
42
|
+
return await readPackageJsonVersion(pkgJson);
|
|
61
43
|
}
|
|
62
44
|
|
|
63
45
|
async function getInvokerVersion({ rootDir }) {
|
|
64
|
-
return await
|
|
46
|
+
return await readPackageJsonVersion(join(rootDir, 'package.json'));
|
|
65
47
|
}
|
|
66
48
|
|
|
67
49
|
async function fetchLatestVersion() {
|
|
@@ -102,7 +84,7 @@ async function cmdStatus({ rootDir, argv }) {
|
|
|
102
84
|
const runtimeDir = getRuntimeDir();
|
|
103
85
|
const runtimeVersion = await getRuntimeInstalledVersion();
|
|
104
86
|
|
|
105
|
-
const cached = await
|
|
87
|
+
const cached = await readJsonIfExists(updateJson, { defaultValue: null });
|
|
106
88
|
|
|
107
89
|
let latest = cached?.latest ?? null;
|
|
108
90
|
let checkedAt = cached?.checkedAt ?? null;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { getRootDir } from './utils/paths.mjs';
|
|
4
|
-
import { ensureEnvFileUpdated } from './utils/env_file.mjs';
|
|
5
|
-
import { resolveUserConfigEnvPath } from './utils/config.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
6
|
import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
7
7
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
8
|
|
package/scripts/service.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
2
|
-
import { run, runCapture } from './utils/proc.mjs';
|
|
3
|
-
import { getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
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';
|
|
7
8
|
import { spawn } from 'node:child_process';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
10
|
import { existsSync } from 'node:fs';
|
|
@@ -12,6 +13,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
14
15
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { readLastLines } from './utils/fs/tail.mjs';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Manage the autostart service installed by `happys bootstrap -- --autostart`.
|
|
@@ -35,11 +37,6 @@ function getUid() {
|
|
|
35
37
|
return Number.isFinite(n) ? n : null;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
function getInternalUrl() {
|
|
39
|
-
const port = process.env.HAPPY_LOCAL_SERVER_PORT?.trim() ? Number(process.env.HAPPY_LOCAL_SERVER_PORT) : 3005;
|
|
40
|
-
return `http://127.0.0.1:${port}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
40
|
function getAutostartEnv({ rootDir }) {
|
|
44
41
|
// IMPORTANT:
|
|
45
42
|
// LaunchAgents should NOT bake the entire config into the plist, because that would require
|
|
@@ -267,16 +264,19 @@ async function startLaunchAgent({ persistent }) {
|
|
|
267
264
|
|
|
268
265
|
async function postStartDiagnostics() {
|
|
269
266
|
const rootDir = getRootDir(import.meta.url);
|
|
270
|
-
const internalUrl =
|
|
267
|
+
const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
|
|
271
268
|
|
|
272
269
|
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
273
270
|
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
274
271
|
: join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
275
272
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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 });
|
|
280
280
|
|
|
281
281
|
const cliDir = join(rootDir, 'components', 'happy-cli');
|
|
282
282
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
@@ -286,16 +286,6 @@ async function postStartDiagnostics() {
|
|
|
286
286
|
const lockFile = join(cliHomeDir, 'daemon.state.json.lock');
|
|
287
287
|
const logsDir = join(cliHomeDir, 'logs');
|
|
288
288
|
|
|
289
|
-
const readLastLines = async (path, lines = 60) => {
|
|
290
|
-
try {
|
|
291
|
-
const raw = await readFile(path, 'utf-8');
|
|
292
|
-
const parts = raw.split('\n');
|
|
293
|
-
return parts.slice(Math.max(0, parts.length - lines)).join('\n');
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
289
|
const latestDaemonLog = async () => {
|
|
300
290
|
try {
|
|
301
291
|
const ls = await runCapture('bash', ['-lc', `ls -1t "${logsDir}"/*-daemon.log 2>/dev/null | head -1 || true`]);
|
|
@@ -461,7 +451,7 @@ async function waitForLaunchAgentStopped({ timeoutMs = 8000 } = {}) {
|
|
|
461
451
|
|
|
462
452
|
async function showStatus() {
|
|
463
453
|
const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
|
|
464
|
-
const internalUrl =
|
|
454
|
+
const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
|
|
465
455
|
|
|
466
456
|
console.log(`label: ${label}`);
|
|
467
457
|
console.log(`plist: ${plistPath} ${existsSync(plistPath) ? '(present)' : '(missing)'}`);
|
|
@@ -550,7 +540,7 @@ async function main() {
|
|
|
550
540
|
return;
|
|
551
541
|
case 'status':
|
|
552
542
|
if (json) {
|
|
553
|
-
const internalUrl =
|
|
543
|
+
const internalUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
|
|
554
544
|
let health = null;
|
|
555
545
|
try {
|
|
556
546
|
const res = await fetch(`${internalUrl}/health`, { method: 'GET' });
|
package/scripts/setup.mjs
CHANGED
|
@@ -1,137 +1,41 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
6
6
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
7
|
-
import { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
7
|
+
import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
8
8
|
import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
9
|
-
import { getCanonicalHomeDir } from './utils/config.mjs';
|
|
10
|
-
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
11
|
-
import { run, runCapture } from './utils/proc.mjs';
|
|
12
|
-
import {
|
|
9
|
+
import { getCanonicalHomeDir } from './utils/env/config.mjs';
|
|
10
|
+
import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
|
|
11
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
12
|
+
import { waitForHappyHealthOk } from './utils/server/server.mjs';
|
|
13
13
|
import { tailscaleServeEnable, tailscaleServeHttpsUrlForInternalServerUrl } from './tailscale.mjs';
|
|
14
|
-
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
14
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
15
15
|
import { readFile } from 'node:fs/promises';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
|
-
import { parseDotenv } from './utils/dotenv.mjs';
|
|
18
17
|
import { installService } from './service.mjs';
|
|
19
|
-
import { getDevAuthKeyPath } from './utils/
|
|
20
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y') return true;
|
|
28
|
-
if (raw === '0' || raw === 'false' || raw === 'no' || raw === 'n') return false;
|
|
29
|
-
}
|
|
30
|
-
return defaultValue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeProfile(raw) {
|
|
34
|
-
const v = (raw ?? '').trim().toLowerCase();
|
|
35
|
-
if (!v) return '';
|
|
36
|
-
if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
|
|
37
|
-
if (v === 'dev' || v === 'developer' || v === 'develop') return 'dev';
|
|
38
|
-
return '';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeServer(raw) {
|
|
42
|
-
const v = (raw ?? '').trim().toLowerCase();
|
|
43
|
-
if (!v) return '';
|
|
44
|
-
if (v === 'light' || v === 'server-light' || v === 'happy-server-light') return 'happy-server-light';
|
|
45
|
-
if (v === 'server' || v === 'full' || v === 'happy-server') return 'happy-server';
|
|
46
|
-
return '';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function boolFromFlags({ flags, onFlag, offFlag, defaultValue }) {
|
|
50
|
-
if (flags.has(offFlag)) return false;
|
|
51
|
-
if (flags.has(onFlag)) return true;
|
|
52
|
-
return defaultValue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function commandExists(cmd) {
|
|
56
|
-
try {
|
|
57
|
-
const out = (await runCapture('sh', ['-lc', `command -v ${cmd} >/dev/null 2>&1 && echo yes || echo no`])).trim();
|
|
58
|
-
return out === 'yes';
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function openUrl(url) {
|
|
65
|
-
const u = String(url ?? '').trim();
|
|
66
|
-
if (!u) return false;
|
|
67
|
-
if (process.platform === 'darwin') {
|
|
68
|
-
await run('open', [u]).catch(() => {});
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
if (process.platform === 'linux') {
|
|
72
|
-
if (await commandExists('xdg-open')) {
|
|
73
|
-
await run('xdg-open', [u]).catch(() => {});
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function waitForHealthOk(internalServerUrl, { timeoutMs = 60_000 } = {}) {
|
|
82
|
-
const deadline = Date.now() + timeoutMs;
|
|
83
|
-
while (Date.now() < deadline) {
|
|
84
|
-
// eslint-disable-next-line no-await-in-loop
|
|
85
|
-
const health = await fetchHappyHealth(internalServerUrl);
|
|
86
|
-
if (health.ok) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
// eslint-disable-next-line no-await-in-loop
|
|
90
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
91
|
-
}
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseEnvFileText(text) {
|
|
96
|
-
try {
|
|
97
|
-
return parseDotenv(text ?? '');
|
|
98
|
-
} catch {
|
|
99
|
-
return new Map();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function readEnvValueFromFile(envPath, key) {
|
|
104
|
-
try {
|
|
105
|
-
if (!envPath || !existsSync(envPath)) return '';
|
|
106
|
-
const raw = await readFile(envPath, 'utf-8');
|
|
107
|
-
const parsed = parseEnvFileText(raw);
|
|
108
|
-
return (parsed.get(key) ?? '').trim();
|
|
109
|
-
} catch {
|
|
110
|
-
return '';
|
|
111
|
-
}
|
|
112
|
-
}
|
|
18
|
+
import { getDevAuthKeyPath } from './utils/auth/dev_key.mjs';
|
|
19
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
20
|
+
import { boolFromFlags, boolFromFlagsOrKv } from './utils/cli/flags.mjs';
|
|
21
|
+
import { normalizeProfile, normalizeServerComponent } from './utils/cli/normalize.mjs';
|
|
22
|
+
import { openUrlInBrowser } from './utils/ui/browser.mjs';
|
|
23
|
+
import { commandExists } from './utils/proc/commands.mjs';
|
|
24
|
+
import { readEnvValueFromFile } from './utils/env/read.mjs';
|
|
25
|
+
import { readServerPortFromEnvFile, resolveServerPortFromEnv } from './utils/server/port.mjs';
|
|
113
26
|
|
|
114
27
|
async function resolveMainServerPort() {
|
|
115
28
|
// Priority:
|
|
116
29
|
// - explicit env var
|
|
117
30
|
// - main stack env file (preferred)
|
|
118
31
|
// - default
|
|
119
|
-
const
|
|
120
|
-
(process.env.
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
return Number.isFinite(n) && n > 0 ? n : 3005;
|
|
32
|
+
const hasEnvOverride =
|
|
33
|
+
(process.env.HAPPY_STACKS_SERVER_PORT ?? process.env.HAPPY_LOCAL_SERVER_PORT ?? '').toString().trim() !== '';
|
|
34
|
+
if (hasEnvOverride) {
|
|
35
|
+
return resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
|
|
124
36
|
}
|
|
125
37
|
const envPath = resolveStackEnvPath('main').envPath;
|
|
126
|
-
|
|
127
|
-
(await readEnvValueFromFile(envPath, 'HAPPY_LOCAL_SERVER_PORT')) ||
|
|
128
|
-
(await readEnvValueFromFile(envPath, 'HAPPY_STACKS_SERVER_PORT')) ||
|
|
129
|
-
'';
|
|
130
|
-
if (v) {
|
|
131
|
-
const n = Number(v);
|
|
132
|
-
return Number.isFinite(n) && n > 0 ? n : 3005;
|
|
133
|
-
}
|
|
134
|
-
return 3005;
|
|
38
|
+
return await readServerPortFromEnvFile(envPath, { defaultPort: 3005 });
|
|
135
39
|
}
|
|
136
40
|
|
|
137
41
|
async function ensureSetupConfigPersisted({ rootDir, profile, serverComponent, tailscaleWanted, menubarMode }) {
|
|
@@ -393,8 +297,8 @@ async function cmdSetup({ rootDir, argv }) {
|
|
|
393
297
|
const supportsAutostart = platform === 'darwin' || platform === 'linux';
|
|
394
298
|
const supportsMenubar = platform === 'darwin';
|
|
395
299
|
|
|
396
|
-
const serverFromArg =
|
|
397
|
-
let serverComponent = serverFromArg ||
|
|
300
|
+
const serverFromArg = normalizeServerComponent(kv.get('--server'));
|
|
301
|
+
let serverComponent = serverFromArg || normalizeServerComponent(process.env.HAPPY_STACKS_SERVER_COMPONENT) || 'happy-server-light';
|
|
398
302
|
if (profile === 'selfhost' && interactive && !serverFromArg) {
|
|
399
303
|
serverComponent = await withRl(async (rl) => {
|
|
400
304
|
const picked = await promptSelect(rl, {
|
|
@@ -645,12 +549,12 @@ async function cmdSetup({ rootDir, argv }) {
|
|
|
645
549
|
// eslint-disable-next-line no-console
|
|
646
550
|
console.log(res.enableUrl);
|
|
647
551
|
// Best-effort open
|
|
648
|
-
await
|
|
552
|
+
await openUrlInBrowser(res.enableUrl).catch(() => {});
|
|
649
553
|
}
|
|
650
554
|
} catch (e) {
|
|
651
555
|
// eslint-disable-next-line no-console
|
|
652
556
|
console.log('[setup] tailscale not available. Install it from: https://tailscale.com/download');
|
|
653
|
-
await
|
|
557
|
+
await openUrlInBrowser('https://tailscale.com/download').catch(() => {});
|
|
654
558
|
}
|
|
655
559
|
}
|
|
656
560
|
|
|
@@ -664,7 +568,7 @@ async function cmdSetup({ rootDir, argv }) {
|
|
|
664
568
|
await spawnDetachedNodeScript({ rootDir, rel: 'scripts/run.mjs', args: [] });
|
|
665
569
|
}
|
|
666
570
|
|
|
667
|
-
const ready = await
|
|
571
|
+
const ready = await waitForHappyHealthOk(internalServerUrl, { timeoutMs: 90_000 });
|
|
668
572
|
if (!ready) {
|
|
669
573
|
// eslint-disable-next-line no-console
|
|
670
574
|
console.log(`[setup] started, but server did not become healthy yet: ${internalServerUrl}`);
|
|
@@ -742,7 +646,7 @@ async function cmdSetup({ rootDir, argv }) {
|
|
|
742
646
|
console.log('[setup] tip: when you are ready, authenticate with: happys auth login');
|
|
743
647
|
}
|
|
744
648
|
|
|
745
|
-
await
|
|
649
|
+
await openUrlInBrowser(openTarget).catch(() => {});
|
|
746
650
|
// eslint-disable-next-line no-console
|
|
747
651
|
console.log(`[setup] open: ${openTarget}`);
|
|
748
652
|
}
|
package/scripts/setup_pr.mjs
CHANGED
|
@@ -1,43 +1,26 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
-
import { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
5
|
-
import { run } from './utils/proc.mjs';
|
|
6
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
4
|
+
import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
5
|
+
import { run } from './utils/proc/proc.mjs';
|
|
6
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
7
|
+
import { parseGithubPullRequest } from './utils/git/refs.mjs';
|
|
8
|
+
import { sanitizeStackName } from './utils/stack/names.mjs';
|
|
7
9
|
import { existsSync } from 'node:fs';
|
|
8
10
|
import { homedir } from 'node:os';
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
|
|
11
|
-
function sanitizeStackName(raw) {
|
|
12
|
-
const s = String(raw ?? '')
|
|
13
|
-
.trim()
|
|
14
|
-
.toLowerCase()
|
|
15
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
16
|
-
.replace(/-+/g, '-')
|
|
17
|
-
.replace(/^-+/, '')
|
|
18
|
-
.replace(/-+$/, '');
|
|
19
|
-
return s || 'pr';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function parseGithubPullRequestNumber(input) {
|
|
23
|
-
const raw = String(input ?? '').trim();
|
|
24
|
-
if (!raw) return null;
|
|
25
|
-
if (/^\d+$/.test(raw)) return Number(raw);
|
|
26
|
-
const m = raw.match(/github\.com\/[^/]+\/[^/]+\/pull\/(?<num>\d+)/);
|
|
27
|
-
return m?.groups?.num ? Number(m.groups.num) : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
13
|
function inferStackNameFromPrArgs({ happy, happyCli, server, serverLight }) {
|
|
31
14
|
const parts = [];
|
|
32
|
-
const hn =
|
|
33
|
-
const cn =
|
|
34
|
-
const sn =
|
|
35
|
-
const sln =
|
|
15
|
+
const hn = parseGithubPullRequest(happy)?.number ?? null;
|
|
16
|
+
const cn = parseGithubPullRequest(happyCli)?.number ?? null;
|
|
17
|
+
const sn = parseGithubPullRequest(server)?.number ?? null;
|
|
18
|
+
const sln = parseGithubPullRequest(serverLight)?.number ?? null;
|
|
36
19
|
if (hn) parts.push(`happy${hn}`);
|
|
37
20
|
if (cn) parts.push(`cli${cn}`);
|
|
38
21
|
if (sn) parts.push(`server${sn}`);
|
|
39
22
|
if (sln) parts.push(`light${sln}`);
|
|
40
|
-
return sanitizeStackName(parts.length ? `pr-${parts.join('-')}` : 'pr');
|
|
23
|
+
return sanitizeStackName(parts.length ? `pr-${parts.join('-')}` : 'pr', { fallback: 'pr', maxLen: 64 });
|
|
41
24
|
}
|
|
42
25
|
|
|
43
26
|
function detectBestAuthSource() {
|