happy-stacks 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -25
- package/bin/happys.mjs +116 -17
- package/docs/happy-development.md +2 -2
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/package.json +5 -1
- package/scripts/auth.mjs +59 -208
- package/scripts/build.mjs +58 -12
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +168 -20
- package/scripts/dev.mjs +196 -70
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +105 -78
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +8 -14
- package/scripts/install.mjs +119 -23
- package/scripts/lint.mjs +31 -32
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +93 -108
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +217 -0
- package/scripts/review_pr.mjs +368 -0
- package/scripts/run.mjs +95 -21
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +19 -29
- package/scripts/setup.mjs +63 -160
- package/scripts/setup_pr.mjs +592 -52
- package/scripts/stack.mjs +608 -200
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +44 -11
- package/scripts/test.mjs +52 -36
- package/scripts/tui.mjs +314 -74
- package/scripts/typecheck.mjs +31 -32
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
- package/scripts/utils/auth/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
- package/scripts/utils/auth/sources.mjs +38 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +24 -0
- package/scripts/utils/cli/cwd_scope.mjs +82 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
- package/scripts/utils/dev/expo_dev.mjs +246 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
- package/scripts/utils/dev_auth_key.mjs +1 -1
- package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
- package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
- package/scripts/utils/handy_master_secret.mjs +2 -2
- package/scripts/utils/mobile/config.mjs +31 -0
- package/scripts/utils/mobile/dev_client_links.mjs +60 -0
- package/scripts/utils/mobile/identifiers.mjs +47 -0
- package/scripts/utils/mobile/identifiers.test.mjs +42 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +56 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/runners/coderabbit.mjs +19 -0
- package/scripts/utils/review/runners/codex.mjs +51 -0
- package/scripts/utils/review/targets.mjs +24 -0
- package/scripts/utils/review/targets.test.mjs +36 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- package/scripts/utils/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +101 -0
- package/scripts/utils/server/validate.mjs +88 -0
- package/scripts/utils/service/autostart_darwin.mjs +182 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +23 -0
- package/scripts/utils/stack/dirs.mjs +27 -0
- package/scripts/utils/stack/editor_workspace.mjs +152 -0
- package/scripts/utils/stack/names.mjs +12 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
- package/scripts/utils/stack_context.mjs +3 -3
- package/scripts/utils/stack_runtime_state.mjs +1 -1
- package/scripts/utils/stacks.mjs +2 -2
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/validate.mjs +1 -1
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +171 -113
- package/scripts/utils/auth_sources.mjs +0 -12
- package/scripts/utils/dev_expo_web.mjs +0 -112
- package/scripts/utils/localhost_host.mjs +0 -17
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
- /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
- /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/scripts/worktrees.mjs
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
5
|
-
import { pathExists } from './utils/fs.mjs';
|
|
6
|
-
import { run, runCapture } from './utils/proc.mjs';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
6
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
7
|
+
import { commandExists, resolveCommandPath } from './utils/proc/commands.mjs';
|
|
8
|
+
import { componentDirEnvKey, getComponentDir, getComponentsDir, getHappyStacksHomeDir, getRootDir, getWorkspaceDir } from './utils/paths/paths.mjs';
|
|
9
|
+
import { inferRemoteNameForOwner, parseGithubOwner } from './utils/git/worktrees.mjs';
|
|
10
|
+
import { getWorktreesRoot } from './utils/git/worktrees.mjs';
|
|
11
|
+
import { parseGithubPullRequest, sanitizeSlugPart } from './utils/git/refs.mjs';
|
|
12
|
+
import { readTextIfExists } from './utils/fs/ops.mjs';
|
|
9
13
|
import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
10
14
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
11
|
-
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
12
|
-
import { ensureEnvFileUpdated } from './utils/env_file.mjs';
|
|
15
|
+
import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
|
|
16
|
+
import { ensureEnvFileUpdated } from './utils/env/env_file.mjs';
|
|
17
|
+
import { isSandboxed } from './utils/env/sandbox.mjs';
|
|
13
18
|
import { existsSync } from 'node:fs';
|
|
14
|
-
import { getHomeEnvLocalPath, getHomeEnvPath, resolveUserConfigEnvPath } from './utils/config.mjs';
|
|
15
|
-
import { detectServerComponentDirMismatch } from './utils/validate.mjs';
|
|
19
|
+
import { getHomeEnvLocalPath, getHomeEnvPath, resolveUserConfigEnvPath } from './utils/env/config.mjs';
|
|
20
|
+
import { detectServerComponentDirMismatch } from './utils/server/validate.mjs';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
16
23
|
|
|
17
24
|
function getActiveStackName() {
|
|
18
25
|
return (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
@@ -22,10 +29,6 @@ function isMainStack() {
|
|
|
22
29
|
return getActiveStackName() === 'main';
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
function getWorktreesRoot(rootDir) {
|
|
26
|
-
return join(getComponentsDir(rootDir), '.worktrees');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
32
|
function resolveComponentWorktreeDir({ rootDir, component, spec }) {
|
|
30
33
|
const worktreesRoot = getWorktreesRoot(rootDir);
|
|
31
34
|
const raw = (spec ?? '').trim();
|
|
@@ -51,32 +54,6 @@ function resolveComponentWorktreeDir({ rootDir, component, spec }) {
|
|
|
51
54
|
return join(worktreesRoot, component, ...raw.split('/'));
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
function parseGithubPullRequest(input) {
|
|
55
|
-
const raw = (input ?? '').trim();
|
|
56
|
-
if (!raw) return null;
|
|
57
|
-
if (/^\d+$/.test(raw)) {
|
|
58
|
-
return { number: Number(raw), owner: null, repo: null };
|
|
59
|
-
}
|
|
60
|
-
// https://github.com/<owner>/<repo>/pull/<num>
|
|
61
|
-
const m = raw.match(/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/pull\/(?<num>\d+)/);
|
|
62
|
-
if (!m?.groups?.num) return null;
|
|
63
|
-
return {
|
|
64
|
-
number: Number(m.groups.num),
|
|
65
|
-
owner: m.groups.owner ?? null,
|
|
66
|
-
repo: m.groups.repo ?? null,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function sanitizeSlugPart(s) {
|
|
71
|
-
return (s ?? '')
|
|
72
|
-
.toString()
|
|
73
|
-
.trim()
|
|
74
|
-
.toLowerCase()
|
|
75
|
-
.replace(/[^a-z0-9._/-]+/g, '-')
|
|
76
|
-
.replace(/-+/g, '-')
|
|
77
|
-
.replace(/^-+|-+$/g, '');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
57
|
async function isWorktreeClean(dir) {
|
|
81
58
|
const dirty = (await git(dir, ['status', '--porcelain'])).trim();
|
|
82
59
|
return !dirty;
|
|
@@ -188,41 +165,100 @@ async function installDependencies({ dir }) {
|
|
|
188
165
|
return { installed: false, reason: 'no package manager detected (no package.json)' };
|
|
189
166
|
}
|
|
190
167
|
|
|
168
|
+
// IMPORTANT:
|
|
169
|
+
// When a caller requests --json, stdout must be reserved for JSON output only.
|
|
170
|
+
// Package managers (especially Yarn) write progress to stdout, which would corrupt JSON parsing
|
|
171
|
+
// in wrappers like `stack pr`.
|
|
172
|
+
const jsonMode = Boolean((process.argv ?? []).includes('--json'));
|
|
173
|
+
const runForJson = async (cmd, args) => {
|
|
174
|
+
try {
|
|
175
|
+
const out = await runCapture(cmd, args, { cwd: dir });
|
|
176
|
+
if (out) process.stderr.write(out);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
const out = String(e?.out ?? '');
|
|
179
|
+
const err = String(e?.err ?? '');
|
|
180
|
+
if (out) process.stderr.write(out);
|
|
181
|
+
if (err) process.stderr.write(err);
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
191
186
|
if (pm.kind === 'pnpm') {
|
|
192
|
-
|
|
187
|
+
if (jsonMode) {
|
|
188
|
+
await runForJson('pnpm', ['install', '--frozen-lockfile']);
|
|
189
|
+
} else {
|
|
190
|
+
await run('pnpm', ['install', '--frozen-lockfile'], { cwd: dir });
|
|
191
|
+
}
|
|
193
192
|
return { installed: true, reason: null };
|
|
194
193
|
}
|
|
195
194
|
if (pm.kind === 'yarn') {
|
|
196
195
|
// Works for yarn classic; yarn berry will ignore/translate flags as needed.
|
|
197
|
-
|
|
196
|
+
if (jsonMode) {
|
|
197
|
+
await runForJson('yarn', ['install', '--frozen-lockfile']);
|
|
198
|
+
} else {
|
|
199
|
+
await run('yarn', ['install', '--frozen-lockfile'], { cwd: dir });
|
|
200
|
+
}
|
|
198
201
|
return { installed: true, reason: null };
|
|
199
202
|
}
|
|
200
203
|
// npm
|
|
201
204
|
if (pm.lockfile && pm.lockfile !== 'package.json') {
|
|
202
|
-
|
|
205
|
+
if (jsonMode) {
|
|
206
|
+
await runForJson('npm', ['ci']);
|
|
207
|
+
} else {
|
|
208
|
+
await run('npm', ['ci'], { cwd: dir });
|
|
209
|
+
}
|
|
203
210
|
} else {
|
|
204
|
-
|
|
211
|
+
if (jsonMode) {
|
|
212
|
+
await runForJson('npm', ['install']);
|
|
213
|
+
} else {
|
|
214
|
+
await run('npm', ['install'], { cwd: dir });
|
|
215
|
+
}
|
|
205
216
|
}
|
|
206
217
|
return { installed: true, reason: null };
|
|
207
218
|
}
|
|
208
219
|
|
|
209
|
-
|
|
220
|
+
function allowNodeModulesSymlinkForComponent(component) {
|
|
221
|
+
const c = String(component ?? '').trim();
|
|
222
|
+
if (!c) return true;
|
|
223
|
+
// Expo/Metro commonly breaks with symlinked node_modules. Avoid symlinks for the Happy UI worktree by default.
|
|
224
|
+
// Override if you *really* want to experiment:
|
|
225
|
+
// HAPPY_STACKS_WT_ALLOW_HAPPY_NODE_MODULES_SYMLINK=1
|
|
226
|
+
const allowHappySymlink =
|
|
227
|
+
(process.env.HAPPY_STACKS_WT_ALLOW_HAPPY_NODE_MODULES_SYMLINK ?? process.env.HAPPY_LOCAL_WT_ALLOW_HAPPY_NODE_MODULES_SYMLINK ?? '')
|
|
228
|
+
.toString()
|
|
229
|
+
.trim() === '1';
|
|
230
|
+
if (c === 'happy' && !allowHappySymlink) return false;
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function maybeSetupDeps({ repoRoot, baseDir, worktreeDir, depsMode, component }) {
|
|
210
235
|
if (!depsMode || depsMode === 'none') {
|
|
211
236
|
return { mode: 'none', linked: false, installed: false, message: null };
|
|
212
237
|
}
|
|
213
238
|
|
|
214
239
|
// Prefer explicit baseDir if provided, otherwise link from the primary checkout (repoRoot).
|
|
215
240
|
const linkFrom = baseDir || repoRoot;
|
|
241
|
+
const allowSymlink = allowNodeModulesSymlinkForComponent(component);
|
|
216
242
|
|
|
217
243
|
if (depsMode === 'link' || depsMode === 'link-or-install') {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
if (!allowSymlink) {
|
|
245
|
+
const msg =
|
|
246
|
+
`[wt] refusing to symlink node_modules for ${component} (Expo/Metro is often broken by symlinks).\n` +
|
|
247
|
+
`[wt] Fix: use --deps=install (recommended). To override: set HAPPY_STACKS_WT_ALLOW_HAPPY_NODE_MODULES_SYMLINK=1`;
|
|
248
|
+
if (depsMode === 'link') {
|
|
249
|
+
return { mode: depsMode, linked: false, installed: false, message: msg };
|
|
250
|
+
}
|
|
251
|
+
// link-or-install: fall through to install.
|
|
252
|
+
} else {
|
|
253
|
+
const res = await linkNodeModules({ fromDir: linkFrom, toDir: worktreeDir });
|
|
254
|
+
if (res.linked) {
|
|
255
|
+
return { mode: depsMode, linked: true, installed: false, message: null };
|
|
256
|
+
}
|
|
257
|
+
if (depsMode === 'link') {
|
|
258
|
+
return { mode: depsMode, linked: false, installed: false, message: res.reason };
|
|
259
|
+
}
|
|
260
|
+
// fall through to install
|
|
224
261
|
}
|
|
225
|
-
// fall through to install
|
|
226
262
|
}
|
|
227
263
|
|
|
228
264
|
const inst = await installDependencies({ dir: worktreeDir });
|
|
@@ -456,11 +492,9 @@ async function migrateComponentWorktrees({ rootDir, component }) {
|
|
|
456
492
|
}
|
|
457
493
|
|
|
458
494
|
async function cmdMigrate({ rootDir }) {
|
|
459
|
-
const components = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
460
|
-
|
|
461
495
|
let totalMoved = 0;
|
|
462
496
|
let totalRenamed = 0;
|
|
463
|
-
for (const component of
|
|
497
|
+
for (const component of DEFAULT_COMPONENTS) {
|
|
464
498
|
const res = await migrateComponentWorktrees({ rootDir, component });
|
|
465
499
|
totalMoved += res.moved;
|
|
466
500
|
totalRenamed += res.renamed;
|
|
@@ -469,13 +503,13 @@ async function cmdMigrate({ rootDir }) {
|
|
|
469
503
|
// If the persisted config pins any component dir to a legacy location, attempt to rewrite it.
|
|
470
504
|
const envUpdates = [];
|
|
471
505
|
|
|
472
|
-
// Keep in sync with scripts/utils/env_local.mjs selection logic.
|
|
506
|
+
// Keep in sync with scripts/utils/env/env_local.mjs selection logic.
|
|
473
507
|
const explicitEnv = (process.env.HAPPY_STACKS_ENV_FILE ?? process.env.HAPPY_LOCAL_ENV_FILE ?? '').trim();
|
|
474
508
|
const hasHomeConfig = existsSync(getHomeEnvPath()) || existsSync(getHomeEnvLocalPath());
|
|
475
509
|
const envPath = explicitEnv ? explicitEnv : hasHomeConfig ? resolveUserConfigEnvPath({ cliRootDir: rootDir }) : join(rootDir, 'env.local');
|
|
476
510
|
|
|
477
511
|
if (await pathExists(envPath)) {
|
|
478
|
-
const raw = await
|
|
512
|
+
const raw = (await readTextIfExists(envPath)) ?? '';
|
|
479
513
|
const rewrite = (v) => {
|
|
480
514
|
if (!v.includes('/components/')) {
|
|
481
515
|
return v;
|
|
@@ -705,7 +739,7 @@ async function cmdNew({ rootDir, argv }) {
|
|
|
705
739
|
}
|
|
706
740
|
|
|
707
741
|
const depsMode = parseDepsMode(kv.get('--deps'));
|
|
708
|
-
const deps = await maybeSetupDeps({ repoRoot, baseDir: baseWorktreeDir || '', worktreeDir: destPath, depsMode });
|
|
742
|
+
const deps = await maybeSetupDeps({ repoRoot, baseDir: baseWorktreeDir || '', worktreeDir: destPath, depsMode, component });
|
|
709
743
|
|
|
710
744
|
const shouldUse = flags.has('--use');
|
|
711
745
|
const force = flags.has('--force');
|
|
@@ -791,8 +825,14 @@ async function cmdPr({ rootDir, argv }) {
|
|
|
791
825
|
throw new Error(`[wt] unable to parse PR: ${prInput}`);
|
|
792
826
|
}
|
|
793
827
|
|
|
794
|
-
const
|
|
795
|
-
const
|
|
828
|
+
const remoteFromArg = (kv.get('--remote') ?? '').trim();
|
|
829
|
+
const canFetchByUrl = !remoteFromArg && pr.owner && pr.repo;
|
|
830
|
+
const fetchTarget = canFetchByUrl ? `https://github.com/${pr.owner}/${pr.repo}.git` : null;
|
|
831
|
+
|
|
832
|
+
// If we can fetch directly from the PR URL's repo, do it. This avoids any assumptions about local
|
|
833
|
+
// remote names like "origin" vs "upstream" and works even when the repo doesn't have that remote set up.
|
|
834
|
+
const remoteName = canFetchByUrl ? '' : await normalizeRemoteName(repoRoot, remoteFromArg || 'upstream');
|
|
835
|
+
const { owner } = canFetchByUrl ? { owner: pr.owner } : await resolveRemoteOwner(repoRoot, remoteName);
|
|
796
836
|
|
|
797
837
|
const slugExtra = sanitizeSlugPart(kv.get('--slug') ?? '');
|
|
798
838
|
const slug = slugExtra ? `pr/${pr.number}-${slugExtra}` : `pr/${pr.number}`;
|
|
@@ -809,7 +849,9 @@ async function cmdPr({ rootDir, argv }) {
|
|
|
809
849
|
}
|
|
810
850
|
|
|
811
851
|
// Fetch PR head ref (GitHub convention). Use + to allow force-updated PR branches when --force is set.
|
|
812
|
-
|
|
852
|
+
// In sandbox mode, be more aggressive: the entire workspace is disposable, so it's safe to
|
|
853
|
+
// reset an existing local PR branch to the fetched PR head if needed.
|
|
854
|
+
const force = flags.has('--force') || isSandboxed();
|
|
813
855
|
let oldHead = null;
|
|
814
856
|
const prRef = `refs/pull/${pr.number}/head`;
|
|
815
857
|
if (exists) {
|
|
@@ -825,14 +867,17 @@ async function cmdPr({ rootDir, argv }) {
|
|
|
825
867
|
}
|
|
826
868
|
|
|
827
869
|
oldHead = (await git(destPath, ['rev-parse', 'HEAD'])).trim();
|
|
828
|
-
await git(repoRoot, ['fetch', '--quiet', remoteName, prRef]);
|
|
870
|
+
await git(repoRoot, ['fetch', '--quiet', fetchTarget ?? remoteName, prRef]);
|
|
829
871
|
const newTip = (await git(repoRoot, ['rev-parse', 'FETCH_HEAD'])).trim();
|
|
830
872
|
|
|
831
873
|
const isAncestor = await gitOk(repoRoot, ['merge-base', '--is-ancestor', oldHead, newTip]);
|
|
832
874
|
if (!isAncestor && !force) {
|
|
875
|
+
const hint = fetchTarget
|
|
876
|
+
? `[wt] re-run with: happys wt pr ${component} ${pr.number} --update --force`
|
|
877
|
+
: `[wt] re-run with: happys wt pr ${component} ${pr.number} --remote=${remoteName} --update --force`;
|
|
833
878
|
throw new Error(
|
|
834
879
|
`[wt] PR update is not a fast-forward (likely force-push) for ${branchName}\n` +
|
|
835
|
-
|
|
880
|
+
hint
|
|
836
881
|
);
|
|
837
882
|
}
|
|
838
883
|
|
|
@@ -863,16 +908,22 @@ async function cmdPr({ rootDir, argv }) {
|
|
|
863
908
|
);
|
|
864
909
|
}
|
|
865
910
|
} else {
|
|
866
|
-
await git(repoRoot, ['fetch', '--quiet', remoteName, prRef]);
|
|
911
|
+
await git(repoRoot, ['fetch', '--quiet', fetchTarget ?? remoteName, prRef]);
|
|
867
912
|
const newTip = (await git(repoRoot, ['rev-parse', 'FETCH_HEAD'])).trim();
|
|
868
913
|
|
|
869
914
|
const branchExists = await gitOk(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`]);
|
|
870
915
|
if (branchExists) {
|
|
871
916
|
if (!force) {
|
|
872
|
-
|
|
917
|
+
// If the branch already points at the fetched PR tip, we can safely just attach a worktree.
|
|
918
|
+
const branchHead = (await git(repoRoot, ['rev-parse', branchName])).trim();
|
|
919
|
+
if (branchHead !== newTip) {
|
|
920
|
+
throw new Error(`[wt] branch already exists: ${branchName}\n[wt] re-run with --force to reset it to the PR head`);
|
|
921
|
+
}
|
|
922
|
+
await git(repoRoot, ['worktree', 'add', destPath, branchName]);
|
|
923
|
+
} else {
|
|
924
|
+
await git(repoRoot, ['branch', '-f', branchName, newTip]);
|
|
925
|
+
await git(repoRoot, ['worktree', 'add', destPath, branchName]);
|
|
873
926
|
}
|
|
874
|
-
await git(repoRoot, ['branch', '-f', branchName, newTip]);
|
|
875
|
-
await git(repoRoot, ['worktree', 'add', destPath, branchName]);
|
|
876
927
|
} else {
|
|
877
928
|
// Create worktree at PR head (new local branch).
|
|
878
929
|
await git(repoRoot, ['worktree', 'add', '-b', branchName, destPath, newTip]);
|
|
@@ -881,7 +932,7 @@ async function cmdPr({ rootDir, argv }) {
|
|
|
881
932
|
|
|
882
933
|
// Optional deps handling (useful when PR branches add/change dependencies).
|
|
883
934
|
const depsMode = parseDepsMode(kv.get('--deps'));
|
|
884
|
-
const deps = await maybeSetupDeps({ repoRoot, baseDir: repoRoot, worktreeDir: destPath, depsMode });
|
|
935
|
+
const deps = await maybeSetupDeps({ repoRoot, baseDir: repoRoot, worktreeDir: destPath, depsMode, component });
|
|
885
936
|
|
|
886
937
|
const shouldUse = flags.has('--use');
|
|
887
938
|
if (shouldUse) {
|
|
@@ -1245,15 +1296,6 @@ async function cmdSync({ rootDir, argv }) {
|
|
|
1245
1296
|
return { component, remote: remoteName, mirrorBranch, upstreamRef: `${remoteName}/${defaultBranch}` };
|
|
1246
1297
|
}
|
|
1247
1298
|
|
|
1248
|
-
async function commandExists(cmd) {
|
|
1249
|
-
try {
|
|
1250
|
-
const out = (await runCapture('sh', ['-lc', `command -v ${cmd} >/dev/null 2>&1 && echo yes || echo no`])).trim();
|
|
1251
|
-
return out === 'yes';
|
|
1252
|
-
} catch {
|
|
1253
|
-
return false;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
1299
|
async function fileExists(path) {
|
|
1258
1300
|
try {
|
|
1259
1301
|
return await pathExists(path);
|
|
@@ -1408,14 +1450,15 @@ async function cmdCode({ rootDir, argv }) {
|
|
|
1408
1450
|
if (!(await pathExists(dir))) {
|
|
1409
1451
|
throw new Error(`[wt] target does not exist: ${dir}`);
|
|
1410
1452
|
}
|
|
1411
|
-
|
|
1453
|
+
const codePath = await resolveCommandPath('code', { cwd: rootDir, env: process.env });
|
|
1454
|
+
if (!codePath) {
|
|
1412
1455
|
throw new Error("[wt] VS Code CLI 'code' not found on PATH. In VS Code: Cmd+Shift+P → 'Shell Command: Install code command in PATH'.");
|
|
1413
1456
|
}
|
|
1414
1457
|
if (json) {
|
|
1415
|
-
return { component, dir, cmd: 'code' };
|
|
1458
|
+
return { component, dir, cmd: 'code', resolvedCmd: codePath };
|
|
1416
1459
|
}
|
|
1417
|
-
await run(
|
|
1418
|
-
return { component, dir, cmd: 'code' };
|
|
1460
|
+
await run(codePath, [dir], { cwd: rootDir, env: process.env, stdio: 'inherit' });
|
|
1461
|
+
return { component, dir, cmd: 'code', resolvedCmd: codePath };
|
|
1419
1462
|
}
|
|
1420
1463
|
|
|
1421
1464
|
async function cmdCursor({ rootDir, argv }) {
|
|
@@ -1432,14 +1475,20 @@ async function cmdCursor({ rootDir, argv }) {
|
|
|
1432
1475
|
throw new Error(`[wt] target does not exist: ${dir}`);
|
|
1433
1476
|
}
|
|
1434
1477
|
|
|
1435
|
-
const
|
|
1478
|
+
const cursorPath = await resolveCommandPath('cursor', { cwd: rootDir, env: process.env });
|
|
1479
|
+
const hasCursorCli = Boolean(cursorPath);
|
|
1436
1480
|
if (json) {
|
|
1437
|
-
return {
|
|
1481
|
+
return {
|
|
1482
|
+
component,
|
|
1483
|
+
dir,
|
|
1484
|
+
cmd: hasCursorCli ? 'cursor' : process.platform === 'darwin' ? 'open -a Cursor' : null,
|
|
1485
|
+
resolvedCmd: cursorPath || null,
|
|
1486
|
+
};
|
|
1438
1487
|
}
|
|
1439
1488
|
|
|
1440
1489
|
if (hasCursorCli) {
|
|
1441
|
-
await run(
|
|
1442
|
-
return { component, dir, cmd: 'cursor' };
|
|
1490
|
+
await run(cursorPath, [dir], { cwd: rootDir, env: process.env, stdio: 'inherit' });
|
|
1491
|
+
return { component, dir, cmd: 'cursor', resolvedCmd: cursorPath };
|
|
1443
1492
|
}
|
|
1444
1493
|
|
|
1445
1494
|
if (process.platform === 'darwin') {
|
|
@@ -1450,8 +1499,6 @@ async function cmdCursor({ rootDir, argv }) {
|
|
|
1450
1499
|
throw new Error("[wt] Cursor CLI 'cursor' not found on PATH (and non-macOS fallback is unavailable).");
|
|
1451
1500
|
}
|
|
1452
1501
|
|
|
1453
|
-
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
1454
|
-
|
|
1455
1502
|
async function cmdSyncAll({ rootDir, argv }) {
|
|
1456
1503
|
const { flags, kv } = parseArgs(argv);
|
|
1457
1504
|
const json = wantsJson(argv, { flags });
|
|
@@ -1584,43 +1631,48 @@ async function cmdNewInteractive({ rootDir, argv }) {
|
|
|
1584
1631
|
});
|
|
1585
1632
|
}
|
|
1586
1633
|
|
|
1587
|
-
async function
|
|
1588
|
-
const component = args[0];
|
|
1589
|
-
if (!component) {
|
|
1590
|
-
throw new Error('[wt] usage: happys wt list <component>');
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1634
|
+
async function cmdListOne({ rootDir, component }) {
|
|
1593
1635
|
const wtRoot = getWorktreesRoot(rootDir);
|
|
1594
1636
|
const dir = join(wtRoot, component);
|
|
1637
|
+
const key = componentDirEnvKey(component);
|
|
1638
|
+
const active = (process.env[key] ?? '').trim() || join(getComponentsDir(rootDir), component);
|
|
1639
|
+
|
|
1595
1640
|
if (!(await pathExists(dir))) {
|
|
1596
|
-
return { component, activeDir:
|
|
1641
|
+
return { component, activeDir: active, worktrees: [] };
|
|
1597
1642
|
}
|
|
1598
1643
|
|
|
1599
|
-
const
|
|
1644
|
+
const worktrees = [];
|
|
1600
1645
|
const walk = async (d) => {
|
|
1646
|
+
// In git worktrees, ".git" is usually a file that points to the shared git dir.
|
|
1647
|
+
// If this is a worktree root, record it and do not descend into it (avoids traversing huge trees like node_modules).
|
|
1648
|
+
if (await pathExists(join(d, '.git'))) {
|
|
1649
|
+
worktrees.push(d);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1601
1652
|
const entries = await readdir(d, { withFileTypes: true });
|
|
1602
1653
|
for (const e of entries) {
|
|
1603
|
-
if (!e.isDirectory())
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
leafs.push(p);
|
|
1608
|
-
await walk(p);
|
|
1654
|
+
if (!e.isDirectory()) continue;
|
|
1655
|
+
if (e.name === 'node_modules') continue;
|
|
1656
|
+
if (e.name.startsWith('.')) continue;
|
|
1657
|
+
await walk(join(d, e.name));
|
|
1609
1658
|
}
|
|
1610
1659
|
};
|
|
1611
1660
|
await walk(dir);
|
|
1612
|
-
|
|
1661
|
+
worktrees.sort();
|
|
1613
1662
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1663
|
+
return { component, activeDir: active, worktrees };
|
|
1664
|
+
}
|
|
1616
1665
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1666
|
+
async function cmdList({ rootDir, args }) {
|
|
1667
|
+
const component = args[0];
|
|
1668
|
+
if (!component) {
|
|
1669
|
+
const results = [];
|
|
1670
|
+
for (const c of DEFAULT_COMPONENTS) {
|
|
1671
|
+
results.push(await cmdListOne({ rootDir, component: c }));
|
|
1621
1672
|
}
|
|
1673
|
+
return { components: DEFAULT_COMPONENTS, results };
|
|
1622
1674
|
}
|
|
1623
|
-
return {
|
|
1675
|
+
return await cmdListOne({ rootDir, component });
|
|
1624
1676
|
}
|
|
1625
1677
|
|
|
1626
1678
|
async function main() {
|
|
@@ -1644,7 +1696,7 @@ async function main() {
|
|
|
1644
1696
|
' happys wt migrate [--json]',
|
|
1645
1697
|
' happys wt sync <component> [--remote=<name>] [--json]',
|
|
1646
1698
|
' happys wt sync-all [--remote=<name>] [--json]',
|
|
1647
|
-
' happys wt list
|
|
1699
|
+
' happys wt list [component] [--json]',
|
|
1648
1700
|
' happys wt new <component> <slug> [--from=upstream|origin] [--remote=<name>] [--base=<ref>|--base-worktree=<spec>] [--deps=none|link|install|link-or-install] [--use] [--force] [--interactive|-i] [--json]',
|
|
1649
1701
|
' happys wt duplicate <component> <fromWorktreeSpec|path|active|default> <newSlug> [--remote=<name>] [--deps=none|link|install|link-or-install] [--use] [--json]',
|
|
1650
1702
|
' happys wt pr <component> <pr-url|number> [--remote=upstream] [--slug=<name>] [--deps=none|link|install|link-or-install] [--use] [--update] [--stash|--stash-keep] [--force] [--json]',
|
|
@@ -1665,7 +1717,7 @@ async function main() {
|
|
|
1665
1717
|
' "<absolute path>": explicit checkout path',
|
|
1666
1718
|
'',
|
|
1667
1719
|
'components:',
|
|
1668
|
-
'
|
|
1720
|
+
` ${DEFAULT_COMPONENTS.join(' | ')}`,
|
|
1669
1721
|
].join('\n'),
|
|
1670
1722
|
});
|
|
1671
1723
|
return;
|
|
@@ -1824,9 +1876,15 @@ async function main() {
|
|
|
1824
1876
|
if (json) {
|
|
1825
1877
|
printResult({ json, data: res });
|
|
1826
1878
|
} else {
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1879
|
+
const results = Array.isArray(res?.results) ? res.results : [res];
|
|
1880
|
+
const lines = [];
|
|
1881
|
+
for (const r of results) {
|
|
1882
|
+
lines.push(`[wt] ${r.component} worktrees:`);
|
|
1883
|
+
lines.push(`- active: ${r.activeDir}`);
|
|
1884
|
+
for (const p of r.worktrees) {
|
|
1885
|
+
lines.push(`- ${p}`);
|
|
1886
|
+
}
|
|
1887
|
+
lines.push('');
|
|
1830
1888
|
}
|
|
1831
1889
|
printResult({ json: false, text: lines.join('\n') });
|
|
1832
1890
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
export function isLegacyAuthSourceName(name) {
|
|
5
|
-
const s = String(name ?? '').trim().toLowerCase();
|
|
6
|
-
return s === 'legacy' || s === 'system' || s === 'local-install';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getLegacyHappyBaseDir() {
|
|
10
|
-
return join(homedir(), '.happy');
|
|
11
|
-
}
|
|
12
|
-
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { ensureDepsInstalled, pmSpawnBin } from './pm.mjs';
|
|
2
|
-
import { ensureExpoIsolationEnv, getExpoStatePaths, isStateProcessRunning, wantsExpoClearCache, writePidState } from './expo.mjs';
|
|
3
|
-
import { pickDevMetroPort, resolveStackUiDevPortStart } from './dev_server.mjs';
|
|
4
|
-
import { recordStackRuntimeUpdate } from './stack_runtime_state.mjs';
|
|
5
|
-
import { killProcessGroupOwnedByStack } from './ownership.mjs';
|
|
6
|
-
|
|
7
|
-
export async function startDevExpoWebUi({
|
|
8
|
-
startUi,
|
|
9
|
-
uiDir,
|
|
10
|
-
autostart,
|
|
11
|
-
baseEnv,
|
|
12
|
-
apiServerUrl,
|
|
13
|
-
restart,
|
|
14
|
-
stackMode,
|
|
15
|
-
runtimeStatePath,
|
|
16
|
-
stackName,
|
|
17
|
-
envPath,
|
|
18
|
-
children,
|
|
19
|
-
spawnOptions = {},
|
|
20
|
-
}) {
|
|
21
|
-
if (!startUi) return { ok: true, skipped: true, reason: 'disabled' };
|
|
22
|
-
|
|
23
|
-
await ensureDepsInstalled(uiDir, 'happy');
|
|
24
|
-
const uiEnv = { ...baseEnv };
|
|
25
|
-
delete uiEnv.CI;
|
|
26
|
-
uiEnv.EXPO_PUBLIC_HAPPY_SERVER_URL = apiServerUrl;
|
|
27
|
-
uiEnv.EXPO_PUBLIC_DEBUG = uiEnv.EXPO_PUBLIC_DEBUG ?? '1';
|
|
28
|
-
|
|
29
|
-
// We own the browser opening behavior in Happy Stacks so we can reliably open the correct origin.
|
|
30
|
-
uiEnv.EXPO_NO_BROWSER = '1';
|
|
31
|
-
uiEnv.BROWSER = 'none';
|
|
32
|
-
|
|
33
|
-
const uiPaths = getExpoStatePaths({
|
|
34
|
-
baseDir: autostart.baseDir,
|
|
35
|
-
kind: 'ui-dev',
|
|
36
|
-
projectDir: uiDir,
|
|
37
|
-
stateFileName: 'ui.state.json',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await ensureExpoIsolationEnv({
|
|
41
|
-
env: uiEnv,
|
|
42
|
-
stateDir: uiPaths.stateDir,
|
|
43
|
-
expoHomeDir: uiPaths.expoHomeDir,
|
|
44
|
-
tmpDir: uiPaths.tmpDir,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const uiRunning = await isStateProcessRunning(uiPaths.statePath);
|
|
48
|
-
const uiAlreadyRunning = Boolean(uiRunning.running);
|
|
49
|
-
|
|
50
|
-
if (uiAlreadyRunning && !restart) {
|
|
51
|
-
const pid = Number(uiRunning.state?.pid);
|
|
52
|
-
const port = Number(uiRunning.state?.port);
|
|
53
|
-
if (stackMode && runtimeStatePath && Number.isFinite(pid) && pid > 1) {
|
|
54
|
-
await recordStackRuntimeUpdate(runtimeStatePath, {
|
|
55
|
-
processes: { expoWebPid: pid },
|
|
56
|
-
expo: { webPort: Number.isFinite(port) && port > 0 ? port : null },
|
|
57
|
-
}).catch(() => {});
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
ok: true,
|
|
61
|
-
skipped: true,
|
|
62
|
-
reason: 'already_running',
|
|
63
|
-
pid: Number.isFinite(pid) ? pid : null,
|
|
64
|
-
port: Number.isFinite(port) ? port : null,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const strategy =
|
|
69
|
-
(baseEnv.HAPPY_STACKS_UI_DEV_PORT_STRATEGY ?? baseEnv.HAPPY_LOCAL_UI_DEV_PORT_STRATEGY ?? 'ephemeral').toString().trim() ||
|
|
70
|
-
'ephemeral';
|
|
71
|
-
const stable = strategy === 'stable';
|
|
72
|
-
const startPort = stackMode && stable ? resolveStackUiDevPortStart({ env: baseEnv, stackName }) : 8081;
|
|
73
|
-
const metroPort = await pickDevMetroPort({ startPort });
|
|
74
|
-
uiEnv.RCT_METRO_PORT = String(metroPort);
|
|
75
|
-
|
|
76
|
-
const uiArgs = ['start', '--web', '--port', String(metroPort)];
|
|
77
|
-
if (wantsExpoClearCache({ env: baseEnv })) {
|
|
78
|
-
uiArgs.push('--clear');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (restart && uiRunning.state?.pid) {
|
|
82
|
-
const prevPid = Number(uiRunning.state.pid);
|
|
83
|
-
const res = await killProcessGroupOwnedByStack(prevPid, { stackName, envPath, label: 'expo-web', json: true });
|
|
84
|
-
if (!res.killed) {
|
|
85
|
-
// eslint-disable-next-line no-console
|
|
86
|
-
console.warn(
|
|
87
|
-
`[local] ui: not stopping existing Expo pid=${prevPid} because it does not look stack-owned.\n` +
|
|
88
|
-
`[local] ui: continuing by starting a new Expo process on a free port.`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// eslint-disable-next-line no-console
|
|
94
|
-
console.log(`[local] ui: starting Expo web (metro port=${metroPort})`);
|
|
95
|
-
const ui = await pmSpawnBin({ label: 'ui', dir: uiDir, bin: 'expo', args: uiArgs, env: uiEnv, options: spawnOptions });
|
|
96
|
-
children.push(ui);
|
|
97
|
-
|
|
98
|
-
if (stackMode && runtimeStatePath) {
|
|
99
|
-
await recordStackRuntimeUpdate(runtimeStatePath, {
|
|
100
|
-
processes: { expoWebPid: ui.pid },
|
|
101
|
-
expo: { webPort: metroPort },
|
|
102
|
-
}).catch(() => {});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
await writePidState(uiPaths.statePath, { pid: ui.pid, port: metroPort, uiDir, startedAt: new Date().toISOString() });
|
|
107
|
-
} catch {
|
|
108
|
-
// ignore
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { ok: true, skipped: false, pid: ui.pid, port: metroPort, proc: ui };
|
|
112
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
12
|
-
|
|
13
|
-
export function resolveLocalhostHost({ stackMode, stackName = getStackName() } = {}) {
|
|
14
|
-
if (!stackMode) return 'localhost';
|
|
15
|
-
if (!stackName || stackName === 'main') return 'localhost';
|
|
16
|
-
return `happy-${sanitizeDnsLabel(stackName)}.localhost`;
|
|
17
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export function resolveServerPortFromEnv({ env = process.env, defaultPort = 3005 } = {}) {
|
|
2
|
-
const raw =
|
|
3
|
-
(env.HAPPY_STACKS_SERVER_PORT ?? '').toString().trim() ||
|
|
4
|
-
(env.HAPPY_LOCAL_SERVER_PORT ?? '').toString().trim() ||
|
|
5
|
-
'';
|
|
6
|
-
const n = raw ? Number(raw) : Number(defaultPort);
|
|
7
|
-
return Number.isFinite(n) && n > 0 ? n : Number(defaultPort);
|
|
8
|
-
}
|
|
9
|
-
|