Setting the file. One moment.
Scan · Acquire Codebase Knowledge · github/awesome-copilot · Skills Docs
ContentsBack to the top of the page def get_git_churn
— line 339
This file
Number 1.3
Position 3 of 10
Type Python
Size 23 KB
Lines 712 scripts/ scan.py
Python · 712 lines · 23 KB
18 import sys
19 import argparse
20 import subprocess
21 import json
22 from pathlib import Path
23 from typing import List, Set
24 import re
25
26 TREE_LIMIT = 200
27 TREE_MAX_DEPTH = 3
28 TODO_LIMIT = 60
29 MANIFEST_PREVIEW_LINES = 80
30 RECENT_COMMITS_LIMIT = 20
31 CHURN_LIMIT = 20
32
33 EXCLUDE_DIRS = {
34 "node_modules" , ".git" , "dist" , "build" , "out" , ".next" , ".nuxt" ,
35 "__pycache__" , ".venv" , "venv" , ".tox" , "target" , "vendor" ,
36 "coverage" , ".nyc_output" , "generated" , ".cache" , ".turbo" ,
37 ".yarn" , ".pnp" , "bin" , "obj"
38 }
39
40 MANIFESTS = [
41 # JavaScript/Node.js
42 "package.json" , "package-lock.json" , "yarn.lock" , "pnpm-lock.yaml" , "bun.lockb" ,
43 "deno.json" , "deno.jsonc" ,
44 # Python
45 "requirements.txt" , "Pipfile" , "Pipfile.lock" , "pyproject.toml" , "setup.py" , "setup.cfg" ,
46 "poetry.lock" , "pdm.lock" , "uv.lock" ,
47 # Go
48 "go.mod" , "go.sum" ,
49 # Rust
50 "Cargo.toml" , "Cargo.lock" ,
51 # Java/Kotlin
52 "pom.xml" , "build.gradle" , "build.gradle.kts" , "settings.gradle" , "settings.gradle.kts" ,
53 "gradle.properties" ,
54 # PHP/Composer
55 "composer.json" , "composer.lock" ,
56 # Ruby
57 "Gemfile" , "Gemfile.lock" , "*.gemspec" ,
58 # Elixir
59 "mix.exs" , "mix.lock" ,
60 # Dart/Flutter
61 "pubspec.yaml" , "pubspec.lock" ,
62 # .NET/C#
63 "*.csproj" , "*.sln" , "*.slnx" , "global.json" , "packages.config" ,
64 # Swift
65 "Package.swift" , "Package.resolved" ,
66 # Scala
67 "build.sbt" , "scala-cli.yml" ,
68 # Haskell
69 "*.cabal" , "stack.yaml" , "cabal.project" , "cabal.project.local" ,
70 # OCaml
71 "dune-project" , "opam" , "opam.lock" ,
72 # Nim
73 "*.nimble" , "nim.cfg" ,
74 # Crystal
75 "shard.yml" , "shard.lock" ,
76 # R
77 "DESCRIPTION" , "renv.lock" ,
78 # Julia
79 "Project.toml" , "Manifest.toml" ,
80 # Build systems
81 "CMakeLists.txt" , "Makefile" , "GNUmakefile" ,
82 "SConstruct" , "build.xml" ,
83 "BUILD" , "BUILD.bazel" , "WORKSPACE" , "bazel.lock" ,
84 "justfile" , ".justfile" , "Taskfile.yml" ,
85 "tox.ini" , "Vagrantfile"
86 ]
87
88 ENTRY_CANDIDATES = [
89 # JavaScript/Node.js/TypeScript
90 "src/index.ts" , "src/index.js" , "src/index.mjs" ,
91 "src/main.ts" , "src/main.js" , "src/main.py" ,
92 "src/app.ts" , "src/app.js" ,
93 "src/server.ts" , "src/server.js" ,
94 "index.ts" , "index.js" , "app.ts" , "app.js" ,
95 "lib/index.ts" , "lib/index.js" ,
96 # Go
97 "main.go" , "cmd/main.go" , "cmd/*/main.go" ,
98 # Python
99 "main.py" , "app.py" , "server.py" , "run.py" , "cli.py" ,
100 "src/main.py" , "src/__main__.py" ,
101 # .NET/C#
102 "Program.cs" , "src/Program.cs" , "Main.cs" ,
103 # Java
104 "Main.java" , "Application.java" , "App.java" ,
105 "src/main/java/Main.java" ,
106 # Kotlin
107 "Main.kt" , "Application.kt" , "App.kt" ,
108 # Rust
109 "src/main.rs" , "src/lib.rs" ,
110 # Swift
111 "main.swift" , "Package.swift" , "Sources/main.swift" ,
112 # Ruby
113 "app.rb" , "main.rb" , "lib/app.rb" ,
114 # PHP
115 "index.php" , "app.php" , "public/index.php" ,
116 # Go
117 "cmd/*/main.go" ,
118 # Scala
119 "src/main/scala/Main.scala" ,
120 # Haskell
121 "Main.hs" , "app/Main.hs" ,
122 # Clojure
123 "src/core.clj" , "-main.clj" ,
124 # Elixir
125 "lib/application.ex" , "mix.exs" ,
126 ]
127
128 LINT_FILES = [
129 ".eslintrc" , ".eslintrc.json" , ".eslintrc.js" , ".eslintrc.cjs" , ".eslintrc.yml" , ".eslintrc.yaml" ,
130 "eslint.config.js" , "eslint.config.mjs" , "eslint.config.cjs" ,
131 ".prettierrc" , ".prettierrc.json" , ".prettierrc.js" , ".prettierrc.yml" ,
132 "prettier.config.js" , "prettier.config.mjs" ,
133 ".editorconfig" ,
134 "tsconfig.json" , "tsconfig.base.json" , "tsconfig.build.json" ,
135 ".golangci.yml" , ".golangci.yaml" ,
136 "setup.cfg" , ".flake8" , ".pylintrc" , "mypy.ini" ,
137 ".rubocop.yml" , "phpcs.xml" , "phpstan.neon" ,
138 "biome.json" , "biome.jsonc"
139 ]
140
141 ENV_TEMPLATES = [ ".env.example" , ".env.template" , ".env.sample" , ".env.defaults" , ".env.local.example" ]
142
143 SOURCE_EXTS = [
144 "ts" , "tsx" , "js" , "jsx" , "mjs" , "cjs" ,
145 "py" , "go" , "java" , "kt" , "rb" , "php" ,
146 "rs" , "cs" , "cpp" , "c" , "h" , "ex" , "exs" ,
147 "swift" , "scala" , "clj" , "cljs" , "lua" ,
148 "vim" , "vim" , "hs" , "ml" , "ml" , "nim" , "cr" ,
149 "r" , "jl" , "groovy" , "gradle" , "xml" , "json"
150 ]
151
152 MONOREPO_FILES = [ "pnpm-workspace.yaml" , "lerna.json" , "nx.json" , "rush.json" , "turbo.json" , "moon.yml" ]
153 MONOREPO_DIRS = [ "packages" , "apps" , "libs" , "services" , "modules" ]
154
155 CI_CD_CONFIGS = {
156 ".github/workflows" : "GitHub Actions" ,
157 ".gitlab-ci.yml" : "GitLab CI" ,
158 "Jenkinsfile" : "Jenkins" ,
159 ".circleci/config.yml" : "CircleCI" ,
160 ".travis.yml" : "Travis CI" ,
161 "azure-pipelines.yml" : "Azure Pipelines" ,
162 "appveyor.yml" : "AppVeyor" ,
163 ".drone.yml" : "Drone CI" ,
164 ".woodpecker.yml" : "Woodpecker CI" ,
165 "bitbucket-pipelines.yml" : "Bitbucket Pipelines"
166 }
167
168 CONTAINER_FILES = [
169 "Dockerfile" , "docker-compose.yml" , "docker-compose.yaml" ,
170 ".dockerignore" , "Dockerfile.*" ,
171 "k8s" , "kustomization.yaml" , "Chart.yaml" ,
172 "Vagrantfile" , "podman-compose.yml"
173 ]
174
175 SECURITY_CONFIGS = [
176 ".snyk" , "security.txt" , "SECURITY.md" ,
177 ".dependabot.yml" , ".whitesource" ,
178 "sbom.json" , "sbom.spdx" , ".bandit.yaml"
179 ]
180
181 PERFORMANCE_MARKERS = [
182 "benchmark" , "bench" , "perf.data" , ".prof" ,
183 "k6.js" , "locustfile.py" , "jmeter.jmx"
184 ]
185
186
187 def parse_args ():
188 """Parse command-line arguments."""
189 parser = argparse.ArgumentParser(
190 description = "Scan the current directory (project root) and output discovery information "
191 "for the acquire-codebase-knowledge skill." ,
192 add_help = True
193 )
194 parser.add_argument(
195 "--output" ,
196 type = str ,
197 help = "Write output to FILE instead of stdout"
198 )
199 return parser.parse_args()
200
201
202 def should_exclude (path: Path) -> bool :
203 """Check if a path should be excluded from scanning."""
204 return any (part in EXCLUDE_DIRS for part in path.parts)
205
206
207 def get_directory_tree (max_depth: int = TREE_MAX_DEPTH ) -> List[ str ]:
208 """Get directory tree up to max_depth."""
209 files = []
210
211 def walk (path: Path, depth: int ):
212 if depth > max_depth or should_exclude(path):
213 return
214 try :
215 for item in sorted (path.iterdir()):
216 if should_exclude(item):
217 continue
218 rel_path = item.relative_to(Path.cwd())
219 files.append( str (rel_path))
220 if item.is_dir():
221 walk(item, depth + 1 )
222 except ( PermissionError , OSError ):
223 pass
224
225 walk(Path.cwd(), 0 )
226 return files[: TREE_LIMIT ]
227
228
229 def find_manifest_files () -> List[ str ]:
230 """Find manifest files matching patterns."""
231 found = []
232 for pattern in MANIFESTS :
233 if "*" in pattern:
234 # Handle glob patterns
235 for path in Path.cwd().glob(pattern):
236 if path.is_file() and not should_exclude(path):
237 found.append(path.name)
238 else :
239 path = Path.cwd() / pattern
240 if path.is_file():
241 found.append(pattern)
242 return sorted ( set (found))
243
244
245 def read_file_preview (filepath: Path, max_lines: int = MANIFEST_PREVIEW_LINES ) -> str :
246 """Read file with line limit."""
247 try :
248 with open (filepath, 'r' , encoding = 'utf-8' , errors = 'replace' ) as f:
249 lines = f.readlines()
250
251 if not lines:
252 return "None found."
253
254 preview = '' .join(lines[:max_lines])
255 if len (lines) > max_lines:
256 preview += f " \n [TRUNCATED] Showing first { max_lines } of { len (lines) } lines."
257 return preview
258 except Exception as e:
259 return f "[Error reading file: { e } ]"
260
261
262 def find_entry_points () -> List[ str ]:
263 """Find entry point candidates."""
264 found = []
265 for candidate in ENTRY_CANDIDATES :
266 if Path(candidate).exists():
267 found.append(candidate)
268 return found
269
270
271 def find_lint_config () -> List[ str ]:
272 """Find linting and formatting config files."""
273 found = []
274 for filename in LINT_FILES :
275 if Path(filename).exists():
276 found.append(filename)
277 return found
278
279
280 def find_env_templates () -> List[ tuple ]:
281 """Find environment variable templates."""
282 found = []
283 for filename in ENV_TEMPLATES :
284 path = Path(filename)
285 if path.exists():
286 found.append((filename, path))
287 return found
288
289
290 def search_todos () -> List[ str ]:
291 """Search for TODO / FIXME / HACK comments."""
292 todos = []
293 patterns = [ "TODO" , "FIXME" , "HACK" ]
294 exclude_dirs_str = "|" .join( EXCLUDE_DIRS | { "test" , "tests" , "__tests__" , "spec" , "__mocks__" , "fixtures" })
295
296 try :
297 for root, dirs, files in os.walk(Path.cwd()):
298 # Remove excluded directories from dirs to prevent os.walk from descending
299 dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and d not in { "test" , "tests" , "__tests__" , "spec" , "__mocks__" , "fixtures" }]
300
301 for file in files:
302 # Check file extension
303 ext = Path( file ).suffix.lstrip( '.' )
304 if ext not in SOURCE_EXTS :
305 continue
306
307 filepath = Path(root) / file
308 try :
309 with open (filepath, 'r' , encoding = 'utf-8' , errors = 'replace' ) as f:
310 for line_num, line in enumerate (f, 1 ):
311 for pattern in patterns:
312 if pattern in line:
313 rel_path = filepath.relative_to(Path.cwd())
314 todos.append( f " { rel_path } : { line_num } : { line.strip() } " )
315 except Exception :
316 pass
317 except Exception :
318 pass
319
320 return todos[: TODO_LIMIT ]
321
322
323 def get_git_commits () -> List[ str ]:
324 """Get recent git commits."""
325 try :
326 result = subprocess.run(
327 [ "git" , "log" , "--oneline" , "-n" , str ( RECENT_COMMITS_LIMIT )],
328 capture_output = True ,
329 text = True ,
330 cwd = Path.cwd()
331 )
332 if result.returncode == 0 :
333 return result.stdout.strip().split( ' \n ' ) if result.stdout.strip() else []
334 return []
335 except Exception :
336 return []
337
338
339 def get_git_churn () -> List[ str ]:
340 """Get high-churn files from last 90 days."""
341 try :
342 result = subprocess.run(
343 [ "git" , "log" , "--since=90 days ago" , "--name-only" , "--pretty=format:" ],
344 capture_output = True ,
345 text = True ,
346 cwd = Path.cwd()
347 )
348 if result.returncode == 0 :
349 files = [f.strip() for f in result.stdout.split( ' \n ' ) if f.strip()]
350 # Count occurrences
351 from collections import Counter
352 counts = Counter(files)
353 churn = sorted (counts.items(), key =lambda x: x[ 1 ], reverse = True )
354 return [ f " { count :4d} { filename } " for filename, count in churn[: CHURN_LIMIT ]]
355 return []
356 except Exception :
357 return []
358
359
360 def is_git_repo () -> bool :
361 """Check if current directory is a git repository."""
362 try :
363 subprocess.run(
364 [ "git" , "rev-parse" , "--git-dir" ],
365 capture_output = True ,
366 cwd = Path.cwd(),
367 timeout = 2
368 )
369 return True
370 except Exception :
371 return False
372
373
374 def detect_monorepo () -> List[ str ]:
375 """Detect monorepo signals."""
376 signals = []
377
378 for filename in MONOREPO_FILES :
379 if Path(filename).exists():
380 signals.append( f "Monorepo tool detected: { filename } " )
381
382 for dirname in MONOREPO_DIRS :
383 if Path(dirname).is_dir():
384 signals.append( f "Sub-package directory found: { dirname } /" )
385
386 # Check package.json workspaces
387 if Path( "package.json" ).exists():
388 try :
389 with open ( "package.json" , 'r' ) as f:
390 content = f.read()
391 if '"workspaces"' in content:
392 signals.append( "package.json has 'workspaces' field (npm/yarn workspaces monorepo)" )
393 except Exception :
394 pass
395
396 return signals
397
398
399 def detect_ci_cd_pipelines () -> List[ str ]:
400 """Detect CI/CD pipeline configurations."""
401 pipelines = []
402
403 for config_path, pipeline_name in CI_CD_CONFIGS .items():
404 path = Path(config_path)
405 if path.is_file():
406 pipelines.append( f "CI/CD: { pipeline_name } " )
407 elif path.is_dir():
408 # Check for workflow files in directory
409 try :
410 if list (path.glob( "*.yml" )) or list (path.glob( "*.yaml" )):
411 pipelines.append( f "CI/CD: { pipeline_name } " )
412 except Exception :
413 pass
414
415 return pipelines
416
417
418 def detect_containers () -> List[ str ]:
419 """Detect containerization and orchestration configs."""
420 containers = []
421
422 for config in CONTAINER_FILES :
423 path = Path(config)
424 if path.is_file():
425 if "Dockerfile" in config:
426 containers.append( "Container: Docker found" )
427 elif "docker-compose" in config:
428 containers.append( "Orchestration: Docker Compose found" )
429 elif config.endswith( ".yaml" ) or config.endswith( ".yml" ):
430 containers.append( f "Container/Orchestration: { config } " )
431 elif path.is_dir():
432 if config in [ "k8s" , "kubernetes" ]:
433 containers.append( "Orchestration: Kubernetes configs found" )
434 try :
435 if list (path.glob( "*.yml" )) or list (path.glob( "*.yaml" )):
436 containers.append( f "Container/Orchestration: { config } / directory found" )
437 except Exception :
438 pass
439
440 return containers
441
442
443 def detect_security_configs () -> List[ str ]:
444 """Detect security and compliance configurations."""
445 security = []
446
447 for config in SECURITY_CONFIGS :
448 if Path(config).exists():
449 config_name = config.replace( ".yml" , "" ).replace( ".yaml" , "" ).lstrip( "." )
450 security.append( f "Security: { config_name } " )
451
452 return security
453
454
455 def detect_performance_markers () -> List[ str ]:
456 """Detect performance testing and profiling markers."""
457 performance = []
458
459 for marker in PERFORMANCE_MARKERS :
460 if Path(marker).exists():
461 performance.append( f "Performance: { marker } found" )
462 else :
463 # Check for directories
464 try :
465 if Path(marker).is_dir():
466 performance.append( f "Performance: { marker } / directory found" )
467 except Exception :
468 pass
469
470 return performance
471
472
473 def collect_code_metrics () -> dict :
474 """Collect code metrics: file counts by extension, total LOC."""
475 metrics = {
476 "total_files" : 0 ,
477 "by_extension" : {},
478 "by_language" : {},
479 "total_lines" : 0 ,
480 "largest_files" : []
481 }
482
483 # Language mapping
484 lang_map = {
485 "ts" : "TypeScript" , "tsx" : "TypeScript/React" , "js" : "JavaScript" ,
486 "jsx" : "JavaScript/React" , "py" : "Python" , "go" : "Go" ,
487 "java" : "Java" , "kt" : "Kotlin" , "rs" : "Rust" ,
488 "cs" : "C#" , "rb" : "Ruby" , "php" : "PHP" ,
489 "swift" : "Swift" , "scala" : "Scala" , "ex" : "Elixir" ,
490 "cpp" : "C++" , "c" : "C" , "h" : "C Header" ,
491 "clj" : "Clojure" , "lua" : "Lua" , "hs" : "Haskell"
492 }
493
494 file_sizes = []
495
496 try :
497 for root, dirs, files in os.walk(Path.cwd()):
498 dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS ]
499
500 for file in files:
501 filepath = Path(root) / file
502 ext = filepath.suffix.lstrip( '.' )
503
504 if not ext or ext in { "pyc" , "o" , "a" , "so" }:
505 continue
506
507 try :
508 size = filepath.stat().st_size
509 file_sizes.append((filepath.relative_to(Path.cwd()), size))
510
511 metrics[ "total_files" ] += 1
512 metrics[ "by_extension" ][ext] = metrics[ "by_extension" ].get(ext, 0 ) + 1
513
514 lang = lang_map.get(ext, "Other" )
515 metrics[ "by_language" ][lang] = metrics[ "by_language" ].get(lang, 0 ) + 1
516
517 # Count lines for text files
518 if ext in SOURCE_EXTS and size < 1_000_000 : # Skip huge files
519 try :
520 with open (filepath, 'r' , encoding = 'utf-8' , errors = 'ignore' ) as f:
521 metrics[ "total_lines" ] += len (f.readlines())
522 except Exception :
523 pass
524 except Exception :
525 pass
526
527 # Top 10 largest files
528 file_sizes.sort( key =lambda x: x[ 1 ], reverse = True )
529 metrics[ "largest_files" ] = [
530 f " { str (f) } : { s / 1024 :.1f} KB" for f, s in file_sizes[: 10 ]
531 ]
532
533 except Exception :
534 pass
535
536 return metrics
537
538
539 def print_section (title: str , content: List[ str ], output_file = None ) -> None :
540 """Print a section with title and content."""
541 lines = [ f " \n === { title } ===" ]
542
543 if isinstance (content, list ):
544 lines.extend(content if content else [ "None found." ])
545 elif isinstance (content, str ):
546 lines.append(content)
547
548 text = ' \n ' .join(lines) + ' \n '
549
550 if output_file:
551 output_file.write(text)
552 else :
553 print (text, end = '' )
554
555
556 def main ():
557 """Main entry point."""
558 args = parse_args()
559
560 output_file = None
561 if args.output:
562 output_dir = Path(args.output).parent
563 output_dir.mkdir( parents = True , exist_ok = True )
564 output_file = open (args.output, 'w' , encoding = 'utf-8' )
565 print ( f "Writing output to: { args.output } " , file = sys.stderr)
566
567 try :
568 # Directory tree
569 print_section(
570 f "DIRECTORY TREE (max depth { TREE_MAX_DEPTH } , source files only)" ,
571 get_directory_tree(),
572 output_file
573 )
574
575 # Stack detection
576 manifests = find_manifest_files()
577 if manifests:
578 manifest_content = [ "" ]
579 for manifest in manifests:
580 manifest_path = Path(manifest)
581 manifest_content.append( f "--- { manifest } ---" )
582 if manifest == "bun.lockb" :
583 manifest_content.append( "[Binary lockfile — see package.json for dependency details.]" )
584 else :
585 manifest_content.append(read_file_preview(manifest_path))
586 print_section( "STACK DETECTION (manifest files)" , manifest_content, output_file)
587 else :
588 print_section( "STACK DETECTION (manifest files)" , [ "No recognized manifest files found in project root." ], output_file)
589
590 # Entry points
591 entries = find_entry_points()
592 if entries:
593 entry_content = [ f "Found: { e } " for e in entries]
594 print_section( "ENTRY POINTS" , entry_content, output_file)
595 else :
596 print_section( "ENTRY POINTS" , [ "No common entry points found. Check 'main' or 'scripts.start' in manifest files above." ], output_file)
597
598 # Linting config
599 lint = find_lint_config()
600 if lint:
601 lint_content = [ f "Found: { l } " for l in lint]
602 print_section( "LINTING AND FORMATTING CONFIG" , lint_content, output_file)
603 else :
604 print_section( "LINTING AND FORMATTING CONFIG" , [ "No linting or formatting config files found in project root." ], output_file)
605
606 # Environment templates
607 envs = find_env_templates()
608 if envs:
609 env_content = []
610 for filename, filepath in envs:
611 env_content.append( f "--- { filename } ---" )
612 env_content.append(read_file_preview(filepath))
613 print_section( "ENVIRONMENT VARIABLE TEMPLATES" , env_content, output_file)
614 else :
615 print_section( "ENVIRONMENT VARIABLE TEMPLATES" , [ "No .env.example or .env.template found. Identify required environment variables by searching the code and config for environment variable reads." ], output_file)
616
617 # TODOs
618 todos = search_todos()
619 if todos:
620 print_section( "TODO / FIXME / HACK (production code only, test dirs excluded)" , todos, output_file)
621 else :
622 print_section( "TODO / FIXME / HACK (production code only, test dirs excluded)" , [ "None found." ], output_file)
623
624 # Git info
625 if is_git_repo():
626 commits = get_git_commits()
627 if commits:
628 print_section( "GIT RECENT COMMITS (last 20)" , commits, output_file)
629 else :
630 print_section( "GIT RECENT COMMITS (last 20)" , [ "No commits found." ], output_file)
631
632 churn = get_git_churn()
633 if churn:
634 print_section( "HIGH-CHURN FILES (last 90 days, top 20)" , churn, output_file)
635 else :
636 print_section( "HIGH-CHURN FILES (last 90 days, top 20)" , [ "None found." ], output_file)
637 else :
638 print_section( "GIT RECENT COMMITS (last 20)" , [ "Not a git repository or no commits yet." ], output_file)
639 print_section( "HIGH-CHURN FILES (last 90 days, top 20)" , [ "Not a git repository." ], output_file)
640
641 # Monorepo detection
642 monorepo = detect_monorepo()
643 if monorepo:
644 print_section( "MONOREPO SIGNALS" , monorepo, output_file)
645 else :
646 print_section( "MONOREPO SIGNALS" , [ "No monorepo signals detected." ], output_file)
647
648 # Code metrics
649 metrics = collect_code_metrics()
650 metrics_output = [
651 f "Total files scanned: { metrics[ 'total_files' ] } " ,
652 f "Total lines of code: { metrics[ 'total_lines' ] } " ,
653 ""
654 ]
655 if metrics[ "by_language" ]:
656 metrics_output.append( "Files by language:" )
657 for lang, count in sorted (metrics[ "by_language" ].items(), key =lambda x: x[ 1 ], reverse = True ):
658 metrics_output.append( f " { lang } : { count } " )
659 if metrics[ "largest_files" ]:
660 metrics_output.append( "" )
661 metrics_output.append( "Top 10 largest files:" )
662 metrics_output.extend(metrics[ "largest_files" ])
663 print_section( "CODE METRICS" , metrics_output, output_file)
664
665 # CI/CD Detection
666 ci_cd = detect_ci_cd_pipelines()
667 if ci_cd:
668 print_section( "CI/CD PIPELINES" , ci_cd, output_file)
669 else :
670 print_section( "CI/CD PIPELINES" , [ "No CI/CD pipelines detected." ], output_file)
671
672 # Container Detection
673 containers = detect_containers()
674 if containers:
675 print_section( "CONTAINERS & ORCHESTRATION" , containers, output_file)
676 else :
677 print_section( "CONTAINERS & ORCHESTRATION" , [ "No containerization configs detected." ], output_file)
678
679 # Security Configs
680 security = detect_security_configs()
681 if security:
682 print_section( "SECURITY & COMPLIANCE" , security, output_file)
683 else :
684 print_section( "SECURITY & COMPLIANCE" , [ "No security configs detected." ], output_file)
685
686 # Performance Markers
687 performance = detect_performance_markers()
688 if performance:
689 print_section( "PERFORMANCE & TESTING" , performance, output_file)
690 else :
691 print_section( "PERFORMANCE & TESTING" , [ "No performance testing configs detected." ], output_file)
692
693 # Final message
694 final_msg = " \n === SCAN COMPLETE === \n "
695 if output_file:
696 output_file.write(final_msg)
697 else :
698 print (final_msg, end = '' )
699
700 return 0
701
702 except Exception as e:
703 print ( f "Error: { e } " , file = sys.stderr)
704 return 1
705
706 finally :
707 if output_file:
708 output_file.close()
709
710
711 if __name__ == "__main__" :
712 sys.exit(main())