@boshu2/vibe-check 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.agents/plans/2025-12-29-complexity-driver-plan.md +225 -0
  2. package/.agents/plans/2025-12-29-complexity-drivers-plan.md +253 -0
  3. package/.agents/research/2025-12-29-complexity-driver-architecture.md +392 -0
  4. package/.agents/research/2025-12-29-complexity-drivers.md +227 -0
  5. package/.beads/issues.jsonl +12 -0
  6. package/CHANGELOG.md +27 -0
  7. package/README.md +71 -0
  8. package/dist/analyzers/complexity.d.ts +92 -0
  9. package/dist/analyzers/complexity.d.ts.map +1 -0
  10. package/dist/analyzers/complexity.js +79 -0
  11. package/dist/analyzers/complexity.js.map +1 -0
  12. package/dist/analyzers/modularity.d.ts +3 -1
  13. package/dist/analyzers/modularity.d.ts.map +1 -1
  14. package/dist/analyzers/modularity.js +32 -6
  15. package/dist/analyzers/modularity.js.map +1 -1
  16. package/dist/cli.js +2 -1
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/driver.d.ts +18 -0
  19. package/dist/commands/driver.d.ts.map +1 -0
  20. package/dist/commands/driver.js +58 -0
  21. package/dist/commands/driver.js.map +1 -0
  22. package/dist/commands/index.d.ts +1 -0
  23. package/dist/commands/index.d.ts.map +1 -1
  24. package/dist/commands/index.js +1 -0
  25. package/dist/commands/index.js.map +1 -1
  26. package/dist/commands/modularity.d.ts +2 -0
  27. package/dist/commands/modularity.d.ts.map +1 -1
  28. package/dist/commands/modularity.js +86 -7
  29. package/dist/commands/modularity.js.map +1 -1
  30. package/drivers/README.md +327 -0
  31. package/drivers/go.sh +131 -0
  32. package/drivers/java.sh +137 -0
  33. package/drivers/javascript.sh +134 -0
  34. package/drivers/php.sh +132 -0
  35. package/drivers/python.sh +90 -0
  36. package/drivers/rust.sh +132 -0
  37. package/package.json +1 -1
