systemlink-cli 1.4.0__tar.gz → 1.4.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 (75) hide show
  1. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/PKG-INFO +1 -1
  2. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/pyproject.toml +6 -2
  3. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/config_click.py +39 -10
  5. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/main.py +29 -21
  6. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/platform.py +151 -62
  7. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/slcli/SKILL.md +1 -1
  8. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/system_click.py +11 -8
  9. systemlink_cli-1.4.0/dff-editor/README.md +0 -182
  10. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/LICENSE +0 -0
  11. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/dff-editor/editor.js +0 -0
  12. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/dff-editor/index.html +0 -0
  13. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/__init__.py +0 -0
  14. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/__main__.py +0 -0
  15. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/asset_click.py +0 -0
  16. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/cli_formatters.py +0 -0
  17. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/cli_utils.py +0 -0
  18. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/comment_click.py +0 -0
  19. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/completion_click.py +0 -0
  20. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/config.py +0 -0
  21. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/dff_click.py +0 -0
  22. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/dff_decorators.py +0 -0
  23. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/example_click.py +0 -0
  24. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/example_loader.py +0 -0
  25. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/example_provisioner.py +0 -0
  26. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/README.md +0 -0
  27. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/_schema/schema-v1.0.json +0 -0
  28. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/demo-complete-workflow/README.md +0 -0
  29. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  30. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/demo-test-plans/README.md +0 -0
  31. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/demo-test-plans/config.yaml +0 -0
  32. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  33. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  34. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  35. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  36. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  37. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  38. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  39. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  40. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  41. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  42. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/feed_click.py +0 -0
  43. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/file_click.py +0 -0
  44. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/function_click.py +0 -0
  45. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/function_templates.py +0 -0
  46. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/mcp_click.py +0 -0
  47. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/mcp_server.py +0 -0
  48. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/notebook_click.py +0 -0
  49. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/policy_click.py +0 -0
  50. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/policy_utils.py +0 -0
  51. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/profiles.py +0 -0
  52. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/response_handlers.py +0 -0
  53. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/routine_click.py +0 -0
  54. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skill_click.py +0 -0
  55. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  56. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/slcli/references/filtering.md +0 -0
  57. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
  58. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
  59. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
  60. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
  61. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/ssl_trust.py +0 -0
  62. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/table_utils.py +0 -0
  63. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/tag_click.py +0 -0
  64. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/templates_click.py +0 -0
  65. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/testmonitor_click.py +0 -0
  66. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/universal_handlers.py +0 -0
  67. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/user_click.py +0 -0
  68. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/utils.py +0 -0
  69. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/web_editor.py +0 -0
  70. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/webapp_click.py +0 -0
  71. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/workflow_preview.py +0 -0
  72. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/workflows_click.py +0 -0
  73. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/workitem_click.py +0 -0
  74. {systemlink_cli-1.4.0 → systemlink_cli-1.4.2}/slcli/workspace_click.py +0 -0
  75. {systemlink_cli-1.4.0 → systemlink_cli-1.4.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.4.0
3
+ Version: 1.4.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
@@ -1,10 +1,14 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.4.0"
3
+ version = "1.4.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" }]
7
- include = ["dff-editor/**/*", "slcli/skills/**/*"]
7
+ include = [
8
+ { path = "dff-editor/index.html", format = ["wheel", "sdist"] },
9
+ { path = "dff-editor/editor.js", format = ["wheel", "sdist"] },
10
+ "slcli/skills/**/*",
11
+ ]
8
12
 
9
13
 
10
14
  [tool.poetry.scripts]
@@ -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.4.0"
4
+ __version__ = "1.4.2"
@@ -8,7 +8,11 @@ from typing import Any, Optional
8
8
  import click
9
9
  import questionary
10
10
 
11
- from .platform import PLATFORM_SLE, PLATFORM_SLS, detect_platform
11
+ from .platform import (
12
+ PLATFORM_SLE,
13
+ PLATFORM_SLS,
14
+ check_service_status,
15
+ )
12
16
  from .profiles import ProfileConfig, Profile, check_config_file_permissions
13
17
  from .table_utils import output_formatted_list
14
18
  from .utils import ExitCodes
