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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infrakit/cli/commands/llm.py
|
|
3
|
+
-----------------------------
|
|
4
|
+
Typer command group for infrakit.llm — key status and quota management.
|
|
5
|
+
|
|
6
|
+
Commands
|
|
7
|
+
--------
|
|
8
|
+
ik llm status
|
|
9
|
+
ik llm status --provider openai
|
|
10
|
+
ik llm status --key sk-abc123
|
|
11
|
+
|
|
12
|
+
ik llm quota set --provider openai --key sk-abc123 --rpm 60
|
|
13
|
+
ik llm quota set --provider gemini --key AIza-abc1 --model gemini-2.5-pro --daily 250000
|
|
14
|
+
ik llm quota set --provider gemini --key AIza-abc1 --daily 1500000 # default for all models
|
|
15
|
+
|
|
16
|
+
Connecting to your main CLI
|
|
17
|
+
----------------------------
|
|
18
|
+
Typer root::
|
|
19
|
+
|
|
20
|
+
from infrakit.cli.commands.llm import app as llm_app
|
|
21
|
+
root_app.add_typer(llm_app, name="llm")
|
|
22
|
+
|
|
23
|
+
Click root::
|
|
24
|
+
|
|
25
|
+
from infrakit.cli.commands.llm import click_group as llm_group
|
|
26
|
+
cli.add_command(llm_group, name="llm")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
from enum import Enum
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
import typer
|
|
37
|
+
from typing_extensions import Annotated
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── enums ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
class ProviderChoice(str, Enum):
|
|
43
|
+
openai = "openai"
|
|
44
|
+
gemini = "gemini"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── shared option types ────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
_StorageDirOption = Annotated[
|
|
50
|
+
Optional[Path],
|
|
51
|
+
typer.Option(
|
|
52
|
+
"--storage-dir", "-d",
|
|
53
|
+
help=(
|
|
54
|
+
"Directory where key state is persisted. "
|
|
55
|
+
"Defaults to ~/.infrakit/llm/"
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
_QuotaFileOption = Annotated[
|
|
61
|
+
Optional[Path],
|
|
62
|
+
typer.Option(
|
|
63
|
+
"--quota-file", "-q",
|
|
64
|
+
help=(
|
|
65
|
+
"Path to quotas.json. "
|
|
66
|
+
"Defaults to ~/.infrakit/llm/quotas.json if that file exists."
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
_KeysFileOption = Annotated[
|
|
72
|
+
Optional[Path],
|
|
73
|
+
typer.Option(
|
|
74
|
+
"--keys-file", "-k",
|
|
75
|
+
help=(
|
|
76
|
+
'JSON file containing API keys: {"openai_keys": [...], "gemini_keys": [...]}. '
|
|
77
|
+
"Only needed to register new keys; omit when inspecting persisted state."
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
_ProviderFilterOption = Annotated[
|
|
83
|
+
Optional[ProviderChoice],
|
|
84
|
+
typer.Option("--provider", "-p", help="Filter to a specific provider."),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
_KeyFilterOption = Annotated[
|
|
88
|
+
Optional[str],
|
|
89
|
+
typer.Option("--key", "-K", help="Filter to a specific key (first 8 chars)."),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── apps ───────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
app = typer.Typer(
|
|
96
|
+
name="llm",
|
|
97
|
+
help="Manage infrakit LLM keys, quotas, and usage.",
|
|
98
|
+
no_args_is_help=True,
|
|
99
|
+
rich_markup_mode="markdown",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
quota_app = typer.Typer(
|
|
103
|
+
name="quota",
|
|
104
|
+
help="Manage quota limits for LLM API keys.",
|
|
105
|
+
no_args_is_help=True,
|
|
106
|
+
rich_markup_mode="markdown",
|
|
107
|
+
)
|
|
108
|
+
app.add_typer(quota_app, name="quota")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── helpers ────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
def _load_client(
|
|
114
|
+
storage_dir: Optional[Path],
|
|
115
|
+
quota_file: Optional[Path],
|
|
116
|
+
keys_file: Optional[Path],
|
|
117
|
+
):
|
|
118
|
+
from infrakit.llm import LLMClient
|
|
119
|
+
|
|
120
|
+
keys: dict = {"openai_keys": [], "gemini_keys": []}
|
|
121
|
+
if keys_file is not None:
|
|
122
|
+
if keys_file.exists():
|
|
123
|
+
with open(keys_file) as f:
|
|
124
|
+
try:
|
|
125
|
+
keys = json.load(f)
|
|
126
|
+
except json.JSONDecodeError as exc:
|
|
127
|
+
typer.echo(f"[error] Cannot parse keys file: {exc}", err=True)
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
else:
|
|
130
|
+
typer.echo(f"[warn] Keys file not found: {keys_file}", err=True)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
return LLMClient(
|
|
134
|
+
keys=keys,
|
|
135
|
+
storage_dir=storage_dir,
|
|
136
|
+
quota_file=quota_file,
|
|
137
|
+
)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
typer.echo(f"[error] Failed to initialise LLMClient: {exc}", err=True)
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _prov(provider: Optional[ProviderChoice]) -> Optional[str]:
|
|
144
|
+
return provider.value if provider is not None else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ── ik llm status ──────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
@app.command("status")
|
|
150
|
+
def status(
|
|
151
|
+
storage_dir: _StorageDirOption = None,
|
|
152
|
+
quota_file: _QuotaFileOption = None,
|
|
153
|
+
keys_file: _KeysFileOption = None,
|
|
154
|
+
provider: _ProviderFilterOption = None,
|
|
155
|
+
key: _KeyFilterOption = None,
|
|
156
|
+
output_json: Annotated[
|
|
157
|
+
bool,
|
|
158
|
+
typer.Option("--json", help="Output raw JSON.", is_flag=True),
|
|
159
|
+
] = False,
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
Show quota and usage status for LLM API keys.
|
|
163
|
+
|
|
164
|
+
Deactivation is tracked per model — a key can have gemini-2.5-pro
|
|
165
|
+
exhausted while gemini-2.0-flash is still active.
|
|
166
|
+
|
|
167
|
+
**Examples**
|
|
168
|
+
|
|
169
|
+
ik llm status
|
|
170
|
+
|
|
171
|
+
ik llm status --provider gemini
|
|
172
|
+
|
|
173
|
+
ik llm status --key AIza-abc1
|
|
174
|
+
|
|
175
|
+
ik llm status --json
|
|
176
|
+
"""
|
|
177
|
+
client = _load_client(storage_dir, quota_file, keys_file)
|
|
178
|
+
rows = client.status(provider=_prov(provider), key_id=key)
|
|
179
|
+
|
|
180
|
+
if not rows:
|
|
181
|
+
typer.echo(
|
|
182
|
+
"No keys found. Pass --keys-file to register keys on first use."
|
|
183
|
+
)
|
|
184
|
+
raise typer.Exit(0)
|
|
185
|
+
|
|
186
|
+
if output_json:
|
|
187
|
+
typer.echo(json.dumps(rows, indent=2, default=str))
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
client.print_status(provider=_prov(provider), key_id=key)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ── ik llm quota set ───────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
@quota_app.command("set")
|
|
196
|
+
def quota_set(
|
|
197
|
+
provider: Annotated[
|
|
198
|
+
ProviderChoice,
|
|
199
|
+
typer.Option("--provider", "-p", help="Provider the key belongs to."),
|
|
200
|
+
],
|
|
201
|
+
key: Annotated[
|
|
202
|
+
str,
|
|
203
|
+
typer.Option("--key", "-K", help="Key ID (first 8 chars of the API key)."),
|
|
204
|
+
],
|
|
205
|
+
storage_dir: _StorageDirOption = None,
|
|
206
|
+
quota_file: _QuotaFileOption = None,
|
|
207
|
+
keys_file: _KeysFileOption = None,
|
|
208
|
+
model: Annotated[
|
|
209
|
+
Optional[str],
|
|
210
|
+
typer.Option(
|
|
211
|
+
"--model", "-m",
|
|
212
|
+
help=(
|
|
213
|
+
"Scope quota to a specific model "
|
|
214
|
+
"(e.g. gemini-2.5-pro, gpt-4o). "
|
|
215
|
+
"Omit to set a default that applies to all models on this key."
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
] = None,
|
|
219
|
+
rpm: Annotated[
|
|
220
|
+
Optional[int],
|
|
221
|
+
typer.Option("--rpm", help="Requests-per-minute limit (key-level)."),
|
|
222
|
+
] = None,
|
|
223
|
+
tpm: Annotated[
|
|
224
|
+
Optional[int],
|
|
225
|
+
typer.Option("--tpm", help="Tokens-per-minute limit (model-level)."),
|
|
226
|
+
] = None,
|
|
227
|
+
daily: Annotated[
|
|
228
|
+
Optional[int],
|
|
229
|
+
typer.Option("--daily", help="Daily token limit (model-level)."),
|
|
230
|
+
] = None,
|
|
231
|
+
reset_hour: Annotated[
|
|
232
|
+
int,
|
|
233
|
+
typer.Option(
|
|
234
|
+
"--reset-hour",
|
|
235
|
+
help="UTC hour (0-23) when daily quota resets.",
|
|
236
|
+
min=0, max=23, show_default=True,
|
|
237
|
+
),
|
|
238
|
+
] = 0,
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Set quota limits for a specific API key, optionally scoped to one model.
|
|
242
|
+
|
|
243
|
+
Omitting **--model** sets a default that applies to all models on the key
|
|
244
|
+
that don't have their own entry. Providing **--model** overrides only
|
|
245
|
+
that model.
|
|
246
|
+
|
|
247
|
+
**Examples**
|
|
248
|
+
|
|
249
|
+
# default for all models on this key
|
|
250
|
+
ik llm quota set --provider gemini --key AIza-abc1 --rpm 15 --daily 1500000
|
|
251
|
+
|
|
252
|
+
# tighter limit for one expensive model
|
|
253
|
+
ik llm quota set --provider gemini --key AIza-abc1 \\
|
|
254
|
+
--model gemini-2.5-pro --daily 250000 --reset-hour 0
|
|
255
|
+
|
|
256
|
+
# openai key-level RPM
|
|
257
|
+
ik llm quota set --provider openai --key sk-abc123 --rpm 60 --tpm 90000
|
|
258
|
+
"""
|
|
259
|
+
from infrakit.llm import QuotaConfig
|
|
260
|
+
|
|
261
|
+
client = _load_client(storage_dir, quota_file, keys_file)
|
|
262
|
+
|
|
263
|
+
quota = QuotaConfig(
|
|
264
|
+
model=model,
|
|
265
|
+
rpm_limit=rpm,
|
|
266
|
+
tpm_limit=tpm,
|
|
267
|
+
daily_token_limit=daily,
|
|
268
|
+
reset_hour_utc=reset_hour,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
client.set_quota(provider=provider.value, key_id=key, quota=quota)
|
|
273
|
+
except KeyError:
|
|
274
|
+
typer.echo(
|
|
275
|
+
f"[error] Key '{key}' not found for provider '{provider.value}'.\n"
|
|
276
|
+
"Tip: key IDs are the first 8 characters of the raw API key.",
|
|
277
|
+
err=True,
|
|
278
|
+
)
|
|
279
|
+
raise typer.Exit(1)
|
|
280
|
+
|
|
281
|
+
scope = f"model '{model}'" if model else "all models (default)"
|
|
282
|
+
lines = [f"Quota updated for {provider.value} key '{key}...' ({scope}):"]
|
|
283
|
+
lines.append(f" RPM limit : {rpm if rpm is not None else '(unchanged)'}")
|
|
284
|
+
lines.append(f" TPM limit : {tpm if tpm is not None else '(unchanged)'}")
|
|
285
|
+
lines.append(f" Daily limit : {daily if daily is not None else '(unchanged)'}")
|
|
286
|
+
lines.append(f" Reset hour : {reset_hour:02d}:00 UTC")
|
|
287
|
+
typer.echo("\n".join(lines))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ── click shim ─────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
def _make_click_group():
|
|
293
|
+
return typer.main.get_command(app)
|
|
294
|
+
|
|
295
|
+
click_group = _make_click_group()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infrakit.cli.commands.logger
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
``infrakit logger`` subcommand group.
|
|
5
|
+
|
|
6
|
+
Commands
|
|
7
|
+
--------
|
|
8
|
+
infrakit logger check — show active INFRAKIT_LOG_* env vars
|
|
9
|
+
infrakit logger clean <log_dir> — manually run a retention sweep
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
logger_app = typer.Typer(
|
|
19
|
+
name="logger",
|
|
20
|
+
help="Inspect and maintain infrakit logger state.",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
def _abort(msg: str) -> None:
|
|
27
|
+
typer.echo(typer.style(f"✗ {msg}", fg=typer.colors.RED), err=True)
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ok(msg: str) -> None:
|
|
32
|
+
typer.echo(typer.style(f"✓ {msg}", fg=typer.colors.GREEN))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── infrakit logger check ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
_ENV_VARS = [
|
|
38
|
+
("INFRAKIT_LOG_DIR", "log_dir", "logs"),
|
|
39
|
+
("INFRAKIT_LOG_STRATEGY", "strategy", "date"),
|
|
40
|
+
("INFRAKIT_LOG_STREAM", "stream", "stdout"),
|
|
41
|
+
("INFRAKIT_LOG_FORMAT", "format", "human"),
|
|
42
|
+
("INFRAKIT_LOG_LEVEL", "level", "DEBUG"),
|
|
43
|
+
("INFRAKIT_LOG_SESSION", "session", "<timestamped>"),
|
|
44
|
+
("INFRAKIT_LOG_RETENTION", "retention_days", "<none>"),
|
|
45
|
+
("INFRAKIT_LOG_DRY_RUN", "dry_run", "false"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@logger_app.command("check")
|
|
50
|
+
def cmd_check() -> None:
|
|
51
|
+
"""
|
|
52
|
+
Show the current INFRAKIT_LOG_* environment variables.
|
|
53
|
+
|
|
54
|
+
Useful for verifying your env before running an application —
|
|
55
|
+
whatever is shown here is exactly what setup() will pick up.
|
|
56
|
+
|
|
57
|
+
\b
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
ik logger check
|
|
61
|
+
"""
|
|
62
|
+
import os
|
|
63
|
+
|
|
64
|
+
typer.echo(typer.style("infrakit logger — env configuration", bold=True))
|
|
65
|
+
typer.echo()
|
|
66
|
+
|
|
67
|
+
any_set = False
|
|
68
|
+
for env_key, label, default in _ENV_VARS:
|
|
69
|
+
raw = os.environ.get(env_key)
|
|
70
|
+
if raw is not None:
|
|
71
|
+
any_set = True
|
|
72
|
+
val_str = typer.style(raw, fg=typer.colors.GREEN)
|
|
73
|
+
src = typer.style("(env)", fg=typer.colors.BRIGHT_BLACK)
|
|
74
|
+
else:
|
|
75
|
+
val_str = typer.style(default, fg=typer.colors.BRIGHT_BLACK)
|
|
76
|
+
src = typer.style("(default)", fg=typer.colors.BRIGHT_BLACK)
|
|
77
|
+
|
|
78
|
+
label_str = typer.style(f"{label:<20}", fg=typer.colors.CYAN)
|
|
79
|
+
typer.echo(f" {label_str} {val_str} {src}")
|
|
80
|
+
|
|
81
|
+
typer.echo()
|
|
82
|
+
if any_set:
|
|
83
|
+
_ok("Some INFRAKIT_LOG_* vars are set — setup() will use them automatically.")
|
|
84
|
+
else:
|
|
85
|
+
typer.echo(typer.style(
|
|
86
|
+
" No INFRAKIT_LOG_* vars set — all defaults will apply.",
|
|
87
|
+
fg=typer.colors.BRIGHT_BLACK,
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── infrakit logger clean ─────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
@logger_app.command("clean")
|
|
94
|
+
def cmd_clean(
|
|
95
|
+
log_dir: Path = typer.Argument(..., help="Log directory to sweep."),
|
|
96
|
+
days: int = typer.Option(..., "--days", "-d", help="Delete files older than this many days."),
|
|
97
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview what would be deleted without removing anything."),
|
|
98
|
+
confirm: bool = typer.Option(True, "--confirm/--no-confirm", help="Prompt before deleting (default: on)."),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Run a retention sweep on a log directory.
|
|
102
|
+
|
|
103
|
+
Always does a dry-run preview first, then (if --no-confirm is not set)
|
|
104
|
+
prompts before actually deleting.
|
|
105
|
+
|
|
106
|
+
\b
|
|
107
|
+
Examples
|
|
108
|
+
--------
|
|
109
|
+
ik logger clean ./logs --days 7
|
|
110
|
+
ik logger clean /var/log/myapp --days 30 --dry-run
|
|
111
|
+
ik logger clean ./logs --days 3 --no-confirm
|
|
112
|
+
"""
|
|
113
|
+
if not log_dir.exists():
|
|
114
|
+
_abort(f"Directory not found: {log_dir}")
|
|
115
|
+
|
|
116
|
+
if days < 1:
|
|
117
|
+
_abort("--days must be at least 1.")
|
|
118
|
+
|
|
119
|
+
from infrakit.core.logger.retention import sweep
|
|
120
|
+
|
|
121
|
+
# ── dry-run pass: find candidates without deleting ────────────────────────
|
|
122
|
+
try:
|
|
123
|
+
preview = sweep(log_dir, retention_days=days, dry_run=True)
|
|
124
|
+
except Exception as exc: # noqa: BLE001
|
|
125
|
+
_abort(f"Retention scan failed: {exc}")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if not preview.deleted:
|
|
129
|
+
_ok(f"No log files older than {days} day(s) found in {log_dir}.")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
typer.echo(typer.style(
|
|
133
|
+
f" Found {len(preview.deleted)} file(s) older than {days} day(s):",
|
|
134
|
+
bold=True,
|
|
135
|
+
))
|
|
136
|
+
for p in preview.deleted:
|
|
137
|
+
typer.echo(f" {typer.style(str(p), fg=typer.colors.YELLOW)}")
|
|
138
|
+
typer.echo()
|
|
139
|
+
|
|
140
|
+
# ── dry-run mode: stop here ───────────────────────────────────────────────
|
|
141
|
+
if dry_run:
|
|
142
|
+
typer.echo(typer.style(" Dry-run — nothing deleted.", fg=typer.colors.BRIGHT_BLACK))
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# ── confirm prompt ────────────────────────────────────────────────────────
|
|
146
|
+
if confirm:
|
|
147
|
+
typer.confirm(f" Delete {len(preview.deleted)} file(s)?", abort=True)
|
|
148
|
+
|
|
149
|
+
# ── live pass: actually delete ────────────────────────────────────────────
|
|
150
|
+
try:
|
|
151
|
+
result = sweep(log_dir, retention_days=days, dry_run=False)
|
|
152
|
+
except Exception as exc: # noqa: BLE001
|
|
153
|
+
_abort(f"Deletion failed: {exc}")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
_ok(f"Deleted {len(result.deleted)} file(s).")
|
|
157
|
+
|
|
158
|
+
if result.errors:
|
|
159
|
+
for path, exc in result.errors:
|
|
160
|
+
typer.echo(typer.style(f"⚠ Error on {path}: {exc}", fg=typer.colors.YELLOW), err=True)
|