systemlink-cli 1.17.4__tar.gz → 1.19.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.
Files changed (147) hide show
  1. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/PKG-INFO +1 -1
  2. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/pyproject.toml +1 -1
  3. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/config_click.py +200 -3
  5. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/main.py +23 -4
  6. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/mcp_server.py +19 -4
  7. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/platform.py +28 -4
  8. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/commands.md +1 -0
  9. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/systemlink-services.md +40 -37
  10. systemlink_cli-1.19.0/slcli/ssl_trust.py +376 -0
  11. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/tag_click.py +246 -0
  12. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/utils.py +72 -38
  13. systemlink_cli-1.17.4/slcli/ssl_trust.py +0 -93
  14. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/LICENSE +0 -0
  15. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/dff-editor/editor.js +0 -0
  16. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/dff-editor/index.html +0 -0
  17. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/__init__.py +0 -0
  18. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/__main__.py +0 -0
  19. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/asset_click.py +0 -0
  20. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/cli_formatters.py +0 -0
  21. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/cli_utils.py +0 -0
  22. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/comment_click.py +0 -0
  23. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/completion_click.py +0 -0
  24. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/config.py +0 -0
  25. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/dataframe_click.py +0 -0
  26. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/dff_click.py +0 -0
  27. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/dff_decorators.py +0 -0
  28. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/example_click.py +0 -0
  29. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/example_loader.py +0 -0
  30. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/example_provisioner.py +0 -0
  31. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/README.md +0 -0
  32. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/_schema/schema-v1.0.json +0 -0
  33. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/demo-complete-workflow/README.md +0 -0
  34. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  35. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/demo-test-plans/README.md +0 -0
  36. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/demo-test-plans/config.yaml +0 -0
  37. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  38. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  39. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  40. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  41. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  42. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  43. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  44. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  45. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  46. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  47. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/feed_click.py +0 -0
  48. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/file_click.py +0 -0
  49. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/function_click.py +0 -0
  50. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/function_templates.py +0 -0
  51. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/mcp_click.py +0 -0
  52. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/mcp_reachability.py +0 -0
  53. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/notebook_click.py +0 -0
  54. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/policy_click.py +0 -0
  55. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/policy_utils.py +0 -0
  56. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/profiles.py +0 -0
  57. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/response_handlers.py +0 -0
  58. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/rich_output.py +0 -0
  59. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/routine_click.py +0 -0
  60. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skill_click.py +0 -0
  61. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/SKILL.md +0 -0
  62. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  63. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/datasheet-workflow.md +0 -0
  64. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/filtering.md +0 -0
  65. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/import-specs.min.json +0 -0
  66. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/job-debugging/overview.md +0 -0
  67. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/nipkg/overview.md +0 -0
  68. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/notebook/interfaces.md +0 -0
  69. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/notebook/notebook-patterns.md +0 -0
  70. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/notebook/overview.md +0 -0
  71. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/python-test/overview.md +0 -0
  72. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/routine-examples.md +0 -0
  73. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/troubleshooting.md +0 -0
  74. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/angular-ui-packages.md +0 -0
  75. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/deployment.md +0 -0
  76. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/layout-patterns.md +0 -0
  77. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/nimble-angular.md +0 -0
  78. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/overview.md +0 -0
  79. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/references/webapp/troubleshooting.md +0 -0
  80. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/skills/slcli/scripts/spec_import_helper.py +0 -0
  81. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/spec_click.py +0 -0
  82. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/state_click.py +0 -0
  83. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/system_click.py +0 -0
  84. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/system_query_utils.py +0 -0
  85. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/table_utils.py +0 -0
  86. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/templates_click.py +0 -0
  87. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/testmonitor_click.py +0 -0
  88. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/universal_handlers.py +0 -0
  89. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/user_click.py +0 -0
  90. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/web_editor.py +0 -0
  91. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_bootstrap.py +0 -0
  92. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_click.py +0 -0
  93. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/.gitignore +0 -0
  94. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/README.md +0 -0
  95. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/angular.json +0 -0
  96. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/package.json +0 -0
  97. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/app-routing.module.ts +0 -0
  98. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/app.component.html +0 -0
  99. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/app.component.scss +0 -0
  100. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/app.component.ts +0 -0
  101. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/app.module.ts +0 -0
  102. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/layout/app-shell.component.html +0 -0
  103. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/layout/app-shell.component.scss +0 -0
  104. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/layout/app-shell.component.ts +0 -0
  105. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/state/app-view-state.service.ts +0 -0
  106. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/systemlink/systemlink-context.service.ts +0 -0
  107. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/systemlink/theme-sync.service.ts +0 -0
  108. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/core/systemlink/webapp-home-data.service.ts +0 -0
  109. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/assets/assets-page.component.html +0 -0
  110. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/assets/assets-page.component.scss +0 -0
  111. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/assets/assets-page.component.ts +0 -0
  112. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/datasets/datasets-page.component.html +0 -0
  113. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/datasets/datasets-page.component.scss +0 -0
  114. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/datasets/datasets-page.component.ts +0 -0
  115. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/home/home-page.component.html +0 -0
  116. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/home/home-page.component.scss +0 -0
  117. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/home/home-page.component.ts +0 -0
  118. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/master-detail/master-detail-page.component.html +0 -0
  119. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/master-detail/master-detail-page.component.scss +0 -0
  120. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/master-detail/master-detail-page.component.ts +0 -0
  121. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/operations/operations-page.component.html +0 -0
  122. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/operations/operations-page.component.scss +0 -0
  123. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/operations/operations-page.component.ts +0 -0
  124. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/settings/settings-page.component.html +0 -0
  125. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/settings/settings-page.component.scss +0 -0
  126. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/features/settings/settings-page.component.ts +0 -0
  127. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/empty-state.component.html +0 -0
  128. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/empty-state.component.scss +0 -0
  129. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/empty-state.component.ts +0 -0
  130. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/error-banner.component.html +0 -0
  131. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/error-banner.component.scss +0 -0
  132. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/error-banner.component.ts +0 -0
  133. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/loading-state.component.html +0 -0
  134. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/loading-state.component.scss +0 -0
  135. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/components/loading-state.component.ts +0 -0
  136. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/app/shared/states/view-state.model.ts +0 -0
  137. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/assets/README.md +0 -0
  138. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/index.html +0 -0
  139. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/main.ts +0 -0
  140. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/src/styles.scss +0 -0
  141. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/tsconfig.app.json +0 -0
  142. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/webapp_templates/angular/blank/tsconfig.json +0 -0
  143. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/workflow_preview.py +0 -0
  144. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/workflows_click.py +0 -0
  145. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/workitem_click.py +0 -0
  146. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/workspace_click.py +0 -0
  147. {systemlink_cli-1.17.4 → systemlink_cli-1.19.0}/slcli/workspace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemlink-cli
