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/lint.mjs
CHANGED
|
@@ -1,36 +1,16 @@
|
|
|
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 {
|
|
4
|
+
import { componentDirEnvKey, 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';
|
|
9
|
+
import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
|
|
10
10
|
|
|
11
11
|
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
12
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
13
|
function pickLintScript(scripts) {
|
|
33
|
-
if (!scripts) return null;
|
|
34
14
|
const candidates = [
|
|
35
15
|
'lint',
|
|
36
16
|
'lint:ci',
|
|
@@ -39,7 +19,7 @@ function pickLintScript(scripts) {
|
|
|
39
19
|
'eslint',
|
|
40
20
|
'eslint:check',
|
|
41
21
|
];
|
|
42
|
-
return
|
|
22
|
+
return pickFirstScript(scripts, candidates);
|
|
43
23
|
}
|
|
44
24
|
|
|
45
25
|
async function main() {
|
|
@@ -61,18 +41,37 @@ async function main() {
|
|
|
61
41
|
'examples:',
|
|
62
42
|
' happys lint',
|
|
63
43
|
' happys lint happy happy-cli',
|
|
44
|
+
'',
|
|
45
|
+
'note:',
|
|
46
|
+
' If run from inside a component checkout/worktree and no components are provided, defaults to that component.',
|
|
64
47
|
].join('\n'),
|
|
65
48
|
});
|
|
66
49
|
return;
|
|
67
50
|
}
|
|
68
51
|
|
|
52
|
+
const rootDir = getRootDir(import.meta.url);
|
|
53
|
+
|
|
69
54
|
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
70
|
-
const
|
|
55
|
+
const inferred =
|
|
56
|
+
positionals.length === 0
|
|
57
|
+
? inferComponentFromCwd({
|
|
58
|
+
rootDir,
|
|
59
|
+
invokedCwd: getInvokedCwd(process.env),
|
|
60
|
+
components: DEFAULT_COMPONENTS,
|
|
61
|
+
})
|
|
62
|
+
: null;
|
|
63
|
+
if (inferred) {
|
|
64
|
+
const stacksKey = componentDirEnvKey(inferred.component);
|
|
65
|
+
const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
|
|
66
|
+
if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
|
|
67
|
+
process.env[stacksKey] = inferred.repoDir;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const requested = positionals.length ? positionals : inferred ? [inferred.component] : ['all'];
|
|
71
72
|
const wantAll = requested.includes('all');
|
|
72
73
|
const components = wantAll ? DEFAULT_COMPONENTS : requested;
|
|
73
74
|
|
|
74
|
-
const rootDir = getRootDir(import.meta.url);
|
|
75
|
-
|
|
76
75
|
const results = [];
|
|
77
76
|
for (const component of components) {
|
|
78
77
|
if (!DEFAULT_COMPONENTS.includes(component)) {
|
|
@@ -86,7 +85,7 @@ async function main() {
|
|
|
86
85
|
continue;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
const scripts = await
|
|
88
|
+
const scripts = await readPackageJsonScripts(dir);
|
|
90
89
|
if (!scripts) {
|
|
91
90
|
results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
|
|
92
91
|
continue;
|
package/scripts/menubar.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
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
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
|
|
7
|
+
import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
|
|
8
8
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
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';
|
|
10
|
+
import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
|
|
11
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
12
|
+
import { normalizeProfile } from './utils/cli/normalize.mjs';
|
|
12
13
|
|
|
13
14
|
async function ensureSwiftbarAssets({ cliRootDir }) {
|
|
14
15
|
const homeDir = getHappyStacksHomeDir();
|
|
@@ -59,14 +60,6 @@ function removeSwiftbarPlugins({ patterns }) {
|
|
|
59
60
|
return out || null;
|
|
60
61
|
}
|
|
61
62
|
|
|
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
|
-
|
|
70
63
|
async function main() {
|
|
71
64
|
const rawArgv = process.argv.slice(2);
|
|
72
65
|
const argv = rawArgv[0] === 'menubar' ? rawArgv.slice(1) : rawArgv;
|
|
@@ -128,7 +121,7 @@ async function main() {
|
|
|
128
121
|
if (cmd === 'mode') {
|
|
129
122
|
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
130
123
|
const raw = positionals[1] ?? '';
|
|
131
|
-
const mode =
|
|
124
|
+
const mode = normalizeProfile(raw);
|
|
132
125
|
if (!mode) {
|
|
133
126
|
throw new Error('[menubar] usage: happys menubar mode <selfhost|dev> [--json]');
|
|
134
127
|
}
|
package/scripts/migrate.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
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
6
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
7
7
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { resolveStackEnvPath } from './utils/paths.mjs';
|
|
11
|
-
import { ensureDepsInstalled } from './utils/pm.mjs';
|
|
12
|
-
import { ensureHappyServerManagedInfra, applyHappyServerMigrations } from './utils/happy_server_infra.mjs';
|
|
13
|
-
import { runCapture } from './utils/proc.mjs';
|
|
14
|
-
import { pickNextFreeTcpPort } from './utils/ports.mjs';
|
|
8
|
+
import { ensureEnvFileUpdated } from './utils/env/env_file.mjs';
|
|
9
|
+
import { readEnvObjectFromFile } from './utils/env/read.mjs';
|
|
10
|
+
import { resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
11
|
+
import { ensureDepsInstalled } from './utils/proc/pm.mjs';
|
|
12
|
+
import { ensureHappyServerManagedInfra, applyHappyServerMigrations } from './utils/server/infra/happy_server_infra.mjs';
|
|
13
|
+
import { runCapture } from './utils/proc/proc.mjs';
|
|
14
|
+
import { pickNextFreeTcpPort } from './utils/net/ports.mjs';
|
|
15
|
+
import { getEnvValue } from './utils/env/values.mjs';
|
|
15
16
|
|
|
16
17
|
function usage() {
|
|
17
18
|
return [
|
|
@@ -25,18 +26,7 @@ function usage() {
|
|
|
25
26
|
].join('\n');
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const raw = await readFile(envPath, 'utf-8');
|
|
31
|
-
return Object.fromEntries(parseDotenv(raw).entries());
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getEnvValue(env, key) {
|
|
38
|
-
return (env?.[key] ?? '').toString().trim();
|
|
39
|
-
}
|
|
29
|
+
const readEnvObject = readEnvObjectFromFile;
|
|
40
30
|
|
|
41
31
|
function parseFileDatabaseUrl(url) {
|
|
42
32
|
const raw = String(url ?? '').trim();
|
|
@@ -290,7 +280,7 @@ async function main() {
|
|
|
290
280
|
throw new Error('[migrate] --to-stack is required');
|
|
291
281
|
}
|
|
292
282
|
|
|
293
|
-
const rootDir = (await import('./utils/paths.mjs')).getRootDir(import.meta.url);
|
|
283
|
+
const rootDir = (await import('./utils/paths/paths.mjs')).getRootDir(import.meta.url);
|
|
294
284
|
await migrateLightToServer({ rootDir, fromStack, toStack, includeFiles, force, json });
|
|
295
285
|
}
|
|
296
286
|
|
package/scripts/mobile.mjs
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/pm.mjs';
|
|
3
|
+
import { run, runCapture, spawnProc } from './utils/proc/proc.mjs';
|
|
4
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { ensureDepsInstalled, pmExecBin, pmSpawnBin, requireDir } from './utils/proc/pm.mjs';
|
|
7
6
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
|
-
import { ensureExpoIsolationEnv, getExpoStatePaths
|
|
9
|
-
import {
|
|
7
|
+
import { ensureExpoIsolationEnv, getExpoStatePaths } from './utils/expo/expo.mjs';
|
|
8
|
+
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
|
|
9
|
+
import { resolveMobileExpoConfig } from './utils/mobile/config.mjs';
|
|
10
|
+
import { resolveStackContext } from './utils/stack/context.mjs';
|
|
11
|
+
import { expoExec } from './utils/expo/command.mjs';
|
|
12
|
+
import { ensureDevExpoServer } from './utils/dev/expo_dev.mjs';
|
|
13
|
+
import { resolveMobileReachableServerUrl } from './utils/server/mobile_api_url.mjs';
|
|
14
|
+
import { patchIosXcodeProjectsForSigningAndIdentity, resolveIosAppXcodeProjects } from './utils/mobile/ios_xcodeproj_patch.mjs';
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Mobile dev helper for the embedded `components/happy` Expo app.
|
|
@@ -58,7 +63,7 @@ async function main() {
|
|
|
58
63
|
'',
|
|
59
64
|
'Notes:',
|
|
60
65
|
'- This script is designed to avoid editing upstream `components/happy` config in-place.',
|
|
61
|
-
'-
|
|
66
|
+
'- If you explicitly set HAPPY_STACKS_SERVER_URL (legacy: HAPPY_LOCAL_SERVER_URL), it bakes that URL into the app via EXPO_PUBLIC_HAPPY_SERVER_URL.',
|
|
62
67
|
].join('\n'),
|
|
63
68
|
});
|
|
64
69
|
return;
|
|
@@ -69,19 +74,6 @@ async function main() {
|
|
|
69
74
|
await requireDir('happy', uiDir);
|
|
70
75
|
await ensureDepsInstalled(uiDir, 'happy');
|
|
71
76
|
|
|
72
|
-
const sanitizeBundleIdSegment = (s) =>
|
|
73
|
-
(s ?? '')
|
|
74
|
-
.toString()
|
|
75
|
-
.trim()
|
|
76
|
-
.toLowerCase()
|
|
77
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
78
|
-
.replace(/^-+|-+$/g, '') || 'user';
|
|
79
|
-
|
|
80
|
-
const defaultLocalBundleId = (() => {
|
|
81
|
-
const user = sanitizeBundleIdSegment(process.env.USER ?? process.env.USERNAME ?? 'user');
|
|
82
|
-
return `com.happy.local.${user}.dev`;
|
|
83
|
-
})();
|
|
84
|
-
|
|
85
77
|
async function readXcdeviceList() {
|
|
86
78
|
if (process.platform !== 'darwin') {
|
|
87
79
|
return [];
|
|
@@ -96,21 +88,6 @@ async function main() {
|
|
|
96
88
|
// Default to the existing dev bundle identifier, which is also registered as a URL scheme
|
|
97
89
|
// (Info.plist includes `com.slopus.happy.dev`), so iOS will open the dev build instead of the App Store app.
|
|
98
90
|
const appEnv = process.env.APP_ENV ?? kv.get('--app-env') ?? 'development';
|
|
99
|
-
const iosAppName =
|
|
100
|
-
kv.get('--ios-app-name') ??
|
|
101
|
-
process.env.HAPPY_STACKS_IOS_APP_NAME ??
|
|
102
|
-
process.env.HAPPY_LOCAL_IOS_APP_NAME ??
|
|
103
|
-
'';
|
|
104
|
-
const iosBundleId =
|
|
105
|
-
kv.get('--ios-bundle-id') ??
|
|
106
|
-
process.env.HAPPY_STACKS_IOS_BUNDLE_ID ??
|
|
107
|
-
process.env.HAPPY_LOCAL_IOS_BUNDLE_ID ??
|
|
108
|
-
defaultLocalBundleId;
|
|
109
|
-
const scheme =
|
|
110
|
-
kv.get('--scheme') ??
|
|
111
|
-
process.env.HAPPY_STACKS_MOBILE_SCHEME ??
|
|
112
|
-
process.env.HAPPY_LOCAL_MOBILE_SCHEME ??
|
|
113
|
-
iosBundleId;
|
|
114
91
|
const host = kv.get('--host') ?? process.env.HAPPY_STACKS_MOBILE_HOST ?? process.env.HAPPY_LOCAL_MOBILE_HOST ?? 'lan';
|
|
115
92
|
const portRaw = kv.get('--port') ?? process.env.HAPPY_STACKS_MOBILE_PORT ?? process.env.HAPPY_LOCAL_MOBILE_PORT ?? '8081';
|
|
116
93
|
// Default behavior:
|
|
@@ -125,26 +102,56 @@ async function main() {
|
|
|
125
102
|
APP_ENV: appEnv,
|
|
126
103
|
};
|
|
127
104
|
|
|
105
|
+
const cfgBase = resolveMobileExpoConfig({ env });
|
|
106
|
+
const iosAppName = (kv.get('--ios-app-name') ?? cfgBase.iosAppName ?? '').toString();
|
|
107
|
+
const iosBundleId = (kv.get('--ios-bundle-id') ?? cfgBase.iosBundleId ?? '').toString();
|
|
108
|
+
const scheme = (kv.get('--scheme') ?? cfgBase.scheme ?? iosBundleId).toString();
|
|
109
|
+
|
|
128
110
|
const autostart = getDefaultAutostartPaths();
|
|
129
|
-
const
|
|
111
|
+
const stackCtx = resolveStackContext({ env, autostart });
|
|
112
|
+
const { stackMode, runtimeStatePath, stackName, envPath } = stackCtx;
|
|
113
|
+
|
|
114
|
+
// Ensure the built iOS app registers the same scheme we use for dev-client QR links.
|
|
115
|
+
// (Happy app reads EXPO_APP_SCHEME in app.config.js; default remains unchanged when unset.)
|
|
116
|
+
env.EXPO_APP_SCHEME = scheme;
|
|
117
|
+
// Ensure the app display name + bundle id are consistent with what we install.
|
|
118
|
+
// (app.config.js keeps upstream defaults unless these are explicitly set.)
|
|
119
|
+
if (iosAppName && iosAppName.trim()) {
|
|
120
|
+
env.EXPO_APP_NAME = iosAppName.trim();
|
|
121
|
+
}
|
|
122
|
+
if (iosBundleId && iosBundleId.trim()) {
|
|
123
|
+
env.EXPO_APP_BUNDLE_ID = iosBundleId.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Always isolate Expo home + TMPDIR to avoid cross-worktree cache pollution (and to keep sandbox runs contained).
|
|
127
|
+
const expoPaths = getExpoStatePaths({
|
|
130
128
|
baseDir: autostart.baseDir,
|
|
131
|
-
kind: '
|
|
129
|
+
kind: 'expo-dev',
|
|
132
130
|
projectDir: uiDir,
|
|
133
|
-
stateFileName: '
|
|
131
|
+
stateFileName: 'expo.state.json',
|
|
134
132
|
});
|
|
135
133
|
await ensureExpoIsolationEnv({
|
|
136
134
|
env,
|
|
137
|
-
stateDir:
|
|
138
|
-
expoHomeDir:
|
|
139
|
-
tmpDir:
|
|
135
|
+
stateDir: expoPaths.stateDir,
|
|
136
|
+
expoHomeDir: expoPaths.expoHomeDir,
|
|
137
|
+
tmpDir: expoPaths.tmpDir,
|
|
140
138
|
});
|
|
141
139
|
|
|
142
140
|
// Allow happy-stacks to define the default server URL baked into the app bundle.
|
|
143
141
|
// This is read by the app via `process.env.EXPO_PUBLIC_HAPPY_SERVER_URL`.
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
const serverPort = resolveServerPortFromEnv({ env, defaultPort: 3005 });
|
|
143
|
+
const allowEnableTailscale =
|
|
144
|
+
!stackMode || stackName === 'main' || (env.HAPPY_STACKS_TAILSCALE_SERVE ?? env.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0').toString().trim() === '1';
|
|
145
|
+
const resolvedUrls = await resolveServerUrls({ env, serverPort, allowEnable: allowEnableTailscale });
|
|
146
|
+
if (resolvedUrls.publicServerUrl && !env.EXPO_PUBLIC_HAPPY_SERVER_URL) {
|
|
147
|
+
env.EXPO_PUBLIC_HAPPY_SERVER_URL = resolvedUrls.publicServerUrl;
|
|
148
|
+
}
|
|
149
|
+
if (env.EXPO_PUBLIC_HAPPY_SERVER_URL) {
|
|
150
|
+
env.EXPO_PUBLIC_HAPPY_SERVER_URL = resolveMobileReachableServerUrl({
|
|
151
|
+
env,
|
|
152
|
+
serverUrl: env.EXPO_PUBLIC_HAPPY_SERVER_URL,
|
|
153
|
+
serverPort,
|
|
154
|
+
});
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
if (json) {
|
|
@@ -178,13 +185,12 @@ async function main() {
|
|
|
178
185
|
if (shouldClean) {
|
|
179
186
|
prebuildArgs.push('--clean');
|
|
180
187
|
}
|
|
181
|
-
await
|
|
188
|
+
await expoExec({ dir: uiDir, args: prebuildArgs, env, ensureDepsLabel: 'happy' });
|
|
182
189
|
|
|
183
190
|
// Always patch iOS props if iOS was generated.
|
|
184
191
|
if (platform === 'ios' || platform === 'all') {
|
|
185
192
|
const fs = await import('node:fs/promises');
|
|
186
193
|
const podPropsPath = `${uiDir}/ios/Podfile.properties.json`;
|
|
187
|
-
const pbxprojPath = `${uiDir}/ios/Happydev.xcodeproj/project.pbxproj`;
|
|
188
194
|
try {
|
|
189
195
|
const raw = await fs.readFile(podPropsPath, 'utf-8');
|
|
190
196
|
const json = JSON.parse(raw);
|
|
@@ -195,14 +201,17 @@ async function main() {
|
|
|
195
201
|
// ignore if path missing (platform != ios)
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
const iosProjects = await resolveIosAppXcodeProjects({ uiDir });
|
|
205
|
+
for (const project of iosProjects) {
|
|
206
|
+
try {
|
|
207
|
+
const raw = await fs.readFile(project.pbxprojPath, 'utf-8');
|
|
208
|
+
const next = raw.replaceAll('IPHONEOS_DEPLOYMENT_TARGET = 15.1;', 'IPHONEOS_DEPLOYMENT_TARGET = 16.0;');
|
|
209
|
+
if (next !== raw) {
|
|
210
|
+
await fs.writeFile(project.pbxprojPath, next, 'utf-8');
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// ignore missing/invalid pbxproj; Expo will surface actionable errors if needed
|
|
203
214
|
}
|
|
204
|
-
} catch {
|
|
205
|
-
// ignore missing pbxproj (unexpected)
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
// Ensure CocoaPods doesn't crash due to locale issues.
|
|
@@ -263,25 +272,9 @@ async function main() {
|
|
|
263
272
|
// xcodebuild fails with:
|
|
264
273
|
// "Automatic signing is disabled ... pass -allowProvisioningUpdates"
|
|
265
274
|
//
|
|
266
|
-
// We force Expo CLI to go through its signing configuration path by clearing
|
|
267
|
-
// so it will re-set the team and include the provisioning flags.
|
|
268
|
-
|
|
269
|
-
const fs = await import('node:fs/promises');
|
|
270
|
-
const pbxprojPath = `${uiDir}/ios/Happydev.xcodeproj/project.pbxproj`;
|
|
271
|
-
const raw = await fs.readFile(pbxprojPath, 'utf-8');
|
|
272
|
-
let next = raw.replaceAll(/^\s*DEVELOPMENT_TEAM = ".*";\s*$/gm, '');
|
|
273
|
-
next = next.replaceAll(/PRODUCT_BUNDLE_IDENTIFIER = [^;]+;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${iosBundleId};`);
|
|
274
|
-
if (iosAppName && iosAppName.trim()) {
|
|
275
|
-
const name = iosAppName.trim();
|
|
276
|
-
const quoted = name.includes(' ') || name.includes('"') ? `"${name.replaceAll('"', '\\"')}"` : name;
|
|
277
|
-
next = next.replaceAll(/PRODUCT_NAME = [^;]+;/g, `PRODUCT_NAME = ${quoted};`);
|
|
278
|
-
}
|
|
279
|
-
if (next !== raw) {
|
|
280
|
-
await fs.writeFile(pbxprojPath, next, 'utf-8');
|
|
281
|
-
}
|
|
282
|
-
} catch {
|
|
283
|
-
// ignore
|
|
284
|
-
}
|
|
275
|
+
// We force Expo CLI to go through its signing configuration path by clearing any pre-existing
|
|
276
|
+
// team/profile identifiers, so it will re-set the team and include the provisioning flags.
|
|
277
|
+
await patchIosXcodeProjectsForSigningAndIdentity({ uiDir, iosBundleId, iosAppName });
|
|
285
278
|
}
|
|
286
279
|
|
|
287
280
|
const configuration = kv.get('--configuration') ?? 'Debug';
|
|
@@ -292,47 +285,39 @@ async function main() {
|
|
|
292
285
|
// Ensure CocoaPods doesn't crash due to locale issues.
|
|
293
286
|
env.LANG = env.LANG ?? 'en_US.UTF-8';
|
|
294
287
|
env.LC_ALL = env.LC_ALL ?? 'en_US.UTF-8';
|
|
295
|
-
await
|
|
288
|
+
await expoExec({ dir: uiDir, args, env, ensureDepsLabel: 'happy' });
|
|
296
289
|
}
|
|
297
290
|
|
|
298
291
|
if (!shouldStartMetro) {
|
|
299
292
|
return;
|
|
300
293
|
}
|
|
301
294
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const requestedPort = Number.parseInt(String(portRaw), 10);
|
|
323
|
-
const startPort = Number.isFinite(requestedPort) && requestedPort > 0 ? requestedPort : 8081;
|
|
324
|
-
const portNumber = await pickNextFreeTcpPort(startPort);
|
|
325
|
-
env.RCT_METRO_PORT = String(portNumber);
|
|
295
|
+
// Unify Expo: one Expo dev server per stack/worktree. If dev mode already started Expo, we reuse it.
|
|
296
|
+
// If Expo is already running without dev-client enabled, we fail closed (no second Expo).
|
|
297
|
+
env.HAPPY_STACKS_EXPO_HOST = host;
|
|
298
|
+
env.HAPPY_LOCAL_EXPO_HOST = host;
|
|
299
|
+
env.HAPPY_STACKS_MOBILE_HOST = host;
|
|
300
|
+
env.HAPPY_LOCAL_MOBILE_HOST = host;
|
|
301
|
+
env.HAPPY_STACKS_MOBILE_SCHEME = scheme;
|
|
302
|
+
env.HAPPY_LOCAL_MOBILE_SCHEME = scheme;
|
|
303
|
+
env.HAPPY_STACKS_EXPO_DEV_PORT = String(portRaw);
|
|
304
|
+
env.HAPPY_LOCAL_EXPO_DEV_PORT = String(portRaw);
|
|
326
305
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
306
|
+
const children = [];
|
|
307
|
+
await ensureDevExpoServer({
|
|
308
|
+
startUi: false,
|
|
309
|
+
startMobile: true,
|
|
310
|
+
uiDir,
|
|
311
|
+
autostart,
|
|
312
|
+
baseEnv: env,
|
|
313
|
+
apiServerUrl: env.EXPO_PUBLIC_HAPPY_SERVER_URL ?? '',
|
|
314
|
+
restart,
|
|
315
|
+
stackMode,
|
|
316
|
+
runtimeStatePath,
|
|
317
|
+
stackName,
|
|
318
|
+
envPath,
|
|
319
|
+
children,
|
|
320
|
+
});
|
|
336
321
|
|
|
337
322
|
await new Promise(() => {});
|
|
338
323
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
+
import { run } from './utils/proc/proc.mjs';
|
|
5
|
+
import { getRootDir } from './utils/paths/paths.mjs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { defaultDevClientIdentity } from './utils/mobile/identifiers.mjs';
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const argv = process.argv.slice(2);
|
|
12
|
+
const { flags, kv } = parseArgs(argv);
|
|
13
|
+
const json = wantsJson(argv, { flags });
|
|
14
|
+
|
|
15
|
+
if (wantsHelp(argv, { flags }) || flags.has('--help') || argv.length === 0) {
|
|
16
|
+
printResult({
|
|
17
|
+
json,
|
|
18
|
+
data: {
|
|
19
|
+
flags: ['--device=<id-or-name>', '--clean', '--configuration=Debug|Release', '--json'],
|
|
20
|
+
},
|
|
21
|
+
text: [
|
|
22
|
+
'[mobile-dev-client] usage:',
|
|
23
|
+
' happys mobile-dev-client --install [--device=...] [--clean] [--configuration=Debug|Release] [--json]',
|
|
24
|
+
'',
|
|
25
|
+
'Notes:',
|
|
26
|
+
'- Installs a dedicated "Happy Stacks Dev" Expo dev-client app on your iPhone.',
|
|
27
|
+
'- This app is intended to be reused across stacks (no per-stack installs for dev-client).',
|
|
28
|
+
].join('\n'),
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!flags.has('--install')) {
|
|
34
|
+
printResult({
|
|
35
|
+
json,
|
|
36
|
+
data: { ok: false, error: 'missing_install_flag' },
|
|
37
|
+
text: '[mobile-dev-client] missing --install. Run: happys mobile-dev-client --help',
|
|
38
|
+
});
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rootDir = getRootDir(import.meta.url);
|
|
43
|
+
const mobileScript = join(rootDir, 'scripts', 'mobile.mjs');
|
|
44
|
+
|
|
45
|
+
const device = kv.get('--device') ?? '';
|
|
46
|
+
const clean = flags.has('--clean');
|
|
47
|
+
const configuration = kv.get('--configuration') ?? 'Debug';
|
|
48
|
+
|
|
49
|
+
const id = defaultDevClientIdentity({ user: process.env.USER ?? process.env.USERNAME ?? 'user' });
|
|
50
|
+
|
|
51
|
+
const args = [
|
|
52
|
+
mobileScript,
|
|
53
|
+
'--app-env=development',
|
|
54
|
+
`--ios-app-name=${id.iosAppName}`,
|
|
55
|
+
`--ios-bundle-id=${id.iosBundleId}`,
|
|
56
|
+
`--scheme=${id.scheme}`,
|
|
57
|
+
'--prebuild',
|
|
58
|
+
...(clean ? ['--clean'] : []),
|
|
59
|
+
'--run-ios',
|
|
60
|
+
`--configuration=${configuration}`,
|
|
61
|
+
'--no-metro',
|
|
62
|
+
...(device ? [`--device=${device}`] : []),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const env = {
|
|
66
|
+
...process.env,
|
|
67
|
+
// Ensure Expo app config uses the dev-client scheme.
|
|
68
|
+
EXPO_APP_SCHEME: id.scheme,
|
|
69
|
+
// Ensure per-stack storage isolation is available during dev-client usage.
|
|
70
|
+
EXPO_PUBLIC_HAPPY_STORAGE_SCOPE: process.env.EXPO_PUBLIC_HAPPY_STORAGE_SCOPE ?? '',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const out = await run(process.execPath, args, { cwd: rootDir, env });
|
|
74
|
+
if (json) {
|
|
75
|
+
printResult({ json, data: { ok: true, installed: true, identity: id, out } });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main().catch((err) => {
|
|
80
|
+
console.error('[mobile-dev-client] failed:', err);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Provision a fresh Ubuntu VM for running happy-local's `review-pr` end-to-end.
|
|
5
|
+
# Intended for Apple Silicon users running Ubuntu ARM64 via Lima/UTM.
|
|
6
|
+
#
|
|
7
|
+
# This installs:
|
|
8
|
+
# - Node (via nvm)
|
|
9
|
+
# - corepack (yarn/pnpm shims)
|
|
10
|
+
# - basic build tooling for native deps used by Expo/React Native ecosystem
|
|
11
|
+
|
|
12
|
+
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
13
|
+
echo "[provision] expected Linux; got: $(uname -s)" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
18
|
+
|
|
19
|
+
echo "[provision] installing apt dependencies..."
|
|
20
|
+
sudo apt-get update -y
|
|
21
|
+
sudo apt-get install -y \
|
|
22
|
+
ca-certificates \
|
|
23
|
+
curl \
|
|
24
|
+
git \
|
|
25
|
+
build-essential \
|
|
26
|
+
python3 \
|
|
27
|
+
pkg-config
|
|
28
|
+
|
|
29
|
+
echo "[provision] installing nvm + Node..."
|
|
30
|
+
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
31
|
+
if [[ ! -s "$NVM_DIR/nvm.sh" ]]; then
|
|
32
|
+
mkdir -p "$NVM_DIR"
|
|
33
|
+
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# shellcheck disable=SC1090
|
|
37
|
+
source "$NVM_DIR/nvm.sh"
|
|
38
|
+
|
|
39
|
+
# Use a modern Node; match the repo's expectations if it ever adds .nvmrc.
|
|
40
|
+
NODE_VERSION="${NODE_VERSION:-22}"
|
|
41
|
+
nvm install "$NODE_VERSION"
|
|
42
|
+
nvm use "$NODE_VERSION"
|
|
43
|
+
|
|
44
|
+
echo "[provision] enabling corepack..."
|
|
45
|
+
corepack enable >/dev/null 2>&1 || true
|
|
46
|
+
|
|
47
|
+
echo "[provision] done."
|
|
48
|
+
echo "[provision] Node: $(node --version)"
|
|
49
|
+
echo "[provision] npm: $(npm --version)"
|
|
50
|
+
echo "[provision] git: $(git --version)"
|
|
51
|
+
|