kd-cli 2.4.1__tar.gz → 2.5.1__tar.gz
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.
- {kd_cli-2.4.1/kd_cli.egg-info → kd_cli-2.5.1}/PKG-INFO +1 -1
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/tags.py +1 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/article/__init__.py +5 -1
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/issue/__init__.py +21 -1
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/system/__init__.py +10 -2
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/tag/__init__.py +16 -4
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/client.py +148 -55
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/__init__.py +2 -1
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/tag.py +17 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/CHANGELOG.md +23 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/error-messages.yaml +11 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/operating-guide.md +10 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.4.1 → kd_cli-2.5.1}/pyproject.toml +1 -1
- {kd_cli-2.4.1 → kd_cli-2.5.1}/LICENSE +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/MANIFEST.in +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/README.md +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/articles.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/http.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/hub.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/api/youtrack.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/article_marker.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/auth.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/core/confirm.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/input.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/main.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/output.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/cli/tree.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/config.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/config_read.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/config_schema.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/connection.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/debug.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/exceptions.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/history.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/locking.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/article.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/assist.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/attachment.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/comment.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/common.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/issue.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/project.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/saved_query.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/search.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/user.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/models/work_item.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/operating_guide.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/primer.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/query_parser.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/release_notes.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/primers/__init__.py +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd/resources/primers/hitl.yaml +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.4.1 → kd_cli-2.5.1}/setup.cfg +0 -0
|
@@ -353,7 +353,11 @@ class ArticleCreateCommand(Command):
|
|
|
353
353
|
"title. Pass --parent to attach the new article under an "
|
|
354
354
|
"existing one in a single call, avoiding a create-then-move "
|
|
355
355
|
"two-step in migration scripts. Pass --tag (repeatable) to "
|
|
356
|
-
"apply tags at creation time
|
|
356
|
+
"apply tags at creation time: every --tag must exist, the "
|
|
357
|
+
"article is refused before creation if any tag is missing "
|
|
358
|
+
"(no orphan article on unknown tags), and the structured "
|
|
359
|
+
"response reflects post-tag state so it matches a "
|
|
360
|
+
"subsequent /article show on the returned id."
|
|
357
361
|
),
|
|
358
362
|
examples=[
|
|
359
363
|
'kd /article create "Setup Guide" --project FB --content @guide.md',
|
|
@@ -270,11 +270,20 @@ class IssueCreateCommand(Command):
|
|
|
270
270
|
"Create an issue",
|
|
271
271
|
requires_client=True,
|
|
272
272
|
usage='kd /issue create "Summary" --project P [--field NAME=VALUE] [--attach FILE]',
|
|
273
|
-
long_description=
|
|
273
|
+
long_description=(
|
|
274
|
+
"Summary is the issue title. --project is the project key (e.g. FB). "
|
|
275
|
+
"--field NAME=VALUE supports bundle families (enum, state, owned, "
|
|
276
|
+
"version, build), user logins, period, and string custom fields; the "
|
|
277
|
+
"field name is fuzzy-matched and the value is passed verbatim. "
|
|
278
|
+
"Unsupported families (text, Multi*, and integer/float/date subtypes "
|
|
279
|
+
"of SimpleProjectCustomField) fail client-side with an error naming "
|
|
280
|
+
"both the project $type and fieldType.id subtype."
|
|
281
|
+
),
|
|
274
282
|
examples=[
|
|
275
283
|
'kd /issue create "Fix router config" --project FB --type Task',
|
|
276
284
|
'kd /issue create "Bug report" -p FB --attach screenshot.png --attach log.txt',
|
|
277
285
|
'kd /issue create "Sprint task" -p FB --field "Sprint=Sprint 47"',
|
|
286
|
+
'kd /issue create "Tracked" -p VIS --field "Milestone=M039"',
|
|
278
287
|
'kd /issue create "Design doc" -p FB --description @design.md',
|
|
279
288
|
],
|
|
280
289
|
see_also=["/issue list", "/issue show", "/issue/attachment add", "/project/field list"],
|
|
@@ -437,10 +446,21 @@ class IssueUpdateCommand(Command):
|
|
|
437
446
|
"Update an issue",
|
|
438
447
|
requires_client=True,
|
|
439
448
|
usage="kd /issue update ID [--title T] [--field NAME=VALUE]",
|
|
449
|
+
long_description=(
|
|
450
|
+
"--field NAME=VALUE supports bundle families (enum, state, owned, "
|
|
451
|
+
"version, build), user logins, period, and string custom fields; the "
|
|
452
|
+
"field name is fuzzy-matched and the value is passed verbatim. "
|
|
453
|
+
"Clear optional custom fields with NAME= (empty value). Unsupported "
|
|
454
|
+
"families (text, Multi*, and integer/float/date subtypes of "
|
|
455
|
+
"SimpleProjectCustomField) fail client-side with an error naming "
|
|
456
|
+
"both the project $type and fieldType.id subtype."
|
|
457
|
+
),
|
|
440
458
|
examples=[
|
|
441
459
|
'kd /issue update FB-42 --title "New title"',
|
|
442
460
|
"kd /issue update FB-42 --state Open --priority Critical",
|
|
443
461
|
'kd /issue update FB-42 --field "Sprint=Sprint 48"',
|
|
462
|
+
'kd /issue update VIS-21 --field "Milestone=M039"',
|
|
463
|
+
'kd /issue update FB-42 --field "Milestone="',
|
|
444
464
|
"kd /issue update FB-42 --description @updated.md",
|
|
445
465
|
],
|
|
446
466
|
see_also=["/issue show", "/project/field list"],
|
|
@@ -9,10 +9,12 @@ copy-on-write history for config and primer files.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import difflib
|
|
12
|
+
import platform as _platform
|
|
12
13
|
from dataclasses import asdict
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
17
|
+
from kd import __version__ as _kd_version
|
|
16
18
|
from kd.cli.core.command_base import Command, CommandResult
|
|
17
19
|
from kd.cli.core.namespaces import Namespace
|
|
18
20
|
from kd.cli.core.option_registry import OptionDeclaration, OptionLayer, ResolvedOptions
|
|
@@ -51,10 +53,13 @@ class SystemInfoCommand(Command):
|
|
|
51
53
|
requires_client=True,
|
|
52
54
|
usage="kd /system info",
|
|
53
55
|
long_description="Connects to the YouTrack instance and displays the current "
|
|
54
|
-
"user, server URL, and connection status
|
|
55
|
-
"(
|
|
56
|
+
"user, server URL, and connection status, plus kd client/runtime metadata "
|
|
57
|
+
"(`kd_version`, `python_version`, `platform`) so bug reports are "
|
|
58
|
+
"self-describing without hand-typing the version. Uses the active "
|
|
59
|
+
"connection (selected via -c or 'default_connection').",
|
|
56
60
|
examples=[
|
|
57
61
|
"kd /system info",
|
|
62
|
+
"kd /system info -o yaml",
|
|
58
63
|
"kd -c upstream /system info",
|
|
59
64
|
],
|
|
60
65
|
see_also=["/system/config list"],
|
|
@@ -73,6 +78,9 @@ class SystemInfoCommand(Command):
|
|
|
73
78
|
"user": me.get("login", "unknown"),
|
|
74
79
|
"name": me.get("name", ""),
|
|
75
80
|
"email": me.get("email", ""),
|
|
81
|
+
"kd_version": _kd_version,
|
|
82
|
+
"python_version": _platform.python_version(),
|
|
83
|
+
"platform": _platform.platform(),
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
|
|
@@ -152,7 +152,7 @@ class TagShowCommand(Command):
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
class TagCreateCommand(Command):
|
|
155
|
-
"""Create a tag."""
|
|
155
|
+
"""Create a tag (idempotent)."""
|
|
156
156
|
|
|
157
157
|
def __init__(self) -> None:
|
|
158
158
|
super().__init__(
|
|
@@ -160,8 +160,16 @@ class TagCreateCommand(Command):
|
|
|
160
160
|
"Create a tag",
|
|
161
161
|
requires_client=True,
|
|
162
162
|
usage="kd /tag create TAG_NAME",
|
|
163
|
+
long_description=(
|
|
164
|
+
"Idempotent: if a tag with the requested name is already "
|
|
165
|
+
"visible, the command returns the existing tag with "
|
|
166
|
+
"action: exists and does not POST. Response envelope: "
|
|
167
|
+
"{ok, action, tag} with action ∈ {create, exists} in both "
|
|
168
|
+
"branches. Name resolution is case-insensitive (same "
|
|
169
|
+
"resolver /tag show uses)."
|
|
170
|
+
),
|
|
163
171
|
examples=['kd /tag create "sprint-47"'],
|
|
164
|
-
see_also=["/tag list", "/tag delete"],
|
|
172
|
+
see_also=["/tag list", "/tag show", "/tag delete"],
|
|
165
173
|
)
|
|
166
174
|
|
|
167
175
|
def execute(
|
|
@@ -169,8 +177,12 @@ class TagCreateCommand(Command):
|
|
|
169
177
|
) -> CommandResult:
|
|
170
178
|
if not args:
|
|
171
179
|
raise ValidationError("Usage: kd /tag create TAG_NAME")
|
|
172
|
-
|
|
173
|
-
return
|
|
180
|
+
result = client.tags.create(args[0])
|
|
181
|
+
return {
|
|
182
|
+
"ok": True,
|
|
183
|
+
"action": result.action,
|
|
184
|
+
"tag": _tag_to_detail(result.tag),
|
|
185
|
+
}
|
|
174
186
|
|
|
175
187
|
|
|
176
188
|
class TagRenameCommand(Command):
|
|
@@ -22,6 +22,7 @@ import builtins
|
|
|
22
22
|
import difflib
|
|
23
23
|
import shlex
|
|
24
24
|
from collections import OrderedDict
|
|
25
|
+
from collections.abc import Callable
|
|
25
26
|
from dataclasses import dataclass, field as dataclass_field
|
|
26
27
|
from datetime import datetime, timezone
|
|
27
28
|
from pathlib import Path
|
|
@@ -72,6 +73,7 @@ from kd.models import (
|
|
|
72
73
|
EntityRef,
|
|
73
74
|
KindMeta,
|
|
74
75
|
Tag,
|
|
76
|
+
TagCreateResult,
|
|
75
77
|
TagEntitiesResult,
|
|
76
78
|
TimeTrackingSettings,
|
|
77
79
|
User,
|
|
@@ -129,15 +131,43 @@ def _resolve_link_command(link_type: str) -> str:
|
|
|
129
131
|
return result
|
|
130
132
|
|
|
131
133
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
# Dispatch over (project_field_type, field_type_id) → (issue_type, payload_builder).
|
|
135
|
+
#
|
|
136
|
+
# The builder wraps a validated value into the exact shape YouTrack expects in
|
|
137
|
+
# the ``customFields[].value`` slot of an issue create/update payload. Bundle
|
|
138
|
+
# families wrap as ``{"name": ...}``; user fields wrap as ``{"login": ...}``;
|
|
139
|
+
# the scalar simple-string family passes through verbatim.
|
|
140
|
+
#
|
|
141
|
+
# ``""`` as the second tuple element means "any field_type_id under this
|
|
142
|
+
# ``$type``" — used when every subtype of a family shares one payload shape.
|
|
143
|
+
# A specific ``field_type_id`` key wins over the wildcard when both are present
|
|
144
|
+
# (see ``SimpleProjectCustomField`` which only supports the ``string`` subtype
|
|
145
|
+
# this session; ``integer``/``float``/``date`` stay unmapped).
|
|
146
|
+
|
|
147
|
+
FieldValueBuilder = Callable[[str], Any]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _bundle_name(value: str) -> dict[str, str]:
|
|
151
|
+
return {"name": value}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _user_login(value: str) -> dict[str, str]:
|
|
155
|
+
return {"login": value}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _scalar(value: str) -> str:
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_FIELD_DISPATCH: dict[tuple[str, str], tuple[str, FieldValueBuilder]] = {
|
|
163
|
+
("EnumProjectCustomField", ""): ("SingleEnumIssueCustomField", _bundle_name),
|
|
164
|
+
("StateProjectCustomField", ""): ("StateIssueCustomField", _bundle_name),
|
|
165
|
+
("UserProjectCustomField", ""): ("SingleUserIssueCustomField", _user_login),
|
|
166
|
+
("OwnedProjectCustomField", ""): ("SingleOwnedIssueCustomField", _bundle_name),
|
|
167
|
+
("VersionProjectCustomField", ""): ("MultiVersionIssueCustomField", _bundle_name),
|
|
168
|
+
("BuildProjectCustomField", ""): ("SingleBuildIssueCustomField", _bundle_name),
|
|
169
|
+
("PeriodProjectCustomField", ""): ("PeriodIssueCustomField", _bundle_name),
|
|
170
|
+
("SimpleProjectCustomField", "string"): ("SimpleIssueCustomField", _scalar),
|
|
141
171
|
}
|
|
142
172
|
|
|
143
173
|
# Default target field names for semantic CLI slots.
|
|
@@ -273,14 +303,10 @@ class FieldSchema:
|
|
|
273
303
|
self._debug.debug("fields", f"resolving slot '{slot}' -> target '{target_name}'")
|
|
274
304
|
|
|
275
305
|
field = self._find_field(project_fields, target_name, slot)
|
|
276
|
-
issue_type =
|
|
277
|
-
if not issue_type:
|
|
278
|
-
raise ValidationError(
|
|
279
|
-
f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
|
|
280
|
-
)
|
|
306
|
+
issue_type, builder = self._resolve_dispatch(field)
|
|
281
307
|
|
|
282
308
|
validated_value = self._validate_field_value(field, value)
|
|
283
|
-
field_value =
|
|
309
|
+
field_value = builder(validated_value) if validated_value is not None else None
|
|
284
310
|
self._debug.debug(
|
|
285
311
|
"fields",
|
|
286
312
|
f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
|
|
@@ -341,13 +367,9 @@ class FieldSchema:
|
|
|
341
367
|
"""
|
|
342
368
|
project_fields = self.get_fields(project_id)
|
|
343
369
|
field = self._find_field_fuzzy(project_fields, field_name)
|
|
344
|
-
issue_type =
|
|
345
|
-
if not issue_type:
|
|
346
|
-
raise ValidationError(
|
|
347
|
-
f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
|
|
348
|
-
)
|
|
370
|
+
issue_type, builder = self._resolve_dispatch(field)
|
|
349
371
|
validated_value = self._validate_field_value(field, value)
|
|
350
|
-
field_value =
|
|
372
|
+
field_value = builder(validated_value) if validated_value is not None else None
|
|
351
373
|
self._debug.debug(
|
|
352
374
|
"fields",
|
|
353
375
|
f"resolved generic '{field_name}' -> field '{field.name}' "
|
|
@@ -387,19 +409,30 @@ class FieldSchema:
|
|
|
387
409
|
"""Return True if this field is a user-type field (user[1])."""
|
|
388
410
|
return field.field_type_id.startswith("user")
|
|
389
411
|
|
|
390
|
-
|
|
391
|
-
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _resolve_dispatch(
|
|
392
414
|
field: ProjectCustomField,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
415
|
+
) -> tuple[str, FieldValueBuilder]:
|
|
416
|
+
"""Look up the issue-side ``$type`` and payload builder for *field*.
|
|
417
|
+
|
|
418
|
+
Prefers an exact ``(project_field_type, field_type_id)`` key, then
|
|
419
|
+
falls back to the wildcard ``(project_field_type, "")`` key for
|
|
420
|
+
families where every subtype shares a payload shape. Unmapped
|
|
421
|
+
combinations raise a ``ValidationError`` naming both identifiers so
|
|
422
|
+
a follow-on ticket has the upstream type info it needs.
|
|
399
423
|
"""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
424
|
+
key = (field.project_field_type, field.field_type_id)
|
|
425
|
+
if key in _FIELD_DISPATCH:
|
|
426
|
+
return _FIELD_DISPATCH[key]
|
|
427
|
+
wildcard = (field.project_field_type, "")
|
|
428
|
+
if wildcard in _FIELD_DISPATCH:
|
|
429
|
+
return _FIELD_DISPATCH[wildcard]
|
|
430
|
+
raise ValidationError(
|
|
431
|
+
f"Unsupported custom field type '{field.project_field_type}' "
|
|
432
|
+
f"with subtype '{field.field_type_id}' for field '{field.name}'. "
|
|
433
|
+
"kd does not know how to build a safe value payload for this "
|
|
434
|
+
"YouTrack field family."
|
|
435
|
+
)
|
|
403
436
|
|
|
404
437
|
def _validate_field_value(
|
|
405
438
|
self,
|
|
@@ -420,7 +453,7 @@ class FieldSchema:
|
|
|
420
453
|
if self._is_user_field(field):
|
|
421
454
|
return value # User fields: server validates against group members
|
|
422
455
|
if not field.bundle_values:
|
|
423
|
-
return value # Freeform
|
|
456
|
+
return value # Freeform family (period or scalar string) — server validates
|
|
424
457
|
|
|
425
458
|
# Case-insensitive exact match → return canonical name
|
|
426
459
|
value_lower = value.lower()
|
|
@@ -683,7 +716,7 @@ class IssueOperations:
|
|
|
683
716
|
if "=" not in pair:
|
|
684
717
|
raise ValidationError(f"Invalid field format: '{pair}'. Expected NAME=VALUE")
|
|
685
718
|
name, value = pair.split("=", 1)
|
|
686
|
-
cf = self._field_schema.resolve_field(project_obj.id, name.strip(), value
|
|
719
|
+
cf = self._field_schema.resolve_field(project_obj.id, name.strip(), value)
|
|
687
720
|
result.append(cf)
|
|
688
721
|
return result
|
|
689
722
|
|
|
@@ -1031,24 +1064,44 @@ def _extract_tag_suggestions(error: NotFoundError) -> builtins.list[str]:
|
|
|
1031
1064
|
return [part.strip() for part in raw.split(",") if part.strip()]
|
|
1032
1065
|
|
|
1033
1066
|
|
|
1034
|
-
def _missing_tag_resolution(
|
|
1035
|
-
|
|
1067
|
+
def _missing_tag_resolution(
|
|
1068
|
+
tag: str,
|
|
1069
|
+
error: NotFoundError,
|
|
1070
|
+
*,
|
|
1071
|
+
context: Literal["create", "push"] = "push",
|
|
1072
|
+
) -> str:
|
|
1073
|
+
"""Format an actionable missing-tag instruction for ``/article create`` or ``/article push``.
|
|
1074
|
+
|
|
1075
|
+
*context* selects the retry-wording so the create path does not ship
|
|
1076
|
+
push-specific recovery text and vice versa. The shared prelude
|
|
1077
|
+
(heading, near-matches, ``/tag list --prefix``, ``/tag create``)
|
|
1078
|
+
is identical across callers.
|
|
1079
|
+
"""
|
|
1036
1080
|
lines = [f"Tag '{tag}' not found."]
|
|
1037
1081
|
suggestions = _extract_tag_suggestions(error)
|
|
1038
1082
|
if suggestions:
|
|
1039
1083
|
lines.append(f" Near matches: {', '.join(suggestions)}")
|
|
1040
1084
|
prefix = _tag_prefix_for_list(tag)
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1085
|
+
shared = [
|
|
1086
|
+
"",
|
|
1087
|
+
f"To use an existing tag: kd /tag list --prefix {shlex.quote(prefix)}",
|
|
1088
|
+
f"To create a new tag: kd /tag create {shlex.quote(tag)}",
|
|
1089
|
+
]
|
|
1090
|
+
if context == "push":
|
|
1091
|
+
recovery = [
|
|
1046
1092
|
f"To retry push without this tag: omit --tag {shlex.quote(tag)}",
|
|
1047
1093
|
"",
|
|
1048
1094
|
"If you are still curating the tag set, push without --tag and apply tags",
|
|
1049
1095
|
f"separately with kd /tag add ARTICLE-ID {shlex.quote(tag)}.",
|
|
1050
1096
|
]
|
|
1051
|
-
|
|
1097
|
+
else:
|
|
1098
|
+
recovery = [
|
|
1099
|
+
f"To retry create without this tag: omit --tag {shlex.quote(tag)} from kd /article create",
|
|
1100
|
+
"",
|
|
1101
|
+
"If you are still curating the tag set, create the article without --tag and apply",
|
|
1102
|
+
f"tags separately with kd /tag add ARTICLE-ID {shlex.quote(tag)}.",
|
|
1103
|
+
]
|
|
1104
|
+
lines.extend(shared + recovery)
|
|
1052
1105
|
return "\n".join(lines)
|
|
1053
1106
|
|
|
1054
1107
|
|
|
@@ -1126,9 +1179,21 @@ class ArticleOperations:
|
|
|
1126
1179
|
|
|
1127
1180
|
When *parent* is given (readable id like ``FB-A-1`` or internal id),
|
|
1128
1181
|
the new article is attached to that parent in a single POST.
|
|
1129
|
-
When *tags* is given,
|
|
1182
|
+
When *tags* is given, tag existence is validated **before** the
|
|
1183
|
+
article write so an unknown tag aborts with an actionable error
|
|
1184
|
+
instead of creating an orphan article that then fails on
|
|
1185
|
+
attachment. After every applied tag, the article is re-fetched
|
|
1186
|
+
so the returned :class:`Article` reflects post-tag state and
|
|
1187
|
+
matches a subsequent ``/article show`` on the returned ID.
|
|
1130
1188
|
"""
|
|
1131
1189
|
self._debug.info("api", f"creating article in project {project}")
|
|
1190
|
+
if tags:
|
|
1191
|
+
preflight_failed = self._preflight_tags(tags, context="create")
|
|
1192
|
+
if preflight_failed:
|
|
1193
|
+
raise KdError(
|
|
1194
|
+
"Article create aborted because tagging failed.\n\n"
|
|
1195
|
+
+ "\n\n".join(preflight_failed)
|
|
1196
|
+
)
|
|
1132
1197
|
project_obj = self._yt.get_project(project)
|
|
1133
1198
|
parent_ref: IdEntity | None = None
|
|
1134
1199
|
if parent is not None:
|
|
@@ -1152,6 +1217,8 @@ class ArticleOperations:
|
|
|
1152
1217
|
f"Applied: {applied or '(none)'}. "
|
|
1153
1218
|
f"Failed: {', '.join(failed)}."
|
|
1154
1219
|
)
|
|
1220
|
+
if applied:
|
|
1221
|
+
result = self._articles.get_article(result.id_readable)
|
|
1155
1222
|
return result
|
|
1156
1223
|
|
|
1157
1224
|
def _apply_tags(
|
|
@@ -1182,12 +1249,14 @@ class ArticleOperations:
|
|
|
1182
1249
|
*,
|
|
1183
1250
|
create_missing: bool = False,
|
|
1184
1251
|
dry_run: bool = False,
|
|
1252
|
+
context: Literal["create", "push"] = "push",
|
|
1185
1253
|
) -> builtins.list[str]:
|
|
1186
|
-
"""Resolve tags before
|
|
1254
|
+
"""Resolve tags before ``/article create`` or ``/article push`` writes.
|
|
1187
1255
|
|
|
1188
|
-
Returns actionable failure blocks.
|
|
1189
|
-
missing
|
|
1190
|
-
|
|
1256
|
+
Returns actionable failure blocks. *context* selects the retry-wording
|
|
1257
|
+
for missing-tag recovery so create and push do not share push-only
|
|
1258
|
+
prose. ``create_missing`` and ``dry_run`` are push-only flags today
|
|
1259
|
+
and are expected to stay false on the create path.
|
|
1191
1260
|
"""
|
|
1192
1261
|
if self._tag_ops is None:
|
|
1193
1262
|
raise KdError("Tag operations not available")
|
|
@@ -1206,7 +1275,7 @@ class ArticleOperations:
|
|
|
1206
1275
|
f"Tag '{tag}' not found and could not be created. {create_exc}"
|
|
1207
1276
|
)
|
|
1208
1277
|
continue
|
|
1209
|
-
failed.append(_missing_tag_resolution(tag, exc))
|
|
1278
|
+
failed.append(_missing_tag_resolution(tag, exc, context=context))
|
|
1210
1279
|
except KdError as exc:
|
|
1211
1280
|
failed.append(f"Tag '{tag}' could not be resolved. {exc}")
|
|
1212
1281
|
return failed
|
|
@@ -1631,12 +1700,28 @@ class TagOperations:
|
|
|
1631
1700
|
tag_id = self._resolve_tag(name)
|
|
1632
1701
|
return self._tags.get_tag(tag_id)
|
|
1633
1702
|
|
|
1634
|
-
def create(self, name: str) ->
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1703
|
+
def create(self, name: str) -> TagCreateResult:
|
|
1704
|
+
"""Idempotent tag creation.
|
|
1705
|
+
|
|
1706
|
+
Pre-checks existence via :meth:`_resolve_tag` — the same resolver
|
|
1707
|
+
``/tag show`` uses, so case-insensitive by construction — and
|
|
1708
|
+
short-circuits with ``action="exists"`` if a tag by that name is
|
|
1709
|
+
already visible. Otherwise POSTs and returns ``action="create"``.
|
|
1710
|
+
``Tag.name-is-invalid`` from the upstream POST is enriched by
|
|
1711
|
+
``kd/resources/error-messages.yaml`` into a factual diagnostic
|
|
1712
|
+
naming sharing/visibility and name-acceptance as plausible causes.
|
|
1713
|
+
"""
|
|
1714
|
+
try:
|
|
1715
|
+
tag_id = self._resolve_tag(name)
|
|
1716
|
+
except NotFoundError:
|
|
1717
|
+
self._debug.info("api", f"creating tag '{name}'")
|
|
1718
|
+
created = self._tags.create_tag(name)
|
|
1719
|
+
self._name_cache[name.lower()] = created.id
|
|
1720
|
+
self._debug.info("api", f"created tag '{name}' ({created.id})")
|
|
1721
|
+
return TagCreateResult(action="create", tag=created)
|
|
1722
|
+
self._debug.info("api", f"tag '{name}' already exists ({tag_id})")
|
|
1723
|
+
existing = self._tags.get_tag(tag_id)
|
|
1724
|
+
return TagCreateResult(action="exists", tag=existing)
|
|
1640
1725
|
|
|
1641
1726
|
def rename(self, name: str, new_name: str) -> Tag:
|
|
1642
1727
|
tag_id = self._resolve_tag(name)
|
|
@@ -1697,6 +1782,14 @@ class TagOperations:
|
|
|
1697
1782
|
composes the OR query (``tag: A or tag: B``). The single-tag fast
|
|
1698
1783
|
path is unaffected by ``match`` because one tag is itself the same
|
|
1699
1784
|
set regardless of combinator.
|
|
1785
|
+
|
|
1786
|
+
Both dispatch paths unconditionally project ``tags(id,name)`` on
|
|
1787
|
+
issue rows — the fast-path field-set (``_TAGGED_ISSUE_FIELDS`` in
|
|
1788
|
+
:mod:`kd.api.tags`) includes it, and the composed-query call
|
|
1789
|
+
passes ``with_tags=True`` to :meth:`YouTrackAPI.list_issues`.
|
|
1790
|
+
Keeps ``/tag entities`` discovery rows in parity with
|
|
1791
|
+
``/issue show --rich`` and ``/issue list --with-tags`` so
|
|
1792
|
+
tag-matched rows expose which tags caused the match.
|
|
1700
1793
|
"""
|
|
1701
1794
|
tag_names = [tags] if isinstance(tags, str) else list(tags)
|
|
1702
1795
|
if not tag_names:
|
|
@@ -1710,7 +1803,7 @@ class TagOperations:
|
|
|
1710
1803
|
if project is not None:
|
|
1711
1804
|
query += f" project: {youtrack_query_literal(project)}"
|
|
1712
1805
|
self._debug.info("api", f"composing multi-tag issue query ({match}): {query}")
|
|
1713
|
-
return self._yt.list_issues(query=query, top=top, skip=skip)
|
|
1806
|
+
return self._yt.list_issues(query=query, top=top, skip=skip, with_tags=True)
|
|
1714
1807
|
|
|
1715
1808
|
def articles(
|
|
1716
1809
|
self,
|
|
@@ -24,7 +24,7 @@ from kd.models.saved_query import (
|
|
|
24
24
|
SavedQueryUpdate,
|
|
25
25
|
)
|
|
26
26
|
from kd.models.search import SearchResult
|
|
27
|
-
from kd.models.tag import Tag, TagColor
|
|
27
|
+
from kd.models.tag import Tag, TagColor, TagCreateResult
|
|
28
28
|
from kd.models.tag_entities import EntityRef, KindMeta, TagEntitiesResult
|
|
29
29
|
from kd.models.user import Group, ProjectTeam, User, UserCreate
|
|
30
30
|
from kd.models.work_item import (
|
|
@@ -68,6 +68,7 @@ __all__ = [
|
|
|
68
68
|
"SearchResult",
|
|
69
69
|
"Tag",
|
|
70
70
|
"TagColor",
|
|
71
|
+
"TagCreateResult",
|
|
71
72
|
"TagEntitiesResult",
|
|
72
73
|
"TimeTrackingSettings",
|
|
73
74
|
"User",
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
5
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
6
8
|
|
|
7
9
|
from kd.models.common import IdEntity, NamedEntity
|
|
@@ -28,3 +30,18 @@ class Tag(IdEntity):
|
|
|
28
30
|
owner: NamedEntity | None = None
|
|
29
31
|
color: TagColor | None = None
|
|
30
32
|
untag_on_resolve: bool = Field(default=False, alias="untagOnResolve")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TagCreateResult(BaseModel):
|
|
36
|
+
"""Result of an idempotent tag-create call.
|
|
37
|
+
|
|
38
|
+
``action`` is ``"create"`` when YouTrack minted a new tag and
|
|
39
|
+
``"exists"`` when the pre-check resolved a tag with the requested
|
|
40
|
+
name (short-circuit, no POST). The ``tag`` field carries the current
|
|
41
|
+
upstream state either way.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
45
|
+
|
|
46
|
+
action: Literal["create", "exists"]
|
|
47
|
+
tag: Tag
|
|
@@ -4,6 +4,29 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.5.1] — 2026-04-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `kd /issue create --field NAME=VALUE` and `kd /issue update --field NAME=VALUE` now accept **string custom fields**. Before: `kd /issue update VIS-21 --field Milestone=M039` rejected with `Error: Unsupported field type 'SimpleProjectCustomField' for field 'Milestone'`, blocking workflows that track freeform per-issue metadata such as a rolling milestone string. `--field "NAME="` clears an optional string field just like it clears enum/user fields today; required string fields reject empty values with the same client-side error shipped in 2.2.2. Whitespace inside the value is preserved — `--field "Milestone= M 039 "` sends ` M 039 ` verbatim. Only the field **name** is trimmed (KDCLI-106).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Unsupported custom-field errors now name both identifiers and the cause. Before: `Error: Unsupported field type 'X' for field 'Y'`. Now: `Error: Unsupported custom field type 'X' with subtype 'Y' for field 'Z'. kd does not know how to build a safe value payload for this YouTrack field family.` Applies to YouTrack text fields, multi-arity enum/user variants, and integer/float/date simple-field subtypes — paste the message into a bug report and the two type identifiers are already there (KDCLI-106).
|
|
14
|
+
|
|
15
|
+
## [2.5.0] — 2026-04-19
|
|
16
|
+
|
|
17
|
+
Dogfooding friction fixes across tag and article workflows, plus self-describing `/system info` output for bug reports. Focus of this release: the taxonomy and article-create loops now behave the way field use expects, and a paste of `kd /system info -o yaml` replaces hand-typed version footers.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `kd /system info` now carries three additional fields alongside the existing connection identity: `kd_version` (matches `kd --version`), `python_version`, and `platform`. Paste `kd /system info -o yaml` into a bug report to tie an observed behavior to a specific build without hand-typing the version. The four existing keys (`url`, `user`, `name`, `email`) are unchanged; redact `url` / `email` before sharing externally (KDCLI-104).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `kd /tag create TAG_NAME` is now idempotent and returns a `{ok, action, tag}` envelope with `action` being either `create` or `exists`. Re-running create on an already-visible tag is safe and no longer surfaces `Tag.name-is-invalid`. **Breaking vs. the prior flat tag object** — downstream scripts and SDK consumers that parsed the top-level tag fields must now read from the nested `tag:` block. Python SDK: the `create(...)` method on the tag operations client returns a `TagCreateResult(action, tag)` object; unpack `.tag` to reach the tag fields (KDCLI-105).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `kd /tag create TAG_NAME` against an already-existing tag no longer emits the raw upstream `Tag.name-is-invalid` error. When the POST still fails (genuinely unacceptable name, or a tag hidden by sharing/visibility that the caller cannot see), the error names both plausible causes without inventing a character grammar (KDCLI-105).
|
|
27
|
+
- `kd /tag entities` structured output (`-o yaml` / `-o json`, including `--primer-scope`) now populates the `tags` field on issue rows, bringing the verb to parity with `/issue show --rich` and `/issue list --with-tags`. Before: structured issue rows reported `tags: []` even when they matched via a pinned tag, forcing a second `/issue show` round-trip to see which tag caused the match. Table output is unchanged and still omits the tag column by design (KDCLI-100).
|
|
28
|
+
- `kd /article create --tag TAG` structured output now returns the post-tag article state: the response `tags` field matches a subsequent `kd /article show` on the returned id. Missing tags are rejected **before** the article is created, so a typo in `--tag` no longer leaves an untagged article behind; the error names `kd /tag create` for the missing tag, retrying `kd /article create` without the tag, or applying the tag after create via `kd /tag add ARTICLE-ID TAG`. Before: the CLI rendered `tags: ''` even when tags were attached upstream (KDCLI-101).
|
|
29
|
+
|
|
7
30
|
## [2.4.1] — 2026-04-19
|
|
8
31
|
|
|
9
32
|
### Fixed
|
|
@@ -70,6 +70,17 @@
|
|
|
70
70
|
message: 'Issue {id} not found'
|
|
71
71
|
suggestion: 'Check the issue ID and project key.'
|
|
72
72
|
|
|
73
|
+
# --- Tag create 400 (path-scoped so rename POSTs are unaffected) ---
|
|
74
|
+
|
|
75
|
+
- status: 400
|
|
76
|
+
path: '/tags$'
|
|
77
|
+
pattern: 'Tag\.name-is-invalid'
|
|
78
|
+
message: 'YouTrack rejected the tag name.'
|
|
79
|
+
suggestion: >-
|
|
80
|
+
The tag may already exist but be hidden by sharing or visibility
|
|
81
|
+
settings, or the name may contain characters YouTrack does not accept.
|
|
82
|
+
Verify with 'kd /tag show TAG_NAME' or try a different name.
|
|
83
|
+
|
|
73
84
|
# --- Query parse errors ---
|
|
74
85
|
|
|
75
86
|
- pattern: "(?i)can.t parse search query"
|
|
@@ -108,6 +108,8 @@ kd /project/field list KDCLI
|
|
|
108
108
|
|
|
109
109
|
Clear optional custom fields with an empty value, for example `kd /issue update KDCLI-42 --field "Milestone="`. Required fields reject empty values before any upstream write.
|
|
110
110
|
|
|
111
|
+
`--field` supports bundle families (enum, state, owned, version, build), user fields, period, and string `SimpleProjectCustomField` subtypes; whitespace inside the value is preserved. Unsupported families (text, `Multi*` enum/user variants, and `SimpleProjectCustomField` with `integer` / `float` / `date` subtypes) fail client-side with a message naming both the project `$type` and the `fieldType.id` subtype so a follow-on bug report can skip the probe.
|
|
112
|
+
|
|
111
113
|
Attachment metadata download URLs may start with `/api/`. kd handles this internally on download commands; do not assume the raw URL can be appended unchanged to an API base URL in custom scripts.
|
|
112
114
|
|
|
113
115
|
Tag existence, tag visibility, and tag sharing rights are different concerns. A visible tag may not be usable for every operation, and sharing settings are provider-specific security posture, not just display metadata.
|
|
@@ -158,6 +160,8 @@ Do not use push for one-off web edits or broad tree migration. It is single-file
|
|
|
158
160
|
|
|
159
161
|
When pushing with `--tag`, kd validates all tags before the article write. Missing tags abort with commands to list the namespace, create the tag, retry without it, or apply the tag separately after push. `--create-tags` is the explicit opt-in for automation that should create missing tags first.
|
|
160
162
|
|
|
163
|
+
`/article create --tag` applies the same preflight: every tag must exist, missing tags abort before the article write (no orphan article on a typo), and the structured response reflects post-tag state so it matches an immediate `/article show` on the returned id. There is no `--create-tags` on `/article create` — curate the namespace with `/tag create` first, or omit `--tag` and apply tags after creation via `/tag add ARTICLE-ID TAG`.
|
|
164
|
+
|
|
161
165
|
## Tags And Discovery
|
|
162
166
|
|
|
163
167
|
Tags are the main YouTrack-native cross-entity discovery primitive. A namespaced taxonomy such as `kind:*`, `component:*`, and `outcome:*` gives both humans and LLMs a stable way to move across issues and articles.
|
|
@@ -182,6 +186,10 @@ Use `/tag entities` when the question is "what do we know about this topic acros
|
|
|
182
186
|
|
|
183
187
|
Do not treat `--primer-scope` as implicit global state. It is a deliberate flag for content-management triage and workspace orientation. When you need a precise topical intersection, pass the tags explicitly.
|
|
184
188
|
|
|
189
|
+
`/tag entities` structured output (`-o yaml` / `-o json`) populates the `tags` field on both issue and article rows with the entity's visible tags, so a primer-scope or explicit-tag match is self-describing — no second `/issue show` round-trip is needed to confirm which tags caused a row to surface. Table output groups by kind and does not render a tag column.
|
|
190
|
+
|
|
191
|
+
`/tag create TAG_NAME` is idempotent. It resolves the name first (same case-insensitive resolver `/tag show` uses) and returns `{ok, action, tag}` with `action: exists` when the tag is already visible, or `action: create` after a successful POST. Taxonomy bootstrapping and fixture setup can re-run the command safely without decoding `Tag.name-is-invalid`. When the POST still fails — either a genuinely unacceptable name or an existing tag hidden by sharing/visibility — kd surfaces a factual diagnostic naming both plausible causes rather than the raw upstream error.
|
|
192
|
+
|
|
185
193
|
## Output And Debugging
|
|
186
194
|
|
|
187
195
|
For deterministic machine consumption, always pass an explicit output format:
|
|
@@ -211,6 +219,8 @@ kd /system/config which primer_show.output
|
|
|
211
219
|
|
|
212
220
|
If a command exits 0 but includes `warnings` in JSON or YAML, keep the primary result and inspect the warning code. For example, `tag_not_found` on `/primer/tag add` means the local primer changed but the upstream tag did not resolve at validation time.
|
|
213
221
|
|
|
222
|
+
When filing a kd bug, include the output of `kd /system info -o yaml`. It names the active connection alongside `kd_version`, `python_version`, and `platform`, so the report is self-describing without hand-typing the version. Redact `url` / `email` before sharing externally.
|
|
223
|
+
|
|
214
224
|
## Where To Go Next
|
|
215
225
|
|
|
216
226
|
Use these sources together:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|