blast-radius-analyzer 1.2.1 → 1.3.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "/Users/didi/Documents/code3/logic-inverse-v2/blast-radius-analyzer/dist/index.js": {
3
+ "mtime": 1774934096510.6582,
4
+ "hash": "16fb35ae"
5
+ },
6
+ "/Users/didi/Documents/code3/logic-inverse-v2/blast-radius-analyzer/src/index.ts": {
7
+ "mtime": 1774934091686.9543,
8
+ "hash": "4feb087c"
9
+ }
10
+ }
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ function parseArgs(argv) {
30
30
  let format = 'text';
31
31
  let useCache = true;
32
32
  let clearCache = false;
33
+ let autoDetect = false; // 自动检测 git diff
33
34
  let threshold = undefined;
34
35
  for (let i = 2; i < argv.length; i++) {
35
36
  const arg = argv[i];
@@ -80,6 +81,9 @@ function parseArgs(argv) {
80
81
  else if (arg === '--clear-cache') {
81
82
  clearCache = true;
82
83
  }
84
+ else if (arg === '--auto' || arg === '-a') {
85
+ autoDetect = true;
86
+ }
83
87
  else if (arg === '--threshold' && argv[i + 1]) {
84
88
  // 解析阈值: --threshold files:5,score:100,typeErrors:0
85
89
  const thresholdStr = argv[++i];
@@ -115,6 +119,7 @@ function parseArgs(argv) {
115
119
  format,
116
120
  useCache,
117
121
  clearCache,
122
+ autoDetect,
118
123
  threshold: threshold,
119
124
  };
120
125
  }
@@ -132,6 +137,7 @@ Options:
132
137
  -c, --change <file> File that changed (can be specified multiple times)
133
138
  --symbol <name> Specific symbol that changed (function/class name)
134
139
  --type <type> Change type: add|modify|delete|rename (default: modify)
140
+ -a, --auto Auto-detect changes via git diff
135
141
  --max-depth <n> Maximum analysis depth (default: 10)
136
142
  -t, --include-tests Include test files in analysis
137
143
  -o, --output <file> Output file (auto-detect format from extension)
@@ -151,6 +157,9 @@ CI/CD Examples:
151
157
  blast-radius -p ./src -c src/api/task.ts --threshold files:3 -o result.json
152
158
 
153
159
  Examples:
160
+ # Auto-detect all changes via git diff
161
+ blast-radius -p ./src --auto
162
+
154
163
  # Analyze a single file change
155
164
  blast-radius -p ./src -c src/api/user.ts
156
165
 
@@ -760,11 +769,169 @@ function formatHtml(scope, callChains, callStackView, typeFlowResult, dataFlowRe
760
769
  </html>`;
761
770
  return html;
762
771
  }
772
+ /**
773
+ * 使用 git diff 自动检测改动的文件和符号
774
+ */
775
+ async function autoDetectChanges(projectRoot) {
776
+ const { execSync } = await import('child_process');
777
+ try {
778
+ // 获取 staged + unstaged 的改动
779
+ const diffOutput = execSync('git diff --cached --name-only HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || git diff --name-only 2>/dev/null', {
780
+ cwd: projectRoot,
781
+ encoding: 'utf8',
782
+ stdio: ['pipe', 'pipe', 'pipe'],
783
+ });
784
+ const changedFiles = diffOutput.trim().split('\n').filter(f => f.trim());
785
+ if (changedFiles.length === 0 || changedFiles[0] === '') {
786
+ // 没有 git 改动,检查工作区
787
+ const unstaged = execSync('git diff --name-only 2>/dev/null || echo ""', {
788
+ cwd: projectRoot,
789
+ encoding: 'utf8',
790
+ }).trim().split('\n').filter(f => f.trim());
791
+ if (unstaged.length === 0 || unstaged[0] === '') {
792
+ return [];
793
+ }
794
+ changedFiles.length = 0;
795
+ changedFiles.push(...unstaged);
796
+ }
797
+ // 过滤只保留 TypeScript/JavaScript 文件
798
+ const codeFiles = changedFiles.filter(f => /\.(ts|tsx|js|jsx)$/.test(f) && !f.includes('node_modules'));
799
+ if (codeFiles.length === 0) {
800
+ return [];
801
+ }
802
+ // 获取每个文件的详细 diff,包括函数/变量名
803
+ const changedSymbols = [];
804
+ for (const file of codeFiles) {
805
+ try {
806
+ // 获取文件的 diff
807
+ const fileDiff = execSync(`git diff HEAD -- "${file}" 2>/dev/null || git diff -- "${file}" 2>/dev/null || echo ""`, {
808
+ cwd: projectRoot,
809
+ encoding: 'utf8',
810
+ stdio: ['pipe', 'pipe', 'pipe'],
811
+ });
812
+ // 解析 diff 中的符号 (函数名、变量名等)
813
+ const symbols = parseDiffSymbols(fileDiff, file);
814
+ if (symbols.length > 0) {
815
+ for (const sym of symbols) {
816
+ changedSymbols.push({
817
+ file: path.resolve(projectRoot, file),
818
+ symbol: sym,
819
+ type: 'modify',
820
+ diff: fileDiff,
821
+ });
822
+ }
823
+ }
824
+ else {
825
+ // 如果无法解析符号,仍添加文件
826
+ changedSymbols.push({
827
+ file: path.resolve(projectRoot, file),
828
+ symbol: '', // 空符号表示分析整个文件
829
+ type: 'modify',
830
+ diff: fileDiff,
831
+ });
832
+ }
833
+ }
834
+ catch {
835
+ // 单个文件出错继续处理其他的
836
+ }
837
+ }
838
+ return changedSymbols;
839
+ }
840
+ catch (error) {
841
+ console.error('⚠️ 无法获取 git diff:', error.message);
842
+ return [];
843
+ }
844
+ }
845
+ /**
846
+ * 从 diff 中解析改动的符号名
847
+ */
848
+ function parseDiffSymbols(diffContent, file) {
849
+ const symbols = [];
850
+ const lines = diffContent.split('\n');
851
+ // 匹配函数定义: +functionName, +const functionName, +export functionName
852
+ const functionPattern = /^[+]export\s+(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)|(\w+)\s*=)/;
853
+ // 匹配变量声明: +export const name, +export let name, +export var name
854
+ const constPattern = /^[+]export\s+(?:const|let|var)\s+(\w+)/;
855
+ // 匹配类型定义: +export type Name, +export interface Name
856
+ const typePattern = /^[+]export\s+(?:type|interface|class|enum)\s+(\w+)/;
857
+ // 匹配 class 定义: +class ClassName
858
+ const classPattern = /^[+]class\s+(\w+)/;
859
+ for (const line of lines) {
860
+ // 跳过 diff header
861
+ if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff')) {
862
+ continue;
863
+ }
864
+ // 匹配函数
865
+ let match = line.match(functionPattern);
866
+ if (match) {
867
+ const name = match[1] || match[2] || match[3];
868
+ if (name && !symbols.includes(name)) {
869
+ symbols.push(name);
870
+ }
871
+ }
872
+ // 匹配 const/let/var
873
+ match = line.match(constPattern);
874
+ if (match && match[1]) {
875
+ if (!symbols.includes(match[1])) {
876
+ symbols.push(match[1]);
877
+ }
878
+ }
879
+ // 匹配类型
880
+ match = line.match(typePattern);
881
+ if (match && match[1]) {
882
+ if (!symbols.includes(match[1])) {
883
+ symbols.push(match[1]);
884
+ }
885
+ }
886
+ // 匹配 class
887
+ match = line.match(classPattern);
888
+ if (match && match[1]) {
889
+ if (!symbols.includes(match[1])) {
890
+ symbols.push(match[1]);
891
+ }
892
+ }
893
+ }
894
+ return symbols;
895
+ }
763
896
  // ─── Main ────────────────────────────────────────────────────────────────────
764
897
  async function main() {
765
898
  const args = parseArgs(process.argv);
899
+ // 如果指定了 --auto,自动检测 git 改动
900
+ if (args.autoDetect) {
901
+ console.log('🔍 Auto-detecting changes via git diff...');
902
+ const detectedChanges = await autoDetectChanges(args.projectRoot);
903
+ if (detectedChanges.length === 0) {
904
+ console.log('✅ No code changes detected (or not a git repository)');
905
+ process.exit(0);
906
+ }
907
+ console.log(`📝 Found ${detectedChanges.length} changed symbol(s):\n`);
908
+ for (const change of detectedChanges) {
909
+ const relPath = path.relative(args.projectRoot, change.file);
910
+ const symDisplay = change.symbol ? `#${change.symbol}` : '(file)';
911
+ console.log(` • ${relPath}${symDisplay}`);
912
+ }
913
+ console.log('');
914
+ // 将检测到的改动合并到 args.changes
915
+ for (const change of detectedChanges) {
916
+ // 检查是否已存在相同的文件
917
+ const existing = args.changes.find(c => c.file === change.file);
918
+ if (existing) {
919
+ // 如果已有符号,保留;否则添加新符号
920
+ if (!existing.symbol && change.symbol) {
921
+ existing.symbol = change.symbol;
922
+ }
923
+ }
924
+ else {
925
+ args.changes.push({
926
+ file: change.file,
927
+ symbol: change.symbol || undefined,
928
+ type: change.type,
929
+ });
930
+ }
931
+ }
932
+ }
766
933
  if (args.changes.length === 0) {
767
- console.error('❌ Error: No changes specified. Use --change <file>');
934
+ console.error('❌ Error: No changes specified. Use --change <file> or --auto');
768
935
  console.error(' Run with --help for usage information');
769
936
  process.exit(1);
770
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blast-radius-analyzer",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Analyze code change impact and blast radius - 改动影响范围分析器",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "license": "MIT",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": ""
27
+ "url": "https://github.com/huabuyu05100510/blast-radius-analyzer"
28
28
  },
29
29
  "dependencies": {
30
30
  "ts-morph": "^25.0.0"
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ interface CLIArgs {
39
39
  format: 'json' | 'text' | 'html' | 'graph';
40
40
  useCache: boolean;
41
41
  clearCache: boolean;
42
+ autoDetect: boolean; // 自动检测 git diff
42
43
  threshold?: {
43
44
  files?: number;
44
45
  score?: number;
@@ -57,6 +58,7 @@ function parseArgs(argv: string[]): CLIArgs {
57
58
  let format: 'json' | 'text' | 'html' | 'graph' = 'text';
58
59
  let useCache = true;
59
60
  let clearCache = false;
61
+ let autoDetect = false; // 自动检测 git diff
60
62
  let threshold: CLIArgs['threshold'] = undefined;
61
63
 
62
64
  for (let i = 2; i < argv.length; i++) {
@@ -95,6 +97,8 @@ function parseArgs(argv: string[]): CLIArgs {
95
97
  useCache = false;
96
98
  } else if (arg === '--clear-cache') {
97
99
  clearCache = true;
100
+ } else if (arg === '--auto' || arg === '-a') {
101
+ autoDetect = true;
98
102
  } else if (arg === '--threshold' && argv[i + 1]) {
99
103
  // 解析阈值: --threshold files:5,score:100,typeErrors:0
100
104
  const thresholdStr = argv[++i];
@@ -128,6 +132,7 @@ function parseArgs(argv: string[]): CLIArgs {
128
132
  format,
129
133
  useCache,
130
134
  clearCache,
135
+ autoDetect,
131
136
  threshold: threshold,
132
137
  };
133
138
  }
@@ -146,6 +151,7 @@ Options:
146
151
  -c, --change <file> File that changed (can be specified multiple times)
147
152
  --symbol <name> Specific symbol that changed (function/class name)
148
153
  --type <type> Change type: add|modify|delete|rename (default: modify)
154
+ -a, --auto Auto-detect changes via git diff
149
155
  --max-depth <n> Maximum analysis depth (default: 10)
150
156
  -t, --include-tests Include test files in analysis
151
157
  -o, --output <file> Output file (auto-detect format from extension)
@@ -165,6 +171,9 @@ CI/CD Examples:
165
171
  blast-radius -p ./src -c src/api/task.ts --threshold files:3 -o result.json
166
172
 
167
173
  Examples:
174
+ # Auto-detect all changes via git diff
175
+ blast-radius -p ./src --auto
176
+
168
177
  # Analyze a single file change
169
178
  blast-radius -p ./src -c src/api/user.ts
170
179
 
@@ -832,13 +841,202 @@ function formatHtml(
832
841
  return html;
833
842
  }
834
843
 
844
+ // ─── Auto Detect Changes via Git ───────────────────────────────────────────
845
+
846
+ interface ChangedSymbol {
847
+ file: string;
848
+ symbol: string;
849
+ type: 'modify' | 'delete' | 'rename' | 'add';
850
+ diff: string; // diff content for context
851
+ }
852
+
853
+ /**
854
+ * 使用 git diff 自动检测改动的文件和符号
855
+ */
856
+ async function autoDetectChanges(projectRoot: string): Promise<ChangedSymbol[]> {
857
+ const { execSync } = await import('child_process');
858
+
859
+ try {
860
+ // 获取 staged + unstaged 的改动
861
+ const diffOutput = execSync('git diff --cached --name-only HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || git diff --name-only 2>/dev/null', {
862
+ cwd: projectRoot,
863
+ encoding: 'utf8',
864
+ stdio: ['pipe', 'pipe', 'pipe'],
865
+ });
866
+
867
+ const changedFiles = diffOutput.trim().split('\n').filter(f => f.trim());
868
+
869
+ if (changedFiles.length === 0 || changedFiles[0] === '') {
870
+ // 没有 git 改动,检查工作区
871
+ const unstaged = execSync('git diff --name-only 2>/dev/null || echo ""', {
872
+ cwd: projectRoot,
873
+ encoding: 'utf8',
874
+ }).trim().split('\n').filter(f => f.trim());
875
+
876
+ if (unstaged.length === 0 || unstaged[0] === '') {
877
+ return [];
878
+ }
879
+ changedFiles.length = 0;
880
+ changedFiles.push(...unstaged);
881
+ }
882
+
883
+ // 过滤只保留 TypeScript/JavaScript 文件
884
+ const codeFiles = changedFiles.filter(f =>
885
+ /\.(ts|tsx|js|jsx)$/.test(f) && !f.includes('node_modules')
886
+ );
887
+
888
+ if (codeFiles.length === 0) {
889
+ return [];
890
+ }
891
+
892
+ // 获取每个文件的详细 diff,包括函数/变量名
893
+ const changedSymbols: ChangedSymbol[] = [];
894
+
895
+ for (const file of codeFiles) {
896
+ try {
897
+ // 获取文件的 diff
898
+ const fileDiff = execSync(`git diff HEAD -- "${file}" 2>/dev/null || git diff -- "${file}" 2>/dev/null || echo ""`, {
899
+ cwd: projectRoot,
900
+ encoding: 'utf8',
901
+ stdio: ['pipe', 'pipe', 'pipe'],
902
+ });
903
+
904
+ // 解析 diff 中的符号 (函数名、变量名等)
905
+ const symbols = parseDiffSymbols(fileDiff, file);
906
+
907
+ if (symbols.length > 0) {
908
+ for (const sym of symbols) {
909
+ changedSymbols.push({
910
+ file: path.resolve(projectRoot, file),
911
+ symbol: sym,
912
+ type: 'modify',
913
+ diff: fileDiff,
914
+ });
915
+ }
916
+ } else {
917
+ // 如果无法解析符号,仍添加文件
918
+ changedSymbols.push({
919
+ file: path.resolve(projectRoot, file),
920
+ symbol: '', // 空符号表示分析整个文件
921
+ type: 'modify',
922
+ diff: fileDiff,
923
+ });
924
+ }
925
+ } catch {
926
+ // 单个文件出错继续处理其他的
927
+ }
928
+ }
929
+
930
+ return changedSymbols;
931
+ } catch (error) {
932
+ console.error('⚠️ 无法获取 git diff:', (error as Error).message);
933
+ return [];
934
+ }
935
+ }
936
+
937
+ /**
938
+ * 从 diff 中解析改动的符号名
939
+ */
940
+ function parseDiffSymbols(diffContent: string, file: string): string[] {
941
+ const symbols: string[] = [];
942
+ const lines = diffContent.split('\n');
943
+
944
+ // 匹配函数定义: +functionName, +const functionName, +export functionName
945
+ const functionPattern = /^[+]export\s+(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)|(\w+)\s*=)/;
946
+ // 匹配变量声明: +export const name, +export let name, +export var name
947
+ const constPattern = /^[+]export\s+(?:const|let|var)\s+(\w+)/;
948
+ // 匹配类型定义: +export type Name, +export interface Name
949
+ const typePattern = /^[+]export\s+(?:type|interface|class|enum)\s+(\w+)/;
950
+ // 匹配 class 定义: +class ClassName
951
+ const classPattern = /^[+]class\s+(\w+)/;
952
+
953
+ for (const line of lines) {
954
+ // 跳过 diff header
955
+ if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff')) {
956
+ continue;
957
+ }
958
+
959
+ // 匹配函数
960
+ let match = line.match(functionPattern);
961
+ if (match) {
962
+ const name = match[1] || match[2] || match[3];
963
+ if (name && !symbols.includes(name)) {
964
+ symbols.push(name);
965
+ }
966
+ }
967
+
968
+ // 匹配 const/let/var
969
+ match = line.match(constPattern);
970
+ if (match && match[1]) {
971
+ if (!symbols.includes(match[1])) {
972
+ symbols.push(match[1]);
973
+ }
974
+ }
975
+
976
+ // 匹配类型
977
+ match = line.match(typePattern);
978
+ if (match && match[1]) {
979
+ if (!symbols.includes(match[1])) {
980
+ symbols.push(match[1]);
981
+ }
982
+ }
983
+
984
+ // 匹配 class
985
+ match = line.match(classPattern);
986
+ if (match && match[1]) {
987
+ if (!symbols.includes(match[1])) {
988
+ symbols.push(match[1]);
989
+ }
990
+ }
991
+ }
992
+
993
+ return symbols;
994
+ }
995
+
835
996
  // ─── Main ────────────────────────────────────────────────────────────────────
836
997
 
837
998
  async function main(): Promise<void> {
838
999
  const args = parseArgs(process.argv);
839
1000
 
1001
+ // 如果指定了 --auto,自动检测 git 改动
1002
+ if (args.autoDetect) {
1003
+ console.log('🔍 Auto-detecting changes via git diff...');
1004
+ const detectedChanges = await autoDetectChanges(args.projectRoot);
1005
+
1006
+ if (detectedChanges.length === 0) {
1007
+ console.log('✅ No code changes detected (or not a git repository)');
1008
+ process.exit(0);
1009
+ }
1010
+
1011
+ console.log(`📝 Found ${detectedChanges.length} changed symbol(s):\n`);
1012
+ for (const change of detectedChanges) {
1013
+ const relPath = path.relative(args.projectRoot, change.file);
1014
+ const symDisplay = change.symbol ? `#${change.symbol}` : '(file)';
1015
+ console.log(` • ${relPath}${symDisplay}`);
1016
+ }
1017
+ console.log('');
1018
+
1019
+ // 将检测到的改动合并到 args.changes
1020
+ for (const change of detectedChanges) {
1021
+ // 检查是否已存在相同的文件
1022
+ const existing = args.changes.find(c => c.file === change.file);
1023
+ if (existing) {
1024
+ // 如果已有符号,保留;否则添加新符号
1025
+ if (!existing.symbol && change.symbol) {
1026
+ existing.symbol = change.symbol;
1027
+ }
1028
+ } else {
1029
+ args.changes.push({
1030
+ file: change.file,
1031
+ symbol: change.symbol || undefined,
1032
+ type: change.type,
1033
+ });
1034
+ }
1035
+ }
1036
+ }
1037
+
840
1038
  if (args.changes.length === 0) {
841
- console.error('❌ Error: No changes specified. Use --change <file>');
1039
+ console.error('❌ Error: No changes specified. Use --change <file> or --auto');
842
1040
  console.error(' Run with --help for usage information');
843
1041
  process.exit(1);
844
1042
  }