fix(design-system): make the skill work outside a vendored checkout and on Windows (#460)

* fix(design-system): resolve project root from cwd, not __file__

fetch-background.py and html-token-validator.py derived PROJECT_ROOT with five
.parent hops, which only reaches the project root when the skill is vendored at
<project>/.claude/skills/design-system/scripts/. Installed at user level
(~/.claude/skills/) or as a plugin, PROJECT_ROOT pointed outside the project, so
both scripts silently ran against no tokens at all.

Resolve from the working directory instead, matching generate-tokens.cjs and
validate-tokens.cjs which already use process.cwd(). DESIGN_SYSTEM_PROJECT_ROOT
overrides it when the project root cannot be inferred.

slide_search_core.py is left alone: it resolves skill-relative data, which is
the correct use of __file__.

Refs #459

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(design-system): stop findProjectRoot from hanging on Windows

embed-tokens.cjs walked up the tree with `while (dir !== '/')`. On Windows the
filesystem root is 'C:\', so that condition is never true, and path.dirname('C:\')
returns 'C:\' unchanged -- the loop spins forever at 100% CPU instead of erroring
out, whenever assets/design-tokens.css is not found above the cwd.

Stop when dirname stops changing, which terminates on every platform.

Refs #459

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(design-system): force UTF-8 stdout so emoji output works on cp1252 consoles

search-slides.py --context and html-token-validator.py print emoji. On a Windows
console the default encoding is cp1252, so the first emoji raises
UnicodeEncodeError and the command dies with a traceback instead of output --
this takes out --context, the entry point of the contextual slide system.

Reuse the guard already shipped in src/ui-ux-pro-max/scripts/search.py.

Refs #459

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(design-system): decode subprocess output as UTF-8 in validate-tokens tests

test_validate_tokens.py drives validate-tokens.cjs through subprocess.run with
text=True but no explicit encoding, so Python decodes the pipe with the locale
codec. On Windows (cp1252) the validator's emoji output raises UnicodeDecodeError
inside the reader thread, result.stdout comes back as None, and the assertion
fails with a confusing `TypeError: argument of type 'NoneType' is not a
container` -- this suite cannot pass on Windows at all today.

Pin the pipe and the fixture write to UTF-8. The validator itself was never at
fault: run by hand it flags the hardcoded hex correctly.

Note: brand/scripts/tests/test_sync_brand_to_tokens.py uses the same
text=True-without-encoding pattern and is one emoji away from failing the same
way. Left alone to keep this PR scoped to design-system.

Refs #459

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
djatadougbewilfried-star 2026-08-31 18:42:36 +00:00 committed by GitHub
parent bd19ab9070
commit f23267105a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 158 additions and 14 deletions

View File

@ -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;
}

View File

@ -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'

View File

@ -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'

View File

@ -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"""

View File

@ -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",
)

View File

@ -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;
}

View File

@ -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'

View File

@ -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'

View File

@ -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"""

View File

@ -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",
)