python-infrakit-dev 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.
- infrakit/__init__.py +0 -0
- infrakit/cli/__init__.py +1 -0
- infrakit/cli/commands/__init__.py +1 -0
- infrakit/cli/commands/deps.py +530 -0
- infrakit/cli/commands/init.py +129 -0
- infrakit/cli/commands/llm.py +295 -0
- infrakit/cli/commands/logger.py +160 -0
- infrakit/cli/commands/module.py +342 -0
- infrakit/cli/commands/time.py +81 -0
- infrakit/cli/main.py +65 -0
- infrakit/core/__init__.py +0 -0
- infrakit/core/config/__init__.py +0 -0
- infrakit/core/config/converter.py +480 -0
- infrakit/core/config/exporter.py +304 -0
- infrakit/core/config/loader.py +713 -0
- infrakit/core/config/validator.py +389 -0
- infrakit/core/logger/__init__.py +21 -0
- infrakit/core/logger/formatters.py +143 -0
- infrakit/core/logger/handlers.py +322 -0
- infrakit/core/logger/retention.py +176 -0
- infrakit/core/logger/setup.py +314 -0
- infrakit/deps/__init__.py +239 -0
- infrakit/deps/clean.py +141 -0
- infrakit/deps/depfile.py +405 -0
- infrakit/deps/health.py +357 -0
- infrakit/deps/optimizer.py +642 -0
- infrakit/deps/scanner.py +550 -0
- infrakit/llm/__init__.py +35 -0
- infrakit/llm/batch.py +165 -0
- infrakit/llm/client.py +575 -0
- infrakit/llm/key_manager.py +728 -0
- infrakit/llm/llm_readme.md +306 -0
- infrakit/llm/models.py +148 -0
- infrakit/llm/providers/__init__.py +5 -0
- infrakit/llm/providers/base.py +112 -0
- infrakit/llm/providers/gemini.py +164 -0
- infrakit/llm/providers/openai.py +168 -0
- infrakit/llm/rate_limiter.py +54 -0
- infrakit/scaffolder/__init__.py +31 -0
- infrakit/scaffolder/ai.py +508 -0
- infrakit/scaffolder/backend.py +555 -0
- infrakit/scaffolder/cli_tool.py +386 -0
- infrakit/scaffolder/generator.py +338 -0
- infrakit/scaffolder/pipeline.py +562 -0
- infrakit/scaffolder/registry.py +121 -0
- infrakit/time/__init__.py +60 -0
- infrakit/time/profiler.py +511 -0
- python_infrakit_dev-0.1.0.dist-info/METADATA +124 -0
- python_infrakit_dev-0.1.0.dist-info/RECORD +51 -0
- python_infrakit_dev-0.1.0.dist-info/WHEEL +4 -0
- python_infrakit_dev-0.1.0.dist-info/entry_points.txt +3 -0
infrakit/__init__.py
ADDED
|
File without changes
|
infrakit/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""infrakit.cli — command-line interface for infrakit."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""infrakit.cli.commands — subcommand groups."""
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infrakit/cli/commands/deps.py
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Typer-based CLI commands for dependency management.
|
|
5
|
+
|
|
6
|
+
Register in your main CLI app:
|
|
7
|
+
|
|
8
|
+
from infrakit.cli.commands.deps import app as deps_app
|
|
9
|
+
main_app.add_typer(deps_app, name="deps")
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
ik deps export — write a file of only the deps actually used
|
|
13
|
+
ik deps check — health check: outdated / vulns / licenses
|
|
14
|
+
ik deps clean — remove unused packages from venv
|
|
15
|
+
ik deps optimize — sort, deduplicate, and clean imports
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
from typing_extensions import Annotated
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Typer app
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
deps_app = typer.Typer(
|
|
31
|
+
name="deps",
|
|
32
|
+
help="Dependency management — scan, export, check, clean, optimise.",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
rich_markup_mode="markdown",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Shared display helpers
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def _section(title: str) -> None:
|
|
43
|
+
typer.echo()
|
|
44
|
+
typer.echo(typer.style(f" ── {title}", fg=typer.colors.BRIGHT_WHITE, bold=True))
|
|
45
|
+
typer.echo(typer.style(" " + "─" * (len(title) + 5), fg=typer.colors.BRIGHT_BLACK))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _ok(msg: str) -> None:
|
|
49
|
+
typer.echo(typer.style(" ✓ ", fg=typer.colors.GREEN) + msg)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _warn(msg: str) -> None:
|
|
53
|
+
typer.echo(typer.style(" ⚠ ", fg=typer.colors.YELLOW) + msg)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _err(msg: str) -> None:
|
|
57
|
+
typer.echo(typer.style(" ✗ ", fg=typer.colors.RED) + msg)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _info(msg: str) -> None:
|
|
61
|
+
typer.echo(typer.style(" · ", fg=typer.colors.BRIGHT_BLACK) + msg)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _header(cmd: str) -> None:
|
|
65
|
+
typer.echo()
|
|
66
|
+
typer.echo(typer.style(f" infrakit deps {cmd}", fg=typer.colors.GREEN, bold=True))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _progress(label: str) -> None:
|
|
70
|
+
typer.echo(typer.style(f" {label}…", fg=typer.colors.BRIGHT_BLACK))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# ik deps export
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
@deps_app.command("export")
|
|
78
|
+
def deps_export(
|
|
79
|
+
root: Annotated[Path, typer.Option("--root", "-r", help="Project root directory to scan.")] = Path("."),
|
|
80
|
+
output: Annotated[Optional[Path], typer.Option("--output", "-o", help="Path for new requirements file.")] = None,
|
|
81
|
+
inplace: Annotated[bool, typer.Option("--inplace", "-i", help="Update existing requirements.txt / pyproject.toml in-place.")] = False,
|
|
82
|
+
no_versions: Annotated[bool, typer.Option("--no-versions", help="Omit version specifiers from output.")] = False,
|
|
83
|
+
notebooks: Annotated[bool, typer.Option("--notebooks", "-n", help="Also scan Jupyter notebooks (.ipynb).")] = False,
|
|
84
|
+
no_gitignore: Annotated[bool, typer.Option("--no-gitignore", help="Disable .gitignore filtering.")] = False,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Scan the project and export **only** the dependencies that are actually used.
|
|
88
|
+
|
|
89
|
+
Possibly-unused imports (imported but name never referenced) are printed
|
|
90
|
+
to stdout for your review but are **not** written to the output file.
|
|
91
|
+
|
|
92
|
+
**Examples**
|
|
93
|
+
|
|
94
|
+
ik deps export -o requirements.used.txt
|
|
95
|
+
|
|
96
|
+
ik deps export --inplace
|
|
97
|
+
|
|
98
|
+
ik deps export -o deps.txt --notebooks
|
|
99
|
+
"""
|
|
100
|
+
from infrakit.deps import export as _export
|
|
101
|
+
|
|
102
|
+
root_path = root.resolve()
|
|
103
|
+
|
|
104
|
+
if not inplace and output is None:
|
|
105
|
+
_err("Provide --output <path> or use --inplace.")
|
|
106
|
+
_info("Example: ik deps export -o requirements.used.txt")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
_header("export")
|
|
110
|
+
typer.echo(typer.style(f" root: {root_path}", fg=typer.colors.BRIGHT_BLACK))
|
|
111
|
+
typer.echo()
|
|
112
|
+
|
|
113
|
+
_progress("Scanning files")
|
|
114
|
+
scan_result, dep_files = _export(
|
|
115
|
+
root=root_path,
|
|
116
|
+
output=output,
|
|
117
|
+
inplace=inplace,
|
|
118
|
+
keep_versions=not no_versions,
|
|
119
|
+
include_notebooks=notebooks,
|
|
120
|
+
use_gitignore=not no_gitignore,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ── Scan summary ──────────────────────────────────────────────────────
|
|
124
|
+
_section("Scan results")
|
|
125
|
+
typer.echo(f" Files scanned: {typer.style(str(len(scan_result.files)), fg=typer.colors.CYAN)}")
|
|
126
|
+
typer.echo(f" Used packages: {typer.style(str(len(scan_result.used_packages)), fg=typer.colors.GREEN)}")
|
|
127
|
+
typer.echo(f" Dep files found: {typer.style(str(len(dep_files)), fg=typer.colors.CYAN)}")
|
|
128
|
+
|
|
129
|
+
if scan_result.errors:
|
|
130
|
+
_section("Parse errors")
|
|
131
|
+
for path, err in scan_result.errors:
|
|
132
|
+
_warn(f"{path.name}: {err}")
|
|
133
|
+
|
|
134
|
+
# ── Used packages ─────────────────────────────────────────────────────
|
|
135
|
+
_section("Used packages")
|
|
136
|
+
for pkg in sorted(scan_result.used_packages, key=str.lower):
|
|
137
|
+
fc = len(scan_result.used_packages[pkg])
|
|
138
|
+
plural = "s" if fc != 1 else ""
|
|
139
|
+
typer.echo(
|
|
140
|
+
f" {typer.style(pkg, fg=typer.colors.GREEN)}"
|
|
141
|
+
f" {typer.style(f'{fc} file{plural}', fg=typer.colors.BRIGHT_BLACK)}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# ── Possibly unused ───────────────────────────────────────────────────
|
|
145
|
+
if scan_result.possibly_unused:
|
|
146
|
+
_section("Possibly unused imports (not written to output)")
|
|
147
|
+
typer.echo(typer.style(
|
|
148
|
+
" These packages are imported but their names were\n"
|
|
149
|
+
" never referenced in code. Review before removing.\n",
|
|
150
|
+
fg=typer.colors.BRIGHT_BLACK,
|
|
151
|
+
))
|
|
152
|
+
for pkg, files in sorted(scan_result.possibly_unused.items()):
|
|
153
|
+
sample = ", ".join(f.name for f in sorted(files)[:3])
|
|
154
|
+
suffix = " …" if len(files) > 3 else ""
|
|
155
|
+
typer.echo(
|
|
156
|
+
f" {typer.style(pkg, fg=typer.colors.YELLOW)}"
|
|
157
|
+
f" {typer.style(f'in: {sample}{suffix}', fg=typer.colors.BRIGHT_BLACK)}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# ── Unknown pip names ─────────────────────────────────────────────────
|
|
161
|
+
if scan_result.unknown_imports:
|
|
162
|
+
_section("Unknown package names")
|
|
163
|
+
typer.echo(typer.style(
|
|
164
|
+
" Could not match to a known pip package name.\n"
|
|
165
|
+
" May be a local module or a package with a non-standard name.\n",
|
|
166
|
+
fg=typer.colors.BRIGHT_BLACK,
|
|
167
|
+
))
|
|
168
|
+
for pkg in sorted(scan_result.unknown_imports):
|
|
169
|
+
_warn(f"{pkg} — verify this is not a pip package")
|
|
170
|
+
|
|
171
|
+
# ── Output ────────────────────────────────────────────────────────────
|
|
172
|
+
_section("Output")
|
|
173
|
+
if inplace:
|
|
174
|
+
for df in dep_files:
|
|
175
|
+
_ok(f"Updated in-place: {df.path}")
|
|
176
|
+
elif output:
|
|
177
|
+
_ok(f"Written to: {output.resolve()}")
|
|
178
|
+
|
|
179
|
+
typer.echo()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# ik deps check
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
@deps_app.command("check")
|
|
187
|
+
def deps_check(
|
|
188
|
+
root: Annotated[Path, typer.Option("--root", "-r", help="Project root (scanned for used packages if --packages not given).")] = Path("."),
|
|
189
|
+
packages: Annotated[Optional[list[str]], typer.Option("--package", "-p", help="Specific package to check. Repeat for multiple.")] = None,
|
|
190
|
+
outdated: Annotated[bool, typer.Option("--outdated/--no-outdated", help="Check for outdated packages via PyPI.")] = True,
|
|
191
|
+
security: Annotated[bool, typer.Option("--security/--no-security", help="Scan for known vulnerabilities via pip-audit.")] = True,
|
|
192
|
+
licenses: Annotated[bool, typer.Option("--licenses/--no-licenses", help="Report license information.")] = True,
|
|
193
|
+
notebooks: Annotated[bool, typer.Option("--notebooks", "-n", help="Also scan Jupyter notebooks.")] = False,
|
|
194
|
+
):
|
|
195
|
+
"""
|
|
196
|
+
Health check your dependencies.
|
|
197
|
+
|
|
198
|
+
All three checks are **enabled by default** — toggle with flags.
|
|
199
|
+
|
|
200
|
+
**Examples**
|
|
201
|
+
|
|
202
|
+
ik deps check
|
|
203
|
+
|
|
204
|
+
ik deps check --no-security
|
|
205
|
+
|
|
206
|
+
ik deps check -p requests -p numpy
|
|
207
|
+
"""
|
|
208
|
+
from infrakit.deps import scan as _scan, check as _check
|
|
209
|
+
|
|
210
|
+
root_path = root.resolve()
|
|
211
|
+
_header("check")
|
|
212
|
+
|
|
213
|
+
pkg_list: list[str]
|
|
214
|
+
if packages:
|
|
215
|
+
pkg_list = list(packages)
|
|
216
|
+
typer.echo(typer.style(f" Checking {len(pkg_list)} specified package(s).", fg=typer.colors.BRIGHT_BLACK))
|
|
217
|
+
else:
|
|
218
|
+
typer.echo(typer.style(f" root: {root_path}", fg=typer.colors.BRIGHT_BLACK))
|
|
219
|
+
_progress("Scanning project for used packages")
|
|
220
|
+
result = _scan(root_path, include_notebooks=notebooks)
|
|
221
|
+
pkg_list = list(result.used_packages.keys())
|
|
222
|
+
typer.echo(typer.style(f" Found {len(pkg_list)} packages.", fg=typer.colors.BRIGHT_BLACK))
|
|
223
|
+
|
|
224
|
+
if not pkg_list:
|
|
225
|
+
_warn("No packages found to check.")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
_progress("Running checks")
|
|
229
|
+
report = _check(packages=pkg_list, outdated=outdated, security=security, licenses=licenses)
|
|
230
|
+
|
|
231
|
+
# ── Outdated ──────────────────────────────────────────────────────────
|
|
232
|
+
if outdated and report.outdated:
|
|
233
|
+
_section("Outdated packages")
|
|
234
|
+
n_out = sum(1 for p in report.outdated if p.status == "outdated")
|
|
235
|
+
n_up = sum(1 for p in report.outdated if p.status == "up-to-date")
|
|
236
|
+
|
|
237
|
+
if n_out == 0:
|
|
238
|
+
_ok(f"All {n_up} packages are up-to-date.")
|
|
239
|
+
else:
|
|
240
|
+
typer.echo(
|
|
241
|
+
f" {typer.style(str(n_out), fg=typer.colors.YELLOW)} outdated, "
|
|
242
|
+
f"{typer.style(str(n_up), fg=typer.colors.GREEN)} up-to-date"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
typer.echo()
|
|
246
|
+
col = max((len(p.name) for p in report.outdated), default=10) + 2
|
|
247
|
+
typer.echo(typer.style(
|
|
248
|
+
f" {'Package':<{col}} {'Installed':<14} {'Latest':<14} Status",
|
|
249
|
+
fg=typer.colors.BRIGHT_BLACK,
|
|
250
|
+
))
|
|
251
|
+
typer.echo(typer.style(" " + "─" * (col + 46), fg=typer.colors.BRIGHT_BLACK))
|
|
252
|
+
|
|
253
|
+
for p in report.outdated:
|
|
254
|
+
if p.status == "outdated":
|
|
255
|
+
s = typer.style("● outdated", fg=typer.colors.YELLOW)
|
|
256
|
+
elif p.status == "up-to-date":
|
|
257
|
+
s = typer.style("✓ up-to-date", fg=typer.colors.GREEN)
|
|
258
|
+
elif p.status == "not-installed":
|
|
259
|
+
s = typer.style("○ not installed", fg=typer.colors.BRIGHT_BLACK)
|
|
260
|
+
else:
|
|
261
|
+
s = typer.style(f"? {p.status}", fg=typer.colors.BRIGHT_BLACK)
|
|
262
|
+
|
|
263
|
+
typer.echo(
|
|
264
|
+
f" {p.name:<{col}}"
|
|
265
|
+
f"{typer.style(p.current, fg=typer.colors.BRIGHT_WHITE):<14}"
|
|
266
|
+
f"{typer.style(p.latest, fg=typer.colors.CYAN):<14}"
|
|
267
|
+
f"{s}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# ── Security ──────────────────────────────────────────────────────────
|
|
271
|
+
if security:
|
|
272
|
+
_section("Security vulnerabilities")
|
|
273
|
+
sec_err = next((e for e in report.errors if "Security scan" in e), None)
|
|
274
|
+
if sec_err:
|
|
275
|
+
_warn(sec_err.replace("Security scan: ", ""))
|
|
276
|
+
elif not report.vulnerabilities:
|
|
277
|
+
_ok("No known vulnerabilities found.")
|
|
278
|
+
else:
|
|
279
|
+
_err(f"{len(report.vulnerabilities)} vulnerability/ies found!")
|
|
280
|
+
typer.echo()
|
|
281
|
+
for v in report.vulnerabilities:
|
|
282
|
+
typer.echo(
|
|
283
|
+
f" {typer.style(v.vuln_id, fg=typer.colors.RED, bold=True)} "
|
|
284
|
+
f"{typer.style(v.package, fg=typer.colors.BRIGHT_WHITE)} {v.installed_version}"
|
|
285
|
+
)
|
|
286
|
+
typer.echo(f" Fix: {typer.style(v.fix_version, fg=typer.colors.YELLOW)}")
|
|
287
|
+
desc = v.description[:97] + "…" if len(v.description) > 100 else v.description
|
|
288
|
+
typer.echo(f" Desc: {typer.style(desc, fg=typer.colors.BRIGHT_BLACK)}")
|
|
289
|
+
typer.echo()
|
|
290
|
+
|
|
291
|
+
# ── Licenses ──────────────────────────────────────────────────────────
|
|
292
|
+
if licenses and report.licenses:
|
|
293
|
+
_section("Licenses")
|
|
294
|
+
col = max((len(l.package) for l in report.licenses), default=10) + 2
|
|
295
|
+
lw = max((len(l.license) for l in report.licenses), default=10) + 2
|
|
296
|
+
typer.echo(typer.style(
|
|
297
|
+
f" {'Package':<{col}} {'License':<{lw}} Notes",
|
|
298
|
+
fg=typer.colors.BRIGHT_BLACK,
|
|
299
|
+
))
|
|
300
|
+
typer.echo(typer.style(" " + "─" * (col + lw + 30), fg=typer.colors.BRIGHT_BLACK))
|
|
301
|
+
|
|
302
|
+
for lic in report.licenses:
|
|
303
|
+
if lic.compatible is True:
|
|
304
|
+
ind = typer.style("✓", fg=typer.colors.GREEN)
|
|
305
|
+
elif lic.compatible is False:
|
|
306
|
+
ind = typer.style("✗", fg=typer.colors.RED)
|
|
307
|
+
else:
|
|
308
|
+
ind = typer.style("?", fg=typer.colors.YELLOW)
|
|
309
|
+
|
|
310
|
+
typer.echo(
|
|
311
|
+
f" {lic.package:<{col}}{lic.license:<{lw}}"
|
|
312
|
+
f"{ind} {typer.style(lic.notes, fg=typer.colors.BRIGHT_BLACK)}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# ── Other errors ──────────────────────────────────────────────────────
|
|
316
|
+
other = [e for e in report.errors if "Security scan" not in e]
|
|
317
|
+
if other:
|
|
318
|
+
_section("Errors")
|
|
319
|
+
for e in other:
|
|
320
|
+
_err(e)
|
|
321
|
+
|
|
322
|
+
typer.echo()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
# ik deps clean
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
@deps_app.command("clean")
|
|
330
|
+
def deps_clean(
|
|
331
|
+
root: Annotated[Path, typer.Option("--root", "-r", help="Project root directory to scan.")] = Path("."),
|
|
332
|
+
dry_run: Annotated[bool, typer.Option("--dry-run/--no-dry-run", help="Preview without uninstalling.")] = True,
|
|
333
|
+
keep: Annotated[Optional[list[str]], typer.Option("--keep", "-k", help="Package(s) to protect from removal.")] = None,
|
|
334
|
+
notebooks: Annotated[bool, typer.Option("--notebooks", "-n", help="Also scan notebooks when computing used packages.")] = False,
|
|
335
|
+
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompt.")] = False,
|
|
336
|
+
):
|
|
337
|
+
"""
|
|
338
|
+
Remove unused packages from the virtual environment.
|
|
339
|
+
|
|
340
|
+
Does **not** touch your requirements file — use `ik deps export` for that.
|
|
341
|
+
Dry-run is the default; pass `--no-dry-run` to actually uninstall.
|
|
342
|
+
|
|
343
|
+
**Examples**
|
|
344
|
+
|
|
345
|
+
ik deps clean # dry-run preview
|
|
346
|
+
|
|
347
|
+
ik deps clean --no-dry-run # actually uninstall
|
|
348
|
+
|
|
349
|
+
ik deps clean --no-dry-run -k boto3 # keep boto3 even if unused
|
|
350
|
+
"""
|
|
351
|
+
from infrakit.deps import clean as _clean
|
|
352
|
+
|
|
353
|
+
root_path = root.resolve()
|
|
354
|
+
_header("clean")
|
|
355
|
+
|
|
356
|
+
if dry_run:
|
|
357
|
+
typer.echo(typer.style(
|
|
358
|
+
" mode: dry-run (pass --no-dry-run to actually remove)",
|
|
359
|
+
fg=typer.colors.YELLOW,
|
|
360
|
+
))
|
|
361
|
+
else:
|
|
362
|
+
typer.echo(typer.style(
|
|
363
|
+
" mode: LIVE — packages will be uninstalled",
|
|
364
|
+
fg=typer.colors.RED, bold=True,
|
|
365
|
+
))
|
|
366
|
+
typer.echo(typer.style(f" root: {root_path}", fg=typer.colors.BRIGHT_BLACK))
|
|
367
|
+
typer.echo()
|
|
368
|
+
|
|
369
|
+
_progress("Scanning project")
|
|
370
|
+
preview = _clean(root=root_path, protected=set(keep or []), dry_run=True)
|
|
371
|
+
|
|
372
|
+
if not preview.to_remove:
|
|
373
|
+
_ok("Nothing to remove — your environment is clean.")
|
|
374
|
+
typer.echo()
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
_section("Packages to remove")
|
|
378
|
+
for pkg in preview.to_remove:
|
|
379
|
+
typer.echo(f" {typer.style('−', fg=typer.colors.RED)} {pkg}")
|
|
380
|
+
|
|
381
|
+
typer.echo()
|
|
382
|
+
typer.echo(typer.style(
|
|
383
|
+
f" {len(preview.to_remove)} package(s) would be removed.",
|
|
384
|
+
fg=typer.colors.YELLOW,
|
|
385
|
+
))
|
|
386
|
+
|
|
387
|
+
if dry_run:
|
|
388
|
+
typer.echo()
|
|
389
|
+
_info("Run with --no-dry-run to actually uninstall.")
|
|
390
|
+
typer.echo()
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
if not yes:
|
|
394
|
+
typer.echo()
|
|
395
|
+
confirmed = typer.confirm(
|
|
396
|
+
typer.style(" Proceed with uninstall?", fg=typer.colors.YELLOW),
|
|
397
|
+
default=False,
|
|
398
|
+
)
|
|
399
|
+
if not confirmed:
|
|
400
|
+
_info("Aborted.")
|
|
401
|
+
typer.echo()
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
typer.echo()
|
|
405
|
+
_progress("Uninstalling")
|
|
406
|
+
live = _clean(root=root_path, protected=set(keep or []), dry_run=False)
|
|
407
|
+
|
|
408
|
+
_section("Results")
|
|
409
|
+
for pkg in live.removed:
|
|
410
|
+
_ok(f"Removed: {pkg}")
|
|
411
|
+
for pkg in live.skipped:
|
|
412
|
+
_warn(f"Skipped: {pkg}")
|
|
413
|
+
for e in live.errors:
|
|
414
|
+
_err(e)
|
|
415
|
+
|
|
416
|
+
color = typer.colors.GREEN if not live.errors else typer.colors.YELLOW
|
|
417
|
+
typer.echo()
|
|
418
|
+
typer.echo(typer.style(
|
|
419
|
+
f" Removed {len(live.removed)} / {len(preview.to_remove)} packages.",
|
|
420
|
+
fg=color,
|
|
421
|
+
))
|
|
422
|
+
typer.echo()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ---------------------------------------------------------------------------
|
|
426
|
+
# ik deps optimize
|
|
427
|
+
# ---------------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
@deps_app.command("optimize")
|
|
430
|
+
def deps_optimize(
|
|
431
|
+
root: Annotated[Path, typer.Option("--root", "-r", help="Project root directory.")] = Path("."),
|
|
432
|
+
files: Annotated[Optional[list[Path]], typer.Option("--file", "-f", help="Specific file(s) to optimise.")] = None,
|
|
433
|
+
convert: Annotated[Optional[str], typer.Option("--convert", "-c", help="Convert imports: 'absolute' or 'relative'.")] = None,
|
|
434
|
+
no_isort: Annotated[bool, typer.Option("--no-isort", help="Disable isort backend, use built-in sorter.")] = False,
|
|
435
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", "-d", help="Show changes without writing files.")] = False,
|
|
436
|
+
):
|
|
437
|
+
"""
|
|
438
|
+
Optimise imports across your project.
|
|
439
|
+
|
|
440
|
+
- Sort into groups: stdlib → third-party → local (via isort or built-in)
|
|
441
|
+
- Remove duplicate imports
|
|
442
|
+
- Convert relative ↔ absolute imports (optional)
|
|
443
|
+
- Multi-line formatting for long `from`-imports
|
|
444
|
+
|
|
445
|
+
**Examples**
|
|
446
|
+
|
|
447
|
+
ik deps optimize
|
|
448
|
+
|
|
449
|
+
ik deps optimize --dry-run
|
|
450
|
+
|
|
451
|
+
ik deps optimize -f src/main.py
|
|
452
|
+
|
|
453
|
+
ik deps optimize --convert absolute
|
|
454
|
+
"""
|
|
455
|
+
from infrakit.deps import optimise as _optimise
|
|
456
|
+
|
|
457
|
+
if convert and convert not in ("absolute", "relative"):
|
|
458
|
+
_err("--convert must be 'absolute' or 'relative'")
|
|
459
|
+
raise typer.Exit(1)
|
|
460
|
+
|
|
461
|
+
root_path = root.resolve()
|
|
462
|
+
file_paths = [f.resolve() for f in files] if files else None
|
|
463
|
+
|
|
464
|
+
_header("optimize")
|
|
465
|
+
if dry_run:
|
|
466
|
+
typer.echo(typer.style(" mode: dry-run", fg=typer.colors.YELLOW))
|
|
467
|
+
typer.echo(typer.style(f" root: {root_path}", fg=typer.colors.BRIGHT_BLACK))
|
|
468
|
+
if convert:
|
|
469
|
+
typer.echo(typer.style(f" convert: → {convert}", fg=typer.colors.CYAN))
|
|
470
|
+
typer.echo()
|
|
471
|
+
|
|
472
|
+
results = _optimise(
|
|
473
|
+
root=root_path,
|
|
474
|
+
files=file_paths,
|
|
475
|
+
convert_to=convert,
|
|
476
|
+
use_isort=not no_isort,
|
|
477
|
+
dry_run=dry_run,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
changed = [r for r in results if r.changed]
|
|
481
|
+
errors = [r for r in results if r.error]
|
|
482
|
+
|
|
483
|
+
if changed:
|
|
484
|
+
label = "Would change" if dry_run else "Changed"
|
|
485
|
+
n_changed = len(changed)
|
|
486
|
+
plural = "s" if n_changed != 1 else ""
|
|
487
|
+
_section(f"{label} ({n_changed} file{plural})")
|
|
488
|
+
for r in changed:
|
|
489
|
+
try:
|
|
490
|
+
rel = r.path.relative_to(root_path)
|
|
491
|
+
except ValueError:
|
|
492
|
+
rel = r.path
|
|
493
|
+
typer.echo(f" {typer.style(str(rel), fg=typer.colors.CYAN)}")
|
|
494
|
+
for change in r.changes:
|
|
495
|
+
if change.startswith(" "):
|
|
496
|
+
typer.echo(f" {typer.style(change.strip(), fg=typer.colors.BRIGHT_BLACK)}")
|
|
497
|
+
else:
|
|
498
|
+
typer.echo(f" {typer.style('·', fg=typer.colors.BRIGHT_BLACK)} {change}")
|
|
499
|
+
else:
|
|
500
|
+
_section("No changes needed")
|
|
501
|
+
_ok("All imports are already well-organised.")
|
|
502
|
+
|
|
503
|
+
if errors:
|
|
504
|
+
n_errors = len(errors)
|
|
505
|
+
plural = "s" if n_errors != 1 else ""
|
|
506
|
+
_section(f"Errors ({n_errors} file{plural})")
|
|
507
|
+
for r in errors:
|
|
508
|
+
try:
|
|
509
|
+
rel = r.path.relative_to(root_path)
|
|
510
|
+
except ValueError:
|
|
511
|
+
rel = r.path
|
|
512
|
+
_err(f"{rel}: {r.error}")
|
|
513
|
+
|
|
514
|
+
_section("Summary")
|
|
515
|
+
typer.echo(f" Files scanned: {typer.style(str(len(results)), fg=typer.colors.CYAN)}")
|
|
516
|
+
label = "Would change" if dry_run else "Changed"
|
|
517
|
+
typer.echo(
|
|
518
|
+
f" {label}: "
|
|
519
|
+
f"{typer.style(str(len(changed)), fg=typer.colors.GREEN if changed else typer.colors.BRIGHT_BLACK)}"
|
|
520
|
+
)
|
|
521
|
+
typer.echo(
|
|
522
|
+
f" Errors: "
|
|
523
|
+
f"{typer.style(str(len(errors)), fg=typer.colors.RED if errors else typer.colors.BRIGHT_BLACK)}"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if dry_run and changed:
|
|
527
|
+
typer.echo()
|
|
528
|
+
_info("Run without --dry-run to apply changes.")
|
|
529
|
+
|
|
530
|
+
typer.echo()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infrakit.cli.commands.init
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
``infrakit init`` command — scaffold a new project from a template.
|
|
5
|
+
|
|
6
|
+
This is registered directly on the root app (not as a sub-Typer) so that
|
|
7
|
+
``ik init <project>`` works without an extra subcommand layer.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from infrakit.scaffolder import *
|
|
17
|
+
|
|
18
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
_CONFIG_FORMATS = {"env", "yaml", "json"}
|
|
21
|
+
_DEPS_FORMATS = {"toml", "requirements"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _abort(msg: str) -> None:
|
|
25
|
+
typer.echo(typer.style(f"✗ {msg}", fg=typer.colors.RED), err=True)
|
|
26
|
+
raise typer.Exit(1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render_entry(entry: ScaffoldEntry, project_dir: Path) -> None:
|
|
30
|
+
rel = entry.path.relative_to(project_dir.parent)
|
|
31
|
+
kind_tag = typer.style("dir " if entry.kind == "dir" else "file", fg=typer.colors.BRIGHT_BLACK)
|
|
32
|
+
|
|
33
|
+
if entry.status == "created":
|
|
34
|
+
icon = typer.style("+", fg=typer.colors.GREEN, bold=True)
|
|
35
|
+
label = typer.style(str(rel), fg=typer.colors.GREEN)
|
|
36
|
+
else:
|
|
37
|
+
icon = typer.style("~", fg=typer.colors.BRIGHT_BLACK)
|
|
38
|
+
label = typer.style(str(rel), fg=typer.colors.BRIGHT_BLACK)
|
|
39
|
+
|
|
40
|
+
typer.echo(f" {icon} {kind_tag} {label}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── command function (registered on root app in main.py) ──────────────────────
|
|
44
|
+
|
|
45
|
+
template_map = {"basic": scaffold_basic,
|
|
46
|
+
"ai": scaffold_ai,
|
|
47
|
+
"cli_tool": scaffold_cli_tool,
|
|
48
|
+
"pipeline": scaffold_pipeline,
|
|
49
|
+
"backend": scaffold_backend}
|
|
50
|
+
|
|
51
|
+
def cmd_init(
|
|
52
|
+
project: str = typer.Argument(..., help="Project folder name."),
|
|
53
|
+
template: str = typer.Option("basic", "--template", "-t", help="Template to use."),
|
|
54
|
+
include_llm: bool = typer.Option(False, "--include-llm", "-l", help="Include LLM client."),
|
|
55
|
+
version: str = typer.Option("0.1.0", "--version", "-v", help="Starting version."),
|
|
56
|
+
description: str = typer.Option("", "--description", "-d", help="Short project description."),
|
|
57
|
+
author: str = typer.Option("", "--author", "-a", help='Author e.g. "Jane Doe <jane@example.com>".'),
|
|
58
|
+
config_fmt: str = typer.Option("env", "--config", "-c", help="Config format: env (default) | yaml | json."),
|
|
59
|
+
deps: str = typer.Option("toml", "--deps", help="Dependency file: toml (default) | requirements."),
|
|
60
|
+
target_dir: Optional[Path] = typer.Option(None, "--dir", help="Parent directory (default: cwd)."),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Scaffold a new project. Safe to re-run — existing files are never overwritten.
|
|
64
|
+
|
|
65
|
+
\b
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
ik init my-project
|
|
69
|
+
ik init my-project --version 0.2.0 --author "Jane Doe" --description "My app"
|
|
70
|
+
ik init my-project --config yaml --deps requirements
|
|
71
|
+
ik init my-project --dir ~/projects
|
|
72
|
+
"""
|
|
73
|
+
if config_fmt not in _CONFIG_FORMATS:
|
|
74
|
+
_abort(f"Unknown config format '{config_fmt}'. Choose from: {', '.join(sorted(_CONFIG_FORMATS))}")
|
|
75
|
+
|
|
76
|
+
if deps not in _DEPS_FORMATS:
|
|
77
|
+
_abort(f"Unknown deps format '{deps}'. Choose from: {', '.join(sorted(_DEPS_FORMATS))}")
|
|
78
|
+
|
|
79
|
+
base = target_dir.resolve() if target_dir else Path.cwd()
|
|
80
|
+
project_dir = base / project
|
|
81
|
+
|
|
82
|
+
typer.echo()
|
|
83
|
+
typer.echo(typer.style(f"Scaffolding project: {project}", bold=True))
|
|
84
|
+
typer.echo(typer.style(f"Location: {project_dir}", fg=typer.colors.BRIGHT_BLACK))
|
|
85
|
+
typer.echo()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
scaffolder = template_map.get(template)
|
|
89
|
+
if scaffolder is None:
|
|
90
|
+
_abort(f"Unknown template '{template}'. Choose from: {', '.join(sorted(template_map.keys()))}")
|
|
91
|
+
result = scaffolder(
|
|
92
|
+
project_dir,
|
|
93
|
+
version=version,
|
|
94
|
+
description=description,
|
|
95
|
+
author=author,
|
|
96
|
+
config_fmt=config_fmt,
|
|
97
|
+
deps=deps,
|
|
98
|
+
include_llm = include_llm,
|
|
99
|
+
)
|
|
100
|
+
except Exception as exc: # noqa: BLE001
|
|
101
|
+
_abort(f"Scaffolding failed: {exc}")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
for entry in result.entries:
|
|
105
|
+
_render_entry(entry, project_dir)
|
|
106
|
+
|
|
107
|
+
typer.echo()
|
|
108
|
+
|
|
109
|
+
n_created = len(result.created)
|
|
110
|
+
n_skipped = len(result.skipped)
|
|
111
|
+
|
|
112
|
+
if n_created == 0:
|
|
113
|
+
typer.echo(typer.style(" Nothing new — project already up to date.", fg=typer.colors.BRIGHT_BLACK))
|
|
114
|
+
else:
|
|
115
|
+
typer.echo(
|
|
116
|
+
typer.style(f" ✓ {n_created} item(s) created", fg=typer.colors.GREEN)
|
|
117
|
+
+ (typer.style(f", {n_skipped} skipped", fg=typer.colors.BRIGHT_BLACK) if n_skipped else "")
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
typer.echo()
|
|
121
|
+
|
|
122
|
+
if n_created > 0:
|
|
123
|
+
typer.echo(typer.style(" Next steps:", fg=typer.colors.BRIGHT_BLACK))
|
|
124
|
+
typer.echo(f" {typer.style(f'cd {project}', fg=typer.colors.CYAN)}")
|
|
125
|
+
if deps == "toml":
|
|
126
|
+
typer.echo(f" {typer.style('uv pip install -e .', fg=typer.colors.CYAN)}")
|
|
127
|
+
else:
|
|
128
|
+
typer.echo(f" {typer.style('pip install -r requirements.txt', fg=typer.colors.CYAN)}")
|
|
129
|
+
typer.echo()
|