UNPKG

@oletizi/audio-tools

Version:

Monorepo for hardware sampler utilities and format parsers

1,000 lines (705 loc) 25.6 kB
# @oletizi/audiotools-config **Shared configuration system for audio-tools packages.** ## Purpose The `audiotools-config` package provides a unified configuration system for all audio-tools packages (backup, export, and future tools). It manages a single configuration file at `~/.audiotools/config.json`, enabling zero-flag workflows and consistent configuration across all tools. ## Features - **Single config file**: `~/.audiotools/config.json` for all tools - **Type-safe API**: Full TypeScript support with strict types - **CRUD operations**: Complete create, read, update, delete operations for configuration - **Auto-discovery**: Automatically discover existing backups and import them - **Interactive wizard**: User-friendly configuration via terminal prompts - **Extensible**: Easy to add configuration for new tools - **Validation**: Built-in validation for all configuration values ## Installation ### As a Library Dependency ```bash # In your package.json { "dependencies": { "@oletizi/audiotools-config": "^1.0.0" } } ``` ### Standalone (Not Recommended) ```bash npm install -g @oletizi/audiotools-config ``` **Note:** This package is typically installed as a dependency of other audio-tools packages. For end users, install `@oletizi/audiotools` instead. ## Configuration File Format The configuration file is stored at `~/.audiotools/config.json`: ```json { "version": "1.0", "backup": { "backupRoot": "~/.audiotools/backup", "sources": [ { "name": "pi-scsi2", "type": "remote", "source": "pi-scsi2.local:~/images/", "device": "images", "sampler": "pi-scsi2", "enabled": true }, { "name": "s5k-gotek", "type": "local", "source": "/Volumes/GOTEK", "device": "floppy", "sampler": "s5k-studio", "enabled": true } ] }, "export": { "outputRoot": "~/.audiotools/sampler-export/extracted", "formats": ["sfz", "decentsampler"], "skipUnchanged": true, "enabledSources": ["pi-scsi2", "s5k-gotek"] } } ``` ### Configuration Schema #### `AudioToolsConfig` Top-level configuration object: - `version`: Config format version (currently "1.0") - `backup?`: Backup tool configuration (see `BackupConfig`) - `export?`: Export tool configuration (see `ExportConfig`) - `[key: string]`: Future tools can add their own configuration sections #### `BackupConfig` Configuration for the backup tool: - `backupRoot`: Root directory for all backups (default: `~/.audiotools/backup`) - `sources`: Array of backup sources (see `BackupSource`) #### `BackupSource` Individual backup source configuration: - `name`: Unique identifier for this source (e.g., "pi-scsi2", "s5k-gotek") - `type`: Source type - `"remote"` (SSH) or `"local"` (mounted media) - `source`: Source path (e.g., "pi-scsi2.local:~/images/" or "/Volumes/GOTEK") - `device`: Device identifier (e.g., "images", "floppy", "scsi0") - `sampler?`: Sampler name (optional, defaults to source name) - `enabled`: Whether this source is active for backups **Device UUID Tracking (Optional):** For local media sources, additional fields enable device UUID tracking to recognize the same physical device across remounts: - `volumeUUID?`: Primary device identifier (e.g., "12345678-1234-1234-1234-123456789012") - `volumeLabel?`: Human-readable volume name (e.g., "SDCARD", "S5000_BACKUP") - `volumeSerial?`: Fallback identifier for devices without UUID (e.g., "12345678-02") - `lastSeen?`: ISO 8601 timestamp of last detection (e.g., "2025-10-09T14:30:00.000Z") - `registeredAt?`: ISO 8601 timestamp when device was first registered (e.g., "2025-10-09T14:30:00.000Z") **Example with UUID tracking:** ```typescript { name: 's5k-sdcard', type: 'local', source: '/Volumes/SDCARD', device: 'sd-card', sampler: 's5000', enabled: true, volumeUUID: '12345678-1234-1234-1234-123456789012', volumeLabel: 'SDCARD', lastSeen: '2025-10-09T14:30:00.000Z', registeredAt: '2025-10-09T14:00:00.000Z' } ``` **Backward Compatibility:** All UUID fields are optional. Existing configurations remain valid without modification. #### `ExportConfig` Configuration for the export tool: - `outputRoot`: Root directory for extracted files - `formats`: Array of output formats - `"sfz"` and/or `"decentsampler"` - `skipUnchanged`: Skip unchanged disk images during batch processing - `enabledSources`: Array of source names enabled for export ## API Reference ### Configuration Loading and Saving #### `loadConfig(path?: string): Promise<AudioToolsConfig>` Load configuration from disk. Creates default config if none exists. ```typescript import { loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); console.log(config.backup.sources); ``` **Parameters:** - `path` (optional): Custom config file path (default: `~/.audiotools/config.json`) **Returns:** Promise resolving to the loaded configuration #### `saveConfig(config: AudioToolsConfig, path?: string): Promise<void>` Save configuration to disk. ```typescript import { saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); config.backup.sources.push(newSource); await saveConfig(config); ``` **Parameters:** - `config`: Configuration object to save - `path` (optional): Custom config file path (default: `~/.audiotools/config.json`) #### `getDefaultConfig(): AudioToolsConfig` Get default configuration structure. ```typescript import { getDefaultConfig } from '@oletizi/audiotools-config'; const defaultConfig = getDefaultConfig(); ``` **Returns:** Default configuration object ### Backup Source Operations #### `addBackupSource(config: AudioToolsConfig, source: BackupSource): AudioToolsConfig` Add a new backup source to configuration. ```typescript import { addBackupSource, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const newSource = { name: 's5k-zulu', type: 'local', source: '/Volumes/ZULUSCSI', device: 'scsi0', sampler: 's5k-studio', enabled: true, }; const updatedConfig = addBackupSource(config, newSource); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `source`: Backup source to add **Returns:** Updated configuration (immutable) **Throws:** Error if source with same name already exists #### `updateBackupSource(config: AudioToolsConfig, name: string, updates: Partial<BackupSource>): AudioToolsConfig` Update an existing backup source. ```typescript import { updateBackupSource, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const updatedConfig = updateBackupSource(config, 'pi-scsi2', { enabled: false, source: 'pi-scsi2.local:~/new-path/', }); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `name`: Name of source to update - `updates`: Partial source object with fields to update **Returns:** Updated configuration (immutable) **Throws:** Error if source not found #### `removeBackupSource(config: AudioToolsConfig, name: string): AudioToolsConfig` Remove a backup source from configuration. ```typescript import { removeBackupSource, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const updatedConfig = removeBackupSource(config, 'old-source'); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `name`: Name of source to remove **Returns:** Updated configuration (immutable) **Throws:** Error if source not found #### `getEnabledBackupSources(config: AudioToolsConfig): BackupSource[]` Get all backup sources where `enabled: true`. ```typescript import { getEnabledBackupSources, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const enabledSources = getEnabledBackupSources(config); for (const source of enabledSources) { console.log(`Backing up: ${source.name}`); } ``` **Parameters:** - `config`: Current configuration **Returns:** Array of enabled backup sources #### `toggleBackupSource(config: AudioToolsConfig, name: string): AudioToolsConfig` Toggle the `enabled` state of a backup source. ```typescript import { toggleBackupSource, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const updatedConfig = toggleBackupSource(config, 'pi-scsi2'); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `name`: Name of source to toggle **Returns:** Updated configuration (immutable) **Throws:** Error if source not found ### Export Configuration Operations #### `updateExportConfig(config: AudioToolsConfig, updates: Partial<ExportConfig>): AudioToolsConfig` Update export configuration. ```typescript import { updateExportConfig, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const updatedConfig = updateExportConfig(config, { formats: ['sfz'], skipUnchanged: false, }); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `updates`: Partial export config with fields to update **Returns:** Updated configuration (immutable) #### `enableSourceForExport(config: AudioToolsConfig, sourceName: string): AudioToolsConfig` Enable a backup source for export processing. ```typescript import { enableSourceForExport, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const updatedConfig = enableSourceForExport(config, 's5k-gotek'); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `sourceName`: Name of source to enable for export **Returns:** Updated configuration (immutable) #### `disableSourceForExport(config: AudioToolsConfig, sourceName: string): AudioToolsConfig` Disable a backup source for export processing. ```typescript import { disableSourceForExport, loadConfig, saveConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const updatedConfig = disableSourceForExport(config, 'old-source'); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `sourceName`: Name of source to disable for export **Returns:** Updated configuration (immutable) #### `getEnabledExportSources(config: AudioToolsConfig): BackupSource[]` Get all backup sources enabled for export. ```typescript import { getEnabledExportSources, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const exportSources = getEnabledExportSources(config); for (const source of exportSources) { console.log(`Exporting: ${source.name}`); } ``` **Parameters:** - `config`: Current configuration **Returns:** Array of backup sources enabled for export ### Generic Tool Configuration These functions support configuration for future tools. #### `getToolConfig<T>(config: AudioToolsConfig, toolName: string): T | undefined` Get configuration for a specific tool. ```typescript import { getToolConfig } from '@oletizi/audiotools-config'; interface MyToolConfig { setting1: string; setting2: number; } const config = await loadConfig(); const myToolConfig = getToolConfig<MyToolConfig>(config, 'myTool'); if (myToolConfig) { console.log(myToolConfig.setting1); } ``` **Parameters:** - `config`: Current configuration - `toolName`: Name of tool configuration section **Returns:** Tool configuration or `undefined` if not found #### `setToolConfig<T>(config: AudioToolsConfig, toolName: string, toolConfig: T): AudioToolsConfig` Set configuration for a specific tool. ```typescript import { setToolConfig, saveConfig } from '@oletizi/audiotools-config'; interface MyToolConfig { setting1: string; setting2: number; } const config = await loadConfig(); const myToolConfig: MyToolConfig = { setting1: 'value', setting2: 42, }; const updatedConfig = setToolConfig(config, 'myTool', myToolConfig); await saveConfig(updatedConfig); ``` **Parameters:** - `config`: Current configuration - `toolName`: Name of tool configuration section - `toolConfig`: Configuration object for the tool **Returns:** Updated configuration (immutable) ### Validation #### `validateBackupSource(source: Partial<BackupSource>): ValidationResult` Validate a backup source configuration. ```typescript import { validateBackupSource } from '@oletizi/audiotools-config'; const result = validateBackupSource({ name: 'my-source', type: 'remote', source: 'host:~/path/', device: 'images', }); if (!result.valid) { console.error('Validation errors:', result.errors); } ``` **Returns:** `ValidationResult` with `valid: boolean` and `errors: string[]` #### `validateExportConfig(exportConfig: Partial<ExportConfig>): ValidationResult` Validate export configuration. ```typescript import { validateExportConfig } from '@oletizi/audiotools-config'; const result = validateExportConfig({ outputRoot: '~/.audiotools/export', formats: ['sfz', 'decentsampler'], }); if (!result.valid) { console.error('Validation errors:', result.errors); } ``` **Returns:** `ValidationResult` with `valid: boolean` and `errors: string[]` #### `validateUnifiedConfig(config: AudioToolsConfig): ValidationResult` Validate entire configuration structure. ```typescript import { validateUnifiedConfig, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const result = validateUnifiedConfig(config); if (!result.valid) { console.error('Configuration has errors:', result.errors); } ``` **Returns:** `ValidationResult` with `valid: boolean` and `errors: string[]` #### `testBackupSourceConnection(source: BackupSource): Promise<ValidationResult>` Test connectivity to a backup source (SSH or local path). ```typescript import { testBackupSourceConnection } from '@oletizi/audiotools-config'; const source = { name: 'pi-scsi2', type: 'remote', source: 'pi-scsi2.local:~/images/', device: 'images', enabled: true, }; const result = await testBackupSourceConnection(source); if (result.valid) { console.log('Connection successful!'); } else { console.error('Connection failed:', result.errors); } ``` **Returns:** Promise resolving to `ValidationResult` ### Auto-Discovery #### `discoverExistingBackups(backupRoot?: string): Promise<DiscoveredBackup[]>` Discover existing backups in a directory structure. ```typescript import { discoverExistingBackups } from '@oletizi/audiotools-config'; const discovered = await discoverExistingBackups('~/.audiotools/backup'); for (const backup of discovered) { console.log(`Found: ${backup.sampler}/${backup.device}`); console.log(` Files: ${backup.fileCount}`); console.log(` Size: ${(backup.totalSize / 1024 / 1024).toFixed(1)} MB`); } ``` **Parameters:** - `backupRoot` (optional): Directory to search (default: `~/.audiotools/backup`) **Returns:** Promise resolving to array of discovered backups #### `inferBackupType(samplerName: string): 'remote' | 'local'` Infer backup type from sampler name. ```typescript import { inferBackupType } from '@oletizi/audiotools-config'; const type1 = inferBackupType('pi-scsi2'); // 'remote' (SSH-based) const type2 = inferBackupType('s5k-gotek'); // 'local' (local media) ``` **Parameters:** - `samplerName`: Sampler/source name **Returns:** Inferred type: `'remote'` or `'local'` #### `importDiscoveredBackups(discovered: DiscoveredBackup[]): Promise<BackupSource[]>` Convert discovered backups into backup source configurations. ```typescript import { discoverExistingBackups, importDiscoveredBackups } from '@oletizi/audiotools-config'; const discovered = await discoverExistingBackups(); const sources = await importDiscoveredBackups(discovered); // Add sources to config const config = await loadConfig(); for (const source of sources) { config = addBackupSource(config, source); } await saveConfig(config); ``` **Parameters:** - `discovered`: Array of discovered backups **Returns:** Promise resolving to array of backup source configurations ### Interactive Wizard #### `runConfigWizard(configPath?: string): Promise<void>` Run the full interactive configuration wizard. ```typescript import { runConfigWizard } from '@oletizi/audiotools-config'; // Run wizard (will create or load config, prompt user, and save) await runConfigWizard(); ``` **Parameters:** - `configPath` (optional): Custom config file path **Returns:** Promise that resolves when wizard completes #### `mainMenu(config: AudioToolsConfig): Promise<void>` Display the main configuration menu. ```typescript import { mainMenu, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); await mainMenu(config); ``` **Parameters:** - `config`: Current configuration **Returns:** Promise that resolves when menu interaction completes #### `addBackupSourceWizard(config: AudioToolsConfig): Promise<BackupSource>` Interactive wizard to add a backup source. ```typescript import { addBackupSourceWizard, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); const newSource = await addBackupSourceWizard(config); console.log(`Added source: ${newSource.name}`); ``` **Parameters:** - `config`: Current configuration **Returns:** Promise resolving to the new backup source #### `editBackupSourceWizard(config: AudioToolsConfig): Promise<void>` Interactive wizard to edit backup sources. ```typescript import { editBackupSourceWizard, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); await editBackupSourceWizard(config); ``` **Parameters:** - `config`: Current configuration **Returns:** Promise that resolves when editing completes #### `removeBackupSourceWizard(config: AudioToolsConfig): Promise<void>` Interactive wizard to remove backup sources. ```typescript import { removeBackupSourceWizard, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); await removeBackupSourceWizard(config); ``` **Parameters:** - `config`: Current configuration **Returns:** Promise that resolves when removal completes #### `editExportConfigWizard(config: AudioToolsConfig): Promise<void>` Interactive wizard to edit export configuration. ```typescript import { editExportConfigWizard, loadConfig } from '@oletizi/audiotools-config'; const config = await loadConfig(); await editExportConfigWizard(config); ``` **Parameters:** - `config`: Current configuration **Returns:** Promise that resolves when editing completes #### `importBackupsWizard(discovered: DiscoveredBackup[]): Promise<BackupSource[]>` Interactive wizard to import discovered backups. ```typescript import { discoverExistingBackups, importBackupsWizard } from '@oletizi/audiotools-config'; const discovered = await discoverExistingBackups(); const sources = await importBackupsWizard(discovered); console.log(`Imported ${sources.length} sources`); ``` **Parameters:** - `discovered`: Array of discovered backups **Returns:** Promise resolving to array of selected backup sources ## Usage Examples ### Basic Configuration Workflow ```typescript import { loadConfig, saveConfig, addBackupSource, enableSourceForExport, } from '@oletizi/audiotools-config'; // Load existing config or create default const config = await loadConfig(); // Add a backup source const piScsiSource = { name: 'pi-scsi2', type: 'remote' as const, source: 'pi-scsi2.local:~/images/', device: 'images', enabled: true, }; let updatedConfig = addBackupSource(config, piScsiSource); // Enable source for export updatedConfig = enableSourceForExport(updatedConfig, 'pi-scsi2'); // Save configuration await saveConfig(updatedConfig); ``` ### Auto-Discovery and Import ```typescript import { discoverExistingBackups, importDiscoveredBackups, loadConfig, saveConfig, addBackupSource, } from '@oletizi/audiotools-config'; // Discover existing backups const discovered = await discoverExistingBackups(); console.log(`Found ${discovered.length} existing backup structures`); // Import as backup sources const sources = await importDiscoveredBackups(discovered); // Add to configuration let config = await loadConfig(); for (const source of sources) { config = addBackupSource(config, source); } await saveConfig(config); ``` ### Reading Configuration in a Tool ```typescript import { loadConfig, getEnabledBackupSources } from '@oletizi/audiotools-config'; async function runBackup() { const config = await loadConfig(); const sources = getEnabledBackupSources(config); if (sources.length === 0) { console.error('No backup sources configured'); process.exit(1); } for (const source of sources) { console.log(`Backing up ${source.name}...`); // Perform backup using source configuration } } ``` ### Interactive Configuration ```typescript import { runConfigWizard } from '@oletizi/audiotools-config'; // Launch interactive wizard await runConfigWizard(); ``` The wizard will: 1. Detect existing backups and offer to import them 2. Allow user to add/edit/remove backup sources 3. Configure export settings (formats, output paths) 4. Validate all inputs 5. Save configuration to `~/.audiotools/config.json` ## Integration Guide for New Tools Adding configuration support for a new audio-tool: ### 1. Define Tool Configuration Type ```typescript // In your tool package export interface MyToolConfig { setting1: string; setting2: number; enabled: boolean; } ``` ### 2. Load and Use Configuration ```typescript import { loadConfig, getToolConfig } from '@oletizi/audiotools-config'; async function myToolMain() { const config = await loadConfig(); const myConfig = getToolConfig<MyToolConfig>(config, 'myTool'); if (!myConfig) { console.error('Tool not configured. Run: audiotools config'); process.exit(1); } // Use configuration console.log(myConfig.setting1); } ``` ### 3. Add Wizard Support (Optional) ```typescript import { loadConfig, saveConfig, setToolConfig } from '@oletizi/audiotools-config'; import inquirer from 'inquirer'; export async function configureMyTool() { const config = await loadConfig(); const answers = await inquirer.prompt([ { type: 'input', name: 'setting1', message: 'Enter setting1:', }, { type: 'number', name: 'setting2', message: 'Enter setting2:', }, ]); const myConfig: MyToolConfig = { setting1: answers.setting1, setting2: answers.setting2, enabled: true, }; const updatedConfig = setToolConfig(config, 'myTool', myConfig); await saveConfig(updatedConfig); } ``` ### 4. Register with Unified CLI ```typescript // In audiotools-cli/src/commands/my-tool.ts import { Command } from 'commander'; import { loadConfig, getToolConfig } from '@oletizi/audiotools-config'; import { MyToolConfig, runMyTool } from '@oletizi/my-tool'; export const myToolCommand = new Command('my-tool') .description('Run my tool') .action(async () => { const config = await loadConfig(); const toolConfig = getToolConfig<MyToolConfig>(config, 'myTool'); if (!toolConfig) { console.error('Not configured. Run: audiotools config'); process.exit(1); } await runMyTool(toolConfig); }); // In audiotools-cli/src/index.ts import { myToolCommand } from './commands/my-tool.js'; program.addCommand(myToolCommand); ``` Now users can run: ```bash audiotools config # Configure your tool audiotools my-tool # Run your tool ``` ## Design Principles ### Immutability All configuration operations return new configuration objects rather than mutating existing ones: ```typescript // Good: Immutable updates const updated = addBackupSource(config, newSource); await saveConfig(updated); // Also valid: Chain operations const updated = enableSourceForExport( addBackupSource(config, newSource), 'new-source' ); ``` ### Type Safety All configuration structures are fully typed with TypeScript: ```typescript import type { AudioToolsConfig, BackupSource, ExportConfig } from '@oletizi/audiotools-config'; // Full type inference and checking const source: BackupSource = { name: 'test', type: 'remote', // Only 'remote' | 'local' allowed source: 'host:path/', device: 'images', enabled: true, }; ``` ### Extensibility The configuration system supports future tools without breaking changes: ```typescript // Future tools can add their config interface AudioToolsConfig { version: string; backup?: BackupConfig; export?: ExportConfig; // Any other tool can add here [key: string]: any; } // Generic accessors support any tool const myToolConfig = getToolConfig<MyToolConfig>(config, 'myTool'); ``` ### Validation All inputs are validated before being saved: ```typescript const result = validateBackupSource(source); if (!result.valid) { console.error('Invalid source:', result.errors); process.exit(1); } ``` ## Constants - `DEFAULT_CONFIG_PATH`: `~/.audiotools/config.json` - Default configuration file location ## TypeScript Types All types are exported and available for import: ```typescript import type { AudioToolsConfig, BackupSource, BackupConfig, ExportConfig, DiscoveredBackup, ValidationResult, } from '@oletizi/audiotools-config'; ``` ## License Apache-2.0 ## Repository Part of the [ol_dsp](https://github.com/oletizi/ol_dsp) monorepo. See the [audio-tools documentation](../docs/1.0/unified-cli/README.md) for more information about the unified CLI system.