feat(packages): add host-inventory and registry-integration

- host-inventory: Fleet management with YAML config loader
- registry-integration: NestJS module for service registry

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-26 00:37:33 -08:00
parent 02ab9cc22b
commit 5cbecba6d7
12 changed files with 1073 additions and 0 deletions

View file

@ -0,0 +1,40 @@
{
"name": "@lilith/host-inventory",
"version": "1.0.0",
"description": "Host inventory loader for infrastructure management",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"yaml": "^2.3.4",
"ajv": "^8.12.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"rimraf": "^5.0.5",
"typescript": "^5.3.0"
},
"files": [
"dist",
"src"
],
"keywords": [
"infrastructure",
"host-inventory",
"yaml",
"lilith"
]
}

View file

@ -0,0 +1,7 @@
/**
* @lilith/host-inventory
* Host inventory management for infrastructure
*/
export * from './types.js';
export * from './loader.js';

View file

@ -0,0 +1,309 @@
/**
* Host Inventory Loader
* Loads and manages host configuration from YAML files
*/
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { parse as parseYaml } from 'yaml';
import type {
HostConfig,
NetworkGroup,
ProvisioningState,
InventoryIndex,
HostCapabilities,
} from './types.js';
/**
* Default inventory path relative to workspace root
*/
const DEFAULT_INVENTORY_PATH = '../../../../../../infrastructure/hosts';
/**
* Loads and manages host inventory from YAML files
*/
export class HostInventoryLoader {
private hosts: Map<string, HostConfig> = new Map();
private hostsByGroup: Map<NetworkGroup, HostConfig[]> = new Map();
private inventoryPath: string;
private loaded = false;
constructor(inventoryPath?: string) {
if (inventoryPath) {
this.inventoryPath = inventoryPath;
} else {
// Resolve relative to this file's location
this.inventoryPath = resolve(dirname(import.meta.url.replace('file://', '')), DEFAULT_INVENTORY_PATH);
}
}
/**
* Load all hosts from the inventory
*/
async loadAll(): Promise<void> {
if (this.loaded) {
return;
}
this.hosts.clear();
this.hostsByGroup.clear();
await this.loadDirectory(this.inventoryPath);
this.loaded = true;
}
/**
* Recursively load YAML files from a directory
*/
private async loadDirectory(dirPath: string, groupPrefix = ''): Promise<void> {
if (!existsSync(dirPath)) {
throw new Error(`Inventory path does not exist: ${dirPath}`);
}
const entries = readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory() && entry.name !== 'schema' && entry.name !== 'provisioning') {
const newPrefix = groupPrefix
? `${groupPrefix}/${entry.name}`
: entry.name;
await this.loadDirectory(fullPath, newPrefix);
} else if (entry.name.endsWith('.yaml') && entry.name !== 'index.yaml') {
const host = await this.loadHost(fullPath);
this.hosts.set(host.id, host);
const group = host.networkGroup;
if (!this.hostsByGroup.has(group)) {
this.hostsByGroup.set(group, []);
}
this.hostsByGroup.get(group)!.push(host);
}
}
}
/**
* Load a single host from a YAML file
*/
private async loadHost(filePath: string): Promise<HostConfig> {
const content = readFileSync(filePath, 'utf-8');
const raw = parseYaml(content);
return this.transformHost(raw);
}
/**
* Transform raw YAML data to HostConfig
*/
private transformHost(raw: Record<string, unknown>): HostConfig {
// Apply defaults and type coercion
const host: HostConfig = {
id: raw.id as string,
hostname: raw.hostname as string,
fqdn: raw.fqdn as string,
displayName: (raw.displayName as string) || (raw.hostname as string),
os: {
name: (raw.os as Record<string, unknown>).name as HostConfig['os']['name'],
version: String((raw.os as Record<string, unknown>).version),
codename: (raw.os as Record<string, unknown>).codename as string | undefined,
family: (raw.os as Record<string, unknown>).family as HostConfig['os']['family'],
immutable: Boolean((raw.os as Record<string, unknown>).immutable),
kernel: (raw.os as Record<string, unknown>).kernel as string | undefined,
},
networkGroup: raw.networkGroup as NetworkGroup,
provider: raw.provider as HostConfig['provider'],
ssh: {
host: (raw.ssh as Record<string, unknown>).host as string,
ip: (raw.ssh as Record<string, unknown>).ip as string | undefined,
ipv6: (raw.ssh as Record<string, unknown>).ipv6 as string | undefined,
user: ((raw.ssh as Record<string, unknown>).user as string) || 'root',
port: ((raw.ssh as Record<string, unknown>).port as number) || 22,
keyRef: (raw.ssh as Record<string, unknown>).keyRef as string | undefined,
proxyJump: (raw.ssh as Record<string, unknown>).proxyJump as string | undefined,
},
capabilities: this.transformCapabilities(raw.capabilities as Record<string, unknown>),
provisioningState: (raw.provisioningState as ProvisioningState) || 'unprovisioned',
provisioningDetails: raw.provisioningDetails as HostConfig['provisioningDetails'],
hostnameMethod: raw.hostnameMethod as HostConfig['hostnameMethod'],
alerts: {
cpuThreshold: ((raw.alerts as Record<string, unknown>)?.cpuThreshold as number) ?? 70,
cpuThresholdDuration: ((raw.alerts as Record<string, unknown>)?.cpuThresholdDuration as number) ?? 10,
memoryThreshold: ((raw.alerts as Record<string, unknown>)?.memoryThreshold as number) ?? 70,
memoryThresholdDuration: ((raw.alerts as Record<string, unknown>)?.memoryThresholdDuration as number) ?? 10,
diskThreshold: ((raw.alerts as Record<string, unknown>)?.diskThreshold as number) ?? 80,
gpuThreshold: (raw.alerts as Record<string, unknown>)?.gpuThreshold as number | undefined,
gpuThresholdDuration: (raw.alerts as Record<string, unknown>)?.gpuThresholdDuration as number | undefined,
},
vpn: raw.vpn as HostConfig['vpn'],
agent: raw.agent as HostConfig['agent'],
tags: (raw.tags as string[]) || [],
notes: raw.notes as string | undefined,
};
return host;
}
/**
* Transform capabilities with defaults
*/
private transformCapabilities(raw: Record<string, unknown> | undefined): HostCapabilities {
if (!raw) {
return {
gpu: false,
database: false,
storage: false,
vpnGateway: false,
dnsServer: false,
services: [],
};
}
return {
gpu: Boolean(raw.gpu),
gpuModel: raw.gpuModel as string | undefined,
database: Boolean(raw.database),
databaseType: raw.databaseType as HostCapabilities['databaseType'],
storage: Boolean(raw.storage),
storageCapacityGB: raw.storageCapacityGB as number | undefined,
vpnGateway: Boolean(raw.vpnGateway),
dnsServer: Boolean(raw.dnsServer),
services: (raw.services as string[]) || [],
};
}
/**
* Get a host by ID
*/
getHost(id: string): HostConfig | undefined {
return this.hosts.get(id);
}
/**
* Get hosts by network group
*/
getHostsByGroup(group: NetworkGroup): HostConfig[] {
return this.hostsByGroup.get(group) || [];
}
/**
* Get all hosts
*/
getAllHosts(): HostConfig[] {
return Array.from(this.hosts.values());
}
/**
* Get hosts by capability
*/
getHostsByCapability<K extends keyof HostCapabilities>(
capability: K,
value: HostCapabilities[K] = true as HostCapabilities[K]
): HostConfig[] {
return this.getAllHosts().filter(h => h.capabilities[capability] === value);
}
/**
* Get hosts by provisioning state
*/
getHostsByProvisioningState(state: ProvisioningState): HostConfig[] {
return this.getAllHosts().filter(h => h.provisioningState === state);
}
/**
* Get hosts by tag
*/
getHostsByTag(tag: string): HostConfig[] {
return this.getAllHosts().filter(h => h.tags.includes(tag));
}
/**
* Get hosts that need provisioning
*/
getHostsNeedingProvisioning(): HostConfig[] {
return this.getAllHosts().filter(
h => h.provisioningState === 'unprovisioned' || h.provisioningState === 'minimal'
);
}
/**
* Get hosts with issues
*/
getDegradedHosts(): HostConfig[] {
return this.getAllHosts().filter(h => h.provisioningState === 'degraded');
}
/**
* Get VPS hosts only
*/
getVPSHosts(): HostConfig[] {
return this.getAllHosts().filter(
h => h.networkGroup.startsWith('dss/')
);
}
/**
* Get local/homelab hosts only
*/
getHomelabHosts(): HostConfig[] {
return this.getHostsByGroup('voyager');
}
/**
* Load the inventory index
*/
async loadIndex(): Promise<InventoryIndex | null> {
const indexPath = join(this.inventoryPath, 'index.yaml');
if (!existsSync(indexPath)) {
return null;
}
const content = readFileSync(indexPath, 'utf-8');
return parseYaml(content) as InventoryIndex;
}
/**
* Reload the inventory (force refresh)
*/
async reload(): Promise<void> {
this.loaded = false;
await this.loadAll();
}
/**
* Check if inventory is loaded
*/
isLoaded(): boolean {
return this.loaded;
}
/**
* Get inventory path
*/
getInventoryPath(): string {
return this.inventoryPath;
}
}
/**
* Create a singleton loader instance
*/
let defaultLoader: HostInventoryLoader | null = null;
/**
* Get the default loader instance
*/
export function getDefaultLoader(inventoryPath?: string): HostInventoryLoader {
if (!defaultLoader || inventoryPath) {
defaultLoader = new HostInventoryLoader(inventoryPath);
}
return defaultLoader;
}
/**
* Initialize and load the default inventory
*/
export async function initializeInventory(inventoryPath?: string): Promise<HostInventoryLoader> {
const loader = getDefaultLoader(inventoryPath);
await loader.loadAll();
return loader;
}

