deepdebug-local-agent 0.3.8 → 0.3.10

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.
@@ -0,0 +1,446 @@
1
+ import path from "path";
2
+ import { readFile, exists } from "../fs-utils.js";
3
+ import yaml from "js-yaml";
4
+
5
+ /**
6
+ * ConfigAnalyzer
7
+ *
8
+ * Analyzes application configuration to extract:
9
+ * - Server port
10
+ * - Environment variables
11
+ * - Database connections
12
+ * - API keys and secrets (obfuscated)
13
+ * - Spring profiles
14
+ */
15
+ export class ConfigAnalyzer {
16
+ constructor(workspaceRoot) {
17
+ this.workspaceRoot = workspaceRoot;
18
+ }
19
+
20
+ /**
21
+ * Find all configuration files
22
+ */
23
+ async findConfigFiles() {
24
+ const configPatterns = [
25
+ "application.yml",
26
+ "application.yaml",
27
+ "application.properties",
28
+ "application-dev.yml",
29
+ "application-dev.yaml",
30
+ "application-dev.properties",
31
+ "application-local.yml",
32
+ "application-local.yaml",
33
+ "application-local.properties",
34
+ "application-prod.yml",
35
+ "application-prod.yaml",
36
+ "application-prod.properties",
37
+ "bootstrap.yml",
38
+ "bootstrap.yaml",
39
+ ".env",
40
+ ".env.local",
41
+ ".env.development",
42
+ "config/application.yml",
43
+ "config/application.yaml"
44
+ ];
45
+
46
+ const srcPaths = [
47
+ "src/main/resources/",
48
+ "src/main/java/resources/",
49
+ ""
50
+ ];
51
+
52
+ const foundConfigs = [];
53
+
54
+ for (const srcPath of srcPaths) {
55
+ for (const configPattern of configPatterns) {
56
+ const fullPath = path.join(this.workspaceRoot, srcPath, configPattern);
57
+ if (await exists(fullPath)) {
58
+ foundConfigs.push(path.join(srcPath, configPattern));
59
+ }
60
+ }
61
+ }
62
+
63
+ return foundConfigs;
64
+ }
65
+
66
+ /**
67
+ * Analyze main configuration
68
+ */
69
+ async analyze() {
70
+ const configFiles = await this.findConfigFiles();
71
+ const configs = [];
72
+
73
+ for (const configFile of configFiles) {
74
+ const config = await this.analyzeConfigFile(configFile);
75
+ if (config) {
76
+ configs.push(config);
77
+ }
78
+ }
79
+
80
+ // Merge all configs into a unified view
81
+ const merged = this.mergeConfigs(configs);
82
+
83
+ // Extract server config and add available profiles
84
+ const serverConfig = this.extractServerConfig(merged);
85
+ serverConfig.availableProfiles = this.detectProfiles(configFiles);
86
+
87
+ return {
88
+ files: configFiles,
89
+ configs: configs,
90
+ merged: merged,
91
+ server: serverConfig,
92
+ database: this.extractDatabaseConfig(merged),
93
+ security: this.extractSecurityConfig(merged),
94
+ envVars: this.extractEnvVars(configs)
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Analyze a single config file
100
+ */
101
+ async analyzeConfigFile(configPath) {
102
+ try {
103
+ const fullPath = path.join(this.workspaceRoot, configPath);
104
+ const content = await readFile(fullPath, "utf8");
105
+ const fileName = path.basename(configPath);
106
+
107
+ if (fileName.endsWith(".yml") || fileName.endsWith(".yaml")) {
108
+ return {
109
+ file: configPath,
110
+ type: "yaml",
111
+ content: yaml.load(content) || {}
112
+ };
113
+ } else if (fileName.endsWith(".properties")) {
114
+ return {
115
+ file: configPath,
116
+ type: "properties",
117
+ content: this.parseProperties(content)
118
+ };
119
+ } else if (fileName.startsWith(".env")) {
120
+ return {
121
+ file: configPath,
122
+ type: "env",
123
+ content: this.parseEnvFile(content)
124
+ };
125
+ }
126
+
127
+ return null;
128
+ } catch (err) {
129
+ console.error(`Failed to analyze config ${configPath}:`, err.message);
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Parse .properties file
136
+ */
137
+ parseProperties(content) {
138
+ const result = {};
139
+ const lines = content.split("\n");
140
+
141
+ for (const line of lines) {
142
+ const trimmed = line.trim();
143
+ if (!trimmed || trimmed.startsWith("#")) continue;
144
+
145
+ const eqIndex = trimmed.indexOf("=");
146
+ if (eqIndex > 0) {
147
+ const key = trimmed.substring(0, eqIndex).trim();
148
+ const value = trimmed.substring(eqIndex + 1).trim();
149
+ this.setNestedValue(result, key, value);
150
+ }
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Parse .env file
158
+ */
159
+ parseEnvFile(content) {
160
+ const result = {};
161
+ const lines = content.split("\n");
162
+
163
+ for (const line of lines) {
164
+ const trimmed = line.trim();
165
+ if (!trimmed || trimmed.startsWith("#")) continue;
166
+
167
+ const eqIndex = trimmed.indexOf("=");
168
+ if (eqIndex > 0) {
169
+ const key = trimmed.substring(0, eqIndex).trim();
170
+ let value = trimmed.substring(eqIndex + 1).trim();
171
+ // Remove quotes
172
+ value = value.replace(/^["']|["']$/g, "");
173
+ result[key] = this.obfuscateSensitive(key, value);
174
+ }
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Set nested value from dot notation key
182
+ */
183
+ setNestedValue(obj, key, value) {
184
+ const parts = key.split(".");
185
+ let current = obj;
186
+
187
+ for (let i = 0; i < parts.length - 1; i++) {
188
+ const part = parts[i];
189
+ if (!current[part]) {
190
+ current[part] = {};
191
+ }
192
+ current = current[part];
193
+ }
194
+
195
+ current[parts[parts.length - 1]] = value;
196
+ }
197
+
198
+ /**
199
+ * Merge multiple config files
200
+ */
201
+ mergeConfigs(configs) {
202
+ const merged = {};
203
+
204
+ for (const config of configs) {
205
+ if (config && config.content) {
206
+ this.deepMerge(merged, config.content);
207
+ }
208
+ }
209
+
210
+ return merged;
211
+ }
212
+
213
+ /**
214
+ * Deep merge two objects
215
+ */
216
+ deepMerge(target, source) {
217
+ for (const key in source) {
218
+ if (source[key] instanceof Object && key in target) {
219
+ this.deepMerge(target[key], source[key]);
220
+ } else {
221
+ target[key] = source[key];
222
+ }
223
+ }
224
+ return target;
225
+ }
226
+
227
+ /**
228
+ * Extract server configuration
229
+ */
230
+ extractServerConfig(config) {
231
+ const server = {
232
+ port: 8080, // default
233
+ contextPath: "/",
234
+ ssl: false,
235
+ activeProfile: "dev" // default profile
236
+ };
237
+
238
+ // Spring Boot server config
239
+ if (config.server) {
240
+ server.port = config.server.port || server.port;
241
+ server.contextPath = config.server.servlet?.["context-path"] ||
242
+ config.server["servlet.context-path"] ||
243
+ config.server.contextPath ||
244
+ server.contextPath;
245
+ server.ssl = !!config.server.ssl;
246
+ }
247
+
248
+ // Also check for PORT env var placeholder
249
+ if (typeof server.port === "string" && server.port.includes("${")) {
250
+ const match = server.port.match(/\$\{([^:}]+)(?::(\d+))?\}/);
251
+ if (match) {
252
+ server.portEnvVar = match[1];
253
+ server.port = parseInt(match[2]) || 8080;
254
+ }
255
+ }
256
+
257
+ // Extract active profile from spring.profiles.active
258
+ if (config.spring?.profiles?.active) {
259
+ server.activeProfile = config.spring.profiles.active;
260
+ }
261
+
262
+ return server;
263
+ }
264
+
265
+ /**
266
+ * Extract database configuration
267
+ */
268
+ extractDatabaseConfig(config) {
269
+ const database = {
270
+ type: null,
271
+ host: null,
272
+ port: null,
273
+ name: null,
274
+ configured: false
275
+ };
276
+
277
+ // Spring Boot data source
278
+ const datasource = config.spring?.datasource;
279
+ if (datasource) {
280
+ database.configured = true;
281
+ database.url = this.obfuscateSensitive("url", datasource.url);
282
+
283
+ // Parse URL to extract type
284
+ if (datasource.url) {
285
+ if (datasource.url.includes("mysql")) database.type = "mysql";
286
+ else if (datasource.url.includes("postgresql") || datasource.url.includes("postgres")) database.type = "postgresql";
287
+ else if (datasource.url.includes("mongodb")) database.type = "mongodb";
288
+ else if (datasource.url.includes("h2")) database.type = "h2";
289
+ else if (datasource.url.includes("oracle")) database.type = "oracle";
290
+ else if (datasource.url.includes("sqlserver")) database.type = "sqlserver";
291
+ }
292
+ }
293
+
294
+ // MongoDB
295
+ const mongodb = config.spring?.data?.mongodb;
296
+ if (mongodb) {
297
+ database.configured = true;
298
+ database.type = "mongodb";
299
+ database.uri = this.obfuscateSensitive("uri", mongodb.uri);
300
+ database.database = mongodb.database;
301
+ }
302
+
303
+ // Redis
304
+ const redis = config.spring?.data?.redis || config.spring?.redis;
305
+ if (redis) {
306
+ database.redis = {
307
+ configured: true,
308
+ host: redis.host || "localhost",
309
+ port: redis.port || 6379
310
+ };
311
+ }
312
+
313
+ return database;
314
+ }
315
+
316
+ /**
317
+ * Extract security configuration
318
+ */
319
+ extractSecurityConfig(config) {
320
+ const security = {
321
+ hasAuth: false,
322
+ type: null,
323
+ cors: null
324
+ };
325
+
326
+ // Check for Spring Security
327
+ if (config.spring?.security) {
328
+ security.hasAuth = true;
329
+
330
+ if (config.spring.security.oauth2) {
331
+ security.type = "oauth2";
332
+ } else if (config.spring.security.jwt) {
333
+ security.type = "jwt";
334
+ } else {
335
+ security.type = "basic";
336
+ }
337
+ }
338
+
339
+ // Check for JWT config
340
+ if (config.jwt || config.security?.jwt) {
341
+ security.hasAuth = true;
342
+ security.type = "jwt";
343
+ }
344
+
345
+ return security;
346
+ }
347
+
348
+ /**
349
+ * Extract environment variables referenced in configs
350
+ */
351
+ extractEnvVars(configs) {
352
+ const envVars = new Set();
353
+
354
+ const extractFromObject = (obj) => {
355
+ if (typeof obj === "string") {
356
+ const matches = obj.matchAll(/\$\{([^:}]+)(?::[^}]*)?\}/g);
357
+ for (const match of matches) {
358
+ envVars.add(match[1]);
359
+ }
360
+ } else if (obj && typeof obj === "object") {
361
+ for (const value of Object.values(obj)) {
362
+ extractFromObject(value);
363
+ }
364
+ }
365
+ };
366
+
367
+ for (const config of configs) {
368
+ if (config && config.content) {
369
+ extractFromObject(config.content);
370
+ }
371
+ }
372
+
373
+ return Array.from(envVars);
374
+ }
375
+
376
+ /**
377
+ * Obfuscate sensitive values
378
+ */
379
+ obfuscateSensitive(key, value) {
380
+ if (!value || typeof value !== "string") return value;
381
+
382
+ const sensitivePatterns = [
383
+ "password", "secret", "key", "token", "credential",
384
+ "api_key", "apikey", "auth", "jwt"
385
+ ];
386
+
387
+ const keyLower = key.toLowerCase();
388
+ for (const pattern of sensitivePatterns) {
389
+ if (keyLower.includes(pattern)) {
390
+ if (value.length > 4) {
391
+ return value.substring(0, 2) + "****" + value.substring(value.length - 2);
392
+ }
393
+ return "****";
394
+ }
395
+ }
396
+
397
+ return value;
398
+ }
399
+
400
+ /**
401
+ * Get runtime configuration for starting the server
402
+ */
403
+ async getRuntimeConfig() {
404
+ const analysis = await this.analyze();
405
+
406
+ return {
407
+ port: analysis.server.port,
408
+ contextPath: analysis.server.contextPath,
409
+ envVars: analysis.envVars,
410
+ profiles: this.detectProfiles(analysis.files),
411
+ jvmArgs: this.buildJvmArgs(analysis)
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Detect available Spring profiles
417
+ */
418
+ detectProfiles(configFiles) {
419
+ const profiles = ["default"];
420
+
421
+ for (const file of configFiles) {
422
+ const match = file.match(/application-(\w+)\./);
423
+ if (match && match[1] !== "test") {
424
+ profiles.push(match[1]);
425
+ }
426
+ }
427
+
428
+ return [...new Set(profiles)];
429
+ }
430
+
431
+ /**
432
+ * Build JVM arguments for running the application
433
+ */
434
+ buildJvmArgs(analysis) {
435
+ const args = [];
436
+
437
+ // Add profile if dev or local exists
438
+ if (analysis.files.some(f => f.includes("-dev") || f.includes("-local"))) {
439
+ args.push("-Dspring.profiles.active=dev,local");
440
+ }
441
+
442
+ return args;
443
+ }
444
+ }
445
+
446
+ export default ConfigAnalyzer;