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.
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/PKG-INFO +29 -2
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/README.md +28 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/__init__.py +1 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/cli.py +187 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/scanner/rust_scanner.py +32 -10
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/PKG-INFO +29 -2
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyproject.toml +1 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/LICENSE +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/__main__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/benchmark.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/config.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/atomic.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/engine.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/manifest.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/marker_cleanup.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/scope_guard.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/semantic_guard.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/type_shield.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/core/types.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/pre_commit.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/ai_bugs.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/base.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/comments.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/conservative.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/dataclass.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/deadcode.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/debug.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/destructive.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/duplication.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/fstring.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/imports.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/init_protection.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/is_not_none.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/isolated.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/magic_numbers.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/match_case.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/naming.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/performance.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/quality.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/range_len_pattern.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/redundant.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/refactoring.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/safe.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/critical.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/high.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/info.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/low.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_pack/medium.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/security_registry.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/typing.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/rules/unused.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/__main__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/debug_logger.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/fuzz_runner.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/github_fuzz/github_client.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/security/advisory_db.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/tools/security/dependency_scanner.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat/utils/naming.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/SOURCES.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/dependency_links.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/entry_points.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/requires.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/pyneat_cli.egg-info/top_level.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/setup.cfg +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/setup.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_engine.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_fuzz.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_fuzz_github.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_integration.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_layers.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_rust_scanner.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.1}/tests/test_semantic_integrity.py +0 -0
- {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
|
+
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.
|
|
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
|
+
**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
|
|
@@ -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
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
85
|
-
if
|
|
105
|
+
cargo_toml = cargo_path / "Cargo.toml"
|
|
106
|
+
if cargo_toml.exists():
|
|
86
107
|
self._available = True
|
|
87
108
|
self._binary_path = "<cargo>"
|
|
88
|
-
|
|
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
|
+
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|