3
- Version: 1.17.4
3
+ Version: 1.19.0
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.17.4"
3
+ version = "1.19.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" }]
@@ -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.17.4"
4
+ __version__ = "1.19.0"
@@ -4,8 +4,9 @@ import getpass
4
4
  import json
5
5
  import os
6
6
  import re
7
+ import ssl
7
8
  import sys
8
- from typing import Any, Optional
9
+ from typing import Any, NoReturn, Optional
9
10
  from urllib.parse import urlparse
10
11
 
11
12
  import click
@@ -18,8 +19,15 @@ from .platform import (
18
19
  )
19
20
  from .profiles import ProfileConfig, Profile, check_config_file_permissions
20
21
  from .rich_output import render_table
22
+ from .ssl_trust import (
23
+ get_managed_trust_records,
24
+ get_ssl_server_origin,
25
+ inspect_server_certificate,
26
+ remove_managed_trust,
27
+ save_managed_certificate,
28
+ )
21
29
  from .table_utils import output_formatted_list
22
- from .utils import ExitCodes
30
+ from .utils import ExitCodes, get_base_url
23
31
 
24
32
  API_KEY_LENGTH = 42
25
33
  API_KEY_PATTERN = re.compile(rf"^[A-Za-z0-9_-]{{{API_KEY_LENGTH}}}$")
@@ -30,7 +38,7 @@ ENV_OVERRIDE_FIELDS = (
30
38
  )
31
39
 
32
40
 
33
- def _exit_with_validation_error(message: str, exit_code: int = ExitCodes.INVALID_INPUT) -> None:
41
+ def _exit_with_validation_error(message: str, exit_code: int = ExitCodes.INVALID_INPUT) -> NoReturn:
34
42
  """Exit the command with a consistent validation message."""