package/drivers/go.sh ADDED
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # drivers/go.sh
3
+ # Wraps gocyclo to produce standard complexity JSON
4
+ #
5
+ # Usage: ./drivers/go.sh [directory]
6
+ # directory: Path to Go code to analyze (default: current directory)
7
+ #
8
+ # Output: JSON conforming to ComplexityReport schema
9
+ # Exit codes: 0 = success, 1 = error (gocyclo not installed or other failure)
10
+
11
+ set -euo pipefail
12
+
13
+ TARGET_DIR="${1:-.}"
14
+
15
+ # Check gocyclo is installed
16
+ if ! command -v gocyclo &> /dev/null; then
17
+ echo '{"error": "gocyclo not installed. Run: go install github.com/fzipp/gocyclo/cmd/gocyclo@latest"}' >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Check if target directory exists
22
+ if [ ! -d "$TARGET_DIR" ]; then
23
+ echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # Check if there are any Go files
28
+ if ! find "$TARGET_DIR" -name "*.go" ! -path "*/vendor/*" -print -quit 2>/dev/null | grep -q .; then
29
+ # No Go files found, output empty result
30
+ echo '{"tool":"gocyclo","language":"go","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
31
+ exit 0
32
+ fi
33
+
34
+ # Run gocyclo and transform output
35
+ # gocyclo outputs lines like:
36
+ # 8 main complex /tmp/go-test/main.go:16:1
37
+ # <complexity> <package> <function> <file:row:column>
38
+
39
+ gocyclo "$TARGET_DIR" 2>/dev/null | awk -v target_dir="$TARGET_DIR" '
40
+ BEGIN {
41
+ # Initialize
42
+ file_count = 0
43
+ func_count = 0
44
+ total_complexity = 0
45
+ grade_a = 0; grade_b = 0; grade_c = 0; grade_d = 0; grade_e = 0; grade_f = 0
46
+ }
47
+
48
+ function complexity_to_grade(c) {
49
+ if (c <= 5) return "A"
50
+ else if (c <= 10) return "B"
51
+ else if (c <= 20) return "C"
52
+ else if (c <= 30) return "D"
53
+ else if (c <= 40) return "E"
54
+ else return "F"
55
+ }
56
+
57
+ function increment_grade(g) {
58
+ if (g == "A") grade_a++
59
+ else if (g == "B") grade_b++
60
+ else if (g == "C") grade_c++
61
+ else if (g == "D") grade_d++
62
+ else if (g == "E") grade_e++
63
+ else grade_f++
64
+ }
65
+
66
+ {
67
+ # Parse: complexity package function file:line:col
68
+ complexity = $1
69
+ pkg = $2
70
+ func_name = $3
71
+ file_line = $4
72
+
73
+ # Split file:line:col
74
+ split(file_line, parts, ":")
75
+ file = parts[1]
76
+ line = parts[2]
77
+
78
+ # Track functions per file
79
+ if (!(file in files)) {
80
+ files[file] = 1
81
+ file_count++
82
+ file_functions[file] = ""
83
+ file_complexities[file] = ""
84
+ file_total[file] = 0
85
+ file_max[file] = 0
86
+ file_func_count[file] = 0
87
+ }
88
+
89
+ func_count++
90
+ total_complexity += complexity
91
+
92
+ grade = complexity_to_grade(complexity)
93
+
94
+ # Append function data (JSON format)
95
+ sep = (file_functions[file] == "") ? "" : ","
96
+ file_functions[file] = file_functions[file] sep sprintf("{\"name\":\"%s\",\"complexity\":%d,\"grade\":\"%s\",\"line\":%d,\"endLine\":null}", func_name, complexity, grade, line)
97
+
98
+ # Track file stats
99
+ file_total[file] += complexity
100
+ file_func_count[file]++
101
+ if (complexity > file_max[file]) {
102
+ file_max[file] = complexity
103
+ }
104
+ }
105
+
106
+ END {
107
+ # Output JSON
108
+ printf "{\"tool\":\"gocyclo\",\"language\":\"go\",\"generatedAt\":\""
109
+ # Get current timestamp
110
+ cmd = "date -u +\"%Y-%m-%dT%H:%M:%SZ\""
111
+ cmd | getline timestamp
112
+ close(cmd)
113
+ printf "%s\",\"files\":{", timestamp
114
+
115
+ first_file = 1
116
+ for (file in files) {
117
+ if (!first_file) printf ","
118
+ first_file = 0
119
+
120
+ avg = (file_func_count[file] > 0) ? file_total[file] / file_func_count[file] : 0
121
+ file_grade = complexity_to_grade(avg)
122
+ increment_grade(file_grade)
123
+
124
+ printf "\"%s\":{\"functions\":[%s],\"avgComplexity\":%.2f,\"maxComplexity\":%d,\"grade\":\"%s\"}", file, file_functions[file], avg, file_max[file], file_grade
125
+ }
126
+
127
+ avg_complexity = (func_count > 0) ? total_complexity / func_count : 0
128
+
129
+ printf "},\"summary\":{\"totalFiles\":%d,\"totalFunctions\":%d,\"avgComplexity\":%.2f,\"gradeDistribution\":{\"A\":%d,\"B\":%d,\"C\":%d,\"D\":%d,\"E\":%d,\"F\":%d}}}", file_count, func_count, avg_complexity, grade_a, grade_b, grade_c, grade_d, grade_e, grade_f
130
+ }
131
+ '
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ # drivers/java.sh
3
+ # Wraps PMD to produce standard complexity JSON
4
+ #
5
+ # Usage: ./drivers/java.sh [directory]
6
+ # directory: Path to Java code to analyze (default: current directory)
7
+ #
8
+ # Output: JSON conforming to ComplexityReport schema
9
+ # Exit codes: 0 = success, 1 = error (pmd not installed or other failure)
10
+
11
+ set -euo pipefail
12
+
13
+ TARGET_DIR="${1:-.}"
14
+
15
+ # Check pmd is installed
16
+ if ! command -v pmd &> /dev/null; then
17
+ echo '{"error": "pmd not installed. Download from: https://pmd.github.io/"}' >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Check if target directory exists
22
+ if [ ! -d "$TARGET_DIR" ]; then
23
+ echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # Check if there are any Java files
28
+ if ! find "$TARGET_DIR" -name "*.java" ! -path "*/target/*" ! -path "*/build/*" -print -quit 2>/dev/null | grep -q .; then
29
+ # No Java files found, output empty result
30
+ echo '{"tool":"pmd","language":"java","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
31
+ exit 0
32
+ fi
33
+
34
+ # Run PMD and transform output
35
+ # PMD outputs JSON with violations grouped by file
36
+ # We filter for CyclomaticComplexity rule violations
37
+ # Using --no-cache to avoid caching issues
38
+ # --ignore-violations to ensure exit 0 even with violations found
39
+
40
+ pmd check -d "$TARGET_DIR" \
41
+ -R category/java/design.xml/CyclomaticComplexity \
42
+ -f json \
43
+ --no-cache \
44
+ --ignore-violations-on-exit 2>/dev/null | jq -c '
45
+ # Helper function for complexity to grade conversion
46
+ def complexity_to_grade:
47
+ if . <= 5 then "A"
48
+ elif . <= 10 then "B"
49
+ elif . <= 20 then "C"
50
+ elif . <= 30 then "D"
51
+ elif . <= 40 then "E"
52
+ else "F"
53
+ end;
54
+
55
+ # Process PMD violations format
56
+ # PMD JSON format has .files array with violations
57
+ (.files // []) |
58
+ map(
59
+ select(.violations | length > 0) |
60
+ {
61
+ path: .filename,
62
+ violations: [
63
+ .violations[] |
64
+ # PMD description format: "The {class|method} {name} has a {Cyclomatic|Standard} Complexity of {X}."
65
+ # or "The {class|method} '{name}' has a {Cyclomatic|Standard} cyclomatic complexity of {X}."
66
+ {
67
+ description: .description,
68
+ method: (.description | capture("method .(?<name>[^']+).") | .name),
69
+ beginLine: .beginLine,
70
+ endLine: .endLine,
71
+ # Extract complexity number from description
72
+ complexity: (
73
+ .description |
74
+ capture("complexity of (?<num>[0-9]+)") |
75
+ .num | tonumber
76
+ )
77
+ }
78
+ ]
79
+ } |
80
+ select(.violations | length > 0)
81
+ ) |
82
+
83
+ # Group by file and calculate metrics
84
+ map({
85
+ key: .path,
86
+ value: {
87
+ functions: (
88
+ .violations | map({
89
+ name: .method,
90
+ complexity: .complexity,
91
+ grade: (.complexity | complexity_to_grade),
92
+ line: .beginLine,
93
+ endLine: .endLine
94
+ })
95
+ ),
96
+ avgComplexity: (
97
+ if (.violations | length) > 0 then
98
+ ((.violations | map(.complexity) | add) / (.violations | length))
99
+ else 0 end
100
+ ),
101
+ maxComplexity: (
102
+ if (.violations | length) > 0 then
103
+ (.violations | map(.complexity) | max)
104
+ else 0 end
105
+ ),
106
+ grade: (
107
+ if (.violations | length) > 0 then
108
+ (((.violations | map(.complexity) | add) / (.violations | length)) | complexity_to_grade)
109
+ else "A" end
110
+ )
111
+ }
112
+ }) |
113
+ from_entries as $files |
114
+
115
+ # Build final output
116
+ {
117
+ tool: "pmd",
118
+ language: "java",
119
+ generatedAt: (now | todate),
120
+ files: $files,
121
+ summary: {
122
+ totalFiles: ($files | to_entries | length),
123
+ totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
124
+ avgComplexity: (
125
+ [$files | to_entries[].value.avgComplexity] |
126
+ if length > 0 then (add / length) else 0 end
127
+ ),
128
+ gradeDistribution: (
129
+ [$files | to_entries[].value.grade] |
130
+ reduce .[] as $grade (
131
+ {A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
132
+ .[$grade] += 1
133
+ )
134
+ )
135
+ }
136
+ }
137
+ '
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+ # drivers/javascript.sh
3
+ # Wraps cyclomatic-complexity to produce standard complexity JSON
4
+ #
5
+ # Usage: ./drivers/javascript.sh [directory]
6
+ # directory: Path to JavaScript/TypeScript code to analyze (default: current directory)
7
+ #
8
+ # Output: JSON conforming to ComplexityReport schema
9
+ # Exit codes: 0 = success, 1 = error (npx not available or other failure)
10
+
11
+ set -euo pipefail
12
+
13
+ TARGET_DIR="${1:-.}"
14
+
15
+ # Check if npx is available
16
+ if ! command -v npx &> /dev/null; then
17
+ echo '{"error": "npx not found. Install Node.js and npm."}' >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Check if target directory exists
22
+ if [ ! -d "$TARGET_DIR" ]; then
23
+ echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # Find all JS/TS files (excluding common build/dep directories)
28
+ FILES=$(find "$TARGET_DIR" -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) \
29
+ ! -path "*/node_modules/*" \
30
+ ! -path "*/dist/*" \
31
+ ! -path "*/build/*" \
32
+ ! -path "*/.next/*" \
33
+ ! -path "*/coverage/*" 2>/dev/null)
34
+
35
+ # If no files found, output empty result
36
+ if [ -z "$FILES" ]; then
37
+ echo '{"tool":"cyclomatic-complexity","language":"javascript","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
38
+ exit 0
39
+ fi
40
+
41
+ # Build glob pattern for cyclomatic-complexity
42
+ # It expects quoted glob patterns like 'src/**/*.ts'
43
+ PATTERN="${TARGET_DIR}/**/*.{js,ts,jsx,tsx}"
44
+
45
+ # Run cyclomatic-complexity and transform output
46
+ # cyclomatic-complexity outputs JSON like:
47
+ # [
48
+ # {
49
+ # "file": "path/to/file.js",
50
+ # "functionComplexities": [
51
+ # {"name": "func", "complexity": 5, "line": 10}
52
+ # ],
53
+ # "complexitySum": 5,
54
+ # "complexityLevel": "ok"
55
+ # }
56
+ # ]
57
+
58
+ npx --yes cyclomatic-complexity "$PATTERN" --json 2>/dev/null | jq -c '
59
+ # Filter out empty files and global scope functions
60
+ map(
61
+ select(.functionComplexities | length > 0) |
62
+ . + {functionComplexities: [.functionComplexities[] | select(.name != "global")]}
63
+ ) |
64
+ map(select(.functionComplexities | length > 0)) |
65
+
66
+ # Transform to standard schema
67
+ (
68
+ map({
69
+ key: .file,
70
+ value: {
71
+ functions: (
72
+ .functionComplexities | map({
73
+ name: .name,
74
+ complexity: .complexity,
75
+ grade: (
76
+ if .complexity <= 5 then "A"
77
+ elif .complexity <= 10 then "B"
78
+ elif .complexity <= 20 then "C"
79
+ elif .complexity <= 30 then "D"
80
+ elif .complexity <= 40 then "E"
81
+ else "F"
82
+ end
83
+ ),
84
+ line: .line,
85
+ endLine: null
86
+ })
87
+ ),
88
+ avgComplexity: (
89
+ if (.functionComplexities | length) > 0 then
90
+ ((.functionComplexities | map(.complexity) | add) / (.functionComplexities | length))
91
+ else 0 end
92
+ ),
93
+ maxComplexity: (
94
+ if (.functionComplexities | length) > 0 then
95
+ (.functionComplexities | map(.complexity) | max)
96
+ else 0 end
97
+ ),
98
+ grade: (
99
+ if (.functionComplexities | length) > 0 then
100
+ ((.functionComplexities | map(.complexity) | add) / (.functionComplexities | length)) as $avg |
101
+ if $avg <= 5 then "A"
102
+ elif $avg <= 10 then "B"
103
+ elif $avg <= 20 then "C"
104
+ elif $avg <= 30 then "D"
105
+ elif $avg <= 40 then "E"
106
+ else "F"
107
+ end
108
+ else "A" end
109
+ )
110
+ }
111
+ }) | from_entries
112
+ ) as $files |
113
+ {
114
+ tool: "cyclomatic-complexity",
115
+ language: "javascript",
116
+ generatedAt: (now | todate),
117
+ files: $files,
118
+ summary: {
119
+ totalFiles: ($files | to_entries | length),
120
+ totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
121
+ avgComplexity: (
122
+ [$files | to_entries[].value.avgComplexity] |
123
+ if length > 0 then (add / length) else 0 end
124
+ ),
125
+ gradeDistribution: (
126
+ [$files | to_entries[].value.grade] |
127
+ reduce .[] as $grade (
128
+ {A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
129
+ .[$grade] += 1
130
+ )
131
+ )
132
+ }
133
+ }
134
+ '
package/drivers/php.sh ADDED
@@ -0,0 +1,132 @@
1
+ #!/bin/bash
2
+ # drivers/php.sh
3
+ # Wraps PHPMD to produce standard complexity JSON
4
+ #
5
+ # Usage: ./drivers/php.sh [directory]
6
+ # directory: Path to PHP code to analyze (default: current directory)
7
+ #
8
+ # Output: JSON conforming to ComplexityReport schema
9
+ # Exit codes: 0 = success, 1 = error (phpmd not installed or other failure)
10
+
11
+ set -euo pipefail
12
+
13
+ TARGET_DIR="${1:-.}"
14
+
15
+ # Check phpmd is installed
16
+ if ! command -v phpmd &> /dev/null; then
17
+ echo '{"error": "phpmd not installed. Run: composer global require phpmd/phpmd"}' >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Check if target directory exists
22
+ if [ ! -d "$TARGET_DIR" ]; then
23
+ echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # Check if there are any PHP files
28
+ if ! find "$TARGET_DIR" -name "*.php" ! -path "*/vendor/*" -print -quit 2>/dev/null | grep -q .; then
29
+ # No PHP files found, output empty result
30
+ echo '{"tool":"phpmd","language":"php","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
31
+ exit 0
32
+ fi
33
+
34
+ # Run PHPMD and transform output
35
+ # PHPMD outputs JSON violations, we need to extract cyclomatic complexity violations
36
+ # and transform them to our schema
37
+ # Using --ignore-violations-on-exit to ensure exit 0 even with violations
38
+
39
+ phpmd "$TARGET_DIR" json codesize --exclude vendor --ignore-violations-on-exit 2>/dev/null | jq -c '
40
+ # Helper function for complexity to grade conversion
41
+ def complexity_to_grade:
42
+ if . <= 5 then "A"
43
+ elif . <= 10 then "B"
44
+ elif . <= 20 then "C"
45
+ elif . <= 30 then "D"
46
+ elif . <= 40 then "E"
47
+ else "F"
48
+ end;
49
+
50
+ # Process PHPMD violations format
51
+ # Extract only CyclomaticComplexity violations
52
+ .files |
53
+ map(
54
+ select(.violations | length > 0) |
55
+ {
56
+ path: .file,
57
+ violations: [
58
+ .violations[] |
59
+ select(.rule == "CyclomaticComplexity") |
60
+ {
61
+ # Parse complexity from description like "The method foo() has a Cyclomatic Complexity of 12."
62
+ description: .description,
63
+ method: .method,
64
+ beginLine: .beginLine,
65
+ endLine: .endLine,
66
+ # Extract complexity number from description using regex
67
+ complexity: (
68
+ .description |
69
+ capture("Cyclomatic Complexity of (?<num>[0-9]+)") |
70
+ .num | tonumber
71
+ )
72
+ }
73
+ ]
74
+ } |
75
+ select(.violations | length > 0)
76
+ ) |
77
+
78
+ # Group by file and calculate metrics
79
+ map({
80
+ key: .path,
81
+ value: {
82
+ functions: (
83
+ .violations | map({
84
+ name: .method,
85
+ complexity: .complexity,
86
+ grade: (.complexity | complexity_to_grade),
87
+ line: .beginLine,
88
+ endLine: .endLine
89
+ })
90
+ ),
91
+ avgComplexity: (
92
+ if (.violations | length) > 0 then
93
+ ((.violations | map(.complexity) | add) / (.violations | length))
94
+ else 0 end
95
+ ),
96
+ maxComplexity: (
97
+ if (.violations | length) > 0 then
98
+ (.violations | map(.complexity) | max)
99
+ else 0 end
100
+ ),
101
+ grade: (
102
+ if (.violations | length) > 0 then
103
+ (((.violations | map(.complexity) | add) / (.violations | length)) | complexity_to_grade)
104
+ else "A" end
105
+ )
106
+ }
107
+ }) |
108
+ from_entries as $files |
109
+
110
+ # Build final output
111
+ {
112
+ tool: "phpmd",
113
+ language: "php",
114
+ generatedAt: (now | todate),
115
+ files: $files,
116
+ summary: {
117
+ totalFiles: ($files | to_entries | length),
118
+ totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
119
+ avgComplexity: (
120
+ [$files | to_entries[].value.avgComplexity] |
121
+ if length > 0 then (add / length) else 0 end
122
+ ),
123
+ gradeDistribution: (
124
+ [$files | to_entries[].value.grade] |
125
+ reduce .[] as $grade (
126
+ {A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
127
+ .[$grade] += 1
128
+ )
129
+ )
130
+ }
131
+ }
132
+ '
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # drivers/python.sh
3
+ # Wraps radon to produce standard complexity JSON
4
+ #
5
+ # Usage: ./drivers/python.sh [directory]
6
+ # directory: Path to Python code to analyze (default: current directory)
7
+ #
8
+ # Output: JSON conforming to ComplexityReport schema
9
+ # Exit codes: 0 = success, 1 = error (radon not installed or other failure)
10
+
11
+ set -euo pipefail
12
+
13
+ TARGET_DIR="${1:-.}"
14
+
15
+ # Check radon is installed
16
+ if ! command -v radon &> /dev/null; then
17
+ echo '{"error": "radon not installed. Run: pip install radon"}' >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Check if target directory exists
22
+ if [ ! -d "$TARGET_DIR" ]; then
23
+ echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # Run radon and transform output
28
+ # radon cc outputs JSON like:
29
+ # {
30
+ # "file.py": [
31
+ # {"name": "func", "complexity": 5, "rank": "A", "lineno": 10, ...}
32
+ # ]
33
+ # }
34
+ radon cc "$TARGET_DIR" -j --total-average 2>/dev/null | jq -c '
35
+ (to_entries | map({
36
+ key: .key,
37
+ value: {
38
+ functions: (.value | map({
39
+ name: .name,
40
+ complexity: .complexity,
41
+ grade: .rank,
42
+ line: .lineno,
43
+ endLine: .endline
44
+ })),
45
+ avgComplexity: (
46
+ if (.value | length) > 0 then
47
+ ((.value | map(.complexity) | add) / (.value | length))
48
+ else 0 end
49
+ ),
50
+ maxComplexity: (
51
+ if (.value | length) > 0 then
52
+ (.value | map(.complexity) | max)
53
+ else 0 end
54
+ ),
55
+ grade: (
56
+ if (.value | length) > 0 then
57
+ ((.value | map(.complexity) | add) / (.value | length)) as $avg |
58
+ if $avg <= 5 then "A"
59
+ elif $avg <= 10 then "B"
60
+ elif $avg <= 20 then "C"
61
+ elif $avg <= 30 then "D"
62
+ elif $avg <= 40 then "E"
63
+ else "F"
64
+ end
65
+ else "A" end
66
+ )
67
+ }
68
+ }) | from_entries) as $files |
69
+ {
70
+ tool: "radon",
71
+ language: "python",
72
+ generatedAt: (now | todate),
73
+ files: $files,
74
+ summary: {
75
+ totalFiles: ($files | to_entries | length),
76
+ totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
77
+ avgComplexity: (
78
+ [$files | to_entries[].value.avgComplexity] |
79
+ if length > 0 then (add / length) else 0 end
80
+ ),
81
+ gradeDistribution: (
82
+ [$files | to_entries[].value.grade] |
83
+ reduce .[] as $grade (
84
+ {A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
85
+ .[$grade] += 1
86
+ )
87
+ )
88
+ }
89
+ }
90
+ '