From bea2bd54604dedb7db28e84bdc0832de62b8f263 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 16:52:14 -0400 Subject: [PATCH 1/3] CS-10564, CS-10699: Fix env var port leaks and prerender heartbeat escape in test harness Replace module-level env var constants (DEFAULT_REALM_SERVER_PORT, DEFAULT_COMPAT_REALM_SERVER_PORT, DEFAULT_PRERENDER_PORT, CONFIGURED_PRERENDER_URL) with explicit CLI args and function parameters, following the pattern established in CS-10560 for the worker-manager port. Changes: - shared.ts: Strip ambient port env vars at module load (like HOST_URL), remove module-level constants, accept ports via function params - isolated-realm-stack.ts: Accept realmServerPort and prerenderURL as explicit params - api.ts: Thread new port params through FactoryRealmOptions to startIsolatedRealmStack - serve-realm.ts: Parse --realmServerPort, --compatRealmServerPort, --prerenderURL CLI args - fixtures.ts: Pass ports as CLI args to serve-realm.ts instead of env vars - support-services.ts: Set PRERENDER_MANAGER_URL to unreachable address when spawning test prerender servers, preventing them from registering with external managers Co-Authored-By: Claude Opus 4.6 (1M context) --- .../software-factory/src/cli/serve-realm.ts | 18 +++++++- packages/software-factory/src/harness/api.ts | 5 +++ .../src/harness/isolated-realm-stack.ts | 21 ++++++--- .../software-factory/src/harness/shared.ts | 45 ++++++++++--------- .../src/harness/support-services.ts | 8 +++- packages/software-factory/tests/fixtures.ts | 17 ++++--- 6 files changed, 73 insertions(+), 41 deletions(-) diff --git a/packages/software-factory/src/cli/serve-realm.ts b/packages/software-factory/src/cli/serve-realm.ts index 803801f90c0..91bd49ea43e 100644 --- a/packages/software-factory/src/cli/serve-realm.ts +++ b/packages/software-factory/src/cli/serve-realm.ts @@ -6,10 +6,23 @@ import type { RealmPermissions } from '@cardstack/runtime-common'; import { startFactoryRealmServer } from '../harness'; +function parseCliArg(name: string): string | undefined { + let prefix = `--${name}=`; + let arg = process.argv.find((a) => a.startsWith(prefix)); + return arg ? arg.slice(prefix.length) : undefined; +} + +function parseCliNumber(name: string): number | undefined { + let value = parseCliArg(name); + return value != null ? Number(value) : undefined; +} + async function main(): Promise { + // First positional arg is realmDir (skip --flags) + let positional = process.argv.slice(2).filter((a) => !a.startsWith('--')); let realmDir = resolve( process.cwd(), - process.argv[2] ?? 'test-fixtures/darkfactory-adopter', + positional[0] ?? 'test-fixtures/darkfactory-adopter', ); if (!process.env.SOFTWARE_FACTORY_CONTEXT) { @@ -38,6 +51,9 @@ async function main(): Promise { .SOFTWARE_FACTORY_TEMPLATE_REALM_SERVER_URL ? new URL(process.env.SOFTWARE_FACTORY_TEMPLATE_REALM_SERVER_URL) : undefined, + realmServerPort: parseCliNumber('realmServerPort'), + compatRealmServerPort: parseCliNumber('compatRealmServerPort'), + prerenderURL: parseCliArg('prerenderURL'), }); let payload = { diff --git a/packages/software-factory/src/harness/api.ts b/packages/software-factory/src/harness/api.ts index c94916c1ffa..321472e3b70 100644 --- a/packages/software-factory/src/harness/api.ts +++ b/packages/software-factory/src/harness/api.ts @@ -77,6 +77,7 @@ export async function startFactoryGlobalContext( let { realmURL, realmServerURL } = await resolveFactoryRealmLocation({ realmURL: options.realmURL, realmServerURL: options.realmServerURL, + compatRealmServerPort: options.compatRealmServerPort, }); let support = await startFactorySupportServices(); try { @@ -125,6 +126,7 @@ export async function ensureFactoryRealmTemplate( let { realmURL, realmServerURL } = await resolveFactoryRealmLocation({ realmURL: options.realmURL ?? contextRealmURL, realmServerURL: options.realmServerURL ?? contextRealmServerURL, + compatRealmServerPort: options.compatRealmServerPort, }); let permissions = options.permissions ?? DEFAULT_PERMISSIONS; let fixtureHash = hashRealmFixture(realmDir); @@ -356,6 +358,7 @@ export async function startFactoryRealmServer( let { realmURL, realmServerURL } = await resolveFactoryRealmLocation({ realmURL: options.realmURL ?? contextRealmURL, realmServerURL: options.realmServerURL ?? contextRealmServerURL, + compatRealmServerPort: options.compatRealmServerPort, }); let templateDatabaseName = options.templateDatabaseName; let databaseName = runtimeDatabaseName(); @@ -448,6 +451,8 @@ export async function startFactoryRealmServer( context, migrateDB: false, fullIndexOnStartup: false, + realmServerPort: options.realmServerPort, + prerenderURL: options.prerenderURL, }); } catch (error) { let cleanupError: unknown; diff --git a/packages/software-factory/src/harness/isolated-realm-stack.ts b/packages/software-factory/src/harness/isolated-realm-stack.ts index e794cf0f002..bf490d1dd30 100644 --- a/packages/software-factory/src/harness/isolated-realm-stack.ts +++ b/packages/software-factory/src/harness/isolated-realm-stack.ts @@ -19,7 +19,6 @@ import { baseRealmDir, baseRealmURLFor, captureProcessLogs, - CONFIGURED_PRERENDER_URL, createProcessExitPromise, DEFAULT_MATRIX_SERVER_USERNAME, DEFAULT_PG_HOST, @@ -27,7 +26,6 @@ import { DEFAULT_PG_PORT, DEFAULT_PG_USER, DEFAULT_REALM_LOG_LEVELS, - DEFAULT_REALM_SERVER_PORT, findAvailablePort, FIXTURE_SOURCE_REALM_URL_PLACEHOLDER, FULL_INDEX_REALM_STARTUP_TIMEOUT_MS, @@ -277,6 +275,8 @@ export async function startIsolatedRealmStack({ fullIndexOnStartup, additionalRealms, workerManagerPort: explicitWorkerManagerPort, + realmServerPort: explicitRealmServerPort, + prerenderURL: explicitPrerenderURL, }: { realmDir: string; realmURL: URL; @@ -290,6 +290,13 @@ export async function startIsolatedRealmStack({ * picking one dynamically. This lets callers know the port upfront (e.g. * for progress monitoring via /_indexing-status). */ workerManagerPort?: number; + /** When provided, the realm-server will listen on this port instead of + * picking one dynamically. */ + realmServerPort?: number; + /** When provided, reuse this existing prerender server URL instead of + * starting a new one. The Playwright harness keeps prerender alive for + * the lifetime of a testWorker and passes its URL here. */ + prerenderURL?: string; }): Promise { let rootDir = mkdtempSync(join(tmpdir(), 'software-factory-realms-')); let testRealmDir = join(rootDir, 'test'); @@ -298,9 +305,9 @@ export async function startIsolatedRealmStack({ let actualWorkerManagerPort = explicitWorkerManagerPort ?? (await findAvailablePort()); let actualRealmServerPort = - DEFAULT_REALM_SERVER_PORT === 0 - ? await findAvailablePort() - : DEFAULT_REALM_SERVER_PORT; + explicitRealmServerPort && explicitRealmServerPort !== 0 + ? explicitRealmServerPort + : await findAvailablePort(); let actualRealmServerURL = withPort(realmServerURL, actualRealmServerPort); let actualRealmPath = realmRelativePath(realmURL, realmServerURL); let actualRealmURL = realmURLWithinServer( @@ -360,12 +367,12 @@ export async function startIsolatedRealmStack({ // lifetime of a Playwright testWorker even though the realm stack itself is // recreated per test. When provided, reuse that long-lived prerender URL so // we only restart realm-server and worker-manager here. - let prerender = CONFIGURED_PRERENDER_URL + let prerender = explicitPrerenderURL ? undefined : await startHarnessPrerenderServer({ boxelHostURL: realmServerURL.href.replace(/\/$/, ''), }); - let prerenderURL = CONFIGURED_PRERENDER_URL?.href ?? prerender?.url; + let prerenderURL = explicitPrerenderURL ?? prerender?.url; if (!prerenderURL) { throw new Error( 'Unable to determine prerender URL for isolated realm stack', diff --git a/packages/software-factory/src/harness/shared.ts b/packages/software-factory/src/harness/shared.ts index e7355d8b812..9c192905e38 100644 --- a/packages/software-factory/src/harness/shared.ts +++ b/packages/software-factory/src/harness/shared.ts @@ -13,15 +13,18 @@ import '../setup-logger'; import { logger } from '../logger'; // Strip ambient env vars that could break the hermetic test seal. -// The harness always sets HOST_URL explicitly via context.hostURL when -// spawning child processes — an ambient HOST_URL (e.g. from a dev shell -// that sets HOST_URL=http://localhost:4200) would be inherited by child -// processes that don't explicitly override it, causing them to talk to -// a different Matrix/realm server than the hermetic test infrastructure. +// The harness always passes port values explicitly through CLI args or +// function parameters — ambient env vars (e.g. from a dev shell running +// mise dev-all) would be inherited by child processes, overriding the +// dynamically allocated ports and breaking test isolation. // This module is only imported by harness code (test infrastructure), // so it's safe to strip unconditionally — NODE_ENV may be 'test' or // 'development' depending on how the harness is invoked. delete process.env.HOST_URL; +delete process.env.SOFTWARE_FACTORY_REALM_PORT; +delete process.env.SOFTWARE_FACTORY_COMPAT_REALM_PORT; +delete process.env.SOFTWARE_FACTORY_PRERENDER_PORT; +delete process.env.SOFTWARE_FACTORY_PRERENDER_URL; export type RealmAction = 'read' | 'write' | 'realm-owner' | 'assume-user'; @@ -49,6 +52,12 @@ export interface FactoryRealmOptions { cacheSalt?: string; templateDatabaseName?: string; context?: FactoryTestContext | FactorySupportContext; + /** Explicit compat realm-server port (the public-facing proxy port). */ + compatRealmServerPort?: number; + /** Explicit realm-server port (the internal realm-server listen port). */ + realmServerPort?: number; + /** Explicit prerender URL to reuse instead of starting a new prerender. */ + prerenderURL?: string; } export interface FactoryRealmTemplate { @@ -145,12 +154,6 @@ export const prepareTestPgScript = resolve( ); export const CACHE_VERSION = 8; -export const DEFAULT_REALM_SERVER_PORT = Number( - process.env.SOFTWARE_FACTORY_REALM_PORT ?? 0, -); -export const DEFAULT_COMPAT_REALM_SERVER_PORT = Number( - process.env.SOFTWARE_FACTORY_COMPAT_REALM_PORT ?? 0, -); export const CONFIGURED_REALM_URL = process.env.SOFTWARE_FACTORY_REALM_URL ? new URL(process.env.SOFTWARE_FACTORY_REALM_URL) : undefined; @@ -176,13 +179,6 @@ export const DEFAULT_PG_HOST = process.env.SOFTWARE_FACTORY_PGHOST ?? '127.0.0.1'; export const DEFAULT_PG_USER = process.env.SOFTWARE_FACTORY_PGUSER ?? 'postgres'; -export const DEFAULT_PRERENDER_PORT = Number( - process.env.SOFTWARE_FACTORY_PRERENDER_PORT ?? 0, -); -export const CONFIGURED_PRERENDER_URL = process.env - .SOFTWARE_FACTORY_PRERENDER_URL - ? new URL(process.env.SOFTWARE_FACTORY_PRERENDER_URL) - : undefined; // The seeded test Postgres used by the harness runs with max_connections=50, so // isolated workers need a smaller per-process pool cap to keep workers=3 stable. export const DEFAULT_PG_POOL_MAX = Number( @@ -299,6 +295,7 @@ export async function findAvailablePort(): Promise { export async function resolveFactoryRealmServerURL( realmServerURL?: URL, + compatRealmServerPort?: number, ): Promise { if (realmServerURL) { return new URL(realmServerURL.href); @@ -309,15 +306,16 @@ export async function resolveFactoryRealmServerURL( } let port = - DEFAULT_COMPAT_REALM_SERVER_PORT === 0 - ? await findAvailablePort() - : DEFAULT_COMPAT_REALM_SERVER_PORT; + compatRealmServerPort && compatRealmServerPort !== 0 + ? compatRealmServerPort + : await findAvailablePort(); return new URL(`http://localhost:${port}/`); } export async function resolveFactoryRealmLocation(options: { realmURL?: URL; realmServerURL?: URL; + compatRealmServerPort?: number; }): Promise<{ realmURL: URL; realmServerURL: URL; @@ -334,7 +332,10 @@ export async function resolveFactoryRealmLocation(options: { : undefined; if (!realmURL && !realmServerURL) { - realmServerURL = await resolveFactoryRealmServerURL(); + realmServerURL = await resolveFactoryRealmServerURL( + undefined, + options.compatRealmServerPort, + ); realmURL = new URL('test/', realmServerURL); } else if (!realmServerURL) { throw new Error( diff --git a/packages/software-factory/src/harness/support-services.ts b/packages/software-factory/src/harness/support-services.ts index c4d925ea836..ace6b8a8b8e 100644 --- a/packages/software-factory/src/harness/support-services.ts +++ b/packages/software-factory/src/harness/support-services.ts @@ -11,7 +11,6 @@ import { DEFAULT_MATRIX_SERVER_USERNAME, DEFAULT_PG_HOST, DEFAULT_PG_PORT, - DEFAULT_PRERENDER_PORT, CONFIGURED_HOST_URL, findAvailablePort, findHostDistPackageDir, @@ -387,7 +386,7 @@ export async function startHarnessPrerenderServer(options: { url: string; stop(): Promise; }> { - let port = options.port ?? DEFAULT_PRERENDER_PORT; + let port = options.port ?? 0; if (port === 0) { port = await findAvailablePort(); } @@ -406,6 +405,11 @@ export async function startHarnessPrerenderServer(options: { LOG_LEVELS: process.env.SOFTWARE_FACTORY_PRERENDER_LOG_LEVELS ?? process.env.LOG_LEVELS, + // Prevent test harness prerender servers from registering with + // external prerender managers (e.g. the dev-all manager on :4222). + // Port 1 is privileged and unreachable — heartbeat fetches fail + // silently via the existing try/catch in prerender-app.ts. + PRERENDER_MANAGER_URL: 'http://127.0.0.1:1', }, }, ); diff --git a/packages/software-factory/tests/fixtures.ts b/packages/software-factory/tests/fixtures.ts index 18f10422617..55bb882749e 100644 --- a/packages/software-factory/tests/fixtures.ts +++ b/packages/software-factory/tests/fixtures.ts @@ -278,7 +278,14 @@ async function startRealmProcess( let child = spawn( tsNodeBin, - ['--transpileOnly', 'src/cli/serve-realm.ts', realmDir], + [ + '--transpileOnly', + 'src/cli/serve-realm.ts', + realmDir, + `--compatRealmServerPort=${testWorkerPortSet.compatRealmServerPort}`, + `--realmServerPort=${testWorkerPortSet.realmServerPort}`, + `--prerenderURL=${testWorkerPrerenderURL}`, + ], { cwd: packageRoot, detached: true, @@ -287,14 +294,6 @@ async function startRealmProcess( NODE_NO_WARNINGS: '1', SOFTWARE_FACTORY_METADATA_FILE: metadataFile, SOFTWARE_FACTORY_SOURCE_REALM_DIR: testSourceRealmDir, - SOFTWARE_FACTORY_COMPAT_REALM_PORT: String( - testWorkerPortSet.compatRealmServerPort, - ), - SOFTWARE_FACTORY_REALM_PORT: String(testWorkerPortSet.realmServerPort), - SOFTWARE_FACTORY_PRERENDER_PORT: String( - testWorkerPortSet.prerenderPort, - ), - SOFTWARE_FACTORY_PRERENDER_URL: testWorkerPrerenderURL, ...(supportMetadata?.context ? { SOFTWARE_FACTORY_CONTEXT: JSON.stringify(supportMetadata.context), From c1f23916d27f20ea17667db543f8234c3fa8c53d Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 17:02:47 -0400 Subject: [PATCH 2/3] Fix host dist lookup in worktrees for QUnit test execution test-run-execution.ts defaulted hostDistDir to a worktree-relative path (../../../host/dist), which fails in worktrees where the host app hasn't been built locally. Now uses findHostDistPackageDir() to check the root repo checkout first, matching how support-services.ts resolves the host dist for the main app. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/test-run-execution.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/software-factory/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index b891c76427f..3c4b152e184 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -1,6 +1,6 @@ import { createServer, type Server } from 'node:http'; import { readFileSync } from 'node:fs'; -import { normalize, resolve } from 'node:path'; +import { join, normalize, resolve } from 'node:path'; import { chromium } from '@playwright/test'; @@ -13,6 +13,7 @@ import type { TestRunHandle, TestRunRealmOptions, } from './test-run-types'; +import { findHostDistPackageDir } from '../../src/harness/shared'; // --------------------------------------------------------------------------- // Resume Logic @@ -425,9 +426,15 @@ export async function executeTestRunFromRealm( let testPageServer: Server | undefined; try { - // Locate the host app's dist directory — contains tests/index.html and assets + // Locate the host app's dist directory — contains tests/index.html and assets. + // In worktrees, the local host/dist may not exist; fall back to the root + // repo checkout's host dist (same logic as the harness support services). let hostDistDir = - options.hostDistDir ?? resolve(__dirname, '../../../host/dist'); + options.hostDistDir ?? + join( + findHostDistPackageDir() ?? resolve(__dirname, '../../../host'), + 'dist', + ); // Start a local server to serve both the test HTML page and the host's // dist assets. All asset references point to our server, so no external From 291d649fc1a9e49141736f38970521145b3237c9 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 17:18:09 -0400 Subject: [PATCH 3/3] Address PR review feedback - Extract findHostDistPackageDir, findRootRepoCheckoutDir, fileExists into a side-effect-free module (src/host-dist.ts) so scripts/lib can import without pulling in harness env var stripping from shared.ts - Add Number.isFinite validation to parseCliNumber in serve-realm.ts - Reword PRERENDER_MANAGER_URL comment (port 1 is "expected closed", not "unreachable") Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/test-run-execution.ts | 2 +- .../software-factory/src/cli/serve-realm.ts | 9 ++- .../software-factory/src/harness/shared.ts | 69 ++--------------- .../src/harness/support-services.ts | 4 +- packages/software-factory/src/host-dist.ts | 75 +++++++++++++++++++ 5 files changed, 94 insertions(+), 65 deletions(-) create mode 100644 packages/software-factory/src/host-dist.ts diff --git a/packages/software-factory/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index 3c4b152e184..0beaf62d56e 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -13,7 +13,7 @@ import type { TestRunHandle, TestRunRealmOptions, } from './test-run-types'; -import { findHostDistPackageDir } from '../../src/harness/shared'; +import { findHostDistPackageDir } from '../../src/host-dist'; // --------------------------------------------------------------------------- // Resume Logic diff --git a/packages/software-factory/src/cli/serve-realm.ts b/packages/software-factory/src/cli/serve-realm.ts index 91bd49ea43e..5a70ed47803 100644 --- a/packages/software-factory/src/cli/serve-realm.ts +++ b/packages/software-factory/src/cli/serve-realm.ts @@ -14,7 +14,14 @@ function parseCliArg(name: string): string | undefined { function parseCliNumber(name: string): number | undefined { let value = parseCliArg(name); - return value != null ? Number(value) : undefined; + if (value == null) { + return undefined; + } + let parsed = Number(value); + if (!Number.isFinite(parsed)) { + throw new Error(`--${name} must be a valid number, received: ${value}`); + } + return parsed; } async function main(): Promise { diff --git a/packages/software-factory/src/harness/shared.ts b/packages/software-factory/src/harness/shared.ts index 9c192905e38..3be1e1540eb 100644 --- a/packages/software-factory/src/harness/shared.ts +++ b/packages/software-factory/src/harness/shared.ts @@ -6,7 +6,7 @@ import { import { createHash } from 'node:crypto'; import { createServer as createNetServer } from 'node:net'; import { readdirSync, readFileSync, statSync } from 'node:fs'; -import { dirname, join, relative, resolve } from 'node:path'; +import { join, relative, resolve } from 'node:path'; import jwt from 'jsonwebtoken'; import '../setup-logger'; @@ -604,66 +604,13 @@ export function maybeRequire(specifier: string) { return undefined; } -export function fileExists(path: string): boolean { - try { - return statSync(path).isFile(); - } catch { - return false; - } -} - -export function findRootRepoCheckoutDir(): string | undefined { - let result = spawnSync( - 'git', - ['rev-parse', '--path-format=absolute', '--git-common-dir'], - { - cwd: workspaceRoot, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }, - ); - - if (result.status !== 0) { - return undefined; - } - - let commonDir = result.stdout.trim(); - if (!commonDir.endsWith(`${join('.git')}`)) { - return undefined; - } - - return dirname(commonDir); -} - -export function findHostDistPackageDir(): string | undefined { - let rootRepoCheckoutDir = findRootRepoCheckoutDir(); - let rootRepoHostDir = - rootRepoCheckoutDir && rootRepoCheckoutDir !== workspaceRoot - ? resolve(rootRepoCheckoutDir, 'packages', 'host') - : undefined; - - let candidates = [ - process.env.SOFTWARE_FACTORY_HOST_DIST_PACKAGE_DIR, - hostDir, - rootRepoHostDir, - ] - .filter((value): value is string => Boolean(value)) - .map((value) => resolve(value)); - - let seen = new Set(); - for (let candidate of candidates) { - if (seen.has(candidate)) { - continue; - } - seen.add(candidate); - - if (fileExists(join(candidate, 'dist', 'index.html'))) { - return candidate; - } - } - - return undefined; -} +// Re-export host dist utilities from the side-effect-free module so +// existing harness consumers don't need to change their imports. +export { + fileExists, + findRootRepoCheckoutDir, + findHostDistPackageDir, +} from '../host-dist'; export function browserPassword(username: string): string { let cleanUsername = username.replace(/^@/, '').replace(/:.*$/, ''); diff --git a/packages/software-factory/src/harness/support-services.ts b/packages/software-factory/src/harness/support-services.ts index ace6b8a8b8e..2389c7b0f17 100644 --- a/packages/software-factory/src/harness/support-services.ts +++ b/packages/software-factory/src/harness/support-services.ts @@ -407,8 +407,8 @@ export async function startHarnessPrerenderServer(options: { process.env.LOG_LEVELS, // Prevent test harness prerender servers from registering with // external prerender managers (e.g. the dev-all manager on :4222). - // Port 1 is privileged and unreachable — heartbeat fetches fail - // silently via the existing try/catch in prerender-app.ts. + // Port 1 is expected to be closed, so heartbeat fetches fail fast + // and are silently caught by the try/catch in prerender-app.ts. PRERENDER_MANAGER_URL: 'http://127.0.0.1:1', }, }, diff --git a/packages/software-factory/src/host-dist.ts b/packages/software-factory/src/host-dist.ts new file mode 100644 index 00000000000..f15c77e5a37 --- /dev/null +++ b/packages/software-factory/src/host-dist.ts @@ -0,0 +1,75 @@ +/** + * Side-effect-free utilities for locating the host app dist directory. + * + * These are intentionally NOT in harness/shared.ts because that module + * strips ambient env vars at import time (a harness-only side effect). + * Code outside the harness (e.g. scripts/lib) can safely import from here. + */ +import { spawnSync } from 'node:child_process'; +import { statSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; + +const packageRoot = resolve(process.cwd()); +const workspaceRoot = resolve(packageRoot, '..', '..'); +const hostDir = resolve(packageRoot, '..', 'host'); + +export function fileExists(path: string): boolean { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +export function findRootRepoCheckoutDir(): string | undefined { + let result = spawnSync( + 'git', + ['rev-parse', '--path-format=absolute', '--git-common-dir'], + { + cwd: workspaceRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }, + ); + + if (result.status !== 0) { + return undefined; + } + + let commonDir = result.stdout.trim(); + if (!commonDir.endsWith(`${join('.git')}`)) { + return undefined; + } + + return dirname(commonDir); +} + +export function findHostDistPackageDir(): string | undefined { + let rootRepoCheckoutDir = findRootRepoCheckoutDir(); + let rootRepoHostDir = + rootRepoCheckoutDir && rootRepoCheckoutDir !== workspaceRoot + ? resolve(rootRepoCheckoutDir, 'packages', 'host') + : undefined; + + let candidates = [ + process.env.SOFTWARE_FACTORY_HOST_DIST_PACKAGE_DIR, + hostDir, + rootRepoHostDir, + ] + .filter((value): value is string => Boolean(value)) + .map((value) => resolve(value)); + + let seen = new Set(); + for (let candidate of candidates) { + if (seen.has(candidate)) { + continue; + } + seen.add(candidate); + + if (fileExists(join(candidate, 'dist', 'index.html'))) { + return candidate; + } + } + + return undefined; +}