35
43
  click.echo(f"✗ {message}", err=True)
36
44
  sys.exit(exit_code)
@@ -100,6 +108,91 @@ def _all_service_probes_unauthorized(services: dict[str, str]) -> bool:
100
108
  return bool(services) and all(status == "unauthorized" for status in services.values())
101
109
 
102
110
 
111
+ def _normalize_fingerprint(fingerprint: str) -> str:
112
+ """Normalize a SHA-256 certificate fingerprint supplied by a user."""
113
+ normalized = fingerprint.replace(":", "").replace(" ", "").strip().upper()
114
+ if len(normalized) != 64 or any(
115
+ character not in "0123456789ABCDEF" for character in normalized
116
+ ):
117
+ _exit_with_validation_error("Certificate fingerprint must be a 64-character SHA-256 value.")
118
+ return normalized
119
+
120
+
121
+ def _show_certificate_warning(certificate: dict[str, Any]) -> None:
122
+ """Display the identity of a certificate before it is trusted."""
123
+ click.echo("\n⚠️ TLS certificate verification failed.", err=True)
124
+ click.echo(f" Server: {certificate.get('origin', 'unknown')}", err=True)
125
+ click.echo(f" Subject: {certificate.get('subject', 'unknown')}", err=True)
126
+ click.echo(f" Issuer: {certificate.get('issuer', 'unknown')}", err=True)
127
+ click.echo(f" SHA-256: {certificate.get('fingerprint', 'unknown')}", err=True)
128
+ click.echo(f" Self-signed: {'yes' if certificate.get('self-signed') else 'no'}", err=True)
129
+ click.echo(
130
+ f" Valid: {certificate.get('not-before', 'unknown')} to "
131
+ f"{certificate.get('not-after', 'unknown')}",
132
+ err=True,
133
+ )
134
+
135
+
136
+ def _trust_certificate_if_requested(
137
+ url: str, api_key: str, status: dict[str, Any], trust_fingerprint: Optional[str]
138
+ ) -> dict[str, Any]:
139
+ """Prompt for or apply certificate trust after a verification failure."""
140
+ certificate_details = status.get("certificate")
141
+ if not isinstance(certificate_details, dict):
142
+ _exit_with_validation_error(
143
+ "TLS certificate verification failed, but the server certificate could not be "
144
+ "inspected. Profile was not saved.",
145
+ ExitCodes.NETWORK_ERROR,
146
+ )
147
+
148
+ _show_certificate_warning(certificate_details)
149
+ expected_fingerprint = _normalize_fingerprint(trust_fingerprint) if trust_fingerprint else None
150
+ actual_fingerprint = str(certificate_details.get("fingerprint", "")).upper()
151
+ if expected_fingerprint:
152
+ if expected_fingerprint != actual_fingerprint:
153
+ _exit_with_validation_error(
154
+ "The server certificate fingerprint does not match --trust-fingerprint. "
155
+ "Profile was not saved.",
156
+ ExitCodes.INVALID_INPUT,
157
+ )
158
+ elif not click.confirm("Trust this certificate for this server?", default=False):
159
+ _exit_with_validation_error("Certificate was not trusted. Profile was not saved.")
160
+
161
+ try:
162
+ certificate = inspect_server_certificate(url)
163
+ except (OSError, ValueError, ssl.SSLError) as exc:
164
+ _exit_with_validation_error(
165
+ f"Could not inspect the server certificate: {exc}. Profile was not saved.",
166
+ ExitCodes.NETWORK_ERROR,
167
+ )
168
+
169
+ if certificate.fingerprint != actual_fingerprint:
170
+ _exit_with_validation_error(
171
+ "The server certificate changed during approval. Profile was not saved.",
172
+ ExitCodes.NETWORK_ERROR,
173
+ )
174
+
175
+ try:
176
+ save_managed_certificate(certificate)
177
+ retry_status = check_service_status(url, api_key)
178
+ except (OSError, ValueError) as exc:
179
+ _exit_with_validation_error(
180
+ f"Could not save the trusted certificate: {exc}. Profile was not saved.",
181
+ ExitCodes.GENERAL_ERROR,
182
+ )
183
+
184
+ if not retry_status.get("server_reachable") or retry_status.get("certificate_error"):
185
+ remove_managed_trust(url)
186
+ _exit_with_validation_error(
187
+ "The server could not be verified after trusting its certificate. "
188
+ "Profile was not saved.",
189
+ ExitCodes.NETWORK_ERROR,
190
+ )
191
+
192
+ click.echo(" Certificate: ✓ Trusted for this server")
193
+ return retry_status
194
+
195
+
103
196
  def _add_profile_impl(
104
197
  profile: Optional[str],
105
198
  url: Optional[str],
@@ -108,6 +201,7 @@ def _add_profile_impl(
108
201
  workspace: Optional[str],
109
202
  set_current: bool,
110
203
  readonly: bool,
204
+ trust_fingerprint: Optional[str] = None,
111
205
  ) -> None:
112
206
  """Shared implementation for add-profile and login commands.
113
207
 
@@ -122,6 +216,7 @@ def _add_profile_impl(
122
216
  workspace: Default workspace for this profile
123
217
  set_current: Whether to set as the current profile
124
218
  readonly: Whether to enable readonly mode
219
+ trust_fingerprint: Optional SHA-256 fingerprint for non-interactive trust approval
125
220
  """
126
221
  # Get profile name
127
222
  if not profile:
@@ -152,6 +247,10 @@ def _add_profile_impl(
152
247
  # Detect platform type and check service status
153
248
  click.echo("Checking server connectivity and services...")
154
249
  status = check_service_status(url, api_key)
250
+
251
+ if status.get("certificate_error") and not status.get("server_reachable"):
252
+ status = _trust_certificate_if_requested(url, api_key, status, trust_fingerprint)
253
+
155
254
  platform = status["platform"]
156
255
  services = status.get("services", {})
157
256
 
@@ -458,6 +557,98 @@ def register_config_commands(cli: Any) -> None:
458
557
  if warning:
459
558
  click.echo(f"\n⚠️ {warning}", err=True)
460
559
 
560
+ @config.group(name="trust")
561
+ def trust() -> None:
562
+ """Manage explicitly trusted server certificates."""
563
+ pass
564
+
565
+ @trust.command(name="list")
566
+ @click.option(
567
+ "--format",
568
+ "output_format",
569
+ type=click.Choice(["table", "json"]),
570
+ default="table",
571
+ help="Output format",
572
+ )
573
+ def list_trusted_certificates(output_format: str) -> None:
574
+ """List certificates trusted by slcli."""
575
+ records = get_managed_trust_records()
576
+ if output_format == "json":
577
+ click.echo(json.dumps(records, indent=2))
578
+ return
579
+ if not records:
580
+ click.echo("No managed server certificates.")
581
+ return
582
+
583
+ rows = [
584
+ [
585
+ str(record.get("origin", "")),
586
+ str(record.get("fingerprint", "")),
587
+ "yes" if record.get("self-signed") else "no",
588
+ ]
589
+ for record in records
590
+ ]
591
+ render_table(
592
+ headers=["SERVER", "SHA-256", "SELF-SIGNED"],
593
+ column_widths=[40, 64, 12],
594
+ rows=rows,
595
+ show_total=True,
596
+ total_label="certificate(s)",
597
+ )
598
+
599
+ @trust.command(name="add")
600
+ @click.option("--url", help="HTTPS server URL (defaults to the active API URL)")
601
+ @click.option(
602
+ "--fingerprint",
603
+ required=True,
604
+ help="Expected SHA-256 fingerprint of the certificate to trust",
605
+ )
606
+ def add_trusted_certificate(url: Optional[str], fingerprint: str) -> None:
607
+ """Trust a server certificate after verifying its fingerprint."""
608
+ server_url = url or get_base_url()
609
+ try:
610
+ expected_fingerprint = _normalize_fingerprint(fingerprint)
611
+ certificate = inspect_server_certificate(server_url)
612
+ except (OSError, ValueError, ssl.SSLError) as exc:
613
+ _exit_with_validation_error(f"Could not inspect the server certificate: {exc}.")
614
+
615
+ _show_certificate_warning(certificate.to_dict())
616
+ if certificate.fingerprint != expected_fingerprint:
617
+ _exit_with_validation_error(
618
+ "The server certificate fingerprint does not match the supplied fingerprint."
619
+ )
620
+ try:
621
+ path = save_managed_certificate(certificate)
622
+ except OSError as exc:
623
+ _exit_with_validation_error(f"Could not save the trusted certificate: {exc}.")
624
+ click.echo(f"✓ Trusted certificate for {certificate.origin}")
625
+ click.echo(f" Certificate file: {path}")
626
+
627
+ @trust.command(name="remove")
628
+ @click.option("--url", help="HTTPS server URL (defaults to the active API URL)")
629
+ @click.option("--force", is_flag=True, help="Skip confirmation prompt")
630
+ def remove_trusted_certificate(url: Optional[str], force: bool) -> None:
631
+ """Remove a server certificate from the managed trust store."""
632
+ server_url = url or get_base_url()
633
+ try:
634
+ origin = get_ssl_server_origin(server_url)
635
+ except ValueError as exc:
636
+ _exit_with_validation_error(str(exc))
637
+ if not force and not click.confirm(
638
+ f"Remove trusted certificate for {origin}?", default=False
639
+ ):
640
+ click.echo("Certificate was not removed.")
641
+ return
642
+ try:
643
+ removed = remove_managed_trust(server_url)
644
+ except (OSError, ValueError) as exc:
645
+ _exit_with_validation_error(f"Could not remove the trusted certificate: {exc}.")
646
+ if not removed:
647
+ _exit_with_validation_error(
648
+ f"No trusted certificate found for {origin}.", ExitCodes.NOT_FOUND
649
+ )
650
+ click.echo(f"✓ Removed trusted certificate for {origin}")
651
+
461
652
  @config.command(name="delete")
462
653
  @click.argument("name")
463
654
  @click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
@@ -558,6 +749,10 @@ def register_config_commands(cli: Any) -> None:
558
749
  "publish, and disable commands)"
559
750
  ),
560
751
  )
