systemlink-cli 1.4.1__tar.gz → 1.4.3__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.
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/PKG-INFO +1 -1
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/pyproject.toml +1 -1
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/_version.py +1 -1
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/config_click.py +48 -11
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/file_click.py +136 -11
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/main.py +38 -20
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/platform.py +253 -62
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/slcli/SKILL.md +1 -1
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/system_click.py +11 -8
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/LICENSE +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/dff-editor/index.html +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/__init__.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/__main__.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/config.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/example_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/demo-complete-workflow/README.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/function_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/mcp_server.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/profiles.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/user_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/utils.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/webapp_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.3"
|
|
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" }]
|
|
@@ -8,7 +8,11 @@ from typing import Any, Optional
|
|
|
8
8
|
import click
|
|
9
9
|
import questionary
|
|
10
10
|
|
|
11
|
-
from .platform import
|
|
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
|
|
@@ -60,6 +64,7 @@ def _add_profile_impl(
|
|
|
60
64
|
elif not url.startswith("https://"):
|
|
61
65
|
click.echo("⚠️ Warning: Adding HTTPS protocol to URL.")
|
|
62
66
|
url = f"https://{url}"
|
|
67
|
+
url = url.rstrip("/")
|
|
63
68
|
|
|
64
69
|
# Get API key - either from flag or prompt
|
|
65
70
|
if not api_key:
|
|
@@ -82,17 +87,49 @@ def _add_profile_impl(
|
|
|
82
87
|
elif not web_url.startswith("https://"):
|
|
83
88
|
click.echo("⚠️ Warning: Adding HTTPS protocol to web URL.")
|
|
84
89
|
web_url = f"https://{web_url}"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
click.echo("
|
|
90
|
+
web_url = web_url.rstrip("/")
|
|
91
|
+
|
|
92
|
+
# Detect platform type and check service status
|
|
93
|
+
click.echo("Checking server connectivity and services...")
|
|
94
|
+
status = check_service_status(url, api_key.strip())
|
|
95
|
+
platform = status["platform"]
|
|
96
|
+
|
|
97
|
+
if not status["server_reachable"]:
|
|
98
|
+
click.echo(" ⚠️ Could not connect to server", err=True)
|
|
99
|
+
click.echo(" Verify the URL is correct and the server is reachable.", err=True)
|
|
100
|
+
click.echo(
|
|
101
|
+
" Profile will be saved — run login again when the server is available.",
|
|
102
|
+
err=True,
|
|
103
|
+
)
|
|
94
104
|
else:
|
|
95
|
-
|
|
105
|
+
if platform == PLATFORM_SLE:
|
|
106
|
+
click.echo(" Platform: SystemLink Enterprise (Cloud)")
|
|
107
|
+
elif platform == PLATFORM_SLS:
|
|
108
|
+
click.echo(" Platform: SystemLink Server (On-Premises)")
|
|
109
|
+
else:
|
|
110
|
+
click.echo(" Platform: Unknown (will attempt all features)")
|
|
111
|
+
|
|
112
|
+
# Report authorization status
|
|
113
|
+
if status["auth_valid"] is False:
|
|
114
|
+
click.echo(" ⚠️ API key: Unauthorized — check that the key is valid", err=True)
|
|
115
|
+
elif status["auth_valid"] is True:
|
|
116
|
+
click.echo(" API key: ✓ Authorized")
|
|
117
|
+
|
|
118
|
+
if status.get("elasticsearch_available") is False:
|
|
119
|
+
click.echo(" File query: query-files-linq (Elasticsearch unavailable)")
|
|
120
|
+
click.echo(
|
|
121
|
+
" 'slcli file list' will fall back automatically; 'slcli file query' requires search-files."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Report individual service status
|
|
125
|
+
services = status.get("services", {})
|
|
126
|
+
problem_services = [
|
|
127
|
+
name for name, svc_status in services.items() if svc_status == "unauthorized"
|
|
128
|
+
]
|
|
129
|
+
if problem_services and status["auth_valid"] is not False:
|
|
130
|
+
# Only show per-service issues if overall auth isn't completely invalid
|
|
131
|
+
for svc_name in problem_services:
|
|
132
|
+
click.echo(f" ⚠️ {svc_name}: unauthorized", err=True)
|
|
96
133
|
|
|
97
134
|
# Get default workspace (optional)
|
|
98
135
|
if workspace is None:
|
|
@@ -10,9 +10,10 @@ from typing import Any, Dict, List, Optional
|
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
12
|
import questionary
|
|
13
|
+
import requests as requests_lib
|
|
13
14
|
|
|
14
15
|
from .cli_utils import validate_output_format
|
|
15
|
-
from .universal_handlers import UniversalResponseHandler
|
|
16
|
+
from .universal_handlers import FilteredResponse, UniversalResponseHandler
|
|
16
17
|
from .utils import (
|
|
17
18
|
ExitCodes,
|
|
18
19
|
format_success,
|
|
@@ -29,6 +30,120 @@ def _get_file_service_url() -> str:
|
|
|
29
30
|
return f"{get_base_url()}/nifile/v1/service-groups/Default"
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def _get_search_files_url() -> str:
|
|
34
|
+
"""Get the search-files endpoint URL."""
|
|
35
|
+
return f"{_get_file_service_url()}/search-files"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_search_files_unavailable(exc: requests_lib.HTTPError) -> bool:
|
|
39
|
+
"""Return whether the search-files endpoint is unavailable."""
|
|
40
|
+
return exc.response is not None and exc.response.status_code in (404, 501)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _exit_search_files_required() -> None:
|
|
44
|
+
"""Exit with a user-facing message when search-files is unavailable."""
|
|
45
|
+
click.echo(
|
|
46
|
+
"✗ File query requires the search-files endpoint, but Elasticsearch is not available.",
|
|
47
|
+
err=True,
|
|
48
|
+
)
|
|
49
|
+
click.echo(
|
|
50
|
+
" Use 'slcli file list' for automatic fallback to query-files-linq.",
|
|
51
|
+
err=True,
|
|
52
|
+
)
|
|
53
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _search_files_with_fallback(
|
|
57
|
+
payload: Dict[str, Any],
|
|
58
|
+
workspace_id: Optional[str],
|
|
59
|
+
name_filter: Optional[str],
|
|
60
|
+
id_filter: Optional[str],
|
|
61
|
+
) -> Any:
|
|
62
|
+
"""Try search-files endpoint, fall back to query-files-linq if unavailable.
|
|
63
|
+
|
|
64
|
+
The search-files endpoint requires additional software (DataFinder) to be
|
|
65
|
+
installed. If it is not available (HTTP 404), we fall back to query-files-linq.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
payload: The search-files request payload
|
|
69
|
+
workspace_id: Optional workspace ID filter
|
|
70
|
+
name_filter: Optional name filter string
|
|
71
|
+
id_filter: Optional comma-separated file IDs
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Response object (real or FilteredResponse)
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
return make_api_request(
|
|
78
|
+
"POST", _get_search_files_url(), payload=payload, handle_errors=False
|
|
79
|
+
)
|
|
80
|
+
except requests_lib.HTTPError as exc:
|
|
81
|
+
if _is_search_files_unavailable(exc):
|
|
82
|
+
return _query_files_linq_fallback(
|
|
83
|
+
take=payload.get("take", 25),
|
|
84
|
+
workspace_id=workspace_id,
|
|
85
|
+
name_filter=name_filter,
|
|
86
|
+
id_filter=id_filter,
|
|
87
|
+
)
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _query_files_linq_fallback(
|
|
92
|
+
take: int,
|
|
93
|
+
workspace_id: Optional[str],
|
|
94
|
+
name_filter: Optional[str],
|
|
95
|
+
id_filter: Optional[str],
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""List files using query-files-linq as a fallback when search-files is unavailable.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
take: Maximum number of files to return
|
|
101
|
+
workspace_id: Optional workspace ID filter
|
|
102
|
+
name_filter: Optional name filter string (applied client-side)
|
|
103
|
+
id_filter: Optional comma-separated file IDs
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Response or FilteredResponse with availableFiles data
|
|
107
|
+
"""
|
|
108
|
+
base_url = _get_file_service_url()
|
|
109
|
+
url = f"{base_url}/query-files-linq"
|
|
110
|
+
|
|
111
|
+
# Build LINQ-style filter
|
|
112
|
+
filter_parts: List[str] = []
|
|
113
|
+
|
|
114
|
+
if id_filter:
|
|
115
|
+
ids = [fid.strip() for fid in id_filter.split(",")]
|
|
116
|
+
id_conditions = [f'id = "{fid}"' for fid in ids]
|
|
117
|
+
filter_parts.append("(" + " or ".join(id_conditions) + ")")
|
|
118
|
+
|
|
119
|
+
linq_payload: Dict[str, Any] = {}
|
|
120
|
+
|
|
121
|
+
# Request more if we need to filter client-side by name
|
|
122
|
+
if name_filter:
|
|
123
|
+
linq_payload["take"] = max(take * 4, 1000)
|
|
124
|
+
else:
|
|
125
|
+
linq_payload["take"] = take
|
|
126
|
+
|
|
127
|
+
if filter_parts:
|
|
128
|
+
linq_payload["filter"] = " and ".join(filter_parts)
|
|
129
|
+
|
|
130
|
+
# Workspace filtering via query parameter
|
|
131
|
+
if workspace_id:
|
|
132
|
+
url += f"?workspace={workspace_id}"
|
|
133
|
+
|
|
134
|
+
resp = make_api_request("POST", url, payload=linq_payload)
|
|
135
|
+
|
|
136
|
+
# Client-side name filtering (query-files-linq doesn't support name wildcards)
|
|
137
|
+
if name_filter:
|
|
138
|
+
data = resp.json()
|
|
139
|
+
files = data.get("availableFiles", [])
|
|
140
|
+
needle = name_filter.lower()
|
|
141
|
+
filtered = [f for f in files if needle in _get_file_name(f).lower()][:take]
|
|
142
|
+
return FilteredResponse({"availableFiles": filtered})
|
|
143
|
+
|
|
144
|
+
return resp
|
|
145
|
+
|
|
146
|
+
|
|
32
147
|
def _format_file_size(size_bytes: Optional[int]) -> str:
|
|
33
148
|
"""Format file size in human-readable format.
|
|
34
149
|
|
|
@@ -178,9 +293,6 @@ def register_file_commands(cli: Any) -> None:
|
|
|
178
293
|
"""
|
|
179
294
|
format_output = validate_output_format(format)
|
|
180
295
|
|
|
181
|
-
# Use search-files endpoint for better performance
|
|
182
|
-
url = f"{_get_file_service_url()}/search-files"
|
|
183
|
-
|
|
184
296
|
try:
|
|
185
297
|
# Resolve workspace name to ID if needed
|
|
186
298
|
workspace_id = None
|
|
@@ -189,7 +301,7 @@ def register_file_commands(cli: Any) -> None:
|
|
|
189
301
|
workspace_map = get_workspace_map()
|
|
190
302
|
workspace_id = resolve_workspace_filter(workspace, workspace_map)
|
|
191
303
|
|
|
192
|
-
# Build search filter
|
|
304
|
+
# Build search filter for search-files endpoint
|
|
193
305
|
filter_parts = []
|
|
194
306
|
|
|
195
307
|
if workspace_id:
|
|
@@ -221,7 +333,13 @@ def register_file_commands(cli: Any) -> None:
|
|
|
221
333
|
if filter_parts:
|
|
222
334
|
payload["filter"] = " AND ".join(filter_parts)
|
|
223
335
|
|
|
224
|
-
|
|
336
|
+
# Try search-files, fall back to query-files-linq if unavailable
|
|
337
|
+
resp = _search_files_with_fallback(
|
|
338
|
+
payload=payload,
|
|
339
|
+
workspace_id=workspace_id,
|
|
340
|
+
name_filter=name_filter,
|
|
341
|
+
id_filter=id_filter,
|
|
342
|
+
)
|
|
225
343
|
|
|
226
344
|
def file_formatter(file_item: dict) -> list:
|
|
227
345
|
name = _get_file_name(file_item)
|
|
@@ -567,11 +685,8 @@ def register_file_commands(cli: Any) -> None:
|
|
|
567
685
|
"""
|
|
568
686
|
format_output = validate_output_format(format)
|
|
569
687
|
|
|
570
|
-
# Use search-files endpoint for better performance
|
|
571
|
-
url = f"{_get_file_service_url()}/search-files"
|
|
572
|
-
|
|
573
688
|
try:
|
|
574
|
-
# Build request body
|
|
689
|
+
# Build request body for search-files
|
|
575
690
|
query_body: Dict[str, Any] = {
|
|
576
691
|
"take": take if format_output.lower() == "json" else (take if take != 25 else 1000),
|
|
577
692
|
"orderByDescending": descending,
|
|
@@ -584,6 +699,7 @@ def register_file_commands(cli: Any) -> None:
|
|
|
584
699
|
filter_parts.append(filter_query)
|
|
585
700
|
|
|
586
701
|
# Resolve workspace name to ID if needed
|
|
702
|
+
workspace_id = None
|
|
587
703
|
workspace = get_effective_workspace(workspace)
|
|
588
704
|
if workspace:
|
|
589
705
|
workspace_map = get_workspace_map()
|
|
@@ -598,7 +714,12 @@ def register_file_commands(cli: Any) -> None:
|
|
|
598
714
|
else:
|
|
599
715
|
query_body["orderBy"] = "updated"
|
|
600
716
|
|
|
601
|
-
resp = make_api_request(
|
|
717
|
+
resp = make_api_request(
|
|
718
|
+
"POST",
|
|
719
|
+
_get_search_files_url(),
|
|
720
|
+
payload=query_body,
|
|
721
|
+
handle_errors=False,
|
|
722
|
+
)
|
|
602
723
|
|
|
603
724
|
def file_formatter(file_item: dict) -> list:
|
|
604
725
|
name = _get_file_name(file_item)
|
|
@@ -620,6 +741,10 @@ def register_file_commands(cli: Any) -> None:
|
|
|
620
741
|
page_size=take,
|
|
621
742
|
)
|
|
622
743
|
|
|
744
|
+
except requests_lib.HTTPError as exc:
|
|
745
|
+
if _is_search_files_unavailable(exc):
|
|
746
|
+
_exit_search_files_required()
|
|
747
|
+
handle_api_error(exc)
|
|
623
748
|
except Exception as exc:
|
|
624
749
|
handle_api_error(exc)
|
|
625
750
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,34 @@ def info(format: str) -> None:
|
|
|
361
368
|
workspace_display = truncate(workspace)
|
|
362
369
|
click.echo(f"│ Workspace: {workspace_display:<48}│")
|
|
363
370
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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. │")
|
|
371
|
+
file_query_endpoint = platform_info.get("file_query_endpoint")
|
|
372
|
+
if file_query_endpoint:
|
|
373
|
+
if platform_info.get("elasticsearch_available") is False:
|
|
374
|
+
file_query_display = f"{file_query_endpoint} (Elasticsearch unavailable)"
|
|
379
375
|
else:
|
|
380
|
-
|
|
376
|
+
file_query_display = str(file_query_endpoint)
|
|
377
|
+
file_query_display = truncate(file_query_display)
|
|
378
|
+
click.echo(f"│ File Query:{file_query_display:<48}│")
|
|
379
|
+
|
|
380
|
+
# Service Health section (only when services were checked)
|
|
381
|
+
services = platform_info.get("services")
|
|
382
|
+
if services:
|
|
383
|
+
click.echo("├" + "─" * content_width + "┤")
|
|
384
|
+
click.echo("│" + "Service Health".center(content_width) + "│")
|
|
385
|
+
click.echo("├" + "─" * content_width + "┤")
|
|
386
|
+
|
|
387
|
+
status_display = {
|
|
388
|
+
"ok": ("✓", "OK"),
|
|
389
|
+
"fallback": ("!", "Fallback (no Elasticsearch)"),
|
|
390
|
+
"unauthorized": ("✗", "Unauthorized"),
|
|
391
|
+
"not_found": ("—", "Not available"),
|
|
392
|
+
"error": ("✗", "Error"),
|
|
393
|
+
"unreachable": ("✗", "Unreachable"),
|
|
394
|
+
}
|
|
395
|
+
for svc_name, svc_status in services.items():
|
|
396
|
+
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}│")
|
|
381
399
|
|
|
382
400
|
click.echo("└" + "─" * content_width + "┘\n")
|
|
383
401
|
|
|
@@ -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]] = {
|
|
@@ -55,6 +56,9 @@ FEATURE_DISPLAY_NAMES: Dict[str, str] = {
|
|
|
55
56
|
"webapp": "Web Applications",
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
FILE_SEARCH_PATH = "/nifile/v1/service-groups/Default/search-files"
|
|
60
|
+
FILE_QUERY_LINQ_PATH = "/nifile/v1/service-groups/Default/query-files-linq"
|
|
61
|
+
|
|
58
62
|
|
|
59
63
|
def _get_keyring_config() -> Dict[str, Any]:
|
|
60
64
|
"""Attempt to read a single JSON config entry from keyring.
|
|
@@ -80,64 +84,21 @@ def _get_keyring_config() -> Dict[str, Any]:
|
|
|
80
84
|
def detect_platform(api_url: str, api_key: str) -> str:
|
|
81
85
|
"""Detect the SystemLink platform type by probing endpoints.
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- If matches -> SLE
|
|
88
|
-
3. Default to SLS for on-premises/custom URLs
|
|
87
|
+
Uses check_service_status to probe services and determine:
|
|
88
|
+
- Platform type (SLE vs SLS)
|
|
89
|
+
- Server reachability
|
|
90
|
+
Falls back to URL pattern matching when probe is inconclusive.
|
|
89
91
|
|
|
90
92
|
Args:
|
|
91
93
|
api_url: The SystemLink API base URL
|
|
92
94
|
api_key: The API key for authentication
|
|
93
95
|
|
|
94
96
|
Returns:
|
|
95
|
-
Platform identifier (PLATFORM_SLE, PLATFORM_SLS,
|
|
97
|
+
Platform identifier (PLATFORM_SLE, PLATFORM_SLS, PLATFORM_UNREACHABLE,
|
|
98
|
+
or PLATFORM_UNKNOWN)
|
|
96
99
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
100
|
+
status = check_service_status(api_url, api_key)
|
|
101
|
+
return status["platform"]
|
|
141
102
|
|
|
142
103
|
|
|
143
104
|
def _detect_platform_from_url(api_url: str) -> str:
|
|
@@ -274,11 +235,220 @@ def require_feature(feature_name: str) -> None:
|
|
|
274
235
|
sys.exit(ExitCodes.INVALID_INPUT)
|
|
275
236
|
|
|
276
237
|
|
|
277
|
-
|
|
238
|
+
# Services to probe during health checks.
|
|
239
|
+
# Each entry: (display_name, method, url_path)
|
|
240
|
+
SERVICE_CHECKS: List[List[str]] = [
|
|
241
|
+
["Auth", "GET", "/niauth/v1/policies"],
|
|
242
|
+
["Test Monitor", "GET", "/nitestmonitor/v2/results?take=0"],
|
|
243
|
+
["Asset Management", "POST", "/niapm/v1/query-assets"],
|
|
244
|
+
["Systems", "POST", "/nisysmgmt/v1/query-systems"],
|
|
245
|
+
["Tag", "GET", "/nitag/v2/tags?take=0"],
|
|
246
|
+
["File", "POST", FILE_SEARCH_PATH],
|
|
247
|
+
["Notebook", "POST", "/ninotebook/v1/notebook/query"],
|
|
248
|
+
["Web Application", "POST", "/niapp/v1/webapps/query"],
|
|
249
|
+
["Dynamic Form Fields", "GET", "/nidynamicformfields/v1/groups"],
|
|
250
|
+
["Work Order", "POST", "/niworkorder/v1/query-testplan-templates"],
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_file_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
255
|
+
"""Determine which file query endpoint is available for this server."""
|
|
256
|
+
headers = {
|
|
257
|
+
"x-ni-api-key": api_key,
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
"User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
|
|
260
|
+
}
|
|
261
|
+
ssl_verify = get_ssl_verify()
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
search_resp = requests.post(
|
|
265
|
+
f"{api_url}{FILE_SEARCH_PATH}",
|
|
266
|
+
headers=headers,
|
|
267
|
+
json={"take": 1},
|
|
268
|
+
verify=ssl_verify,
|
|
269
|
+
timeout=10,
|
|
270
|
+
)
|
|
271
|
+
if search_resp.status_code in (200, 400):
|
|
272
|
+
return {
|
|
273
|
+
"status": "ok",
|
|
274
|
+
"file_query_endpoint": "search-files",
|
|
275
|
+
"elasticsearch_available": True,
|
|
276
|
+
}
|
|
277
|
+
if search_resp.status_code in (401, 403):
|
|
278
|
+
return {
|
|
279
|
+
"status": "unauthorized",
|
|
280
|
+
"file_query_endpoint": "search-files",
|
|
281
|
+
"elasticsearch_available": True,
|
|
282
|
+
}
|
|
283
|
+
if search_resp.status_code not in (404, 501):
|
|
284
|
+
return {
|
|
285
|
+
"status": "error" if search_resp.status_code >= 500 else "not_found",
|
|
286
|
+
"file_query_endpoint": None,
|
|
287
|
+
"elasticsearch_available": True,
|
|
288
|
+
}
|
|
289
|
+
except requests.RequestException:
|
|
290
|
+
return {
|
|
291
|
+
"status": "unreachable",
|
|
292
|
+
"file_query_endpoint": None,
|
|
293
|
+
"elasticsearch_available": None,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
fallback_resp = requests.post(
|
|
298
|
+
f"{api_url}{FILE_QUERY_LINQ_PATH}",
|
|
299
|
+
headers=headers,
|
|
300
|
+
json={"take": 1},
|
|
301
|
+
verify=ssl_verify,
|
|
302
|
+
timeout=10,
|
|
303
|
+
)
|
|
304
|
+
if fallback_resp.status_code in (200, 400):
|
|
305
|
+
return {
|
|
306
|
+
"status": "fallback",
|
|
307
|
+
"file_query_endpoint": "query-files-linq",
|
|
308
|
+
"elasticsearch_available": False,
|
|
309
|
+
}
|
|
310
|
+
if fallback_resp.status_code in (401, 403):
|
|
311
|
+
return {
|
|
312
|
+
"status": "unauthorized",
|
|
313
|
+
"file_query_endpoint": "query-files-linq",
|
|
314
|
+
"elasticsearch_available": False,
|
|
315
|
+
}
|
|
316
|
+
if fallback_resp.status_code in (404, 501):
|
|
317
|
+
return {
|
|
318
|
+
"status": "not_found",
|
|
319
|
+
"file_query_endpoint": None,
|
|
320
|
+
"elasticsearch_available": False,
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
"status": "error",
|
|
324
|
+
"file_query_endpoint": None,
|
|
325
|
+
"elasticsearch_available": False,
|
|
326
|
+
}
|
|
327
|
+
except requests.RequestException:
|
|
328
|
+
return {
|
|
329
|
+
"status": "unreachable",
|
|
330
|
+
"file_query_endpoint": None,
|
|
331
|
+
"elasticsearch_available": False,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
336
|
+
"""Probe key SystemLink services and report their status.
|
|
337
|
+
|
|
338
|
+
Checks reachability, authorization, and availability of core services.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
api_url: The SystemLink API base URL.
|
|
342
|
+
api_key: The API key for authentication.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dictionary with:
|
|
346
|
+
- server_reachable: bool - whether any service responded
|
|
347
|
+
- auth_valid: bool | None - whether the API key is authorized (None if unreachable)
|
|
348
|
+
- services: dict mapping service name to status string
|
|
349
|
+
("ok", "unauthorized", "not_found", "error", "unreachable")
|
|
350
|
+
- file_query_endpoint: selected file query endpoint, if available
|
|
351
|
+
- elasticsearch_available: bool | None - whether search-files is available
|
|
352
|
+
- platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
|
|
353
|
+
PLATFORM_UNREACHABLE)
|
|
354
|
+
"""
|
|
355
|
+
headers = {
|
|
356
|
+
"x-ni-api-key": api_key,
|
|
357
|
+
"Content-Type": "application/json",
|
|
358
|
+
"User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
|
|
359
|
+
}
|
|
360
|
+
ssl_verify = get_ssl_verify()
|
|
361
|
+
|
|
362
|
+
services: Dict[str, str] = {}
|
|
363
|
+
any_responded = False
|
|
364
|
+
any_authorized = False
|
|
365
|
+
all_unauthorized = True
|
|
366
|
+
|
|
367
|
+
for display_name, method, url_path in SERVICE_CHECKS:
|
|
368
|
+
try:
|
|
369
|
+
full_url = f"{api_url}{url_path}"
|
|
370
|
+
if method == "POST":
|
|
371
|
+
resp = requests.post(
|
|
372
|
+
full_url,
|
|
373
|
+
headers=headers,
|
|
374
|
+
json={"take": 1},
|
|
375
|
+
verify=ssl_verify,
|
|
376
|
+
timeout=10,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
resp = requests.get(
|
|
380
|
+
full_url,
|
|
381
|
+
headers=headers,
|
|
382
|
+
verify=ssl_verify,
|
|
383
|
+
timeout=10,
|
|
384
|
+
)
|
|
385
|
+
any_responded = True
|
|
386
|
+
|
|
387
|
+
if resp.status_code in (200, 400):
|
|
388
|
+
services[display_name] = "ok"
|
|
389
|
+
any_authorized = True
|
|
390
|
+
all_unauthorized = False
|
|
391
|
+
elif resp.status_code == 401:
|
|
392
|
+
services[display_name] = "unauthorized"
|
|
393
|
+
elif resp.status_code == 403:
|
|
394
|
+
services[display_name] = "unauthorized"
|
|
395
|
+
elif resp.status_code == 404:
|
|
396
|
+
services[display_name] = "not_found"
|
|
397
|
+
all_unauthorized = False
|
|
398
|
+
else:
|
|
399
|
+
services[display_name] = "error"
|
|
400
|
+
all_unauthorized = False
|
|
401
|
+
except requests.RequestException:
|
|
402
|
+
services[display_name] = "unreachable"
|
|
403
|
+
|
|
404
|
+
# Determine overall status
|
|
405
|
+
if not any_responded:
|
|
406
|
+
return {
|
|
407
|
+
"server_reachable": False,
|
|
408
|
+
"auth_valid": None,
|
|
409
|
+
"services": services,
|
|
410
|
+
"file_query_endpoint": None,
|
|
411
|
+
"elasticsearch_available": None,
|
|
412
|
+
"platform": PLATFORM_UNREACHABLE,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Determine auth status: valid if any service accepted the key
|
|
416
|
+
# If all responding services returned 401/403, the key is invalid
|
|
417
|
+
auth_valid = any_authorized if any_responded else None
|
|
418
|
+
if all_unauthorized and any_responded:
|
|
419
|
+
auth_valid = False
|
|
420
|
+
|
|
421
|
+
# Determine platform from service responses
|
|
422
|
+
workorder_status = services.get("Work Order")
|
|
423
|
+
if workorder_status in ("ok",):
|
|
424
|
+
platform = PLATFORM_SLE
|
|
425
|
+
elif workorder_status == "not_found":
|
|
426
|
+
platform = PLATFORM_SLS
|
|
427
|
+
else:
|
|
428
|
+
# Fall back to URL pattern matching
|
|
429
|
+
platform = _detect_platform_from_url(api_url)
|
|
430
|
+
|
|
431
|
+
file_capability = get_file_query_capability(api_url, api_key)
|
|
432
|
+
services["File"] = file_capability["status"]
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
"server_reachable": True,
|
|
436
|
+
"auth_valid": auth_valid,
|
|
437
|
+
"services": services,
|
|
438
|
+
"file_query_endpoint": file_capability["file_query_endpoint"],
|
|
439
|
+
"elasticsearch_available": file_capability["elasticsearch_available"],
|
|
440
|
+
"platform": platform,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
278
445
|
"""Get detailed information about the current platform configuration.
|
|
279
446
|
|
|
447
|
+
Args:
|
|
448
|
+
skip_health: If True, skip live service health checks.
|
|
449
|
+
|
|
280
450
|
Returns:
|
|
281
|
-
Dictionary with platform info including URL, platform type, and
|
|
451
|
+
Dictionary with platform info including URL, platform type, and services.
|
|
282
452
|
"""
|
|
283
453
|
from .utils import get_api_key, get_base_url, get_web_url
|
|
284
454
|
|
|
@@ -304,11 +474,28 @@ def get_platform_info() -> Dict[str, Any]:
|
|
|
304
474
|
|
|
305
475
|
active_profile = get_active_profile()
|
|
306
476
|
if active_profile and active_profile.platform:
|
|
307
|
-
|
|
477
|
+
stored_platform = active_profile.platform
|
|
308
478
|
else:
|
|
309
479
|
# Fall back to keyring config
|
|
310
480
|
cfg = _get_keyring_config()
|
|
311
|
-
|
|
481
|
+
stored_platform = cfg.get("platform", PLATFORM_UNKNOWN)
|
|
482
|
+
|
|
483
|
+
# Live service health check when logged in
|
|
484
|
+
server_reachable: Optional[bool] = None
|
|
485
|
+
auth_valid: Optional[bool] = None
|
|
486
|
+
services: Optional[Dict[str, str]] = None
|
|
487
|
+
platform = stored_platform
|
|
488
|
+
file_query_endpoint: Optional[str] = None
|
|
489
|
+
elasticsearch_available: Optional[bool] = None
|
|
490
|
+
|
|
491
|
+
if not skip_health and logged_in and isinstance(api_url, str) and api_url != "Not configured":
|
|
492
|
+
status = check_service_status(api_url, api_key)
|
|
493
|
+
server_reachable = status["server_reachable"]
|
|
494
|
+
auth_valid = status["auth_valid"]
|
|
495
|
+
services = status["services"]
|
|
496
|
+
platform = status["platform"]
|
|
497
|
+
file_query_endpoint = status.get("file_query_endpoint")
|
|
498
|
+
elasticsearch_available = status.get("elasticsearch_available")
|
|
312
499
|
|
|
313
500
|
info: Dict[str, Any] = {
|
|
314
501
|
"api_url": api_url,
|
|
@@ -316,14 +503,17 @@ def get_platform_info() -> Dict[str, Any]:
|
|
|
316
503
|
"platform": platform,
|
|
317
504
|
"platform_display": _get_platform_display_name(platform),
|
|
318
505
|
"logged_in": logged_in,
|
|
506
|
+
"server_reachable": server_reachable,
|
|
507
|
+
"auth_valid": auth_valid,
|
|
319
508
|
}
|
|
320
509
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
510
|
+
if file_query_endpoint is not None:
|
|
511
|
+
info["file_query_endpoint"] = file_query_endpoint
|
|
512
|
+
if elasticsearch_available is not None:
|
|
513
|
+
info["elasticsearch_available"] = elasticsearch_available
|
|
514
|
+
|
|
515
|
+
if services is not None:
|
|
516
|
+
info["services"] = services
|
|
327
517
|
|
|
328
518
|
return info
|
|
329
519
|
|
|
@@ -341,5 +531,6 @@ def _get_platform_display_name(platform: str) -> str:
|
|
|
341
531
|
PLATFORM_SLE: "SystemLink Enterprise",
|
|
342
532
|
PLATFORM_SLS: "SystemLink Server",
|
|
343
533
|
PLATFORM_UNKNOWN: "Unknown",
|
|
534
|
+
PLATFORM_UNREACHABLE: "Unreachable (could not connect to server)",
|
|
344
535
|
}
|
|
345
536
|
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]
|
|
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(
|
|
1719
|
-
click.echo("
|
|
1720
|
-
click.echo(
|
|
1721
|
-
click.echo(
|
|
1722
|
-
click.echo(f"
|
|
1723
|
-
click.echo(f"
|
|
1724
|
-
click.echo("
|
|
1725
|
-
click.echo(f"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/demo-complete-workflow/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/demo-complete-workflow/config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/exercise-7-1-test-plans/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/exercise-7-1-test-plans/config.yaml
RENAMED
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/examples/spec-compliance-notebooks/config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.4.1 → systemlink_cli-1.4.3}/slcli/skills/slcli/references/analysis-recipes.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|