@optave/codegraph 1.1.0 → 1.3.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/src/cli.js CHANGED
@@ -1,224 +1,284 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import { buildGraph } from './builder.js';
5
- import { queryName, impactAnalysis, moduleMap, fileDeps, fnDeps, fnImpact, diffImpact } from './queries.js';
6
- import { buildEmbeddings, search, MODELS } from './embedder.js';
7
- import { watchProject } from './watcher.js';
8
- import { exportDOT, exportMermaid, exportJSON } from './export.js';
9
- import { findCycles, formatCycles } from './cycles.js';
10
- import { findDbPath } from './db.js';
11
- import { setVerbose } from './logger.js';
12
- import path from 'path';
13
- import Database from 'better-sqlite3';
14
- import fs from 'fs';
15
-
16
- const program = new Command();
17
- program
18
- .name('codegraph')
19
- .description('Local code dependency graph tool')
20
- .version('1.1.0')
21
- .option('-v, --verbose', 'Enable verbose/debug output')
22
- .hook('preAction', (thisCommand) => {
23
- const opts = thisCommand.opts();
24
- if (opts.verbose) setVerbose(true);
25
- });
26
-
27
- program
28
- .command('build [dir]')
29
- .description('Parse repo and build graph in .codegraph/graph.db')
30
- .option('--no-incremental', 'Force full rebuild (ignore file hashes)')
31
- .action(async (dir, opts) => {
32
- const root = path.resolve(dir || '.');
33
- await buildGraph(root, { incremental: opts.incremental });
34
- });
35
-
36
- program
37
- .command('query <name>')
38
- .description('Find a function/class, show callers and callees')
39
- .option('-d, --db <path>', 'Path to graph.db')
40
- .option('-j, --json', 'Output as JSON')
41
- .action((name, opts) => {
42
- queryName(name, opts.db, { json: opts.json });
43
- });
44
-
45
- program
46
- .command('impact <file>')
47
- .description('Show what depends on this file (transitive)')
48
- .option('-d, --db <path>', 'Path to graph.db')
49
- .option('-j, --json', 'Output as JSON')
50
- .action((file, opts) => {
51
- impactAnalysis(file, opts.db, { json: opts.json });
52
- });
53
-
54
- program
55
- .command('map')
56
- .description('High-level module overview with most-connected nodes')
57
- .option('-d, --db <path>', 'Path to graph.db')
58
- .option('-n, --limit <number>', 'Number of top nodes', '20')
59
- .option('-j, --json', 'Output as JSON')
60
- .action((opts) => {
61
- moduleMap(opts.db, parseInt(opts.limit), { json: opts.json });
62
- });
63
-
64
- program
65
- .command('deps <file>')
66
- .description('Show what this file imports and what imports it')
67
- .option('-d, --db <path>', 'Path to graph.db')
68
- .option('-j, --json', 'Output as JSON')
69
- .action((file, opts) => {
70
- fileDeps(file, opts.db, { json: opts.json });
71
- });
72
-
73
- program
74
- .command('fn <name>')
75
- .description('Function-level dependencies: callers, callees, and transitive call chain')
76
- .option('-d, --db <path>', 'Path to graph.db')
77
- .option('--depth <n>', 'Transitive caller depth', '3')
78
- .option('-T, --no-tests', 'Exclude test/spec files from results')
79
- .option('-j, --json', 'Output as JSON')
80
- .action((name, opts) => {
81
- fnDeps(name, opts.db, { depth: parseInt(opts.depth), noTests: !opts.tests, json: opts.json });
82
- });
83
-
84
- program
85
- .command('fn-impact <name>')
86
- .description('Function-level impact: what functions break if this one changes')
87
- .option('-d, --db <path>', 'Path to graph.db')
88
- .option('--depth <n>', 'Max transitive depth', '5')
89
- .option('-T, --no-tests', 'Exclude test/spec files from results')
90
- .option('-j, --json', 'Output as JSON')
91
- .action((name, opts) => {
92
- fnImpact(name, opts.db, { depth: parseInt(opts.depth), noTests: !opts.tests, json: opts.json });
93
- });
94
-
95
- program
96
- .command('diff-impact [ref]')
97
- .description('Show impact of git changes (unstaged, staged, or vs a ref)')
98
- .option('-d, --db <path>', 'Path to graph.db')
99
- .option('--staged', 'Analyze staged changes instead of unstaged')
100
- .option('--depth <n>', 'Max transitive caller depth', '3')
101
- .option('-T, --no-tests', 'Exclude test/spec files from results')
102
- .option('-j, --json', 'Output as JSON')
103
- .action((ref, opts) => {
104
- diffImpact(opts.db, { ref, staged: opts.staged, depth: parseInt(opts.depth), noTests: !opts.tests, json: opts.json });
105
- });
106
-
107
- // ─── New commands ────────────────────────────────────────────────────────
108
-
109
- program
110
- .command('export')
111
- .description('Export dependency graph as DOT (Graphviz), Mermaid, or JSON')
112
- .option('-d, --db <path>', 'Path to graph.db')
113
- .option('-f, --format <format>', 'Output format: dot, mermaid, json', 'dot')
114
- .option('--functions', 'Function-level graph instead of file-level')
115
- .option('-o, --output <file>', 'Write to file instead of stdout')
116
- .action((opts) => {
117
- const db = new Database(findDbPath(opts.db), { readonly: true });
118
- const exportOpts = { fileLevel: !opts.functions };
119
-
120
- let output;
121
- switch (opts.format) {
122
- case 'mermaid':
123
- output = exportMermaid(db, exportOpts);
124
- break;
125
- case 'json':
126
- output = JSON.stringify(exportJSON(db), null, 2);
127
- break;
128
- case 'dot':
129
- default:
130
- output = exportDOT(db, exportOpts);
131
- break;
132
- }
133
-
134
- db.close();
135
-
136
- if (opts.output) {
137
- fs.writeFileSync(opts.output, output, 'utf-8');
138
- console.log(`Exported ${opts.format} to ${opts.output}`);
139
- } else {
140
- console.log(output);
141
- }
142
- });
143
-
144
- program
145
- .command('cycles')
146
- .description('Detect circular dependencies in the codebase')
147
- .option('-d, --db <path>', 'Path to graph.db')
148
- .option('--functions', 'Function-level cycle detection')
149
- .option('-j, --json', 'Output as JSON')
150
- .action((opts) => {
151
- const db = new Database(findDbPath(opts.db), { readonly: true });
152
- const cycles = findCycles(db, { fileLevel: !opts.functions });
153
- db.close();
154
-
155
- if (opts.json) {
156
- console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2));
157
- } else {
158
- console.log(formatCycles(cycles));
159
- }
160
- });
161
-
162
- program
163
- .command('mcp')
164
- .description('Start MCP (Model Context Protocol) server for AI assistant integration')
165
- .option('-d, --db <path>', 'Path to graph.db')
166
- .action(async (opts) => {
167
- const { startMCPServer } = await import('./mcp.js');
168
- await startMCPServer(opts.db);
169
- });
170
-
171
- // ─── Embedding commands ─────────────────────────────────────────────────
172
-
173
- program
174
- .command('models')
175
- .description('List available embedding models')
176
- .action(() => {
177
- console.log('\nAvailable embedding models:\n');
178
- for (const [key, config] of Object.entries(MODELS)) {
179
- const def = key === 'minilm' ? ' (default)' : '';
180
- console.log(` ${key.padEnd(12)} ${String(config.dim).padStart(4)}d ${config.desc}${def}`);
181
- }
182
- console.log('\nUsage: codegraph embed --model <name>');
183
- console.log(' codegraph search "query" --model <name>\n');
184
- });
185
-
186
- program
187
- .command('embed [dir]')
188
- .description('Build semantic embeddings for all functions/methods/classes (requires prior `build`)')
189
- .option('-m, --model <name>', 'Embedding model: minilm (default), jina-small, jina-base, nomic. Run `codegraph models` for details', 'minilm')
190
- .action(async (dir, opts) => {
191
- const root = path.resolve(dir || '.');
192
- await buildEmbeddings(root, opts.model);
193
- });
194
-
195
- program
196
- .command('search <query>')
197
- .description('Semantic search: find functions by natural language description')
198
- .option('-d, --db <path>', 'Path to graph.db')
199
- .option('-m, --model <name>', 'Override embedding model (auto-detects from DB)')
200
- .option('-n, --limit <number>', 'Max results', '15')
201
- .option('-T, --no-tests', 'Exclude test/spec files')
202
- .option('--min-score <score>', 'Minimum similarity threshold', '0.2')
203
- .option('-k, --kind <kind>', 'Filter by kind: function, method, class')
204
- .option('--file <pattern>', 'Filter by file path pattern')
205
- .action(async (query, opts) => {
206
- await search(query, opts.db, {
207
- limit: parseInt(opts.limit),
208
- noTests: !opts.tests,
209
- minScore: parseFloat(opts.minScore),
210
- model: opts.model,
211
- kind: opts.kind,
212
- filePattern: opts.file
213
- });
214
- });
215
-
216
- program
217
- .command('watch [dir]')
218
- .description('Watch project for file changes and incrementally update the graph')
219
- .action(async (dir) => {
220
- const root = path.resolve(dir || '.');
221
- await watchProject(root);
222
- });
223
-
224
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import Database from 'better-sqlite3';
6
+ import { Command } from 'commander';
7
+ import { buildGraph } from './builder.js';
8
+ import { findCycles, formatCycles } from './cycles.js';
9
+ import { findDbPath } from './db.js';
10
+ import { buildEmbeddings, MODELS, search } from './embedder.js';
11
+ import { exportDOT, exportJSON, exportMermaid } from './export.js';
12
+ import { setVerbose } from './logger.js';
13
+ import {
14
+ diffImpact,
15
+ fileDeps,
16
+ fnDeps,
17
+ fnImpact,
18
+ impactAnalysis,
19
+ moduleMap,
20
+ queryName,
21
+ } from './queries.js';
22
+ import { watchProject } from './watcher.js';
23
+
24
+ const program = new Command();
25
+ program
26
+ .name('codegraph')
27
+ .description('Local code dependency graph tool')
28
+ .version('1.3.0')
29
+ .option('-v, --verbose', 'Enable verbose/debug output')
30
+ .option('--engine <engine>', 'Parser engine: native, wasm, or auto (default: auto)', 'auto')
31
+ .hook('preAction', (thisCommand) => {
32
+ const opts = thisCommand.opts();
33
+ if (opts.verbose) setVerbose(true);
34
+ });
35
+
36
+ program
37
+ .command('build [dir]')
38
+ .description('Parse repo and build graph in .codegraph/graph.db')
39
+ .option('--no-incremental', 'Force full rebuild (ignore file hashes)')
40
+ .action(async (dir, opts) => {
41
+ const root = path.resolve(dir || '.');
42
+ const engine = program.opts().engine;
43
+ await buildGraph(root, { incremental: opts.incremental, engine });
44
+ });
45
+
46
+ program
47
+ .command('query <name>')
48
+ .description('Find a function/class, show callers and callees')
49
+ .option('-d, --db <path>', 'Path to graph.db')
50
+ .option('-j, --json', 'Output as JSON')
51
+ .action((name, opts) => {
52
+ queryName(name, opts.db, { json: opts.json });
53
+ });
54
+
55
+ program
56
+ .command('impact <file>')
57
+ .description('Show what depends on this file (transitive)')
58
+ .option('-d, --db <path>', 'Path to graph.db')
59
+ .option('-j, --json', 'Output as JSON')
60
+ .action((file, opts) => {
61
+ impactAnalysis(file, opts.db, { json: opts.json });
62
+ });
63
+
64
+ program
65
+ .command('map')
66
+ .description('High-level module overview with most-connected nodes')
67
+ .option('-d, --db <path>', 'Path to graph.db')
68
+ .option('-n, --limit <number>', 'Number of top nodes', '20')
69
+ .option('-j, --json', 'Output as JSON')
70
+ .action((opts) => {
71
+ moduleMap(opts.db, parseInt(opts.limit, 10), { json: opts.json });
72
+ });
73
+
74
+ program
75
+ .command('deps <file>')
76
+ .description('Show what this file imports and what imports it')
77
+ .option('-d, --db <path>', 'Path to graph.db')
78
+ .option('-j, --json', 'Output as JSON')
79
+ .action((file, opts) => {
80
+ fileDeps(file, opts.db, { json: opts.json });
81
+ });
82
+
83
+ program
84
+ .command('fn <name>')
85
+ .description('Function-level dependencies: callers, callees, and transitive call chain')
86
+ .option('-d, --db <path>', 'Path to graph.db')
87
+ .option('--depth <n>', 'Transitive caller depth', '3')
88
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
89
+ .option('-j, --json', 'Output as JSON')
90
+ .action((name, opts) => {
91
+ fnDeps(name, opts.db, {
92
+ depth: parseInt(opts.depth, 10),
93
+ noTests: !opts.tests,
94
+ json: opts.json,
95
+ });
96
+ });
97
+
98
+ program
99
+ .command('fn-impact <name>')
100
+ .description('Function-level impact: what functions break if this one changes')
101
+ .option('-d, --db <path>', 'Path to graph.db')
102
+ .option('--depth <n>', 'Max transitive depth', '5')
103
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
104
+ .option('-j, --json', 'Output as JSON')
105
+ .action((name, opts) => {
106
+ fnImpact(name, opts.db, {
107
+ depth: parseInt(opts.depth, 10),
108
+ noTests: !opts.tests,
109
+ json: opts.json,
110
+ });
111
+ });
112
+
113
+ program
114
+ .command('diff-impact [ref]')
115
+ .description('Show impact of git changes (unstaged, staged, or vs a ref)')
116
+ .option('-d, --db <path>', 'Path to graph.db')
117
+ .option('--staged', 'Analyze staged changes instead of unstaged')
118
+ .option('--depth <n>', 'Max transitive caller depth', '3')
119
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
120
+ .option('-j, --json', 'Output as JSON')
121
+ .action((ref, opts) => {
122
+ diffImpact(opts.db, {
123
+ ref,
124
+ staged: opts.staged,
125
+ depth: parseInt(opts.depth, 10),
126
+ noTests: !opts.tests,
127
+ json: opts.json,
128
+ });
129
+ });
130
+
131
+ // ─── New commands ────────────────────────────────────────────────────────
132
+
133
+ program
134
+ .command('export')
135
+ .description('Export dependency graph as DOT (Graphviz), Mermaid, or JSON')
136
+ .option('-d, --db <path>', 'Path to graph.db')
137
+ .option('-f, --format <format>', 'Output format: dot, mermaid, json', 'dot')
138
+ .option('--functions', 'Function-level graph instead of file-level')
139
+ .option('-o, --output <file>', 'Write to file instead of stdout')
140
+ .action((opts) => {
141
+ const db = new Database(findDbPath(opts.db), { readonly: true });
142
+ const exportOpts = { fileLevel: !opts.functions };
143
+
144
+ let output;
145
+ switch (opts.format) {
146
+ case 'mermaid':
147
+ output = exportMermaid(db, exportOpts);
148
+ break;
149
+ case 'json':
150
+ output = JSON.stringify(exportJSON(db), null, 2);
151
+ break;
152
+ default:
153
+ output = exportDOT(db, exportOpts);
154
+ break;
155
+ }
156
+
157
+ db.close();
158
+
159
+ if (opts.output) {
160
+ fs.writeFileSync(opts.output, output, 'utf-8');
161
+ console.log(`Exported ${opts.format} to ${opts.output}`);
162
+ } else {
163
+ console.log(output);
164
+ }
165
+ });
166
+
167
+ program
168
+ .command('cycles')
169
+ .description('Detect circular dependencies in the codebase')
170
+ .option('-d, --db <path>', 'Path to graph.db')
171
+ .option('--functions', 'Function-level cycle detection')
172
+ .option('-j, --json', 'Output as JSON')
173
+ .action((opts) => {
174
+ const db = new Database(findDbPath(opts.db), { readonly: true });
175
+ const cycles = findCycles(db, { fileLevel: !opts.functions });
176
+ db.close();
177
+
178
+ if (opts.json) {
179
+ console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2));
180
+ } else {
181
+ console.log(formatCycles(cycles));
182
+ }
183
+ });
184
+
185
+ program
186
+ .command('mcp')
187
+ .description('Start MCP (Model Context Protocol) server for AI assistant integration')
188
+ .option('-d, --db <path>', 'Path to graph.db')
189
+ .action(async (opts) => {
190
+ const { startMCPServer } = await import('./mcp.js');
191
+ await startMCPServer(opts.db);
192
+ });
193
+
194
+ // ─── Embedding commands ─────────────────────────────────────────────────
195
+
196
+ program
197
+ .command('models')
198
+ .description('List available embedding models')
199
+ .action(() => {
200
+ console.log('\nAvailable embedding models:\n');
201
+ for (const [key, config] of Object.entries(MODELS)) {
202
+ const def = key === 'minilm' ? ' (default)' : '';
203
+ console.log(` ${key.padEnd(12)} ${String(config.dim).padStart(4)}d ${config.desc}${def}`);
204
+ }
205
+ console.log('\nUsage: codegraph embed --model <name>');
206
+ console.log(' codegraph search "query" --model <name>\n');
207
+ });
208
+
209
+ program
210
+ .command('embed [dir]')
211
+ .description(
212
+ 'Build semantic embeddings for all functions/methods/classes (requires prior `build`)',
213
+ )
214
+ .option(
215
+ '-m, --model <name>',
216
+ 'Embedding model: minilm (default), jina-small, jina-base, jina-code, nomic, nomic-v1.5, bge-large. Run `codegraph models` for details',
217
+ 'minilm',
218
+ )
219
+ .action(async (dir, opts) => {
220
+ const root = path.resolve(dir || '.');
221
+ await buildEmbeddings(root, opts.model);
222
+ });
223
+
224
+ program
225
+ .command('search <query>')
226
+ .description('Semantic search: find functions by natural language description')
227
+ .option('-d, --db <path>', 'Path to graph.db')
228
+ .option('-m, --model <name>', 'Override embedding model (auto-detects from DB)')
229
+ .option('-n, --limit <number>', 'Max results', '15')
230
+ .option('-T, --no-tests', 'Exclude test/spec files')
231
+ .option('--min-score <score>', 'Minimum similarity threshold', '0.2')
232
+ .option('-k, --kind <kind>', 'Filter by kind: function, method, class')
233
+ .option('--file <pattern>', 'Filter by file path pattern')
234
+ .option('--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60')
235
+ .action(async (query, opts) => {
236
+ await search(query, opts.db, {
237
+ limit: parseInt(opts.limit, 10),
238
+ noTests: !opts.tests,
239
+ minScore: parseFloat(opts.minScore),
240
+ model: opts.model,
241
+ kind: opts.kind,
242
+ filePattern: opts.file,
243
+ rrfK: parseInt(opts.rrfK, 10),
244
+ });
245
+ });
246
+
247
+ program
248
+ .command('watch [dir]')
249
+ .description('Watch project for file changes and incrementally update the graph')
250
+ .action(async (dir) => {
251
+ const root = path.resolve(dir || '.');
252
+ const engine = program.opts().engine;
253
+ await watchProject(root, { engine });
254
+ });
255
+
256
+ program
257
+ .command('info')
258
+ .description('Show codegraph engine info and diagnostics')
259
+ .action(async () => {
260
+ const { isNativeAvailable, loadNative } = await import('./native.js');
261
+ const { getActiveEngine } = await import('./parser.js');
262
+
263
+ const engine = program.opts().engine;
264
+ const { name: activeName, version: activeVersion } = getActiveEngine({ engine });
265
+ const nativeAvailable = isNativeAvailable();
266
+
267
+ console.log('\nCodegraph Diagnostics');
268
+ console.log('====================');
269
+ console.log(` Version : ${program.version()}`);
270
+ console.log(` Node.js : ${process.version}`);
271
+ console.log(` Platform : ${process.platform}-${process.arch}`);
272
+ console.log(` Native engine : ${nativeAvailable ? 'available' : 'unavailable'}`);
273
+ if (nativeAvailable) {
274
+ const native = loadNative();
275
+ const nativeVersion =
276
+ typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
277
+ console.log(` Native version: ${nativeVersion}`);
278
+ }
279
+ console.log(` Engine flag : --engine ${engine}`);
280
+ console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
281
+ console.log();
282
+ });
283
+
284
+ program.parse();
package/src/config.js CHANGED
@@ -1,55 +1,61 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { debug } from './logger.js';
4
-
5
- export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
6
-
7
- export const DEFAULTS = {
8
- include: [],
9
- exclude: [],
10
- ignoreDirs: [],
11
- extensions: [],
12
- aliases: {},
13
- build: {
14
- incremental: true,
15
- dbPath: '.codegraph/graph.db'
16
- },
17
- query: {
18
- defaultDepth: 3,
19
- defaultLimit: 20
20
- }
21
- };
22
-
23
- /**
24
- * Load project configuration from a .codegraphrc.json or similar file.
25
- * Returns merged config with defaults.
26
- */
27
- export function loadConfig(cwd) {
28
- cwd = cwd || process.cwd();
29
- for (const name of CONFIG_FILES) {
30
- const filePath = path.join(cwd, name);
31
- if (fs.existsSync(filePath)) {
32
- try {
33
- const raw = fs.readFileSync(filePath, 'utf-8');
34
- const config = JSON.parse(raw);
35
- debug(`Loaded config from ${filePath}`);
36
- return mergeConfig(DEFAULTS, config);
37
- } catch (err) {
38
- debug(`Failed to parse config ${filePath}: ${err.message}`);
39
- }
40
- }
41
- }
42
- return { ...DEFAULTS };
43
- }
44
-
45
- function mergeConfig(defaults, overrides) {
46
- const result = { ...defaults };
47
- for (const [key, value] of Object.entries(overrides)) {
48
- if (value && typeof value === 'object' && !Array.isArray(value) && defaults[key] && typeof defaults[key] === 'object') {
49
- result[key] = { ...defaults[key], ...value };
50
- } else {
51
- result[key] = value;
52
- }
53
- }
54
- return result;
55
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { debug } from './logger.js';
4
+
5
+ export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
6
+
7
+ export const DEFAULTS = {
8
+ include: [],
9
+ exclude: [],
10
+ ignoreDirs: [],
11
+ extensions: [],
12
+ aliases: {},
13
+ build: {
14
+ incremental: true,
15
+ dbPath: '.codegraph/graph.db',
16
+ },
17
+ query: {
18
+ defaultDepth: 3,
19
+ defaultLimit: 20,
20
+ },
21
+ };
22
+
23
+ /**
24
+ * Load project configuration from a .codegraphrc.json or similar file.
25
+ * Returns merged config with defaults.
26
+ */
27
+ export function loadConfig(cwd) {
28
+ cwd = cwd || process.cwd();
29
+ for (const name of CONFIG_FILES) {
30
+ const filePath = path.join(cwd, name);
31
+ if (fs.existsSync(filePath)) {
32
+ try {
33
+ const raw = fs.readFileSync(filePath, 'utf-8');
34
+ const config = JSON.parse(raw);
35
+ debug(`Loaded config from ${filePath}`);
36
+ return mergeConfig(DEFAULTS, config);
37
+ } catch (err) {
38
+ debug(`Failed to parse config ${filePath}: ${err.message}`);
39
+ }
40
+ }
41
+ }
42
+ return { ...DEFAULTS };
43
+ }
44
+
45
+ function mergeConfig(defaults, overrides) {
46
+ const result = { ...defaults };
47
+ for (const [key, value] of Object.entries(overrides)) {
48
+ if (
49
+ value &&
50
+ typeof value === 'object' &&
51
+ !Array.isArray(value) &&
52
+ defaults[key] &&
53
+ typeof defaults[key] === 'object'
54
+ ) {
55
+ result[key] = { ...defaults[key], ...value };
56
+ } else {
57
+ result[key] = value;
58
+ }
59
+ }
60
+ return result;
61
+ }