752
+ @click.option(
753
+ "--trust-fingerprint",
754
+ help="Trust a certificate after its SHA-256 fingerprint matches exactly",
755
+ )
561
756
  def add_profile(
562
757
  profile: Optional[str],
563
758
  url: Optional[str],
@@ -566,6 +761,7 @@ def register_config_commands(cli: Any) -> None:
566
761
  workspace: Optional[str],
567
762
  set_current: bool,
568
763
  readonly: bool,
764
+ trust_fingerprint: Optional[str],
569
765
  ) -> None:
570
766
  """Add or update a SystemLink profile.
571
767
 
@@ -588,4 +784,5 @@ def register_config_commands(cli: Any) -> None:
588
784
  workspace=workspace,
589
785
  set_current=set_current,
590
786
  readonly=readonly,
787
+ trust_fingerprint=trust_fingerprint,
591
788
  )
@@ -8,7 +8,7 @@ import ssl
8
8
  import tomllib
9
9
  from pathlib import Path
10
10
  from types import ModuleType
11
- from typing import List, Optional, Tuple
11
+ from typing import List, Optional, Tuple, Union
12
12
  from urllib.parse import urlparse
13
13
 
14
14
  import click as base_click
@@ -103,6 +103,16 @@ def _configure_rich_click_command_groups() -> None:
103
103
 
104
104
  def _get_ca_source_display() -> str:
105
105
  """Describe the CA source used for HTTPS verification."""
106
+ try:
107
+ from .ssl_trust import get_managed_trust_path
108
+ from .utils import get_base_url
109
+
110
+ managed_path = get_managed_trust_path(get_base_url())
111
+ if managed_path is not None:
112
+ return f"managed-pem ({managed_path})"
113
+ except (OSError, ValueError):
114
+ pass
115
+
106
116
  if OS_TRUST_INJECTED:
107
117
  return f"system (reason={OS_TRUST_REASON})"
108
118
 
@@ -113,7 +123,7 @@ def _get_ca_source_display() -> str:
113
123
  return f"certifi (reason={OS_TRUST_REASON})"
114
124
 
115
125
 
116
- def _build_tls_debug_context(ssl_verify: bool) -> ssl.SSLContext:
126
+ def _build_tls_debug_context(ssl_verify: Union[bool, str]) -> ssl.SSLContext:
117
127
  """Build an SSL context aligned with the current CLI trust configuration."""
118
128
  if not ssl_verify:
119
129
  ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -121,6 +131,9 @@ def _build_tls_debug_context(ssl_verify: bool) -> ssl.SSLContext:
121
131
  ctx.verify_mode = ssl.CERT_NONE
122
132
  return ctx
123
133
 
134
+ if isinstance(ssl_verify, str):
135
+ return ssl.create_default_context(cafile=ssl_verify)
136
+
124
137
  verify_env = os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE")
125
138
  if verify_env:
126
139
  return ssl.create_default_context(cafile=verify_env)
@@ -204,7 +217,7 @@ def _get_proxy_debug_rows(api_url: str) -> List[Tuple[str, str]]:
204
217
  ]
205
218
 
206
219
 
207
- def _probe_tls_connection(api_url: str, ssl_verify: bool) -> List[Tuple[str, str]]:
220
+ def _probe_tls_connection(api_url: str, ssl_verify: Union[bool, str]) -> List[Tuple[str, str]]:
208
221
  """Collect TLS handshake and leaf certificate details for the API host."""
209
222
  import requests
210
223
 
@@ -268,7 +281,7 @@ def _collect_info_debug_rows(api_url: str) -> List[Tuple[str, str]]:
268
281
  """Return structured connection diagnostics for info --debug."""
269
282
  from .utils import get_ssl_verify
270
283
 
271
- ssl_verify = get_ssl_verify()
284
+ ssl_verify = get_ssl_verify(api_url)
272
285
  rows = [
273
286
  ("SSL Verify", "enabled" if ssl_verify else "disabled"),
274
287
  ("CA Source", _get_ca_source_display()),
@@ -414,6 +427,10 @@ def ca_info() -> None:
414
427
  "publish, and disable commands)"
415
428
  ),
416
429
  )
430
+ @click.option(
431
+ "--trust-fingerprint",
432
+ help="Trust a certificate after its SHA-256 fingerprint matches exactly",
433
+ )
417
434
  def login(
418
435
  profile: Optional[str],
419
436
  url: Optional[str],
@@ -422,6 +439,7 @@ def login(
422
439
  workspace: Optional[str],
423
440
  set_current: bool,
424
441
  readonly: bool,
442
+ trust_fingerprint: Optional[str],
425
443
  ) -> None:
426
444
  """Create or update a SystemLink profile with credentials.
