Files
EJClaw/setup/verify.ts
2026-03-23 08:09:15 +09:00

139 lines
4.0 KiB
TypeScript

/**
* Step: verify — End-to-end health check of the full installation.
* Replaces 09-verify.sh
*
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import Database from 'better-sqlite3';
import { STORE_DIR } from '../src/config.js';
import { readEnvFile } from '../src/env.js';
import { logger } from '../src/logger.js';
import { getPlatform, getServiceManager, isRoot } from './platform.js';
import { emitStatus } from './status.js';
export async function run(_args: string[]): Promise<void> {
const projectRoot = process.cwd();
const platform = getPlatform();
logger.info('Starting verification');
// 1. Check service status
let service = 'not_found';
const mgr = getServiceManager();
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';
}
}
}
logger.info({ service }, 'Service status');
// 2. Check credentials
let credentials = 'missing';
const envFile = path.join(projectRoot, '.env');
if (fs.existsSync(envFile)) {
const envContent = fs.readFileSync(envFile, 'utf-8');
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)) {
credentials = 'configured';
}
}
// 3. Check channel auth (detect configured channels by credentials)
const envVars = readEnvFile(['DISCORD_BOT_TOKEN']);
const channelAuth: Record<string, string> = {};
if (process.env.DISCORD_BOT_TOKEN || envVars.DISCORD_BOT_TOKEN) {
channelAuth.discord = 'configured';
}
const configuredChannels = Object.keys(channelAuth);
const anyChannelConfigured = configuredChannels.length > 0;
// 4. Check registered groups (using better-sqlite3, not sqlite3 CLI)
let registeredGroups = 0;
const dbPath = path.join(STORE_DIR, 'messages.db');
if (fs.existsSync(dbPath)) {
try {
const db = new Database(dbPath, { readonly: true });
const row = db
.prepare('SELECT COUNT(*) as count FROM registered_groups')
.get() as { count: number };
registeredGroups = row.count;
db.close();
} catch {
// Table might not exist
}
}
// Determine overall status
const status =
service === 'running' &&
credentials !== 'missing' &&
anyChannelConfigured &&
registeredGroups > 0
? 'success'
: 'failed';
logger.info({ status, channelAuth }, 'Verification complete');
emitStatus('VERIFY', {
SERVICE: service,
CREDENTIALS: credentials,
CONFIGURED_CHANNELS: configuredChannels.join(','),
CHANNEL_AUTH: JSON.stringify(channelAuth),
REGISTERED_GROUPS: registeredGroups,
STATUS: status,
LOG: 'logs/setup.log',
});
if (status === 'failed') process.exit(1);
}