#!/usr/bin/env node /** * Actual-sync - Automated bank synchronization service for Actual Budget * Entry point for the application */ // Crash handlers first, so an early crash is a classified record, not Node's raw printer (#439) require('./src/lib/crashHandlers').installCrashHandlers(); const fs = require('fs'); const bootstrap = require('./src/lib/bootstrapLogger'); // If startup dies before the root logger exists, buffered records go to stderr as JSON (#442) bootstrap.installExitDump(); const path = require('path'); const { ensureConfig, resolveSchemaPath } = require('./src/lib/configBootstrap'); const ConfigLoader = require('./src/lib/configLoader'); // Get version from environment (CI build-arg) or package.json (#132) const { resolveVersion } = require('./src/lib/version'); const VERSION = resolveVersion(); bootstrap.info('Starting Actual-sync', { appVersion: VERSION }); // Validate environment and configuration before starting function validateStartup() { const errors = []; const warnings = []; // Check Node.js version const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1)); if (majorVersion < 22) { errors.push(`Node.js version ${nodeVersion} is not supported. Please use Node.js 22 or higher.`); } // A single server can be configured via ACTUAL_SYNC_SERVER_* env vars, in // which case config.json is optional. (#120) const hasEnvServer = ConfigLoader.hasEnvServerConfig(); const configDir = path.join(__dirname, 'config'); const configFile = path.join(configDir, 'config.json'); const configFileExists = fs.existsSync(configFile); if (configFileExists) { // Verify config.json is readable and valid JSON try { JSON.parse(fs.readFileSync(configFile, 'utf8')); } catch (error) { if (error.code === 'EACCES') { errors.push(`Configuration file is not readable: ${error.message}`); } else if (error instanceof SyntaxError) { errors.push( `Configuration file contains invalid JSON:\n` + ` ${error.message}\n` + ` Please check config/config.json for syntax errors.` ); } else { errors.push(`Failed to read configuration file: ${error.message}`); } } } else if (hasEnvServer) { // Env-var single-server setup: no config.json needed. (#120) bootstrap.info('Using single-server configuration from ACTUAL_SYNC_SERVER_* environment variables (no config.json).'); } else if (!fs.existsSync(configDir)) { errors.push( 'Configuration directory not found: config/\n' + ' Please create the config directory and add config.json file.\n' + ' See config/config.example.json for reference.' ); } else { // First run: seed an example template into the (likely empty, // freshly-mounted) config dir so the user has something to fill in. (#96) const { seeded } = ensureConfig({ configDir }); const syncIdHint = ' Sync ID: in Actual Budget open the budget → Settings → Advanced → "Sync ID".'; if (seeded) { errors.push( 'No config.json found — a starter template was written to config/config.example.json.\n' + ' Fill in each server\'s url / password / syncId, rename it to config.json, and restart.\n' + ' (Or configure a single server with the ACTUAL_SYNC_SERVER_* environment variables.)\n' + syncIdHint ); } else { errors.push( 'Configuration file not found: config/config.json\n' + ' Create config.json based on config.example.json, or use the ACTUAL_SYNC_SERVER_* env vars.\n' + syncIdHint ); } } // Check if schema exists (optional, but warn if missing). Resolve from the // bundled defaults dir so a shadowed config mount doesn't degrade validation. (#96) const schemaFile = resolveSchemaPath(__dirname); if (!fs.existsSync(schemaFile)) { warnings.push('Configuration schema not found (validation will be limited)'); } // Check if required npm packages are installed const packageJsonPath = path.join(__dirname, 'package.json'); if (fs.existsSync(packageJsonPath)) { const nodeModulesPath = path.join(__dirname, 'node_modules'); if (!fs.existsSync(nodeModulesPath)) { errors.push( 'Dependencies not installed: node_modules/ not found\n' + ' Please run: npm install' ); } else { // Check critical dependencies const criticalPackages = ['@actual-app/api', 'node-schedule', 'ajv']; for (const pkg of criticalPackages) { const pkgPath = path.join(nodeModulesPath, pkg); if (!fs.existsSync(pkgPath)) { errors.push( `Critical dependency missing: ${pkg}\n` + ' Please run: npm install' ); break; // Don't spam with all missing packages } } } } // Display warnings if (warnings.length > 0) { warnings.forEach(warning => bootstrap.warn(warning)); } // Display errors and exit if any if (errors.length > 0) { errors.forEach(error => bootstrap.error(error)); bootstrap.error('Startup validation failed. Please fix the above errors before starting the service.'); // The exit hook writes the buffered records to stderr as JSON lines process.exit(1); } bootstrap.info('Startup validation passed'); } // Run validation validateStartup(); // Export version for use by other modules global.APP_VERSION = VERSION; // Start the sync service require('./src/syncService');