427
445
 
@@ -447,6 +465,7 @@ def login(
447
465
  workspace=workspace,
448
466
  set_current=set_current,
449
467
  readonly=readonly,
468
+ trust_fingerprint=trust_fingerprint,
450
469
  )
451
470
 
452
471
 
@@ -254,12 +254,27 @@ def query_tag_history(path: str, take: int = 100) -> str:
254
254
  from .utils import get_base_url
255
255
 
256
256
  path = _require(path, "path")
257
- encoded_path = urllib.parse.quote(path, safe="")
258
- url = f"{get_base_url()}/nitag/v2/tags/{encoded_path}/values/history?take={take}"
259
- data = _get_json(url)
257
+ url = f"{get_base_url()}/nitaghistorian/v2/tags/query-history"
258
+ data = _post_json(
259
+ url,
260
+ {
261
+ "path": path,
262
+ "startTime": "0001-01-01T00:00:00Z",
263
+ "endTime": "9999-12-31T23:59:59Z",
264
+ "take": take,
265
+ "sortOrder": "DESCENDING",
266
+ },
267
+ )
260
268
  if isinstance(data, list):
261
269
  return _dump(data)
262
- return _dump(data.get("values", data.get("tagsWithAggregates", [])))
270
+ values = data.get("values", [])
271
+ value_type = data.get("type")
272
+ if isinstance(values, list) and isinstance(value_type, str):
273
+ values = [
274
+ {**item, "type": value_type} if isinstance(item, dict) and "type" not in item else item
275
+ for item in values
276
+ ]
277
+ return _dump(values)
263
278
 
