src — versioning
src — versioning
The src/versioning module is a critical component responsible for managing the application's version lifecycle, including detecting current versions, migrating configuration files, and orchestrating complex data and schema migrations. It ensures that the application can gracefully evolve, handling changes in data structures, configuration schemas, and application logic across different releases.
This module provides robust mechanisms for:
- Version Detection: Identifying the application's version from various sources (e.g.,
package.json, stored state, configuration files). - Configuration Migration: Applying structured transformations to user configuration files to adapt them to new schema versions.
- Data/Schema Migration: Executing versioned
upanddownscripts for broader data and schema changes, complete with transactional safety, file-level locking, and an audit trail.
Core Concepts
Before diving into the individual components, it's important to understand the foundational concepts that underpin this module:
- Semantic Versioning (SemVer): All versioning within this module strictly adheres to SemVer (Major.Minor.Patch). This allows for clear comparison, ordering, and determination of upgrade paths. The
semverlibrary is used extensively. - Configuration vs. Data Migrations: The module distinguishes between two types of migrations:
- Configuration Migrations: Handled by
ConfigMigrator, these focus on transforming JSON-based configuration files (e.g.,settings.json). They involve schema changes, default value updates, and field renames/removals. - Data/Schema Migrations: Handled by
MigrationManager, these are more general-purpose and can involve changes to databases, file structures, or any persistent data. They are defined byupanddownscripts. - Idempotency: Migration
upanddownfunctions are expected to be idempotent. Running them multiple times should produce the same result as running them once. This is crucial for reliability, especially during retries or rollbacks. - Transactions & Rollbacks: The
MigrationManagerprovides transactional support by backing up files before modifications. If a migration fails, these backups are used to automatically revert the system to its previous state, ensuring data integrity. - File-level Locking: To prevent race conditions and ensure only one migration process runs at a time, the
MigrationManagerimplements a file-based locking mechanism. This is vital in environments where multiple instances or processes might attempt to perform migrations concurrently. - Audit Trail: All significant migration operations are logged to an audit file, providing a comprehensive history of changes, successes, and failures.
Module Architecture
The versioning module is composed of three main classes, each with a distinct responsibility:
graph TD
A[Application Startup] --> B(VersionDetector)
B --> C{Needs Upgrade?}
C -- Yes, Config --> D(ConfigMigrator)
C -- Yes, Data/Schema --> E(MigrationManager)
D -- Transforms Config Files --> F(Configuration Files)
E -- Migrates Data/Schema --> G(Data Directory)
E -- Manages Locks & Audit --> H(Shared Resources)
D -- Creates Backups --> I(Backup Directory)
E -- Tracks History --> J(Migration History)
E -- Logs Operations --> K(Audit Log)
1. VersionDetector
The VersionDetector class is responsible for identifying the current version of the application from various sources and providing utilities for version comparison and manipulation.
Purpose: To provide a unified way to determine the application's current version, compare it against target versions, and understand if an upgrade is necessary.
Key Methods & Concepts:
initialize(): Asynchronously detects all available versions and caches them.detectPackageVersion(): Reads theversionfield from the application'spackage.json.detectStoredVersion(): Reads a dedicatedversion.jsonfile (e.g., in the user's data directory) which stores the last known application version. This is often the "source of truth" for the application's internal state.detectConfigVersion(): Reads the version from a configuration file (e.g.,settings.json), looking for fields like_version,version, orconfigVersion. It can coerce non-SemVer strings to valid SemVer if possible.getCurrentVersion(): Returns the most authoritative version, prioritizingpackage.json>version.json>settings.json. Defaults to0.0.0if no version is found.storeVersion(version: string, metadata?: Record: Persists a given version to the) version.jsonfile. This is typically called after a successful migration or application update.compareVersions(current: string, target: string): Compares two SemVer strings and returns aVersionComparisonobject indicating their relation (equal,older,newer,invalid) and whether an upgrade is needed.needsUpgrade(): A convenience method to check if thegetStoredVersion()is older than thegetPackageVersion().getUpgradePath(from: string, to: string): Generates a list of intermediate major/minor versions between two given versions, useful for planning staged migrations.isValidVersion(version: string),coerceVersion(version: string),parseVersion(version: string),satisfiesRange(version: string, range: string): Utility methods for SemVer string validation and manipulation.
Usage Pattern:
import { getVersionDetector } from 39;./version-detector.js39;;
async function checkAppVersion() {
const detector = getVersionDetector();
await detector.initialize();
const packageVersion = detector.getPackageVersion();
const storedVersion = detector.getStoredVersion();
const configVersion = detector.getConfigVersion();
const currentAppVersion = detector.getCurrentVersion();
console.log(`Package Version: ${packageVersion}`);
console.log(`Stored Version: ${storedVersion}`);
console.log(`Config Version: ${configVersion}`);
console.log(`Current App Version: ${currentAppVersion}`);
if (detector.needsUpgrade()) {
console.log(39;Application upgrade is needed!39;);
const upgradePath = detector.getUpgradePath(storedVersion || 39;0.0.039;, packageVersion!);
console.log(39;Upgrade path:39;, upgradePath);
}
}
2. ConfigMigrator
The ConfigMigrator class specializes in applying versioned transformations to configuration files, typically JSON-based settings.
Purpose: To manage the evolution of configuration schemas, ensuring that user settings are automatically updated to match the latest application requirements. This includes adding new default fields, removing deprecated ones, renaming fields, and performing complex data transformations.
Key Interfaces & Concepts:
ConfigTransform:
export interface ConfigTransform {
version: string; class="hl-cmt">// The target version this transform applies to
name: string;
description?: string;
transform: (config: Record<string, unknown>) => Record<string, unknown>; class="hl-cmt">// The actual transformation logic
validate?: (config: Record<string, unknown>) => boolean; class="hl-cmt">// Optional validation after transform
}
Each ConfigTransform defines a single step in the migration process, associated with a specific target version.
ConfigMigrationResult: An object detailing the outcome of a migration, including success status, versions, applied transforms, detected changes, and any errors.ConfigChange: Describes a specific change (add,remove,modify,rename) made to the configuration during a transform, including paths and old/new values.
Key Methods:
initialize(): Ensures the configuration and backup directories exist.registerTransform(transform: ConfigTransform): Adds a single transformation to the migrator. Transforms are ordered by version.registerTransforms(transforms: ConfigTransform[]): Registers multiple transforms.loadConfig(): Reads the configuration file (e.g.,settings.json) from disk.saveConfig(config: Record: Writes the (potentially migrated) configuration back to disk.) createBackup(): Creates a timestamped backup of the current configuration file before any changes are applied. This is crucial for rollback.restoreFromBackup(backupPath: string): Restores a configuration file from a specified backup.getConfigVersion(config: Record: Extracts the version string from a configuration object, handling common field names () _version,version,configVersion).setConfigVersion(config: Record: Updates the, version: string) _versionfield in the configuration object.migrate(targetVersion: string): The core migration orchestrator.
- Loads the current configuration.
- Determines the current version.
- Creates a backup if enabled.
- Identifies all
ConfigTransforms between the current andtargetVersion. - Applies each transform sequentially, tracking changes and running optional validations.
- Updates the configuration's internal version.
- Saves the migrated configuration.
- If any transform fails, it attempts to
restoreFromBackup().
detectChanges(before: Record: A private utility to compare two configuration objects and generate a list of, after: Record ) ConfigChangeentries.validateConfig(),applyDefaults(),removeDeprecatedFields(),renameField(): Helper methods that can be used withinConfigTransform.transformfunctions to perform common migration tasks.
Usage Pattern:
import { getConfigMigrator, ConfigTransform } from 39;./config-migrator.js39;;
import * as path from 39;path39;;
import * as os from 39;os39;;
const migrator = getConfigMigrator({
configDir: path.join(os.homedir(), 39;.my-app39;, 39;config39;),
configFile: 39;settings.json39;,
});
class="hl-cmt">// Define a transform
const transformV1_1_0: ConfigTransform = {
version: 39;1.1.039;,
name: 39;Add new default setting39;,
description: 39;Adds a new `featureFlags.darkMode` setting with a default value.39;,
transform: (config) => {
class="hl-cmt">// Use helper methods for common tasks
let newConfig = migrator.applyDefaults(config, {
featureFlags: {
darkMode: false,
},
});
newConfig = migrator.removeDeprecatedFields(newConfig, [39;oldSetting39;]);
return newConfig;
},
validate: (config) => 39;featureFlags39; in config && typeof (config as any).featureFlags.darkMode === 39;boolean39;,
};
const transformV1_2_0: ConfigTransform = {
version: 39;1.2.039;,
name: 39;Rename user.name to user.fullName39;,
transform: (config) => {
if (config.user && typeof config.user === 39;object39; && 39;name39; in config.user) {
const user = config.user as Record<string, unknown>;
user.fullName = user.name;
delete user.name;
}
return config;
},
};
async function runConfigMigration() {
await migrator.initialize();
migrator.registerTransforms([transformV1_1_0, transformV1_2_0]);
const targetVersion = 39;1.2.039;;
console.log(`Attempting to migrate config to ${targetVersion}...`);
const result = await migrator.migrate(targetVersion);
if (result.success) {
console.log(`Config migrated successfully from ${result.fromVersion} to ${result.toVersion}.`);
console.log(`Transforms applied: ${result.transformsApplied}`);
console.log(39;Changes:39;, result.changes);
} else {
console.error(39;Config migration failed:39;, result.errors);
if (result.backup) {
console.log(`Configuration restored from backup: ${result.backup}`);
}
}
}
3. MigrationManager
The MigrationManager is the most comprehensive component, designed for managing broader data and schema migrations with robust transactional guarantees and concurrency control.
Purpose: To provide a safe and auditable framework for evolving the application's persistent data structures. It's suitable for database schema changes, file system reorganizations, or any operation that modifies critical application data.
Key Interfaces & Concepts:
Migration:
export interface Migration {
version: string;
name: string;
description?: string;
up: (context: MigrationContext) => Promise<void>; class="hl-cmt">// Logic to apply the migration
down: (context: MigrationContext) => Promise<void>; class="hl-cmt">// Logic to revert the migration
appliedAt?: Date;
}
Each Migration defines a pair of up and down functions for a specific version. The up function applies the changes, and the down function reverts them.
MigrationContext: An object passed toupanddownfunctions, providing access to:dataDir,configDir: Paths to relevant directories.logger: A dedicated logger for migration output.dryRun: A flag indicating if changes should be simulated.backupFile(filePath: string): A crucial function to back up a file before modification, enabling transactional rollback.MigrationHistory: Records details of each applied migration (version, name, status, duration, transaction ID, checksum).MigrationAuditEntry: A detailed log entry for every significant operation (lock acquired/released, migration start/complete/fail, rollback, state backup/restore).StateBackup: An internal structure used to track files backed up during a transaction, allowing for precise restoration during rollback.
Key Methods & Internal Mechanisms:
initialize(): Ensures data and config directories exist, and loads existing migration history and audit logs.registerMigration(migration: Migration): Adds a migration to the manager.getPendingMigrations(),getAppliedMigrations(),getCurrentVersion(),getLatestVersion(),hasPendingMigrations(),getStatus(): Methods to query the current state of migrations.migrate(): The primary method to apply all pending migrations.- Locking (
acquireLock(),releaseLock()): Before starting, it attempts to acquire an exclusive file-based lock. This prevents multiple processes from running migrations simultaneously. It includes logic to detect and remove stale locks. - Transactions (
beginTransaction(),backupFile(),commitTransaction(),rollbackTransaction()): For each migration, a transaction is started. TheMigrationContext.backupFile()function allowsupanddownscripts to mark files for backup. If a migration'supfunction fails,rollbackTransaction()is automatically called to restore all backed-up files to their original state. - Audit Trail (
writeAuditEntry()): Every step of the migration process (lock, transaction, migration start/complete/fail) is recorded in a persistent audit log. - History (
loadHistory(),saveHistory()): The status of each migration (success, failed, rolled_back) is recorded and persisted. - Checksums (
calculateMigrationChecksum()): A checksum of each migration'supanddownfunctions is stored in the history, allowing detection of changes to migration scripts after they've been applied. - Signal Handlers (
installSignalHandlers(),emergencyCleanup()): Installs handlers for signals likeSIGINTandSIGTERMto ensure the lock is released even if the process terminates unexpectedly. migrateTo(targetVersion: string): Migrates forward or backward to a specific version. If migrating backward, it internally callsrollbackTo().rollback(): Reverts the last successfully applied migration by executing itsdownfunction. This also uses the transaction mechanism for safety.rollbackTo(targetVersion: string): Repeatedly callsrollback()until thegetCurrentVersion()is less than or equal to thetargetVersion.getAuditLog(): Retrieves entries from the audit log, with optional filtering.
Usage Pattern:
import { getMigrationManager, Migration, MigrationContext } from 39;./migration-manager.js39;;
import * as path from 39;path39;;
import * as fs from 39;fs-extra39;;
import * as os from 39;os39;;
const manager = getMigrationManager({
dataDir: path.join(os.homedir(), 39;.my-app39;),
configDir: path.join(os.homedir(), 39;.my-app39;, 39;config39;),
verbose: true,
});
class="hl-cmt">// Define a migration
const migrationV1_0_0: Migration = {
version: 39;1.0.039;,
name: 39;Initial database setup39;,
description: 39;Creates the initial user data file.39;,
up: async (context: MigrationContext) => {
const userFilePath = path.join(context.dataDir, 39;users.json39;);
context.logger.info(`Creating ${userFilePath}...`);
await context.backupFile(userFilePath); class="hl-cmt">// Backup if it exists, or mark as new
await fs.writeJson(userFilePath, [{ id: 39;admin39;, name: 39;Administrator39; }], { spaces: 2 });
},
down: async (context: MigrationContext) => {
const userFilePath = path.join(context.dataDir, 39;users.json39;);
context.logger.info(`Removing ${userFilePath}...`);
await context.backupFile(userFilePath); class="hl-cmt">// Backup before removing
await fs.remove(userFilePath);
},
};
const migrationV1_1_0: Migration = {
version: 39;1.1.039;,
name: 39;Add settings file39;,
description: 39;Creates a default settings file.39;,
up: async (context: MigrationContext) => {
const settingsFilePath = path.join(context.configDir, 39;app-settings.json39;);
context.logger.info(`Creating ${settingsFilePath}...`);
await context.backupFile(settingsFilePath);
await fs.writeJson(settingsFilePath, { theme: 39;dark39;, notifications: true }, { spaces: 2 });
},
down: async (context: MigrationContext) => {
const settingsFilePath = path.join(context.configDir, 39;app-settings.json39;);
context.logger.info(`Removing ${settingsFilePath}...`);
await context.backupFile(settingsFilePath);
await fs.remove(settingsFilePath);
},
};
async function runDataMigrations() {
await manager.initialize();
manager.registerMigrations([migrationV1_0_0, migrationV1_1_0]);
console.log(39;Current migration status:39;, manager.getStatus());
if (manager.hasPendingMigrations()) {
console.log(39;Running pending migrations...39;);
const result = await manager.migrate();
if (result.success) {
console.log(`Migrations completed successfully. Applied ${result.migrationsApplied} migrations.`);
} else {
console.error(39;Migrations failed:39;, result.errors);
}
} else {
console.log(39;No pending migrations.39;);
}
console.log(39;Updated migration status:39;, manager.getStatus());
class="hl-cmt">// Example of rollback
class="hl-cmt">// console.log(39;Rolling back last migration...39;);
class="hl-cmt">// const rollbackResult = await manager.rollback();
class="hl-cmt">// if (rollbackResult.success) {
class="hl-cmt">// console.log(39;Last migration rolled back successfully.39;);
class="hl-cmt">// } else {
class="hl-cmt">// console.error(39;Rollback failed:39;, rollbackResult.errors);
class="hl-cmt">// }
}
Integration Points
The src/versioning module is designed to be a foundational service for the application. Based on the provided call graph, here's how it integrates with other parts of the codebase:
- Application Startup/Initialization: It's highly probable that
VersionDetector.initialize(),ConfigMigrator.initialize(), andMigrationManager.initialize()are called early in the application's lifecycle to establish the current version and prepare for any necessary migrations. - Configuration Management: The
ConfigMigratoris directly involved in managingsettings.jsonor similar configuration files. - Data Persistence: The
MigrationManagerinteracts with thedataDirandconfigDirto perform file-based operations, suggesting it's used for migrating application-specific data files or local databases. - Resource Locking: The
MigrationManager'sreleaseLock()method is called by several modules (src/providers/local-llm-provider.ts,src/models/model-hub.ts,context/codebase-rag/ollama-embeddings.ts,src/providers/gemini-provider.ts). This indicates that theMigrationManager's file-based locking mechanism is leveraged not just for migrations, but potentially for any critical operation that requires exclusive access to shared resources (e.g., downloading models, managing LLM providers) to prevent conflicts. This is a powerful pattern for ensuring data integrity during sensitive operations. - File System Operations: Both
ConfigMigratorandMigrationManagerextensively usefs-extrafor directory creation (ensureDir), file reading/writing (readJson,writeJson,readFile,writeFile), copying (copy), and deletion (unlink,remove). - Process Management:
MigrationManager.acquireLock()usesprocess.kill(pid, 0)to check if a process holding a lock is still alive, demonstrating interaction with the operating system's process management. - Testing: The presence of
version-detector.test.tsandmigration-manager.test.tsindicates thorough unit testing of these components.
Design Considerations & Best Practices for Contributors
- Migration Granularity: Keep individual
ConfigTransformandMigrationunits small and focused on a single version increment or a specific change. This makes them easier to understand, test, and debug. - Idempotency is Key: Always ensure your
upanddownfunctions (forMigrationManager) andtransformfunctions (forConfigMigrator) are idempotent. This means they can be run multiple times without causing unintended side effects. - Thorough Testing: Write unit tests for each
ConfigTransformandMigrationto verify theirupanddownlogic works as expected, especially edge cases. - Error Handling within Migrations: While
MigrationManagerprovides transactional rollback, individualupanddownfunctions should still include robust error handling for their specific operations. - Use
MigrationContext.backupFile(): When implementingMigration.uporMigration.down, always callcontext.backupFile(filePath)before modifying or deleting any critical file. This is essential for the automatic rollback mechanism to function correctly. - Configuration: Leverage the
ConfigMigratorConfigandMigrationManagerConfiginterfaces to customize paths, enable/disable backups, or set dry-run modes for testing. - Singleton Awareness: Be mindful that
getConfigMigrator(),getMigrationManager(), andgetVersionDetector()return singleton instances. For testing or specific scenarios where a fresh instance is needed, useresetConfigMigrator(),resetMigrationManager(), orresetVersionDetector()respectively. - Audit Log Review: Regularly review the audit log (
migration-audit.json) to understand the history of migrations and diagnose any issues. - Locking Implications: Understand that the
MigrationManager's lock is a file-based lock. While robust, it relies on file system semantics and timeouts. Be aware of potential issues in highly distributed or unusual file system environments.