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
package/scripts/tui.mjs
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import './utils/env/env.mjs';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
4
|
import { join, resolve, sep } from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { printResult } from './utils/cli/cli.mjs';
|
|
6
7
|
import { readEnvObjectFromFile } from './utils/env/read.mjs';
|
|
7
|
-
import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
8
|
+
import { getComponentsDir, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
8
9
|
import { getStackRuntimeStatePath, readStackRuntimeStateFile } from './utils/stack/runtime_state.mjs';
|
|
9
10
|
import { getEnvValueAny } from './utils/env/values.mjs';
|
|
10
11
|
import { padRight, parsePrefixedLabel, stripAnsi } from './utils/ui/text.mjs';
|
|
12
|
+
import { commandExists } from './utils/proc/commands.mjs';
|
|
13
|
+
import { renderQrAscii } from './utils/ui/qr.mjs';
|
|
14
|
+
import { resolveMobileQrPayload } from './utils/mobile/dev_client_links.mjs';
|
|
11
15
|
|
|
12
16
|
function nowTs() {
|
|
13
17
|
const d = new Date();
|
|
14
18
|
return d.toISOString().slice(11, 19);
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
function supportsAnsi() {
|
|
22
|
+
if (!process.stdout.isTTY) return false;
|
|
23
|
+
if (process.env.NO_COLOR) return false;
|
|
24
|
+
if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cyan(s) {
|
|
29
|
+
return supportsAnsi() ? `\x1b[36m${s}\x1b[0m` : String(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
function clamp(n, lo, hi) {
|
|
18
33
|
return Math.max(lo, Math.min(hi, n));
|
|
19
34
|
}
|
|
@@ -29,7 +44,13 @@ function pushLine(pane, line, { maxLines = 4000 } = {}) {
|
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
function
|
|
47
|
+
function getPaneHeightForLines(lines, { min = 3, max = 16 } = {}) {
|
|
48
|
+
const n = Array.isArray(lines) ? lines.length : 0;
|
|
49
|
+
// +2 for box borders
|
|
50
|
+
return clamp(n + 2, min, max);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function drawBox({ x, y, w, h, title, lines, scroll, active = false }) {
|
|
33
54
|
const top = y;
|
|
34
55
|
const bottom = y + h - 1;
|
|
35
56
|
const left = x;
|
|
@@ -53,12 +74,14 @@ function drawBox({ x, y, w, h, title, lines, scroll }) {
|
|
|
53
74
|
const midLine = '│' + ' '.repeat(Math.max(0, w - 2)) + '│';
|
|
54
75
|
const botLine = '└' + horiz + '┘';
|
|
55
76
|
|
|
77
|
+
const style = (s) => (active ? cyan(s) : s);
|
|
78
|
+
|
|
56
79
|
const out = [];
|
|
57
|
-
out.push({ row: top, col: left, text: topLine });
|
|
80
|
+
out.push({ row: top, col: left, text: style(topLine) });
|
|
58
81
|
for (let r = top + 1; r < bottom; r++) {
|
|
59
|
-
out.push({ row: r, col: left, text: midLine });
|
|
82
|
+
out.push({ row: r, col: left, text: style(midLine) });
|
|
60
83
|
}
|
|
61
|
-
out.push({ row: bottom, col: left, text: botLine });
|
|
84
|
+
out.push({ row: bottom, col: left, text: style(botLine) });
|
|
62
85
|
|
|
63
86
|
const innerW = Math.max(0, w - 2);
|
|
64
87
|
const innerH = Math.max(0, h - 2);
|
|
@@ -93,13 +116,104 @@ function inferStackNameFromForwardedArgs(args) {
|
|
|
93
116
|
|
|
94
117
|
const readEnvObject = readEnvObjectFromFile;
|
|
95
118
|
|
|
119
|
+
async function preflightCorepackYarnForStack({ envPath }) {
|
|
120
|
+
// Corepack caches (and therefore "download yarn?" prompts) are tied to XDG/HOME.
|
|
121
|
+
// In stack mode we isolate HOME/XDG caches per stack, which can cause Corepack to prompt
|
|
122
|
+
// the first time a stack runs Yarn.
|
|
123
|
+
//
|
|
124
|
+
// In `happys tui`, the child runs under a pseudo-TTY (via `script`) and the TUI consumes
|
|
125
|
+
// all keyboard input, so Corepack's interactive prompt deadlocks.
|
|
126
|
+
//
|
|
127
|
+
// Fix: pre-download Yarn in a *non-tty* subprocess using the stack's isolated HOME/XDG,
|
|
128
|
+
// so later pty runs don't prompt.
|
|
129
|
+
if (!envPath) return;
|
|
130
|
+
const baseDir = resolve(join(envPath, '..'));
|
|
131
|
+
const stackHome = join(baseDir, 'home');
|
|
132
|
+
const cacheBase = join(baseDir, 'cache');
|
|
133
|
+
const env = {
|
|
134
|
+
...process.env,
|
|
135
|
+
HOME: stackHome,
|
|
136
|
+
USERPROFILE: stackHome,
|
|
137
|
+
XDG_CACHE_HOME: join(cacheBase, 'xdg'),
|
|
138
|
+
YARN_CACHE_FOLDER: join(cacheBase, 'yarn'),
|
|
139
|
+
npm_config_cache: join(cacheBase, 'npm'),
|
|
140
|
+
// Avoid Corepack mutating package.json automatically.
|
|
141
|
+
COREPACK_ENABLE_AUTO_PIN: '0',
|
|
142
|
+
// Best-effort: disable download prompts (may not be honored by all Corepack versions).
|
|
143
|
+
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0',
|
|
144
|
+
// Treat this as non-interactive (helps some tooling).
|
|
145
|
+
CI: process.env.CI ?? '1',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await mkdir(stackHome, { recursive: true }).catch(() => {});
|
|
149
|
+
await mkdir(env.XDG_CACHE_HOME, { recursive: true }).catch(() => {});
|
|
150
|
+
await mkdir(env.YARN_CACHE_FOLDER, { recursive: true }).catch(() => {});
|
|
151
|
+
await mkdir(env.npm_config_cache, { recursive: true }).catch(() => {});
|
|
152
|
+
await mkdir(env.COREPACK_HOME, { recursive: true }).catch(() => {});
|
|
153
|
+
|
|
154
|
+
await new Promise((resolvePromise) => {
|
|
155
|
+
const proc = spawn('yarn', ['--version'], {
|
|
156
|
+
env,
|
|
157
|
+
cwd: baseDir,
|
|
158
|
+
// Non-tty stdio: Corepack typically won't prompt; if it does, we still provide "y\n".
|
|
159
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
160
|
+
shell: false,
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
proc.stdin?.write('y\n');
|
|
164
|
+
proc.stdin?.end();
|
|
165
|
+
} catch {
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const t = setTimeout(() => {
|
|
170
|
+
try {
|
|
171
|
+
proc.kill('SIGKILL');
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
resolvePromise();
|
|
176
|
+
}, 60_000);
|
|
177
|
+
|
|
178
|
+
proc.on('exit', () => {
|
|
179
|
+
clearTimeout(t);
|
|
180
|
+
resolvePromise();
|
|
181
|
+
});
|
|
182
|
+
proc.on('error', () => {
|
|
183
|
+
clearTimeout(t);
|
|
184
|
+
resolvePromise();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getEnvVal(env, key, legacyKey) {
|
|
190
|
+
return getEnvValueAny(env, [key, legacyKey]) || '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function nextLineBreakIndex(s) {
|
|
194
|
+
const n = s.indexOf('\n');
|
|
195
|
+
const r = s.indexOf('\r');
|
|
196
|
+
if (n < 0) return r;
|
|
197
|
+
if (r < 0) return n;
|
|
198
|
+
return Math.min(n, r);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function consumeLineBreak(buf) {
|
|
202
|
+
if (buf.startsWith('\r\n')) return buf.slice(2);
|
|
203
|
+
if (buf.startsWith('\n') || buf.startsWith('\r')) return buf.slice(1);
|
|
204
|
+
return buf;
|
|
205
|
+
}
|
|
206
|
+
|
|
96
207
|
function formatComponentRef({ rootDir, component, dir }) {
|
|
97
208
|
const raw = String(dir ?? '').trim();
|
|
98
209
|
if (!raw) return '(unset)';
|
|
99
210
|
|
|
100
211
|
const abs = resolve(raw);
|
|
101
|
-
|
|
102
|
-
|
|
212
|
+
// Respect sandbox workspace layout:
|
|
213
|
+
// - default: <workspace>/components/<component>
|
|
214
|
+
// - worktrees: <workspace>/components/.worktrees/<component>/<owner>/<branch...>
|
|
215
|
+
const defaultDir = resolve(join(getComponentsDir(rootDir), component));
|
|
216
|
+
const worktreesPrefix = resolve(join(getComponentsDir(rootDir), '.worktrees', component)) + sep;
|
|
103
217
|
|
|
104
218
|
if (abs === defaultDir) return 'default';
|
|
105
219
|
if (abs.startsWith(worktreesPrefix)) {
|
|
@@ -118,7 +232,9 @@ async function buildStackSummaryLines({ rootDir, stackName }) {
|
|
|
118
232
|
getEnvValueAny(env, ['HAPPY_STACKS_SERVER_COMPONENT', 'HAPPY_LOCAL_SERVER_COMPONENT']) || 'happy-server-light';
|
|
119
233
|
|
|
120
234
|
const ports = runtime?.ports && typeof runtime.ports === 'object' ? runtime.ports : {};
|
|
121
|
-
const
|
|
235
|
+
const expo = runtime?.expo && typeof runtime.expo === 'object' ? runtime.expo : {};
|
|
236
|
+
const expoPort = expo?.port ?? expo?.webPort ?? expo?.mobilePort ?? null;
|
|
237
|
+
const expoDevClientEnabled = Boolean(expo?.devClientEnabled);
|
|
122
238
|
const processes = runtime?.processes && typeof runtime.processes === 'object' ? runtime.processes : {};
|
|
123
239
|
|
|
124
240
|
const components = [
|
|
@@ -150,13 +266,21 @@ async function buildStackSummaryLines({ rootDir, stackName }) {
|
|
|
150
266
|
lines.push('');
|
|
151
267
|
lines.push('ports:');
|
|
152
268
|
lines.push(` server: ${ports?.server ?? '(unknown)'}`);
|
|
153
|
-
if (
|
|
269
|
+
if (expoPort) lines.push(` expo: ${expoPort}`);
|
|
154
270
|
if (ports?.backend) lines.push(` backend: ${ports.backend}`);
|
|
155
271
|
|
|
272
|
+
if (expoPort && expoDevClientEnabled) {
|
|
273
|
+
const payload = resolveMobileQrPayload({ env: process.env, port: Number(expoPort) });
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push('expo dev-client links:');
|
|
276
|
+
if (payload.metroUrl) lines.push(` metro: ${payload.metroUrl}`);
|
|
277
|
+
if (payload.scheme && payload.deepLink) lines.push(` link: ${payload.deepLink}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
156
280
|
lines.push('');
|
|
157
281
|
lines.push('pids:');
|
|
158
282
|
if (processes?.serverPid) lines.push(` serverPid: ${processes.serverPid}`);
|
|
159
|
-
if (processes?.
|
|
283
|
+
if (processes?.expoPid) lines.push(` expoPid: ${processes.expoPid}`);
|
|
160
284
|
if (processes?.daemonPid) lines.push(` daemonPid: ${processes.daemonPid}`);
|
|
161
285
|
if (processes?.uiGatewayPid) lines.push(` uiGatewayPid: ${processes.uiGatewayPid}`);
|
|
162
286
|
|
|
@@ -170,6 +294,29 @@ async function buildStackSummaryLines({ rootDir, stackName }) {
|
|
|
170
294
|
return lines;
|
|
171
295
|
}
|
|
172
296
|
|
|
297
|
+
async function buildExpoQrPaneLines({ stackName }) {
|
|
298
|
+
const runtimePath = getStackRuntimeStatePath(stackName);
|
|
299
|
+
const runtime = await readStackRuntimeStateFile(runtimePath);
|
|
300
|
+
const expo = runtime?.expo && typeof runtime.expo === 'object' ? runtime.expo : {};
|
|
301
|
+
const port = Number(expo?.port ?? expo?.mobilePort ?? expo?.webPort);
|
|
302
|
+
const enabled = Boolean(expo?.devClientEnabled);
|
|
303
|
+
if (!enabled || !Number.isFinite(port) || port <= 0) {
|
|
304
|
+
return { visible: false, lines: [] };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const payload = resolveMobileQrPayload({ env: process.env, port });
|
|
308
|
+
// Try to keep the QR compact:
|
|
309
|
+
// - qrcode-terminal uses a terminal-friendly pattern with adequate quiet-zone.
|
|
310
|
+
const qr = await renderQrAscii(payload.payload, { small: true });
|
|
311
|
+
const lines = [];
|
|
312
|
+
if (qr.ok) {
|
|
313
|
+
lines.push(...qr.lines);
|
|
314
|
+
} else {
|
|
315
|
+
lines.push(`(QR unavailable) ${qr.error || ''}`.trim());
|
|
316
|
+
}
|
|
317
|
+
return { visible: true, lines };
|
|
318
|
+
}
|
|
319
|
+
|
|
173
320
|
async function main() {
|
|
174
321
|
const argv = process.argv.slice(2);
|
|
175
322
|
|
|
@@ -203,7 +350,7 @@ async function main() {
|
|
|
203
350
|
' q / Ctrl+C : quit (sends SIGINT to child)',
|
|
204
351
|
'',
|
|
205
352
|
'panes (default):',
|
|
206
|
-
' orchestration | summary | local | server |
|
|
353
|
+
' orchestration | summary | local | server | expo | daemon | stack logs',
|
|
207
354
|
].join('\n'),
|
|
208
355
|
});
|
|
209
356
|
return;
|
|
@@ -218,15 +365,18 @@ async function main() {
|
|
|
218
365
|
const forwarded = argv;
|
|
219
366
|
|
|
220
367
|
const stackName = inferStackNameFromForwardedArgs(forwarded);
|
|
368
|
+
const { envPath: stackEnvPath } = resolveStackEnvPath(stackName);
|
|
221
369
|
|
|
222
370
|
const panes = [
|
|
223
371
|
mkPane('orch', 'orchestration', { visible: true, kind: 'log' }),
|
|
224
372
|
mkPane('summary', `stack summary (${stackName})`, { visible: true, kind: 'summary' }),
|
|
373
|
+
// Data-only pane: we render QR inside the Expo pane (no separate box).
|
|
374
|
+
mkPane('qr', 'expo QR', { visible: false, kind: 'qr' }),
|
|
225
375
|
mkPane('local', 'local', { visible: true, kind: 'log' }),
|
|
226
|
-
mkPane('server', 'server', { visible:
|
|
227
|
-
mkPane('
|
|
228
|
-
mkPane('daemon', 'daemon', { visible:
|
|
229
|
-
mkPane('stacklog', 'stack logs', { visible:
|
|
376
|
+
mkPane('server', 'server', { visible: false, kind: 'log' }),
|
|
377
|
+
mkPane('expo', 'expo', { visible: false, kind: 'log' }),
|
|
378
|
+
mkPane('daemon', 'daemon', { visible: false, kind: 'log' }),
|
|
379
|
+
mkPane('stacklog', 'stack logs', { visible: false, kind: 'log' }),
|
|
230
380
|
];
|
|
231
381
|
|
|
232
382
|
const paneIndexById = new Map(panes.map((p, i) => [p.id, i]));
|
|
@@ -237,12 +387,18 @@ async function main() {
|
|
|
237
387
|
|
|
238
388
|
let paneId = 'local';
|
|
239
389
|
if (normalized.includes('server')) paneId = 'server';
|
|
240
|
-
else if (normalized === 'ui') paneId = '
|
|
390
|
+
else if (normalized === 'ui') paneId = 'expo';
|
|
391
|
+
else if (normalized === 'mobile') paneId = 'expo';
|
|
392
|
+
else if (normalized === 'expo') paneId = 'expo';
|
|
241
393
|
else if (normalized.includes('daemon')) paneId = 'daemon';
|
|
242
394
|
else if (normalized === 'stack') paneId = 'stacklog';
|
|
243
395
|
else if (normalized === 'local') paneId = 'local';
|
|
244
396
|
|
|
245
397
|
const idx = paneIndexById.get(paneId) ?? paneIndexById.get('local');
|
|
398
|
+
if (panes[idx] && !panes[idx].visible && panes[idx].kind === 'log') {
|
|
399
|
+
panes[idx].visible = true;
|
|
400
|
+
// If the focused pane was hidden before, keep focus stable but ensure render updates layout.
|
|
401
|
+
}
|
|
246
402
|
pushLine(panes[idx], line);
|
|
247
403
|
};
|
|
248
404
|
|
|
@@ -250,29 +406,55 @@ async function main() {
|
|
|
250
406
|
pushLine(panes[paneIndexById.get('orch')], `[${nowTs()}] ${msg}`);
|
|
251
407
|
};
|
|
252
408
|
|
|
409
|
+
// Preflight Yarn/Corepack for this stack before spawning the pty child.
|
|
410
|
+
// This prevents Corepack "download yarn? [Y/n]" prompts from deadlocking the TUI.
|
|
411
|
+
await preflightCorepackYarnForStack({ envPath: stackEnvPath });
|
|
412
|
+
|
|
253
413
|
let layout = 'columns'; // single | split | columns
|
|
254
|
-
let focused =
|
|
414
|
+
let focused = paneIndexById.get('local'); // default focus
|
|
255
415
|
let paused = false;
|
|
256
416
|
let renderScheduled = false;
|
|
257
417
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
418
|
+
const wantsPty = process.platform !== 'win32' && (await commandExists('script', { cwd: rootDir }));
|
|
419
|
+
// In TUI mode, we intentionally do not forward keyboard input to the child process (stdin is ignored),
|
|
420
|
+
// so any interactive prompts inside the child would deadlock.
|
|
421
|
+
// Mark the child env so dependency installers can auto-approve safe prompts (Corepack yarn downloads).
|
|
422
|
+
const childEnv = {
|
|
423
|
+
...process.env,
|
|
424
|
+
HAPPY_STACKS_TUI: '1',
|
|
425
|
+
HAPPY_LOCAL_TUI: '1',
|
|
426
|
+
// Avoid Corepack mutating package.json automatically.
|
|
427
|
+
COREPACK_ENABLE_AUTO_PIN: '0',
|
|
428
|
+
};
|
|
429
|
+
const child = wantsPty
|
|
430
|
+
? // Use a pseudo-terminal so tools like Expo print QR/status output that they hide in non-TTY mode.
|
|
431
|
+
// `script` is available by default on macOS (and common on Linux).
|
|
432
|
+
spawn('script', ['-q', '/dev/null', process.execPath, happysBin, ...forwarded], {
|
|
433
|
+
cwd: rootDir,
|
|
434
|
+
env: childEnv,
|
|
435
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
436
|
+
detached: process.platform !== 'win32',
|
|
437
|
+
})
|
|
438
|
+
: spawn(process.execPath, [happysBin, ...forwarded], {
|
|
439
|
+
cwd: rootDir,
|
|
440
|
+
env: childEnv,
|
|
441
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
442
|
+
detached: process.platform !== 'win32',
|
|
443
|
+
});
|
|
264
444
|
|
|
265
|
-
logOrch(
|
|
445
|
+
logOrch(
|
|
446
|
+
`spawned: ${wantsPty ? 'script -q /dev/null ' : ''}node ${happysBin} ${forwarded.join(' ')} (pid=${child.pid})`
|
|
447
|
+
);
|
|
266
448
|
|
|
267
449
|
const buf = { out: '', err: '' };
|
|
268
450
|
const flush = (kind) => {
|
|
269
451
|
const key = kind === 'stderr' ? 'err' : 'out';
|
|
270
452
|
let b = buf[key];
|
|
271
453
|
while (true) {
|
|
272
|
-
const idx = b
|
|
454
|
+
const idx = nextLineBreakIndex(b);
|
|
273
455
|
if (idx < 0) break;
|
|
274
456
|
const line = b.slice(0, idx);
|
|
275
|
-
b = b.slice(idx
|
|
457
|
+
b = consumeLineBreak(b.slice(idx));
|
|
276
458
|
routeLine(line);
|
|
277
459
|
}
|
|
278
460
|
buf[key] = b;
|
|
@@ -301,6 +483,19 @@ async function main() {
|
|
|
301
483
|
} catch (e) {
|
|
302
484
|
panes[idx].lines = [`summary error: ${e instanceof Error ? e.message : String(e)}`];
|
|
303
485
|
}
|
|
486
|
+
|
|
487
|
+
// QR pane: driven by runtime state (expo port) and rendered independently of logs.
|
|
488
|
+
try {
|
|
489
|
+
const qrIdx = paneIndexById.get('qr');
|
|
490
|
+
const qr = await buildExpoQrPaneLines({ stackName });
|
|
491
|
+
// Data-only pane (kept hidden): rendered inside the expo pane.
|
|
492
|
+
panes[qrIdx].visible = false;
|
|
493
|
+
panes[qrIdx].lines = qr.lines;
|
|
494
|
+
} catch {
|
|
495
|
+
const qrIdx = paneIndexById.get('qr');
|
|
496
|
+
panes[qrIdx].visible = false;
|
|
497
|
+
panes[qrIdx].lines = [];
|
|
498
|
+
}
|
|
304
499
|
scheduleRender();
|
|
305
500
|
}
|
|
306
501
|
|
|
@@ -374,7 +569,9 @@ async function main() {
|
|
|
374
569
|
process.stdout.write('\x1b[?25l');
|
|
375
570
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
376
571
|
|
|
377
|
-
const
|
|
572
|
+
const focusPane = panes[focused];
|
|
573
|
+
const focusLabel = focusPane ? `${focusPane.id} (${focusPane.title})` : String(focused);
|
|
574
|
+
const header = `happys tui | ${forwarded.join(' ')} | layout=${layout} | focus=${focusLabel}`;
|
|
378
575
|
process.stdout.write(padRight(header, cols) + '\n');
|
|
379
576
|
|
|
380
577
|
const bodyY = 1;
|
|
@@ -382,9 +579,22 @@ async function main() {
|
|
|
382
579
|
const footerY = rows - 1;
|
|
383
580
|
|
|
384
581
|
const drawWrites = [];
|
|
582
|
+
|
|
583
|
+
const contentY = bodyY;
|
|
584
|
+
let contentH = bodyH;
|
|
585
|
+
|
|
385
586
|
if (layout === 'single') {
|
|
386
587
|
const pane = panes[focused];
|
|
387
|
-
const box = drawBox({
|
|
588
|
+
const box = drawBox({
|
|
589
|
+
x: 0,
|
|
590
|
+
y: contentY,
|
|
591
|
+
w: cols,
|
|
592
|
+
h: contentH,
|
|
593
|
+
title: pane.title,
|
|
594
|
+
lines: pane.lines,
|
|
595
|
+
scroll: pane.scroll,
|
|
596
|
+
active: true,
|
|
597
|
+
});
|
|
388
598
|
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
389
599
|
drawWrites.push(...box.out);
|
|
390
600
|
} else if (layout === 'split') {
|
|
@@ -394,38 +604,184 @@ async function main() {
|
|
|
394
604
|
const leftPane = panes[paneIndexById.get('orch')];
|
|
395
605
|
const rightPane = panes[focused === paneIndexById.get('orch') ? paneIndexById.get('local') : focused];
|
|
396
606
|
|
|
397
|
-
const leftBox = drawBox({
|
|
607
|
+
const leftBox = drawBox({
|
|
608
|
+
x: 0,
|
|
609
|
+
y: contentY,
|
|
610
|
+
w: leftW,
|
|
611
|
+
h: contentH,
|
|
612
|
+
title: leftPane.title,
|
|
613
|
+
lines: leftPane.lines,
|
|
614
|
+
scroll: leftPane.scroll,
|
|
615
|
+
active: focused === paneIndexById.get('orch'),
|
|
616
|
+
});
|
|
398
617
|
leftPane.scroll = clamp(leftPane.scroll, 0, leftBox.maxScroll);
|
|
399
618
|
drawWrites.push(...leftBox.out);
|
|
400
619
|
|
|
401
|
-
const rightBox = drawBox({
|
|
620
|
+
const rightBox = drawBox({
|
|
621
|
+
x: leftW,
|
|
622
|
+
y: contentY,
|
|
623
|
+
w: rightW,
|
|
624
|
+
h: contentH,
|
|
625
|
+
title: rightPane.title,
|
|
626
|
+
lines: rightPane.lines,
|
|
627
|
+
scroll: rightPane.scroll,
|
|
628
|
+
active: focused === (paneIndexById.get(rightPane.id) ?? focused),
|
|
629
|
+
});
|
|
402
630
|
rightPane.scroll = clamp(rightPane.scroll, 0, rightBox.maxScroll);
|
|
403
631
|
drawWrites.push(...rightBox.out);
|
|
404
632
|
} else {
|
|
405
|
-
// columns: render
|
|
406
|
-
const
|
|
633
|
+
// columns: render a compact top row (orch + summary), then render QR alongside Expo logs.
|
|
634
|
+
const orchIdx = paneIndexById.get('orch');
|
|
635
|
+
const summaryIdx = paneIndexById.get('summary');
|
|
636
|
+
const qrIdx = paneIndexById.get('qr');
|
|
637
|
+
const qrPane = panes[qrIdx];
|
|
638
|
+
const qrVisible = Boolean(qrPane?.visible && qrPane.lines?.length);
|
|
639
|
+
|
|
640
|
+
const topPanes = [panes[orchIdx], panes[summaryIdx]];
|
|
641
|
+
const topCount = topPanes.length;
|
|
642
|
+
const topH = getPaneHeightForLines(panes[summaryIdx].lines, { min: 6, max: 14 });
|
|
643
|
+
|
|
644
|
+
const topY = contentY;
|
|
645
|
+
const belowY = contentY + topH;
|
|
646
|
+
const belowH = Math.max(0, contentH - topH);
|
|
647
|
+
|
|
648
|
+
const colW = Math.floor(cols / topCount);
|
|
649
|
+
for (let i = 0; i < topCount; i++) {
|
|
650
|
+
const pane = topPanes[i];
|
|
651
|
+
const x = i === topCount - 1 ? colW * i : colW * i;
|
|
652
|
+
const w = i === topCount - 1 ? cols - colW * i : colW;
|
|
653
|
+
const box = drawBox({
|
|
654
|
+
x,
|
|
655
|
+
y: topY,
|
|
656
|
+
w,
|
|
657
|
+
h: topH,
|
|
658
|
+
title: pane.title,
|
|
659
|
+
lines: pane.lines,
|
|
660
|
+
scroll: pane.scroll,
|
|
661
|
+
active: paneIndexById.get(pane.id) === focused,
|
|
662
|
+
});
|
|
663
|
+
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
664
|
+
drawWrites.push(...box.out);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Remaining panes: exclude the top-row panes. QR is rendered inside the expo pane.
|
|
668
|
+
const visibleAll = visiblePaneIndexes()
|
|
669
|
+
.filter((idx) => idx !== orchIdx && idx !== summaryIdx && idx !== qrIdx)
|
|
670
|
+
.map((idx) => panes[idx]);
|
|
407
671
|
const leftW = Math.floor(cols / 2);
|
|
408
672
|
const rightW = cols - leftW;
|
|
409
673
|
|
|
410
674
|
const leftPanes = [];
|
|
411
675
|
const rightPanes = [];
|
|
676
|
+
const expoPane = panes[paneIndexById.get('expo')];
|
|
677
|
+
const visible = visibleAll.filter((p) => p !== expoPane);
|
|
412
678
|
for (let i = 0; i < visible.length; i++) {
|
|
413
679
|
(i % 2 === 0 ? leftPanes : rightPanes).push(visible[i]);
|
|
414
680
|
}
|
|
681
|
+
if (expoPane?.visible) {
|
|
682
|
+
rightPanes.unshift(expoPane);
|
|
683
|
+
}
|
|
415
684
|
|
|
416
685
|
const layoutColumn = (colX, colW, colPanes) => {
|
|
417
686
|
if (!colPanes.length) return;
|
|
418
687
|
const n = colPanes.length;
|
|
419
|
-
const base = Math.max(3, Math.floor(
|
|
420
|
-
let y =
|
|
688
|
+
const base = Math.max(3, Math.floor(belowH / n));
|
|
689
|
+
let y = belowY;
|
|
421
690
|
for (let i = 0; i < n; i++) {
|
|
422
691
|
const pane = colPanes[i];
|
|
423
|
-
const remaining =
|
|
424
|
-
|
|
692
|
+
const remaining = belowY + belowH - y;
|
|
693
|
+
let h = i === n - 1 ? remaining : Math.min(base, remaining);
|
|
425
694
|
if (h < 3) break;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
695
|
+
if (pane.id === 'expo') {
|
|
696
|
+
const qrLines = Array.isArray(qrPane?.lines) ? qrPane.lines : [];
|
|
697
|
+
const qrHas = Boolean(qrLines.length);
|
|
698
|
+
const qrMinH = qrHas ? Math.max(6, qrLines.length + 2) : 0; // +2 borders
|
|
699
|
+
if (qrMinH && h < qrMinH) {
|
|
700
|
+
h = Math.min(remaining, qrMinH);
|
|
701
|
+
if (h < 3) break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (qrHas) {
|
|
705
|
+
// Split the expo pane horizontally:
|
|
706
|
+
// left = expo logs, right = QR. This uses width instead of extra height.
|
|
707
|
+
const maxLineLen = qrLines.reduce((m, l) => Math.max(m, stripAnsi(l).length), 0);
|
|
708
|
+
const minLogW = 24;
|
|
709
|
+
const minQrW = 22;
|
|
710
|
+
const maxQrW = Math.max(0, Math.min(80, colW - minLogW));
|
|
711
|
+
const fixedQrWRaw = (process.env.HAPPY_STACKS_TUI_QR_WIDTH ?? process.env.HAPPY_LOCAL_TUI_QR_WIDTH ?? '').toString().trim();
|
|
712
|
+
const fixedQrW = fixedQrWRaw ? Number(fixedQrWRaw) : 44;
|
|
713
|
+
const qrW = clamp(Number.isFinite(fixedQrW) && fixedQrW > 0 ? fixedQrW : maxLineLen + 2, minQrW, maxQrW);
|
|
714
|
+
const canSplit = qrW >= minQrW && colW - qrW >= minLogW;
|
|
715
|
+
|
|
716
|
+
if (canSplit) {
|
|
717
|
+
const logW = colW - qrW;
|
|
718
|
+
const logBox = drawBox({
|
|
719
|
+
x: colX,
|
|
720
|
+
y,
|
|
721
|
+
w: logW,
|
|
722
|
+
h,
|
|
723
|
+
title: pane.title,
|
|
724
|
+
lines: pane.lines,
|
|
725
|
+
scroll: pane.scroll,
|
|
726
|
+
active: paneIndexById.get(pane.id) === focused,
|
|
727
|
+
});
|
|
728
|
+
pane.scroll = clamp(pane.scroll, 0, logBox.maxScroll);
|
|
729
|
+
drawWrites.push(...logBox.out);
|
|
730
|
+
|
|
731
|
+
const qrBox = drawBox({
|
|
732
|
+
x: colX + logW,
|
|
733
|
+
y,
|
|
734
|
+
w: qrW,
|
|
735
|
+
h,
|
|
736
|
+
title: qrPane.title,
|
|
737
|
+
lines: qrLines,
|
|
738
|
+
scroll: 0,
|
|
739
|
+
active: paneIndexById.get(pane.id) === focused,
|
|
740
|
+
});
|
|
741
|
+
drawWrites.push(...qrBox.out);
|
|
742
|
+
} else {
|
|
743
|
+
// Too narrow to split cleanly: fallback to single expo log box.
|
|
744
|
+
const box = drawBox({
|
|
745
|
+
x: colX,
|
|
746
|
+
y,
|
|
747
|
+
w: colW,
|
|
748
|
+
h,
|
|
749
|
+
title: pane.title,
|
|
750
|
+
lines: pane.lines,
|
|
751
|
+
scroll: pane.scroll,
|
|
752
|
+
active: paneIndexById.get(pane.id) === focused,
|
|
753
|
+
});
|
|
754
|
+
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
755
|
+
drawWrites.push(...box.out);
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
const box = drawBox({
|
|
759
|
+
x: colX,
|
|
760
|
+
y,
|
|
761
|
+
w: colW,
|
|
762
|
+
h,
|
|
763
|
+
title: pane.title,
|
|
764
|
+
lines: pane.lines,
|
|
765
|
+
scroll: pane.scroll,
|
|
766
|
+
active: paneIndexById.get(pane.id) === focused,
|
|
767
|
+
});
|
|
768
|
+
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
769
|
+
drawWrites.push(...box.out);
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
const box = drawBox({
|
|
773
|
+
x: colX,
|
|
774
|
+
y,
|
|
775
|
+
w: colW,
|
|
776
|
+
h,
|
|
777
|
+
title: pane.title,
|
|
778
|
+
lines: pane.lines,
|
|
779
|
+
scroll: pane.scroll,
|
|
780
|
+
active: paneIndexById.get(pane.id) === focused,
|
|
781
|
+
});
|
|
782
|
+
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
783
|
+
drawWrites.push(...box.out);
|
|
784
|
+
}
|
|
429
785
|
y += h;
|
|
430
786
|
}
|
|
431
787
|
};
|
package/scripts/typecheck.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
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/paths.mjs';
|
|
4
|
+
import { componentDirEnvKey, getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
5
5
|
import { ensureDepsInstalled } from './utils/proc/pm.mjs';
|
|
6
6
|
import { pathExists } from './utils/fs/fs.mjs';
|
|
7
7
|
import { run } from './utils/proc/proc.mjs';
|
|
8
8
|
import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
|
|
9
|
+
import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
|
|
9
10
|
|
|
10
11
|
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
11
12
|
|
|
@@ -40,18 +41,37 @@ async function main() {
|
|
|
40
41
|
'examples:',
|
|
41
42
|
' happys typecheck',
|
|
42
43
|
' happys typecheck happy happy-cli',
|
|
44
|
+
'',
|
|
45
|
+
'note:',
|
|
46
|
+
' If run from inside a component checkout/worktree and no components are provided, defaults to that component.',
|
|
43
47
|
].join('\n'),
|
|
44
48
|
});
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
const rootDir = getRootDir(import.meta.url);
|
|
53
|
+
|
|
48
54
|
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
49
|
-
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'];
|
|
50
72
|
const wantAll = requested.includes('all');
|
|
51
73
|
const components = wantAll ? DEFAULT_COMPONENTS : requested;
|
|
52
74
|
|
|
53
|
-
const rootDir = getRootDir(import.meta.url);
|
|
54
|
-
|
|
55
75
|
const results = [];
|
|
56
76
|
for (const component of components) {
|
|
57
77
|
if (!DEFAULT_COMPONENTS.includes(component)) {
|