264
279
 
265
280
  @server.tool()
@@ -6,6 +6,7 @@ This module provides utilities to detect and manage the target platform
6
6
 
7
7
  import json
8
8
  import os
9
+ import ssl
9
10
  import sys
10
11
  import time
11
12
  from functools import lru_cache
@@ -15,6 +16,7 @@ import click
15
16
  import keyring
16
17
  import requests
17
18
 
19
+ from .ssl_trust import use_standard_ssl_context
18
20
  from .utils import ExitCodes, get_ssl_verify
19
21
 
20
22
  DEFAULT_SERVICE_PROBE_CACHE_TTL_SECONDS = 300
@@ -206,7 +208,7 @@ def _probe_service_status(api_url: str, api_key: str, method: str, url_path: str
206
208
  "Content-Type": "application/json",
207
209
  "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
208
210
  }
209
- ssl_verify = get_ssl_verify()
211
+ ssl_verify = get_ssl_verify(api_url)
210
212
 
211
213
  try:
212
214
  full_url = f"{api_url}{url_path}"
@@ -469,7 +471,7 @@ def get_file_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
469
471
  "Content-Type": "application/json",
470
472
  "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
471
473
  }
472
- ssl_verify = get_ssl_verify()
474
+ ssl_verify = get_ssl_verify(api_url)
473
475
 
