ostruct-cli 0.8.29__py3-none-any.whl → 1.0.1__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.
- ostruct/cli/__init__.py +3 -15
- ostruct/cli/attachment_processor.py +455 -0
- ostruct/cli/attachment_template_bridge.py +973 -0
- ostruct/cli/cli.py +157 -33
- ostruct/cli/click_options.py +775 -692
- ostruct/cli/code_interpreter.py +195 -12
- ostruct/cli/commands/__init__.py +0 -3
- ostruct/cli/commands/run.py +289 -62
- ostruct/cli/config.py +23 -22
- ostruct/cli/constants.py +89 -0
- ostruct/cli/errors.py +175 -5
- ostruct/cli/explicit_file_processor.py +0 -15
- ostruct/cli/file_info.py +97 -15
- ostruct/cli/file_list.py +43 -1
- ostruct/cli/file_search.py +68 -2
- ostruct/cli/help_json.py +235 -0
- ostruct/cli/mcp_integration.py +13 -16
- ostruct/cli/params.py +217 -0
- ostruct/cli/plan_assembly.py +335 -0
- ostruct/cli/plan_printing.py +385 -0
- ostruct/cli/progress_reporting.py +8 -56
- ostruct/cli/quick_ref_help.py +128 -0
- ostruct/cli/rich_config.py +299 -0
- ostruct/cli/runner.py +397 -190
- ostruct/cli/security/__init__.py +2 -0
- ostruct/cli/security/allowed_checker.py +41 -0
- ostruct/cli/security/normalization.py +13 -9
- ostruct/cli/security/security_manager.py +558 -17
- ostruct/cli/security/types.py +15 -0
- ostruct/cli/template_debug.py +283 -261
- ostruct/cli/template_debug_help.py +233 -142
- ostruct/cli/template_env.py +46 -5
- ostruct/cli/template_filters.py +415 -8
- ostruct/cli/template_processor.py +240 -619
- ostruct/cli/template_rendering.py +49 -73
- ostruct/cli/template_validation.py +2 -1
- ostruct/cli/token_validation.py +35 -15
- ostruct/cli/types.py +15 -19
- ostruct/cli/unicode_compat.py +283 -0
- ostruct/cli/upload_manager.py +448 -0
- ostruct/cli/validators.py +255 -54
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/METADATA +231 -128
- ostruct_cli-1.0.1.dist-info/RECORD +80 -0
- ostruct/cli/commands/quick_ref.py +0 -54
- ostruct/cli/template_optimizer.py +0 -478
- ostruct_cli-0.8.29.dist-info/RECORD +0 -71
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/entry_points.txt +0 -0
ostruct/cli/template_debug.py
CHANGED
@@ -5,13 +5,243 @@ including proper logging configuration and template visibility features.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import logging
|
8
|
+
import os
|
8
9
|
import time
|
9
10
|
from dataclasses import dataclass
|
10
|
-
from
|
11
|
+
from enum import Enum
|
12
|
+
from typing import Any, Dict, List, Optional, Set
|
11
13
|
|
12
14
|
import click
|
13
15
|
|
14
16
|
|
17
|
+
class TDCap(str, Enum):
|
18
|
+
"""Template Debug Capacity enum for granular debugging control."""
|
19
|
+
|
20
|
+
PRE_EXPAND = "pre-expand"
|
21
|
+
VARS = "vars"
|
22
|
+
PREVIEW = "preview"
|
23
|
+
STEPS = "steps"
|
24
|
+
POST_EXPAND = "post-expand"
|
25
|
+
|
26
|
+
|
27
|
+
ALL_CAPS: Set[TDCap] = set(TDCap)
|
28
|
+
|
29
|
+
# Tag mapping for consistent output prefixes
|
30
|
+
TAG_MAP = {
|
31
|
+
TDCap.PRE_EXPAND: "PRE",
|
32
|
+
TDCap.VARS: "VARS",
|
33
|
+
TDCap.PREVIEW: "PREVIEW",
|
34
|
+
TDCap.STEPS: "STEP",
|
35
|
+
TDCap.POST_EXPAND: "TPL",
|
36
|
+
}
|
37
|
+
|
38
|
+
|
39
|
+
def parse_td(value: str | None) -> Set[TDCap]:
|
40
|
+
"""Parse template debug capacity string into set of capacities.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
value: Comma-separated capacity list or "all" or None
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Set of TDCap enum values
|
47
|
+
|
48
|
+
Raises:
|
49
|
+
click.BadParameter: If invalid capacity specified
|
50
|
+
"""
|
51
|
+
if value is None or value.lower() == "all":
|
52
|
+
return ALL_CAPS
|
53
|
+
|
54
|
+
caps = set()
|
55
|
+
for tok in value.split(","):
|
56
|
+
tok = tok.strip()
|
57
|
+
try:
|
58
|
+
caps.add(TDCap(tok))
|
59
|
+
except ValueError:
|
60
|
+
valid_caps = ", ".join(c.value for c in TDCap)
|
61
|
+
raise click.BadParameter(
|
62
|
+
f"Unknown template-debug capacity '{tok}'. "
|
63
|
+
f"Valid: all, {valid_caps}"
|
64
|
+
)
|
65
|
+
return caps
|
66
|
+
|
67
|
+
|
68
|
+
def td_log(ctx: click.Context, cap: TDCap, msg: str) -> None:
|
69
|
+
"""Log template debug message if capacity is enabled.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
ctx: Click context containing template debug configuration
|
73
|
+
cap: Template debug capacity for this message
|
74
|
+
msg: Message to log
|
75
|
+
"""
|
76
|
+
if not ctx.obj:
|
77
|
+
return
|
78
|
+
|
79
|
+
active: Set[TDCap] | None = ctx.obj.get("_template_debug_caps")
|
80
|
+
if active and cap in active:
|
81
|
+
tag = TAG_MAP[cap]
|
82
|
+
click.echo(f"[{tag}] {msg}", err=True)
|
83
|
+
|
84
|
+
|
85
|
+
MAX_PREVIEW = int(os.getenv("OSTRUCT_TEMPLATE_PREVIEW_LIMIT", "4096"))
|
86
|
+
|
87
|
+
|
88
|
+
def preview_snip(val: Any, max_size: int | None = None) -> str:
|
89
|
+
"""Create size-aware preview of variable content.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
val: Variable value to preview
|
93
|
+
max_size: Maximum preview size (default: OSTRUCT_TEMPLATE_PREVIEW_LIMIT)
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Preview string with truncation info if needed
|
97
|
+
"""
|
98
|
+
if max_size is None:
|
99
|
+
max_size = MAX_PREVIEW
|
100
|
+
|
101
|
+
# Handle FileInfoList objects specially to avoid multi-file content access errors
|
102
|
+
from .file_list import FileInfoList
|
103
|
+
|
104
|
+
if isinstance(val, FileInfoList):
|
105
|
+
if len(val) == 0:
|
106
|
+
txt = "No files attached"
|
107
|
+
type_info = ""
|
108
|
+
elif len(val) == 1:
|
109
|
+
try:
|
110
|
+
txt = str(val.content)
|
111
|
+
type_info = f" ({type(val).__name__})"
|
112
|
+
except Exception:
|
113
|
+
txt = "1 file attached (content not accessible)"
|
114
|
+
type_info = ""
|
115
|
+
else:
|
116
|
+
txt = f"{len(val)} files attached (use indexing or loop to access individual files)"
|
117
|
+
type_info = ""
|
118
|
+
# Handle other objects with content property
|
119
|
+
elif hasattr(
|
120
|
+
val, "content"
|
121
|
+
): # FileInfo objects and other content-bearing objects
|
122
|
+
try:
|
123
|
+
txt = str(val.content)
|
124
|
+
type_info = f" ({type(val).__name__})"
|
125
|
+
except ValueError:
|
126
|
+
# Handle other content access failures
|
127
|
+
txt = f"Content access failed for {type(val).__name__}"
|
128
|
+
type_info = ""
|
129
|
+
elif isinstance(val, (dict, list)):
|
130
|
+
import json
|
131
|
+
|
132
|
+
txt = json.dumps(val, indent=2)
|
133
|
+
type_info = f" ({type(val).__name__} with {len(val)} items)"
|
134
|
+
else:
|
135
|
+
txt = str(val)
|
136
|
+
type_info = f" ({type(val).__name__})"
|
137
|
+
|
138
|
+
if len(txt) > max_size:
|
139
|
+
return f"{txt[:max_size]}... [truncated {len(txt) - max_size} chars]{type_info}"
|
140
|
+
return f"{txt}{type_info}"
|
141
|
+
|
142
|
+
|
143
|
+
def get_active_capacities(ctx: click.Context | None = None) -> Set[TDCap]:
|
144
|
+
"""Get currently active template debug capacities.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
ctx: Click context (auto-detected if None)
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Set of active capacities, empty if none
|
151
|
+
"""
|
152
|
+
if ctx is None:
|
153
|
+
try:
|
154
|
+
ctx = click.get_current_context()
|
155
|
+
except RuntimeError:
|
156
|
+
return set()
|
157
|
+
|
158
|
+
if not ctx.obj:
|
159
|
+
return set()
|
160
|
+
|
161
|
+
result = ctx.obj.get("_template_debug_caps", set())
|
162
|
+
return result if isinstance(result, set) else set()
|
163
|
+
|
164
|
+
|
165
|
+
def is_capacity_active(cap: TDCap, ctx: click.Context | None = None) -> bool:
|
166
|
+
"""Check if specific template debug capacity is active.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
cap: Capacity to check
|
170
|
+
ctx: Click context (auto-detected if None)
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
True if capacity is active
|
174
|
+
"""
|
175
|
+
active_caps = get_active_capacities(ctx)
|
176
|
+
return cap in active_caps
|
177
|
+
|
178
|
+
|
179
|
+
def td_log_if_active(
|
180
|
+
cap: TDCap, msg: str, ctx: click.Context | None = None
|
181
|
+
) -> None:
|
182
|
+
"""Log template debug message if capacity is active (convenience wrapper).
|
183
|
+
|
184
|
+
Args:
|
185
|
+
cap: Template debug capacity
|
186
|
+
msg: Message to log
|
187
|
+
ctx: Click context (auto-detected if None)
|
188
|
+
"""
|
189
|
+
if ctx is None:
|
190
|
+
try:
|
191
|
+
ctx = click.get_current_context()
|
192
|
+
except RuntimeError:
|
193
|
+
return
|
194
|
+
|
195
|
+
td_log(ctx, cap, msg)
|
196
|
+
|
197
|
+
|
198
|
+
def td_log_vars(
|
199
|
+
variables: Dict[str, Any], ctx: click.Context | None = None
|
200
|
+
) -> None:
|
201
|
+
"""Log variable summary for VARS capacity.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
variables: Dictionary of template variables
|
205
|
+
ctx: Click context (auto-detected if None)
|
206
|
+
"""
|
207
|
+
if not is_capacity_active(TDCap.VARS, ctx):
|
208
|
+
return
|
209
|
+
|
210
|
+
for name, value in variables.items():
|
211
|
+
type_name = type(value).__name__
|
212
|
+
td_log_if_active(TDCap.VARS, f"{name} : {type_name}", ctx)
|
213
|
+
|
214
|
+
|
215
|
+
def td_log_preview(
|
216
|
+
variables: Dict[str, Any], ctx: click.Context | None = None
|
217
|
+
) -> None:
|
218
|
+
"""Log variable content preview for PREVIEW capacity.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
variables: Dictionary of template variables
|
222
|
+
ctx: Click context (auto-detected if None)
|
223
|
+
"""
|
224
|
+
if not is_capacity_active(TDCap.PREVIEW, ctx):
|
225
|
+
return
|
226
|
+
|
227
|
+
for name, value in variables.items():
|
228
|
+
preview = preview_snip(value)
|
229
|
+
td_log_if_active(TDCap.PREVIEW, f"{name} → {preview}", ctx)
|
230
|
+
|
231
|
+
|
232
|
+
def td_log_step(
|
233
|
+
step_num: int, description: str, ctx: click.Context | None = None
|
234
|
+
) -> None:
|
235
|
+
"""Log template expansion step for STEPS capacity.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
step_num: Step number in expansion process
|
239
|
+
description: Description of what this step does
|
240
|
+
ctx: Click context (auto-detected if None)
|
241
|
+
"""
|
242
|
+
td_log_if_active(TDCap.STEPS, f"Step {step_num}: {description}", ctx)
|
243
|
+
|
244
|
+
|
15
245
|
def configure_debug_logging(
|
16
246
|
verbose: bool = False, debug: bool = False
|
17
247
|
) -> None:
|
@@ -81,7 +311,6 @@ def log_template_expansion(
|
|
81
311
|
def show_template_content(
|
82
312
|
system_prompt: str,
|
83
313
|
user_prompt: str,
|
84
|
-
show_templates: bool = False,
|
85
314
|
debug: bool = False,
|
86
315
|
) -> None:
|
87
316
|
"""Show template content with appropriate formatting.
|
@@ -89,12 +318,14 @@ def show_template_content(
|
|
89
318
|
Args:
|
90
319
|
system_prompt: System prompt content
|
91
320
|
user_prompt: User prompt content
|
92
|
-
show_templates: Show templates flag
|
93
321
|
debug: Debug flag
|
94
322
|
"""
|
95
323
|
logger = logging.getLogger(__name__)
|
96
324
|
|
97
|
-
if
|
325
|
+
# Show if debug mode or if post-expand capacity is active
|
326
|
+
should_show = debug or is_capacity_active(TDCap.POST_EXPAND)
|
327
|
+
|
328
|
+
if should_show:
|
98
329
|
# Use click.echo for immediate output that bypasses logging
|
99
330
|
click.echo("📝 Template Content:", err=True)
|
100
331
|
click.echo("=" * 50, err=True)
|
@@ -126,10 +357,54 @@ def show_file_content_expansions(context: Dict[str, Any]) -> None:
|
|
126
357
|
|
127
358
|
logger.debug("📁 File Content Expansions:")
|
128
359
|
for key, value in context.items():
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
360
|
+
# Check for specific file-related types more safely
|
361
|
+
from .attachment_template_bridge import LazyFileContent
|
362
|
+
from .file_info import FileInfo
|
363
|
+
from .file_list import FileInfoList
|
364
|
+
|
365
|
+
if isinstance(value, FileInfo):
|
366
|
+
try:
|
367
|
+
content_len = len(value.content) if value.content else 0
|
368
|
+
logger.debug(f" → {key}: {value.path} ({content_len} chars)")
|
369
|
+
except Exception as e:
|
370
|
+
logger.debug(
|
371
|
+
f" → {key}: FileInfo at {value.path} (content access failed: {e})"
|
372
|
+
)
|
373
|
+
elif isinstance(value, FileInfoList):
|
374
|
+
try:
|
375
|
+
file_count = len(value)
|
376
|
+
if file_count > 0:
|
377
|
+
# Try to get content length safely
|
378
|
+
try:
|
379
|
+
content_len = (
|
380
|
+
len(value.content) if value.content else 0
|
381
|
+
)
|
382
|
+
logger.debug(
|
383
|
+
f" → {key}: FileInfoList with {file_count} files ({content_len} chars)"
|
384
|
+
)
|
385
|
+
except ValueError:
|
386
|
+
logger.debug(
|
387
|
+
f" → {key}: FileInfoList with {file_count} files (empty)"
|
388
|
+
)
|
389
|
+
else:
|
390
|
+
logger.debug(f" → {key}: FileInfoList (empty)")
|
391
|
+
except Exception as e:
|
392
|
+
logger.debug(f" → {key}: FileInfoList (access failed: {e})")
|
393
|
+
elif isinstance(value, LazyFileContent):
|
394
|
+
try:
|
395
|
+
# Show user-friendly file information instead of class name
|
396
|
+
file_size = getattr(value, "actual_size", None) or 0
|
397
|
+
if file_size > 0:
|
398
|
+
size_str = f"{file_size:,} bytes"
|
399
|
+
else:
|
400
|
+
size_str = "unknown size"
|
401
|
+
logger.debug(
|
402
|
+
f" → {key}: file {value.name} ({size_str}) at {value.path}"
|
403
|
+
)
|
404
|
+
except Exception as e:
|
405
|
+
logger.debug(
|
406
|
+
f" → {key}: file {getattr(value, 'name', 'unknown')} (access failed: {e})"
|
407
|
+
)
|
133
408
|
elif isinstance(value, str) and len(value) > 100:
|
134
409
|
logger.debug(f" → {key}: {len(value)} chars")
|
135
410
|
elif isinstance(value, dict):
|
@@ -467,75 +742,6 @@ def display_context_detailed(context: Dict[str, Any]) -> None:
|
|
467
742
|
click.echo("=" * 50, err=True)
|
468
743
|
|
469
744
|
|
470
|
-
def show_pre_optimization_template(template_content: str) -> None:
|
471
|
-
"""Display template content before optimization is applied.
|
472
|
-
|
473
|
-
Args:
|
474
|
-
template_content: Raw template content before optimization
|
475
|
-
"""
|
476
|
-
click.echo("🔧 Template Before Optimization:", err=True)
|
477
|
-
click.echo("=" * 50, err=True)
|
478
|
-
click.echo(template_content, err=True)
|
479
|
-
click.echo("=" * 50, err=True)
|
480
|
-
|
481
|
-
|
482
|
-
def show_optimization_diff(original: str, optimized: str) -> None:
|
483
|
-
"""Show template optimization changes in a readable diff format.
|
484
|
-
|
485
|
-
Args:
|
486
|
-
original: Original template content
|
487
|
-
optimized: Optimized template content
|
488
|
-
"""
|
489
|
-
click.echo("🔄 Template Optimization Changes:", err=True)
|
490
|
-
click.echo("=" * 50, err=True)
|
491
|
-
|
492
|
-
# Simple line-by-line comparison
|
493
|
-
original_lines = original.split("\n")
|
494
|
-
optimized_lines = optimized.split("\n")
|
495
|
-
|
496
|
-
# Show basic statistics
|
497
|
-
click.echo(
|
498
|
-
f"Original: {len(original_lines)} lines, {len(original)} chars",
|
499
|
-
err=True,
|
500
|
-
)
|
501
|
-
click.echo(
|
502
|
-
f"Optimized: {len(optimized_lines)} lines, {len(optimized)} chars",
|
503
|
-
err=True,
|
504
|
-
)
|
505
|
-
|
506
|
-
if original == optimized:
|
507
|
-
click.echo("✅ No optimization changes made", err=True)
|
508
|
-
click.echo("=" * 50, err=True)
|
509
|
-
return
|
510
|
-
|
511
|
-
click.echo("\nChanges:", err=True)
|
512
|
-
|
513
|
-
# Find differences line by line
|
514
|
-
max_lines = max(len(original_lines), len(optimized_lines))
|
515
|
-
changes_found = False
|
516
|
-
|
517
|
-
for i in range(max_lines):
|
518
|
-
orig_line = original_lines[i] if i < len(original_lines) else ""
|
519
|
-
opt_line = optimized_lines[i] if i < len(optimized_lines) else ""
|
520
|
-
|
521
|
-
if orig_line != opt_line:
|
522
|
-
changes_found = True
|
523
|
-
click.echo(f" Line {i + 1}:", err=True)
|
524
|
-
if orig_line:
|
525
|
-
click.echo(f" - {orig_line}", err=True)
|
526
|
-
if opt_line:
|
527
|
-
click.echo(f" + {opt_line}", err=True)
|
528
|
-
|
529
|
-
if not changes_found:
|
530
|
-
# If no line-by-line differences but content differs, show character-level diff
|
531
|
-
click.echo(
|
532
|
-
" Content differs but not at line level (whitespace/formatting changes)",
|
533
|
-
err=True,
|
534
|
-
)
|
535
|
-
|
536
|
-
click.echo("=" * 50, err=True)
|
537
|
-
|
538
|
-
|
539
745
|
def detect_undefined_variables(
|
540
746
|
template_content: str, context: Dict[str, Any]
|
541
747
|
) -> List[str]:
|
@@ -562,187 +768,3 @@ def detect_undefined_variables(
|
|
562
768
|
undefined_vars.append(var)
|
563
769
|
|
564
770
|
return undefined_vars
|
565
|
-
|
566
|
-
|
567
|
-
@dataclass
|
568
|
-
class OptimizationStep:
|
569
|
-
"""Information about a single optimization step."""
|
570
|
-
|
571
|
-
name: str
|
572
|
-
before: str
|
573
|
-
after: str
|
574
|
-
reason: str
|
575
|
-
timestamp: float
|
576
|
-
chars_changed: int = 0
|
577
|
-
|
578
|
-
def __post_init__(self) -> None:
|
579
|
-
"""Calculate character changes after initialization."""
|
580
|
-
self.chars_changed = len(self.after) - len(self.before)
|
581
|
-
|
582
|
-
|
583
|
-
class OptimizationStepTracker:
|
584
|
-
"""Tracker for detailed optimization step analysis."""
|
585
|
-
|
586
|
-
def __init__(self, enabled: bool = False):
|
587
|
-
"""Initialize the optimization step tracker.
|
588
|
-
|
589
|
-
Args:
|
590
|
-
enabled: Whether step tracking is enabled
|
591
|
-
"""
|
592
|
-
self.enabled = enabled
|
593
|
-
self.steps: List[OptimizationStep] = []
|
594
|
-
|
595
|
-
def log_step(
|
596
|
-
self,
|
597
|
-
step_name: str,
|
598
|
-
before: str,
|
599
|
-
after: str,
|
600
|
-
reason: str,
|
601
|
-
) -> None:
|
602
|
-
"""Log an optimization step.
|
603
|
-
|
604
|
-
Args:
|
605
|
-
step_name: Name/description of the optimization step
|
606
|
-
before: Content before this step
|
607
|
-
after: Content after this step
|
608
|
-
reason: Explanation of why this step was applied
|
609
|
-
"""
|
610
|
-
if self.enabled:
|
611
|
-
step = OptimizationStep(
|
612
|
-
name=step_name,
|
613
|
-
before=before,
|
614
|
-
after=after,
|
615
|
-
reason=reason,
|
616
|
-
timestamp=time.time(),
|
617
|
-
)
|
618
|
-
self.steps.append(step)
|
619
|
-
|
620
|
-
def show_step_summary(self) -> None:
|
621
|
-
"""Show a summary of optimization steps."""
|
622
|
-
if not self.enabled or not self.steps:
|
623
|
-
return
|
624
|
-
|
625
|
-
click.echo("🔧 Optimization Steps Summary:", err=True)
|
626
|
-
click.echo("=" * 50, err=True)
|
627
|
-
|
628
|
-
total_chars_changed = 0
|
629
|
-
for i, step in enumerate(self.steps, 1):
|
630
|
-
total_chars_changed += step.chars_changed
|
631
|
-
change_indicator = (
|
632
|
-
"📈"
|
633
|
-
if step.chars_changed > 0
|
634
|
-
else "📉" if step.chars_changed < 0 else "📊"
|
635
|
-
)
|
636
|
-
|
637
|
-
click.echo(f" {i}. {step.name}: {step.reason}", err=True)
|
638
|
-
if step.before != step.after:
|
639
|
-
click.echo(
|
640
|
-
f" {change_indicator} Changed: {len(step.before)} → {len(step.after)} chars ({step.chars_changed:+d})",
|
641
|
-
err=True,
|
642
|
-
)
|
643
|
-
else:
|
644
|
-
click.echo(" 📊 No changes made", err=True)
|
645
|
-
|
646
|
-
click.echo(
|
647
|
-
f"\n📊 Total: {total_chars_changed:+d} characters changed",
|
648
|
-
err=True,
|
649
|
-
)
|
650
|
-
click.echo("=" * 50, err=True)
|
651
|
-
|
652
|
-
def show_detailed_steps(self) -> None:
|
653
|
-
"""Show detailed information for each optimization step."""
|
654
|
-
if not self.enabled or not self.steps:
|
655
|
-
return
|
656
|
-
|
657
|
-
click.echo("🔍 Detailed Optimization Steps:", err=True)
|
658
|
-
click.echo("=" * 50, err=True)
|
659
|
-
|
660
|
-
for i, step in enumerate(self.steps, 1):
|
661
|
-
click.echo(f"\n--- Step {i}: {step.name} ---", err=True)
|
662
|
-
click.echo(f"Reason: {step.reason}", err=True)
|
663
|
-
click.echo(f"Character change: {step.chars_changed:+d}", err=True)
|
664
|
-
|
665
|
-
if step.before != step.after:
|
666
|
-
click.echo("Changes:", err=True)
|
667
|
-
_show_step_diff(step.before, step.after)
|
668
|
-
else:
|
669
|
-
click.echo("✅ No changes made", err=True)
|
670
|
-
|
671
|
-
click.echo("=" * 50, err=True)
|
672
|
-
|
673
|
-
def get_step_stats(self) -> Dict[str, Any]:
|
674
|
-
"""Get statistics about optimization steps.
|
675
|
-
|
676
|
-
Returns:
|
677
|
-
Dictionary with step statistics
|
678
|
-
"""
|
679
|
-
if not self.steps:
|
680
|
-
return {}
|
681
|
-
|
682
|
-
total_steps = len(self.steps)
|
683
|
-
total_chars_changed = sum(step.chars_changed for step in self.steps)
|
684
|
-
steps_with_changes = sum(
|
685
|
-
1 for step in self.steps if step.before != step.after
|
686
|
-
)
|
687
|
-
|
688
|
-
return {
|
689
|
-
"total_steps": total_steps,
|
690
|
-
"steps_with_changes": steps_with_changes,
|
691
|
-
"total_chars_changed": total_chars_changed,
|
692
|
-
"step_names": [step.name for step in self.steps],
|
693
|
-
}
|
694
|
-
|
695
|
-
|
696
|
-
def _show_step_diff(before: str, after: str) -> None:
|
697
|
-
"""Show a simple diff between before and after content.
|
698
|
-
|
699
|
-
Args:
|
700
|
-
before: Content before changes
|
701
|
-
after: Content after changes
|
702
|
-
"""
|
703
|
-
before_lines = before.split("\n")
|
704
|
-
after_lines = after.split("\n")
|
705
|
-
|
706
|
-
max_lines = max(len(before_lines), len(after_lines))
|
707
|
-
changes_shown = 0
|
708
|
-
max_changes = 5 # Limit output for readability
|
709
|
-
|
710
|
-
for i in range(max_lines):
|
711
|
-
if changes_shown >= max_changes:
|
712
|
-
click.echo(
|
713
|
-
f" ... ({max_lines - i} more lines not shown)", err=True
|
714
|
-
)
|
715
|
-
break
|
716
|
-
|
717
|
-
before_line = before_lines[i] if i < len(before_lines) else ""
|
718
|
-
after_line = after_lines[i] if i < len(after_lines) else ""
|
719
|
-
|
720
|
-
if before_line != after_line:
|
721
|
-
changes_shown += 1
|
722
|
-
click.echo(f" Line {i + 1}:", err=True)
|
723
|
-
if before_line:
|
724
|
-
click.echo(f" - {before_line}", err=True)
|
725
|
-
if after_line:
|
726
|
-
click.echo(f" + {after_line}", err=True)
|
727
|
-
|
728
|
-
|
729
|
-
def show_optimization_steps(
|
730
|
-
steps: List[OptimizationStep], detail_level: str = "summary"
|
731
|
-
) -> None:
|
732
|
-
"""Show optimization steps with specified detail level.
|
733
|
-
|
734
|
-
Args:
|
735
|
-
steps: List of optimization steps
|
736
|
-
detail_level: Level of detail ("summary" or "detailed")
|
737
|
-
"""
|
738
|
-
if not steps:
|
739
|
-
click.echo("ℹ️ No optimization steps were recorded", err=True)
|
740
|
-
return
|
741
|
-
|
742
|
-
tracker = OptimizationStepTracker(enabled=True)
|
743
|
-
tracker.steps = steps
|
744
|
-
|
745
|
-
if detail_level == "detailed":
|
746
|
-
tracker.show_detailed_steps()
|
747
|
-
else:
|
748
|
-
tracker.show_step_summary()
|