diff --git a/.claude/skills/design-system/scripts/embed-tokens.cjs b/.claude/skills/design-system/scripts/embed-tokens.cjs index 419c104..e677f0c 100644 --- a/.claude/skills/design-system/scripts/embed-tokens.cjs +++ b/.claude/skills/design-system/scripts/embed-tokens.cjs @@ -15,12 +15,16 @@ const path = require('path'); // Find project root (look for assets/design-tokens.css) function findProjectRoot(startDir) { + // Walk up until dirname stops changing: on Windows the root is 'C:\', so a + // `dir !== '/'` guard never terminates. let dir = startDir; - while (dir !== '/') { + for (;;) { if (fs.existsSync(path.join(dir, 'assets', 'design-tokens.css'))) { return dir; } - dir = path.dirname(dir); + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; } return null; } diff --git a/.claude/skills/design-system/scripts/fetch-background.py b/.claude/skills/design-system/scripts/fetch-background.py index bcbd357..08a9970 100644 --- a/.claude/skills/design-system/scripts/fetch-background.py +++ b/.claude/skills/design-system/scripts/fetch-background.py @@ -9,10 +9,32 @@ import json import csv import re import sys +import os from pathlib import Path -# Project root relative to this script -PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent +# The skill can be installed outside the project it operates on (user-level +# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from +# this file's location. Resolve it from the working directory instead -- the same +# convention generate-tokens.cjs and validate-tokens.cjs already use via +# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly. +def _find_project_root(): + override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT') + if override: + return Path(override).resolve() + start = Path.cwd().resolve() + markers = ( + Path('assets') / 'design-tokens.json', + Path('assets') / 'design-tokens.css', + Path('package.json'), + Path('.git'), + ) + for candidate in (start, *start.parents): + if any((candidate / marker).exists() for marker in markers): + return candidate + return start + + +PROJECT_ROOT = _find_project_root() TOKENS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json' BACKGROUNDS_CSV = Path(__file__).parent.parent / 'data' / 'slide-backgrounds.csv' diff --git a/.claude/skills/design-system/scripts/html-token-validator.py b/.claude/skills/design-system/scripts/html-token-validator.py index a722498..2b9c5d0 100644 --- a/.claude/skills/design-system/scripts/html-token-validator.py +++ b/.claude/skills/design-system/scripts/html-token-validator.py @@ -15,11 +15,43 @@ Usage: import re import json import sys +import os from pathlib import Path from typing import Dict, List, Tuple, Optional -# Project root relative to this script -PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent +# The skill can be installed outside the project it operates on (user-level +# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from +# this file's location. Resolve it from the working directory instead -- the same +# convention generate-tokens.cjs and validate-tokens.cjs already use via +# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly. +def _find_project_root(): + override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT') + if override: + return Path(override).resolve() + start = Path.cwd().resolve() + markers = ( + Path('assets') / 'design-tokens.json', + Path('assets') / 'design-tokens.css', + Path('package.json'), + Path('.git'), + ) + for candidate in (start, *start.parents): + if any((candidate / marker).exists() for marker in markers): + return candidate + return start + + +PROJECT_ROOT = _find_project_root() + +# Force UTF-8 on stdout/stderr: this script prints emoji, which raises +# UnicodeEncodeError on a Windows console (cp1252). Same guard as +# src/ui-ux-pro-max/scripts/search.py. +import io + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json' TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css' diff --git a/.claude/skills/design-system/scripts/search-slides.py b/.claude/skills/design-system/scripts/search-slides.py index ff5200a..fc9ce74 100755 --- a/.claude/skills/design-system/scripts/search-slides.py +++ b/.claude/skills/design-system/scripts/search-slides.py @@ -13,6 +13,16 @@ from slide_search_core import ( get_color_for_emotion, get_background_config ) +# Force UTF-8 on stdout/stderr: this script prints emoji, which raises +# UnicodeEncodeError on a Windows console (cp1252). Same guard as +# src/ui-ux-pro-max/scripts/search.py. +import io + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + def format_result(result, domain): """Format a single search result for display""" diff --git a/.claude/skills/design-system/scripts/tests/test_validate_tokens.py b/.claude/skills/design-system/scripts/tests/test_validate_tokens.py index bbde4ec..6754246 100644 --- a/.claude/skills/design-system/scripts/tests/test_validate_tokens.py +++ b/.claude/skills/design-system/scripts/tests/test_validate_tokens.py @@ -20,11 +20,15 @@ def _run(tmp_path: Path, css: str) -> subprocess.CompletedProcess: node = shutil.which("node") if not node: pytest.skip("node not available") - (tmp_path / "sample.css").write_text(css) + (tmp_path / "sample.css").write_text(css, encoding="utf-8") return subprocess.run( [node, str(SCRIPT), "--dir", str(tmp_path)], capture_output=True, text=True, + # validate-tokens.cjs prints emoji; without an explicit encoding Python + # decodes the pipe with the locale codec (cp1252 on Windows), which + # raises in the reader thread and leaves result.stdout set to None. + encoding="utf-8", ) diff --git a/cli/assets/skills/design-system/scripts/embed-tokens.cjs b/cli/assets/skills/design-system/scripts/embed-tokens.cjs index 419c104..e677f0c 100644 --- a/cli/assets/skills/design-system/scripts/embed-tokens.cjs +++ b/cli/assets/skills/design-system/scripts/embed-tokens.cjs @@ -15,12 +15,16 @@ const path = require('path'); // Find project root (look for assets/design-tokens.css) function findProjectRoot(startDir) { + // Walk up until dirname stops changing: on Windows the root is 'C:\', so a + // `dir !== '/'` guard never terminates. let dir = startDir; - while (dir !== '/') { + for (;;) { if (fs.existsSync(path.join(dir, 'assets', 'design-tokens.css'))) { return dir; } - dir = path.dirname(dir); + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; } return null; } diff --git a/cli/assets/skills/design-system/scripts/fetch-background.py b/cli/assets/skills/design-system/scripts/fetch-background.py index bcbd357..08a9970 100644 --- a/cli/assets/skills/design-system/scripts/fetch-background.py +++ b/cli/assets/skills/design-system/scripts/fetch-background.py @@ -9,10 +9,32 @@ import json import csv import re import sys +import os from pathlib import Path -# Project root relative to this script -PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent +# The skill can be installed outside the project it operates on (user-level +# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from +# this file's location. Resolve it from the working directory instead -- the same +# convention generate-tokens.cjs and validate-tokens.cjs already use via +# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly. +def _find_project_root(): + override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT') + if override: + return Path(override).resolve() + start = Path.cwd().resolve() + markers = ( + Path('assets') / 'design-tokens.json', + Path('assets') / 'design-tokens.css', + Path('package.json'), + Path('.git'), + ) + for candidate in (start, *start.parents): + if any((candidate / marker).exists() for marker in markers): + return candidate + return start + + +PROJECT_ROOT = _find_project_root() TOKENS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json' BACKGROUNDS_CSV = Path(__file__).parent.parent / 'data' / 'slide-backgrounds.csv' diff --git a/cli/assets/skills/design-system/scripts/html-token-validator.py b/cli/assets/skills/design-system/scripts/html-token-validator.py index a722498..2b9c5d0 100644 --- a/cli/assets/skills/design-system/scripts/html-token-validator.py +++ b/cli/assets/skills/design-system/scripts/html-token-validator.py @@ -15,11 +15,43 @@ Usage: import re import json import sys +import os from pathlib import Path from typing import Dict, List, Tuple, Optional -# Project root relative to this script -PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent +# The skill can be installed outside the project it operates on (user-level +# ~/.claude/skills/, or as a plugin), so the project root cannot be derived from +# this file's location. Resolve it from the working directory instead -- the same +# convention generate-tokens.cjs and validate-tokens.cjs already use via +# process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT overrides it explicitly. +def _find_project_root(): + override = os.environ.get('DESIGN_SYSTEM_PROJECT_ROOT') + if override: + return Path(override).resolve() + start = Path.cwd().resolve() + markers = ( + Path('assets') / 'design-tokens.json', + Path('assets') / 'design-tokens.css', + Path('package.json'), + Path('.git'), + ) + for candidate in (start, *start.parents): + if any((candidate / marker).exists() for marker in markers): + return candidate + return start + + +PROJECT_ROOT = _find_project_root() + +# Force UTF-8 on stdout/stderr: this script prints emoji, which raises +# UnicodeEncodeError on a Windows console (cp1252). Same guard as +# src/ui-ux-pro-max/scripts/search.py. +import io + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json' TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css' diff --git a/cli/assets/skills/design-system/scripts/search-slides.py b/cli/assets/skills/design-system/scripts/search-slides.py index ff5200a..fc9ce74 100644 --- a/cli/assets/skills/design-system/scripts/search-slides.py +++ b/cli/assets/skills/design-system/scripts/search-slides.py @@ -13,6 +13,16 @@ from slide_search_core import ( get_color_for_emotion, get_background_config ) +# Force UTF-8 on stdout/stderr: this script prints emoji, which raises +# UnicodeEncodeError on a Windows console (cp1252). Same guard as +# src/ui-ux-pro-max/scripts/search.py. +import io + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + def format_result(result, domain): """Format a single search result for display""" diff --git a/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py b/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py index bbde4ec..6754246 100644 --- a/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py +++ b/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py @@ -20,11 +20,15 @@ def _run(tmp_path: Path, css: str) -> subprocess.CompletedProcess: node = shutil.which("node") if not node: pytest.skip("node not available") - (tmp_path / "sample.css").write_text(css) + (tmp_path / "sample.css").write_text(css, encoding="utf-8") return subprocess.run( [node, str(SCRIPT), "--dir", str(tmp_path)], capture_output=True, text=True, + # validate-tokens.cjs prints emoji; without an explicit encoding Python + # decodes the pipe with the locale codec (cp1252 on Windows), which + # raises in the reader thread and leaves result.stdout set to None. + encoding="utf-8", )