@config-bound/cli 0.1.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.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-format$colon$ci.log +6 -0
  3. package/.turbo/turbo-lint$colon$ci.log +4 -0
  4. package/.turbo/turbo-test.log +19 -0
  5. package/CHANGELOG.md +15 -0
  6. package/README.md +66 -0
  7. package/bin/configbound.js +3 -0
  8. package/dist/cli.module.d.ts +3 -0
  9. package/dist/cli.module.d.ts.map +1 -0
  10. package/dist/cli.module.js +29 -0
  11. package/dist/cli.module.js.map +1 -0
  12. package/dist/commands/export.command.d.ts +30 -0
  13. package/dist/commands/export.command.d.ts.map +1 -0
  14. package/dist/commands/export.command.js +226 -0
  15. package/dist/commands/export.command.js.map +1 -0
  16. package/dist/commands/list.command.d.ts +13 -0
  17. package/dist/commands/list.command.d.ts.map +1 -0
  18. package/dist/commands/list.command.js +93 -0
  19. package/dist/commands/list.command.js.map +1 -0
  20. package/dist/main.d.ts +3 -0
  21. package/dist/main.d.ts.map +1 -0
  22. package/dist/main.js +17 -0
  23. package/dist/main.js.map +1 -0
  24. package/dist/services/config-discovery.service.d.ts +15 -0
  25. package/dist/services/config-discovery.service.d.ts.map +1 -0
  26. package/dist/services/config-discovery.service.js +191 -0
  27. package/dist/services/config-discovery.service.js.map +1 -0
  28. package/dist/services/config-discovery.service.spec.d.ts +2 -0
  29. package/dist/services/config-discovery.service.spec.d.ts.map +1 -0
  30. package/dist/services/config-discovery.service.spec.js +137 -0
  31. package/dist/services/config-discovery.service.spec.js.map +1 -0
  32. package/dist/services/config-loader.service.d.ts +13 -0
  33. package/dist/services/config-loader.service.d.ts.map +1 -0
  34. package/dist/services/config-loader.service.js +241 -0
  35. package/dist/services/config-loader.service.js.map +1 -0
  36. package/dist/services/file-writer.service.d.ts +6 -0
  37. package/dist/services/file-writer.service.d.ts.map +1 -0
  38. package/dist/services/file-writer.service.js +38 -0
  39. package/dist/services/file-writer.service.js.map +1 -0
  40. package/dist/services/file-writer.service.spec.d.ts +2 -0
  41. package/dist/services/file-writer.service.spec.d.ts.map +1 -0
  42. package/dist/services/file-writer.service.spec.js +98 -0
  43. package/dist/services/file-writer.service.spec.js.map +1 -0
  44. package/dist/services/schema-export.service.d.ts +14 -0
  45. package/dist/services/schema-export.service.d.ts.map +1 -0
  46. package/dist/services/schema-export.service.js +58 -0
  47. package/dist/services/schema-export.service.js.map +1 -0
  48. package/dist/services/schema-export.service.spec.d.ts +2 -0
  49. package/dist/services/schema-export.service.spec.d.ts.map +1 -0
  50. package/dist/services/schema-export.service.spec.js +69 -0
  51. package/dist/services/schema-export.service.spec.js.map +1 -0
  52. package/eslint.config.mjs +5 -0
  53. package/jest.config.js +39 -0
  54. package/package.json +62 -0
  55. package/src/cli.module.ts +21 -0
  56. package/src/commands/export.command.ts +253 -0
  57. package/src/commands/list.command.ts +108 -0
  58. package/src/main.ts +19 -0
  59. package/src/services/config-discovery.service.spec.ts +209 -0
  60. package/src/services/config-discovery.service.ts +237 -0
  61. package/src/services/config-loader.service.ts +379 -0
  62. package/src/services/file-writer.service.spec.ts +139 -0
  63. package/src/services/file-writer.service.ts +32 -0
  64. package/src/services/schema-export.service.spec.ts +124 -0
  65. package/src/services/schema-export.service.ts +85 -0
  66. package/tsconfig.json +10 -0
