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 ADDED
@@ -0,0 +1,6 @@
1
+ """DevFlow — Developer Workflow Automation CLI.
2
+
3
+ Scaffold, audit, fix, and ship projects with a single tool.
4
+ """
5
+
6
+ __version__ = "1.0.0"
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))