pyneat-cli 2.3.0__tar.gz → 2.4.0__tar.gz

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 (79) hide show
  1. {pyneat_cli-2.3.0/pyneat_cli.egg-info → pyneat_cli-2.4.0}/PKG-INFO +1 -1
  2. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/__init__.py +1 -1
  3. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/cli.py +207 -0
  4. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/scanner/rust_scanner.py +32 -10
  5. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0/pyneat_cli.egg-info}/PKG-INFO +1 -1
  6. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyproject.toml +1 -1
  7. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/LICENSE +0 -0
  8. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/README.md +0 -0
  9. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/__main__.py +0 -0
  10. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/benchmark.py +0 -0
  11. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/config.py +0 -0
  12. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/__init__.py +0 -0
  13. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/atomic.py +0 -0
  14. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/engine.py +0 -0
  15. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/manifest.py +0 -0
  16. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/marker_cleanup.py +0 -0
  17. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/scope_guard.py +0 -0
  18. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/semantic_guard.py +0 -0
  19. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/type_shield.py +0 -0
  20. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/types.py +0 -0
  21. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/pre_commit.py +0 -0
  22. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/__init__.py +0 -0
  23. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/ai_bugs.py +0 -0
  24. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/base.py +0 -0
  25. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/comments.py +0 -0
  26. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/conservative.py +0 -0
  27. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/dataclass.py +0 -0
  28. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/deadcode.py +0 -0
  29. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/debug.py +0 -0
  30. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/destructive.py +0 -0
  31. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/duplication.py +0 -0
  32. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/fstring.py +0 -0
  33. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/imports.py +0 -0
  34. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/init_protection.py +0 -0
  35. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/is_not_none.py +0 -0
  36. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/isolated.py +0 -0
  37. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/magic_numbers.py +0 -0
  38. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/match_case.py +0 -0
  39. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/naming.py +0 -0
  40. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/performance.py +0 -0
  41. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/quality.py +0 -0
  42. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/range_len_pattern.py +0 -0
  43. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/redundant.py +0 -0
  44. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/refactoring.py +0 -0
  45. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/safe.py +0 -0
  46. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security.py +0 -0
  47. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/__init__.py +0 -0
  48. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/critical.py +0 -0
  49. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/high.py +0 -0
  50. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/info.py +0 -0
  51. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/low.py +0 -0
  52. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/medium.py +0 -0
  53. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_registry.py +0 -0
  54. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/typing.py +0 -0
  55. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/unused.py +0 -0
  56. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/__init__.py +0 -0
  57. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/__init__.py +0 -0
  58. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/__main__.py +0 -0
  59. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/debug_logger.py +0 -0
  60. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/fuzz_runner.py +0 -0
  61. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/github_client.py +0 -0
  62. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/security/advisory_db.py +0 -0
  63. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/security/dependency_scanner.py +0 -0
  64. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/utils/naming.py +0 -0
  65. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/SOURCES.txt +0 -0
  66. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/dependency_links.txt +0 -0
  67. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/entry_points.txt +0 -0
  68. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/requires.txt +0 -0
  69. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/top_level.txt +0 -0
  70. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/setup.cfg +0 -0
  71. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/setup.py +0 -0
  72. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_engine.py +0 -0
  73. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_fuzz.py +0 -0
  74. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_fuzz_github.py +0 -0
  75. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_integration.py +0 -0
  76. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_layers.py +0 -0
  77. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_rust_scanner.py +0 -0
  78. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_semantic_integrity.py +0 -0
  79. {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyneat-cli
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Neat Python AI Code Cleaner — removes AI-generated code artifacts, dead code, and security vulnerabilities
5
5
  License: GNU AGPL-3.0-or-later
6
6
  Keywords: ai,code-cleaner,python,linter,formatter,security,dead-code,refactoring,ast,auto-fix
@@ -85,7 +85,7 @@ from .rules.ai_bugs import AIBugRule
85
85
  from .rules.duplication import CodeDuplicationRule
86
86
  from .rules.naming import NamingInconsistencyRule
87
87
 
88
- __version__ = "2.3.0"
88
+ __version__ = "2.4.0"
89
89
 
90
90
  __all__ = [
91
91
  # Core
@@ -374,6 +374,9 @@ def clean(input_file: str, output: str, in_place: bool, verbose: bool,
374
374
  click.echo(f"[ERROR] Write failed: {str(e)}", err=True)
375
375
  return 1
376
376
 
377
+ # Hiển thị menu gợi ý tính năng khác
378
+ show_feature_menu("clean", f"{len(result.changes_made)} changes made")
379
+
377
380
  return 0
378
381
 
379
382
 
@@ -802,6 +805,7 @@ def check(target, severity, cvss, output, format, fail_on, skip_deps, verbose, u
802
805
 
803
806
  start_time = time.time()
804
807
  all_findings: List[SecurityFinding] = []
808
+ rust_findings: List[SecurityFinding] = [] # Rust scanner results
805
809
  total_files = 0
806
810
 
807
811
  # Scan code files
@@ -861,11 +865,78 @@ def check(target, severity, cvss, output, format, fail_on, skip_deps, verbose, u
861
865
 
862
866
  elapsed = time.time() - start_time
863
867
 
868
+ # --------------------------------------------------------------------------
869
+ # Merge Rust scanner results into all_findings
870
+ # --------------------------------------------------------------------------
871
+ # rust_results is a list of dicts from the Rust scanner, keyed by byte offset
872
+ # We need to convert them to SecurityFinding objects and add to all_findings
873
+ rust_findings: List[SecurityFinding] = []
874
+ for rust_finding in rust_results:
875
+ # Extract line number from byte offset (approximate: count newlines)
876
+ start_byte = rust_finding.get("start", 0)
877
+ end_byte = rust_finding.get("end", start_byte)
878
+
879
+ # Get file content for line calculation
880
+ try:
881
+ file_content = Path(str(target_path)).read_text(encoding="utf-8")
882
+ except Exception:
883
+ file_content = ""
884
+
885
+ # Convert byte offset to line number (1-indexed)
886
+ start_line = file_content[:start_byte].count('\n') + 1 if start_byte > 0 else 1
887
+ end_line = file_content[:end_byte].count('\n') + 1 if end_byte > 0 else start_line
888
+
889
+ # Parse severity (Rust uses "critical", Python uses "critical")
890
+ sev_str = rust_finding.get("severity", "info")
891
+ if sev_str == "critical":
892
+ sev = SecuritySeverity.CRITICAL
893
+ elif sev_str == "high":
894
+ sev = SecuritySeverity.HIGH
895
+ elif sev_str == "medium":
896
+ sev = SecuritySeverity.MEDIUM
897
+ elif sev_str == "low":
898
+ sev = SecuritySeverity.LOW
899
+ else:
900
+ sev = SecuritySeverity.INFO
901
+
902
+ # Build CVSS vector from score
903
+ cvss_score = rust_finding.get("cvss_score")
904
+ cvss_vector = f"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" if cvss_score else ""
905
+
906
+ rust_finding_obj = SecurityFinding(
907
+ rule_id=rust_finding.get("rule_id", "SEC-UNK"),
908
+ severity=sev,
909
+ confidence=0.95, # Rust scanner has high confidence
910
+ cwe_id=rust_finding.get("cwe_id") or "",
911
+ owasp_id=rust_finding.get("owasp_id") or "",
912
+ cvss_score=float(cvss_score) if cvss_score else 0.0,
913
+ cvss_vector=cvss_vector,
914
+ file=str(target_path),
915
+ start_line=start_line,
916
+ end_line=end_line,
917
+ snippet=rust_finding.get("snippet", "")[:200],
918
+ problem=rust_finding.get("problem", "Security issue detected"),
919
+ fix_constraints=(rust_finding.get("fix_hint", "Fix this security issue"),),
920
+ do_not=("Do not ignore this finding.",),
921
+ verify=("Review and fix the code.",),
922
+ resources=(),
923
+ can_auto_fix=rust_finding.get("auto_fix_available", False),
924
+ auto_fix_available=rust_finding.get("auto_fix_available", False),
925
+ )
926
+ rust_findings.append(rust_finding_obj)
927
+
864
928
  # Aggregate by severity
865
929
  summary = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0, "dep": len(dep_findings)}
930
+
931
+ # Count Python findings
866
932
  for f in all_findings:
867
933
  summary[f.severity] = summary.get(f.severity, 0) + 1
868
934
 
935
+ # Count Rust findings (merge into all_findings)
936
+ all_findings.extend(rust_findings)
937
+ for f in rust_findings:
938
+ summary[f.severity] = summary.get(f.severity, 0) + 1
939
+
869
940
  if format == 'json':
870
941
  output_data = {
871
942
  "scan_version": "1.0.0",
@@ -963,6 +1034,9 @@ def check(target, severity, cvss, output, format, fail_on, skip_deps, verbose, u
963
1034
  click.echo("")
964
1035
  click.secho(f" [FAIL] Found {sev_label} issue(s) - exiting with code 1", fg="red", bold=True)
965
1036
 
1037
+ # Hiển thị menu gợi ý tính năng khác
1038
+ show_feature_menu("check", f"{sum(summary.values()) - summary.get('dep', 0)} issues found")
1039
+
966
1040
  return exit_code
967
1041
 
968
1042
 
@@ -1362,5 +1436,138 @@ def security_db(update, status, force):
1362
1436
  return 0
1363
1437
 
1364
1438
 
1439
+ # --------------------------------------------------------------------------
1440
+ # Interactive Feature Menu
1441
+ # --------------------------------------------------------------------------
1442
+
1443
+ def show_feature_menu(last_command: str = "", context: str = "") -> None:
1444
+ """Show interactive menu guiding users to other features.
1445
+
1446
+ Args:
1447
+ last_command: The command that was just run (e.g., "check", "clean")
1448
+ context: Optional context about what was scanned/analyzed
1449
+ """
1450
+ click.echo("")
1451
+ click.echo("")
1452
+ click.echo(" ┌─────────────────────────────────────────────────────────────┐")
1453
+ click.echo(" │ EXPLORE MORE FEATURES │")
1454
+ click.echo(" └─────────────────────────────────────────────────────────────┘")
1455
+ click.echo("")
1456
+
1457
+ # Gợi ý thông minh dựa trên command vừa chạy
1458
+ suggestions = _get_menu_suggestions(last_command, context)
1459
+ for key, (icon, title, desc, cmd) in suggestions.items():
1460
+ click.echo(f" {click.style(f'[{key}]', fg='cyan', bold=True)} {icon} {click.style(title, bold=True)}")
1461
+ click.echo(f" {desc}")
1462
+ click.echo(f" → {click.style(cmd, fg='green')}")
1463
+ click.echo("")
1464
+
1465
+ click.echo(f" {click.style('[q]', fg='yellow', bold=True)} Exit - return to terminal")
1466
+ click.echo(f" {click.style('[Enter]', fg='white', dim=True)} Skip this menu")
1467
+ click.echo("")
1468
+ click.echo(" ────────────────────────────────────────────────────────────")
1469
+ click.echo("")
1470
+
1471
+ # Chờ input
1472
+ try:
1473
+ choice = input(" Select option (Enter to exit): ").strip().lower()
1474
+ except (EOFError, KeyboardInterrupt):
1475
+ choice = 'q'
1476
+
1477
+ if choice in ('q', 'quit', 'exit', ''):
1478
+ return
1479
+
1480
+ # Xử lý lựa chọn
1481
+ _handle_menu_choice(choice, suggestions)
1482
+
1483
+
1484
+ def _get_menu_suggestions(last_command: str, context: str) -> Dict[str, tuple]:
1485
+ """Get smart menu suggestions based on the last command.
1486
+
1487
+ Returns dict of {key: (icon, title, description, command_example)}
1488
+ """
1489
+ all_options = {
1490
+ # Bảo mật & Quét lỗi
1491
+ '1': ('🔒', 'Security Check',
1492
+ 'Quét lỗ hổng: SQL injection, path traversal, hardcoded secrets...',
1493
+ 'pyneat check file.py'),
1494
+ '2': ('📖', 'Explain Rule',
1495
+ 'Nguyên nhân, cách fix, CWE/OWASP, verification steps...',
1496
+ 'pyneat explain SEC-001'),
1497
+
1498
+ # Làm sạch code
1499
+ '3': ('🧹', 'Clean Code',
1500
+ 'Thêm type hints, xóa unused imports, số magic, debug prints...',
1501
+ 'pyneat clean file.py'),
1502
+
1503
+ # Báo cáo & CI/CD
1504
+ '4': ('📊', 'Export Report (JSON/SARIF)',
1505
+ 'Tích hợp CI/CD: GitHub Code Scanning, GitLab SAST...',
1506
+ 'pyneat report . -f sarif -o security.sarif'),
1507
+
1508
+ # So sánh & Debug
1509
+ '5': ('🔍', 'View Diff',
1510
+ 'So sánh code trước và sau khi làm sạch...',
1511
+ 'pyneat clean file.py --diff'),
1512
+
1513
+ # Cấu hình
1514
+ '6': ('📋', 'View All Rules',
1515
+ 'Trạng thái bật/tắt, severity, mô tả ngắn của mỗi rule...',
1516
+ 'pyneat rules'),
1517
+ '7': ('⚙️', 'Configure & Optimize',
1518
+ 'Bật/tắt rules, đặt ngưỡng, cấu hình package...',
1519
+ 'pyneat rules --verbose'),
1520
+ }
1521
+
1522
+ # Smart suggestions dựa trên command vừa chạy
1523
+ if last_command == 'check':
1524
+ # Sau check bảo mật → clean + explain + report
1525
+ ordered = ['3', '2', '4', '5']
1526
+ elif last_command == 'clean':
1527
+ # Sau clean → check bảo mật + diff + report
1528
+ ordered = ['1', '5', '4', '7']
1529
+ elif last_command == 'explain':
1530
+ # Sau explain → check + clean + report
1531
+ ordered = ['1', '3', '4', '5']
1532
+ elif last_command == 'rules':
1533
+ # Sau rules → check + clean
1534
+ ordered = ['1', '3', '2', '4']
1535
+ elif last_command == 'report':
1536
+ # Sau report → check + clean
1537
+ ordered = ['1', '3', '2', '7']
1538
+ else:
1539
+ ordered = ['1', '3', '2', '4']
1540
+
1541
+ return {k: all_options[k] for k in ordered if k in all_options}
1542
+
1543
+
1544
+ def _handle_menu_choice(choice: str, suggestions: Dict[str, tuple]) -> None:
1545
+ """Handle user's menu choice."""
1546
+ choice_map = {
1547
+ '1': ('check', 'pyneat check file.py --help'),
1548
+ '2': ('explain', 'pyneat explain --help'),
1549
+ '3': ('clean', 'pyneat clean file.py --help'),
1550
+ '4': ('report', 'pyneat report --help'),
1551
+ '5': ('diff', 'pyneat clean file.py --diff --help'),
1552
+ '6': ('rules', 'pyneat rules --help'),
1553
+ '7': ('config', 'pyneat rules --help'),
1554
+ }
1555
+
1556
+ if choice in choice_map:
1557
+ feature, help_cmd = choice_map[choice]
1558
+ click.echo("")
1559
+ click.echo(f" 📌 To use '{feature}':")
1560
+ click.echo(f" {click.style(help_cmd, fg='cyan')}")
1561
+ click.echo("")
1562
+ click.echo(f" 💡 View all options:")
1563
+ click.echo(f" {click.style(f'pyneat {feature} --help', fg='green')}")
1564
+ else:
1565
+ click.echo("")
1566
+ click.echo(f" {click.style('[!]', fg='yellow', bold=True)} Invalid option.")
1567
+ click.echo(f" Press 'q' or Enter to exit.")
1568
+ click.echo("")
1569
+ click.echo(" 📚 Docs: https://pyneat.dev/docs")
1570
+
1571
+
1365
1572
  if __name__ == '__main__':
1366
1573
  cli()
@@ -28,6 +28,7 @@ class RustScanner:
28
28
  _available: bool = False
29
29
  _binary_path: Optional[str] = None
30
30
  _version: Optional[str] = None
31
+ _cargo_root: Optional[str] = None
31
32
 
32
33
  def __new__(cls) -> 'RustScanner':
33
34
  """Singleton pattern to ensure only one Rust scanner instance."""
@@ -47,16 +48,26 @@ class RustScanner:
47
48
 
48
49
  def _find_binary(self) -> None:
49
50
  """Find the Rust binary in common locations."""
51
+ # Resolve relative paths from the project root (where pyneat package lives)
52
+ # This ensures the scanner works regardless of current working directory
53
+ package_dir = Path(__file__).resolve().parent # .../pyneat/scanner/
54
+ project_root = package_dir.parent.parent # .../pyneat/ -> .../
55
+ pyneat_rs_dir = project_root / "pyneat-rs"
56
+
50
57
  possible_paths = [
51
- # Current directory
52
- Path("pyneat-rs/target/debug/pyneat-rs.exe"),
53
- Path("pyneat-rs/target/debug/pyneat-rs"),
58
+ # Relative to project root (where pyneat-rs/ directory lives)
59
+ pyneat_rs_dir / "target" / "debug" / "pyneat-rs.exe",
60
+ pyneat_rs_dir / "target" / "debug" / "pyneat-rs",
61
+ pyneat_rs_dir / "target" / "release" / "pyneat-rs.exe",
62
+ pyneat_rs_dir / "target" / "release" / "pyneat-rs",
54
63
  # Installed via maturin
55
64
  Path(sys.prefix) / "bin" / "pyneat-rs",
56
65
  Path(sys.prefix) / "Scripts" / "pyneat-rs.exe",
57
66
  # User installation
58
67
  Path.home() / ".cargo" / "bin" / "pyneat-rs",
59
- # Current working directory
68
+ # Legacy relative paths (for backward compatibility)
69
+ Path("pyneat-rs") / "target" / "debug" / "pyneat-rs.exe",
70
+ Path("pyneat-rs") / "target" / "debug" / "pyneat-rs",
60
71
  Path.cwd() / "pyneat-rs.exe",
61
72
  Path.cwd() / "pyneat-rs",
62
73
  ]
@@ -78,14 +89,25 @@ class RustScanner:
78
89
  except ImportError:
79
90
  pass
80
91
 
81
- # Try cargo run
92
+ # Try cargo run (relative to project root)
93
+ if pyneat_rs_dir.exists():
94
+ cargo_toml = pyneat_rs_dir / "Cargo.toml"
95
+ if cargo_toml.exists():
96
+ self._available = True
97
+ self._binary_path = "<cargo>"
98
+ self._cargo_root = str(pyneat_rs_dir)
99
+ logger.info("Rust scanner available via cargo run")
100
+ return
101
+
102
+ # Fallback: try relative to cwd (legacy support)
82
103
  cargo_path = Path("pyneat-rs")
83
104
  if cargo_path.exists():
84
- Cargo_toml = cargo_path / "Cargo.toml"
85
- if Cargo_toml.exists():
105
+ cargo_toml = cargo_path / "Cargo.toml"
106
+ if cargo_toml.exists():
86
107
  self._available = True
87
108
  self._binary_path = "<cargo>"
88
- logger.info("Rust scanner available via cargo run")
109
+ self._cargo_root = str(cargo_path.resolve())
110
+ logger.info("Rust scanner available via cargo run (cwd fallback)")
89
111
 
90
112
  def _validate_binary(self) -> None:
91
113
  """Validate that the Rust binary is working."""
@@ -93,7 +115,7 @@ class RustScanner:
93
115
  if self._binary_path == "<cargo>":
94
116
  result = subprocess.run(
95
117
  ["cargo", "run", "--", "--version"],
96
- cwd="pyneat-rs",
118
+ cwd=self._cargo_root or "pyneat-rs",
97
119
  capture_output=True,
98
120
  text=True,
99
121
  timeout=30
@@ -161,7 +183,7 @@ class RustScanner:
161
183
  result = subprocess.run(
162
184
  ["cargo", "run", "--", "scan", "-f", "json"],
163
185
  input=code,
164
- cwd="pyneat-rs",
186
+ cwd=self._cargo_root or "pyneat-rs",
165
187
  capture_output=True,
166
188
  text=True,
167
189
  timeout=60
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyneat-cli
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Neat Python AI Code Cleaner — removes AI-generated code artifacts, dead code, and security vulnerabilities
5
5
  License: GNU AGPL-3.0-or-later
6
6
  Keywords: ai,code-cleaner,python,linter,formatter,security,dead-code,refactoring,ast,auto-fix
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyneat-cli"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "Neat Python AI Code Cleaner — removes AI-generated code artifacts, dead code, and security vulnerabilities"
9
9
  license = {text = "GNU AGPL-3.0-or-later"}
10
10
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes