systemlink-cli 1.3.1__py3-none-any.whl
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.
- slcli/__init__.py +1 -0
- slcli/__main__.py +23 -0
- slcli/_version.py +4 -0
- slcli/asset_click.py +1289 -0
- slcli/cli_formatters.py +218 -0
- slcli/cli_utils.py +504 -0
- slcli/comment_click.py +602 -0
- slcli/completion_click.py +418 -0
- slcli/config.py +81 -0
- slcli/config_click.py +498 -0
- slcli/dff_click.py +979 -0
- slcli/dff_decorators.py +24 -0
- slcli/example_click.py +404 -0
- slcli/example_loader.py +274 -0
- slcli/example_provisioner.py +2777 -0
- slcli/examples/README.md +134 -0
- slcli/examples/_schema/schema-v1.0.json +169 -0
- slcli/examples/demo-complete-workflow/README.md +323 -0
- slcli/examples/demo-complete-workflow/config.yaml +638 -0
- slcli/examples/demo-test-plans/README.md +132 -0
- slcli/examples/demo-test-plans/config.yaml +154 -0
- slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
- slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
- slcli/examples/exercise-7-1-test-plans/README.md +93 -0
- slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
- slcli/examples/spec-compliance-notebooks/README.md +140 -0
- slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
- slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- slcli/feed_click.py +892 -0
- slcli/file_click.py +932 -0
- slcli/function_click.py +1400 -0
- slcli/function_templates.py +85 -0
- slcli/main.py +406 -0
- slcli/mcp_click.py +269 -0
- slcli/mcp_server.py +748 -0
- slcli/notebook_click.py +1770 -0
- slcli/platform.py +345 -0
- slcli/policy_click.py +679 -0
- slcli/policy_utils.py +411 -0
- slcli/profiles.py +411 -0
- slcli/response_handlers.py +359 -0
- slcli/routine_click.py +763 -0
- slcli/skill_click.py +253 -0
- slcli/skills/slcli/SKILL.md +713 -0
- slcli/skills/slcli/references/analysis-recipes.md +474 -0
- slcli/skills/slcli/references/filtering.md +236 -0
- slcli/skills/systemlink-webapp/SKILL.md +744 -0
- slcli/skills/systemlink-webapp/references/deployment.md +123 -0
- slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
- slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
- slcli/ssl_trust.py +93 -0
- slcli/system_click.py +2216 -0
- slcli/table_utils.py +124 -0
- slcli/tag_click.py +794 -0
- slcli/templates_click.py +599 -0
- slcli/testmonitor_click.py +1667 -0
- slcli/universal_handlers.py +305 -0
- slcli/user_click.py +1218 -0
- slcli/utils.py +832 -0
- slcli/web_editor.py +295 -0
- slcli/webapp_click.py +981 -0
- slcli/workflow_preview.py +287 -0
- slcli/workflows_click.py +988 -0
- slcli/workitem_click.py +2258 -0
- slcli/workspace_click.py +576 -0
- slcli/workspace_utils.py +206 -0
- systemlink_cli-1.3.1.dist-info/METADATA +20 -0
- systemlink_cli-1.3.1.dist-info/RECORD +74 -0
- systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
- systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
- systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Helpers for initializing local function templates (TypeScript / Python).
|
|
2
|
+
|
|
3
|
+
This module encapsulates downloading and extracting subfolders from the
|
|
4
|
+
SystemLink Enterprise examples repository, adding safety checks against
|
|
5
|
+
path traversal, symlinks, and unexpected archive contents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import sys
|
|
12
|
+
import tarfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from .utils import ExitCodes
|
|
20
|
+
|
|
21
|
+
TEMPLATE_REPO = "ni/systemlink-enterprise-examples"
|
|
22
|
+
TEMPLATE_BRANCH = "function-examples" # Treated as stable per user direction
|
|
23
|
+
TEMPLATE_SUBFOLDERS: Dict[str, str] = {
|
|
24
|
+
"typescript": "function-examples/typescript-hono-function",
|
|
25
|
+
"python": "function-examples/python-http-function",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_DOWNLOAD_TIMEOUT_SECONDS = 60
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def download_and_extract_template(language: str, destination: Path) -> None:
|
|
32
|
+
"""Download and extract the specified language template into destination.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
language: Normalized language key ('typescript' or 'python').
|
|
36
|
+
destination: Directory to populate (must already exist).
|
|
37
|
+
"""
|
|
38
|
+
if language not in TEMPLATE_SUBFOLDERS:
|
|
39
|
+
click.echo(f"✗ Unsupported template language: {language}", err=True)
|
|
40
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
41
|
+
|
|
42
|
+
subfolder = TEMPLATE_SUBFOLDERS[language]
|
|
43
|
+
tarball_url = f"https://codeload.github.com/{TEMPLATE_REPO}/tar.gz/{TEMPLATE_BRANCH}"
|
|
44
|
+
resp = None
|
|
45
|
+
try:
|
|
46
|
+
resp = requests.get(tarball_url, timeout=_DOWNLOAD_TIMEOUT_SECONDS)
|
|
47
|
+
except requests.RequestException as exc: # noqa: BLE001
|
|
48
|
+
click.echo(f"✗ Network error downloading template: {exc}", err=True)
|
|
49
|
+
sys.exit(ExitCodes.NETWORK_ERROR)
|
|
50
|
+
if resp.status_code != 200:
|
|
51
|
+
click.echo(
|
|
52
|
+
f"✗ Failed to download template (HTTP {resp.status_code}) from {tarball_url}",
|
|
53
|
+
err=True,
|
|
54
|
+
)
|
|
55
|
+
sys.exit(ExitCodes.NETWORK_ERROR)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with tarfile.open(fileobj=io.BytesIO(resp.content), mode="r:gz") as tf: # type: ignore[arg-type]
|
|
59
|
+
for member in tf.getmembers():
|
|
60
|
+
# Skip symlinks / hard links for safety
|
|
61
|
+
if member.issym() or member.islnk(): # pragma: no cover - defensive
|
|
62
|
+
continue
|
|
63
|
+
parts = member.name.split("/", 1)
|
|
64
|
+
if len(parts) < 2:
|
|
65
|
+
continue
|
|
66
|
+
remainder = parts[1]
|
|
67
|
+
if not remainder.startswith(subfolder.rstrip("/")):
|
|
68
|
+
continue
|
|
69
|
+
# Compute relative path inside desired subfolder
|
|
70
|
+
relative_path = Path(remainder).relative_to(subfolder)
|
|
71
|
+
if any(p == ".." for p in relative_path.parts): # Path traversal guard
|
|
72
|
+
continue
|
|
73
|
+
target_path = destination / relative_path
|
|
74
|
+
if member.isdir():
|
|
75
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
continue
|
|
77
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
extracted = tf.extractfile(member)
|
|
79
|
+
if not extracted:
|
|
80
|
+
continue
|
|
81
|
+
with open(target_path, "wb") as f_out:
|
|
82
|
+
f_out.write(extracted.read())
|
|
83
|
+
except Exception as exc: # noqa: BLE001
|
|
84
|
+
click.echo(f"✗ Error extracting template: {exc}", err=True)
|
|
85
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
slcli/main.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""slcli entry points."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import keyring
|
|
9
|
+
import questionary
|
|
10
|
+
import tomllib
|
|
11
|
+
|
|
12
|
+
from .asset_click import register_asset_commands
|
|
13
|
+
from .comment_click import register_comment_commands
|
|
14
|
+
from .completion_click import register_completion_command
|
|
15
|
+
from .config_click import register_config_commands
|
|
16
|
+
from .dff_click import register_dff_commands
|
|
17
|
+
from .example_click import register_example_commands
|
|
18
|
+
from .feed_click import register_feed_commands
|
|
19
|
+
from .file_click import register_file_commands
|
|
20
|
+
from .function_click import register_function_commands
|
|
21
|
+
from .mcp_click import register_mcp_commands
|
|
22
|
+
from .notebook_click import register_notebook_commands
|
|
23
|
+
from .platform import (
|
|
24
|
+
PLATFORM_UNKNOWN,
|
|
25
|
+
get_platform_info,
|
|
26
|
+
)
|
|
27
|
+
from .policy_click import register_policy_commands
|
|
28
|
+
from .profiles import set_profile_override
|
|
29
|
+
from .routine_click import register_routine_commands
|
|
30
|
+
from .skill_click import register_skill_commands
|
|
31
|
+
from .ssl_trust import OS_TRUST_INJECTED, OS_TRUST_REASON
|
|
32
|
+
from .system_click import register_system_commands
|
|
33
|
+
from .tag_click import register_tag_commands
|
|
34
|
+
from .templates_click import register_templates_commands
|
|
35
|
+
from .testmonitor_click import register_testmonitor_commands
|
|
36
|
+
from .user_click import register_user_commands
|
|
37
|
+
from .webapp_click import register_webapp_commands
|
|
38
|
+
from .workitem_click import register_workitem_commands
|
|
39
|
+
from .workspace_click import register_workspace_commands
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_version() -> str:
|
|
43
|
+
"""Get version from _version.py (built binary) or pyproject.toml (development)."""
|
|
44
|
+
try:
|
|
45
|
+
# Try to import from _version.py first (works in built binary)
|
|
46
|
+
from ._version import __version__
|
|
47
|
+
|
|
48
|
+
return __version__
|
|
49
|
+
except ImportError:
|
|
50
|
+
# Fall back to reading pyproject.toml (works in development)
|
|
51
|
+
try:
|
|
52
|
+
current_dir = Path(__file__).parent
|
|
53
|
+
pyproject_path = current_dir.parent / "pyproject.toml"
|
|
54
|
+
|
|
55
|
+
with open(pyproject_path, "rb") as f:
|
|
56
|
+
pyproject_data = tomllib.load(f)
|
|
57
|
+
|
|
58
|
+
return pyproject_data["tool"]["poetry"]["version"]
|
|
59
|
+
except Exception:
|
|
60
|
+
return "unknown"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_ascii_art() -> str:
|
|
67
|
+
"""Return ASCII art for SystemLink CLI."""
|
|
68
|
+
return """
|
|
69
|
+
███████╗██╗ ██╗███████╗████████╗███████╗███╗ ███╗██╗ ██╗███╗ ██╗██╗ ██╗ ██████╗██╗ ██╗
|
|
70
|
+
██╔════╝╚██╗ ██╔╝██╔════╝╚══██╔══╝██╔════╝████╗ ████║██║ ██║████╗ ██║██║ ██╔╝ ██╔════╝██║ ██║
|
|
71
|
+
███████╗ ╚████╔╝ ███████╗ ██║ █████╗ ██╔████╔██║██║ ██║██╔██╗ ██║█████╔╝ ██║ ██║ ██║
|
|
72
|
+
╚════██║ ╚██╔╝ ╚════██║ ██║ ██╔══╝ ██║╚██╔╝██║██║ ██║██║╚██╗██║██╔═██╗ ██║ ██║ ██║
|
|
73
|
+
███████║ ██║ ███████║ ██║ ███████╗██║ ╚═╝ ██║███████╗██║██║ ╚████║██║ ██╗ ╚██████╗███████╗██║
|
|
74
|
+
╚══════╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
|
|
79
|
+
@click.option("--version", "-v", is_flag=True, help="Show version and exit")
|
|
80
|
+
@click.option(
|
|
81
|
+
"--profile",
|
|
82
|
+
"-p",
|
|
83
|
+
envvar="SLCLI_PROFILE",
|
|
84
|
+
help="Use a specific profile for this command",
|
|
85
|
+
)
|
|
86
|
+
@click.pass_context
|
|
87
|
+
def cli(ctx: click.Context, version: bool, profile: Optional[str]) -> None:
|
|
88
|
+
"""SystemLink CLI for managing SystemLink resources.""" # noqa: D403
|
|
89
|
+
if version:
|
|
90
|
+
click.echo(f"slcli version {get_version()}")
|
|
91
|
+
ctx.exit()
|
|
92
|
+
|
|
93
|
+
# Set profile override if specified (applies to all subcommands)
|
|
94
|
+
if profile:
|
|
95
|
+
set_profile_override(profile)
|
|
96
|
+
|
|
97
|
+
# Check for mandatory migration BEFORE any command runs
|
|
98
|
+
# Skip migration check only for version flag and config migrate command
|
|
99
|
+
if ctx.invoked_subcommand not in (None, "config"):
|
|
100
|
+
from .profiles import ProfileConfig, has_keyring_credentials, migrate_from_keyring
|
|
101
|
+
|
|
102
|
+
config_path = ProfileConfig.get_config_path()
|
|
103
|
+
if not config_path.exists() and has_keyring_credentials():
|
|
104
|
+
click.echo("⚠️ Migration Required")
|
|
105
|
+
click.echo("")
|
|
106
|
+
click.echo("slcli now uses profile-based configuration.")
|
|
107
|
+
click.echo("Existing keyring credentials detected and will be migrated to:")
|
|
108
|
+
click.echo(f" {config_path}")
|
|
109
|
+
click.echo("")
|
|
110
|
+
click.echo("Migrating credentials...")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
migrated_profile = migrate_from_keyring(profile_name="default", delete_keyring=True)
|
|
114
|
+
if migrated_profile:
|
|
115
|
+
click.echo(f"✓ Migrated credentials to profile 'default'")
|
|
116
|
+
click.echo(f" Server: {migrated_profile.server}")
|
|
117
|
+
if migrated_profile.web_url:
|
|
118
|
+
click.echo(f" Web URL: {migrated_profile.web_url}")
|
|
119
|
+
if migrated_profile.platform:
|
|
120
|
+
click.echo(f" Platform: {migrated_profile.platform}")
|
|
121
|
+
click.echo("✓ Deleted keyring entries")
|
|
122
|
+
click.echo("")
|
|
123
|
+
click.echo("Migration complete! Continuing with your command...")
|
|
124
|
+
click.echo("")
|
|
125
|
+
else:
|
|
126
|
+
click.echo(
|
|
127
|
+
"✗ Migration failed: No valid credentials found in keyring.", err=True
|
|
128
|
+
)
|
|
129
|
+
ctx.exit(1)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
click.echo(f"✗ Migration failed: {e}", err=True)
|
|
132
|
+
click.echo("Run 'slcli config migrate' to try again.", err=True)
|
|
133
|
+
ctx.exit(1)
|
|
134
|
+
|
|
135
|
+
if ctx.invoked_subcommand is None:
|
|
136
|
+
click.echo(get_ascii_art())
|
|
137
|
+
click.echo(ctx.get_help())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@cli.command(hidden=True, name="_ca-info")
|
|
141
|
+
def ca_info() -> None:
|
|
142
|
+
"""Show TLS CA trust source (hidden diagnostic)."""
|
|
143
|
+
if OS_TRUST_INJECTED:
|
|
144
|
+
click.echo(f"CA Source: system (reason={OS_TRUST_REASON})")
|
|
145
|
+
else:
|
|
146
|
+
# Determine if custom verify path set via env
|
|
147
|
+
import os
|
|
148
|
+
|
|
149
|
+
verify_env = os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE")
|
|
150
|
+
if verify_env:
|
|
151
|
+
click.echo(f"CA Source: custom-pem ({verify_env})")
|
|
152
|
+
else:
|
|
153
|
+
click.echo(f"CA Source: certifi (reason={OS_TRUST_REASON})")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@cli.command()
|
|
157
|
+
@click.option("--profile", "-p", help="Profile name (default: 'default')")
|
|
158
|
+
@click.option("--url", help="SystemLink API URL")
|
|
159
|
+
@click.option("--api-key", help="SystemLink API key")
|
|
160
|
+
@click.option("--web-url", help="SystemLink Web UI base URL")
|
|
161
|
+
@click.option("--workspace", "-w", help="Default workspace for this profile")
|
|
162
|
+
@click.option(
|
|
163
|
+
"--set-current/--no-set-current",
|
|
164
|
+
default=True,
|
|
165
|
+
help="Set as current profile (default: yes)",
|
|
166
|
+
)
|
|
167
|
+
@click.option(
|
|
168
|
+
"--readonly",
|
|
169
|
+
is_flag=True,
|
|
170
|
+
help=(
|
|
171
|
+
"Enable readonly mode (disables create, update, delete, import, upload, "
|
|
172
|
+
"publish, and disable commands)"
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
def login(
|
|
176
|
+
profile: Optional[str],
|
|
177
|
+
url: Optional[str],
|
|
178
|
+
api_key: Optional[str],
|
|
179
|
+
web_url: Optional[str],
|
|
180
|
+
workspace: Optional[str],
|
|
181
|
+
set_current: bool,
|
|
182
|
+
readonly: bool,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Save SystemLink credentials to a profile.
|
|
185
|
+
|
|
186
|
+
This is an alias for 'slcli config add'. Use that command
|
|
187
|
+
for the same functionality and more configuration options.
|
|
188
|
+
|
|
189
|
+
Profiles allow you to configure multiple SystemLink environments and switch
|
|
190
|
+
between them. Credentials are stored in ~/.config/slcli/config.json.
|
|
191
|
+
|
|
192
|
+
Examples:
|
|
193
|
+
slcli login --profile dev
|
|
194
|
+
slcli login -p prod --url https://prod-api.example.com
|
|
195
|
+
slcli login --profile test --workspace "Testing" --readonly
|
|
196
|
+
"""
|
|
197
|
+
from .config_click import _add_profile_impl
|
|
198
|
+
|
|
199
|
+
# Invoke the shared implementation
|
|
200
|
+
_add_profile_impl(
|
|
201
|
+
profile=profile,
|
|
202
|
+
url=url,
|
|
203
|
+
api_key=api_key,
|
|
204
|
+
web_url=web_url,
|
|
205
|
+
workspace=workspace,
|
|
206
|
+
set_current=set_current,
|
|
207
|
+
readonly=readonly,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@cli.command()
|
|
212
|
+
@click.option("--profile", "-p", help="Profile to remove (default: current profile)")
|
|
213
|
+
@click.option("--all", "remove_all", is_flag=True, help="Remove all profiles")
|
|
214
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
|
|
215
|
+
def logout(profile: Optional[str], remove_all: bool, force: bool) -> None:
|
|
216
|
+
"""Remove stored SystemLink credentials.
|
|
217
|
+
|
|
218
|
+
By default, removes the current profile. Use --profile to remove a specific
|
|
219
|
+
profile, or --all to remove all profiles.
|
|
220
|
+
|
|
221
|
+
Also cleans up any legacy keyring entries.
|
|
222
|
+
"""
|
|
223
|
+
from .profiles import ProfileConfig
|
|
224
|
+
|
|
225
|
+
cfg = ProfileConfig.load()
|
|
226
|
+
|
|
227
|
+
if remove_all:
|
|
228
|
+
if not force:
|
|
229
|
+
if not questionary.confirm(
|
|
230
|
+
"Remove all profiles and legacy keyring entries?",
|
|
231
|
+
default=False,
|
|
232
|
+
).ask():
|
|
233
|
+
click.echo("Aborted.")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# Clear all profiles
|
|
237
|
+
cfg.profiles.clear()
|
|
238
|
+
cfg.current_profile = None
|
|
239
|
+
cfg.save()
|
|
240
|
+
click.echo("✓ All profiles removed.")
|
|
241
|
+
|
|
242
|
+
elif profile:
|
|
243
|
+
# Remove specific profile
|
|
244
|
+
if profile not in cfg.profiles:
|
|
245
|
+
click.echo(f"✗ Profile '{profile}' not found.", err=True)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
if not force:
|
|
249
|
+
if not questionary.confirm(
|
|
250
|
+
f"Remove profile '{profile}'?",
|
|
251
|
+
default=False,
|
|
252
|
+
).ask():
|
|
253
|
+
click.echo("Aborted.")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
cfg.delete_profile(profile)
|
|
257
|
+
cfg.save()
|
|
258
|
+
click.echo(f"✓ Profile '{profile}' removed.")
|
|
259
|
+
|
|
260
|
+
else:
|
|
261
|
+
# Remove current profile
|
|
262
|
+
if not cfg.current_profile:
|
|
263
|
+
click.echo("No current profile set.", err=True)
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
current = cfg.current_profile
|
|
267
|
+
if not force:
|
|
268
|
+
if not questionary.confirm(
|
|
269
|
+
f"Remove current profile '{current}'?",
|
|
270
|
+
default=False,
|
|
271
|
+
).ask():
|
|
272
|
+
click.echo("Aborted.")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
cfg.delete_profile(current)
|
|
276
|
+
cfg.save()
|
|
277
|
+
click.echo(f"✓ Profile '{current}' removed.")
|
|
278
|
+
if cfg.current_profile:
|
|
279
|
+
click.echo(f" Current profile is now: {cfg.current_profile}")
|
|
280
|
+
|
|
281
|
+
# Also clean up legacy keyring entries
|
|
282
|
+
try:
|
|
283
|
+
keyring.delete_password("systemlink-cli", "SYSTEMLINK_API_KEY")
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
try:
|
|
287
|
+
keyring.delete_password("systemlink-cli", "SYSTEMLINK_API_URL")
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
try:
|
|
291
|
+
keyring.delete_password("systemlink-cli", "SYSTEMLINK_CONFIG")
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@cli.command()
|
|
297
|
+
@click.option("--format", "-f", type=click.Choice(["table", "json"]), default="table")
|
|
298
|
+
def info(format: str) -> None:
|
|
299
|
+
"""Show current configuration and detected platform."""
|
|
300
|
+
from .profiles import ProfileConfig, get_active_profile
|
|
301
|
+
|
|
302
|
+
platform_info = get_platform_info()
|
|
303
|
+
|
|
304
|
+
# Add profile information
|
|
305
|
+
cfg = ProfileConfig.load()
|
|
306
|
+
active_profile = get_active_profile()
|
|
307
|
+
platform_info["current_profile"] = cfg.current_profile
|
|
308
|
+
platform_info["profile_count"] = len(cfg.profiles)
|
|
309
|
+
if active_profile:
|
|
310
|
+
platform_info["active_profile_workspace"] = active_profile.workspace
|
|
311
|
+
platform_info["active_profile_name"] = active_profile.name
|
|
312
|
+
|
|
313
|
+
if format == "json":
|
|
314
|
+
click.echo(json.dumps(platform_info, indent=2))
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
# Table format using box-drawing characters for key-value display.
|
|
318
|
+
# Note: This uses a custom layout rather than table_utils because table_utils
|
|
319
|
+
# is designed for list-style output (multiple uniform rows), while this command
|
|
320
|
+
# displays a single record with key-value pairs and feature availability.
|
|
321
|
+
# All text fields are truncated to prevent formatting issues with long values.
|
|
322
|
+
max_value_width = 45 # Maximum width for values before truncation
|
|
323
|
+
content_width = 61 # Total width inside the box
|
|
324
|
+
|
|
325
|
+
def truncate(value: str, max_len: int = max_value_width) -> str:
|
|
326
|
+
"""Truncate a string with ellipsis if it exceeds max length."""
|
|
327
|
+
if len(value) > max_len:
|
|
328
|
+
return value[: max_len - 3] + "..."
|
|
329
|
+
return value
|
|
330
|
+
|
|
331
|
+
click.echo("\n┌" + "─" * content_width + "┐")
|
|
332
|
+
click.echo("│" + "SystemLink CLI Info".center(content_width) + "│")
|
|
333
|
+
click.echo("├" + "─" * content_width + "┤")
|
|
334
|
+
|
|
335
|
+
# Connection status
|
|
336
|
+
status = "✓ Connected" if platform_info["logged_in"] else "✗ Not logged in"
|
|
337
|
+
click.echo(f"│ Status: {status:<48}│")
|
|
338
|
+
|
|
339
|
+
# Profile information
|
|
340
|
+
profile_display = platform_info.get("active_profile_name", "None")
|
|
341
|
+
if platform_info.get("profile_count", 0) > 1:
|
|
342
|
+
profile_display = f"{profile_display} (1 of {platform_info['profile_count']})"
|
|
343
|
+
profile_display = truncate(profile_display)
|
|
344
|
+
click.echo(f"│ Profile: {profile_display:<48}│")
|
|
345
|
+
|
|
346
|
+
# Platform
|
|
347
|
+
platform_display = truncate(platform_info.get("platform_display", "Unknown"))
|
|
348
|
+
click.echo(f"│ Platform: {platform_display:<48}│")
|
|
349
|
+
|
|
350
|
+
# API URL
|
|
351
|
+
api_url = truncate(platform_info.get("api_url", "Not configured"))
|
|
352
|
+
click.echo(f"│ API URL: {api_url:<48}│")
|
|
353
|
+
|
|
354
|
+
# Web URL
|
|
355
|
+
web_url = truncate(platform_info.get("web_url", "Not configured"))
|
|
356
|
+
click.echo(f"│ Web URL: {web_url:<48}│")
|
|
357
|
+
|
|
358
|
+
# Default workspace
|
|
359
|
+
workspace = platform_info.get("active_profile_workspace")
|
|
360
|
+
if workspace:
|
|
361
|
+
workspace_display = truncate(workspace)
|
|
362
|
+
click.echo(f"│ Workspace: {workspace_display:<48}│")
|
|
363
|
+
|
|
364
|
+
click.echo("├" + "─" * content_width + "┤")
|
|
365
|
+
click.echo("│" + "Feature Availability".center(content_width) + "│")
|
|
366
|
+
click.echo("├" + "─" * content_width + "┤")
|
|
367
|
+
|
|
368
|
+
features = platform_info.get("features", {})
|
|
369
|
+
if features:
|
|
370
|
+
for feature_name, available in features.items():
|
|
371
|
+
status_icon = "✓" if available else "✗"
|
|
372
|
+
status_text = "Available" if available else "Not available"
|
|
373
|
+
# Truncate feature name if needed
|
|
374
|
+
display_name = truncate(feature_name, 29)
|
|
375
|
+
click.echo(f"│ {status_icon} {display_name:<30} {status_text:<26}│")
|
|
376
|
+
else:
|
|
377
|
+
if platform_info["platform"] == PLATFORM_UNKNOWN:
|
|
378
|
+
click.echo("│ Run 'slcli login' to detect platform features. │")
|
|
379
|
+
else:
|
|
380
|
+
click.echo("│ No feature information available. │")
|
|
381
|
+
|
|
382
|
+
click.echo("└" + "─" * content_width + "┘\n")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
register_completion_command(cli)
|
|
386
|
+
register_asset_commands(cli)
|
|
387
|
+
register_comment_commands(cli)
|
|
388
|
+
register_dff_commands(cli)
|
|
389
|
+
register_config_commands(cli)
|
|
390
|
+
register_example_commands(cli)
|
|
391
|
+
register_feed_commands(cli)
|
|
392
|
+
register_file_commands(cli)
|
|
393
|
+
register_function_commands(cli)
|
|
394
|
+
register_mcp_commands(cli)
|
|
395
|
+
register_templates_commands(cli)
|
|
396
|
+
register_notebook_commands(cli)
|
|
397
|
+
register_policy_commands(cli)
|
|
398
|
+
register_routine_commands(cli)
|
|
399
|
+
register_system_commands(cli)
|
|
400
|
+
register_tag_commands(cli)
|
|
401
|
+
register_testmonitor_commands(cli)
|
|
402
|
+
register_webapp_commands(cli)
|
|
403
|
+
register_skill_commands(cli)
|
|
404
|
+
register_user_commands(cli)
|
|
405
|
+
register_workitem_commands(cli)
|
|
406
|
+
register_workspace_commands(cli)
|