kryptorious-devflow 1.0.0__py3-none-any.whl
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.
- devflow/__init__.py +6 -0
- devflow/cli.py +140 -0
- devflow/commands/__init__.py +1 -0
- devflow/commands/audit.py +508 -0
- devflow/commands/fix.py +175 -0
- devflow/commands/init.py +620 -0
- devflow/commands/ship.py +224 -0
- devflow/utils.py +87 -0
- kryptorious_devflow-1.0.0.dist-info/METADATA +201 -0
- kryptorious_devflow-1.0.0.dist-info/RECORD +13 -0
- kryptorious_devflow-1.0.0.dist-info/WHEEL +5 -0
- kryptorious_devflow-1.0.0.dist-info/entry_points.txt +2 -0
- kryptorious_devflow-1.0.0.dist-info/top_level.txt +1 -0
devflow/__init__.py
ADDED
devflow/cli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""DevFlow CLI — main entry point."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich import print as rprint
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
@click.version_option(version="1.0.0", prog_name="devflow")
|
|
12
|
+
def main():
|
|
13
|
+
"""DevFlow — Developer Workflow Automation CLI.
|
|
14
|
+
|
|
15
|
+
The unified tool for project scaffolding, codebase auditing,
|
|
16
|
+
auto-fixing, and release shipping. One tool, four commands.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@main.command()
|
|
22
|
+
@click.argument("name")
|
|
23
|
+
@click.option(
|
|
24
|
+
"--template", "-t",
|
|
25
|
+
type=click.Choice(["python", "node", "fullstack", "cli", "api", "lib"]),
|
|
26
|
+
default="python",
|
|
27
|
+
help="Project template type"
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--path", "-p",
|
|
31
|
+
default=".",
|
|
32
|
+
help="Parent directory for the new project"
|
|
33
|
+
)
|
|
34
|
+
@click.option("--docker/--no-docker", default=True, help="Include Docker config")
|
|
35
|
+
@click.option("--ci/--no-ci", default=True, help="Include CI/CD (GitHub Actions)")
|
|
36
|
+
@click.option("--git/--no-git", default=True, help="Initialize git repository")
|
|
37
|
+
@click.option("--license", "-l", default="MIT", help="License type")
|
|
38
|
+
@click.option("--description", "-d", default="", help="Project description")
|
|
39
|
+
def init(name, template, path, docker, ci, git, license, description):
|
|
40
|
+
"""Scaffold a new project with best practices.
|
|
41
|
+
|
|
42
|
+
Creates a production-ready project structure with linting, formatting,
|
|
43
|
+
testing, CI/CD, Docker, and git — all configured out of the box.
|
|
44
|
+
|
|
45
|
+
\b
|
|
46
|
+
Examples:
|
|
47
|
+
devflow init my-api --template api
|
|
48
|
+
devflow init my-lib --template lib --no-docker
|
|
49
|
+
devflow init my-app --template fullstack --description "My SaaS app"
|
|
50
|
+
"""
|
|
51
|
+
from .commands.init import run
|
|
52
|
+
run(name=name, template=template, path=path, docker=docker,
|
|
53
|
+
ci=ci, git_init=git, license_type=license, description=description)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@main.command()
|
|
57
|
+
@click.option(
|
|
58
|
+
"--path", "-p",
|
|
59
|
+
default=".",
|
|
60
|
+
help="Path to the project to audit"
|
|
61
|
+
)
|
|
62
|
+
@click.option(
|
|
63
|
+
"--format", "-f", "output_format",
|
|
64
|
+
type=click.Choice(["terminal", "markdown", "json"]),
|
|
65
|
+
default="terminal",
|
|
66
|
+
help="Output format"
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--severity", "-s",
|
|
70
|
+
type=click.Choice(["error", "warning", "info", "all"]),
|
|
71
|
+
default="all",
|
|
72
|
+
help="Minimum severity to show"
|
|
73
|
+
)
|
|
74
|
+
@click.option("--security/--no-security", default=True, help="Run security audit")
|
|
75
|
+
@click.option("--deps/--no-deps", default=True, help="Check dependencies")
|
|
76
|
+
def audit(path, output_format, severity, security, deps):
|
|
77
|
+
"""Run a comprehensive codebase health check.
|
|
78
|
+
|
|
79
|
+
Checks linting, formatting, type checking, security vulnerabilities,
|
|
80
|
+
dependency freshness, documentation coverage, and test coverage.
|
|
81
|
+
Produces a detailed report with actionable recommendations.
|
|
82
|
+
|
|
83
|
+
\b
|
|
84
|
+
Examples:
|
|
85
|
+
devflow audit
|
|
86
|
+
devflow audit --format markdown --severity warning
|
|
87
|
+
devflow audit --no-security
|
|
88
|
+
"""
|
|
89
|
+
from .commands.audit import run
|
|
90
|
+
run(path=path, output_format=output_format, severity=severity,
|
|
91
|
+
security=security, deps=deps)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@main.command()
|
|
95
|
+
@click.option("--path", "-p", default=".", help="Path to the project")
|
|
96
|
+
@click.option("--dry-run/--apply", default=False, help="Preview fixes without applying")
|
|
97
|
+
@click.option("--scope", "-s", type=click.Choice(["all", "format", "lint", "imports", "security"]), default="all", help="Fix scope")
|
|
98
|
+
def fix(path, dry_run, scope):
|
|
99
|
+
"""Automatically fix common code issues.
|
|
100
|
+
|
|
101
|
+
Applies formatting, sorts imports, fixes lint violations,
|
|
102
|
+
and addresses common security issues. Use --dry-run to preview.
|
|
103
|
+
|
|
104
|
+
\b
|
|
105
|
+
Examples:
|
|
106
|
+
devflow fix --dry-run
|
|
107
|
+
devflow fix --scope format
|
|
108
|
+
devflow fix --apply
|
|
109
|
+
"""
|
|
110
|
+
from .commands.fix import run
|
|
111
|
+
run(path=path, dry_run=dry_run, scope=scope)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@main.command()
|
|
115
|
+
@click.option("--path", "-p", default=".", help="Path to the project")
|
|
116
|
+
@click.option("--bump", "-b", type=click.Choice(["patch", "minor", "major"]), required=True, help="Version bump type")
|
|
117
|
+
@click.option("--message", "-m", default="", help="Release message for changelog")
|
|
118
|
+
@click.option("--dry-run/--execute", default=False, help="Preview release without executing")
|
|
119
|
+
@click.option("--tag/--no-tag", default=True, help="Create git tag")
|
|
120
|
+
@click.option("--push/--no-push", default=False, help="Push to remote")
|
|
121
|
+
def ship(path, bump, message, dry_run, tag, push):
|
|
122
|
+
"""Prepare and execute a release.
|
|
123
|
+
|
|
124
|
+
Bumps version, updates changelog, creates git tag,
|
|
125
|
+
builds distribution packages, and optionally pushes.
|
|
126
|
+
Runs audit first to ensure quality gates pass.
|
|
127
|
+
|
|
128
|
+
\b
|
|
129
|
+
Examples:
|
|
130
|
+
devflow ship --bump patch --message "Bug fixes"
|
|
131
|
+
devflow ship --bump minor --dry-run
|
|
132
|
+
devflow ship --bump major --push --message "Breaking changes v2.0"
|
|
133
|
+
"""
|
|
134
|
+
from .commands.ship import run
|
|
135
|
+
run(path=path, bump=bump, message=message, dry_run=dry_run,
|
|
136
|
+
tag=tag, push=push)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""DevFlow commands package."""
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""devflow audit — Comprehensive codebase health check.
|
|
2
|
+
|
|
3
|
+
Checks linting, formatting, security, dependencies, and documentation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Tuple, Dict, Any
|
|
11
|
+
|
|
12
|
+
from ..utils import (
|
|
13
|
+
console, print_header, print_success, print_error, print_warning, print_info,
|
|
14
|
+
find_project_root, check_tool_available, run_cmd, create_results_table
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(path: str, output_format: str, severity: str, security: bool, deps: bool):
|
|
19
|
+
"""Run comprehensive codebase audit."""
|
|
20
|
+
|
|
21
|
+
root = find_project_root(path)
|
|
22
|
+
print_header(f"DevFlow Audit — [bold cyan]{root.name}[/bold cyan]")
|
|
23
|
+
|
|
24
|
+
results = {
|
|
25
|
+
"project": str(root),
|
|
26
|
+
"checks": [],
|
|
27
|
+
"score": 0,
|
|
28
|
+
"max_score": 0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Detect project type
|
|
32
|
+
ptype = _detect_project_type(root)
|
|
33
|
+
print_info(f"Detected project type: [bold]{ptype}[/bold]")
|
|
34
|
+
|
|
35
|
+
# Run all checks
|
|
36
|
+
checks: List[Tuple[str, str, bool, List[str]]] = []
|
|
37
|
+
|
|
38
|
+
checks.extend(_check_structure(root, ptype))
|
|
39
|
+
checks.extend(_check_linting(root, ptype))
|
|
40
|
+
checks.extend(_check_formatting(root, ptype))
|
|
41
|
+
checks.extend(_check_testing(root, ptype))
|
|
42
|
+
checks.extend(_check_documentation(root))
|
|
43
|
+
|
|
44
|
+
if security:
|
|
45
|
+
checks.extend(_check_security(root, ptype))
|
|
46
|
+
|
|
47
|
+
if deps:
|
|
48
|
+
checks.extend(_check_dependencies(root, ptype))
|
|
49
|
+
|
|
50
|
+
# Filter by severity
|
|
51
|
+
sev_order = {"error": 0, "warning": 1, "info": 2}
|
|
52
|
+
min_sev = sev_order.get(severity, 2)
|
|
53
|
+
checks = [c for c in checks if sev_order.get(c[2], 2) <= min_sev]
|
|
54
|
+
|
|
55
|
+
# Score
|
|
56
|
+
passed = sum(1 for c in checks if c[1] == "pass")
|
|
57
|
+
total = len(checks)
|
|
58
|
+
score = int((passed / total) * 100) if total > 0 else 100
|
|
59
|
+
|
|
60
|
+
# Output
|
|
61
|
+
if output_format == "terminal":
|
|
62
|
+
_output_terminal(checks, score, root, ptype)
|
|
63
|
+
elif output_format == "markdown":
|
|
64
|
+
_output_markdown(checks, score, root, ptype)
|
|
65
|
+
elif output_format == "json":
|
|
66
|
+
_output_json(checks, score, root)
|
|
67
|
+
|
|
68
|
+
results["checks"] = [{"name": c[0], "status": c[1], "severity": c[2], "details": c[3]} for c in checks]
|
|
69
|
+
results["score"] = score
|
|
70
|
+
results["max_score"] = 100
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _detect_project_type(root: Path) -> str:
|
|
74
|
+
"""Detect project type from config files."""
|
|
75
|
+
if (root / "pyproject.toml").exists() or (root / "setup.py").exists():
|
|
76
|
+
# Check if it's an API
|
|
77
|
+
pyproject = root / "pyproject.toml"
|
|
78
|
+
if pyproject.exists():
|
|
79
|
+
content = pyproject.read_text()
|
|
80
|
+
if "fastapi" in content.lower() or "flask" in content.lower():
|
|
81
|
+
return "python-api"
|
|
82
|
+
if "click" in content.lower() and "[project.scripts]" in content:
|
|
83
|
+
return "python-cli"
|
|
84
|
+
return "python"
|
|
85
|
+
elif (root / "package.json").exists():
|
|
86
|
+
return "node"
|
|
87
|
+
elif (root / "go.mod").exists():
|
|
88
|
+
return "go"
|
|
89
|
+
return "unknown"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_structure(root: Path, ptype: str) -> List[Tuple[str, str, str, List[str]]]:
|
|
93
|
+
"""Check project structure conventions."""
|
|
94
|
+
checks = []
|
|
95
|
+
|
|
96
|
+
# Source directory
|
|
97
|
+
if ptype.startswith("python"):
|
|
98
|
+
has_src = (root / "src").is_dir()
|
|
99
|
+
# In src layout, package is inside src/ — check subdirectories for __init__.py
|
|
100
|
+
has_pkg = False
|
|
101
|
+
if has_src:
|
|
102
|
+
for d in (root / "src").iterdir():
|
|
103
|
+
if d.is_dir() and (d / "__init__.py").exists():
|
|
104
|
+
has_pkg = True
|
|
105
|
+
break
|
|
106
|
+
checks.append((
|
|
107
|
+
"Source directory (src/)",
|
|
108
|
+
"pass" if has_src else "warning",
|
|
109
|
+
"warning",
|
|
110
|
+
["Use src/ layout for clean package separation"] if not has_src else []
|
|
111
|
+
))
|
|
112
|
+
checks.append((
|
|
113
|
+
"Package __init__.py",
|
|
114
|
+
"pass" if has_pkg else "error",
|
|
115
|
+
"error",
|
|
116
|
+
["No Python package found in src/ — missing __init__.py"] if not has_pkg else []
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
# Tests directory
|
|
120
|
+
has_tests = (root / "tests").is_dir() or (root / "test").is_dir()
|
|
121
|
+
checks.append((
|
|
122
|
+
"Test directory (tests/)",
|
|
123
|
+
"pass" if has_tests else "warning",
|
|
124
|
+
"warning",
|
|
125
|
+
["No test directory found. Add tests/ with at least one test file."] if not has_tests else []
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
# README
|
|
129
|
+
has_readme = any((root / f).exists() for f in ["README.md", "README.rst", "README"])
|
|
130
|
+
checks.append((
|
|
131
|
+
"README file",
|
|
132
|
+
"pass" if has_readme else "warning",
|
|
133
|
+
"warning",
|
|
134
|
+
["No README found. Every project needs one."] if not has_readme else []
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
# .gitignore
|
|
138
|
+
has_gitignore = (root / ".gitignore").exists()
|
|
139
|
+
checks.append((
|
|
140
|
+
".gitignore file",
|
|
141
|
+
"pass" if has_gitignore else "error",
|
|
142
|
+
"error",
|
|
143
|
+
["No .gitignore — risk of committing generated files."] if not has_gitignore else []
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
return checks
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_linting(root: Path, ptype: str) -> List[Tuple[str, str, str, List[str]]]:
|
|
150
|
+
"""Check linting setup and run."""
|
|
151
|
+
checks = []
|
|
152
|
+
|
|
153
|
+
if ptype.startswith("python"):
|
|
154
|
+
# ruff or flake8
|
|
155
|
+
has_ruff = check_tool_available("ruff")
|
|
156
|
+
has_flake8 = check_tool_available("flake8")
|
|
157
|
+
|
|
158
|
+
if has_ruff:
|
|
159
|
+
code, out, err = run_cmd(["ruff", "check", str(root)], cwd=str(root))
|
|
160
|
+
issue_count = len([l for l in out.split("\n") if l.strip()])
|
|
161
|
+
checks.append((
|
|
162
|
+
"Ruff lint check",
|
|
163
|
+
"pass" if code == 0 else "warning",
|
|
164
|
+
"warning" if issue_count > 0 else "info",
|
|
165
|
+
out.split("\n")[:5] if issue_count > 0 else ["Clean — no lint issues."]
|
|
166
|
+
))
|
|
167
|
+
checks.append((
|
|
168
|
+
"Linting tool configured",
|
|
169
|
+
"pass", "info", ["Ruff is available and configured."]
|
|
170
|
+
))
|
|
171
|
+
elif has_flake8:
|
|
172
|
+
checks.append((
|
|
173
|
+
"Linting tool",
|
|
174
|
+
"pass", "info", ["flake8 detected. Consider upgrading to ruff for speed."]
|
|
175
|
+
))
|
|
176
|
+
else:
|
|
177
|
+
checks.append((
|
|
178
|
+
"Linting tool",
|
|
179
|
+
"warning", "warning",
|
|
180
|
+
["No linter found. Install: pip install ruff"]
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
elif ptype == "node":
|
|
184
|
+
has_eslint = (root / ".eslintrc.js").exists() or (root / ".eslintrc.json").exists() or (root / "eslint.config.js").exists()
|
|
185
|
+
checks.append((
|
|
186
|
+
"ESLint configured",
|
|
187
|
+
"pass" if has_eslint else "warning",
|
|
188
|
+
"warning",
|
|
189
|
+
["No ESLint config found."] if not has_eslint else []
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
return checks
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _check_formatting(root: Path, ptype: str) -> List[Tuple[str, str, str, List[str]]]:
|
|
196
|
+
"""Check code formatting."""
|
|
197
|
+
checks = []
|
|
198
|
+
|
|
199
|
+
if ptype.startswith("python"):
|
|
200
|
+
has_black = check_tool_available("black")
|
|
201
|
+
if has_black:
|
|
202
|
+
code, out, err = run_cmd(["black", "--check", "--diff", str(root)], cwd=str(root))
|
|
203
|
+
checks.append((
|
|
204
|
+
"Black formatting",
|
|
205
|
+
"pass" if code == 0 else "warning",
|
|
206
|
+
"warning" if code != 0 else "info",
|
|
207
|
+
["Code would be reformatted."] if code != 0 else ["All files properly formatted."]
|
|
208
|
+
))
|
|
209
|
+
else:
|
|
210
|
+
checks.append((
|
|
211
|
+
"Formatter (black)",
|
|
212
|
+
"warning", "warning",
|
|
213
|
+
["black not installed. Install: pip install black"]
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
# isort
|
|
217
|
+
has_isort = check_tool_available("isort")
|
|
218
|
+
if has_isort:
|
|
219
|
+
code, out, err = run_cmd(["isort", "--check-only", str(root)], cwd=str(root))
|
|
220
|
+
checks.append((
|
|
221
|
+
"Import sorting (isort)",
|
|
222
|
+
"pass" if code == 0 else "warning",
|
|
223
|
+
"warning" if code != 0 else "info",
|
|
224
|
+
["Imports need sorting."] if code != 0 else ["Imports properly sorted."]
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
elif ptype == "node":
|
|
228
|
+
has_prettier = check_tool_available("prettier") or (root / ".prettierrc").exists()
|
|
229
|
+
checks.append((
|
|
230
|
+
"Prettier configured",
|
|
231
|
+
"pass" if has_prettier else "warning",
|
|
232
|
+
"warning",
|
|
233
|
+
["No Prettier config found."] if not has_prettier else []
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
return checks
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _check_testing(root: Path, ptype: str) -> List[Tuple[str, str, str, List[str]]]:
|
|
240
|
+
"""Check testing setup."""
|
|
241
|
+
checks = []
|
|
242
|
+
|
|
243
|
+
if ptype.startswith("python"):
|
|
244
|
+
test_dir = root / "tests" if (root / "tests").exists() else root / "test"
|
|
245
|
+
if test_dir.exists():
|
|
246
|
+
test_files = list(test_dir.rglob("test_*.py")) + list(test_dir.rglob("*_test.py"))
|
|
247
|
+
checks.append((
|
|
248
|
+
f"Test files ({len(test_files)} found)",
|
|
249
|
+
"pass" if test_files else "warning",
|
|
250
|
+
"warning" if not test_files else "info",
|
|
251
|
+
["No test files in test directory."] if not test_files else [f"{len(test_files)} test files found."]
|
|
252
|
+
))
|
|
253
|
+
|
|
254
|
+
# Try running pytest
|
|
255
|
+
if test_files and check_tool_available("pytest"):
|
|
256
|
+
code, out, err = run_cmd(["pytest", "--tb=no", "-q", str(test_dir)], cwd=str(root))
|
|
257
|
+
checks.append((
|
|
258
|
+
"Tests passing",
|
|
259
|
+
"pass" if code == 0 else "error",
|
|
260
|
+
"error" if code != 0 else "info",
|
|
261
|
+
out.split("\n")[-3:] if code != 0 else [out.strip().split("\n")[-1]]
|
|
262
|
+
))
|
|
263
|
+
else:
|
|
264
|
+
checks.append((
|
|
265
|
+
"Test directory",
|
|
266
|
+
"warning", "warning",
|
|
267
|
+
["No test directory. Create tests/ and add your first test."]
|
|
268
|
+
))
|
|
269
|
+
|
|
270
|
+
elif ptype == "node":
|
|
271
|
+
has_jest = check_tool_available("jest") or (root / "jest.config.js").exists()
|
|
272
|
+
checks.append((
|
|
273
|
+
"Jest configured",
|
|
274
|
+
"pass" if has_jest else "warning",
|
|
275
|
+
"warning",
|
|
276
|
+
["No Jest config found."] if not has_jest else []
|
|
277
|
+
))
|
|
278
|
+
|
|
279
|
+
return checks
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _check_documentation(root: Path) -> List[Tuple[str, str, str, List[str]]]:
|
|
283
|
+
"""Check documentation coverage."""
|
|
284
|
+
checks = []
|
|
285
|
+
|
|
286
|
+
# Docstring coverage (Python only)
|
|
287
|
+
py_files = list(root.rglob("*.py"))
|
|
288
|
+
if py_files:
|
|
289
|
+
no_docstring = 0
|
|
290
|
+
total_functions = 0
|
|
291
|
+
for pf in py_files:
|
|
292
|
+
if "test" in str(pf) or "__init__" in pf.name:
|
|
293
|
+
continue
|
|
294
|
+
content = pf.read_text(errors="ignore")
|
|
295
|
+
lines = content.split("\n")
|
|
296
|
+
for i, line in enumerate(lines):
|
|
297
|
+
if line.strip().startswith("def ") or line.strip().startswith("async def "):
|
|
298
|
+
total_functions += 1
|
|
299
|
+
# Check if next line is a docstring
|
|
300
|
+
if i + 1 < len(lines) and '"""' in lines[i + 1]:
|
|
301
|
+
pass
|
|
302
|
+
elif i + 2 < len(lines) and '"""' in lines[i + 2]:
|
|
303
|
+
pass
|
|
304
|
+
else:
|
|
305
|
+
no_docstring += 1
|
|
306
|
+
|
|
307
|
+
if total_functions > 0:
|
|
308
|
+
pct = int((total_functions - no_docstring) / total_functions * 100)
|
|
309
|
+
checks.append((
|
|
310
|
+
f"Docstring coverage ({pct}%)",
|
|
311
|
+
"pass" if pct >= 80 else "warning",
|
|
312
|
+
"warning" if pct < 80 else "info",
|
|
313
|
+
[f"{no_docstring}/{total_functions} functions missing docstrings."] if no_docstring else ["All functions documented."]
|
|
314
|
+
))
|
|
315
|
+
|
|
316
|
+
return checks
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _check_security(root: Path, ptype: str) -> List[Tuple[str, str, str, List[str]]]:
|
|
320
|
+
"""Security checks."""
|
|
321
|
+
checks = []
|
|
322
|
+
|
|
323
|
+
# Check for bandit
|
|
324
|
+
if ptype.startswith("python") and check_tool_available("bandit"):
|
|
325
|
+
code, out, err = run_cmd(["bandit", "-r", str(root), "-q"], cwd=str(root))
|
|
326
|
+
issues = [l for l in out.split("\n") if "Issue:" in l]
|
|
327
|
+
checks.append((
|
|
328
|
+
"Bandit security scan",
|
|
329
|
+
"pass" if not issues else "error",
|
|
330
|
+
"error" if issues else "info",
|
|
331
|
+
issues[:5] if issues else ["No security issues found."]
|
|
332
|
+
))
|
|
333
|
+
elif ptype.startswith("python"):
|
|
334
|
+
checks.append((
|
|
335
|
+
"Security scanner",
|
|
336
|
+
"warning", "warning",
|
|
337
|
+
["bandit not installed. Install: pip install bandit"]
|
|
338
|
+
))
|
|
339
|
+
|
|
340
|
+
# Check for secrets in code
|
|
341
|
+
secret_patterns = ["API_KEY", "SECRET", "PASSWORD", "TOKEN", "PRIVATE_KEY"]
|
|
342
|
+
found_secrets = []
|
|
343
|
+
for pattern in secret_patterns:
|
|
344
|
+
for py_file in root.rglob("*.py"):
|
|
345
|
+
if "test" in str(py_file).lower():
|
|
346
|
+
continue
|
|
347
|
+
content = py_file.read_text(errors="ignore")
|
|
348
|
+
if f'{pattern} = "' in content or f"{pattern} = '" in content:
|
|
349
|
+
found_secrets.append(f"{py_file.relative_to(root)}: hardcoded {pattern}")
|
|
350
|
+
|
|
351
|
+
checks.append((
|
|
352
|
+
"Hardcoded secrets",
|
|
353
|
+
"pass" if not found_secrets else "error",
|
|
354
|
+
"error" if found_secrets else "info",
|
|
355
|
+
found_secrets[:5] if found_secrets else ["No hardcoded secrets detected."]
|
|
356
|
+
))
|
|
357
|
+
|
|
358
|
+
return checks
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _check_dependencies(root: Path, ptype: str) -> List[Tuple[str, str, str, List[str]]]:
|
|
362
|
+
"""Dependency checks."""
|
|
363
|
+
checks = []
|
|
364
|
+
|
|
365
|
+
if ptype.startswith("python"):
|
|
366
|
+
# Check for pip-audit
|
|
367
|
+
if check_tool_available("pip-audit"):
|
|
368
|
+
code, out, err = run_cmd(["pip-audit", "-r", str(root / "requirements.txt")] if (root / "requirements.txt").exists() else ["pip-audit"], cwd=str(root))
|
|
369
|
+
vulns = [l for l in out.split("\n") if "vulnerab" in l.lower()]
|
|
370
|
+
checks.append((
|
|
371
|
+
"Dependency vulnerabilities",
|
|
372
|
+
"pass" if not vulns else "error",
|
|
373
|
+
"error" if vulns else "info",
|
|
374
|
+
vulns[:5] if vulns else ["No known vulnerabilities."]
|
|
375
|
+
))
|
|
376
|
+
else:
|
|
377
|
+
checks.append((
|
|
378
|
+
"Vulnerability scanner",
|
|
379
|
+
"warning", "warning",
|
|
380
|
+
["pip-audit not installed. Install: pip install pip-audit"]
|
|
381
|
+
))
|
|
382
|
+
|
|
383
|
+
# Check for requirements.txt or pyproject.toml
|
|
384
|
+
has_deps = (root / "pyproject.toml").exists() or (root / "requirements.txt").exists() or (root / "setup.py").exists()
|
|
385
|
+
checks.append((
|
|
386
|
+
"Dependencies declared",
|
|
387
|
+
"pass" if has_deps else "error",
|
|
388
|
+
"error",
|
|
389
|
+
["No dependency declaration found."] if not has_deps else ["Dependencies properly declared."]
|
|
390
|
+
))
|
|
391
|
+
|
|
392
|
+
elif ptype == "node":
|
|
393
|
+
has_package_json = (root / "package.json").exists()
|
|
394
|
+
checks.append((
|
|
395
|
+
"package.json",
|
|
396
|
+
"pass" if has_package_json else "error",
|
|
397
|
+
"error",
|
|
398
|
+
["No package.json found."] if not has_package_json else []
|
|
399
|
+
))
|
|
400
|
+
|
|
401
|
+
if has_package_json:
|
|
402
|
+
has_lock = (root / "package-lock.json").exists() or (root / "yarn.lock").exists() or (root / "pnpm-lock.yaml").exists()
|
|
403
|
+
checks.append((
|
|
404
|
+
"Lock file",
|
|
405
|
+
"pass" if has_lock else "warning",
|
|
406
|
+
"warning",
|
|
407
|
+
["No lock file — builds may not be reproducible."] if not has_lock else []
|
|
408
|
+
))
|
|
409
|
+
|
|
410
|
+
return checks
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _output_terminal(checks: List[Tuple[str, str, str, List[str]]], score: int, root: Path, ptype: str):
|
|
414
|
+
"""Print results to terminal."""
|
|
415
|
+
from rich.table import Table
|
|
416
|
+
from rich.panel import Panel
|
|
417
|
+
from rich import box
|
|
418
|
+
|
|
419
|
+
passed = sum(1 for c in checks if c[1] == "pass")
|
|
420
|
+
warnings = sum(1 for c in checks if c[1] == "warning")
|
|
421
|
+
errors = sum(1 for c in checks if c[1] == "error")
|
|
422
|
+
|
|
423
|
+
# Score panel
|
|
424
|
+
color = "green" if score >= 80 else "yellow" if score >= 50 else "red"
|
|
425
|
+
console.print()
|
|
426
|
+
console.print(Panel(
|
|
427
|
+
f"[bold {color}]{score}/100[/bold {color}] — "
|
|
428
|
+
f"[green]{passed} passed[/green], "
|
|
429
|
+
f"[yellow]{warnings} warnings[/yellow], "
|
|
430
|
+
f"[red]{errors} errors[/red]",
|
|
431
|
+
title="Audit Score",
|
|
432
|
+
border_style=color
|
|
433
|
+
))
|
|
434
|
+
|
|
435
|
+
# Results table
|
|
436
|
+
table = Table(title="Audit Results", border_style="blue")
|
|
437
|
+
table.add_column("Status", style="bold", width=8)
|
|
438
|
+
table.add_column("Check", style="bold")
|
|
439
|
+
table.add_column("Details")
|
|
440
|
+
|
|
441
|
+
for name, status, severity, details in checks:
|
|
442
|
+
icon = {"pass": "[green]✓[/green]", "warning": "[yellow]![/yellow]", "error": "[red]✗[/red]"}.get(status, "?")
|
|
443
|
+
detail_text = details[0] if details else ""
|
|
444
|
+
table.add_row(icon, name, detail_text)
|
|
445
|
+
|
|
446
|
+
console.print(table)
|
|
447
|
+
|
|
448
|
+
# Recommendations
|
|
449
|
+
if errors > 0 or warnings > 0:
|
|
450
|
+
console.print()
|
|
451
|
+
console.print("[bold]Top recommendations:[/bold]")
|
|
452
|
+
if errors > 0:
|
|
453
|
+
console.print(" [red]Fix errors first:[/red]")
|
|
454
|
+
for name, status, severity, details in checks:
|
|
455
|
+
if status == "error":
|
|
456
|
+
console.print(f" • {name}")
|
|
457
|
+
if warnings > 0:
|
|
458
|
+
console.print(" [yellow]Then address warnings:[/yellow]")
|
|
459
|
+
for name, status, severity, details in checks:
|
|
460
|
+
if status == "warning":
|
|
461
|
+
console.print(f" • {name}")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _output_markdown(checks: List[Tuple[str, str, str, List[str]]], score: int, root: Path, ptype: str):
|
|
465
|
+
"""Output results as markdown."""
|
|
466
|
+
passed = sum(1 for c in checks if c[1] == "pass")
|
|
467
|
+
warnings = sum(1 for c in checks if c[1] == "warning")
|
|
468
|
+
errors = sum(1 for c in checks if c[1] == "error")
|
|
469
|
+
|
|
470
|
+
md = f"""# DevFlow Audit Report
|
|
471
|
+
|
|
472
|
+
**Project:** {root.name}
|
|
473
|
+
**Path:** {root}
|
|
474
|
+
**Type:** {ptype}
|
|
475
|
+
**Score:** {score}/100 ({passed} passed, {warnings} warnings, {errors} errors)
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Results
|
|
480
|
+
|
|
481
|
+
| Status | Check | Details |
|
|
482
|
+
|--------|-------|---------|
|
|
483
|
+
"""
|
|
484
|
+
for name, status, severity, details in checks:
|
|
485
|
+
icon = {"pass": "✅", "warning": "⚠️", "error": "❌"}.get(status, "❓")
|
|
486
|
+
detail_text = details[0] if details else ""
|
|
487
|
+
md += f"| {icon} | {name} | {detail_text} |\n"
|
|
488
|
+
|
|
489
|
+
md += f"""
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
*Generated by [DevFlow](https://devflow.sh)*
|
|
493
|
+
"""
|
|
494
|
+
console.print(md)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _output_json(checks, score, root):
|
|
498
|
+
"""Output results as JSON."""
|
|
499
|
+
import json
|
|
500
|
+
data = {
|
|
501
|
+
"project": str(root),
|
|
502
|
+
"score": score,
|
|
503
|
+
"passed": sum(1 for c in checks if c[1] == "pass"),
|
|
504
|
+
"warnings": sum(1 for c in checks if c[1] == "warning"),
|
|
505
|
+
"errors": sum(1 for c in checks if c[1] == "error"),
|
|
506
|
+
"checks": [{"name": c[0], "status": c[1], "severity": c[2], "details": c[3]} for c in checks]
|
|
507
|
+
}
|
|
508
|
+
console.print(json.dumps(data, indent=2))
|