kd-cli 2.1.4__tar.gz → 2.1.5__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.1.4/kd_cli.egg-info → kd_cli-2.1.5}/PKG-INFO +1 -1
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/core/command_base.py +0 -2
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/core/option_registry.py +1 -1
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/main.py +1 -8
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/primer/__init__.py +50 -9
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/output.py +23 -6
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/resources/CHANGELOG.md +10 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.1.4 → kd_cli-2.1.5}/pyproject.toml +1 -1
- {kd_cli-2.1.4 → kd_cli-2.1.5}/LICENSE +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/MANIFEST.in +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/README.md +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/articles.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/http.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/hub.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/tags.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/api/youtrack.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/article_marker.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/auth.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/input.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/article/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/cli/tree.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/client.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/config.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/config_schema.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/connection.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/debug.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/exceptions.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/history.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/article.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/assist.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/attachment.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/comment.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/common.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/issue.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/project.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/saved_query.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/search.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/tag.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/user.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/models/work_item.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/primer.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/query_parser.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/release_notes.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/resources/__init__.py +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.1.4 → kd_cli-2.1.5}/setup.cfg +0 -0
|
@@ -114,8 +114,6 @@ class Command(ABC):
|
|
|
114
114
|
if opt.aliases:
|
|
115
115
|
alias_str = ", ".join(f"--{a}" for a in opt.aliases)
|
|
116
116
|
flag += f" (alias: {alias_str})"
|
|
117
|
-
if opt.choices:
|
|
118
|
-
flag += f" ({', '.join(opt.choices)})"
|
|
119
117
|
if opt.help_text:
|
|
120
118
|
flag += f" {opt.help_text}"
|
|
121
119
|
lines.append(flag)
|
|
@@ -253,7 +253,7 @@ OUTPUT_OPTIONS: list[OptionDeclaration] = [
|
|
|
253
253
|
layer=OptionLayer.OUTPUT,
|
|
254
254
|
type=str,
|
|
255
255
|
choices=["table", "json", "yaml", "md"],
|
|
256
|
-
help_text="Output format
|
|
256
|
+
help_text="Output format: table|json|yaml|md (default: table)",
|
|
257
257
|
),
|
|
258
258
|
OptionDeclaration(
|
|
259
259
|
name="fields",
|
|
@@ -503,8 +503,6 @@ def _show_reference(
|
|
|
503
503
|
flag += f"/-{decl.short_name}"
|
|
504
504
|
if decl.type is not bool:
|
|
505
505
|
flag += f" {decl.name.upper()}"
|
|
506
|
-
if decl.choices:
|
|
507
|
-
flag += f" ({'/'.join(decl.choices)})"
|
|
508
506
|
parts.append(flag)
|
|
509
507
|
print(f"Global options: {', '.join(parts)}")
|
|
510
508
|
print()
|
|
@@ -539,8 +537,6 @@ def _print_namespace_reference(ns: Namespace, depth: int = 0) -> None:
|
|
|
539
537
|
flag = f"--{opt.name}"
|
|
540
538
|
if opt.short_name:
|
|
541
539
|
flag += f"/-{opt.short_name}"
|
|
542
|
-
if opt.choices:
|
|
543
|
-
flag += f" ({', '.join(opt.choices)})"
|
|
544
540
|
line = f" {flag}"
|
|
545
541
|
if opt.help_text:
|
|
546
542
|
line += f" {opt.help_text}"
|
|
@@ -621,8 +617,5 @@ def _show_discovery(namespace: Namespace, *, file: Any = None) -> None:
|
|
|
621
617
|
flag = f"--{decl.name}"
|
|
622
618
|
if decl.short_name:
|
|
623
619
|
flag += f", -{decl.short_name}"
|
|
624
|
-
|
|
625
|
-
if decl.choices:
|
|
626
|
-
extra = f" ({', '.join(decl.choices)})"
|
|
627
|
-
print(f" {flag:<16}{decl.help_text}{extra}", file=file)
|
|
620
|
+
print(f" {flag:<16}{decl.help_text}", file=file)
|
|
628
621
|
print(file=file)
|
|
@@ -10,7 +10,12 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from kd.cli.core.command_base import Command, CommandResult
|
|
12
12
|
from kd.cli.core.namespaces import Namespace
|
|
13
|
-
from kd.cli.core.option_registry import
|
|
13
|
+
from kd.cli.core.option_registry import (
|
|
14
|
+
OptionDeclaration,
|
|
15
|
+
OptionLayer,
|
|
16
|
+
ResolvedOptions,
|
|
17
|
+
project_option,
|
|
18
|
+
)
|
|
14
19
|
from kd.cli.input import resolve_text_input
|
|
15
20
|
from kd.exceptions import ValidationError
|
|
16
21
|
from kd.primer import (
|
|
@@ -25,6 +30,26 @@ from kd.primer import (
|
|
|
25
30
|
)
|
|
26
31
|
|
|
27
32
|
|
|
33
|
+
def _assert_workspace_project(command: Command, options: ResolvedOptions) -> None:
|
|
34
|
+
"""Enforce the ``--project`` assertion on workspace-scoped primer commands.
|
|
35
|
+
|
|
36
|
+
The primer lives in ``.kd/primer.yaml`` and has no project dimension.
|
|
37
|
+
``--project`` is accepted as an assertion of which workspace's primer the
|
|
38
|
+
caller thinks it's editing: matches ``default_project`` → silent no-op;
|
|
39
|
+
mismatches → raise so an agent running from the wrong workspace fails loud.
|
|
40
|
+
"""
|
|
41
|
+
asserted = options.command.get("project")
|
|
42
|
+
if not asserted:
|
|
43
|
+
return
|
|
44
|
+
current = command.config.default_project
|
|
45
|
+
if current and asserted != current:
|
|
46
|
+
raise ValidationError(
|
|
47
|
+
f"Primer is workspace-scoped. --project={asserted} does not match "
|
|
48
|
+
f"this workspace's default_project={current}. To edit {asserted}'s "
|
|
49
|
+
f"primer, run from that workspace."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
28
53
|
# ---------------------------------------------------------------------------
|
|
29
54
|
# /primer show (root default)
|
|
30
55
|
# ---------------------------------------------------------------------------
|
|
@@ -46,19 +71,16 @@ class PrimerShowCommand(Command):
|
|
|
46
71
|
],
|
|
47
72
|
see_also=["/primer/note list", "/primer/article list"],
|
|
48
73
|
)
|
|
74
|
+
self.display_hint = "primer_show"
|
|
49
75
|
|
|
50
76
|
def execute(
|
|
51
77
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
52
78
|
) -> CommandResult:
|
|
53
79
|
data = load_primer()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
result["notes"] = [{"#": i + 1, "note": n} for i, n in enumerate(data.notes)]
|
|
59
|
-
if data.articles:
|
|
60
|
-
result["articles"] = [{"id": a.id, "summary": a.summary or ""} for a in data.articles]
|
|
61
|
-
return result
|
|
80
|
+
return {
|
|
81
|
+
"notes": [{"#": i + 1, "note": n} for i, n in enumerate(data.notes)],
|
|
82
|
+
"articles": [{"id": a.id, "summary": a.summary or ""} for a in data.articles],
|
|
83
|
+
}
|
|
62
84
|
|
|
63
85
|
|
|
64
86
|
# ---------------------------------------------------------------------------
|
|
@@ -93,6 +115,7 @@ class PrimerNoteAddCommand(Command):
|
|
|
93
115
|
super().__init__(
|
|
94
116
|
"add",
|
|
95
117
|
"Add a primer note",
|
|
118
|
+
requires_config=True,
|
|
96
119
|
usage="kd /primer/note add TEXT",
|
|
97
120
|
long_description="Appends a note to the project primer. TEXT can be inline, "
|
|
98
121
|
"@file to read from a file, or - to read from stdin.",
|
|
@@ -103,9 +126,13 @@ class PrimerNoteAddCommand(Command):
|
|
|
103
126
|
see_also=["/primer/note list", "/primer/note remove"],
|
|
104
127
|
)
|
|
105
128
|
|
|
129
|
+
def _register_options(self) -> None:
|
|
130
|
+
self.register_option(project_option(help_text="Assert which project's primer this edits"))
|
|
131
|
+
|
|
106
132
|
def execute(
|
|
107
133
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
108
134
|
) -> CommandResult:
|
|
135
|
+
_assert_workspace_project(self, options)
|
|
109
136
|
if not args:
|
|
110
137
|
raise ValidationError("Usage: kd /primer/note add TEXT")
|
|
111
138
|
text = resolve_text_input(" ".join(args))
|
|
@@ -122,6 +149,7 @@ class PrimerNoteRemoveCommand(Command):
|
|
|
122
149
|
super().__init__(
|
|
123
150
|
"remove",
|
|
124
151
|
"Remove a primer note by index or all notes",
|
|
152
|
+
requires_config=True,
|
|
125
153
|
usage="kd /primer/note remove INDEX | --all",
|
|
126
154
|
long_description="Removes the note at the given 1-based index, or all notes "
|
|
127
155
|
"if --all is passed. Use '/primer/note list' to see current indices.",
|
|
@@ -142,10 +170,12 @@ class PrimerNoteRemoveCommand(Command):
|
|
|
142
170
|
help_text="Remove all notes",
|
|
143
171
|
)
|
|
144
172
|
)
|
|
173
|
+
self.register_option(project_option(help_text="Assert which project's primer this edits"))
|
|
145
174
|
|
|
146
175
|
def execute(
|
|
147
176
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
148
177
|
) -> CommandResult:
|
|
178
|
+
_assert_workspace_project(self, options)
|
|
149
179
|
if options.command.get("all"):
|
|
150
180
|
count = clear_notes()
|
|
151
181
|
return {"ok": True, "action": "clear", "removed": count}
|
|
@@ -166,6 +196,7 @@ class PrimerNoteUpdateCommand(Command):
|
|
|
166
196
|
super().__init__(
|
|
167
197
|
"update",
|
|
168
198
|
"Replace a primer note by index",
|
|
199
|
+
requires_config=True,
|
|
169
200
|
usage="kd /primer/note update INDEX TEXT",
|
|
170
201
|
long_description="Replaces the note at the given 1-based index with new text. "
|
|
171
202
|
"TEXT can be inline, @file to read from a file, or - to read from stdin. "
|
|
@@ -177,9 +208,13 @@ class PrimerNoteUpdateCommand(Command):
|
|
|
177
208
|
see_also=["/primer/note list", "/primer/note remove"],
|
|
178
209
|
)
|
|
179
210
|
|
|
211
|
+
def _register_options(self) -> None:
|
|
212
|
+
self.register_option(project_option(help_text="Assert which project's primer this edits"))
|
|
213
|
+
|
|
180
214
|
def execute(
|
|
181
215
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
182
216
|
) -> CommandResult:
|
|
217
|
+
_assert_workspace_project(self, options)
|
|
183
218
|
if not args:
|
|
184
219
|
raise ValidationError("Usage: kd /primer/note update INDEX TEXT")
|
|
185
220
|
try:
|
|
@@ -227,6 +262,7 @@ class PrimerArticleAddCommand(Command):
|
|
|
227
262
|
super().__init__(
|
|
228
263
|
"add",
|
|
229
264
|
"Pin an article to the primer",
|
|
265
|
+
requires_config=True,
|
|
230
266
|
usage="kd /primer/article add ARTICLE_ID [--summary S]",
|
|
231
267
|
long_description="Adds a YouTrack article reference to the project primer. "
|
|
232
268
|
"Use --summary to store a description alongside the ID.",
|
|
@@ -246,10 +282,12 @@ class PrimerArticleAddCommand(Command):
|
|
|
246
282
|
help_text="Description to store with the article reference",
|
|
247
283
|
)
|
|
248
284
|
)
|
|
285
|
+
self.register_option(project_option(help_text="Assert which project's primer this edits"))
|
|
249
286
|
|
|
250
287
|
def execute(
|
|
251
288
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
252
289
|
) -> CommandResult:
|
|
290
|
+
_assert_workspace_project(self, options)
|
|
253
291
|
if not args:
|
|
254
292
|
raise ValidationError("Usage: kd /primer/article add ARTICLE_ID")
|
|
255
293
|
article_id = args[0]
|
|
@@ -268,6 +306,7 @@ class PrimerArticleRemoveCommand(Command):
|
|
|
268
306
|
super().__init__(
|
|
269
307
|
"remove",
|
|
270
308
|
"Unpin an article or all articles",
|
|
309
|
+
requires_config=True,
|
|
271
310
|
usage="kd /primer/article remove ARTICLE_ID | --all",
|
|
272
311
|
long_description="Removes the article with the given ID from the primer, "
|
|
273
312
|
"or all pinned articles if --all is passed.",
|
|
@@ -288,10 +327,12 @@ class PrimerArticleRemoveCommand(Command):
|
|
|
288
327
|
help_text="Remove all pinned articles",
|
|
289
328
|
)
|
|
290
329
|
)
|
|
330
|
+
self.register_option(project_option(help_text="Assert which project's primer this edits"))
|
|
291
331
|
|
|
292
332
|
def execute(
|
|
293
333
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
294
334
|
) -> CommandResult:
|
|
335
|
+
_assert_workspace_project(self, options)
|
|
295
336
|
if options.command.get("all"):
|
|
296
337
|
count = clear_articles()
|
|
297
338
|
return {"ok": True, "action": "clear", "removed": count}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Output formatting.
|
|
2
2
|
|
|
3
3
|
Renders command results in multiple formats:
|
|
4
|
-
- table: human-readable tabular output (default
|
|
4
|
+
- table: human-readable tabular output (unconditional default)
|
|
5
5
|
- json: JSON output for piping to jq
|
|
6
6
|
- yaml: YAML output
|
|
7
7
|
- md: Markdown table
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
Default is ``table`` regardless of stdout context; TTY state does not
|
|
10
|
+
influence format (ADR-006 — TTY detection stays config-first). Consumers
|
|
11
|
+
that want JSON or YAML pass ``-o json`` / ``-o yaml`` explicitly.
|
|
11
12
|
|
|
12
13
|
Value normalization runs before serialization: epoch-ms timestamps become
|
|
13
14
|
ISO 8601, enums become their ``.value``, and nested collections render
|
|
@@ -17,7 +18,6 @@ cleanly in every format.
|
|
|
17
18
|
from __future__ import annotations
|
|
18
19
|
|
|
19
20
|
import json
|
|
20
|
-
import sys
|
|
21
21
|
from datetime import datetime, timezone
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
@@ -75,7 +75,8 @@ class OutputFormatter:
|
|
|
75
75
|
|
|
76
76
|
Args:
|
|
77
77
|
data: Structured data from a command.
|
|
78
|
-
fmt: Output format.
|
|
78
|
+
fmt: Output format. Defaults to ``"table"`` when None; TTY state
|
|
79
|
+
does not influence the default (ADR-006).
|
|
79
80
|
fields: Comma-separated field names to include.
|
|
80
81
|
display_hint: Rendering hint from the command (e.g. ``"tree"``).
|
|
81
82
|
"""
|
|
@@ -87,12 +88,28 @@ class OutputFormatter:
|
|
|
87
88
|
normalized: list[dict[str, Any]] | dict[str, Any] = self._normalize(data)
|
|
88
89
|
|
|
89
90
|
if fmt is None:
|
|
90
|
-
fmt = "table"
|
|
91
|
+
fmt = "table"
|
|
91
92
|
|
|
92
93
|
# Tree display: table/md render as pretty tree, json/yaml serialize structure.
|
|
93
94
|
if display_hint == "tree" and isinstance(normalized, dict) and fmt in ("table", "md"):
|
|
94
95
|
return self._render_tree_display(normalized)
|
|
95
96
|
|
|
97
|
+
# Primer show: table/md formats drop empty sections so a notes-only
|
|
98
|
+
# primer renders without an empty "articles" row. When both sections
|
|
99
|
+
# are empty, show a populate hint. json/yaml pass through unchanged.
|
|
100
|
+
if (
|
|
101
|
+
display_hint == "primer_show"
|
|
102
|
+
and fmt in ("table", "md")
|
|
103
|
+
and isinstance(normalized, dict)
|
|
104
|
+
):
|
|
105
|
+
if not normalized.get("notes") and not normalized.get("articles"):
|
|
106
|
+
return (
|
|
107
|
+
"Primer is empty.\n"
|
|
108
|
+
' Populate with: kd /primer/note add "<text>"\n'
|
|
109
|
+
" kd /primer/article add <ARTICLE_ID>"
|
|
110
|
+
)
|
|
111
|
+
normalized = {k: v for k, v in normalized.items() if v}
|
|
112
|
+
|
|
96
113
|
field_list = [f.strip() for f in fields.split(",")] if fields else None
|
|
97
114
|
|
|
98
115
|
match fmt:
|
|
@@ -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.1.5] — 2026-04-17
|
|
8
|
+
|
|
9
|
+
Milestone 005 — CLI friction audit, session S1. Four first-time-user surface fixes on the `--help` / `--output` axis.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `--help`, `--reference`, and per-command help now render the `--output` choices list exactly once. The literal help text carries the list and the default (`Output format: table|json|yaml|md (default: table)`); four renderer sites that previously auto-appended `(table, json, yaml, md)` a second time stopped doing so (KDCLI-73).
|
|
13
|
+
- `kd` no longer flips the default output format to JSON when stdout is a pipe. `table` is the unconditional default across TTY, pipes, and subprocess contexts, per [ADR-006](../../docs/design/ADR-006-tty-detection-config-first.md). Consumers wanting JSON or YAML pass `-o json` / `-o yaml` explicitly (KDCLI-74).
|
|
14
|
+
- `kd /primer show` on an empty primer now prints a populate hint in `table` and `md` output, instead of rendering zero bytes. JSON and YAML continue to return the canonical empty container `{"notes": [], "articles": []}` (KDCLI-76).
|
|
15
|
+
- `kd /primer/note add`, `kd /primer/article add`, and their sibling `remove` / `update` commands accept `--project/-p` as a workspace assertion. A match against the active connection's `default_project` is a silent no-op; a mismatch raises a `ValidationError` naming both the asserted and resolved keys so an agent running from the wrong workspace fails loud instead of silently writing to the wrong primer (KDCLI-70).
|
|
16
|
+
|
|
7
17
|
## [2.1.4] — 2026-04-15
|
|
8
18
|
|
|
9
19
|
### Fixed
|
|
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
|