@justmpm/ai-tool 0.8.2 → 0.9.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.
@@ -577,6 +577,28 @@ function formatImpactText(result) {
577
577
  `;
578
578
  }
579
579
  }
580
+ if (result.gitHistory && result.gitHistory.hasGitRepo) {
581
+ out += `
582
+ \u{1F4DC} HIST\xD3RICO GIT (\xFAltimos ${result.gitHistory.recentCommits.length} commits)
583
+
584
+ `;
585
+ if (result.gitHistory.recentCommits.length === 0) {
586
+ out += ` Arquivo n\xE3o est\xE1 no reposit\xF3rio Git ou sem hist\xF3rico.
587
+ `;
588
+ } else {
589
+ for (const commit of result.gitHistory.recentCommits) {
590
+ const date = commit.date;
591
+ const author = commit.author;
592
+ const hash = commit.shortHash;
593
+ const message = commit.message.length > 60 ? commit.message.substring(0, 60) + "..." : commit.message;
594
+ out += ` ${date} ${author}
595
+ `;
596
+ out += ` ${hash} ${message}
597
+
598
+ `;
599
+ }
600
+ }
601
+ }
580
602
  return out;
581
603
  }
582
604
  function formatSuggestText(result) {
@@ -699,6 +721,19 @@ function formatSuggestText(result) {
699
721
  out += ` \u{1F7E2} Opcionais: ${byPriority.low.length}
700
722
  `;
701
723
  }
724
+ if (result.testSuggestions.length > 0) {
725
+ out += `
726
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
727
+
728
+ `;
729
+ out += `\u{1F9EA} TESTES E VERIFICA\xC7\xD5ES
730
+
731
+ `;
732
+ for (const testSuggestion of result.testSuggestions) {
733
+ out += ` ${testSuggestion}
734
+ `;
735
+ }
736
+ }
702
737
  return out;
703
738
  }
704
739
  function formatContextText(result) {
@@ -1320,6 +1355,85 @@ function clearFirebaseCache() {
1320
1355
  indexContentCache = null;
1321
1356
  }
1322
1357
 
1358
+ // src/cache/schemas.ts
1359
+ import { z } from "zod";
1360
+ var mapResultSchema = z.object({
1361
+ version: z.string(),
1362
+ timestamp: z.string(),
1363
+ cwd: z.string(),
1364
+ summary: z.object({
1365
+ totalFiles: z.number(),
1366
+ totalFolders: z.number(),
1367
+ categories: z.record(z.string(), z.number()).optional()
1368
+ }),
1369
+ folders: z.array(
1370
+ z.object({
1371
+ path: z.string(),
1372
+ depth: z.number(),
1373
+ fileCount: z.number()
1374
+ })
1375
+ ),
1376
+ files: z.array(
1377
+ z.object({
1378
+ path: z.string(),
1379
+ category: z.string(),
1380
+ size: z.number()
1381
+ })
1382
+ ),
1383
+ circularDependencies: z.array(z.string())
1384
+ });
1385
+ var deadResultSchema = z.object({
1386
+ version: z.string(),
1387
+ timestamp: z.string(),
1388
+ cwd: z.string(),
1389
+ summary: z.object({
1390
+ totalDead: z.number(),
1391
+ byType: z.object({
1392
+ files: z.number(),
1393
+ exports: z.number(),
1394
+ dependencies: z.number()
1395
+ })
1396
+ }),
1397
+ files: z.array(
1398
+ z.object({
1399
+ path: z.string(),
1400
+ category: z.string(),
1401
+ type: z.enum(["file", "export", "dependency"])
1402
+ })
1403
+ ),
1404
+ exports: z.array(
1405
+ z.object({
1406
+ file: z.string(),
1407
+ export: z.string()
1408
+ })
1409
+ ),
1410
+ dependencies: z.array(z.string())
1411
+ });
1412
+ var projectIndexSchema = z.object({
1413
+ version: z.string(),
1414
+ timestamp: z.string(),
1415
+ files: z.record(
1416
+ z.object({
1417
+ path: z.string(),
1418
+ category: z.string(),
1419
+ symbols: z.array(z.any()),
1420
+ // SymbolInfo simplificado
1421
+ imports: z.array(
1422
+ z.object({
1423
+ source: z.string(),
1424
+ specifiers: z.array(z.string()),
1425
+ isTypeOnly: z.boolean()
1426
+ })
1427
+ ),
1428
+ exports: z.array(z.string())
1429
+ })
1430
+ ),
1431
+ symbolsByName: z.record(z.array(z.any())),
1432
+ // SymbolInfo[] simplificado
1433
+ fileCount: z.number(),
1434
+ symbolCount: z.number()
1435
+ });
1436
+
1323
1437
  // src/cache/index.ts
1324
1438
  var CACHE_DIR = ".analyze";
1325
1439
  var META_FILE = "meta.json";
@@ -1414,14 +1528,18 @@ function isCacheValid(cwd) {
1414
1528
  return false;
1415
1529
  }
1416
1530
  }
1417
- function readCache(cwd, file) {
1531
+ function readCache(cwd, file, schema) {
1418
1532
  const cachePath = join2(getCacheDir(cwd), file);
1419
1533
  if (!existsSync2(cachePath)) {
1420
1534
  return null;
1421
1535
  }
1422
1536
  try {
1423
- return JSON.parse(readFileSync2(cachePath, "utf-8"));
1424
- } catch {
1537
+ const data = JSON.parse(readFileSync2(cachePath, "utf-8"));
1538
+ return schema ? schema.parse(data) : data;
1539
+ } catch (error) {
1540
+ if (error && typeof error === "object" && "name" in error && error.name === "ZodError") {
1541
+ return null;
1542
+ }
1425
1543
  return null;
1426
1544
  }
1427
1545
  }
@@ -1460,7 +1578,7 @@ function getCachedMapResult(cwd) {
1460
1578
  if (!isCacheValid(cwd)) {
1461
1579
  return null;
1462
1580
  }
1463
- return readCache(cwd, MAP_FILE);
1581
+ return readCache(cwd, MAP_FILE, mapResultSchema);
1464
1582
  }
1465
1583
  function cacheDeadResult(cwd, result) {
1466
1584
  writeCache(cwd, DEAD_FILE, result);
@@ -1469,7 +1587,7 @@ function getCachedDeadResult(cwd) {
1469
1587
  if (!isCacheValid(cwd)) {
1470
1588
  return null;
1471
1589
  }
1472
- return readCache(cwd, DEAD_FILE);
1590
+ return readCache(cwd, DEAD_FILE, deadResultSchema);
1473
1591
  }
1474
1592
  function invalidateCache(cwd) {
1475
1593
  const metaPath = join2(getCacheDir(cwd), META_FILE);
@@ -1489,7 +1607,7 @@ function getCachedSymbolsIndex(cwd) {
1489
1607
  if (!isCacheValid(cwd)) {
1490
1608
  return null;
1491
1609
  }
1492
- return readCache(cwd, SYMBOLS_FILE);
1610
+ return readCache(cwd, SYMBOLS_FILE, projectIndexSchema);
1493
1611
  }
1494
1612
 
1495
1613
  // src/areas/config.ts
@@ -1694,10 +1812,25 @@ function inferFileDescription(filePath, category) {
1694
1812
  return categoryDescriptions[category];
1695
1813
  }
1696
1814
 
1815
+ // src/commands/base.ts
1816
+ function parseCommandOptions(options) {
1817
+ return {
1818
+ cwd: options.cwd || process.cwd(),
1819
+ format: options.format || "text",
1820
+ ...options
1821
+ };
1822
+ }
1823
+ function formatOutput(result, format, textFormatter, cacheMarker) {
1824
+ if (format === "json") {
1825
+ return JSON.stringify(result, null, 2);
1826
+ }
1827
+ const output = textFormatter(result);
1828
+ return cacheMarker ? output + "\n\n\u{1F4E6} (cache)" : output;
1829
+ }
1830
+
1697
1831
  // src/commands/map.ts
1698
1832
  async function map(options = {}) {
1699
- const cwd = options.cwd || process.cwd();
1700
- const format = options.format || "text";
1833
+ const { cwd, format } = parseCommandOptions(options);
1701
1834
  const useCache = options.cache !== false;
1702
1835
  const full = options.full ?? false;
1703
1836
  if (useCache && isCacheValid(cwd)) {
@@ -1843,17 +1976,13 @@ function generateKnipConfig(cwd) {
1843
1976
  return configPath;
1844
1977
  }
1845
1978
  async function dead(options = {}) {
1846
- const cwd = options.cwd || process.cwd();
1847
- const format = options.format || "text";
1979
+ const { cwd, format } = parseCommandOptions(options);
1848
1980
  const useCache = options.cache !== false;
1849
1981
  if (useCache && isCacheValid(cwd)) {
1850
1982
  const cached = getCachedDeadResult(cwd);
1851
1983
  if (cached) {
1852
1984
  const result = { ...cached, timestamp: (/* @__PURE__ */ new Date()).toISOString(), fromCache: true };
1853
- if (format === "json") {
1854
- return JSON.stringify(result, null, 2);
1855
- }
1856
- return formatDeadText(result) + "\n\n\u{1F4E6} (resultado do cache)";
1985
+ return formatOutput(result, format, formatDeadText, true);
1857
1986
  }
1858
1987
  }
1859
1988
  try {
@@ -1936,10 +2065,7 @@ async function dead(options = {}) {
1936
2065
  cacheDeadResult(cwd, result);
1937
2066
  updateCacheMeta(cwd);
1938
2067
  }
1939
- if (format === "json") {
1940
- return JSON.stringify(result, null, 2);
1941
- }
1942
- return formatDeadText(result);
2068
+ return formatOutput(result, format, formatDeadText);
1943
2069
  } catch (error) {
1944
2070
  const message = error instanceof Error ? error.message : String(error);
1945
2071
  throw new Error(`Erro ao executar dead: ${message}`);
@@ -2211,10 +2337,97 @@ function formatInvalidCommand(command) {
2211
2337
  return out;
2212
2338
  }
2213
2339
 
2340
+ // src/utils/file-matcher.ts
2341
+ function findTargetFile(target, allFiles) {
2342
+ const normalizedTarget = target.replace(/\\/g, "/").toLowerCase();
2343
+ if (allFiles.includes(normalizedTarget)) {
2344
+ return normalizedTarget;
2345
+ }
2346
+ const exactMatch = allFiles.find((f) => f.toLowerCase() === normalizedTarget);
2347
+ if (exactMatch) {
2348
+ return exactMatch;
2349
+ }
2350
+ const targetParts = normalizedTarget.split("/");
2351
+ const targetName = targetParts.pop() || "";
2352
+ const targetNameNoExt = targetName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2353
+ const targetDir = targetParts.join("/");
2354
+ const matches = [];
2355
+ for (const file of allFiles) {
2356
+ const fileLower = file.toLowerCase();
2357
+ const fileParts = fileLower.split("/");
2358
+ const fileName = fileParts.pop() || "";
2359
+ const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2360
+ const fileDir = fileParts.join("/");
2361
+ if (fileLower === normalizedTarget) {
2362
+ matches.push({ file, priority: 1 });
2363
+ } else if (fileNameNoExt === targetNameNoExt) {
2364
+ if (targetDir && fileDir.includes(targetDir)) {
2365
+ matches.push({ file, priority: 2 });
2366
+ } else if (targetDir && normalizedTarget.includes(fileDir)) {
2367
+ matches.push({ file, priority: 3 });
2368
+ } else {
2369
+ matches.push({ file, priority: 4 });
2370
+ }
2371
+ } else if (fileLower.includes(normalizedTarget)) {
2372
+ matches.push({ file, priority: 5 });
2373
+ }
2374
+ }
2375
+ if (matches.length === 0) {
2376
+ for (const file of allFiles) {
2377
+ if (file.toLowerCase().includes(targetNameNoExt)) {
2378
+ matches.push({ file, priority: 6 });
2379
+ }
2380
+ }
2381
+ }
2382
+ if (matches.length > 0) {
2383
+ matches.sort((a, b) => a.priority - b.priority);
2384
+ return matches[0].file;
2385
+ }
2386
+ return null;
2387
+ }
2388
+
2389
+ // src/integrations/git.ts
2390
+ import { existsSync as existsSync5 } from "fs";
2391
+ import { execSync as execSync2 } from "child_process";
2392
+ function hasGitRepo(cwd) {
2393
+ return existsSync5(cwd + "/.git");
2394
+ }
2395
+ async function getCommitsForFile(filePath, cwd, limit = 10) {
2396
+ if (!hasGitRepo(cwd)) {
2397
+ return [];
2398
+ }
2399
+ try {
2400
+ const format = "%H|%h|%s|%ad|%an";
2401
+ const cmd = `git log --follow --oneline --date=short --format="${format}" -n ${limit} -- "${filePath}"`;
2402
+ const output = execSync2(cmd, {
2403
+ cwd,
2404
+ encoding: "utf-8",
2405
+ stdio: ["pipe", "pipe", "pipe"]
2406
+ // Silenciar stderr
2407
+ });
2408
+ const commits = [];
2409
+ for (const line of output.trim().split("\n")) {
2410
+ if (!line) continue;
2411
+ const parts = line.split("|");
2412
+ if (parts.length >= 5) {
2413
+ commits.push({
2414
+ hash: parts[0],
2415
+ shortHash: parts[1],
2416
+ message: parts[2],
2417
+ date: parts[3],
2418
+ author: parts[4]
2419
+ });
2420
+ }
2421
+ }
2422
+ return commits;
2423
+ } catch (error) {
2424
+ return [];
2425
+ }
2426
+ }
2427
+
2214
2428
  // src/commands/impact.ts
2215
2429
  async function impact(target, options = {}) {
2216
- const cwd = options.cwd || process.cwd();
2217
- const format = options.format || "text";
2430
+ const { cwd, format } = parseCommandOptions(options);
2218
2431
  const useCache = options.cache !== false;
2219
2432
  if (!target) {
2220
2433
  throw new Error("Target \xE9 obrigat\xF3rio. Exemplo: ai-tool impact src/components/Button.tsx");
@@ -2261,13 +2474,20 @@ async function impact(target, options = {}) {
2261
2474
  }
2262
2475
  const targetPath = findTargetFile(target, allFiles);
2263
2476
  if (!targetPath) {
2264
- return formatNotFound(target, allFiles);
2477
+ return formatFileNotFound({ target, allFiles, command: "impact" });
2265
2478
  }
2266
2479
  const { directUpstream, indirectUpstream, directDownstream, indirectDownstream } = calculateDependencies(targetPath, graph);
2267
2480
  const dependingOn = [...directUpstream, ...indirectUpstream];
2268
2481
  const dependencies = [...directDownstream, ...indirectDownstream];
2269
2482
  const risks = detectRisks(targetPath, dependingOn, dependencies, findCircular);
2270
2483
  const suggestions = generateSuggestions(dependingOn, dependencies, risks);
2484
+ const gitHistory = hasGitRepo(cwd) ? {
2485
+ hasGitRepo: true,
2486
+ recentCommits: await getCommitsForFile(targetPath, cwd, 5)
2487
+ } : {
2488
+ hasGitRepo: false,
2489
+ recentCommits: []
2490
+ };
2271
2491
  const result = {
2272
2492
  version: "1.0.0",
2273
2493
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2284,13 +2504,10 @@ async function impact(target, options = {}) {
2284
2504
  total: dependencies.length
2285
2505
  },
2286
2506
  risks,
2287
- suggestions
2507
+ suggestions,
2508
+ gitHistory
2288
2509
  };
2289
- if (format === "json") {
2290
- return JSON.stringify(result, null, 2);
2291
- }
2292
- const output = formatImpactText(result);
2293
- return fromCache ? output + "\n\n\u{1F4E6} (grafo do cache)" : output;
2510
+ return formatOutput(result, format, formatImpactText, fromCache);
2294
2511
  } catch (error) {
2295
2512
  const message = error instanceof Error ? error.message : String(error);
2296
2513
  throw new Error(`Erro ao executar impact: ${message}`);
@@ -2378,56 +2595,6 @@ function findCircularFromGraph(graph) {
2378
2595
  }
2379
2596
  return cycles;
2380
2597
  }
2381
- function findTargetFile(target, allFiles) {
2382
- const normalizedTarget = target.replace(/\\/g, "/").toLowerCase();
2383
- if (allFiles.includes(normalizedTarget)) {
2384
- return normalizedTarget;
2385
- }
2386
- const exactMatch = allFiles.find((f) => f.toLowerCase() === normalizedTarget);
2387
- if (exactMatch) {
2388
- return exactMatch;
2389
- }
2390
- const targetParts = normalizedTarget.split("/");
2391
- const targetName = targetParts.pop() || "";
2392
- const targetNameNoExt = targetName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2393
- const targetDir = targetParts.join("/");
2394
- const matches = [];
2395
- for (const file of allFiles) {
2396
- const fileLower = file.toLowerCase();
2397
- const fileParts = fileLower.split("/");
2398
- const fileName = fileParts.pop() || "";
2399
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2400
- const fileDir = fileParts.join("/");
2401
- if (fileLower === normalizedTarget) {
2402
- matches.push({ file, priority: 1 });
2403
- } else if (fileNameNoExt === targetNameNoExt) {
2404
- if (targetDir && fileDir.includes(targetDir)) {
2405
- matches.push({ file, priority: 2 });
2406
- } else if (targetDir && normalizedTarget.includes(fileDir)) {
2407
- matches.push({ file, priority: 3 });
2408
- } else {
2409
- matches.push({ file, priority: 4 });
2410
- }
2411
- } else if (fileLower.includes(normalizedTarget)) {
2412
- matches.push({ file, priority: 5 });
2413
- }
2414
- }
2415
- if (matches.length === 0) {
2416
- for (const file of allFiles) {
2417
- if (file.toLowerCase().includes(targetNameNoExt)) {
2418
- matches.push({ file, priority: 6 });
2419
- }
2420
- }
2421
- }
2422
- if (matches.length > 0) {
2423
- matches.sort((a, b) => a.priority - b.priority);
2424
- return matches[0].file;
2425
- }
2426
- return null;
2427
- }
2428
- function formatNotFound(target, allFiles) {
2429
- return formatFileNotFound({ target, allFiles, command: "impact" });
2430
- }
2431
2598
  function toImpactFile(isDirect) {
2432
2599
  return (path) => ({
2433
2600
  path,
@@ -2499,8 +2666,7 @@ function generateSuggestions(upstream, downstream, risks) {
2499
2666
  // src/commands/suggest.ts
2500
2667
  import skott3 from "skott";
2501
2668
  async function suggest(target, options = {}) {
2502
- const cwd = options.cwd || process.cwd();
2503
- const format = options.format || "text";
2669
+ const { cwd, format } = parseCommandOptions(options);
2504
2670
  const useCache = options.cache !== false;
2505
2671
  const limit = options.limit || 10;
2506
2672
  if (!target) {
@@ -2542,23 +2708,21 @@ async function suggest(target, options = {}) {
2542
2708
  updateCacheMeta(cwd);
2543
2709
  }
2544
2710
  }
2545
- const targetPath = findTargetFile2(target, allFiles);
2711
+ const targetPath = findTargetFile(target, allFiles);
2546
2712
  if (!targetPath) {
2547
- return formatNotFound2(target, allFiles);
2713
+ return formatFileNotFound({ target, allFiles, command: "suggest" });
2548
2714
  }
2549
2715
  const suggestions = collectSuggestions(targetPath, graph, allFiles, limit);
2716
+ const testSuggestions = generateTestSuggestions(suggestions, allFiles);
2550
2717
  const result = {
2551
2718
  version: "1.0.0",
2552
2719
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2553
2720
  target: targetPath,
2554
2721
  category: detectCategory(targetPath),
2555
- suggestions
2722
+ suggestions,
2723
+ testSuggestions
2556
2724
  };
2557
- if (format === "json") {
2558
- return JSON.stringify(result, null, 2);
2559
- }
2560
- const output = formatSuggestText(result);
2561
- return fromCache ? output + "\n\n(grafo do cache)" : output;
2725
+ return formatOutput(result, format, formatSuggestText, fromCache);
2562
2726
  } catch (error) {
2563
2727
  const message = error instanceof Error ? error.message : String(error);
2564
2728
  throw new Error(`Erro ao executar suggest: ${message}`);
@@ -2653,53 +2817,6 @@ function findRelatedTests(targetPath, allFiles) {
2653
2817
  }
2654
2818
  return tests;
2655
2819
  }
2656
- function findTargetFile2(target, allFiles) {
2657
- const normalizedTarget = target.replace(/\\/g, "/").toLowerCase();
2658
- if (allFiles.includes(normalizedTarget)) {
2659
- return normalizedTarget;
2660
- }
2661
- const exactMatch = allFiles.find((f) => f.toLowerCase() === normalizedTarget);
2662
- if (exactMatch) {
2663
- return exactMatch;
2664
- }
2665
- const targetParts = normalizedTarget.split("/");
2666
- const targetName = targetParts.pop() || "";
2667
- const targetNameNoExt = targetName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2668
- const targetDir = targetParts.join("/");
2669
- const matches = [];
2670
- for (const file of allFiles) {
2671
- const fileLower = file.toLowerCase();
2672
- const fileParts = fileLower.split("/");
2673
- const fileName = fileParts.pop() || "";
2674
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2675
- const fileDir = fileParts.join("/");
2676
- if (fileLower === normalizedTarget) {
2677
- matches.push({ file, priority: 1 });
2678
- } else if (fileNameNoExt === targetNameNoExt) {
2679
- if (targetDir && fileDir.includes(targetDir)) {
2680
- matches.push({ file, priority: 2 });
2681
- } else if (targetDir && normalizedTarget.includes(fileDir)) {
2682
- matches.push({ file, priority: 3 });
2683
- } else {
2684
- matches.push({ file, priority: 4 });
2685
- }
2686
- } else if (fileLower.includes(normalizedTarget)) {
2687
- matches.push({ file, priority: 5 });
2688
- }
2689
- }
2690
- if (matches.length === 0) {
2691
- for (const file of allFiles) {
2692
- if (file.toLowerCase().includes(targetNameNoExt)) {
2693
- matches.push({ file, priority: 6 });
2694
- }
2695
- }
2696
- }
2697
- if (matches.length > 0) {
2698
- matches.sort((a, b) => a.priority - b.priority);
2699
- return matches[0].file;
2700
- }
2701
- return null;
2702
- }
2703
2820
  function suggestFirebaseRules(targetPath, allFiles) {
2704
2821
  const suggestions = [];
2705
2822
  if (!targetPath.includes("functions/src/")) {
@@ -2757,38 +2874,124 @@ function suggestFirebaseRules(targetPath, allFiles) {
2757
2874
  }
2758
2875
  return suggestions;
2759
2876
  }
2760
- function formatNotFound2(target, allFiles) {
2761
- return formatFileNotFound({ target, allFiles, command: "suggest" });
2877
+ function generateTestSuggestions(suggestions, allFiles) {
2878
+ const testSuggestions = [];
2879
+ const relatedTests = allFiles.filter((f) => {
2880
+ const isTestFile = f.includes(".test.") || f.includes(".spec.") || f.startsWith("tests/") || f.includes("/__tests__/");
2881
+ if (!isTestFile) return false;
2882
+ const testName = f.split("/").pop()?.toLowerCase() || "";
2883
+ for (const suggestion of suggestions) {
2884
+ const suggestedName = suggestion.path.split("/").pop()?.toLowerCase() || "";
2885
+ const suggestedNameNoExt = suggestedName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2886
+ const testNameNoExt = testName.replace(/\.(test|spec)\.(tsx?|jsx?|mjs|cjs)$/, "");
2887
+ if (testNameNoExt.includes(suggestedNameNoExt) || suggestedNameNoExt.includes(testNameNoExt)) {
2888
+ return true;
2889
+ }
2890
+ }
2891
+ return false;
2892
+ });
2893
+ if (relatedTests.length > 0) {
2894
+ const testNames = relatedTests.slice(0, 3).map((f) => f.split("/").pop()).join(", ");
2895
+ testSuggestions.push(`\u{1F9EA} Teste os arquivos modificados: ${testNames}`);
2896
+ if (relatedTests.length > 3) {
2897
+ testSuggestions.push(` ... e mais ${relatedTests.length - 3} teste(s)`);
2898
+ }
2899
+ } else {
2900
+ const hasTestDirectory = allFiles.some((f) => f.startsWith("tests/") || f.includes("/__tests__/"));
2901
+ if (hasTestDirectory) {
2902
+ testSuggestions.push(`\u{1F9EA} Rode a su\xEDte de testes completa: npm test`);
2903
+ } else {
2904
+ testSuggestions.push(`\u26A0\uFE0F Nenhum teste encontrado para os arquivos modificados`);
2905
+ testSuggestions.push(`\u{1F4A1} Considere criar testes para garantir qualidade`);
2906
+ }
2907
+ }
2908
+ const hasCritical = suggestions.some((s) => s.priority === "critical" && s.category === "type");
2909
+ if (hasCritical) {
2910
+ testSuggestions.push(`\u{1F534} Arquivos de tipos modificados - considere adicionar testes de tipagem`);
2911
+ }
2912
+ const hasHighPriority = suggestions.some((s) => s.priority === "high" && s.category === "service");
2913
+ if (hasHighPriority) {
2914
+ testSuggestions.push(`\u{1F7E1} Servi\xE7os modificados - verifique testes de integra\xE7\xE3o`);
2915
+ }
2916
+ return testSuggestions;
2762
2917
  }
2763
2918
 
2764
2919
  // src/commands/context.ts
2765
- import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2920
+ import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2766
2921
  import { join as join6, resolve as resolve2, basename, extname as extname3 } from "path";
2767
2922
 
2768
2923
  // src/ts/extractor.ts
2769
2924
  import { Project, SyntaxKind } from "ts-morph";
2770
- function createProject(cwd) {
2771
- return new Project({
2772
- tsConfigFilePath: `${cwd}/tsconfig.json`,
2773
- skipAddingFilesFromTsConfig: true
2774
- });
2925
+
2926
+ // src/ts/utils.ts
2927
+ function simplifyType(typeText) {
2928
+ if (!typeText) return "unknown";
2929
+ let simplified = typeText.replace(/import\([^)]+\)\./g, "");
2930
+ if (simplified.length > 80) {
2931
+ simplified = simplified.slice(0, 77) + "...";
2932
+ }
2933
+ return simplified;
2775
2934
  }
2776
- function addSourceFile(project, filePath) {
2777
- return project.addSourceFileAtPath(filePath);
2935
+ function safeGetTypeText(getTypeFn) {
2936
+ try {
2937
+ const type = getTypeFn();
2938
+ if (!type) return "unknown";
2939
+ return type.getText();
2940
+ } catch {
2941
+ return "unknown";
2942
+ }
2778
2943
  }
2779
- function extractImports(sourceFile) {
2780
- const imports = [];
2781
- for (const importDecl of sourceFile.getImportDeclarations()) {
2782
- const specifiers = [];
2783
- const defaultImport = importDecl.getDefaultImport();
2784
- if (defaultImport) {
2785
- specifiers.push(defaultImport.getText());
2786
- }
2787
- for (const namedImport of importDecl.getNamedImports()) {
2788
- const alias = namedImport.getAliasNode();
2789
- if (alias) {
2790
- specifiers.push(`${namedImport.getName()} as ${alias.getText()}`);
2791
- } else {
2944
+ function formatInterfaceDefinition(iface) {
2945
+ const parts = [];
2946
+ const extendsClauses = iface.getExtends();
2947
+ if (extendsClauses.length > 0) {
2948
+ parts.push(
2949
+ `extends ${extendsClauses.map((e) => e.getText()).join(", ")}`
2950
+ );
2951
+ }
2952
+ const props = iface.getProperties();
2953
+ for (const prop of props.slice(0, 10)) {
2954
+ const propType = simplifyType(safeGetTypeText(() => prop.getType()));
2955
+ parts.push(`${prop.getName()}: ${propType}`);
2956
+ }
2957
+ if (props.length > 10) {
2958
+ parts.push(`... +${props.length - 10} props`);
2959
+ }
2960
+ if (iface.getMethods) {
2961
+ const methods = iface.getMethods();
2962
+ for (const method of methods) {
2963
+ const returnType = simplifyType(
2964
+ safeGetTypeText(() => method.getReturnType())
2965
+ );
2966
+ parts.push(`${method.getName()}(): ${returnType}`);
2967
+ }
2968
+ }
2969
+ return parts.length > 0 ? `{ ${parts.join("; ")} }` : "{}";
2970
+ }
2971
+
2972
+ // src/ts/extractor.ts
2973
+ function createProject(cwd) {
2974
+ return new Project({
2975
+ tsConfigFilePath: `${cwd}/tsconfig.json`,
2976
+ skipAddingFilesFromTsConfig: true
2977
+ });
2978
+ }
2979
+ function addSourceFile(project, filePath) {
2980
+ return project.addSourceFileAtPath(filePath);
2981
+ }
2982
+ function extractImports(sourceFile) {
2983
+ const imports = [];
2984
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2985
+ const specifiers = [];
2986
+ const defaultImport = importDecl.getDefaultImport();
2987
+ if (defaultImport) {
2988
+ specifiers.push(defaultImport.getText());
2989
+ }
2990
+ for (const namedImport of importDecl.getNamedImports()) {
2991
+ const alias = namedImport.getAliasNode();
2992
+ if (alias) {
2993
+ specifiers.push(`${namedImport.getName()} as ${alias.getText()}`);
2994
+ } else {
2792
2995
  specifiers.push(namedImport.getName());
2793
2996
  }
2794
2997
  }
@@ -2824,13 +3027,6 @@ function extractParams(params) {
2824
3027
  type: simplifyType(p.getType().getText())
2825
3028
  }));
2826
3029
  }
2827
- function simplifyType(typeText) {
2828
- let simplified = typeText.replace(/import\([^)]+\)\./g, "");
2829
- if (simplified.length > 80) {
2830
- simplified = simplified.slice(0, 77) + "...";
2831
- }
2832
- return simplified;
2833
- }
2834
3030
  function extractFunctions(sourceFile) {
2835
3031
  const functions2 = [];
2836
3032
  for (const func of sourceFile.getFunctions()) {
@@ -2909,24 +3105,6 @@ function extractTypes(sourceFile) {
2909
3105
  }
2910
3106
  return types;
2911
3107
  }
2912
- function formatInterfaceDefinition(iface) {
2913
- const parts = [];
2914
- const extendsClauses = iface.getExtends();
2915
- if (extendsClauses.length > 0) {
2916
- parts.push(`extends ${extendsClauses.map((e) => e.getText()).join(", ")}`);
2917
- }
2918
- const props = iface.getProperties();
2919
- for (const prop of props) {
2920
- const propType = simplifyType(prop.getType().getText());
2921
- parts.push(`${prop.getName()}: ${propType}`);
2922
- }
2923
- const methods = iface.getMethods();
2924
- for (const method of methods) {
2925
- const returnType = simplifyType(method.getReturnType().getText());
2926
- parts.push(`${method.getName()}(): ${returnType}`);
2927
- }
2928
- return parts.length > 0 ? `{ ${parts.join("; ")} }` : "{}";
2929
- }
2930
3108
  function extractExports(sourceFile) {
2931
3109
  const exports = [];
2932
3110
  for (const exportDecl of sourceFile.getExportDeclarations()) {
@@ -2969,23 +3147,53 @@ function extractExports(sourceFile) {
2969
3147
  return [...new Set(exports)];
2970
3148
  }
2971
3149
 
2972
- // src/ts/indexer.ts
3150
+ // src/ts/cache.ts
3151
+ import { resolve } from "path";
3152
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
3153
+
3154
+ // src/ts/index.ts
2973
3155
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2974
- import { join as join5, extname as extname2, resolve } from "path";
2975
- import { Project as Project2, SyntaxKind as SyntaxKind2, Node as Node2 } from "ts-morph";
2976
- var CODE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
2977
- var DEBUG = process.env.DEBUG_ANALYZE === "true";
2978
- var DEBUG_FUNCTIONS = process.env.DEBUG_FUNCTIONS === "true" || DEBUG;
2979
- function debugLog(...args) {
2980
- if (DEBUG) {
2981
- console.error("[analyze:debug]", ...args);
2982
- }
2983
- }
2984
- function debugFunctions(...args) {
2985
- if (DEBUG_FUNCTIONS) {
2986
- console.error("[functions:debug]", ...args);
3156
+ import { join as join5, extname as extname2 } from "path";
3157
+ import { Project as Project2 } from "ts-morph";
3158
+
3159
+ // src/utils/logger.ts
3160
+ var Logger = class {
3161
+ /** Flag de debug geral - ativada via DEBUG_ANALYZE=true */
3162
+ _debug = process.env.DEBUG_ANALYZE === "true";
3163
+ /** Flag de debug para Cloud Functions - ativada via DEBUG_FUNCTIONS=true ou DEBUG_ANALYZE=true */
3164
+ _debugFunctions = process.env.DEBUG_FUNCTIONS === "true" || this._debug;
3165
+ /**
3166
+ * Log de debug geral
3167
+ *
3168
+ * Só produz output quando DEBUG_ANALYZE=true
3169
+ *
3170
+ * @param args - Argumentos para log (qualquer tipo)
3171
+ * @example
3172
+ * logger.debug("Indexando", files.length, "arquivos");
3173
+ */
3174
+ debug(...args) {
3175
+ if (this._debug) {
3176
+ console.error("[analyze:debug]", ...args);
3177
+ }
3178
+ }
3179
+ /**
3180
+ * Log de debug para Cloud Functions
3181
+ *
3182
+ * Só produz output quando DEBUG_FUNCTIONS=true ou DEBUG_ANALYZE=true
3183
+ *
3184
+ * @param args - Argumentos para log (qualquer tipo)
3185
+ * @example
3186
+ * logger.debugFunctions("Trigger detectado:", triggerName);
3187
+ */
3188
+ debugFunctions(...args) {
3189
+ if (this._debugFunctions) {
3190
+ console.error("[functions:debug]", ...args);
3191
+ }
2987
3192
  }
2988
- }
3193
+ };
3194
+ var logger = new Logger();
3195
+
3196
+ // src/ts/index.ts
2989
3197
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2990
3198
  "node_modules",
2991
3199
  "dist",
@@ -3008,6 +3216,106 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
3008
3216
  "firestore-debug.log",
3009
3217
  "pubsub-debug.log"
3010
3218
  ]);
3219
+ var CODE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
3220
+ function buildImportMap(sourceFile) {
3221
+ const map2 = /* @__PURE__ */ new Map();
3222
+ if (!sourceFile.getImportDeclarations) return map2;
3223
+ for (const decl of sourceFile.getImportDeclarations()) {
3224
+ const module = decl.getModuleSpecifierValue();
3225
+ const ns = decl.getNamespaceImport();
3226
+ if (ns) {
3227
+ map2.set(ns.getText(), { name: "*", module });
3228
+ }
3229
+ for (const named of decl.getNamedImports()) {
3230
+ const alias = named.getAliasNode();
3231
+ const name = named.getName();
3232
+ const localName = alias ? alias.getText() : name;
3233
+ map2.set(localName, { name, module });
3234
+ }
3235
+ const def = decl.getDefaultImport();
3236
+ if (def) {
3237
+ map2.set(def.getText(), { name: "default", module });
3238
+ }
3239
+ }
3240
+ return map2;
3241
+ }
3242
+ function createProject2(cwd) {
3243
+ try {
3244
+ const project = new Project2({
3245
+ tsConfigFilePath: `${cwd}/tsconfig.json`,
3246
+ skipAddingFilesFromTsConfig: true
3247
+ });
3248
+ logger.debug(`Projeto ts-morph criado com tsconfig: ${cwd}/tsconfig.json`);
3249
+ return project;
3250
+ } catch {
3251
+ logger.debug(`Falha ao ler tsconfig, criando projeto b\xE1sico`);
3252
+ return new Project2({
3253
+ skipAddingFilesFromTsConfig: true,
3254
+ compilerOptions: {
3255
+ allowJs: true,
3256
+ checkJs: false,
3257
+ target: 2,
3258
+ // ES2020
3259
+ module: 200,
3260
+ // ESNext
3261
+ moduleResolution: 100
3262
+ // Bundler
3263
+ }
3264
+ });
3265
+ }
3266
+ }
3267
+ function getAllCodeFiles(dir, files = [], baseDir = dir) {
3268
+ try {
3269
+ const entries = readdirSync2(dir);
3270
+ for (const entry of entries) {
3271
+ const fullPath = join5(dir, entry);
3272
+ if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) {
3273
+ continue;
3274
+ }
3275
+ try {
3276
+ const stat = statSync2(fullPath);
3277
+ if (stat.isDirectory()) {
3278
+ getAllCodeFiles(fullPath, files, baseDir);
3279
+ } else {
3280
+ const ext = extname2(entry).toLowerCase();
3281
+ if (CODE_EXTENSIONS2.has(ext)) {
3282
+ const relativePath = fullPath.slice(baseDir.length + 1).replace(/\\/g, "/");
3283
+ files.push(relativePath);
3284
+ }
3285
+ }
3286
+ } catch {
3287
+ }
3288
+ }
3289
+ } catch {
3290
+ }
3291
+ return files;
3292
+ }
3293
+ function inferSymbolKind(name, context2) {
3294
+ if (name.startsWith("use") && name.length > 3 && name[3] === name[3].toUpperCase()) {
3295
+ return "hook";
3296
+ }
3297
+ if (context2 === "function" && name[0] === name[0].toUpperCase() && !name.includes("_")) {
3298
+ return "component";
3299
+ }
3300
+ return context2 === "function" ? "function" : "const";
3301
+ }
3302
+ function safeGetReturnType(node) {
3303
+ try {
3304
+ const returnType = node.getReturnType();
3305
+ if (!returnType) return "unknown";
3306
+ return returnType.getText();
3307
+ } catch {
3308
+ return "unknown";
3309
+ }
3310
+ }
3311
+ function truncateCode(code, maxLen) {
3312
+ const oneLine = code.replace(/\s+/g, " ").trim();
3313
+ if (oneLine.length <= maxLen) return oneLine;
3314
+ return oneLine.slice(0, maxLen - 3) + "...";
3315
+ }
3316
+
3317
+ // src/ts/triggers.ts
3318
+ import { Node as Node2 } from "ts-morph";
3011
3319
  var FIREBASE_V2_TRIGGERS = /* @__PURE__ */ new Set([
3012
3320
  // HTTPS (firebase-functions/v2/https)
3013
3321
  "onCall",
@@ -3064,36 +3372,127 @@ var FIREBASE_V2_TRIGGERS = /* @__PURE__ */ new Set([
3064
3372
  // Test Lab (firebase-functions/v2/testLab)
3065
3373
  "onTestMatrixCompleted"
3066
3374
  ]);
3067
- function buildImportMap(sourceFile) {
3068
- const map2 = /* @__PURE__ */ new Map();
3069
- if (!sourceFile.getImportDeclarations) return map2;
3070
- for (const decl of sourceFile.getImportDeclarations()) {
3071
- const module = decl.getModuleSpecifierValue();
3072
- const ns = decl.getNamespaceImport();
3073
- if (ns) {
3074
- map2.set(ns.getText(), { name: "*", module });
3375
+ function extractFirebaseTriggerName(init, filePath, varName, importMap) {
3376
+ const text = init.getText().trim();
3377
+ const shouldDebug = filePath && filePath.includes("functions/src/");
3378
+ if (shouldDebug) {
3379
+ logger.debugFunctions(`[extractFirebaseTriggerName] Iniciando an\xE1lise`);
3380
+ logger.debugFunctions(` VarName: ${varName}`);
3381
+ logger.debugFunctions(` Node Kind: ${init.getKindName()} (${init.getKind()})`);
3382
+ logger.debugFunctions(` \xC9 CallExpression: ${Node2.isCallExpression(init)}`);
3383
+ logger.debugFunctions(` ImportMap dispon\xEDvel: ${importMap ? "SIM" : "N\xC3O"}`);
3384
+ }
3385
+ if (importMap && Node2.isCallExpression(init)) {
3386
+ const expr = init.getExpression();
3387
+ if (shouldDebug) {
3388
+ logger.debugFunctions(` Expression Kind: ${expr.getKindName()} (${expr.getKind()})`);
3389
+ logger.debugFunctions(` \xC9 Identifier: ${Node2.isIdentifier(expr)}`);
3390
+ logger.debugFunctions(` \xC9 PropertyAccess: ${Node2.isPropertyAccessExpression(expr)}`);
3075
3391
  }
3076
- for (const named of decl.getNamedImports()) {
3077
- const alias = named.getAliasNode();
3078
- const name = named.getName();
3079
- const localName = alias ? alias.getText() : name;
3080
- map2.set(localName, { name, module });
3392
+ if (Node2.isIdentifier(expr)) {
3393
+ const name = expr.getText();
3394
+ const importInfo = importMap.get(name);
3395
+ if (shouldDebug) {
3396
+ logger.debugFunctions(` [Caso 1: Identifier] Nome: ${name}`);
3397
+ logger.debugFunctions(` ImportInfo: ${importInfo ? JSON.stringify(importInfo) : "n\xE3o encontrado"}`);
3398
+ }
3399
+ if (importInfo && importInfo.module.includes("firebase-functions")) {
3400
+ if (FIREBASE_V2_TRIGGERS.has(importInfo.name)) {
3401
+ if (shouldDebug) logger.debugFunctions(` \u2713 Import detectado: ${name} -> ${importInfo.name} from ${importInfo.module}`);
3402
+ return importInfo.name;
3403
+ }
3404
+ }
3405
+ if (FIREBASE_V2_TRIGGERS.has(name)) {
3406
+ if (shouldDebug) logger.debugFunctions(` \u2713 Trigger conhecido detectado: ${name}`);
3407
+ return name;
3408
+ }
3409
+ } else if (Node2.isPropertyAccessExpression(expr)) {
3410
+ const lastPart = expr.getName();
3411
+ if (shouldDebug) {
3412
+ logger.debugFunctions(` [Caso 2: PropertyAccess] \xDAltima parte: ${lastPart}`);
3413
+ }
3414
+ if (FIREBASE_V2_TRIGGERS.has(lastPart)) {
3415
+ let root = expr.getExpression();
3416
+ let depth = 0;
3417
+ while (Node2.isPropertyAccessExpression(root) && depth < 10) {
3418
+ root = root.getExpression();
3419
+ depth++;
3420
+ }
3421
+ if (Node2.isIdentifier(root)) {
3422
+ const rootName = root.getText();
3423
+ const importInfo = importMap.get(rootName);
3424
+ if (shouldDebug) {
3425
+ logger.debugFunctions(` Raiz: ${rootName} (profundidade: ${depth})`);
3426
+ logger.debugFunctions(` ImportInfo da raiz: ${importInfo ? JSON.stringify(importInfo) : "n\xE3o encontrado"}`);
3427
+ }
3428
+ if (importInfo && importInfo.module.includes("firebase-functions")) {
3429
+ if (shouldDebug) logger.debugFunctions(` \u2713 Chain detectada: ${rootName}...${lastPart} from ${importInfo.module}`);
3430
+ return lastPart;
3431
+ }
3432
+ if (["v2", "functions", "firebase", "admin"].includes(rootName)) {
3433
+ if (shouldDebug) logger.debugFunctions(` \u2713 Heur\xEDstica: raiz "${rootName}" \xE9 conhecida do Firebase`);
3434
+ return lastPart;
3435
+ }
3436
+ }
3437
+ }
3081
3438
  }
3082
- const def = decl.getDefaultImport();
3083
- if (def) {
3084
- map2.set(def.getText(), { name: "default", module });
3439
+ }
3440
+ if (shouldDebug && varName) {
3441
+ logger.debugFunctions(` [regex] Analisando texto: "${text.slice(0, 60)}..."`);
3442
+ }
3443
+ for (const trigger of FIREBASE_V2_TRIGGERS) {
3444
+ const pattern = new RegExp(`(?:^|\\.|\\s|\\()${trigger}(?:<[\\s\\S]*?>)?\\s*\\(`);
3445
+ if (shouldDebug && varName) {
3446
+ const testResult = pattern.test(text);
3447
+ logger.debugFunctions(` [regex] Testando ${trigger}: ${testResult ? "\u2713 MATCH" : "\u2717 no match"}`);
3448
+ }
3449
+ if (pattern.test(text)) {
3450
+ if (shouldDebug && varName) {
3451
+ logger.debugFunctions(` [regex] \u2713\u2713\u2713 TRIGGER ENCONTRADO: ${trigger}`);
3452
+ }
3453
+ return trigger;
3085
3454
  }
3086
3455
  }
3087
- return map2;
3456
+ return null;
3088
3457
  }
3458
+ function extractTriggerInfo(init, triggerName) {
3459
+ const text = init.getText();
3460
+ const info = { triggerType: triggerName };
3461
+ if (triggerName.startsWith("onDocument") || triggerName.startsWith("onValue")) {
3462
+ const pathMatch = text.match(/\(\s*["'`]([^"'`]+)["'`]/);
3463
+ if (pathMatch) {
3464
+ info.triggerPath = pathMatch[1];
3465
+ }
3466
+ }
3467
+ if (triggerName === "onSchedule") {
3468
+ const scheduleMatch = text.match(/onSchedule\s*\(\s*["'`]([^"'`]+)["'`]/);
3469
+ if (scheduleMatch) {
3470
+ info.triggerSchedule = scheduleMatch[1];
3471
+ } else {
3472
+ const objectScheduleMatch = text.match(/schedule\s*:\s*["'`]([^"'`]+)["'`]/);
3473
+ if (objectScheduleMatch) {
3474
+ info.triggerSchedule = objectScheduleMatch[1];
3475
+ }
3476
+ }
3477
+ }
3478
+ if (triggerName.startsWith("onObject") || triggerName === "onMetadataUpdated") {
3479
+ const bucketMatch = text.match(/bucket\s*:\s*["'`]([^"'`]+)["'`]/);
3480
+ if (bucketMatch) {
3481
+ info.triggerPath = bucketMatch[1];
3482
+ }
3483
+ }
3484
+ return info;
3485
+ }
3486
+
3487
+ // src/ts/cache.ts
3089
3488
  function indexProject(cwd) {
3090
3489
  const allFiles = getAllCodeFiles(cwd);
3091
- debugLog(`Indexando ${allFiles.length} arquivos em ${cwd}`);
3490
+ logger.debug(`Indexando ${allFiles.length} arquivos em ${cwd}`);
3092
3491
  const functionFiles = allFiles.filter((f) => f.includes("functions/src/"));
3093
3492
  if (functionFiles.length > 0) {
3094
- debugLog(`Encontrados ${functionFiles.length} arquivos em functions/src/:`, functionFiles);
3095
- debugFunctions(`Arquivos em functions/src/:`);
3096
- functionFiles.forEach((f) => debugFunctions(` - ${f}`));
3493
+ logger.debug(`Encontrados ${functionFiles.length} arquivos em functions/src/:`, functionFiles);
3494
+ logger.debugFunctions(`Arquivos em functions/src/:`);
3495
+ functionFiles.forEach((f) => logger.debugFunctions(` - ${f}`));
3097
3496
  }
3098
3497
  const project = createProject2(cwd);
3099
3498
  let addedCount = 0;
@@ -3104,21 +3503,21 @@ function indexProject(cwd) {
3104
3503
  addedCount++;
3105
3504
  } catch {
3106
3505
  errorCount++;
3107
- if (DEBUG && file.includes("functions/src/")) {
3108
- debugLog(`[indexer] Erro ao adicionar: ${file}`);
3506
+ if (file.includes("functions/src/")) {
3507
+ logger.debug(`[indexer] Erro ao adicionar: ${file}`);
3109
3508
  }
3110
3509
  }
3111
3510
  }
3112
- debugLog(`[indexer] Total de arquivos encontrados: ${allFiles.length}`);
3113
- debugLog(`[indexer] Arquivos adicionados ao projeto: ${addedCount}`);
3114
- debugLog(`[indexer] Arquivos com erro: ${errorCount}`);
3115
- debugLog(`[indexer] SourceFiles no projeto: ${project.getSourceFiles().length}`);
3511
+ logger.debug(`[indexer] Total de arquivos encontrados: ${allFiles.length}`);
3512
+ logger.debug(`[indexer] Arquivos adicionados ao projeto: ${addedCount}`);
3513
+ logger.debug(`[indexer] Arquivos com erro: ${errorCount}`);
3514
+ logger.debug(`[indexer] SourceFiles no projeto: ${project.getSourceFiles().length}`);
3116
3515
  const functionsInProject = project.getSourceFiles().filter(
3117
3516
  (sf) => sf.getFilePath().includes("functions/src/")
3118
3517
  );
3119
- debugFunctions(`[indexer] Arquivos functions/src/ no projeto: ${functionsInProject.length}`);
3518
+ logger.debugFunctions(`[indexer] Arquivos functions/src/ no projeto: ${functionsInProject.length}`);
3120
3519
  functionsInProject.forEach((sf) => {
3121
- debugFunctions(` - ${sf.getFilePath()}`);
3520
+ logger.debugFunctions(` - ${sf.getFilePath()}`);
3122
3521
  });
3123
3522
  const files = {};
3124
3523
  const symbolsByName = {};
@@ -3160,7 +3559,7 @@ function indexProject(cwd) {
3160
3559
  if (!name) continue;
3161
3560
  const isExported = func.isExported();
3162
3561
  const params = func.getParameters().map((p) => p.getName());
3163
- const returnType = simplifyType2(safeGetReturnType(func));
3562
+ const returnType = simplifyType(safeGetReturnType(func));
3164
3563
  const kind = inferSymbolKind(name, "function");
3165
3564
  const symbol = {
3166
3565
  name,
@@ -3189,14 +3588,14 @@ function indexProject(cwd) {
3189
3588
  if (!init) continue;
3190
3589
  const initKind = init.getKind();
3191
3590
  const initKindName = init.getKindName();
3192
- if (DEBUG_FUNCTIONS && filePath.includes("functions/src/")) {
3193
- debugFunctions(`[kind] ${name}: ${initKindName} (kind=${initKind})`);
3591
+ if (filePath.includes("functions/src/")) {
3592
+ logger.debugFunctions(`[kind] ${name}: ${initKindName} (kind=${initKind})`);
3194
3593
  }
3195
3594
  if (initKind === SyntaxKind2.ArrowFunction || initKind === SyntaxKind2.FunctionExpression) {
3196
3595
  const funcLike = init.asKind(SyntaxKind2.ArrowFunction) || init.asKind(SyntaxKind2.FunctionExpression);
3197
3596
  if (!funcLike) continue;
3198
3597
  const params = funcLike.getParameters().map((p) => p.getName());
3199
- const returnType = simplifyType2(safeGetReturnType(funcLike));
3598
+ const returnType = simplifyType(safeGetReturnType(funcLike));
3200
3599
  const kind = inferSymbolKind(name, "function");
3201
3600
  const symbol = {
3202
3601
  name,
@@ -3218,23 +3617,23 @@ function indexProject(cwd) {
3218
3617
  }
3219
3618
  } else if (initKind === SyntaxKind2.CallExpression) {
3220
3619
  const importMap = buildImportMap(sourceFile);
3221
- if (DEBUG_FUNCTIONS && filePath.includes("functions/src/")) {
3620
+ if (filePath.includes("functions/src/")) {
3222
3621
  const initText = init.getText().slice(0, 100).replace(/\s+/g, " ");
3223
- debugFunctions(`
3622
+ logger.debugFunctions(`
3224
3623
  [CallExpression] ${filePath}:${varDecl.getStartLineNumber()}`);
3225
- debugFunctions(` Vari\xE1vel: ${name}`);
3226
- debugFunctions(` C\xF3digo: ${initText}...`);
3227
- debugFunctions(` Imports encontrados: ${importMap.size}`);
3624
+ logger.debugFunctions(` Vari\xE1vel: ${name}`);
3625
+ logger.debugFunctions(` C\xF3digo: ${initText}...`);
3626
+ logger.debugFunctions(` Imports encontrados: ${importMap.size}`);
3228
3627
  importMap.forEach((info, key) => {
3229
- debugFunctions(` - ${key} -> ${info.name} from ${info.module}`);
3628
+ logger.debugFunctions(` - ${key} -> ${info.name} from ${info.module}`);
3230
3629
  });
3231
3630
  }
3232
3631
  const triggerName = extractFirebaseTriggerName(init, filePath, name, importMap);
3233
- if (DEBUG_FUNCTIONS && filePath.includes("functions/src/")) {
3632
+ if (filePath.includes("functions/src/")) {
3234
3633
  if (triggerName) {
3235
- debugFunctions(` \u2713\u2713\u2713 Trigger FINAL detectado: ${triggerName}`);
3634
+ logger.debugFunctions(` \u2713\u2713\u2713 Trigger FINAL detectado: ${triggerName}`);
3236
3635
  } else {
3237
- debugFunctions(` \u2717\u2717\u2717 Nenhum trigger detectado para: ${name}`);
3636
+ logger.debugFunctions(` \u2717\u2717\u2717 Nenhum trigger detectado para: ${name}`);
3238
3637
  }
3239
3638
  }
3240
3639
  if (triggerName && FIREBASE_V2_TRIGGERS.has(triggerName)) {
@@ -3309,7 +3708,7 @@ function indexProject(cwd) {
3309
3708
  kind: "interface",
3310
3709
  signature: `${isExported ? "export " : ""}interface ${name}`,
3311
3710
  isExported,
3312
- definition: formatInterfaceDefinition2(iface)
3711
+ definition: formatInterfaceDefinition(iface)
3313
3712
  };
3314
3713
  symbols.push(symbol);
3315
3714
  if (!symbolsByName[name]) {
@@ -3330,7 +3729,7 @@ function indexProject(cwd) {
3330
3729
  kind: "type",
3331
3730
  signature: `${isExported ? "export " : ""}type ${name}`,
3332
3731
  isExported,
3333
- definition: simplifyType2(safeGetTypeText(() => typeAlias.getType()))
3732
+ definition: simplifyType(safeGetTypeText(() => typeAlias.getType()))
3334
3733
  };
3335
3734
  symbols.push(symbol);
3336
3735
  if (!symbolsByName[name]) {
@@ -3380,236 +3779,18 @@ function indexProject(cwd) {
3380
3779
  symbolCount
3381
3780
  };
3382
3781
  }
3383
- function createProject2(cwd) {
3384
- try {
3385
- const project = new Project2({
3386
- tsConfigFilePath: `${cwd}/tsconfig.json`,
3387
- skipAddingFilesFromTsConfig: true
3388
- });
3389
- debugLog(`Projeto ts-morph criado com tsconfig: ${cwd}/tsconfig.json`);
3390
- return project;
3391
- } catch {
3392
- debugLog(`Falha ao ler tsconfig, criando projeto b\xE1sico`);
3393
- return new Project2({
3394
- skipAddingFilesFromTsConfig: true,
3395
- compilerOptions: {
3396
- allowJs: true,
3397
- checkJs: false,
3398
- target: 2,
3399
- // ES2020
3400
- module: 200,
3401
- // ESNext
3402
- moduleResolution: 100
3403
- // Bundler
3404
- }
3405
- });
3406
- }
3407
- }
3408
- function getAllCodeFiles(dir, files = [], baseDir = dir) {
3409
- try {
3410
- const entries = readdirSync2(dir);
3411
- for (const entry of entries) {
3412
- const fullPath = join5(dir, entry);
3413
- if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) {
3414
- continue;
3415
- }
3416
- try {
3417
- const stat = statSync2(fullPath);
3418
- if (stat.isDirectory()) {
3419
- getAllCodeFiles(fullPath, files, baseDir);
3420
- } else {
3421
- const ext = extname2(entry).toLowerCase();
3422
- if (CODE_EXTENSIONS2.has(ext)) {
3423
- const relativePath = fullPath.slice(baseDir.length + 1).replace(/\\/g, "/");
3424
- files.push(relativePath);
3425
- }
3426
- }
3427
- } catch {
3428
- }
3429
- }
3430
- } catch {
3431
- }
3432
- return files;
3433
- }
3434
- function inferSymbolKind(name, context2) {
3435
- if (name.startsWith("use") && name.length > 3 && name[3] === name[3].toUpperCase()) {
3436
- return "hook";
3437
- }
3438
- if (context2 === "function" && name[0] === name[0].toUpperCase() && !name.includes("_")) {
3439
- return "component";
3440
- }
3441
- return context2 === "function" ? "function" : "const";
3442
- }
3443
- function extractFirebaseTriggerName(init, filePath, varName, importMap) {
3444
- const text = init.getText().trim();
3445
- const shouldDebug = DEBUG_FUNCTIONS && filePath && filePath.includes("functions/src/");
3446
- if (shouldDebug) {
3447
- debugFunctions(`[extractFirebaseTriggerName] Iniciando an\xE1lise`);
3448
- debugFunctions(` VarName: ${varName}`);
3449
- debugFunctions(` Node Kind: ${init.getKindName()} (${init.getKind()})`);
3450
- debugFunctions(` \xC9 CallExpression: ${Node2.isCallExpression(init)}`);
3451
- debugFunctions(` ImportMap dispon\xEDvel: ${importMap ? "SIM" : "N\xC3O"}`);
3452
- }
3453
- if (importMap && Node2.isCallExpression(init)) {
3454
- const expr = init.getExpression();
3455
- if (shouldDebug) {
3456
- debugFunctions(` Expression Kind: ${expr.getKindName()} (${expr.getKind()})`);
3457
- debugFunctions(` \xC9 Identifier: ${Node2.isIdentifier(expr)}`);
3458
- debugFunctions(` \xC9 PropertyAccess: ${Node2.isPropertyAccessExpression(expr)}`);
3459
- }
3460
- if (Node2.isIdentifier(expr)) {
3461
- const name = expr.getText();
3462
- const importInfo = importMap.get(name);
3463
- if (shouldDebug) {
3464
- debugFunctions(` [Caso 1: Identifier] Nome: ${name}`);
3465
- debugFunctions(` ImportInfo: ${importInfo ? JSON.stringify(importInfo) : "n\xE3o encontrado"}`);
3466
- }
3467
- if (importInfo && importInfo.module.includes("firebase-functions")) {
3468
- if (FIREBASE_V2_TRIGGERS.has(importInfo.name)) {
3469
- if (shouldDebug) debugFunctions(` \u2713 Import detectado: ${name} -> ${importInfo.name} from ${importInfo.module}`);
3470
- return importInfo.name;
3471
- }
3472
- }
3473
- if (FIREBASE_V2_TRIGGERS.has(name)) {
3474
- if (shouldDebug) debugFunctions(` \u2713 Trigger conhecido detectado: ${name}`);
3475
- return name;
3476
- }
3477
- } else if (Node2.isPropertyAccessExpression(expr)) {
3478
- const lastPart = expr.getName();
3479
- if (shouldDebug) {
3480
- debugFunctions(` [Caso 2: PropertyAccess] \xDAltima parte: ${lastPart}`);
3481
- }
3482
- if (FIREBASE_V2_TRIGGERS.has(lastPart)) {
3483
- let root = expr.getExpression();
3484
- let depth = 0;
3485
- while (Node2.isPropertyAccessExpression(root) && depth < 10) {
3486
- root = root.getExpression();
3487
- depth++;
3488
- }
3489
- if (Node2.isIdentifier(root)) {
3490
- const rootName = root.getText();
3491
- const importInfo = importMap.get(rootName);
3492
- if (shouldDebug) {
3493
- debugFunctions(` Raiz: ${rootName} (profundidade: ${depth})`);
3494
- debugFunctions(` ImportInfo da raiz: ${importInfo ? JSON.stringify(importInfo) : "n\xE3o encontrado"}`);
3495
- }
3496
- if (importInfo && importInfo.module.includes("firebase-functions")) {
3497
- if (shouldDebug) debugFunctions(` \u2713 Chain detectada: ${rootName}...${lastPart} from ${importInfo.module}`);
3498
- return lastPart;
3499
- }
3500
- if (["v2", "functions", "firebase", "admin"].includes(rootName)) {
3501
- if (shouldDebug) debugFunctions(` \u2713 Heur\xEDstica: raiz "${rootName}" \xE9 conhecida do Firebase`);
3502
- return lastPart;
3503
- }
3504
- }
3505
- }
3506
- }
3507
- }
3508
- if (shouldDebug && varName) {
3509
- debugFunctions(` [regex] Analisando texto: "${text.slice(0, 60)}..."`);
3510
- }
3511
- for (const trigger of FIREBASE_V2_TRIGGERS) {
3512
- const pattern = new RegExp(`(?:^|\\.|\\s|\\()${trigger}(?:<[\\s\\S]*?>)?\\s*\\(`);
3513
- if (shouldDebug && varName) {
3514
- const testResult = pattern.test(text);
3515
- debugFunctions(` [regex] Testando ${trigger}: ${testResult ? "\u2713 MATCH" : "\u2717 no match"}`);
3516
- }
3517
- if (pattern.test(text)) {
3518
- if (shouldDebug && varName) {
3519
- debugFunctions(` [regex] \u2713\u2713\u2713 TRIGGER ENCONTRADO: ${trigger}`);
3520
- }
3521
- return trigger;
3522
- }
3523
- }
3524
- return null;
3525
- }
3526
- function extractTriggerInfo(init, triggerName) {
3527
- const text = init.getText();
3528
- const info = { triggerType: triggerName };
3529
- if (triggerName.startsWith("onDocument") || triggerName.startsWith("onValue")) {
3530
- const pathMatch = text.match(/\(\s*["'`]([^"'`]+)["'`]/);
3531
- if (pathMatch) {
3532
- info.triggerPath = pathMatch[1];
3533
- }
3534
- }
3535
- if (triggerName === "onSchedule") {
3536
- const scheduleMatch = text.match(/onSchedule\s*\(\s*["'`]([^"'`]+)["'`]/);
3537
- if (scheduleMatch) {
3538
- info.triggerSchedule = scheduleMatch[1];
3539
- } else {
3540
- const objectScheduleMatch = text.match(/schedule\s*:\s*["'`]([^"'`]+)["'`]/);
3541
- if (objectScheduleMatch) {
3542
- info.triggerSchedule = objectScheduleMatch[1];
3543
- }
3544
- }
3545
- }
3546
- if (triggerName.startsWith("onObject") || triggerName === "onMetadataUpdated") {
3547
- const bucketMatch = text.match(/bucket\s*:\s*["'`]([^"'`]+)["'`]/);
3548
- if (bucketMatch) {
3549
- info.triggerPath = bucketMatch[1];
3550
- }
3551
- }
3552
- return info;
3553
- }
3554
- function simplifyType2(typeText) {
3555
- if (!typeText) return "unknown";
3556
- let simplified = typeText.replace(/import\([^)]+\)\./g, "");
3557
- if (simplified.length > 80) {
3558
- simplified = simplified.slice(0, 77) + "...";
3559
- }
3560
- return simplified;
3561
- }
3562
- function safeGetTypeText(getTypeFn) {
3563
- try {
3564
- const type = getTypeFn();
3565
- if (!type) return "unknown";
3566
- return type.getText();
3567
- } catch {
3568
- return "unknown";
3569
- }
3570
- }
3571
- function safeGetReturnType(node) {
3572
- try {
3573
- const returnType = node.getReturnType();
3574
- if (!returnType) return "unknown";
3575
- return returnType.getText();
3576
- } catch {
3577
- return "unknown";
3578
- }
3579
- }
3580
- function truncateCode(code, maxLen) {
3581
- const oneLine = code.replace(/\s+/g, " ").trim();
3582
- if (oneLine.length <= maxLen) return oneLine;
3583
- return oneLine.slice(0, maxLen - 3) + "...";
3584
- }
3585
- function formatInterfaceDefinition2(iface) {
3586
- const parts = [];
3587
- const extendsClauses = iface.getExtends();
3588
- if (extendsClauses.length > 0) {
3589
- parts.push(`extends ${extendsClauses.map((e) => e.getText()).join(", ")}`);
3590
- }
3591
- const props = iface.getProperties();
3592
- for (const prop of props.slice(0, 10)) {
3593
- const propType = simplifyType2(safeGetTypeText(() => prop.getType()));
3594
- parts.push(`${prop.getName()}: ${propType}`);
3595
- }
3596
- if (props.length > 10) {
3597
- parts.push(`... +${props.length - 10} props`);
3598
- }
3599
- return parts.length > 0 ? `{ ${parts.join("; ")} }` : "{}";
3600
- }
3601
3782
 
3602
3783
  // src/commands/context.ts
3603
3784
  async function context(target, options = {}) {
3604
- const cwd = options.cwd || process.cwd();
3605
- const format = options.format || "text";
3785
+ const { cwd, format } = parseCommandOptions(options);
3606
3786
  if (!target) {
3607
3787
  throw new Error("Target e obrigatorio. Exemplo: ai-tool context src/components/Button.tsx");
3608
3788
  }
3609
3789
  try {
3610
- const targetPath = findTargetFile3(target, cwd);
3790
+ const targetPath = findTargetFile2(target, cwd);
3611
3791
  if (!targetPath) {
3612
- return formatNotFound3(target, cwd);
3792
+ const allFiles = getAllCodeFiles2(cwd);
3793
+ return formatFileNotFound({ target, allFiles, command: "context" });
3613
3794
  }
3614
3795
  const project = createProject(cwd);
3615
3796
  const absolutePath = resolve2(cwd, targetPath);
@@ -3628,25 +3809,22 @@ async function context(target, options = {}) {
3628
3809
  functions: functions2,
3629
3810
  types
3630
3811
  };
3631
- if (format === "json") {
3632
- return JSON.stringify(result, null, 2);
3633
- }
3634
- return formatContextText(result);
3812
+ return formatOutput(result, format, formatContextText);
3635
3813
  } catch (error) {
3636
3814
  const message = error instanceof Error ? error.message : String(error);
3637
3815
  throw new Error(`Erro ao executar context: ${message}`);
3638
3816
  }
3639
3817
  }
3640
3818
  var CODE_EXTENSIONS3 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
3641
- function findTargetFile3(target, cwd) {
3819
+ function findTargetFile2(target, cwd) {
3642
3820
  const normalizedTarget = target.replace(/\\/g, "/");
3643
3821
  const directPath = resolve2(cwd, normalizedTarget);
3644
- if (existsSync5(directPath) && isCodeFile2(directPath)) {
3822
+ if (existsSync6(directPath) && isCodeFile2(directPath)) {
3645
3823
  return normalizedTarget;
3646
3824
  }
3647
3825
  for (const ext of CODE_EXTENSIONS3) {
3648
3826
  const withExt = directPath + ext;
3649
- if (existsSync5(withExt)) {
3827
+ if (existsSync6(withExt)) {
3650
3828
  return normalizedTarget + ext;
3651
3829
  }
3652
3830
  }
@@ -3709,10 +3887,6 @@ function shouldIgnore(name) {
3709
3887
  ];
3710
3888
  return ignoredDirs.includes(name) || name.startsWith(".");
3711
3889
  }
3712
- function formatNotFound3(target, cwd) {
3713
- const allFiles = getAllCodeFiles2(cwd);
3714
- return formatFileNotFound({ target, allFiles, command: "context" });
3715
- }
3716
3890
  async function areaContext(areaName, options = {}) {
3717
3891
  const cwd = options.cwd || process.cwd();
3718
3892
  const format = options.format || "text";
@@ -4725,8 +4899,7 @@ function getTriggerIcon(trigger) {
4725
4899
 
4726
4900
  // src/commands/find.ts
4727
4901
  async function find(query, options = {}) {
4728
- const cwd = options.cwd || process.cwd();
4729
- const format = options.format || "text";
4902
+ const { cwd, format } = parseCommandOptions(options);
4730
4903
  const filterType = options.type || "all";
4731
4904
  const filterArea = options.area;
4732
4905
  const defOnly = options.def ?? false;
@@ -4817,10 +4990,7 @@ async function find(query, options = {}) {
4817
4990
  },
4818
4991
  fromCache
4819
4992
  };
4820
- if (format === "json") {
4821
- return JSON.stringify(result, null, 2);
4822
- }
4823
- return formatFindText(result);
4993
+ return formatOutput(result, format, formatFindText, fromCache);
4824
4994
  } catch (error) {
4825
4995
  const message = error instanceof Error ? error.message : String(error);
4826
4996
  throw new Error(`Erro ao executar find: ${message}`);
@@ -4967,6 +5137,8 @@ export {
4967
5137
  getAreaName,
4968
5138
  getAreaDescription,
4969
5139
  inferFileDescription,
5140
+ parseCommandOptions,
5141
+ formatOutput,
4970
5142
  map,
4971
5143
  dead,
4972
5144
  deadFix,