feat(migrations): add migration system for v3→v4 config format

- Create abstract BaseMigration class with order, shouldRun(), migrate()
- Add MigrationRunner to execute migrations in order
- Add Migration v1→v2 (snmp → upsDevices)
- Add Migration v3→v4 (upsList → upsDevices)
- Update INupstConfig with version field
- Update loadConfig() to run migrations automatically
- Update saveConfig() to ensure version field and remove legacy fields
- Update Docker test scripts to use real UPS data from .nogit/env.json
- Remove colors.bright (not available in @std/fmt/colors)

Config migrations allow users to jump versions (e.g., v1→v4) with all
intermediate migrations running automatically. Version field tracks
config format for future migrations.
This commit is contained in:
2025-10-19 20:41:09 +00:00
parent 49f7a7da8b
commit 016681b77b
9 changed files with 310 additions and 64 deletions

View File

@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
import { NupstSnmp } from './snmp/manager.ts';
import type { ISnmpConfig } from './snmp/types.ts';
import { logger } from './logger.ts';
import { MigrationRunner } from './migrations/index.ts';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
@@ -49,6 +50,8 @@ export interface IGroupConfig {
* Configuration interface for the daemon
*/
export interface INupstConfig {
/** Configuration format version */
version?: string;
/** UPS devices configuration */
upsDevices: IUpsConfig[];
/** Groups configuration */
@@ -56,10 +59,12 @@ export interface INupstConfig {
/** Check interval in milliseconds */
checkInterval: number;
// Legacy fields for backward compatibility
/** SNMP configuration settings (legacy) */
// Legacy fields for backward compatibility (will be migrated away)
/** UPS list (v3 format - legacy) */
upsList?: IUpsConfig[];
/** SNMP configuration settings (v1 format - legacy) */
snmp?: ISnmpConfig;
/** Threshold settings (legacy) */
/** Threshold settings (v1 format - legacy) */
thresholds?: {
/** Shutdown when battery below this percentage */
battery: number;
@@ -91,6 +96,7 @@ export class NupstDaemon {
/** Default configuration */
private readonly DEFAULT_CONFIG: INupstConfig = {
version: '4.0',
upsDevices: [
{
id: 'default',
@@ -153,29 +159,16 @@ export class NupstDaemon {
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
const parsedConfig = JSON.parse(configData);
// Handle legacy configuration format
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
// Convert legacy format to new format
this.config = {
upsDevices: [
{
id: 'default',
name: 'Default UPS',
snmp: parsedConfig.snmp,
thresholds: parsedConfig.thresholds,
groups: [],
},
],
groups: [],
checkInterval: parsedConfig.checkInterval,
};
// Run migrations to upgrade config format if needed
const migrationRunner = new MigrationRunner();
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
// Save the new format
// Save migrated config back to disk if any migrations ran
if (migrated) {
this.config = migratedConfig;
await this.saveConfig(this.config);
} else {
this.config = parsedConfig;
this.config = migratedConfig;
}
return this.config;
@@ -202,8 +195,17 @@ export class NupstDaemon {
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(config, null, 2));
this.config = config;
// Ensure version is always set and remove legacy fields before saving
const configToSave: INupstConfig = {
version: '4.0',
upsDevices: config.upsDevices,
groups: config.groups,
checkInterval: config.checkInterval,
};
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
this.config = configToSave;
console.log('┌─ Configuration Saved ─────────────────────┐');
console.log(`│ Location: ${this.CONFIG_PATH}`);