kd-cli 2.2.2__tar.gz → 2.3.0__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.2/kd_cli.egg-info → kd_cli-2.3.0}/PKG-INFO +1 -1
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/article/__init__.py +12 -1
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/client.py +88 -1
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/CHANGELOG.md +8 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/operating-guide.md +3 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.2.2 → kd_cli-2.3.0}/pyproject.toml +1 -1
- {kd_cli-2.2.2 → kd_cli-2.3.0}/LICENSE +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/MANIFEST.in +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/README.md +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/articles.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/http.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/hub.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/tags.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/youtrack.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/article_marker.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/auth.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/input.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/main.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/output.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/tree.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/config.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/config_read.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/config_schema.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/connection.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/debug.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/exceptions.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/history.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/article.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/assist.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/attachment.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/comment.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/common.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/issue.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/project.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/saved_query.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/search.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/tag.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/user.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/work_item.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/operating_guide.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/primer.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/query_parser.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/release_notes.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/__init__.py +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.2.2 → kd_cli-2.3.0}/setup.cfg +0 -0
|
@@ -181,7 +181,7 @@ class ArticlePushCommand(Command):
|
|
|
181
181
|
requires_client=True,
|
|
182
182
|
usage=(
|
|
183
183
|
"kd /article push FILE --project P --title T "
|
|
184
|
-
"[--slug S] [--parent ID] [--tag T] [--force] [--dry-run]"
|
|
184
|
+
"[--slug S] [--parent ID] [--tag T] [--create-tags] [--force] [--dry-run]"
|
|
185
185
|
),
|
|
186
186
|
long_description=(
|
|
187
187
|
"Push a local markdown file to YouTrack as a knowledge base "
|
|
@@ -201,6 +201,7 @@ class ArticlePushCommand(Command):
|
|
|
201
201
|
'kd /article push guide.md --project FB --title "User Guide" --parent FB-A-1',
|
|
202
202
|
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide"',
|
|
203
203
|
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --tag "platform:rdk-e"',
|
|
204
|
+
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --create-tags',
|
|
204
205
|
'kd /article push guide.md --project FB --title "Guide" --dry-run',
|
|
205
206
|
'kd /article push guide.md --project FB --title "Guide" --force',
|
|
206
207
|
],
|
|
@@ -264,6 +265,15 @@ class ArticlePushCommand(Command):
|
|
|
264
265
|
help_text="Resolve and validate, but do not write anywhere",
|
|
265
266
|
)
|
|
266
267
|
)
|
|
268
|
+
self.register_option(
|
|
269
|
+
OptionDeclaration(
|
|
270
|
+
name="create-tags",
|
|
271
|
+
layer=OptionLayer.COMMAND,
|
|
272
|
+
type=bool,
|
|
273
|
+
default=False,
|
|
274
|
+
help_text="Create missing tags before pushing",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
267
277
|
|
|
268
278
|
def execute(
|
|
269
279
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -289,6 +299,7 @@ class ArticlePushCommand(Command):
|
|
|
289
299
|
force=bool(options.command.get("force")),
|
|
290
300
|
dry_run=bool(options.command.get("dry-run")),
|
|
291
301
|
tags=tags or None,
|
|
302
|
+
create_tags=bool(options.command.get("create-tags")),
|
|
292
303
|
)
|
|
293
304
|
return {
|
|
294
305
|
"ok": True,
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import builtins
|
|
22
22
|
import difflib
|
|
23
|
+
import shlex
|
|
23
24
|
from collections import OrderedDict
|
|
24
25
|
from dataclasses import dataclass, field as dataclass_field
|
|
25
26
|
from datetime import datetime, timezone
|
|
@@ -43,7 +44,7 @@ from kd.article_marker import (
|
|
|
43
44
|
from kd.config import ResolvedConfig
|
|
44
45
|
from kd.connection import ConnectionContext
|
|
45
46
|
from kd.debug import DebugLog
|
|
46
|
-
from kd.exceptions import APIError, ConfigError, NotFoundError, ValidationError
|
|
47
|
+
from kd.exceptions import APIError, ConfigError, KdError, NotFoundError, ValidationError
|
|
47
48
|
from kd.query_parser import youtrack_query_literal
|
|
48
49
|
from kd.models import (
|
|
49
50
|
Article,
|
|
@@ -1013,6 +1014,44 @@ class TimeOperations:
|
|
|
1013
1014
|
raise ValidationError(f"Unknown work item type '{name}'. Available: {available}")
|
|
1014
1015
|
|
|
1015
1016
|
|
|
1017
|
+
def _tag_prefix_for_list(tag: str) -> str:
|
|
1018
|
+
"""Return the smallest useful prefix for missing-tag discovery."""
|
|
1019
|
+
if ":" in tag:
|
|
1020
|
+
return tag.split(":", 1)[0] + ":"
|
|
1021
|
+
return tag
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _extract_tag_suggestions(error: NotFoundError) -> builtins.list[str]:
|
|
1025
|
+
"""Extract close-match names from the existing tag resolver message."""
|
|
1026
|
+
message = str(error)
|
|
1027
|
+
marker = "Did you mean: "
|
|
1028
|
+
if marker not in message:
|
|
1029
|
+
return []
|
|
1030
|
+
raw = message.split(marker, 1)[1].rstrip(".?")
|
|
1031
|
+
return [part.strip() for part in raw.split(",") if part.strip()]
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _missing_tag_resolution(tag: str, error: NotFoundError) -> str:
|
|
1035
|
+
"""Format an actionable missing-tag instruction for article push."""
|
|
1036
|
+
lines = [f"Tag '{tag}' not found."]
|
|
1037
|
+
suggestions = _extract_tag_suggestions(error)
|
|
1038
|
+
if suggestions:
|
|
1039
|
+
lines.append(f" Near matches: {', '.join(suggestions)}")
|
|
1040
|
+
prefix = _tag_prefix_for_list(tag)
|
|
1041
|
+
lines.extend(
|
|
1042
|
+
[
|
|
1043
|
+
"",
|
|
1044
|
+
f"To use an existing tag: kd /tag list --prefix {shlex.quote(prefix)}",
|
|
1045
|
+
f"To create a new tag: kd /tag create {shlex.quote(tag)}",
|
|
1046
|
+
f"To retry push without this tag: omit --tag {shlex.quote(tag)}",
|
|
1047
|
+
"",
|
|
1048
|
+
"If you are still curating the tag set, push without --tag and apply tags",
|
|
1049
|
+
f"separately with kd /tag add ARTICLE-ID {shlex.quote(tag)}.",
|
|
1050
|
+
]
|
|
1051
|
+
)
|
|
1052
|
+
return "\n".join(lines)
|
|
1053
|
+
|
|
1054
|
+
|
|
1016
1055
|
class ArticleOperations:
|
|
1017
1056
|
"""High-level knowledge base article operations."""
|
|
1018
1057
|
|
|
@@ -1137,6 +1176,41 @@ class ArticleOperations:
|
|
|
1137
1176
|
failed.append(f"{tag} ({exc})")
|
|
1138
1177
|
return applied, failed
|
|
1139
1178
|
|
|
1179
|
+
def _preflight_tags(
|
|
1180
|
+
self,
|
|
1181
|
+
tags: builtins.list[str],
|
|
1182
|
+
*,
|
|
1183
|
+
create_missing: bool = False,
|
|
1184
|
+
dry_run: bool = False,
|
|
1185
|
+
) -> builtins.list[str]:
|
|
1186
|
+
"""Resolve tags before mutating an article.
|
|
1187
|
+
|
|
1188
|
+
Returns actionable failure blocks. When ``create_missing`` is true,
|
|
1189
|
+
missing tags are created before the article write; ``dry_run`` never
|
|
1190
|
+
writes tags and treats missing tags as would-create.
|
|
1191
|
+
"""
|
|
1192
|
+
if self._tag_ops is None:
|
|
1193
|
+
raise KdError("Tag operations not available")
|
|
1194
|
+
failed: builtins.list[str] = []
|
|
1195
|
+
for tag in tags:
|
|
1196
|
+
try:
|
|
1197
|
+
self._tag_ops.get(tag)
|
|
1198
|
+
except NotFoundError as exc:
|
|
1199
|
+
if create_missing:
|
|
1200
|
+
if dry_run:
|
|
1201
|
+
continue
|
|
1202
|
+
try:
|
|
1203
|
+
self._tag_ops.create(tag)
|
|
1204
|
+
except KdError as create_exc:
|
|
1205
|
+
failed.append(
|
|
1206
|
+
f"Tag '{tag}' not found and could not be created. {create_exc}"
|
|
1207
|
+
)
|
|
1208
|
+
continue
|
|
1209
|
+
failed.append(_missing_tag_resolution(tag, exc))
|
|
1210
|
+
except KdError as exc:
|
|
1211
|
+
failed.append(f"Tag '{tag}' could not be resolved. {exc}")
|
|
1212
|
+
return failed
|
|
1213
|
+
|
|
1140
1214
|
def update(
|
|
1141
1215
|
self,
|
|
1142
1216
|
article_id: str,
|
|
@@ -1160,6 +1234,7 @@ class ArticleOperations:
|
|
|
1160
1234
|
force: bool = False,
|
|
1161
1235
|
dry_run: bool = False,
|
|
1162
1236
|
tags: builtins.list[str] | None = None,
|
|
1237
|
+
create_tags: bool = False,
|
|
1163
1238
|
) -> PushResult:
|
|
1164
1239
|
"""Idempotently upsert a local markdown file as a YouTrack article.
|
|
1165
1240
|
|
|
@@ -1278,6 +1353,18 @@ class ArticleOperations:
|
|
|
1278
1353
|
f"move {existing_remote.id_readable} --parent {parent}."
|
|
1279
1354
|
)
|
|
1280
1355
|
|
|
1356
|
+
if tags:
|
|
1357
|
+
preflight_failed = self._preflight_tags(
|
|
1358
|
+
tags,
|
|
1359
|
+
create_missing=create_tags,
|
|
1360
|
+
dry_run=dry_run,
|
|
1361
|
+
)
|
|
1362
|
+
if preflight_failed:
|
|
1363
|
+
raise KdError(
|
|
1364
|
+
f"Article {'update' if is_update else 'create'} aborted because "
|
|
1365
|
+
"tagging failed.\n\n" + "\n\n".join(preflight_failed)
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1281
1368
|
# Dry-run bails out here with all validation done but no writes.
|
|
1282
1369
|
if dry_run:
|
|
1283
1370
|
existing_sec = 0
|
|
@@ -4,6 +4,14 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.3.0] — 2026-04-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `/article push --create-tags` explicitly creates missing tags before pushing and attaching them, for scripted flows that own their tag namespace (KDCLI-85).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `/article push --tag` missing-tag failures now print actionable recovery commands instead of a bare not-found / close-match message (KDCLI-85).
|
|
14
|
+
|
|
7
15
|
## [2.2.2] — 2026-04-18
|
|
8
16
|
|
|
9
17
|
### Fixed
|
|
@@ -80,6 +80,7 @@ kd /issue update KDCLI-42 --state Fixed -o yaml
|
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
For article push, use `--dry-run` before a risky write and inspect conflicts with `/article pull`.
|
|
83
|
+
`/article push --tag` rejects missing tags by default and prints the concrete recovery commands. Use `--create-tags` only for scripted flows that already own the tag namespace.
|
|
83
84
|
|
|
84
85
|
## YouTrack Gotchas
|
|
85
86
|
|
|
@@ -143,6 +144,8 @@ Use `/article show` for metadata plus rendered content. Use `/article pull` when
|
|
|
143
144
|
|
|
144
145
|
Do not use push for one-off web edits or broad tree migration. It is single-file by design. If the remote changed, pull, diff, merge by hand, then push again. Use `--force` only after inspecting the remote state.
|
|
145
146
|
|
|
147
|
+
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.
|
|
148
|
+
|
|
146
149
|
## Tags And Discovery
|
|
147
150
|
|
|
148
151
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|