import { existsSync } from 'node:fs' import { readFile, realpath } from 'node:fs/promises' import { createRequire } from 'node:module' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { LinterToolError } from './diagnostic.js' const brandedPackages = [ '@astrale-os/kernel-client', '@astrale-os/kernel-core', '@astrale-os/kernel-dsl', ] as const const conventionalMembers = ['client', 'frontend', 'schema', 'worker'] as const type Resolution = { consumer: string path: string } type ConsumerPackageJson = { name?: string dependencies?: Record optionalDependencies?: Record workspaces?: string[] | { packages?: string[] } } export async function assertSingleTypeUniverse(root: string): Promise { const manifests = await collectConsumerManifests(root) const consumers = [ ...manifests.map((path) => ({ label: relative(root, dirname(path)) || '.', path })), { label: '@astrale-os/sdk/linter', path: fileURLPackageJson() }, ] for (const packageName of brandedPackages) { const resolutions: Resolution[] = [] for (const consumer of consumers) { const resolved = await resolvePackageRoot(packageName, consumer.path) if (resolved) resolutions.push({ consumer: consumer.label, path: resolved }) } const paths = new Set(resolutions.map((resolution) => resolution.path)) if (paths.size <= 1) continue const detail = formatResolutions(resolutions) throw new LinterToolError( `Multiple physical installations of ${packageName} would create incompatible Astrale types.\n` + `${detail}\nReinstall the workspace so every member resolves one compatible package instance.`, { code: 'LINTER_PREFLIGHT_FAILED' }, ) } } function formatResolutions(resolutions: readonly Resolution[]): string { const byPath = new Map() for (const resolution of resolutions) { const consumers = byPath.get(resolution.path) ?? [] consumers.push(resolution.consumer) byPath.set(resolution.path, consumers) } return [...byPath] .sort(([a], [b]) => a.localeCompare(b)) .map(([path, consumers]) => { const sorted = [...new Set(consumers)].sort() const shown = sorted.slice(0, 4).join(', ') const omitted = sorted.length > 4 ? ` (+${sorted.length - 4} more)` : '' return ` ${path}\n from: ${shown}${omitted}` }) .join('\n') } async function collectConsumerManifests(root: string): Promise { const rootManifest = join(root, 'package.json') if (!existsSync(rootManifest)) return [] const manifests = new Set([rootManifest]) const packageJson = await readPackageJson(rootManifest) const workspaces = Array.isArray(packageJson?.workspaces) ? packageJson.workspaces : packageJson?.workspaces?.packages for (const workspace of workspaces ?? []) { if (typeof workspace !== 'string' || hasGlob(workspace)) continue addManifestWithinRoot(manifests, root, workspace) } for (const member of conventionalMembers) addManifestWithinRoot(manifests, root, member) await addDomainContractDependencyManifests(manifests) return [...manifests].sort() } async function readPackageJson(manifest: string): Promise { try { return JSON.parse(await readFile(manifest, 'utf8')) } catch { return undefined } } async function addDomainContractDependencyManifests(manifests: Set): Promise { const pending = [...manifests] for (let index = 0; index < pending.length; index++) { const consumer = pending[index]! const packageJson = await readPackageJson(consumer) const dependencies = { ...packageJson?.optionalDependencies, ...packageJson?.dependencies, } for (const packageName of Object.keys(dependencies).sort()) { if (!isDomainContractPackage(packageName)) continue const manifest = await resolveDependencyManifest(packageName, consumer) if (!manifest || manifests.has(manifest)) continue manifests.add(manifest) pending.push(manifest) } } } async function resolveDependencyManifest( packageName: string, consumerManifest: string, ): Promise { const require = createRequire(consumerManifest) try { return await realpath(require.resolve(`${packageName}/package.json`)) } catch { try { let directory = dirname(await realpath(require.resolve(packageName))) for (;;) { const manifest = join(directory, 'package.json') if (existsSync(manifest)) { const packageJson = await readPackageJson(manifest) if (packageJson?.name === packageName) return await realpath(manifest) } const parent = dirname(directory) if (parent === directory) return undefined directory = parent } } catch { return undefined } } } function addManifestWithinRoot(manifests: Set, root: string, member: string): void { const manifest = resolve(root, member, 'package.json') const parent = relative(root, manifest) if (parent.startsWith('..') || parent === '') return if (existsSync(manifest)) manifests.add(manifest) } function hasGlob(path: string): boolean { return /[*?[\]{}!]/u.test(path) } function isDomainContractPackage(packageName: string): boolean { return packageName.startsWith('@astrale-domains/') } async function resolvePackageRoot( packageName: (typeof brandedPackages)[number], manifest: string, ): Promise { if (!existsSync(manifest)) return undefined try { const resolved = createRequire(manifest).resolve(`${packageName}/package.json`) return await realpath(dirname(resolved)) } catch { return undefined } } function fileURLPackageJson(): string { let directory = dirname(fileURLToPath(import.meta.url)) for (;;) { const candidate = join(directory, 'package.json') if (existsSync(candidate)) return candidate const parent = dirname(directory) if (parent === directory) break directory = parent } return join(directory, 'package.json') }