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
slcli/config_click.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""CLI commands for managing slcli configuration and profiles."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import questionary
|
|
10
|
+
|
|
11
|
+
from .platform import PLATFORM_SLE, PLATFORM_SLS, detect_platform
|
|
12
|
+
from .profiles import ProfileConfig, Profile, check_config_file_permissions
|
|
13
|
+
from .table_utils import output_formatted_list
|
|
14
|
+
from .utils import ExitCodes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _add_profile_impl(
|
|
18
|
+
profile: Optional[str],
|
|
19
|
+
url: Optional[str],
|
|
20
|
+
api_key: Optional[str],
|
|
21
|
+
web_url: Optional[str],
|
|
22
|
+
workspace: Optional[str],
|
|
23
|
+
set_current: bool,
|
|
24
|
+
readonly: bool,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Shared implementation for add-profile and login commands.
|
|
27
|
+
|
|
28
|
+
This function contains the common logic for both the config add-profile
|
|
29
|
+
and login commands. Both commands invoke this function with the same parameters.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
profile: Profile name (default: 'default')
|
|
33
|
+
url: SystemLink API URL
|
|
34
|
+
api_key: SystemLink API key
|
|
35
|
+
web_url: SystemLink Web UI base URL
|
|
36
|
+
workspace: Default workspace for this profile
|
|
37
|
+
set_current: Whether to set as the current profile
|
|
38
|
+
readonly: Whether to enable readonly mode
|
|
39
|
+
"""
|
|
40
|
+
# Get profile name
|
|
41
|
+
if not profile:
|
|
42
|
+
profile = click.prompt("Profile name", default="default")
|
|
43
|
+
assert isinstance(profile, str)
|
|
44
|
+
|
|
45
|
+
# Get URL - either from flag or prompt
|
|
46
|
+
if not url:
|
|
47
|
+
click.echo("Example: https://api.my-systemlink.com")
|
|
48
|
+
url = click.prompt("Enter your SystemLink API URL")
|
|
49
|
+
# Ensure url is a string now
|
|
50
|
+
assert isinstance(url, str)
|
|
51
|
+
if not url.strip():
|
|
52
|
+
click.echo("SystemLink URL cannot be empty.")
|
|
53
|
+
raise click.ClickException("SystemLink URL cannot be empty.")
|
|
54
|
+
|
|
55
|
+
# Ensure URL uses HTTPS
|
|
56
|
+
url = url.strip()
|
|
57
|
+
if url.startswith("http://"):
|
|
58
|
+
click.echo("⚠️ Warning: Converting HTTP to HTTPS for security.")
|
|
59
|
+
url = url.replace("http://", "https://", 1)
|
|
60
|
+
elif not url.startswith("https://"):
|
|
61
|
+
click.echo("⚠️ Warning: Adding HTTPS protocol to URL.")
|
|
62
|
+
url = f"https://{url}"
|
|
63
|
+
|
|
64
|
+
# Get API key - either from flag or prompt
|
|
65
|
+
if not api_key:
|
|
66
|
+
api_key = getpass.getpass("Enter your SystemLink API key: ")
|
|
67
|
+
# Ensure api_key is a string now
|
|
68
|
+
assert isinstance(api_key, str)
|
|
69
|
+
if not api_key.strip():
|
|
70
|
+
click.echo("API key cannot be empty.")
|
|
71
|
+
raise click.ClickException("API key cannot be empty.")
|
|
72
|
+
|
|
73
|
+
# Normalize and validate web_url (prompt if not provided)
|
|
74
|
+
if not web_url:
|
|
75
|
+
click.echo("Example: https://my-systemlink.com")
|
|
76
|
+
web_url = click.prompt("Enter your SystemLink Web UI URL")
|
|
77
|
+
assert isinstance(web_url, str)
|
|
78
|
+
web_url = web_url.strip()
|
|
79
|
+
if web_url.startswith("http://"):
|
|
80
|
+
click.echo("⚠️ Warning: Converting HTTP to HTTPS for security.")
|
|
81
|
+
web_url = web_url.replace("http://", "https://", 1)
|
|
82
|
+
elif not web_url.startswith("https://"):
|
|
83
|
+
click.echo("⚠️ Warning: Adding HTTPS protocol to web URL.")
|
|
84
|
+
web_url = f"https://{web_url}"
|
|
85
|
+
|
|
86
|
+
# Detect platform type
|
|
87
|
+
click.echo("Detecting platform type...")
|
|
88
|
+
platform = detect_platform(url, api_key.strip())
|
|
89
|
+
|
|
90
|
+
if platform == PLATFORM_SLE:
|
|
91
|
+
click.echo(" Platform: SystemLink Enterprise (Cloud)")
|
|
92
|
+
elif platform == PLATFORM_SLS:
|
|
93
|
+
click.echo(" Platform: SystemLink Server (On-Premises)")
|
|
94
|
+
else:
|
|
95
|
+
click.echo(" Platform: Unknown (will attempt all features)")
|
|
96
|
+
|
|
97
|
+
# Get default workspace (optional)
|
|
98
|
+
if workspace is None:
|
|
99
|
+
workspace_input = click.prompt(
|
|
100
|
+
"Default workspace (optional, press Enter to skip)", default="", show_default=False
|
|
101
|
+
)
|
|
102
|
+
workspace = workspace_input if workspace_input else None
|
|
103
|
+
|
|
104
|
+
# Create profile
|
|
105
|
+
new_profile = Profile(
|
|
106
|
+
name=profile,
|
|
107
|
+
server=url,
|
|
108
|
+
api_key=api_key.strip(),
|
|
109
|
+
web_url=web_url,
|
|
110
|
+
platform=platform,
|
|
111
|
+
workspace=workspace,
|
|
112
|
+
readonly=readonly,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Load config and add profile
|
|
116
|
+
cfg = ProfileConfig.load()
|
|
117
|
+
cfg.add_profile(new_profile, set_current=set_current)
|
|
118
|
+
cfg.save()
|
|
119
|
+
|
|
120
|
+
click.echo(f"\n✓ Profile '{profile}' saved successfully.")
|
|
121
|
+
click.echo(f" Server: {url}")
|
|
122
|
+
click.echo(f" Web URL: {web_url}")
|
|
123
|
+
if workspace:
|
|
124
|
+
click.echo(f" Default workspace: {workspace}")
|
|
125
|
+
if readonly:
|
|
126
|
+
click.echo(f" Readonly mode: enabled (mutation operations disabled)")
|
|
127
|
+
if set_current:
|
|
128
|
+
click.echo(f" Set as current profile: yes")
|
|
129
|
+
click.echo(f"\nConfig file: {ProfileConfig.get_config_path()}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def register_config_commands(cli: Any) -> None:
|
|
133
|
+
"""Register the 'config' command group and its subcommands."""
|
|
134
|
+
|
|
135
|
+
@cli.group()
|
|
136
|
+
def config() -> None:
|
|
137
|
+
"""Manage slcli configuration and profiles.
|
|
138
|
+
|
|
139
|
+
Profiles allow you to configure multiple SystemLink environments
|
|
140
|
+
(dev, test, prod) and switch between them easily.
|
|
141
|
+
"""
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
@config.command(name="list")
|
|
145
|
+
@click.option(
|
|
146
|
+
"--format",
|
|
147
|
+
"-f",
|
|
148
|
+
type=click.Choice(["table", "json"]),
|
|
149
|
+
default="table",
|
|
150
|
+
help="Output format",
|
|
151
|
+
)
|
|
152
|
+
@click.option(
|
|
153
|
+
"--take",
|
|
154
|
+
"-t",
|
|
155
|
+
type=int,
|
|
156
|
+
default=25,
|
|
157
|
+
show_default=True,
|
|
158
|
+
help="Maximum number of profiles to display per page",
|
|
159
|
+
)
|
|
160
|
+
def list_profiles(format: str, take: int) -> None:
|
|
161
|
+
"""List all configured profiles."""
|
|
162
|
+
cfg = ProfileConfig.load()
|
|
163
|
+
profiles = cfg.list_profiles()
|
|
164
|
+
|
|
165
|
+
if format == "json":
|
|
166
|
+
output = []
|
|
167
|
+
for p in profiles:
|
|
168
|
+
item = {
|
|
169
|
+
"name": p.name,
|
|
170
|
+
"server": p.server,
|
|
171
|
+
"current": p.name == cfg.current_profile,
|
|
172
|
+
}
|
|
173
|
+
if p.web_url:
|
|
174
|
+
item["web-url"] = p.web_url
|
|
175
|
+
if p.platform:
|
|
176
|
+
item["platform"] = p.platform
|
|
177
|
+
if p.workspace:
|
|
178
|
+
item["workspace"] = p.workspace
|
|
179
|
+
if p.readonly:
|
|
180
|
+
item["readonly"] = p.readonly
|
|
181
|
+
output.append(item)
|
|
182
|
+
click.echo(json.dumps(output, indent=2))
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if not profiles:
|
|
186
|
+
click.echo("No profiles configured.")
|
|
187
|
+
click.echo("\nRun 'slcli login --profile <name>' to create a profile.")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Check for permission warning
|
|
191
|
+
warning = check_config_file_permissions()
|
|
192
|
+
if warning:
|
|
193
|
+
click.echo(f"⚠️ {warning}\n", err=True)
|
|
194
|
+
|
|
195
|
+
# Convert Profile objects to dictionaries for type compatibility
|
|
196
|
+
from typing import Any, Dict, List
|
|
197
|
+
|
|
198
|
+
table_items: List[Dict[str, Any]] = []
|
|
199
|
+
for p in profiles:
|
|
200
|
+
table_items.append(
|
|
201
|
+
{
|
|
202
|
+
"name": p.name,
|
|
203
|
+
"server": p.server,
|
|
204
|
+
"workspace": p.workspace,
|
|
205
|
+
"readonly": p.readonly,
|
|
206
|
+
"is_current": p.name == cfg.current_profile,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def format_row(profile_dict: Dict[str, Any]) -> List[str]:
|
|
211
|
+
current = "*" if profile_dict.get("is_current") else ""
|
|
212
|
+
# Truncate workspace if too long
|
|
213
|
+
workspace = profile_dict.get("workspace") or "-"
|
|
214
|
+
if profile_dict.get("workspace") and len(str(profile_dict["workspace"])) > 20:
|
|
215
|
+
workspace = str(profile_dict["workspace"])[:17] + "..."
|
|
216
|
+
# Truncate server URL if too long
|
|
217
|
+
server = profile_dict["server"]
|
|
218
|
+
if len(server) > 40:
|
|
219
|
+
server = server[:37] + "..."
|
|
220
|
+
readonly = "✓" if profile_dict.get("readonly") else ""
|
|
221
|
+
return [current, profile_dict["name"], server, workspace, readonly]
|
|
222
|
+
|
|
223
|
+
output_formatted_list(
|
|
224
|
+
items=table_items,
|
|
225
|
+
output_format="table",
|
|
226
|
+
headers=["", "NAME", "SERVER", "WORKSPACE", "READONLY"],
|
|
227
|
+
column_widths=[1, 15, 40, 20, 8],
|
|
228
|
+
row_formatter_func=format_row,
|
|
229
|
+
empty_message="No profiles configured.",
|
|
230
|
+
total_label="profile(s)",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@config.command(name="current")
|
|
234
|
+
def current_profile() -> None:
|
|
235
|
+
"""Show the current profile name."""
|
|
236
|
+
cfg = ProfileConfig.load()
|
|
237
|
+
|
|
238
|
+
if not cfg.current_profile:
|
|
239
|
+
click.echo("No current profile set.", err=True)
|
|
240
|
+
click.echo("Run 'slcli config use <name>' to set one.", err=True)
|
|
241
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
242
|
+
|
|
243
|
+
click.echo(cfg.current_profile)
|
|
244
|
+
|
|
245
|
+
@config.command(name="use")
|
|
246
|
+
@click.argument("name")
|
|
247
|
+
def use_profile(name: str) -> None:
|
|
248
|
+
"""Switch to a different profile."""
|
|
249
|
+
cfg = ProfileConfig.load()
|
|
250
|
+
|
|
251
|
+
if name not in cfg.profiles:
|
|
252
|
+
click.echo(f"✗ Profile '{name}' not found.", err=True)
|
|
253
|
+
if cfg.profiles:
|
|
254
|
+
click.echo(f"Available profiles: {', '.join(cfg.profiles.keys())}", err=True)
|
|
255
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
256
|
+
|
|
257
|
+
cfg.set_current_profile(name)
|
|
258
|
+
cfg.save()
|
|
259
|
+
|
|
260
|
+
profile = cfg.get_profile(name)
|
|
261
|
+
click.echo(f"✓ Switched to profile '{name}'")
|
|
262
|
+
if profile:
|
|
263
|
+
click.echo(f" Server: {profile.server}")
|
|
264
|
+
if profile.workspace:
|
|
265
|
+
click.echo(f" Default workspace: {profile.workspace}")
|
|
266
|
+
|
|
267
|
+
@config.command(name="view")
|
|
268
|
+
@click.option(
|
|
269
|
+
"--format",
|
|
270
|
+
"-f",
|
|
271
|
+
type=click.Choice(["table", "json"]),
|
|
272
|
+
default="table",
|
|
273
|
+
help="Output format",
|
|
274
|
+
)
|
|
275
|
+
@click.option(
|
|
276
|
+
"--show-secrets",
|
|
277
|
+
is_flag=True,
|
|
278
|
+
help="Show API keys in output (use with caution)",
|
|
279
|
+
)
|
|
280
|
+
def view(format: str, show_secrets: bool) -> None:
|
|
281
|
+
"""View the full configuration."""
|
|
282
|
+
cfg = ProfileConfig.load()
|
|
283
|
+
|
|
284
|
+
if format == "json":
|
|
285
|
+
data: dict = {}
|
|
286
|
+
if cfg.current_profile:
|
|
287
|
+
data["current-profile"] = cfg.current_profile
|
|
288
|
+
if cfg.profiles:
|
|
289
|
+
# Mask API keys unless --show-secrets is specified
|
|
290
|
+
data["profiles"] = {}
|
|
291
|
+
for name, profile in cfg.profiles.items():
|
|
292
|
+
profile_dict = profile.to_dict()
|
|
293
|
+
if not show_secrets and "api-key" in profile_dict:
|
|
294
|
+
# Show only last 4 characters
|
|
295
|
+
key = profile_dict["api-key"]
|
|
296
|
+
profile_dict["api-key"] = "****" + key[-4:] if len(key) >= 4 else "****"
|
|
297
|
+
data["profiles"][name] = profile_dict
|
|
298
|
+
if cfg.settings:
|
|
299
|
+
data.update(cfg.settings)
|
|
300
|
+
click.echo(json.dumps(data, indent=2))
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
# Table format
|
|
304
|
+
click.echo("┌─────────────────────────────────────────────────────────────┐")
|
|
305
|
+
click.echo("│ slcli Configuration │")
|
|
306
|
+
click.echo("├─────────────────────────────────────────────────────────────┤")
|
|
307
|
+
|
|
308
|
+
if cfg.current_profile:
|
|
309
|
+
click.echo(f"│ Current Profile: {cfg.current_profile:<42} │")
|
|
310
|
+
else:
|
|
311
|
+
click.echo("│ Current Profile: (none) │")
|
|
312
|
+
|
|
313
|
+
config_path_str = str(ProfileConfig.get_config_path())
|
|
314
|
+
if len(config_path_str) > 46:
|
|
315
|
+
config_path_str = config_path_str[:43] + "..."
|
|
316
|
+
click.echo(f"│ Config File: {config_path_str:<46} │")
|
|
317
|
+
|
|
318
|
+
# Show current profile details
|
|
319
|
+
if cfg.current_profile and cfg.current_profile in cfg.profiles:
|
|
320
|
+
profile = cfg.profiles[cfg.current_profile]
|
|
321
|
+
click.echo("├─────────────────────────────────────────────────────────────┤")
|
|
322
|
+
|
|
323
|
+
# Server
|
|
324
|
+
server_str = profile.server
|
|
325
|
+
if len(server_str) > 47:
|
|
326
|
+
server_str = server_str[:44] + "..."
|
|
327
|
+
click.echo(f"│ Server: {server_str:<51} │")
|
|
328
|
+
|
|
329
|
+
# Web URL
|
|
330
|
+
if profile.web_url:
|
|
331
|
+
web_url_str = profile.web_url
|
|
332
|
+
if len(web_url_str) > 45:
|
|
333
|
+
web_url_str = web_url_str[:42] + "..."
|
|
334
|
+
click.echo(f"│ Web URL: {web_url_str:<50} │")
|
|
335
|
+
|
|
336
|
+
# Platform
|
|
337
|
+
if profile.platform:
|
|
338
|
+
platform_str = profile.platform or "Unknown"
|
|
339
|
+
click.echo(f"│ Platform: {platform_str:<49} │")
|
|
340
|
+
|
|
341
|
+
# API Key (redacted)
|
|
342
|
+
if show_secrets:
|
|
343
|
+
click.echo(f"│ API Key: {profile.api_key:<50} │")
|
|
344
|
+
else:
|
|
345
|
+
# Show only last 4 characters
|
|
346
|
+
key = profile.api_key
|
|
347
|
+
redacted_key = "****" + key[-4:] if len(key) >= 4 else "****"
|
|
348
|
+
click.echo(f"│ API Key: {redacted_key:<50} │")
|
|
349
|
+
|
|
350
|
+
# Workspace
|
|
351
|
+
if profile.workspace:
|
|
352
|
+
workspace_str = profile.workspace
|
|
353
|
+
if len(workspace_str) > 45:
|
|
354
|
+
workspace_str = workspace_str[:42] + "..."
|
|
355
|
+
click.echo(f"│ Workspace: {workspace_str:<48} │")
|
|
356
|
+
|
|
357
|
+
# Readonly
|
|
358
|
+
if profile.readonly:
|
|
359
|
+
click.echo("│ Readonly: enabled │")
|
|
360
|
+
|
|
361
|
+
click.echo("└─────────────────────────────────────────────────────────────┘")
|
|
362
|
+
|
|
363
|
+
# Check for permission warning
|
|
364
|
+
warning = check_config_file_permissions()
|
|
365
|
+
if warning:
|
|
366
|
+
click.echo(f"\n⚠️ {warning}", err=True)
|
|
367
|
+
|
|
368
|
+
@config.command(name="delete")
|
|
369
|
+
@click.argument("name")
|
|
370
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
|
|
371
|
+
def delete_profile(name: str, force: bool) -> None:
|
|
372
|
+
"""Delete a profile."""
|
|
373
|
+
from .utils import check_readonly_mode
|
|
374
|
+
|
|
375
|
+
check_readonly_mode("delete a profile")
|
|
376
|
+
|
|
377
|
+
cfg = ProfileConfig.load()
|
|
378
|
+
|
|
379
|
+
if name not in cfg.profiles:
|
|
380
|
+
click.echo(f"✗ Profile '{name}' not found.", err=True)
|
|
381
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
382
|
+
|
|
383
|
+
if not force:
|
|
384
|
+
if not questionary.confirm(
|
|
385
|
+
f"Delete profile '{name}'?",
|
|
386
|
+
default=False,
|
|
387
|
+
).ask():
|
|
388
|
+
click.echo("Aborted.")
|
|
389
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
390
|
+
|
|
391
|
+
was_current = cfg.current_profile == name
|
|
392
|
+
cfg.delete_profile(name)
|
|
393
|
+
cfg.save()
|
|
394
|
+
|
|
395
|
+
click.echo(f"✓ Profile '{name}' deleted.")
|
|
396
|
+
if was_current and cfg.current_profile:
|
|
397
|
+
click.echo(f" Current profile is now: {cfg.current_profile}")
|
|
398
|
+
|
|
399
|
+
@config.command(name="migrate")
|
|
400
|
+
@click.option(
|
|
401
|
+
"--profile-name",
|
|
402
|
+
"-n",
|
|
403
|
+
default="default",
|
|
404
|
+
help="Name for the migrated profile",
|
|
405
|
+
)
|
|
406
|
+
@click.option(
|
|
407
|
+
"--delete-keyring",
|
|
408
|
+
is_flag=True,
|
|
409
|
+
help="Delete keyring entries after migration",
|
|
410
|
+
)
|
|
411
|
+
def migrate(profile_name: str, delete_keyring: bool) -> None:
|
|
412
|
+
"""Migrate credentials from keyring to config file.
|
|
413
|
+
|
|
414
|
+
This command reads existing credentials from the system keyring
|
|
415
|
+
and creates a new profile in the config file.
|
|
416
|
+
"""
|
|
417
|
+
from .profiles import migrate_from_keyring
|
|
418
|
+
|
|
419
|
+
# Check if profile already exists
|
|
420
|
+
cfg = ProfileConfig.load()
|
|
421
|
+
if profile_name in cfg.profiles:
|
|
422
|
+
if not questionary.confirm(
|
|
423
|
+
f"Profile '{profile_name}' already exists. Overwrite?",
|
|
424
|
+
default=False,
|
|
425
|
+
).ask():
|
|
426
|
+
click.echo("Aborted.")
|
|
427
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
428
|
+
|
|
429
|
+
# Use centralized migration function
|
|
430
|
+
profile = migrate_from_keyring(profile_name=profile_name, delete_keyring=delete_keyring)
|
|
431
|
+
|
|
432
|
+
if not profile:
|
|
433
|
+
click.echo("✗ No credentials found in keyring.", err=True)
|
|
434
|
+
click.echo("Run 'slcli login --profile <name>' to create a new profile.", err=True)
|
|
435
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
436
|
+
|
|
437
|
+
click.echo(f"✓ Migrated credentials to profile '{profile_name}'")
|
|
438
|
+
click.echo(f" Server: {profile.server}")
|
|
439
|
+
if profile.web_url:
|
|
440
|
+
click.echo(f" Web URL: {profile.web_url}")
|
|
441
|
+
if profile.platform:
|
|
442
|
+
click.echo(f" Platform: {profile.platform}")
|
|
443
|
+
|
|
444
|
+
if delete_keyring:
|
|
445
|
+
click.echo("✓ Deleted keyring entries")
|
|
446
|
+
else:
|
|
447
|
+
click.echo("\nNote: Keyring entries still exist. Use --delete-keyring to remove them.")
|
|
448
|
+
|
|
449
|
+
@config.command(name="add")
|
|
450
|
+
@click.option("--profile", "-p", help="Profile name (default: 'default')")
|
|
451
|
+
@click.option("--url", help="SystemLink API URL")
|
|
452
|
+
@click.option("--api-key", help="SystemLink API key")
|
|
453
|
+
@click.option("--web-url", help="SystemLink Web UI base URL")
|
|
454
|
+
@click.option("--workspace", "-w", help="Default workspace for this profile")
|
|
455
|
+
@click.option(
|
|
456
|
+
"--set-current/--no-set-current",
|
|
457
|
+
default=True,
|
|
458
|
+
help="Set as current profile (default: yes)",
|
|
459
|
+
)
|
|
460
|
+
@click.option(
|
|
461
|
+
"--readonly",
|
|
462
|
+
is_flag=True,
|
|
463
|
+
help=(
|
|
464
|
+
"Enable readonly mode (disables create, update, delete, import, upload, "
|
|
465
|
+
"publish, and disable commands)"
|
|
466
|
+
),
|
|
467
|
+
)
|
|
468
|
+
def add_profile(
|
|
469
|
+
profile: Optional[str],
|
|
470
|
+
url: Optional[str],
|
|
471
|
+
api_key: Optional[str],
|
|
472
|
+
web_url: Optional[str],
|
|
473
|
+
workspace: Optional[str],
|
|
474
|
+
set_current: bool,
|
|
475
|
+
readonly: bool,
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Add or update a SystemLink profile.
|
|
478
|
+
|
|
479
|
+
Profiles allow you to configure multiple SystemLink environments and switch
|
|
480
|
+
between them. Credentials are stored in ~/.config/slcli/config.json.
|
|
481
|
+
|
|
482
|
+
The readonly flag enables readonly mode, which disables all delete and edit
|
|
483
|
+
commands in slcli. This is useful for AI agents or untrusted environments.
|
|
484
|
+
|
|
485
|
+
Examples:
|
|
486
|
+
slcli config add --profile dev
|
|
487
|
+
slcli config add -p prod --url https://prod-api.example.com
|
|
488
|
+
slcli config add --profile test --workspace "Testing" --readonly
|
|
489
|
+
"""
|
|
490
|
+
_add_profile_impl(
|
|
491
|
+
profile=profile,
|
|
492
|
+
url=url,
|
|
493
|
+
api_key=api_key,
|
|
494
|
+
web_url=web_url,
|
|
495
|
+
workspace=workspace,
|
|
496
|
+
set_current=set_current,
|
|
497
|
+
readonly=readonly,
|
|
498
|
+
)
|