pyneat-cli 2.3.0__tar.gz → 2.4.1__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-2.4.1}/PKG-INFO +29 -2
  2. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/README.md +28 -1
  3. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/__init__.py +1 -1
  4. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/cli.py +187 -0
  5. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/scanner/rust_scanner.py +32 -10
  6. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/PKG-INFO +29 -2
  7. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyproject.toml +1 -1
  8. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/LICENSE +0 -0
  9. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/__main__.py +0 -0
  10. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/benchmark.py +0 -0
  11. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/config.py +0 -0
  12. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/__init__.py +0 -0
  13. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/atomic.py +0 -0
  14. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/engine.py +0 -0
  15. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/manifest.py +0 -0
  16. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/marker_cleanup.py +0 -0
  17. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/scope_guard.py +0 -0
  18. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/semantic_guard.py +0 -0
  19. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/type_shield.py +0 -0
  20. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/types.py +0 -0
  21. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/pre_commit.py +0 -0
  22. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/__init__.py +0 -0
  23. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/ai_bugs.py +0 -0
  24. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/base.py +0 -0
  25. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/comments.py +0 -0
  26. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/conservative.py +0 -0
  27. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/dataclass.py +0 -0
  28. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/deadcode.py +0 -0
  29. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/debug.py +0 -0
  30. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/destructive.py +0 -0
  31. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/duplication.py +0 -0
  32. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/fstring.py +0 -0
  33. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/imports.py +0 -0
  34. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/init_protection.py +0 -0
  35. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/is_not_none.py +0 -0
  36. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/isolated.py +0 -0
  37. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/magic_numbers.py +0 -0
  38. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/match_case.py +0 -0
  39. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/naming.py +0 -0
  40. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/performance.py +0 -0
  41. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/quality.py +0 -0
  42. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/range_len_pattern.py +0 -0
  43. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/redundant.py +0 -0
  44. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/refactoring.py +0 -0
  45. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/safe.py +0 -0
  46. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security.py +0 -0
  47. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/__init__.py +0 -0
  48. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/critical.py +0 -0
  49. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/high.py +0 -0
  50. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/info.py +0 -0
  51. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/low.py +0 -0
  52. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/medium.py +0 -0
  53. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_registry.py +0 -0
  54. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/typing.py +0 -0
  55. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/unused.py +0 -0
  56. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/__init__.py +0 -0
  57. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/__init__.py +0 -0
  58. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/__main__.py +0 -0
  59. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/debug_logger.py +0 -0
  60. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/fuzz_runner.py +0 -0
  61. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/github_client.py +0 -0
  62. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/security/advisory_db.py +0 -0
  63. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/security/dependency_scanner.py +0 -0
  64. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/utils/naming.py +0 -0
  65. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/SOURCES.txt +0 -0
  66. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/dependency_links.txt +0 -0
  67. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/entry_points.txt +0 -0
  68. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/requires.txt +0 -0
  69. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/top_level.txt +0 -0
  70. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/setup.cfg +0 -0
  71. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/setup.py +0 -0
  72. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_engine.py +0 -0
  73. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_fuzz.py +0 -0
  74. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_fuzz_github.py +0 -0
  75. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_integration.py +0 -0
  76. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_layers.py +0 -0
  77. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_rust_scanner.py +0 -0
  78. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_semantic_integrity.py +0 -0
  79. {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/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.1
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
@@ -32,7 +32,7 @@ Dynamic: license-file
32
32
 
33
33
  # PyNeat: The Anti-Spaghetti Code Cleaner
34
34
 
35
- **PyNeat 2.3.0** is an aggressive, AST-based Python code refactoring tool designed to clean up messy, legacy, or AI-generated code. Unlike standard formatters that only fix whitespace, PyNeat performs deep structural surgery on your logic in a single optimized pass using LibCST.
35
+ **PyNeat 2.4.0** is an aggressive, AST-based Python code refactoring tool designed to clean up messy, legacy, or AI-generated code. Unlike standard formatters that only fix whitespace, PyNeat performs deep structural surgery on your logic in a single optimized pass using LibCST.
36
36
 
37
37
  ## Features
38
38
 
@@ -120,6 +120,33 @@ Aggressive rules that **may break code** — always review changes:
120
120
  - `--backup` + `--in-place` — safe file modification
121
121
  - `--export-manifest` — auto-export PYNAGENT manifest
122
122
 
123
+ ### Interactive Feature Menu
124
+ After running `check` or `clean`, an interactive menu appears with smart suggestions:
125
+ - Shows 4-7 relevant features based on the last command
126
+ - Option names in English, descriptions in Vietnamese
127
+ - Press Enter or q to skip
128
+
129
+ ```
130
+ ┌─────────────────────────────────────────────────────────────┐
131
+ │ EXPLORE MORE FEATURES │
132
+ └─────────────────────────────────────────────────────────────┘
133
+
134
+ [3] 🧹 Clean Code
135
+ Thêm type hints, xóa unused imports, số magic, debug prints...
136
+ → pyneat clean file.py
137
+
138
+ [2] 📖 Explain Rule
139
+ Nguyên nhân, cách fix, CWE/OWASP, verification steps...
140
+ → pyneat explain SEC-001
141
+
142
+ [4] 📊 Export Report (JSON/SARIF)
143
+ Tích hợp CI/CD: GitHub Code Scanning, GitLab SAST...
144
+ → pyneat report . -f sarif -o security.sarif
145
+
146
+ [q] Exit - return to terminal
147
+ [Enter] Skip this menu
148
+ ```
149
+
123
150
  ### Pre-commit + GitHub Actions
124
151
  - Auto-generate `.pyneat.manifest.json` on commit
125
152
  - CI/CD job for automated manifest export on push/PR
@@ -1,6 +1,6 @@
1
1
  # PyNeat: The Anti-Spaghetti Code Cleaner
2
2
 
3
- **PyNeat 2.3.0** is an aggressive, AST-based Python code refactoring tool designed to clean up messy, legacy, or AI-generated code. Unlike standard formatters that only fix whitespace, PyNeat performs deep structural surgery on your logic in a single optimized pass using LibCST.
3
+ **PyNeat 2.4.0** is an aggressive, AST-based Python code refactoring tool designed to clean up messy, legacy, or AI-generated code. Unlike standard formatters that only fix whitespace, PyNeat performs deep structural surgery on your logic in a single optimized pass using LibCST.
4
4
 
5
5
  ## Features
6
6
 
@@ -88,6 +88,33 @@ Aggressive rules that **may break code** — always review changes:
88
88
  - `--backup` + `--in-place` — safe file modification
89
89
  - `--export-manifest` — auto-export PYNAGENT manifest
90
90
 
91
+ ### Interactive Feature Menu
92
+ After running `check` or `clean`, an interactive menu appears with smart suggestions:
93
+ - Shows 4-7 relevant features based on the last command
94
+ - Option names in English, descriptions in Vietnamese
95
+ - Press Enter or q to skip
96
+
97
+ ```
98
+ ┌─────────────────────────────────────────────────────────────┐
99
+ │ EXPLORE MORE FEATURES │
100
+ └─────────────────────────────────────────────────────────────┘
101
+
102
+ [3] 🧹 Clean Code
103
+ Thêm type hints, xóa unused imports, số magic, debug prints...
104
+ → pyneat clean file.py
105
+
106
+ [2] 📖 Explain Rule
107
+ Nguyên nhân, cách fix, CWE/OWASP, verification steps...
108
+ → pyneat explain SEC-001
109
+
110
+ [4] 📊 Export Report (JSON/SARIF)
111
+ Tích hợp CI/CD: GitHub Code Scanning, GitLab SAST...
112
+ → pyneat report . -f sarif -o security.sarif
113
+
114
+ [q] Exit - return to terminal
115
+ [Enter] Skip this menu
116
+ ```
117
+
91
118
  ### Pre-commit + GitHub Actions
92
119
  - Auto-generate `.pyneat.manifest.json` on commit
93
120
  - CI/CD job for automated manifest export on push/PR
@@ -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.1"
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,118 @@ 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
+ # Chữ cái A, B, C, D cố định - luôn hiển thị 4 lựa chọn liên tục
1490
+ all_options = {
1491
+ 'A': ('🔒', 'Security Check',
1492
+ 'Quét lỗ hổng: SQL injection, path traversal, hardcoded secrets...',
1493
+ 'pyneat check file.py'),
1494
+ 'B': ('📖', 'Explain Rule',
1495
+ 'Nguyên nhân, cách fix, CWE/OWASP, verification steps...',
1496
+ 'pyneat explain SEC-001'),
1497
+ 'C': ('🧹', 'Clean Code',
1498
+ 'Thêm type hints, xóa unused imports, số magic, debug prints...',
1499
+ 'pyneat clean file.py'),
1500
+ 'D': ('📊', 'Export Report (JSON/SARIF)',
1501
+ 'Tích hợp CI/CD: GitHub Code Scanning, GitLab SAST...',
1502
+ 'pyneat report . -f sarif -o security.sarif'),
1503
+ }
1504
+
1505
+ # Smart suggestions dựa trên command vừa chạy - LUÔN dùng A, B, C, D
1506
+ if last_command == 'check':
1507
+ # Sau check bảo mật → clean + explain + report + diff
1508
+ ordered = ['A', 'B', 'C', 'D']
1509
+ elif last_command == 'clean':
1510
+ # Sau clean → check bảo mật + diff + report + config
1511
+ ordered = ['A', 'B', 'C', 'D']
1512
+ elif last_command == 'explain':
1513
+ # Sau explain → check + clean + report + diff
1514
+ ordered = ['A', 'B', 'C', 'D']
1515
+ elif last_command == 'rules':
1516
+ # Sau rules → check + clean + explain + report
1517
+ ordered = ['A', 'B', 'C', 'D']
1518
+ elif last_command == 'report':
1519
+ # Sau report → check + clean + explain + config
1520
+ ordered = ['A', 'B', 'C', 'D']
1521
+ else:
1522
+ ordered = ['A', 'B', 'C', 'D']
1523
+
1524
+ return {k: all_options[k] for k in ordered if k in all_options}
1525
+
1526
+
1527
+ def _handle_menu_choice(choice: str, suggestions: Dict[str, tuple]) -> None:
1528
+ """Handle user's menu choice."""
1529
+ choice_map = {
1530
+ 'A': ('check', 'pyneat check file.py --help'),
1531
+ 'B': ('explain', 'pyneat explain --help'),
1532
+ 'C': ('clean', 'pyneat clean file.py --help'),
1533
+ 'D': ('report', 'pyneat report --help'),
1534
+ }
1535
+
1536
+ if choice in choice_map:
1537
+ feature, help_cmd = choice_map[choice]
1538
+ click.echo("")
1539
+ click.echo(f" 📌 To use '{feature}':")
1540
+ click.echo(f" {click.style(help_cmd, fg='cyan')}")
1541
+ click.echo("")
1542
+ click.echo(f" 💡 View all options:")
1543
+ click.echo(f" {click.style(f'pyneat {feature} --help', fg='green')}")
1544
+ else:
1545
+ click.echo("")
1546
+ click.echo(f" {click.style('[!]', fg='yellow', bold=True)} Invalid option.")
1547
+ click.echo(f" Press 'q' or Enter to exit.")
1548
+ click.echo("")
1549
+ click.echo(" 📚 Docs: https://pyneat.dev/docs")
1550
+
1551
+
1365
1552
  if __name__ == '__main__':
1366
1553
  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.1
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
@@ -32,7 +32,7 @@ Dynamic: license-file
32
32
 
33
33
  # PyNeat: The Anti-Spaghetti Code Cleaner
34
34
 
35
- **PyNeat 2.3.0** is an aggressive, AST-based Python code refactoring tool designed to clean up messy, legacy, or AI-generated code. Unlike standard formatters that only fix whitespace, PyNeat performs deep structural surgery on your logic in a single optimized pass using LibCST.
35
+ **PyNeat 2.4.0** is an aggressive, AST-based Python code refactoring tool designed to clean up messy, legacy, or AI-generated code. Unlike standard formatters that only fix whitespace, PyNeat performs deep structural surgery on your logic in a single optimized pass using LibCST.
36
36
 
37
37
  ## Features
38
38
 
@@ -120,6 +120,33 @@ Aggressive rules that **may break code** — always review changes:
120
120
  - `--backup` + `--in-place` — safe file modification
121
121
  - `--export-manifest` — auto-export PYNAGENT manifest
122
122
 
123
+ ### Interactive Feature Menu
124
+ After running `check` or `clean`, an interactive menu appears with smart suggestions:
125
+ - Shows 4-7 relevant features based on the last command
126
+ - Option names in English, descriptions in Vietnamese
127
+ - Press Enter or q to skip
128
+
129
+ ```
130
+ ┌─────────────────────────────────────────────────────────────┐
131
+ │ EXPLORE MORE FEATURES │
132
+ └─────────────────────────────────────────────────────────────┘
133
+
134
+ [3] 🧹 Clean Code
135
+ Thêm type hints, xóa unused imports, số magic, debug prints...
136
+ → pyneat clean file.py
137
+
138
+ [2] 📖 Explain Rule
139
+ Nguyên nhân, cách fix, CWE/OWASP, verification steps...
140
+ → pyneat explain SEC-001
141
+
142
+ [4] 📊 Export Report (JSON/SARIF)
143
+ Tích hợp CI/CD: GitHub Code Scanning, GitLab SAST...
144
+ → pyneat report . -f sarif -o security.sarif
145
+
146
+ [q] Exit - return to terminal
147
+ [Enter] Skip this menu
148
+ ```
149
+
123
150
  ### Pre-commit + GitHub Actions
124
151
  - Auto-generate `.pyneat.manifest.json` on commit
125
152
  - CI/CD job for automated manifest export on push/PR
@@ -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.1"
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