acex-cli 4.1.2__tar.gz → 4.2.0__tar.gz
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.
- {acex_cli-4.1.2 → acex_cli-4.2.0}/PKG-INFO +3 -3
- {acex_cli-4.1.2 → acex_cli-4.2.0}/pyproject.toml +3 -3
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/commands/node.py +43 -155
- acex_cli-4.2.0/src/acex_cli/diff_formatters.py +345 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/main.py +2 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/print_utils.py +15 -0
- acex_cli-4.1.2/src/acex_cli/diff_formatters.py +0 -261
- {acex_cli-4.1.2 → acex_cli-4.2.0}/README.md +0 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/__init__.py +0 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/commands/__init__.py +0 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/commands/asset.py +0 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/commands/context.py +0 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/context.py +0 -0
- {acex_cli-4.1.2 → acex_cli-4.2.0}/src/acex_cli/sdk.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acex-cli
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: ACE-X CLI - Command-line interface for ACE-X
|
|
5
5
|
License: AGPL-3.0
|
|
6
6
|
Keywords: automation,cli,control
|
|
@@ -11,8 +11,8 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
Requires-Dist: acex-client (>=4.
|
|
15
|
-
Requires-Dist: acex-driver-cisco-ioscli (>=0.0.
|
|
14
|
+
Requires-Dist: acex-client (>=4.2.0,<5.0.0)
|
|
15
|
+
Requires-Dist: acex-driver-cisco-ioscli (>=0.0.17,<0.0.18)
|
|
16
16
|
Requires-Dist: click (==8.1.7)
|
|
17
17
|
Requires-Dist: ntc-templates (>=8.1.0,<9.0.0)
|
|
18
18
|
Requires-Dist: rich (>=13.0.0,<14.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "acex-cli"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.2.0"
|
|
4
4
|
description = "ACE-X CLI - Command-line interface for ACE-X"
|
|
5
5
|
authors = ["Johan Lahti <johan.lahti@acebit.se>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -17,8 +17,8 @@ python = "^3.13"
|
|
|
17
17
|
typer = "^0.12.0"
|
|
18
18
|
rich = "^13.0.0"
|
|
19
19
|
click = "8.1.7"
|
|
20
|
-
acex-client = "^4.
|
|
21
|
-
acex-driver-cisco-ioscli = "^0.0.
|
|
20
|
+
acex-client = "^4.2.0" # Changes to real dep. by switch_acex_client_dep for publishing
|
|
21
|
+
acex-driver-cisco-ioscli = "^0.0.17"
|
|
22
22
|
ntc-templates = "^8.1.0"
|
|
23
23
|
|
|
24
24
|
[tool.poetry.scripts]
|
|
@@ -8,7 +8,7 @@ import requests
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
10
|
from acex_cli.sdk import get_sdk
|
|
11
|
-
from acex_cli.print_utils import print_list_table, print_object
|
|
11
|
+
from acex_cli.print_utils import print_list_table, print_object, print_commands_box
|
|
12
12
|
from acex_client.models.generated_models import Node
|
|
13
13
|
|
|
14
14
|
from acex_devkit.models.composed_configuration import ComposedConfiguration
|
|
@@ -29,6 +29,10 @@ def _request_get(sdk, path: str, params: Optional[dict] = None) -> requests.Resp
|
|
|
29
29
|
response.raise_for_status()
|
|
30
30
|
return response
|
|
31
31
|
|
|
32
|
+
def _get_connection(sdk, node_id):
|
|
33
|
+
response = _request_get(sdk, f"/inventory/management_connections/?node_id={node_id}")
|
|
34
|
+
return response.json()
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
def _get_rendered_config(sdk, node_id: str) -> str:
|
|
34
38
|
response = _request_get(sdk, f"/inventory/node_instances/{node_id}/config")
|
|
@@ -249,7 +253,8 @@ def show_diff_plan(
|
|
|
249
253
|
dfg = observed_config
|
|
250
254
|
d = diff
|
|
251
255
|
ned = sdk.neds.get_driver_instance(node_instance.asset.ned_id)
|
|
252
|
-
commands = ned.jusify_diff_commands(observed_config, d, node_instance.asset)
|
|
256
|
+
# commands = ned.jusify_diff_commands(observed_config, d, node_instance.asset)
|
|
257
|
+
commands = ned.render_patch(d, node_instance)
|
|
253
258
|
|
|
254
259
|
print(commands)
|
|
255
260
|
else:
|
|
@@ -258,171 +263,54 @@ def show_diff_plan(
|
|
|
258
263
|
|
|
259
264
|
|
|
260
265
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
####################################################################
|
|
272
267
|
|
|
273
268
|
# acex node config diff {from} {to}
|
|
274
269
|
|
|
275
270
|
####################################################################
|
|
276
271
|
|
|
272
|
+
# NODE CONFIG APPLY COMMANDS
|
|
273
|
+
@config_diff_app.command("apply")
|
|
274
|
+
def show_diff_plan(
|
|
275
|
+
ctx: typer.Context,
|
|
276
|
+
node_id: str,
|
|
277
|
+
):
|
|
278
|
+
|
|
279
|
+
sdk = get_sdk(ctx.obj.get_active_context())
|
|
280
|
+
differ = sdk.differ
|
|
281
|
+
|
|
282
|
+
# Fetch observed config
|
|
283
|
+
observed_config = _get_latest_observed_config(sdk, node_id, "parsed").get("content")
|
|
284
|
+
observed_config = ComposedConfiguration(**observed_config)
|
|
277
285
|
|
|
278
286
|
|
|
287
|
+
# Fetch desired config
|
|
288
|
+
node_instance = sdk.node_instances.get(node_id)
|
|
289
|
+
if not node_instance:
|
|
290
|
+
typer.echo("Node instance not found.")
|
|
291
|
+
return
|
|
279
292
|
|
|
293
|
+
logical_node = sdk.logical_nodes.get(node_instance.logical_node_id)
|
|
294
|
+
if not logical_node:
|
|
295
|
+
typer.echo("Logical node not found.")
|
|
296
|
+
return
|
|
280
297
|
|
|
298
|
+
desired_config = getattr(logical_node, "configuration", None)
|
|
299
|
+
if desired_config is None:
|
|
300
|
+
typer.echo("No desired configuration found.")
|
|
301
|
+
return
|
|
281
302
|
|
|
303
|
+
diff = differ.diff(observed_config=observed_config, desired_config=desired_config)
|
|
304
|
+
ned = sdk.neds.get_driver_instance(node_instance.asset.ned_id)
|
|
282
305
|
|
|
306
|
+
commands = ned.render_patch(diff, node_instance)
|
|
307
|
+
print_commands_box(commands, node_id)
|
|
308
|
+
proceed = typer.confirm(f"Are you sure you want to apply the config above?")
|
|
283
309
|
|
|
310
|
+
if not proceed:
|
|
311
|
+
exit()
|
|
284
312
|
|
|
313
|
+
connection = _get_connection(sdk, node_id)
|
|
285
314
|
|
|
286
|
-
|
|
287
|
-
# @config_diff_app.command("observed")
|
|
288
|
-
# def observed_config_diff(
|
|
289
|
-
# ctx: typer.Context,
|
|
290
|
-
# node_id: str,
|
|
291
|
-
# diff_from: Optional[str] = typer.Option(
|
|
292
|
-
# None, "--from-hash", help="Diff from this backup hash"
|
|
293
|
-
# ),
|
|
294
|
-
# diff_to: Optional[str] = typer.Option(
|
|
295
|
-
# None, "--to-hash", help="Diff to this backup hash"
|
|
296
|
-
# ),
|
|
297
|
-
# diff_from_time: Optional[str] = typer.Option(
|
|
298
|
-
# None,
|
|
299
|
-
# "--from-time",
|
|
300
|
-
# help="Diff from latest backup at or before this timestamp (ISO 8601)",
|
|
301
|
-
# ),
|
|
302
|
-
# diff_to_time: Optional[str] = typer.Option(
|
|
303
|
-
# None,
|
|
304
|
-
# "--to-time",
|
|
305
|
-
# help="Diff to latest backup at or before this timestamp (ISO 8601)",
|
|
306
|
-
# ),
|
|
307
|
-
# ):
|
|
308
|
-
# """Diff observed (backup) configs between two hashes."""
|
|
309
|
-
# sdk = get_sdk(ctx.obj.get_active_context())
|
|
310
|
-
|
|
311
|
-
# using_hashes = diff_from is not None and diff_to is not None
|
|
312
|
-
# using_times = diff_from_time is not None and diff_to_time is not None
|
|
313
|
-
|
|
314
|
-
# if using_hashes and using_times:
|
|
315
|
-
# typer.echo("Use either --from-hash/--to-hash or --from-time/--to-time, not both.")
|
|
316
|
-
# raise typer.Exit(code=2)
|
|
317
|
-
|
|
318
|
-
# if not using_hashes and not using_times:
|
|
319
|
-
# typer.echo("Provide --from-hash/--to-hash or --from-time/--to-time.")
|
|
320
|
-
# raise typer.Exit(code=2)
|
|
321
|
-
|
|
322
|
-
# if using_times:
|
|
323
|
-
# left = _get_observed_config_before(sdk, node_id, diff_from_time)
|
|
324
|
-
# right = _get_observed_config_before(sdk, node_id, diff_to_time)
|
|
325
|
-
# left_hash = left.get("hash") if isinstance(left, dict) else None
|
|
326
|
-
# right_hash = right.get("hash") if isinstance(right, dict) else None
|
|
327
|
-
# from_label = (
|
|
328
|
-
# f"{node_id}:observed@{diff_from_time}"
|
|
329
|
-
# f"{f'#{left_hash}' if left_hash else ''}"
|
|
330
|
-
# )
|
|
331
|
-
# to_label = (
|
|
332
|
-
# f"{node_id}:observed@{diff_to_time}"
|
|
333
|
-
# f"{f'#{right_hash}' if right_hash else ''}"
|
|
334
|
-
# )
|
|
335
|
-
# else:
|
|
336
|
-
# left = _get_observed_config_by_hash(sdk, node_id, diff_from)
|
|
337
|
-
# right = _get_observed_config_by_hash(sdk, node_id, diff_to)
|
|
338
|
-
# from_label = f"{node_id}:{diff_from}"
|
|
339
|
-
# to_label = f"{node_id}:{diff_to}"
|
|
340
|
-
|
|
341
|
-
# if not left or not right:
|
|
342
|
-
# typer.echo("Observed config not found for one or both inputs.")
|
|
343
|
-
# return
|
|
344
|
-
|
|
345
|
-
# left_content = left.get("content") if isinstance(left, dict) else None
|
|
346
|
-
# right_content = right.get("content") if isinstance(right, dict) else None
|
|
347
|
-
|
|
348
|
-
# if left_content is None or right_content is None:
|
|
349
|
-
# typer.echo("Observed config content missing for one or both hashes.")
|
|
350
|
-
# return
|
|
351
|
-
|
|
352
|
-
# left_text = _decode_config_content(left_content).splitlines(keepends=True)
|
|
353
|
-
# right_text = _decode_config_content(right_content).splitlines(keepends=True)
|
|
354
|
-
|
|
355
|
-
# diff = difflib.unified_diff(
|
|
356
|
-
# left_text,
|
|
357
|
-
# right_text,
|
|
358
|
-
# fromfile=from_label,
|
|
359
|
-
# tofile=to_label,
|
|
360
|
-
# )
|
|
361
|
-
# typer.echo("".join(diff))
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
# @config_diff_app.command("desired-observed")
|
|
365
|
-
# def desired_observed_diff(
|
|
366
|
-
# ctx: typer.Context,
|
|
367
|
-
# node_id: str,
|
|
368
|
-
# config_hash: Optional[str] = typer.Option(
|
|
369
|
-
# None, "--hash", help="Diff against a specific observed backup hash"
|
|
370
|
-
# ),
|
|
371
|
-
# point_in_time: Optional[str] = typer.Option(
|
|
372
|
-
# None,
|
|
373
|
-
# "--time",
|
|
374
|
-
# help="Diff against latest observed backup at or before this timestamp (ISO 8601)",
|
|
375
|
-
# ),
|
|
376
|
-
# ):
|
|
377
|
-
# """Diff desired config against observed (backup) config."""
|
|
378
|
-
# sdk = get_sdk(ctx.obj.get_active_context())
|
|
379
|
-
|
|
380
|
-
# if config_hash and point_in_time:
|
|
381
|
-
# typer.echo("Use either --hash or --time, not both.")
|
|
382
|
-
# raise typer.Exit(code=2)
|
|
383
|
-
|
|
384
|
-
# desired = _get_rendered_config(sdk, node_id)
|
|
385
|
-
# if config_hash:
|
|
386
|
-
# observed = _get_observed_config_by_hash(sdk, node_id, config_hash)
|
|
387
|
-
# observed_label = f"{node_id}:observed@{config_hash}"
|
|
388
|
-
# elif point_in_time:
|
|
389
|
-
# observed = _get_observed_config_before(sdk, node_id, point_in_time)
|
|
390
|
-
# observed_label = f"{node_id}:observed@{point_in_time}"
|
|
391
|
-
# else:
|
|
392
|
-
# observed = _get_latest_observed_config(sdk, node_id)
|
|
393
|
-
# observed_label = f"{node_id}:observed"
|
|
394
|
-
|
|
395
|
-
# if not observed:
|
|
396
|
-
# typer.echo("No observed config found.")
|
|
397
|
-
# return
|
|
398
|
-
|
|
399
|
-
# content = observed.get("content") if isinstance(observed, dict) else None
|
|
400
|
-
# if content is None:
|
|
401
|
-
# typer.echo("Observed config has no content.")
|
|
402
|
-
# return
|
|
403
|
-
|
|
404
|
-
# observed_text = _decode_config_content(content)
|
|
405
|
-
# diff = difflib.unified_diff(
|
|
406
|
-
# desired.splitlines(keepends=True),
|
|
407
|
-
# observed_text.splitlines(keepends=True),
|
|
408
|
-
# fromfile=f"{node_id}:desired",
|
|
409
|
-
# tofile=observed_label,
|
|
410
|
-
# )
|
|
411
|
-
# typer.echo("".join(diff))
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
# @config_app.command("observed-list")
|
|
415
|
-
# def observed_config_list(ctx: typer.Context, node_id: str):
|
|
416
|
-
# """List observed config backups for a node instance."""
|
|
417
|
-
# sdk = get_sdk(ctx.obj.get_active_context())
|
|
418
|
-
# backups = _list_observed_config_hashes(sdk, node_id)
|
|
419
|
-
# if not backups:
|
|
420
|
-
# typer.echo("No observed config backups found.")
|
|
421
|
-
# return
|
|
422
|
-
|
|
423
|
-
# print_list_table(
|
|
424
|
-
# backups,
|
|
425
|
-
# columns=["id", "hash", "created_at", "node_instance_id"],
|
|
426
|
-
# title=f"Observed config backups for node {node_id}",
|
|
427
|
-
# )
|
|
315
|
+
ned.apply_patch(diff, node_instance, connection[0].get("target_ip"))
|
|
428
316
|
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Formatters for displaying configuration diffs in CLI."""
|
|
2
|
+
import json
|
|
3
|
+
from rich.tree import Tree
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
from acex_devkit.configdiffer import Diff, ComponentDiffOp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _as_dict(obj) -> dict:
|
|
15
|
+
"""Convert a Pydantic model or dict to a plain dict for iteration."""
|
|
16
|
+
if isinstance(obj, BaseModel):
|
|
17
|
+
return obj.model_dump()
|
|
18
|
+
if isinstance(obj, dict):
|
|
19
|
+
return obj
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def print_diff_summary(diff):
|
|
24
|
+
"""Print a summary of the diff statistics."""
|
|
25
|
+
|
|
26
|
+
if not isinstance(diff, Diff):
|
|
27
|
+
console.print("[red]Invalid diff object[/red]")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if diff.is_empty():
|
|
31
|
+
console.print("[green]✓ No changes detected[/green]")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
summary = diff.summary()
|
|
35
|
+
|
|
36
|
+
table = Table(title="Configuration Diff Summary")
|
|
37
|
+
table.add_column("Operation", style="bold")
|
|
38
|
+
table.add_column("Count", style="cyan", justify="right")
|
|
39
|
+
|
|
40
|
+
if summary.get("added", 0) > 0:
|
|
41
|
+
table.add_row("Added", f"[green]{summary['added']}[/green]")
|
|
42
|
+
if summary.get("removed", 0) > 0:
|
|
43
|
+
table.add_row("Removed", f"[red]{summary['removed']}[/red]")
|
|
44
|
+
if summary.get("changed", 0) > 0:
|
|
45
|
+
table.add_row("Changed", f"[yellow]{summary['changed']}[/yellow]")
|
|
46
|
+
|
|
47
|
+
console.print(table)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
51
|
+
"""
|
|
52
|
+
Print diff as a hierarchical tree view.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
diff: Diff object from configdiffer
|
|
56
|
+
max_depth: Maximum depth to show (None for unlimited)
|
|
57
|
+
show_values: Whether to show before/after values
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if not isinstance(diff, Diff):
|
|
61
|
+
console.print("[red]Invalid diff object[/red]")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if diff.is_empty():
|
|
65
|
+
console.print("[green]✓ No changes detected[/green]")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Create colored title with summary
|
|
69
|
+
summary = diff.summary()
|
|
70
|
+
title = Text("🔀 Configuration Changes ", style="bold")
|
|
71
|
+
title.append("(", style="dim")
|
|
72
|
+
|
|
73
|
+
parts = []
|
|
74
|
+
if summary.get("added", 0) > 0:
|
|
75
|
+
parts.append((f"+{summary['added']}", "green"))
|
|
76
|
+
if summary.get("removed", 0) > 0:
|
|
77
|
+
parts.append((f"-{summary['removed']}", "red"))
|
|
78
|
+
if summary.get("changed", 0) > 0:
|
|
79
|
+
parts.append((f"~{summary['changed']}", "yellow"))
|
|
80
|
+
|
|
81
|
+
for i, (text, color) in enumerate(parts):
|
|
82
|
+
if i > 0:
|
|
83
|
+
title.append(" ", style="dim")
|
|
84
|
+
title.append(text, style=f"bold {color}")
|
|
85
|
+
|
|
86
|
+
title.append(")", style="dim")
|
|
87
|
+
|
|
88
|
+
tree = Tree(title)
|
|
89
|
+
|
|
90
|
+
# Build a hierarchical tree structure based on component paths
|
|
91
|
+
# We'll use a dict to track nodes: path_tuple -> tree_node
|
|
92
|
+
path_nodes = {}
|
|
93
|
+
|
|
94
|
+
# Group changes by top-level section first to get counts
|
|
95
|
+
sections = {}
|
|
96
|
+
for change in diff.get_all_changes():
|
|
97
|
+
section = change.path[0] if change.path else "root"
|
|
98
|
+
if section not in sections:
|
|
99
|
+
sections[section] = []
|
|
100
|
+
sections[section].append(change)
|
|
101
|
+
|
|
102
|
+
# Process each change and build nested structure
|
|
103
|
+
for section_name in sorted(sections.keys()):
|
|
104
|
+
section_changes = sections[section_name]
|
|
105
|
+
|
|
106
|
+
# Create section node
|
|
107
|
+
section_label = Text()
|
|
108
|
+
section_label.append(section_name, style="bold cyan")
|
|
109
|
+
section_label.append(f" ({len(section_changes)} changes)", style="dim")
|
|
110
|
+
section_node = tree.add(section_label)
|
|
111
|
+
path_nodes[(section_name,)] = section_node
|
|
112
|
+
|
|
113
|
+
# Add each change, building nested path structure
|
|
114
|
+
for change in section_changes:
|
|
115
|
+
# Build path incrementally
|
|
116
|
+
path = tuple(change.path)
|
|
117
|
+
|
|
118
|
+
# Find or create parent nodes for nested path
|
|
119
|
+
parent_node = section_node
|
|
120
|
+
for i in range(1, len(path)):
|
|
121
|
+
partial_path = path[:i+1]
|
|
122
|
+
|
|
123
|
+
# Check if this is the final component or an intermediate path
|
|
124
|
+
if i == len(path) - 1:
|
|
125
|
+
# This is the actual component with the change
|
|
126
|
+
if change.op == ComponentDiffOp.ADD:
|
|
127
|
+
style = "green"
|
|
128
|
+
prefix = "+ "
|
|
129
|
+
elif change.op == ComponentDiffOp.REMOVE:
|
|
130
|
+
style = "red"
|
|
131
|
+
prefix = "- "
|
|
132
|
+
else: # CHANGE
|
|
133
|
+
style = "yellow"
|
|
134
|
+
prefix = "~ "
|
|
135
|
+
|
|
136
|
+
# Component label
|
|
137
|
+
comp_label = Text()
|
|
138
|
+
comp_label.append(prefix, style=f"bold {style}")
|
|
139
|
+
comp_label.append(f"{change.component_type}: ", style=f"dim {style}")
|
|
140
|
+
comp_label.append(change.component_name, style=style)
|
|
141
|
+
|
|
142
|
+
comp_node = parent_node.add(comp_label)
|
|
143
|
+
|
|
144
|
+
if show_values:
|
|
145
|
+
# Show attributes for ADD operations
|
|
146
|
+
if change.op == ComponentDiffOp.ADD and change.after_dict:
|
|
147
|
+
for attr_name, attr_value in change.after_dict.items():
|
|
148
|
+
# Skip metadata and type fields
|
|
149
|
+
if attr_name in ['metadata', 'type']:
|
|
150
|
+
continue
|
|
151
|
+
attr_label = Text()
|
|
152
|
+
attr_label.append(" ", style="dim")
|
|
153
|
+
attr_label.append(attr_name, style="cyan")
|
|
154
|
+
attr_label.append(": ", style="dim")
|
|
155
|
+
formatted_val = _format_value(attr_value)
|
|
156
|
+
attr_label.append(formatted_val, style="dim green" if formatted_val == "null" else "green")
|
|
157
|
+
comp_node.add(attr_label)
|
|
158
|
+
|
|
159
|
+
# Show attributes for REMOVE operations
|
|
160
|
+
elif change.op == ComponentDiffOp.REMOVE and change.before_dict:
|
|
161
|
+
for attr_name, attr_value in change.before_dict.items():
|
|
162
|
+
# Skip metadata and type fields
|
|
163
|
+
if attr_name in ['metadata', 'type']:
|
|
164
|
+
continue
|
|
165
|
+
attr_label = Text()
|
|
166
|
+
attr_label.append(" ", style="dim")
|
|
167
|
+
attr_label.append(attr_name, style="cyan")
|
|
168
|
+
attr_label.append(": ", style="dim")
|
|
169
|
+
formatted_val = _format_value(attr_value)
|
|
170
|
+
attr_label.append(formatted_val, style="dim red" if formatted_val == "null" else "red")
|
|
171
|
+
comp_node.add(attr_label)
|
|
172
|
+
|
|
173
|
+
# Show changed attributes for CHANGE operations
|
|
174
|
+
elif change.op == ComponentDiffOp.CHANGE and change.changed_attributes:
|
|
175
|
+
for attr in change.changed_attributes:
|
|
176
|
+
attr_label = Text()
|
|
177
|
+
attr_label.append(" ", style="dim")
|
|
178
|
+
attr_label.append(attr.attribute_name, style="cyan")
|
|
179
|
+
attr_label.append(": ", style="dim")
|
|
180
|
+
before_val = _format_value(attr.before)
|
|
181
|
+
attr_label.append(before_val, style="dim red" if before_val == "null" else "red")
|
|
182
|
+
attr_label.append(" → ", style="dim")
|
|
183
|
+
after_val = _format_value(attr.after)
|
|
184
|
+
attr_label.append(after_val, style="dim green" if after_val == "null" else "green")
|
|
185
|
+
comp_node.add(attr_label)
|
|
186
|
+
else:
|
|
187
|
+
# Intermediate path element - create if doesn't exist
|
|
188
|
+
if partial_path not in path_nodes:
|
|
189
|
+
path_label = Text()
|
|
190
|
+
path_label.append(path[i], style="dim cyan")
|
|
191
|
+
path_node = parent_node.add(path_label)
|
|
192
|
+
path_nodes[partial_path] = path_node
|
|
193
|
+
parent_node = path_nodes[partial_path]
|
|
194
|
+
|
|
195
|
+
console.print(tree)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def print_diff_compact(diff, show_unchanged: bool = False):
|
|
199
|
+
"""
|
|
200
|
+
Print diff in a compact, git-like format.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
diff: Diff object from configdiffer
|
|
204
|
+
show_unchanged: Whether to show unchanged paths
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
if not isinstance(diff, Diff):
|
|
208
|
+
console.print("[red]Invalid diff object[/red]")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if diff.is_empty():
|
|
212
|
+
console.print("[green]✓ No changes detected[/green]")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if diff.added:
|
|
216
|
+
console.print("\n[bold green]✚ Added:[/bold green]")
|
|
217
|
+
for change in diff.added:
|
|
218
|
+
path = change.get_path_str()
|
|
219
|
+
console.print(f" [green]+ {change.component_type_name_name}:[/green] {change.component_name}")
|
|
220
|
+
|
|
221
|
+
if diff.removed:
|
|
222
|
+
console.print("\n[bold red]✖ Removed:[/bold red]")
|
|
223
|
+
for change in diff.removed:
|
|
224
|
+
path = change.get_path_str()
|
|
225
|
+
console.print(f" [red]- {change.component_type_name_name}:[/red] {change.component_name}")
|
|
226
|
+
|
|
227
|
+
if diff.changed:
|
|
228
|
+
console.print("\n[bold yellow]⚡ Modified:[/bold yellow]")
|
|
229
|
+
for change in diff.changed:
|
|
230
|
+
console.print(f" [yellow]~ {change.component_type_name_name}:[/yellow] {change.component_name}")
|
|
231
|
+
if change.changed_attributes:
|
|
232
|
+
for attr in change.changed_attributes:
|
|
233
|
+
before_str = _format_value(attr.before, max_length=40)
|
|
234
|
+
after_str = _format_value(attr.after, max_length=40)
|
|
235
|
+
console.print(f" {attr.attribute_name}:")
|
|
236
|
+
console.print(f" [red]- {before_str}[/red]")
|
|
237
|
+
console.print(f" [green]+ {after_str}[/green]")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def print_diff_flat(diff):
|
|
241
|
+
"""
|
|
242
|
+
Print diff in a flat list format, good for quick scanning.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
diff: Diff object from configdiffer
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
if not isinstance(diff, Diff):
|
|
249
|
+
console.print("[red]Invalid diff object[/red]")
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if diff.is_empty():
|
|
253
|
+
console.print("[green]✓ No changes detected[/green]")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# Create table
|
|
257
|
+
table = Table(title="Configuration Changes", show_lines=True)
|
|
258
|
+
table.add_column("Op", style="bold", width=3)
|
|
259
|
+
table.add_column("Type", style="cyan")
|
|
260
|
+
table.add_column("Component", style="bold")
|
|
261
|
+
table.add_column("Attribute", style="magenta")
|
|
262
|
+
table.add_column("Before", style="red", max_width=30)
|
|
263
|
+
table.add_column("After", style="green", max_width=30)
|
|
264
|
+
|
|
265
|
+
for change in diff.added:
|
|
266
|
+
table.add_row(
|
|
267
|
+
"[green]+[/green]",
|
|
268
|
+
change.component_type_name_name,
|
|
269
|
+
change.component_name,
|
|
270
|
+
"[dim]--[/dim]",
|
|
271
|
+
"",
|
|
272
|
+
"[dim]new component[/dim]"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
for change in diff.removed:
|
|
276
|
+
table.add_row(
|
|
277
|
+
"[red]-[/red]",
|
|
278
|
+
change.component_type_name_name,
|
|
279
|
+
change.component_name,
|
|
280
|
+
"[dim]--[/dim]",
|
|
281
|
+
"[dim]component deleted[/dim]",
|
|
282
|
+
""
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
for change in diff.changed:
|
|
286
|
+
if change.changed_attributes:
|
|
287
|
+
for i, attr in enumerate(change.changed_attributes):
|
|
288
|
+
table.add_row(
|
|
289
|
+
"[yellow]~[/yellow]" if i == 0 else "",
|
|
290
|
+
change.component_type_name_name if i == 0 else "",
|
|
291
|
+
change.component_name if i == 0 else "",
|
|
292
|
+
attr.attribute_name,
|
|
293
|
+
_format_value(attr.before, max_length=30),
|
|
294
|
+
_format_value(attr.after, max_length=30)
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
table.add_row(
|
|
298
|
+
"[yellow]~[/yellow]",
|
|
299
|
+
change.component_type_name_name,
|
|
300
|
+
change.component_name,
|
|
301
|
+
"[dim]--[/dim]",
|
|
302
|
+
"[dim]changed[/dim]",
|
|
303
|
+
"[dim]changed[/dim]"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
console.print(table)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _format_value(value, max_length: int = 100) -> str:
|
|
310
|
+
"""Format a value for display, truncating if necessary."""
|
|
311
|
+
|
|
312
|
+
if value is None:
|
|
313
|
+
return "null"
|
|
314
|
+
|
|
315
|
+
# Check if this is an AttributeValue (Pydantic model or dict with 'value' key)
|
|
316
|
+
if isinstance(value, BaseModel) and hasattr(value, 'value'):
|
|
317
|
+
value = value.value
|
|
318
|
+
if value is None:
|
|
319
|
+
return "null"
|
|
320
|
+
elif isinstance(value, dict) and 'value' in value:
|
|
321
|
+
value = value['value']
|
|
322
|
+
if value is None:
|
|
323
|
+
return "null"
|
|
324
|
+
|
|
325
|
+
if isinstance(value, BaseModel):
|
|
326
|
+
value_str = json.dumps(value.model_dump(), separators=(',', ':'))
|
|
327
|
+
elif isinstance(value, (dict, list)):
|
|
328
|
+
# Convert any nested BaseModel instances before serializing
|
|
329
|
+
def _to_serializable(obj):
|
|
330
|
+
if isinstance(obj, BaseModel):
|
|
331
|
+
return obj.model_dump()
|
|
332
|
+
if isinstance(obj, dict):
|
|
333
|
+
return {k: _to_serializable(v) for k, v in obj.items()}
|
|
334
|
+
if isinstance(obj, list):
|
|
335
|
+
return [_to_serializable(i) for i in obj]
|
|
336
|
+
return obj
|
|
337
|
+
value_str = json.dumps(_to_serializable(value), separators=(',', ':'))
|
|
338
|
+
else:
|
|
339
|
+
value_str = str(value)
|
|
340
|
+
|
|
341
|
+
# Truncate if too long
|
|
342
|
+
if len(value_str) > max_length:
|
|
343
|
+
return value_str[:max_length-3] + "..."
|
|
344
|
+
|
|
345
|
+
return value_str
|
|
@@ -10,8 +10,10 @@ app = typer.Typer(
|
|
|
10
10
|
name="acex",
|
|
11
11
|
help="ACE-X - Automation & Control Ecosystem CLI",
|
|
12
12
|
add_completion=True,
|
|
13
|
+
pretty_exceptions_enable=False
|
|
13
14
|
)
|
|
14
15
|
|
|
16
|
+
|
|
15
17
|
# Dynamiskt importera och lägg till alla Typer-appar från commands-mappen
|
|
16
18
|
commands_path = pathlib.Path(__file__).parent / "commands"
|
|
17
19
|
for module_info in pkgutil.iter_modules([str(commands_path)]):
|
|
@@ -2,6 +2,8 @@ from rich.table import Table
|
|
|
2
2
|
from rich.console import Console
|
|
3
3
|
from rich.tree import Tree
|
|
4
4
|
from rich.text import Text
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.syntax import Syntax
|
|
5
7
|
|
|
6
8
|
console = Console()
|
|
7
9
|
|
|
@@ -45,3 +47,16 @@ def print_list_table(data, columns=None, title=None, pydantic_class=None):
|
|
|
45
47
|
table.add_row(*(str(row.get(col, "")) for col in columns))
|
|
46
48
|
console.print(table)
|
|
47
49
|
|
|
50
|
+
|
|
51
|
+
def print_commands_box(commands: str, node_id: str):
|
|
52
|
+
"""Print CLI commands in a styled panel box."""
|
|
53
|
+
syntax = Syntax(commands, "bash", theme="monokai", line_numbers=True, word_wrap=True)
|
|
54
|
+
console.print(
|
|
55
|
+
Panel(
|
|
56
|
+
syntax,
|
|
57
|
+
title=f"[bold cyan]node_id: {node_id}[/bold cyan]",
|
|
58
|
+
subtitle="[dim]commands to apply[/dim]",
|
|
59
|
+
border_style="cyan",
|
|
60
|
+
padding=(1, 2),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
"""Formatters for displaying configuration diffs in CLI."""
|
|
2
|
-
|
|
3
|
-
from rich.tree import Tree
|
|
4
|
-
from rich.text import Text
|
|
5
|
-
from rich.table import Table
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
|
|
8
|
-
console = Console()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def print_diff_summary(diff):
|
|
12
|
-
"""Print a summary of the diff statistics."""
|
|
13
|
-
from acex_devkit.configdiffer.configdiffer import Diff
|
|
14
|
-
|
|
15
|
-
if not isinstance(diff, Diff):
|
|
16
|
-
console.print("[red]Invalid diff object[/red]")
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
if diff.is_empty():
|
|
20
|
-
console.print("[green]✓ No changes detected[/green]")
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
summary = diff.summary()
|
|
24
|
-
|
|
25
|
-
table = Table(title="Configuration Diff Summary")
|
|
26
|
-
table.add_column("Operation", style="bold")
|
|
27
|
-
table.add_column("Count", style="cyan", justify="right")
|
|
28
|
-
|
|
29
|
-
if summary.get("add", 0) > 0:
|
|
30
|
-
table.add_row("Added", f"[green]{summary['add']}[/green]")
|
|
31
|
-
if summary.get("remove", 0) > 0:
|
|
32
|
-
table.add_row("Removed", f"[red]{summary['remove']}[/red]")
|
|
33
|
-
if summary.get("change", 0) > 0:
|
|
34
|
-
table.add_row("Changed", f"[yellow]{summary['change']}[/yellow]")
|
|
35
|
-
|
|
36
|
-
console.print(table)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
40
|
-
"""
|
|
41
|
-
Print diff as a hierarchical tree view.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
diff: Diff object from configdiffer
|
|
45
|
-
max_depth: Maximum depth to show (None for unlimited)
|
|
46
|
-
show_values: Whether to show before/after values
|
|
47
|
-
"""
|
|
48
|
-
from acex_devkit.configdiffer.configdiffer import Diff, DiffNode, DiffOp
|
|
49
|
-
|
|
50
|
-
if not isinstance(diff, Diff):
|
|
51
|
-
console.print("[red]Invalid diff object[/red]")
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
if diff.is_empty():
|
|
55
|
-
console.print("[green]✓ No changes detected[/green]")
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
# Create colored title with summary
|
|
59
|
-
summary = diff.summary()
|
|
60
|
-
title = Text("🔀 Configuration Changes ", style="bold")
|
|
61
|
-
title.append("(", style="dim")
|
|
62
|
-
|
|
63
|
-
parts = []
|
|
64
|
-
if summary.get("add", 0) > 0:
|
|
65
|
-
parts.append((f"+{summary['add']}", "green"))
|
|
66
|
-
if summary.get("remove", 0) > 0:
|
|
67
|
-
parts.append((f"-{summary['remove']}", "red"))
|
|
68
|
-
if summary.get("change", 0) > 0:
|
|
69
|
-
parts.append((f"~{summary['change']}", "yellow"))
|
|
70
|
-
|
|
71
|
-
for i, (text, color) in enumerate(parts):
|
|
72
|
-
if i > 0:
|
|
73
|
-
title.append(" ", style="dim")
|
|
74
|
-
title.append(text, style=f"bold {color}")
|
|
75
|
-
|
|
76
|
-
title.append(")", style="dim")
|
|
77
|
-
|
|
78
|
-
tree = Tree(title)
|
|
79
|
-
|
|
80
|
-
def add_node(parent_tree, node: DiffNode, key: str, depth: int = 0):
|
|
81
|
-
"""Recursively add nodes to the tree."""
|
|
82
|
-
if max_depth is not None and depth > max_depth:
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
# Format the node based on operation
|
|
86
|
-
if node.op == DiffOp.ADD:
|
|
87
|
-
style = "green"
|
|
88
|
-
prefix = "+ "
|
|
89
|
-
value_str = f" = {_format_value(node.after)}" if show_values and node.after is not None else ""
|
|
90
|
-
elif node.op == DiffOp.REMOVE:
|
|
91
|
-
style = "red"
|
|
92
|
-
prefix = "- "
|
|
93
|
-
value_str = f" = {_format_value(node.before)}" if show_values and node.before is not None else ""
|
|
94
|
-
elif node.op == DiffOp.CHANGE:
|
|
95
|
-
style = "yellow"
|
|
96
|
-
prefix = "~ "
|
|
97
|
-
if show_values and node.children is None:
|
|
98
|
-
# Leaf node with changed value
|
|
99
|
-
value_str = f" {_format_value(node.before)} → {_format_value(node.after)}"
|
|
100
|
-
else:
|
|
101
|
-
value_str = ""
|
|
102
|
-
else:
|
|
103
|
-
style = "white"
|
|
104
|
-
prefix = " "
|
|
105
|
-
value_str = ""
|
|
106
|
-
|
|
107
|
-
# Create the tree node
|
|
108
|
-
label = Text()
|
|
109
|
-
label.append(prefix, style=f"bold {style}")
|
|
110
|
-
label.append(key, style=style)
|
|
111
|
-
label.append(value_str, style=style)
|
|
112
|
-
|
|
113
|
-
branch = parent_tree.add(label)
|
|
114
|
-
|
|
115
|
-
# Add children if they exist
|
|
116
|
-
if node.children:
|
|
117
|
-
for child_key, child_node in sorted(node.children.items()):
|
|
118
|
-
add_node(branch, child_node, child_key, depth + 1)
|
|
119
|
-
|
|
120
|
-
# Start with root's children
|
|
121
|
-
if diff.root.children:
|
|
122
|
-
for key, node in sorted(diff.root.children.items()):
|
|
123
|
-
add_node(tree, node, key, depth=0)
|
|
124
|
-
|
|
125
|
-
console.print(tree)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def print_diff_compact(diff, show_unchanged: bool = False):
|
|
129
|
-
"""
|
|
130
|
-
Print diff in a compact, git-like format.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
diff: Diff object from configdiffer
|
|
134
|
-
show_unchanged: Whether to show unchanged paths
|
|
135
|
-
"""
|
|
136
|
-
from acex_devkit.configdiffer.configdiffer import Diff, DiffNode, DiffOp
|
|
137
|
-
|
|
138
|
-
if not isinstance(diff, Diff):
|
|
139
|
-
console.print("[red]Invalid diff object[/red]")
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
if diff.is_empty():
|
|
143
|
-
console.print("[green]✓ No changes detected[/green]")
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
changes = []
|
|
147
|
-
|
|
148
|
-
def collect_changes(node: DiffNode, path: str = ""):
|
|
149
|
-
"""Recursively collect all changes with their paths."""
|
|
150
|
-
if node.children:
|
|
151
|
-
for key, child in sorted(node.children.items()):
|
|
152
|
-
new_path = f"{path}.{key}" if path else key
|
|
153
|
-
collect_changes(child, new_path)
|
|
154
|
-
else:
|
|
155
|
-
# Leaf node
|
|
156
|
-
changes.append((path, node))
|
|
157
|
-
|
|
158
|
-
collect_changes(diff.root)
|
|
159
|
-
|
|
160
|
-
# Group by operation type
|
|
161
|
-
adds = [(p, n) for p, n in changes if n.op == DiffOp.ADD]
|
|
162
|
-
removes = [(p, n) for p, n in changes if n.op == DiffOp.REMOVE]
|
|
163
|
-
modifications = [(p, n) for p, n in changes if n.op == DiffOp.CHANGE]
|
|
164
|
-
|
|
165
|
-
if adds:
|
|
166
|
-
console.print("\n[bold green]✚ Added:[/bold green]")
|
|
167
|
-
for path, node in adds:
|
|
168
|
-
value_str = _format_value(node.after, max_length=60)
|
|
169
|
-
console.print(f" [green]+ {path}[/green] = {value_str}")
|
|
170
|
-
|
|
171
|
-
if removes:
|
|
172
|
-
console.print("\n[bold red]✖ Removed:[/bold red]")
|
|
173
|
-
for path, node in removes:
|
|
174
|
-
value_str = _format_value(node.before, max_length=60)
|
|
175
|
-
console.print(f" [red]- {path}[/red] = {value_str}")
|
|
176
|
-
|
|
177
|
-
if modifications:
|
|
178
|
-
console.print("\n[bold yellow]⚡ Modified:[/bold yellow]")
|
|
179
|
-
for path, node in modifications:
|
|
180
|
-
before_str = _format_value(node.before, max_length=40)
|
|
181
|
-
after_str = _format_value(node.after, max_length=40)
|
|
182
|
-
console.print(f" [yellow]~ {path}[/yellow]")
|
|
183
|
-
console.print(f" [red]- {before_str}[/red]")
|
|
184
|
-
console.print(f" [green]+ {after_str}[/green]")
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def print_diff_flat(diff):
|
|
188
|
-
"""
|
|
189
|
-
Print diff in a flat list format, good for quick scanning.
|
|
190
|
-
|
|
191
|
-
Args:
|
|
192
|
-
diff: Diff object from configdiffer
|
|
193
|
-
"""
|
|
194
|
-
from acex_devkit.configdiffer.configdiffer import Diff, DiffNode, DiffOp
|
|
195
|
-
|
|
196
|
-
if not isinstance(diff, Diff):
|
|
197
|
-
console.print("[red]Invalid diff object[/red]")
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
if diff.is_empty():
|
|
201
|
-
console.print("[green]✓ No changes detected[/green]")
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
changes = []
|
|
205
|
-
|
|
206
|
-
def collect_changes(node: DiffNode, path: str = ""):
|
|
207
|
-
"""Recursively collect all changes with their paths."""
|
|
208
|
-
if node.children:
|
|
209
|
-
for key, child in sorted(node.children.items()):
|
|
210
|
-
new_path = f"{path}.{key}" if path else key
|
|
211
|
-
collect_changes(child, new_path)
|
|
212
|
-
else:
|
|
213
|
-
# Leaf node
|
|
214
|
-
changes.append((path, node))
|
|
215
|
-
|
|
216
|
-
collect_changes(diff.root)
|
|
217
|
-
|
|
218
|
-
# Create table
|
|
219
|
-
table = Table(title="Configuration Changes", show_lines=True)
|
|
220
|
-
table.add_column("Op", style="bold", width=3)
|
|
221
|
-
table.add_column("Path", style="cyan")
|
|
222
|
-
table.add_column("Before", style="red", max_width=40)
|
|
223
|
-
table.add_column("After", style="green", max_width=40)
|
|
224
|
-
|
|
225
|
-
for path, node in changes:
|
|
226
|
-
if node.op == DiffOp.ADD:
|
|
227
|
-
op_symbol = "[green]+[/green]"
|
|
228
|
-
before = ""
|
|
229
|
-
after = _format_value(node.after, max_length=40)
|
|
230
|
-
elif node.op == DiffOp.REMOVE:
|
|
231
|
-
op_symbol = "[red]-[/red]"
|
|
232
|
-
before = _format_value(node.before, max_length=40)
|
|
233
|
-
after = ""
|
|
234
|
-
else: # CHANGE
|
|
235
|
-
op_symbol = "[yellow]~[/yellow]"
|
|
236
|
-
before = _format_value(node.before, max_length=40)
|
|
237
|
-
after = _format_value(node.after, max_length=40)
|
|
238
|
-
|
|
239
|
-
table.add_row(op_symbol, path, before, after)
|
|
240
|
-
|
|
241
|
-
console.print(table)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _format_value(value, max_length: int = 100) -> str:
|
|
245
|
-
"""Format a value for display, truncating if necessary."""
|
|
246
|
-
import json
|
|
247
|
-
|
|
248
|
-
if value is None:
|
|
249
|
-
return "[dim]null[/dim]"
|
|
250
|
-
|
|
251
|
-
if isinstance(value, (dict, list)):
|
|
252
|
-
# For complex objects, show compact JSON
|
|
253
|
-
value_str = json.dumps(value, separators=(',', ':'))
|
|
254
|
-
else:
|
|
255
|
-
value_str = str(value)
|
|
256
|
-
|
|
257
|
-
# Truncate if too long
|
|
258
|
-
if len(value_str) > max_length:
|
|
259
|
-
return value_str[:max_length-3] + "..."
|
|
260
|
-
|
|
261
|
-
return value_str
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|