entkapp 5.3.1 → 5.4.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.
@@ -111,6 +111,31 @@ export class PluginRegistry {
111
111
  return this.plugins.get(name);
112
112
  }
113
113
 
114
+ /**
115
+ * Version 5.4.0: Collect entry points from all plugins for a given file.
116
+ * @param {string} content - The file content
117
+ * @param {string} filePath - The absolute path to the file
118
+ * @returns {Array<string>} List of detected entry points
119
+ */
120
+ detectEntryPointsFromContent(content, filePath) {
121
+ const entryPoints = [];
122
+ for (const plugin of this.plugins.values()) {
123
+ if (typeof plugin.detectEntryPoints === 'function') {
124
+ try {
125
+ const detected = plugin.detectEntryPoints(content, filePath);
126
+ if (Array.isArray(detected)) {
127
+ entryPoints.push(...detected);
128
+ }
129
+ } catch (e) {
130
+ if (this.context?.verbose) {
131
+ console.warn(`[PluginRegistry] Entry detection failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return entryPoints;
137
+ }
138
+
114
139
  async getActivePlugins(baseDir) {
115
140
  const active = [];
116
141
  for (const plugin of this.plugins.values()) {
@@ -121,6 +146,25 @@ export class PluginRegistry {
121
146
  return active;
122
147
  }
123
148
 
149
+ /**
150
+ * Version 5.3.0: Run content analysis on a node using all active plugins.
151
+ * @param {GraphNode} node - The node to analyze
152
+ * @param {string} filePath - The absolute path to the file
153
+ */
154
+ async runPluginAnalysis(node, filePath) {
155
+ for (const plugin of this.plugins.values()) {
156
+ if (typeof plugin.analyze === 'function') {
157
+ try {
158
+ await plugin.analyze(node, filePath);
159
+ } catch (e) {
160
+ if (this.context?.verbose) {
161
+ console.warn(`[PluginRegistry] Analysis failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
124
168
  /**
125
169
  * Version 5.0.0: Run dependency diagnostics across all registered plugins.
126
170
  * Returns a consolidated list of missing/misplaced dependencies and orphaned configs.
@@ -8,6 +8,7 @@
8
8
  import path from 'path';
9
9
  import fs from 'fs/promises';
10
10
  import { BasePlugin } from '../BasePlugin.js';
11
+ import { FrameworkConfigParser } from '../../resolution/FrameworkConfigParser.js';
11
12
 
12
13
 
13
14
  export class GraphQLPlugin extends BasePlugin {
@@ -211,6 +212,27 @@ export class NuxtPlugin extends BasePlugin {
211
212
  }
212
213
  return false;
213
214
  }
215
+ detectEntryPoints(content, filePath) {
216
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
217
+ const parser = new FrameworkConfigParser(this.context);
218
+ const { entries } = parser.parse(content, filePath);
219
+ return Array.from(entries);
220
+ }
221
+ return [];
222
+ }
223
+ async analyze(node, filePath) {
224
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
225
+ node.isEntry = true;
226
+ if (node.rawCode) {
227
+ const parser = new FrameworkConfigParser(this.context);
228
+ const { aliases, entries } = parser.parse(node.rawCode, filePath);
229
+ if (aliases.size > 0) node.frameworkAliases = aliases;
230
+ if (entries.size > 0) node.frameworkEntries = entries;
231
+ if (node.rawCode.includes('modules:')) node.hasNuxtModules = true;
232
+ if (node.rawCode.includes('components:')) node.hasNuxtComponentsConfig = true;
233
+ }
234
+ }
235
+ }
214
236
  }
215
237
 
216
238
  export class RemixPlugin extends BasePlugin {
@@ -233,6 +255,21 @@ export class RemixPlugin extends BasePlugin {
233
255
  }
234
256
  return false;
235
257
  }
258
+ detectEntryPoints(content, filePath) {
259
+ if (filePath.endsWith('remix.config.js') || filePath.includes('vite.config.')) {
260
+ const parser = new FrameworkConfigParser(this.context);
261
+ const { entries } = parser.parse(content, filePath);
262
+ return Array.from(entries);
263
+ }
264
+ return [];
265
+ }
266
+ async analyze(node, filePath) {
267
+ if (filePath.endsWith('remix.config.js')) {
268
+ node.isEntry = true;
269
+ if (node.rawCode?.includes('ignoredRouteFiles:')) node.hasRemixIgnoredRoutes = true;
270
+ if (node.rawCode?.includes('serverBuildPath:')) node.hasRemixServerBuildPath = true;
271
+ }
272
+ }
236
273
  }
237
274
 
238
275
  export class SvelteKitPlugin extends BasePlugin {
@@ -251,6 +288,29 @@ export class SvelteKitPlugin extends BasePlugin {
251
288
  async isActive(baseDir) {
252
289
  try { await fs.access(path.join(baseDir, 'svelte.config.js')); return true; } catch { return false; }
253
290
  }
291
+ detectEntryPoints(content, filePath) {
292
+ if (filePath.includes('svelte.config.')) {
293
+ const parser = new FrameworkConfigParser(this.context);
294
+ const { entries } = parser.parse(content, filePath);
295
+ return Array.from(entries);
296
+ }
297
+ return [];
298
+ }
299
+ async analyze(node, filePath) {
300
+ if (filePath.includes('svelte.config.')) {
301
+ node.isEntry = true;
302
+ if (node.rawCode) {
303
+ const parser = new FrameworkConfigParser(this.context);
304
+ const { aliases, entries } = parser.parse(node.rawCode, filePath);
305
+ if (aliases.size > 0) {
306
+ node.frameworkAliases = aliases;
307
+ node.hasSvelteAliases = true;
308
+ }
309
+ if (entries.size > 0) node.frameworkEntries = entries;
310
+ if (node.rawCode.includes('adapter:')) node.hasSvelteAdapter = true;
311
+ }
312
+ }
313
+ }
254
314
  }
255
315
 
256
316
  export class AstroPlugin extends BasePlugin {
@@ -265,12 +325,39 @@ export class AstroPlugin extends BasePlugin {
265
325
  }
266
326
  return false;
267
327
  }
328
+ detectEntryPoints(content, filePath) {
329
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
330
+ const parser = new FrameworkConfigParser(this.context);
331
+ const { entries } = parser.parse(content, filePath);
332
+ return Array.from(entries);
333
+ }
334
+ return [];
335
+ }
336
+ async analyze(node, filePath) {
337
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
338
+ node.isEntry = true;
339
+ if (node.rawCode) {
340
+ const parser = new FrameworkConfigParser(this.context);
341
+ const { aliases, entries } = parser.parse(node.rawCode, filePath);
342
+ if (aliases.size > 0) node.frameworkAliases = aliases;
343
+ if (entries.size > 0) node.frameworkEntries = entries;
344
+ if (node.rawCode.includes('integrations:')) node.hasAstroIntegrations = true;
345
+ if (node.rawCode.includes('output:')) node.hasAstroOutputConfig = true;
346
+ }
347
+ }
348
+ }
268
349
  }
269
350
 
270
351
  export class VitepressPlugin extends BasePlugin {
271
352
  get name() { return 'vitepress'; }
272
353
  getConfigFiles() { return ['package.json']; }
273
354
  getRequiredPackages() { return [{ name: 'vitepress', dev: true }]; }
355
+
356
+ getRoutePatterns() {
357
+ // Protection: Every file inside a .vitepress folder is treated as an entry point
358
+ return [/\/\.vitepress\//];
359
+ }
360
+
274
361
  async isActive(baseDir) {
275
362
  try {
276
363
  const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
@@ -282,6 +369,62 @@ export class VitepressPlugin extends BasePlugin {
282
369
  return hasDep;
283
370
  } catch { return false; }
284
371
  }
372
+
373
+ async runDependencyDiagnostics(baseDir) {
374
+ const diagnostics = await super.runDependencyDiagnostics(baseDir);
375
+
376
+ let pkg = null;
377
+ try {
378
+ const raw = await fs.readFile(path.join(baseDir, 'package.json'), 'utf8');
379
+ pkg = JSON.parse(raw);
380
+ } catch { return diagnostics; }
381
+
382
+ const scripts = pkg.scripts || {};
383
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
384
+ const hasVitepressDep = 'vitepress' in allDeps;
385
+
386
+ // Check if any script uses vitepress
387
+ const hasVitepressCommand = Object.values(scripts).some(s => s.includes('vitepress'));
388
+
389
+ // Logic for .vitepress folder existence
390
+ let hasVitepressFolder = false;
391
+ const possibleDirs = ['.vitepress', 'docs/.vitepress', 'docs/docs/.vitepress'];
392
+ for (const d of possibleDirs) {
393
+ try {
394
+ await fs.access(path.join(baseDir, d));
395
+ hasVitepressFolder = true;
396
+ break;
397
+ } catch {}
398
+ }
399
+
400
+ // Scenario 1: build cmd/dependency exists but no folder -> unused dependency
401
+ if ((hasVitepressCommand || hasVitepressDep) && !hasVitepressFolder) {
402
+ diagnostics.push({
403
+ plugin: this.name,
404
+ severity: 'warning',
405
+ message: `Plugin "${this.name}": "vitepress" is defined in package.json or scripts, but no ".vitepress" folder was found. This might be an unused dependency.`
406
+ });
407
+ }
408
+
409
+ // Scenario 2: folder exists but no dependency -> missing dependency
410
+ if (hasVitepressFolder && !hasVitepressDep) {
411
+ diagnostics.push({
412
+ plugin: this.name,
413
+ severity: 'error',
414
+ message: `Plugin "${this.name}": ".vitepress" folder exists, but "vitepress" package is not installed in package.json.`
415
+ });
416
+ }
417
+
418
+ return diagnostics;
419
+ }
420
+
421
+ async analyze(node, filePath) {
422
+ // If it's a config file inside .vitepress, mark it
423
+ if (filePath.includes('/.vitepress/config.') || filePath.includes('/.vitepress/theme/')) {
424
+ node.isEntry = true;
425
+ node.isVitepressInternal = true;
426
+ }
427
+ }
285
428
  }
286
429
 
287
430
  export class GatsbyPlugin extends BasePlugin {
@@ -321,6 +464,14 @@ export class NextJsPlugin extends BasePlugin {
321
464
  }
322
465
  return false;
323
466
  }
467
+ async analyze(node, filePath) {
468
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
469
+ node.isEntry = true;
470
+ if (node.rawCode?.includes('rewrites')) node.hasNextRewrites = true;
471
+ if (node.rawCode?.includes('redirects')) node.hasNextRedirects = true;
472
+ if (node.rawCode?.includes('images:')) node.hasNextImageConfig = true;
473
+ }
474
+ }
324
475
  }
325
476
 
326
477
  export class ReactPlugin extends BasePlugin {
@@ -443,6 +594,42 @@ export class VitePlugin extends BasePlugin {
443
594
  }
444
595
  return false;
445
596
  }
597
+ detectEntryPoints(content, filePath) {
598
+ if (filePath.includes('vite.config.')) {
599
+ const parser = new FrameworkConfigParser(this.context);
600
+ const { entries } = parser.parse(content, filePath);
601
+ return Array.from(entries);
602
+ }
603
+ return [];
604
+ }
605
+ async analyze(node, filePath) {
606
+ if (filePath.includes('vite.config.')) {
607
+ // Mark config as an entry so its own dependencies are tracked
608
+ node.isEntry = true;
609
+
610
+ // UPGRADE: Use FrameworkConfigParser for deep structural extraction
611
+ if (node.rawCode) {
612
+ const parser = new FrameworkConfigParser(this.context);
613
+ const { aliases, entries } = parser.parse(node.rawCode, filePath);
614
+
615
+ if (aliases.size > 0) {
616
+ node.frameworkAliases = aliases;
617
+ node.viteAliasesDetected = true;
618
+ }
619
+
620
+ if (entries.size > 0) {
621
+ node.frameworkEntries = entries;
622
+ node.hasCustomViteInputs = true;
623
+ }
624
+
625
+ // Detect Plugins (Keep existing simple check)
626
+ const pluginsPattern = /plugins\s*:\s*\[([\s\S]*?)\]/g;
627
+ if (pluginsPattern.test(node.rawCode)) {
628
+ node.hasVitePlugins = true;
629
+ }
630
+ }
631
+ }
632
+ }
446
633
  }
447
634
 
448
635
  export class EsbuildPlugin extends BasePlugin {
@@ -479,6 +666,30 @@ export class WebpackPlugin extends BasePlugin {
479
666
  }
480
667
  return false;
481
668
  }
669
+ detectEntryPoints(content, filePath) {
670
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
671
+ const parser = new FrameworkConfigParser(this.context);
672
+ const { entries } = parser.parse(content, filePath);
673
+ return Array.from(entries);
674
+ }
675
+ return [];
676
+ }
677
+ detectEntryPoints(content, filePath) {
678
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
679
+ const parser = new FrameworkConfigParser(this.context);
680
+ const { entries } = parser.parse(content, filePath);
681
+ return Array.from(entries);
682
+ }
683
+ return [];
684
+ }
685
+ async analyze(node, filePath) {
686
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
687
+ node.isEntry = true;
688
+ if (node.rawCode?.includes('entry:')) node.hasWebpackEntries = true;
689
+ if (node.rawCode?.includes('resolve:')) node.hasWebpackResolve = true;
690
+ if (node.rawCode?.includes('plugins:')) node.hasWebpackPlugins = true;
691
+ }
692
+ }
482
693
  }
483
694
 
484
695
  export class ParcelPlugin extends BasePlugin {
@@ -521,6 +732,14 @@ export class TailwindPlugin extends BasePlugin {
521
732
  }
522
733
  return false;
523
734
  }
735
+ async analyze(node, filePath) {
736
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
737
+ node.isEntry = true;
738
+ if (node.rawCode?.includes('content:')) node.hasTailwindContent = true;
739
+ if (node.rawCode?.includes('theme:')) node.hasTailwindTheme = true;
740
+ if (node.rawCode?.includes('plugins:')) node.hasTailwindPlugins = true;
741
+ }
742
+ }
524
743
  }
525
744
 
526
745
  export class PostcssPlugin extends BasePlugin {
@@ -665,6 +884,13 @@ export class JestPlugin extends BasePlugin {
665
884
  async isActive(baseDir) {
666
885
  try { await fs.access(path.join(baseDir, 'jest.config.js')); return true; } catch { return false; }
667
886
  }
887
+ async analyze(node, filePath) {
888
+ if (filePath.endsWith('jest.config.js')) {
889
+ node.isEntry = true;
890
+ if (node.rawCode?.includes('testEnvironment:')) node.hasJestTestEnv = true;
891
+ if (node.rawCode?.includes('setupFiles:')) node.hasJestSetupFiles = true;
892
+ }
893
+ }
668
894
  }
669
895
 
670
896
  export class VitestPlugin extends BasePlugin {
@@ -677,6 +903,39 @@ export class VitestPlugin extends BasePlugin {
677
903
  }
678
904
  return false;
679
905
  }
906
+ detectEntryPoints(content, filePath) {
907
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
908
+ const parser = new FrameworkConfigParser(this.context);
909
+ const { entries } = parser.parse(content, filePath);
910
+ return Array.from(entries);
911
+ }
912
+ return [];
913
+ }
914
+ detectEntryPoints(content, filePath) {
915
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
916
+ const parser = new FrameworkConfigParser(this.context);
917
+ const { entries } = parser.parse(content, filePath);
918
+ return Array.from(entries);
919
+ }
920
+ return [];
921
+ }
922
+ async analyze(node, filePath) {
923
+ if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
924
+ node.isEntry = true;
925
+ if (node.rawCode) {
926
+ const parser = new FrameworkConfigParser(this.context);
927
+ const { aliases, entries } = parser.parse(node.rawCode, filePath);
928
+ if (aliases.size > 0) {
929
+ node.frameworkAliases = aliases;
930
+ node.hasWebpackAliases = true;
931
+ }
932
+ if (entries.size > 0) {
933
+ node.frameworkEntries = entries;
934
+ node.hasWebpackEntries = true;
935
+ }
936
+ }
937
+ }
938
+ }
680
939
  }
681
940
 
682
941
  export class PlaywrightPlugin extends BasePlugin {
@@ -3,73 +3,239 @@ import path from 'path';
3
3
 
4
4
  /**
5
5
  * ConfigLoader
6
- * Loads and merges entkapp configuration from multiple sources:
7
- * - entkapp/config.json (project config)
8
- * - entkapp.config.js / entkapp.config.mjs (JS config)
9
- * - package.json "entkapp" key
10
- * - CLI flags (passed as overrides)
6
+ *
7
+ * Loads and merges entkapp configuration from multiple sources.
8
+ * Supports Monorepo detection for pnpm, npm/yarn/bun, Lerna, Turbo, and Nx.
11
9
  */
12
10
  export class ConfigLoader {
13
- constructor(cwd) {
14
- this.cwd = cwd;
11
+ constructor(cwd) {
12
+ this.cwd = cwd;
15
13
  }
16
14
 
17
15
  async loadConfig(overrides = {}) {
18
16
  let config = this._defaultConfig();
19
17
 
20
- // 1. Try entkapp/config.json
18
+ // 1. entkapp/config.json
21
19
  const jsonConfigPath = path.join(this.cwd, 'entkapp', 'config.json');
22
20
  try {
23
21
  const raw = await fs.readFile(jsonConfigPath, 'utf8');
24
- // Strip comments from JSON (JSONC support)
25
22
  const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
26
- const parsed = JSON.parse(stripped);
27
- config = this._merge(config, parsed);
23
+ config = this._merge(config, JSON.parse(stripped));
28
24
  } catch (e) {}
29
25
 
30
- // 2. Try entkapp.config.js / entkapp.config.mjs
31
- for (const configFile of ['entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
26
+ // 2. entkapp.config.ts / .mjs / .js / .cjs
27
+ for (const configFile of ['entkapp.config.ts', 'entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
32
28
  const jsConfigPath = path.join(this.cwd, configFile);
33
29
  try {
34
30
  const mod = await import(jsConfigPath);
35
31
  const jsConfig = mod.default || mod;
36
- if (typeof jsConfig === 'object') {
32
+ if (typeof jsConfig === 'object' && jsConfig !== null) {
37
33
  config = this._merge(config, jsConfig);
38
34
  break;
39
35
  }
40
36
  } catch (e) {}
41
37
  }
42
38
 
43
- // 3. Try package.json "entkapp" key
44
- const pkgPath = path.join(this.cwd, 'package.json');
39
+ // 3. package.json "entkapp" key
45
40
  try {
46
- const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
41
+ const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, 'package.json'), 'utf8'));
47
42
  if (pkg.entkapp && typeof pkg.entkapp === 'object') {
48
43
  config = this._merge(config, pkg.entkapp);
49
44
  }
50
45
  } catch (e) {}
51
46
 
52
- // 4. Apply CLI overrides
47
+ // 4. Workspace / monorepo packages
48
+ const workspaceData = await this.loadWorkspaceConfigs();
49
+ if (workspaceData.packages.length > 0) {
50
+ config.workspace = true;
51
+ config.workspacePackages = workspaceData.packages;
52
+ config.workspaceTsConfigs = workspaceData.tsConfigs;
53
+ config.workspaceConfigFiles = workspaceData.configFiles;
54
+ config.monorepoConfigs = workspaceData.monorepoConfigs;
55
+ }
56
+
57
+ // 5. CLI overrides
53
58
  config = this._merge(config, overrides);
54
59
 
60
+ // Final sanity check: ensure arrays exist
61
+ if (!Array.isArray(config.entryPoints)) config.entryPoints = [];
62
+ if (!Array.isArray(config.workspacePackages)) config.workspacePackages = [];
63
+ if (!Array.isArray(config.workspaceTsConfigs)) config.workspaceTsConfigs = [];
64
+ if (!Array.isArray(config.workspaceConfigFiles)) config.workspaceConfigFiles = [];
65
+
55
66
  return config;
56
67
  }
57
68
 
69
+ async loadWorkspaceConfigs() {
70
+ const result = {
71
+ packages: [],
72
+ tsConfigs: [],
73
+ configFiles: [],
74
+ monorepoConfigs: {}
75
+ };
76
+
77
+ // Load global configs first
78
+ const globalConfigs = ['turbo.json', 'nx.json', 'lerna.json'];
79
+ for (const file of globalConfigs) {
80
+ try {
81
+ const content = JSON.parse(await fs.readFile(path.join(this.cwd, file), 'utf8'));
82
+ result.monorepoConfigs[file] = content;
83
+ } catch (e) {}
84
+ }
85
+
86
+ const patterns = await this._resolveWorkspacePatterns();
87
+ if (patterns.length === 0) return result;
88
+
89
+ for (const pattern of patterns) {
90
+ const dirs = await this._expandGlob(pattern);
91
+ for (const dir of dirs) {
92
+ await this._readPackageDir(dir, result);
93
+ }
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ async _resolveWorkspacePatterns() {
100
+ const pnpmPatterns = await this._parseWorkspaceYaml('pnpm-workspace.yaml');
101
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
102
+
103
+ try {
104
+ const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, 'package.json'), 'utf8'));
105
+ if (pkg.workspaces) {
106
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
107
+ }
108
+ } catch (e) {}
109
+
110
+ try {
111
+ const lerna = JSON.parse(await fs.readFile(path.join(this.cwd, 'lerna.json'), 'utf8'));
112
+ if (lerna.packages) return lerna.packages;
113
+ } catch (e) {}
114
+
115
+ return this._parseWorkspaceYaml('workspace.yaml');
116
+ }
117
+
118
+ async _parseWorkspaceYaml(fileName) {
119
+ const filePath = path.join(this.cwd, fileName);
120
+ try {
121
+ const raw = await fs.readFile(filePath, 'utf8');
122
+ const patterns = [];
123
+ const lines = raw.split('\n');
124
+ let inPackages = false;
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+ if (!trimmed || trimmed.startsWith('#')) continue;
128
+ if (/^packages\s*:/.test(trimmed)) {
129
+ inPackages = true;
130
+ const inlineMatch = trimmed.match(/^packages\s*:\s*\[([^\]]*)\]/);
131
+ if (inlineMatch) {
132
+ inPackages = false;
133
+ for (const item of inlineMatch[1].split(',')) {
134
+ const val = item.trim().replace(/^['"]|['"]$/g, '');
135
+ if (val) patterns.push(val);
136
+ }
137
+ }
138
+ continue;
139
+ }
140
+ if (inPackages) {
141
+ if (line.match(/^[a-zA-Z0-9_-]/) && trimmed.endsWith(':')) {
142
+ inPackages = false;
143
+ continue;
144
+ }
145
+ if (trimmed.startsWith('-')) {
146
+ const val = trimmed.substring(1).trim().replace(/^['"]|['"]$/g, '');
147
+ if (val) patterns.push(val);
148
+ }
149
+ }
150
+ }
151
+ return patterns;
152
+ } catch (e) {
153
+ return [];
154
+ }
155
+ }
156
+
157
+ async _readPackageDir(dir, result) {
158
+ const normalizedDir = dir.replace(/\\/g, '/');
159
+ let packageName;
160
+
161
+ try {
162
+ const pkg = JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf8'));
163
+ packageName = pkg.name;
164
+ result.packages.push({
165
+ dir: normalizedDir,
166
+ name: packageName,
167
+ entkappConfig: (pkg.entkapp && typeof pkg.entkapp === 'object') ? pkg.entkapp : {}
168
+ });
169
+ } catch (e) {
170
+ // If no package.json, skip as requested in WorkSpaceGraph logic
171
+ return;
172
+ }
173
+
174
+ // tsconfig.json
175
+ try {
176
+ const tsconfigRaw = await fs.readFile(path.join(dir, 'tsconfig.json'), 'utf8');
177
+ const stripped = tsconfigRaw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
178
+ result.tsConfigs.push({ dir: normalizedDir, name: packageName, tsconfig: JSON.parse(stripped) });
179
+ } catch (e) {}
180
+
181
+ // *.config.ts / .js
182
+ try {
183
+ const entries = await fs.readdir(dir, { withFileTypes: true });
184
+ for (const entry of entries) {
185
+ if (!entry.isFile() || !/\.config\.(ts|js|mjs|cjs)$/.test(entry.name)) continue;
186
+ const filePath = path.join(dir, entry.name).replace(/\\/g, '/');
187
+ try {
188
+ const content = await fs.readFile(filePath, 'utf8');
189
+ result.configFiles.push({ dir: normalizedDir, name: packageName, fileName: entry.name, filePath, content });
190
+ } catch (e) {}
191
+ }
192
+ } catch (e) {}
193
+ }
194
+
195
+ async _expandGlob(pattern) {
196
+ const results = [];
197
+ const cleanPattern = pattern.replace(/\/$/, '');
198
+ if (cleanPattern.includes('*')) {
199
+ const parts = cleanPattern.split('/');
200
+ const wildcardIndex = parts.findIndex(p => p.includes('*'));
201
+ const baseDir = path.join(this.cwd, ...parts.slice(0, wildcardIndex));
202
+ try {
203
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
204
+ for (const entry of entries) {
205
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
206
+ const fullPath = path.join(baseDir, entry.name);
207
+ try {
208
+ await fs.access(path.join(fullPath, 'package.json'));
209
+ results.push(fullPath.replace(/\\/g, '/'));
210
+ } catch (e) {}
211
+ }
212
+ }
213
+ } catch (e) {}
214
+ } else {
215
+ const fullPath = path.resolve(this.cwd, cleanPattern);
216
+ try {
217
+ const stat = await fs.stat(fullPath);
218
+ if (stat.isDirectory()) results.push(fullPath.replace(/\\/g, '/'));
219
+ } catch (e) {}
220
+ }
221
+ return results;
222
+ }
223
+
58
224
  _defaultConfig() {
59
225
  return {
60
226
  interface: 'CLI',
61
227
  useBuiltinPlugins: true,
62
228
  useCustomPlugins: true,
63
- options: {
64
- verbose: false,
65
- fastMode: true,
66
- selfHealing: true
67
- },
229
+ options: { verbose: false, fastMode: true, selfHealing: true },
68
230
  enabledPlugins: [],
69
231
  ignoreDependencies: ['entkapp', '@types/*'],
70
232
  exclude: ['node_modules', '.git', 'dist', 'build', 'coverage'],
71
233
  entryPoints: [],
72
- workspace: false
234
+ workspace: false,
235
+ workspacePackages: [],
236
+ workspaceTsConfigs: [],
237
+ workspaceConfigFiles: [],
238
+ monorepoConfigs: {}
73
239
  };
74
240
  }
75
241
 
@@ -79,8 +245,6 @@ export class ConfigLoader {
79
245
  for (const key of Object.keys(override)) {
80
246
  if (key === 'options' && typeof override[key] === 'object') {
81
247
  result.options = { ...(base.options || {}), ...override[key] };
82
- } else if (Array.isArray(override[key])) {
83
- result[key] = override[key];
84
248
  } else {
85
249
  result[key] = override[key];
86
250
  }