feat(cli): ✨ Update TypeScript files in CLI module
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
6e5f4feba6
commit
05019fbcc2
4 changed files with 146 additions and 8 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue