@buoy-design/cli 0.3.28 → 0.3.30

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.
@@ -112,11 +112,20 @@ export declare function applySeverityOverrides(drifts: DriftSignal[], overrides:
112
112
  */
113
113
  export declare class DriftAnalysisService {
114
114
  private config;
115
- constructor(config: BuoyConfig);
115
+ private projectRoot;
116
+ constructor(config: BuoyConfig, projectRoot?: string);
116
117
  /**
117
118
  * Run full drift analysis pipeline
118
119
  */
119
120
  analyze(options?: DriftAnalysisOptions): Promise<DriftAnalysisResult>;
121
+ private getUsageCollectorGlobs;
122
+ private applyTailwindConfigAliasUsages;
123
+ private resolveTailwindSemanticAliasMap;
124
+ private findTailwindConfigFile;
125
+ private extractLocalImportSpecifiers;
126
+ private resolveLocalImport;
127
+ private extractTailwindColorAliasesFromSource;
128
+ private extractTailwindClassTokens;
120
129
  /**
121
130
  * Scan barrel files (index.ts) for re-exports and count re-exported components as used.
122
131
  * Components re-exported from barrel files are part of the public API.
@@ -1 +1 @@
1
- {"version":3,"file":"drift-analysis.d.ts","sourceRoot":"","sources":["../../src/services/drift-analysis.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"drift-analysis.d.ts","sourceRoot":"","sources":["../../src/services/drift-analysis.ts"],"names":[],"mappings":"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"}
@@ -15,7 +15,8 @@ import { detectRepeatedPatterns, checkVariantConsistency, checkExampleCompliance
15
15
  import { glob } from "glob";
16
16
  import { minimatch } from "minimatch";
17
17
  import { readFile } from "fs/promises";
18
- import { resolve } from "path";
18
+ import { existsSync } from "fs";
19
+ import { dirname, extname, resolve } from "path";
19
20
  /**
20
21
  * Severity order for filtering and sorting (0 = lowest, 2 = highest)
21
22
  * Use getSeverityWeight from @buoy-design/core for consistent ordering
@@ -149,6 +150,118 @@ function ruleMatches(d, rule, onWarning) {
149
150
  }
150
151
  return true;
151
152
  }
153
+ function extractTailwindSemanticTokenNameLocal(classToken) {
154
+ const core = classToken.split(":").pop()?.trim() ?? "";
155
+ if (!core)
156
+ return null;
157
+ const normalizedCore = core.replace(/^!/, "").replace(/^-/, "");
158
+ if (!normalizedCore)
159
+ return null;
160
+ const withoutOpacity = normalizedCore.replace(/\/[a-zA-Z0-9.[\]-]+$/, "");
161
+ const match = withoutOpacity.match(/^(?:bg|text|border|ring|fill|stroke|outline|caret|accent|from|via|to)-([a-zA-Z0-9_-]+)$/);
162
+ if (!match?.[1])
163
+ return null;
164
+ const value = match[1];
165
+ if (/^(transparent|current|black|white|inherit)$/.test(value) ||
166
+ /^[a-z]+-\d{2,3}$/.test(value) ||
167
+ /^\[[^\]]+\]$/.test(value)) {
168
+ return null;
169
+ }
170
+ return value;
171
+ }
172
+ function extractNamedObjectBlocks(content, key) {
173
+ const results = [];
174
+ const patterns = [
175
+ new RegExp(`${key}\\s*:\\s*\\{`, "g"),
176
+ new RegExp(`['"]${key}['"]\\s*:\\s*\\{`, "g"),
177
+ ];
178
+ for (const pattern of patterns) {
179
+ let match;
180
+ while ((match = pattern.exec(content)) !== null) {
181
+ const openBraceIndex = match.index + match[0].length - 1;
182
+ const block = extractBalancedBracesContent(content, openBraceIndex);
183
+ if (block != null)
184
+ results.push(block);
185
+ }
186
+ }
187
+ return results;
188
+ }
189
+ function extractBalancedBracesContent(content, openBraceIndex) {
190
+ if (content[openBraceIndex] !== "{")
191
+ return null;
192
+ let depth = 0;
193
+ for (let i = openBraceIndex; i < content.length; i++) {
194
+ const ch = content[i];
195
+ if (ch === "{")
196
+ depth++;
197
+ if (ch === "}")
198
+ depth--;
199
+ if (depth === 0) {
200
+ return content.slice(openBraceIndex + 1, i);
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+ function parseColorObjectAliases(colorsBlock) {
206
+ const aliases = new Map();
207
+ // First parse one-level nested scales (e.g., surface: { DEFAULT: "var(--surface)" ... })
208
+ const nestedPattern = /['"]?([a-zA-Z0-9_-]+)['"]?\s*:\s*\{/g;
209
+ let nestedMatch;
210
+ while ((nestedMatch = nestedPattern.exec(colorsBlock)) !== null) {
211
+ const parent = nestedMatch[1];
212
+ const openBraceIndex = nestedMatch.index + nestedMatch[0].length - 1;
213
+ const nestedContent = extractBalancedBracesContent(colorsBlock, openBraceIndex);
214
+ if (!nestedContent)
215
+ continue;
216
+ const nestedStart = openBraceIndex;
217
+ const nestedEnd = nestedStart + nestedContent.length + 2; // braces
218
+ const kvPattern = /['"]?([a-zA-Z0-9_-]+)['"]?\s*:\s*["'`]([^"'`]+)["'`]/g;
219
+ let kv;
220
+ while ((kv = kvPattern.exec(nestedContent)) !== null) {
221
+ const child = kv[1];
222
+ const value = kv[2];
223
+ const refs = extractCssVarRefs(value);
224
+ if (refs.length === 0)
225
+ continue;
226
+ const semantic = child === "DEFAULT" ? parent : `${parent}-${child}`;
227
+ for (const ref of refs)
228
+ addAlias(aliases, semantic, ref);
229
+ }
230
+ // Skip over nested object contents so top-level string scan doesn't double-process nested entries.
231
+ nestedPattern.lastIndex = nestedEnd;
232
+ }
233
+ // Top-level direct string colors (e.g., background: "var(--surface)")
234
+ const topLevelPattern = /['"]?([a-zA-Z0-9_-]+)['"]?\s*:\s*["'`]([^"'`]+)["'`]/g;
235
+ let top;
236
+ while ((top = topLevelPattern.exec(colorsBlock)) !== null) {
237
+ const semantic = top[1];
238
+ const value = top[2];
239
+ const refs = extractCssVarRefs(value);
240
+ if (refs.length === 0)
241
+ continue;
242
+ for (const ref of refs)
243
+ addAlias(aliases, semantic, ref);
244
+ }
245
+ return aliases;
246
+ }
247
+ function extractCssVarRefs(value) {
248
+ const refs = [];
249
+ const pattern = /var\(\s*--([a-zA-Z0-9_-]+)/g;
250
+ let match;
251
+ while ((match = pattern.exec(value)) !== null) {
252
+ if (match[1])
253
+ refs.push(match[1]);
254
+ }
255
+ return refs;
256
+ }
257
+ function addAlias(map, semantic, tokenName) {
258
+ let bucket = map.get(semantic);
259
+ if (!bucket) {
260
+ bucket = new Set();
261
+ map.set(semantic, bucket);
262
+ }
263
+ bucket.add(tokenName);
264
+ }
152
265
  /**
153
266
  * Apply severity filter to drifts
154
267
  */
@@ -211,8 +324,10 @@ function isEntryPointComponent(component) {
211
324
  */
212
325
  export class DriftAnalysisService {
213
326
  config;
214
- constructor(config) {
327
+ projectRoot;
328
+ constructor(config, projectRoot = process.cwd()) {
215
329
  this.config = config;
330
+ this.projectRoot = projectRoot;
216
331
  }
217
332
  /**
218
333
  * Run full drift analysis pipeline
@@ -221,7 +336,7 @@ export class DriftAnalysisService {
221
336
  const { onProgress, includeIgnored, minSeverity, filterType, cache, checkVariants, checkTokenUtilities, checkExamples, } = options;
222
337
  // Step 1: Scan components
223
338
  onProgress?.("Scanning components...");
224
- const orchestrator = new ScanOrchestrator(this.config, process.cwd(), {
339
+ const orchestrator = new ScanOrchestrator(this.config, this.projectRoot, {
225
340
  cache,
226
341
  });
227
342
  const { components } = await orchestrator.scanComponents({
@@ -245,7 +360,7 @@ export class DriftAnalysisService {
245
360
  // Step 2.2: Check framework sprawl
246
361
  onProgress?.("Checking for framework sprawl...");
247
362
  const { ProjectDetector } = await import("../detect/project-detector.js");
248
- const detector = new ProjectDetector(process.cwd());
363
+ const detector = new ProjectDetector(this.projectRoot);
249
364
  const projectInfo = await detector.detect();
250
365
  if (projectInfo.frameworks.length > 0) {
251
366
  const sprawlDrift = engine.checkFrameworkSprawl(projectInfo.frameworks.map((f) => ({ name: f.name, version: f.version })));
@@ -257,10 +372,13 @@ export class DriftAnalysisService {
257
372
  // Step 2.3: Scan for unused components and tokens
258
373
  onProgress?.("Scanning for unused components and tokens...");
259
374
  const { collectUsages } = await import("@buoy-design/core");
375
+ const usageGlobs = this.getUsageCollectorGlobs();
260
376
  const componentNames = components.map((c) => c.name);
261
377
  const tokenNames = scannedTokens.map((t) => t.name);
262
378
  const usageResult = await collectUsages({
263
- projectRoot: process.cwd(),
379
+ projectRoot: this.projectRoot,
380
+ include: usageGlobs.include,
381
+ exclude: usageGlobs.exclude,
264
382
  knownComponents: componentNames,
265
383
  knownTokens: tokenNames,
266
384
  });
@@ -273,6 +391,9 @@ export class DriftAnalysisService {
273
391
  for (const tu of usageResult.tokenUsages) {
274
392
  tokenUsageMap.set(tu.tokenName, (tokenUsageMap.get(tu.tokenName) || 0) + 1);
275
393
  }
394
+ // Tailwind semantic aliases may map class names to differently named CSS vars
395
+ // via imported presets (e.g., bg-background -> var(--surface)).
396
+ await this.applyTailwindConfigAliasUsages(tokenUsageMap);
276
397
  // Fix 2: Count barrel file re-exports as usage
277
398
  // Components re-exported from barrel files (index.ts) are part of the public API
278
399
  await this.scanBarrelReExports(componentUsageMap);
@@ -348,7 +469,7 @@ export class DriftAnalysisService {
348
469
  if (this.config.sources.tailwind?.enabled) {
349
470
  onProgress?.("Scanning for Tailwind arbitrary values...");
350
471
  const tailwindScanner = new TailwindScanner({
351
- projectRoot: process.cwd(),
472
+ projectRoot: this.projectRoot,
352
473
  include: this.config.sources.tailwind.files,
353
474
  exclude: this.config.sources.tailwind.exclude,
354
475
  detectArbitraryValues: true,
@@ -449,12 +570,195 @@ export class DriftAnalysisService {
449
570
  summary: calculateDriftSummary(drifts),
450
571
  };
451
572
  }
573
+ getUsageCollectorGlobs() {
574
+ const include = new Set();
575
+ const exclude = new Set();
576
+ const addMany = (target, values) => {
577
+ if (!values)
578
+ return;
579
+ for (const value of values) {
580
+ if (value?.trim())
581
+ target.add(value);
582
+ }
583
+ };
584
+ const { react, nextjs, vue, svelte, angular, webcomponent, templates, tailwind, tokens, } = this.config.sources;
585
+ for (const source of [react, nextjs, vue, svelte, angular, webcomponent, templates]) {
586
+ if (!source?.enabled)
587
+ continue;
588
+ addMany(include, source.include);
589
+ addMany(exclude, source.exclude);
590
+ }
591
+ if (tailwind?.enabled) {
592
+ addMany(include, tailwind.files);
593
+ addMany(exclude, tailwind.exclude);
594
+ }
595
+ if (tokens?.enabled) {
596
+ addMany(include, tokens.files);
597
+ }
598
+ return {
599
+ include: include.size > 0 ? [...include] : undefined,
600
+ exclude: exclude.size > 0 ? [...exclude] : undefined,
601
+ };
602
+ }
603
+ async applyTailwindConfigAliasUsages(tokenUsageMap) {
604
+ if (!this.config.sources.tailwind?.enabled)
605
+ return;
606
+ const aliasMap = await this.resolveTailwindSemanticAliasMap();
607
+ if (aliasMap.size === 0)
608
+ return;
609
+ const usageGlobs = this.getUsageCollectorGlobs();
610
+ const include = usageGlobs.include ?? ["**/*.{ts,tsx,js,jsx,vue,svelte,astro,html,mdx,md}"];
611
+ const exclude = usageGlobs.exclude ?? ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"];
612
+ const files = await glob(include, {
613
+ cwd: this.projectRoot,
614
+ ignore: exclude,
615
+ absolute: false,
616
+ nodir: true,
617
+ });
618
+ for (const file of files) {
619
+ let content = "";
620
+ try {
621
+ content = await readFile(resolve(this.projectRoot, file), "utf-8");
622
+ }
623
+ catch {
624
+ continue;
625
+ }
626
+ for (const classToken of this.extractTailwindClassTokens(content)) {
627
+ const semantic = extractTailwindSemanticTokenNameLocal(classToken);
628
+ if (!semantic)
629
+ continue;
630
+ const mapped = aliasMap.get(semantic);
631
+ if (!mapped || mapped.size === 0)
632
+ continue;
633
+ for (const tokenName of mapped) {
634
+ tokenUsageMap.set(tokenName, (tokenUsageMap.get(tokenName) || 0) + 1);
635
+ }
636
+ }
637
+ }
638
+ }
639
+ async resolveTailwindSemanticAliasMap() {
640
+ const configFile = this.findTailwindConfigFile();
641
+ if (!configFile)
642
+ return new Map();
643
+ const visited = new Set();
644
+ const queue = [configFile];
645
+ const aliases = new Map();
646
+ while (queue.length > 0 && visited.size < 64) {
647
+ const filePath = queue.shift();
648
+ if (visited.has(filePath))
649
+ continue;
650
+ visited.add(filePath);
651
+ let content = "";
652
+ try {
653
+ content = await readFile(filePath, "utf-8");
654
+ }
655
+ catch {
656
+ continue;
657
+ }
658
+ this.extractTailwindColorAliasesFromSource(content, aliases);
659
+ for (const specifier of this.extractLocalImportSpecifiers(content)) {
660
+ const resolved = this.resolveLocalImport(filePath, specifier);
661
+ if (resolved && !visited.has(resolved)) {
662
+ queue.push(resolved);
663
+ }
664
+ }
665
+ }
666
+ return aliases;
667
+ }
668
+ findTailwindConfigFile() {
669
+ for (const name of [
670
+ "tailwind.config.ts",
671
+ "tailwind.config.js",
672
+ "tailwind.config.mjs",
673
+ "tailwind.config.cjs",
674
+ ]) {
675
+ const path = resolve(this.projectRoot, name);
676
+ if (existsSync(path))
677
+ return path;
678
+ }
679
+ return null;
680
+ }
681
+ extractLocalImportSpecifiers(content) {
682
+ const out = new Set();
683
+ const patterns = [
684
+ /import[\s\S]*?\sfrom\s*["']([^"']+)["']/g,
685
+ /(?:export\s+\{[^}]*\}\s+from|export\s+\*\s+from)\s*["']([^"']+)["']/g,
686
+ /require\(\s*["']([^"']+)["']\s*\)/g,
687
+ ];
688
+ for (const pattern of patterns) {
689
+ let match;
690
+ while ((match = pattern.exec(content)) !== null) {
691
+ const spec = match[1];
692
+ if (spec?.startsWith("."))
693
+ out.add(spec);
694
+ }
695
+ }
696
+ return [...out];
697
+ }
698
+ resolveLocalImport(fromFile, specifier) {
699
+ const base = resolve(dirname(fromFile), specifier);
700
+ const candidates = extname(base)
701
+ ? [base]
702
+ : [
703
+ base,
704
+ `${base}.ts`,
705
+ `${base}.tsx`,
706
+ `${base}.js`,
707
+ `${base}.mjs`,
708
+ `${base}.cjs`,
709
+ resolve(base, "index.ts"),
710
+ resolve(base, "index.tsx"),
711
+ resolve(base, "index.js"),
712
+ resolve(base, "index.mjs"),
713
+ resolve(base, "index.cjs"),
714
+ ];
715
+ for (const candidate of candidates) {
716
+ if (existsSync(candidate))
717
+ return candidate;
718
+ }
719
+ return null;
720
+ }
721
+ extractTailwindColorAliasesFromSource(content, aliases) {
722
+ for (const colorsBlock of extractNamedObjectBlocks(content, "colors")) {
723
+ const parsed = parseColorObjectAliases(colorsBlock);
724
+ for (const [semantic, tokenNames] of parsed) {
725
+ let bucket = aliases.get(semantic);
726
+ if (!bucket) {
727
+ bucket = new Set();
728
+ aliases.set(semantic, bucket);
729
+ }
730
+ for (const tokenName of tokenNames)
731
+ bucket.add(tokenName);
732
+ }
733
+ }
734
+ }
735
+ extractTailwindClassTokens(content) {
736
+ const classTokens = new Set();
737
+ // Generic string literal scan catches JSX, Astro, template literals, and utility wrappers.
738
+ const stringPattern = /(['"`])((?:\\.|(?!\1)[\s\S])*)\1/g;
739
+ let match;
740
+ while ((match = stringPattern.exec(content)) !== null) {
741
+ const raw = match[2];
742
+ if (!raw || (!raw.includes("-") && !raw.includes(":")))
743
+ continue;
744
+ for (const token of raw.split(/\s+/)) {
745
+ if (token)
746
+ classTokens.add(token);
747
+ }
748
+ }
749
+ // Also include extractor output for nested utility helper patterns.
750
+ for (const extracted of extractStaticClassStrings(content)) {
751
+ for (const token of extracted.classes)
752
+ classTokens.add(token);
753
+ }
754
+ return [...classTokens];
755
+ }
452
756
  /**
453
757
  * Scan barrel files (index.ts) for re-exports and count re-exported components as used.
454
758
  * Components re-exported from barrel files are part of the public API.
455
759
  */
456
760
  async scanBarrelReExports(componentUsageMap) {
457
- const cwd = process.cwd();
761
+ const cwd = this.projectRoot;
458
762
  const barrelFiles = await glob('**/index.{ts,tsx,js,jsx}', {
459
763
  cwd,
460
764
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
@@ -505,7 +809,7 @@ export class DriftAnalysisService {
505
809
  * Handles React.lazy(() => import('./Component')) and next/dynamic patterns.
506
810
  */
507
811
  async scanDynamicImports(componentUsageMap) {
508
- const cwd = process.cwd();
812
+ const cwd = this.projectRoot;
509
813
  const sourceFiles = await glob('**/*.{tsx,jsx,ts,js}', {
510
814
  cwd,
511
815
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.test.*', '**/*.spec.*'],
@@ -542,7 +846,7 @@ export class DriftAnalysisService {
542
846
  async scanTemplateComponentUsage(componentUsageMap, knownComponents) {
543
847
  if (knownComponents.length === 0)
544
848
  return;
545
- const cwd = process.cwd();
849
+ const cwd = this.projectRoot;
546
850
  const templateFiles = await glob('**/*.{vue,svelte,html}', {
547
851
  cwd,
548
852
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
@@ -591,7 +895,7 @@ export class DriftAnalysisService {
591
895
  * Detects app.component('Name', ...) and Vue.component('Name', ...) calls.
592
896
  */
593
897
  async scanAutoRegistration(componentUsageMap) {
594
- const cwd = process.cwd();
898
+ const cwd = this.projectRoot;
595
899
  const sourceFiles = await glob('**/*.{ts,js,tsx,jsx}', {
596
900
  cwd,
597
901
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
@@ -621,7 +925,7 @@ export class DriftAnalysisService {
621
925
  * Components in declarations: [...] are registered and should count as used.
622
926
  */
623
927
  async scanNgModuleDeclarations(componentUsageMap) {
624
- const cwd = process.cwd();
928
+ const cwd = this.projectRoot;
625
929
  const moduleFiles = await glob('**/*.module.ts', {
626
930
  cwd,
627
931
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
@@ -666,7 +970,7 @@ export class DriftAnalysisService {
666
970
  * A component referenced in a .stories file is documented/tested and should count as used.
667
971
  */
668
972
  async scanStoryFileUsages(componentUsageMap) {
669
- const cwd = process.cwd();
973
+ const cwd = this.projectRoot;
670
974
  const storyFiles = await glob('**/*.stories.{ts,tsx,js,jsx}', {
671
975
  cwd,
672
976
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
@@ -712,7 +1016,7 @@ export class DriftAnalysisService {
712
1016
  * mean the component is registered and used by the browser.
713
1017
  */
714
1018
  async scanWebComponentRegistrations(componentUsageMap) {
715
- const cwd = process.cwd();
1019
+ const cwd = this.projectRoot;
716
1020
  const files = await glob('**/*.{ts,js}', {
717
1021
  cwd,
718
1022
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.d.ts', '**/*.spec.*', '**/*.test.*'],
@@ -748,7 +1052,7 @@ export class DriftAnalysisService {
748
1052
  * If we detect Nuxt (nuxt.config.ts exists), mark all components in components/ as used.
749
1053
  */
750
1054
  async scanNuxtAutoImports(componentUsageMap, componentNames) {
751
- const cwd = process.cwd();
1055
+ const cwd = this.projectRoot;
752
1056
  // Check for Nuxt config
753
1057
  const nuxtConfigs = await glob('nuxt.config.{ts,js,mjs}', { cwd, nodir: true });
754
1058
  if (nuxtConfigs.length === 0)
@@ -764,7 +1068,7 @@ export class DriftAnalysisService {
764
1068
  * Components imported in .test.tsx/.spec.tsx are actively maintained/tested.
765
1069
  */
766
1070
  async scanTestFileUsages(componentUsageMap) {
767
- const cwd = process.cwd();
1071
+ const cwd = this.projectRoot;
768
1072
  const testFiles = await glob('**/*.{test,spec}.{ts,tsx,js,jsx}', {
769
1073
  cwd,
770
1074
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
@@ -805,7 +1109,7 @@ export class DriftAnalysisService {
805
1109
  * compound component patterns like Component.Sub = SubComponent.
806
1110
  */
807
1111
  async scanHOCWrapperUsages(componentUsageMap) {
808
- const cwd = process.cwd();
1112
+ const cwd = this.projectRoot;
809
1113
  const sourceFiles = await glob('**/*.{ts,tsx,js,jsx}', {
810
1114
  cwd,
811
1115
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts'],
@@ -878,7 +1182,7 @@ export class DriftAnalysisService {
878
1182
  * all components re-exported from that barrel are the product's public API.
879
1183
  */
880
1184
  async scanPackageExports(componentUsageMap) {
881
- const cwd = process.cwd();
1185
+ const cwd = this.projectRoot;
882
1186
  try {
883
1187
  const pkgContent = await readFile(resolve(cwd, 'package.json'), 'utf-8');
884
1188
  const pkg = JSON.parse(pkgContent);
@@ -949,7 +1253,7 @@ export class DriftAnalysisService {
949
1253
  async scanComponentAsValueUsages(componentUsageMap, knownComponents) {
950
1254
  if (knownComponents.length === 0)
951
1255
  return;
952
- const cwd = process.cwd();
1256
+ const cwd = this.projectRoot;
953
1257
  const sourceFiles = await glob('**/*.{tsx,jsx,ts,js}', {
954
1258
  cwd,
955
1259
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts'],
@@ -987,7 +1291,7 @@ export class DriftAnalysisService {
987
1291
  */
988
1292
  async detectRepeatedPatterns(config) {
989
1293
  const occurrences = [];
990
- const cwd = process.cwd();
1294
+ const cwd = this.projectRoot;
991
1295
  // Find all source files
992
1296
  const patterns = ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte"];
993
1297
  const ignore = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"];