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,41 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { resolveMobileReachableServerUrl } from './mobile_api_url.mjs';
|
|
5
|
+
|
|
6
|
+
test('resolveMobileReachableServerUrl rewrites localhost to LAN IP (env override)', () => {
|
|
7
|
+
const out = resolveMobileReachableServerUrl({
|
|
8
|
+
env: { HAPPY_STACKS_LAN_IP: '192.168.0.50' },
|
|
9
|
+
serverUrl: 'http://localhost:3005',
|
|
10
|
+
serverPort: 3005,
|
|
11
|
+
});
|
|
12
|
+
assert.equal(out, 'http://192.168.0.50:3005');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('resolveMobileReachableServerUrl rewrites *.localhost to LAN IP (env override)', () => {
|
|
16
|
+
const out = resolveMobileReachableServerUrl({
|
|
17
|
+
env: { HAPPY_STACKS_LAN_IP: '10.0.0.12' },
|
|
18
|
+
serverUrl: 'http://happy-exp1.localhost:3009/',
|
|
19
|
+
serverPort: 3009,
|
|
20
|
+
});
|
|
21
|
+
assert.equal(out, 'http://10.0.0.12:3009');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('resolveMobileReachableServerUrl preserves path and query', () => {
|
|
25
|
+
const out = resolveMobileReachableServerUrl({
|
|
26
|
+
env: { HAPPY_STACKS_LAN_IP: '10.0.0.12' },
|
|
27
|
+
serverUrl: 'http://127.0.0.1:3005/api?x=1',
|
|
28
|
+
serverPort: 3005,
|
|
29
|
+
});
|
|
30
|
+
assert.equal(out, 'http://10.0.0.12:3005/api?x=1');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('resolveMobileReachableServerUrl does not rewrite non-local URLs', () => {
|
|
34
|
+
const out = resolveMobileReachableServerUrl({
|
|
35
|
+
env: { HAPPY_STACKS_LAN_IP: '192.168.0.50' },
|
|
36
|
+
serverUrl: 'https://my-machine.tailnet.ts.net',
|
|
37
|
+
serverPort: 3005,
|
|
38
|
+
});
|
|
39
|
+
assert.equal(out, 'https://my-machine.tailnet.ts.net');
|
|
40
|
+
});
|
|
41
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { isUnifiedHappyServerLight } from './flavor_scripts.mjs';
|
|
6
|
+
|
|
7
|
+
function extractPrismaClient(mod) {
|
|
8
|
+
return mod?.PrismaClient ?? mod?.default?.PrismaClient ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function importPrismaClientFromFile(path) {
|
|
12
|
+
const mod = await import(pathToFileURL(path).href);
|
|
13
|
+
const PrismaClient = extractPrismaClient(mod);
|
|
14
|
+
if (!PrismaClient) {
|
|
15
|
+
throw new Error(`[prisma] PrismaClient export not found in: ${path}`);
|
|
16
|
+
}
|
|
17
|
+
return PrismaClient;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function importPrismaClientFromNodeModules({ dir }) {
|
|
21
|
+
const req = createRequire(import.meta.url);
|
|
22
|
+
const resolved = req.resolve('@prisma/client', { paths: [dir] });
|
|
23
|
+
return await importPrismaClientFromFile(resolved);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function importPrismaClientFromGeneratedSqlite({ dir }) {
|
|
27
|
+
const path = join(dir, 'generated', 'sqlite-client', 'index.js');
|
|
28
|
+
return await importPrismaClientFromFile(path);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function importPrismaClientForHappyServerLight({ serverDir }) {
|
|
32
|
+
if (isUnifiedHappyServerLight({ serverDir })) {
|
|
33
|
+
return await importPrismaClientFromGeneratedSqlite({ dir: serverDir });
|
|
34
|
+
}
|
|
35
|
+
return await importPrismaClientFromNodeModules({ dir: serverDir });
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
importPrismaClientForHappyServerLight,
|
|
9
|
+
importPrismaClientFromGeneratedSqlite,
|
|
10
|
+
importPrismaClientFromNodeModules,
|
|
11
|
+
} from './prisma_import.mjs';
|
|
12
|
+
|
|
13
|
+
async function writeJson(path, obj) {
|
|
14
|
+
await writeFile(path, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('importPrismaClientFromNodeModules imports PrismaClient via node_modules resolution', async () => {
|
|
18
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-prisma-import-'));
|
|
19
|
+
try {
|
|
20
|
+
await mkdir(join(dir, 'node_modules', '@prisma', 'client'), { recursive: true });
|
|
21
|
+
await writeJson(join(dir, 'node_modules', '@prisma', 'client', 'package.json'), { name: '@prisma/client', type: 'module', main: './index.js' });
|
|
22
|
+
await writeFile(join(dir, 'node_modules', '@prisma', 'client', 'index.js'), 'export class PrismaClient {}\n', 'utf-8');
|
|
23
|
+
|
|
24
|
+
const PrismaClient = await importPrismaClientFromNodeModules({ dir });
|
|
25
|
+
assert.equal(typeof PrismaClient, 'function');
|
|
26
|
+
assert.equal(PrismaClient.name, 'PrismaClient');
|
|
27
|
+
} finally {
|
|
28
|
+
await rm(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('importPrismaClientFromGeneratedSqlite imports PrismaClient from generated/sqlite-client', async () => {
|
|
33
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-prisma-import-'));
|
|
34
|
+
try {
|
|
35
|
+
await writeJson(join(dir, 'package.json'), { type: 'module' });
|
|
36
|
+
await mkdir(join(dir, 'generated', 'sqlite-client'), { recursive: true });
|
|
37
|
+
await writeFile(join(dir, 'generated', 'sqlite-client', 'index.js'), 'export class PrismaClient {}\n', 'utf-8');
|
|
38
|
+
|
|
39
|
+
const PrismaClient = await importPrismaClientFromGeneratedSqlite({ dir });
|
|
40
|
+
assert.equal(typeof PrismaClient, 'function');
|
|
41
|
+
assert.equal(PrismaClient.name, 'PrismaClient');
|
|
42
|
+
} finally {
|
|
43
|
+
await rm(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('importPrismaClientForHappyServerLight prefers generated sqlite client when prisma/sqlite/schema.prisma exists', async () => {
|
|
48
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-prisma-import-'));
|
|
49
|
+
try {
|
|
50
|
+
await writeJson(join(dir, 'package.json'), { type: 'module' });
|
|
51
|
+
await mkdir(join(dir, 'prisma', 'sqlite'), { recursive: true });
|
|
52
|
+
await writeFile(join(dir, 'prisma', 'sqlite', 'schema.prisma'), 'datasource db { provider = "sqlite" }\n', 'utf-8');
|
|
53
|
+
|
|
54
|
+
await mkdir(join(dir, 'generated', 'sqlite-client'), { recursive: true });
|
|
55
|
+
await writeFile(join(dir, 'generated', 'sqlite-client', 'index.js'), 'export class PrismaClient {}\n', 'utf-8');
|
|
56
|
+
|
|
57
|
+
// Also create a node_modules PrismaClient, but unified should prefer generated.
|
|
58
|
+
await mkdir(join(dir, 'node_modules', '@prisma', 'client'), { recursive: true });
|
|
59
|
+
await writeJson(join(dir, 'node_modules', '@prisma', 'client', 'package.json'), { name: '@prisma/client', type: 'module', main: './index.js' });
|
|
60
|
+
await writeFile(join(dir, 'node_modules', '@prisma', 'client', 'index.js'), 'export class PrismaClient { static which = "node_modules"; }\n', 'utf-8');
|
|
61
|
+
|
|
62
|
+
const PrismaClient = await importPrismaClientForHappyServerLight({ serverDir: dir });
|
|
63
|
+
assert.equal(typeof PrismaClient, 'function');
|
|
64
|
+
assert.equal(PrismaClient.name, 'PrismaClient');
|
|
65
|
+
// If we imported node_modules we'd have this static property.
|
|
66
|
+
assert.equal(PrismaClient.which, undefined);
|
|
67
|
+
} finally {
|
|
68
|
+
await rm(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function resolveServerUiEnv({ serveUi, uiBuildDir, uiPrefix, uiBuildDirExists }) {
|
|
2
|
+
if (!serveUi) return {};
|
|
3
|
+
if (!uiBuildDirExists) return {};
|
|
4
|
+
if (!uiBuildDir) return {};
|
|
5
|
+
|
|
6
|
+
// Set both the canonical env vars (new) and legacy keys (for older server builds).
|
|
7
|
+
return {
|
|
8
|
+
HAPPY_SERVER_UI_DIR: uiBuildDir,
|
|
9
|
+
HAPPY_SERVER_UI_PREFIX: uiPrefix,
|
|
10
|
+
HAPPY_SERVER_LIGHT_UI_DIR: uiBuildDir,
|
|
11
|
+
HAPPY_SERVER_LIGHT_UI_PREFIX: uiPrefix,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { resolveServerUiEnv } from './ui_env.mjs';
|
|
5
|
+
|
|
6
|
+
test('resolveServerUiEnv returns empty when UI serving is disabled', () => {
|
|
7
|
+
assert.deepEqual(
|
|
8
|
+
resolveServerUiEnv({
|
|
9
|
+
serveUi: false,
|
|
10
|
+
uiBuildDir: '/tmp/ui',
|
|
11
|
+
uiPrefix: '/',
|
|
12
|
+
uiBuildDirExists: true,
|
|
13
|
+
}),
|
|
14
|
+
{}
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('resolveServerUiEnv returns empty when UI build dir is missing', () => {
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
resolveServerUiEnv({
|
|
21
|
+
serveUi: true,
|
|
22
|
+
uiBuildDir: '/tmp/ui',
|
|
23
|
+
uiPrefix: '/',
|
|
24
|
+
uiBuildDirExists: false,
|
|
25
|
+
}),
|
|
26
|
+
{}
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('resolveServerUiEnv sets both canonical and legacy env keys when enabled', () => {
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
resolveServerUiEnv({
|
|
33
|
+
serveUi: true,
|
|
34
|
+
uiBuildDir: '/tmp/ui',
|
|
35
|
+
uiPrefix: '/ui',
|
|
36
|
+
uiBuildDirExists: true,
|
|
37
|
+
}),
|
|
38
|
+
{
|
|
39
|
+
HAPPY_SERVER_UI_DIR: '/tmp/ui',
|
|
40
|
+
HAPPY_SERVER_UI_PREFIX: '/ui',
|
|
41
|
+
HAPPY_SERVER_LIGHT_UI_DIR: '/tmp/ui',
|
|
42
|
+
HAPPY_SERVER_LIGHT_UI_PREFIX: '/ui',
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
3
|
import { getStackName, resolveStackEnvPath } from '../paths/paths.mjs';
|
|
4
|
+
import { preferStackLocalhostUrl } from '../paths/localhost_host.mjs';
|
|
4
5
|
import { resolvePublicServerUrl } from '../../tailscale.mjs';
|
|
5
6
|
import { resolveServerPortFromEnv } from './port.mjs';
|
|
7
|
+
import { normalizeUrlNoTrailingSlash } from '../net/url.mjs';
|
|
6
8
|
|
|
7
9
|
function stackEnvExplicitlySetsPublicUrl({ env, stackName }) {
|
|
8
10
|
try {
|
|
@@ -31,14 +33,15 @@ function stackEnvExplicitlySetsWebappUrl({ env, stackName }) {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function getPublicServerUrlEnvOverride({ env = process.env, serverPort, stackName = null } = {}) {
|
|
34
|
-
const defaultPublicUrl = `http://localhost:${serverPort}`;
|
|
35
36
|
const name =
|
|
36
37
|
(stackName ?? '').toString().trim() ||
|
|
37
38
|
(env.HAPPY_STACKS_STACK ?? env.HAPPY_LOCAL_STACK ?? '').toString().trim() ||
|
|
38
|
-
getStackName();
|
|
39
|
+
getStackName(env);
|
|
40
|
+
const defaultPublicUrl = `http://localhost:${serverPort}`;
|
|
39
41
|
|
|
40
42
|
let envPublicUrl =
|
|
41
43
|
(env.HAPPY_STACKS_SERVER_URL ?? env.HAPPY_LOCAL_SERVER_URL ?? '').toString().trim() || '';
|
|
44
|
+
envPublicUrl = normalizeUrlNoTrailingSlash(envPublicUrl);
|
|
42
45
|
|
|
43
46
|
// Safety: for non-main stacks, ignore a global SERVER_URL unless it was explicitly set in the stack env file.
|
|
44
47
|
if (name !== 'main' && envPublicUrl && !stackEnvExplicitlySetsPublicUrl({ env, stackName: name })) {
|
|
@@ -52,7 +55,7 @@ export function getWebappUrlEnvOverride({ env = process.env, stackName = null }
|
|
|
52
55
|
const name =
|
|
53
56
|
(stackName ?? '').toString().trim() ||
|
|
54
57
|
(env.HAPPY_STACKS_STACK ?? env.HAPPY_LOCAL_STACK ?? '').toString().trim() ||
|
|
55
|
-
getStackName();
|
|
58
|
+
getStackName(env);
|
|
56
59
|
|
|
57
60
|
let envWebappUrl = (env.HAPPY_WEBAPP_URL ?? '').toString().trim() || '';
|
|
58
61
|
|
|
@@ -66,18 +69,25 @@ export function getWebappUrlEnvOverride({ env = process.env, stackName = null }
|
|
|
66
69
|
|
|
67
70
|
export async function resolveServerUrls({ env = process.env, serverPort, allowEnable = true } = {}) {
|
|
68
71
|
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
72
|
+
const stackName =
|
|
73
|
+
(env.HAPPY_STACKS_STACK ?? env.HAPPY_LOCAL_STACK ?? '').toString().trim() ||
|
|
74
|
+
getStackName(env);
|
|
69
75
|
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env, serverPort });
|
|
70
76
|
const resolved = await resolvePublicServerUrl({
|
|
71
77
|
internalServerUrl,
|
|
72
78
|
defaultPublicUrl,
|
|
73
79
|
envPublicUrl,
|
|
74
80
|
allowEnable,
|
|
81
|
+
stackName,
|
|
75
82
|
});
|
|
83
|
+
const publicServerUrl = normalizeUrlNoTrailingSlash(
|
|
84
|
+
await preferStackLocalhostUrl(resolved.publicServerUrl, { stackName })
|
|
85
|
+
);
|
|
76
86
|
return {
|
|
77
87
|
internalServerUrl,
|
|
78
88
|
defaultPublicUrl,
|
|
79
89
|
envPublicUrl,
|
|
80
|
-
publicServerUrl
|
|
90
|
+
publicServerUrl,
|
|
81
91
|
publicServerUrlSource: resolved.source,
|
|
82
92
|
};
|
|
83
93
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join, resolve, sep } from 'node:path';
|
|
3
|
-
import { getComponentsDir } from '../paths/paths.mjs';
|
|
3
|
+
import { getComponentRepoDir, getComponentsDir, isHappyMonorepoRoot } from '../paths/paths.mjs';
|
|
4
4
|
|
|
5
5
|
function isInside(path, dir) {
|
|
6
6
|
const p = resolve(path);
|
|
@@ -16,8 +16,14 @@ export function detectServerComponentDirMismatch({ rootDir, serverComponentName,
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
19
|
+
const expectedRepo = resolve(getComponentRepoDir(rootDir, serverComponentName));
|
|
20
|
+
const otherRepo = resolve(getComponentRepoDir(rootDir, other));
|
|
21
|
+
// Unified server flavors can legitimately share a single repo/dir.
|
|
22
|
+
if (expectedRepo === otherRepo) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const otherKey = isHappyMonorepoRoot(otherRepo) ? 'happy' : other;
|
|
26
|
+
const otherWts = resolve(componentsDir, '.worktrees', otherKey);
|
|
21
27
|
|
|
22
28
|
if (isInside(serverDir, otherRepo) || isInside(serverDir, otherWts)) {
|
|
23
29
|
return { expected: serverComponentName, actual: other, serverDir };
|
|
@@ -32,10 +38,13 @@ export function assertServerComponentDirMatches({ rootDir, serverComponentName,
|
|
|
32
38
|
return;
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
const expectedRepoDir = getComponentRepoDir(rootDir, mismatch.expected);
|
|
42
|
+
const expectedRepoKey = isHappyMonorepoRoot(expectedRepoDir) ? 'happy' : mismatch.expected;
|
|
43
|
+
|
|
35
44
|
const hint =
|
|
36
45
|
mismatch.expected === 'happy-server-light'
|
|
37
|
-
?
|
|
38
|
-
:
|
|
46
|
+
? `Fix: either switch flavor (\`happys srv use happy-server\`) or switch the active checkout for happy-server-light (\`happys wt use happy-server-light default\` or a worktree under .worktrees/${expectedRepoKey}/).`
|
|
47
|
+
: `Fix: either switch flavor (\`happys srv use happy-server-light\`) or switch the active checkout for happy-server (\`happys wt use happy-server default\` or a worktree under .worktrees/${expectedRepoKey}/).`;
|
|
39
48
|
|
|
40
49
|
throw new Error(
|
|
41
50
|
`[server] server component dir mismatch:\n` +
|
|
@@ -55,6 +64,11 @@ function detectPrismaProvider(schemaText) {
|
|
|
55
64
|
|
|
56
65
|
export function assertServerPrismaProviderMatches({ serverComponentName, serverDir }) {
|
|
57
66
|
const schemaPath = join(serverDir, 'prisma', 'schema.prisma');
|
|
67
|
+
const sqliteSchemaPaths = [
|
|
68
|
+
join(serverDir, 'prisma', 'sqlite', 'schema.prisma'),
|
|
69
|
+
join(serverDir, 'prisma', 'schema.sqlite.prisma'),
|
|
70
|
+
];
|
|
71
|
+
|
|
58
72
|
let schemaText = '';
|
|
59
73
|
try {
|
|
60
74
|
schemaText = readFileSync(schemaPath, 'utf-8');
|
|
@@ -64,17 +78,41 @@ export function assertServerPrismaProviderMatches({ serverComponentName, serverD
|
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
const provider = detectPrismaProvider(schemaText);
|
|
67
|
-
if (!provider)
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
81
|
+
if (!provider) return;
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
// Unified happy-server flavors:
|
|
84
|
+
// - full: prisma/schema.prisma (postgresql)
|
|
85
|
+
// - light: prisma/sqlite/schema.prisma (sqlite) (legacy: prisma/schema.sqlite.prisma)
|
|
86
|
+
if (serverComponentName === 'happy-server-light') {
|
|
87
|
+
for (const sqliteSchemaPath of sqliteSchemaPaths) {
|
|
88
|
+
try {
|
|
89
|
+
const sqliteSchemaText = readFileSync(sqliteSchemaPath, 'utf-8');
|
|
90
|
+
const sqliteProvider = detectPrismaProvider(sqliteSchemaText);
|
|
91
|
+
if (sqliteProvider && sqliteProvider !== 'sqlite') {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[server] happy-server-light expects Prisma datasource provider \"sqlite\", but found \"${sqliteProvider}\" in:\n` +
|
|
94
|
+
`- ${sqliteSchemaPath}\n` +
|
|
95
|
+
`Fix: point happy-server-light at a checkout that includes sqlite support, or switch server flavor to happy-server.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (sqliteProvider === 'sqlite') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Exists, but could not parse provider: keep checking other variants and fall through to legacy behavior.
|
|
102
|
+
} catch {
|
|
103
|
+
// missing/unreadable: try other variants and then fall through to legacy behavior below
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (provider !== 'sqlite') {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`[server] happy-server-light expects Prisma datasource provider \"sqlite\", but found \"${provider}\" in:\n` +
|
|
110
|
+
`- ${schemaPath}\n` +
|
|
111
|
+
`This usually means you're pointing happy-server-light at a postgres-only happy-server checkout/PR.\n` +
|
|
112
|
+
`Fix: either switch server flavor to happy-server, or use a checkout that supports the light flavor (e.g. one that contains prisma/sqlite/schema.prisma or prisma/schema.sqlite.prisma).`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
78
116
|
}
|
|
79
117
|
|
|
80
118
|
if (serverComponentName === 'happy-server' && provider === 'sqlite') {
|
|
@@ -85,4 +123,3 @@ export function assertServerPrismaProviderMatches({ serverComponentName, serverD
|
|
|
85
123
|
);
|
|
86
124
|
}
|
|
87
125
|
}
|
|
88
|
-
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { assertServerPrismaProviderMatches, detectServerComponentDirMismatch } from './validate.mjs';
|
|
8
|
+
|
|
9
|
+
const PG_SCHEMA = `
|
|
10
|
+
datasource db {
|
|
11
|
+
provider = "postgresql"
|
|
12
|
+
url = env("DATABASE_URL")
|
|
13
|
+
}
|
|
14
|
+
`.trim();
|
|
15
|
+
|
|
16
|
+
const SQLITE_SCHEMA = `
|
|
17
|
+
datasource db {
|
|
18
|
+
provider = "sqlite"
|
|
19
|
+
url = env("DATABASE_URL")
|
|
20
|
+
}
|
|
21
|
+
`.trim();
|
|
22
|
+
|
|
23
|
+
async function writeSchemas({ dir, schemaPrisma, schemaSqlitePrisma }) {
|
|
24
|
+
const prismaDir = join(dir, 'prisma');
|
|
25
|
+
await mkdir(prismaDir, { recursive: true });
|
|
26
|
+
if (schemaPrisma != null) {
|
|
27
|
+
await writeFile(join(prismaDir, 'schema.prisma'), schemaPrisma + '\n', 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
if (schemaSqlitePrisma != null) {
|
|
30
|
+
await mkdir(join(prismaDir, 'sqlite'), { recursive: true });
|
|
31
|
+
await writeFile(join(prismaDir, 'sqlite', 'schema.prisma'), schemaSqlitePrisma + '\n', 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('assertServerPrismaProviderMatches accepts unified light flavor (prisma/sqlite/schema.prisma)', async () => {
|
|
36
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-validate-'));
|
|
37
|
+
try {
|
|
38
|
+
await writeSchemas({ dir, schemaPrisma: PG_SCHEMA, schemaSqlitePrisma: SQLITE_SCHEMA });
|
|
39
|
+
assert.doesNotThrow(() => assertServerPrismaProviderMatches({ serverComponentName: 'happy-server-light', serverDir: dir }));
|
|
40
|
+
} finally {
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('assertServerPrismaProviderMatches rejects happy-server-light when only postgres schema exists', async () => {
|
|
46
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-validate-'));
|
|
47
|
+
try {
|
|
48
|
+
await writeSchemas({ dir, schemaPrisma: PG_SCHEMA, schemaSqlitePrisma: null });
|
|
49
|
+
assert.throws(() => assertServerPrismaProviderMatches({ serverComponentName: 'happy-server-light', serverDir: dir }));
|
|
50
|
+
} finally {
|
|
51
|
+
await rm(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('assertServerPrismaProviderMatches rejects happy-server when schema.prisma is sqlite', async () => {
|
|
56
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-validate-'));
|
|
57
|
+
try {
|
|
58
|
+
await writeSchemas({ dir, schemaPrisma: SQLITE_SCHEMA, schemaSqlitePrisma: null });
|
|
59
|
+
assert.throws(() => assertServerPrismaProviderMatches({ serverComponentName: 'happy-server', serverDir: dir }));
|
|
60
|
+
} finally {
|
|
61
|
+
await rm(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('detectServerComponentDirMismatch allows unified happy-server-light pointing at happy-server dir', async () => {
|
|
66
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'hs-validate-root-'));
|
|
67
|
+
const envKeys = ['HAPPY_STACKS_WORKSPACE_DIR', 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER', 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT'];
|
|
68
|
+
const old = Object.fromEntries(envKeys.map((k) => [k, process.env[k]]));
|
|
69
|
+
try {
|
|
70
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR = rootDir;
|
|
71
|
+
const unifiedDir = join(rootDir, 'components', 'happy-server');
|
|
72
|
+
await mkdir(unifiedDir, { recursive: true });
|
|
73
|
+
process.env.HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER = unifiedDir;
|
|
74
|
+
process.env.HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT = unifiedDir;
|
|
75
|
+
|
|
76
|
+
const mismatch = detectServerComponentDirMismatch({
|
|
77
|
+
rootDir,
|
|
78
|
+
serverComponentName: 'happy-server-light',
|
|
79
|
+
serverDir: unifiedDir,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(mismatch, null);
|
|
82
|
+
} finally {
|
|
83
|
+
for (const k of envKeys) {
|
|
84
|
+
if (old[k] == null) delete process.env[k];
|
|
85
|
+
else process.env[k] = old[k];
|
|
86
|
+
}
|
|
87
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
|
|
5
6
|
import { runCapture } from '../proc/proc.mjs';
|
|
6
7
|
import { getDefaultAutostartPaths } from '../paths/paths.mjs';
|
|
7
8
|
import { resolveInstalledCliRoot, resolveInstalledPath } from '../paths/runtime.mjs';
|
|
9
|
+
import { getCanonicalHomeDir } from '../env/config.mjs';
|
|
8
10
|
|
|
9
11
|
function plistPathForLabel(label) {
|
|
10
12
|
return join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
function splitPath(p) {
|
|
16
|
+
return String(p ?? '')
|
|
17
|
+
.split(':')
|
|
18
|
+
.map((s) => s.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildLaunchdPath({ execPath = process.execPath, basePath = process.env.PATH } = {}) {
|
|
23
|
+
// launchd starts with a minimal environment; ensure common tool paths exist,
|
|
24
|
+
// and include the current Node binary directory so shell shims that exec `node`
|
|
25
|
+
// still work (e.g. nvm-managed installs).
|
|
26
|
+
const nodeDir = execPath ? dirname(execPath) : '';
|
|
27
|
+
const defaults = splitPath('/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin');
|
|
28
|
+
const fromNode = nodeDir ? [nodeDir] : [];
|
|
29
|
+
const fromEnv = splitPath(basePath);
|
|
30
|
+
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const part of [...fromNode, ...fromEnv, ...defaults]) {
|
|
34
|
+
if (seen.has(part)) continue;
|
|
35
|
+
seen.add(part);
|
|
36
|
+
out.push(part);
|
|
37
|
+
}
|
|
38
|
+
return out.join(':') || '/usr/bin:/bin:/usr/sbin:/sbin';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function pickLaunchdProgramArgs({ rootDir, execPath = process.execPath } = {}) {
|
|
42
|
+
// Prefer the stable shim under the canonical home dir (used by selfhost installs).
|
|
43
|
+
// This keeps the LaunchAgent pointing at a stable path while allowing runtime updates.
|
|
44
|
+
const happysShim = join(getCanonicalHomeDir(), 'bin', 'happys');
|
|
45
|
+
if (existsSync(happysShim)) {
|
|
46
|
+
return [happysShim, 'start'];
|
|
47
|
+
}
|
|
48
|
+
// Fallback: call the Node entry directly (works in repo-only installs).
|
|
49
|
+
return [execPath, resolveInstalledPath(rootDir, 'bin/happys.mjs'), 'start'];
|
|
50
|
+
}
|
|
51
|
+
|
|
13
52
|
function xmlEscape(s) {
|
|
14
53
|
return String(s ?? '')
|
|
15
54
|
.replaceAll('&', '&')
|
|
@@ -72,11 +111,12 @@ export async function ensureMacAutostartEnabled({ rootDir, label, env }) {
|
|
|
72
111
|
await mkdir(dirname(stdoutPath), { recursive: true }).catch(() => {});
|
|
73
112
|
await mkdir(dirname(stderrPath), { recursive: true }).catch(() => {});
|
|
74
113
|
|
|
75
|
-
const programArgs =
|
|
114
|
+
const programArgs = pickLaunchdProgramArgs({ rootDir, execPath: process.execPath });
|
|
76
115
|
const mergedEnv = {
|
|
77
116
|
...(env ?? {}),
|
|
78
117
|
// Ensure a reasonable PATH for subprocesses (git/docker/etc) in launchd’s minimal environment.
|
|
79
|
-
PATH
|
|
118
|
+
// Also ensure Node is on PATH for shell shims that exec `node` (common with nvm installs).
|
|
119
|
+
PATH: buildLaunchdPath({ execPath: process.execPath, basePath: process.env.PATH }),
|
|
80
120
|
};
|
|
81
121
|
|
|
82
122
|
const xml = plistXml({
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
import { buildLaunchdPath, pickLaunchdProgramArgs } from './autostart_darwin.mjs';
|
|
8
|
+
|
|
9
|
+
test('buildLaunchdPath includes node dir and common tool paths', () => {
|
|
10
|
+
const execPath = '/Users/me/.nvm/versions/node/v22.14.0/bin/node';
|
|
11
|
+
const p = buildLaunchdPath({ execPath, basePath: '' });
|
|
12
|
+
|
|
13
|
+
assert.ok(p.includes('/Users/me/.nvm/versions/node/v22.14.0/bin'), 'includes node dir');
|
|
14
|
+
assert.ok(p.includes('/usr/bin'), 'includes /usr/bin');
|
|
15
|
+
assert.ok(p.includes('/bin'), 'includes /bin');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('pickLaunchdProgramArgs uses stable happys shim when present', async () => {
|
|
19
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-home-'));
|
|
20
|
+
const shim = join(dir, 'bin', 'happys');
|
|
21
|
+
await mkdir(join(dir, 'bin'), { recursive: true });
|
|
22
|
+
await writeFile(shim, '#!/bin/sh\necho ok\n', { encoding: 'utf-8' });
|
|
23
|
+
|
|
24
|
+
// Temporarily point canonical home at our temp dir via env var used by getCanonicalHomeDirFromEnv().
|
|
25
|
+
const prev = process.env.HAPPY_STACKS_CANONICAL_HOME_DIR;
|
|
26
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = dir;
|
|
27
|
+
try {
|
|
28
|
+
const args = pickLaunchdProgramArgs({ rootDir: '/fake/root' });
|
|
29
|
+
assert.deepEqual(args, [shim, 'start']);
|
|
30
|
+
} finally {
|
|
31
|
+
if (prev == null) delete process.env.HAPPY_STACKS_CANONICAL_HOME_DIR;
|
|
32
|
+
else process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = prev;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('pickLaunchdProgramArgs falls back to node + happys.mjs when shim missing', () => {
|
|
37
|
+
const prev = process.env.HAPPY_STACKS_CANONICAL_HOME_DIR;
|
|
38
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = '/definitely-not-a-real-path';
|
|
39
|
+
try {
|
|
40
|
+
const execPath = '/usr/local/bin/node';
|
|
41
|
+
const args = pickLaunchdProgramArgs({ rootDir: '/cli/root', execPath });
|
|
42
|
+
assert.equal(args[0], execPath);
|
|
43
|
+
assert.ok(String(args[1]).endsWith('/bin/happys.mjs'));
|
|
44
|
+
assert.equal(args[2], 'start');
|
|
45
|
+
} finally {
|
|
46
|
+
if (prev == null) delete process.env.HAPPY_STACKS_CANONICAL_HOME_DIR;
|
|
47
|
+
else process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = prev;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
@@ -3,12 +3,12 @@ import { getStackRuntimeStatePath } from './runtime_state.mjs';
|
|
|
3
3
|
|
|
4
4
|
export function resolveStackContext({ env = process.env, autostart = null } = {}) {
|
|
5
5
|
const explicitStack = (env.HAPPY_STACKS_STACK ?? env.HAPPY_LOCAL_STACK ?? '').toString().trim();
|
|
6
|
-
const stackName = explicitStack || (autostart?.stackName ?? '') || getStackName();
|
|
6
|
+
const stackName = explicitStack || (autostart?.stackName ?? '') || getStackName(env);
|
|
7
7
|
const stackMode = Boolean(explicitStack);
|
|
8
8
|
|
|
9
9
|
const envPath =
|
|
10
10
|
(env.HAPPY_STACKS_ENV_FILE ?? env.HAPPY_LOCAL_ENV_FILE ?? '').toString().trim() ||
|
|
11
|
-
resolveStackEnvPath(stackName).envPath;
|
|
11
|
+
resolveStackEnvPath(stackName, env).envPath;
|
|
12
12
|
|
|
13
13
|
const runtimeStatePath =
|
|
14
14
|
(env.HAPPY_STACKS_RUNTIME_STATE_PATH ?? env.HAPPY_LOCAL_RUNTIME_STATE_PATH ?? '').toString().trim() ||
|
|
@@ -2,7 +2,7 @@ import { join, resolve } from 'node:path';
|
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
3
|
|
|
4
4
|
import { expandHome } from '../paths/canonical_home.mjs';
|
|
5
|
-
import { getComponentDir, getWorkspaceDir, resolveStackEnvPath } from '../paths/paths.mjs';
|
|
5
|
+
import { coerceHappyMonorepoRootFromPath, getComponentDir, getWorkspaceDir, resolveStackEnvPath } from '../paths/paths.mjs';
|
|
6
6
|
import { ensureDir } from '../fs/ops.mjs';
|
|
7
7
|
import { getEnvValueAny } from '../env/values.mjs';
|
|
8
8
|
import { readEnvObjectFromFile } from '../env/read.mjs';
|
|
@@ -13,7 +13,7 @@ import { getCliHomeDirFromEnvOrDefault } from './dirs.mjs';
|
|
|
13
13
|
function resolveWorkspaceDirFromStackEnv({ rootDir, stackEnv }) {
|
|
14
14
|
const raw = getEnvValueAny(stackEnv, ['HAPPY_STACKS_WORKSPACE_DIR', 'HAPPY_LOCAL_WORKSPACE_DIR']);
|
|
15
15
|
if (!raw) {
|
|
16
|
-
return getWorkspaceDir(rootDir);
|
|
16
|
+
return getWorkspaceDir(rootDir, stackEnv);
|
|
17
17
|
}
|
|
18
18
|
const expanded = expandHome(raw);
|
|
19
19
|
return expanded.startsWith('/') ? expanded : resolve(rootDir, expanded);
|
|
@@ -21,7 +21,7 @@ function resolveWorkspaceDirFromStackEnv({ rootDir, stackEnv }) {
|
|
|
21
21
|
|
|
22
22
|
function resolveComponentDirFromStackEnv({ rootDir, stackEnv, keys, component }) {
|
|
23
23
|
const raw = getEnvValueAny(stackEnv, keys);
|
|
24
|
-
if (!raw) return getComponentDir(rootDir, component);
|
|
24
|
+
if (!raw) return getComponentDir(rootDir, component, stackEnv);
|
|
25
25
|
const expanded = expandHome(raw);
|
|
26
26
|
if (expanded.startsWith('/')) return expanded;
|
|
27
27
|
const workspaceDir = resolveWorkspaceDirFromStackEnv({ rootDir, stackEnv });
|
|
@@ -108,8 +108,9 @@ export async function writeStackCodeWorkspace({
|
|
|
108
108
|
}
|
|
109
109
|
for (const component of selectedComponents) {
|
|
110
110
|
const keys = byName.get(component) ?? [];
|
|
111
|
-
const
|
|
112
|
-
|
|
111
|
+
const componentDir = resolveComponentDirFromStackEnv({ rootDir, stackEnv, keys, component });
|
|
112
|
+
const monoRoot = coerceHappyMonorepoRootFromPath(componentDir);
|
|
113
|
+
folders.push({ name: component, path: monoRoot || componentDir });
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
// Deduplicate by path (can happen if multiple components are pointed at the same dir).
|
|
@@ -149,4 +150,3 @@ export async function writeStackCodeWorkspace({
|
|
|
149
150
|
},
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
|
-
|