happy-stacks 0.3.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 +29 -7
- package/bin/happys.mjs +114 -15
- 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 +11 -7
- package/scripts/build.mjs +54 -7
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +181 -46
- package/scripts/edison.mjs +4 -2
- package/scripts/init.mjs +3 -1
- package/scripts/install.mjs +112 -16
- package/scripts/lint.mjs +24 -4
- package/scripts/mobile.mjs +88 -104
- 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 +83 -9
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +42 -43
- package/scripts/setup_pr.mjs +591 -34
- package/scripts/stack.mjs +503 -45
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +309 -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 +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/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/dev/daemon.mjs +47 -3
- 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 +15 -25
- package/scripts/utils/dev_auth_key.mjs +169 -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 +24 -20
- 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/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 +42 -38
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +69 -12
- package/scripts/utils/proc/proc.mjs +76 -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/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/urls.mjs +14 -4
- 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 +2 -2
- 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 +7 -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/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/worktrees.mjs +141 -55
- package/scripts/utils/dev/expo_web.mjs +0 -112
package/scripts/tailscale.mjs
CHANGED
|
@@ -40,7 +40,9 @@ function extractHttpsUrl(serveStatusText) {
|
|
|
40
40
|
.find((l) => l.toLowerCase().includes('https://'));
|
|
41
41
|
if (!line) return null;
|
|
42
42
|
const m = line.match(/https:\/\/\S+/i);
|
|
43
|
-
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
// Avoid trailing slash for base URLs (some consumers treat it as a path prefix).
|
|
45
|
+
return m[0].replace(/\/+$/, '');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function tailscaleStatusMatchesInternalServerUrl(status, internalServerUrl) {
|
|
@@ -80,6 +82,16 @@ function extractServeEnableUrl(text) {
|
|
|
80
82
|
return m ? m[0] : null;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
function assertTailscaleAllowed(action) {
|
|
86
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[local] tailscale ${action} is disabled in sandbox mode.\n` +
|
|
89
|
+
`Reason: Tailscale Serve is global machine state and sandbox runs must be isolated.\n` +
|
|
90
|
+
`If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
function parseTimeoutMs(raw, defaultMs) {
|
|
84
96
|
const s = (raw ?? '').trim();
|
|
85
97
|
if (!s) return defaultMs;
|
|
@@ -173,11 +185,13 @@ export async function tailscaleServeHttpsUrl() {
|
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
export async function tailscaleServeStatus() {
|
|
188
|
+
assertTailscaleAllowed('status');
|
|
176
189
|
const cmd = await resolveTailscaleCmd();
|
|
177
190
|
return await runCapture(cmd, ['serve', 'status'], { env: tailscaleEnv(), timeoutMs: tailscaleProbeTimeoutMs() });
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
export async function tailscaleServeEnable({ internalServerUrl, timeoutMs } = {}) {
|
|
194
|
+
assertTailscaleAllowed('enable');
|
|
181
195
|
const cmd = await resolveTailscaleCmd();
|
|
182
196
|
const { upstream, servePath } = getServeConfig(internalServerUrl);
|
|
183
197
|
const args = ['serve', '--bg'];
|
|
@@ -215,12 +229,16 @@ export async function tailscaleServeEnable({ internalServerUrl, timeoutMs } = {}
|
|
|
215
229
|
}
|
|
216
230
|
|
|
217
231
|
export async function tailscaleServeReset({ timeoutMs } = {}) {
|
|
232
|
+
assertTailscaleAllowed('reset');
|
|
218
233
|
const cmd = await resolveTailscaleCmd();
|
|
219
234
|
const timeout = Number.isFinite(timeoutMs) ? (timeoutMs > 0 ? timeoutMs : 0) : tailscaleUserResetTimeoutMs();
|
|
220
235
|
await run(cmd, ['serve', 'reset'], { env: tailscaleEnv(), timeoutMs: timeout });
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
export async function maybeEnableTailscaleServe({ internalServerUrl }) {
|
|
239
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
224
242
|
const enabled = (process.env.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0') === '1';
|
|
225
243
|
if (!enabled) {
|
|
226
244
|
return null;
|
|
@@ -234,6 +252,9 @@ export async function maybeEnableTailscaleServe({ internalServerUrl }) {
|
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
export async function maybeResetTailscaleServe() {
|
|
255
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
237
258
|
const enabled = (process.env.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0') === '1';
|
|
238
259
|
const resetOnExit = (process.env.HAPPY_LOCAL_TAILSCALE_RESET_ON_EXIT ?? '0') === '1';
|
|
239
260
|
if (!enabled || !resetOnExit) {
|
|
@@ -266,6 +287,7 @@ export async function resolvePublicServerUrl({
|
|
|
266
287
|
defaultPublicUrl,
|
|
267
288
|
envPublicUrl,
|
|
268
289
|
allowEnable = true,
|
|
290
|
+
stackName = 'main',
|
|
269
291
|
}) {
|
|
270
292
|
const preferTailscalePublicUrl = (process.env.HAPPY_LOCAL_TAILSCALE_PREFER_PUBLIC_URL ?? '1') !== '0';
|
|
271
293
|
const userExplicitlySetPublicUrl =
|
|
@@ -275,6 +297,20 @@ export async function resolvePublicServerUrl({
|
|
|
275
297
|
return { publicServerUrl: envPublicUrl || defaultPublicUrl, source: 'env' };
|
|
276
298
|
}
|
|
277
299
|
|
|
300
|
+
// Non-main stacks:
|
|
301
|
+
// - Never auto-enable (global machine state) by default.
|
|
302
|
+
// - If the caller explicitly allows it AND Tailscale Serve is already configured for this stack's
|
|
303
|
+
// internal URL, prefer the HTTPS URL (safe: status must match the internal URL).
|
|
304
|
+
if (stackName && stackName !== 'main') {
|
|
305
|
+
if (allowEnable) {
|
|
306
|
+
const existing = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
|
|
307
|
+
if (existing) {
|
|
308
|
+
return { publicServerUrl: existing, source: 'tailscale-status' };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { publicServerUrl: envPublicUrl || defaultPublicUrl, source: envPublicUrl ? 'env' : 'default' };
|
|
312
|
+
}
|
|
313
|
+
|
|
278
314
|
// If serve is already configured, use its HTTPS URL if present.
|
|
279
315
|
const existing = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
|
|
280
316
|
if (existing) {
|
package/scripts/test.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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'];
|
|
12
|
+
const EXTRA_COMPONENTS = ['stacks'];
|
|
13
|
+
const VALID_COMPONENTS = [...DEFAULT_COMPONENTS, ...EXTRA_COMPONENTS];
|
|
11
14
|
|
|
12
15
|
function pickTestScript(scripts) {
|
|
13
16
|
const candidates = [
|
|
@@ -28,33 +31,67 @@ async function main() {
|
|
|
28
31
|
if (wantsHelp(argv, { flags })) {
|
|
29
32
|
printResult({
|
|
30
33
|
json,
|
|
31
|
-
data: { components:
|
|
34
|
+
data: { components: VALID_COMPONENTS, flags: ['--json'] },
|
|
32
35
|
text: [
|
|
33
36
|
'[test] usage:',
|
|
34
37
|
' happys test [component...] [--json]',
|
|
35
38
|
'',
|
|
36
39
|
'components:',
|
|
37
|
-
` ${
|
|
40
|
+
` ${VALID_COMPONENTS.join(' | ')}`,
|
|
38
41
|
'',
|
|
39
42
|
'examples:',
|
|
40
43
|
' happys test',
|
|
44
|
+
' happys test stacks',
|
|
41
45
|
' happys test happy happy-cli',
|
|
46
|
+
'',
|
|
47
|
+
'note:',
|
|
48
|
+
' If run from inside a component checkout/worktree and no components are provided, defaults to that component.',
|
|
42
49
|
].join('\n'),
|
|
43
50
|
});
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
const rootDir = getRootDir(import.meta.url);
|
|
55
|
+
|
|
47
56
|
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
48
|
-
const
|
|
57
|
+
const inferred =
|
|
58
|
+
positionals.length === 0
|
|
59
|
+
? inferComponentFromCwd({
|
|
60
|
+
rootDir,
|
|
61
|
+
invokedCwd: getInvokedCwd(process.env),
|
|
62
|
+
components: DEFAULT_COMPONENTS,
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
65
|
+
if (inferred) {
|
|
66
|
+
const stacksKey = componentDirEnvKey(inferred.component);
|
|
67
|
+
const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
|
|
68
|
+
if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
|
|
69
|
+
process.env[stacksKey] = inferred.repoDir;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requested = positionals.length ? positionals : inferred ? [inferred.component] : ['all'];
|
|
49
74
|
const wantAll = requested.includes('all');
|
|
75
|
+
// Default `all` excludes "stacks" to avoid coupling to component repos and their test baselines.
|
|
50
76
|
const components = wantAll ? DEFAULT_COMPONENTS : requested;
|
|
51
77
|
|
|
52
|
-
const rootDir = getRootDir(import.meta.url);
|
|
53
|
-
|
|
54
78
|
const results = [];
|
|
55
79
|
for (const component of components) {
|
|
56
|
-
if (!
|
|
57
|
-
results.push({ component, ok: false, skipped: false, error: `unknown component (expected one of: ${
|
|
80
|
+
if (!VALID_COMPONENTS.includes(component)) {
|
|
81
|
+
results.push({ component, ok: false, skipped: false, error: `unknown component (expected one of: ${VALID_COMPONENTS.join(', ')})` });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (component === 'stacks') {
|
|
86
|
+
try {
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log('[test] stacks: running node --test (happy-stacks unit tests)');
|
|
89
|
+
// Restrict to explicit *.test.mjs files to avoid accidentally executing scripts/test.mjs.
|
|
90
|
+
await run('sh', ['-lc', 'node --test "scripts/**/*.test.mjs"'], { cwd: rootDir, env: process.env });
|
|
91
|
+
results.push({ component, ok: true, skipped: false, dir: rootDir, pm: 'node', script: '--test' });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
results.push({ component, ok: false, skipped: false, dir: rootDir, pm: 'node', script: '--test', error: String(e?.message ?? e) });
|
|
94
|
+
}
|
|
58
95
|
continue;
|
|
59
96
|
}
|
|
60
97
|
|