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.
Files changed (74) hide show
  1. slcli/__init__.py +1 -0
  2. slcli/__main__.py +23 -0
  3. slcli/_version.py +4 -0
  4. slcli/asset_click.py +1289 -0
  5. slcli/cli_formatters.py +218 -0
  6. slcli/cli_utils.py +504 -0
  7. slcli/comment_click.py +602 -0
  8. slcli/completion_click.py +418 -0
  9. slcli/config.py +81 -0
  10. slcli/config_click.py +498 -0
  11. slcli/dff_click.py +979 -0
  12. slcli/dff_decorators.py +24 -0
  13. slcli/example_click.py +404 -0
  14. slcli/example_loader.py +274 -0
  15. slcli/example_provisioner.py +2777 -0
  16. slcli/examples/README.md +134 -0
  17. slcli/examples/_schema/schema-v1.0.json +169 -0
  18. slcli/examples/demo-complete-workflow/README.md +323 -0
  19. slcli/examples/demo-complete-workflow/config.yaml +638 -0
  20. slcli/examples/demo-test-plans/README.md +132 -0
  21. slcli/examples/demo-test-plans/config.yaml +154 -0
  22. slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
  23. slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
  24. slcli/examples/exercise-7-1-test-plans/README.md +93 -0
  25. slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
  26. slcli/examples/spec-compliance-notebooks/README.md +140 -0
  27. slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
  28. slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
  29. slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
  30. slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
  31. slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  32. slcli/feed_click.py +892 -0
  33. slcli/file_click.py +932 -0
  34. slcli/function_click.py +1400 -0
  35. slcli/function_templates.py +85 -0
  36. slcli/main.py +406 -0
  37. slcli/mcp_click.py +269 -0
  38. slcli/mcp_server.py +748 -0
  39. slcli/notebook_click.py +1770 -0
  40. slcli/platform.py +345 -0
  41. slcli/policy_click.py +679 -0
  42. slcli/policy_utils.py +411 -0
  43. slcli/profiles.py +411 -0
  44. slcli/response_handlers.py +359 -0
  45. slcli/routine_click.py +763 -0
  46. slcli/skill_click.py +253 -0
  47. slcli/skills/slcli/SKILL.md +713 -0
  48. slcli/skills/slcli/references/analysis-recipes.md +474 -0
  49. slcli/skills/slcli/references/filtering.md +236 -0
  50. slcli/skills/systemlink-webapp/SKILL.md +744 -0
  51. slcli/skills/systemlink-webapp/references/deployment.md +123 -0
  52. slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
  53. slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
  54. slcli/ssl_trust.py +93 -0
  55. slcli/system_click.py +2216 -0
  56. slcli/table_utils.py +124 -0
  57. slcli/tag_click.py +794 -0
  58. slcli/templates_click.py +599 -0
  59. slcli/testmonitor_click.py +1667 -0
  60. slcli/universal_handlers.py +305 -0
  61. slcli/user_click.py +1218 -0
  62. slcli/utils.py +832 -0
  63. slcli/web_editor.py +295 -0
  64. slcli/webapp_click.py +981 -0
  65. slcli/workflow_preview.py +287 -0
  66. slcli/workflows_click.py +988 -0
  67. slcli/workitem_click.py +2258 -0
  68. slcli/workspace_click.py +576 -0
  69. slcli/workspace_utils.py +206 -0
  70. systemlink_cli-1.3.1.dist-info/METADATA +20 -0
  71. systemlink_cli-1.3.1.dist-info/RECORD +74 -0
  72. systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
  73. systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
  74. 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
+ )