@happier-dev/stack 0.2.0 → 0.2.1-preview.1775586714.26562

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 (107) hide show
  1. package/node_modules/@happier-dev/agents/dist/.tsbuildinfo +1 -1
  2. package/node_modules/@happier-dev/agents/dist/index.d.ts +1 -1
  3. package/node_modules/@happier-dev/agents/dist/index.d.ts.map +1 -1
  4. package/node_modules/@happier-dev/agents/dist/index.js.map +1 -1
  5. package/node_modules/@happier-dev/agents/dist/providerSettings/definitions/claudeRemote.d.ts +9 -9
  6. package/node_modules/@happier-dev/agents/dist/providerSettings/definitions/codex.d.ts +5 -5
  7. package/node_modules/@happier-dev/agents/dist/providers/providerCliRuntime.d.ts +14 -1
  8. package/node_modules/@happier-dev/agents/dist/providers/providerCliRuntime.d.ts.map +1 -1
  9. package/node_modules/@happier-dev/agents/dist/providers/providerCliRuntime.js +25 -12
  10. package/node_modules/@happier-dev/agents/dist/providers/providerCliRuntime.js.map +1 -1
  11. package/node_modules/@happier-dev/cli-common/dist/.tsbuildinfo +1 -1
  12. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/buildCliBinaryArtifactPayload.js +1 -1
  13. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/buildCliBinaryArtifactPayload.js.map +1 -1
  14. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/commands.d.ts.map +1 -1
  15. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/commands.js +2 -1
  16. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/commands.js.map +1 -1
  17. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/commands.test.js +22 -0
  18. package/node_modules/@happier-dev/cli-common/dist/componentArtifacts/commands.test.js.map +1 -1
  19. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/index.d.ts +2 -2
  20. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/index.d.ts.map +1 -1
  21. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/index.js +1 -1
  22. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/index.js.map +1 -1
  23. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/relayRuntime.d.ts +12 -0
  24. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/relayRuntime.d.ts.map +1 -1
  25. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/relayRuntime.js +18 -0
  26. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/relayRuntime.js.map +1 -1
  27. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/relayRuntime.test.js +39 -0
  28. package/node_modules/@happier-dev/cli-common/dist/firstPartyRuntime/relayRuntime.test.js.map +1 -1
  29. package/node_modules/@happier-dev/cli-common/dist/process/index.d.ts +1 -1
  30. package/node_modules/@happier-dev/cli-common/dist/process/index.d.ts.map +1 -1
  31. package/node_modules/@happier-dev/cli-common/dist/process/index.js +1 -1
  32. package/node_modules/@happier-dev/cli-common/dist/process/index.js.map +1 -1
  33. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.d.ts +1 -0
  34. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.d.ts.map +1 -1
  35. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.js +23 -5
  36. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.js.map +1 -1
  37. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.test.d.ts +2 -0
  38. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.test.d.ts.map +1 -0
  39. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.test.js +64 -0
  40. package/node_modules/@happier-dev/cli-common/dist/process/windows/resolveWindowsCommandInvocation.test.js.map +1 -0
  41. package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts +1 -1
  42. package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts.map +1 -1
  43. package/node_modules/@happier-dev/cli-common/dist/providers/index.js +1 -1
  44. package/node_modules/@happier-dev/cli-common/dist/providers/index.js.map +1 -1
  45. package/node_modules/@happier-dev/cli-common/dist/providers/install.d.ts.map +1 -1
  46. package/node_modules/@happier-dev/cli-common/dist/providers/install.js +3 -1
  47. package/node_modules/@happier-dev/cli-common/dist/providers/install.js.map +1 -1
  48. package/node_modules/@happier-dev/cli-common/dist/providers/managedJavaScriptRuntime.d.ts.map +1 -1
  49. package/node_modules/@happier-dev/cli-common/dist/providers/managedJavaScriptRuntime.js +10 -1
  50. package/node_modules/@happier-dev/cli-common/dist/providers/managedJavaScriptRuntime.js.map +1 -1
  51. package/node_modules/@happier-dev/cli-common/dist/providers/managedJavaScriptRuntime.systemNodeFallback.test.js +58 -1
  52. package/node_modules/@happier-dev/cli-common/dist/providers/managedJavaScriptRuntime.systemNodeFallback.test.js.map +1 -1
  53. package/node_modules/@happier-dev/cli-common/dist/providers/resolution.d.ts +2 -0
  54. package/node_modules/@happier-dev/cli-common/dist/providers/resolution.d.ts.map +1 -1
  55. package/node_modules/@happier-dev/cli-common/dist/providers/resolution.js +120 -32
  56. package/node_modules/@happier-dev/cli-common/dist/providers/resolution.js.map +1 -1
  57. package/node_modules/@happier-dev/cli-common/dist/providers/resolveHappyHomeDir.d.ts.map +1 -1
  58. package/node_modules/@happier-dev/cli-common/dist/providers/resolveHappyHomeDir.js +10 -1
  59. package/node_modules/@happier-dev/cli-common/dist/providers/resolveHappyHomeDir.js.map +1 -1
  60. package/node_modules/@happier-dev/cli-common/dist/providers/resolveHappyHomeDir.test.js +6 -0
  61. package/node_modules/@happier-dev/cli-common/dist/providers/resolveHappyHomeDir.test.js.map +1 -1
  62. package/node_modules/@happier-dev/protocol/dist/.tsbuildinfo +1 -1
  63. package/node_modules/@happier-dev/protocol/dist/e2e/providerSpec.d.ts +2 -2
  64. package/node_modules/@happier-dev/protocol/dist/features/decision.d.ts +2 -2
  65. package/node_modules/@happier-dev/protocol/dist/features/payload/capabilities/authCapabilities.d.ts +1 -1
  66. package/node_modules/@happier-dev/protocol/dist/features/payload/capabilities/capabilitiesSchema.d.ts +1 -1
  67. package/node_modules/@happier-dev/protocol/dist/features/payload/featuresResponseSchema.d.ts +1 -1
  68. package/node_modules/@happier-dev/protocol/dist/structuredMessages/reviewFindingsV1.d.ts +3 -3
  69. package/node_modules/@happier-dev/protocol/dist/structuredMessages/reviewFindingsV2.d.ts +1 -1
  70. package/node_modules/@happier-dev/release-runtime/dist/.tsbuildinfo +1 -1
  71. package/package.json +1 -1
  72. package/scripts/doctor.mjs +10 -2
  73. package/scripts/env.mjs +2 -5
  74. package/scripts/env_cmd.test.mjs +32 -0
  75. package/scripts/ghops.mjs +7 -4
  76. package/scripts/ghops.test.mjs +42 -1
  77. package/scripts/happier.mjs +2 -2
  78. package/scripts/menubar.mjs +2 -2
  79. package/scripts/run.mjs +2 -2
  80. package/scripts/self_host_runtime.mjs +10 -7
  81. package/scripts/self_host_runtime.test.mjs +47 -0
  82. package/scripts/service.mjs +10 -3
  83. package/scripts/stack.mjs +3 -2
  84. package/scripts/tailscale.mjs +2 -2
  85. package/scripts/utils/env/config.mjs +2 -7
  86. package/scripts/utils/env/env.mjs +9 -5
  87. package/scripts/utils/env/env_local.mjs +2 -1
  88. package/scripts/utils/paths/canonical_home.mjs +11 -3
  89. package/scripts/utils/paths/canonical_home.test.mjs +11 -3
  90. package/scripts/utils/paths/paths.mjs +17 -5
  91. package/scripts/utils/paths/paths_home_env.test.mjs +43 -0
  92. package/scripts/utils/proc/pm.mjs +9 -4
  93. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +28 -0
  94. package/scripts/utils/review/targets.mjs +2 -2
  95. package/scripts/utils/server/apply_server_light_env_defaults.mjs +4 -3
  96. package/scripts/utils/server/apply_server_light_env_defaults.test.mjs +25 -0
  97. package/scripts/utils/server/urls.mjs +3 -5
  98. package/scripts/utils/server/urls.test.mjs +22 -1
  99. package/scripts/utils/service/stack_autostart_resolution.mjs +6 -2
  100. package/scripts/utils/service/stack_autostart_resolution.test.mjs +13 -1
  101. package/scripts/utils/stack/context.mjs +2 -3
  102. package/scripts/utils/stack/context.test.mjs +18 -0
  103. package/scripts/utils/stack/dirs.mjs +4 -4
  104. package/scripts/utils/stack/dirs.test.mjs +33 -0
  105. package/scripts/utils/stack/stop.mjs +2 -2
  106. package/scripts/where.mjs +15 -4
  107. package/scripts/worktrees.mjs +2 -1
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { spawnSync } from 'node:child_process';
3
- import { chmodSync, mkdtempSync, writeFileSync } from 'node:fs';
3
+ import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join, resolve } from 'node:path';
6
6
  import test from 'node:test';
