kd-cli 2.6.3__tar.gz → 2.6.4__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.3/kd_cli.egg-info → kd_cli-2.6.4}/PKG-INFO +1 -1
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/input.py +23 -3
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/main.py +14 -7
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/system/__init__.py +6 -6
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/config.py +7 -7
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/config_read.py +1 -1
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/primer.py +1 -1
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/CHANGELOG.md +28 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/operating-guide.md +1 -1
- {kd_cli-2.6.3 → kd_cli-2.6.4/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.6.3 → kd_cli-2.6.4}/pyproject.toml +1 -1
- {kd_cli-2.6.3 → kd_cli-2.6.4}/LICENSE +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/MANIFEST.in +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/README.md +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/articles.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/http.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/hub.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/tags.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/api/youtrack.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/article_marker.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/auth.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cache.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/core/confirm.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/article/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/cache/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/output.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/cli/tree.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/client.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/config_schema.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/connection.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/debug.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/exceptions.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/history.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/locking.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/article.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/assist.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/attachment.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/comment.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/common.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/issue.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/project.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/saved_query.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/search.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/tag.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/user.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/models/work_item.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/operating_guide.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/query_parser.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/release_notes.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/primers/__init__.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/primers/hitl.yaml +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/resources/sdlc-doctrine.md +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd/sdlc.py +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.6.3 → kd_cli-2.6.4}/setup.cfg +0 -0
|
@@ -25,9 +25,23 @@ def resolve_text_input(value: str | None) -> str | None:
|
|
|
25
25
|
if not value:
|
|
26
26
|
return value
|
|
27
27
|
if value == "-":
|
|
28
|
-
|
|
28
|
+
try:
|
|
29
|
+
return sys.stdin.read()
|
|
30
|
+
except UnicodeDecodeError as e:
|
|
31
|
+
raise ValidationError(
|
|
32
|
+
f"Stdin input could not be decoded as UTF-8: {e.reason}",
|
|
33
|
+
suggestion="Ensure the upstream shell pipes UTF-8 bytes "
|
|
34
|
+
"(heredoc, pipe, or redirected file).",
|
|
35
|
+
) from e
|
|
29
36
|
if value == "@-":
|
|
30
|
-
|
|
37
|
+
try:
|
|
38
|
+
text = sys.stdin.read()
|
|
39
|
+
except UnicodeDecodeError as e:
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
f"Stdin input could not be decoded as UTF-8: {e.reason}",
|
|
42
|
+
suggestion="Ensure the upstream shell pipes UTF-8 bytes "
|
|
43
|
+
"(heredoc, pipe, or redirected file).",
|
|
44
|
+
) from e
|
|
31
45
|
if not text:
|
|
32
46
|
raise ValidationError("No input received from stdin")
|
|
33
47
|
return text
|
|
@@ -35,7 +49,13 @@ def resolve_text_input(value: str | None) -> str | None:
|
|
|
35
49
|
file_path = Path(value[1:])
|
|
36
50
|
if not file_path.is_file():
|
|
37
51
|
raise ValidationError(f"File not found: {file_path}")
|
|
38
|
-
|
|
52
|
+
try:
|
|
53
|
+
text = file_path.read_text(encoding="utf-8")
|
|
54
|
+
except UnicodeDecodeError as e:
|
|
55
|
+
raise ValidationError(
|
|
56
|
+
f"File {file_path} could not be decoded as UTF-8: {e.reason}",
|
|
57
|
+
suggestion="Save the file as UTF-8 (without a BOM) and retry.",
|
|
58
|
+
) from e
|
|
39
59
|
if not text:
|
|
40
60
|
raise ValidationError(f"File is empty: {file_path}")
|
|
41
61
|
return text
|
|
@@ -142,17 +142,24 @@ def _print_error(error: KdError) -> None:
|
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
def _configure_windows_stdio() -> None:
|
|
145
|
-
"""Force UTF-8 on stdout/stderr when running on Windows.
|
|
146
|
-
|
|
147
|
-
No-op on POSIX.
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
"""Force UTF-8 on stdin/stdout/stderr when running on Windows.
|
|
146
|
+
|
|
147
|
+
No-op on POSIX. Stdout/stderr use ``errors="replace"`` so a single
|
|
148
|
+
corrupted byte never aborts a whole command's output. Stdin uses
|
|
149
|
+
``errors="strict"`` so invalid bytes raise loudly rather than silently
|
|
150
|
+
writing replacement chars to disk via the input layer (see
|
|
151
|
+
``kd/cli/input.py``). Errors during reconfigure are swallowed; embedded
|
|
152
|
+
interpreters and pytest captures may lack ``reconfigure`` on any of the
|
|
153
|
+
three streams.
|
|
150
154
|
"""
|
|
151
155
|
if sys.platform != "win32":
|
|
152
156
|
return
|
|
153
|
-
for stream in (sys.stdout, sys.stderr):
|
|
157
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
154
158
|
try:
|
|
155
|
-
stream.reconfigure(
|
|
159
|
+
stream.reconfigure( # type: ignore[union-attr,unused-ignore]
|
|
160
|
+
encoding="utf-8",
|
|
161
|
+
errors="strict" if stream is sys.stdin else "replace",
|
|
162
|
+
)
|
|
156
163
|
except Exception:
|
|
157
164
|
pass
|
|
158
165
|
|
|
@@ -709,7 +709,7 @@ def _resolve_target(name: str) -> tuple[Path, Path, str]:
|
|
|
709
709
|
|
|
710
710
|
def _diff_summary(snapshot: SnapshotInfo, current_text: str) -> str:
|
|
711
711
|
"""One-line diff summary: ``+N -M`` lines vs current file."""
|
|
712
|
-
snap_lines = snapshot.path.read_text().splitlines()
|
|
712
|
+
snap_lines = snapshot.path.read_text(encoding="utf-8").splitlines()
|
|
713
713
|
curr_lines = current_text.splitlines()
|
|
714
714
|
added = removed = 0
|
|
715
715
|
for line in difflib.unified_diff(snap_lines, curr_lines, n=0):
|
|
@@ -751,7 +751,7 @@ class SystemHistoryCommand(Command):
|
|
|
751
751
|
rows: list[dict[str, Any]] = []
|
|
752
752
|
for target_name in targets:
|
|
753
753
|
live_path, history_dir, filename = _resolve_target(target_name)
|
|
754
|
-
current_text = live_path.read_text() if live_path.exists() else ""
|
|
754
|
+
current_text = live_path.read_text(encoding="utf-8") if live_path.exists() else ""
|
|
755
755
|
for snap in list_snapshots(history_dir, filename):
|
|
756
756
|
rows.append(
|
|
757
757
|
{
|
|
@@ -791,7 +791,7 @@ class SystemRestoreCommand(Command):
|
|
|
791
791
|
snap_path = get_snapshot_path(history_dir, filename, timestamp)
|
|
792
792
|
# Snapshot current before restoring (so restore is undoable)
|
|
793
793
|
snapshot_before_write(live_path)
|
|
794
|
-
restored_text = snap_path.read_text()
|
|
794
|
+
restored_text = snap_path.read_text(encoding="utf-8")
|
|
795
795
|
atomic_write_text(live_path, restored_text)
|
|
796
796
|
return {
|
|
797
797
|
"ok": True,
|
|
@@ -830,15 +830,15 @@ class SystemDiffCommand(Command):
|
|
|
830
830
|
live_path, history_dir, filename = _resolve_target(target_name)
|
|
831
831
|
|
|
832
832
|
snap_a = get_snapshot_path(history_dir, filename, args[1])
|
|
833
|
-
text_a = snap_a.read_text()
|
|
833
|
+
text_a = snap_a.read_text(encoding="utf-8")
|
|
834
834
|
label_a = f"{filename}.{args[1]}"
|
|
835
835
|
|
|
836
836
|
if len(args) >= 3:
|
|
837
837
|
snap_b = get_snapshot_path(history_dir, filename, args[2])
|
|
838
|
-
text_b = snap_b.read_text()
|
|
838
|
+
text_b = snap_b.read_text(encoding="utf-8")
|
|
839
839
|
label_b = f"{filename}.{args[2]}"
|
|
840
840
|
else:
|
|
841
|
-
text_b = live_path.read_text() if live_path.exists() else ""
|
|
841
|
+
text_b = live_path.read_text(encoding="utf-8") if live_path.exists() else ""
|
|
842
842
|
label_b = filename
|
|
843
843
|
|
|
844
844
|
result = diff_text(text_a, text_b, label_a, label_b)
|
|
@@ -406,7 +406,7 @@ def load_config(path: Path | None = None) -> YtctlConfig:
|
|
|
406
406
|
if not config_path.exists():
|
|
407
407
|
return YtctlConfig()
|
|
408
408
|
try:
|
|
409
|
-
raw = yaml.safe_load(config_path.read_text()) or {}
|
|
409
|
+
raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
410
410
|
return YtctlConfig(**raw)
|
|
411
411
|
except (yaml.YAMLError, TypeError, ValueError) as e:
|
|
412
412
|
raise ConfigError(f"Invalid config at {config_path}: {e}") from e
|
|
@@ -511,7 +511,7 @@ def update_config_path(path: str, value: str, config_path: Path) -> tuple[str, A
|
|
|
511
511
|
# Load raw YAML, capture original text for rollback.
|
|
512
512
|
raw = _load_raw_yaml(config_path)
|
|
513
513
|
original_text: str | None
|
|
514
|
-
original_text = config_path.read_text() if config_path.exists() else None
|
|
514
|
+
original_text = config_path.read_text(encoding="utf-8") if config_path.exists() else None
|
|
515
515
|
|
|
516
516
|
retention = _retention_from_raw(raw, DEFAULT_RETENTION)
|
|
517
517
|
snapshot_before_write(config_path, retention=retention)
|
|
@@ -619,7 +619,7 @@ def _load_raw_yaml(path: Path) -> dict[str, Any]:
|
|
|
619
619
|
if not path.exists():
|
|
620
620
|
return {}
|
|
621
621
|
try:
|
|
622
|
-
data = yaml.safe_load(path.read_text())
|
|
622
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
623
623
|
except yaml.YAMLError as e:
|
|
624
624
|
raise ConfigError(f"Invalid YAML in {path}: {e}") from e
|
|
625
625
|
return data or {}
|
|
@@ -1395,16 +1395,16 @@ def _manage_gitignore(directory: Path) -> str:
|
|
|
1395
1395
|
|
|
1396
1396
|
gitignore = git_root / ".gitignore"
|
|
1397
1397
|
if gitignore.exists():
|
|
1398
|
-
content = gitignore.read_text()
|
|
1398
|
+
content = gitignore.read_text(encoding="utf-8")
|
|
1399
1399
|
lines = {line.strip() for line in content.splitlines()}
|
|
1400
1400
|
if lines & _GITIGNORE_PATTERNS:
|
|
1401
1401
|
return "already_ignored"
|
|
1402
1402
|
# Append with a trailing newline if file doesn't end with one
|
|
1403
1403
|
sep = "" if content.endswith("\n") else "\n"
|
|
1404
|
-
gitignore.write_text(f"{content}{sep}\n# kd local config\n.kd/\n")
|
|
1404
|
+
gitignore.write_text(f"{content}{sep}\n# kd local config\n.kd/\n", encoding="utf-8")
|
|
1405
1405
|
return "added"
|
|
1406
1406
|
|
|
1407
|
-
gitignore.write_text("# kd local config\n.kd/\n")
|
|
1407
|
+
gitignore.write_text("# kd local config\n.kd/\n", encoding="utf-8")
|
|
1408
1408
|
return "created"
|
|
1409
1409
|
|
|
1410
1410
|
|
|
@@ -1466,7 +1466,7 @@ def init_project_config(
|
|
|
1466
1466
|
)
|
|
1467
1467
|
|
|
1468
1468
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
1469
|
-
config_path.write_text(template)
|
|
1469
|
+
config_path.write_text(template, encoding="utf-8")
|
|
1470
1470
|
|
|
1471
1471
|
if global_tier:
|
|
1472
1472
|
gitignore_action = "not_applicable"
|
|
@@ -174,7 +174,7 @@ def _get_history(path: str, key: str) -> dict[str, Any] | None:
|
|
|
174
174
|
if value is None:
|
|
175
175
|
continue
|
|
176
176
|
# Only surface values actually written to the file, not schema defaults.
|
|
177
|
-
raw = yaml.safe_load(tier_path.read_text()) or {}
|
|
177
|
+
raw = yaml.safe_load(tier_path.read_text(encoding="utf-8")) or {}
|
|
178
178
|
history_block = raw.get("history") if isinstance(raw.get("history"), dict) else {}
|
|
179
179
|
if not isinstance(history_block, dict) or key not in history_block:
|
|
180
180
|
continue
|
|
@@ -121,7 +121,7 @@ def load_primer(path: Path | None = None) -> PrimerData:
|
|
|
121
121
|
if not primer_file.exists():
|
|
122
122
|
return PrimerData()
|
|
123
123
|
try:
|
|
124
|
-
raw = yaml.safe_load(primer_file.read_text()) or {}
|
|
124
|
+
raw = yaml.safe_load(primer_file.read_text(encoding="utf-8")) or {}
|
|
125
125
|
return PrimerData(**raw)
|
|
126
126
|
except (yaml.YAMLError, TypeError, ValueError) as e:
|
|
127
127
|
raise ConfigError(f"Invalid primer at {primer_file}: {e}") from e
|
|
@@ -4,6 +4,34 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.6.4] — 2026-04-30
|
|
8
|
+
|
|
9
|
+
This release closes the Windows UTF-8 audit started in 2.6.3. The
|
|
10
|
+
stream-encoding side (stdout/stderr) shipped earlier; 2.6.4 closes the
|
|
11
|
+
file-IO and stdin sides so primer notes, `--content @file`, and heredoc
|
|
12
|
+
or piped input round-trip non-cp1252 characters cleanly.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- File reads and writes on Windows are now explicit UTF-8 at every
|
|
16
|
+
kd-owned text-IO call site (primer, config, history/diff snapshots,
|
|
17
|
+
`--description @file` / `--content @file` content). The headline
|
|
18
|
+
reproducer was `kd /primer show` raising `'charmap' codec can't
|
|
19
|
+
decode byte 0x9d` whenever `.kd/primer.yaml` contained any non-cp1252
|
|
20
|
+
byte (em-dash, arrow, right double quotation mark) — a single primer
|
|
21
|
+
note authored on macOS/Linux was enough to read-block every
|
|
22
|
+
downstream Windows session (KDCLI-170).
|
|
23
|
+
- Stdin on Windows is now forced to UTF-8 with strict error handling,
|
|
24
|
+
so heredocs and pipes carrying non-ASCII content (em-dash, arrow,
|
|
25
|
+
right double quotation mark) into `kd /primer/note update -`,
|
|
26
|
+
`kd /article create --content @-`, `--description -`, or any other
|
|
27
|
+
stdin-consuming command no longer get cp1252-mojibaked at the input
|
|
28
|
+
layer before being persisted. Invalid UTF-8 bytes on stdin (and
|
|
29
|
+
non-UTF-8 `@file` content) now surface as a clean
|
|
30
|
+
`Error: Stdin input could not be decoded as UTF-8 ... / Hint: Ensure
|
|
31
|
+
the upstream shell pipes UTF-8 bytes ...` (or
|
|
32
|
+
`Hint: Save the file as UTF-8 ...`) instead of the generic "internal
|
|
33
|
+
error: ... This is a bug." block (KDCLI-169).
|
|
34
|
+
|
|
7
35
|
## [2.6.3] — 2026-04-30
|
|
8
36
|
|
|
9
37
|
This release focuses on Windows shakedown: encoding crashes, primer
|
|
@@ -244,7 +244,7 @@ kd -DDD /issue list --project KDCLI
|
|
|
244
244
|
|
|
245
245
|
Debug output goes to stderr and tokens are redacted. Use `-DDD` when investigating upstream API behavior because it includes request and response details.
|
|
246
246
|
|
|
247
|
-
On Windows, kd forces UTF-8 stdout and stderr at the entry point so non-ASCII characters (`→`, `—`, `≤`, em-dash) render correctly regardless of the console's default code page (cp1252 on most installs). No `PYTHONIOENCODING` workaround is needed.
|
|
247
|
+
On Windows, kd forces UTF-8 on stdin, stdout, and stderr at the entry point so non-ASCII characters (`→`, `—`, `≤`, em-dash) render correctly regardless of the console's default code page (cp1252 on most installs), and every kd-owned file read/write (primer, config, history snapshots, `--content @file` and `--description @file`) uses explicit UTF-8 so primer/config files written by macOS or Linux load cleanly on Windows. Stdin uses strict decoding so invalid UTF-8 piped into `kd /primer/note update -` or `kd /article create --content @-` fails loudly with a clear diagnostic (`Error: Stdin input could not be decoded as UTF-8 ...`) rather than silently corrupting persisted content. No `PYTHONIOENCODING` or `PYTHONUTF8` workaround is needed.
|
|
248
248
|
|
|
249
249
|
Unhandled exceptions render a one-line or one-paragraph friendly diagnostic and exit non-zero. To see the full traceback (for bug reports or local debugging), re-run with `--debug` / `-D`; the wrapper re-raises the original exception so the runtime prints the traceback. `KdError` (expected user-facing failures) keep their existing one-line `Error: ...` / optional `Hint: ...` shape on every platform.
|
|
250
250
|
|
|
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
|