/** * uat/cli/lib/app-config.ts — Resolve the generated app's runtime endpoints and the * initial admin credentials from its own config files (no live process needed). * * API URL ← spec override → Properties/launchSettings.json (first profile's * applicationUrl, http preferred) → http://localhost:5142 * Frontend URL ← spec override → vite.config.* `port:` → http://localhost:5173 * Initial admin← spec override → Security.InitialAdmin in appsettings.Local.json * → appsettings.Development.json → appsettings.json (same cascade * as the DB connection; `ss dev configure` writes Local). * * Every resolver reports its source so envelopes can say where a value came from. */ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; export interface Resolved { value: T; /** Where the value came from (file name, 'spec', or 'default'). */ source: string; } function readJsonSafe(filePath: string): Record | null { try { if (!existsSync(filePath)) return null; return JSON.parse(readFileSync(filePath, 'utf-8')) as Record; } catch { return null; } } const stripTrailingSlash = (url: string): string => url.replace(/\/+$/, ''); /** Pick the http:// URL out of a launchSettings `applicationUrl` (";"-separated). PURE. */ export function pickHttpUrl(applicationUrl: string): string | null { const urls = applicationUrl .split(';') .map((u) => u.trim()) .filter(Boolean); return urls.find((u) => u.startsWith('http://')) ?? urls[0] ?? null; } /** Extract the first profile applicationUrl from a parsed launchSettings.json. PURE. */ export function apiUrlFromLaunchSettings(json: Record): string | null { const profiles = json.profiles; if (!profiles || typeof profiles !== 'object') return null; for (const profile of Object.values(profiles as Record)) { if (!profile || typeof profile !== 'object') continue; const appUrl = (profile as Record).applicationUrl; if (typeof appUrl === 'string' && appUrl.length > 0) { const picked = pickHttpUrl(appUrl); if (picked) return stripTrailingSlash(picked); } } return null; } export function resolveApiUrl(apiDir: string, override?: string): Resolved { if (override) return { value: stripTrailingSlash(override), source: 'spec' }; const json = readJsonSafe(join(apiDir, 'Properties', 'launchSettings.json')); if (json) { const url = apiUrlFromLaunchSettings(json); if (url) return { value: url, source: 'Properties/launchSettings.json' }; } return { value: 'http://localhost:5142', source: 'default' }; } /** Extract a `port: NNNN` from a vite config source. PURE. */ export function portFromViteConfig(content: string): number | null { const m = /\bport\s*:\s*(\d{2,5})/.exec(content); return m ? Number(m[1]) : null; } export function resolveFrontendUrl(webDir: string, override?: string): Resolved { if (override) return { value: stripTrailingSlash(override), source: 'spec' }; for (const name of ['vite.config.ts', 'vite.config.mts', 'vite.config.js']) { const file = join(webDir, name); if (!existsSync(file)) continue; try { const port = portFromViteConfig(readFileSync(file, 'utf-8')); if (port) return { value: `http://localhost:${port}`, source: name }; } catch { /* unreadable config falls through to the default */ } } return { value: 'http://localhost:5173', source: 'default' }; } export interface InitialAdmin { email: string; password: string | null; } /** Default seeded admin email when none is configured (SmartStack.app UserSeedData). */ export const DEFAULT_ADMIN_EMAIL = 'local.admin@smartstack.local'; export function resolveInitialAdmin( apiDir: string, overrides: { email?: string; password?: string } = {}, ): Resolved { if (overrides.email && overrides.password) { return { value: { email: overrides.email, password: overrides.password }, source: 'spec' }; } let email = overrides.email ?? null; let password = overrides.password ?? null; let source = overrides.email || overrides.password ? 'spec+' : ''; for (const file of ['appsettings.Local.json', 'appsettings.Development.json', 'appsettings.json']) { if (email && password) break; const json = readJsonSafe(join(apiDir, file)); const admin = (json?.Security as Record | undefined)?.InitialAdmin as | Record | undefined; if (!admin) continue; if (!email && typeof admin.Email === 'string' && admin.Email.length > 0) email = admin.Email; if (!password && typeof admin.Password === 'string' && admin.Password.length > 0) { password = admin.Password; source += file; } } return { value: { email: email ?? DEFAULT_ADMIN_EMAIL, password }, source: source || 'default', }; }