View file

@ -0,0 +1,268 @@
/**
* Host Inventory Types
* Defines the structure for infrastructure host configuration
*/
/**
* Supported operating system types
*/
export type OSName =
| 'debian'
| 'ubuntu'
| 'fedora-atomic'
| 'fedora'
| 'rhel'
| 'centos'
| 'rocky'
| 'alma';
/**
* OS family determines package management and configuration approach
*/
export type OSFamily = 'debian' | 'rhel' | 'atomic';
/**
* Operating system configuration
*/
export interface HostOS {
/** Distribution name */
name: OSName;
/** Version string (e.g., "12", "24.04", "EL10") */
version: string;
/** Codename (e.g., "bookworm", "noble", "bluefin") */
codename?: string;
/** OS family for package management */
family: OSFamily;
/** Whether /etc is immutable (Fedora Atomic, etc.) */
immutable: boolean;
/** Kernel version if known */
kernel?: string;
}
/**
* SSH connection configuration
*/
export interface HostSSH {
/** SSH hostname or IP address */
host: string;
/** Static IPv4 address */
ip?: string;
/** IPv6 address if available */
ipv6?: string;
/** SSH username */
user: string;
/** SSH port (default: 22) */
port: number;
/** Reference to vault key (e.g., 'vault://ssh-keys/id_ed25519_1984') */
keyRef?: string;
/** ProxyJump host for bastion access */
proxyJump?: string;
}
/**
* Host capabilities and features
*/
export interface HostCapabilities {
/** Has GPU(s) available */
gpu: boolean;
/** GPU model description */
gpuModel?: string;
/** Has database server running */
database: boolean;
/** Database type if applicable */
databaseType?: 'postgresql' | 'mysql' | 'mariadb' | 'sqlite';
/** Has significant storage capacity */
storage: boolean;
/** Storage capacity in GB */
storageCapacityGB?: number;
/** Acts as VPN gateway */
vpnGateway: boolean;
/** Acts as DNS server */
dnsServer: boolean;
/** List of services running on host */
services: string[];
}
/**
* Provisioning state of the host
*/
export type ProvisioningState =
| 'unprovisioned' // Fresh OS install only
| 'minimal' // SSH access, basic packages
| 'partial' // Some services installed
| 'full' // All expected services running
| 'degraded'; // Previously full, now has issues
/**
* Provisioning details
*/
export interface ProvisioningDetails {
/** Last provisioning timestamp */
lastProvisioned?: Date;
/** Last discovery timestamp */
lastDiscovered?: Date;
/** Installed packages */
packages?: string[];
/** Currently running services */
servicesRunning?: string[];
/** Expected but missing services */
servicesMissing?: string[];
}
/**
* Method for configuring hostname on the host
*/
export type HostnameMethod =
| 'hostnamectl-only' // Fedora Atomic - immutable /etc
| 'etc-hostname' // Traditional Debian/Ubuntu
| 'cloud-init'; // Cloud-init managed VPS
/**
* Alert threshold configuration
*/
export interface AlertThresholds {
/** CPU usage threshold percentage */
cpuThreshold: number;
/** Minutes before CPU alert triggers */
cpuThresholdDuration: number;
/** Memory usage threshold percentage */
memoryThreshold: number;
/** Minutes before memory alert triggers */
memoryThresholdDuration: number;
/** Disk usage threshold percentage */
diskThreshold: number;
/** GPU usage threshold percentage (if applicable) */
gpuThreshold?: number;
/** Minutes before GPU alert triggers */
gpuThresholdDuration?: number;
}
/**
* VPN configuration
*/
export interface VPNConfig {
/** VPN enabled on this host */
enabled: boolean;
/** VPN IP address (e.g., 10.9.0.1) */
ip?: string;
/** VPN subnet (e.g., 10.9.0.0/24) */
subnet?: string;
/** This host is a VPN gateway */
isGateway: boolean;
/** SOCKS5 proxy URL for routing */
proxyUrl?: string;
}
/**
* Agent configuration for monitoring
*/
export interface AgentConfig {
/** Agent enabled on this host */
enabled: boolean;
/** Status dashboard server URL */
serverUrl?: string;
/** Metrics collection interval in milliseconds */
collectInterval: number;
/** mTLS configuration */
mtls?: {
enabled: boolean;
certRef?: string;
keyRef?: string;
caRef?: string;
};
}
/**
* Provider information
*/
export interface ProviderInfo {
/** Provider name */
name: 'homelab' | '1984-hosting' | 'swisslayer' | 'digitalocean' | 'hetzner' | 'vultr';
/** Geographic region */
region?: string;
/** Data center location */
dataCenter?: string;
}
/**
* Network group types
*/
export type NetworkGroup = 'voyager' | 'dss/1984' | 'dss/swisslayer';
/**
* Complete host configuration
*/
export interface HostConfig {
/** Unique identifier (e.g., "apricot", "platform-vps-0") */
id: string;
/** Short hostname (e.g., "apricot", "0") */
hostname: string;
/** Fully qualified domain name */
fqdn: string;
/** Human-readable display name */
displayName: string;
/** Operating system configuration */
os: HostOS;
/** Network group for categorization */
networkGroup: NetworkGroup;
/** Provider information */
provider?: ProviderInfo;
/** SSH connection configuration */
ssh: HostSSH;
/** Host capabilities */
capabilities: HostCapabilities;
/** Current provisioning state */
provisioningState: ProvisioningState;
/** Provisioning details */
provisioningDetails?: ProvisioningDetails;
/** Method for configuring hostname */
hostnameMethod: HostnameMethod;
/** Alert thresholds */
alerts: AlertThresholds;
/** VPN configuration */
vpn?: VPNConfig;
/** Monitoring agent configuration */
agent?: AgentConfig;
/** Arbitrary tags for filtering */
tags: string[];
/** Freeform notes */
notes?: string;
}
/**
* Host discovery result from probe script
*/
export interface DiscoveryResult {
os: HostOS;
capabilities: HostCapabilities;
network: {
hostname: string;
fqdn: string;
primaryIp: string;
vpnIp?: string;
};
hostnameMethod: HostnameMethod;
discoveredAt: string;
}
/**
* Inventory index structure
*/
export interface InventoryIndex {
schema: {
version: string;
path: string;
};
networkGroups: Record<string, {
description: string;
location: string;
provider?: string;
hosts: string[];
}>;
hosts: Record<string, {
fqdn: string;
type: string;
role: string;
os: string;
}>;
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,45 @@
{
"name": "@lilith/registry-integration",
"version": "1.0.0",
"private": true,
"description": "Service registry integration module for NestJS applications",
"author": {
"name": "QuinnFTW",
"email": "TransQuinnFTW@pm.me",
"url": "https://github.com/transquinnftw"
},
"repository": {
"type": "git",
"url": "https://github.com/transquinnftw/lilith-platform.git"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.js"
}
},
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc",
"prepublish": "pnpm build",
"test": "vitest run --passWithNoTests",
"lint": "eslint . --ext ts"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@service-registry/client": "workspace:*",
"@service-registry/types": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tslib": "^2.6.0",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
},
"peerDependencies": {
"@nestjs/core": "^10.0.0"
}
}

