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/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)
|