@kadi.build/file-manager 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # @kadi.build/file-manager
2
+
3
+ > Foundational file management package providing local and remote file operations, file watching, compression utilities, and file streaming.
4
+
5
+ ## Features
6
+
7
+ - **Local File Operations** — Upload, download, copy, move, rename, delete files and folders
8
+ - **Remote File Operations** — SSH/SFTP-based remote operations with bastion/jump host support
9
+ - **File Watching** — Real-time file system monitoring with configurable debouncing and filtering
10
+ - **Compression** — ZIP and TAR.GZ compression/decompression with progress tracking and batch operations
11
+ - **File Streaming** — Memory-efficient streaming with range request support, ETag generation, and download tracking
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @kadi.build/file-manager
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```javascript
22
+ import { createFileManager } from '@kadi.build/file-manager';
23
+
24
+ // Create a file manager instance
25
+ const fm = await createFileManager();
26
+
27
+ // Local file operations
28
+ await fm.uploadFile('./source.txt', './destination.txt');
29
+ const info = await fm.getFileInfo('./myfile.txt');
30
+ const files = await fm.listFiles('./my-directory');
31
+
32
+ // Compression
33
+ await fm.compressFile('./folder', './backup.zip', { format: 'zip' });
34
+ await fm.decompressFile('./backup.zip', './extracted/');
35
+
36
+ // File watching
37
+ const watcher = await fm.startWatching('./watched-dir');
38
+ fm.on('fileEvent', (event) => {
39
+ console.log(`${event.type}: ${event.path}`);
40
+ });
41
+ ```
42
+
43
+ ## API Reference
44
+
45
+ ### FileManager
46
+
47
+ The main orchestrator that provides a unified interface to all providers.
48
+
49
+ ```javascript
50
+ import { FileManager, ConfigManager } from '@kadi.build/file-manager';
51
+
52
+ const config = new ConfigManager();
53
+ await config.load();
54
+ const fm = new FileManager(config);
55
+ ```
56
+
57
+ #### Connection & Validation
58
+
59
+ | Method | Description |
60
+ |--------|-------------|
61
+ | `testConnection(providerName)` | Test connectivity for a specific provider (`'local'`, `'watch'`, `'compression'`, `'remote'`) |
62
+ | `validateProvider(providerName)` | Validate provider configuration |
63
+ | `getSystemInfo()` | Get system information (platform, memory, disk space) |
64
+
65
+ #### File Operations
66
+
67
+ | Method | Description |
68
+ |--------|-------------|
69
+ | `uploadFile(source, dest, providerName?)` | Copy/upload a file |
70
+ | `downloadFile(source, dest, providerName?)` | Copy/download a file |
71
+ | `getFileInfo(filePath, providerName?)` | Get file metadata |
72
+ | `listFiles(directory, providerName?, options?)` | List files in a directory |
73
+ | `deleteFile(filePath, providerName?)` | Delete a file |
74
+ | `renameFile(oldPath, newPath, providerName?)` | Rename a file |
75
+ | `copyFile(source, dest, providerName?)` | Copy a file |
76
+ | `moveFile(source, dest, providerName?)` | Move a file |
77
+ | `searchFiles(directory, pattern, options?)` | Search for files matching a pattern |
78
+
79
+ #### Folder Operations
80
+
81
+ | Method | Description |
82
+ |--------|-------------|
83
+ | `createFolder(folderPath, providerName?)` | Create a directory (recursive) |
84
+ | `listFolders(directory, providerName?)` | List subdirectories |
85
+ | `deleteFolder(folderPath, options?)` | Delete a directory |
86
+ | `renameFolder(oldPath, newPath)` | Rename a directory |
87
+ | `copyFolder(source, dest)` | Copy a directory recursively |
88
+ | `moveFolder(source, dest)` | Move a directory |
89
+ | `getFolderInfo(folderPath)` | Get directory metadata |
90
+
91
+ #### Compression
92
+
93
+ | Method | Description |
94
+ |--------|-------------|
95
+ | `compressFile(input, output, options?)` | Compress a file or directory |
96
+ | `decompressFile(archive, outputDir, options?)` | Decompress an archive |
97
+ | `compress(input, output, options?)` | Alias for `compressFile` |
98
+ | `decompress(archive, outputDir, options?)` | Alias for `decompressFile` |
99
+ | `compressMultipleFiles(files, outputDir, options?)` | Batch compress multiple files |
100
+ | `decompressMultipleFiles(archives, outputDir, options?)` | Batch decompress multiple archives |
101
+ | `getCompressionStatus()` | Get compression provider status |
102
+ | `getCompressionProvider()` | Get the underlying compression provider |
103
+
104
+ **Compression Options:**
105
+ ```javascript
106
+ {
107
+ format: 'zip' | 'tar.gz', // Archive format (default: 'zip')
108
+ level: 1-9, // Compression level (default: 6)
109
+ includeRoot: true, // Include root directory in archive
110
+ overwrite: true // Overwrite existing files during extraction
111
+ }
112
+ ```
113
+
114
+ #### File Watching
115
+
116
+ | Method | Description |
117
+ |--------|-------------|
118
+ | `startWatching(path, options?)` | Start watching a path for changes |
119
+ | `stopWatching(watchIdOrPath)` | Stop a specific watcher |
120
+ | `stopAllWatching()` | Stop all active watchers |
121
+ | `listActiveWatchers()` | List all active watchers |
122
+ | `getWatcherInfo(watchId)` | Get details about a watcher |
123
+ | `getWatchingStatus()` | Get overall watching status |
124
+
125
+ **Watch Options:**
126
+ ```javascript
127
+ {
128
+ recursive: true, // Watch subdirectories
129
+ events: ['add', 'change', 'unlink'], // Event types to listen for
130
+ ignoreDotfiles: false, // Ignore dotfiles
131
+ debounceMs: 100 // Debounce interval in ms
132
+ }
133
+ ```
134
+
135
+ **Events:**
136
+ ```javascript
137
+ fm.on('fileEvent', (event) => {
138
+ // event.type: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
139
+ // event.path: string
140
+ // event.watchId: string
141
+ });
142
+ ```
143
+
144
+ #### Remote Operations
145
+
146
+ | Method | Description |
147
+ |--------|-------------|
148
+ | `connectRemote()` | Connect to the remote server |
149
+ | `disconnectRemote()` | Disconnect from the remote server |
150
+ | `syncToRemote(localPath, remotePath)` | Upload files to remote |
151
+ | `syncFromRemote(remotePath, localPath)` | Download files from remote |
152
+
153
+ **Remote Configuration (via environment variables):**
154
+ ```
155
+ KADI_REMOTE_HOST=example.com
156
+ KADI_REMOTE_PORT=22
157
+ KADI_REMOTE_USERNAME=user
158
+ KADI_REMOTE_PASSWORD=pass
159
+ KADI_REMOTE_PRIVATE_KEY_PATH=~/.ssh/id_rsa
160
+ ```
161
+
162
+ ### Individual Providers
163
+
164
+ For advanced use cases, you can import providers directly:
165
+
166
+ ```javascript
167
+ import {
168
+ LocalProvider,
169
+ WatchProvider,
170
+ CompressionProvider,
171
+ RemoteProvider,
172
+ FileStreamingUtils,
173
+ FileStreamer,
174
+ DownloadTracker,
175
+ PathUtils
176
+ } from '@kadi.build/file-manager';
177
+ ```
178
+
179
+ ### Factory Functions
180
+
181
+ ```javascript
182
+ import { createFileManager, compressFile, decompressFile, watchDirectory } from '@kadi.build/file-manager';
183
+
184
+ // Quick file manager creation
185
+ const fm = await createFileManager(options);
186
+
187
+ // One-shot compression
188
+ await compressFile('./input', './output.zip', { format: 'zip' });
189
+
190
+ // One-shot decompression
191
+ await decompressFile('./archive.zip', './output/');
192
+
193
+ // Quick directory watching
194
+ const watcher = await watchDirectory('./path', (event) => {
195
+ console.log(event);
196
+ });
197
+ ```
198
+
199
+ ## Configuration
200
+
201
+ Configuration is loaded from multiple sources (in priority order):
202
+
203
+ 1. Constructor options
204
+ 2. Environment variables (prefixed with `KADI_`)
205
+ 3. `.env` file
206
+
207
+ | Variable | Description | Default |
208
+ |----------|-------------|---------|
209
+ | `KADI_LOCAL_ROOT` | Root directory for local operations | `.` |
210
+ | `KADI_WATCH_DEBOUNCE_MS` | Debounce interval for file watching | `100` |
211
+ | `KADI_WATCH_MAX_WATCHERS` | Maximum concurrent watchers | `10` |
212
+ | `KADI_COMPRESSION_FORMAT` | Default compression format | `zip` |
213
+ | `KADI_COMPRESSION_LEVEL` | Default compression level | `6` |
214
+ | `KADI_REMOTE_HOST` | Remote SSH host | — |
215
+ | `KADI_REMOTE_PORT` | Remote SSH port | `22` |
216
+ | `KADI_REMOTE_USERNAME` | Remote SSH username | — |
217
+ | `KADI_REMOTE_PASSWORD` | Remote SSH password | — |
218
+ | `KADI_REMOTE_PRIVATE_KEY_PATH` | Path to SSH private key | — |
219
+
220
+ ## Testing
221
+
222
+ ```bash
223
+ # Run all tests
224
+ npm test
225
+
226
+ # Run individual test suites
227
+ npm run test:local # Local file operations (33 tests)
228
+ npm run test:watch # File watching (30 tests)
229
+ npm run test:compression # Compression (42 tests)
230
+ npm run test:streaming # File streaming (12 tests)
231
+ npm run test:remote # Remote operations (18 tests)
232
+ ```
233
+
234
+ **Total: 135 tests across 5 suites**
235
+
236
+ ## Project Structure
237
+
238
+ ```
239
+ packages/file-manager/
240
+ ├── package.json
241
+ ├── README.md
242
+ ├── src/
243
+ │ ├── index.js # Main entry point & exports
244
+ │ ├── FileManager.js # Main orchestrator
245
+ │ ├── ConfigManager.js # Configuration management
246
+ │ ├── providers/
247
+ │ │ ├── LocalProvider.js # Local file operations
248
+ │ │ ├── WatchProvider.js # File watching (chokidar)
249
+ │ │ ├── CompressionProvider.js # ZIP & TAR.GZ compression
250
+ │ │ └── RemoteProvider.js # SSH/SFTP remote operations
251
+ │ └── utils/
252
+ │ ├── FileStreamingUtils.js # Streaming, MIME detection, ETags
253
+ │ └── PathUtils.js # Path validation & normalization
254
+ └── tests/
255
+ ├── run-all-tests.js
256
+ ├── test-utils.js
257
+ ├── test-helpers/
258
+ │ └── index.js
259
+ ├── test-local-operations.js
260
+ ├── test-file-watching.js
261
+ ├── test-compression.js
262
+ ├── test-file-streaming.js
263
+ └── test-remote-operations.js
264
+ ```
265
+
266
+ ## License
267
+
268
+ MIT
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@kadi.build/file-manager",
3
+ "version": "1.0.0",
4
+ "description": "Complete local and remote file management with watching and compression",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "src/",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "node tests/run-all-tests.js",
18
+ "test:local": "node tests/test-local-operations.js",
19
+ "test:watch": "node tests/test-file-watching.js",
20
+ "test:compression": "node tests/test-compression.js",
21
+ "test:streaming": "node tests/test-file-streaming.js",
22
+ "test:remote": "node tests/test-remote-operations.js"
23
+ },
24
+ "keywords": [
25
+ "file-management",
26
+ "file-operations",
27
+ "file-watching",
28
+ "compression",
29
+ "sftp",
30
+ "ssh",
31
+ "remote-files"
32
+ ],
33
+ "dependencies": {
34
+ "chokidar": "^3.5.3",
35
+ "archiver": "^6.0.0",
36
+ "unzipper": "^0.10.14",
37
+ "tar": "^6.2.0",
38
+ "ssh2": "^1.15.0",
39
+ "ssh2-sftp-client": "^9.1.0"
40
+ },
41
+ "devDependencies": {
42
+ "fs-extra": "^11.2.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "license": "MIT"
48
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * ConfigManager for @kadi.build/file-manager
3
+ *
4
+ * Handles configuration for file operations only.
5
+ * Simplified from the monolith ConfigManager - no tunnel/server config.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+
12
+ class ConfigManager {
13
+ constructor() {
14
+ this.config = {};
15
+ this.defaults = {
16
+ // Local File System Configuration
17
+ LOCAL_ROOT: process.cwd(),
18
+ DEFAULT_UPLOAD_DIRECTORY: './uploads',
19
+ DEFAULT_DOWNLOAD_DIRECTORY: './downloads',
20
+ DEFAULT_TEMP_DIRECTORY: './temp',
21
+
22
+ // File Operation Configuration
23
+ MAX_FILE_SIZE: 1073741824, // 1GB in bytes
24
+ CHUNK_SIZE: 8388608, // 8MB chunks
25
+ MAX_RETRY_ATTEMPTS: 3,
26
+ TIMEOUT_MS: 30000,
27
+
28
+ // File Watching Configuration
29
+ WATCH_ENABLED: true,
30
+ WATCH_RECURSIVE: true,
31
+ WATCH_IGNORE_DOTFILES: true,
32
+ WATCH_DEBOUNCE_MS: 100,
33
+ WATCH_MAX_WATCHERS: 50,
34
+
35
+ // Compression Configuration
36
+ COMPRESSION_ENABLED: true,
37
+ COMPRESSION_LEVEL: 6,
38
+ COMPRESSION_FORMAT: 'zip',
39
+ COMPRESSION_MAX_FILE_SIZE: 1073741824,
40
+ COMPRESSION_CHUNK_SIZE: 8388608,
41
+ COMPRESSION_ENABLE_PROGRESS_TRACKING: true,
42
+ COMPRESSION_ENABLE_CHECKSUM_VERIFICATION: true,
43
+
44
+ // Remote Configuration
45
+ REMOTE_HOST: '',
46
+ REMOTE_PORT: 22,
47
+ REMOTE_USERNAME: '',
48
+ REMOTE_PASSWORD: '',
49
+ REMOTE_PRIVATE_KEY_PATH: '~/.ssh/id_rsa',
50
+ REMOTE_PASSPHRASE: '',
51
+ REMOTE_ROOT: '/home',
52
+ REMOTE_TIMEOUT: 30000,
53
+ REMOTE_BASTION_HOST: '',
54
+ REMOTE_BASTION_PORT: 22,
55
+
56
+ // Performance Configuration
57
+ MAX_CONCURRENT_OPERATIONS: 5,
58
+ ENABLE_PROGRESS_TRACKING: true,
59
+ ENABLE_CHECKSUM_VERIFICATION: true,
60
+
61
+ // Security Configuration
62
+ ALLOW_SYMLINKS: false,
63
+ RESTRICT_TO_BASE_PATH: true,
64
+ MAX_PATH_LENGTH: 255,
65
+
66
+ // Logging
67
+ LOG_LEVEL: 'info'
68
+ };
69
+ }
70
+
71
+ async load(options = {}) {
72
+ // Apply defaults
73
+ this.config = { ...this.defaults };
74
+
75
+ // Load from .env file if it exists
76
+ try {
77
+ const envPath = path.join(process.cwd(), '.env');
78
+ const envContent = await fs.readFile(envPath, 'utf8');
79
+ this._parseEnvContent(envContent);
80
+ } catch (error) {
81
+ // .env file not found, that's fine
82
+ }
83
+
84
+ // Load from environment variables
85
+ this._loadFromEnvironment();
86
+
87
+ // Apply passed options (highest priority)
88
+ if (options) {
89
+ for (const [key, value] of Object.entries(options)) {
90
+ // Support both UPPER_CASE config keys and camelCase options
91
+ const upperKey = key.replace(/([A-Z])/g, '_$1').toUpperCase().replace(/^_/, '');
92
+ if (this.defaults.hasOwnProperty(upperKey)) {
93
+ this.config[upperKey] = value;
94
+ } else if (this.defaults.hasOwnProperty(key)) {
95
+ this.config[key] = value;
96
+ } else {
97
+ // Store as-is for custom options
98
+ this.config[key] = value;
99
+ }
100
+ }
101
+ }
102
+
103
+ // Ensure required directories exist
104
+ await this._ensureDirectories();
105
+
106
+ return this;
107
+ }
108
+
109
+ _parseEnvContent(content) {
110
+ const lines = content.split('\n');
111
+ for (const line of lines) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed || trimmed.startsWith('#')) continue;
114
+
115
+ const eqIndex = trimmed.indexOf('=');
116
+ if (eqIndex === -1) continue;
117
+
118
+ const key = trimmed.substring(0, eqIndex).trim();
119
+ let value = trimmed.substring(eqIndex + 1).trim();
120
+
121
+ // Remove surrounding quotes
122
+ if ((value.startsWith('"') && value.endsWith('"')) ||
123
+ (value.startsWith("'") && value.endsWith("'"))) {
124
+ value = value.slice(1, -1);
125
+ }
126
+
127
+ // Only apply if it's a known config key
128
+ if (this.defaults.hasOwnProperty(key)) {
129
+ this.config[key] = this._parseValue(value, this.defaults[key]);
130
+ }
131
+ }
132
+ }
133
+
134
+ _loadFromEnvironment() {
135
+ for (const key of Object.keys(this.defaults)) {
136
+ // Check KADI_ prefixed env vars
137
+ const envKey = `KADI_${key}`;
138
+ if (process.env[envKey] !== undefined) {
139
+ this.config[key] = this._parseValue(process.env[envKey], this.defaults[key]);
140
+ }
141
+ // Also check non-prefixed env vars
142
+ if (process.env[key] !== undefined) {
143
+ this.config[key] = this._parseValue(process.env[key], this.defaults[key]);
144
+ }
145
+ }
146
+ }
147
+
148
+ _parseValue(value, defaultValue) {
149
+ if (typeof defaultValue === 'boolean') {
150
+ return value === 'true' || value === '1' || value === true;
151
+ }
152
+ if (typeof defaultValue === 'number') {
153
+ const parsed = parseInt(value, 10);
154
+ return isNaN(parsed) ? defaultValue : parsed;
155
+ }
156
+ return value;
157
+ }
158
+
159
+ async _ensureDirectories() {
160
+ const dirs = [
161
+ this.config.DEFAULT_UPLOAD_DIRECTORY,
162
+ this.config.DEFAULT_DOWNLOAD_DIRECTORY,
163
+ this.config.DEFAULT_TEMP_DIRECTORY
164
+ ];
165
+
166
+ for (const dir of dirs) {
167
+ try {
168
+ await fs.mkdir(dir, { recursive: true });
169
+ } catch (error) {
170
+ // Directory creation failures are non-fatal
171
+ }
172
+ }
173
+ }
174
+
175
+ // ============================================================================
176
+ // CONFIG GETTERS
177
+ // ============================================================================
178
+
179
+ getLocalConfig() {
180
+ return {
181
+ localRoot: this.config.LOCAL_ROOT,
182
+ uploadDirectory: this.config.DEFAULT_UPLOAD_DIRECTORY,
183
+ downloadDirectory: this.config.DEFAULT_DOWNLOAD_DIRECTORY,
184
+ tempDirectory: this.config.DEFAULT_TEMP_DIRECTORY,
185
+ maxFileSize: this.config.MAX_FILE_SIZE,
186
+ chunkSize: this.config.CHUNK_SIZE,
187
+ maxRetryAttempts: this.config.MAX_RETRY_ATTEMPTS,
188
+ timeoutMs: this.config.TIMEOUT_MS,
189
+ allowSymlinks: this.config.ALLOW_SYMLINKS,
190
+ restrictToBasePath: this.config.RESTRICT_TO_BASE_PATH,
191
+ maxPathLength: this.config.MAX_PATH_LENGTH
192
+ };
193
+ }
194
+
195
+ getWatchConfig() {
196
+ return {
197
+ localRoot: this.config.LOCAL_ROOT,
198
+ enabled: this.config.WATCH_ENABLED,
199
+ recursive: this.config.WATCH_RECURSIVE,
200
+ ignoreDotfiles: this.config.WATCH_IGNORE_DOTFILES,
201
+ debounceMs: this.config.WATCH_DEBOUNCE_MS,
202
+ maxWatchers: this.config.WATCH_MAX_WATCHERS
203
+ };
204
+ }
205
+
206
+ getCompressionConfig() {
207
+ return {
208
+ localRoot: this.config.LOCAL_ROOT,
209
+ enabled: this.config.COMPRESSION_ENABLED,
210
+ level: this.config.COMPRESSION_LEVEL,
211
+ format: this.config.COMPRESSION_FORMAT,
212
+ maxFileSize: this.config.COMPRESSION_MAX_FILE_SIZE,
213
+ chunkSize: this.config.COMPRESSION_CHUNK_SIZE,
214
+ enableProgressTracking: this.config.COMPRESSION_ENABLE_PROGRESS_TRACKING,
215
+ enableChecksumVerification: this.config.COMPRESSION_ENABLE_CHECKSUM_VERIFICATION
216
+ };
217
+ }
218
+
219
+ getRemoteConfig() {
220
+ return {
221
+ host: this.config.REMOTE_HOST,
222
+ port: this.config.REMOTE_PORT,
223
+ username: this.config.REMOTE_USERNAME,
224
+ password: this.config.REMOTE_PASSWORD,
225
+ privateKeyPath: this.config.REMOTE_PRIVATE_KEY_PATH,
226
+ passphrase: this.config.REMOTE_PASSPHRASE,
227
+ remoteRoot: this.config.REMOTE_ROOT,
228
+ timeout: this.config.REMOTE_TIMEOUT,
229
+ bastionHost: this.config.REMOTE_BASTION_HOST,
230
+ bastionPort: this.config.REMOTE_BASTION_PORT
231
+ };
232
+ }
233
+
234
+ getPerformanceConfig() {
235
+ return {
236
+ maxConcurrentOperations: this.config.MAX_CONCURRENT_OPERATIONS,
237
+ enableProgressTracking: this.config.ENABLE_PROGRESS_TRACKING,
238
+ enableChecksumVerification: this.config.ENABLE_CHECKSUM_VERIFICATION
239
+ };
240
+ }
241
+
242
+ hasRemoteConfig() {
243
+ return !!(this.config.REMOTE_HOST && this.config.REMOTE_USERNAME);
244
+ }
245
+
246
+ validate() {
247
+ const errors = [];
248
+ const warnings = [];
249
+
250
+ if (this.config.MAX_FILE_SIZE <= 0) {
251
+ errors.push('MAX_FILE_SIZE must be positive');
252
+ }
253
+
254
+ if (this.config.CHUNK_SIZE <= 0) {
255
+ errors.push('CHUNK_SIZE must be positive');
256
+ }
257
+
258
+ if (this.config.WATCH_MAX_WATCHERS > 100) {
259
+ warnings.push('High number of watchers may impact performance');
260
+ }
261
+
262
+ if (this.config.COMPRESSION_LEVEL < 0 || this.config.COMPRESSION_LEVEL > 9) {
263
+ errors.push('COMPRESSION_LEVEL must be between 0 and 9');
264
+ }
265
+
266
+ if (this.hasRemoteConfig()) {
267
+ if (!this.config.REMOTE_PASSWORD && !this.config.REMOTE_PRIVATE_KEY_PATH) {
268
+ warnings.push('Remote configuration missing both password and private key path');
269
+ }
270
+ }
271
+
272
+ return {
273
+ isValid: errors.length === 0,
274
+ errors,
275
+ warnings
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Get a specific config value
281
+ */
282
+ get(key) {
283
+ return this.config[key];
284
+ }
285
+
286
+ /**
287
+ * Set a specific config value
288
+ */
289
+ set(key, value) {
290
+ this.config[key] = value;
291
+ }
292
+
293
+ /**
294
+ * Get all config as a plain object
295
+ */
296
+ toJSON() {
297
+ return { ...this.config };
298
+ }
299
+ }
300
+
301
+ export { ConfigManager };