@buenojs/bueno 0.8.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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Configuration file loader for Bueno Framework
3
+ * Uses Bun's native TypeScript loader to import config files
4
+ */
5
+
6
+ import type { BuenoConfig, DeepPartial, UserConfig, UserConfigFn } from "./types";
7
+ import { deepMerge } from "./merge";
8
+
9
+ /**
10
+ * Configuration file search order
11
+ */
12
+ const CONFIG_FILES = [
13
+ "bueno.config.ts",
14
+ "bueno.config.js",
15
+ ".buenorc.ts",
16
+ ".buenorc.js",
17
+ "bueno.config.mjs",
18
+ ];
19
+
20
+ /**
21
+ * Loaded configuration information
22
+ */
23
+ export interface LoadedConfig {
24
+ /** The loaded configuration */
25
+ config: DeepPartial<BuenoConfig>;
26
+ /** Path to the config file that was loaded */
27
+ filePath?: string;
28
+ /** Whether the config was loaded from cache */
29
+ fromCache: boolean;
30
+ }
31
+
32
+ /**
33
+ * Config loader cache
34
+ */
35
+ const configCache = new Map<string, DeepPartial<BuenoConfig>>();
36
+
37
+ /**
38
+ * Check if a file exists
39
+ */
40
+ async function fileExists(path: string): Promise<boolean> {
41
+ try {
42
+ const file = Bun.file(path);
43
+ return await file.exists();
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Find the first existing config file
51
+ */
52
+ export async function findConfigFile(
53
+ cwd?: string,
54
+ ): Promise<string | undefined> {
55
+ const baseDir = cwd ?? process.cwd();
56
+
57
+ for (const file of CONFIG_FILES) {
58
+ const filePath = `${baseDir}/${file}`;
59
+ if (await fileExists(filePath)) {
60
+ return filePath;
61
+ }
62
+ }
63
+
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * Clear the config cache
69
+ */
70
+ export function clearConfigCache(): void {
71
+ configCache.clear();
72
+ }
73
+
74
+ /**
75
+ * Get cached config
76
+ */
77
+ export function getCachedConfig(path: string): DeepPartial<BuenoConfig> | undefined {
78
+ return configCache.get(path);
79
+ }
80
+
81
+ /**
82
+ * Load a configuration file
83
+ * Supports both default exports and named exports
84
+ */
85
+ export async function loadConfigFile<T extends BuenoConfig = BuenoConfig>(
86
+ filePath: string,
87
+ options?: {
88
+ /** Whether to use cache */
89
+ useCache?: boolean;
90
+ /** Additional context to pass to config function */
91
+ context?: Record<string, unknown>;
92
+ },
93
+ ): Promise<LoadedConfig> {
94
+ const useCache = options?.useCache !== false;
95
+
96
+ // Check cache first
97
+ if (useCache) {
98
+ const cached = configCache.get(filePath);
99
+ if (cached) {
100
+ return {
101
+ config: cached,
102
+ filePath,
103
+ fromCache: true,
104
+ };
105
+ }
106
+ }
107
+
108
+ // Check if file exists
109
+ if (!(await fileExists(filePath))) {
110
+ throw new Error(`Config file not found: ${filePath}`);
111
+ }
112
+
113
+ try {
114
+ // Use Bun's native TypeScript loader
115
+ const module = await import(filePath);
116
+
117
+ let config: DeepPartial<T>;
118
+
119
+ // Handle different export styles
120
+ if (typeof module.default === "function") {
121
+ // Function export: export default defineConfig(() => ({ ... }))
122
+ config = await module.default(options?.context);
123
+ } else if (typeof module.default === "object" && module.default !== null) {
124
+ // Object export: export default { ... }
125
+ config = module.default;
126
+ } else if (module.config) {
127
+ // Named export: export const config = { ... }
128
+ config = module.config;
129
+ } else {
130
+ // Try to use the module itself as config
131
+ config = module;
132
+ }
133
+
134
+ // Cache the result
135
+ if (useCache) {
136
+ configCache.set(filePath, config as DeepPartial<BuenoConfig>);
137
+ }
138
+
139
+ return {
140
+ config: config as DeepPartial<BuenoConfig>,
141
+ filePath,
142
+ fromCache: false,
143
+ };
144
+ } catch (error) {
145
+ throw new Error(
146
+ `Failed to load config from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
147
+ );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Load configuration from file
153
+ * Searches for config files in order and loads the first one found
154
+ */
155
+ export async function loadConfig<T extends BuenoConfig = BuenoConfig>(
156
+ options?: {
157
+ /** Custom config file path */
158
+ configPath?: string;
159
+ /** Working directory to search for config */
160
+ cwd?: string;
161
+ /** Whether to use cache */
162
+ useCache?: boolean;
163
+ /** Additional context to pass to config function */
164
+ context?: Record<string, unknown>;
165
+ },
166
+ ): Promise<LoadedConfig> {
167
+ // If a specific path is provided, use it
168
+ if (options?.configPath) {
169
+ return loadConfigFile<T>(options.configPath, {
170
+ useCache: options.useCache,
171
+ context: options.context,
172
+ });
173
+ }
174
+
175
+ // Find config file
176
+ const filePath = await findConfigFile(options?.cwd);
177
+
178
+ if (!filePath) {
179
+ return {
180
+ config: {},
181
+ filePath: undefined,
182
+ fromCache: false,
183
+ };
184
+ }
185
+
186
+ return loadConfigFile<T>(filePath, {
187
+ useCache: options?.useCache,
188
+ context: options?.context,
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Load and merge multiple config files
194
+ * Later files override earlier ones
195
+ */
196
+ export async function loadConfigFiles(
197
+ filePaths: string[],
198
+ options?: {
199
+ /** Whether to use cache */
200
+ useCache?: boolean;
201
+ },
202
+ ): Promise<LoadedConfig> {
203
+ const configs: DeepPartial<BuenoConfig>[] = [];
204
+ let lastFilePath: string | undefined;
205
+
206
+ for (const filePath of filePaths) {
207
+ if (await fileExists(filePath)) {
208
+ const { config } = await loadConfigFile(filePath, options);
209
+ configs.push(config);
210
+ lastFilePath = filePath;
211
+ }
212
+ }
213
+
214
+ const mergedConfig = configs.reduce(
215
+ (acc, config) => deepMerge(acc, config),
216
+ {} as DeepPartial<BuenoConfig>,
217
+ );
218
+
219
+ return {
220
+ config: mergedConfig,
221
+ filePath: lastFilePath,
222
+ fromCache: false,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Watch a config file for changes
228
+ * Returns an unsubscribe function
229
+ */
230
+ export function watchConfig(
231
+ filePath: string,
232
+ callback: (config: DeepPartial<BuenoConfig>) => void,
233
+ options?: {
234
+ /** Debounce time in milliseconds */
235
+ debounce?: number;
236
+ },
237
+ ): () => void {
238
+ let timeout: Timer | undefined;
239
+ const debounce = options?.debounce ?? 100;
240
+
241
+ // Use Bun's file watcher
242
+ const watcher = Bun.file(filePath);
243
+
244
+ // Note: Bun doesn't have a built-in file watcher API yet
245
+ // This is a placeholder for future implementation
246
+ // For now, we'll use a polling approach
247
+
248
+ const interval = setInterval(async () => {
249
+ try {
250
+ // Clear cache to force reload
251
+ configCache.delete(filePath);
252
+ const { config } = await loadConfigFile(filePath, { useCache: false });
253
+
254
+ if (timeout) {
255
+ clearTimeout(timeout);
256
+ }
257
+
258
+ timeout = setTimeout(() => {
259
+ callback(config);
260
+ }, debounce);
261
+ } catch {
262
+ // Ignore errors during watch
263
+ }
264
+ }, 1000);
265
+
266
+ return () => {
267
+ clearInterval(interval);
268
+ if (timeout) {
269
+ clearTimeout(timeout);
270
+ }
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Validate that a config object has the expected structure
276
+ */
277
+ export function validateConfigStructure(
278
+ config: unknown,
279
+ ): config is DeepPartial<BuenoConfig> {
280
+ if (config === null || typeof config !== "object") {
281
+ return false;
282
+ }
283
+
284
+ // Basic validation - check that all top-level keys are valid
285
+ const validKeys = new Set([
286
+ "server",
287
+ "database",
288
+ "cache",
289
+ "logger",
290
+ "health",
291
+ "metrics",
292
+ "telemetry",
293
+ "frontend",
294
+ ]);
295
+
296
+ const cfg = config as Record<string, unknown>;
297
+ for (const key of Object.keys(cfg)) {
298
+ if (!validKeys.has(key)) {
299
+ console.warn(`Unknown config key: ${key}`);
300
+ }
301
+ }
302
+
303
+ return true;
304
+ }
305
+
306
+ /**
307
+ * Extract config file path from CLI args
308
+ */
309
+ export function getConfigPathFromArgs(args: string[] = process.argv): string | undefined {
310
+ const configIndex = args.indexOf("--config");
311
+ if (configIndex !== -1 && args[configIndex + 1]) {
312
+ return args[configIndex + 1];
313
+ }
314
+
315
+ // Also support -c shorthand
316
+ const shortIndex = args.indexOf("-c");
317
+ if (shortIndex !== -1 && args[shortIndex + 1]) {
318
+ return args[shortIndex + 1];
319
+ }
320
+
321
+ return undefined;
322
+ }
323
+
324
+ /**
325
+ * Extract config file path from environment
326
+ */
327
+ export function getConfigPathFromEnv(): string | undefined {
328
+ return Bun.env.BUENO_CONFIG;
329
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Deep merge utilities for configuration
3
+ */
4
+
5
+ import type { BuenoConfig, DeepPartial } from "./types";
6
+
7
+ /**
8
+ * Check if a value is a plain object (not an array, not null, not a class instance)
9
+ */
10
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
11
+ if (value === null || typeof value !== "object") {
12
+ return false;
13
+ }
14
+ const proto = Object.getPrototypeOf(value);
15
+ return proto === null || proto === Object.prototype;
16
+ }
17
+
18
+ /**
19
+ * Check if a value is an object (alias for isPlainObject)
20
+ */
21
+ export function isObject(value: unknown): value is Record<string, unknown> {
22
+ return isPlainObject(value);
23
+ }
24
+
25
+ /**
26
+ * Deep merge two values
27
+ * - Objects are merged recursively
28
+ * - Arrays are concatenated (not merged element-wise)
29
+ * - Primitive values from source override target
30
+ */
31
+ export function deepMerge<T>(target: T, source: DeepPartial<T>): T {
32
+ // Handle null/undefined source
33
+ if (source === null || source === undefined) {
34
+ return target;
35
+ }
36
+
37
+ // Handle null/undefined target
38
+ if (target === null || target === undefined) {
39
+ return source as T;
40
+ }
41
+
42
+ // If source is not an object, return source (override)
43
+ if (!isPlainObject(source)) {
44
+ return source as T;
45
+ }
46
+
47
+ // If target is not an object but source is, return source
48
+ if (!isPlainObject(target)) {
49
+ return source as T;
50
+ }
51
+
52
+ // Both are objects, merge them
53
+ const result = { ...target } as Record<string, unknown>;
54
+ const sourceRecord = source as Record<string, unknown>;
55
+ const targetRecord = target as Record<string, unknown>;
56
+
57
+ for (const key of Object.keys(source)) {
58
+ const sourceValue = sourceRecord[key];
59
+ const targetValue = targetRecord[key];
60
+
61
+ if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
62
+ // Both are objects, merge recursively
63
+ result[key] = deepMerge(targetValue, sourceValue as DeepPartial<typeof targetValue>);
64
+ } else if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
65
+ // Both are arrays, concatenate them
66
+ result[key] = [...targetValue, ...sourceValue];
67
+ } else {
68
+ // Override with source value
69
+ result[key] = sourceValue;
70
+ }
71
+ }
72
+
73
+ return result as T;
74
+ }
75
+
76
+ /**
77
+ * Merge multiple configuration objects
78
+ * Later configs have higher priority
79
+ */
80
+ export function mergeConfigs<T extends BuenoConfig = BuenoConfig>(
81
+ ...configs: (DeepPartial<T> | undefined | null)[]
82
+ ): DeepPartial<T> {
83
+ return configs.reduce<DeepPartial<T>>((acc, config) => {
84
+ if (config === undefined || config === null) {
85
+ return acc;
86
+ }
87
+ // Use unknown as intermediate type to avoid recursive type issues
88
+ return deepMerge(acc as unknown as T, config as unknown as DeepPartial<T>) as unknown as DeepPartial<T>;
89
+ }, {} as DeepPartial<T>);
90
+ }
91
+
92
+ /**
93
+ * Deep clone a configuration object
94
+ */
95
+ export function deepClone<T>(obj: T): T {
96
+ if (obj === null || typeof obj !== "object") {
97
+ return obj;
98
+ }
99
+
100
+ if (Array.isArray(obj)) {
101
+ return obj.map((item) => deepClone(item)) as T;
102
+ }
103
+
104
+ if (isPlainObject(obj)) {
105
+ const result: Record<string, unknown> = {};
106
+ for (const key of Object.keys(obj)) {
107
+ result[key] = deepClone(obj[key]);
108
+ }
109
+ return result as T;
110
+ }
111
+
112
+ // For other types (Date, Map, Set, etc.), return as-is
113
+ return obj;
114
+ }
115
+
116
+ /**
117
+ * Get a value from an object using dot notation
118
+ * @example getNestedValue({ a: { b: { c: 1 } } }, 'a.b.c') // returns 1
119
+ */
120
+ export function getNestedValue<T = unknown>(
121
+ obj: Record<string, unknown>,
122
+ path: string,
123
+ ): T | undefined {
124
+ const keys = path.split(".");
125
+ let current: unknown = obj;
126
+
127
+ for (const key of keys) {
128
+ if (current === null || current === undefined) {
129
+ return undefined;
130
+ }
131
+ if (isPlainObject(current)) {
132
+ current = current[key];
133
+ } else {
134
+ return undefined;
135
+ }
136
+ }
137
+
138
+ return current as T;
139
+ }
140
+
141
+ /**
142
+ * Set a value in an object using dot notation
143
+ * @example setNestedValue({}, 'a.b.c', 1) // returns { a: { b: { c: 1 } } }
144
+ */
145
+ export function setNestedValue(
146
+ obj: Record<string, unknown>,
147
+ path: string,
148
+ value: unknown,
149
+ ): Record<string, unknown> {
150
+ const keys = path.split(".");
151
+ const result = deepClone(obj);
152
+ let current: Record<string, unknown> = result;
153
+
154
+ for (let i = 0; i < keys.length - 1; i++) {
155
+ const key = keys[i];
156
+ if (!isPlainObject(current[key])) {
157
+ current[key] = {};
158
+ }
159
+ current = current[key] as Record<string, unknown>;
160
+ }
161
+
162
+ current[keys[keys.length - 1]] = value;
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * Delete a value from an object using dot notation
168
+ * @example deleteNestedValue({ a: { b: { c: 1 } } }, 'a.b.c') // returns { a: { b: {} } }
169
+ */
170
+ export function deleteNestedValue(
171
+ obj: Record<string, unknown>,
172
+ path: string,
173
+ ): Record<string, unknown> {
174
+ const keys = path.split(".");
175
+ const result = deepClone(obj);
176
+ let current: Record<string, unknown> = result;
177
+
178
+ for (let i = 0; i < keys.length - 1; i++) {
179
+ const key = keys[i];
180
+ if (!isPlainObject(current[key])) {
181
+ return result; // Path doesn't exist, nothing to delete
182
+ }
183
+ current = current[key] as Record<string, unknown>;
184
+ }
185
+
186
+ delete current[keys[keys.length - 1]];
187
+ return result;
188
+ }
189
+
190
+ /**
191
+ * Check if a path exists in an object
192
+ */
193
+ export function hasNestedValue(
194
+ obj: Record<string, unknown>,
195
+ path: string,
196
+ ): boolean {
197
+ return getNestedValue(obj, path) !== undefined;
198
+ }
199
+
200
+ /**
201
+ * Flatten a nested object to dot notation keys
202
+ * @example flattenObject({ a: { b: { c: 1 } } }) // returns { 'a.b.c': 1 }
203
+ */
204
+ export function flattenObject(
205
+ obj: Record<string, unknown>,
206
+ prefix = "",
207
+ ): Record<string, unknown> {
208
+ const result: Record<string, unknown> = {};
209
+
210
+ for (const key of Object.keys(obj)) {
211
+ const newKey = prefix ? `${prefix}.${key}` : key;
212
+ const value = obj[key];
213
+
214
+ if (isPlainObject(value)) {
215
+ Object.assign(result, flattenObject(value, newKey));
216
+ } else {
217
+ result[newKey] = value;
218
+ }
219
+ }
220
+
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * Unflatten a dot notation object to nested object
226
+ * @example unflattenObject({ 'a.b.c': 1 }) // returns { a: { b: { c: 1 } } }
227
+ */
228
+ export function unflattenObject(
229
+ obj: Record<string, unknown>,
230
+ ): Record<string, unknown> {
231
+ const result: Record<string, unknown> = {};
232
+
233
+ for (const key of Object.keys(obj)) {
234
+ const keys = key.split(".");
235
+ let current: Record<string, unknown> = result;
236
+
237
+ for (let i = 0; i < keys.length - 1; i++) {
238
+ const k = keys[i];
239
+ if (!isPlainObject(current[k])) {
240
+ current[k] = {};
241
+ }
242
+ current = current[k] as Record<string, unknown>;
243
+ }
244
+
245
+ current[keys[keys.length - 1]] = obj[key];
246
+ }
247
+
248
+ return result;
249
+ }
250
+
251
+ /**
252
+ * Compare two configurations and return the differences
253
+ */
254
+ export function diffConfigs(
255
+ target: BuenoConfig,
256
+ source: BuenoConfig,
257
+ ): { added: string[]; removed: string[]; changed: string[] } {
258
+ const flatTarget = flattenObject(target as Record<string, unknown>);
259
+ const flatSource = flattenObject(source as Record<string, unknown>);
260
+
261
+ const targetKeys = new Set(Object.keys(flatTarget));
262
+ const sourceKeys = new Set(Object.keys(flatSource));
263
+
264
+ const added: string[] = [];
265
+ const removed: string[] = [];
266
+ const changed: string[] = [];
267
+
268
+ // Find added keys
269
+ for (const key of sourceKeys) {
270
+ if (!targetKeys.has(key)) {
271
+ added.push(key);
272
+ } else if (flatTarget[key] !== flatSource[key]) {
273
+ changed.push(key);
274
+ }
275
+ }
276
+
277
+ // Find removed keys
278
+ for (const key of targetKeys) {
279
+ if (!sourceKeys.has(key)) {
280
+ removed.push(key);
281
+ }
282
+ }
283
+
284
+ return { added, removed, changed };
285
+ }