kd-cli 2.5.1__tar.gz → 2.5.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.5.1/kd_cli.egg-info → kd_cli-2.5.2}/PKG-INFO +1 -1
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/article/__init__.py +26 -2
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/client.py +32 -4
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/CHANGELOG.md +8 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/operating-guide.md +2 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.5.1 → kd_cli-2.5.2}/pyproject.toml +1 -1
- {kd_cli-2.5.1 → kd_cli-2.5.2}/LICENSE +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/MANIFEST.in +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/README.md +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/articles.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/http.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/hub.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/tags.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/api/youtrack.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/article_marker.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/auth.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/core/confirm.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/input.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/main.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/output.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/cli/tree.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/config.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/config_read.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/config_schema.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/connection.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/debug.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/exceptions.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/history.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/locking.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/article.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/assist.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/attachment.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/comment.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/common.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/issue.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/project.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/saved_query.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/search.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/tag.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/user.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/models/work_item.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/operating_guide.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/primer.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/query_parser.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/release_notes.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/primers/__init__.py +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd/resources/primers/hitl.yaml +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.5.1 → kd_cli-2.5.2}/setup.cfg +0 -0
|
@@ -181,7 +181,8 @@ 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] [--create-tags]
|
|
184
|
+
"[--slug S] [--parent ID] [--tag T] [--create-tags] "
|
|
185
|
+
"[--force] [--dry-run] [--no-marker-write]"
|
|
185
186
|
),
|
|
186
187
|
long_description=(
|
|
187
188
|
"Push a local markdown file to YouTrack as a knowledge base "
|
|
@@ -194,7 +195,13 @@ class ArticlePushCommand(Command):
|
|
|
194
195
|
"the marker — treat the file as owned by push after the first "
|
|
195
196
|
"run, like Cargo.lock. "
|
|
196
197
|
"Refuses to overwrite a remotely-edited article unless --force "
|
|
197
|
-
"is passed. --dry-run exits 1 on would-conflict, 0 on would-succeed."
|
|
198
|
+
"is passed. --dry-run exits 1 on would-conflict, 0 on would-succeed. "
|
|
199
|
+
"Pass --no-marker-write for one-way migrations where the local "
|
|
200
|
+
"file will be deleted after the push: the article is still "
|
|
201
|
+
"created/updated and tags are still applied, but the local file "
|
|
202
|
+
"is not rewritten. Re-running --no-marker-write pays the "
|
|
203
|
+
"scan-by-slug cost on every invocation since no upstream-id is "
|
|
204
|
+
"cached locally."
|
|
198
205
|
),
|
|
199
206
|
examples=[
|
|
200
207
|
'kd /article push docs/infra/dns-setup.md --project FB --title "DNS Setup Guide"',
|
|
@@ -204,6 +211,8 @@ class ArticlePushCommand(Command):
|
|
|
204
211
|
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --create-tags',
|
|
205
212
|
'kd /article push guide.md --project FB --title "Guide" --dry-run',
|
|
206
213
|
'kd /article push guide.md --project FB --title "Guide" --force',
|
|
214
|
+
'kd /article push migrated.md --project FB --title "Migrated" --no-marker-write',
|
|
215
|
+
'kd /article push migrated.md --project FB --title "Migrated" --no-marker-write --tag "type:guide"',
|
|
207
216
|
],
|
|
208
217
|
see_also=[
|
|
209
218
|
"/article pull",
|
|
@@ -274,6 +283,18 @@ class ArticlePushCommand(Command):
|
|
|
274
283
|
help_text="Create missing tags before pushing",
|
|
275
284
|
)
|
|
276
285
|
)
|
|
286
|
+
self.register_option(
|
|
287
|
+
OptionDeclaration(
|
|
288
|
+
name="no-marker-write",
|
|
289
|
+
layer=OptionLayer.COMMAND,
|
|
290
|
+
type=bool,
|
|
291
|
+
default=False,
|
|
292
|
+
help_text=(
|
|
293
|
+
"Skip the local kd-meta marker rewrite for one-way "
|
|
294
|
+
"migrations; the server write and tag apply still run"
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
)
|
|
277
298
|
|
|
278
299
|
def execute(
|
|
279
300
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -300,6 +321,7 @@ class ArticlePushCommand(Command):
|
|
|
300
321
|
dry_run=bool(options.command.get("dry-run")),
|
|
301
322
|
tags=tags or None,
|
|
302
323
|
create_tags=bool(options.command.get("create-tags")),
|
|
324
|
+
write_marker=not bool(options.command.get("no-marker-write")),
|
|
303
325
|
)
|
|
304
326
|
return {
|
|
305
327
|
"ok": True,
|
|
@@ -310,6 +332,8 @@ class ArticlePushCommand(Command):
|
|
|
310
332
|
"dry_run": result.dry_run,
|
|
311
333
|
"tags_applied": result.tags_applied,
|
|
312
334
|
"tags_failed": result.tags_failed,
|
|
335
|
+
"marker_written": result.marker_written,
|
|
336
|
+
"parent": result.parent,
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
|
|
@@ -195,6 +195,11 @@ class PushResult:
|
|
|
195
195
|
- ``remote_updated_sec`` is the epoch-seconds timestamp of the
|
|
196
196
|
article's ``updated`` field on the upstream side after the push.
|
|
197
197
|
Zero for dry-runs that did not resolve an existing article.
|
|
198
|
+
- ``marker_written`` is ``True`` iff the local file was rewritten
|
|
199
|
+
on this invocation. ``False`` for dry-runs and for
|
|
200
|
+
``write_marker=False`` pushes (one-way migrations).
|
|
201
|
+
- ``parent`` is the ``id_readable`` of the target article's parent
|
|
202
|
+
on the upstream side, or ``None`` for root articles.
|
|
198
203
|
"""
|
|
199
204
|
|
|
200
205
|
action: Literal["create", "update"]
|
|
@@ -205,6 +210,8 @@ class PushResult:
|
|
|
205
210
|
dry_run: bool
|
|
206
211
|
tags_applied: builtins.list[str] = dataclass_field(default_factory=list)
|
|
207
212
|
tags_failed: builtins.list[str] = dataclass_field(default_factory=list)
|
|
213
|
+
marker_written: bool = False
|
|
214
|
+
parent: str | None = None
|
|
208
215
|
|
|
209
216
|
|
|
210
217
|
def _derive_slug_from_path(file_path: Path) -> str:
|
|
@@ -1304,6 +1311,7 @@ class ArticleOperations:
|
|
|
1304
1311
|
dry_run: bool = False,
|
|
1305
1312
|
tags: builtins.list[str] | None = None,
|
|
1306
1313
|
create_tags: bool = False,
|
|
1314
|
+
write_marker: bool = True,
|
|
1307
1315
|
) -> PushResult:
|
|
1308
1316
|
"""Idempotently upsert a local markdown file as a YouTrack article.
|
|
1309
1317
|
|
|
@@ -1314,6 +1322,10 @@ class ArticleOperations:
|
|
|
1314
1322
|
|
|
1315
1323
|
On success, the local file is rewritten atomically to refresh
|
|
1316
1324
|
the marker's ``upstream-id`` and ``upstream-updated`` fields.
|
|
1325
|
+
Pass ``write_marker=False`` for one-way migrations where the
|
|
1326
|
+
local file will be deleted after the push — the outgoing
|
|
1327
|
+
server payload is unchanged, only the local write-back is
|
|
1328
|
+
skipped. See ``PushResult.marker_written`` (KDCLI-110).
|
|
1317
1329
|
"""
|
|
1318
1330
|
# Step 1: read the local file and parse the marker.
|
|
1319
1331
|
try:
|
|
@@ -1437,8 +1449,12 @@ class ArticleOperations:
|
|
|
1437
1449
|
# Dry-run bails out here with all validation done but no writes.
|
|
1438
1450
|
if dry_run:
|
|
1439
1451
|
existing_sec = 0
|
|
1440
|
-
|
|
1441
|
-
|
|
1452
|
+
existing_parent: str | None = None
|
|
1453
|
+
if existing_remote is not None:
|
|
1454
|
+
if existing_remote.updated is not None:
|
|
1455
|
+
existing_sec = existing_remote.updated // 1000
|
|
1456
|
+
if existing_remote.parent_article is not None:
|
|
1457
|
+
existing_parent = existing_remote.parent_article.id_readable
|
|
1442
1458
|
return PushResult(
|
|
1443
1459
|
action="update" if is_update else "create",
|
|
1444
1460
|
article_id=existing_remote.id_readable if existing_remote else "",
|
|
@@ -1446,6 +1462,8 @@ class ArticleOperations:
|
|
|
1446
1462
|
path=file_path,
|
|
1447
1463
|
remote_updated_sec=existing_sec,
|
|
1448
1464
|
dry_run=True,
|
|
1465
|
+
marker_written=False,
|
|
1466
|
+
parent=existing_parent,
|
|
1449
1467
|
)
|
|
1450
1468
|
|
|
1451
1469
|
# Step 7: build outgoing content. `upstream-updated` is intentionally
|
|
@@ -1514,10 +1532,18 @@ class ArticleOperations:
|
|
|
1514
1532
|
)
|
|
1515
1533
|
final_content = render_content(final)
|
|
1516
1534
|
|
|
1517
|
-
# Step 10: atomic local write-back
|
|
1518
|
-
|
|
1535
|
+
# Step 10: atomic local write-back (skipped for one-way
|
|
1536
|
+
# migration flows that opt out via write_marker=False; the
|
|
1537
|
+
# server payload in Step 7 is unchanged either way).
|
|
1538
|
+
if write_marker:
|
|
1539
|
+
_atomic_write_text(file_path, final_content)
|
|
1519
1540
|
|
|
1520
1541
|
# Step 11: return result.
|
|
1542
|
+
result_parent: str | None = (
|
|
1543
|
+
result_article.parent_article.id_readable
|
|
1544
|
+
if result_article.parent_article is not None
|
|
1545
|
+
else None
|
|
1546
|
+
)
|
|
1521
1547
|
push_result = PushResult(
|
|
1522
1548
|
action="update" if is_update else "create",
|
|
1523
1549
|
article_id=result_article.id_readable,
|
|
@@ -1527,6 +1553,8 @@ class ArticleOperations:
|
|
|
1527
1553
|
dry_run=False,
|
|
1528
1554
|
tags_applied=tags_applied,
|
|
1529
1555
|
tags_failed=tags_failed,
|
|
1556
|
+
marker_written=write_marker,
|
|
1557
|
+
parent=result_parent,
|
|
1530
1558
|
)
|
|
1531
1559
|
if tags_failed:
|
|
1532
1560
|
raise KdError(
|
|
@@ -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.5.2] — 2026-04-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `kd /article push` one-way migrations no longer force `git rm -f`. Before: push always rewrote the local markdown file with a `kd-meta` marker on success, which left the file "locally modified" and made `git rm` refuse without `-f` on migrations that delete the local copy after push. Now: pass `--no-marker-write` to skip the local rewrite. The upstream article is still created or updated, `--tag` values are still applied, and the outgoing server payload still carries a marker with the slug — so scan-by-slug resolves the article on any later push even without the local file. Default behavior is unchanged; the flag is opt-in (KDCLI-110).
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `kd /article push` structured output (`-o yaml` / `-o json`) now carries two sibling keys on every push: `marker_written` (`false` for dry-runs and `--no-marker-write` pushes, `true` on the default path after the local rewrite) and `parent` (readable id of the target article's upstream parent, or `null` for root articles). Scripts can assert local-file state and the article's place in the tree from the push response directly (KDCLI-110).
|
|
14
|
+
|
|
7
15
|
## [2.5.1] — 2026-04-19
|
|
8
16
|
|
|
9
17
|
### Fixed
|
|
@@ -158,6 +158,8 @@ Use `/article show` for metadata plus rendered content. Use `/article pull` when
|
|
|
158
158
|
|
|
159
159
|
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.
|
|
160
160
|
|
|
161
|
+
For one-way migrations where the local file will be deleted after the push, pass `--no-marker-write`. The server article is still created or updated and tags are still applied, but kd skips the local `kd-meta` rewrite so `git rm` works without `-f`. The structured response gains `marker_written: false`; re-running `--no-marker-write` pays the scan-by-slug cost every invocation because no `upstream-id` is cached locally.
|
|
162
|
+
|
|
161
163
|
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.
|
|
162
164
|
|
|
163
165
|
`/article create --tag` applies the same preflight: every tag must exist, missing tags abort before the article write (no orphan article on a typo), and the structured response reflects post-tag state so it matches an immediate `/article show` on the returned id. There is no `--create-tags` on `/article create` — curate the namespace with `/tag create` first, or omit `--tag` and apply tags after creation via `/tag add ARTICLE-ID TAG`.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|