@justmpm/ai-tool 0.1.0 → 0.2.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.
@@ -374,10 +374,143 @@ function formatImpactText(result) {
374
374
  return out;
375
375
  }
376
376
 
377
+ // src/cache/index.ts
378
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, readdirSync } from "fs";
379
+ import { join, extname } from "path";
380
+ var CACHE_DIR = ".analyze";
381
+ var META_FILE = "meta.json";
382
+ var GRAPH_FILE = "graph.json";
383
+ var MAP_FILE = "map.json";
384
+ var DEAD_FILE = "dead.json";
385
+ function calculateFilesHash(cwd) {
386
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
387
+ const timestamps = [];
388
+ function scanDir(dir) {
389
+ try {
390
+ const entries = readdirSync(dir, { withFileTypes: true });
391
+ for (const entry of entries) {
392
+ const fullPath = join(dir, entry.name);
393
+ if (entry.isDirectory()) {
394
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === ".analyze") {
395
+ continue;
396
+ }
397
+ scanDir(fullPath);
398
+ } else if (entry.isFile()) {
399
+ const ext = extname(entry.name).toLowerCase();
400
+ if (extensions.includes(ext)) {
401
+ try {
402
+ const stat = statSync(fullPath);
403
+ timestamps.push(stat.mtimeMs);
404
+ } catch {
405
+ }
406
+ }
407
+ }
408
+ }
409
+ } catch {
410
+ }
411
+ }
412
+ scanDir(cwd);
413
+ const sum = timestamps.reduce((a, b) => a + b, 0);
414
+ return `${timestamps.length}-${Math.floor(sum)}`;
415
+ }
416
+ function getCacheDir(cwd) {
417
+ return join(cwd, CACHE_DIR);
418
+ }
419
+ function isCacheValid(cwd) {
420
+ const cacheDir = getCacheDir(cwd);
421
+ const metaPath = join(cacheDir, META_FILE);
422
+ if (!existsSync(metaPath)) {
423
+ return false;
424
+ }
425
+ try {
426
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
427
+ const currentHash = calculateFilesHash(cwd);
428
+ return meta.filesHash === currentHash;
429
+ } catch {
430
+ return false;
431
+ }
432
+ }
433
+ function readCache(cwd, file) {
434
+ const cachePath = join(getCacheDir(cwd), file);
435
+ if (!existsSync(cachePath)) {
436
+ return null;
437
+ }
438
+ try {
439
+ return JSON.parse(readFileSync(cachePath, "utf-8"));
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+ function writeCache(cwd, file, data) {
445
+ const cacheDir = getCacheDir(cwd);
446
+ if (!existsSync(cacheDir)) {
447
+ mkdirSync(cacheDir, { recursive: true });
448
+ }
449
+ const cachePath = join(cacheDir, file);
450
+ writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
451
+ }
452
+ function updateCacheMeta(cwd) {
453
+ const meta = {
454
+ version: "1.0.0",
455
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
456
+ lastCheck: (/* @__PURE__ */ new Date()).toISOString(),
457
+ filesHash: calculateFilesHash(cwd)
458
+ };
459
+ writeCache(cwd, META_FILE, meta);
460
+ }
461
+ function cacheGraph(cwd, graph) {
462
+ writeCache(cwd, GRAPH_FILE, graph);
463
+ updateCacheMeta(cwd);
464
+ }
465
+ function getCachedGraph(cwd) {
466
+ if (!isCacheValid(cwd)) {
467
+ return null;
468
+ }
469
+ return readCache(cwd, GRAPH_FILE);
470
+ }
471
+ function cacheMapResult(cwd, result) {
472
+ writeCache(cwd, MAP_FILE, result);
473
+ }
474
+ function getCachedMapResult(cwd) {
475
+ if (!isCacheValid(cwd)) {
476
+ return null;
477
+ }
478
+ return readCache(cwd, MAP_FILE);
479
+ }
480
+ function cacheDeadResult(cwd, result) {
481
+ writeCache(cwd, DEAD_FILE, result);
482
+ }
483
+ function getCachedDeadResult(cwd) {
484
+ if (!isCacheValid(cwd)) {
485
+ return null;
486
+ }
487
+ return readCache(cwd, DEAD_FILE);
488
+ }
489
+ function invalidateCache(cwd) {
490
+ const metaPath = join(getCacheDir(cwd), META_FILE);
491
+ if (existsSync(metaPath)) {
492
+ try {
493
+ writeFileSync(metaPath, "{}", "utf-8");
494
+ } catch {
495
+ }
496
+ }
497
+ }
498
+
377
499
  // src/commands/map.ts
378
500
  async function map(options = {}) {
379
501
  const cwd = options.cwd || process.cwd();
380
502
  const format = options.format || "text";
503
+ const useCache = options.cache !== false;
504
+ if (useCache && isCacheValid(cwd)) {
505
+ const cached = getCachedMapResult(cwd);
506
+ if (cached) {
507
+ const result = { ...cached, timestamp: (/* @__PURE__ */ new Date()).toISOString(), fromCache: true };
508
+ if (format === "json") {
509
+ return JSON.stringify(result, null, 2);
510
+ }
511
+ return formatMapText(result) + "\n\n\u{1F4E6} (resultado do cache)";
512
+ }
513
+ }
381
514
  try {
382
515
  const { getStructure, useGraph } = await skott({
383
516
  cwd,
@@ -391,7 +524,16 @@ async function map(options = {}) {
391
524
  tsConfigPath: "tsconfig.json"
392
525
  });
393
526
  const structure = getStructure();
394
- const { findCircularDependencies } = useGraph();
527
+ const graphApi = useGraph();
528
+ const { findCircularDependencies } = graphApi;
529
+ if (useCache) {
530
+ const graphData = {
531
+ graph: structure.graph,
532
+ files: Object.keys(structure.graph),
533
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
534
+ };
535
+ cacheGraph(cwd, graphData);
536
+ }
395
537
  const files = Object.entries(structure.graph).map(([path]) => ({
396
538
  path,
397
539
  category: detectCategory(path),
@@ -433,6 +575,10 @@ async function map(options = {}) {
433
575
  files,
434
576
  circularDependencies: circular
435
577
  };
578
+ if (useCache) {
579
+ cacheMapResult(cwd, result);
580
+ updateCacheMeta(cwd);
581
+ }
436
582
  if (format === "json") {
437
583
  return JSON.stringify(result, null, 2);
438
584
  }
@@ -448,6 +594,17 @@ import { execSync } from "child_process";
448
594
  async function dead(options = {}) {
449
595
  const cwd = options.cwd || process.cwd();
450
596
  const format = options.format || "text";
597
+ const useCache = options.cache !== false;
598
+ if (useCache && isCacheValid(cwd)) {
599
+ const cached = getCachedDeadResult(cwd);
600
+ if (cached) {
601
+ const result = { ...cached, timestamp: (/* @__PURE__ */ new Date()).toISOString(), fromCache: true };
602
+ if (format === "json") {
603
+ return JSON.stringify(result, null, 2);
604
+ }
605
+ return formatDeadText(result) + "\n\n\u{1F4E6} (resultado do cache)";
606
+ }
607
+ }
451
608
  try {
452
609
  let knipOutput;
453
610
  try {
@@ -506,6 +663,10 @@ async function dead(options = {}) {
506
663
  exports: deadExports,
507
664
  dependencies: deadDependencies
508
665
  };
666
+ if (useCache) {
667
+ cacheDeadResult(cwd, result);
668
+ updateCacheMeta(cwd);
669
+ }
509
670
  if (format === "json") {
510
671
  return JSON.stringify(result, null, 2);
511
672
  }
@@ -537,39 +698,57 @@ import skott2 from "skott";
537
698
  async function impact(target, options = {}) {
538
699
  const cwd = options.cwd || process.cwd();
539
700
  const format = options.format || "text";
701
+ const useCache = options.cache !== false;
540
702
  if (!target) {
541
703
  throw new Error("Target \xE9 obrigat\xF3rio. Exemplo: ai-tool impact src/components/Button.tsx");
542
704
  }
543
705
  try {
544
- const { getStructure, useGraph } = await skott2({
545
- cwd,
546
- includeBaseDir: false,
547
- dependencyTracking: {
548
- thirdParty: false,
549
- builtin: false,
550
- typeOnly: false
551
- },
552
- fileExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
553
- tsConfigPath: "tsconfig.json"
554
- });
555
- const structure = getStructure();
556
- const graphApi = useGraph();
557
- const allFiles = Object.keys(structure.graph);
706
+ let graph;
707
+ let allFiles = [];
708
+ let findCircular = () => [];
709
+ let fromCache = false;
710
+ if (useCache && isCacheValid(cwd)) {
711
+ const cached = getCachedGraph(cwd);
712
+ if (cached) {
713
+ graph = cached.graph;
714
+ allFiles = cached.files;
715
+ findCircular = () => findCircularFromGraph(graph);
716
+ fromCache = true;
717
+ }
718
+ }
719
+ if (!graph) {
720
+ const { getStructure, useGraph } = await skott2({
721
+ cwd,
722
+ includeBaseDir: false,
723
+ dependencyTracking: {
724
+ thirdParty: false,
725
+ builtin: false,
726
+ typeOnly: false
727
+ },
728
+ fileExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
729
+ tsConfigPath: "tsconfig.json"
730
+ });
731
+ const structure = getStructure();
732
+ const graphApi = useGraph();
733
+ graph = structure.graph;
734
+ allFiles = Object.keys(graph);
735
+ findCircular = () => graphApi.findCircularDependencies();
736
+ if (useCache) {
737
+ cacheGraph(cwd, {
738
+ graph,
739
+ files: allFiles,
740
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
741
+ });
742
+ updateCacheMeta(cwd);
743
+ }
744
+ }
558
745
  const targetPath = findTargetFile(target, allFiles);
559
746
  if (!targetPath) {
560
747
  return formatNotFound(target, allFiles);
561
748
  }
562
- const dependingOnNodes = graphApi.collectFilesDependingOn(targetPath, "deep");
563
- const dependingOnShallow = graphApi.collectFilesDependingOn(targetPath, "shallow");
564
- const dependenciesNodes = graphApi.collectFilesDependencies(targetPath, "deep");
565
- const dependenciesShallow = graphApi.collectFilesDependencies(targetPath, "shallow");
566
- const dependingOn = dependingOnNodes.map((n) => n.id);
567
- const dependencies = dependenciesNodes.map((n) => n.id);
568
- const directUpstream = dependingOnShallow.map((n) => n.id);
569
- const directDownstream = dependenciesShallow.map((n) => n.id);
570
- const indirectUpstream = dependingOn.filter((f) => !directUpstream.includes(f));
571
- const indirectDownstream = dependencies.filter((f) => !directDownstream.includes(f));
572
- const findCircular = () => graphApi.findCircularDependencies();
749
+ const { directUpstream, indirectUpstream, directDownstream, indirectDownstream } = calculateDependencies(targetPath, graph);
750
+ const dependingOn = [...directUpstream, ...indirectUpstream];
751
+ const dependencies = [...directDownstream, ...indirectDownstream];
573
752
  const risks = detectRisks(targetPath, dependingOn, dependencies, findCircular);
574
753
  const suggestions = generateSuggestions(dependingOn, dependencies, risks);
575
754
  const result = {
@@ -593,12 +772,95 @@ async function impact(target, options = {}) {
593
772
  if (format === "json") {
594
773
  return JSON.stringify(result, null, 2);
595
774
  }
596
- return formatImpactText(result);
775
+ const output = formatImpactText(result);
776
+ return fromCache ? output + "\n\n\u{1F4E6} (grafo do cache)" : output;
597
777
  } catch (error) {
598
778
  const message = error instanceof Error ? error.message : String(error);
599
779
  throw new Error(`Erro ao executar impact: ${message}`);
600
780
  }
601
781
  }
782
+ function calculateDependencies(targetPath, graph) {
783
+ const targetNode = graph[targetPath];
784
+ const directDownstream = targetNode ? targetNode.adjacentTo : [];
785
+ const allDownstream = /* @__PURE__ */ new Set();
786
+ const visited = /* @__PURE__ */ new Set();
787
+ function collectDownstream(file) {
788
+ if (visited.has(file)) return;
789
+ visited.add(file);
790
+ const node = graph[file];
791
+ if (node) {
792
+ for (const dep of node.adjacentTo) {
793
+ allDownstream.add(dep);
794
+ collectDownstream(dep);
795
+ }
796
+ }
797
+ }
798
+ collectDownstream(targetPath);
799
+ const indirectDownstream = Array.from(allDownstream).filter(
800
+ (f) => !directDownstream.includes(f)
801
+ );
802
+ const directUpstream = [];
803
+ const allUpstream = /* @__PURE__ */ new Set();
804
+ for (const [file, node] of Object.entries(graph)) {
805
+ if (node.adjacentTo.includes(targetPath)) {
806
+ directUpstream.push(file);
807
+ }
808
+ }
809
+ visited.clear();
810
+ function collectUpstream(file) {
811
+ if (visited.has(file)) return;
812
+ visited.add(file);
813
+ for (const [f, node] of Object.entries(graph)) {
814
+ if (node.adjacentTo.includes(file) && !visited.has(f)) {
815
+ allUpstream.add(f);
816
+ collectUpstream(f);
817
+ }
818
+ }
819
+ }
820
+ for (const file of directUpstream) {
821
+ collectUpstream(file);
822
+ }
823
+ const indirectUpstream = Array.from(allUpstream).filter(
824
+ (f) => !directUpstream.includes(f)
825
+ );
826
+ return {
827
+ directUpstream,
828
+ indirectUpstream,
829
+ directDownstream,
830
+ indirectDownstream
831
+ };
832
+ }
833
+ function findCircularFromGraph(graph) {
834
+ const cycles = [];
835
+ const visited = /* @__PURE__ */ new Set();
836
+ const stack = /* @__PURE__ */ new Set();
837
+ const path = [];
838
+ function dfs(node) {
839
+ if (stack.has(node)) {
840
+ const cycleStart = path.indexOf(node);
841
+ if (cycleStart !== -1) {
842
+ cycles.push(path.slice(cycleStart));
843
+ }
844
+ return;
845
+ }
846
+ if (visited.has(node)) return;
847
+ visited.add(node);
848
+ stack.add(node);
849
+ path.push(node);
850
+ const nodeData = graph[node];
851
+ if (nodeData) {
852
+ for (const dep of nodeData.adjacentTo) {
853
+ dfs(dep);
854
+ }
855
+ }
856
+ path.pop();
857
+ stack.delete(node);
858
+ }
859
+ for (const node of Object.keys(graph)) {
860
+ dfs(node);
861
+ }
862
+ return cycles;
863
+ }
602
864
  function findTargetFile(target, allFiles) {
603
865
  const normalizedTarget = target.replace(/\\/g, "/");
604
866
  if (allFiles.includes(normalizedTarget)) {
@@ -749,13 +1011,16 @@ function levenshteinDistance(a, b) {
749
1011
  }
750
1012
 
751
1013
  // src/index.ts
752
- var VERSION = "0.1.0";
1014
+ var VERSION = "0.2.0";
753
1015
 
754
1016
  export {
755
1017
  detectCategory,
756
1018
  categoryIcons,
757
1019
  isEntryPoint,
758
1020
  isCodeFile,
1021
+ getCacheDir,
1022
+ isCacheValid,
1023
+ invalidateCache,
759
1024
  map,
760
1025
  dead,
761
1026
  deadFix,
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  deadFix,
6
6
  impact,
7
7
  map
8
- } from "./chunk-EGBEXF4G.js";
8
+ } from "./chunk-HOVHD2N4.js";
9
9
 
10
10
  // src/cli.ts
11
11
  var HELP = `
@@ -20,12 +20,19 @@ COMANDOS:
20
20
  OP\xC7\xD5ES:
21
21
  --format=text|json Formato de sa\xEDda (default: text)
22
22
  --cwd=<path> Diret\xF3rio do projeto (default: cwd)
23
+ --no-cache Ignora cache e for\xE7a regenera\xE7\xE3o
23
24
  --help, -h Mostra esta ajuda
24
25
  --version, -v Mostra vers\xE3o
25
26
 
27
+ CACHE:
28
+ Resultados s\xE3o salvos em .analyze/ para acelerar execu\xE7\xF5es futuras.
29
+ O cache \xE9 invalidado automaticamente quando arquivos mudam.
30
+ Use --no-cache para for\xE7ar regenera\xE7\xE3o.
31
+
26
32
  EXEMPLOS:
27
33
  ai-tool map
28
34
  ai-tool map --format=json
35
+ ai-tool map --no-cache
29
36
  ai-tool dead
30
37
  ai-tool dead --fix
31
38
  ai-tool impact Button
@@ -63,17 +70,18 @@ async function main() {
63
70
  const target = positional[1];
64
71
  const format = flags.format || "text";
65
72
  const cwd = flags.cwd || process.cwd();
73
+ const cache = !flags["no-cache"];
66
74
  try {
67
75
  let result;
68
76
  switch (command) {
69
77
  case "map":
70
- result = await map({ format, cwd });
78
+ result = await map({ format, cwd, cache });
71
79
  break;
72
80
  case "dead":
73
81
  if (flags.fix) {
74
82
  result = await deadFix({ cwd });
75
83
  } else {
76
- result = await dead({ format, cwd });
84
+ result = await dead({ format, cwd, cache });
77
85
  }
78
86
  break;
79
87
  case "impact":
@@ -83,7 +91,7 @@ async function main() {
83
91
  console.error(" Exemplo: ai-tool impact Button");
84
92
  process.exit(1);
85
93
  }
86
- result = await impact(target, { format, cwd });
94
+ result = await impact(target, { format, cwd, cache });
87
95
  break;
88
96
  default:
89
97
  console.error(`\u274C Comando desconhecido: ${command}`);
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ interface CommandOptions {
7
7
  format?: OutputFormat;
8
8
  cwd?: string;
9
9
  save?: boolean;
10
+ cache?: boolean;
10
11
  }
11
12
  interface MapOptions extends CommandOptions {
12
13
  trackDependencies?: boolean;
@@ -147,6 +148,19 @@ declare function isEntryPoint(filePath: string): boolean;
147
148
  */
148
149
  declare function isCodeFile(filePath: string): boolean;
149
150
 
151
+ /**
152
+ * Obtém o caminho da pasta de cache
153
+ */
154
+ declare function getCacheDir(cwd: string): string;
155
+ /**
156
+ * Verifica se o cache existe e é válido
157
+ */
158
+ declare function isCacheValid(cwd: string): boolean;
159
+ /**
160
+ * Invalida o cache (força regeneração)
161
+ */
162
+ declare function invalidateCache(cwd: string): void;
163
+
150
164
  /**
151
165
  * ai-tool - Ferramenta de análise de dependências e impacto
152
166
  *
@@ -167,6 +181,6 @@ declare function isCodeFile(filePath: string): boolean;
167
181
  * ```
168
182
  */
169
183
 
170
- declare const VERSION = "0.1.0";
184
+ declare const VERSION = "0.2.0";
171
185
 
172
- export { type CommandOptions, type DeadFile, type DeadOptions, type DeadResult, type FileCategory, type FileInfo, type FolderStats, type ImpactFile, type ImpactOptions, type ImpactResult, type MapOptions, type MapResult, type OutputFormat, type RiskInfo, VERSION, categoryIcons, dead, deadFix, detectCategory, impact, isCodeFile, isEntryPoint, map };
186
+ export { type CommandOptions, type DeadFile, type DeadOptions, type DeadResult, type FileCategory, type FileInfo, type FolderStats, type ImpactFile, type ImpactOptions, type ImpactResult, type MapOptions, type MapResult, type OutputFormat, type RiskInfo, VERSION, categoryIcons, dead, deadFix, detectCategory, getCacheDir, impact, invalidateCache, isCacheValid, isCodeFile, isEntryPoint, map };
package/dist/index.js CHANGED
@@ -4,18 +4,24 @@ import {
4
4
  dead,
5
5
  deadFix,
6
6
  detectCategory,
7
+ getCacheDir,
7
8
  impact,
9
+ invalidateCache,
10
+ isCacheValid,
8
11
  isCodeFile,
9
12
  isEntryPoint,
10
13
  map
11
- } from "./chunk-EGBEXF4G.js";
14
+ } from "./chunk-HOVHD2N4.js";
12
15
  export {
13
16
  VERSION,
14
17
  categoryIcons,
15
18
  dead,
16
19
  deadFix,
17
20
  detectCategory,
21
+ getCacheDir,
18
22
  impact,
23
+ invalidateCache,
24
+ isCacheValid,
19
25
  isCodeFile,
20
26
  isEntryPoint,
21
27
  map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/ai-tool",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Ferramenta de análise de dependências e impacto para projetos TypeScript/JavaScript. Usa Skott + Knip internamente.",
5
5
  "keywords": [
6
6
  "dependency-analysis",
@@ -18,13 +18,13 @@
18
18
  "license": "MIT",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/anthropic-studio/ai-tool"
21
+ "url": "git+https://github.com/anthropic-studio/ai-tool.git"
22
22
  },
23
23
  "type": "module",
24
24
  "main": "./dist/index.js",
25
25
  "types": "./dist/index.d.ts",
26
26
  "bin": {
27
- "ai-tool": "./dist/cli.js"
27
+ "ai-tool": "dist/cli.js"
28
28
  },
29
29
  "exports": {
30
30
  ".": {