chore: align setup with EJClaw dual-service architecture
This commit is contained in:
203
setup/verify.ts
203
setup/verify.ts
@@ -2,6 +2,10 @@
|
||||
* Step: verify — End-to-end health check of the full installation.
|
||||
* Replaces 09-verify.sh
|
||||
*
|
||||
* Supports dual-service architecture:
|
||||
* - ejclaw (Claude Code) — always checked
|
||||
* - ejclaw-codex (Codex) — checked when .env.codex exists
|
||||
*
|
||||
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
@@ -16,64 +20,125 @@ import { logger } from '../src/logger.js';
|
||||
import { getPlatform, getServiceManager, isRoot } from './platform.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type ServiceStatus = 'running' | 'stopped' | 'not_found' | 'not_configured';
|
||||
|
||||
interface ServiceCheck {
|
||||
name: string;
|
||||
status: ServiceStatus;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Service status checks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function checkLaunchdService(label: string): ServiceStatus {
|
||||
try {
|
||||
const output = execSync('launchctl list', { encoding: 'utf-8' });
|
||||
if (output.includes(label)) {
|
||||
const line = output.split('\n').find((l) => l.includes(label));
|
||||
if (line) {
|
||||
const pidField = line.trim().split(/\s+/)[0];
|
||||
return pidField !== '-' && pidField ? 'running' : 'stopped';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// launchctl not available
|
||||
}
|
||||
return 'not_found';
|
||||
}
|
||||
|
||||
function checkSystemdService(name: string): ServiceStatus {
|
||||
const prefix = isRoot() ? 'systemctl' : 'systemctl --user';
|
||||
try {
|
||||
execSync(`${prefix} is-active ${name}`, { stdio: 'ignore' });
|
||||
return 'running';
|
||||
} catch {
|
||||
try {
|
||||
const output = execSync(`${prefix} list-unit-files`, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
if (output.includes(name)) {
|
||||
return 'stopped';
|
||||
}
|
||||
} catch {
|
||||
// systemctl not available
|
||||
}
|
||||
}
|
||||
return 'not_found';
|
||||
}
|
||||
|
||||
function checkNohupService(
|
||||
projectRoot: string,
|
||||
serviceName: string,
|
||||
): ServiceStatus {
|
||||
const pidFile = path.join(projectRoot, `${serviceName}.pid`);
|
||||
if (fs.existsSync(pidFile)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(pidFile, 'utf-8').trim();
|
||||
const pid = Number(raw);
|
||||
if (raw && Number.isInteger(pid) && pid > 0) {
|
||||
process.kill(pid, 0);
|
||||
return 'running';
|
||||
}
|
||||
} catch {
|
||||
return 'stopped';
|
||||
}
|
||||
}
|
||||
return 'not_found';
|
||||
}
|
||||
|
||||
function checkService(
|
||||
projectRoot: string,
|
||||
mgr: ReturnType<typeof getServiceManager>,
|
||||
serviceName: string,
|
||||
launchdLabel: string,
|
||||
): ServiceStatus {
|
||||
if (mgr === 'launchd') return checkLaunchdService(launchdLabel);
|
||||
if (mgr === 'systemd') return checkSystemdService(serviceName);
|
||||
return checkNohupService(projectRoot, serviceName);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Main */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export async function run(_args: string[]): Promise<void> {
|
||||
const projectRoot = process.cwd();
|
||||
const platform = getPlatform();
|
||||
const mgr = getServiceManager();
|
||||
|
||||
logger.info('Starting verification');
|
||||
|
||||
// 1. Check service status
|
||||
let service = 'not_found';
|
||||
const mgr = getServiceManager();
|
||||
// 1. Check service statuses
|
||||
const services: ServiceCheck[] = [];
|
||||
|
||||
if (mgr === 'launchd') {
|
||||
try {
|
||||
const output = execSync('launchctl list', { encoding: 'utf-8' });
|
||||
if (output.includes('com.ejclaw')) {
|
||||
// Check if it has a PID (actually running)
|
||||
const line = output.split('\n').find((l) => l.includes('com.ejclaw'));
|
||||
if (line) {
|
||||
const pidField = line.trim().split(/\s+/)[0];
|
||||
service = pidField !== '-' && pidField ? 'running' : 'stopped';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// launchctl not available
|
||||
}
|
||||
} else if (mgr === 'systemd') {
|
||||
const prefix = isRoot() ? 'systemctl' : 'systemctl --user';
|
||||
try {
|
||||
execSync(`${prefix} is-active ejclaw`, { stdio: 'ignore' });
|
||||
service = 'running';
|
||||
} catch {
|
||||
try {
|
||||
const output = execSync(`${prefix} list-unit-files`, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
if (output.includes('ejclaw')) {
|
||||
service = 'stopped';
|
||||
}
|
||||
} catch {
|
||||
// systemctl not available
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for nohup PID file
|
||||
const pidFile = path.join(projectRoot, 'ejclaw.pid');
|
||||
if (fs.existsSync(pidFile)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(pidFile, 'utf-8').trim();
|
||||
const pid = Number(raw);
|
||||
if (raw && Number.isInteger(pid) && pid > 0) {
|
||||
process.kill(pid, 0);
|
||||
service = 'running';
|
||||
}
|
||||
} catch {
|
||||
service = 'stopped';
|
||||
}
|
||||
}
|
||||
// Primary service (always checked)
|
||||
services.push({
|
||||
name: 'ejclaw',
|
||||
status: checkService(projectRoot, mgr, 'ejclaw', 'com.ejclaw'),
|
||||
});
|
||||
|
||||
// Codex service (checked when .env.codex exists)
|
||||
const codexEnvPath = path.join(projectRoot, '.env.codex');
|
||||
const codexConfigured = fs.existsSync(codexEnvPath);
|
||||
if (codexConfigured) {
|
||||
services.push({
|
||||
name: 'ejclaw-codex',
|
||||
status: checkService(
|
||||
projectRoot,
|
||||
mgr,
|
||||
'ejclaw-codex',
|
||||
'com.ejclaw-codex',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
for (const svc of services) {
|
||||
logger.info({ service: svc.name, status: svc.status }, 'Service status');
|
||||
}
|
||||
logger.info({ service }, 'Service status');
|
||||
|
||||
// 2. Check credentials
|
||||
let credentials = 'missing';
|
||||
@@ -99,6 +164,7 @@ export async function run(_args: string[]): Promise<void> {
|
||||
|
||||
// 4. Check registered groups (using better-sqlite3, not sqlite3 CLI)
|
||||
let registeredGroups = 0;
|
||||
let groupsByAgent: Record<string, number> = {};
|
||||
const dbPath = path.join(STORE_DIR, 'messages.db');
|
||||
if (fs.existsSync(dbPath)) {
|
||||
try {
|
||||
@@ -107,6 +173,21 @@ export async function run(_args: string[]): Promise<void> {
|
||||
.prepare('SELECT COUNT(*) as count FROM registered_groups')
|
||||
.get() as { count: number };
|
||||
registeredGroups = row.count;
|
||||
|
||||
// Count by agent type
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(
|
||||
'SELECT agent_type, COUNT(*) as count FROM registered_groups GROUP BY agent_type',
|
||||
)
|
||||
.all() as { agent_type: string; count: number }[];
|
||||
for (const r of rows) {
|
||||
groupsByAgent[r.agent_type || 'unknown'] = r.count;
|
||||
}
|
||||
} catch {
|
||||
// agent_type column might not exist in older schema
|
||||
}
|
||||
|
||||
db.close();
|
||||
} catch {
|
||||
// Table might not exist
|
||||
@@ -114,22 +195,36 @@ export async function run(_args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
const primaryRunning = services[0].status === 'running';
|
||||
const codexOk = !codexConfigured || services[1]?.status === 'running';
|
||||
|
||||
const status =
|
||||
service === 'running' &&
|
||||
primaryRunning &&
|
||||
codexOk &&
|
||||
credentials !== 'missing' &&
|
||||
anyChannelConfigured &&
|
||||
registeredGroups > 0
|
||||
? 'success'
|
||||
: 'failed';
|
||||
|
||||
logger.info({ status, channelAuth }, 'Verification complete');
|
||||
// Build service status summary
|
||||
const servicesSummary: Record<string, string> = {};
|
||||
for (const svc of services) {
|
||||
servicesSummary[svc.name] = svc.status;
|
||||
}
|
||||
|
||||
logger.info({ status, channelAuth, servicesSummary }, 'Verification complete');
|
||||
|
||||
emitStatus('VERIFY', {
|
||||
SERVICE: service,
|
||||
SERVICES: JSON.stringify(servicesSummary),
|
||||
// Legacy field (keep for backward compatibility)
|
||||
SERVICE: services[0].status,
|
||||
CREDENTIALS: credentials,
|
||||
CONFIGURED_CHANNELS: configuredChannels.join(','),
|
||||
CHANNEL_AUTH: JSON.stringify(channelAuth),
|
||||
REGISTERED_GROUPS: registeredGroups,
|
||||
GROUPS_BY_AGENT: JSON.stringify(groupsByAgent),
|
||||
CODEX_CONFIGURED: codexConfigured,
|
||||
STATUS: status,
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user