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/comment_click.py ADDED
@@ -0,0 +1,602 @@
1
+ """CLI commands for managing SystemLink comments.
2
+
3
+ Provides CLI commands for listing, creating, updating, and deleting comments
4
+ on any SystemLink resource via the nicomments v1 service.
5
+
6
+ Supported resource types:
7
+ testmonitor:Result - Test Monitor results
8
+ niapm:Asset - Assets
9
+ nisysmgmt:System - Systems
10
+ workorder:workorder - Work Orders
11
+ workitem:workitem - Work Items
12
+ DataSpace - Data Spaces
13
+ """
14
+
15
+ import json
16
+ import sys
17
+ from typing import Any, Dict, List, Optional, Tuple
18
+ from urllib.parse import urlencode
19
+
20
+ import click
21
+
22
+ from .cli_utils import validate_output_format
23
+ from .universal_handlers import FilteredResponse, UniversalResponseHandler
24
+ from .utils import (
25
+ ExitCodes,
26
+ check_readonly_mode,
27
+ format_success,
28
+ get_base_url,
29
+ get_workspace_map,
30
+ handle_api_error,
31
+ make_api_request,
32
+ )
33
+ from .workspace_utils import get_effective_workspace, resolve_workspace_filter
34
+
35
+ # Human-readable display names for known resource types, used in mention notification emails.
36
+ _RESOURCE_TYPE_NAMES: Dict[str, str] = {
37
+ "testmonitor:Result": "Test Result",
38
+ "niapm:Asset": "Asset",
39
+ "nisysmgmt:System": "System",
40
+ "workorder:workorder": "Work Order",
41
+ "workitem:workitem": "Work Item",
42
+ "DataSpace": "Data Space",
43
+ }
44
+
45
+
46
+ def _get_comment_base_url() -> str:
47
+ """Get the base URL for the Comments API."""
48
+ return f"{get_base_url()}/nicomments/v1"
49
+
50
+
51
+ def _resource_type_name(resource_type: str) -> str:
52
+ """Return the human-readable display name for a resource type.
53
+
54
+ Falls back to the raw resource_type string for unknown types.
55
+
56
+ Args:
57
+ resource_type: API resource type string (e.g. "testmonitor:Result").
58
+
59
+ Returns:
60
+ Human-readable label (e.g. "Test Result").
61
+ """
62
+ return _RESOURCE_TYPE_NAMES.get(resource_type, resource_type)
63
+
64
+
65
+ def _fetch_user_display_name(user_id: str) -> Optional[str]:
66
+ """Fetch a user's display name (First Last) from the niuser service.
67
+
68
+ Silently returns None on any failure so callers can fall back to the raw ID.
69
+
70
+ Args:
71
+ user_id: The user's unique ID.
72
+
73
+ Returns:
74
+ Display name string or None if the lookup fails.
75
+ """
76
+ try:
77
+ url = f"{get_base_url()}/niuser/v1/users/{user_id}"
78
+ resp = make_api_request("GET", url, payload=None, handle_errors=False)
79
+ data = resp.json()
80
+ first = data.get("firstName", "")
81
+ last = data.get("lastName", "")
82
+ name = f"{first} {last}".strip()
83
+ return name if name else None
84
+ except Exception:
85
+ return None
86
+
87
+
88
+ def _build_user_map(user_ids: List[str]) -> Dict[str, str]:
89
+ """Build a map of user IDs to display names for a set of IDs.
90
+
91
+ Deduplicates IDs before fetching. Silently ignores failed lookups.
92
+
93
+ Args:
94
+ user_ids: List of user IDs (may contain duplicates or empty strings).
95
+
96
+ Returns:
97
+ Dictionary mapping user ID to "First Last" display name.
98
+ """
99
+ user_map: Dict[str, str] = {}
100
+ seen = set()
101
+ for uid in user_ids:
102
+ if uid and uid not in seen:
103
+ seen.add(uid)
104
+ name = _fetch_user_display_name(uid)
105
+ if name:
106
+ user_map[uid] = name
107
+ return user_map
108
+
109
+
110
+ def _format_user(user_id: Optional[str], user_map: Dict[str, str]) -> str:
111
+ """Return display name for a user ID, falling back to the raw ID.
112
+
113
+ Args:
114
+ user_id: The user's unique ID, or None/empty.
115
+ user_map: Mapping of ID to display name.
116
+
117
+ Returns:
118
+ Display name or raw ID or empty string.
119
+ """
120
+ if not user_id:
121
+ return ""
122
+ return user_map.get(user_id, user_id)
123
+
124
+
125
+ def _truncate(text: str, max_len: int) -> str:
126
+ """Truncate text to max_len characters, appending '…' if truncated.
127
+
128
+ Args:
129
+ text: Text to truncate.
130
+ max_len: Maximum character length including the ellipsis.
131
+
132
+ Returns:
133
+ Truncated string.
134
+ """
135
+ if not text:
136
+ return ""
137
+ # Strip newlines for table display
138
+ flat = text.replace("\n", " ").replace("\r", "")
139
+ if len(flat) <= max_len:
140
+ return flat
141
+ return flat[: max_len - 1] + "…"
142
+
143
+
144
+ def _format_datetime(value: Optional[str]) -> str:
145
+ """Format an ISO-8601 timestamp to 'YYYY-MM-DD HH:MM' for table display.
146
+
147
+ Args:
148
+ value: ISO-8601 datetime string or None.
149
+
150
+ Returns:
151
+ Formatted date string or empty string.
152
+ """
153
+ if not value:
154
+ return ""
155
+ try:
156
+ # Accept "2018-05-09T15:07:42.527921Z" → "2018-05-09 15:07"
157
+ dt_part = value.replace("Z", "").split(".")[0]
158
+ date, time = dt_part.split("T", 1)
159
+ hour_min = time[:5]
160
+ return f"{date} {hour_min}"
161
+ except Exception:
162
+ return value
163
+
164
+
165
+ def _comment_row_formatter(comment: Dict[str, Any], user_map: Dict[str, str]) -> List[str]:
166
+ """Format a single comment into table columns.
167
+
168
+ Args:
169
+ comment: Comment data dict from API response.
170
+ user_map: User ID → display name map.
171
+
172
+ Returns:
173
+ List of column values: [ID, Message, Author, Created At, Updated At].
174
+ """
175
+ return [
176
+ comment.get("id", ""),
177
+ _truncate(comment.get("message", ""), 55),
178
+ _format_user(comment.get("createdBy"), user_map),
179
+ _format_datetime(comment.get("createdAt")),
180
+ _format_datetime(comment.get("updatedAt")),
181
+ ]
182
+
183
+
184
+ def register_comment_commands(cli: Any) -> None:
185
+ """Register the 'comment' command group and its subcommands."""
186
+
187
+ @cli.group()
188
+ def comment() -> None:
189
+ """Manage comments on SystemLink resources.
190
+
191
+ Comments can be attached to any resource identified by a resource type
192
+ and resource ID. Known resource types: testmonitor:Result, niapm:Asset,
193
+ nisysmgmt:System, workorder:workorder, workitem:workitem, DataSpace.
194
+ """
195
+ pass
196
+
197
+ # ─────────────────────────────────────────────────────────────
198
+ # comment list
199
+ # ─────────────────────────────────────────────────────────────
200
+
201
+ @comment.command(name="list")
202
+ @click.option(
203
+ "--resource-type",
204
+ "-r",
205
+ required=True,
206
+ help=(
207
+ "Resource type the comments belong to "
208
+ "(e.g. testmonitor:Result, niapm:Asset, nisysmgmt:System, "
209
+ "workorder:workorder, workitem:workitem, DataSpace)"
210
+ ),
211
+ )
212
+ @click.option(
213
+ "--resource-id",
214
+ "-i",
215
+ required=True,
216
+ help="ID of the resource whose comments to list.",
217
+ )
218
+ @click.option(
219
+ "--format",
220
+ "-f",
221
+ type=click.Choice(["table", "json"]),
222
+ default="table",
223
+ show_default=True,
224
+ help="Output format.",
225
+ )
226
+ def list_comments(
227
+ resource_type: str,
228
+ resource_id: str,
229
+ format: str = "table",
230
+ ) -> None:
231
+ """List comments for a resource.
232
+
233
+ The API returns the most recent 1000 comments ordered by creation time.
234
+
235
+ \b
236
+ Examples:
237
+ slcli comment list --resource-type testmonitor:Result --resource-id <id>
238
+ slcli comment list -r niapm:Asset -i <id> --format json
239
+ """
240
+ format_output = validate_output_format(format)
241
+
242
+ try:
243
+ # Note: "ResourceType" and "ResourceId" must use this exact PascalCase
244
+ # casing to match the nicomments v1 API query parameter specification.
245
+ params = urlencode({"ResourceType": resource_type, "ResourceId": resource_id})
246
+ url = f"{_get_comment_base_url()}/comments?{params}"
247
+ resp = make_api_request("GET", url)
248
+ data = resp.json()
249
+ comments: List[Dict[str, Any]] = data.get("comments") or []
250
+
251
+ if format_output.lower() == "json":
252
+ click.echo(json.dumps(comments, indent=2))
253
+ return
254
+
255
+ if not comments:
256
+ click.echo("No comments found.")
257
+ return
258
+
259
+ # Build user map once for all comments
260
+ user_ids = []
261
+ for c in comments:
262
+ for field in ("createdBy", "updatedBy"):
263
+ uid = c.get(field)
264
+ if uid:
265
+ user_ids.append(uid)
266
+ user_map = _build_user_map(user_ids)
267
+
268
+ def _row(c: Dict[str, Any]) -> List[str]:
269
+ return _comment_row_formatter(c, user_map)
270
+
271
+ filtered_resp: Any = FilteredResponse({"comments": comments})
272
+ UniversalResponseHandler.handle_list_response(
273
+ resp=filtered_resp,
274
+ data_key="comments",
275
+ item_name="comment",
276
+ format_output=format_output,
277
+ formatter_func=_row,
278
+ headers=["ID", "Message", "Author", "Created At", "Updated At"],
279
+ column_widths=[24, 55, 22, 16, 16],
280
+ empty_message="No comments found.",
281
+ enable_pagination=True,
282
+ page_size=25,
283
+ )
284
+
285
+ except Exception as exc:
286
+ handle_api_error(exc)
287
+
288
+ # ─────────────────────────────────────────────────────────────
289
+ # comment add
290
+ # ─────────────────────────────────────────────────────────────
291
+
292
+ @comment.command(name="add")
293
+ @click.option(
294
+ "--resource-type",
295
+ "-r",
296
+ required=True,
297
+ help=(
298
+ "Resource type to comment on "
299
+ "(e.g. testmonitor:Result, niapm:Asset, nisysmgmt:System, "
300
+ "workorder:workorder, workitem:workitem, DataSpace)"
301
+ ),
302
+ )
303
+ @click.option(
304
+ "--resource-id",
305
+ "-i",
306
+ required=True,
307
+ help="ID of the resource to comment on.",
308
+ )
309
+ @click.option(
310
+ "--workspace",
311
+ "-w",
312
+ required=True,
313
+ help="Workspace name or ID that owns the resource.",
314
+ )
315
+ @click.option(
316
+ "--message",
317
+ "-m",
318
+ required=True,
319
+ help=(
320
+ "Comment message (supports Markdown). "
321
+ "To mention a user, embed a tag in the form <user:USER_ID> in the message "
322
+ "and also pass the same user ID to --mention."
323
+ ),
324
+ )
325
+ @click.option(
326
+ "--resource-name",
327
+ "-n",
328
+ default=None,
329
+ help=(
330
+ "Human-readable name of the resource. Required when using --mention "
331
+ "so the API can include it in mention notification emails."
332
+ ),
333
+ )
334
+ @click.option(
335
+ "--comment-url",
336
+ "-u",
337
+ default=None,
338
+ help=(
339
+ "URL to the comment in the SystemLink web UI. Required when using --mention "
340
+ "so the API can include a link in mention notification emails."
341
+ ),
342
+ )
343
+ @click.option(
344
+ "--mention",
345
+ "mentions",
346
+ multiple=True,
347
+ help=(
348
+ "User ID to register as a mentioned user. Must match a <user:USER_ID> tag "
349
+ "embedded in the --message. Can be specified multiple times."
350
+ ),
351
+ )
352
+ def add_comment(
353
+ resource_type: str,
354
+ resource_id: str,
355
+ workspace: str,
356
+ message: str,
357
+ resource_name: Optional[str],
358
+ comment_url: Optional[str],
359
+ mentions: Tuple[str, ...],
360
+ ) -> None:
361
+ """Add a comment to a resource.
362
+
363
+ To mention a user, embed a \\b<user:USER_ID> tag in the message and pass
364
+ the same user ID to --mention. All three of --resource-name, --comment-url,
365
+ and the implicit --resource-type are then also required.
366
+
367
+ \b
368
+ Examples:
369
+ slcli comment add -r testmonitor:Result -i <id> -w default -m "Looks good!"
370
+ slcli comment add -r niapm:Asset -i <id> -w "My Workspace" \\
371
+ -n "My Asset" -u "https://<server>/..." \\
372
+ -m "Review needed: <user:f9d5c5c9-e098-4a82-8e55-fede326a4ec3>" \\
373
+ --mention f9d5c5c9-e098-4a82-8e55-fede326a4ec3
374
+ """
375
+ check_readonly_mode("add a comment")
376
+
377
+ if mentions and not resource_name:
378
+ click.echo(
379
+ "✗ --resource-name / -n is required when using --mention.",
380
+ err=True,
381
+ )
382
+ sys.exit(ExitCodes.INVALID_INPUT)
383
+
384
+ if mentions and not comment_url:
385
+ click.echo(
386
+ "✗ --comment-url / -u is required when using --mention.",
387
+ err=True,
388
+ )
389
+ sys.exit(ExitCodes.INVALID_INPUT)
390
+
391
+ try:
392
+ workspace_map = get_workspace_map()
393
+ workspace = get_effective_workspace(workspace) or workspace
394
+ workspace_id = resolve_workspace_filter(workspace, workspace_map)
395
+
396
+ comment_entry: Dict[str, Any] = {
397
+ "resourceType": resource_type,
398
+ "resourceId": resource_id,
399
+ "workspace": workspace_id,
400
+ "message": message,
401
+ }
402
+ if mentions:
403
+ comment_entry["mentionedUsers"] = list(mentions)
404
+ comment_entry["resourceName"] = resource_name
405
+ comment_entry["resourceTypeName"] = _resource_type_name(resource_type)
406
+ comment_entry["commentUrl"] = comment_url
407
+
408
+ payload: Dict[str, Any] = {"comments": [comment_entry]}
409
+
410
+ url = f"{_get_comment_base_url()}/comments"
411
+ resp = make_api_request("POST", url, payload=payload)
412
+ resp_data = resp.json()
413
+
414
+ # API returns 201 on full success, 200 on partial success
415
+ created = resp_data.get("createdComments") or []
416
+ failed = resp_data.get("failedComments") or []
417
+
418
+ if created:
419
+ comment_id = created[0].get("id", "")
420
+ format_success("Comment added", {"ID": comment_id})
421
+
422
+ if failed:
423
+ click.echo(
424
+ f"✗ {len(failed)} comment(s) failed to create.",
425
+ err=True,
426
+ )
427
+ sys.exit(ExitCodes.GENERAL_ERROR)
428
+
429
+ except Exception as exc:
430
+ handle_api_error(exc)
431
+
432
+ # ─────────────────────────────────────────────────────────────
433
+ # comment update
434
+ # ─────────────────────────────────────────────────────────────
435
+
436
+ @comment.command(name="update")
437
+ @click.argument("comment_id")
438
+ @click.option(
439
+ "--message",
440
+ "-m",
441
+ required=True,
442
+ help=(
443
+ "New comment message (supports Markdown). Replaces the existing message. "
444
+ "To mention a user, embed a tag in the form <user:USER_ID> in the message "
445
+ "and also pass the same user ID to --mention."
446
+ ),
447
+ )
448
+ @click.option(
449
+ "--resource-name",
450
+ "-n",
451
+ default=None,
452
+ help=(
453
+ "Human-readable name of the resource. Required when using --mention "
454
+ "so the API can include it in mention notification emails."
455
+ ),
456
+ )
457
+ @click.option(
458
+ "--resource-type",
459
+ "-r",
460
+ default=None,
461
+ help=(
462
+ "Resource type of the comment's resource "
463
+ "(e.g. testmonitor:Result, niapm:Asset, DataSpace). "
464
+ "Required when using --mention so the API can send notification emails."
465
+ ),
466
+ )
467
+ @click.option(
468
+ "--comment-url",
469
+ "-u",
470
+ default=None,
471
+ help=(
472
+ "URL to the comment in the SystemLink web UI. Required when using --mention "
473
+ "so the API can include a link in mention notification emails."
474
+ ),
475
+ )
476
+ @click.option(
477
+ "--mention",
478
+ "mentions",
479
+ multiple=True,
480
+ help=(
481
+ "User ID to register as a mentioned user. Must match a <user:USER_ID> tag "
482
+ "embedded in the --message. Can be specified multiple times. "
483
+ "Replaces the full mentioned-users list."
484
+ ),
485
+ )
486
+ def update_comment(
487
+ comment_id: str,
488
+ message: str,
489
+ resource_name: Optional[str],
490
+ resource_type: Optional[str],
491
+ comment_url: Optional[str],
492
+ mentions: Tuple[str, ...],
493
+ ) -> None:
494
+ """Update the message of an existing comment.
495
+
496
+ You can only update your own comments. Mentioning users replaces
497
+ the previous mention list entirely. To mention a user, embed a
498
+ \\b<user:USER_ID> tag in the message and pass the same user ID to --mention.
499
+ Also requires --resource-name, --resource-type, and --comment-url.
500
+
501
+ \b
502
+ Examples:
503
+ slcli comment update <comment-id> --message "Updated text"
504
+ slcli comment update <comment-id> \\
505
+ -m "FYI: <user:f9d5c5c9-e098-4a82-8e55-fede326a4ec3>" \\
506
+ -n "My Result" -r testmonitor:Result -u "https://<server>/..." \\
507
+ --mention f9d5c5c9-e098-4a82-8e55-fede326a4ec3
508
+ """
509
+ check_readonly_mode("update this comment")
510
+
511
+ if mentions and not resource_name:
512
+ click.echo(
513
+ "✗ --resource-name / -n is required when using --mention.",
514
+ err=True,
515
+ )
516
+ sys.exit(ExitCodes.INVALID_INPUT)
517
+
518
+ if mentions and not resource_type:
519
+ click.echo(
520
+ "✗ --resource-type / -r is required when using --mention.",
521
+ err=True,
522
+ )
523
+ sys.exit(ExitCodes.INVALID_INPUT)
524
+
525
+ if mentions and not comment_url:
526
+ click.echo(
527
+ "✗ --comment-url / -u is required when using --mention.",
528
+ err=True,
529
+ )
530
+ sys.exit(ExitCodes.INVALID_INPUT)
531
+
532
+ try:
533
+ payload: Dict[str, Any] = {
534
+ "message": message,
535
+ }
536
+ if mentions:
537
+ payload["mentionedUsers"] = list(mentions)
538
+ payload["resourceName"] = resource_name
539
+ payload["resourceTypeName"] = _resource_type_name(resource_type or "")
540
+ payload["commentUrl"] = comment_url
541
+
542
+ url = f"{_get_comment_base_url()}/comments/{comment_id}"
543
+ make_api_request("PATCH", url, payload=payload)
544
+ format_success("Comment updated", {"ID": comment_id})
545
+
546
+ except Exception as exc:
547
+ handle_api_error(exc)
548
+
549
+ # ─────────────────────────────────────────────────────────────
550
+ # comment delete
551
+ # ─────────────────────────────────────────────────────────────
552
+
553
+ @comment.command(name="delete")
554
+ @click.argument("comment_ids", nargs=-1, required=True)
555
+ def delete_comments(
556
+ comment_ids: Tuple[str, ...],
557
+ ) -> None:
558
+ """Delete one or more comments by ID.
559
+
560
+ You can delete your own comments. Elevated permissions are required
561
+ to delete others' comments. Accepts up to 1000 IDs per invocation.
562
+
563
+ \b
564
+ Examples:
565
+ slcli comment delete <comment-id>
566
+ slcli comment delete <id1> <id2> <id3>
567
+ """
568
+ check_readonly_mode("delete comment(s)")
569
+
570
+ if len(comment_ids) > 1000:
571
+ click.echo(
572
+ "✗ Cannot delete more than 1000 comments per request.",
573
+ err=True,
574
+ )
575
+ sys.exit(ExitCodes.INVALID_INPUT)
576
+
577
+ try:
578
+ payload: Dict[str, Any] = {"ids": list(comment_ids)}
579
+ url = f"{_get_comment_base_url()}/delete-comments"
580
+ resp = make_api_request("POST", url, payload=payload)
581
+
582
+ # 204 = full success (no body), 200 = partial success (body with details)
583
+ if resp.status_code == 204:
584
+ format_success(f"Deleted {len(comment_ids)} comment(s)")
585
+ return
586
+
587
+ resp_data = resp.json()
588
+ deleted_ids: List[str] = resp_data.get("deletedCommentIds") or []
589
+ failed_ids: List[str] = resp_data.get("failedCommentIds") or []
590
+
591
+ if deleted_ids:
592
+ format_success(f"Deleted {len(deleted_ids)} comment(s)")
593
+
594
+ if failed_ids:
595
+ click.echo(
596
+ f"✗ Failed to delete {len(failed_ids)} comment(s): " + ", ".join(failed_ids),
597
+ err=True,
598
+ )
599
+ sys.exit(ExitCodes.GENERAL_ERROR)
600
+
601
+ except Exception as exc:
602
+ handle_api_error(exc)