devit-cli 0.1.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.
- _devkit_entry.py +59 -0
- devit_cli-0.1.0.dist-info/METADATA +273 -0
- devit_cli-0.1.0.dist-info/RECORD +52 -0
- devit_cli-0.1.0.dist-info/WHEEL +5 -0
- devit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- devit_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- devit_cli-0.1.0.dist-info/top_level.txt +2 -0
- devkit_cli/__init__.py +4 -0
- devkit_cli/commands/__init__.py +0 -0
- devkit_cli/commands/archive.py +166 -0
- devkit_cli/commands/clean.py +130 -0
- devkit_cli/commands/env.py +156 -0
- devkit_cli/commands/find.py +122 -0
- devkit_cli/commands/info.py +119 -0
- devkit_cli/commands/init.py +451 -0
- devkit_cli/commands/run.py +236 -0
- devkit_cli/main.py +89 -0
- devkit_cli/templates/aws/.gitignore +43 -0
- devkit_cli/templates/aws/README.md +23 -0
- devkit_cli/templates/aws/requirements.txt +2 -0
- devkit_cli/templates/aws/scripts/__init__.py +0 -0
- devkit_cli/templates/aws/scripts/ec2.py +9 -0
- devkit_cli/templates/aws/scripts/main.py +25 -0
- devkit_cli/templates/aws/scripts/s3.py +10 -0
- devkit_cli/templates/aws/tests/test_scripts.py +6 -0
- devkit_cli/templates/django/.gitignore +43 -0
- devkit_cli/templates/django/README.md +15 -0
- devkit_cli/templates/django/apps/__init__.py +0 -0
- devkit_cli/templates/django/apps/core/__init__.py +0 -0
- devkit_cli/templates/django/apps/core/apps.py +6 -0
- devkit_cli/templates/django/apps/core/urls.py +6 -0
- devkit_cli/templates/django/apps/core/views.py +7 -0
- devkit_cli/templates/django/manage.py +20 -0
- devkit_cli/templates/django/requirements.txt +1 -0
- devkit_cli/templates/django/{{module_name}}/__init__.py +0 -0
- devkit_cli/templates/django/{{module_name}}/settings.py +61 -0
- devkit_cli/templates/django/{{module_name}}/urls.py +9 -0
- devkit_cli/templates/django/{{module_name}}/wsgi.py +7 -0
- devkit_cli/templates/fastapi/.gitignore +43 -0
- devkit_cli/templates/fastapi/README.md +21 -0
- devkit_cli/templates/fastapi/app/__init__.py +0 -0
- devkit_cli/templates/fastapi/app/routers/__init__.py +0 -0
- devkit_cli/templates/fastapi/app/routers/health.py +10 -0
- devkit_cli/templates/fastapi/main.py +13 -0
- devkit_cli/templates/fastapi/requirements.txt +6 -0
- devkit_cli/templates/fastapi/tests/test_api.py +17 -0
- devkit_cli/templates/package/.gitignore +43 -0
- devkit_cli/templates/package/README.md +15 -0
- devkit_cli/templates/package/pyproject.toml +29 -0
- devkit_cli/templates/package/tests/test_core.py +8 -0
- devkit_cli/templates/package/{{module_name}}/__init__.py +3 -0
- devkit_cli/templates/package/{{module_name}}/core.py +5 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""devkit clean — Remove build artifacts, caches, and junk files."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Directories to delete entirely
|
|
13
|
+
CLEAN_DIRS = [
|
|
14
|
+
"__pycache__",
|
|
15
|
+
".pytest_cache",
|
|
16
|
+
".mypy_cache",
|
|
17
|
+
".ruff_cache",
|
|
18
|
+
"build",
|
|
19
|
+
"dist",
|
|
20
|
+
"*.egg-info",
|
|
21
|
+
".eggs",
|
|
22
|
+
"node_modules",
|
|
23
|
+
".next",
|
|
24
|
+
".nuxt",
|
|
25
|
+
"htmlcov",
|
|
26
|
+
"site",
|
|
27
|
+
".tox",
|
|
28
|
+
".nox",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Individual file patterns to delete
|
|
32
|
+
CLEAN_FILES = [
|
|
33
|
+
"*.pyc",
|
|
34
|
+
"*.pyo",
|
|
35
|
+
"*.pyd",
|
|
36
|
+
".DS_Store",
|
|
37
|
+
"Thumbs.db",
|
|
38
|
+
"*.log",
|
|
39
|
+
"*.tmp",
|
|
40
|
+
"*.bak",
|
|
41
|
+
"*.swp",
|
|
42
|
+
"*.swo",
|
|
43
|
+
".coverage",
|
|
44
|
+
"coverage.xml",
|
|
45
|
+
"*.orig",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _iter_matches(root: Path, patterns: list[str]):
|
|
50
|
+
for pat in patterns:
|
|
51
|
+
yield from root.rglob(pat)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@click.command()
|
|
55
|
+
@click.argument("path", default=".", type=click.Path(exists=True, file_okay=False))
|
|
56
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Show what would be removed without deleting.")
|
|
57
|
+
@click.option("-y", "--yes", is_flag=True, default=False, help="Skip confirmation prompt.")
|
|
58
|
+
@click.option("--include-venv", is_flag=True, default=False, help="Also remove .venv/ directory.")
|
|
59
|
+
def clean(path, dry_run, yes, include_venv):
|
|
60
|
+
"""
|
|
61
|
+
Remove __pycache__, build artifacts, .DS_Store, and other junk.
|
|
62
|
+
|
|
63
|
+
\b
|
|
64
|
+
Examples:
|
|
65
|
+
devkit clean # clean current directory
|
|
66
|
+
devkit clean ./my-project # clean a specific directory
|
|
67
|
+
devkit clean --dry-run # preview only
|
|
68
|
+
"""
|
|
69
|
+
root = Path(path).resolve()
|
|
70
|
+
to_remove: list[Path] = []
|
|
71
|
+
|
|
72
|
+
dir_patterns = list(CLEAN_DIRS)
|
|
73
|
+
if include_venv:
|
|
74
|
+
dir_patterns.append(".venv")
|
|
75
|
+
|
|
76
|
+
for item in _iter_matches(root, dir_patterns):
|
|
77
|
+
if item.is_dir():
|
|
78
|
+
to_remove.append(item)
|
|
79
|
+
|
|
80
|
+
for item in _iter_matches(root, CLEAN_FILES):
|
|
81
|
+
if item.is_file():
|
|
82
|
+
to_remove.append(item)
|
|
83
|
+
|
|
84
|
+
# De-duplicate: skip children of already-queued dirs
|
|
85
|
+
to_remove_set = sorted(set(to_remove), key=lambda p: len(p.parts))
|
|
86
|
+
final: list[Path] = []
|
|
87
|
+
queued_dirs: list[Path] = []
|
|
88
|
+
for item in to_remove_set:
|
|
89
|
+
if any(item.is_relative_to(d) for d in queued_dirs):
|
|
90
|
+
continue
|
|
91
|
+
final.append(item)
|
|
92
|
+
if item.is_dir():
|
|
93
|
+
queued_dirs.append(item)
|
|
94
|
+
|
|
95
|
+
if not final:
|
|
96
|
+
console.print("[green]✔[/green] Nothing to clean.")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
table = Table(title=f"Items to remove ({len(final)})", show_lines=False)
|
|
100
|
+
table.add_column("Type", style="dim", width=5)
|
|
101
|
+
table.add_column("Path", style="cyan")
|
|
102
|
+
|
|
103
|
+
for item in final:
|
|
104
|
+
kind = "[blue]DIR[/blue]" if item.is_dir() else "FILE"
|
|
105
|
+
table.add_row(kind, str(item.relative_to(root)))
|
|
106
|
+
|
|
107
|
+
console.print(table)
|
|
108
|
+
|
|
109
|
+
if dry_run:
|
|
110
|
+
console.print("[yellow]Dry-run mode — nothing was deleted.[/yellow]")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if not yes:
|
|
114
|
+
click.confirm(f"Delete {len(final)} item(s)?", abort=True)
|
|
115
|
+
|
|
116
|
+
removed_count = 0
|
|
117
|
+
errors = 0
|
|
118
|
+
for item in final:
|
|
119
|
+
try:
|
|
120
|
+
if item.is_dir():
|
|
121
|
+
shutil.rmtree(item)
|
|
122
|
+
else:
|
|
123
|
+
item.unlink()
|
|
124
|
+
removed_count += 1
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
console.print(f"[red]Error removing {item}: {exc}[/red]")
|
|
127
|
+
errors += 1
|
|
128
|
+
|
|
129
|
+
console.print(f"[green]✔[/green] Removed [bold]{removed_count}[/bold] item(s)." +
|
|
130
|
+
(f" [red]{errors} error(s).[/red]" if errors else ""))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""devkit env — List, export, diff, and set environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_dotenv(path: Path) -> dict[str, str]:
|
|
15
|
+
"""Parse a .env file into a dict."""
|
|
16
|
+
result = {}
|
|
17
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
18
|
+
line = line.strip()
|
|
19
|
+
if not line or line.startswith("#"):
|
|
20
|
+
continue
|
|
21
|
+
if "=" in line:
|
|
22
|
+
key, _, val = line.partition("=")
|
|
23
|
+
result[key.strip()] = val.strip().strip('"').strip("'")
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group()
|
|
28
|
+
def env():
|
|
29
|
+
"""Manage and inspect environment variables."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@env.command("list")
|
|
34
|
+
@click.option("--filter", "filter_str", default=None, metavar="KEYWORD",
|
|
35
|
+
help="Only show vars containing this keyword (case-insensitive).")
|
|
36
|
+
@click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
|
|
37
|
+
def env_list(filter_str, as_json):
|
|
38
|
+
"""
|
|
39
|
+
List all current environment variables.
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
Examples:
|
|
43
|
+
devkit env list
|
|
44
|
+
devkit env list --filter PATH
|
|
45
|
+
devkit env list --json
|
|
46
|
+
"""
|
|
47
|
+
vars_dict = dict(os.environ)
|
|
48
|
+
|
|
49
|
+
if filter_str:
|
|
50
|
+
keyword = filter_str.lower()
|
|
51
|
+
vars_dict = {k: v for k, v in vars_dict.items()
|
|
52
|
+
if keyword in k.lower() or keyword in v.lower()}
|
|
53
|
+
|
|
54
|
+
if as_json:
|
|
55
|
+
click.echo(json.dumps(vars_dict, indent=2))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
table = Table(title=f"Environment Variables ({len(vars_dict)})")
|
|
59
|
+
table.add_column("Variable", style="cyan", no_wrap=True)
|
|
60
|
+
table.add_column("Value", style="green", overflow="fold")
|
|
61
|
+
|
|
62
|
+
for key in sorted(vars_dict):
|
|
63
|
+
table.add_row(key, vars_dict[key])
|
|
64
|
+
|
|
65
|
+
console.print(table)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@env.command("export")
|
|
69
|
+
@click.argument("output", default=".env", metavar="OUTPUT_FILE")
|
|
70
|
+
@click.option("--filter", "filter_str", default=None, metavar="KEYWORD",
|
|
71
|
+
help="Only export vars containing this keyword.")
|
|
72
|
+
@click.option("--format", "fmt",
|
|
73
|
+
type=click.Choice(["dotenv", "json", "shell", "powershell", "cmd"]),
|
|
74
|
+
default="dotenv", show_default=True,
|
|
75
|
+
help="Output format. 'shell'=bash export, 'powershell'=$env: syntax, 'cmd'=set syntax.")
|
|
76
|
+
def env_export(output, filter_str, fmt):
|
|
77
|
+
"""
|
|
78
|
+
Export environment variables to a file.
|
|
79
|
+
|
|
80
|
+
\b
|
|
81
|
+
Examples:
|
|
82
|
+
devkit env export # exports all to .env
|
|
83
|
+
devkit env export prod.env --filter AWS
|
|
84
|
+
devkit env export vars.json --format json
|
|
85
|
+
devkit env export vars.ps1 --format powershell # Windows PowerShell
|
|
86
|
+
devkit env export vars.bat --format cmd # Windows CMD
|
|
87
|
+
"""
|
|
88
|
+
vars_dict = dict(os.environ)
|
|
89
|
+
if filter_str:
|
|
90
|
+
keyword = filter_str.lower()
|
|
91
|
+
vars_dict = {k: v for k, v in vars_dict.items()
|
|
92
|
+
if keyword in k.lower()}
|
|
93
|
+
|
|
94
|
+
out = Path(output)
|
|
95
|
+
|
|
96
|
+
if fmt == "dotenv":
|
|
97
|
+
lines = [f'{k}="{v}"' for k, v in sorted(vars_dict.items())]
|
|
98
|
+
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
99
|
+
elif fmt == "json":
|
|
100
|
+
out.write_text(json.dumps(vars_dict, indent=2), encoding="utf-8")
|
|
101
|
+
elif fmt == "shell":
|
|
102
|
+
lines = [f"export {k}={json.dumps(v)}" for k, v in sorted(vars_dict.items())]
|
|
103
|
+
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
104
|
+
elif fmt == "powershell":
|
|
105
|
+
lines = [f'$env:{k} = {json.dumps(v)}' for k, v in sorted(vars_dict.items())]
|
|
106
|
+
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
107
|
+
elif fmt == "cmd":
|
|
108
|
+
# Values with special CMD characters are quoted; newlines replaced with space
|
|
109
|
+
lines = [f"set {k}={v.replace(chr(10), ' ')}" for k, v in sorted(vars_dict.items())]
|
|
110
|
+
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
console.print(f"[green]✔[/green] Exported [bold]{len(vars_dict)}[/bold] variables to [cyan]{out}[/cyan]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@env.command("diff")
|
|
116
|
+
@click.argument("file_a", type=click.Path(exists=True))
|
|
117
|
+
@click.argument("file_b", type=click.Path(exists=True))
|
|
118
|
+
def env_diff(file_a, file_b):
|
|
119
|
+
"""
|
|
120
|
+
Diff two .env files and show what changed.
|
|
121
|
+
|
|
122
|
+
\b
|
|
123
|
+
Example:
|
|
124
|
+
devkit env diff .env .env.production
|
|
125
|
+
"""
|
|
126
|
+
a = _load_dotenv(Path(file_a))
|
|
127
|
+
b = _load_dotenv(Path(file_b))
|
|
128
|
+
|
|
129
|
+
all_keys = sorted(set(a) | set(b))
|
|
130
|
+
|
|
131
|
+
table = Table(title=f"Diff: {file_a} → {file_b}")
|
|
132
|
+
table.add_column("Key", style="cyan")
|
|
133
|
+
table.add_column(Path(file_a).name, style="dim")
|
|
134
|
+
table.add_column(Path(file_b).name, style="bold")
|
|
135
|
+
table.add_column("Status", style="bold")
|
|
136
|
+
|
|
137
|
+
changed = 0
|
|
138
|
+
for key in all_keys:
|
|
139
|
+
va = a.get(key)
|
|
140
|
+
vb = b.get(key)
|
|
141
|
+
if va == vb:
|
|
142
|
+
continue
|
|
143
|
+
if va is None:
|
|
144
|
+
status = "[green]+added[/green]"
|
|
145
|
+
elif vb is None:
|
|
146
|
+
status = "[red]-removed[/red]"
|
|
147
|
+
else:
|
|
148
|
+
status = "[yellow]~changed[/yellow]"
|
|
149
|
+
table.add_row(key, va or "—", vb or "—", status)
|
|
150
|
+
changed += 1
|
|
151
|
+
|
|
152
|
+
if changed == 0:
|
|
153
|
+
console.print("[green]✔[/green] Files are identical.")
|
|
154
|
+
else:
|
|
155
|
+
console.print(table)
|
|
156
|
+
console.print(f"[dim]{changed} difference(s)[/dim]")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""devkit find — Fast file search with filters."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _bytes_to_human(n: int) -> str:
|
|
17
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
18
|
+
if n < 1024:
|
|
19
|
+
return f"{n:.1f} {unit}"
|
|
20
|
+
n /= 1024
|
|
21
|
+
return f"{n:.1f} TB"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_size(s: str) -> int:
|
|
25
|
+
"""Parse size strings like '10kb', '2mb', '500' (bytes)."""
|
|
26
|
+
s = s.strip().lower()
|
|
27
|
+
multipliers = {"kb": 1024, "mb": 1024**2, "gb": 1024**3, "b": 1}
|
|
28
|
+
for suffix, mult in multipliers.items():
|
|
29
|
+
if s.endswith(suffix):
|
|
30
|
+
return int(float(s[: -len(suffix)]) * mult)
|
|
31
|
+
return int(s)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.argument("pattern", default="*")
|
|
36
|
+
@click.option("-d", "--dir", "search_dir", default=".", show_default=True,
|
|
37
|
+
type=click.Path(exists=True, file_okay=False),
|
|
38
|
+
help="Root directory to search.")
|
|
39
|
+
@click.option("-e", "--ext", multiple=True,
|
|
40
|
+
help="Filter by extension(s), e.g. -e py -e js")
|
|
41
|
+
@click.option("--min-size", default=None, help="Minimum file size, e.g. 10kb.")
|
|
42
|
+
@click.option("--max-size", default=None, help="Maximum file size, e.g. 5mb.")
|
|
43
|
+
@click.option("--newer-than", default=None, metavar="DAYS",
|
|
44
|
+
help="Files modified within last N days.")
|
|
45
|
+
@click.option("--older-than", default=None, metavar="DAYS",
|
|
46
|
+
help="Files modified more than N days ago.")
|
|
47
|
+
@click.option("-l", "--limit", default=100, show_default=True,
|
|
48
|
+
help="Maximum results to display.")
|
|
49
|
+
@click.option("--dirs-only", is_flag=True, default=False,
|
|
50
|
+
help="Match directories instead of files.")
|
|
51
|
+
def find(pattern, search_dir, ext, min_size, max_size, newer_than, older_than, limit, dirs_only):
|
|
52
|
+
"""
|
|
53
|
+
Search for files by name pattern with optional filters.
|
|
54
|
+
|
|
55
|
+
\b
|
|
56
|
+
Examples:
|
|
57
|
+
devkit find "*.py"
|
|
58
|
+
devkit find "config" -e toml -e ini
|
|
59
|
+
devkit find "*" --min-size 1mb --newer-than 7
|
|
60
|
+
devkit find --dirs-only "src"
|
|
61
|
+
"""
|
|
62
|
+
root = Path(search_dir).resolve()
|
|
63
|
+
min_bytes = _parse_size(min_size) if min_size else None
|
|
64
|
+
max_bytes = _parse_size(max_size) if max_size else None
|
|
65
|
+
newer_cutoff = (datetime.now() - timedelta(days=int(newer_than))).timestamp() if newer_than else None
|
|
66
|
+
older_cutoff = (datetime.now() - timedelta(days=int(older_than))).timestamp() if older_than else None
|
|
67
|
+
|
|
68
|
+
table = Table(title=f"Search: [cyan]{pattern}[/cyan] in [dim]{root}[/dim]",
|
|
69
|
+
show_lines=False, expand=False)
|
|
70
|
+
table.add_column("#", style="dim", width=4)
|
|
71
|
+
table.add_column("Path", style="cyan")
|
|
72
|
+
if not dirs_only:
|
|
73
|
+
table.add_column("Size", justify="right", style="green")
|
|
74
|
+
table.add_column("Modified", style="dim")
|
|
75
|
+
|
|
76
|
+
results: list[Path] = []
|
|
77
|
+
|
|
78
|
+
for item in root.rglob(pattern):
|
|
79
|
+
if dirs_only and not item.is_dir():
|
|
80
|
+
continue
|
|
81
|
+
if not dirs_only and not item.is_file():
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
if ext and item.suffix.lstrip(".").lower() not in [e.lstrip(".").lower() for e in ext]:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
st = item.stat()
|
|
89
|
+
except OSError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if not dirs_only:
|
|
93
|
+
if min_bytes is not None and st.st_size < min_bytes:
|
|
94
|
+
continue
|
|
95
|
+
if max_bytes is not None and st.st_size > max_bytes:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
mtime = st.st_mtime
|
|
99
|
+
if newer_cutoff and mtime < newer_cutoff:
|
|
100
|
+
continue
|
|
101
|
+
if older_cutoff and mtime > older_cutoff:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
results.append(item)
|
|
105
|
+
if len(results) >= limit:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
if not results:
|
|
109
|
+
console.print("[yellow]No matches found.[/yellow]")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
for i, item in enumerate(results, 1):
|
|
113
|
+
rel = item.relative_to(root)
|
|
114
|
+
if dirs_only:
|
|
115
|
+
table.add_row(str(i), str(rel))
|
|
116
|
+
else:
|
|
117
|
+
st = item.stat()
|
|
118
|
+
mtime_str = datetime.fromtimestamp(st.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
119
|
+
table.add_row(str(i), str(rel), _bytes_to_human(st.st_size), mtime_str)
|
|
120
|
+
|
|
121
|
+
console.print(table)
|
|
122
|
+
console.print(f"[dim]{len(results)} result(s) — limit {limit}[/dim]")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""devkit info — Display system, Python, and environment information."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import psutil
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.columns import Columns
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _bytes_to_human(n: int) -> str:
|
|
20
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
21
|
+
if n < 1024:
|
|
22
|
+
return f"{n:.1f} {unit}"
|
|
23
|
+
n /= 1024
|
|
24
|
+
return f"{n:.1f} PB"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _detect_env() -> tuple[str, str]:
|
|
28
|
+
"""Return (env_type, env_path)."""
|
|
29
|
+
conda = os.environ.get("CONDA_DEFAULT_ENV")
|
|
30
|
+
if conda:
|
|
31
|
+
return "conda", conda
|
|
32
|
+
venv = os.environ.get("VIRTUAL_ENV")
|
|
33
|
+
if venv:
|
|
34
|
+
return "venv", venv
|
|
35
|
+
return "none", "—"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command()
|
|
39
|
+
@click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
|
|
40
|
+
def info(as_json):
|
|
41
|
+
"""
|
|
42
|
+
Show system info: OS, Python, CPU, memory, disk, and active environment.
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Examples:
|
|
46
|
+
devkit info
|
|
47
|
+
devkit info --json
|
|
48
|
+
"""
|
|
49
|
+
# --- Gather data ---
|
|
50
|
+
uname = platform.uname()
|
|
51
|
+
vm = psutil.virtual_memory()
|
|
52
|
+
disk = psutil.disk_usage(Path.cwd().anchor)
|
|
53
|
+
cpu_count = psutil.cpu_count(logical=True)
|
|
54
|
+
cpu_phys = psutil.cpu_count(logical=False)
|
|
55
|
+
cpu_freq = psutil.cpu_freq()
|
|
56
|
+
env_type, env_path = _detect_env()
|
|
57
|
+
|
|
58
|
+
data = {
|
|
59
|
+
"os": f"{uname.system} {uname.release}",
|
|
60
|
+
"machine": uname.machine,
|
|
61
|
+
"hostname": uname.node,
|
|
62
|
+
"python_version": sys.version.split()[0],
|
|
63
|
+
"python_impl": platform.python_implementation(),
|
|
64
|
+
"python_path": sys.executable,
|
|
65
|
+
"cpu_logical": cpu_count,
|
|
66
|
+
"cpu_physical": cpu_phys,
|
|
67
|
+
"cpu_freq_mhz": f"{cpu_freq.current:.0f} MHz" if cpu_freq else "n/a",
|
|
68
|
+
"mem_total": _bytes_to_human(vm.total),
|
|
69
|
+
"mem_used": _bytes_to_human(vm.used),
|
|
70
|
+
"mem_pct": f"{vm.percent}%",
|
|
71
|
+
"disk_total": _bytes_to_human(disk.total),
|
|
72
|
+
"disk_used": _bytes_to_human(disk.used),
|
|
73
|
+
"disk_pct": f"{disk.percent}%",
|
|
74
|
+
"env_type": env_type,
|
|
75
|
+
"env_path": env_path,
|
|
76
|
+
"cwd": str(Path.cwd()),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if as_json:
|
|
80
|
+
import json
|
|
81
|
+
click.echo(json.dumps(data, indent=2))
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# --- Rich tables ---
|
|
85
|
+
def kv_table(title: str, rows: list[tuple[str, str]]) -> Table:
|
|
86
|
+
t = Table(title=title, show_header=False, box=None, padding=(0, 2))
|
|
87
|
+
t.add_column("Key", style="dim", no_wrap=True)
|
|
88
|
+
t.add_column("Value", style="bold")
|
|
89
|
+
for k, v in rows:
|
|
90
|
+
t.add_row(k, v)
|
|
91
|
+
return t
|
|
92
|
+
|
|
93
|
+
sys_table = kv_table("System", [
|
|
94
|
+
("OS", data["os"]),
|
|
95
|
+
("Arch", data["machine"]),
|
|
96
|
+
("Hostname", data["hostname"]),
|
|
97
|
+
("CWD", data["cwd"]),
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
py_table = kv_table("Python", [
|
|
101
|
+
("Version", data["python_version"]),
|
|
102
|
+
("Impl", data["python_impl"]),
|
|
103
|
+
("Executable", data["python_path"]),
|
|
104
|
+
("Env Type", data["env_type"]),
|
|
105
|
+
("Env Path", data["env_path"]),
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
hw_table = kv_table("Hardware", [
|
|
109
|
+
("CPU (logical)", str(data["cpu_logical"])),
|
|
110
|
+
("CPU (physical)", str(data["cpu_physical"])),
|
|
111
|
+
("CPU Freq", data["cpu_freq_mhz"]),
|
|
112
|
+
("RAM Total", data["mem_total"]),
|
|
113
|
+
("RAM Used", f"{data['mem_used']} ({data['mem_pct']})"),
|
|
114
|
+
("Disk Total", data["disk_total"]),
|
|
115
|
+
("Disk Used", f"{data['disk_used']} ({data['disk_pct']})"),
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
console.print(Panel("[bold cyan]devkit info[/bold cyan]", expand=False))
|
|
119
|
+
console.print(Columns([sys_table, py_table, hw_table], equal=False, expand=False))
|