systemlink-cli 1.5.2__tar.gz → 1.6.2__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.
Files changed (76) hide show
  1. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/PKG-INFO +3 -1
  2. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/pyproject.toml +3 -1
  3. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/cli_utils.py +6 -42
  5. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/config_click.py +23 -45
  6. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/main.py +46 -37
  7. systemlink_cli-1.6.2/slcli/rich_output.py +435 -0
  8. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/slcli/SKILL.md +6 -5
  9. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/systemlink-webapp/SKILL.md +3 -2
  10. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/system_click.py +16 -14
  11. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/table_utils.py +7 -53
  12. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/tag_click.py +2 -4
  13. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/user_click.py +12 -34
  14. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/utils.py +26 -85
  15. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/webapp_click.py +122 -25
  16. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/workspace_click.py +36 -33
  17. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/LICENSE +0 -0
  18. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/dff-editor/editor.js +0 -0
  19. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/dff-editor/index.html +0 -0
  20. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/__init__.py +0 -0
  21. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/__main__.py +0 -0
  22. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/asset_click.py +0 -0
  23. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/cli_formatters.py +0 -0
  24. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/comment_click.py +0 -0
  25. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/completion_click.py +0 -0
  26. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/config.py +0 -0
  27. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/dff_click.py +0 -0
  28. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/dff_decorators.py +0 -0
  29. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/example_click.py +0 -0
  30. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/example_loader.py +0 -0
  31. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/example_provisioner.py +0 -0
  32. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/README.md +0 -0
  33. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/_schema/schema-v1.0.json +0 -0
  34. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/demo-complete-workflow/README.md +0 -0
  35. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  36. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/demo-test-plans/README.md +0 -0
  37. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/demo-test-plans/config.yaml +0 -0
  38. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  39. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  40. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  41. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  42. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  43. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  44. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  45. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  46. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  47. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  48. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/feed_click.py +0 -0
  49. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/file_click.py +0 -0
  50. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/function_click.py +0 -0
  51. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/function_templates.py +0 -0
  52. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/mcp_click.py +0 -0
  53. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/mcp_server.py +0 -0
  54. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/notebook_click.py +0 -0
  55. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/platform.py +0 -0
  56. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/policy_click.py +0 -0
  57. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/policy_utils.py +0 -0
  58. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/profiles.py +0 -0
  59. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/response_handlers.py +0 -0
  60. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/routine_click.py +0 -0
  61. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skill_click.py +0 -0
  62. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  63. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/slcli/references/filtering.md +0 -0
  64. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
  65. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
  66. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
  67. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
  68. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/ssl_trust.py +0 -0
  69. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/templates_click.py +0 -0
  70. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/testmonitor_click.py +0 -0
  71. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/universal_handlers.py +0 -0
  72. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/web_editor.py +0 -0
  73. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/workflow_preview.py +0 -0
  74. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/workflows_click.py +0 -0
  75. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/workitem_click.py +0 -0
  76. {systemlink_cli-1.5.2 → systemlink_cli-1.6.2}/slcli/workspace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemlink-cli
3
- Version: 1.5.2
3
+ Version: 1.6.2
4
4
  Summary: SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates.
5
5
  License-File: LICENSE
6
6
  Author: Fred Visser
@@ -15,6 +15,8 @@ Requires-Dist: keyring (>=25.6.0,<26.0.0)
15
15
  Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
16
16
  Requires-Dist: questionary (>=2.1.1,<3.0.0)
17
17
  Requires-Dist: requests (>=2.32.4,<3.0.0)
18
+ Requires-Dist: rich (>=13.7,<15)
19
+ Requires-Dist: rich-click (>=1.8,<2)
18
20
  Requires-Dist: tabulate (>=0.10.0,<0.11.0)
19
21
  Requires-Dist: truststore (>=0.9,<0.11)
