kd-cli 2.0.4__py3-none-any.whl
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/__init__.py +45 -0
- kd/api/__init__.py +15 -0
- kd/api/articles.py +232 -0
- kd/api/http.py +234 -0
- kd/api/hub.py +69 -0
- kd/api/saved_queries.py +76 -0
- kd/api/tags.py +107 -0
- kd/api/youtrack.py +414 -0
- kd/article_marker.py +348 -0
- kd/auth.py +96 -0
- kd/cli/__init__.py +5 -0
- kd/cli/core/__init__.py +25 -0
- kd/cli/core/command_base.py +133 -0
- kd/cli/core/command_parser.py +246 -0
- kd/cli/core/namespaces.py +175 -0
- kd/cli/core/option_registry.py +265 -0
- kd/cli/input.py +42 -0
- kd/cli/main.py +612 -0
- kd/cli/namespaces/__init__.py +38 -0
- kd/cli/namespaces/alias/__init__.py +87 -0
- kd/cli/namespaces/article/__init__.py +843 -0
- kd/cli/namespaces/init.py +42 -0
- kd/cli/namespaces/issue/__init__.py +959 -0
- kd/cli/namespaces/primer/__init__.py +337 -0
- kd/cli/namespaces/project/__init__.py +243 -0
- kd/cli/namespaces/query/__init__.py +465 -0
- kd/cli/namespaces/search/__init__.py +82 -0
- kd/cli/namespaces/system/__init__.py +528 -0
- kd/cli/namespaces/tag/__init__.py +337 -0
- kd/cli/namespaces/time/__init__.py +307 -0
- kd/cli/namespaces/user/__init__.py +99 -0
- kd/cli/output.py +287 -0
- kd/cli/tree.py +65 -0
- kd/client.py +1953 -0
- kd/config.py +853 -0
- kd/connection.py +156 -0
- kd/debug.py +107 -0
- kd/exceptions.py +189 -0
- kd/history.py +169 -0
- kd/models/__init__.py +75 -0
- kd/models/article.py +64 -0
- kd/models/assist.py +47 -0
- kd/models/attachment.py +20 -0
- kd/models/comment.py +25 -0
- kd/models/common.py +39 -0
- kd/models/issue.py +67 -0
- kd/models/project.py +60 -0
- kd/models/saved_query.py +54 -0
- kd/models/search.py +20 -0
- kd/models/tag.py +30 -0
- kd/models/user.py +42 -0
- kd/models/work_item.py +59 -0
- kd/primer.py +231 -0
- kd/query_parser.py +181 -0
- kd/resources/__init__.py +1 -0
- kd/resources/command-synonyms.yaml +19 -0
- kd/resources/config-template.yaml +71 -0
- kd/resources/error-messages.yaml +101 -0
- kd_cli-2.0.4.dist-info/METADATA +149 -0
- kd_cli-2.0.4.dist-info/RECORD +64 -0
- kd_cli-2.0.4.dist-info/WHEEL +5 -0
- kd_cli-2.0.4.dist-info/entry_points.txt +2 -0
- kd_cli-2.0.4.dist-info/licenses/LICENSE +21 -0
- kd_cli-2.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Primer namespace: /primer, /primer/note, /primer/article.
|
|
2
|
+
|
|
3
|
+
Project-local knowledge capsule for LLM consumers. Stores conventions,
|
|
4
|
+
key article references, and do's/don'ts in ``.kd /primer.yaml``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from kd.cli.core.command_base import Command, CommandResult
|
|
12
|
+
from kd.cli.core.namespaces import Namespace
|
|
13
|
+
from kd.cli.core.option_registry import OptionDeclaration, OptionLayer, ResolvedOptions
|
|
14
|
+
from kd.cli.input import resolve_text_input
|
|
15
|
+
from kd.exceptions import ValidationError
|
|
16
|
+
from kd.primer import (
|
|
17
|
+
add_article,
|
|
18
|
+
add_note,
|
|
19
|
+
clear_articles,
|
|
20
|
+
clear_notes,
|
|
21
|
+
load_primer,
|
|
22
|
+
remove_article,
|
|
23
|
+
remove_note,
|
|
24
|
+
update_note,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# /primer show (root default)
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PrimerShowCommand(Command):
|
|
34
|
+
"""Show the full project primer."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
super().__init__(
|
|
38
|
+
"show",
|
|
39
|
+
"Show project primer",
|
|
40
|
+
usage="kd /primer show",
|
|
41
|
+
long_description="Displays the full project primer — notes, pinned articles, "
|
|
42
|
+
"and all other sections. Returns an empty primer if none exists yet.",
|
|
43
|
+
examples=[
|
|
44
|
+
"kd /primer show",
|
|
45
|
+
"kd /primer show --output json",
|
|
46
|
+
],
|
|
47
|
+
see_also=["/primer/note list", "/primer/article list"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def execute(
|
|
51
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
52
|
+
) -> CommandResult:
|
|
53
|
+
data = load_primer()
|
|
54
|
+
if not data.notes and not data.articles:
|
|
55
|
+
return None
|
|
56
|
+
result: dict[str, Any] = {}
|
|
57
|
+
if data.notes:
|
|
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
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# /primer/note
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PrimerNoteListCommand(Command):
|
|
70
|
+
"""List all primer notes."""
|
|
71
|
+
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
super().__init__(
|
|
74
|
+
"list",
|
|
75
|
+
"List primer notes",
|
|
76
|
+
usage="kd /primer/note list",
|
|
77
|
+
long_description="Lists all notes in the project primer with their 1-based index.",
|
|
78
|
+
examples=["kd /primer/note list"],
|
|
79
|
+
see_also=["/primer/note add", "/primer/note remove"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def execute(
|
|
83
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
84
|
+
) -> CommandResult:
|
|
85
|
+
data = load_primer()
|
|
86
|
+
return [{"index": i + 1, "note": note} for i, note in enumerate(data.notes)]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PrimerNoteAddCommand(Command):
|
|
90
|
+
"""Add a note to the primer."""
|
|
91
|
+
|
|
92
|
+
def __init__(self) -> None:
|
|
93
|
+
super().__init__(
|
|
94
|
+
"add",
|
|
95
|
+
"Add a primer note",
|
|
96
|
+
usage="kd /primer/note add TEXT",
|
|
97
|
+
long_description="Appends a note to the project primer. TEXT can be inline, "
|
|
98
|
+
"@file to read from a file, or - to read from stdin.",
|
|
99
|
+
examples=[
|
|
100
|
+
'kd /primer/note add "Sprint field is called Board"',
|
|
101
|
+
"kd /primer/note add @conventions.txt",
|
|
102
|
+
],
|
|
103
|
+
see_also=["/primer/note list", "/primer/note remove"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def execute(
|
|
107
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
108
|
+
) -> CommandResult:
|
|
109
|
+
if not args:
|
|
110
|
+
raise ValidationError("Usage: kd /primer/note add TEXT")
|
|
111
|
+
text = resolve_text_input(" ".join(args))
|
|
112
|
+
if not text:
|
|
113
|
+
raise ValidationError("Note text cannot be empty")
|
|
114
|
+
index = add_note(text)
|
|
115
|
+
return {"ok": True, "action": "add", "index": index, "note": text}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PrimerNoteRemoveCommand(Command):
|
|
119
|
+
"""Remove a note by index, or all notes with --all."""
|
|
120
|
+
|
|
121
|
+
def __init__(self) -> None:
|
|
122
|
+
super().__init__(
|
|
123
|
+
"remove",
|
|
124
|
+
"Remove a primer note by index or all notes",
|
|
125
|
+
usage="kd /primer/note remove INDEX | --all",
|
|
126
|
+
long_description="Removes the note at the given 1-based index, or all notes "
|
|
127
|
+
"if --all is passed. Use '/primer/note list' to see current indices.",
|
|
128
|
+
examples=[
|
|
129
|
+
"kd /primer/note remove 2",
|
|
130
|
+
"kd /primer/note remove --all",
|
|
131
|
+
],
|
|
132
|
+
see_also=["/primer/note list", "/primer/note add"],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _register_options(self) -> None:
|
|
136
|
+
self.register_option(
|
|
137
|
+
OptionDeclaration(
|
|
138
|
+
name="all",
|
|
139
|
+
layer=OptionLayer.COMMAND,
|
|
140
|
+
type=bool,
|
|
141
|
+
default=False,
|
|
142
|
+
help_text="Remove all notes",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def execute(
|
|
147
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
148
|
+
) -> CommandResult:
|
|
149
|
+
if options.command.get("all"):
|
|
150
|
+
count = clear_notes()
|
|
151
|
+
return {"ok": True, "action": "clear", "removed": count}
|
|
152
|
+
if not args:
|
|
153
|
+
raise ValidationError("Usage: kd /primer/note remove INDEX | --all")
|
|
154
|
+
try:
|
|
155
|
+
index = int(args[0])
|
|
156
|
+
except ValueError:
|
|
157
|
+
raise ValidationError(f"INDEX must be a number, got: {args[0]}") from None
|
|
158
|
+
removed = remove_note(index)
|
|
159
|
+
return {"ok": True, "action": "remove", "index": index, "note": removed}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PrimerNoteUpdateCommand(Command):
|
|
163
|
+
"""Replace a note by index."""
|
|
164
|
+
|
|
165
|
+
def __init__(self) -> None:
|
|
166
|
+
super().__init__(
|
|
167
|
+
"update",
|
|
168
|
+
"Replace a primer note by index",
|
|
169
|
+
usage="kd /primer/note update INDEX TEXT",
|
|
170
|
+
long_description="Replaces the note at the given 1-based index with new text. "
|
|
171
|
+
"TEXT can be inline, @file to read from a file, or - to read from stdin. "
|
|
172
|
+
"Use '/primer/note list' to see current indices.",
|
|
173
|
+
examples=[
|
|
174
|
+
'kd /primer/note update 1 "Updated convention"',
|
|
175
|
+
"kd /primer/note update 2 @updated.txt",
|
|
176
|
+
],
|
|
177
|
+
see_also=["/primer/note list", "/primer/note remove"],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def execute(
|
|
181
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
182
|
+
) -> CommandResult:
|
|
183
|
+
if not args:
|
|
184
|
+
raise ValidationError("Usage: kd /primer/note update INDEX TEXT")
|
|
185
|
+
try:
|
|
186
|
+
index = int(args[0])
|
|
187
|
+
except ValueError:
|
|
188
|
+
raise ValidationError(f"INDEX must be a number, got: {args[0]}") from None
|
|
189
|
+
if len(args) < 2:
|
|
190
|
+
raise ValidationError("Usage: kd /primer/note update INDEX TEXT")
|
|
191
|
+
text = resolve_text_input(" ".join(args[1:]))
|
|
192
|
+
if not text:
|
|
193
|
+
raise ValidationError("Note text cannot be empty")
|
|
194
|
+
old = update_note(index, text)
|
|
195
|
+
return {"ok": True, "action": "update", "index": index, "old": old, "new": text}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# /primer/article
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class PrimerArticleListCommand(Command):
|
|
204
|
+
"""List pinned articles."""
|
|
205
|
+
|
|
206
|
+
def __init__(self) -> None:
|
|
207
|
+
super().__init__(
|
|
208
|
+
"list",
|
|
209
|
+
"List pinned articles",
|
|
210
|
+
usage="kd /primer/article list",
|
|
211
|
+
long_description="Lists all articles pinned in the project primer.",
|
|
212
|
+
examples=["kd /primer/article list"],
|
|
213
|
+
see_also=["/primer/article add", "/primer/article remove"],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def execute(
|
|
217
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
218
|
+
) -> CommandResult:
|
|
219
|
+
data = load_primer()
|
|
220
|
+
return [{"id": a.id, "summary": a.summary or ""} for a in data.articles]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class PrimerArticleAddCommand(Command):
|
|
224
|
+
"""Pin an article to the primer."""
|
|
225
|
+
|
|
226
|
+
def __init__(self) -> None:
|
|
227
|
+
super().__init__(
|
|
228
|
+
"add",
|
|
229
|
+
"Pin an article to the primer",
|
|
230
|
+
usage="kd /primer/article add ARTICLE_ID [--summary S]",
|
|
231
|
+
long_description="Adds a YouTrack article reference to the project primer. "
|
|
232
|
+
"Use --summary to store a description alongside the ID.",
|
|
233
|
+
examples=[
|
|
234
|
+
"kd /primer/article add FB-A-42",
|
|
235
|
+
'kd /primer/article add FB-A-42 --summary "Architecture Overview"',
|
|
236
|
+
],
|
|
237
|
+
see_also=["/primer/article list", "/primer/article remove"],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _register_options(self) -> None:
|
|
241
|
+
self.register_option(
|
|
242
|
+
OptionDeclaration(
|
|
243
|
+
name="summary",
|
|
244
|
+
short_name="s",
|
|
245
|
+
layer=OptionLayer.COMMAND,
|
|
246
|
+
help_text="Description to store with the article reference",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def execute(
|
|
251
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
252
|
+
) -> CommandResult:
|
|
253
|
+
if not args:
|
|
254
|
+
raise ValidationError("Usage: kd /primer/article add ARTICLE_ID")
|
|
255
|
+
article_id = args[0]
|
|
256
|
+
summary = options.command.get("summary")
|
|
257
|
+
article = add_article(article_id, summary=summary)
|
|
258
|
+
result: dict[str, Any] = {"ok": True, "action": "add", "id": article.id}
|
|
259
|
+
if article.summary:
|
|
260
|
+
result["summary"] = article.summary
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class PrimerArticleRemoveCommand(Command):
|
|
265
|
+
"""Unpin an article by ID, or all articles with --all."""
|
|
266
|
+
|
|
267
|
+
def __init__(self) -> None:
|
|
268
|
+
super().__init__(
|
|
269
|
+
"remove",
|
|
270
|
+
"Unpin an article or all articles",
|
|
271
|
+
usage="kd /primer/article remove ARTICLE_ID | --all",
|
|
272
|
+
long_description="Removes the article with the given ID from the primer, "
|
|
273
|
+
"or all pinned articles if --all is passed.",
|
|
274
|
+
examples=[
|
|
275
|
+
"kd /primer/article remove FB-A-42",
|
|
276
|
+
"kd /primer/article remove --all",
|
|
277
|
+
],
|
|
278
|
+
see_also=["/primer/article list", "/primer/article add"],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _register_options(self) -> None:
|
|
282
|
+
self.register_option(
|
|
283
|
+
OptionDeclaration(
|
|
284
|
+
name="all",
|
|
285
|
+
layer=OptionLayer.COMMAND,
|
|
286
|
+
type=bool,
|
|
287
|
+
default=False,
|
|
288
|
+
help_text="Remove all pinned articles",
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def execute(
|
|
293
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
294
|
+
) -> CommandResult:
|
|
295
|
+
if options.command.get("all"):
|
|
296
|
+
count = clear_articles()
|
|
297
|
+
return {"ok": True, "action": "clear", "removed": count}
|
|
298
|
+
if not args:
|
|
299
|
+
raise ValidationError("Usage: kd /primer/article remove ARTICLE_ID | --all")
|
|
300
|
+
removed = remove_article(args[0])
|
|
301
|
+
return {"ok": True, "action": "remove", "id": removed.id}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Namespace factory
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def create_primer_namespace() -> Namespace:
|
|
310
|
+
"""Build the /primer namespace with /primer/note and /primer/article."""
|
|
311
|
+
ns = Namespace(
|
|
312
|
+
"primer",
|
|
313
|
+
"Project primer — local knowledge for LLM consumers",
|
|
314
|
+
path="primer",
|
|
315
|
+
)
|
|
316
|
+
ns.register_command("show", PrimerShowCommand())
|
|
317
|
+
|
|
318
|
+
# /primer/note
|
|
319
|
+
note_ns = Namespace("note", "Primer notes and conventions", path="primer/note")
|
|
320
|
+
note_ns.register_command("list", PrimerNoteListCommand())
|
|
321
|
+
note_ns.register_command("add", PrimerNoteAddCommand())
|
|
322
|
+
note_ns.register_command("remove", PrimerNoteRemoveCommand())
|
|
323
|
+
note_ns.register_command("update", PrimerNoteUpdateCommand())
|
|
324
|
+
ns.register_sub_namespace(note_ns)
|
|
325
|
+
|
|
326
|
+
# /primer/article
|
|
327
|
+
article_ns = Namespace(
|
|
328
|
+
"article",
|
|
329
|
+
"Pinned article references",
|
|
330
|
+
path="primer/article",
|
|
331
|
+
)
|
|
332
|
+
article_ns.register_command("list", PrimerArticleListCommand())
|
|
333
|
+
article_ns.register_command("add", PrimerArticleAddCommand())
|
|
334
|
+
article_ns.register_command("remove", PrimerArticleRemoveCommand())
|
|
335
|
+
ns.register_sub_namespace(article_ns)
|
|
336
|
+
|
|
337
|
+
return ns
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Project namespace: /project
|
|
2
|
+
|
|
3
|
+
Commands: list, create, show, configure, delete
|
|
4
|
+
API layer: YouTrack REST API (/api/admin/projects)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from kd.cli.core.command_base import Command, CommandResult
|
|
12
|
+
from kd.cli.core.namespaces import Namespace
|
|
13
|
+
from kd.cli.core.option_registry import (
|
|
14
|
+
OptionDeclaration,
|
|
15
|
+
OptionLayer,
|
|
16
|
+
ResolvedOptions,
|
|
17
|
+
project_option,
|
|
18
|
+
)
|
|
19
|
+
from kd.exceptions import ValidationError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProjectListCommand(Command):
|
|
23
|
+
"""List all projects."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
super().__init__(
|
|
27
|
+
"list",
|
|
28
|
+
"List all projects",
|
|
29
|
+
requires_client=True,
|
|
30
|
+
usage="kd /project list",
|
|
31
|
+
examples=["kd /project list", "kd /project list --output json"],
|
|
32
|
+
see_also=["/project show", "/project create"],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def execute(
|
|
36
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
37
|
+
) -> CommandResult:
|
|
38
|
+
projects = client.projects.list()
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"key": p.short_name,
|
|
42
|
+
"name": p.name,
|
|
43
|
+
"leader": p.leader.name if p.leader else "",
|
|
44
|
+
"archived": p.archived,
|
|
45
|
+
}
|
|
46
|
+
for p in projects
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProjectCreateCommand(Command):
|
|
51
|
+
"""Create a project."""
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
"create",
|
|
56
|
+
"Create a project",
|
|
57
|
+
requires_client=True,
|
|
58
|
+
usage="kd /project create KEY NAME",
|
|
59
|
+
long_description="KEY is the project short name (max 10 chars, uppercase + digits). "
|
|
60
|
+
"NAME is the full project name.",
|
|
61
|
+
examples=[
|
|
62
|
+
'kd /project create FB "Franco Bet"',
|
|
63
|
+
"kd /project create DEMO Demo",
|
|
64
|
+
],
|
|
65
|
+
see_also=["/project list", "/project show"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def execute(
|
|
69
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
70
|
+
) -> CommandResult:
|
|
71
|
+
if len(args) < 2:
|
|
72
|
+
raise ValidationError("Usage: kd /project create KEY NAME")
|
|
73
|
+
key = args[0]
|
|
74
|
+
name = " ".join(args[1:])
|
|
75
|
+
project = client.projects.create(name=name, short_name=key)
|
|
76
|
+
return {"key": project.short_name, "name": project.name, "id": project.id}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ProjectShowCommand(Command):
|
|
80
|
+
"""Show project details."""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
super().__init__(
|
|
84
|
+
"show",
|
|
85
|
+
"Show project details",
|
|
86
|
+
requires_client=True,
|
|
87
|
+
usage="kd /project show KEY",
|
|
88
|
+
examples=["kd /project show FB", "kd /project show FB --output json"],
|
|
89
|
+
see_also=["/project list", "/project configure"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def execute(
|
|
93
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
94
|
+
) -> CommandResult:
|
|
95
|
+
if not args:
|
|
96
|
+
raise ValidationError("Usage: kd /project show KEY")
|
|
97
|
+
project = client.projects.get(args[0])
|
|
98
|
+
return {
|
|
99
|
+
"key": project.short_name,
|
|
100
|
+
"name": project.name,
|
|
101
|
+
"description": project.description or "",
|
|
102
|
+
"leader": project.leader.name if project.leader else "",
|
|
103
|
+
"archived": project.archived,
|
|
104
|
+
"id": project.id,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ProjectConfigureCommand(Command):
|
|
109
|
+
"""Configure project settings."""
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
super().__init__(
|
|
113
|
+
"configure",
|
|
114
|
+
"Configure project settings",
|
|
115
|
+
requires_client=True,
|
|
116
|
+
usage="kd /project configure KEY --time-tracking true",
|
|
117
|
+
long_description="Configure project-level settings like time tracking.",
|
|
118
|
+
examples=[
|
|
119
|
+
"kd /project configure FB --time-tracking true",
|
|
120
|
+
"kd /project configure FB --time-tracking false",
|
|
121
|
+
],
|
|
122
|
+
see_also=["/project show"],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _register_options(self) -> None:
|
|
126
|
+
self.register_option(
|
|
127
|
+
OptionDeclaration(
|
|
128
|
+
name="time-tracking",
|
|
129
|
+
layer=OptionLayer.COMMAND,
|
|
130
|
+
type=bool,
|
|
131
|
+
required=True,
|
|
132
|
+
help_text="Enable or disable time tracking",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def execute(
|
|
137
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
138
|
+
) -> CommandResult:
|
|
139
|
+
if not args:
|
|
140
|
+
raise ValidationError("Usage: kd /project configure KEY --time-tracking true|false")
|
|
141
|
+
key = args[0]
|
|
142
|
+
enabled = options.command["time-tracking"]
|
|
143
|
+
settings = client.projects.configure_time_tracking(key, enabled)
|
|
144
|
+
return {"project": key, "time_tracking": settings.enabled}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ProjectDeleteCommand(Command):
|
|
148
|
+
"""Delete a project."""
|
|
149
|
+
|
|
150
|
+
def __init__(self) -> None:
|
|
151
|
+
super().__init__(
|
|
152
|
+
"delete",
|
|
153
|
+
"Delete a project",
|
|
154
|
+
requires_client=True,
|
|
155
|
+
usage="kd /project delete KEY",
|
|
156
|
+
examples=["kd /project delete TST"],
|
|
157
|
+
see_also=["/project list"],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def execute(
|
|
161
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
162
|
+
) -> CommandResult:
|
|
163
|
+
if not args:
|
|
164
|
+
raise ValidationError("Usage: kd /project delete KEY")
|
|
165
|
+
key = args[0]
|
|
166
|
+
client.projects.delete(key)
|
|
167
|
+
return {"ok": True, "action": "delete", "project": key}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ProjectFieldListCommand(Command):
|
|
171
|
+
"""List custom fields for a project."""
|
|
172
|
+
|
|
173
|
+
def __init__(self) -> None:
|
|
174
|
+
super().__init__(
|
|
175
|
+
"list",
|
|
176
|
+
"List custom fields for a project",
|
|
177
|
+
requires_client=True,
|
|
178
|
+
usage="kd /project/field list KEY [-p KEY] [--field NAME]",
|
|
179
|
+
examples=[
|
|
180
|
+
"kd /project/field list FB",
|
|
181
|
+
"kd /project/field list -p FB",
|
|
182
|
+
"kd /project/field list FB --field State",
|
|
183
|
+
"kd /project/field list FB --output json",
|
|
184
|
+
],
|
|
185
|
+
see_also=["/project show", "/issue create", "/issue update"],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _register_options(self) -> None:
|
|
189
|
+
self.register_option(project_option(help_text="Project key"))
|
|
190
|
+
self.register_option(
|
|
191
|
+
OptionDeclaration(
|
|
192
|
+
name="field",
|
|
193
|
+
layer=OptionLayer.COMMAND,
|
|
194
|
+
help_text="Show only this field and its values",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def execute(
|
|
199
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
200
|
+
) -> CommandResult:
|
|
201
|
+
positional = args[0] if args else None
|
|
202
|
+
from_option = options.command.get("project")
|
|
203
|
+
key = positional or from_option
|
|
204
|
+
if not key:
|
|
205
|
+
raise ValidationError("Usage: kd /project/field list KEY (or --project/-p KEY)")
|
|
206
|
+
|
|
207
|
+
fields = client.projects.list_fields(key)
|
|
208
|
+
|
|
209
|
+
field_name = options.command.get("field")
|
|
210
|
+
if field_name is not None:
|
|
211
|
+
match = [f for f in fields if f.name == field_name]
|
|
212
|
+
if not match:
|
|
213
|
+
names = ", ".join(f.name for f in fields)
|
|
214
|
+
raise ValidationError(
|
|
215
|
+
f"No field named '{field_name}' in project {key}. Available: {names}"
|
|
216
|
+
)
|
|
217
|
+
fields = match
|
|
218
|
+
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
"name": f.name,
|
|
222
|
+
"type": f.field_type_id,
|
|
223
|
+
"required": not f.can_be_empty,
|
|
224
|
+
"values": ", ".join(v.name for v in f.bundle_values) if f.bundle_values else "",
|
|
225
|
+
}
|
|
226
|
+
for f in fields
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def create_project_namespace() -> Namespace:
|
|
231
|
+
"""Build the /project namespace tree."""
|
|
232
|
+
ns = Namespace("project", "Project management", path="project")
|
|
233
|
+
ns.register_command("list", ProjectListCommand())
|
|
234
|
+
ns.register_command("create", ProjectCreateCommand())
|
|
235
|
+
ns.register_command("show", ProjectShowCommand())
|
|
236
|
+
ns.register_command("configure", ProjectConfigureCommand())
|
|
237
|
+
ns.register_command("delete", ProjectDeleteCommand())
|
|
238
|
+
|
|
239
|
+
field_ns = Namespace("field", "Project custom fields", path="project/field")
|
|
240
|
+
field_ns.register_command("list", ProjectFieldListCommand())
|
|
241
|
+
ns.register_sub_namespace(field_ns)
|
|
242
|
+
|
|
243
|
+
return ns
|