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/env.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
+
import { ensureEnvFilePruned, ensureEnvFileUpdated } from './utils/env/env_file.mjs';
|
|
5
|
+
import { parseEnvToObject } from './utils/env/dotenv.mjs';
|
|
6
|
+
import { resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
7
|
+
import { readTextOrEmpty } from './utils/fs/ops.mjs';
|
|
8
|
+
|
|
9
|
+
function resolveTargetEnvPath() {
|
|
10
|
+
// If we're already running under a stack wrapper, respect it.
|
|
11
|
+
const explicit = (process.env.HAPPY_STACKS_ENV_FILE ?? process.env.HAPPY_LOCAL_ENV_FILE ?? '').trim();
|
|
12
|
+
if (explicit) return explicit;
|
|
13
|
+
|
|
14
|
+
// Self-host default: no stacks knowledge required; persist in the main stack env file.
|
|
15
|
+
return resolveStackEnvPath('main').envPath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const { flags } = parseArgs(argv);
|
|
21
|
+
const json = wantsJson(argv, { flags });
|
|
22
|
+
|
|
23
|
+
const helpText = [
|
|
24
|
+
'[env] usage:',
|
|
25
|
+
' happys env set KEY=VALUE [KEY2=VALUE2...]',
|
|
26
|
+
' happys env unset KEY [KEY2...]',
|
|
27
|
+
' happys env get KEY',
|
|
28
|
+
' happys env list',
|
|
29
|
+
' happys env path',
|
|
30
|
+
'',
|
|
31
|
+
'defaults:',
|
|
32
|
+
' - If running under a stack wrapper (HAPPY_STACKS_ENV_FILE is set), edits that stack env file.',
|
|
33
|
+
' - Otherwise, edits the main stack env file (~/.happy/stacks/main/env).',
|
|
34
|
+
'',
|
|
35
|
+
'notes:',
|
|
36
|
+
' - Changes take effect on next stack/daemon start (restart to apply).',
|
|
37
|
+
].join('\n');
|
|
38
|
+
|
|
39
|
+
if (wantsHelp(argv, { flags })) {
|
|
40
|
+
printResult({
|
|
41
|
+
json,
|
|
42
|
+
data: {
|
|
43
|
+
usage:
|
|
44
|
+
'happys env set KEY=VALUE [KEY2=VALUE2...] | unset KEY [KEY2...] | get KEY | list | path [--json]',
|
|
45
|
+
},
|
|
46
|
+
text: helpText,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
52
|
+
const subcmd = (positionals[0] ?? '').trim() || 'help';
|
|
53
|
+
const envPath = resolveTargetEnvPath();
|
|
54
|
+
|
|
55
|
+
if (subcmd === 'help') {
|
|
56
|
+
printResult({
|
|
57
|
+
json,
|
|
58
|
+
data: {
|
|
59
|
+
usage: 'happys env set|unset|get|list|path [--json]',
|
|
60
|
+
},
|
|
61
|
+
text: helpText,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (subcmd === 'path') {
|
|
67
|
+
printResult({
|
|
68
|
+
json,
|
|
69
|
+
data: { ok: true, envPath },
|
|
70
|
+
text: envPath,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const raw = await readTextOrEmpty(envPath);
|
|
76
|
+
const parsed = parseEnvToObject(raw);
|
|
77
|
+
|
|
78
|
+
if (subcmd === 'list') {
|
|
79
|
+
const keys = Object.keys(parsed ?? {}).sort((a, b) => a.localeCompare(b));
|
|
80
|
+
const text = [
|
|
81
|
+
`[env] path: ${envPath}`,
|
|
82
|
+
...keys.map((k) => `${k}=${parsed[k] ?? ''}`),
|
|
83
|
+
].join('\n');
|
|
84
|
+
printResult({ json, data: { ok: true, envPath, env: parsed }, text });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (subcmd === 'get') {
|
|
89
|
+
const key = (positionals[1] ?? '').trim();
|
|
90
|
+
if (!key) {
|
|
91
|
+
throw new Error('[env] usage: happys env get KEY');
|
|
92
|
+
}
|
|
93
|
+
const value = Object.prototype.hasOwnProperty.call(parsed, key) ? parsed[key] : null;
|
|
94
|
+
printResult({
|
|
95
|
+
json,
|
|
96
|
+
data: { ok: true, envPath, key, value },
|
|
97
|
+
text: value == null ? '' : String(value),
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (subcmd === 'set') {
|
|
103
|
+
const pairs = positionals.slice(1);
|
|
104
|
+
if (!pairs.length) {
|
|
105
|
+
throw new Error('[env] usage: happys env set KEY=VALUE [KEY2=VALUE2...]');
|
|
106
|
+
}
|
|
107
|
+
const updates = pairs.map((p) => {
|
|
108
|
+
const idx = p.indexOf('=');
|
|
109
|
+
if (idx <= 0) {
|
|
110
|
+
throw new Error(`[env] set: expected KEY=VALUE, got: ${p}`);
|
|
111
|
+
}
|
|
112
|
+
const key = p.slice(0, idx).trim();
|
|
113
|
+
const value = p.slice(idx + 1);
|
|
114
|
+
if (!key) {
|
|
115
|
+
throw new Error(`[env] set: invalid key in: ${p}`);
|
|
116
|
+
}
|
|
117
|
+
return { key, value };
|
|
118
|
+
});
|
|
119
|
+
await ensureEnvFileUpdated({ envPath, updates });
|
|
120
|
+
const updatedKeys = updates.map((u) => u.key);
|
|
121
|
+
printResult({
|
|
122
|
+
json,
|
|
123
|
+
data: { ok: true, envPath, updatedKeys },
|
|
124
|
+
text: `[env] ok: set ${updatedKeys.join(', ')}\n[env] path: ${envPath}`,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (subcmd === 'unset' || subcmd === 'remove' || subcmd === 'rm') {
|
|
130
|
+
const keys = positionals.slice(1).map((k) => k.trim()).filter(Boolean);
|
|
131
|
+
if (!keys.length) {
|
|
132
|
+
throw new Error('[env] usage: happys env unset KEY [KEY2...]');
|
|
133
|
+
}
|
|
134
|
+
await ensureEnvFilePruned({ envPath, removeKeys: keys });
|
|
135
|
+
printResult({
|
|
136
|
+
json,
|
|
137
|
+
data: { ok: true, envPath, removedKeys: keys },
|
|
138
|
+
text: `[env] ok: unset ${keys.join(', ')}\n[env] path: ${envPath}`,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new Error(`[env] unknown subcommand: ${subcmd}\n[env] usage: happys env set|unset|get|list|path`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main().catch((err) => {
|
|
147
|
+
console.error('[env] failed:', err);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
@@ -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, readFile, 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
|
+
test('happys env path defaults to main stack env file when no explicit env file is set', async () => {
|
|
27
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const rootDir = dirname(scriptsDir);
|
|
29
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-env-cmd-'));
|
|
30
|
+
|
|
31
|
+
const storageDir = join(tmp, 'storage');
|
|
32
|
+
const homeDir = join(tmp, 'home');
|
|
33
|
+
await mkdir(storageDir, { recursive: true });
|
|
34
|
+
await mkdir(homeDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const baseEnv = {
|
|
37
|
+
...process.env,
|
|
38
|
+
// Prevent loading the user's real ~/.happy-stacks/.env via canonical discovery.
|
|
39
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
40
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const res = await runNode([join(rootDir, 'scripts', 'env.mjs'), 'path', '--json'], {
|
|
44
|
+
cwd: rootDir,
|
|
45
|
+
env: baseEnv,
|
|
46
|
+
});
|
|
47
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
48
|
+
|
|
49
|
+
const out = JSON.parse(res.stdout || '{}');
|
|
50
|
+
assert.equal(out.ok, true);
|
|
51
|
+
assert.ok(
|
|
52
|
+
typeof out.envPath === 'string' && out.envPath.endsWith('/main/env'),
|
|
53
|
+
`expected main env path to end with /main/env, got: ${out.envPath}`
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('happys env edits the explicit stack env file when HAPPY_STACKS_ENV_FILE is set', async () => {
|
|
58
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const rootDir = dirname(scriptsDir);
|
|
60
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-env-cmd-'));
|
|
61
|
+
|
|
62
|
+
const storageDir = join(tmp, 'storage');
|
|
63
|
+
const homeDir = join(tmp, 'home');
|
|
64
|
+
const stackName = 'exp1';
|
|
65
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
66
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
67
|
+
await mkdir(homeDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
const baseEnv = {
|
|
70
|
+
...process.env,
|
|
71
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
72
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
73
|
+
HAPPY_STACKS_ENV_FILE: envPath,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const res = await runNode([join(rootDir, 'scripts', 'env.mjs'), 'set', 'FOO=bar'], { 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
|
+
const raw = await readFile(envPath, 'utf-8');
|
|
80
|
+
assert.ok(raw.includes('FOO=bar'), `expected FOO in explicit env file\n${raw}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('happys env (no subcommand) prints usage and exits 0', async () => {
|
|
84
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
85
|
+
const rootDir = dirname(scriptsDir);
|
|
86
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-env-cmd-'));
|
|
87
|
+
|
|
88
|
+
const storageDir = join(tmp, 'storage');
|
|
89
|
+
const homeDir = join(tmp, 'home');
|
|
90
|
+
await mkdir(storageDir, { recursive: true });
|
|
91
|
+
await mkdir(homeDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const baseEnv = {
|
|
94
|
+
...process.env,
|
|
95
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
96
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const res = await runNode([join(rootDir, 'scripts', 'env.mjs')], { cwd: rootDir, env: baseEnv });
|
|
100
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
101
|
+
assert.ok(res.stdout.includes('[env] usage:'), `expected usage output\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('happys env list prints keys in text mode', async () => {
|
|
105
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
106
|
+
const rootDir = dirname(scriptsDir);
|
|
107
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-env-cmd-'));
|
|
108
|
+
|
|
109
|
+
const storageDir = join(tmp, 'storage');
|
|
110
|
+
const homeDir = join(tmp, 'home');
|
|
111
|
+
const stackName = 'exp1';
|
|
112
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
113
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
114
|
+
await mkdir(homeDir, { recursive: true });
|
|
115
|
+
await writeFile(envPath, 'FOO=bar\n', 'utf-8');
|
|
116
|
+
|
|
117
|
+
const baseEnv = {
|
|
118
|
+
...process.env,
|
|
119
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
120
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
121
|
+
HAPPY_STACKS_ENV_FILE: envPath,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const res = await runNode([join(rootDir, 'scripts', 'env.mjs'), 'list'], { cwd: rootDir, env: baseEnv });
|
|
125
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
126
|
+
assert.ok(res.stdout.includes('FOO=bar'), `expected list output to include FOO=bar\nstdout:\n${res.stdout}`);
|
|
127
|
+
});
|
|
128
|
+
|
package/scripts/init.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { ensureCanonicalHomeEnvUpdated, ensureHomeEnvUpdated } from './utils/env/config.mjs';
|
|
@@ -177,12 +177,15 @@ async function main() {
|
|
|
177
177
|
process.env.HAPPY_LOCAL_HOME_DIR = process.env.HAPPY_LOCAL_HOME_DIR ?? homeDir;
|
|
178
178
|
|
|
179
179
|
const workspaceDirRaw = parseArgValue(argv, 'workspace-dir');
|
|
180
|
-
const
|
|
180
|
+
const workspaceDirExpanded = expandHome(firstNonEmpty(
|
|
181
181
|
workspaceDirRaw,
|
|
182
182
|
process.env.HAPPY_STACKS_WORKSPACE_DIR,
|
|
183
183
|
process.env.HAPPY_LOCAL_WORKSPACE_DIR,
|
|
184
184
|
join(homeDir, 'workspace'),
|
|
185
185
|
));
|
|
186
|
+
// If the user passes a relative --workspace-dir, interpret it as relative to the home dir
|
|
187
|
+
// (not the current cwd). This keeps setup predictable, especially when invoked via `npx`.
|
|
188
|
+
const workspaceDir = workspaceDirExpanded.startsWith('/') ? workspaceDirExpanded : resolve(homeDir, workspaceDirExpanded);
|
|
186
189
|
process.env.HAPPY_STACKS_WORKSPACE_DIR = workspaceDir;
|
|
187
190
|
process.env.HAPPY_LOCAL_WORKSPACE_DIR = process.env.HAPPY_LOCAL_WORKSPACE_DIR ?? workspaceDir;
|
|
188
191
|
|
|
@@ -199,7 +202,9 @@ async function main() {
|
|
|
199
202
|
const storageDirRaw = parseArgValue(argv, 'storage-dir');
|
|
200
203
|
const storageDirOverride = expandHome((storageDirRaw ?? '').trim());
|
|
201
204
|
if (storageDirOverride) {
|
|
202
|
-
|
|
205
|
+
// In sandbox mode, storage dir MUST be isolated and must override any pre-existing env.
|
|
206
|
+
process.env.HAPPY_STACKS_STORAGE_DIR = isSandboxed() ? storageDirOverride : (process.env.HAPPY_STACKS_STORAGE_DIR ?? storageDirOverride);
|
|
207
|
+
process.env.HAPPY_LOCAL_STORAGE_DIR = process.env.HAPPY_LOCAL_STORAGE_DIR ?? process.env.HAPPY_STACKS_STORAGE_DIR;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
const cliRootDirRaw = parseArgValue(argv, 'cli-root-dir');
|