fix(cli): add allow_unicode=True and encoding="utf-8" to YAML I/O (#1936)

None of the yaml.dump() calls specify allow_unicode=True, causing
non-ASCII characters in extension descriptions to be escaped to
\uXXXX sequences in generated .agent.md frontmatter and config files.

Add allow_unicode=True to all 6 yaml.dump() call sites, and
encoding="utf-8" to all corresponding write_text() and read_text()
calls to ensure consistent UTF-8 handling across platforms.
This commit is contained in:
Seiya Kojima
2026-03-23 22:49:10 +09:00
committed by GitHub
parent 6223d10d84
commit a351c826ee
5 changed files with 31 additions and 18 deletions

View File

@@ -3042,7 +3042,7 @@ def preset_catalog_add(
# Load existing config # Load existing config
if config_path.exists(): if config_path.exists():
try: try:
config = yaml.safe_load(config_path.read_text()) or {} config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception as e: except Exception as e:
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}") console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
raise typer.Exit(1) raise typer.Exit(1)
@@ -3070,7 +3070,7 @@ def preset_catalog_add(
}) })
config["catalogs"] = catalogs config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
install_label = "install allowed" if install_allowed else "discovery only" install_label = "install allowed" if install_allowed else "discovery only"
console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})") console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})")
@@ -3098,7 +3098,7 @@ def preset_catalog_remove(
raise typer.Exit(1) raise typer.Exit(1)
try: try:
config = yaml.safe_load(config_path.read_text()) or {} config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception: except Exception:
console.print("[red]Error:[/red] Failed to read preset catalog config.") console.print("[red]Error:[/red] Failed to read preset catalog config.")
raise typer.Exit(1) raise typer.Exit(1)
@@ -3115,7 +3115,7 @@ def preset_catalog_remove(
raise typer.Exit(1) raise typer.Exit(1)
config["catalogs"] = catalogs config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
console.print(f"[green]✓[/green] Removed catalog '{name}'") console.print(f"[green]✓[/green] Removed catalog '{name}'")
if not catalogs: if not catalogs:
@@ -3384,7 +3384,7 @@ def catalog_add(
# Load existing config # Load existing config
if config_path.exists(): if config_path.exists():
try: try:
config = yaml.safe_load(config_path.read_text()) or {} config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception as e: except Exception as e:
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}") console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
raise typer.Exit(1) raise typer.Exit(1)
@@ -3412,7 +3412,7 @@ def catalog_add(
}) })
config["catalogs"] = catalogs config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
install_label = "install allowed" if install_allowed else "discovery only" install_label = "install allowed" if install_allowed else "discovery only"
console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})") console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})")
@@ -3440,7 +3440,7 @@ def catalog_remove(
raise typer.Exit(1) raise typer.Exit(1)
try: try:
config = yaml.safe_load(config_path.read_text()) or {} config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception: except Exception:
console.print("[red]Error:[/red] Failed to read catalog config.") console.print("[red]Error:[/red] Failed to read catalog config.")
raise typer.Exit(1) raise typer.Exit(1)
@@ -3457,7 +3457,7 @@ def catalog_remove(
raise typer.Exit(1) raise typer.Exit(1)
config["catalogs"] = catalogs config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
console.print(f"[green]✓[/green] Removed catalog '{name}'") console.print(f"[green]✓[/green] Removed catalog '{name}'")
if not catalogs: if not catalogs:

View File

@@ -207,7 +207,7 @@ class CommandRegistrar:
if not fm: if not fm:
return "" return ""
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False) yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True)
return f"---\n{yaml_str}---\n" return f"---\n{yaml_str}---\n"
def _adjust_script_paths(self, frontmatter: dict) -> dict: def _adjust_script_paths(self, frontmatter: dict) -> dict:

View File

@@ -975,8 +975,8 @@ class ExtensionCatalog:
if not config_path.exists(): if not config_path.exists():
return None return None
try: try:
data = yaml.safe_load(config_path.read_text()) or {} data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError) as e: except (yaml.YAMLError, OSError, UnicodeError) as e:
raise ValidationError( raise ValidationError(
f"Failed to read catalog config {config_path}: {e}" f"Failed to read catalog config {config_path}: {e}"
) )
@@ -1467,8 +1467,8 @@ class ConfigManager:
return {} return {}
try: try:
return yaml.safe_load(file_path.read_text()) or {} return yaml.safe_load(file_path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError): except (yaml.YAMLError, OSError, UnicodeError):
return {} return {}
def _get_extension_defaults(self) -> Dict[str, Any]: def _get_extension_defaults(self) -> Dict[str, Any]:
@@ -1659,8 +1659,8 @@ class HookExecutor:
} }
try: try:
return yaml.safe_load(self.config_file.read_text()) or {} return yaml.safe_load(self.config_file.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError): except (yaml.YAMLError, OSError, UnicodeError):
return { return {
"installed": [], "installed": [],
"settings": {"auto_execute_hooks": True}, "settings": {"auto_execute_hooks": True},
@@ -1675,7 +1675,8 @@ class HookExecutor:
""" """
self.config_file.parent.mkdir(parents=True, exist_ok=True) self.config_file.parent.mkdir(parents=True, exist_ok=True)
self.config_file.write_text( self.config_file.write_text(
yaml.dump(config, default_flow_style=False, sort_keys=False) yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True),
encoding="utf-8",
) )
def register_hooks(self, manifest: ExtensionManifest): def register_hooks(self, manifest: ExtensionManifest):

View File

@@ -1062,8 +1062,8 @@ class PresetCatalog:
if not config_path.exists(): if not config_path.exists():
return None return None
try: try:
data = yaml.safe_load(config_path.read_text()) or {} data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError) as e: except (yaml.YAMLError, OSError, UnicodeError) as e:
raise PresetValidationError( raise PresetValidationError(
f"Failed to read catalog config {config_path}: {e}" f"Failed to read catalog config {config_path}: {e}"
) )

View File

@@ -747,6 +747,18 @@ $ARGUMENTS
assert output.endswith("---\n") assert output.endswith("---\n")
assert "description: Test command" in output assert "description: Test command" in output
def test_render_frontmatter_unicode(self):
"""Test rendering frontmatter preserves non-ASCII characters."""
frontmatter = {
"description": "Prüfe Konformität der Implementierung"
}
registrar = CommandRegistrar()
output = registrar.render_frontmatter(frontmatter)
assert "Prüfe Konformität" in output
assert "\\u" not in output
def test_register_commands_for_claude(self, extension_dir, project_dir): def test_register_commands_for_claude(self, extension_dir, project_dir):
"""Test registering commands for Claude agent.""" """Test registering commands for Claude agent."""
# Create .claude directory # Create .claude directory