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.
Files changed (51) hide show
  1. infrakit/__init__.py +0 -0
  2. infrakit/cli/__init__.py +1 -0
  3. infrakit/cli/commands/__init__.py +1 -0
  4. infrakit/cli/commands/deps.py +530 -0
  5. infrakit/cli/commands/init.py +129 -0
  6. infrakit/cli/commands/llm.py +295 -0
  7. infrakit/cli/commands/logger.py +160 -0
  8. infrakit/cli/commands/module.py +342 -0
  9. infrakit/cli/commands/time.py +81 -0
  10. infrakit/cli/main.py +65 -0
  11. infrakit/core/__init__.py +0 -0
  12. infrakit/core/config/__init__.py +0 -0
  13. infrakit/core/config/converter.py +480 -0
  14. infrakit/core/config/exporter.py +304 -0
  15. infrakit/core/config/loader.py +713 -0
  16. infrakit/core/config/validator.py +389 -0
  17. infrakit/core/logger/__init__.py +21 -0
  18. infrakit/core/logger/formatters.py +143 -0
  19. infrakit/core/logger/handlers.py +322 -0
  20. infrakit/core/logger/retention.py +176 -0
  21. infrakit/core/logger/setup.py +314 -0
  22. infrakit/deps/__init__.py +239 -0
  23. infrakit/deps/clean.py +141 -0
  24. infrakit/deps/depfile.py +405 -0
  25. infrakit/deps/health.py +357 -0
  26. infrakit/deps/optimizer.py +642 -0
  27. infrakit/deps/scanner.py +550 -0
  28. infrakit/llm/__init__.py +35 -0
  29. infrakit/llm/batch.py +165 -0
  30. infrakit/llm/client.py +575 -0
  31. infrakit/llm/key_manager.py +728 -0
  32. infrakit/llm/llm_readme.md +306 -0
  33. infrakit/llm/models.py +148 -0
  34. infrakit/llm/providers/__init__.py +5 -0
  35. infrakit/llm/providers/base.py +112 -0
  36. infrakit/llm/providers/gemini.py +164 -0
  37. infrakit/llm/providers/openai.py +168 -0
  38. infrakit/llm/rate_limiter.py +54 -0
  39. infrakit/scaffolder/__init__.py +31 -0
  40. infrakit/scaffolder/ai.py +508 -0
  41. infrakit/scaffolder/backend.py +555 -0
  42. infrakit/scaffolder/cli_tool.py +386 -0
  43. infrakit/scaffolder/generator.py +338 -0
  44. infrakit/scaffolder/pipeline.py +562 -0
  45. infrakit/scaffolder/registry.py +121 -0
  46. infrakit/time/__init__.py +60 -0
  47. infrakit/time/profiler.py +511 -0
  48. python_infrakit_dev-0.1.0.dist-info/METADATA +124 -0
  49. python_infrakit_dev-0.1.0.dist-info/RECORD +51 -0
  50. python_infrakit_dev-0.1.0.dist-info/WHEEL +4 -0
  51. python_infrakit_dev-0.1.0.dist-info/entry_points.txt +3 -0
infrakit/__init__.py ADDED
File without changes
@@ -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()