@@ -76,3 +76,44 @@ process.stdout.write(JSON.stringify(payload));
76
76
  assert.equal(out.env.GH_PROMPT_DISABLED, '1');
77
77
  assert.equal(out.env.GH_CONFIG_DIR, configDir);
78
78
  });
79
+
80
+ test('expands ~/ overrides for gh binary and config dir against HOME', () => {
81
+ const dir = mkdtempSync(join(tmpdir(), 'ghops-home-test-'));
82
+ const homeDir = join(dir, 'home');
83
+ const fakeGh = join(homeDir, 'bin', 'fake-gh');
84
+ const configDir = join(homeDir, 'gh-config');
85
+ mkdirSync(join(homeDir, 'bin'), { recursive: true });
86
+
87
+ writeFileSync(
88
+ fakeGh,
89
+ `#!/usr/bin/env node
90
+ const payload = {
91
+ argv: process.argv.slice(2),
92
+ env: {
93
+ GH_TOKEN: process.env.GH_TOKEN ?? null,
94
+ GH_PROMPT_DISABLED: process.env.GH_PROMPT_DISABLED ?? null,
95
+ GH_CONFIG_DIR: process.env.GH_CONFIG_DIR ?? null,
96
+ },
97
+ };
98
+ process.stdout.write(JSON.stringify(payload));
99
+ `,
100
+ 'utf8',
101
+ );
102
+ chmodSync(fakeGh, 0o755);
103
+
104
+ const token = 'test-bot-token';
105
+ const res = runGhop(['api', 'user'], {
106
+ HOME: homeDir,
107
+ USERPROFILE: homeDir,
108
+ HAPPIER_GITHUB_BOT_TOKEN: token,
109
+ HAPPIER_GHOPS_GH_PATH: '~/bin/fake-gh',
110
+ HAPPIER_GHOPS_CONFIG_DIR: '~/gh-config',
111
+ });
112
+
113
+ assert.equal(res.status, 0, res.stderr);
114
+ const out = JSON.parse(res.stdout);
115
+ assert.deepEqual(out.argv, ['api', 'user']);
116
+ assert.equal(out.env.GH_TOKEN, token);
117
+ assert.equal(out.env.GH_PROMPT_DISABLED, '1');
118
+ assert.equal(out.env.GH_CONFIG_DIR, configDir);
119
+ });
@@ -5,7 +5,7 @@ import { createRequire } from 'node:module';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { parseArgs } from './utils/cli/args.mjs';
7
7
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
8
- import { getComponentDir, getRootDir, getStackName } from './utils/paths/paths.mjs';
8
+ import { getComponentDir, getRootDir, getStackName, resolveExplicitStackEnvFilePath } from './utils/paths/paths.mjs';
9
9
  import { resolveCliHomeDir } from './utils/stack/dirs.mjs';
10
10
  import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv } from './utils/server/urls.mjs';
11
11
  import { resolveLocalServerPortForStack } from './utils/server/resolve_stack_server_port.mjs';
