/** * Main entry point for nopy * @module nopy.main */ import { configure, getAnsiColorFormatter, getLogger } from '@logtape/logtape'; import { loadCubes } from './cubes/index.js'; import { BuildContext } from './cubes/dependencies.js'; import { Variables } from './nopy.common.js'; import { getConfigPaths, loadConfig } from './nopy.config.js'; import { executeDeployCalls, summarizeResults } from './nopy.executor.js'; import { DEFAULT_HISTORY_SIZE, addToHistory } from './nopy.history.js'; import { saveSession } from './nopy.session.js'; import { runWorkflow } from './nopy.workflow.js'; /** * Configures the logtape logger for console output */ function configureLogtape() { configure({ sinks: { console: (() => { const formatter = getAnsiColorFormatter(); return (record) => { const formatted = formatter(record); if (typeof formatted === 'string') { const msg = formatted.replace(/\r?\n$/, ''); const props = record.properties; console.log(msg, ...Object.values(props)); } }; })(), }, loggers: [ { category: ['logtape', 'meta'], level: 'error', sinks: ['console'], }, { category: 'nopy', level: 'debug', sinks: ['console'], }, ], }); } // Initialize logging configureLogtape(); /** * Prints the active configuration summary */ function printActiveConfig(config, opts) { const configPaths = getConfigPaths(); const cwd = process.cwd(); const lines = ['']; lines.push(' Configuration'); lines.push(' ─────────────'); const relativePaths = configPaths.map((p) => { if (p.startsWith(cwd)) return `.${p.slice(cwd.length)}`; if (p.startsWith(process.env.HOME || '')) return `~${p.slice((process.env.HOME || '').length)}`; return p; }); lines.push(` Config: ${relativePaths.join(' → ')}`); if (config.hosts.length > 0) lines.push(` Hosts: ${config.hosts.join(', ')}`); if (config.cubeDirs.length > 0) lines.push(` Cube dirs: ${config.cubeDirs.join(', ')}`); if (opts.continueOnError) lines.push(' Execution: continue-on-error'); const envEntries = Object.entries(config.env); if (envEntries.length > 0) { lines.push(' Env vars:'); for (const [key, value] of envEntries) { const isEmpty = value === null || value === undefined || value === ''; lines.push(` ${key}: ${isEmpty ? '' : ''}`); } } lines.push(''); console.log(lines.join('\n')); } /** * Main entry point for nopy deployments */ export async function nopy(opts = {}) { const { useDefaults = false, useAuthKey, saveSession: saveSessionPath, loadSession: loadSessionPath, replaySession, dryRun = false, printOnly = false, continueOnError = false, jsonOutput = false, saveToHistory = true, } = opts; const log = getLogger(['nopy']); const config = loadConfig(); if (!jsonOutput && !replaySession && !loadSessionPath) { printActiveConfig(config, { continueOnError }); } const { cubes, errors } = await loadCubes(); const variables = new Variables(config.env); if (errors.length > 0) { log.error('Errors found during cube loading:'); errors.forEach((error) => log.error(error)); if (jsonOutput) console.log(JSON.stringify({ success: false, errors }, null, 2)); return undefined; } const workflow = await runWorkflow(loadSessionPath, cubes, config, { useDefaults, useAuthKey }, replaySession); // Step 3: Build deployment calls using BuildContext const context = new BuildContext(cubes, variables, workflow.session, config, { method: workflow.authMethod, username: workflow.username, password: workflow.password, }, { useDefaults, isSessionReplay: workflow.isReplay, }); for (const host of workflow.session.hosts) { for (const cubeId of workflow.selectedCubes) { await context.resolveCube(cubeId, host); } } const sessionForSaving = { ...workflow.session, cubes: context.cubeSessions, env: variables.get('global'), }; if (saveSessionPath && !workflow.isReplay) { saveSession(sessionForSaving, saveSessionPath); } if (saveToHistory && !dryRun && !workflow.isReplay && context.deployCalls.length > 0) { const historySize = config.history?.maxSessions ?? DEFAULT_HISTORY_SIZE; if (config.history?.autoSave !== false) { addToHistory(sessionForSaving, historySize); } } if (printOnly) { console.log('\n Deploy Commands\n ───────────────\n'); for (const call of context.deployCalls) { console.log(` # ${call.cube} -> ${call.host}`); console.log(` ${call.command.join(' ')}\n`); } return { success: true, results: [], summary: { total: context.deployCalls.length, successful: 0, failed: 0, totalDuration: 0 }, }; } const results = await executeDeployCalls(context.deployCalls, { dryRun, continueOnError, onProgress: (result, completed, total) => { if (!jsonOutput) { const status = result.success ? '✓' : '✗'; log.info(`[${completed}/${total}] ${status} ${result.cube} -> ${result.host}`); } }, }); const summary = summarizeResults(results); return { success: summary.failed === 0, results, summary: { total: summary.total, successful: summary.successful, failed: summary.failed, totalDuration: summary.totalDuration, }, }; }