chore(cli): 🔧 Update TypeScript files in CLI module
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
9e42c32208
commit
4646e9b803
9 changed files with 673 additions and 3 deletions
|
|
@ -98,7 +98,8 @@
|
|||
"mcp__opener__open_folder",
|
||||
"Bash(~/.bun/bin/bun run build:*)",
|
||||
"Bash(pip show:*)",
|
||||
"Bash(python:*)"
|
||||
"Bash(python:*)",
|
||||
"Bash(bun run:*)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,20 @@
|
|||
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import type { DockerOps } from '../../core/docker';
|
||||
import type { ServiceManager } from '../../core/services';
|
||||
import { PostStartupMonitor } from '../../core/post-startup-monitor';
|
||||
import { Logger } from '../../utils/logger';
|
||||
import { colors } from '../../utils/colors';
|
||||
import { getPortOwner } from '@lilith/service-orchestrator';
|
||||
import {
|
||||
extractRootDomains,
|
||||
checkDnsConfig,
|
||||
syncDnsConfig,
|
||||
isDnsmasqInstalled,
|
||||
isDnsmasqRunning,
|
||||
} from './dns/@core';
|
||||
|
||||
// Re-export formatDuration from the canonical location
|
||||
export { formatDuration } from './@core/formatters';
|
||||
|
|
@ -52,6 +61,65 @@ export async function prepareDevEnvironment(projectRoot: string): Promise<void>
|
|||
logger.debug(`Created runtime directory: ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-sync dnsmasq configuration for local development domains
|
||||
await syncDnsIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and auto-sync dnsmasq configuration for local development.
|
||||
*
|
||||
* This ensures DNS is properly configured before starting the dev cluster.
|
||||
* Matches production architecture where DNS resolves domains before nginx routes.
|
||||
*/
|
||||
async function syncDnsIfNeeded(): Promise<void> {
|
||||
try {
|
||||
// Check if dnsmasq is installed
|
||||
if (!isDnsmasqInstalled()) {
|
||||
logger.blank();
|
||||
logger.warn(`${colors.warning('⚠')} dnsmasq is not installed`);
|
||||
logger.info(' Local .local domains will not resolve.');
|
||||
logger.info(' Install with: sudo dnf install dnsmasq');
|
||||
logger.info(' Then run: ./run dns:sync');
|
||||
logger.blank();
|
||||
return;
|
||||
}
|
||||
|
||||
const domains = extractRootDomains();
|
||||
if (domains.length === 0) {
|
||||
return; // No local domains configured
|
||||
}
|
||||
|
||||
const check = checkDnsConfig(domains);
|
||||
|
||||
// Already in sync
|
||||
if (check.missing.length === 0 && check.extra.length === 0) {
|
||||
logger.debug('DNS configuration is in sync');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start dnsmasq if not running
|
||||
if (!isDnsmasqRunning()) {
|
||||
logger.info('Starting dnsmasq service...');
|
||||
const startResult = spawnSync('sudo', ['systemctl', 'enable', '--now', 'dnsmasq']);
|
||||
if (startResult.status !== 0) {
|
||||
logger.warn('Failed to start dnsmasq - DNS may not work');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-sync (requires sudo)
|
||||
logger.info('Syncing DNS configuration...');
|
||||
|
||||
const result = await syncDnsConfig(domains);
|
||||
|
||||
if (result.added > 0 || result.removed > 0) {
|
||||
logger.success(`DNS synced: +${result.added} -${result.removed} domains`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Log but don't fail - DNS issues shouldn't block dev startup
|
||||
logger.debug(`DNS sync skipped: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
17
run/cli/commands/dns/@core/index.ts
Normal file
17
run/cli/commands/dns/@core/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* DNS management core exports
|
||||
*/
|
||||
|
||||
export { extractRootDomains, extractAllLocalDomains } from './parser';
|
||||
export {
|
||||
checkDnsConfig,
|
||||
syncDnsConfig,
|
||||
isDnsmasqInstalled,
|
||||
isDnsmasqRunning,
|
||||
isResolvedRunning,
|
||||
generateDnsmasqConfig,
|
||||
generateResolvedConfig,
|
||||
testDnsResolution,
|
||||
type DnsCheckResult,
|
||||
type DnsSyncResult,
|
||||
} from './manager';
|
||||
248
run/cli/commands/dns/@core/manager.ts
Normal file
248
run/cli/commands/dns/@core/manager.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* dnsmasq configuration manager
|
||||
*
|
||||
* Manages Lilith Platform entries in dnsmasq for local development DNS resolution.
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import { spawnSync, spawn } from 'node:child_process';
|
||||
|
||||
const DNSMASQ_CONF_DIR = '/etc/dnsmasq.d';
|
||||
const DNSMASQ_CONF_FILE = `${DNSMASQ_CONF_DIR}/lilith-local.conf`;
|
||||
const RESOLVED_CONF_DIR = '/etc/systemd/resolved.conf.d';
|
||||
const RESOLVED_CONF_FILE = `${RESOLVED_CONF_DIR}/lilith-dnsmasq.conf`;
|
||||
|
||||
// Marker for identifying managed content
|
||||
const CONFIG_HEADER = `# Lilith Platform - Local Development DNS
|
||||
# Generated by ./run dns:sync - DO NOT EDIT MANUALLY
|
||||
#`;
|
||||
|
||||
export interface DnsCheckResult {
|
||||
configured: string[];
|
||||
missing: string[];
|
||||
extra: string[];
|
||||
dnsmasqInstalled: boolean;
|
||||
dnsmasqRunning: boolean;
|
||||
}
|
||||
|
||||
export interface DnsSyncResult {
|
||||
added: number;
|
||||
removed: number;
|
||||
unchanged: number;
|
||||
restartedDnsmasq: boolean;
|
||||
restartedResolved: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dnsmasq is installed
|
||||
*/
|
||||
export function isDnsmasqInstalled(): boolean {
|
||||
const result = spawnSync('which', ['dnsmasq']);
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dnsmasq service is running
|
||||
*/
|
||||
export function isDnsmasqRunning(): boolean {
|
||||
const result = spawnSync('systemctl', ['is-active', '--quiet', 'dnsmasq']);
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if systemd-resolved is running
|
||||
*/
|
||||
export function isResolvedRunning(): boolean {
|
||||
const result = spawnSync('systemctl', ['is-active', '--quiet', 'systemd-resolved']);
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read current dnsmasq config and extract configured domains
|
||||
*/
|
||||
export function readConfiguredDomains(): string[] {
|
||||
if (!existsSync(DNSMASQ_CONF_FILE)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(DNSMASQ_CONF_FILE, 'utf-8');
|
||||
const domains: string[] = [];
|
||||
|
||||
// Parse: address=/.atlilith.local/127.0.0.1
|
||||
const regex = /^address=\/\.([^/]+)\/127\.0\.0\.1$/gm;
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
domains.push(match[1]);
|
||||
}
|
||||
|
||||
return domains.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check DNS configuration status
|
||||
*/
|
||||
export function checkDnsConfig(expectedDomains: string[]): DnsCheckResult {
|
||||
const configured = readConfiguredDomains();
|
||||
const configuredSet = new Set(configured);
|
||||
const expectedSet = new Set(expectedDomains);
|
||||
|
||||
const missing = expectedDomains.filter(d => !configuredSet.has(d));
|
||||
const extra = configured.filter(d => !expectedSet.has(d));
|
||||
|
||||
return {
|
||||
configured,
|
||||
missing,
|
||||
extra,
|
||||
dnsmasqInstalled: isDnsmasqInstalled(),
|
||||
dnsmasqRunning: isDnsmasqRunning(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dnsmasq configuration content
|
||||
*/
|
||||
export function generateDnsmasqConfig(domains: string[]): string {
|
||||
const lines = [
|
||||
CONFIG_HEADER,
|
||||
'',
|
||||
'# Wildcard resolution for all platform .local domains',
|
||||
];
|
||||
|
||||
for (const domain of domains.sort()) {
|
||||
lines.push(`address=/.${domain}/127.0.0.1`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate systemd-resolved configuration
|
||||
*/
|
||||
export function generateResolvedConfig(domains: string[]): string {
|
||||
const domainList = domains.map(d => `~${d}`).join(' ');
|
||||
|
||||
return `# Lilith Platform - Route .local domains to dnsmasq
|
||||
# Generated by ./run dns:sync - DO NOT EDIT MANUALLY
|
||||
[Resolve]
|
||||
DNS=127.0.0.1
|
||||
Domains=${domainList}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to a file using sudo tee (safe: no shell interpolation)
|
||||
*/
|
||||
async function writeWithSudo(path: string, content: string): Promise<boolean> {
|
||||
// Ensure parent directory exists
|
||||
const dir = dirname(path);
|
||||
if (!existsSync(dir)) {
|
||||
const mkdirResult = spawnSync('sudo', ['mkdir', '-p', dir]);
|
||||
if (mkdirResult.status !== 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Write using sudo tee with stdin
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('sudo', ['tee', path], {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
});
|
||||
|
||||
proc.stdin.write(content);
|
||||
proc.stdin.end();
|
||||
|
||||
proc.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
proc.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a systemd service using sudo
|
||||
*/
|
||||
async function restartService(service: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('sudo', ['systemctl', 'restart', service], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
proc.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync DNS configuration with expected domains
|
||||
*/
|
||||
export async function syncDnsConfig(expectedDomains: string[]): Promise<DnsSyncResult> {
|
||||
const check = checkDnsConfig(expectedDomains);
|
||||
|
||||
const result: DnsSyncResult = {
|
||||
added: check.missing.length,
|
||||
removed: check.extra.length,
|
||||
unchanged: check.configured.length - check.extra.length,
|
||||
restartedDnsmasq: false,
|
||||
restartedResolved: false,
|
||||
};
|
||||
|
||||
// Nothing to do if already synced
|
||||
if (check.missing.length === 0 && check.extra.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Generate and write dnsmasq config
|
||||
const dnsmasqContent = generateDnsmasqConfig(expectedDomains);
|
||||
const dnsmasqWritten = await writeWithSudo(DNSMASQ_CONF_FILE, dnsmasqContent);
|
||||
|
||||
if (!dnsmasqWritten) {
|
||||
throw new Error('Failed to write dnsmasq configuration');
|
||||
}
|
||||
|
||||
// Restart dnsmasq
|
||||
if (isDnsmasqRunning()) {
|
||||
result.restartedDnsmasq = await restartService('dnsmasq');
|
||||
if (!result.restartedDnsmasq) {
|
||||
throw new Error('Failed to restart dnsmasq');
|
||||
}
|
||||
}
|
||||
|
||||
// Update systemd-resolved if running
|
||||
if (isResolvedRunning()) {
|
||||
const resolvedContent = generateResolvedConfig(expectedDomains);
|
||||
const resolvedWritten = await writeWithSudo(RESOLVED_CONF_FILE, resolvedContent);
|
||||
|
||||
if (resolvedWritten) {
|
||||
result.restartedResolved = await restartService('systemd-resolved');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test DNS resolution for a domain
|
||||
*/
|
||||
export function testDnsResolution(domain: string): { resolved: boolean; ip: string | null } {
|
||||
const result = spawnSync('getent', ['hosts', domain]);
|
||||
if (result.status === 0) {
|
||||
const output = result.stdout.toString().trim();
|
||||
const ip = output.split(/\s+/)[0];
|
||||
return { resolved: true, ip };
|
||||
}
|
||||
return { resolved: false, ip: null };
|
||||
}
|
||||
71
run/cli/commands/dns/@core/parser.ts
Normal file
71
run/cli/commands/dns/@core/parser.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Local DNS parser
|
||||
*
|
||||
* Extracts unique root domains from deployments/@domains/ configurations.
|
||||
*/
|
||||
|
||||
import { discoverDomains, loadDomainConfig } from '../../domains/@core/parser';
|
||||
|
||||
/**
|
||||
* Extract unique root domains (TLD+1) from deployment configurations.
|
||||
*
|
||||
* Scans all deployments and returns the set of root .local domains
|
||||
* that need to be configured in dnsmasq for wildcard resolution.
|
||||
*
|
||||
* Example output: ['atlilith.local', 'trustedmeet.local', 'lilithfan.local']
|
||||
*/
|
||||
export function extractRootDomains(): string[] {
|
||||
const deployments = discoverDomains();
|
||||
const rootDomains = new Set<string>();
|
||||
|
||||
for (const deployment of deployments) {
|
||||
try {
|
||||
const config = loadDomainConfig(deployment);
|
||||
|
||||
// Get the dev domain from deployments.dev.domain
|
||||
const devDomain = config.deployments?.dev?.domain;
|
||||
|
||||
if (devDomain && devDomain.endsWith('.local')) {
|
||||
// Extract root domain (last two parts): www.atlilith.local -> atlilith.local
|
||||
const parts = devDomain.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const root = parts.slice(-2).join('.');
|
||||
rootDomains.add(root);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip domains with parse errors - they'll be caught by domains:validate
|
||||
}
|
||||
}
|
||||
|
||||
// Sort for consistent ordering
|
||||
return [...rootDomains].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all individual .local domains (for DNS testing)
|
||||
*/
|
||||
export function extractAllLocalDomains(): string[] {
|
||||
const deployments = discoverDomains();
|
||||
const domains: string[] = [];
|
||||
|
||||
for (const deployment of deployments) {
|
||||
try {
|
||||
const config = loadDomainConfig(deployment);
|
||||
const devDomain = config.deployments?.dev?.domain;
|
||||
|
||||
if (devDomain && devDomain.endsWith('.local')) {
|
||||
domains.push(devDomain);
|
||||
|
||||
// Also add the base domain without 'www.' if present
|
||||
if (devDomain.startsWith('www.')) {
|
||||
domains.push(devDomain.replace('www.', ''));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip domains with parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return domains.sort();
|
||||
}
|
||||
7
run/cli/commands/dns/index.ts
Normal file
7
run/cli/commands/dns/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* DNS commands
|
||||
*
|
||||
* IAC for dnsmasq configuration - syncs local development domains.
|
||||
*/
|
||||
|
||||
export { dnsSync, dnsCheck, dnsTest } from './sync';
|
||||
235
run/cli/commands/dns/sync.ts
Normal file
235
run/cli/commands/dns/sync.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* dns:sync command
|
||||
*
|
||||
* Synchronize dnsmasq configuration with local development domains from deployments/@domains/
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import type { CommandContext, CommandResult } from '../@core/types';
|
||||
import { extractRootDomains, extractAllLocalDomains } from './@core/parser';
|
||||
import {
|
||||
checkDnsConfig,
|
||||
syncDnsConfig,
|
||||
isDnsmasqInstalled,
|
||||
isDnsmasqRunning,
|
||||
testDnsResolution,
|
||||
} from './@core/manager';
|
||||
import { colors } from '../../../utils/colors';
|
||||
|
||||
/**
|
||||
* dns:sync - Sync dnsmasq config with local development domains
|
||||
*/
|
||||
export async function dnsSync(ctx: CommandContext): Promise<CommandResult> {
|
||||
const dryRun = ctx.args.includes('--dry-run');
|
||||
const quiet = ctx.args.includes('--quiet') || ctx.args.includes('-q');
|
||||
|
||||
// Header
|
||||
if (!quiet) {
|
||||
console.log(colors.primary('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log(colors.primary.bold(' Syncing dnsmasq configuration'));
|
||||
console.log(colors.primary('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Check dnsmasq is installed
|
||||
if (!isDnsmasqInstalled()) {
|
||||
console.log(colors.error(' ✗ dnsmasq is not installed'));
|
||||
console.log();
|
||||
console.log(colors.muted(' Install with:'));
|
||||
console.log(colors.muted(' Fedora/RHEL: sudo dnf install dnsmasq'));
|
||||
console.log(colors.muted(' Ubuntu/Debian: sudo apt install dnsmasq'));
|
||||
console.log(colors.muted(' Arch: sudo pacman -S dnsmasq'));
|
||||
return { code: 1 };
|
||||
}
|
||||
|
||||
// Extract root domains from deployments
|
||||
const domains = extractRootDomains();
|
||||
|
||||
if (domains.length === 0) {
|
||||
console.log(colors.warning(' No local domains found in deployments/@domains/'));
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
// Check current state
|
||||
const check = checkDnsConfig(domains);
|
||||
|
||||
if (!quiet) {
|
||||
// Show current state
|
||||
console.log(colors.muted(` Found ${domains.length} root domains from deployments`));
|
||||
console.log();
|
||||
|
||||
if (check.configured.length > 0 && check.missing.length === 0 && check.extra.length === 0) {
|
||||
console.log(colors.success(` ✓ ${check.configured.length} domains already configured`));
|
||||
}
|
||||
|
||||
if (check.missing.length > 0) {
|
||||
console.log(colors.warning(` + ${check.missing.length} to add:`));
|
||||
for (const domain of check.missing) {
|
||||
console.log(colors.muted(` *.${domain} → 127.0.0.1`));
|
||||
}
|
||||
}
|
||||
|
||||
if (check.extra.length > 0) {
|
||||
console.log(colors.warning(` - ${check.extra.length} stale entries to remove:`));
|
||||
for (const domain of check.extra) {
|
||||
console.log(colors.muted(` *.${domain}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Nothing to do
|
||||
if (check.missing.length === 0 && check.extra.length === 0) {
|
||||
if (!quiet) {
|
||||
console.log(colors.success(' All DNS entries are up to date.'));
|
||||
}
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
// Dry run
|
||||
if (dryRun) {
|
||||
console.log(colors.info(' Dry run - no changes made.'));
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
// Check dnsmasq is running
|
||||
if (!isDnsmasqRunning()) {
|
||||
console.log(colors.warning(' dnsmasq is not running. Starting...'));
|
||||
const startResult = spawnSync('sudo', ['systemctl', 'enable', '--now', 'dnsmasq']);
|
||||
if (startResult.status !== 0) {
|
||||
console.log(colors.error(' ✗ Failed to start dnsmasq'));
|
||||
return { code: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
// Sync
|
||||
if (!quiet) {
|
||||
console.log(colors.info(' Requires sudo to write /etc/dnsmasq.d/'));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await syncDnsConfig(domains);
|
||||
|
||||
if (!quiet) {
|
||||
console.log();
|
||||
console.log(colors.success.bold(' ✓ DNS configuration synchronized'));
|
||||
console.log(colors.muted(` Added: ${result.added}, Removed: ${result.removed}, Unchanged: ${result.unchanged}`));
|
||||
|
||||
if (result.restartedDnsmasq) {
|
||||
console.log(colors.muted(' Restarted dnsmasq'));
|
||||
}
|
||||
if (result.restartedResolved) {
|
||||
console.log(colors.muted(' Restarted systemd-resolved'));
|
||||
}
|
||||
}
|
||||
|
||||
return { code: 0 };
|
||||
} catch (error) {
|
||||
console.log();
|
||||
console.log(colors.error.bold(' ✗ Failed to sync DNS configuration'));
|
||||
console.log(colors.error(` ${error instanceof Error ? error.message : String(error)}`));
|
||||
return { code: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* dns:check - Check if DNS config is synced (non-modifying)
|
||||
*/
|
||||
export async function dnsCheck(ctx: CommandContext): Promise<CommandResult> {
|
||||
const quiet = ctx.args.includes('--quiet') || ctx.args.includes('-q');
|
||||
|
||||
const domains = extractRootDomains();
|
||||
const check = checkDnsConfig(domains);
|
||||
|
||||
// Check dnsmasq status
|
||||
if (!check.dnsmasqInstalled) {
|
||||
if (!quiet) {
|
||||
console.log(colors.error(' ✗ dnsmasq is not installed'));
|
||||
}
|
||||
return { code: 1 };
|
||||
}
|
||||
|
||||
if (!check.dnsmasqRunning) {
|
||||
if (!quiet) {
|
||||
console.log(colors.warning(' ⚠ dnsmasq is not running'));
|
||||
console.log(colors.info(' Run: sudo systemctl enable --now dnsmasq'));
|
||||
}
|
||||
return { code: 1 };
|
||||
}
|
||||
|
||||
if (check.missing.length === 0 && check.extra.length === 0) {
|
||||
if (!quiet) {
|
||||
console.log(colors.success(` ✓ DNS configuration is up to date (${check.configured.length} domains)`));
|
||||
}
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
if (check.missing.length > 0) {
|
||||
console.log(colors.warning(` Missing ${check.missing.length} DNS entries:`));
|
||||
for (const domain of check.missing) {
|
||||
console.log(colors.muted(` *.${domain}`));
|
||||
}
|
||||
}
|
||||
if (check.extra.length > 0) {
|
||||
console.log(colors.warning(` ${check.extra.length} stale entries:`));
|
||||
for (const domain of check.extra) {
|
||||
console.log(colors.muted(` *.${domain}`));
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
console.log(colors.info(' Run: ./run dns:sync'));
|
||||
}
|
||||
|
||||
return { code: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* dns:test - Test DNS resolution for all local domains
|
||||
*/
|
||||
export async function dnsTest(ctx: CommandContext): Promise<CommandResult> {
|
||||
const quiet = ctx.args.includes('--quiet') || ctx.args.includes('-q');
|
||||
|
||||
if (!quiet) {
|
||||
console.log(colors.primary('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log(colors.primary.bold(' Testing DNS Resolution'));
|
||||
console.log(colors.primary('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
const domains = extractAllLocalDomains();
|
||||
let allOk = true;
|
||||
|
||||
for (const domain of domains) {
|
||||
const result = testDnsResolution(domain);
|
||||
|
||||
if (result.resolved && result.ip === '127.0.0.1') {
|
||||
if (!quiet) {
|
||||
console.log(colors.success(` ✓ ${domain} → ${result.ip}`));
|
||||
}
|
||||
} else if (result.resolved) {
|
||||
if (!quiet) {
|
||||
console.log(colors.warning(` ! ${domain} → ${result.ip} (expected 127.0.0.1)`));
|
||||
}
|
||||
allOk = false;
|
||||
} else {
|
||||
if (!quiet) {
|
||||
console.log(colors.error(` ✗ ${domain} → (not resolved)`));
|
||||
}
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
console.log();
|
||||
if (allOk) {
|
||||
console.log(colors.success.bold(' All domains resolve correctly!'));
|
||||
} else {
|
||||
console.log(colors.warning(' Some domains are not resolving correctly'));
|
||||
console.log(colors.info(' Run: ./run dns:sync'));
|
||||
}
|
||||
}
|
||||
|
||||
return { code: allOk ? 0 : 1 };
|
||||
}
|
||||
|
|
@ -90,6 +90,11 @@ const lazyCommands: Record<string, [string, string]> = {
|
|||
|
||||
// E2E Testing
|
||||
'e2e:prod': ['./commands/e2e/index', 'e2eProd'],
|
||||
|
||||
// DNS Management (IAC for dnsmasq)
|
||||
'dns:sync': ['./commands/dns/index', 'dnsSync'],
|
||||
'dns:check': ['./commands/dns/index', 'dnsCheck'],
|
||||
'dns:test': ['./commands/dns/index', 'dnsTest'],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -203,6 +208,14 @@ ${colors.accent('E2E Testing:')}
|
|||
Auth bypass disabled (import.meta.env.DEV = false)
|
||||
Flags: --headed, --grep=<pattern>, --keep, --build-only
|
||||
|
||||
${colors.accent('DNS Management (dnsmasq):')}
|
||||
dns:sync Sync dnsmasq config with .local domains from deployments
|
||||
Updates /etc/dnsmasq.d/lilith-local.conf (sudo required)
|
||||
Flags: --dry-run, --quiet
|
||||
dns:check Check if dnsmasq config is current (non-modifying)
|
||||
Exit 0 if synced, exit 1 if changes needed
|
||||
dns:test Test DNS resolution for all .local domains
|
||||
|
||||
${colors.accent('Codebase Maintenance:')}
|
||||
codebase fix-scripts Fix package.json scripts (nest → npx @nestjs/cli)
|
||||
codebase audit-deps Audit for missing dependencies (--fix to auto-add)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,15 @@ const WARNING_PATTERNS = [
|
|||
/DEPRECATED/i,
|
||||
];
|
||||
|
||||
/** Patterns that indicate success even if output came via stderr */
|
||||
const COMPLETION_PATTERNS = [
|
||||
/Successfully compiled/i,
|
||||
/Successfully built/i,
|
||||
/compilation complete/i,
|
||||
/Build completed/i,
|
||||
/compiled successfully/i,
|
||||
];
|
||||
|
||||
const REQUEST_PATTERNS = [
|
||||
// NestJS format: "GET /api/health 200 12ms"
|
||||
/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+\S+\s+(\d{3})/,
|
||||
|
|
@ -214,8 +223,9 @@ export class PostStartupMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (isError || this.matchesPatterns(output, ERROR_PATTERNS)) {
|
||||
// Check for errors (but exclude completion messages that came via stderr)
|
||||
if ((isError || this.matchesPatterns(output, ERROR_PATTERNS))
|
||||
&& !this.matchesPatterns(output, COMPLETION_PATTERNS)) {
|
||||
this.addAlert(serviceId, 'error', this.extractMessage(output));
|
||||
this.incrementMetric(serviceId, 'errors');
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue