164 lines
6.0 KiB
JavaScript
164 lines
6.0 KiB
JavaScript
/**
|
|
* 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 ? '<EMPTY>' : '<VALUE>'}`);
|
|
}
|
|
}
|
|
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,
|
|
},
|
|
};
|
|
}
|