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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
function runNode(args, { cwd, env }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const proc = spawn(process.execPath, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
14
|
+
proc.stdout.on('data', (d) => (stdout += String(d)));
|
|
15
|
+
proc.stderr.on('data', (d) => (stderr += String(d)));
|
|
16
|
+
proc.on('error', reject);
|
|
17
|
+
proc.on('exit', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('happys <stack> <cmd> ... rewrites to happys stack <cmd> <stack> ... when stack exists', async () => {
|
|
22
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const rootDir = dirname(scriptsDir);
|
|
24
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-shorthand-'));
|
|
25
|
+
|
|
26
|
+
const storageDir = join(tmp, 'storage');
|
|
27
|
+
const homeDir = join(tmp, 'home');
|
|
28
|
+
const stackName = 'exp-test';
|
|
29
|
+
|
|
30
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
31
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
32
|
+
await mkdir(homeDir, { recursive: true });
|
|
33
|
+
await writeFile(envPath, 'FOO=bar\n', 'utf-8');
|
|
34
|
+
|
|
35
|
+
const baseEnv = {
|
|
36
|
+
...process.env,
|
|
37
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
38
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
39
|
+
HAPPY_STACKS_CLI_ROOT_DISABLE: '1',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const res = await runNode([join(rootDir, 'bin', 'happys.mjs'), stackName, 'env', 'path', '--json'], {
|
|
43
|
+
cwd: rootDir,
|
|
44
|
+
env: baseEnv,
|
|
45
|
+
});
|
|
46
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
47
|
+
|
|
48
|
+
const out = JSON.parse(res.stdout || '{}');
|
|
49
|
+
assert.equal(out.ok, true);
|
|
50
|
+
assert.ok(
|
|
51
|
+
typeof out.envPath === 'string' && out.envPath.endsWith(`/${stackName}/env`),
|
|
52
|
+
`expected envPath to end with /${stackName}/env, got: ${out.envPath}`
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
function runNode(args, { cwd, env }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const cleanEnv = {};
|
|
12
|
+
for (const [k, v] of Object.entries(env ?? {})) {
|
|
13
|
+
if (v == null) continue;
|
|
14
|
+
cleanEnv[k] = String(v);
|
|
15
|
+
}
|
|
16
|
+
const proc = spawn(process.execPath, args, { cwd, env: cleanEnv, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
17
|
+
let stdout = '';
|
|
18
|
+
let stderr = '';
|
|
19
|
+
proc.stdout.on('data', (d) => (stdout += String(d)));
|
|
20
|
+
proc.stderr.on('data', (d) => (stderr += String(d)));
|
|
21
|
+
proc.on('error', reject);
|
|
22
|
+
proc.on('exit', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function touchWorktree(dir) {
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
// In a git worktree, ".git" is often a file; our detection treats either file or dir as truthy.
|
|
29
|
+
await writeFile(join(dir, '.git'), 'gitdir: /dev/null\n', 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('happys stack wt <stack> -- list defaults to active-only (no exhaustive enumeration)', async () => {
|
|
33
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const rootDir = dirname(scriptsDir);
|
|
35
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-stack-wt-list-'));
|
|
36
|
+
|
|
37
|
+
const storageDir = join(tmp, 'storage');
|
|
38
|
+
const homeDir = join(tmp, 'home');
|
|
39
|
+
const workspaceDir = join(tmp, 'workspace');
|
|
40
|
+
const componentsDir = join(workspaceDir, 'components');
|
|
41
|
+
const stackName = 'exp-test';
|
|
42
|
+
|
|
43
|
+
// Create isolated worktrees on disk (inside our temp workspace).
|
|
44
|
+
const wtRoot = join(componentsDir, '.worktrees');
|
|
45
|
+
const happyActive = join(wtRoot, 'happy', 'slopus', 'pr', 'active-branch');
|
|
46
|
+
const happyOther = join(wtRoot, 'happy', 'slopus', 'pr', 'other-branch');
|
|
47
|
+
const cliActive = join(wtRoot, 'happy-cli', 'slopus', 'pr', 'cli-active');
|
|
48
|
+
const cliOther = join(wtRoot, 'happy-cli', 'slopus', 'pr', 'cli-other');
|
|
49
|
+
await touchWorktree(happyActive);
|
|
50
|
+
await touchWorktree(happyOther);
|
|
51
|
+
await touchWorktree(cliActive);
|
|
52
|
+
await touchWorktree(cliOther);
|
|
53
|
+
|
|
54
|
+
// Stack env selects the active worktrees.
|
|
55
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
56
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
57
|
+
await writeFile(
|
|
58
|
+
envPath,
|
|
59
|
+
[
|
|
60
|
+
`HAPPY_STACKS_STACK=${stackName}`,
|
|
61
|
+
`HAPPY_STACKS_COMPONENT_DIR_HAPPY=${happyActive}`,
|
|
62
|
+
`HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI=${cliActive}`,
|
|
63
|
+
'',
|
|
64
|
+
].join('\n'),
|
|
65
|
+
'utf-8'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const baseEnv = {
|
|
69
|
+
...process.env,
|
|
70
|
+
// Prevent loading the user's real ~/.happy-stacks/.env via canonical discovery.
|
|
71
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
72
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
73
|
+
HAPPY_STACKS_WORKSPACE_DIR: workspaceDir,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const res = await runNode([join(rootDir, 'scripts', 'stack.mjs'), 'wt', stackName, '--', 'list'], { cwd: rootDir, env: baseEnv });
|
|
77
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
78
|
+
|
|
79
|
+
assert.ok(res.stdout.includes(`- active: ${happyActive}`), `expected happy active in output\n${res.stdout}`);
|
|
80
|
+
assert.ok(res.stdout.includes(`- active: ${cliActive}`), `expected happy-cli active in output\n${res.stdout}`);
|
|
81
|
+
|
|
82
|
+
// Should NOT enumerate other worktrees unless --all was passed.
|
|
83
|
+
assert.ok(!res.stdout.includes(`- ${happyOther}`), `expected happy other to be omitted\n${res.stdout}`);
|
|
84
|
+
assert.ok(!res.stdout.includes(`- ${cliOther}`), `expected happy-cli other to be omitted\n${res.stdout}`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('happys stack wt <stack> -- list --all shows all worktrees (opt-in)', async () => {
|
|
88
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
89
|
+
const rootDir = dirname(scriptsDir);
|
|
90
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-stack-wt-list-'));
|
|
91
|
+
|
|
92
|
+
const storageDir = join(tmp, 'storage');
|
|
93
|
+
const homeDir = join(tmp, 'home');
|
|
94
|
+
const workspaceDir = join(tmp, 'workspace');
|
|
95
|
+
const componentsDir = join(workspaceDir, 'components');
|
|
96
|
+
const stackName = 'exp-test';
|
|
97
|
+
|
|
98
|
+
const wtRoot = join(componentsDir, '.worktrees');
|
|
99
|
+
const happyActive = join(wtRoot, 'happy', 'slopus', 'pr', 'active-branch');
|
|
100
|
+
const happyOther = join(wtRoot, 'happy', 'slopus', 'pr', 'other-branch');
|
|
101
|
+
await touchWorktree(happyActive);
|
|
102
|
+
await touchWorktree(happyOther);
|
|
103
|
+
|
|
104
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
105
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
106
|
+
await writeFile(
|
|
107
|
+
envPath,
|
|
108
|
+
[`HAPPY_STACKS_STACK=${stackName}`, `HAPPY_STACKS_COMPONENT_DIR_HAPPY=${happyActive}`, ''].join('\n'),
|
|
109
|
+
'utf-8'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const baseEnv = {
|
|
113
|
+
...process.env,
|
|
114
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
115
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
116
|
+
HAPPY_STACKS_WORKSPACE_DIR: workspaceDir,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const res = await runNode([join(rootDir, 'scripts', 'stack.mjs'), 'wt', stackName, '--', 'list', '--all'], {
|
|
120
|
+
cwd: rootDir,
|
|
121
|
+
env: baseEnv,
|
|
122
|
+
});
|
|
123
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
124
|
+
|
|
125
|
+
assert.ok(res.stdout.includes(`- active: ${happyActive}`), `expected happy active in output\n${res.stdout}`);
|
|
126
|
+
assert.ok(res.stdout.includes(`- ${happyOther}`), `expected happy other to be listed with --all\n${res.stdout}`);
|
|
127
|
+
});
|
|
128
|
+
|
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
|
|