kd-cli 2.2.0__tar.gz → 2.2.2__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.2.0/kd_cli.egg-info → kd_cli-2.2.2}/PKG-INFO +1 -1
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/youtrack.py +25 -2
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/main.py +12 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/client.py +24 -10
- kd_cli-2.2.2/kd/operating_guide.py +20 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/CHANGELOG.md +10 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/operating-guide.md +2 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/SOURCES.txt +1 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/pyproject.toml +1 -1
- {kd_cli-2.2.0 → kd_cli-2.2.2}/LICENSE +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/MANIFEST.in +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/README.md +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/articles.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/http.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/hub.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/tags.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/article_marker.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/auth.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/input.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/article/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/output.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/tree.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/config.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/config_read.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/config_schema.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/connection.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/debug.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/exceptions.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/history.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/article.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/assist.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/attachment.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/comment.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/common.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/issue.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/project.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/saved_query.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/search.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/tag.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/user.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/work_item.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/primer.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/query_parser.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/release_notes.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/__init__.py +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.2.0 → kd_cli-2.2.2}/setup.cfg +0 -0
|
@@ -24,6 +24,7 @@ from kd.models import (
|
|
|
24
24
|
Attachment,
|
|
25
25
|
Comment,
|
|
26
26
|
CommentCreate,
|
|
27
|
+
CustomField,
|
|
27
28
|
FieldSet,
|
|
28
29
|
Issue,
|
|
29
30
|
IssueCreate,
|
|
@@ -38,6 +39,28 @@ from kd.models import (
|
|
|
38
39
|
WorkItemType,
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
|
|
43
|
+
def _custom_field_payload(field: CustomField) -> dict[str, Any]:
|
|
44
|
+
payload = field.model_dump(by_alias=True, exclude_none=True)
|
|
45
|
+
if field.value is None:
|
|
46
|
+
payload["value"] = None
|
|
47
|
+
return payload
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _issue_create_payload(issue: IssueCreate) -> dict[str, Any]:
|
|
51
|
+
payload = issue.model_dump(by_alias=True, exclude_none=True)
|
|
52
|
+
if issue.custom_fields is not None:
|
|
53
|
+
payload["customFields"] = [_custom_field_payload(field) for field in issue.custom_fields]
|
|
54
|
+
return payload
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _issue_update_payload(update: IssueUpdate) -> dict[str, Any]:
|
|
58
|
+
payload = update.model_dump(by_alias=True, exclude_none=True)
|
|
59
|
+
if update.custom_fields is not None:
|
|
60
|
+
payload["customFields"] = [_custom_field_payload(field) for field in update.custom_fields]
|
|
61
|
+
return payload
|
|
62
|
+
|
|
63
|
+
|
|
41
64
|
# Default field selections for common queries
|
|
42
65
|
_PROJECT_LIST_FIELDS = str(
|
|
43
66
|
FieldSet(
|
|
@@ -226,7 +249,7 @@ class YouTrackAPI:
|
|
|
226
249
|
def create_issue(self, issue: IssueCreate) -> Issue:
|
|
227
250
|
data = self._http.post(
|
|
228
251
|
"/issues",
|
|
229
|
-
json=issue
|
|
252
|
+
json=_issue_create_payload(issue),
|
|
230
253
|
fields=_ISSUE_FIELDS,
|
|
231
254
|
)
|
|
232
255
|
return Issue.model_validate(data)
|
|
@@ -234,7 +257,7 @@ class YouTrackAPI:
|
|
|
234
257
|
def update_issue(self, issue_id: str, update: IssueUpdate) -> Issue:
|
|
235
258
|
data = self._http.post(
|
|
236
259
|
f"/issues/{issue_id}",
|
|
237
|
-
json=update
|
|
260
|
+
json=_issue_update_payload(update),
|
|
238
261
|
fields=_ISSUE_FIELDS,
|
|
239
262
|
)
|
|
240
263
|
return Issue.model_validate(data)
|
|
@@ -30,6 +30,7 @@ from kd.config import ResolvedConfig, resolve_command_defaults, resolve_config
|
|
|
30
30
|
from kd.connection import ConnectionContext, resolve_connection
|
|
31
31
|
from kd.debug import DebugLevel, DebugLog, init_debug_log
|
|
32
32
|
from kd.exceptions import KdError
|
|
33
|
+
from kd.operating_guide import load_guide
|
|
33
34
|
|
|
34
35
|
# Global options — single source of truth for extraction, help, and reference.
|
|
35
36
|
GLOBAL_OPTIONS: list[OptionDeclaration] = [
|
|
@@ -54,6 +55,13 @@ GLOBAL_OPTIONS: list[OptionDeclaration] = [
|
|
|
54
55
|
type=bool,
|
|
55
56
|
help_text="Print full command reference (LLM-friendly)",
|
|
56
57
|
),
|
|
58
|
+
OptionDeclaration(
|
|
59
|
+
name="guide",
|
|
60
|
+
short_name="G",
|
|
61
|
+
layer=OptionLayer.GLOBAL,
|
|
62
|
+
type=bool,
|
|
63
|
+
help_text="Print embedded operating guide (LLM-friendly)",
|
|
64
|
+
),
|
|
57
65
|
OptionDeclaration(
|
|
58
66
|
name="debug",
|
|
59
67
|
short_name="D",
|
|
@@ -158,6 +166,10 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
158
166
|
print(f"kd {__version__}")
|
|
159
167
|
return
|
|
160
168
|
|
|
169
|
+
if global_opts.get("guide"):
|
|
170
|
+
print(load_guide(), end="")
|
|
171
|
+
return
|
|
172
|
+
|
|
161
173
|
# Alias expansion: detect `alias <name>` and expand before parsing.
|
|
162
174
|
# Config is loaded early only when needed; stored for reuse later.
|
|
163
175
|
resolved: ResolvedConfig | None = None
|
|
@@ -279,11 +279,7 @@ class FieldSchema:
|
|
|
279
279
|
)
|
|
280
280
|
|
|
281
281
|
validated_value = self._validate_field_value(field, value)
|
|
282
|
-
field_value = (
|
|
283
|
-
{"login": validated_value}
|
|
284
|
-
if self._is_user_field(field)
|
|
285
|
-
else {"name": validated_value}
|
|
286
|
-
)
|
|
282
|
+
field_value = self._field_value_payload(field, validated_value)
|
|
287
283
|
self._debug.debug(
|
|
288
284
|
"fields",
|
|
289
285
|
f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
|
|
@@ -350,9 +346,7 @@ class FieldSchema:
|
|
|
350
346
|
f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
|
|
351
347
|
)
|
|
352
348
|
validated_value = self._validate_field_value(field, value)
|
|
353
|
-
field_value = (
|
|
354
|
-
{"login": validated_value} if self._is_user_field(field) else {"name": validated_value}
|
|
355
|
-
)
|
|
349
|
+
field_value = self._field_value_payload(field, validated_value)
|
|
356
350
|
self._debug.debug(
|
|
357
351
|
"fields",
|
|
358
352
|
f"resolved generic '{field_name}' -> field '{field.name}' "
|
|
@@ -392,16 +386,36 @@ class FieldSchema:
|
|
|
392
386
|
"""Return True if this field is a user-type field (user[1])."""
|
|
393
387
|
return field.field_type_id.startswith("user")
|
|
394
388
|
|
|
389
|
+
def _field_value_payload(
|
|
390
|
+
self,
|
|
391
|
+
field: ProjectCustomField,
|
|
392
|
+
value: str | None,
|
|
393
|
+
) -> dict[str, str] | None:
|
|
394
|
+
"""Build the YouTrack issue-field value payload.
|
|
395
|
+
|
|
396
|
+
``None`` is a deliberate clear operation and must serialize as
|
|
397
|
+
``"value": null`` in the final issue mutation payload.
|
|
398
|
+
"""
|
|
399
|
+
if value is None:
|
|
400
|
+
return None
|
|
401
|
+
return {"login": value} if self._is_user_field(field) else {"name": value}
|
|
402
|
+
|
|
395
403
|
def _validate_field_value(
|
|
396
404
|
self,
|
|
397
405
|
field: ProjectCustomField,
|
|
398
406
|
value: str,
|
|
399
|
-
) -> str:
|
|
407
|
+
) -> str | None:
|
|
400
408
|
"""Validate a value against the field's bundle.
|
|
401
409
|
|
|
402
|
-
Returns the canonical value name (case-corrected)
|
|
410
|
+
Returns the canonical value name (case-corrected), or ``None``
|
|
411
|
+
when an optional field should be cleared.
|
|
403
412
|
Raises ValidationError with allowed values on mismatch.
|
|
404
413
|
"""
|
|
414
|
+
if value == "":
|
|
415
|
+
if field.can_be_empty:
|
|
416
|
+
return None
|
|
417
|
+
raise ValidationError(f"Field '{field.name}' is required and cannot be cleared.")
|
|
418
|
+
|
|
405
419
|
if self._is_user_field(field):
|
|
406
420
|
return value # User fields: server validates against group members
|
|
407
421
|
if not field.bundle_values:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Operating guide loader.
|
|
2
|
+
|
|
3
|
+
Reads the bundled ``kd/resources/operating-guide.md`` for ``kd --guide``.
|
|
4
|
+
The guide carries durable operating doctrine (product boundary, primer
|
|
5
|
+
workflow, cost classes, YouTrack gotchas) — a complement to the
|
|
6
|
+
syntax-focused ``kd --reference``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.resources
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_guide() -> str:
|
|
15
|
+
"""Read the bundled operating guide as text."""
|
|
16
|
+
return (
|
|
17
|
+
importlib.resources.files("kd.resources")
|
|
18
|
+
.joinpath("operating-guide.md")
|
|
19
|
+
.read_text(encoding="utf-8")
|
|
20
|
+
)
|
|
@@ -4,6 +4,16 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.2.2] — 2026-04-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `kd /issue update ID --field "NAME="` now clears optional custom fields by sending `value: null` to YouTrack. Required fields reject empty values with a clear client-side error instead of the generic "invalid value" bundle message (KDCLI-81).
|
|
11
|
+
|
|
12
|
+
## [2.2.1] — 2026-04-18
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `kd --guide` / `-G` prints an embedded operating guide covering kd's product boundary, primer workflow, cost classes, and YouTrack gotchas — the doctrine complement to the syntax-focused `kd --reference`. Runs without a connection or working config, so it's safe in fresh or broken-config environments (KDCLI-94).
|
|
16
|
+
|
|
7
17
|
## [2.2.0] — 2026-04-18
|
|
8
18
|
|
|
9
19
|
Milestone 006 — tag discovery and taxonomy hygiene v1. Eight sequenced sessions that turn tags into a load-bearing cross-entity discovery primitive: `/issue` tag parity with `/article`, multi-tag intersection on `/tag issues`, the `/tag entities` cross-kind verb, `/tag list --prefix|--namespace` taxonomy introspection, and workspace-pinned tags on the primer consumed as an implicit OR scope.
|
|
@@ -97,6 +97,8 @@ Issue custom fields are project-specific and need `$type` discriminators on muta
|
|
|
97
97
|
kd /project/field list KDCLI
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
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.
|
|
101
|
+
|
|
100
102
|
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.
|
|
101
103
|
|
|
102
104
|
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.
|
|
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
|