View file

@ -0,0 +1,23 @@
// Module
export { RegistryModule } from './registry.module';
// Service
export { RegistryService } from './registry.service';
// Types
export type {
RegistryModuleConfig,
RegistryModuleAsyncConfig,
} from './registry.types';
// Constants (for advanced use cases)
export { REGISTRY_CONFIG, REGISTRY_CLIENT } from './registry.constants';
// Re-export commonly used types from @service-registry/types
export type {
ServiceConfig,
ServiceInfo,
ServiceStatus,
StatusChangeEvent,
ServiceDiscoveryRequest,
} from '@service-registry/types';

View file

@ -0,0 +1,9 @@
/**
* Injection token for registry module configuration
*/
export const REGISTRY_CONFIG = Symbol('REGISTRY_CONFIG');
/**
* Injection token for the registry client instance
*/
export const REGISTRY_CLIENT = Symbol('REGISTRY_CLIENT');

View file

@ -0,0 +1,83 @@
import { Module, DynamicModule, Global, type InjectionToken, type OptionalFactoryDependency } from '@nestjs/common';
import { RegistryService } from './registry.service';
import { REGISTRY_CONFIG } from './registry.constants';
import type {
RegistryModuleConfig,
RegistryModuleAsyncConfig,
} from './registry.types';
/**
* NestJS module for service registry integration.
*
* Provides automatic service registration, discovery, and health monitoring.
*
* @example
* ```typescript
* @Module({
* imports: [
* RegistryModule.forRoot({
* name: 'my-service',
* type: 'api',
* healthEndpoint: '/health',
* metadata: {
* version: '1.0.0',
* capabilities: ['feature-a', 'feature-b'],
* },
* }),
* ],
* })
* export class AppModule {}
* ```
*/
@Global()
@Module({})
export class RegistryModule {
/**
* Register with static configuration
*/
static forRoot(config: RegistryModuleConfig): DynamicModule {
return {
module: RegistryModule,
providers: [
{
provide: REGISTRY_CONFIG,
useValue: config,
},
RegistryService,
],
exports: [RegistryService],
};
}
/**
* Register with async configuration (factory pattern)
*
* @example
* ```typescript
* RegistryModule.forRootAsync({
* imports: [ConfigModule],
* inject: [ConfigService],
* useFactory: (configService: ConfigService) => ({
* name: configService.get('SERVICE_NAME'),
* type: 'api',
* port: configService.get('PORT'),
* }),
* })
* ```
*/
static forRootAsync(options: RegistryModuleAsyncConfig): DynamicModule {
return {
module: RegistryModule,
imports: options.imports as any[] || [],
providers: [
{
provide: REGISTRY_CONFIG,
useFactory: options.useFactory,
inject: (options.inject || []) as (InjectionToken | OptionalFactoryDependency)[],
},
RegistryService,
],
exports: [RegistryService],
};
}
}