@@ -0,0 +1,237 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { Project, Node, SyntaxKind, SourceFile } from 'ts-morph';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { ensureError } from '@config-bound/config-bound/utilities';
6
+
7
+ export interface DiscoveredConfig {
8
+ filePath: string;
9
+ exportName: string;
10
+ isDefault: boolean;
11
+ configName?: string;
12
+ lineNumber: number;
13
+ }
14
+
15
+ @Injectable()
16
+ export class ConfigDiscoveryService {
17
+ async discoverConfigs(
18
+ searchPath: string,
19
+ recursive: boolean = true
20
+ ): Promise<DiscoveredConfig[]> {
21
+ const tsFiles = this.findTypeScriptFiles(searchPath, recursive);
22
+ const discovered: DiscoveredConfig[] = [];
23
+
24
+ // Create a single project instance for all files
25
+ const project = new Project({
26
+ skipAddingFilesFromTsConfig: true,
27
+ compilerOptions: {
28
+ allowJs: false,
29
+ skipLibCheck: true
30
+ }
31
+ });
32
+
33
+ // Add all files at once
34
+ for (const filePath of tsFiles) {
35
+ try {
36
+ project.addSourceFileAtPath(filePath);
37
+ } catch (error: unknown) {
38
+ console.warn(`skipping ${filePath}: ${ensureError(error).message}`);
39
+ // Skip files with parse errors
40
+ continue;
41
+ }
42
+ }
43
+
44
+ // Now extract configs from all source files
45
+ for (const sourceFile of project.getSourceFiles()) {
46
+ try {
47
+ const configs = this.extractConfigsFromFile(sourceFile);
48
+ discovered.push(...configs);
49
+ } catch (error: unknown) {
50
+ console.warn(`skipping ${sourceFile}: ${ensureError(error).message}`);
51
+ continue;
52
+ }
53
+ }
54
+
55
+ return discovered;
56
+ }
57
+
58
+ private extractConfigsFromFile(sourceFile: SourceFile): DiscoveredConfig[] {
59
+ const discovered: DiscoveredConfig[] = [];
60
+ const filePath = sourceFile.getFilePath();
61
+
62
+ // Find all variable declarations
63
+ const variableStatements = sourceFile.getVariableStatements();
64
+
65
+ for (const statement of variableStatements) {
66
+ const declarations = statement.getDeclarations();
67
+
68
+ for (const declaration of declarations) {
69
+ const initializer = declaration.getInitializer();
70
+ if (!initializer) continue;
71
+
72
+ // Check if it's a call to ConfigBound.createConfig()
73
+ if (this.isConfigBoundCreateConfig(initializer)) {
74
+ const isExported = statement
75
+ .getModifiers()
76
+ .some((mod: Node) => mod.getKind() === SyntaxKind.ExportKeyword);
77
+
78
+ if (isExported) {
79
+ const exportName = declaration.getName();
80
+ const configName = this.extractConfigName(initializer);
81
+
82
+ discovered.push({
83
+ filePath,
84
+ exportName,
85
+ isDefault: false,
86
+ configName,
87
+ lineNumber: declaration.getStartLineNumber()
88
+ });
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // Check for default exports
95
+ const defaultExport = sourceFile.getDefaultExportSymbol();
96
+ if (defaultExport) {
97
+ const declarations = defaultExport.getDeclarations();
98
+ for (const declaration of declarations) {
99
+ if (Node.isExportAssignment(declaration)) {
100
+ const expression = declaration.getExpression();
101
+ if (this.isConfigBoundCreateConfig(expression)) {
102
+ const configName = this.extractConfigName(expression);
103
+ discovered.push({
104
+ filePath,
105
+ exportName: 'default',
106
+ isDefault: true,
107
+ configName,
108
+ lineNumber: declaration.getStartLineNumber()
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ return discovered;
116
+ }
117
+
118
+ private isConfigBoundCreateConfig(node: Node): boolean {
119
+ if (!Node.isCallExpression(node)) return false;
120
+
121
+ const expression = node.getExpression();
122
+
123
+ // Check for ConfigBound.createConfig()
124
+ if (Node.isPropertyAccessExpression(expression)) {
125
+ const obj = expression.getExpression();
126
+ const prop = expression.getName();
127
+
128
+ if (
129
+ Node.isIdentifier(obj) &&
130
+ obj.getText() === 'ConfigBound' &&
131
+ prop === 'createConfig'
132
+ ) {
133
+ return true;
134
+ }
135
+ }
136
+
137
+ // Check for createConfig() (named import)
138
+ if (Node.isIdentifier(expression)) {
139
+ if (expression.getText() === 'createConfig') {
140
+ return true;
141
+ }
142
+ }
143
+
144
+ return false;
145
+ }
146
+
147
+ private extractConfigName(node: Node): string | undefined {
148
+ if (!Node.isCallExpression(node)) return undefined;
149
+
150
+ const args = node.getArguments();
151
+ if (args.length === 0) return undefined;
152
+
153
+ const firstArg = args[0];
154
+
155
+ // Look for name property in object literal
156
+ if (Node.isObjectLiteralExpression(firstArg)) {
157
+ const nameProperty = firstArg
158
+ .getProperties()
159
+ .find(
160
+ (prop) => Node.isPropertyAssignment(prop) && prop.getName() === 'name'
161
+ );
162
+
163
+ if (nameProperty && Node.isPropertyAssignment(nameProperty)) {
164
+ const initializer = nameProperty.getInitializer();
165
+ if (initializer && Node.isStringLiteral(initializer)) {
166
+ return initializer.getLiteralValue();
167
+ }
168
+ }
169
+ }
170
+
171
+ return undefined;
172
+ }
173
+
174
+ private findTypeScriptFiles(
175
+ searchPath: string,
176
+ recursive: boolean
177
+ ): string[] {
178
+ const files: string[] = [];
179
+ const excludedDirs = new Set([
180
+ 'node_modules',
181
+ 'dist',
182
+ 'build',
183
+ 'coverage',
184
+ '.git',
185
+ '.next',
186
+ '.nuxt',
187
+ 'out',
188
+ 'public',
189
+ 'static',
190
+ '__pycache__',
191
+ 'venv',
192
+ 'env'
193
+ ]);
194
+
195
+ function traverse(dir: string) {
196
+ let entries;
197
+ try {
198
+ entries = fs.readdirSync(dir, { withFileTypes: true });
199
+ } catch {
200
+ // Skip directories we can't read
201
+ return;
202
+ }
203
+
204
+ for (const entry of entries) {
205
+ const fullPath = path.join(dir, entry.name);
206
+
207
+ // Skip excluded directories and hidden directories
208
+ if (entry.isDirectory()) {
209
+ if (excludedDirs.has(entry.name) || entry.name.startsWith('.')) {
210
+ continue;
211
+ }
212
+ if (recursive) {
213
+ traverse(fullPath);
214
+ }
215
+ } else if (entry.isFile() && entry.name.endsWith('.ts')) {
216
+ // Skip test files and declaration files
217
+ if (
218
+ !entry.name.endsWith('.spec.ts') &&
219
+ !entry.name.endsWith('.test.ts') &&
220
+ !entry.name.endsWith('.d.ts')
221
+ ) {
222
+ files.push(fullPath);
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ const stats = fs.statSync(searchPath);
229
+ if (stats.isDirectory()) {
230
+ traverse(searchPath);
231
+ } else if (stats.isFile() && searchPath.endsWith('.ts')) {
232
+ files.push(searchPath);
233
+ }
234
+
235
+ return files;
236
+ }
237
+ }
@@ -0,0 +1,379 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { pathToFileURL } from 'url';
5
+ import { Section } from '@config-bound/config-bound/section';
6
+ import {
7
+ ConfigBound,
8
+ TypedConfigBound,
9
+ ConfigSchema,
10
+ ConfigLoaderException,
11
+ ConfigFileNotFoundException,
12
+ ConfigFileIsDirectoryException,
13
+ ExportNotFoundException,
14
+ NoConfigBoundInstancesException,
15
+ MultipleConfigBoundInstancesException,
16
+ InvalidConfigBoundInstanceException,
17
+ ConfigFileParseException,
18
+ MissingDependencyException
19
+ } from '@config-bound/config-bound';
20
+
21
+ export interface LoadedConfig {
22
+ name: string;
23
+ sections: Section[];
24
+ instance: ConfigBound | TypedConfigBound<ConfigSchema>;
25
+ }
26
+
27
+ @Injectable()
28
+ export class ConfigLoaderService {
29
+ async loadConfig(
30
+ filePath: string,
31
+ exportName?: string
32
+ ): Promise<LoadedConfig> {
33
+ const absolutePath = path.resolve(filePath);
34
+
35
+ if (!fs.existsSync(absolutePath)) {
36
+ const parentDir = path.dirname(absolutePath);
37
+ const parentExists = fs.existsSync(parentDir);
38
+ let similarFiles: string[] | undefined;
39
+ if (parentExists) {
40
+ const fileName = path.basename(absolutePath);
41
+ const dirFiles = fs.readdirSync(parentDir);
42
+ similarFiles = dirFiles.filter((file) =>
43
+ file.toLowerCase().includes(fileName.toLowerCase().slice(0, 3))
44
+ );
45
+ }
46
+ throw new ConfigFileNotFoundException(
47
+ absolutePath,
48
+ parentDir,
49
+ parentExists,
50
+ similarFiles
51
+ );
52
+ }
53
+
54
+ const stat = fs.statSync(absolutePath);
55
+ if (stat.isDirectory()) {
56
+ throw new ConfigFileIsDirectoryException(absolutePath);
57
+ }
58
+
59
+ // If it's a TypeScript file, check if there's a compiled .js version
60
+ const isTypeScript =
61
+ absolutePath.endsWith('.ts') || absolutePath.endsWith('.tsx');
62
+ let fileToLoad = absolutePath;
63
+
64
+ const parseExceptionBaseMessage = `TypeScript file found but no compiled .js file exists. Please compile the file first (e.g., run 'tsc' or 'npm run build').`;
65
+ if (isTypeScript) {
66
+ // First, check for .js in the same directory
67
+ const jsPath = absolutePath.replace(/\.tsx?$/, '.js');
68
+ if (fs.existsSync(jsPath)) {
69
+ fileToLoad = jsPath;
70
+ } else {
71
+ // If in src/, check for corresponding file in dist/
72
+ const srcMatch = absolutePath.match(/^(.*[/\\])src([/\\].+)$/);
73
+ if (srcMatch) {
74
+ const distPath =
75
+ srcMatch[1] + 'dist' + srcMatch[2].replace(/\.tsx?$/, '.js');
76
+ if (fs.existsSync(distPath)) {
77
+ fileToLoad = distPath;
78
+ } else {
79
+ throw new ConfigFileParseException(
80
+ filePath,
81
+ `${parseExceptionBaseMessage} Checked: ${jsPath} and ${distPath}`
82
+ );
83
+ }
84
+ } else {
85
+ throw new ConfigFileParseException(
86
+ filePath,
87
+ `${parseExceptionBaseMessage} Expected compiled file at: ${jsPath}`
88
+ );
89
+ }
90
+ }
91
+ }
92
+
93
+ try {
94
+ const fileURL = pathToFileURL(fileToLoad).href;
95
+ const module = await import(fileURL);
96
+
97
+ let configInstance:
98
+ | ConfigBound
99
+ | TypedConfigBound<ConfigSchema>
100
+ | undefined;
101
+
102
+ if (exportName && exportName !== 'default') {
103
+ configInstance = module[exportName];
104
+ if (!configInstance) {
105
+ const allExports = Object.keys(module).filter(
106
+ (key) => key !== '__esModule'
107
+ );
108
+ const namedExports = allExports.filter((key) => key !== 'default');
109
+ const hasDefault = allExports.includes('default');
110
+ const exportValue = module[exportName];
111
+
112
+ throw new ExportNotFoundException(
113
+ exportName,
114
+ filePath,
115
+ namedExports,
116
+ hasDefault,
117
+ exportValue
118
+ );
119
+ }
120
+ } else if (exportName === 'default' || !exportName) {
121
+ // Try default export first
122
+ const defaultExport = module.default;
123
+
124
+ // If default export is a valid ConfigBound instance, use it
125
+ if (defaultExport && this.isConfigBoundInstance(defaultExport)) {
126
+ configInstance = defaultExport;
127
+ } else if (!exportName) {
128
+ // If no specific export name and default is not a ConfigBound instance,
129
+ // try to find any ConfigBound instance in the file
130
+ const allExports = Object.entries(module).filter(
131
+ ([key]) => key !== '__esModule'
132
+ );
133
+ const configExports = allExports
134
+ .filter(([key]) => key !== 'default')
135
+ .filter(([, value]) => this.isConfigBoundInstance(value));
136
+
137
+ if (configExports.length === 0) {
138
+ const nonConfigExports = allExports
139
+ .filter(([key]) => key !== 'default')
140
+ .filter(([, value]) => !this.isConfigBoundInstance(value))
141
+ .map(([key, value]) => [key, typeof value] as [string, string]);
142
+ const hasDefault = allExports.some(([key]) => key === 'default');
143
+
144
+ throw new NoConfigBoundInstancesException(
145
+ filePath,
146
+ nonConfigExports,
147
+ hasDefault
148
+ );
149
+ }
150
+
151
+ if (configExports.length > 1) {
152
+ const exportNames = configExports.map(([key]) => key);
153
+ throw new MultipleConfigBoundInstancesException(
154
+ filePath,
155
+ exportNames
156
+ );
157
+ }
158
+
159
+ configInstance = configExports[0][1] as
160
+ | ConfigBound
161
+ | TypedConfigBound<ConfigSchema>;
162
+ } else {
163
+ // exportName === 'default' was explicitly requested, so use it even if invalid
164
+ configInstance = defaultExport;
165
+ }
166
+ }
167
+
168
+ if (!configInstance) {
169
+ const targetExport = exportName || 'default';
170
+ const allExports = Object.keys(module).filter(
171
+ (key) => key !== '__esModule'
172
+ );
173
+ const namedExports = allExports.filter((key) => key !== 'default');
174
+ const hasDefault = allExports.includes('default');
175
+
176
+ throw new ExportNotFoundException(
177
+ targetExport,
178
+ filePath,
179
+ namedExports,
180
+ hasDefault,
181
+ module[targetExport]
182
+ );
183
+ }
184
+
185
+ if (!this.isConfigBoundInstance(configInstance)) {
186
+ const targetExport = exportName || 'default';
187
+ const exportValue = module[targetExport];
188
+ const valueType = typeof exportValue;
189
+ const valueDetails =
190
+ exportValue === null
191
+ ? 'null'
192
+ : exportValue === undefined
193
+ ? 'undefined'
194
+ : valueType === 'object'
195
+ ? `${valueType} (missing required properties: name, sections)`
196
+ : valueType;
197
+
198
+ throw new InvalidConfigBoundInstanceException(
199
+ targetExport,
200
+ filePath,
201
+ valueDetails
202
+ );
203
+ }
204
+
205
+ return {
206
+ name: configInstance.name,
207
+ sections: configInstance.sections,
208
+ instance: configInstance
209
+ };
210
+ } catch (error) {
211
+ if (error instanceof ConfigLoaderException) {
212
+ throw error;
213
+ }
214
+ if (error instanceof Error) {
215
+ if (error.message.includes('Cannot find module')) {
216
+ const moduleMatch = error.message.match(
217
+ /Cannot find module ['"]([^'"]+)['"]/
218
+ );
219
+ const missingModule = moduleMatch ? moduleMatch[1] : 'unknown';
220
+ throw new MissingDependencyException(
221
+ filePath,
222
+ missingModule,
223
+ error.message
224
+ );
225
+ }
226
+ if (
227
+ error.message.includes('SyntaxError') ||
228
+ error.message.includes('parse') ||
229
+ error.message.includes('Unexpected token')
230
+ ) {
231
+ throw new ConfigFileParseException(filePath, error.message);
232
+ }
233
+ throw error;
234
+ }
235
+ throw new Error(
236
+ `Failed to load config file: ${filePath}. Unexpected error: ${String(error)}`
237
+ );
238
+ }
239
+ }
240
+
241
+ async loadConfigs(filePath: string): Promise<Map<string, LoadedConfig>> {
242
+ const absolutePath = path.resolve(filePath);
243
+
244
+ if (!fs.existsSync(absolutePath)) {
245
+ const parentDir = path.dirname(absolutePath);
246
+ const parentExists = fs.existsSync(parentDir);
247
+ let similarFiles: string[] | undefined;
248
+ if (parentExists) {
249
+ const fileName = path.basename(absolutePath);
250
+ const dirFiles = fs.readdirSync(parentDir);
251
+ similarFiles = dirFiles.filter((file) =>
252
+ file.toLowerCase().includes(fileName.toLowerCase().slice(0, 3))
253
+ );
254
+ }
255
+ throw new ConfigFileNotFoundException(
256
+ absolutePath,
257
+ parentDir,
258
+ parentExists,
259
+ similarFiles
260
+ );
261
+ }
262
+
263
+ const stat = fs.statSync(absolutePath);
264
+ if (stat.isDirectory()) {
265
+ throw new ConfigFileIsDirectoryException(absolutePath);
266
+ }
267
+
268
+ // If it's a TypeScript file, check if there's a compiled .js version
269
+ const isTypeScript =
270
+ absolutePath.endsWith('.ts') || absolutePath.endsWith('.tsx');
271
+ let fileToLoad = absolutePath;
272
+
273
+ if (isTypeScript) {
274
+ // First, check for .js in the same directory
275
+ const jsPath = absolutePath.replace(/\.tsx?$/, '.js');
276
+ if (fs.existsSync(jsPath)) {
277
+ fileToLoad = jsPath;
278
+ } else {
279
+ // If in src/, check for corresponding file in dist/
280
+ const srcMatch = absolutePath.match(/^(.*[/\\])src([/\\].+)$/);
281
+ if (srcMatch) {
282
+ const distPath =
283
+ srcMatch[1] + 'dist' + srcMatch[2].replace(/\.tsx?$/, '.js');
284
+ if (fs.existsSync(distPath)) {
285
+ fileToLoad = distPath;
286
+ } else {
287
+ throw new ConfigFileParseException(
288
+ filePath,
289
+ `TypeScript file found but no compiled .js file exists. Please compile the file first (e.g., run 'tsc' or 'npm run build'). Checked: ${jsPath} and ${distPath}`
290
+ );
291
+ }
292
+ } else {
293
+ throw new ConfigFileParseException(
294
+ filePath,
295
+ `TypeScript file found but no compiled .js file exists. Please compile the file first (e.g., run 'tsc' or 'npm run build'). Expected compiled file at: ${jsPath}`
296
+ );
297
+ }
298
+ }
299
+ }
300
+
301
+ try {
302
+ const fileURL = pathToFileURL(fileToLoad).href;
303
+ const module = await import(fileURL);
304
+
305
+ const configs = new Map<string, LoadedConfig>();
306
+
307
+ // Check default export
308
+ if (module.default && this.isConfigBoundInstance(module.default)) {
309
+ configs.set('default', {
310
+ name: module.default.name,
311
+ sections: module.default.sections,
312
+ instance: module.default
313
+ });
314
+ }
315
+
316
+ // Check named exports
317
+ for (const [exportName, exportValue] of Object.entries(module)) {
318
+ if (
319
+ exportName !== '__esModule' &&
320
+ exportName !== 'default' &&
321
+ this.isConfigBoundInstance(exportValue)
322
+ ) {
323
+ const typedValue = exportValue as
324
+ | ConfigBound
325
+ | TypedConfigBound<ConfigSchema>;
326
+ configs.set(exportName, {
327
+ name: typedValue.name,
328
+ sections: typedValue.sections,
329
+ instance: typedValue
330
+ });
331
+ }
332
+ }
333
+
334
+ return configs;
335
+ } catch (error) {
336
+ if (error instanceof ConfigLoaderException) {
337
+ throw error;
338
+ }
339
+ if (error instanceof Error) {
340
+ if (error.message.includes('Cannot find module')) {
341
+ const moduleMatch = error.message.match(
342
+ /Cannot find module ['"]([^'"]+)['"]/
343
+ );
344
+ const missingModule = moduleMatch ? moduleMatch[1] : 'unknown';
345
+ throw new MissingDependencyException(
346
+ filePath,
347
+ missingModule,
348
+ error.message
349
+ );
350
+ }
351
+ if (
352
+ error.message.includes('SyntaxError') ||
353
+ error.message.includes('parse') ||
354
+ error.message.includes('Unexpected token')
355
+ ) {
356
+ throw new ConfigFileParseException(filePath, error.message);
357
+ }
358
+ throw error;
359
+ }
360
+ throw new Error(
361
+ `Failed to load config file: ${filePath}. Unexpected error: ${String(error)}`
362
+ );
363
+ }
364
+ }
365
+
366
+ private isConfigBoundInstance(
367
+ value: unknown
368
+ ): value is ConfigBound | TypedConfigBound<ConfigSchema> {
369
+ return (
370
+ value !== null &&
371
+ value !== undefined &&
372
+ typeof value === 'object' &&
373
+ 'name' in value &&
374
+ 'sections' in value &&
375
+ typeof (value as { name: unknown }).name === 'string' &&
376
+ typeof (value as { sections: unknown }).sections === 'object'
377
+ );
378
+ }
379
+ }