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.
Files changed (78) hide show
  1. {kd_cli-2.2.2/kd_cli.egg-info → kd_cli-2.3.0}/PKG-INFO +1 -1
  2. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/article/__init__.py +12 -1
  3. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/client.py +88 -1
  4. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/CHANGELOG.md +8 -0
  5. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/operating-guide.md +3 -0
  6. {kd_cli-2.2.2 → kd_cli-2.3.0/kd_cli.egg-info}/PKG-INFO +1 -1
  7. {kd_cli-2.2.2 → kd_cli-2.3.0}/pyproject.toml +1 -1
  8. {kd_cli-2.2.2 → kd_cli-2.3.0}/LICENSE +0 -0
  9. {kd_cli-2.2.2 → kd_cli-2.3.0}/MANIFEST.in +0 -0
  10. {kd_cli-2.2.2 → kd_cli-2.3.0}/README.md +0 -0
  11. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/__init__.py +0 -0
  12. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/__init__.py +0 -0
  13. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/articles.py +0 -0
  14. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/http.py +0 -0
  15. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/hub.py +0 -0
  16. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/saved_queries.py +0 -0
  17. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/tags.py +0 -0
  18. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/api/youtrack.py +0 -0
  19. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/article_marker.py +0 -0
  20. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/auth.py +0 -0
  21. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/__init__.py +0 -0
  22. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/__init__.py +0 -0
  23. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/command_base.py +0 -0
  24. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/command_parser.py +0 -0
  25. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/namespaces.py +0 -0
  26. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/core/option_registry.py +0 -0
  27. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/input.py +0 -0
  28. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/main.py +0 -0
  29. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/__init__.py +0 -0
  30. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/alias/__init__.py +0 -0
  31. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/init.py +0 -0
  32. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/issue/__init__.py +0 -0
  33. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/primer/__init__.py +0 -0
  34. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/project/__init__.py +0 -0
  35. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/query/__init__.py +0 -0
  36. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/release_notes.py +0 -0
  37. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/search/__init__.py +0 -0
  38. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/system/__init__.py +0 -0
  39. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/tag/__init__.py +0 -0
  40. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/time/__init__.py +0 -0
  41. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/namespaces/user/__init__.py +0 -0
  42. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/output.py +0 -0
  43. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/cli/tree.py +0 -0
  44. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/config.py +0 -0
  45. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/config_read.py +0 -0
  46. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/config_schema.py +0 -0
  47. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/connection.py +0 -0
  48. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/debug.py +0 -0
  49. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/exceptions.py +0 -0
  50. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/history.py +0 -0
  51. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/__init__.py +0 -0
  52. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/article.py +0 -0
  53. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/assist.py +0 -0
  54. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/attachment.py +0 -0
  55. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/comment.py +0 -0
  56. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/common.py +0 -0
  57. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/issue.py +0 -0
  58. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/project.py +0 -0
  59. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/saved_query.py +0 -0
  60. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/search.py +0 -0
  61. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/tag.py +0 -0
  62. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/tag_entities.py +0 -0
  63. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/user.py +0 -0
  64. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/models/work_item.py +0 -0
  65. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/operating_guide.py +0 -0
  66. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/primer.py +0 -0
  67. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/query_parser.py +0 -0
  68. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/release_notes.py +0 -0
  69. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/__init__.py +0 -0
  70. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/command-synonyms.yaml +0 -0
  71. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/config-template.yaml +0 -0
  72. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd/resources/error-messages.yaml +0 -0
  73. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/SOURCES.txt +0 -0
  74. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/dependency_links.txt +0 -0
  75. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/entry_points.txt +0 -0
  76. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/requires.txt +0 -0
  77. {kd_cli-2.2.2 → kd_cli-2.3.0}/kd_cli.egg-info/top_level.txt +0 -0
  78. {kd_cli-2.2.2 → kd_cli-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -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, KdError
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kd-cli"
7
- version = "2.2.2"
7
+ version = "2.3.0"
8
8
  description = "Knowledge Dispatcher for issue trackers and knowledge bases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
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