Ctrl K
ComunidadGuías…

index.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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');