kd-cli 2.6.0__tar.gz → 2.6.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.6.0/kd_cli.egg-info → kd_cli-2.6.2}/PKG-INFO +1 -1
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/core/option_registry.py +16 -1
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/main.py +9 -1
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/article/__init__.py +143 -16
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/issue/__init__.py +144 -17
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/project/__init__.py +42 -6
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/query/__init__.py +62 -6
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/tag/__init__.py +55 -12
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/time/__init__.py +55 -3
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/user/__init__.py +24 -2
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/output.py +170 -34
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/CHANGELOG.md +81 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/operating-guide.md +38 -2
- kd_cli-2.6.2/kd/resources/sdlc-doctrine.md +602 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.6.0 → kd_cli-2.6.2}/pyproject.toml +1 -1
- kd_cli-2.6.0/kd/resources/sdlc-doctrine.md +0 -479
- {kd_cli-2.6.0 → kd_cli-2.6.2}/LICENSE +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/MANIFEST.in +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/README.md +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/articles.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/http.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/hub.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/tags.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/api/youtrack.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/article_marker.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/auth.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cache.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/core/confirm.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/input.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/cache/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/cli/tree.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/client.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/config.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/config_read.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/config_schema.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/connection.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/debug.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/exceptions.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/history.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/locking.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/article.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/assist.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/attachment.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/comment.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/common.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/issue.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/project.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/saved_query.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/search.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/tag.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/user.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/models/work_item.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/operating_guide.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/primer.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/query_parser.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/release_notes.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/primers/__init__.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/resources/primers/hitl.yaml +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd/sdlc.py +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.6.0 → kd_cli-2.6.2}/setup.cfg +0 -0
|
@@ -246,7 +246,12 @@ def state_option(
|
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
-
# Standard OUTPUT options available to all commands
|
|
249
|
+
# Standard OUTPUT options available to all commands.
|
|
250
|
+
#
|
|
251
|
+
# ``--quiet`` is registered globally so any command's parser accepts it,
|
|
252
|
+
# but it only affects mutation commands that opt into the write-output
|
|
253
|
+
# contract (``display_hint="mutation"``). On reads and structured output
|
|
254
|
+
# modes (``-o yaml`` / ``-o json``), ``--quiet`` is silently no-op.
|
|
250
255
|
OUTPUT_OPTIONS: list[OptionDeclaration] = [
|
|
251
256
|
OptionDeclaration(
|
|
252
257
|
name="output",
|
|
@@ -263,4 +268,14 @@ OUTPUT_OPTIONS: list[OptionDeclaration] = [
|
|
|
263
268
|
type=str,
|
|
264
269
|
help_text="Comma-separated field list",
|
|
265
270
|
),
|
|
271
|
+
OptionDeclaration(
|
|
272
|
+
name="quiet",
|
|
273
|
+
layer=OptionLayer.OUTPUT,
|
|
274
|
+
type=bool,
|
|
275
|
+
default=False,
|
|
276
|
+
help_text=(
|
|
277
|
+
"Suppress detail block on mutation commands; print only the "
|
|
278
|
+
"single-line ack. No-op on reads and structured modes (-o yaml/json)."
|
|
279
|
+
),
|
|
280
|
+
),
|
|
266
281
|
]
|
|
@@ -324,8 +324,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
324
324
|
if result is not None:
|
|
325
325
|
fmt = parsed.options.output.get("output")
|
|
326
326
|
fields = parsed.options.output.get("fields")
|
|
327
|
+
quiet = bool(parsed.options.output.get("quiet"))
|
|
327
328
|
display_hint = getattr(parsed.command, "display_hint", None)
|
|
328
|
-
output = formatter.format(
|
|
329
|
+
output = formatter.format(
|
|
330
|
+
result, fmt=fmt, fields=fields, display_hint=display_hint, quiet=quiet
|
|
331
|
+
)
|
|
329
332
|
if output:
|
|
330
333
|
# String results (e.g. /article pull) own their trailing newline.
|
|
331
334
|
# Everything else goes through print() which appends one newline.
|
|
@@ -335,6 +338,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
335
338
|
sys.stdout.write("\n")
|
|
336
339
|
else:
|
|
337
340
|
print(output)
|
|
341
|
+
# Route warnings extracted in text/md modes to stderr so stdout
|
|
342
|
+
# stays parseable for ``tail -1 | awk`` recipes. Structured modes
|
|
343
|
+
# leave warnings in the payload — last_warnings is empty there.
|
|
344
|
+
if formatter.last_warnings:
|
|
345
|
+
print(formatter.render_warnings(formatter.last_warnings), file=sys.stderr)
|
|
338
346
|
|
|
339
347
|
|
|
340
348
|
def _extract_globals(args: list[str]) -> tuple[dict[str, Any], list[str]]:
|
|
@@ -213,7 +213,16 @@ class ArticlePushCommand(Command):
|
|
|
213
213
|
"created/updated and tags are still applied, but the local file "
|
|
214
214
|
"is not rewritten. Re-running --no-marker-write pays the "
|
|
215
215
|
"scan-by-slug cost on every invocation since no upstream-id is "
|
|
216
|
-
"cached locally."
|
|
216
|
+
"cached locally. "
|
|
217
|
+
"Returns a parseable single-line ack: the last stdout line "
|
|
218
|
+
"is ``ok: <ID> created [slug=<slug> ...]`` for the create "
|
|
219
|
+
"branch or ``ok: <ID> updated [slug=<slug> ...]`` for the "
|
|
220
|
+
"update branch. Dry-run create has no upstream article yet "
|
|
221
|
+
"and ``id`` is null in the structured envelope; the tail "
|
|
222
|
+
"line carries ``dry_run=true`` and is **not "
|
|
223
|
+
"ID-extractable** via ``tail -1 | awk '{print $2}'``. "
|
|
224
|
+
"Drop --dry-run before relying on tail extraction. Pass "
|
|
225
|
+
"--quiet to suppress the detail block above the ack."
|
|
217
226
|
),
|
|
218
227
|
examples=[
|
|
219
228
|
'kd /article push docs/infra/dns-setup.md --project FB --title "DNS Setup Guide"',
|
|
@@ -233,6 +242,7 @@ class ArticlePushCommand(Command):
|
|
|
233
242
|
"/article move",
|
|
234
243
|
],
|
|
235
244
|
)
|
|
245
|
+
self.display_hint = "mutation"
|
|
236
246
|
|
|
237
247
|
def _register_options(self) -> None:
|
|
238
248
|
self.register_option(project_option(help_text="Project key for new articles (required)"))
|
|
@@ -345,10 +355,27 @@ class ArticlePushCommand(Command):
|
|
|
345
355
|
f"Wrote kd-meta marker to {result.path} (one-time; commit alongside content).",
|
|
346
356
|
file=sys.stderr,
|
|
347
357
|
)
|
|
358
|
+
# Map SDK PushResult.action ("create"/"update") to the contract's
|
|
359
|
+
# past-tense vocabulary at the CLI boundary. The SDK shape stays
|
|
360
|
+
# imperative; the contract lives here.
|
|
361
|
+
contract_action = {
|
|
362
|
+
"create": "created",
|
|
363
|
+
"update": "updated",
|
|
364
|
+
}.get(result.action, result.action)
|
|
365
|
+
# Dry-run create has no upstream article yet. The SDK returns
|
|
366
|
+
# ``article_id=""`` (empty string) on that path; the contract
|
|
367
|
+
# normalizes that to ``None`` so the tail-line formatter renders
|
|
368
|
+
# the ``<none>`` placeholder and structured consumers can detect
|
|
369
|
+
# the carve-out via ``id is None``. Production creates and
|
|
370
|
+
# production updates carry the article ID as expected, and
|
|
371
|
+
# dry-run updates carry the existing article ID.
|
|
372
|
+
envelope_id: str | None = result.article_id
|
|
373
|
+
if not envelope_id:
|
|
374
|
+
envelope_id = None
|
|
348
375
|
return {
|
|
349
376
|
"ok": True,
|
|
350
|
-
"action":
|
|
351
|
-
"
|
|
377
|
+
"action": contract_action,
|
|
378
|
+
"id": envelope_id,
|
|
352
379
|
"slug": result.slug,
|
|
353
380
|
"path": str(result.path),
|
|
354
381
|
"dry_run": result.dry_run,
|
|
@@ -403,9 +430,14 @@ class ArticleCreateCommand(Command):
|
|
|
403
430
|
"two-step in migration scripts. Pass --tag (repeatable) to "
|
|
404
431
|
"apply tags at creation time: every --tag must exist, the "
|
|
405
432
|
"article is refused before creation if any tag is missing "
|
|
406
|
-
"(no orphan article on unknown tags)
|
|
407
|
-
"
|
|
408
|
-
"
|
|
433
|
+
"(no orphan article on unknown tags). "
|
|
434
|
+
"Returns a parseable single-line ack. The last stdout line is "
|
|
435
|
+
"``ok: <ID> created [parent=<P>] [tags=<a>,<b>]`` so "
|
|
436
|
+
"``id=$(kd /article create ... --quiet | tail -1 | "
|
|
437
|
+
"awk '{print $2}')`` extracts the new article ID. Pass "
|
|
438
|
+
"--quiet to suppress the detail block; structured output "
|
|
439
|
+
"(-o yaml/json) returns the full envelope including the "
|
|
440
|
+
"submitted summary and project."
|
|
409
441
|
),
|
|
410
442
|
examples=[
|
|
411
443
|
'kd /article create "Setup Guide" --project FB --content @guide.md',
|
|
@@ -416,6 +448,7 @@ class ArticleCreateCommand(Command):
|
|
|
416
448
|
],
|
|
417
449
|
see_also=["/article show", "/article update", "/article move"],
|
|
418
450
|
)
|
|
451
|
+
self.display_hint = "mutation"
|
|
419
452
|
|
|
420
453
|
def _register_options(self) -> None:
|
|
421
454
|
self.register_option(project_option(required=True))
|
|
@@ -461,7 +494,19 @@ class ArticleCreateCommand(Command):
|
|
|
461
494
|
parent=parent,
|
|
462
495
|
tags=tags or None,
|
|
463
496
|
)
|
|
464
|
-
|
|
497
|
+
envelope: dict[str, Any] = {
|
|
498
|
+
"ok": True,
|
|
499
|
+
"action": "created",
|
|
500
|
+
"id": article.id_readable,
|
|
501
|
+
"summary": article.summary,
|
|
502
|
+
"project": project_key,
|
|
503
|
+
}
|
|
504
|
+
if parent:
|
|
505
|
+
envelope["parent"] = parent
|
|
506
|
+
applied = [t.name for t in (article.tags or [])]
|
|
507
|
+
if applied:
|
|
508
|
+
envelope["tags"] = applied
|
|
509
|
+
return envelope
|
|
465
510
|
|
|
466
511
|
|
|
467
512
|
class ArticleUpdateCommand(Command):
|
|
@@ -484,7 +529,16 @@ class ArticleUpdateCommand(Command):
|
|
|
484
529
|
"POST. The marker stays at body position 0 regardless of splice "
|
|
485
530
|
"direction. --content, --append, and --prepend are mutually "
|
|
486
531
|
"exclusive; --title (alias --summary) stays orthogonal and may "
|
|
487
|
-
"be combined with any one of them."
|
|
532
|
+
"be combined with any one of them. "
|
|
533
|
+
"Returns a parseable single-line ack: the last stdout line is "
|
|
534
|
+
"``ok: <ID> updated [<key>=<value> ...]`` listing each field "
|
|
535
|
+
"the operator just changed (title/summary). The structured "
|
|
536
|
+
"envelope also carries ``changed: [...]`` listing every "
|
|
537
|
+
"mutated key — including ``content`` for body-only updates, "
|
|
538
|
+
"which carries no tail-line token. Article body, full custom "
|
|
539
|
+
"field dumps, comments, and tags are dropped — run "
|
|
540
|
+
"/article show for the canonical post-write read. Pass "
|
|
541
|
+
"--quiet to suppress the detail block."
|
|
488
542
|
),
|
|
489
543
|
examples=[
|
|
490
544
|
'kd /article update FB-A-1 --title "New Title"',
|
|
@@ -495,6 +549,7 @@ class ArticleUpdateCommand(Command):
|
|
|
495
549
|
],
|
|
496
550
|
see_also=["/article show", "/article pull", "/article push"],
|
|
497
551
|
)
|
|
552
|
+
self.display_hint = "mutation"
|
|
498
553
|
|
|
499
554
|
def _register_options(self) -> None:
|
|
500
555
|
self.register_option(
|
|
@@ -561,7 +616,23 @@ class ArticleUpdateCommand(Command):
|
|
|
561
616
|
append=append,
|
|
562
617
|
prepend=prepend,
|
|
563
618
|
)
|
|
564
|
-
|
|
619
|
+
envelope: dict[str, Any] = {
|
|
620
|
+
"ok": True,
|
|
621
|
+
"action": "updated",
|
|
622
|
+
"id": article.id_readable,
|
|
623
|
+
}
|
|
624
|
+
changed: list[str] = []
|
|
625
|
+
if title:
|
|
626
|
+
envelope["summary"] = title
|
|
627
|
+
changed.append("summary")
|
|
628
|
+
# Body mutations (content / append / prepend) all surface as
|
|
629
|
+
# ``changed: ["content"]`` so structured consumers see a single
|
|
630
|
+
# signal regardless of which body op was used. The body itself is
|
|
631
|
+
# intentionally dropped — /article show is the post-write read.
|
|
632
|
+
if content is not None or append is not None or prepend is not None:
|
|
633
|
+
changed.append("content")
|
|
634
|
+
envelope["changed"] = changed
|
|
635
|
+
return envelope
|
|
565
636
|
|
|
566
637
|
|
|
567
638
|
class ArticleDeleteCommand(Command):
|
|
@@ -573,9 +644,15 @@ class ArticleDeleteCommand(Command):
|
|
|
573
644
|
"Delete an article",
|
|
574
645
|
requires_client=True,
|
|
575
646
|
usage="kd /article delete ARTICLE_ID",
|
|
647
|
+
long_description=(
|
|
648
|
+
"Returns a parseable single-line ack: the last stdout line is "
|
|
649
|
+
"``ok: <ID> deleted``. Pass --quiet to suppress the detail "
|
|
650
|
+
"block above the ack."
|
|
651
|
+
),
|
|
576
652
|
examples=["kd /article delete FB-A-1"],
|
|
577
653
|
see_also=["/article list"],
|
|
578
654
|
)
|
|
655
|
+
self.display_hint = "mutation"
|
|
579
656
|
|
|
580
657
|
def execute(
|
|
581
658
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -584,7 +661,7 @@ class ArticleDeleteCommand(Command):
|
|
|
584
661
|
raise ValidationError("Usage: kd /article delete ARTICLE_ID")
|
|
585
662
|
article_id = args[0]
|
|
586
663
|
client.articles.delete(article_id)
|
|
587
|
-
return {"ok": True, "action": "
|
|
664
|
+
return {"ok": True, "action": "deleted", "id": article_id}
|
|
588
665
|
|
|
589
666
|
|
|
590
667
|
# --- Comment Commands ---
|
|
@@ -630,12 +707,20 @@ class ArticleCommentAddCommand(Command):
|
|
|
630
707
|
"Add a comment to an article",
|
|
631
708
|
requires_client=True,
|
|
632
709
|
usage='kd /article/comment add ARTICLE_ID "Text"',
|
|
710
|
+
long_description=(
|
|
711
|
+
"Returns a parseable single-line ack. The tail line is "
|
|
712
|
+
"``ok: <ARTICLE_ID> added comment_id=<id>``: the parent "
|
|
713
|
+
"article ID is the primary tail-line ID (``$2``), and the "
|
|
714
|
+
"new comment's internal ID is in ``comment_id=<id>``. "
|
|
715
|
+
"Pass --quiet to suppress the detail block."
|
|
716
|
+
),
|
|
633
717
|
examples=[
|
|
634
718
|
'kd /article/comment add FB-A-1 "Needs review"',
|
|
635
719
|
"kd /article/comment add FB-A-1 @note.md",
|
|
636
720
|
],
|
|
637
721
|
see_also=["/article/comment list"],
|
|
638
722
|
)
|
|
723
|
+
self.display_hint = "mutation"
|
|
639
724
|
|
|
640
725
|
def execute(
|
|
641
726
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -646,9 +731,11 @@ class ArticleCommentAddCommand(Command):
|
|
|
646
731
|
text = resolve_text_input(" ".join(args[1:]))
|
|
647
732
|
comment = client.articles.add_comment(article_id, text)
|
|
648
733
|
return {
|
|
649
|
-
"
|
|
734
|
+
"ok": True,
|
|
735
|
+
"action": "added",
|
|
736
|
+
"id": article_id,
|
|
737
|
+
"comment_id": comment.id,
|
|
650
738
|
"author": comment.author.name if comment.author else "",
|
|
651
|
-
"text": comment.text,
|
|
652
739
|
}
|
|
653
740
|
|
|
654
741
|
|
|
@@ -698,12 +785,23 @@ class ArticleAttachmentAddCommand(Command):
|
|
|
698
785
|
"Add attachments to an article",
|
|
699
786
|
requires_client=True,
|
|
700
787
|
usage="kd /article/attachment add ARTICLE_ID FILE [FILE...]",
|
|
788
|
+
long_description=(
|
|
789
|
+
"Returns a parseable single-line ack: the last stdout line is "
|
|
790
|
+
"``ok: <ID> added attachments=<name1>,<name2>,...``. The "
|
|
791
|
+
"structured envelope carries ``attachments: [<name>, ...]`` "
|
|
792
|
+
"(scalar names — the source of the tail-line token) and "
|
|
793
|
+
"``applied: [{name, size, id}, ...]`` (per-file detail). "
|
|
794
|
+
"The shape is consistent for both single-file and "
|
|
795
|
+
"multi-file calls. Pass --quiet to suppress the detail "
|
|
796
|
+
"block above the ack."
|
|
797
|
+
),
|
|
701
798
|
examples=[
|
|
702
799
|
"kd /article/attachment add FB-A-1 diagram.png",
|
|
703
800
|
"kd /article/attachment add FB-A-1 log.txt report.pdf",
|
|
704
801
|
],
|
|
705
802
|
see_also=["/article/attachment list"],
|
|
706
803
|
)
|
|
804
|
+
self.display_hint = "mutation"
|
|
707
805
|
|
|
708
806
|
def execute(
|
|
709
807
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -713,7 +811,14 @@ class ArticleAttachmentAddCommand(Command):
|
|
|
713
811
|
article_id = args[0]
|
|
714
812
|
file_paths = [Path(p) for p in args[1:]]
|
|
715
813
|
attachments = client.articles.add_attachments(article_id, file_paths)
|
|
716
|
-
|
|
814
|
+
applied = [{"name": a.name, "size": a.size, "id": a.id} for a in attachments]
|
|
815
|
+
return {
|
|
816
|
+
"ok": True,
|
|
817
|
+
"action": "added",
|
|
818
|
+
"id": article_id,
|
|
819
|
+
"attachments": [a.name for a in attachments],
|
|
820
|
+
"applied": applied,
|
|
821
|
+
}
|
|
717
822
|
|
|
718
823
|
|
|
719
824
|
class ArticleAttachmentShowCommand(Command):
|
|
@@ -794,9 +899,18 @@ class ArticleAttachmentDeleteCommand(Command):
|
|
|
794
899
|
"Delete an article attachment",
|
|
795
900
|
requires_client=True,
|
|
796
901
|
usage="kd /article/attachment delete ARTICLE_ID ATTACHMENT_ID",
|
|
902
|
+
long_description=(
|
|
903
|
+
"Returns a parseable single-line ack: the last stdout line is "
|
|
904
|
+
"``ok: <ARTICLE_ID> deleted attachment=<ATTACHMENT_ID>``. "
|
|
905
|
+
"The parent article ID is the primary tail-line ID (``$2``); "
|
|
906
|
+
"the removed attachment's internal ID is in "
|
|
907
|
+
"``attachment=<id>``. Pass --quiet to suppress the detail "
|
|
908
|
+
"block above the ack."
|
|
909
|
+
),
|
|
797
910
|
examples=["kd /article/attachment delete FB-A-1 att-1"],
|
|
798
911
|
see_also=["/article/attachment list"],
|
|
799
912
|
)
|
|
913
|
+
self.display_hint = "mutation"
|
|
800
914
|
|
|
801
915
|
def execute(
|
|
802
916
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -804,7 +918,7 @@ class ArticleAttachmentDeleteCommand(Command):
|
|
|
804
918
|
if len(args) < 2:
|
|
805
919
|
raise ValidationError("Usage: kd /article/attachment delete ARTICLE_ID ATTACHMENT_ID")
|
|
806
920
|
client.articles.delete_attachment(args[0], args[1])
|
|
807
|
-
return {"ok": True, "action": "
|
|
921
|
+
return {"ok": True, "action": "deleted", "id": args[0], "attachment": args[1]}
|
|
808
922
|
|
|
809
923
|
|
|
810
924
|
# --- Move Command ---
|
|
@@ -819,13 +933,21 @@ class ArticleMoveCommand(Command):
|
|
|
819
933
|
"Move an article to a new parent",
|
|
820
934
|
requires_client=True,
|
|
821
935
|
usage="kd /article move ARTICLE_ID --parent PARENT_ID",
|
|
822
|
-
long_description=
|
|
936
|
+
long_description=(
|
|
937
|
+
"Use --parent none to move an article to project root. "
|
|
938
|
+
"Returns a parseable single-line ack: the last stdout line "
|
|
939
|
+
"is ``ok: <ID> moved parent=<PARENT_ID>`` (or "
|
|
940
|
+
"``ok: <ID> moved parent=root`` when reparented to project "
|
|
941
|
+
"root). Pass --quiet to suppress the detail block above "
|
|
942
|
+
"the ack."
|
|
943
|
+
),
|
|
823
944
|
examples=[
|
|
824
945
|
"kd /article move FB-A-3 --parent FB-A-1",
|
|
825
946
|
"kd /article move FB-A-3 --parent none",
|
|
826
947
|
],
|
|
827
948
|
see_also=["/article tree", "/article children"],
|
|
828
949
|
)
|
|
950
|
+
self.display_hint = "mutation"
|
|
829
951
|
|
|
830
952
|
def _register_options(self) -> None:
|
|
831
953
|
self.register_option(
|
|
@@ -847,7 +969,12 @@ class ArticleMoveCommand(Command):
|
|
|
847
969
|
article_id = args[0]
|
|
848
970
|
parent = None if parent_value.lower() == "none" else parent_value
|
|
849
971
|
article = client.articles.move(article_id, parent=parent)
|
|
850
|
-
return
|
|
972
|
+
return {
|
|
973
|
+
"ok": True,
|
|
974
|
+
"action": "moved",
|
|
975
|
+
"id": article.id_readable,
|
|
976
|
+
"parent": _parent_label(article) if article.parent_article else "root",
|
|
977
|
+
}
|
|
851
978
|
|
|
852
979
|
|
|
853
980
|
# --- Tree Command ---
|