happy-stacks 0.3.0 → 0.5.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 +93 -40
- package/bin/happys.mjs +158 -16
- package/docs/codex-mcp-resume.md +130 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
- package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
- package/docs/happy-development.md +3 -4
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +5 -1
- package/scripts/auth.mjs +32 -10
- package/scripts/build.mjs +55 -8
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +198 -50
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +6 -4
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +8 -3
- package/scripts/install.mjs +207 -69
- package/scripts/lint.mjs +24 -4
- package/scripts/migrate.mjs +3 -12
- package/scripts/mobile.mjs +88 -104
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +908 -0
- package/scripts/review_pr.mjs +353 -0
- package/scripts/run.mjs +101 -21
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +189 -68
- package/scripts/setup_pr.mjs +586 -38
- package/scripts/stack.mjs +990 -196
- package/scripts/stack_archive_cmd.test.mjs +91 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
- package/scripts/stack_env_cmd.test.mjs +87 -0
- package/scripts/stack_happy_cmd.test.mjs +126 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
- package/scripts/stack_monorepo_defaults.test.mjs +62 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
- package/scripts/stack_shorthand_cmd.test.mjs +55 -0
- package/scripts/stack_wt_list.test.mjs +128 -0
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +395 -39
- package/scripts/typecheck.mjs +24 -4
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- 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/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/auth/login_ux.mjs +32 -13
- package/scripts/utils/auth/sources.mjs +26 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +43 -4
- package/scripts/utils/cli/cwd_scope.mjs +136 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +75 -0
- package/scripts/utils/cli/prereqs.test.mjs +34 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +17 -9
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
- package/scripts/utils/dev/daemon.mjs +61 -4
- package/scripts/utils/dev/expo_dev.mjs +430 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/dev/server.mjs +36 -42
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/edison/git_roots.mjs +29 -0
- package/scripts/utils/edison/git_roots.test.mjs +36 -0
- package/scripts/utils/env/env.mjs +7 -3
- package/scripts/utils/env/env_file.mjs +4 -2
- package/scripts/utils/env/env_file.test.mjs +44 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/expo/expo.mjs +20 -1
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/worktrees.mjs +80 -25
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- 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/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +9 -1
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +50 -3
- package/scripts/utils/paths/paths.mjs +159 -40
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
- package/scripts/utils/proc/commands.mjs +2 -3
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +176 -22
- package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
- package/scripts/utils/proc/proc.mjs +136 -4
- package/scripts/utils/proc/proc.test.mjs +77 -0
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +51 -0
- package/scripts/utils/review/findings.mjs +165 -0
- package/scripts/utils/review/findings.test.mjs +85 -0
- package/scripts/utils/review/head_slice.mjs +153 -0
- package/scripts/utils/review/head_slice.test.mjs +91 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/runners/coderabbit.mjs +61 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +61 -0
- package/scripts/utils/review/runners/codex.test.mjs +35 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +32 -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/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
- 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/prisma_import.mjs +37 -0
- package/scripts/utils/server/prisma_import.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +14 -0
- package/scripts/utils/server/ui_env.test.mjs +46 -0
- package/scripts/utils/server/urls.mjs +14 -4
- package/scripts/utils/server/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/service/autostart_darwin.mjs +42 -2
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +2 -2
- package/scripts/utils/stack/editor_workspace.mjs +6 -6
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +2 -1
- package/scripts/utils/stack/startup.mjs +120 -13
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
- package/scripts/utils/stack/stop.mjs +15 -4
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +755 -179
- package/scripts/worktrees_archive_cmd.test.mjs +245 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
- package/scripts/utils/dev/expo_web.mjs +0 -112
|
@@ -14,6 +14,18 @@ const PRIMARY_STORAGE_ROOT = join(homedir(), '.happy', 'stacks');
|
|
|
14
14
|
const LEGACY_STORAGE_ROOT = join(homedir(), '.happy', 'local');
|
|
15
15
|
const PRIMARY_HOME_DIR = join(homedir(), '.happy-stacks');
|
|
16
16
|
|
|
17
|
+
// Upstream monorepo layout (slopus/happy):
|
|
18
|
+
// - expo-app/ (Happy UI)
|
|
19
|
+
// - cli/ (happy-cli)
|
|
20
|
+
// - server/ (happy-server)
|
|
21
|
+
const HAPPY_MONOREPO_COMPONENT_SUBDIR = {
|
|
22
|
+
happy: 'expo-app',
|
|
23
|
+
'happy-cli': 'cli',
|
|
24
|
+
'happy-server': 'server',
|
|
25
|
+
// Server flavors share a single server package in the monorepo.
|
|
26
|
+
'happy-server-light': 'server',
|
|
27
|
+
};
|
|
28
|
+
|
|
17
29
|
export function getRootDir(importMetaUrl) {
|
|
18
30
|
return dirname(dirname(fileURLToPath(importMetaUrl)));
|
|
19
31
|
}
|
|
@@ -26,8 +38,8 @@ export function getHappyStacksHomeDir(env = process.env) {
|
|
|
26
38
|
return PRIMARY_HOME_DIR;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
export function getWorkspaceDir(cliRootDir = null) {
|
|
30
|
-
const fromEnv = (
|
|
41
|
+
export function getWorkspaceDir(cliRootDir = null, env = process.env) {
|
|
42
|
+
const fromEnv = (env.HAPPY_STACKS_WORKSPACE_DIR ?? '').trim();
|
|
31
43
|
if (fromEnv) {
|
|
32
44
|
return expandHome(fromEnv);
|
|
33
45
|
}
|
|
@@ -41,8 +53,8 @@ export function getWorkspaceDir(cliRootDir = null) {
|
|
|
41
53
|
return cliRootDir ? cliRootDir : defaultWorkspace;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
export function getComponentsDir(rootDir) {
|
|
45
|
-
const workspaceDir = getWorkspaceDir(rootDir);
|
|
56
|
+
export function getComponentsDir(rootDir, env = process.env) {
|
|
57
|
+
const workspaceDir = getWorkspaceDir(rootDir, env);
|
|
46
58
|
return join(workspaceDir, 'components');
|
|
47
59
|
}
|
|
48
60
|
|
|
@@ -50,46 +62,152 @@ export function componentDirEnvKey(name) {
|
|
|
50
62
|
return `HAPPY_STACKS_COMPONENT_DIR_${name.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
function normalizePathForEnv(rootDir, raw) {
|
|
65
|
+
function normalizePathForEnv(rootDir, raw, env = process.env) {
|
|
54
66
|
const trimmed = (raw ?? '').trim();
|
|
55
67
|
if (!trimmed) {
|
|
56
68
|
return '';
|
|
57
69
|
}
|
|
58
70
|
const expanded = expandHome(trimmed);
|
|
59
71
|
// If the path is relative, treat it as relative to the workspace root (default: repo root).
|
|
60
|
-
const workspaceDir = getWorkspaceDir(rootDir);
|
|
72
|
+
const workspaceDir = getWorkspaceDir(rootDir, env);
|
|
61
73
|
return expanded.startsWith('/') ? expanded : resolve(workspaceDir, expanded);
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
export function
|
|
76
|
+
export function isHappyMonorepoComponentName(name) {
|
|
77
|
+
return Object.prototype.hasOwnProperty.call(HAPPY_MONOREPO_COMPONENT_SUBDIR, String(name ?? '').trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function happyMonorepoSubdirForComponent(name) {
|
|
81
|
+
return HAPPY_MONOREPO_COMPONENT_SUBDIR[String(name ?? '').trim()] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isHappyMonorepoRoot(dir) {
|
|
85
|
+
const d = String(dir ?? '').trim();
|
|
86
|
+
if (!d) return false;
|
|
87
|
+
try {
|
|
88
|
+
return (
|
|
89
|
+
existsSync(join(d, 'expo-app', 'package.json')) &&
|
|
90
|
+
existsSync(join(d, 'cli', 'package.json')) &&
|
|
91
|
+
existsSync(join(d, 'server', 'package.json'))
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function coerceHappyMonorepoRootFromPath(path) {
|
|
99
|
+
const p = String(path ?? '').trim();
|
|
100
|
+
if (!p) return null;
|
|
101
|
+
let cur = resolve(p);
|
|
102
|
+
while (true) {
|
|
103
|
+
if (isHappyMonorepoRoot(cur)) return cur;
|
|
104
|
+
const parent = dirname(cur);
|
|
105
|
+
if (parent === cur) return null;
|
|
106
|
+
cur = parent;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveHappyMonorepoPackageDir({ monorepoRoot, component }) {
|
|
111
|
+
const sub = happyMonorepoSubdirForComponent(component);
|
|
112
|
+
if (!sub) return null;
|
|
113
|
+
return join(monorepoRoot, sub);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getComponentRepoDir(rootDir, name, env = process.env) {
|
|
117
|
+
const componentDir = getComponentDir(rootDir, name, env);
|
|
118
|
+
const n = String(name ?? '').trim();
|
|
119
|
+
if (isHappyMonorepoComponentName(n)) {
|
|
120
|
+
const root = coerceHappyMonorepoRootFromPath(componentDir);
|
|
121
|
+
if (root) return root;
|
|
122
|
+
}
|
|
123
|
+
return componentDir;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getComponentDir(rootDir, name, env = process.env) {
|
|
65
127
|
const stacksKey = componentDirEnvKey(name);
|
|
66
128
|
const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
|
|
67
|
-
const fromEnv = normalizePathForEnv(rootDir,
|
|
68
|
-
|
|
129
|
+
const fromEnv = normalizePathForEnv(rootDir, env[stacksKey] ?? env[legacyKey], env);
|
|
130
|
+
const n = String(name ?? '').trim();
|
|
131
|
+
|
|
132
|
+
// If the component is part of the happy monorepo, allow pointing the env var at either:
|
|
133
|
+
// - the monorepo root, OR
|
|
134
|
+
// - the package directory (expo-app/cli/server), OR
|
|
135
|
+
// - any path inside those (we normalize to the package dir).
|
|
136
|
+
if (fromEnv && isHappyMonorepoComponentName(n)) {
|
|
137
|
+
const root = coerceHappyMonorepoRootFromPath(fromEnv);
|
|
138
|
+
if (root) {
|
|
139
|
+
const pkg = resolveHappyMonorepoPackageDir({ monorepoRoot: root, component: n });
|
|
140
|
+
return pkg || fromEnv;
|
|
141
|
+
}
|
|
69
142
|
return fromEnv;
|
|
70
143
|
}
|
|
71
|
-
|
|
144
|
+
|
|
145
|
+
if (fromEnv) return fromEnv;
|
|
146
|
+
|
|
147
|
+
const componentsDir = getComponentsDir(rootDir, env);
|
|
148
|
+
const defaultDir = join(componentsDir, n);
|
|
149
|
+
|
|
150
|
+
// Unified server flavors:
|
|
151
|
+
// If happy-server-light isn't explicitly configured, allow it to reuse the happy-server checkout
|
|
152
|
+
// when that checkout contains the sqlite schema (new: prisma/sqlite/schema.prisma; legacy: prisma/schema.sqlite.prisma).
|
|
153
|
+
if (n === 'happy-server-light') {
|
|
154
|
+
const fullServerDir = getComponentDir(rootDir, 'happy-server', env);
|
|
155
|
+
try {
|
|
156
|
+
if (
|
|
157
|
+
fullServerDir &&
|
|
158
|
+
(existsSync(join(fullServerDir, 'prisma', 'sqlite', 'schema.prisma')) ||
|
|
159
|
+
existsSync(join(fullServerDir, 'prisma', 'schema.sqlite.prisma')))
|
|
160
|
+
) {
|
|
161
|
+
return fullServerDir;
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Monorepo default behavior:
|
|
169
|
+
// - If components/happy is a monorepo checkout, derive all monorepo component dirs from it.
|
|
170
|
+
// - This allows a single checkout at components/happy to satisfy happy, happy-cli, and happy-server.
|
|
171
|
+
if (isHappyMonorepoComponentName(n)) {
|
|
172
|
+
// If the defaultDir is itself a monorepo root (common for "happy"), map to its package dir.
|
|
173
|
+
if (existsSync(defaultDir) && isHappyMonorepoRoot(defaultDir)) {
|
|
174
|
+
return resolveHappyMonorepoPackageDir({ monorepoRoot: defaultDir, component: n }) || defaultDir;
|
|
175
|
+
}
|
|
176
|
+
// If the legacy defaultDir exists (multi-repo), keep it.
|
|
177
|
+
if (existsSync(defaultDir) && existsSync(join(defaultDir, 'package.json'))) {
|
|
178
|
+
return defaultDir;
|
|
179
|
+
}
|
|
180
|
+
// Fallback: derive from the monorepo root at components/happy if present.
|
|
181
|
+
const monorepoRoot = join(componentsDir, 'happy');
|
|
182
|
+
if (existsSync(monorepoRoot) && isHappyMonorepoRoot(monorepoRoot)) {
|
|
183
|
+
return resolveHappyMonorepoPackageDir({ monorepoRoot, component: n }) || defaultDir;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return defaultDir;
|
|
72
188
|
}
|
|
73
189
|
|
|
74
|
-
export function getStackName() {
|
|
75
|
-
const raw =
|
|
76
|
-
?
|
|
77
|
-
:
|
|
78
|
-
?
|
|
190
|
+
export function getStackName(env = process.env) {
|
|
191
|
+
const raw = env.HAPPY_STACKS_STACK?.trim()
|
|
192
|
+
? env.HAPPY_STACKS_STACK.trim()
|
|
193
|
+
: env.HAPPY_LOCAL_STACK?.trim()
|
|
194
|
+
? env.HAPPY_LOCAL_STACK.trim()
|
|
79
195
|
: '';
|
|
80
196
|
return raw || 'main';
|
|
81
197
|
}
|
|
82
198
|
|
|
83
|
-
export function getStackLabel(stackName =
|
|
84
|
-
|
|
199
|
+
export function getStackLabel(stackName = null, env = process.env) {
|
|
200
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
201
|
+
return name === 'main' ? PRIMARY_LABEL_BASE : `${PRIMARY_LABEL_BASE}.${name}`;
|
|
85
202
|
}
|
|
86
203
|
|
|
87
|
-
export function getLegacyStackLabel(stackName =
|
|
88
|
-
|
|
204
|
+
export function getLegacyStackLabel(stackName = null, env = process.env) {
|
|
205
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
206
|
+
return name === 'main' ? LEGACY_LABEL_BASE : `${LEGACY_LABEL_BASE}.${name}`;
|
|
89
207
|
}
|
|
90
208
|
|
|
91
|
-
export function getStacksStorageRoot() {
|
|
92
|
-
const fromEnv = (
|
|
209
|
+
export function getStacksStorageRoot(env = process.env) {
|
|
210
|
+
const fromEnv = (env.HAPPY_STACKS_STORAGE_DIR ?? env.HAPPY_LOCAL_STORAGE_DIR ?? '').trim();
|
|
93
211
|
if (fromEnv) {
|
|
94
212
|
return expandHome(fromEnv);
|
|
95
213
|
}
|
|
@@ -100,19 +218,20 @@ export function getLegacyStorageRoot() {
|
|
|
100
218
|
return LEGACY_STORAGE_ROOT;
|
|
101
219
|
}
|
|
102
220
|
|
|
103
|
-
export function resolveStackBaseDir(stackName =
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
221
|
+
export function resolveStackBaseDir(stackName = null, env = process.env) {
|
|
222
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
223
|
+
const preferredRoot = getStacksStorageRoot(env);
|
|
224
|
+
const newBase = join(preferredRoot, name);
|
|
225
|
+
const legacyBase = name === 'main' ? LEGACY_STORAGE_ROOT : join(LEGACY_STORAGE_ROOT, 'stacks', name);
|
|
107
226
|
const allowLegacy = !isSandboxed() || sandboxAllowsGlobalSideEffects();
|
|
108
227
|
|
|
109
228
|
// Prefer the new layout by default.
|
|
110
229
|
//
|
|
111
230
|
// For non-main stacks, keep legacy layout if the legacy env exists and the new env does not.
|
|
112
231
|
// This avoids breaking existing stacks until `happys stack migrate` is run.
|
|
113
|
-
if (allowLegacy &&
|
|
114
|
-
const newEnv = join(preferredRoot,
|
|
115
|
-
const legacyEnv = join(LEGACY_STORAGE_ROOT, 'stacks',
|
|
232
|
+
if (allowLegacy && name !== 'main') {
|
|
233
|
+
const newEnv = join(preferredRoot, name, 'env');
|
|
234
|
+
const legacyEnv = join(LEGACY_STORAGE_ROOT, 'stacks', name, 'env');
|
|
116
235
|
if (!existsSync(newEnv) && existsSync(legacyEnv)) {
|
|
117
236
|
return { baseDir: legacyBase, isLegacy: true };
|
|
118
237
|
}
|
|
@@ -121,30 +240,31 @@ export function resolveStackBaseDir(stackName = getStackName()) {
|
|
|
121
240
|
return { baseDir: newBase, isLegacy: false };
|
|
122
241
|
}
|
|
123
242
|
|
|
124
|
-
export function resolveStackEnvPath(stackName =
|
|
125
|
-
const
|
|
243
|
+
export function resolveStackEnvPath(stackName = null, env = process.env) {
|
|
244
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
245
|
+
const { baseDir: activeBase, isLegacy } = resolveStackBaseDir(name, env);
|
|
126
246
|
// New layout: ~/.happy/stacks/<name>/env
|
|
127
|
-
const newEnv = join(getStacksStorageRoot(),
|
|
247
|
+
const newEnv = join(getStacksStorageRoot(env), name, 'env');
|
|
128
248
|
// Legacy layout: ~/.happy/local/stacks/<name>/env
|
|
129
|
-
const legacyEnv = join(LEGACY_STORAGE_ROOT, 'stacks',
|
|
249
|
+
const legacyEnv = join(LEGACY_STORAGE_ROOT, 'stacks', name, 'env');
|
|
130
250
|
const allowLegacy = !isSandboxed() || sandboxAllowsGlobalSideEffects();
|
|
131
251
|
|
|
132
252
|
if (existsSync(newEnv)) {
|
|
133
|
-
return { envPath: newEnv, isLegacy: false, baseDir: join(getStacksStorageRoot(),
|
|
253
|
+
return { envPath: newEnv, isLegacy: false, baseDir: join(getStacksStorageRoot(env), name) };
|
|
134
254
|
}
|
|
135
255
|
if (allowLegacy && existsSync(legacyEnv)) {
|
|
136
|
-
return { envPath: legacyEnv, isLegacy: true, baseDir: join(LEGACY_STORAGE_ROOT, 'stacks',
|
|
256
|
+
return { envPath: legacyEnv, isLegacy: true, baseDir: join(LEGACY_STORAGE_ROOT, 'stacks', name) };
|
|
137
257
|
}
|
|
138
258
|
return { envPath: newEnv, isLegacy, baseDir: activeBase };
|
|
139
259
|
}
|
|
140
260
|
|
|
141
|
-
export function getDefaultAutostartPaths() {
|
|
142
|
-
const stackName = getStackName();
|
|
143
|
-
const { baseDir, isLegacy } = resolveStackBaseDir(stackName);
|
|
261
|
+
export function getDefaultAutostartPaths(env = process.env) {
|
|
262
|
+
const stackName = getStackName(env);
|
|
263
|
+
const { baseDir, isLegacy } = resolveStackBaseDir(stackName, env);
|
|
144
264
|
const logsDir = join(baseDir, 'logs');
|
|
145
265
|
|
|
146
|
-
const primaryLabel = getStackLabel(stackName);
|
|
147
|
-
const legacyLabel = getLegacyStackLabel(stackName);
|
|
266
|
+
const primaryLabel = getStackLabel(stackName, env);
|
|
267
|
+
const legacyLabel = getLegacyStackLabel(stackName, env);
|
|
148
268
|
const primaryPlistPath = join(homedir(), 'Library', 'LaunchAgents', `${primaryLabel}.plist`);
|
|
149
269
|
const legacyPlistPath = join(homedir(), 'Library', 'LaunchAgents', `${legacyLabel}.plist`);
|
|
150
270
|
|
|
@@ -187,4 +307,3 @@ export function getDefaultAutostartPaths() {
|
|
|
187
307
|
legacyStderrPath,
|
|
188
308
|
};
|
|
189
309
|
}
|
|
190
|
-
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getComponentDir, getComponentRepoDir } from './paths.mjs';
|
|
8
|
+
|
|
9
|
+
async function withTempRoot(t) {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-paths-monorepo-'));
|
|
11
|
+
t.after(async () => {
|
|
12
|
+
await rm(dir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function writeHappyMonorepoStub({ rootDir }) {
|
|
18
|
+
const monoRoot = join(rootDir, 'components', 'happy');
|
|
19
|
+
await mkdir(join(monoRoot, 'expo-app'), { recursive: true });
|
|
20
|
+
await mkdir(join(monoRoot, 'cli'), { recursive: true });
|
|
21
|
+
await mkdir(join(monoRoot, 'server'), { recursive: true });
|
|
22
|
+
await writeFile(join(monoRoot, 'expo-app', 'package.json'), '{}\n', 'utf-8');
|
|
23
|
+
await writeFile(join(monoRoot, 'cli', 'package.json'), '{}\n', 'utf-8');
|
|
24
|
+
await writeFile(join(monoRoot, 'server', 'package.json'), '{}\n', 'utf-8');
|
|
25
|
+
return monoRoot;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('getComponentDir derives monorepo component package dirs from components/happy', async (t) => {
|
|
29
|
+
const rootDir = await withTempRoot(t);
|
|
30
|
+
const env = { HAPPY_STACKS_WORKSPACE_DIR: rootDir };
|
|
31
|
+
|
|
32
|
+
const monoRoot = await writeHappyMonorepoStub({ rootDir });
|
|
33
|
+
assert.equal(getComponentDir(rootDir, 'happy', env), join(monoRoot, 'expo-app'));
|
|
34
|
+
assert.equal(getComponentDir(rootDir, 'happy-cli', env), join(monoRoot, 'cli'));
|
|
35
|
+
assert.equal(getComponentDir(rootDir, 'happy-server', env), join(monoRoot, 'server'));
|
|
36
|
+
assert.equal(getComponentDir(rootDir, 'happy-server-light', env), join(monoRoot, 'server'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('getComponentRepoDir returns the shared monorepo root for monorepo components', async (t) => {
|
|
40
|
+
const rootDir = await withTempRoot(t);
|
|
41
|
+
const env = { HAPPY_STACKS_WORKSPACE_DIR: rootDir };
|
|
42
|
+
|
|
43
|
+
const monoRoot = await writeHappyMonorepoStub({ rootDir });
|
|
44
|
+
assert.equal(getComponentRepoDir(rootDir, 'happy', env), monoRoot);
|
|
45
|
+
assert.equal(getComponentRepoDir(rootDir, 'happy-cli', env), monoRoot);
|
|
46
|
+
assert.equal(getComponentRepoDir(rootDir, 'happy-server', env), monoRoot);
|
|
47
|
+
assert.equal(getComponentRepoDir(rootDir, 'happy-server-light', env), monoRoot);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('getComponentDir normalizes monorepo env overrides that point inside the repo', async (t) => {
|
|
51
|
+
const rootDir = await withTempRoot(t);
|
|
52
|
+
const env = { HAPPY_STACKS_WORKSPACE_DIR: rootDir };
|
|
53
|
+
|
|
54
|
+
const monoRoot = await writeHappyMonorepoStub({ rootDir });
|
|
55
|
+
|
|
56
|
+
env.HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI = join(monoRoot, 'cli', 'src');
|
|
57
|
+
assert.equal(getComponentDir(rootDir, 'happy-cli', env), join(monoRoot, 'cli'));
|
|
58
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getComponentDir } from './paths.mjs';
|
|
8
|
+
|
|
9
|
+
test('getComponentDir prefers happy-server for happy-server-light when unified schema exists', async (t) => {
|
|
10
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'happy-stacks-paths-server-flavors-'));
|
|
11
|
+
t.after(async () => {
|
|
12
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const env = { HAPPY_STACKS_WORKSPACE_DIR: rootDir };
|
|
16
|
+
const fullDir = join(rootDir, 'components', 'happy-server');
|
|
17
|
+
await mkdir(join(fullDir, 'prisma', 'sqlite'), { recursive: true });
|
|
18
|
+
await writeFile(join(fullDir, 'prisma', 'sqlite', 'schema.prisma'), 'datasource db { provider = "sqlite" }\n', 'utf-8');
|
|
19
|
+
|
|
20
|
+
assert.equal(getComponentDir(rootDir, 'happy-server-light', env), fullDir);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('getComponentDir falls back to components/happy-server-light when unified schema is missing', async (t) => {
|
|
24
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'happy-stacks-paths-server-flavors-'));
|
|
25
|
+
t.after(async () => {
|
|
26
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const env = { HAPPY_STACKS_WORKSPACE_DIR: rootDir };
|
|
30
|
+
const expected = join(rootDir, 'components', 'happy-server-light');
|
|
31
|
+
assert.equal(getComponentDir(rootDir, 'happy-server-light', env), expected);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('getComponentDir does not alias when HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT is set', async (t) => {
|
|
35
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'happy-stacks-paths-server-flavors-'));
|
|
36
|
+
t.after(async () => {
|
|
37
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const env = {
|
|
41
|
+
HAPPY_STACKS_WORKSPACE_DIR: rootDir,
|
|
42
|
+
HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT: '/tmp/custom/server-light',
|
|
43
|
+
};
|
|
44
|
+
assert.equal(getComponentDir(rootDir, 'happy-server-light', env), '/tmp/custom/server-light');
|
|
45
|
+
});
|
|
@@ -28,7 +28,6 @@ export async function runCaptureIfCommandExists(cmd, args, { cwd, env, timeoutMs
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export async function commandExists(cmd, { cwd } = {}) {
|
|
32
|
-
return Boolean(await resolveCommandPath(cmd, { cwd }));
|
|
31
|
+
export async function commandExists(cmd, { cwd, env, timeoutMs } = {}) {
|
|
32
|
+
return Boolean(await resolveCommandPath(cmd, { cwd, env, timeoutMs }));
|
|
33
33
|
}
|
|
34
|
-
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export async function runWithConcurrencyLimit({ items, limit, fn }) {
|
|
2
|
+
const list = Array.isArray(items) ? items : [];
|
|
3
|
+
const max = Number(limit);
|
|
4
|
+
const concurrency = Number.isFinite(max) && max > 0 ? Math.floor(max) : 4;
|
|
5
|
+
|
|
6
|
+
const results = new Array(list.length);
|
|
7
|
+
let nextIndex = 0;
|
|
8
|
+
|
|
9
|
+
const worker = async () => {
|
|
10
|
+
while (true) {
|
|
11
|
+
const i = nextIndex;
|
|
12
|
+
nextIndex += 1;
|
|
13
|
+
if (i >= list.length) return;
|
|
14
|
+
results[i] = await fn(list[i], i);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const workers = [];
|
|
19
|
+
for (let i = 0; i < Math.min(concurrency, list.length); i++) {
|
|
20
|
+
workers.push(worker());
|
|
21
|
+
}
|
|
22
|
+
await Promise.all(workers);
|
|
23
|
+
return results;
|
|
24
|
+
}
|
|
25
|
+
|