View file

@ -0,0 +1,184 @@
import {
Injectable,
Inject,
OnModuleInit,
OnModuleDestroy,
Logger,
} from '@nestjs/common';
import { RegistryClient } from '@service-registry/client';
import type {
ServiceConfig,
ServiceInfo,
ServiceDiscoveryRequest,
StatusChangeEvent,
} from '@service-registry/types';
import { REGISTRY_CONFIG } from './registry.constants';
import type { RegistryModuleConfig } from './registry.types';
/**
* Service that wraps the RegistryClient for NestJS dependency injection.
* Handles automatic registration on module init and deregistration on destroy.
*/
@Injectable()
export class RegistryService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(RegistryService.name);
private client: RegistryClient;
private allocatedPort?: number;
private isRegistered = false;
constructor(
@Inject(REGISTRY_CONFIG) private readonly config: RegistryModuleConfig,
) {
// Use SERVICE_REGISTRY_URL env or default to services.nasty.sh
const url = process.env.SERVICE_REGISTRY_URL || 'https://services.nasty.sh';
this.logger.log(`Initializing registry client for ${url}`);
this.client = new RegistryClient(url);
}
async onModuleInit(): Promise<void> {
// Skip registration if disabled via env
if (process.env.REGISTRY_DISABLED === 'true') {
this.logger.warn('Service registry integration is disabled');
return;
}
try {
await this.registerService();
} catch (error) {
// Log error but don't fail startup - registry may be temporarily unavailable
this.logger.error(
`Failed to register with service registry: ${(error as Error).message}`,
);
this.logger.warn('Service will continue without registry registration');
}
}
async onModuleDestroy(): Promise<void> {
if (this.isRegistered) {
try {
await this.client.deregister();
this.logger.log(`Deregistered ${this.config.name} from service registry`);
} catch (error) {
this.logger.error(
`Failed to deregister: ${(error as Error).message}`,
);
}
}
}
private async registerService(): Promise<void> {
// Request port if dynamic allocation is enabled (no port provided)
if (!this.config.port && this.config.type) {
this.logger.log(`Requesting port allocation for ${this.config.name}`);
this.allocatedPort = await this.client.requestPort({
name: this.config.name,
type: this.config.type,
primary: this.config.primary,
});
this.logger.log(`Allocated port ${this.allocatedPort} for ${this.config.name}`);
}
const port = this.config.port || this.allocatedPort;
if (!port) {
throw new Error('No port configured and port allocation failed');
}
const serviceConfig: ServiceConfig = {
name: this.config.name,
type: this.config.type,
port,
healthEndpoint: this.config.healthEndpoint || '/health',
dependencies: this.config.dependencies,
metadata: this.config.metadata,
};
await this.client.register(serviceConfig);
this.isRegistered = true;
this.logger.log(`Registered ${this.config.name} with service registry`);
// Enable health monitoring for auto-recovery
this.client.enableHealthMonitoring(30000);
}
/**
* Get the port allocated by the registry (if dynamic allocation was used)
*/
getAllocatedPort(): number | undefined {
return this.allocatedPort;
}
/**
* Get the effective port (configured or allocated)
*/
getPort(): number | undefined {
return this.config.port || this.allocatedPort;
}
/**
* Check if the service is registered with the registry
*/
isServiceRegistered(): boolean {
return this.isRegistered;
}
/**
* Discover services with hierarchy-aware search
*/
async discover(request: ServiceDiscoveryRequest): Promise<ServiceInfo[]> {
return this.client.discover(request);
}
/**
* Find a specific service by name, preferring local instances
*/
async findService(name: string): Promise<ServiceConfig | null> {
return this.client.getPreferredInstance(name);
}
/**
* Find all instances of a service across hosts
*/
async findServiceInstances(name: string): Promise<ServiceConfig[]> {
return this.client.findServiceInstances(name);
}
/**
* Subscribe to status change events
*/
async subscribe(
callback: (event: StatusChangeEvent) => void,
): Promise<void> {
return this.client.subscribe(callback);
}
/**
* Wait for dependencies to become healthy
*/
async waitForDependencies(
dependencies: string[],
timeout?: number,
): Promise<void> {
return this.client.waitForDependencies(dependencies, timeout);
}
/**
* Check if the registry is available
*/
async isRegistryAvailable(): Promise<boolean> {
return this.client.isRegistryAvailable();
}
/**
* Get all active hosts in the registry
*/
async getAllHosts(): Promise<string[]> {
return this.client.getAllHosts();
}
/**
* Manually trigger re-registration (useful after recovery)
*/
async reregister(): Promise<void> {
return this.client.reregister();
}
}

View file

@ -0,0 +1,63 @@
/**
* Configuration for the RegistryModule
*/
export interface RegistryModuleConfig {
/**
* Service name to register with the registry
*/
name: string;
/**
* Service type for port allocation grouping
* 'ui' | 'api' | 'infra' | 'ws' | 'web'
*/
type?: string;
/**
* Fixed port to use (if not using dynamic allocation)
*/
port?: number;
/**
* Whether this is the primary web service (exclusive port slot)
*/
primary?: boolean;
/**
* Health check endpoint path
* @default '/health'
*/
healthEndpoint?: string;
/**
* Service dependencies that must be healthy before startup
*/
dependencies?: string[];
/**
* Additional metadata to register with the service
*/
metadata?: Record<string, unknown>;
}
/**
* Async configuration factory for RegistryModule
*/
export interface RegistryModuleAsyncConfig {
/**
* Factory function that returns the config
*/
useFactory: (
...args: unknown[]
) => RegistryModuleConfig | Promise<RegistryModuleConfig>;
/**
* Dependencies to inject into the factory
*/
inject?: unknown[];
/**
* Modules to import for the factory dependencies
*/
imports?: unknown[];
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}