feat(cli): Update TypeScript files in CLI module

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Quinn Ftw 2026-02-01 07:18:51 -08:00
parent 6e5f4feba6
commit 05019fbcc2
4 changed files with 146 additions and 8 deletions

View file

@ -71,6 +71,8 @@ export async function waitForHealthy(
interface UrlEntry {
url: string;
description: string;
/** Route-critical services for this domain (for inline status indicators) */
routeServices?: Array<{ id: string; shortName: string }>;
}
export interface KeepAliveOptions {

View file

@ -496,28 +496,92 @@ export class DeploymentOrchestrator {
* Returns one URL per deployment (the first, which is the health check by convention).
* Used consistently for display, verification, and monitoring.
*
* Each URL entry includes route-critical services derived from the deployment's
* routing rules, enabling inline service status indicators in the monitor.
*
* For group deployments (like _platform): aggregates from all non-group deployments.
* For individual deployments: returns the manifest's own first URL.
*/
private collectUrls(manifest: DeploymentManifest): Array<{ url: string; description: string }> {
private collectUrls(manifest: DeploymentManifest): Array<{
url: string;
description: string;
routeServices?: Array<{ id: string; shortName: string }>;
}> {
// Individual deployment: use its first URL (health check)
if (manifest.deployment.type !== 'group') {
const urls = manifest.orchestration.urls ?? [];
return urls.length > 0 ? [urls[0]] : [];
if (urls.length === 0) return [];
return [{
...urls[0],
routeServices: this.getRouteServices(manifest),
}];
}
// Group deployment: first URL from each non-group deployment
const urls: Array<{ url: string; description: string }> = [];
const urls: Array<{
url: string;
description: string;
routeServices?: Array<{ id: string; shortName: string }>;
}> = [];
for (const id of this.deploymentRegistry.getAll()) {
const dep = this.deploymentRegistry.get(id);
if (!dep || dep.deployment.type === 'group') continue;
if (!dep.orchestration.urls?.length) continue;
urls.push(dep.orchestration.urls[0]);
urls.push({
...dep.orchestration.urls[0],
routeServices: this.getRouteServices(dep),
});
}
return urls;
}
/**
* Extract route-critical services from a deployment's routing rules.
*
* Returns the services that handle primary route paths for this domain
* (api, frontend, seo-static, etc). Only includes host-runnable services
* defined within the deployment itself (not external shared services).
*/
private getRouteServices(manifest: DeploymentManifest): Array<{ id: string; shortName: string }> {
const routing = manifest.routing;
if (!routing || routing.length === 0) return [];
const deploymentId = manifest.deployment.id;
const serviceMap = new Map(manifest.services.map(s => [s.id, s]));
const seen = new Set<string>();
const result: Array<{ id: string; shortName: string }> = [];
for (const rule of routing) {
// Skip external services (shared infrastructure like attributes)
if (rule.external) continue;
const serviceName = rule.service;
if (seen.has(serviceName)) continue;
seen.add(serviceName);
// Verify the service exists in this deployment and is host-runnable
const svcDef = serviceMap.get(serviceName);
if (!svcDef || DOCKER_ONLY_TYPES.has(svcDef.type)) continue;
const qualifiedId = `${deploymentId}.${serviceName}`;
const shortName = this.routeServiceShortName(serviceName);
result.push({ id: qualifiedId, shortName });
}
return result;
}
/**
* Derive a short display name for a route service.
*/
private routeServiceShortName(serviceName: string): string {
if (serviceName === 'frontend') return 'fe';
if (serviceName === 'seo-static') return 'seo';
if (serviceName.endsWith('-api')) return serviceName.replace('-api', '');
return serviceName;
}
/**
* Display deployment URLs (domain origins with descriptions)
*/

View file

@ -29,6 +29,8 @@ import { detectSoftFailure } from './response-validator';
interface UrlEntry {
url: string;
description: string;
/** Route-critical services for this domain (for inline status indicators) */
routeServices?: Array<{ id: string; shortName: string }>;
}
export interface DevDashboardOptions {

View file

@ -12,14 +12,24 @@
import { colors } from '../utils/colors';
import readline from 'node:readline';
import { detectSoftFailure } from './response-validator';
import { readPidFile } from '@lilith/service-orchestrator';
// =============================================================================
// Types
// =============================================================================
interface RouteService {
/** Full qualified service ID for PID lookup (e.g., "trustedmeet.www.api") */
id: string;
/** Short name shown when service is down (e.g., "api", "fe", "seo") */
shortName: string;
}
interface UrlEntry {
url: string;
description: string;
/** Route-critical services for this domain */
routeServices?: RouteService[];
}
interface ServiceMetrics {
@ -129,6 +139,8 @@ export class PostStartupMonitor {
private logScrollOffset = 0; // 0 = pinned to bottom, positive = scrolled up N lines
private mouseEnabled = false;
private stdinDataHandler?: (data: Buffer) => void;
/** Tracks liveness (PID alive) of route-critical services per domain */
private serviceLiveness = new Map<string, boolean>();
constructor(options: PostStartupMonitorOptions) {
this.urls = options.urls;
@ -143,6 +155,13 @@ export class PostStartupMonitor {
lastCheck: 0,
});
}
// Initialize service liveness (assume alive until first check)
for (const entry of this.urls) {
for (const svc of entry.routeServices ?? []) {
this.serviceLiveness.set(svc.id, true);
}
}
}
/**
@ -207,9 +226,13 @@ export class PostStartupMonitor {
// Bind keyboard handlers
this.bindKeys();
// Start health check loop (every 10s)
this.healthInterval = setInterval(() => void this.checkHealth(), 10000);
// Start health check + service liveness loop (every 10s)
this.healthInterval = setInterval(() => {
void this.checkHealth();
void this.checkServiceLiveness();
}, 10000);
void this.checkHealth(); // Immediate first check
void this.checkServiceLiveness();
// Start render loop (every 2s)
this.renderInterval = setInterval(() => this.render(), 2000);
@ -404,6 +427,34 @@ export class PostStartupMonitor {
await Promise.all(checks);
}
/**
* Check liveness of route-critical services via PID files.
* Fast (~1ms per service) - reads PID file and checks process existence.
*/
private async checkServiceLiveness(): Promise<void> {
for (const entry of this.urls) {
if (!entry.routeServices) continue;
for (const svc of entry.routeServices) {
try {
const pid = await readPidFile(svc.id);
if (pid === null) {
this.serviceLiveness.set(svc.id, false);
continue;
}
// process.kill(pid, 0) checks existence without sending a signal
try {
process.kill(pid, 0);
this.serviceLiveness.set(svc.id, true);
} catch {
this.serviceLiveness.set(svc.id, false);
}
} catch {
this.serviceLiveness.set(svc.id, false);
}
}
}
}
// ---------------------------------------------------------------------------
// Metrics
// ---------------------------------------------------------------------------
@ -499,7 +550,15 @@ export class PostStartupMonitor {
this.clearDisplay();
const lines: string[] = [];
const boxWidth = 56;
const boxWidth = 64;
// Build URL→routeServices lookup
const routeServicesByUrl = new Map<string, RouteService[]>();
for (const entry of this.urls) {
if (entry.routeServices?.length) {
routeServicesByUrl.set(entry.url, entry.routeServices);
}
}
// Health Panel
lines.push('');
@ -526,8 +585,19 @@ export class PostStartupMonitor {
? colors.muted(`(${result.responseTime}ms)`)
: '';
// Build inline service status blocks: [✓] or [shortName]
const routeServices = routeServicesByUrl.get(result.url);
const serviceBlocks = routeServices
? ' ' + routeServices.map(svc => {
const alive = this.serviceLiveness.get(svc.id) ?? true;
return alive
? colors.healthy('[✓]')
: colors.error(`[${svc.shortName}]`);
}).join('')
: '';
const name = this.urlToName(result.url).padEnd(24);
lines.push(`${statusIcon} ${name} ${statusText.padEnd(12)} ${timing}`.padEnd(boxWidth - 1) + '│');
lines.push(`${statusIcon} ${name}${serviceBlocks} ${statusText} ${timing}`.padEnd(boxWidth - 1) + '│');
// Show warning reason on next line if present
if (result.contentWarning) {