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/typecheck.mjs
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
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 { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { ensureDepsInstalled
|
|
6
|
-
import { pathExists } from './utils/fs.mjs';
|
|
7
|
-
import { run } from './utils/proc.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { ensureDepsInstalled } from './utils/proc/pm.mjs';
|
|
6
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
7
|
+
import { run } from './utils/proc/proc.mjs';
|
|
8
|
+
import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
|
|
10
9
|
|
|
11
10
|
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
12
11
|
|
|
13
|
-
async function detectPackageManagerCmd(dir) {
|
|
14
|
-
if (await pathExists(join(dir, 'yarn.lock'))) {
|
|
15
|
-
return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
|
|
16
|
-
}
|
|
17
|
-
await requirePnpm();
|
|
18
|
-
return { name: 'pnpm', cmd: 'pnpm', argsForScript: (script) => ['--silent', script] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function readScripts(dir) {
|
|
22
|
-
try {
|
|
23
|
-
const raw = await readFile(join(dir, 'package.json'), 'utf-8');
|
|
24
|
-
const pkg = JSON.parse(raw);
|
|
25
|
-
const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
26
|
-
return scripts;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
12
|
function pickTypecheckScript(scripts) {
|
|
33
|
-
if (!scripts) return null;
|
|
34
13
|
const candidates = [
|
|
35
14
|
'typecheck',
|
|
36
15
|
'type-check',
|
|
@@ -39,7 +18,7 @@ function pickTypecheckScript(scripts) {
|
|
|
39
18
|
'tsc',
|
|
40
19
|
'typescript',
|
|
41
20
|
];
|
|
42
|
-
return
|
|
21
|
+
return pickFirstScript(scripts, candidates);
|
|
43
22
|
}
|
|
44
23
|
|
|
45
24
|
async function main() {
|
|
@@ -86,7 +65,7 @@ async function main() {
|
|
|
86
65
|
continue;
|
|
87
66
|
}
|
|
88
67
|
|
|
89
|
-
const scripts = await
|
|
68
|
+
const scripts = await readPackageJsonScripts(dir);
|
|
90
69
|
if (!scripts) {
|
|
91
70
|
results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
|
|
92
71
|
continue;
|
package/scripts/ui_gateway.mjs
CHANGED
package/scripts/uninstall.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
|
|
3
3
|
import { rm } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
@@ -7,11 +7,11 @@ import { spawnSync } from 'node:child_process';
|
|
|
7
7
|
|
|
8
8
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
9
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
|
-
import { expandHome } from './utils/canonical_home.mjs';
|
|
11
|
-
import { getHappyStacksHomeDir, getRootDir, getStacksStorageRoot } from './utils/paths.mjs';
|
|
12
|
-
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
13
|
-
import { getCanonicalHomeEnvPath } from './utils/config.mjs';
|
|
14
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
10
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
11
|
+
import { getHappyStacksHomeDir, getRootDir, getStacksStorageRoot } from './utils/paths/paths.mjs';
|
|
12
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
13
|
+
import { getCanonicalHomeEnvPath } from './utils/env/config.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
15
15
|
|
|
16
16
|
function resolveWorkspaceDir({ rootDir, homeDir }) {
|
|
17
17
|
// Uninstall should never default to deleting the repo root (getWorkspaceDir() can fall back to cliRootDir).
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
export function resolveHappyStacksHomeDir(env = process.env) {
|
|
9
|
-
const fromEnv = (env.HAPPY_STACKS_HOME_DIR ?? env.HAPPY_LOCAL_HOME_DIR ?? '').toString().trim();
|
|
10
|
-
return fromEnv ? expandHome(fromEnv) : join(homedir(), '.happy-stacks');
|
|
11
|
-
}
|
|
5
|
+
import { getHappyStacksHomeDir } from '../paths/paths.mjs';
|
|
12
6
|
|
|
13
7
|
export function getDevAuthKeyPath(env = process.env) {
|
|
14
|
-
return join(
|
|
8
|
+
return join(getHappyStacksHomeDir(env), 'keys', 'dev-auth.json');
|
|
15
9
|
}
|
|
16
10
|
|
|
17
11
|
function base64UrlToBytes(s) {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { chmod, copyFile, lstat,
|
|
1
|
+
import { chmod, copyFile, lstat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
await mkdir(p, { recursive: true });
|
|
7
|
-
}
|
|
5
|
+
import { ensureDir } from '../fs/ops.mjs';
|
|
8
6
|
|
|
9
7
|
export async function removeFileOrSymlinkIfExists(path) {
|
|
10
8
|
try {
|
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
1
|
import { homedir } from 'node:os';
|
|
4
2
|
import { join } from 'node:path';
|
|
5
3
|
|
|
6
|
-
import {
|
|
7
|
-
import { resolveStackEnvPath } from '
|
|
8
|
-
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!path || !existsSync(path)) return null;
|
|
13
|
-
const raw = await readFile(path, 'utf-8');
|
|
14
|
-
const t = raw.trim();
|
|
15
|
-
return t ? t : null;
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseEnvToObject(raw) {
|
|
22
|
-
const parsed = parseDotenv(raw ?? '');
|
|
23
|
-
return Object.fromEntries(parsed.entries());
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getEnvValue(env, key) {
|
|
27
|
-
const v = (env?.[key] ?? '').toString().trim();
|
|
28
|
-
return v || '';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function stackExistsSync(stackName) {
|
|
32
|
-
if (stackName === 'main') return true;
|
|
33
|
-
const envPath = resolveStackEnvPath(stackName).envPath;
|
|
34
|
-
return existsSync(envPath);
|
|
35
|
-
}
|
|
4
|
+
import { parseEnvToObject } from '../env/dotenv.mjs';
|
|
5
|
+
import { resolveStackEnvPath } from '../paths/paths.mjs';
|
|
6
|
+
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './sources.mjs';
|
|
7
|
+
import { getEnvValue } from '../env/values.mjs';
|
|
8
|
+
import { readTextIfExists } from '../fs/ops.mjs';
|
|
9
|
+
import { stackExistsSync } from '../stack/stacks.mjs';
|
|
36
10
|
|
|
37
11
|
export async function resolveHandyMasterSecretFromStack({
|
|
38
12
|
stackName,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function boolFromFlags({ flags, onFlag, offFlag, defaultValue }) {
|
|
2
|
+
if (flags.has(offFlag)) return false;
|
|
3
|
+
if (flags.has(onFlag)) return true;
|
|
4
|
+
return defaultValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function boolFromFlagsOrKv({ flags, kv, onFlag, offFlag, key, defaultValue }) {
|
|
8
|
+
if (flags.has(offFlag)) return false;
|
|
9
|
+
if (flags.has(onFlag)) return true;
|
|
10
|
+
if (key && kv.has(key)) {
|
|
11
|
+
const raw = String(kv.get(key) ?? '').trim().toLowerCase();
|
|
12
|
+
if (raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y') return true;
|
|
13
|
+
if (raw === '0' || raw === 'false' || raw === 'no' || raw === 'n') return false;
|
|
14
|
+
}
|
|
15
|
+
return defaultValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function normalizeProfile(raw) {
|
|
2
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
3
|
+
if (!v) return '';
|
|
4
|
+
if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
|
|
5
|
+
if (v === 'dev' || v === 'developer' || v === 'develop' || v === 'development') return 'dev';
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeServerComponent(raw) {
|
|
10
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
11
|
+
if (!v) return '';
|
|
12
|
+
if (v === 'light' || v === 'server-light' || v === 'happy-server-light') return 'happy-server-light';
|
|
13
|
+
if (v === 'server' || v === 'full' || v === 'happy-server') return 'happy-server';
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -6,8 +6,8 @@ import { dirname } from 'node:path';
|
|
|
6
6
|
import { getHappysRegistry } from './cli_registry.mjs';
|
|
7
7
|
|
|
8
8
|
function cliRootDir() {
|
|
9
|
-
// scripts/utils/* -> scripts -> repo root
|
|
10
|
-
return dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
9
|
+
// scripts/utils/cli/* -> scripts/utils -> scripts -> repo root
|
|
10
|
+
return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function runOrThrow(label, args) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function base64Url(buf) {
|
|
4
|
+
return Buffer.from(buf)
|
|
5
|
+
.toString('base64')
|
|
6
|
+
.replaceAll('+', '-')
|
|
7
|
+
.replaceAll('/', '_')
|
|
8
|
+
.replaceAll('=', '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function randomToken(lenBytes = 24) {
|
|
12
|
+
return base64Url(randomBytes(lenBytes));
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
|
|
3
|
-
import { ensureCliBuilt, ensureDepsInstalled } from '
|
|
4
|
-
import { watchDebounced } from '
|
|
5
|
-
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from '
|
|
6
|
-
import { startLocalDaemonWithAuth } from '
|
|
3
|
+
import { ensureCliBuilt, ensureDepsInstalled } from '../proc/pm.mjs';
|
|
4
|
+
import { watchDebounced } from '../proc/watch.mjs';
|
|
5
|
+
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from '../stack/startup.mjs';
|
|
6
|
+
import { startLocalDaemonWithAuth } from '../../daemon.mjs';
|
|
7
7
|
|
|
8
8
|
export async function ensureDevCliReady({ cliDir, buildCli }) {
|
|
9
9
|
await ensureDepsInstalled(cliDir, 'happy-cli');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ensureDepsInstalled, pmSpawnBin } from '
|
|
2
|
-
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, wantsExpoClearCache, writePidState } from '
|
|
3
|
-
import { pickDevMetroPort, resolveStackUiDevPortStart } from './
|
|
4
|
-
import { recordStackRuntimeUpdate } from '
|
|
5
|
-
import { killProcessGroupOwnedByStack } from '
|
|
1
|
+
import { ensureDepsInstalled, pmSpawnBin } from '../proc/pm.mjs';
|
|
2
|
+
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, wantsExpoClearCache, writePidState } from '../expo/expo.mjs';
|
|
3
|
+
import { pickDevMetroPort, resolveStackUiDevPortStart } from './server.mjs';
|
|
4
|
+
import { recordStackRuntimeUpdate } from '../stack/runtime_state.mjs';
|
|
5
|
+
import { killProcessGroupOwnedByStack } from '../proc/ownership.mjs';
|
|
6
6
|
|
|
7
7
|
export async function startDevExpoWebUi({
|
|
8
8
|
startUi,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
2
|
|
|
3
|
-
import { ensureDepsInstalled, pmSpawnScript } from '
|
|
4
|
-
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from '
|
|
5
|
-
import { waitForServerReady } from '
|
|
6
|
-
import { isTcpPortFree, pickNextFreeTcpPort } from '
|
|
7
|
-
import { readStackRuntimeStateFile, recordStackRuntimeUpdate } from '
|
|
8
|
-
import { killProcessGroupOwnedByStack } from '
|
|
9
|
-
import { watchDebounced } from '
|
|
3
|
+
import { ensureDepsInstalled, pmSpawnScript } from '../proc/pm.mjs';
|
|
4
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from '../server/infra/happy_server_infra.mjs';
|
|
5
|
+
import { waitForServerReady } from '../server/server.mjs';
|
|
6
|
+
import { isTcpPortFree, pickNextFreeTcpPort } from '../net/ports.mjs';
|
|
7
|
+
import { readStackRuntimeStateFile, recordStackRuntimeUpdate } from '../stack/runtime_state.mjs';
|
|
8
|
+
import { killProcessGroupOwnedByStack } from '../proc/ownership.mjs';
|
|
9
|
+
import { watchDebounced } from '../proc/watch.mjs';
|
|
10
10
|
|
|
11
11
|
function hashStringToInt(s) {
|
|
12
12
|
let h = 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ensureEnvFileUpdated } from './env_file.mjs';
|
|
3
|
-
import { getHappyStacksHomeDir, resolveStackEnvPath } from '
|
|
4
|
-
import { getCanonicalHomeDirFromEnv } from '
|
|
3
|
+
import { getHappyStacksHomeDir, resolveStackEnvPath } from '../paths/paths.mjs';
|
|
4
|
+
import { getCanonicalHomeDirFromEnv } from '../paths/canonical_home.mjs';
|
|
5
5
|
|
|
6
6
|
export function getHomeEnvPath() {
|
|
7
7
|
return join(getHappyStacksHomeDir(), '.env');
|
|
@@ -50,3 +50,4 @@ export async function ensureUserConfigEnvUpdated({ cliRootDir, updates }) {
|
|
|
50
50
|
await ensureEnvFileUpdated({ envPath, updates });
|
|
51
51
|
return envPath;
|
|
52
52
|
}
|
|
53
|
+
|
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseDotenv } from './dotenv.mjs';
|
|
7
|
-
import { expandHome, getCanonicalHomeEnvPathFromEnv } from '
|
|
7
|
+
import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../paths/canonical_home.mjs';
|
|
8
8
|
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './sandbox.mjs';
|
|
9
9
|
|
|
10
10
|
async function loadEnvFile(path, { override = false, overridePrefix = null } = {}) {
|
|
@@ -57,8 +57,9 @@ async function loadEnvFileIgnoringPrefixes(path, { ignorePrefixes = [] } = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Load happy-stacks env (optional). This is intentionally lightweight and does not require extra deps.
|
|
60
|
-
// This file lives under scripts/utils
|
|
61
|
-
const
|
|
60
|
+
// This file lives under scripts/utils/env, so repo root is three directories up.
|
|
61
|
+
const __envDir = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const __utilsDir = dirname(__envDir);
|
|
62
63
|
const __scriptsDir = dirname(__utilsDir);
|
|
63
64
|
const __cliRootDir = dirname(__scriptsDir);
|
|
64
65
|
|
|
@@ -207,3 +208,4 @@ process.env.NPM_CONFIG_PACKAGE_MANAGER_STRICT = process.env.NPM_CONFIG_PACKAGE_M
|
|
|
207
208
|
const next = [...want.filter((p) => p && !current.includes(p)), ...current];
|
|
208
209
|
process.env.PATH = next.join(delimiter);
|
|
209
210
|
})();
|
|
211
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { pathExists } from '
|
|
3
|
+
import { pathExists } from '../fs/fs.mjs';
|
|
4
4
|
|
|
5
5
|
export async function ensureEnvFileUpdated({ envPath, updates }) {
|
|
6
6
|
if (!updates.length) {
|
|
@@ -93,3 +93,4 @@ async function writeFileIfChanged(existingContent, nextContent, path) {
|
|
|
93
93
|
}
|
|
94
94
|
await writeFile(path, normalizedNext, 'utf-8');
|
|
95
95
|
}
|
|
96
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { parseDotenv } from './dotenv.mjs';
|
|
5
|
+
|
|
6
|
+
export async function readEnvValueFromFile(envPath, key, { defaultValue = '' } = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const p = String(envPath ?? '').trim();
|
|
9
|
+
const k = String(key ?? '').trim();
|
|
10
|
+
if (!p || !k) return defaultValue;
|
|
11
|
+
if (!existsSync(p)) return defaultValue;
|
|
12
|
+
const raw = await readFile(p, 'utf-8');
|
|
13
|
+
const parsed = parseDotenv(raw ?? '');
|
|
14
|
+
return String(parsed.get(k) ?? '').trim();
|
|
15
|
+
} catch {
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readEnvObjectFromFile(envPath) {
|
|
21
|
+
try {
|
|
22
|
+
const p = String(envPath ?? '').trim();
|
|
23
|
+
if (!p || !existsSync(p)) return {};
|
|
24
|
+
const raw = await readFile(p, 'utf-8');
|
|
25
|
+
return Object.fromEntries(parseDotenv(raw ?? '').entries());
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getEnvValue(obj, key) {
|
|
2
|
+
const v = (obj?.[key] ?? '').toString().trim();
|
|
3
|
+
return v || '';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function getEnvValueAny(obj, keys) {
|
|
7
|
+
for (const k of keys) {
|
|
8
|
+
const v = getEnvValue(obj, k);
|
|
9
|
+
if (v) return v;
|
|
10
|
+
}
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
|
|
@@ -3,6 +3,9 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { isPidAlive } from '../proc/pids.mjs';
|
|
7
|
+
|
|
8
|
+
export { isPidAlive };
|
|
6
9
|
|
|
7
10
|
function hashDir(dir) {
|
|
8
11
|
return createHash('sha1').update(String(dir ?? '')).digest('hex').slice(0, 12);
|
|
@@ -56,15 +59,6 @@ export async function readPidState(statePath) {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
export function isPidAlive(pid) {
|
|
60
|
-
try {
|
|
61
|
-
process.kill(pid, 0);
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
export async function isStateProcessRunning(statePath) {
|
|
69
63
|
const state = await readPidState(statePath);
|
|
70
64
|
if (!state) return { running: false, state: null };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
export async function readJsonIfExists(path, { defaultValue = null } = {}) {
|
|
6
|
+
try {
|
|
7
|
+
const p = String(path ?? '').trim();
|
|
8
|
+
if (!p || !existsSync(p)) return defaultValue;
|
|
9
|
+
const raw = await readFile(p, 'utf-8');
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return defaultValue;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function writeJsonAtomic(path, value) {
|
|
17
|
+
const p = String(path ?? '').trim();
|
|
18
|
+
if (!p) throw new Error('writeJsonAtomic: path is required');
|
|
19
|
+
const dir = dirname(p);
|
|
20
|
+
await mkdir(dir, { recursive: true }).catch(() => {});
|
|
21
|
+
const tmp = join(dir, `.tmp.${Date.now()}.${Math.random().toString(16).slice(2)}.json`);
|
|
22
|
+
await writeFile(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
23
|
+
await rename(tmp, p);
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export async function ensureDir(path) {
|
|
5
|
+
await mkdir(path, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function readTextIfExists(path) {
|
|
9
|
+
try {
|
|
10
|
+
const p = String(path ?? '').trim();
|
|
11
|
+
if (!p || !existsSync(p)) return null;
|
|
12
|
+
const raw = await readFile(p, 'utf-8');
|
|
13
|
+
const t = raw.trim();
|
|
14
|
+
return t ? t : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readTextOrEmpty(path) {
|
|
21
|
+
try {
|
|
22
|
+
const p = String(path ?? '').trim();
|
|
23
|
+
if (!p || !existsSync(p)) return '';
|
|
24
|
+
return await readFile(p, 'utf-8');
|
|
25
|
+
} catch {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function readLastLines(path, lines = 60) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await readFile(path, 'utf-8');
|
|
6
|
+
const parts = raw.split('\n');
|
|
7
|
+
return parts.slice(Math.max(0, parts.length - lines)).join('\n');
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function parseGithubPullRequest(input) {
|
|
2
|
+
const raw = (input ?? '').trim();
|
|
3
|
+
if (!raw) return null;
|
|
4
|
+
if (/^\d+$/.test(raw)) {
|
|
5
|
+
return { number: Number(raw), owner: null, repo: null };
|
|
6
|
+
}
|
|
7
|
+
// https://github.com/<owner>/<repo>/pull/<num>
|
|
8
|
+
const m = raw.match(/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/pull\/(?<num>\d+)/);
|
|
9
|
+
if (!m?.groups?.num) return null;
|
|
10
|
+
return {
|
|
11
|
+
number: Number(m.groups.num),
|
|
12
|
+
owner: m.groups.owner ?? null,
|
|
13
|
+
repo: m.groups.repo ?? null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sanitizeSlugPart(s) {
|
|
18
|
+
return (s ?? '')
|
|
19
|
+
.toString()
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9._/-]+/g, '-')
|
|
23
|
+
.replace(/-+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import { getComponentsDir } from '
|
|
4
|
-
import { pathExists } from '
|
|
5
|
-
import { run, runCapture } from '
|
|
3
|
+
import { getComponentsDir } from '../paths/paths.mjs';
|
|
4
|
+
import { pathExists } from '../fs/fs.mjs';
|
|
5
|
+
import { run, runCapture } from '../proc/proc.mjs';
|
|
6
6
|
|
|
7
7
|
export function parseGithubOwner(remoteUrl) {
|
|
8
8
|
const raw = (remoteUrl ?? '').trim();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
-
import {
|
|
3
|
+
import { runCaptureIfCommandExists } from '../proc/commands.mjs';
|
|
4
4
|
|
|
5
5
|
async function listListenPids(port) {
|
|
6
6
|
if (!Number.isFinite(port) || port <= 0) return [];
|
|
@@ -9,10 +9,7 @@ async function listListenPids(port) {
|
|
|
9
9
|
let raw = '';
|
|
10
10
|
try {
|
|
11
11
|
// `lsof` exits non-zero if no matches; normalize to empty output.
|
|
12
|
-
raw = await
|
|
13
|
-
'-lc',
|
|
14
|
-
`command -v lsof >/dev/null 2>&1 && lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true`,
|
|
15
|
-
]);
|
|
12
|
+
raw = await runCaptureIfCommandExists('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t']);
|
|
16
13
|
} catch {
|
|
17
14
|
raw = '';
|
|
18
15
|
}
|
|
@@ -102,3 +99,4 @@ export async function pickNextFreeTcpPort(startPort, { reservedPorts = new Set()
|
|
|
102
99
|
}
|
|
103
100
|
throw new Error(`[local] unable to find a free TCP port starting at ${startPort}`);
|
|
104
101
|
}
|
|
102
|
+
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { getStackName } from './paths.mjs';
|
|
2
|
-
|
|
3
|
-
function sanitizeDnsLabel(raw, { fallback = 'stack' } = {}) {
|
|
4
|
-
const s = String(raw ?? '')
|
|
5
|
-
.toLowerCase()
|
|
6
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
7
|
-
.replace(/-+/g, '-')
|
|
8
|
-
.replace(/^-+/, '')
|
|
9
|
-
.replace(/-+$/, '');
|
|
10
|
-
return s || fallback;
|
|
11
|
-
}
|
|
2
|
+
import { sanitizeDnsLabel } from '../net/dns.mjs';
|
|
12
3
|
|
|
13
4
|
export function resolveLocalhostHost({ stackMode, stackName = getStackName() } = {}) {
|
|
14
5
|
if (!stackMode) return 'localhost';
|
|
15
6
|
if (!stackName || stackName === 'main') return 'localhost';
|
|
16
7
|
return `happy-${sanitizeDnsLabel(stackName)}.localhost`;
|
|
17
8
|
}
|
|
9
|
+
|