happy-stacks 0.1.2 → 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 +121 -83
- package/bin/happys.mjs +70 -10
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/stacks.md +39 -0
- package/extras/swiftbar/auth-login.sh +5 -2
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +131 -81
- package/extras/swiftbar/happys-term.sh +15 -38
- package/extras/swiftbar/happys.sh +15 -32
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +209 -80
- package/extras/swiftbar/lib/system.sh +27 -4
- package/extras/swiftbar/lib/utils.sh +311 -28
- package/extras/swiftbar/pnpm.sh +2 -1
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +11 -2
- package/extras/swiftbar/wt-pr.sh +9 -2
- package/package.json +2 -1
- package/scripts/auth.mjs +560 -112
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +130 -20
- package/scripts/dev.mjs +201 -133
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +43 -20
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +25 -15
- package/scripts/mobile.mjs +13 -7
- package/scripts/run.mjs +114 -27
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +15 -2
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +1792 -254
- package/scripts/stop.mjs +6 -3
- package/scripts/tailscale.mjs +17 -2
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +2 -2
- package/scripts/ui_gateway.mjs +2 -2
- package/scripts/uninstall.mjs +18 -10
- 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} +48 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +6 -2
- 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 +60 -11
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +4 -2
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +100 -46
- 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 +121 -20
- package/scripts/utils/proc.mjs +29 -2
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +24 -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 +79 -30
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +82 -8
- /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/happy.mjs
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
4
|
import { join } from 'node:path';
|
|
6
|
-
import { parseArgs } from './utils/args.mjs';
|
|
7
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
5
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
6
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
7
|
+
import { expandHome } from './utils/canonical_home.mjs';
|
|
8
8
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
9
9
|
|
|
10
|
-
function expandHome(p) {
|
|
11
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
function resolveCliHomeDir() {
|
|
15
11
|
const fromExplicit = (process.env.HAPPY_HOME_DIR ?? '').trim();
|
|
16
12
|
if (fromExplicit) {
|
package/scripts/init.mjs
CHANGED
|
@@ -6,10 +6,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { ensureCanonicalHomeEnvUpdated, ensureHomeEnvUpdated } from './utils/config.mjs';
|
|
8
8
|
import { parseDotenv } from './utils/dotenv.mjs';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
12
|
-
}
|
|
9
|
+
import { expandHome } from './utils/canonical_home.mjs';
|
|
10
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
13
11
|
|
|
14
12
|
async function readJsonIfExists(path) {
|
|
15
13
|
try {
|
|
@@ -139,15 +137,15 @@ async function main() {
|
|
|
139
137
|
if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
|
|
140
138
|
console.log([
|
|
141
139
|
'[init] usage:',
|
|
142
|
-
' happys init [--home-dir=/path] [--workspace-dir=/path] [--runtime-dir=/path] [--storage-dir=/path] [--cli-root-dir=/path] [--tailscale-bin=/path] [--tailscale-cmd-timeout-ms=MS] [--tailscale-enable-timeout-ms=MS] [--tailscale-enable-timeout-ms-auto=MS] [--tailscale-reset-timeout-ms=MS] [--install-path] [--no-runtime] [--force-runtime] [--no-bootstrap] [--] [bootstrap args...]',
|
|
140
|
+
' happys init [--canonical-home-dir=/path] [--home-dir=/path] [--workspace-dir=/path] [--runtime-dir=/path] [--storage-dir=/path] [--cli-root-dir=/path] [--tailscale-bin=/path] [--tailscale-cmd-timeout-ms=MS] [--tailscale-enable-timeout-ms=MS] [--tailscale-enable-timeout-ms-auto=MS] [--tailscale-reset-timeout-ms=MS] [--install-path] [--no-runtime] [--force-runtime] [--no-bootstrap] [--] [bootstrap args...]',
|
|
143
141
|
'',
|
|
144
142
|
'notes:',
|
|
145
|
-
' - writes
|
|
146
|
-
' - default workspace:
|
|
147
|
-
' - default runtime:
|
|
143
|
+
' - writes <canonicalHomeDir>/.env (stable pointer file; default: ~/.happy-stacks/.env)',
|
|
144
|
+
' - default workspace: <homeDir>/workspace',
|
|
145
|
+
' - default runtime: <homeDir>/runtime (recommended for services/SwiftBar)',
|
|
148
146
|
' - runtime install is skipped if the same version is already installed (use --force-runtime to reinstall)',
|
|
149
147
|
' - set HAPPY_STACKS_INIT_NO_RUNTIME=1 to persist skipping runtime installs on this machine',
|
|
150
|
-
' - optional: --install-path adds
|
|
148
|
+
' - optional: --install-path adds <homeDir>/bin to your shell PATH (idempotent)',
|
|
151
149
|
' - by default, runs `happys bootstrap --interactive` at the end (TTY only) IF components are not already present',
|
|
152
150
|
].join('\n'));
|
|
153
151
|
return;
|
|
@@ -159,7 +157,17 @@ async function main() {
|
|
|
159
157
|
//
|
|
160
158
|
// Other scripts load this pointer via `scripts/utils/env.mjs`, but `init.mjs` is often run before
|
|
161
159
|
// anything else (or directly from a repo checkout). So we load it here too.
|
|
162
|
-
const
|
|
160
|
+
const canonicalHomeDirRaw = parseArgValue(argv, 'canonical-home-dir');
|
|
161
|
+
const canonicalHomeDir = expandHome(firstNonEmpty(
|
|
162
|
+
canonicalHomeDirRaw,
|
|
163
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR,
|
|
164
|
+
process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR,
|
|
165
|
+
join(homedir(), '.happy-stacks'),
|
|
166
|
+
));
|
|
167
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = canonicalHomeDir;
|
|
168
|
+
process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR = process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR ?? canonicalHomeDir;
|
|
169
|
+
|
|
170
|
+
const canonicalEnvPath = join(canonicalHomeDir, '.env');
|
|
163
171
|
if (existsSync(canonicalEnvPath)) {
|
|
164
172
|
await loadEnvFile(canonicalEnvPath, { override: false });
|
|
165
173
|
await loadEnvFile(canonicalEnvPath, { override: true, overridePrefix: 'HAPPY_STACKS_' });
|
|
@@ -245,6 +253,7 @@ async function main() {
|
|
|
245
253
|
const nodePath = process.execPath;
|
|
246
254
|
|
|
247
255
|
await mkdir(homeDir, { recursive: true });
|
|
256
|
+
await mkdir(canonicalHomeDir, { recursive: true });
|
|
248
257
|
await mkdir(workspaceDir, { recursive: true });
|
|
249
258
|
await mkdir(join(workspaceDir, 'components'), { recursive: true });
|
|
250
259
|
await mkdir(runtimeDir, { recursive: true });
|
|
@@ -306,10 +315,10 @@ async function main() {
|
|
|
306
315
|
const shim = [
|
|
307
316
|
'#!/bin/bash',
|
|
308
317
|
'set -euo pipefail',
|
|
309
|
-
|
|
318
|
+
`CANONICAL_ENV="${canonicalEnvPath}"`,
|
|
310
319
|
'',
|
|
311
320
|
'# Best-effort: if env vars are not exported (common under launchd/SwiftBar),',
|
|
312
|
-
'# read the stable pointer file at
|
|
321
|
+
'# read the stable pointer file at CANONICAL_ENV to discover the real dirs.',
|
|
313
322
|
'if [[ -f "$CANONICAL_ENV" ]]; then',
|
|
314
323
|
' if [[ -z "${HAPPY_STACKS_HOME_DIR:-}" ]]; then',
|
|
315
324
|
' HAPPY_STACKS_HOME_DIR="$(grep -E \'^HAPPY_STACKS_HOME_DIR=\' "$CANONICAL_ENV" | head -n 1 | sed \'s/^HAPPY_STACKS_HOME_DIR=//\')" || true',
|
|
@@ -333,7 +342,7 @@ async function main() {
|
|
|
333
342
|
' fi',
|
|
334
343
|
'fi',
|
|
335
344
|
'',
|
|
336
|
-
|
|
345
|
+
`HOME_DIR="\${HAPPY_STACKS_HOME_DIR:-${canonicalHomeDir}}"`,
|
|
337
346
|
'ENV_FILE="$HOME_DIR/.env"',
|
|
338
347
|
'WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$HOME_DIR/workspace}"',
|
|
339
348
|
'if [[ -d "$WORKDIR" ]]; then',
|
|
@@ -370,22 +379,31 @@ async function main() {
|
|
|
370
379
|
await writeExecutable(happysShimPath, shim);
|
|
371
380
|
await writeExecutable(happyShimPath, `#!/bin/bash\nset -euo pipefail\nexec \"${happysShimPath}\" happy \"$@\"\n`);
|
|
372
381
|
|
|
382
|
+
let didInstallPath = false;
|
|
373
383
|
if (argv.includes('--install-path')) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
console.log(
|
|
384
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
385
|
+
console.log('[init] sandbox mode: skipping --install-path (would modify your shell config)');
|
|
386
|
+
console.log('[init] tip: set HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1 if you really want to test PATH modifications');
|
|
377
387
|
} else {
|
|
378
|
-
|
|
388
|
+
const res = await ensurePathInstalled({ homeDir });
|
|
389
|
+
didInstallPath = true;
|
|
390
|
+
if (res.updated) {
|
|
391
|
+
console.log(`[init] added ${homeDir}/bin to PATH via ${res.path}`);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(`[init] PATH already configured in ${res.path}`);
|
|
394
|
+
}
|
|
379
395
|
}
|
|
380
396
|
}
|
|
381
397
|
|
|
398
|
+
const invokedBySetup = (process.env.HAPPY_STACKS_SETUP_CHILD ?? '').trim() === '1';
|
|
399
|
+
|
|
382
400
|
console.log('[init] complete');
|
|
383
401
|
console.log(`[init] home: ${homeDir}`);
|
|
384
402
|
console.log(`[init] workspace: ${workspaceDir}`);
|
|
385
403
|
console.log(`[init] shims: ${homeDir}/bin`);
|
|
386
404
|
console.log('');
|
|
387
405
|
|
|
388
|
-
if (!argv.includes('--install-path')) {
|
|
406
|
+
if (!argv.includes('--install-path') || !didInstallPath) {
|
|
389
407
|
console.log('[init] note: to use `happys` / `happy` from any terminal, add shims to PATH:');
|
|
390
408
|
console.log(` export PATH="${homeDir}/bin:$PATH"`);
|
|
391
409
|
console.log(' (or re-run: happys init --install-path)');
|
|
@@ -422,13 +440,18 @@ async function main() {
|
|
|
422
440
|
|
|
423
441
|
if (wantBootstrap && alreadyBootstrapped && !bootstrapExplicit) {
|
|
424
442
|
console.log('[init] bootstrap: already set up; skipping');
|
|
425
|
-
console.log('[init] tip:
|
|
443
|
+
console.log('[init] tip: for guided onboarding: happys setup');
|
|
426
444
|
console.log('');
|
|
427
445
|
}
|
|
428
446
|
|
|
447
|
+
// When `happys setup` drives init, avoid printing confusing “next steps”.
|
|
448
|
+
if (invokedBySetup) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
429
452
|
console.log('[init] next steps:');
|
|
430
453
|
console.log(` export PATH=\"${homeDir}/bin:$PATH\"`);
|
|
431
|
-
console.log(' happys
|
|
454
|
+
console.log(' happys setup');
|
|
432
455
|
}
|
|
433
456
|
|
|
434
457
|
main().catch((err) => {
|
package/scripts/install.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { run } from './utils/proc.mjs';
|
|
5
5
|
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
@@ -8,15 +8,16 @@ import { ensureCliBuilt, ensureDepsInstalled, ensureHappyCliLocalNpmLinked } fro
|
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { mkdir } from 'node:fs/promises';
|
|
10
10
|
import { installService, uninstallService } from './service.mjs';
|
|
11
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
11
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
12
12
|
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
13
|
-
import { isTty, prompt, promptSelect, withRl } from './utils/wizard.mjs';
|
|
13
|
+
import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Install/setup the local stack:
|
|
17
18
|
* - ensure components exist (optionally clone if missing)
|
|
18
19
|
* - install dependencies where needed
|
|
19
|
-
* - build happy-cli (optional) and install `happy`/`happys` shims under
|
|
20
|
+
* - build happy-cli (optional) and install `happy`/`happys` shims under `<homeDir>/bin`
|
|
20
21
|
* - build the web UI bundle (so `run` can serve it)
|
|
21
22
|
* - optional macOS autostart (LaunchAgent)
|
|
22
23
|
*/
|
|
@@ -152,7 +153,9 @@ async function interactiveWizard({ rootDir, defaults }) {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
const enableAutostart = await promptSelect(rl, {
|
|
155
|
-
title:
|
|
156
|
+
title: isSandboxed()
|
|
157
|
+
? 'Enable macOS autostart (LaunchAgent)? (NOTE: sandbox mode; this is global OS state)'
|
|
158
|
+
: 'Enable macOS autostart (LaunchAgent)?',
|
|
156
159
|
options: [
|
|
157
160
|
{ label: 'no (default)', value: false },
|
|
158
161
|
{ label: 'yes', value: true },
|
|
@@ -211,6 +214,8 @@ async function main() {
|
|
|
211
214
|
const rootDir = getRootDir(import.meta.url);
|
|
212
215
|
|
|
213
216
|
const interactive = flags.has('--interactive') && isTty();
|
|
217
|
+
const allowGlobal = sandboxAllowsGlobalSideEffects();
|
|
218
|
+
const sandboxed = isSandboxed();
|
|
214
219
|
|
|
215
220
|
// Defaults for wizard.
|
|
216
221
|
const defaultRepoSource = resolveRepoSource({ flags });
|
|
@@ -220,7 +225,7 @@ async function main() {
|
|
|
220
225
|
upstreamOwner: 'slopus',
|
|
221
226
|
serverComponentName: getServerComponentName({ kv }),
|
|
222
227
|
allowClone: !flags.has('--no-clone') && ((process.env.HAPPY_LOCAL_CLONE_MISSING ?? '1') !== '0' || flags.has('--clone')),
|
|
223
|
-
enableAutostart: flags.has('--autostart') || (process.env.HAPPY_LOCAL_AUTOSTART ?? '0') === '1',
|
|
228
|
+
enableAutostart: (!sandboxed || allowGlobal) && (flags.has('--autostart') || (process.env.HAPPY_LOCAL_AUTOSTART ?? '0') === '1'),
|
|
224
229
|
buildTauri: flags.has('--tauri') && !flags.has('--no-tauri'),
|
|
225
230
|
};
|
|
226
231
|
|
|
@@ -260,7 +265,8 @@ async function main() {
|
|
|
260
265
|
const cloneMissingDefault = (process.env.HAPPY_LOCAL_CLONE_MISSING ?? '1') !== '0';
|
|
261
266
|
const allowClone =
|
|
262
267
|
wizard?.allowClone ?? (!flags.has('--no-clone') && (flags.has('--clone') || cloneMissingDefault));
|
|
263
|
-
const
|
|
268
|
+
const enableAutostartRaw = wizard?.enableAutostart ?? (flags.has('--autostart') || (process.env.HAPPY_LOCAL_AUTOSTART ?? '0') === '1');
|
|
269
|
+
const enableAutostart = sandboxed && !allowGlobal ? false : enableAutostartRaw;
|
|
264
270
|
const disableAutostart = flags.has('--no-autostart');
|
|
265
271
|
|
|
266
272
|
const serverComponentName = (wizard?.serverComponentName ?? getServerComponentName({ kv })).trim();
|
|
@@ -366,7 +372,7 @@ async function main() {
|
|
|
366
372
|
serverComponentName,
|
|
367
373
|
dirs: { serverLightDir, serverFullDir, cliDir: cliDirFinal, uiDir: uiDirFinal },
|
|
368
374
|
cloned: allowClone,
|
|
369
|
-
autostart: enableAutostart ? 'enabled' : disableAutostart ? 'disabled' : 'unchanged',
|
|
375
|
+
autostart: enableAutostart ? 'enabled' : sandboxed && enableAutostartRaw && !allowGlobal ? 'skipped (sandbox)' : disableAutostart ? 'disabled' : 'unchanged',
|
|
370
376
|
interactive: Boolean(wizard),
|
|
371
377
|
},
|
|
372
378
|
text: '[local] setup complete',
|
package/scripts/lint.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import './utils/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
+
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
5
|
+
import { ensureDepsInstalled, requirePnpm } from './utils/pm.mjs';
|
|
6
|
+
import { pathExists } from './utils/fs.mjs';
|
|
7
|
+
import { run } from './utils/proc.mjs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
12
|
+
|
|
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
|
+
function pickLintScript(scripts) {
|
|
33
|
+
if (!scripts) return null;
|
|
34
|
+
const candidates = [
|
|
35
|
+
'lint',
|
|
36
|
+
'lint:ci',
|
|
37
|
+
'check',
|
|
38
|
+
'check:lint',
|
|
39
|
+
'eslint',
|
|
40
|
+
'eslint:check',
|
|
41
|
+
];
|
|
42
|
+
return candidates.find((k) => typeof scripts[k] === 'string' && scripts[k].trim()) ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
const argv = process.argv.slice(2);
|
|
47
|
+
const { flags } = parseArgs(argv);
|
|
48
|
+
const json = wantsJson(argv, { flags });
|
|
49
|
+
|
|
50
|
+
if (wantsHelp(argv, { flags })) {
|
|
51
|
+
printResult({
|
|
52
|
+
json,
|
|
53
|
+
data: { components: DEFAULT_COMPONENTS, flags: ['--json'] },
|
|
54
|
+
text: [
|
|
55
|
+
'[lint] usage:',
|
|
56
|
+
' happys lint [component...] [--json]',
|
|
57
|
+
'',
|
|
58
|
+
'components:',
|
|
59
|
+
` ${DEFAULT_COMPONENTS.join(' | ')}`,
|
|
60
|
+
'',
|
|
61
|
+
'examples:',
|
|
62
|
+
' happys lint',
|
|
63
|
+
' happys lint happy happy-cli',
|
|
64
|
+
].join('\n'),
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
70
|
+
const requested = positionals.length ? positionals : ['all'];
|
|
71
|
+
const wantAll = requested.includes('all');
|
|
72
|
+
const components = wantAll ? DEFAULT_COMPONENTS : requested;
|
|
73
|
+
|
|
74
|
+
const rootDir = getRootDir(import.meta.url);
|
|
75
|
+
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const component of components) {
|
|
78
|
+
if (!DEFAULT_COMPONENTS.includes(component)) {
|
|
79
|
+
results.push({ component, ok: false, skipped: false, error: `unknown component (expected one of: ${DEFAULT_COMPONENTS.join(', ')})` });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dir = getComponentDir(rootDir, component);
|
|
84
|
+
if (!(await pathExists(dir))) {
|
|
85
|
+
results.push({ component, ok: false, skipped: false, dir, error: `missing component dir: ${dir}` });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const scripts = await readScripts(dir);
|
|
90
|
+
if (!scripts) {
|
|
91
|
+
results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const script = pickLintScript(scripts);
|
|
96
|
+
if (!script) {
|
|
97
|
+
results.push({ component, ok: true, skipped: true, dir, reason: 'no lint script found in package.json' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await ensureDepsInstalled(dir, component);
|
|
102
|
+
const pm = await detectPackageManagerCmd(dir);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.log(`[lint] ${component}: running ${pm.name} ${script}`);
|
|
107
|
+
await run(pm.cmd, pm.argsForScript(script), { cwd: dir, env: process.env });
|
|
108
|
+
results.push({ component, ok: true, skipped: false, dir, pm: pm.name, script });
|
|
109
|
+
} catch (e) {
|
|
110
|
+
results.push({ component, ok: false, skipped: false, dir, pm: pm.name, script, error: String(e?.message ?? e) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ok = results.every((r) => r.ok);
|
|
115
|
+
if (json) {
|
|
116
|
+
printResult({ json, data: { ok, results } });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines = ['[lint] results:'];
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
if (r.ok && r.skipped) {
|
|
123
|
+
lines.push(`- ↪ ${r.component}: skipped (${r.reason})`);
|
|
124
|
+
} else if (r.ok) {
|
|
125
|
+
lines.push(`- ✅ ${r.component}: ok (${r.pm} ${r.script})`);
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(`- ❌ ${r.component}: failed (${r.pm ?? 'unknown'} ${r.script ?? ''})`);
|
|
128
|
+
if (r.error) lines.push(` - ${r.error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!ok) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('[lint] failed');
|
|
134
|
+
}
|
|
135
|
+
printResult({ json: false, text: lines.join('\n') });
|
|
136
|
+
if (!ok) {
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main().catch((err) => {
|
|
142
|
+
console.error('[lint] failed:', err);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
145
|
+
|
package/scripts/menubar.mjs
CHANGED
|
@@ -3,9 +3,12 @@ import { cp, mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
|
|
7
|
-
import { parseArgs } from './utils/args.mjs';
|
|
8
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
8
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
|
+
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
11
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
9
12
|
|
|
10
13
|
async function ensureSwiftbarAssets({ cliRootDir }) {
|
|
11
14
|
const homeDir = getHappyStacksHomeDir();
|
|
@@ -34,9 +37,20 @@ function openSwiftbarPluginsDir() {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
function
|
|
40
|
+
function sandboxPluginBasename() {
|
|
41
|
+
const sandboxDir = (process.env.HAPPY_STACKS_SANDBOX_DIR ?? '').trim();
|
|
42
|
+
if (!sandboxDir) return '';
|
|
43
|
+
const hash = createHash('sha256').update(sandboxDir).digest('hex').slice(0, 10);
|
|
44
|
+
return `happy-stacks.sandbox-${hash}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeSwiftbarPlugins({ patterns }) {
|
|
48
|
+
const pats = (patterns ?? []).filter(Boolean);
|
|
49
|
+
const args = pats.length ? pats.map((p) => `"${p}"`).join(' ') : '"happy-stacks.*.sh" "happy-local.*.sh"';
|
|
38
50
|
const s =
|
|
39
|
-
|
|
51
|
+
`DIR="$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null)"; ` +
|
|
52
|
+
`if [[ -z "$DIR" ]]; then DIR="$HOME/Library/Application Support/SwiftBar/Plugins"; fi; ` +
|
|
53
|
+
`if [[ -d "$DIR" ]]; then rm -f "$DIR"/${args} 2>/dev/null || true; echo "$DIR"; else echo ""; fi`;
|
|
40
54
|
const res = spawnSync('bash', ['-lc', s], { encoding: 'utf-8' });
|
|
41
55
|
if (res.status !== 0) {
|
|
42
56
|
return null;
|
|
@@ -45,6 +59,14 @@ function removeSwiftbarPlugins() {
|
|
|
45
59
|
return out || null;
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
function normalizeMenubarMode(raw) {
|
|
63
|
+
const v = String(raw ?? '').trim().toLowerCase();
|
|
64
|
+
if (!v) return '';
|
|
65
|
+
if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
|
|
66
|
+
if (v === 'dev' || v === 'developer') return 'dev';
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
async function main() {
|
|
49
71
|
const rawArgv = process.argv.slice(2);
|
|
50
72
|
const argv = rawArgv[0] === 'menubar' ? rawArgv.slice(1) : rawArgv;
|
|
@@ -55,16 +77,19 @@ async function main() {
|
|
|
55
77
|
if (wantsHelp(argv, { flags }) || cmd === 'help') {
|
|
56
78
|
printResult({
|
|
57
79
|
json,
|
|
58
|
-
data: { commands: ['install', 'uninstall', 'open'] },
|
|
80
|
+
data: { commands: ['install', 'uninstall', 'open', 'mode', 'status'] },
|
|
59
81
|
text: [
|
|
60
82
|
'[menubar] usage:',
|
|
61
83
|
' happys menubar install [--json]',
|
|
62
84
|
' happys menubar uninstall [--json]',
|
|
63
85
|
' happys menubar open [--json]',
|
|
86
|
+
' happys menubar mode <selfhost|dev> [--json]',
|
|
87
|
+
' happys menubar status [--json]',
|
|
64
88
|
'',
|
|
65
89
|
'notes:',
|
|
66
90
|
' - installs SwiftBar plugin into the active SwiftBar plugin folder',
|
|
67
|
-
' - keeps plugin source under
|
|
91
|
+
' - keeps plugin source under <homeDir>/extras/swiftbar for stability',
|
|
92
|
+
' - sandbox mode: install/uninstall are disabled by default (set HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1 to override)',
|
|
68
93
|
].join('\n'),
|
|
69
94
|
});
|
|
70
95
|
return;
|
|
@@ -82,15 +107,63 @@ async function main() {
|
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
if (cmd === 'menubar:uninstall' || cmd === 'uninstall') {
|
|
85
|
-
|
|
110
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
111
|
+
printResult({ json, data: { ok: true, skipped: 'sandbox' }, text: '[menubar] uninstall skipped (sandbox mode)' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const patterns = isSandboxed()
|
|
115
|
+
? [`${sandboxPluginBasename()}.*.sh`]
|
|
116
|
+
: ['happy-stacks.*.sh', 'happy-local.*.sh'];
|
|
117
|
+
const dir = removeSwiftbarPlugins({ patterns });
|
|
86
118
|
printResult({ json, data: { ok: true, pluginsDir: dir }, text: dir ? `[menubar] removed plugins from ${dir}` : '[menubar] no plugins dir found' });
|
|
87
119
|
return;
|
|
88
120
|
}
|
|
89
121
|
|
|
122
|
+
if (cmd === 'status') {
|
|
123
|
+
const mode = (process.env.HAPPY_STACKS_MENUBAR_MODE ?? process.env.HAPPY_LOCAL_MENUBAR_MODE ?? 'dev').trim() || 'dev';
|
|
124
|
+
printResult({ json, data: { ok: true, mode }, text: `[menubar] mode: ${mode}` });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (cmd === 'mode') {
|
|
129
|
+
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
130
|
+
const raw = positionals[1] ?? '';
|
|
131
|
+
const mode = normalizeMenubarMode(raw);
|
|
132
|
+
if (!mode) {
|
|
133
|
+
throw new Error('[menubar] usage: happys menubar mode <selfhost|dev> [--json]');
|
|
134
|
+
}
|
|
135
|
+
await ensureEnvLocalUpdated({
|
|
136
|
+
rootDir: cliRootDir,
|
|
137
|
+
updates: [
|
|
138
|
+
{ key: 'HAPPY_STACKS_MENUBAR_MODE', value: mode },
|
|
139
|
+
{ key: 'HAPPY_LOCAL_MENUBAR_MODE', value: mode },
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
printResult({ json, data: { ok: true, mode }, text: `[menubar] mode set: ${mode}` });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
90
146
|
if (cmd === 'menubar:install' || cmd === 'install') {
|
|
147
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'[menubar] install is disabled in sandbox mode.\n' +
|
|
150
|
+
'Reason: SwiftBar plugin installation writes to a global user folder.\n' +
|
|
151
|
+
'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
91
154
|
const { destDir } = await ensureSwiftbarAssets({ cliRootDir });
|
|
92
155
|
const installer = join(destDir, 'install.sh');
|
|
93
|
-
const
|
|
156
|
+
const env = {
|
|
157
|
+
...process.env,
|
|
158
|
+
HAPPY_STACKS_HOME_DIR: getHappyStacksHomeDir(),
|
|
159
|
+
...(isSandboxed()
|
|
160
|
+
? {
|
|
161
|
+
HAPPY_STACKS_SWIFTBAR_PLUGIN_BASENAME: sandboxPluginBasename(),
|
|
162
|
+
HAPPY_STACKS_SWIFTBAR_PLUGIN_WRAPPER: '1',
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
};
|
|
166
|
+
const res = spawnSync('bash', [installer, '--force'], { stdio: 'inherit', env });
|
|
94
167
|
if (res.status !== 0) {
|
|
95
168
|
process.exit(res.status ?? 1);
|
|
96
169
|
}
|
package/scripts/migrate.mjs
CHANGED
|
@@ -3,14 +3,15 @@ import { copyFile, mkdir, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
|
|
6
|
-
import { parseArgs } from './utils/args.mjs';
|
|
7
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
6
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
7
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
8
|
import { parseDotenv } from './utils/dotenv.mjs';
|
|
9
9
|
import { ensureEnvFileUpdated } from './utils/env_file.mjs';
|
|
10
10
|
import { resolveStackEnvPath } from './utils/paths.mjs';
|
|
11
11
|
import { ensureDepsInstalled } from './utils/pm.mjs';
|
|
12
12
|
import { ensureHappyServerManagedInfra, applyHappyServerMigrations } from './utils/happy_server_infra.mjs';
|
|
13
13
|
import { runCapture } from './utils/proc.mjs';
|
|
14
|
+
import { pickNextFreeTcpPort } from './utils/ports.mjs';
|
|
14
15
|
|
|
15
16
|
function usage() {
|
|
16
17
|
return [
|
|
@@ -97,9 +98,15 @@ async function migrateLightToServer({ rootDir, fromStack, toStack, includeFiles,
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
const toPortRaw = getEnvValue(toEnv, 'HAPPY_STACKS_SERVER_PORT') || getEnvValue(toEnv, 'HAPPY_LOCAL_SERVER_PORT');
|
|
100
|
-
|
|
101
|
+
let toPort = toPortRaw ? Number(toPortRaw) : NaN;
|
|
102
|
+
const toEphemeral = !toPortRaw;
|
|
101
103
|
if (!Number.isFinite(toPort) || toPort <= 0) {
|
|
102
|
-
|
|
104
|
+
// Ephemeral-port stacks don't pin ports in env. Pick a free port for this one-off migration run.
|
|
105
|
+
toPort = await pickNextFreeTcpPort(3005);
|
|
106
|
+
if (!json) {
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.log(`[migrate] to-stack has no pinned port; using ephemeral port ${toPort} for this migration run`);
|
|
109
|
+
}
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
// Ensure target secret is the same as source so auth tokens remain valid after migration.
|
|
@@ -111,17 +118,6 @@ async function migrateLightToServer({ rootDir, fromStack, toStack, includeFiles,
|
|
|
111
118
|
updates: [{ key: 'HAPPY_STACKS_HANDY_MASTER_SECRET_FILE', value: targetSecretPath }],
|
|
112
119
|
});
|
|
113
120
|
|
|
114
|
-
// Bring up infra and ensure env vars are present.
|
|
115
|
-
const infra = await ensureHappyServerManagedInfra({
|
|
116
|
-
stackName: toStack,
|
|
117
|
-
baseDir: to.baseDir,
|
|
118
|
-
serverPort: toPort,
|
|
119
|
-
publicServerUrl: `http://127.0.0.1:${toPort}`,
|
|
120
|
-
envPath: to.envPath,
|
|
121
|
-
env: process.env,
|
|
122
|
-
});
|
|
123
|
-
await applyHappyServerMigrations({ serverDir: fullDir, env: { ...process.env, ...infra.env } });
|
|
124
|
-
|
|
125
121
|
// Resolve component dirs (prefer stack-pinned dirs).
|
|
126
122
|
const lightDir = getEnvValue(fromEnv, 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT') || getEnvValue(fromEnv, 'HAPPY_LOCAL_COMPONENT_DIR_HAPPY_SERVER_LIGHT');
|
|
127
123
|
const fullDir = getEnvValue(toEnv, 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER') || getEnvValue(toEnv, 'HAPPY_LOCAL_COMPONENT_DIR_HAPPY_SERVER');
|
|
@@ -132,6 +128,20 @@ async function migrateLightToServer({ rootDir, fromStack, toStack, includeFiles,
|
|
|
132
128
|
await ensureDepsInstalled(lightDir, 'happy-server-light');
|
|
133
129
|
await ensureDepsInstalled(fullDir, 'happy-server');
|
|
134
130
|
|
|
131
|
+
// Bring up infra and ensure env vars are present.
|
|
132
|
+
const infra = await ensureHappyServerManagedInfra({
|
|
133
|
+
stackName: toStack,
|
|
134
|
+
baseDir: to.baseDir,
|
|
135
|
+
serverPort: toPort,
|
|
136
|
+
publicServerUrl: `http://127.0.0.1:${toPort}`,
|
|
137
|
+
envPath: to.envPath,
|
|
138
|
+
env: {
|
|
139
|
+
...process.env,
|
|
140
|
+
...(toEphemeral ? { HAPPY_STACKS_EPHEMERAL_PORTS: '1', HAPPY_LOCAL_EPHEMERAL_PORTS: '1' } : {}),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
await applyHappyServerMigrations({ serverDir: fullDir, env: { ...process.env, ...infra.env } });
|
|
144
|
+
|
|
135
145
|
// Copy sqlite DB to a snapshot so migration is consistent even if the source server is running.
|
|
136
146
|
const snapshotDir = join(to.baseDir, 'migrations');
|
|
137
147
|
await mkdir(snapshotDir, { recursive: true });
|
package/scripts/mobile.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
3
|
-
import {
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { pickNextFreeTcpPort } from './utils/ports.mjs';
|
|
4
4
|
import { run, runCapture, spawnProc } from './utils/proc.mjs';
|
|
5
5
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
6
6
|
import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/pm.mjs';
|
|
7
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
7
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
8
|
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, killPid, wantsExpoClearCache, writePidState } from './utils/expo.mjs';
|
|
9
|
+
import { killProcessGroupOwnedByStack } from './utils/ownership.mjs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Mobile dev helper for the embedded `components/happy` Expo app.
|
|
@@ -306,11 +307,16 @@ async function main() {
|
|
|
306
307
|
}
|
|
307
308
|
if (restart && running.state?.pid) {
|
|
308
309
|
const prevPid = Number(running.state.pid);
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || autostart.stackName;
|
|
311
|
+
const envPath = (process.env.HAPPY_STACKS_ENV_FILE ?? process.env.HAPPY_LOCAL_ENV_FILE ?? '').toString();
|
|
312
|
+
const res = await killProcessGroupOwnedByStack(prevPid, { stackName, envPath, label: 'expo-mobile', json: true });
|
|
313
|
+
if (!res.killed) {
|
|
314
|
+
// eslint-disable-next-line no-console
|
|
315
|
+
console.warn(
|
|
316
|
+
`[mobile] not stopping existing Metro pid=${prevPid} because it does not look stack-owned.\n` +
|
|
317
|
+
`[mobile] continuing by starting a new Metro on a free port.`
|
|
318
|
+
);
|
|
312
319
|
}
|
|
313
|
-
await killPid(prevPid);
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
const requestedPort = Number.parseInt(String(portRaw), 10);
|