@@ -328,7 +328,7 @@ async function main() {
328
328
  // We treat an invocation as "stack-scoped" only when the stack env file actually exists (or when the
329
329
  // CLI home dir is explicitly overridden by the stack). This keeps plain `hstack happier` able to
330
330
  // reuse the user's CLI settings even when stack helper env vars are present in test/dev harnesses.
331
- const stackEnvFilePath = String(env.HAPPIER_STACK_ENV_FILE ?? '').trim();
331
+ const stackEnvFilePath = resolveExplicitStackEnvFilePath(env);
332
332
  const isStackScopedInvocation =
333
333
  Boolean(String(env.HAPPIER_STACK_CLI_HOME_DIR ?? '').trim()) ||
334
334
  Boolean(stackEnvFilePath && existsSync(stackEnvFilePath));
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { createHash } from 'node:crypto';
7
- import { getHappyStacksHomeDir, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
7
+ import { getHappyStacksHomeDir, getRootDir, resolveExplicitStackEnvFilePath, resolveStackEnvPath } from './utils/paths/paths.mjs';
8
8
  import { parseArgs } from './utils/cli/args.mjs';
9
9
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
10
10
  import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
@@ -208,7 +208,7 @@ async function main() {
208
208
  const pluginBasename = String(process.env.HAPPIER_STACK_SWIFTBAR_PLUGIN_BASENAME ?? '').trim() || defaultBasename;
209
209
  const pluginFile = `${pluginBasename}.${interval}.sh`;
210
210
 
211
- const defaultEnvFile = (process.env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim();
211
+ const defaultEnvFile = resolveExplicitStackEnvFilePath(process.env);
212
212
  const resolvedEnvFile = explicitStack
213
213
  ? resolveStackEnvPath(normalizedStack, process.env).envPath
214
214
  : (defaultEnvFile || resolveStackEnvPath(normalizedStack, process.env).envPath);
package/scripts/run.mjs CHANGED
@@ -2,7 +2,7 @@ import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
3
  import { pathExists } from './utils/fs/fs.mjs';
4
4
  import { killProcessTree, runCapture, spawnProc } from './utils/proc/proc.mjs';
5
- import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
5
+ import { getComponentDir, getDefaultAutostartPaths, getRootDir, resolveExplicitStackEnvFilePath } from './utils/paths/paths.mjs';
6
6
  import { killPortListeners } from './utils/net/ports.mjs';
7
7
  import { getServerComponentName, isHappierServerRunning, waitForServerReady } from './utils/server/server.mjs';
8
8
  import { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/proc/pm.mjs';
@@ -387,7 +387,7 @@ async function main() {
387
387
  if (serverComponentName === 'happier-server') {
388
388
  const managed = (baseEnv.HAPPIER_STACK_MANAGED_INFRA ?? '1') !== '0';
389
389
  if (managed) {
390
- const envPath = baseEnv.HAPPIER_STACK_ENV_FILE ?? '';
390
+ const envPath = resolveExplicitStackEnvFilePath(baseEnv);
391
391
  const infra = await ensureHappyServerManagedInfra({
392
392
  stackName: autostart.stackName,
393
393
  baseDir: autostart.baseDir,
@@ -35,6 +35,8 @@ import {
35
35
  } from '@happier-dev/cli-common/service';
36
36
  import {
37
37
  parseEnvText as parseEnvTextShared,
38
+ resolveConfiguredRelayRuntimeBinaryOverride,
39
+ resolveConfiguredRelayRuntimePaths,
38
40
  renderSelfHostServerEnvText as renderSelfHostServerEnvTextShared,
39
41
  } from '@happier-dev/cli-common/firstPartyRuntime';
40
42
  import { DEFAULT_MINISIGN_PUBLIC_KEY } from '@happier-dev/release-runtime/minisign';
@@ -469,13 +471,14 @@ async function applySelfHostSqliteMigrationsAtInstallTime({ env }) {
469
471
  return { applied: appliedNow, skipped: false, reason: 'ok' };
470
472
  }
471
473
 
472
- function resolveConfig({ channel, mode = 'user', platform = process.platform } = {}) {
474
+ export function resolveConfig({ channel, mode = 'user', platform = process.platform } = {}) {
473
475
  const defaults = resolveSelfHostDefaults({ platform, mode, channel, homeDir: homedir() });
474
- const installRoot = String(process.env.HAPPIER_SELF_HOST_INSTALL_ROOT ?? defaults.installRoot).trim();
475
- const binDir = String(process.env.HAPPIER_SELF_HOST_BIN_DIR ?? defaults.binDir).trim();
476
- const configDir = String(process.env.HAPPIER_SELF_HOST_CONFIG_DIR ?? defaults.configDir).trim();
477
- const dataDir = String(process.env.HAPPIER_SELF_HOST_DATA_DIR ?? defaults.dataDir).trim();
478
- const logDir = String(process.env.HAPPIER_SELF_HOST_LOG_DIR ?? defaults.logDir).trim();
476
+ const configuredPaths = resolveConfiguredRelayRuntimePaths({ defaults, env: process.env });
477
+ const installRoot = configuredPaths.installRoot;
478
+ const binDir = configuredPaths.binDir;
479
+ const configDir = configuredPaths.configDir;
480
+ const dataDir = configuredPaths.dataDir;
481
+ const logDir = configuredPaths.logDir;
479
482
  const serviceName = String(process.env.HAPPIER_SELF_HOST_SERVICE_NAME ?? DEFAULTS.serviceName).trim();
480
483
  const serverHost = String(process.env.HAPPIER_SERVER_HOST ?? DEFAULTS.serverHost).trim();
481
484
  const serverPort = parsePort(process.env.HAPPIER_SERVER_PORT, DEFAULTS.serverPort);
@@ -1914,7 +1917,7 @@ async function cmdInstall({ channel, mode, argv, json }) {
1914
1917
  || parseBoolean(process.env.HAPPIER_WITH_UI, true) === false
1915
1918
  || parseBoolean(process.env.HAPPIER_SELF_HOST_WITH_UI, true) === false);
1916
1919
  const nonInteractive = argvSansEnv.includes('--non-interactive') || parseBoolean(process.env.HAPPIER_NONINTERACTIVE, false);
1917
- const serverBinaryOverride = String(process.env.HAPPIER_SELF_HOST_SERVER_BINARY ?? '').trim();
1920
+ const serverBinaryOverride = resolveConfiguredRelayRuntimeBinaryOverride(process.env);
1918
1921
 
1919
1922
  if (normalizeOs(config.platform) !== 'windows' && !commandExists('tar')) {
1920
1923
  throw new Error('[self-host] tar is required to extract release artifacts');
@@ -983,6 +983,53 @@ test('resolveSelfHostDefaults isolates publicdev into a side-by-side self-host r
983
983
  assert.equal(cfg.serviceName, 'happier-server-dev');
984
984
  });
985
985
 
986
+ test('resolveConfig expands ~/ self-host path overrides against HOME', async () => {
987
+ const mod = await import('./self_host_runtime.mjs');
988
+ assert.equal(typeof mod.resolveConfig, 'function');
989
+
990
+ const previous = {
991
+ HOME: process.env.HOME,
992
+ USERPROFILE: process.env.USERPROFILE,
993
+ HAPPIER_SELF_HOST_INSTALL_ROOT: process.env.HAPPIER_SELF_HOST_INSTALL_ROOT,
994
+ HAPPIER_SELF_HOST_BIN_DIR: process.env.HAPPIER_SELF_HOST_BIN_DIR,
995
+ HAPPIER_SELF_HOST_CONFIG_DIR: process.env.HAPPIER_SELF_HOST_CONFIG_DIR,
996
+ HAPPIER_SELF_HOST_DATA_DIR: process.env.HAPPIER_SELF_HOST_DATA_DIR,
997
+ HAPPIER_SELF_HOST_LOG_DIR: process.env.HAPPIER_SELF_HOST_LOG_DIR,
998
+ };
999
+
1000
+ process.env.HOME = '/scoped/home';
1001
+ process.env.USERPROFILE = '/scoped/home';
1002
+ process.env.HAPPIER_SELF_HOST_INSTALL_ROOT = '~/relay/install';
1003
+ process.env.HAPPIER_SELF_HOST_BIN_DIR = '~/relay/bin';
1004
+ process.env.HAPPIER_SELF_HOST_CONFIG_DIR = '~/relay/config';
1005
+ process.env.HAPPIER_SELF_HOST_DATA_DIR = '~/relay/data';
1006
+ process.env.HAPPIER_SELF_HOST_LOG_DIR = '~/relay/logs';
1007
+
1008
+ try {
1009
+ const config = mod.resolveConfig({ platform: 'linux', mode: 'user', channel: 'stable' });
1010
+ assert.equal(config.installRoot, '/scoped/home/relay/install');
1011
+ assert.equal(config.binDir, '/scoped/home/relay/bin');
1012
+ assert.equal(config.configDir, '/scoped/home/relay/config');
1013
+ assert.equal(config.dataDir, '/scoped/home/relay/data');
1014
+ assert.equal(config.logDir, '/scoped/home/relay/logs');
1015
+ } finally {
1016
+ if (previous.HOME === undefined) delete process.env.HOME;
1017
+ else process.env.HOME = previous.HOME;
1018
+ if (previous.USERPROFILE === undefined) delete process.env.USERPROFILE;
1019
+ else process.env.USERPROFILE = previous.USERPROFILE;
1020
+ if (previous.HAPPIER_SELF_HOST_INSTALL_ROOT === undefined) delete process.env.HAPPIER_SELF_HOST_INSTALL_ROOT;
1021
+ else process.env.HAPPIER_SELF_HOST_INSTALL_ROOT = previous.HAPPIER_SELF_HOST_INSTALL_ROOT;
1022
+ if (previous.HAPPIER_SELF_HOST_BIN_DIR === undefined) delete process.env.HAPPIER_SELF_HOST_BIN_DIR;
1023
+ else process.env.HAPPIER_SELF_HOST_BIN_DIR = previous.HAPPIER_SELF_HOST_BIN_DIR;
1024
+ if (previous.HAPPIER_SELF_HOST_CONFIG_DIR === undefined) delete process.env.HAPPIER_SELF_HOST_CONFIG_DIR;
1025
+ else process.env.HAPPIER_SELF_HOST_CONFIG_DIR = previous.HAPPIER_SELF_HOST_CONFIG_DIR;
1026
+ if (previous.HAPPIER_SELF_HOST_DATA_DIR === undefined) delete process.env.HAPPIER_SELF_HOST_DATA_DIR;
1027
+ else process.env.HAPPIER_SELF_HOST_DATA_DIR = previous.HAPPIER_SELF_HOST_DATA_DIR;
1028
+ if (previous.HAPPIER_SELF_HOST_LOG_DIR === undefined) delete process.env.HAPPIER_SELF_HOST_LOG_DIR;
1029
+ else process.env.HAPPIER_SELF_HOST_LOG_DIR = previous.HAPPIER_SELF_HOST_LOG_DIR;
1030
+ }
1031
+ });
1032
+
986
1033
  test('resolveMinisignPublicKeyText prefers inline override and otherwise returns bundled key', () => {
987
1034
  const bundled = resolveMinisignPublicKeyText({});
988
1035
  assert.match(bundled, /minisign public key/i);
@@ -1,6 +1,13 @@
1
1
  import './utils/env/env.mjs';
2
2
  import { run, runCapture } from './utils/proc/proc.mjs';
3
- import { getComponentDir, getDefaultAutostartPaths, getRootDir, getSystemdUnitInfo, resolveStackEnvPath } from './utils/paths/paths.mjs';
3
+ import {
4
+ getComponentDir,
5
+ getDefaultAutostartPaths,
6
+ getRootDir,
7
+ getSystemdUnitInfo,
8
+ resolveExplicitStackEnvFilePath,
9
+ resolveStackEnvPath,
10
+ } from './utils/paths/paths.mjs';
4
11
  import { getInternalServerUrl, getPublicServerUrlEnvOverride } from './utils/server/urls.mjs';
5
12
  import { resolveServerUrls } from './utils/server/urls.mjs';
6
13
  import { installService as installManagedService, uninstallService as uninstallManagedService } from './utils/service/service_manager.mjs';
@@ -67,7 +74,7 @@ function getAutostartEnv({ mode, systemUserHomeDir } = {}) {
67
74
  // Main installs:
68
75
  // - default to the main stack env (outside the repo): ~/.happier/stacks/main/env
69
76
 
70
- const explicitEnvFilePath = process.env.HAPPIER_STACK_ENV_FILE?.trim() ? process.env.HAPPIER_STACK_ENV_FILE.trim() : '';
77
+ const explicitEnvFilePath = resolveExplicitStackEnvFilePath(process.env);
71
78
  const defaultMainEnvFilePath = resolveStackEnvPath('main').envPath;
72
79
  const envFile = resolveAutostartEnvFilePath({
73
80
  mode,
@@ -546,7 +553,7 @@ async function stopLaunchAgent({ persistent, requestedBy = 'service stop', reaso
546
553
 
547
554
  const envFile = resolveAutostartEnvFilePath({
548
555
  mode: 'user',
549
- explicitEnvFilePath: process.env.HAPPIER_STACK_ENV_FILE?.trim() ? process.env.HAPPIER_STACK_ENV_FILE.trim() : '',
556
+ explicitEnvFilePath: resolveExplicitStackEnvFilePath(process.env),
550
557
  defaultMainEnvFilePath: resolveStackEnvPath('main').envPath,
551
558
  systemUserHomeDir: null,
552
559
  });
package/scripts/stack.mjs CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  getRootDir,
16
16
  getWorkspaceDir,
17
17
  happyMonorepoSubdirForComponent,
18
+ resolveExplicitStackEnvFilePath,
18
19
  resolveStackEnvPath,
19
20
  } from './utils/paths/paths.mjs';
20
21
  import { isTcpPortFree, pickNextFreeTcpPort } from './utils/net/ports.mjs';
@@ -1115,7 +1116,7 @@ async function cmdCreateDevAuthSeed({ rootDir, argv }) {
1115
1116
  serverPort,
1116
1117
  internalServerUrl,
1117
1118
  publicServerUrl,
1118
- envPath: env.HAPPIER_STACK_ENV_FILE ?? '',
1119
+ envPath: resolveExplicitStackEnvFilePath(env),
1119
1120
  stackMode: true,
1120
1121
  runtimeStatePath: null,
1121
1122
  serverAlreadyRunning: false,
@@ -1141,7 +1142,7 @@ async function cmdCreateDevAuthSeed({ rootDir, argv }) {
1141
1142
  stackMode: true,
1142
1143
  runtimeStatePath: null,
1143
1144
  stackName: name,
1144
- envPath: env.HAPPIER_STACK_ENV_FILE ?? '',
1145
+ envPath: resolveExplicitStackEnvFilePath(env),
1145
1146
  children,
1146
1147
  spawnOptions: quietAuthFlow ? { silent: true, teeFile: expoLogPath, teeLabel: 'expo' } : {},
1147
1148
  quiet: quietAuthFlow,
@@ -4,7 +4,7 @@ import { run } from './utils/proc/proc.mjs';
4
4
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
5
5
  import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
6
6
  import { getInternalServerUrl } from './utils/server/urls.mjs';
7
- import { getStackName, resolveStackEnvPath } from './utils/paths/paths.mjs';
7
+ import { getStackName, resolveExplicitStackEnvFilePath, resolveStackEnvPath } from './utils/paths/paths.mjs';
8
8
  import { banner, bullets, cmd as cmdFmt, kv, ok, sectionTitle } from './utils/ui/layout.mjs';
9
9
  import { cyan, dim, green } from './utils/ui/ansi.mjs';
10
10
  import {
@@ -352,7 +352,7 @@ async function main() {
352
352
  );
353
353
  }
354
354
  const stackName = getStackName(process.env);
355
- const envPath = (process.env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim() || resolveStackEnvPath(stackName, process.env).envPath;
355
+ const envPath = resolveExplicitStackEnvFilePath(process.env) || resolveStackEnvPath(stackName, process.env).envPath;
356
356
  const { upstream } = getServeConfig(internalServerUrl);
357
357
  const res = await tailscaleServeEnable({ internalServerUrl });
358
358
  if (res?.enableUrl && !res?.httpsUrl) {
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { ensureEnvFileUpdated } from './env_file.mjs';
3
- import { getHappyStacksHomeDir, resolveStackEnvPath } from '../paths/paths.mjs';
3
+ import { getHappyStacksHomeDir, resolveActiveStackEnvFilePath } from '../paths/paths.mjs';
4
4
  import { getCanonicalHomeDirFromEnv } from '../paths/canonical_home.mjs';
5
5
 
6
6
  export function getHomeEnvPath() {
@@ -20,17 +20,12 @@ export function getHomeEnvLocalPath() {
20
20
  }
21
21
 
22
22
  export function resolveUserConfigEnvPath({ cliRootDir }) {
23
- const explicit = (process.env.HAPPIER_STACK_ENV_FILE ?? '').trim();
24
- if (explicit) {
25
- return explicit;
26
- }
27
-
28
23
  // By default, persist configuration to the main stack env file so config is
29
24
  // outside the repo and consistent across install modes.
30
25
  //
31
26
  // This also matches the stack env precedence in scripts/utils/env.mjs.
32
27
  void cliRootDir;
33
- return resolveStackEnvPath('main').envPath;
28
+ return resolveActiveStackEnvFilePath('main', process.env);
34
29
  }
35
30
 
36
31
  export async function ensureHomeEnvUpdated({ updates }) {
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../paths/canonical_home.mjs';
6
+ import { getStacksStorageRoot, resolveExplicitStackEnvFilePath } from '../paths/paths.mjs';
6
7
  import { isSandboxed, sandboxAllowsGlobalSideEffects } from './sandbox.mjs';
7
8
  import { loadEnvFile, loadEnvFileIgnoringPrefixes } from './load_env_file.mjs';
8
9
 
@@ -16,7 +17,7 @@ const __cliRootDir = dirname(__scriptsDir);
16
17
  function resolveHomeDir() {
17
18
  const fromEnv = (process.env.HAPPIER_STACK_HOME_DIR ?? '').trim();
18
19
  if (fromEnv) {
19
- return expandHome(fromEnv);
20
+ return expandHome(fromEnv, process.env);
20
21
  }
21
22
  return join(homedir(), '.happier-stack');
22
23
  }
@@ -76,13 +77,13 @@ if (hasHomeConfig) {
76
77
  if ((process.env.HAPPIER_STACK_DISABLE_STACK_ENV_AUTOLOAD ?? '').toString().trim() === '1') {
77
78
  return;
78
79
  }
79
- const stacksEnv = (process.env.HAPPIER_STACK_ENV_FILE ?? '').trim();
80
+ const stacksEnv = resolveExplicitStackEnvFilePath(process.env);
80
81
  if (stacksEnv) {
82
+ process.env.HAPPIER_STACK_ENV_FILE = stacksEnv;
81
83
  return;
82
84
  }
83
85
  const stackName = (process.env.HAPPIER_STACK_STACK ?? '').trim() || 'main';
84
- const stacksStorageRootRaw = (process.env.HAPPIER_STACK_STORAGE_DIR ?? '').trim();
85
- const stacksStorageRoot = stacksStorageRootRaw ? expandHome(stacksStorageRootRaw) : join(homedir(), '.happier', 'stacks');
86
+ const stacksStorageRoot = getStacksStorageRoot(process.env);
86
87
 
87
88
  const candidates = [
88
89
  join(stacksStorageRoot, stackName, 'env'),
@@ -98,7 +99,10 @@ if (hasHomeConfig) {
98
99
  // Stack env files intentionally include some non-prefixed keys (e.g. DATABASE_URL, HAPPIER_SERVER_LIGHT_DATA_DIR)
99
100
  // that must apply for true per-stack isolation. Do not filter by prefix here.
100
101
  {
101
- const stacksEnv = process.env.HAPPIER_STACK_ENV_FILE?.trim() ? process.env.HAPPIER_STACK_ENV_FILE.trim() : '';
102
+ const stacksEnv = resolveExplicitStackEnvFilePath(process.env);
103
+ if (stacksEnv) {
104
+ process.env.HAPPIER_STACK_ENV_FILE = stacksEnv;
105
+ }
102
106
  const unique = Array.from(new Set([stacksEnv].filter(Boolean)));
103
107
  for (const p of unique) {
104
108
  // eslint-disable-next-line no-await-in-loop
@@ -3,13 +3,14 @@ import { join } from 'node:path';
3
3
 
4
4
  import { ensureEnvFileUpdated } from './env_file.mjs';
5
5
  import { ensureUserConfigEnvUpdated, getHomeEnvLocalPath, getHomeEnvPath } from './config.mjs';
6
+ import { resolveExplicitStackEnvFilePath } from '../paths/paths.mjs';
6
7
 
7
8
  export async function ensureEnvLocalUpdated({ rootDir, updates }) {
8
9
  // Behavior:
9
10
  // - If a stack env file is explicitly set, write there (stack-scoped).
10
11
  // - If the user has run `hstack init` (home config exists), write to the main stack env file (user config).
11
12
  // - If no home config exists (legacy cloned-repo usage), write to <repo>/env.local for repo-local behavior.
12
- const explicit = (process.env.HAPPIER_STACK_ENV_FILE ?? '').trim();
13
+ const explicit = resolveExplicitStackEnvFilePath(process.env);
13
14
  if (explicit) {
14
15
  await ensureEnvFileUpdated({ envPath: explicit, updates });
15
16
  return;
@@ -1,13 +1,21 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
 
4
- export function expandHome(p) {
5
- return String(p ?? '').replace(/^~(?=[/\\])/, homedir());
4
+ function resolveHomeDirFromEnv(env = process.env) {
5
+ const candidate = process.platform === 'win32'
6
+ ? (env?.USERPROFILE ?? env?.HOME)
7
+ : env?.HOME;
8
+ const trimmed = String(candidate ?? '').trim();
9
+ return trimmed || homedir();
10
+ }
11
+
12
+ export function expandHome(p, env = process.env) {
13
+ return String(p ?? '').replace(/^~(?=[/\\])/, resolveHomeDirFromEnv(env));
6
14
  }
7
15
 
8
16
  export function getCanonicalHomeDirFromEnv(env = process.env) {
9
17
  const fromEnv = (env.HAPPIER_STACK_CANONICAL_HOME_DIR ?? '').trim();
10
- return fromEnv ? expandHome(fromEnv) : join(homedir(), '.happier-stack');
18
+ return fromEnv ? expandHome(fromEnv, env) : join(resolveHomeDirFromEnv(env), '.happier-stack');
11
19
  }
12
20
 
13
21
  export function getCanonicalHomeEnvPathFromEnv(env = process.env) {
@@ -5,10 +5,12 @@ import { homedir } from 'node:os';
5
5
  import { expandHome, getCanonicalHomeDirFromEnv, getCanonicalHomeEnvPathFromEnv } from './canonical_home.mjs';
6
6
 
7
7
  test('expandHome expands ~/ paths', () => {
8
+ assert.equal(expandHome('~/x', { HOME: '/scoped/home' }), '/scoped/home/x');
8
9
  assert.equal(expandHome('~/x'), `${homedir()}/x`);
9
10
  });
10
11
 
11
12
  test('expandHome expands ~\\ paths (Windows)', () => {
13
+ assert.equal(expandHome('~\\x', { HOME: '/scoped/home', USERPROFILE: '/scoped/home' }), '/scoped/home\\x');
12
14
  assert.equal(expandHome('~\\x'), `${homedir()}\\x`);
13
15
  });
14
16
 
@@ -18,11 +20,17 @@ test('expandHome leaves non-home-prefixed paths unchanged', () => {
18
20
  });
19
21
 
20
22
  test('getCanonicalHomeDirFromEnv expands override and defaults to ~/.happier-stack', () => {
21
- assert.equal(getCanonicalHomeDirFromEnv({ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' }), `${homedir()}/custom-home`);
23
+ assert.equal(
24
+ getCanonicalHomeDirFromEnv({ HOME: '/scoped/home', HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' }),
25
+ '/scoped/home/custom-home'
26
+ );
22
27
  assert.equal(getCanonicalHomeDirFromEnv({}), `${homedir()}/.happier-stack`);
23
28
  });
24
29
 
25
30
  test('getCanonicalHomeEnvPathFromEnv resolves .env under canonical home', () => {
26
- const envPath = getCanonicalHomeEnvPathFromEnv({ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' });
27
- assert.equal(envPath, `${homedir()}/custom-home/.env`);
31
+ const envPath = getCanonicalHomeEnvPathFromEnv({
32
+ HOME: '/scoped/home',
33
+ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home',
34
+ });
35
+ assert.equal(envPath, '/scoped/home/custom-home/.env');
28
36
  });
@@ -108,7 +108,7 @@ export function getRootDir(importMetaUrl) {
108
108
  export function getHappyStacksHomeDir(env = process.env) {
109
109
  const fromEnv = (env.HAPPIER_STACK_HOME_DIR ?? '').trim();
110
110
  if (fromEnv) {
111
- return expandHome(fromEnv);
111
+ return expandHome(fromEnv, env);
112
112
  }
113
113
  return PRIMARY_HOME_DIR;
114
114
  }
@@ -116,9 +116,9 @@ export function getHappyStacksHomeDir(env = process.env) {
116
116
  export function getWorkspaceDir(cliRootDir = null, env = process.env) {
117
117
  const fromEnv = (env.HAPPIER_STACK_WORKSPACE_DIR ?? '').trim();
118
118
  if (fromEnv) {
119
- return expandHome(fromEnv);
119
+ return expandHome(fromEnv, env);
120
120
  }
121
- const homeDir = getHappyStacksHomeDir();
121
+ const homeDir = getHappyStacksHomeDir(env);
122
122
  return join(homeDir, 'workspace');
123
123
  }
124
124
 
@@ -159,7 +159,7 @@ function normalizePathForEnv(rootDir, raw, env = process.env) {
159
159
  if (!trimmed) {
160
160
  return '';
161
161
  }
162
- const expanded = expandHome(trimmed);
162
+ const expanded = expandHome(trimmed, env);
163
163
  // If the path is relative, treat it as relative to the workspace root (default: repo root).
164
164
  const workspaceDir = getWorkspaceDir(rootDir, env);
165
165
  const abs = isAbsolute(expanded) || isWin32ShapedAbsolutePath(expanded);
@@ -244,11 +244,19 @@ export function getStackLabel(stackName = null, env = process.env) {
244
244
  export function getStacksStorageRoot(env = process.env) {
245
245
  const fromEnv = (env.HAPPIER_STACK_STORAGE_DIR ?? '').trim();
246
246
  if (fromEnv) {
247
- return expandHome(fromEnv);
247
+ return expandHome(fromEnv, env);
248
248
  }
249
249
  return PRIMARY_STORAGE_ROOT;
250
250
  }
251
251
 
252
+ export function resolveExplicitStackEnvFilePath(env = process.env) {
253
+ const explicit = (env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim();
254
+ if (!explicit) {
255
+ return '';
256
+ }
257
+ return expandHome(explicit, env);
258
+ }
259
+
252
260
  export function resolveStackBaseDir(stackName = null, env = process.env) {
253
261
  const name = (stackName ?? '').toString().trim() || getStackName(env);
254
262
  return { baseDir: join(getStacksStorageRoot(env), name), isLegacy: false };
@@ -260,6 +268,10 @@ export function resolveStackEnvPath(stackName = null, env = process.env) {
260
268
  return { envPath: join(baseDir, 'env'), isLegacy: false, baseDir };
261
269
  }
262
270
 
271
+ export function resolveActiveStackEnvFilePath(stackName = null, env = process.env) {
272
+ return resolveExplicitStackEnvFilePath(env) || resolveStackEnvPath(stackName, env).envPath;
273
+ }
274
+
263
275
  export function getDefaultAutostartPaths(env = process.env) {
264
276
  const stackName = getStackName(env);
265
277
  const { baseDir, isLegacy } = resolveStackBaseDir(stackName, env);
@@ -0,0 +1,43 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { join } from 'node:path';
4
+
5
+ import {
6
+ getHappyStacksHomeDir,
7
+ getStacksStorageRoot,
8
+ getWorkspaceDir,
9
+ resolveActiveStackEnvFilePath,
10
+ } from './paths.mjs';
11
+
12
+ test('stack path overrides expand ~/ against the provided HOME env', () => {
13
+ const env = {
14
+ HOME: '/scoped/home',
15
+ HAPPIER_STACK_HOME_DIR: '~/.happier-stack-custom',
16
+ HAPPIER_STACK_STORAGE_DIR: '~/.happier/stacks-custom',
17
+ };
18
+
19
+ assert.equal(getHappyStacksHomeDir(env), '/scoped/home/.happier-stack-custom');
20
+ assert.equal(getWorkspaceDir('/tmp/root', env), '/scoped/home/.happier-stack-custom/workspace');
21
+ assert.equal(getStacksStorageRoot(env), '/scoped/home/.happier/stacks-custom');
22
+ });
23
+
24
+ test('resolveActiveStackEnvFilePath expands ~/ explicit overrides against the provided HOME env', () => {
25
+ const env = {
26
+ HOME: '/scoped/home',
27
+ HAPPIER_STACK_ENV_FILE: '~/.happier/stacks/dev/env',
28
+ };
29
+
30
+ assert.equal(resolveActiveStackEnvFilePath('dev', env), '/scoped/home/.happier/stacks/dev/env');
31
+ });
32
+
33
+ test('resolveActiveStackEnvFilePath falls back to the resolved stack env file when no explicit override is set', () => {
34
+ const env = {
35
+ HOME: '/scoped/home',
36
+ HAPPIER_STACK_STORAGE_DIR: '~/.happier/stacks-custom',
37
+ };
38
+
39
+ assert.equal(
40
+ resolveActiveStackEnvFilePath('dev', env),
41
+ join('/scoped/home/.happier/stacks-custom', 'dev', 'env')
42
+ );
43
+ });
@@ -8,7 +8,12 @@ import { pathExists } from '../fs/fs.mjs';
8
8
  import { readJsonIfExists, writeJsonAtomic } from '../fs/json.mjs';
9
9
  import { run, runCapture, spawnProc } from './proc.mjs';
10
10
  import { commandExists } from './commands.mjs';
11
- import { coerceHappyMonorepoRootFromPath, getDefaultAutostartPaths, getHappyStacksHomeDir } from '../paths/paths.mjs';
11
+ import {
12
+ coerceHappyMonorepoRootFromPath,
13
+ getDefaultAutostartPaths,
14
+ getHappyStacksHomeDir,
15
+ resolveExplicitStackEnvFilePath,
16
+ } from '../paths/paths.mjs';
12
17
  import { resolveInstalledPath, resolveInstalledCliRoot } from '../paths/runtime.mjs';
13
18
  import { expandHome } from '../paths/canonical_home.mjs';
14
19
  import { withCliDistBuildLock } from './cliDistBuildLock.mjs';
@@ -325,12 +330,12 @@ function resolveStackCacheBaseDirFromEnv(env) {
325
330
  const explicit = (env.HAPPIER_STACK_PM_CACHE_BASE_DIR ?? '').toString().trim();
326
331
  if (explicit) {
327
332
  try {
328
- return resolve(expandHome(explicit));
333
+ return resolve(expandHome(explicit, env));
329
334
  } catch {
330
335
  return null;
331
336
  }
332
337
  }
333
- const envFile = (env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim();
338
+ const envFile = resolveExplicitStackEnvFilePath(env);
334
339
  if (!envFile) return null;
335
340
  try {
336
341
  return join(dirname(envFile), 'cache');
@@ -367,7 +372,7 @@ export async function applyStackCacheEnv(baseEnv) {
367
372
  env.NPM_CONFIG_PRODUCTION = 'false';
368
373
  }
369
374
 
370
- const envFile = (env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim();
375
+ const envFile = resolveExplicitStackEnvFilePath(env);
371
376
  const stackCacheBase = resolveStackCacheBaseDirFromEnv(env);
372
377
  if (!stackCacheBase) return env;
373
378
 
@@ -452,6 +452,34 @@ test('ensureDepsInstalled honors HAPPIER_STACK_PM_CACHE_BASE_DIR when no stack e
452
452
  assert.equal(parsed.HOME, join(cacheBase, 'home'));
453
453
  });
454
454
 
455
+ test('ensureDepsInstalled derives stack cache roots from ~/ env file overrides against HOME', async (t) => {
456
+ const fixture = await createStackCacheFixture(t, 'hs-pm-tilde-stack-env-file-');
457
+ const { root, componentDir, binDir } = fixture;
458
+ const outputPath = join(root, 'env.json');
459
+ const homeDir = join(root, 'home');
460
+ const expectedCacheBase = join(homeDir, '.happier', 'stacks', 'dev', 'cache');
461
+ const expectedStackHome = join(homeDir, '.happier', 'stacks', 'dev', 'home');
462
+ await writeYarnEnvDumpStub({ binDir, outputPath });
463
+
464
+ applyEnvOverrides(t, {
465
+ PATH: `${binDir}:${process.env.PATH ?? ''}`,
466
+ OUTPUT_PATH: outputPath,
467
+ HOME: homeDir,
468
+ USERPROFILE: homeDir,
469
+ HAPPIER_STACK_ENV_FILE: '~/.happier/stacks/dev/env',
470
+ XDG_CACHE_HOME: null,
471
+ YARN_CACHE_FOLDER: null,
472
+ npm_config_cache: null,
473
+ });
474
+
475
+ await ensureDepsInstalled(componentDir, 'test-component', { quiet: true });
476
+ const parsed = JSON.parse(await readFile(outputPath, 'utf-8'));
477
+ assert.equal(parsed.XDG_CACHE_HOME, join(expectedCacheBase, 'xdg'));
478
+ assert.equal(parsed.YARN_CACHE_FOLDER, join(expectedCacheBase, 'yarn'));
479
+ assert.equal(parsed.npm_config_cache, join(expectedCacheBase, 'npm'));
480
+ assert.equal(parsed.HOME, expectedStackHome);
481
+ });
482
+
455
483
  test('ensureDepsInstalled prefers the .nvmrc node runtime for yarn shebangs when available', async (t) => {
456
484
  const fixture = await createStackCacheFixture(t, 'hs-pm-nvm-node-runtime-');
457
485
  const { root, componentDir, binDir } = fixture;
@@ -1,8 +1,8 @@
1
- import { getRepoDir } from '../paths/paths.mjs';
1
+ import { getRepoDir, resolveExplicitStackEnvFilePath } from '../paths/paths.mjs';
2
2
 
3
3
  export function isStackMode(env = process.env) {
4
4
  const stack = String(env.HAPPIER_STACK_STACK ?? '').trim();
5
- const envFile = String(env.HAPPIER_STACK_ENV_FILE ?? '').trim();
5
+ const envFile = resolveExplicitStackEnvFilePath(env);
6
6
  return Boolean(stack && envFile);
7
7
  }
8
8
 
@@ -1,14 +1,15 @@
1
1
  import { join } from 'node:path';
2
+ import { expandHome } from '../paths/canonical_home.mjs';
2
3
 
3
4
  export function applyServerLightEnvDefaults({ baseEnv, serverEnv, baseDir }) {
4
5
  const dataDir = baseEnv.HAPPIER_SERVER_LIGHT_DATA_DIR?.trim()
5
- ? baseEnv.HAPPIER_SERVER_LIGHT_DATA_DIR.trim()
6
+ ? expandHome(baseEnv.HAPPIER_SERVER_LIGHT_DATA_DIR.trim(), baseEnv)
6
7
  : join(baseDir, 'server-light');
7
8
  serverEnv.HAPPIER_SERVER_LIGHT_DATA_DIR = dataDir;
8
9
  serverEnv.HAPPIER_SERVER_LIGHT_FILES_DIR = baseEnv.HAPPIER_SERVER_LIGHT_FILES_DIR?.trim()
9
- ? baseEnv.HAPPIER_SERVER_LIGHT_FILES_DIR.trim()
10
+ ? expandHome(baseEnv.HAPPIER_SERVER_LIGHT_FILES_DIR.trim(), baseEnv)
10
11
  : join(dataDir, 'files');
11
12
  serverEnv.HAPPIER_SERVER_LIGHT_DB_DIR = baseEnv.HAPPIER_SERVER_LIGHT_DB_DIR?.trim()
12
- ? baseEnv.HAPPIER_SERVER_LIGHT_DB_DIR.trim()
13
+ ? expandHome(baseEnv.HAPPIER_SERVER_LIGHT_DB_DIR.trim(), baseEnv)
13
14
  : join(dataDir, 'pglite');
14
15
  }