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.
- {pyneat_cli-2.3.0/pyneat_cli.egg-info → pyneat_cli-2.4.0}/PKG-INFO +1 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/__init__.py +1 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/cli.py +207 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/scanner/rust_scanner.py +32 -10
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0/pyneat_cli.egg-info}/PKG-INFO +1 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyproject.toml +1 -1
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/LICENSE +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/README.md +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/__main__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/benchmark.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/config.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/atomic.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/engine.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/manifest.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/marker_cleanup.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/scope_guard.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/semantic_guard.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/type_shield.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/core/types.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/pre_commit.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/ai_bugs.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/base.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/comments.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/conservative.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/dataclass.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/deadcode.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/debug.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/destructive.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/duplication.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/fstring.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/imports.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/init_protection.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/is_not_none.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/isolated.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/magic_numbers.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/match_case.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/naming.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/performance.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/quality.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/range_len_pattern.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/redundant.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/refactoring.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/safe.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/critical.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/high.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/info.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/low.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_pack/medium.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/security_registry.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/typing.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/rules/unused.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/__init__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/__main__.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/debug_logger.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/fuzz_runner.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/github_fuzz/github_client.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/security/advisory_db.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/tools/security/dependency_scanner.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat/utils/naming.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/SOURCES.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/dependency_links.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/entry_points.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/requires.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/pyneat_cli.egg-info/top_level.txt +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/setup.cfg +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/setup.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_engine.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_fuzz.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_fuzz_github.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_integration.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_layers.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_rust_scanner.py +0 -0
- {pyneat_cli-2.3.0 → pyneat_cli-2.4.0}/tests/test_semantic_integrity.py +0 -0
- {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
|
+
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
|
|
@@ -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
|
-
#
|
|
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.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.
|
|
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
|
|
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
|