20
22
  Requires-Dist: watchdog (>=6.0.0,<7.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.5.2"
3
+ version = "1.6.2"
4
4
  description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
5
5
  authors = ["Fred Visser <fred.visser@emerson.com>"]
6
6
  packages = [{ include = "slcli" }]
@@ -35,6 +35,8 @@ truststore = ">=0.9,<0.11"
35
35
  watchdog = "^6.0.0"
36
36
  pyyaml = "^6.0.3"
37
37
  questionary = "^2.1.1"
38
+ rich = ">=13.7,<15"
39
+ rich-click = ">=1.8,<2"
38
40
 
39
41
 
40
42
  [tool.poetry.group.dev.dependencies]
@@ -1,4 +1,4 @@
1
1
  """Version information for slcli."""
2
2
 
3
3
  # This file is auto-generated. Do not edit manually.
4
- __version__ = "1.5.2"
4
+ __version__ = "1.6.2"
@@ -1,12 +1,13 @@
1
1
  """Common utility functions for all CLI commands."""
2
2
 
3
3
  import sys
4
- from typing import Any, Dict, List, Optional, Callable
4
+ from typing import Any, Callable, Dict, List, Optional
5
5
 
6
6
  import click
7
7
  import questionary
8
8
  import requests
9
9
 
10
+ from .rich_output import print_json, render_table
10
11
  from .utils import ExitCodes, handle_api_error
11
12
 
12
13
 
@@ -368,9 +369,7 @@ def paginate_list_output(
368
369
 
369
370
  # For JSON format, show all results at once (no pagination)
370
371
  if format_output.lower() == "json":
371
- import json
372
-
373
- click.echo(json.dumps(items, indent=2))
372
+ print_json(items)
374
373
  return
375
374
 
376
375
  # Table format with pagination
@@ -456,49 +455,14 @@ def _output_formatted_page(
456
455
  if not items:
457
456
  return
458
457
 
459
- # Table format with box-drawing characters (without footer)
460
458
  if len(headers) != len(column_widths):
461
459
  raise ValueError("Headers and column_widths must have the same length")
462
460
 
463
- # Top border
464
- border_chars = ["┌"] + [("─" * (w + 2)) for w in column_widths]
465
- border_line = border_chars[0] + border_chars[1]
466
- for part in border_chars[2:]:
467
- border_line += "┬" + part
468
- border_line += "┐"
469
- click.echo(border_line)
470
-
471
- # Header row
472
- header_parts = ["│"]
473
- for header, width in zip(headers, column_widths):
474
- header_parts.append(f" {header:<{width}} │")
475
- click.echo("".join(header_parts))
476
-
477
- # Middle border
478
- border_chars = ["├"] + [("─" * (w + 2)) for w in column_widths]
479
- border_line = border_chars[0] + border_chars[1]
480
- for part in border_chars[2:]:
481
- border_line += "┼" + part
482
- border_line += "┤"
483
- click.echo(border_line)
484
-
485
- # Data rows
461
+ rows = []
486
462
  for item in items:
487
463
  row_data = row_formatter_func(item)
488
464
  if len(row_data) != len(column_widths):
489
465
  raise ValueError("Row data must match column count")
466
+ rows.append(row_data)
490
467
 
491
- row_parts = ["│"]
492
- for value, width in zip(row_data, column_widths):
493
- # Truncate if necessary
494
- str_value = str(value or "")[:width]
495
- row_parts.append(f" {str_value:<{width}} │")
496
- click.echo("".join(row_parts))
497
-
498
- # Bottom border
499
- border_chars = ["└"] + [("─" * (w + 2)) for w in column_widths]
500
- border_line = border_chars[0] + border_chars[1]
501
- for part in border_chars[2:]:
502
- border_line += "┴" + part
503
- border_line += "┘"
504
- click.echo(border_line)
468
+ render_table(headers, column_widths, rows, show_total=False)
@@ -14,6 +14,7 @@ from .platform import (
14
14
  check_service_status,
15
15
  )
16
16
  from .profiles import ProfileConfig, Profile, check_config_file_permissions
17
+ from .rich_output import render_table
17
18
  from .table_utils import output_formatted_list
18
19
  from .utils import ExitCodes
19
20
 
@@ -337,65 +338,42 @@ def register_config_commands(cli: Any) -> None:
337
338
  click.echo(json.dumps(data, indent=2))
338
339
  return
339
340
 
340
- # Table format
341
- click.echo("┌─────────────────────────────────────────────────────────────┐")
342
- click.echo(" slcli Configuration │")
343
- click.echo("├─────────────────────────────────────────────────────────────┤")
344
-
345
- if cfg.current_profile:
346
- click.echo(f"│ Current Profile: {cfg.current_profile:<42} │")
347
- else:
348
- click.echo("│ Current Profile: (none) │")
349
-
350
- config_path_str = str(ProfileConfig.get_config_path())
351
- if len(config_path_str) > 46:
352
- config_path_str = config_path_str[:43] + "..."
353
- click.echo(f"│ Config File: {config_path_str:<46} │")
341
+ rows = [
342
+ ["Current Profile", cfg.current_profile or "(none)"],
343
+ ["Config File", str(ProfileConfig.get_config_path())],
344
+ ]
354
345
 
355
- # Show current profile details
356
346
  if cfg.current_profile and cfg.current_profile in cfg.profiles:
357
347
  profile = cfg.profiles[cfg.current_profile]
358
- click.echo("├─────────────────────────────────────────────────────────────┤")
348
+ rows.append(["Server", profile.server])
359
349
 
360
- # Server
361
- server_str = profile.server
362
- if len(server_str) > 47:
363
- server_str = server_str[:44] + "..."
364
- click.echo(f"│ Server: {server_str:<51} │")
365
-
366
- # Web URL
367
350
  if profile.web_url:
368
- web_url_str = profile.web_url
369
- if len(web_url_str) > 45:
370
- web_url_str = web_url_str[:42] + "..."
371
- click.echo(f"│ Web URL: {web_url_str:<50} │")
351
+ rows.append(["Web URL", profile.web_url])
372
352
 
373
- # Platform
374
353
  if profile.platform:
375
- platform_str = profile.platform or "Unknown"
376
- click.echo(f"│ Platform: {platform_str:<49} │")
354
+ rows.append(["Platform", profile.platform or "Unknown"])
377
355
 
378
- # API Key (redacted)
379
356
  if show_secrets:
380
- click.echo(f"│ API Key: {profile.api_key:<50} │")
357
+ api_key_display = profile.api_key
381
358
  else:
382
- # Show only last 4 characters
383
- key = profile.api_key
384
- redacted_key = "****" + key[-4:] if len(key) >= 4 else "****"
385
- click.echo(f"API Key: {redacted_key:<50} │")
359
+ api_key_display = (
360
+ "****" + profile.api_key[-4:] if len(profile.api_key) >= 4 else "****"
361
+ )
362
+ rows.append(["API Key", api_key_display])
386
363
 
387
- # Workspace
388
364
  if profile.workspace:
389
- workspace_str = profile.workspace
390
- if len(workspace_str) > 45:
391
- workspace_str = workspace_str[:42] + "..."
392
- click.echo(f"│ Workspace: {workspace_str:<48} │")
365
+ rows.append(["Workspace", profile.workspace])
393
366
 
394
- # Readonly
395
367
  if profile.readonly:
396
- click.echo("Readonly: enabled")
397
-
398
- click.echo("└─────────────────────────────────────────────────────────────┘")
368
+ rows.append(["Readonly", "enabled"])
369
+
370
+ click.echo("slcli Configuration:")
371
+ render_table(
372
+ headers=["SETTING", "VALUE"],
373
+ column_widths=[18, 70],
374
+ rows=rows,
375
+ show_total=False,
376
+ )
399
377
 
400
378
  # Check for permission warning
401
379
  warning = check_config_file_permissions()
@@ -2,9 +2,10 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
+ from types import ModuleType
5
6
  from typing import Optional
6
7
 
7
- import click
8
+ import click as base_click
8
9
  import keyring
9
10
  import questionary
10
11
  import tomllib
@@ -25,6 +26,8 @@ from .platform import (
25
26
  )
26
27
  from .policy_click import register_policy_commands
27
28
  from .profiles import set_profile_override
29
+ from .rich_output import install_rich_output
30
+ from .rich_output import render_table
28
31
  from .routine_click import register_routine_commands
29
32
  from .skill_click import register_skill_commands
30
33
  from .ssl_trust import OS_TRUST_INJECTED, OS_TRUST_REASON
@@ -37,6 +40,14 @@ from .webapp_click import register_webapp_commands
37
40
  from .workitem_click import register_workitem_commands
38
41
  from .workspace_click import register_workspace_commands
39
42
 
43
+ click: ModuleType
44
+ try:
45
+ import rich_click as rich_click_module # type: ignore[import-not-found]
46
+ except ModuleNotFoundError:
47
+ click = base_click
48
+ else:
49
+ click = rich_click_module
50
+
40
51
 
41
52
  def get_version() -> str:
42
53
  """Get version from _version.py (built binary) or pyproject.toml (development)."""
@@ -83,8 +94,10 @@ def get_ascii_art() -> str:
83
94
  help="Use a specific profile for this command",
84
95
  )
85
96
  @click.pass_context
86
- def cli(ctx: click.Context, version: bool, profile: Optional[str]) -> None:
97
+ def cli(ctx: base_click.Context, version: bool, profile: Optional[str]) -> None:
87
98
  """SystemLink CLI for managing SystemLink resources.""" # noqa: D403
99
+ install_rich_output()
100
+
88
101
  if version:
89
102
  click.echo(f"slcli version {get_version()}")
90
103
  ctx.exit()
@@ -314,13 +327,7 @@ def info(format: str, skip_health: bool) -> None:
314
327
  click.echo(json.dumps(platform_info, indent=2))
315
328
  return
316
329
 
317
- # Table format using box-drawing characters for key-value display.
318
- # Note: This uses a custom layout rather than table_utils because table_utils
319
- # is designed for list-style output (multiple uniform rows), while this command
320
- # displays a single record with key-value pairs and feature availability.
321
- # All text fields are truncated to prevent formatting issues with long values.
322
330
  max_value_width = 45 # Maximum width for values before truncation
323
- content_width = 61 # Total width inside the box
324
331
 
325
332
  def truncate(value: str, max_len: int = max_value_width) -> str:
326
333
  """Truncate a string with ellipsis if it exceeds max length."""
@@ -328,11 +335,6 @@ def info(format: str, skip_health: bool) -> None:
328
335
  return value[: max_len - 3] + "..."
329
336
  return value
330
337
 
331
- click.echo("\n┌" + "─" * content_width + "┐")
332
- click.echo("│" + "SystemLink CLI Info".center(content_width) + "│")
333
- click.echo("├" + "─" * content_width + "┤")
334
-
335
- # Connection status
336
338
  if not platform_info["logged_in"]:
337
339
  status = "✗ Not logged in"
338
340
  elif platform_info.get("server_reachable") is False:
@@ -341,32 +343,27 @@ def info(format: str, skip_health: bool) -> None:
341
343
  status = "✗ API key unauthorized"
342
344
  else:
343
345
  status = "✓ Connected"
344
- click.echo(f"│ Status: {status:<48}│")
345
346
 
346
- # Profile information
347
347
  profile_display = platform_info.get("active_profile_name", "None")
348
348
  if platform_info.get("profile_count", 0) > 1:
349
349
  profile_display = f"{profile_display} (1 of {platform_info['profile_count']})"
350
350
  profile_display = truncate(profile_display)
351
- click.echo(f"│ Profile: {profile_display:<48}│")
352
-
353
- # Platform
354
351
  platform_display = truncate(platform_info.get("platform_display", "Unknown"))
355
- click.echo(f"│ Platform: {platform_display:<48}│")
356
-
357
- # API URL
358
352
  api_url = truncate(platform_info.get("api_url", "Not configured"))
359
- click.echo(f"│ API URL: {api_url:<48}│")
360
-
361
- # Web URL
362
353
  web_url = truncate(platform_info.get("web_url", "Not configured"))
363
- click.echo(f"│ Web URL: {web_url:<48}│")
364
354
 
365
- # Default workspace
355
+ info_rows = [
356
+ ["Status", status],
357
+ ["Profile", profile_display],
358
+ ["Platform", platform_display],
359
+ ["API URL", api_url],
360
+ ["Web URL", web_url],
361
+ ]
362
+
366
363
  workspace = platform_info.get("active_profile_workspace")
367
364
  if workspace:
368
365
  workspace_display = truncate(workspace)
369
- click.echo(f"Workspace: {workspace_display:<48}│")
366
+ info_rows.append(["Workspace", workspace_display])
370
367
 
371
368
  file_query_endpoint = platform_info.get("file_query_endpoint")
372
369
  if file_query_endpoint:
@@ -375,15 +372,19 @@ def info(format: str, skip_health: bool) -> None:
375
372
  else:
376
373
  file_query_display = str(file_query_endpoint)
377
374
  file_query_display = truncate(file_query_display)
378
- click.echo(f"File Query:{file_query_display:<48}│")
375
+ info_rows.append(["File Query", file_query_display])
376
+
377
+ click.echo()
378
+ click.echo("SystemLink CLI Info:")
379
+ render_table(
380
+ headers=["SETTING", "VALUE"],
381
+ column_widths=[16, 48],
382
+ rows=info_rows,
383
+ show_total=False,
384
+ )
379
385
 
380
- # Service Health section (only when services were checked)
381
386
  services = platform_info.get("services")
382
387
  if services:
383
- click.echo("├" + "─" * content_width + "┤")
384
- click.echo("│" + "Service Health".center(content_width) + "│")
385
- click.echo("├" + "─" * content_width + "┤")
386
-
387
388
  status_display = {
388
389
  "ok": ("✓", "OK"),
389
390
  "fallback": ("!", "Fallback (no Elasticsearch)"),
@@ -392,12 +393,20 @@ def info(format: str, skip_health: bool) -> None:
392
393
  "error": ("✗", "Error"),
393
394
  "unreachable": ("✗", "Unreachable"),
394
395
  }
396
+ service_rows = []
395
397
  for svc_name, svc_status in services.items():
396
398
  icon, text = status_display.get(svc_status, ("?", svc_status))
397
- display_name = truncate(svc_name, 29)
398
- click.echo(f"│ {icon} {display_name:<30} {text:<26}│")
399
-
400
- click.echo("└" + "─" * content_width + "┘\n")
399
+ service_rows.append([truncate(svc_name, 29), f"{icon} {text}"])
400
+
401
+ click.echo()
402
+ click.echo("Service Health:")
403
+ render_table(
404
+ headers=["SERVICE", "STATUS"],
405
+ column_widths=[26, 32],
406
+ rows=service_rows,
407
+ show_total=False,
408
+ )
409
+ click.echo()
401
410
 
402
411
 
403
412
  register_completion_command(cli)