systemlink-cli 1.9.3__tar.gz → 1.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/PKG-INFO +1 -1
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/pyproject.toml +38 -31
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/_version.py +1 -1
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/main.py +7 -0
- systemlink_cli-1.11.0/slcli/mcp_reachability.py +64 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/mcp_server.py +44 -5
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/platform.py +104 -5
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skill_click.py +51 -21
- systemlink_cli-1.11.0/slcli/skills/nipkg-file-package/SKILL.md +541 -0
- systemlink_cli-1.11.0/slcli/skills/slcli/SKILL.md +180 -0
- systemlink_cli-1.9.3/slcli/skills/slcli/SKILL.md → systemlink_cli-1.11.0/slcli/skills/slcli/references/commands.md +54 -125
- systemlink_cli-1.11.0/slcli/skills/slcli/references/datasheet-workflow.md +510 -0
- systemlink_cli-1.11.0/slcli/skills/slcli/references/troubleshooting.md +45 -0
- systemlink_cli-1.11.0/slcli/skills/systemlink-job-debugging/SKILL.md +325 -0
- systemlink_cli-1.11.0/slcli/skills/systemlink-python-test/SKILL.md +547 -0
- systemlink_cli-1.11.0/slcli/spec_click.py +2141 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/system_click.py +424 -65
- systemlink_cli-1.11.0/slcli/system_query_utils.py +161 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/utils.py +4 -1
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/LICENSE +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/dff-editor/index.html +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/__init__.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/__main__.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/config.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/config_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/example_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-complete-workflow/README.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/file_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/function_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/profiles.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/rich_output.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-notebook/SKILL.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-notebook/references/interfaces.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-notebook/references/notebook-patterns.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/user_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/webapp_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.11.0"
|
|
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" }]
|
|
@@ -16,6 +16,7 @@ slcli = "slcli.__main__:cli"
|
|
|
16
16
|
slcli-mcp = "slcli.mcp_server:main"
|
|
17
17
|
build-pyinstaller = "scripts.build_pyinstaller:main"
|
|
18
18
|
update-version = "scripts.update_version:main"
|
|
19
|
+
release-from-changes = "scripts.towncrier_release:main"
|
|
19
20
|
lint = "scripts.lint:main"
|
|
20
21
|
|
|
21
22
|
[tool.poetry.group.mcp]
|
|
@@ -59,8 +60,8 @@ pyinstaller = "^6.14.2"
|
|
|
59
60
|
types-requests = "*"
|
|
60
61
|
types-tabulate = "*"
|
|
61
62
|
|
|
62
|
-
#
|
|
63
|
-
|
|
63
|
+
# Changelog and release management
|
|
64
|
+
towncrier = "^25.8.0"
|
|
64
65
|
pytest-xdist = "^3.8.0"
|
|
65
66
|
|
|
66
67
|
# Excel file generation
|
|
@@ -114,34 +115,40 @@ check_untyped_defs = true
|
|
|
114
115
|
warn_redundant_casts = true
|
|
115
116
|
warn_unreachable = true
|
|
116
117
|
|
|
117
|
-
[tool.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
118
|
+
[tool.towncrier]
|
|
119
|
+
package = "slcli"
|
|
120
|
+
package_dir = "."
|
|
121
|
+
directory = "newsfragments"
|
|
122
|
+
filename = "CHANGELOG.md"
|
|
123
|
+
template = "towncrier:default.md"
|
|
124
|
+
start_string = "<!-- towncrier release notes start -->\n"
|
|
125
|
+
title_format = "## v{version} ({project_date})"
|
|
126
|
+
ignore = ["README.md"]
|
|
127
|
+
|
|
128
|
+
[[tool.towncrier.type]]
|
|
129
|
+
directory = "major"
|
|
130
|
+
name = "Breaking Changes"
|
|
131
|
+
showcontent = true
|
|
132
|
+
|
|
133
|
+
[[tool.towncrier.type]]
|
|
134
|
+
directory = "minor"
|
|
135
|
+
name = "Features"
|
|
136
|
+
showcontent = true
|
|
137
|
+
|
|
138
|
+
[[tool.towncrier.type]]
|
|
139
|
+
directory = "patch"
|
|
140
|
+
name = "Bug Fixes"
|
|
141
|
+
showcontent = true
|
|
142
|
+
|
|
143
|
+
[[tool.towncrier.type]]
|
|
144
|
+
directory = "doc"
|
|
145
|
+
name = "Documentation"
|
|
146
|
+
showcontent = true
|
|
147
|
+
|
|
148
|
+
[[tool.towncrier.type]]
|
|
149
|
+
directory = "misc"
|
|
150
|
+
name = "Other Changes"
|
|
151
|
+
showcontent = true
|
|
145
152
|
|
|
146
153
|
[build-system]
|
|
147
154
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -30,6 +30,7 @@ from .rich_output import install_rich_output
|
|
|
30
30
|
from .rich_output import render_table
|
|
31
31
|
from .routine_click import register_routine_commands
|
|
32
32
|
from .skill_click import register_skill_commands
|
|
33
|
+
from .spec_click import register_spec_commands
|
|
33
34
|
from .ssl_trust import OS_TRUST_INJECTED, OS_TRUST_REASON
|
|
34
35
|
from .system_click import register_system_commands
|
|
35
36
|
from .tag_click import register_tag_commands
|
|
@@ -365,6 +366,11 @@ def info(format: str, skip_health: bool) -> None:
|
|
|
365
366
|
workspace_display = truncate(workspace)
|
|
366
367
|
info_rows.append(["Workspace", workspace_display])
|
|
367
368
|
|
|
369
|
+
system_query_endpoint = platform_info.get("system_query_endpoint")
|
|
370
|
+
if system_query_endpoint:
|
|
371
|
+
system_query_display = truncate(str(system_query_endpoint))
|
|
372
|
+
info_rows.append(["System Query", system_query_display])
|
|
373
|
+
|
|
368
374
|
file_query_endpoint = platform_info.get("file_query_endpoint")
|
|
369
375
|
if file_query_endpoint:
|
|
370
376
|
if (
|
|
@@ -427,6 +433,7 @@ register_notebook_commands(cli)
|
|
|
427
433
|
register_policy_commands(cli)
|
|
428
434
|
register_routine_commands(cli)
|
|
429
435
|
register_system_commands(cli)
|
|
436
|
+
register_spec_commands(cli)
|
|
430
437
|
register_tag_commands(cli)
|
|
431
438
|
register_testmonitor_commands(cli)
|
|
432
439
|
register_webapp_commands(cli)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Helpers for classifying local MCP client connectivity failures."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterator, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def iter_exceptions(exc: BaseException) -> Iterator[BaseException]:
|
|
7
|
+
"""Yield an exception plus any nested causes, contexts, or exception-group members."""
|
|
8
|
+
stack: List[BaseException] = [exc]
|
|
9
|
+
seen: set[int] = set()
|
|
10
|
+
|
|
11
|
+
while stack:
|
|
12
|
+
current = stack.pop()
|
|
13
|
+
marker = id(current)
|
|
14
|
+
if marker in seen:
|
|
15
|
+
continue
|
|
16
|
+
seen.add(marker)
|
|
17
|
+
yield current
|
|
18
|
+
|
|
19
|
+
cause = getattr(current, "__cause__", None)
|
|
20
|
+
if isinstance(cause, BaseException):
|
|
21
|
+
stack.append(cause)
|
|
22
|
+
|
|
23
|
+
context = getattr(current, "__context__", None)
|
|
24
|
+
if isinstance(context, BaseException):
|
|
25
|
+
stack.append(context)
|
|
26
|
+
|
|
27
|
+
if isinstance(current, BaseExceptionGroup):
|
|
28
|
+
stack.extend(current.exceptions)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_reachability_failure(exc: BaseException) -> bool:
|
|
32
|
+
"""Return True when an exception indicates the local MCP server is unreachable."""
|
|
33
|
+
for candidate in iter_exceptions(exc):
|
|
34
|
+
if isinstance(candidate, (OSError, TimeoutError)):
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
message = str(candidate).lower()
|
|
38
|
+
if "/mcp" in message and any(
|
|
39
|
+
token in message
|
|
40
|
+
for token in (
|
|
41
|
+
"server error",
|
|
42
|
+
"not found",
|
|
43
|
+
"method not allowed",
|
|
44
|
+
"bad request",
|
|
45
|
+
)
|
|
46
|
+
):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
if any(
|
|
50
|
+
token in message
|
|
51
|
+
for token in (
|
|
52
|
+
"connection refused",
|
|
53
|
+
"connect error",
|
|
54
|
+
"all connection attempts failed",
|
|
55
|
+
"timed out",
|
|
56
|
+
"timeout",
|
|
57
|
+
"name or service not known",
|
|
58
|
+
"nodename nor servname provided",
|
|
59
|
+
"server disconnected",
|
|
60
|
+
)
|
|
61
|
+
):
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
return False
|
|
@@ -271,7 +271,41 @@ def query_systems(
|
|
|
271
271
|
take: int = 100,
|
|
272
272
|
) -> str:
|
|
273
273
|
"""Query systems by alias, connection state, workspace, or raw filter."""
|
|
274
|
-
|
|
274
|
+
import requests as requests_lib
|
|
275
|
+
|
|
276
|
+
from .system_query_utils import (
|
|
277
|
+
MATERIALIZED_SYSTEM_MCP_PROJECTION,
|
|
278
|
+
build_materialized_system_search_filter,
|
|
279
|
+
get_system_query_url,
|
|
280
|
+
get_system_search_url,
|
|
281
|
+
)
|
|
282
|
+
from .utils import make_api_request
|
|
283
|
+
|
|
284
|
+
materialized_filter = None
|
|
285
|
+
if filter is None:
|
|
286
|
+
materialized_filter = build_materialized_system_search_filter(
|
|
287
|
+
alias=alias,
|
|
288
|
+
state=state,
|
|
289
|
+
workspace_id=workspace,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if filter is None:
|
|
293
|
+
search_payload: Dict[str, Any] = {
|
|
294
|
+
"take": take,
|
|
295
|
+
"projection": MATERIALIZED_SYSTEM_MCP_PROJECTION,
|
|
296
|
+
}
|
|
297
|
+
if materialized_filter:
|
|
298
|
+
search_payload["filter"] = materialized_filter
|
|
299
|
+
try:
|
|
300
|
+
data = make_api_request(
|
|
301
|
+
"POST",
|
|
302
|
+
get_system_search_url(),
|
|
303
|
+
payload=search_payload,
|
|
304
|
+
handle_errors=False,
|
|
305
|
+
).json()
|
|
306
|
+
return _dump(_normalize_systems(data))
|
|
307
|
+
except requests_lib.RequestException:
|
|
308
|
+
pass
|
|
275
309
|
|
|
276
310
|
filter_parts: List[str] = []
|
|
277
311
|
if alias:
|
|
@@ -283,11 +317,16 @@ def query_systems(
|
|
|
283
317
|
if filter:
|
|
284
318
|
filter_parts.append(filter)
|
|
285
319
|
|
|
286
|
-
|
|
320
|
+
query_payload: Dict[str, Any] = {"take": take}
|
|
287
321
|
if filter_parts:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
data =
|
|
322
|
+
query_payload["filter"] = " and ".join(filter_parts)
|
|
323
|
+
|
|
324
|
+
data = make_api_request(
|
|
325
|
+
"POST",
|
|
326
|
+
get_system_query_url(),
|
|
327
|
+
payload=query_payload,
|
|
328
|
+
handle_errors=False,
|
|
329
|
+
).json()
|
|
291
330
|
return _dump(_normalize_systems(data))
|
|
292
331
|
|
|
293
332
|
|
|
@@ -59,6 +59,8 @@ FEATURE_DISPLAY_NAMES: Dict[str, str] = {
|
|
|
59
59
|
FILE_SEARCH_PATH = "/nifile/v1/service-groups/Default/search-files"
|
|
60
60
|
FILE_QUERY_PATH = "/nifile/v1/service-groups/Default/query-files"
|
|
61
61
|
FILE_QUERY_LINQ_PATH = "/nifile/v1/service-groups/Default/query-files-linq"
|
|
62
|
+
SYSTEM_SEARCH_PATH = "/nisysmgmt/v1/materialized/search-systems"
|
|
63
|
+
SYSTEM_QUERY_PATH = "/nisysmgmt/v1/query-systems"
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def _get_keyring_config() -> Dict[str, Any]:
|
|
@@ -195,7 +197,7 @@ SERVICE_CHECKS: List[List[str]] = [
|
|
|
195
197
|
["Auth", "GET", "/niauth/v1/policies"],
|
|
196
198
|
["Test Monitor", "GET", "/nitestmonitor/v2/results?take=0"],
|
|
197
199
|
["Asset Management", "POST", "/niapm/v1/query-assets"],
|
|
198
|
-
["Systems", "POST",
|
|
200
|
+
["Systems", "POST", SYSTEM_QUERY_PATH],
|
|
199
201
|
["Tag", "GET", "/nitag/v2/tags?take=0"],
|
|
200
202
|
["File", "POST", FILE_SEARCH_PATH],
|
|
201
203
|
["Notebook", "POST", "/ninotebook/v1/notebook/query"],
|
|
@@ -319,6 +321,87 @@ def get_file_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
319
321
|
}
|
|
320
322
|
|
|
321
323
|
|
|
324
|
+
def get_system_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
325
|
+
"""Determine which systems query endpoint is available for this server."""
|
|
326
|
+
headers = {
|
|
327
|
+
"x-ni-api-key": api_key,
|
|
328
|
+
"Content-Type": "application/json",
|
|
329
|
+
"User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
|
|
330
|
+
}
|
|
331
|
+
ssl_verify = get_ssl_verify()
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
search_resp = requests.post(
|
|
335
|
+
f"{api_url}{SYSTEM_SEARCH_PATH}",
|
|
336
|
+
headers=headers,
|
|
337
|
+
json={"take": 1, "projection": ["id"]},
|
|
338
|
+
verify=ssl_verify,
|
|
339
|
+
timeout=10,
|
|
340
|
+
)
|
|
341
|
+
if search_resp.status_code in (200, 400):
|
|
342
|
+
return {
|
|
343
|
+
"status": "ok",
|
|
344
|
+
"system_query_endpoint": "search-systems",
|
|
345
|
+
"materialized_search_available": True,
|
|
346
|
+
}
|
|
347
|
+
if search_resp.status_code in (401, 403):
|
|
348
|
+
return {
|
|
349
|
+
"status": "unauthorized",
|
|
350
|
+
"system_query_endpoint": "search-systems",
|
|
351
|
+
"materialized_search_available": True,
|
|
352
|
+
}
|
|
353
|
+
if search_resp.status_code not in (404, 501):
|
|
354
|
+
return {
|
|
355
|
+
"status": "error" if search_resp.status_code >= 500 else "not_found",
|
|
356
|
+
"system_query_endpoint": None,
|
|
357
|
+
"materialized_search_available": True,
|
|
358
|
+
}
|
|
359
|
+
except requests.RequestException:
|
|
360
|
+
return {
|
|
361
|
+
"status": "unreachable",
|
|
362
|
+
"system_query_endpoint": None,
|
|
363
|
+
"materialized_search_available": None,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
query_resp = requests.post(
|
|
368
|
+
f"{api_url}{SYSTEM_QUERY_PATH}",
|
|
369
|
+
headers=headers,
|
|
370
|
+
json={"take": 1, "projection": "new(id)"},
|
|
371
|
+
verify=ssl_verify,
|
|
372
|
+
timeout=10,
|
|
373
|
+
)
|
|
374
|
+
if query_resp.status_code in (200, 400):
|
|
375
|
+
return {
|
|
376
|
+
"status": "ok",
|
|
377
|
+
"system_query_endpoint": "query-systems",
|
|
378
|
+
"materialized_search_available": False,
|
|
379
|
+
}
|
|
380
|
+
if query_resp.status_code in (401, 403):
|
|
381
|
+
return {
|
|
382
|
+
"status": "unauthorized",
|
|
383
|
+
"system_query_endpoint": "query-systems",
|
|
384
|
+
"materialized_search_available": False,
|
|
385
|
+
}
|
|
386
|
+
if query_resp.status_code in (404, 501):
|
|
387
|
+
return {
|
|
388
|
+
"status": "not_found",
|
|
389
|
+
"system_query_endpoint": None,
|
|
390
|
+
"materialized_search_available": False,
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
"status": "error",
|
|
394
|
+
"system_query_endpoint": None,
|
|
395
|
+
"materialized_search_available": False,
|
|
396
|
+
}
|
|
397
|
+
except requests.RequestException:
|
|
398
|
+
return {
|
|
399
|
+
"status": "unreachable",
|
|
400
|
+
"system_query_endpoint": None,
|
|
401
|
+
"materialized_search_available": False,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
322
405
|
def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
323
406
|
"""Probe key SystemLink services and report their status.
|
|
324
407
|
|
|
@@ -334,10 +417,12 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
334
417
|
- auth_valid: bool | None - whether the API key is authorized (None if unreachable)
|
|
335
418
|
- services: dict mapping service name to status string
|
|
336
419
|
("ok", "unauthorized", "not_found", "error", "unreachable")
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
420
|
+
- file_query_endpoint: selected file query endpoint, if available
|
|
421
|
+
- elasticsearch_available: bool | None - whether search-files is available
|
|
422
|
+
- system_query_endpoint: selected systems query endpoint, if available
|
|
423
|
+
- materialized_search_available: bool | None - whether search-systems is available
|
|
424
|
+
- platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
|
|
425
|
+
PLATFORM_UNREACHABLE, PLATFORM_UNKNOWN)
|
|
341
426
|
"""
|
|
342
427
|
headers = {
|
|
343
428
|
"x-ni-api-key": api_key,
|
|
@@ -396,6 +481,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
396
481
|
"services": services,
|
|
397
482
|
"file_query_endpoint": None,
|
|
398
483
|
"elasticsearch_available": None,
|
|
484
|
+
"system_query_endpoint": None,
|
|
485
|
+
"materialized_search_available": None,
|
|
399
486
|
"platform": PLATFORM_UNREACHABLE,
|
|
400
487
|
}
|
|
401
488
|
|
|
@@ -416,6 +503,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
416
503
|
|
|
417
504
|
file_capability = get_file_query_capability(api_url, api_key)
|
|
418
505
|
services["File"] = file_capability["status"]
|
|
506
|
+
system_capability = get_system_query_capability(api_url, api_key)
|
|
507
|
+
services["Systems"] = system_capability["status"]
|
|
419
508
|
|
|
420
509
|
return {
|
|
421
510
|
"server_reachable": True,
|
|
@@ -423,6 +512,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
423
512
|
"services": services,
|
|
424
513
|
"file_query_endpoint": file_capability["file_query_endpoint"],
|
|
425
514
|
"elasticsearch_available": file_capability["elasticsearch_available"],
|
|
515
|
+
"system_query_endpoint": system_capability["system_query_endpoint"],
|
|
516
|
+
"materialized_search_available": system_capability["materialized_search_available"],
|
|
426
517
|
"platform": platform,
|
|
427
518
|
}
|
|
428
519
|
|
|
@@ -473,6 +564,8 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
|
473
564
|
platform = stored_platform
|
|
474
565
|
file_query_endpoint: Optional[str] = None
|
|
475
566
|
elasticsearch_available: Optional[bool] = None
|
|
567
|
+
system_query_endpoint: Optional[str] = None
|
|
568
|
+
materialized_search_available: Optional[bool] = None
|
|
476
569
|
|
|
477
570
|
if not skip_health and logged_in and isinstance(api_url, str) and api_url != "Not configured":
|
|
478
571
|
status = check_service_status(api_url, api_key)
|
|
@@ -482,6 +575,8 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
|
482
575
|
platform = status["platform"]
|
|
483
576
|
file_query_endpoint = status.get("file_query_endpoint")
|
|
484
577
|
elasticsearch_available = status.get("elasticsearch_available")
|
|
578
|
+
system_query_endpoint = status.get("system_query_endpoint")
|
|
579
|
+
materialized_search_available = status.get("materialized_search_available")
|
|
485
580
|
|
|
486
581
|
info: Dict[str, Any] = {
|
|
487
582
|
"api_url": api_url,
|
|
@@ -497,6 +592,10 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
|
497
592
|
info["file_query_endpoint"] = file_query_endpoint
|
|
498
593
|
if elasticsearch_available is not None:
|
|
499
594
|
info["elasticsearch_available"] = elasticsearch_available
|
|
595
|
+
if system_query_endpoint is not None:
|
|
596
|
+
info["system_query_endpoint"] = system_query_endpoint
|
|
597
|
+
if materialized_search_available is not None:
|
|
598
|
+
info["materialized_search_available"] = materialized_search_available
|
|
500
599
|
|
|
501
600
|
if services is not None:
|
|
502
601
|
info["services"] = services
|
|
@@ -11,7 +11,14 @@ import questionary
|
|
|
11
11
|
from .utils import ExitCodes
|
|
12
12
|
|
|
13
13
|
SKILL_NAME = "slcli"
|
|
14
|
-
|
|
14
|
+
_FALLBACK_SKILL_CHOICES = [
|
|
15
|
+
"nipkg-file-package",
|
|
16
|
+
"slcli",
|
|
17
|
+
"systemlink-job-debugging",
|
|
18
|
+
"systemlink-notebook",
|
|
19
|
+
"systemlink-python-test",
|
|
20
|
+
"systemlink-webapp",
|
|
21
|
+
]
|
|
15
22
|
|
|
16
23
|
# Mapping of client name -> (personal skills dir, project subdir relative to repo root)
|
|
17
24
|
# personal dir uses Path.home() so it's always resolved at call time via _personal_dir().
|
|
@@ -23,6 +30,45 @@ _CLIENT_TABLE: Dict[str, Tuple[str, str]] = {
|
|
|
23
30
|
CLIENT_CHOICES = list(_CLIENT_TABLE.keys())
|
|
24
31
|
|
|
25
32
|
|
|
33
|
+
def _skills_dir_candidates() -> List[Path]:
|
|
34
|
+
"""Return candidate bundled skills directories for source and frozen layouts."""
|
|
35
|
+
candidates: List[Path] = []
|
|
36
|
+
|
|
37
|
+
meipass = getattr(sys, "_MEIPASS", None)
|
|
38
|
+
if meipass:
|
|
39
|
+
candidates.append(Path(meipass) / "skills")
|
|
40
|
+
|
|
41
|
+
if getattr(sys, "frozen", False):
|
|
42
|
+
candidates.append(Path(sys.executable).resolve().parent / "skills")
|
|
43
|
+
|
|
44
|
+
candidates.append(Path(__file__).resolve().parent / "skills")
|
|
45
|
+
return candidates
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _skill_names_in_directory(directory: Path) -> List[str]:
|
|
49
|
+
"""Return sorted bundled skill names for a skills directory."""
|
|
50
|
+
if not directory.exists():
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
return sorted(
|
|
54
|
+
child.name
|
|
55
|
+
for child in directory.iterdir()
|
|
56
|
+
if child.is_dir() and (child / "SKILL.md").exists()
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _discover_skill_choices() -> List[str]:
|
|
61
|
+
"""Discover bundled skills from the current installation layout."""
|
|
62
|
+
for candidate in _skills_dir_candidates():
|
|
63
|
+
names = _skill_names_in_directory(candidate)
|
|
64
|
+
if names:
|
|
65
|
+
return names
|
|
66
|
+
return list(_FALLBACK_SKILL_CHOICES)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
SKILL_CHOICES = _discover_skill_choices()
|
|
70
|
+
|
|
71
|
+
|
|
26
72
|
def _personal_dir(client: str) -> Path:
|
|
27
73
|
"""Return the resolved personal skills directory for a client."""
|
|
28
74
|
return Path(_CLIENT_TABLE[client][0]).expanduser()
|
|
@@ -57,24 +103,8 @@ def _find_bundled_skills_dir() -> Path:
|
|
|
57
103
|
Raises:
|
|
58
104
|
FileNotFoundError: If the skills directory cannot be found.
|
|
59
105
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# PyInstaller onefile mode
|
|
63
|
-
meipass = getattr(sys, "_MEIPASS", None)
|
|
64
|
-
if meipass:
|
|
65
|
-
candidates.append(Path(meipass) / "skills")
|
|
66
|
-
|
|
67
|
-
# PyInstaller onedir / frozen executable
|
|
68
|
-
if getattr(sys, "frozen", False):
|
|
69
|
-
candidates.append(Path(sys.executable).resolve().parent / "skills")
|
|
70
|
-
|
|
71
|
-
# pip install / development: skills/ bundled inside the slcli package
|
|
72
|
-
candidates.append(Path(__file__).resolve().parent / "skills")
|
|
73
|
-
|
|
74
|
-
for candidate in candidates:
|
|
75
|
-
if candidate.exists() and any(
|
|
76
|
-
(candidate / name / "SKILL.md").exists() for name in SKILL_CHOICES
|
|
77
|
-
):
|
|
106
|
+
for candidate in _skills_dir_candidates():
|
|
107
|
+
if _skill_names_in_directory(candidate):
|
|
78
108
|
return candidate
|
|
79
109
|
|
|
80
110
|
raise FileNotFoundError(
|
|
@@ -178,7 +208,7 @@ def register_skill_commands(cli: Any) -> None:
|
|
|
178
208
|
"-k",
|
|
179
209
|
type=click.Choice(SKILL_CHOICES + ["all"], case_sensitive=False),
|
|
180
210
|
default=None,
|
|
181
|
-
help="Skill to install (
|
|
211
|
+
help=f"Skill to install ({', '.join(SKILL_CHOICES)}, or all).",
|
|
182
212
|
)
|
|
183
213
|
@click.option(
|
|
184
214
|
"--client",
|
|
@@ -207,7 +237,7 @@ def register_skill_commands(cli: Any) -> None:
|
|
|
207
237
|
"""Install agent skills for AI coding assistants.
|
|
208
238
|
|
|
209
239
|
Copies bundled skills into the skills directory of one or more AI clients.
|
|
210
|
-
Available skills
|
|
240
|
+
Available skills are shown in `--help` and prompted interactively.
|
|
211
241
|
Supported clients and their skill locations:
|
|
212
242
|
|
|
213
243
|
\b
|