474
476
  try:
475
477
  search_resp = requests.post(
@@ -583,7 +585,7 @@ def get_system_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
583
585
  "Content-Type": "application/json",
584
586
  "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
585
587
  }
586
- ssl_verify = get_ssl_verify()
588
+ ssl_verify = get_ssl_verify(api_url)
587
589
 
588
590
  try:
589
591
  search_resp = requests.post(
@@ -679,17 +681,25 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
679
681
  - platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
680
682
  PLATFORM_UNREACHABLE, PLATFORM_UNKNOWN)
681
683
  """
684
+ ssl_verify = get_ssl_verify(api_url)
685
+ with use_standard_ssl_context(ssl_verify):
686
+ return _check_service_status(api_url, api_key)
687
+
688
+
689
+ def _check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
690
+ """Probe services while the caller has selected the appropriate SSL context."""
682
691
  headers = {
683
692
  "x-ni-api-key": api_key,
684
693
  "Content-Type": "application/json",
685
694
  "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
686
695
  }
687
- ssl_verify = get_ssl_verify()
696
+ ssl_verify = get_ssl_verify(api_url)
688
697
 
689
698
  services: Dict[str, str] = {}
690
699
  any_responded = False
691
700
  any_authorized = False
692
701
  all_unauthorized = True
702
+ certificate_error = False
693
703
 
694
704
  for display_name, method, url_path in SERVICE_CHECKS:
695
705
  try:
@@ -725,15 +735,28 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
725
735
  else:
726
736
  services[display_name] = "error"
727
737
  all_unauthorized = False
738
+ except requests.exceptions.SSLError:
739
+ services[display_name] = "certificate_error"
740
+ certificate_error = True
728
741
  except requests.RequestException:
729
742
  services[display_name] = "unreachable"
730
743
 
731
744
  # Determine overall status
732
745
  if not any_responded:
746
+ certificate_details: Optional[Dict[str, Any]] = None
747
+ if certificate_error:
748
+ try:
749
+ from .ssl_trust import inspect_server_certificate
750
+
751
+ certificate_details = inspect_server_certificate(api_url).to_dict()
752
+ except (OSError, ValueError, ssl.SSLError):
753
+ certificate_details = None
733
754
  return {
734
755
  "server_reachable": False,
735
756
  "auth_valid": None,
736
757
  "services": services,
758
+ "certificate_error": certificate_error,
759
+ "certificate": certificate_details,
737
760
  "file_query_endpoint": None,
738
761
  "elasticsearch_available": None,
739
762
  "system_query_endpoint": None,
@@ -759,6 +782,7 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
759
782
  "server_reachable": True,
760
783
  "auth_valid": auth_valid,
761
784
  "services": services,
785
+ "certificate_error": certificate_error,
762
786
  "file_query_endpoint": file_capability["file_query_endpoint"],
763
787
  "elasticsearch_available": file_capability["elasticsearch_available"],
764
788
  "system_query_endpoint": system_capability["system_query_endpoint"],
@@ -378,6 +378,7 @@ slcli dataframe delete <TABLE_ID>... [--yes]
378
378
  slcli tag list [OPTIONS] # List tags (filter by path glob, workspace)
379
379
  slcli tag get <TAG_PATH> [-f json] # Get tag metadata
380
380
  slcli tag get-value <TAG_PATH> # Read current tag value
381
+ slcli tag history <TAG_PATH> [-w WORKSPACE] [-t TAKE] [-f json] [--graph] # Read or graph history
381
382
  slcli tag set-value <TAG_PATH> <VALUE> # Write a tag value
382
383
  slcli tag create --path <PATH> --data-type <TYPE> # Create a new tag
383
384
  slcli tag update <TAG_PATH> [OPTIONS] # Update tag metadata