refactor: move setup scripts out of src/ to reduce build token count
Setup scripts are standalone CLI tools run via tsx with no runtime imports from the main app. Moving them out of src/ excludes them from the tsc build output and reduces the compiled bundle size. - git mv src/setup/ setup/ - Fix imports to use ../src/logger.js and ../src/config.js - Update package.json, vitest.config.ts, SKILL.md references - Fix platform tests to be cross-platform (macOS + Linux) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
152
setup/verify.ts
Normal file
152
setup/verify.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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 os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
import { STORE_DIR } from '../src/config.js';
|
||||
import { logger } from '../src/logger.js';
|
||||
import { getPlatform, getServiceManager, hasSystemd, isRoot } from './platform.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
export async function run(_args: string[]): Promise<void> {
|
||||
const projectRoot = process.cwd();
|
||||
const platform = getPlatform();
|
||||
const homeDir = os.homedir();
|
||||
|
||||
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.nanoclaw')) {
|
||||
// Check if it has a PID (actually running)
|
||||
const line = output.split('\n').find((l) => l.includes('com.nanoclaw'));
|
||||
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 nanoclaw`, { stdio: 'ignore' });
|
||||
service = 'running';
|
||||
} catch {
|
||||
try {
|
||||
const output = execSync(`${prefix} list-unit-files`, { encoding: 'utf-8' });
|
||||
if (output.includes('nanoclaw')) {
|
||||
service = 'stopped';
|
||||
}
|
||||
} catch {
|
||||
// systemctl not available
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for nohup PID file
|
||||
const pidFile = path.join(projectRoot, 'nanoclaw.pid');
|
||||
if (fs.existsSync(pidFile)) {
|
||||
try {
|
||||
const pid = fs.readFileSync(pidFile, 'utf-8').trim();
|
||||
if (pid) {
|
||||
execSync(`kill -0 ${pid}`, { stdio: 'ignore' });
|
||||
service = 'running';
|
||||
}
|
||||
} catch {
|
||||
service = 'stopped';
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info({ service }, 'Service status');
|
||||
|
||||
// 2. Check container runtime
|
||||
let containerRuntime = 'none';
|
||||
try {
|
||||
execSync('command -v container', { stdio: 'ignore' });
|
||||
containerRuntime = 'apple-container';
|
||||
} catch {
|
||||
try {
|
||||
execSync('docker info', { stdio: 'ignore' });
|
||||
containerRuntime = 'docker';
|
||||
} catch {
|
||||
// No runtime
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 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';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check WhatsApp auth
|
||||
let whatsappAuth = 'not_found';
|
||||
const authDir = path.join(projectRoot, 'store', 'auth');
|
||||
if (fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0) {
|
||||
whatsappAuth = 'authenticated';
|
||||
}
|
||||
|
||||
// 5. 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
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Check mount allowlist
|
||||
let mountAllowlist = 'missing';
|
||||
if (fs.existsSync(path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'))) {
|
||||
mountAllowlist = 'configured';
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
const status =
|
||||
service === 'running' &&
|
||||
credentials !== 'missing' &&
|
||||
whatsappAuth !== 'not_found' &&
|
||||
registeredGroups > 0
|
||||
? 'success'
|
||||
: 'failed';
|
||||
|
||||
logger.info({ status }, 'Verification complete');
|
||||
|
||||
emitStatus('VERIFY', {
|
||||
SERVICE: service,
|
||||
CONTAINER_RUNTIME: containerRuntime,
|
||||
CREDENTIALS: credentials,
|
||||
WHATSAPP_AUTH: whatsappAuth,
|
||||
REGISTERED_GROUPS: registeredGroups,
|
||||
MOUNT_ALLOWLIST: mountAllowlist,
|
||||
STATUS: status,
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
|
||||
if (status === 'failed') process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user