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.
Files changed (165) hide show
  1. package/README.md +93 -40
  2. package/bin/happys.mjs +158 -16
  3. package/docs/codex-mcp-resume.md +130 -0
  4. package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
  5. package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
  6. package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
  7. package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
  8. package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
  9. package/docs/happy-development.md +3 -4
  10. package/docs/isolated-linux-vm.md +82 -0
  11. package/docs/mobile-ios.md +112 -54
  12. package/docs/monorepo-migration.md +286 -0
  13. package/docs/server-flavors.md +19 -3
  14. package/docs/stacks.md +35 -0
  15. package/package.json +5 -1
  16. package/scripts/auth.mjs +32 -10
  17. package/scripts/build.mjs +55 -8
  18. package/scripts/daemon.mjs +166 -10
  19. package/scripts/dev.mjs +198 -50
  20. package/scripts/doctor.mjs +0 -4
  21. package/scripts/edison.mjs +6 -4
  22. package/scripts/env.mjs +150 -0
  23. package/scripts/env_cmd.test.mjs +128 -0
  24. package/scripts/init.mjs +8 -3
  25. package/scripts/install.mjs +207 -69
  26. package/scripts/lint.mjs +24 -4
  27. package/scripts/migrate.mjs +3 -12
  28. package/scripts/mobile.mjs +88 -104
  29. package/scripts/mobile_dev_client.mjs +83 -0
  30. package/scripts/monorepo.mjs +1096 -0
  31. package/scripts/monorepo_port.test.mjs +1470 -0
  32. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  33. package/scripts/review.mjs +908 -0
  34. package/scripts/review_pr.mjs +353 -0
  35. package/scripts/run.mjs +101 -21
  36. package/scripts/service.mjs +2 -2
  37. package/scripts/setup.mjs +189 -68
  38. package/scripts/setup_pr.mjs +586 -38
  39. package/scripts/stack.mjs +990 -196
  40. package/scripts/stack_archive_cmd.test.mjs +91 -0
  41. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
  42. package/scripts/stack_env_cmd.test.mjs +87 -0
  43. package/scripts/stack_happy_cmd.test.mjs +126 -0
  44. package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
  45. package/scripts/stack_monorepo_defaults.test.mjs +62 -0
  46. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
  47. package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
  48. package/scripts/stack_shorthand_cmd.test.mjs +55 -0
  49. package/scripts/stack_wt_list.test.mjs +128 -0
  50. package/scripts/tailscale.mjs +37 -1
  51. package/scripts/test.mjs +45 -8
  52. package/scripts/tui.mjs +395 -39
  53. package/scripts/typecheck.mjs +24 -4
  54. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  55. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  56. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  57. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  58. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  59. package/scripts/utils/auth/login_ux.mjs +32 -13
  60. package/scripts/utils/auth/sources.mjs +26 -0
  61. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  62. package/scripts/utils/cli/cli_registry.mjs +43 -4
  63. package/scripts/utils/cli/cwd_scope.mjs +136 -0
  64. package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
  65. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  66. package/scripts/utils/cli/prereqs.mjs +75 -0
  67. package/scripts/utils/cli/prereqs.test.mjs +34 -0
  68. package/scripts/utils/cli/progress.mjs +126 -0
  69. package/scripts/utils/cli/verbosity.mjs +12 -0
  70. package/scripts/utils/cli/wizard.mjs +17 -9
  71. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
  72. package/scripts/utils/dev/daemon.mjs +61 -4
  73. package/scripts/utils/dev/expo_dev.mjs +430 -0
  74. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  75. package/scripts/utils/dev/server.mjs +36 -42
  76. package/scripts/utils/dev_auth_key.mjs +169 -0
  77. package/scripts/utils/edison/git_roots.mjs +29 -0
  78. package/scripts/utils/edison/git_roots.test.mjs +36 -0
  79. package/scripts/utils/env/env.mjs +7 -3
  80. package/scripts/utils/env/env_file.mjs +4 -2
  81. package/scripts/utils/env/env_file.test.mjs +44 -0
  82. package/scripts/utils/expo/command.mjs +52 -0
  83. package/scripts/utils/expo/expo.mjs +20 -1
  84. package/scripts/utils/expo/metro_ports.mjs +114 -0
  85. package/scripts/utils/git/git.mjs +67 -0
  86. package/scripts/utils/git/worktrees.mjs +80 -25
  87. package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
  88. package/scripts/utils/handy_master_secret.mjs +94 -0
  89. package/scripts/utils/mobile/config.mjs +31 -0
  90. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  91. package/scripts/utils/mobile/identifiers.mjs +47 -0
  92. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  93. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  94. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  95. package/scripts/utils/net/lan_ip.mjs +24 -0
  96. package/scripts/utils/net/ports.mjs +9 -1
  97. package/scripts/utils/net/tcp_forward.mjs +162 -0
  98. package/scripts/utils/net/url.mjs +30 -0
  99. package/scripts/utils/net/url.test.mjs +20 -0
  100. package/scripts/utils/paths/localhost_host.mjs +50 -3
  101. package/scripts/utils/paths/paths.mjs +159 -40
  102. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  103. package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
  104. package/scripts/utils/proc/commands.mjs +2 -3
  105. package/scripts/utils/proc/parallel.mjs +25 -0
  106. package/scripts/utils/proc/pm.mjs +176 -22
  107. package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
  108. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
  109. package/scripts/utils/proc/proc.mjs +136 -4
  110. package/scripts/utils/proc/proc.test.mjs +77 -0
  111. package/scripts/utils/review/base_ref.mjs +74 -0
  112. package/scripts/utils/review/base_ref.test.mjs +54 -0
  113. package/scripts/utils/review/chunks.mjs +55 -0
  114. package/scripts/utils/review/chunks.test.mjs +51 -0
  115. package/scripts/utils/review/findings.mjs +165 -0
  116. package/scripts/utils/review/findings.test.mjs +85 -0
  117. package/scripts/utils/review/head_slice.mjs +153 -0
  118. package/scripts/utils/review/head_slice.test.mjs +91 -0
  119. package/scripts/utils/review/instructions/deep.md +20 -0
  120. package/scripts/utils/review/runners/coderabbit.mjs +61 -0
  121. package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
  122. package/scripts/utils/review/runners/codex.mjs +61 -0
  123. package/scripts/utils/review/runners/codex.test.mjs +35 -0
  124. package/scripts/utils/review/slices.mjs +140 -0
  125. package/scripts/utils/review/slices.test.mjs +32 -0
  126. package/scripts/utils/review/targets.mjs +24 -0
  127. package/scripts/utils/review/targets.test.mjs +36 -0
  128. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  129. package/scripts/utils/server/flavor_scripts.mjs +98 -0
  130. package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
  131. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  132. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  133. package/scripts/utils/server/prisma_import.mjs +37 -0
  134. package/scripts/utils/server/prisma_import.test.mjs +70 -0
  135. package/scripts/utils/server/ui_env.mjs +14 -0
  136. package/scripts/utils/server/ui_env.test.mjs +46 -0
  137. package/scripts/utils/server/urls.mjs +14 -4
  138. package/scripts/utils/server/validate.mjs +53 -16
  139. package/scripts/utils/server/validate.test.mjs +89 -0
  140. package/scripts/utils/service/autostart_darwin.mjs +42 -2
  141. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  142. package/scripts/utils/stack/context.mjs +2 -2
  143. package/scripts/utils/stack/editor_workspace.mjs +6 -6
  144. package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
  145. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  146. package/scripts/utils/stack/runtime_state.mjs +2 -1
  147. package/scripts/utils/stack/startup.mjs +120 -13
  148. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
  149. package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
  150. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
  151. package/scripts/utils/stack/stop.mjs +15 -4
  152. package/scripts/utils/stack_context.mjs +23 -0
  153. package/scripts/utils/stack_runtime_state.mjs +104 -0
  154. package/scripts/utils/stacks.mjs +38 -0
  155. package/scripts/utils/tailscale/ip.mjs +116 -0
  156. package/scripts/utils/ui/ansi.mjs +39 -0
  157. package/scripts/utils/ui/qr.mjs +17 -0
  158. package/scripts/utils/validate.mjs +88 -0
  159. package/scripts/where.mjs +2 -2
  160. package/scripts/worktrees.mjs +755 -179
  161. package/scripts/worktrees_archive_cmd.test.mjs +245 -0
  162. package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
  163. package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
  164. package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
  165. package/scripts/utils/dev/expo_web.mjs +0 -112
@@ -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 workspaceDir = expandHome(firstNonEmpty(
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
- process.env.HAPPY_STACKS_STORAGE_DIR = process.env.HAPPY_STACKS_STORAGE_DIR ?? storageDirOverride;
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');