@@ -83,16 +87,41 @@ def _add_profile_impl(
83
87
  click.echo("⚠️ Warning: Adding HTTPS protocol to web URL.")
84
88
  web_url = f"https://{web_url}"
85
89
 
86
- # Detect platform type
87
- click.echo("Detecting platform type...")
88
- platform = detect_platform(url, api_key.strip())
89
-
90
- if platform == PLATFORM_SLE:
91
- click.echo(" Platform: SystemLink Enterprise (Cloud)")
92
- elif platform == PLATFORM_SLS:
93
- click.echo(" Platform: SystemLink Server (On-Premises)")
90
+ # Detect platform type and check service status
91
+ click.echo("Checking server connectivity and services...")
92
+ status = check_service_status(url, api_key.strip())
93
+ platform = status["platform"]
94
+
95
+ if not status["server_reachable"]:
96
+ click.echo(" ⚠️ Could not connect to server", err=True)
97
+ click.echo(" Verify the URL is correct and the server is reachable.", err=True)
98
+ click.echo(
99
+ " Profile will be saved — run login again when the server is available.",
100
+ err=True,
101
+ )
94
102
  else:
95
- click.echo(" Platform: Unknown (will attempt all features)")
103
+ if platform == PLATFORM_SLE:
104
+ click.echo(" Platform: SystemLink Enterprise (Cloud)")
105
+ elif platform == PLATFORM_SLS:
106
+ click.echo(" Platform: SystemLink Server (On-Premises)")
107
+ else:
108
+ click.echo(" Platform: Unknown (will attempt all features)")
109
+
110
+ # Report authorization status
111
+ if status["auth_valid"] is False:
112
+ click.echo(" ⚠️ API key: Unauthorized — check that the key is valid", err=True)
113
+ elif status["auth_valid"] is True:
114
+ click.echo(" API key: ✓ Authorized")
115
+
116
+ # Report individual service status
117
+ services = status.get("services", {})
118
+ problem_services = [
119
+ name for name, svc_status in services.items() if svc_status == "unauthorized"
120
+ ]
121
+ if problem_services and status["auth_valid"] is not False:
122
+ # Only show per-service issues if overall auth isn't completely invalid
123
+ for svc_name in problem_services:
124
+ click.echo(f" ⚠️ {svc_name}: unauthorized", err=True)
96
125
 
97
126
  # Get default workspace (optional)
98
127
  if workspace is None:
@@ -21,7 +21,6 @@ from .function_click import register_function_commands
21
21
  from .mcp_click import register_mcp_commands
22
22
  from .notebook_click import register_notebook_commands
23
23
  from .platform import (
24
- PLATFORM_UNKNOWN,
25
24
  get_platform_info,
26
25
  )
27
26
  from .policy_click import register_policy_commands
@@ -295,11 +294,12 @@ def logout(profile: Optional[str], remove_all: bool, force: bool) -> None:
295
294
 
296
295
  @cli.command()
297
296
  @click.option("--format", "-f", type=click.Choice(["table", "json"]), default="table")
298
- def info(format: str) -> None:
297
+ @click.option("--skip-health", is_flag=True, default=False, help="Skip live service health checks.")
298
+ def info(format: str, skip_health: bool) -> None:
299
299
  """Show current configuration and detected platform."""
300
300
  from .profiles import ProfileConfig, get_active_profile
301
301
 
302
- platform_info = get_platform_info()
302
+ platform_info = get_platform_info(skip_health=skip_health)
303
303
 
304
304
  # Add profile information
305
305
  cfg = ProfileConfig.load()
@@ -333,7 +333,14 @@ def info(format: str) -> None:
333
333
  click.echo("├" + "─" * content_width + "┤")
334
334
 
335
335
  # Connection status
336
- status = "✓ Connected" if platform_info["logged_in"] else "✗ Not logged in"
336
+ if not platform_info["logged_in"]:
337
+ status = "✗ Not logged in"
338
+ elif platform_info.get("server_reachable") is False:
339
+ status = "✗ Server unreachable"
340
+ elif platform_info.get("auth_valid") is False:
341
+ status = "✗ API key unauthorized"
342
+ else:
343
+ status = "✓ Connected"
337
344
  click.echo(f"│ Status: {status:<48}│")
338
345
 
339
346
  # Profile information
@@ -361,23 +368,24 @@ def info(format: str) -> None:
361
368
  workspace_display = truncate(workspace)
362
369
  click.echo(f"│ Workspace: {workspace_display:<48}│")
363
370
 
364
- click.echo("├" + "─" * content_width + "┤")
365
- click.echo("│" + "Feature Availability".center(content_width) + "")
366
- click.echo("├" + "─" * content_width + "┤")
367
-
368
- features = platform_info.get("features", {})
369
- if features:
370
- for feature_name, available in features.items():
371
- status_icon = "✓" if available else "✗"
372
- status_text = "Available" if available else "Not available"
373
- # Truncate feature name if needed
374
- display_name = truncate(feature_name, 29)
375
- click.echo(f"│ {status_icon} {display_name:<30} {status_text:<26}│")
376
- else:
377
- if platform_info["platform"] == PLATFORM_UNKNOWN:
378
- click.echo("│ Run 'slcli login' to detect platform features. │")
379
- else:
380
- click.echo("│ No feature information available. │")
371
+ # Service Health section (only when services were checked)
372
+ services = platform_info.get("services")
373
+ if services:
374
+ click.echo("├" + "─" * content_width + "┤")
375
+ click.echo("│" + "Service Health".center(content_width) + "")
376
+ click.echo("├" + "─" * content_width + "┤")
377
+
378
+ status_display = {
379
+ "ok": ("✓", "OK"),
380
+ "unauthorized": ("✗", "Unauthorized"),
381
+ "not_found": ("—", "Not available"),
382
+ "error": ("✗", "Error"),
383
+ "unreachable": ("✗", "Unreachable"),
384
+ }
385
+ for svc_name, svc_status in services.items():
386
+ icon, text = status_display.get(svc_status, ("?", svc_status))
387
+ display_name = truncate(svc_name, 29)
388
+ click.echo(f"│ {icon} {display_name:<30} {text:<26}│")
381
389
 
382
390
  click.echo("└" + "─" * content_width + "┘\n")
383
391
 
@@ -8,7 +8,7 @@ import json
8
8
  import os
9
9
  import sys
10
10
  from functools import lru_cache
11
- from typing import Any, Dict
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
13
  import click
14
14
  import keyring
@@ -21,6 +21,7 @@ from .utils import ExitCodes, get_ssl_verify
21
21
  PLATFORM_SLE = "SLE" # SystemLink Enterprise (cloud)
22
22
  PLATFORM_SLS = "SLS" # SystemLink Server (on-premises)
23
23
  PLATFORM_UNKNOWN = "unknown"
24
+ PLATFORM_UNREACHABLE = "unreachable" # Server could not be contacted
24
25
 
25
26
  # Feature matrix: maps features to platform availability
26
27
  PLATFORM_FEATURES: Dict[str, Dict[str, bool]] = {
@@ -80,64 +81,21 @@ def _get_keyring_config() -> Dict[str, Any]:
80
81
  def detect_platform(api_url: str, api_key: str) -> str:
81
82
  """Detect the SystemLink platform type by probing endpoints.
82
83
 
83
- Detection strategy:
84
- 1. Try SLE-only endpoint (/niworkorder/v1/query-testplan-templates)
85
- - If accessible -> SLE
86
- 2. Check URL pattern (*.systemlink.io, *.lifecyclesolutions.ni.com)
87
- - If matches -> SLE
88
- 3. Default to SLS for on-premises/custom URLs
84
+ Uses check_service_status to probe services and determine:
85
+ - Platform type (SLE vs SLS)
86
+ - Server reachability
87
+ Falls back to URL pattern matching when probe is inconclusive.
89
88
 
90
89
  Args:
91
90
  api_url: The SystemLink API base URL
92
91
  api_key: The API key for authentication
93
92
 
94
93
  Returns:
95
- Platform identifier (PLATFORM_SLE, PLATFORM_SLS, or PLATFORM_UNKNOWN)
94
+ Platform identifier (PLATFORM_SLE, PLATFORM_SLS, PLATFORM_UNREACHABLE,
95
+ or PLATFORM_UNKNOWN)
96
96
  """
97
- headers = {
98
- "x-ni-api-key": api_key,
99
- "Content-Type": "application/json",
100
- "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
101
- }
102
- ssl_verify = get_ssl_verify()
103
-
104
- # Strategy 1: Probe SLE-only endpoint (Work Order service)
105
- try:
106
- # This endpoint only exists on SLE
107
- workorder_url = f"{api_url}/niworkorder/v1/query-testplan-templates"
108
- resp = requests.post(
109
- workorder_url,
110
- headers=headers,
111
- json={"take": 1},
112
- verify=ssl_verify,
113
- timeout=10,
114
- )
115
- # If we get a 200 or 400 (bad request but endpoint exists), it's SLE
116
- if resp.status_code in (200, 400):
117
- return PLATFORM_SLE
118
- # 404 means endpoint doesn't exist -> likely SLS
119
- if resp.status_code == 404:
120
- return PLATFORM_SLS
121
- except requests.RequestException:
122
- # Connection error - continue with other detection methods
123
- pass
124
-
125
- # Strategy 2: URL pattern matching
126
- # SLE (cloud and hosted) service has specific URL patterns
127
- api_url_lower = api_url.lower()
128
- sle_patterns = [
129
- "api.systemlink.io", # SLE production
130
- "-api.lifecyclesolutions.ni.com", # SLE dev/demo with -api suffix
131
- "dev-api.lifecyclesolutions",
132
- "demo-api.lifecyclesolutions",
133
- ]
134
- for pattern in sle_patterns:
135
- if pattern in api_url_lower:
136
- return PLATFORM_SLE
137
-
138
- # Strategy 3: Default to SLS for on-premises deployments
139
- # This includes on-prem servers that may use *.systemlink.io subdomains
140
- return PLATFORM_SLS
97
+ status = check_service_status(api_url, api_key)
98
+ return status["platform"]
141
99
 
142
100
 
143
101
  def _detect_platform_from_url(api_url: str) -> str:
@@ -274,11 +232,130 @@ def require_feature(feature_name: str) -> None:
274
232
  sys.exit(ExitCodes.INVALID_INPUT)
275
233
 
276
234
 
277
- def get_platform_info() -> Dict[str, Any]:
235
+ # Services to probe during health checks.
236
+ # Each entry: (display_name, method, url_path)
237
+ SERVICE_CHECKS: List[List[str]] = [
238
+ ["Auth", "GET", "/niauth/v1/policies"],
239
+ ["Test Monitor", "GET", "/nitestmonitor/v2/results?take=0"],
240
+ ["Asset Management", "POST", "/niapm/v1/query-assets"],
241
+ ["Systems", "POST", "/nisysmgmt/v1/query-systems"],
242
+ ["Tag", "GET", "/nitag/v2/tags?take=0"],
243
+ ["File", "POST", "/nifile/v1/service-groups/Default/search-files"],
244
+ ["Notebook", "POST", "/ninotebook/v1/notebook/query"],
245
+ ["Web Application", "POST", "/niapp/v1/webapps/query"],
246
+ ["Dynamic Form Fields", "GET", "/nidynamicformfields/v1/groups"],
247
+ ["Work Order", "POST", "/niworkorder/v1/query-testplan-templates"],
248
+ ]
249
+
250
+
251
+ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
252
+ """Probe key SystemLink services and report their status.
253
+
254
+ Checks reachability, authorization, and availability of core services.
255
+
256
+ Args:
257
+ api_url: The SystemLink API base URL.
258
+ api_key: The API key for authentication.
259
+
260
+ Returns:
261
+ Dictionary with:
262
+ - server_reachable: bool - whether any service responded
263
+ - auth_valid: bool | None - whether the API key is authorized (None if unreachable)
264
+ - services: dict mapping service name to status string
265
+ ("ok", "unauthorized", "not_found", "error", "unreachable")
266
+ - platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
267
+ PLATFORM_UNREACHABLE)
268
+ """
269
+ headers = {
270
+ "x-ni-api-key": api_key,
271
+ "Content-Type": "application/json",
272
+ "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
273
+ }
274
+ ssl_verify = get_ssl_verify()
275
+
276
+ services: Dict[str, str] = {}
277
+ any_responded = False
278
+ any_authorized = False
279
+ all_unauthorized = True
280
+
281
+ for display_name, method, url_path in SERVICE_CHECKS:
282
+ try:
283
+ full_url = f"{api_url}{url_path}"
284
+ if method == "POST":
285
+ resp = requests.post(
286
+ full_url,
287
+ headers=headers,
288
+ json={"take": 1},
289
+ verify=ssl_verify,
290
+ timeout=10,
291
+ )
292
+ else:
293
+ resp = requests.get(
294
+ full_url,
295
+ headers=headers,
296
+ verify=ssl_verify,
297
+ timeout=10,
298
+ )
299
+ any_responded = True
300
+
301
+ if resp.status_code in (200, 400):
302
+ services[display_name] = "ok"
303
+ any_authorized = True
304
+ all_unauthorized = False
305
+ elif resp.status_code == 401:
306
+ services[display_name] = "unauthorized"
307
+ elif resp.status_code == 403:
308
+ services[display_name] = "unauthorized"
309
+ elif resp.status_code == 404:
310
+ services[display_name] = "not_found"
311
+ all_unauthorized = False
312
+ else:
313
+ services[display_name] = "error"
314
+ all_unauthorized = False
315
+ except requests.RequestException:
316
+ services[display_name] = "unreachable"
317
+
318
+ # Determine overall status
319
+ if not any_responded:
320
+ return {
321
+ "server_reachable": False,
322
+ "auth_valid": None,
323
+ "services": services,
324
+ "platform": PLATFORM_UNREACHABLE,
325
+ }
326
+
327
+ # Determine auth status: valid if any service accepted the key
328
+ # If all responding services returned 401/403, the key is invalid
329
+ auth_valid = any_authorized if any_responded else None
330
+ if all_unauthorized and any_responded:
331
+ auth_valid = False
332
+
333
+ # Determine platform from service responses
334
+ workorder_status = services.get("Work Order")
335
+ if workorder_status in ("ok",):
336
+ platform = PLATFORM_SLE
337
+ elif workorder_status == "not_found":
338
+ platform = PLATFORM_SLS
339
+ else:
340
+ # Fall back to URL pattern matching
341
+ platform = _detect_platform_from_url(api_url)
342
+
343
+ return {
344
+ "server_reachable": True,
345
+ "auth_valid": auth_valid,
346
+ "services": services,
347
+ "platform": platform,
348
+ }
349
+
350
+
351
+ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
278
352
  """Get detailed information about the current platform configuration.
279
353
 
354
+ Args:
355
+ skip_health: If True, skip live service health checks.
356
+
280
357
  Returns:
281
- Dictionary with platform info including URL, platform type, and features.
358
+ Dictionary with platform info including URL, platform type, and services.
282
359
  """
283
360
  from .utils import get_api_key, get_base_url, get_web_url
284
361
 
@@ -304,11 +381,24 @@ def get_platform_info() -> Dict[str, Any]:
304
381
 
305
382
  active_profile = get_active_profile()
306
383
  if active_profile and active_profile.platform:
307
- platform = active_profile.platform
384
+ stored_platform = active_profile.platform
308
385
  else:
309
386
  # Fall back to keyring config
310
387
  cfg = _get_keyring_config()
311
- platform = cfg.get("platform", PLATFORM_UNKNOWN)
388
+ stored_platform = cfg.get("platform", PLATFORM_UNKNOWN)
389
+
390
+ # Live service health check when logged in
391
+ server_reachable: Optional[bool] = None
392
+ auth_valid: Optional[bool] = None
393
+ services: Optional[Dict[str, str]] = None
394
+ platform = stored_platform
395
+
396
+ if not skip_health and logged_in and isinstance(api_url, str) and api_url != "Not configured":
397
+ status = check_service_status(api_url, api_key)
398
+ server_reachable = status["server_reachable"]
399
+ auth_valid = status["auth_valid"]
400
+ services = status["services"]
401
+ platform = status["platform"]
312
402
 
313
403
  info: Dict[str, Any] = {
314
404
  "api_url": api_url,
@@ -316,14 +406,12 @@ def get_platform_info() -> Dict[str, Any]:
316
406
  "platform": platform,
317
407
  "platform_display": _get_platform_display_name(platform),
318
408
  "logged_in": logged_in,
409
+ "server_reachable": server_reachable,
410
+ "auth_valid": auth_valid,
319
411
  }
320
412
 
321
- # Add feature availability if platform is known
322
- if platform in PLATFORM_FEATURES:
323
- info["features"] = {}
324
- for feature, available in PLATFORM_FEATURES[platform].items():
325
- display_name = FEATURE_DISPLAY_NAMES.get(feature, feature)
326
- info["features"][display_name] = available
413
+ if services is not None:
414
+ info["services"] = services
327
415
 
328
416
  return info
329
417
 
@@ -341,5 +429,6 @@ def _get_platform_display_name(platform: str) -> str:
341
429
  PLATFORM_SLE: "SystemLink Enterprise",
342
430
  PLATFORM_SLS: "SystemLink Server",
343
431
  PLATFORM_UNKNOWN: "Unknown",
432
+ PLATFORM_UNREACHABLE: "Unreachable (could not connect to server)",
344
433
  }
345
434
  return names.get(platform, platform)
@@ -468,7 +468,7 @@ Manage named connection profiles (dev, test, prod). Credentials are stored in
468
468
  ```bash
469
469
  slcli login [--profile NAME] [--url URL] [--api-key KEY] [--web-url URL] [--workspace NAME]
470
470
  slcli logout [--profile NAME] [--all] [--force]
471
- slcli info [-f json] # Show active profile and feature availability
471
+ slcli info [-f json] [--skip-health] # Show active profile and service health
472
472
  slcli completion [--shell SHELL] [--install] # Generate or install shell tab completion
473
473
 
474
474
  slcli config list [-f json] # List all profiles
@@ -1715,14 +1715,17 @@ def register_system_commands(cli: Any) -> None:
1715
1715
  }
1716
1716
  click.echo(json.dumps(result, indent=2))
1717
1717
  else:
1718
- click.echo("\nSystem Fleet Summary")
1719
- click.echo("──────────────────────────────────────")
1720
- click.echo(f" Connected: {connected}")
1721
- click.echo(f" Disconnected: {disconnected}")
1722
- click.echo(f" Virtual: {virtual}")
1723
- click.echo(f" Pending: {pending}")
1724
- click.echo(" ─────────────────")
1725
- click.echo(f" Total: {total}")
1718
+ click.echo()
1719
+ click.echo("┌────────────────────────┐")
1720
+ click.echo("│ System Fleet Summary │")
1721
+ click.echo("├────────────────┬───────┤")
1722
+ click.echo(f"│ Connected │ {connected:>5}")
1723
+ click.echo(f"│ Disconnected │ {disconnected:>5}")
1724
+ click.echo(f"│ Virtual │ {virtual:>5} │")
1725
+ click.echo(f"│ Pending │ {pending:>5}")
1726
+ click.echo("├────────────────┼───────┤")
1727
+ click.echo(f"│ Total │ {total:>5} │")
1728
+ click.echo("└────────────────┴───────┘")
1726
1729
  click.echo()
1727
1730
 
1728
1731
  except Exception as exc: # noqa: BLE001
@@ -1,182 +0,0 @@
1
- # Custom Fields Editor
2
-
3
- This directory contains a standalone web editor for SystemLink Custom Fields configurations with a VS Code-like interface.
4
-
5
- ## Files
6
-
7
- - index.html - The main editor interface
8
- - editor.js - Editor JavaScript logic
9
- - README.md - This file
10
- - index.html.backup - Backup of original simple editor
11
-
12
- ## Usage
13
-
14
- 1. Start the editor server:
15
-
16
- ```bash
17
- slcli customfield edit --port 8080
18
- ```
19
-
20
- 2. Open your browser to: http://localhost:8080
21
-
22
- 3. Use the visual editor to build and manage your configuration
23
-
24
- 4. Click "Apply to Server" when ready to save
25
-
26
- ## Features
27
-
28
- ### Monaco Editor
29
-
30
- - Syntax highlighting and IntelliSense for JSON
31
- - Real-time validation against custom fields schema
32
- - Auto-formatting (Alt+F) and validate (Alt+V)
33
- - Find/Replace (Ctrl+F / Ctrl+H)
34
- - Minimap, dark theme, format on paste/type
35
- - Auto-save every 30 seconds to local storage
36
-
37
- ### Configuration Tree View
38
-
39
- - Root configuration, configurations, groups, fields
40
- - Counts for groups/fields; required field indicator
41
- - Click to navigate
42
-
43
- ### Add New Items
44
-
45
- - Add Configuration: name, workspace, resource type, group keys
46
- - Add Group: key, name, display text, field keys (with duplicate key guard)
47
- - Add Field: key, name, display text, type, required (with duplicate key guard)
48
- - Templates with inline help and validation
49
-
50
- ### Validation
51
-
52
- - JSON syntax correctness
53
- - Required fields present
54
- - Unique keys for groups/fields
55
- - Reference checks (configs → groups, groups → fields)
56
- - Enum validation (resourceType, fieldType)
57
- - Schema compliance
58
-
59
- ### Server Integration
60
-
61
- - **Load from Server**: Fetch configurations by ID or list all configurations
62
- - **Apply to Server**: Seamlessly creates new or updates existing configurations
63
- - **New configurations** (no ID): Automatically calls create endpoint and saves returned ID
64
- - **Existing configurations** (has ID): Updates configuration on server
65
- - Automatically detects operation type and uses appropriate endpoint
66
- - **Smart ID tracking**: After creating a config, the ID is saved to metadata and injected into the editor
67
- - Confirmation dialog before apply with operation-specific messaging
68
- - Error handling with clear messages
69
- - Metadata persistence: `.editor-metadata.json` tracks configuration IDs for seamless workflows
70
-
71
- ### Keyboard Shortcuts
72
-
73
- - Alt+F: Format document
74
- - Alt+V: Validate document
75
- - Ctrl/Cmd+S: Save to server
76
- - Ctrl+F: Find
77
- - Ctrl+H: Find and replace
78
-
79
- ### Persistence & Safety
80
-
81
- - Auto-save to localStorage every 30 seconds
82
- - Auto-recovery prompt for drafts <24h old
83
- - Unsaved changes warning on navigation
84
- - Download JSON export
85
-
86
- ### Toolbar Actions
87
-
88
- - Format, Validate, Load Example, Download JSON, Reset
89
-
90
- ## Configuration Structure
91
-
92
- ### Configurations
93
-
94
- ```json
95
- {
96
- "name": "Work Order Configuration",
97
- "workspace": "workspace-id",
98
- "resourceType": "workorder:workorder",
99
- "groupKeys": ["group1", "group2"],
100
- "properties": {}
101
- }
102
- ```
103
-
104
- Resource types: workorder:workorder, workitem:workitem, asset:asset, system:system, testmonitor:product
105
-
106
- Note: workorder:testplan has been replaced by workitem:workitem to align with the current API.
107
-
108
- ### Groups
109
-
110
- ```json
111
- {
112
- "key": "basicInfo",
113
- "workspace": "workspace-id",
114
- "name": "Basic Information",
115
- "displayText": "Basic Information",
116
- "fieldKeys": ["field1", "field2"],
117
- "properties": {}
118
- }
119
- ```
120
-
121
- ### Fields
122
-
123
- ```json
124
- {
125
- "key": "deviceId",
126
- "workspace": "workspace-id",
127
- "name": "Device ID",
128
- "displayText": "Device Identifier",
129
- "fieldType": "STRING",
130
- "required": true,
131
- "validation": { "maxLength": 50 },
132
- "properties": {}
133
- }
134
- ```
135
-
136
- Field types: STRING, NUMBER, BOOLEAN, DATE, DATETIME, SELECT, MULTISELECT
137
- Validation options: STRING (minLength, maxLength, pattern), NUMBER (min, max, step), DATE/DATETIME (min, max), SELECT/MULTISELECT (options)
138
-
139
- ## Example Workflow
140
-
141
- 1. Load example or start empty
142
- 2. Add configuration (resource type)
143
- 3. Add groups
144
- 4. Add fields
145
- 5. Link fields to groups via fieldKeys
146
- 6. Link groups to configuration via groupKeys
147
- 7. Validate
148
- 8. Apply to server (automatically creates new or updates existing configuration)
149
- 9. Configuration ID is saved automatically for future updates
150
-
151
- ## Technical Details
152
-
153
- - Monaco Editor 0.45.0 via CDN
154
- - Pure vanilla JS; no build step
155
- - Uses fetch for API calls; localStorage for drafts
156
- - Customize `serverUrl`, schema, and templates in editor.js
157
- - Smart create/update detection based on configuration ID presence
158
- - Metadata tracking in `.editor-metadata.json` for seamless workflows
159
-
160
- ## Troubleshooting
161
-
162
- - Editor not loading: check console/CDN access
163
- - Validation errors: ensure unique keys and valid references
164
- - Server issues: check port, CORS, network tab
165
- - Auto-save: ensure localStorage is available
166
-
167
- ## Future Enhancements
168
-
169
- - Drag-and-drop reordering
170
- - Visual preview
171
- - Diff view (local vs server)
172
- - Undo/redo history
173
- - Import from file
174
- - Field/group duplication
175
- - Bulk operations
176
- - Search/filter in tree
177
- - Theme toggle
178
- - Version history
179
-
180
- ---
181
-
182
- Version: 2.1 | Updated: January 8, 2026
File without changes