mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dacf7d4664 | ||
|
|
634b579b63 | ||
|
|
4c5f3974b3 | ||
|
|
2422ec058f | ||
|
|
0f115e5d37 | ||
|
|
66945a79da | ||
|
|
a82233736c | ||
|
|
67d018fe05 | ||
|
|
3bdb9ff06c | ||
|
|
cd12f8660e | ||
|
|
43088d8fbf | ||
|
|
a8ce5962d3 | ||
|
|
7d6c5bdd92 | ||
|
|
73552b79c5 | ||
|
|
73a6f51a54 |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
allowed-tools:
|
||||
- Bash(npx impeccable *)
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
metadata:
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "4.2.3",
|
||||
"version": "4.3.1",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "4.2.3",
|
||||
"version": "4.3.1",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
allowed-tools:
|
||||
- Bash(npx impeccable *)
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
user-invocable: true
|
||||
license: Apache 2.0
|
||||
allowed-tools:
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -87,6 +87,11 @@ fi
|
||||
# Last resort: fetch this version's binary for the current platform from the
|
||||
# public release channel into the user cache. Needs network; sandboxes without
|
||||
# egress preinstall the binary on PATH instead.
|
||||
setup_help() {
|
||||
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
||||
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
||||
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
||||
}
|
||||
fetch_url() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
||||
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
[ "$os" = windows ] && asset="$asset.exe"
|
||||
url="$base/engine-v$version/$asset"
|
||||
tmp="$cache_root/bin/$version/.impeccable.part.$$"
|
||||
mkdir -p "$cache_root/bin/$version" 2>/dev/null
|
||||
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
# Check the actual staging file, not just directory existence: a cache from
|
||||
# an earlier run can be readable but no longer writable inside a sandbox.
|
||||
if ! (umask 077; : > "$tmp") 2>/dev/null; then
|
||||
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
fetched=0
|
||||
if fetch_url "$url"; then
|
||||
fetched=1
|
||||
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
|
||||
exec "$cached" "$@"
|
||||
fi
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
||||
setup_help
|
||||
exit 127
|
||||
fi
|
||||
|
||||
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
|
||||
|
||||
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
|
||||
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
||||
if not defined version goto fail
|
||||
where curl.exe >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto curl_missing
|
||||
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
||||
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
||||
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
||||
if errorlevel 1 goto cache_directory_failed
|
||||
:cache_ready
|
||||
rem Check the staging file too: an existing directory may be read-only.
|
||||
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
||||
rem branch on the command's failure directly. Never treat a directory as a
|
||||
rem staging file (later del cleanup would prompt to delete its contents).
|
||||
if exist "%cached%.part\" goto cache_write_failed
|
||||
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
||||
set "asset=impeccable-windows-%arch%.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if not errorlevel 1 goto verify
|
||||
if not "%arch%"=="arm64" goto fail
|
||||
if not "%arch%"=="arm64" goto download_failed
|
||||
set "asset=impeccable-windows-x64.exe"
|
||||
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
||||
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
||||
if errorlevel 1 goto fail
|
||||
if errorlevel 1 goto download_failed
|
||||
|
||||
:verify
|
||||
call :check_download
|
||||
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
|
||||
del "%probe_tmp%" >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:cache_directory_failed
|
||||
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:cache_write_failed
|
||||
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:curl_missing
|
||||
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
||||
goto setup_failed
|
||||
|
||||
:download_failed
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
||||
|
||||
:setup_failed
|
||||
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
||||
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
||||
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
||||
exit /b 127
|
||||
|
||||
:fail
|
||||
del "%cached%.part" >nul 2>nul
|
||||
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
||||
|
||||
Generated
+19
-16
@@ -509,6 +509,7 @@ dependencies = [
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
@@ -525,7 +526,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"impeccable-browser",
|
||||
@@ -544,7 +545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-browser"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"impeccable-core",
|
||||
@@ -558,7 +559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-bundle"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"impeccable-core",
|
||||
@@ -567,14 +568,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-common"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-comp"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"image",
|
||||
"once_cell",
|
||||
@@ -586,7 +587,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-comp-verbs"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-common",
|
||||
"impeccable-comp",
|
||||
@@ -599,7 +600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-context"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"impeccable-common",
|
||||
@@ -618,7 +619,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-core",
|
||||
"impeccable-foundation",
|
||||
@@ -630,7 +631,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-detect"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-common",
|
||||
"impeccable-core",
|
||||
@@ -642,7 +643,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-foundation"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"once_cell",
|
||||
@@ -655,7 +656,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-hook"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-common",
|
||||
"impeccable-context",
|
||||
@@ -669,11 +670,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-html"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cssparser",
|
||||
"ego-tree",
|
||||
"html5ever",
|
||||
"image",
|
||||
"impeccable-common",
|
||||
"impeccable-core",
|
||||
"impeccable-detect",
|
||||
@@ -690,7 +693,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-live"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"impeccable-common",
|
||||
@@ -708,7 +711,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-skills"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-common",
|
||||
"impeccable-context",
|
||||
@@ -727,7 +730,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "impeccable-wasm"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-core",
|
||||
"impeccable-detect",
|
||||
@@ -1758,7 +1761,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"impeccable-bundle",
|
||||
]
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ resolver = "2"
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.1.5
|
||||
|
||||
@@ -465,7 +465,7 @@ npx impeccable ignores add-value overused-font Inter --reason "Brand font"
|
||||
|
||||
The detector catches 61 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more).
|
||||
|
||||
Human-readable findings are diagnostics written to stderr, so redirect them with `2> findings.txt`. Use `--json` for machine-readable results on stdout. Exit `0` means the scan completed without primary findings, exit `2` means it completed with primary findings, and exit `1` means at least one requested target could not be scanned; operational failure takes precedence for a partial multi-target scan. URL scans inspect the rendered DOM, computed layout, and accessible linked stylesheets; browser security still prevents reading cross-origin CSS without CORS. A clean detector run is evidence, not proof of visual or accessibility quality: it does not replace inspecting the rendered experience across relevant viewports.
|
||||
Human-readable findings are diagnostics written to stderr, so redirect them with `2> findings.txt`. Use `--json` for machine-readable results on stdout. Exit `0` means the scan completed without primary findings, exit `2` means it completed with primary findings, and exit `1` means at least one requested target could not be scanned; operational failure takes precedence for a partial multi-target scan. URL scans inspect the rendered DOM, computed layout, and accessible linked stylesheets; browser security still prevents reading cross-origin CSS without CORS. File scans read the pixels of local background images behind text, so white copy on a near-white photo fails a CI gate without a browser; a remote image is never fetched. A clean detector run is evidence, not proof of visual or accessibility quality: it does not replace inspecting the rendered experience across relevant viewports.
|
||||
|
||||
By default, `detect` respects the same `.impeccable/config.json` and `.impeccable/config.local.json` detector config as the design hook: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. Hook lifecycle settings such as `hook.enabled` only affect automatic hook execution.
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@impeccable/cli-darwin-arm64": "0.1.4",
|
||||
"@impeccable/cli-darwin-x64": "0.1.4",
|
||||
"@impeccable/cli-linux-arm64": "0.1.4",
|
||||
"@impeccable/cli-linux-x64": "0.1.4",
|
||||
"@impeccable/cli-windows-x64": "0.1.4",
|
||||
"@impeccable/cli-darwin-arm64": "0.1.5",
|
||||
"@impeccable/cli-darwin-x64": "0.1.5",
|
||||
"@impeccable/cli-linux-arm64": "0.1.5",
|
||||
"@impeccable/cli-linux-x64": "0.1.5",
|
||||
"@impeccable/cli-windows-x64": "0.1.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -72,15 +72,15 @@
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||
|
||||
"@impeccable/cli-darwin-arm64": ["@impeccable/cli-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64", "bin": { "impeccable-darwin-arm64": "bin/impeccable" } }, "sha512-NNG8NLjQuNfyXwRBA72Jo2z4H8yd60qhkKF7a25pFR/+DKckH5V6TRIqNfgOaxlOWmY6RTUvPvGR4xSJDWLMGA=="],
|
||||
"@impeccable/cli-darwin-arm64": ["@impeccable/cli-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64", "bin": { "impeccable-darwin-arm64": "bin/impeccable" } }, "sha512-RvSrbsvzHsyfVaL693mMiWWLauZXas00F5VrNoaqOUvOm478HJUIUnl77H393F9k9LWZNiNFLwIinRnGJ92naA=="],
|
||||
|
||||
"@impeccable/cli-darwin-x64": ["@impeccable/cli-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64", "bin": { "impeccable-darwin-x64": "bin/impeccable" } }, "sha512-oGVjCvm/3DjpxDfMJcKVC+JFbZkTK3vnolI2AY5Uqkv82qjGJOgMQTp6XQIuNQEyrHB8Gv2CWj2XRMvnFpNR0Q=="],
|
||||
"@impeccable/cli-darwin-x64": ["@impeccable/cli-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64", "bin": { "impeccable-darwin-x64": "bin/impeccable" } }, "sha512-wYfhNhVtgosKIjxxWdJ3X/KydASdDIAAdmng8shKUgfiCkY9Tld0psuhVJOdkKoJaRajagp7a2f/tCNN1xdBZA=="],
|
||||
|
||||
"@impeccable/cli-linux-arm64": ["@impeccable/cli-linux-arm64@0.1.4", "", { "os": "linux", "cpu": "arm64", "bin": { "impeccable-linux-arm64": "bin/impeccable" } }, "sha512-t67/u941EhnjibHrMwDWrvImaLBUw7T5RMmnRv4FDmb2xPO56Cusi4S2d7P11+wkNMGYWe8xKhn9eelsvvGRsw=="],
|
||||
"@impeccable/cli-linux-arm64": ["@impeccable/cli-linux-arm64@0.1.5", "", { "os": "linux", "cpu": "arm64", "bin": { "impeccable-linux-arm64": "bin/impeccable" } }, "sha512-gjWJbOa7/wAECkIOac/x6mq43lDGaGwH87/ErsnrFwk/s5hgtlBF0sPePeB30e/lu1VzGMJcXVUEA8EULv4cCg=="],
|
||||
|
||||
"@impeccable/cli-linux-x64": ["@impeccable/cli-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64", "bin": { "impeccable-linux-x64": "bin/impeccable" } }, "sha512-0jkLoaVW1m5yVbtJeybSJjgp7p+LMw1EuP0yzosQq5uiSYjUArVDtLHPrBvDJHPVquPLtMV9+X2maDJJkgyvkg=="],
|
||||
"@impeccable/cli-linux-x64": ["@impeccable/cli-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64", "bin": { "impeccable-linux-x64": "bin/impeccable" } }, "sha512-JZU7kd7s4DOz9w2gwbbpI1u3YLhrcAUNZg/awjZcQFHJwpizv/MhPVUjHKYuChrPn+Oxlpw7KU8adKY1YrSq8g=="],
|
||||
|
||||
"@impeccable/cli-windows-x64": ["@impeccable/cli-windows-x64@0.1.4", "", { "os": "win32", "cpu": "x64", "bin": { "impeccable-windows-x64": "bin/impeccable.exe" } }, "sha512-aBBEEYcIlQ//pZEtLHNozSZJCgMZpQlpX1GVwyu9B9hM5PJA4Y9i/0g5jEyQ2Cbrjsfz9kOF6FalQk+AIrXC0Q=="],
|
||||
"@impeccable/cli-windows-x64": ["@impeccable/cli-windows-x64@0.1.5", "", { "os": "win32", "cpu": "x64", "bin": { "impeccable-windows-x64": "bin/impeccable.exe" } }, "sha512-kVZFjQIjb84mF2klxTspxE1OilalO/OmYDQnLkCJeDJCQLr6+L6HvbghJ1TChq87uyMu4GQNDPr9VV+h+Z+tCQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
|
||||
@@ -30,9 +30,39 @@ static LAUNCHER_HOOK_MARKER: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r#"skills/impeccable/scripts/impeccable(?:\.cmd|\.exe)?["']?\s+hook(?:-before-edit|-probe|-after-edit|-stop)?(?:\s|$|["'&|;)])"#).unwrap()
|
||||
});
|
||||
|
||||
/// User-scope Windows commands embed a JSON-quoted path whose backslashes are
|
||||
/// doubled in the command string (#784). #604's single `\`→`/` replace is not
|
||||
/// enough: `\\` becomes `//`, which breaks `skills/impeccable` matching.
|
||||
/// A leading `//` after a quote (or at the start of the string) is a UNC
|
||||
/// prefix and stays two slashes, so doctor still probes `//server/share/...`.
|
||||
fn normalize_hook_separators(command: &str) -> String {
|
||||
let mut out = String::with_capacity(command.len());
|
||||
let mut chars = command.chars().peekable();
|
||||
let mut prev: Option<char> = None;
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch != '\\' && ch != '/' {
|
||||
out.push(ch);
|
||||
prev = Some(ch);
|
||||
continue;
|
||||
}
|
||||
let mut n = 1usize;
|
||||
while matches!(chars.peek(), Some('\\' | '/')) {
|
||||
chars.next();
|
||||
n += 1;
|
||||
}
|
||||
out.push('/');
|
||||
if n >= 2 && matches!(prev, None | Some('"' | '\'')) {
|
||||
out.push('/');
|
||||
}
|
||||
prev = Some('/');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// True when `command` invokes an Impeccable hook in either generation's spelling.
|
||||
pub fn is_impeccable_hook_command(command: &str) -> bool {
|
||||
LEGACY_HOOK_SCRIPT_MARKERS.iter().any(|m| command.contains(m)) || LAUNCHER_HOOK_MARKER.is_match(command)
|
||||
let command = normalize_hook_separators(command);
|
||||
LEGACY_HOOK_SCRIPT_MARKERS.iter().any(|m| command.contains(m)) || LAUNCHER_HOOK_MARKER.is_match(&command)
|
||||
}
|
||||
|
||||
/// True when `command` invokes an Impeccable hook in the launcher generation
|
||||
@@ -40,7 +70,7 @@ pub fn is_impeccable_hook_command(command: &str) -> bool {
|
||||
/// one: install/update use this to decide which manifests still need
|
||||
/// migrating to the launcher form.
|
||||
pub fn is_launcher_hook_command(command: &str) -> bool {
|
||||
LAUNCHER_HOOK_MARKER.is_match(command)
|
||||
LAUNCHER_HOOK_MARKER.is_match(&normalize_hook_separators(command))
|
||||
}
|
||||
|
||||
/// The launcher-era markers `context` and `doctor` treat as the design hook
|
||||
@@ -54,9 +84,10 @@ static LAUNCHER_DESIGN_HOOK: Lazy<Regex> = Lazy::new(|| {
|
||||
/// `hook-before-edit`) in either spelling; the JS `context.mjs` scan and
|
||||
/// `staleness-deep` HOOK_SCRIPT_MARKERS both meant exactly these two.
|
||||
pub fn is_design_hook_command(command: &str) -> bool {
|
||||
let command = normalize_hook_separators(command);
|
||||
command.contains("skills/impeccable/scripts/hook.mjs")
|
||||
|| command.contains("skills/impeccable/scripts/hook-before-edit.mjs")
|
||||
|| LAUNCHER_DESIGN_HOOK.is_match(command)
|
||||
|| LAUNCHER_DESIGN_HOOK.is_match(&command)
|
||||
}
|
||||
|
||||
/// True when `command` runs the design hook (`hook` / `hook-before-edit`) in
|
||||
@@ -66,7 +97,7 @@ pub fn is_design_hook_command(command: &str) -> bool {
|
||||
/// so the hook is dead and `MANUAL_DETECTOR_REQUIRED` must fire until an
|
||||
/// install/update repairs it.
|
||||
pub fn is_launcher_design_hook_command(command: &str) -> bool {
|
||||
LAUNCHER_DESIGN_HOOK.is_match(command)
|
||||
LAUNCHER_DESIGN_HOOK.is_match(&normalize_hook_separators(command))
|
||||
}
|
||||
|
||||
/// The shell token that names the hook program inside `command`, for
|
||||
@@ -74,7 +105,11 @@ pub fn is_launcher_design_hook_command(command: &str) -> bool {
|
||||
/// path in the binary form. `None` when the command carries no marker or
|
||||
/// the token cannot be isolated (a `'\''` escape sequence, for instance).
|
||||
pub fn hook_program_token(command: &str) -> Option<String> {
|
||||
if !is_design_hook_command(command) {
|
||||
if command.contains("'\\''") {
|
||||
return None;
|
||||
}
|
||||
let command = normalize_hook_separators(command);
|
||||
if !is_design_hook_command(&command) {
|
||||
return None;
|
||||
}
|
||||
static QUOTED: Lazy<Regex> = Lazy::new(|| {
|
||||
@@ -86,16 +121,13 @@ pub fn hook_program_token(command: &str) -> Option<String> {
|
||||
static BARE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r#"([^\s"'|&;()]*skills/impeccable/scripts/(?:hook(?:-before-edit)?\.mjs|impeccable(?:\.cmd|\.exe)?))"#).unwrap()
|
||||
});
|
||||
if let Some(m) = QUOTED.captures(command) {
|
||||
if let Some(m) = QUOTED.captures(&command) {
|
||||
return Some(m[1].to_string());
|
||||
}
|
||||
if command.contains("'\\''") {
|
||||
return None;
|
||||
}
|
||||
if let Some(m) = SINGLE.captures(command) {
|
||||
if let Some(m) = SINGLE.captures(&command) {
|
||||
return Some(m[1].to_string());
|
||||
}
|
||||
BARE.captures(command).map(|m| m[1].to_string())
|
||||
BARE.captures(&command).map(|m| m[1].to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -183,4 +215,45 @@ mod tests {
|
||||
assert_eq!(hook_program_token("'/x/it'\\''s/.claude/skills/impeccable/scripts/impeccable' hook"), None);
|
||||
assert_eq!(hook_program_token("echo hi"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_json_escaped_windows_launcher_path() {
|
||||
let launcher = r"C:\Users\alice\.claude\skills\impeccable\scripts\impeccable";
|
||||
let quoted = serde_json::to_string(launcher).unwrap();
|
||||
let cmd = format!("[ ! -f {quoted} ] || {quoted} hook");
|
||||
assert!(is_impeccable_hook_command(&cmd), "{cmd}");
|
||||
assert!(is_launcher_hook_command(&cmd), "{cmd}");
|
||||
assert!(is_design_hook_command(&cmd), "{cmd}");
|
||||
assert_eq!(
|
||||
hook_program_token(&cmd).as_deref(),
|
||||
Some("C:/Users/alice/.claude/skills/impeccable/scripts/impeccable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_single_backslash_windows_path() {
|
||||
let cmd = r#"[ ! -f "C:\Users\alice\.claude\skills\impeccable\scripts\impeccable" ] || "C:\Users\alice\.claude\skills\impeccable\scripts\impeccable" hook"#;
|
||||
assert!(is_impeccable_hook_command(cmd), "{cmd}");
|
||||
assert!(is_launcher_hook_command(cmd), "{cmd}");
|
||||
assert!(is_design_hook_command(cmd), "{cmd}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_unc_prefix_in_program_token() {
|
||||
let launcher = r"\\server\share\.claude\skills\impeccable\scripts\impeccable";
|
||||
let quoted = serde_json::to_string(launcher).unwrap();
|
||||
let json_escaped = format!("[ ! -f {quoted} ] || {quoted} hook");
|
||||
assert!(is_impeccable_hook_command(&json_escaped), "{json_escaped}");
|
||||
assert_eq!(
|
||||
hook_program_token(&json_escaped).as_deref(),
|
||||
Some("//server/share/.claude/skills/impeccable/scripts/impeccable")
|
||||
);
|
||||
|
||||
let single = r#"[ ! -f "\\server\share\.claude\skills\impeccable\scripts\impeccable" ] || "\\server\share\.claude\skills\impeccable\scripts\impeccable" hook"#;
|
||||
assert!(is_impeccable_hook_command(single), "{single}");
|
||||
assert_eq!(
|
||||
hook_program_token(single).as_deref(),
|
||||
Some("//server/share/.claude/skills/impeccable/scripts/impeccable")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
//! checkNumberedSectionLabels, checkEmDashOveruse, isRepeatedTextContainer.
|
||||
//! Open (selector and tag lists, thresholds, the two text parsers):
|
||||
//! `impeccable_foundation::rules::text`.
|
||||
//! - `sampled_contrast`: the static engine's pixel-sampled verdict for text
|
||||
//! over a `url()` background (#560): grid, decoration gate, wash and
|
||||
//! compositing rules, the percentile verdict. No JS ancestor.
|
||||
//!
|
||||
//! Element/document adapters (`checkElement*`, `*DOM`, `*FromDoc`) are NOT in
|
||||
//! core: the static ones live in the `html` crate against its DOM model, the
|
||||
@@ -43,6 +46,7 @@ pub mod css_scan;
|
||||
pub mod html_patterns;
|
||||
pub mod measures;
|
||||
pub mod rules;
|
||||
pub mod sampled_contrast;
|
||||
pub mod text_rules;
|
||||
|
||||
#[cfg(feature = "vectors")]
|
||||
|
||||
@@ -135,21 +135,30 @@ fn is_heading_123(tag: &str) -> bool {
|
||||
matches!(tag, "h1" | "h2" | "h3")
|
||||
}
|
||||
|
||||
/// The `SAFE_TAGS` skip at the top of `checkColors`: a link, control, or
|
||||
/// quote is measured only when it paints its own surface. Shared with the
|
||||
/// sampled-contrast path so both skip the same elements.
|
||||
pub fn safe_tag_unstyled(opts: &ColorOpts) -> bool {
|
||||
if !set_has(SAFE_TAGS, opts.tag.as_str()) {
|
||||
return false;
|
||||
}
|
||||
let bg_image = opts.bg_image.as_deref().unwrap_or("");
|
||||
let own_bg = opts
|
||||
.bg_color
|
||||
.map_or(false, |c| c.a.map_or(false, |a| a > 0.5));
|
||||
let own_gradient = !bg_image.is_empty() && GRADIENT_CI.is_match(bg_image);
|
||||
let is_styled_control =
|
||||
opts.has_direct_text && (own_bg || own_gradient) && opts.font_size >= 9.0;
|
||||
!is_styled_control
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkColors
|
||||
pub fn check_colors(opts: &ColorOpts) -> Vec<RuleHit> {
|
||||
let tag = opts.tag.as_str();
|
||||
let bg_image = opts.bg_image.as_deref().unwrap_or("");
|
||||
let bg_clip = opts.bg_clip.as_deref().unwrap_or("");
|
||||
if set_has(SAFE_TAGS, tag) {
|
||||
let own_bg = opts
|
||||
.bg_color
|
||||
.map_or(false, |c| c.a.map_or(false, |a| a > 0.5));
|
||||
let own_gradient = !bg_image.is_empty() && GRADIENT_CI.is_match(bg_image);
|
||||
let is_styled_control =
|
||||
opts.has_direct_text && (own_bg || own_gradient) && opts.font_size >= 9.0;
|
||||
if !is_styled_control {
|
||||
return Vec::new();
|
||||
}
|
||||
if safe_tag_unstyled(opts) {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut findings = Vec::new();
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
//! Pixel-sampled contrast for text over a `url()` background image in the
|
||||
//! static engine (#560). Outside a browser there is no CORS and no canvas
|
||||
//! taint, so the engine reads the image itself; what it cannot know is
|
||||
//! layout. The verdict is therefore coarse: a fixed grid over the whole
|
||||
//! image, and a finding only when nearly all of it fails. A photo with a dark
|
||||
//! third under the headline passes; white text on a near-white texture does
|
||||
//! not, wherever the text sits.
|
||||
//!
|
||||
//! This module is the decisions: grid geometry, the decoration gate, the
|
||||
//! wash and compositing rules, the percentile verdict and its label. Reading
|
||||
//! and decoding the image is the html crate's job.
|
||||
|
||||
use crate::checks::rules::{safe_tag_unstyled, ColorOpts, RuleHit};
|
||||
use crate::color::{color_to_hex, composite_color_over, contrast_ratio, Rgba};
|
||||
use crate::constants::{WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX};
|
||||
use crate::js::{number_to_string, parse_float, to_fixed};
|
||||
|
||||
/// Grid points per axis: 6x6 = 36 samples, sampling rather than scanning.
|
||||
const GRID: usize = 6;
|
||||
/// A `no-repeat` layer painted smaller than this on a non-repeating axis is
|
||||
/// an icon, badge, or rule, not the ground the text sits on.
|
||||
const DECORATION_MAX_PX: f64 = 160.0;
|
||||
/// The share of the grid that has to resolve to a color before a verdict.
|
||||
const MIN_SAMPLE_SHARE: f64 = 0.75;
|
||||
/// The finding fires when this share of the samples fails: the text is
|
||||
/// somewhere on the image, so only a near-uniform failure is a failure.
|
||||
const FAIL_PERCENTILE: f64 = 0.9;
|
||||
|
||||
/// The color rule's own gates, so the sampler never fires where
|
||||
/// `check_colors` would not have measured a resolved background.
|
||||
pub fn applies(opts: &ColorOpts) -> bool {
|
||||
opts.has_direct_text
|
||||
&& opts.text_color.is_some()
|
||||
&& !opts.is_emoji_only
|
||||
&& opts.bg_clip.as_deref() != Some("text")
|
||||
&& !safe_tag_unstyled(opts)
|
||||
}
|
||||
|
||||
/// The sample positions over a `width` x `height` raster: cell centers of
|
||||
/// the `GRID`, row-major.
|
||||
pub fn grid_points(width: usize, height: usize) -> Vec<(usize, usize)> {
|
||||
if width == 0 || height == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let at = |i: usize, extent: usize| -> usize {
|
||||
let v = ((i as f64 + 0.5) / GRID as f64 * extent as f64).floor() as usize;
|
||||
v.min(extent - 1)
|
||||
};
|
||||
let mut points = Vec::with_capacity(GRID * GRID);
|
||||
for gy in 0..GRID {
|
||||
for gx in 0..GRID {
|
||||
points.push((at(gx, width), at(gy, height)));
|
||||
}
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
/// Whether a layer with this `background-repeat` and `background-size`
|
||||
/// (the layer's own comma-list entries) paints too little to be the ground:
|
||||
/// a non-repeating axis whose painted extent is under
|
||||
/// `DECORATION_MAX_PX`. Repeating, `cover`, `contain`, and percentage sizes
|
||||
/// fill the box and are never decoration.
|
||||
pub fn is_decorative_layer(repeat: &str, size: &str, intrinsic_w: f64, intrinsic_h: f64) -> bool {
|
||||
let repeat = repeat.trim().to_ascii_lowercase();
|
||||
let tokens: Vec<&str> = repeat.split_whitespace().collect();
|
||||
let (repeat_x, repeat_y) = match tokens.as_slice() {
|
||||
["no-repeat"] => (false, false),
|
||||
["repeat-x"] => (true, false),
|
||||
["repeat-y"] => (false, true),
|
||||
[x, y] => (*x != "no-repeat", *y != "no-repeat"),
|
||||
_ => (true, true),
|
||||
};
|
||||
if repeat_x && repeat_y {
|
||||
return false;
|
||||
}
|
||||
let size = size.trim().to_ascii_lowercase();
|
||||
if size == "cover" || size == "contain" || size.contains('%') {
|
||||
return false;
|
||||
}
|
||||
let tokens: Vec<&str> = size.split_whitespace().collect();
|
||||
let px = |t: Option<&&str>| t.filter(|t| t.ends_with("px")).map(|t| parse_float(t));
|
||||
// An `auto` (or absent) axis follows the other at the image's aspect
|
||||
// ratio, as in CSS: `40px`, `40px auto`, and `auto 40px` all scale.
|
||||
let (w, h) = match (px(tokens.first()), px(tokens.get(1))) {
|
||||
(Some(w), Some(h)) => (w, h),
|
||||
(Some(w), None) if intrinsic_w > 0.0 => (w, intrinsic_h * (w / intrinsic_w)),
|
||||
(None, Some(h)) if intrinsic_h > 0.0 => (intrinsic_w * (h / intrinsic_h), h),
|
||||
(Some(w), None) => (w, intrinsic_h),
|
||||
(None, Some(h)) => (intrinsic_w, h),
|
||||
(None, None) => (intrinsic_w, intrinsic_h),
|
||||
};
|
||||
(!repeat_x && w < DECORATION_MAX_PX) || (!repeat_y && h < DECORATION_MAX_PX)
|
||||
}
|
||||
|
||||
/// A translucent gradient whose stops are all one color is a tint over the
|
||||
/// image and composites exactly; a gradient that varies is a scrim placed
|
||||
/// under the text on purpose, and without layout the engine cannot say
|
||||
/// which stop the text sits on. `None` for the varying case.
|
||||
pub fn uniform_wash(stops: &[Rgba]) -> Option<Rgba> {
|
||||
let first = *stops.first()?;
|
||||
let same = |s: &Rgba| {
|
||||
s.r == first.r
|
||||
&& s.g == first.g
|
||||
&& s.b == first.b
|
||||
&& (s.alpha_or_one() - first.alpha_or_one()).abs() < 1e-9
|
||||
};
|
||||
stops.iter().all(same).then_some(first)
|
||||
}
|
||||
|
||||
/// One grid sample's ground color: the pixel, composited over `under` when
|
||||
/// it is translucent, then under every `overlays` layer (top to bottom, as
|
||||
/// the walk collected them). `None` when a translucent pixel has nothing
|
||||
/// known beneath it.
|
||||
pub fn composite_sample(pixel: Rgba, under: Option<Rgba>, overlays: &[Rgba]) -> Option<Rgba> {
|
||||
let mut color = if pixel.alpha_or_one() >= 0.99 {
|
||||
Rgba::new(pixel.r, pixel.g, pixel.b, 1.0)
|
||||
} else {
|
||||
composite_color_over(&pixel, &under?)
|
||||
};
|
||||
for overlay in overlays.iter().rev() {
|
||||
color = composite_color_over(overlay, &color);
|
||||
}
|
||||
Some(color)
|
||||
}
|
||||
|
||||
/// The verdict over the resolved `samples` of a grid of `grid_total` points,
|
||||
/// against the text of `opts`. `ground` names the image in the snippet (its
|
||||
/// file name, or `data:image/png`).
|
||||
pub fn sampled_contrast(
|
||||
opts: &ColorOpts,
|
||||
ground: &str,
|
||||
samples: &[Rgba],
|
||||
grid_total: usize,
|
||||
) -> Option<RuleHit> {
|
||||
if !applies(opts) {
|
||||
return None;
|
||||
}
|
||||
let text = opts.text_color?;
|
||||
let needed = ((grid_total as f64) * MIN_SAMPLE_SHARE).ceil().max(1.0) as usize;
|
||||
if samples.len() < needed {
|
||||
return None;
|
||||
}
|
||||
let mut ratios: Vec<f64> = samples
|
||||
.iter()
|
||||
.map(|bg| {
|
||||
let fg = if text.alpha_or_one() < 1.0 {
|
||||
composite_color_over(&text, bg)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
contrast_ratio(&fg, bg)
|
||||
})
|
||||
.collect();
|
||||
ratios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let n = ratios.len();
|
||||
let pick = |share: f64| ratios[((share * n as f64).floor() as usize).min(n - 1)];
|
||||
let measured = pick(FAIL_PERCENTILE);
|
||||
let median = pick(0.5);
|
||||
let is_large_text = opts.font_size >= WCAG_LARGE_TEXT_PX
|
||||
|| (opts.font_size >= WCAG_LARGE_BOLD_TEXT_PX && opts.font_weight >= 700.0);
|
||||
let threshold = if is_large_text { 3.0 } else { 4.5 };
|
||||
if measured >= threshold {
|
||||
return None;
|
||||
}
|
||||
let ratio_label = if to_fixed(measured, 1) == to_fixed(threshold, 1) {
|
||||
to_fixed(measured, 2)
|
||||
} else {
|
||||
to_fixed(measured, 1)
|
||||
};
|
||||
Some(RuleHit::new(
|
||||
"low-contrast",
|
||||
format!(
|
||||
"sampled (coarse) {}:1 (need {}:1) — text {} on {}; p90 of {} samples, median {}:1",
|
||||
ratio_label,
|
||||
number_to_string(threshold),
|
||||
color_to_hex(Some(&text)),
|
||||
ground,
|
||||
n,
|
||||
to_fixed(median, 1)
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rgba(r: f64, g: f64, b: f64, a: f64) -> Rgba {
|
||||
Rgba::new(r, g, b, a)
|
||||
}
|
||||
|
||||
fn opts(text: Rgba, font_size: f64) -> ColorOpts {
|
||||
ColorOpts {
|
||||
tag: "p".into(),
|
||||
text_color: Some(text),
|
||||
font_size,
|
||||
font_weight: 400.0,
|
||||
has_direct_text: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_covers_cell_centers_and_clamps() {
|
||||
let points = grid_points(96, 64);
|
||||
assert_eq!(points.len(), 36);
|
||||
assert_eq!(points[0], (8, 5));
|
||||
assert_eq!(points[35], (88, 58));
|
||||
assert_eq!(grid_points(1, 1), vec![(0, 0); 36]);
|
||||
assert!(grid_points(0, 10).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoration_gate() {
|
||||
assert!(is_decorative_layer("no-repeat", "auto", 24.0, 24.0));
|
||||
assert!(is_decorative_layer("no-repeat", "40px", 1200.0, 800.0));
|
||||
assert!(is_decorative_layer("no-repeat", "40px auto", 1200.0, 800.0));
|
||||
assert!(is_decorative_layer("no-repeat", "auto 40px", 1200.0, 800.0));
|
||||
assert!(!is_decorative_layer(
|
||||
"no-repeat",
|
||||
"auto 800px",
|
||||
1200.0,
|
||||
800.0
|
||||
));
|
||||
assert!(is_decorative_layer("repeat-y", "auto", 8.0, 400.0));
|
||||
assert!(is_decorative_layer("repeat-x", "auto", 1200.0, 80.0));
|
||||
assert!(!is_decorative_layer("no-repeat", "cover", 24.0, 24.0));
|
||||
assert!(!is_decorative_layer("no-repeat", "100% auto", 24.0, 24.0));
|
||||
assert!(!is_decorative_layer("no-repeat", "auto", 1600.0, 900.0));
|
||||
assert!(!is_decorative_layer("repeat", "auto", 4.0, 4.0));
|
||||
assert!(!is_decorative_layer("", "", 16.0, 16.0));
|
||||
assert!(!is_decorative_layer(
|
||||
"repeat no-repeat",
|
||||
"auto",
|
||||
1200.0,
|
||||
300.0
|
||||
));
|
||||
assert!(is_decorative_layer(
|
||||
"repeat no-repeat",
|
||||
"auto",
|
||||
1200.0,
|
||||
30.0
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn washes_and_compositing() {
|
||||
let tint = rgba(0.0, 0.0, 0.0, 0.4);
|
||||
assert_eq!(uniform_wash(&[tint, tint]), Some(tint));
|
||||
assert_eq!(uniform_wash(&[tint, rgba(0.0, 0.0, 0.0, 0.0)]), None);
|
||||
assert_eq!(uniform_wash(&[]), None);
|
||||
let light = rgba(240.0, 240.0, 240.0, 1.0);
|
||||
assert_eq!(composite_sample(light, None, &[]), Some(light));
|
||||
let dimmed = composite_sample(light, None, &[tint]).unwrap();
|
||||
assert_eq!((dimmed.r, dimmed.g, dimmed.b), (144.0, 144.0, 144.0));
|
||||
let translucent = rgba(240.0, 240.0, 240.0, 0.25);
|
||||
assert_eq!(composite_sample(translucent, None, &[]), None);
|
||||
let over_dark =
|
||||
composite_sample(translucent, Some(rgba(20.0, 20.0, 20.0, 1.0)), &[]).unwrap();
|
||||
assert_eq!((over_dark.r, over_dark.a), (75.0, Some(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verdict_needs_a_near_uniform_failure() {
|
||||
let white = rgba(253.0, 253.0, 253.0, 1.0);
|
||||
let light = rgba(243.0, 239.0, 230.0, 1.0);
|
||||
let dark = rgba(26.0, 24.0, 22.0, 1.0);
|
||||
let all_light: Vec<Rgba> = vec![light; 36];
|
||||
let hit = sampled_contrast(&opts(white, 16.0), "hero.png", &all_light, 36).unwrap();
|
||||
assert_eq!(hit.id, "low-contrast");
|
||||
assert_eq!(
|
||||
hit.snippet,
|
||||
"sampled (coarse) 1.1:1 (need 4.5:1) — text #fdfdfd on hero.png; p90 of 36 samples, median 1.1:1"
|
||||
);
|
||||
// Half the image is dark: the text may well sit there.
|
||||
let split: Vec<Rgba> = (0..36)
|
||||
.map(|i| if i % 2 == 0 { light } else { dark })
|
||||
.collect();
|
||||
assert!(sampled_contrast(&opts(white, 16.0), "hero.png", &split, 36).is_none());
|
||||
// Three dark samples out of 36 are not enough to save it.
|
||||
let mostly: Vec<Rgba> = (0..36).map(|i| if i < 3 { dark } else { light }).collect();
|
||||
assert!(sampled_contrast(&opts(white, 16.0), "hero.png", &mostly, 36).is_some());
|
||||
// Too few resolved samples: no verdict.
|
||||
assert!(sampled_contrast(&opts(white, 16.0), "hero.png", &all_light[..20], 36).is_none());
|
||||
// Large text lowers the bar, and the snippet says so.
|
||||
let mid = rgba(150.0, 150.0, 150.0, 1.0);
|
||||
let on_mid: Vec<Rgba> = vec![mid; 36];
|
||||
assert!(
|
||||
sampled_contrast(&opts(white, 28.0), "hero.png", &on_mid, 36)
|
||||
.unwrap()
|
||||
.snippet
|
||||
.contains("(need 3:1)")
|
||||
);
|
||||
// Safe tags and gradient-clipped text never sample.
|
||||
let mut anchor = opts(white, 16.0);
|
||||
anchor.tag = "a".into();
|
||||
assert!(sampled_contrast(&anchor, "hero.png", &all_light, 36).is_none());
|
||||
let mut clipped = opts(white, 16.0);
|
||||
clipped.bg_clip = Some("text".into());
|
||||
assert!(sampled_contrast(&clipped, "hero.png", &all_light, 36).is_none());
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ html5ever = "0.39"
|
||||
selectors = "0.38"
|
||||
cssparser = "0.37"
|
||||
thiserror = { workspace = true }
|
||||
# Decoding the background images behind text for the sampled-contrast path
|
||||
# (#560): pure-Rust decoders only (the comp crate's set plus `png`), so the
|
||||
# wasm `detect` build keeps working and nothing native is linked.
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
|
||||
base64 = "0.22"
|
||||
|
||||
[dev-dependencies]
|
||||
impeccable-core = { workspace = true, features = ["vectors"] }
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
//! and the computed style and hands plain data over.
|
||||
|
||||
use crate::background::{
|
||||
a_ge, a_gt, read_own_background_color, resolve_background, resolve_background_info,
|
||||
resolve_border_radius_px, resolve_gradient_stops, sv, sv_opt, CustomPropMap,
|
||||
a_ge, a_gt, find_image_ground, read_own_background_color, resolve_background,
|
||||
resolve_background_info, resolve_border_radius_px, resolve_gradient_stops, sv, sv_opt,
|
||||
CustomPropMap,
|
||||
};
|
||||
use crate::cascade::StyleValues;
|
||||
use crate::dom::{StaticDocument, StaticElement};
|
||||
use crate::image_sampling::{ground_label, ImageSampler};
|
||||
use crate::quality::{collapse_ws, pf0, resolve_font_size_px};
|
||||
use impeccable_core::checks::measures::{
|
||||
self, border_colors_from_style, border_widths_from_style, check_gpt_thin_border_wide_shadow,
|
||||
@@ -24,13 +26,14 @@ use impeccable_core::checks::rules::{
|
||||
GlowOpts, HeroEyebrowOpts, HoverContrastOpts, IconTileOpts, ItalicSerifOpts, KickerCandidate,
|
||||
MotionOpts, RuleHit, Sides,
|
||||
};
|
||||
use impeccable_core::checks::sampled_contrast;
|
||||
use impeccable_core::checks::text_rules::{
|
||||
check_numbered_section_labels, is_kicker_candidate, is_numbered_section_label_candidate,
|
||||
parse_numbered_label_text, KickerCandidateInput, NumberedLabelCandidate,
|
||||
NumberedLabelCandidateInput, HEADING_TAGS, KICKER_CARD_CONTEXT_SELECTOR, KICKER_SKIP_SELECTOR,
|
||||
POSITIONED_CHILD_INTERACTIVE_SELECTOR,
|
||||
};
|
||||
use impeccable_core::color::{composite_color_over, parse_any_color, parse_rgb};
|
||||
use impeccable_core::color::{composite_color_over, parse_any_color, parse_rgb, Rgba};
|
||||
use impeccable_core::js::{self, parse_float, parse_int};
|
||||
use impeccable_core::js_ext_a::num_truthy;
|
||||
use impeccable_core::js_ext_b::slice_utf16_prefix;
|
||||
@@ -461,11 +464,15 @@ pub fn check_element_borders(
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkElementColors(el, style, tag, window, customPropMap, hasAnchorInheritRule)
|
||||
///
|
||||
/// `images` serves the one branch the JS never had: text whose ground is a
|
||||
/// `url()` layer is measured against the image's pixels (#560).
|
||||
pub fn check_element_colors(
|
||||
el: &StaticElement<'_>,
|
||||
style: &StyleValues,
|
||||
tag: &str,
|
||||
custom_props: CustomPropMap<'_>,
|
||||
images: &ImageSampler,
|
||||
) -> Vec<RuleHit> {
|
||||
if sv_opt(style, "visibility") == Some("hidden") {
|
||||
return Vec::new();
|
||||
@@ -539,7 +546,7 @@ pub fn check_element_colors(
|
||||
sv(style, "backgroundClip")
|
||||
}
|
||||
};
|
||||
check_colors(&ColorOpts {
|
||||
let opts = ColorOpts {
|
||||
tag: tag.to_string(),
|
||||
text_color,
|
||||
bg_color: own_bg,
|
||||
@@ -557,7 +564,44 @@ pub fn check_element_colors(
|
||||
bg_image: Some(sv(style, "backgroundImage").to_string()),
|
||||
class_list: Some(el.class_name().to_string()),
|
||||
detector_is_browser: false,
|
||||
})
|
||||
};
|
||||
let mut findings = check_colors(&opts);
|
||||
if surface_unresolved {
|
||||
findings.extend(sampled_image_contrast(el, &opts, images));
|
||||
}
|
||||
findings
|
||||
}
|
||||
|
||||
/// The sampled-contrast path (#560): when the analytic walk gave up at a
|
||||
/// `url()` layer, read that image and measure the text against a grid of
|
||||
/// its pixels. Every gate that needs no IO runs first, so a container
|
||||
/// without direct text never touches the file.
|
||||
fn sampled_image_contrast(
|
||||
el: &StaticElement<'_>,
|
||||
opts: &ColorOpts,
|
||||
images: &ImageSampler,
|
||||
) -> Option<RuleHit> {
|
||||
if !sampled_contrast::applies(opts) {
|
||||
return None;
|
||||
}
|
||||
let ground = find_image_ground(el)?;
|
||||
let raster = images.load(&ground.url)?;
|
||||
if sampled_contrast::is_decorative_layer(
|
||||
&ground.repeat,
|
||||
&ground.size,
|
||||
raster.intrinsic_width as f64,
|
||||
raster.intrinsic_height as f64,
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let points = sampled_contrast::grid_points(raster.width as usize, raster.height as usize);
|
||||
let samples: Vec<Rgba> = points
|
||||
.iter()
|
||||
.filter_map(|&(x, y)| {
|
||||
sampled_contrast::composite_sample(raster.pixel(x, y), ground.under, &ground.overlays)
|
||||
})
|
||||
.collect();
|
||||
sampled_contrast::sampled_contrast(opts, &ground_label(&ground.url), &samples, points.len())
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkElementHoverContrast(el, style, tag, window)
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
//! `compositeGradientStops`, `resolveBorderRadiusPx`), static-engine
|
||||
//! branches only (`DETECTOR_IS_BROWSER === false`).
|
||||
|
||||
use crate::cascade::csstree::strings::decode_url;
|
||||
use crate::cascade::StyleValues;
|
||||
use crate::dom::StaticElement;
|
||||
use impeccable_core::checks::measures::{parse_color_resolved, parse_radius_to_px, CustomProps};
|
||||
use impeccable_core::checks::sampled_contrast::uniform_wash;
|
||||
use impeccable_core::color::{
|
||||
composite_color_over, is_no_paint_color_value, parse_any_color, parse_gradient_colors,
|
||||
parse_rgb, split_top_level_commas, Rgba,
|
||||
@@ -331,3 +333,151 @@ pub fn composite_gradient_stops(
|
||||
pub fn resolve_border_radius_px(style: &StyleValues, width_px: f64) -> f64 {
|
||||
parse_radius_to_px(sv_opt(style, "borderRadius"), width_px).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
// ─── the image ground (#560) ────────────────────────────────────────────────
|
||||
|
||||
/// The image a text element sits on when the walk ends at a `url()` layer:
|
||||
/// what the sampled-contrast path reads instead of skipping.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ImageGround {
|
||||
/// The layer's `url()` argument, unescaped and unquoted.
|
||||
pub url: String,
|
||||
/// The layer's own `background-repeat` and `background-size` entries.
|
||||
pub repeat: String,
|
||||
pub size: String,
|
||||
/// Translucent paint between the text and the image, top to bottom:
|
||||
/// tinted surfaces up the chain and uniform gradient washes.
|
||||
pub overlays: Vec<Rgba>,
|
||||
/// What a translucent pixel composites over, when the walk can say.
|
||||
pub under: Option<Rgba>,
|
||||
}
|
||||
|
||||
/// One entry of a comma-separated background list, with the CSS wraparound
|
||||
/// for a list shorter than the image list.
|
||||
fn layer_entry(value: &str, index: usize) -> String {
|
||||
let parts = split_top_level_commas(value);
|
||||
if parts.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
js::trim(&parts[index % parts.len()]).to_string()
|
||||
}
|
||||
|
||||
/// The `url()` argument of one background layer: `url(x\ y.png)` from the
|
||||
/// css-tree generator, `url("x y.png")` from a style attribute. A shorthand
|
||||
/// layer may carry other tokens around the call (`#000 url(x)center/cover`),
|
||||
/// so only the call itself is decoded.
|
||||
fn layer_url(layer: &str) -> String {
|
||||
let Some(call) = URL_CALL_RE.find(layer) else {
|
||||
return String::new();
|
||||
};
|
||||
let open = call.end();
|
||||
let mut depth = 1usize;
|
||||
let mut quote: Option<char> = None;
|
||||
let mut close = layer.len();
|
||||
for (i, ch) in layer[open..].char_indices() {
|
||||
match quote {
|
||||
Some(q) if ch == q => quote = None,
|
||||
Some(_) => {}
|
||||
None if ch == '"' || ch == '\'' => quote = Some(ch),
|
||||
None if ch == '(' => depth += 1,
|
||||
None if ch == ')' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
close = open + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
let decoded = decode_url(&format!("url({}", &layer[open..close]));
|
||||
let trimmed = js::trim(&decoded);
|
||||
let unquoted = trimmed
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.or_else(|| {
|
||||
trimmed
|
||||
.strip_prefix('\'')
|
||||
.and_then(|s| s.strip_suffix('\''))
|
||||
})
|
||||
.unwrap_or(trimmed);
|
||||
unquoted.to_string()
|
||||
}
|
||||
|
||||
/// The mirror of [`resolve_background_info`]'s walk for the case it reports
|
||||
/// as unresolved because a `url()` layer is the ground: that layer plus
|
||||
/// everything painted between the text and it. `None` whenever the analytic
|
||||
/// walk would have resolved a color or given up for another reason (an
|
||||
/// opaque gradient, an unparseable color, a varying scrim).
|
||||
pub fn find_image_ground(el: &StaticElement<'_>) -> Option<ImageGround> {
|
||||
let mut current = Some(*el);
|
||||
let mut overlays: Vec<Rgba> = Vec::new();
|
||||
while let Some(cur) = current {
|
||||
let style = cur.style();
|
||||
let mut bg = read_cascade_background_color(&cur, style, None);
|
||||
if (bg.is_none() || bg.as_ref().is_some_and(|c| a_lt(c, 0.1)))
|
||||
&& CURRENTCOLOR_RE.is_match(js::trim(sv(style, "backgroundColor")))
|
||||
{
|
||||
let color = sv_opt(style, "color");
|
||||
bg = parse_rgb(color).or_else(|| parse_color_resolved(color, None));
|
||||
}
|
||||
if bg.as_ref().is_some_and(|c| a_ge(c, 0.99)) {
|
||||
return None;
|
||||
}
|
||||
if bg.is_none() && !is_no_paint_color_value(sv_opt(style, "backgroundColor")) {
|
||||
return None;
|
||||
}
|
||||
let bg_image = sv(style, "backgroundImage");
|
||||
let layers: Vec<String> = if bg_image.is_empty() || bg_image == "none" {
|
||||
Vec::new()
|
||||
} else {
|
||||
split_top_level_commas(bg_image)
|
||||
};
|
||||
for (index, layer) in layers.iter().enumerate() {
|
||||
let layer = js::trim(layer);
|
||||
// A shorthand layer may put color, position, size, or repeat
|
||||
// tokens before the image call; the call is what matters.
|
||||
let is_gradient = GRADIENT_CALL_RE.is_match(layer);
|
||||
if !is_gradient && URL_CALL_RE.is_match(layer) {
|
||||
// A layer beneath the image is unknown paint; otherwise the
|
||||
// element's own color over its parent's ground, white at the
|
||||
// root like the analytic walk.
|
||||
let under = if index + 1 < layers.len() {
|
||||
None
|
||||
} else {
|
||||
let parent_ground = match cur.parent_element() {
|
||||
Some(p) => resolve_background(&p, None),
|
||||
None => Some(Rgba::new(255.0, 255.0, 255.0, 1.0)),
|
||||
};
|
||||
match bg {
|
||||
Some(c) if a_gt(&c, 0.1) => {
|
||||
parent_ground.map(|p| composite_color_over(&c, &p))
|
||||
}
|
||||
_ => parent_ground,
|
||||
}
|
||||
};
|
||||
return Some(ImageGround {
|
||||
url: layer_url(layer),
|
||||
repeat: layer_entry(sv(style, "backgroundRepeat"), index),
|
||||
size: layer_entry(sv(style, "backgroundSize"), index),
|
||||
overlays,
|
||||
under,
|
||||
});
|
||||
}
|
||||
if is_gradient {
|
||||
let stops = parse_gradient_colors(Some(layer));
|
||||
if stops.is_empty() || stops.iter().all(|s| s.alpha_or_one() >= 0.99) {
|
||||
return None;
|
||||
}
|
||||
overlays.push(uniform_wash(&stops)?);
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(c) = bg.filter(|c| a_gt(c, 0.1)) {
|
||||
overlays.push(c);
|
||||
}
|
||||
current = cur.parent_element();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
|
||||
use super::checks_shim::CustomProps;
|
||||
use super::{
|
||||
apply_static_declaration, collect_static_css_rules, compare_static_priority,
|
||||
is_static_inherited_prop, make_default_style, normalize_static_css_value,
|
||||
parse_static_style_attribute, static_default_style, CssRule, DeclMeta, SpecifiedDecl,
|
||||
SpecifiedStore, StyleValues, STATIC_DEFAULT_STYLE,
|
||||
apply_static_declaration, apply_static_longhand, background_longhands,
|
||||
collect_static_css_rules, compare_static_priority, is_static_inherited_prop,
|
||||
make_default_style, normalize_static_css_value, parse_static_style_attribute,
|
||||
static_default_style, CssRule, DeclMeta, SpecifiedDecl, SpecifiedStore, StyleValues,
|
||||
STATIC_DEFAULT_STYLE,
|
||||
};
|
||||
use crate::dom::StaticDocument;
|
||||
use crate::profile::{self, Meta, ProfileSink};
|
||||
@@ -39,7 +40,7 @@ static REMOTE_HREF_RE: Lazy<Regex> =
|
||||
/// Cache-busting (styles.css?v=3) and root-relative (/static/app.css) hrefs
|
||||
/// must not resolve as OS-absolute paths; otherwise the whole stylesheet is
|
||||
/// invisible to every element-level check.
|
||||
fn resolve_linked_css_path(file_dir: &str, href: &str) -> String {
|
||||
pub(crate) fn resolve_linked_css_path(file_dir: &str, href: &str) -> String {
|
||||
let stripped = href.split(['?', '#']).next().unwrap_or("");
|
||||
let root_relative = stripped.starts_with('/') && !stripped.starts_with("//");
|
||||
if !root_relative {
|
||||
@@ -117,7 +118,15 @@ pub fn collect_static_css_text(
|
||||
|| std::fs::read(&css_path),
|
||||
);
|
||||
match read {
|
||||
Ok(bytes) => style_texts.push(String::from_utf8_lossy(&bytes).into_owned()),
|
||||
Ok(bytes) => {
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
let sheet_dir = jsp::dirname(&css_path);
|
||||
style_texts.push(if sheet_dir == file_dir_str {
|
||||
text.into_owned()
|
||||
} else {
|
||||
rewrite_sheet_urls(&text, &sheet_dir, &file_dir_str)
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
if warned_missing_stylesheets.insert(css_path.clone()) {
|
||||
if let Some(warn) = warn {
|
||||
@@ -132,6 +141,162 @@ pub fn collect_static_css_text(
|
||||
style_texts.join("\n")
|
||||
}
|
||||
|
||||
// A quoted url cannot span a line and an unquoted one cannot hold
|
||||
// whitespace, quotes, parens, braces, or semicolons (CSS syntax), so a
|
||||
// stray `url(` can never pair with a `)` in a later rule.
|
||||
static CSS_URL_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
// The leading group keeps `url(` from matching inside a longer
|
||||
// identifier such as `myurl(`; the closure emits it unchanged.
|
||||
Regex::new(
|
||||
r#"(?i)(^|[^A-Za-z0-9_-])url\(\s*(?:"((?:[^"\\\n\r]|\\.)*)"|'((?:[^'\\\n\r]|\\.)*)'|([^)"'(\s{};]*))\s*\)"#,
|
||||
)
|
||||
.expect("CSS_URL_RE")
|
||||
});
|
||||
static URL_SCHEME_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^[A-Za-z][A-Za-z0-9+.-]*:").expect("URL_SCHEME_RE"));
|
||||
|
||||
/// A relative `url()` in a stylesheet is relative to the sheet, and the
|
||||
/// cascade sees one concatenated text, so a sheet inlined from another
|
||||
/// directory has its relative urls rewritten to page-relative form here.
|
||||
/// This is what lets the sampled-contrast path (#560) resolve the image the
|
||||
/// winning declaration named. Root-relative, remote, `data:`, fragment, and
|
||||
/// escaped urls are left as they are, and comments are copied through
|
||||
/// untouched, so nothing inside one can reach the rules after it.
|
||||
pub fn rewrite_sheet_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String {
|
||||
let mut out = String::with_capacity(css.len());
|
||||
let mut cursor = 0usize;
|
||||
for (start, end) in comment_spans(css) {
|
||||
out.push_str(&rewrite_code_urls(&css[cursor..start], sheet_dir, page_dir));
|
||||
out.push_str(&css[start..end]);
|
||||
cursor = end;
|
||||
}
|
||||
out.push_str(&rewrite_code_urls(&css[cursor..], sheet_dir, page_dir));
|
||||
out
|
||||
}
|
||||
|
||||
/// The byte spans of every `/* */` comment in `css`, found the way the CSS
|
||||
/// tokenizer finds them: a `/*` inside a quoted string or an unquoted
|
||||
/// `url()` is content, not a comment opener, and an unclosed comment runs to
|
||||
/// the end of the sheet. Every span boundary sits on an ASCII byte, so the
|
||||
/// spans are valid `str` indices.
|
||||
/// A byte that can continue a CSS identifier, so `myurl(` is a custom
|
||||
/// function and not the `url(` token.
|
||||
fn is_ident_byte(b: u8) -> bool {
|
||||
b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b >= 0x80
|
||||
}
|
||||
|
||||
fn comment_spans(css: &str) -> Vec<(usize, usize)> {
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum State {
|
||||
Code,
|
||||
Str(u8),
|
||||
Url,
|
||||
}
|
||||
let bytes = css.as_bytes();
|
||||
let mut spans = Vec::new();
|
||||
let mut state = State::Code;
|
||||
let mut i = 0usize;
|
||||
while i < bytes.len() {
|
||||
let b = bytes[i];
|
||||
match state {
|
||||
State::Code => {
|
||||
if b == b'/' && bytes.get(i + 1) == Some(&b'*') {
|
||||
let end = css[i + 2..]
|
||||
.find("*/")
|
||||
.map(|at| i + 2 + at + 2)
|
||||
.unwrap_or(bytes.len());
|
||||
spans.push((i, end));
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
if b == b'"' || b == b'\'' {
|
||||
state = State::Str(b);
|
||||
} else if b.eq_ignore_ascii_case(&b'u')
|
||||
&& css[i..]
|
||||
.get(..4)
|
||||
.is_some_and(|s| s.eq_ignore_ascii_case("url("))
|
||||
&& !(i > 0 && is_ident_byte(bytes[i - 1]))
|
||||
{
|
||||
i += 4;
|
||||
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
|
||||
i += 1;
|
||||
}
|
||||
// A quoted argument is a string like any other; an
|
||||
// unquoted one runs to the closing paren.
|
||||
if i < bytes.len() && bytes[i] != b'"' && bytes[i] != b'\'' {
|
||||
state = State::Url;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
State::Str(quote) => {
|
||||
if b == b'\\' {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
// A raw newline ends a string in CSS (a bad-string token).
|
||||
if b == quote || b == b'\n' || b == b'\r' {
|
||||
state = State::Code;
|
||||
}
|
||||
}
|
||||
State::Url => {
|
||||
if b == b'\\' {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if b == b')' {
|
||||
state = State::Code;
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
spans
|
||||
}
|
||||
|
||||
fn rewrite_code_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String {
|
||||
CSS_URL_RE
|
||||
.replace_all(css, |caps: ®ex::Captures| {
|
||||
let whole = caps.get(0).map(|m| m.as_str()).unwrap_or("");
|
||||
let before = caps.get(1).map(|m| m.as_str()).unwrap_or("");
|
||||
let (target, quote) = match (caps.get(2), caps.get(3), caps.get(4)) {
|
||||
(Some(m), _, _) => (m.as_str(), "\""),
|
||||
(_, Some(m), _) => (m.as_str(), "'"),
|
||||
(_, _, Some(m)) => (js::trim(m.as_str()), ""),
|
||||
_ => return whole.to_string(),
|
||||
};
|
||||
let lower = js::to_lower_case(target);
|
||||
if target.is_empty()
|
||||
|| target.contains('\\')
|
||||
|| target.starts_with('#')
|
||||
|| target.starts_with('/')
|
||||
|| lower.starts_with("data:")
|
||||
|| URL_SCHEME_RE.is_match(target)
|
||||
{
|
||||
return whole.to_string();
|
||||
}
|
||||
let cut = target.find(['?', '#']).unwrap_or(target.len());
|
||||
let (path, suffix) = target.split_at(cut);
|
||||
// Pure POSIX string math on both directories: a CSS url is
|
||||
// POSIX on every OS, and the win32 helpers would render a drive
|
||||
// path (`D:\a\...`) differently from the url beside it.
|
||||
let sheet = jsp::to_posix(sheet_dir);
|
||||
let page = jsp::to_posix(page_dir);
|
||||
let absolute = jsp::posix::resolve("/", &[&sheet, path]);
|
||||
let relative = jsp::posix::relative("/", &page, &absolute);
|
||||
if relative.is_empty() {
|
||||
return whole.to_string();
|
||||
}
|
||||
let needs_quotes = quote.is_empty()
|
||||
&& relative
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\''));
|
||||
let quote = if needs_quotes { "\"" } else { quote };
|
||||
format!("{before}url({quote}{relative}{suffix}{quote})")
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
static PSEUDO_RULE_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(&format!(
|
||||
r"(?i)^(.+?){ws}*::?(?:before|after)$",
|
||||
@@ -308,6 +473,9 @@ pub fn build_static_style_map(
|
||||
inline: false,
|
||||
};
|
||||
apply_static_declaration(store, node, &decl.prop, &decl.value, &meta);
|
||||
for (prop, value) in background_longhands(&decl.prop, &decl.value) {
|
||||
apply_static_longhand(store, node, &prop, &value, &meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +499,9 @@ pub fn build_static_style_map(
|
||||
inline: true,
|
||||
};
|
||||
apply_static_declaration(&mut specified, node, &decl.prop, &decl.value, &meta);
|
||||
for (prop, value) in background_longhands(&decl.prop, &decl.value) {
|
||||
apply_static_longhand(&mut specified, node, &prop, &value, &meta);
|
||||
}
|
||||
}
|
||||
inline_order += 1000;
|
||||
}
|
||||
|
||||
@@ -187,15 +187,39 @@ pub fn apply_static_declaration<K: Hash + Eq>(
|
||||
) {
|
||||
let map = specified.map.entry(node).or_default();
|
||||
for (expanded_prop, expanded_value) in expand_static_declaration(prop, value) {
|
||||
let existing = map.get(&expanded_prop).map(|d| &d.meta);
|
||||
if compare_static_priority(existing, meta) {
|
||||
let next = SpecifiedDecl {
|
||||
meta: meta.clone(),
|
||||
prop: expanded_prop.clone(),
|
||||
value: expanded_value,
|
||||
};
|
||||
map.insert(expanded_prop, next);
|
||||
}
|
||||
apply_expanded(map, &expanded_prop, &expanded_value, meta);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores one already-expanded longhand for `node` under the cascade's
|
||||
/// priority rule, the way `apply_static_declaration` stores each pair the
|
||||
/// shorthand expansion yields. For the longhands that ride beside that
|
||||
/// expansion rather than inside it (`background_longhands`).
|
||||
pub fn apply_static_longhand<K: Hash + Eq>(
|
||||
specified: &mut SpecifiedStore<K>,
|
||||
node: K,
|
||||
prop: &str,
|
||||
value: &str,
|
||||
meta: &DeclMeta,
|
||||
) {
|
||||
let map = specified.map.entry(node).or_default();
|
||||
apply_expanded(map, prop, value, meta);
|
||||
}
|
||||
|
||||
fn apply_expanded(
|
||||
map: &mut IndexMap<String, SpecifiedDecl>,
|
||||
prop: &str,
|
||||
value: &str,
|
||||
meta: &DeclMeta,
|
||||
) {
|
||||
let existing = map.get(prop).map(|d| &d.meta);
|
||||
if compare_static_priority(existing, meta) {
|
||||
let next = SpecifiedDecl {
|
||||
meta: meta.clone(),
|
||||
prop: prop.to_string(),
|
||||
value: value.to_string(),
|
||||
};
|
||||
map.insert(prop.to_string(), next);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,101 @@ pub fn parse_static_animation(value: &str) -> StaticAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
static BG_REPEAT_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)^(?:repeat|no-repeat|repeat-x|repeat-y|space|round)$").expect("BG_REPEAT_RE")
|
||||
});
|
||||
static BG_SIZE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?i)^(?:auto|cover|contain|-?[0-9.]+[a-z%]*)$").expect("BG_SIZE_RE"));
|
||||
|
||||
/// The per-layer `background-repeat` and `background-size` lists of a
|
||||
/// `background` shorthand (`url(a) center / cover no-repeat, url(b)` ->
|
||||
/// `no-repeat, repeat` and `cover, auto`). The css-tree generator may glue
|
||||
/// the first keyword to the image (`url(a)center/cover`), so a token that
|
||||
/// holds a function call is read up to its closing paren and the rest kept.
|
||||
fn parse_static_background_layers(value: &str) -> (String, String) {
|
||||
let mut repeats: Vec<String> = Vec::new();
|
||||
let mut sizes: Vec<String> = Vec::new();
|
||||
for layer in split_css_list(value) {
|
||||
let mut repeat: Vec<String> = Vec::new();
|
||||
let mut size: Vec<String> = Vec::new();
|
||||
let mut in_size = false;
|
||||
for raw in split_css_tokens(&layer) {
|
||||
let token = match raw.rfind(')') {
|
||||
Some(end) if raw.contains('(') => raw[end + 1..].to_string(),
|
||||
_ => raw,
|
||||
};
|
||||
if token.is_empty() {
|
||||
in_size = false;
|
||||
continue;
|
||||
}
|
||||
// `center/cover`, `center / cover`, or `center/ cover`: the size
|
||||
// is what follows the slash, one or two values.
|
||||
if let Some((_, after)) = token.split_once('/') {
|
||||
in_size = true;
|
||||
size.clear();
|
||||
if !after.is_empty() && BG_SIZE_RE.is_match(after) {
|
||||
size.push(js::to_lower_case(after));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if in_size && size.len() < 2 && BG_SIZE_RE.is_match(&token) {
|
||||
size.push(js::to_lower_case(&token));
|
||||
continue;
|
||||
}
|
||||
in_size = false;
|
||||
if repeat.len() < 2 && BG_REPEAT_RE.is_match(&token) {
|
||||
repeat.push(js::to_lower_case(&token));
|
||||
}
|
||||
}
|
||||
repeats.push(if repeat.is_empty() {
|
||||
"repeat".into()
|
||||
} else {
|
||||
repeat.join(" ")
|
||||
});
|
||||
sizes.push(if size.is_empty() {
|
||||
"auto".into()
|
||||
} else {
|
||||
size.join(" ")
|
||||
});
|
||||
}
|
||||
(repeats.join(", "), sizes.join(", "))
|
||||
}
|
||||
|
||||
/// The `backgroundRepeat` / `backgroundSize` pairs a declaration sets. Not
|
||||
/// part of `expand_static_declaration`, whose output the recorded vectors
|
||||
/// pin; the cascade stores these beside it (`apply_static_longhand`) so the
|
||||
/// sampled-contrast path (#560) can tell a tiled or cover image from a
|
||||
/// no-repeat icon. Every `background` shorthand resets both to what it
|
||||
/// names, the defaults when it names nothing, as in CSS; a CSS-wide keyword
|
||||
/// passes through, and a bare `var()` value is left alone the way the
|
||||
/// expansion leaves it.
|
||||
pub fn background_longhands(prop: &str, value: &str) -> Vec<Expanded> {
|
||||
let v = js::trim(value);
|
||||
match js::to_lower_case(prop).as_str() {
|
||||
"background" if VAR_ANYWHERE_RE.is_match(v) && !BG_IMAGE_RE.is_match(v) => Vec::new(),
|
||||
"background" if CSS_WIDE_KEYWORD_RE.is_match(v) => {
|
||||
let keyword = js::to_lower_case(v);
|
||||
vec![
|
||||
("backgroundRepeat".into(), keyword.clone()),
|
||||
("backgroundSize".into(), keyword),
|
||||
]
|
||||
}
|
||||
"background" => {
|
||||
let (repeat, size) = parse_static_background_layers(v);
|
||||
vec![
|
||||
("backgroundRepeat".into(), repeat),
|
||||
("backgroundSize".into(), size),
|
||||
]
|
||||
}
|
||||
"background-repeat" if !v.is_empty() => vec![("backgroundRepeat".into(), v.to_string())],
|
||||
"background-size" if !v.is_empty() => vec![("backgroundSize".into(), v.to_string())],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
static CSS_WIDE_KEYWORD_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)^(?:inherit|initial|unset|revert|revert-layer)$").expect("CSS_WIDE_KEYWORD_RE")
|
||||
});
|
||||
static BG_IMAGE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?i)gradient|url\(").expect("BG_IMAGE_RE"));
|
||||
static BG_IMAGE_SPLIT_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::adapters::{
|
||||
use crate::background::{resolve_background, resolve_border_radius_px, sv};
|
||||
use crate::cascade::{build_static_style_map, collect_static_css_text};
|
||||
use crate::dom::{StaticDocument, StaticElement};
|
||||
use crate::image_sampling::ImageSampler;
|
||||
use crate::page::{
|
||||
check_cream_palette, check_page_layout, check_repeated_container_text_from_doc,
|
||||
check_static_page_typography,
|
||||
@@ -119,14 +120,19 @@ const STATIC_ELEMENT_RULES: &[(&str, &str)] = &[
|
||||
("radial-spotlight-glow", "*"),
|
||||
];
|
||||
|
||||
fn run_rule(rule_id: &str, el: &StaticElement<'_>, tag: &str) -> Vec<RuleHit> {
|
||||
fn run_rule(
|
||||
rule_id: &str,
|
||||
el: &StaticElement<'_>,
|
||||
tag: &str,
|
||||
images: &ImageSampler,
|
||||
) -> Vec<RuleHit> {
|
||||
let style = el.style();
|
||||
match rule_id {
|
||||
"border-rules" => {
|
||||
let radius = resolve_border_radius_px(style, pf0(sv(style, "width")));
|
||||
check_element_borders(tag, style, radius, el)
|
||||
}
|
||||
"color-rules" => check_element_colors(el, style, tag, None),
|
||||
"color-rules" => check_element_colors(el, style, tag, None, images),
|
||||
"hover-color-rules" => check_element_hover_contrast(el, style, tag),
|
||||
"dark-glow" => {
|
||||
let base = el.parent_element().unwrap_or(*el);
|
||||
@@ -210,6 +216,7 @@ pub fn detect_html_source(
|
||||
let css_text = collect_static_css_text(&doc, &file_dir, profile, fp, options.warn);
|
||||
build_static_style_map(&mut doc, css_text.as_str(), profile, fp);
|
||||
let doc = doc;
|
||||
let images = ImageSampler::new(&file_dir.to_string_lossy());
|
||||
|
||||
let mut findings: Vec<Finding> = Vec::new();
|
||||
let mk = |id: &str, snippet: &str| try_finding(id, fp, snippet, 0.0);
|
||||
@@ -222,7 +229,7 @@ pub fn detect_html_source(
|
||||
profile,
|
||||
Meta::new("element", rule_id, fp),
|
||||
|h: &RuleHit| h.id.as_str(),
|
||||
|| run_rule(rule_id, el, &tag),
|
||||
|| run_rule(rule_id, el, &tag, &images),
|
||||
);
|
||||
for h in hits {
|
||||
if scoped_ignore_active(el, &h.id) {
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
//! The pixels behind image-backed text for the static engine (#560). A
|
||||
//! `url()` resolves to bytes from a local file relative to the page (linked
|
||||
//! stylesheets have their urls rewritten to page-relative form when they are
|
||||
//! inlined, see `rewrite_sheet_urls`) or from a base64 data URI, never from
|
||||
//! the network; the pure-Rust decoders turn them into a raster no larger than
|
||||
//! the browser overlay's 640px canvas; and the raster is cached for the rest
|
||||
//! of the process, so a directory scan decodes each hero once. Anything
|
||||
//! unreadable, remote, oversized, or undecodable is `None`, and the caller
|
||||
//! keeps today's skip. The same byte budget bounds a file on disk and a data
|
||||
//! URI's payload, checked before anything is copied or decoded.
|
||||
|
||||
use crate::cascade::resolve_linked_css_path;
|
||||
use base64::Engine;
|
||||
use impeccable_common::jsp;
|
||||
use impeccable_core::color::Rgba;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Cursor;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// The browser overlay draws to a canvas no larger than this on a side.
|
||||
const MAX_RASTER_SIDE: u32 = 640;
|
||||
/// Files above this are not read: a hero is a few megabytes, and the hook
|
||||
/// runs on every edit.
|
||||
const MAX_FILE_BYTES: u64 = 24 * 1024 * 1024;
|
||||
/// The base64 payload length that decodes to [`MAX_FILE_BYTES`], so a data
|
||||
/// URI is refused before its payload is copied or decoded.
|
||||
const MAX_DATA_URI_CHARS: usize = (MAX_FILE_BYTES as usize / 3) * 4 + 4;
|
||||
const MAX_IMAGE_SIDE: u32 = 8192;
|
||||
const MAX_DECODE_BYTES: u64 = 128 * 1024 * 1024;
|
||||
/// Decoded rasters kept per process; the map is cleared when full.
|
||||
const CACHE_ENTRIES: usize = 24;
|
||||
|
||||
/// A decoded image, RGBA8, at most [`MAX_RASTER_SIDE`] on a side.
|
||||
pub struct Raster {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// The size before the downscale: what `background-size: auto` paints.
|
||||
pub intrinsic_width: u32,
|
||||
pub intrinsic_height: u32,
|
||||
rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Raster {
|
||||
pub fn pixel(&self, x: usize, y: usize) -> Rgba {
|
||||
let i = (y.min(self.height as usize - 1) * self.width as usize
|
||||
+ x.min(self.width as usize - 1))
|
||||
* 4;
|
||||
let p = &self.rgba[i..i + 4];
|
||||
Rgba::new(p[0] as f64, p[1] as f64, p[2] as f64, p[3] as f64 / 255.0)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static RASTERS: RefCell<HashMap<String, Option<Rc<Raster>>>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
/// Resolves and decodes the `url()` grounds of one document, relative to
|
||||
/// the document's directory. A url from a linked stylesheet reaches the
|
||||
/// cascade already rewritten to page-relative form.
|
||||
pub struct ImageSampler {
|
||||
base: String,
|
||||
}
|
||||
|
||||
impl ImageSampler {
|
||||
pub fn new(html_dir: &str) -> Self {
|
||||
ImageSampler {
|
||||
base: html_dir.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The raster behind a `url()` argument, or `None` when it cannot be
|
||||
/// read here: a remote URL, a missing or oversized file, a format the
|
||||
/// decoders do not cover (SVG), or an `svg`/text data URI.
|
||||
pub fn load(&self, url: &str) -> Option<Rc<Raster>> {
|
||||
let url = url.trim();
|
||||
if url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let key = if is_data_uri(url) {
|
||||
// Refused before anything is allocated: a project file can carry
|
||||
// any size of data URI, and the hook scans on every edit.
|
||||
if url.len() > MAX_DATA_URI_CHARS + 256 {
|
||||
return None;
|
||||
}
|
||||
data_uri_key(url)
|
||||
} else {
|
||||
self.resolve_file(url)?
|
||||
};
|
||||
if let Some(hit) = RASTERS.with(|c| c.borrow().get(&key).cloned()) {
|
||||
return hit;
|
||||
}
|
||||
let bytes = if is_data_uri(url) {
|
||||
data_uri_bytes(url)
|
||||
} else {
|
||||
read_bounded(&key)
|
||||
};
|
||||
let raster = bytes.and_then(|b| decode(&b)).map(Rc::new);
|
||||
RASTERS.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
if cache.len() >= CACHE_ENTRIES {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key, raster.clone());
|
||||
});
|
||||
raster
|
||||
}
|
||||
|
||||
fn resolve_file(&self, url: &str) -> Option<String> {
|
||||
if url.starts_with("//") || url.contains("://") {
|
||||
return None;
|
||||
}
|
||||
let path = resolve_linked_css_path(&self.base, url);
|
||||
std::fs::metadata(&path)
|
||||
.map(|m| m.is_file())
|
||||
.unwrap_or(false)
|
||||
.then_some(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_data_uri(url: &str) -> bool {
|
||||
// `get`, not a byte slice: a url may open with a multibyte character.
|
||||
url.get(..5)
|
||||
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
|
||||
}
|
||||
|
||||
/// The cache key of a data URI: its length and a hash, so the cache never
|
||||
/// holds a copy of the URI itself.
|
||||
fn data_uri_key(url: &str) -> String {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
url.hash(&mut hasher);
|
||||
format!("data:{}:{:016x}", url.len(), hasher.finish())
|
||||
}
|
||||
|
||||
/// The name a finding gives the image: the file name, or `data:<mime>`.
|
||||
pub fn ground_label(url: &str) -> String {
|
||||
let url = url.trim();
|
||||
if is_data_uri(url) {
|
||||
let header = url[5..].split(',').next().unwrap_or("");
|
||||
let mime = header
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
return if mime.is_empty() {
|
||||
"data:image".into()
|
||||
} else {
|
||||
format!("data:{mime}")
|
||||
};
|
||||
}
|
||||
let stripped = url.split(['?', '#']).next().unwrap_or("");
|
||||
jsp::basename(stripped)
|
||||
}
|
||||
|
||||
fn data_uri_bytes(url: &str) -> Option<Vec<u8>> {
|
||||
let (header, payload) = url[5..].split_once(',')?;
|
||||
if !header
|
||||
.split(';')
|
||||
.any(|p| p.trim().eq_ignore_ascii_case("base64"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if payload.len() > MAX_DATA_URI_CHARS {
|
||||
return None;
|
||||
}
|
||||
let compact: Cow<str> = if payload.chars().any(char::is_whitespace) {
|
||||
Cow::Owned(payload.chars().filter(|c| !c.is_whitespace()).collect())
|
||||
} else {
|
||||
Cow::Borrowed(payload)
|
||||
};
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(compact.as_ref())
|
||||
.or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(compact.as_ref()))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn read_bounded(path: &str) -> Option<Vec<u8>> {
|
||||
let meta = std::fs::metadata(path).ok()?;
|
||||
if !meta.is_file() || meta.len() > MAX_FILE_BYTES {
|
||||
return None;
|
||||
}
|
||||
std::fs::read(path).ok()
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Option<Raster> {
|
||||
let mut reader = image::ImageReader::new(Cursor::new(bytes))
|
||||
.with_guessed_format()
|
||||
.ok()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_image_width = Some(MAX_IMAGE_SIDE);
|
||||
limits.max_image_height = Some(MAX_IMAGE_SIDE);
|
||||
limits.max_alloc = Some(MAX_DECODE_BYTES);
|
||||
reader.limits(limits);
|
||||
let decoded = reader.decode().ok()?;
|
||||
let (intrinsic_width, intrinsic_height) = (decoded.width(), decoded.height());
|
||||
if intrinsic_width == 0 || intrinsic_height == 0 {
|
||||
return None;
|
||||
}
|
||||
// `thumbnail` also upscales, so only images past the budget go through it.
|
||||
let scaled = if intrinsic_width.max(intrinsic_height) > MAX_RASTER_SIDE {
|
||||
decoded.thumbnail(MAX_RASTER_SIDE, MAX_RASTER_SIDE)
|
||||
} else {
|
||||
decoded
|
||||
};
|
||||
let rgba = scaled.to_rgba8();
|
||||
Some(Raster {
|
||||
width: rgba.width(),
|
||||
height: rgba.height(),
|
||||
intrinsic_width,
|
||||
intrinsic_height,
|
||||
rgba: rgba.into_raw(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_pixel(width, height, image::Rgba(rgba));
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(img)
|
||||
.write_to(&mut out, image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
out.into_inner()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_uri_decodes_and_labels() {
|
||||
let bytes = png_bytes(8, 8, [240, 236, 228, 255]);
|
||||
let uri = format!(
|
||||
"data:image/png;base64,{}",
|
||||
base64::engine::general_purpose::STANDARD.encode(&bytes)
|
||||
);
|
||||
let sampler = ImageSampler::new("/nonexistent");
|
||||
let raster = sampler.load(&uri).expect("png data uri decodes");
|
||||
assert_eq!((raster.width, raster.height), (8, 8));
|
||||
assert_eq!(raster.pixel(3, 3).r, 240.0);
|
||||
assert_eq!(ground_label(&uri), "data:image/png");
|
||||
assert!(sampler.load("data:image/svg+xml;utf8,<svg/>").is_none());
|
||||
assert!(sampler.load("https://example.com/hero.jpg").is_none());
|
||||
assert!(sampler.load("//cdn.example.com/hero.jpg").is_none());
|
||||
assert_eq!(ground_label("img/hero.jpg?v=3"), "hero.jpg");
|
||||
// A url that opens with a multibyte character is a local path, not
|
||||
// a panic.
|
||||
assert!(!is_data_uri("dat\u{20ac}"));
|
||||
assert!(sampler.load("abcd\u{20ac}.png").is_none());
|
||||
assert_eq!(ground_label("abcd\u{20ac}.png"), "abcd\u{20ac}.png");
|
||||
// A payload past the byte budget is refused before it is decoded.
|
||||
let huge = format!(
|
||||
"data:image/png;base64,{}",
|
||||
"A".repeat(MAX_DATA_URI_CHARS + 1)
|
||||
);
|
||||
assert!(sampler.load(&huge).is_none());
|
||||
assert_ne!(data_uri_key(&uri), data_uri_key(&huge));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_resolve_against_the_page_dir_and_downscale() {
|
||||
let dir = std::env::temp_dir().join(format!("impeccable-sampler-{}", std::process::id()));
|
||||
std::fs::create_dir_all(dir.join("img")).unwrap();
|
||||
std::fs::write(
|
||||
dir.join("img").join("wide.png"),
|
||||
png_bytes(1280, 320, [20, 20, 20, 255]),
|
||||
)
|
||||
.unwrap();
|
||||
let sampler = ImageSampler::new(&dir.to_string_lossy());
|
||||
assert!(sampler.load("wide.png").is_none());
|
||||
let raster = sampler
|
||||
.load("img/wide.png")
|
||||
.expect("resolves against the page dir");
|
||||
assert_eq!((raster.width, raster.height), (640, 160));
|
||||
assert_eq!(
|
||||
(raster.intrinsic_width, raster.intrinsic_height),
|
||||
(1280, 320)
|
||||
);
|
||||
assert_eq!(raster.pixel(639, 159).g, 20.0);
|
||||
// The second load is the cached raster.
|
||||
assert!(Rc::ptr_eq(&raster, &sampler.load("img/wide.png").unwrap()));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod background;
|
||||
pub mod cascade;
|
||||
pub mod dom;
|
||||
pub mod engine;
|
||||
pub mod image_sampling;
|
||||
pub mod page;
|
||||
pub mod profile;
|
||||
pub mod quality;
|
||||
|
||||
@@ -209,3 +209,209 @@ fn checks_shim_helpers() {
|
||||
assert_eq!(resolve_length_px("50%", 10.0), Some(5.0));
|
||||
assert_eq!(resolve_length_px("1.5", 10.0), Some(15.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_longhands_ride_beside_the_expansion() {
|
||||
use impeccable_html::cascade::rules::apply_static_longhand;
|
||||
use impeccable_html::cascade::shorthand::background_longhands;
|
||||
|
||||
// The expansion itself is pinned by the recorded vectors and stays
|
||||
// image-and-color only; repeat and size come from the side channel.
|
||||
assert_eq!(
|
||||
background_longhands(
|
||||
"background",
|
||||
"url(a.png) center / cover no-repeat, url(b.png)"
|
||||
),
|
||||
vec![
|
||||
(
|
||||
"backgroundRepeat".to_string(),
|
||||
"no-repeat, repeat".to_string()
|
||||
),
|
||||
("backgroundSize".to_string(), "cover, auto".to_string()),
|
||||
]
|
||||
);
|
||||
// The css-tree generator glues the first keyword to the call.
|
||||
assert_eq!(
|
||||
background_longhands("background", "url(a.png)center/cover no-repeat"),
|
||||
vec![
|
||||
("backgroundRepeat".to_string(), "no-repeat".to_string()),
|
||||
("backgroundSize".to_string(), "cover".to_string()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
background_longhands("background", "#fff url(a.png) repeat-x"),
|
||||
vec![
|
||||
("backgroundRepeat".to_string(), "repeat-x".to_string()),
|
||||
("backgroundSize".to_string(), "auto".to_string()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
background_longhands("Background-Size", "100% 32px"),
|
||||
vec![("backgroundSize".to_string(), "100% 32px".to_string())]
|
||||
);
|
||||
// A color-only shorthand still resets both longhands, as in CSS.
|
||||
assert_eq!(
|
||||
background_longhands("background", "#fff"),
|
||||
vec![
|
||||
("backgroundRepeat".to_string(), "repeat".to_string()),
|
||||
("backgroundSize".to_string(), "auto".to_string()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
background_longhands("background", "Inherit"),
|
||||
vec![
|
||||
("backgroundRepeat".to_string(), "inherit".to_string()),
|
||||
("backgroundSize".to_string(), "inherit".to_string()),
|
||||
]
|
||||
);
|
||||
assert!(background_longhands("background", "var(--surface)").is_empty());
|
||||
assert!(background_longhands("color", "red").is_empty());
|
||||
|
||||
// A later shorthand with an image resets an earlier longhand, and a
|
||||
// later longhand overrides a shorthand, under the cascade's priority.
|
||||
let mut specified: SpecifiedStore<&str> = SpecifiedStore::new();
|
||||
let node = "n1";
|
||||
let apply = |specified: &mut SpecifiedStore<&str>, prop: &str, value: &str, m: DeclMeta| {
|
||||
apply_static_declaration(specified, node, prop, value, &m);
|
||||
for (p, v) in background_longhands(prop, value) {
|
||||
apply_static_longhand(specified, node, &p, &v, &m);
|
||||
}
|
||||
};
|
||||
apply(
|
||||
&mut specified,
|
||||
"background-repeat",
|
||||
"no-repeat",
|
||||
meta(false, [0, 1, 0], 0, false),
|
||||
);
|
||||
apply(
|
||||
&mut specified,
|
||||
"background",
|
||||
"url(hero.jpg) center / cover",
|
||||
meta(false, [0, 1, 0], 1, false),
|
||||
);
|
||||
apply(
|
||||
&mut specified,
|
||||
"background-size",
|
||||
"contain",
|
||||
meta(false, [0, 1, 0], 2, false),
|
||||
);
|
||||
let map = specified.get(&node).expect("node entry");
|
||||
assert_eq!(
|
||||
map.get("backgroundRepeat").map(|d| d.value.as_str()),
|
||||
Some("repeat")
|
||||
);
|
||||
assert_eq!(
|
||||
map.get("backgroundSize").map(|d| d.value.as_str()),
|
||||
Some("contain")
|
||||
);
|
||||
assert_eq!(
|
||||
map.get("backgroundImage").map(|d| d.value.as_str()),
|
||||
Some("url(hero.jpg) center / cover")
|
||||
);
|
||||
// `background: #fff` after a longhand resets it, so a later
|
||||
// `background-image` is classified with the defaults, not a stale value.
|
||||
apply(
|
||||
&mut specified,
|
||||
"background-repeat",
|
||||
"no-repeat",
|
||||
meta(false, [0, 1, 0], 3, false),
|
||||
);
|
||||
apply(
|
||||
&mut specified,
|
||||
"background",
|
||||
"#fff",
|
||||
meta(false, [0, 1, 0], 4, false),
|
||||
);
|
||||
let map = specified.get(&node).expect("node entry");
|
||||
assert_eq!(
|
||||
map.get("backgroundRepeat").map(|d| d.value.as_str()),
|
||||
Some("repeat")
|
||||
);
|
||||
assert_eq!(
|
||||
map.get("backgroundSize").map(|d| d.value.as_str()),
|
||||
Some("auto")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_sheet_urls_are_rewritten_page_relative() {
|
||||
use impeccable_html::cascade::build::rewrite_sheet_urls;
|
||||
|
||||
let css = concat!(
|
||||
".a { background: url(light.png) }\n",
|
||||
".b { background: url(\"../img/hero.jpg?v=3\") no-repeat }\n",
|
||||
".c { background-image: url('./x.webp'), url(/root.png), url(data:image/png;base64,AAAA) }\n",
|
||||
".d { background: url(https://cdn.example.com/a.png) }\n",
|
||||
".e { mask: url(#clip) }\n",
|
||||
".f { background: URL( a b.png ) }\n",
|
||||
);
|
||||
let out = rewrite_sheet_urls(css, "/site/css", "/site");
|
||||
assert!(
|
||||
out.contains(".a { background: url(css/light.png) }"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains(".b { background: url(\"img/hero.jpg?v=3\") no-repeat }"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("url('css/x.webp'), url(/root.png), url(data:image/png;base64,AAAA)"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(out.contains("url(https://cdn.example.com/a.png)"), "{out}");
|
||||
assert!(out.contains("url(#clip)"), "{out}");
|
||||
// An unquoted url with a space is not a url token in CSS; it stays put.
|
||||
assert!(out.contains("URL( a b.png )"), "{out}");
|
||||
// A sheet beside the page keeps every path where it was (the engine
|
||||
// does not even call the rewrite for that case).
|
||||
let same = rewrite_sheet_urls(css, "/site", "/site");
|
||||
assert!(same.contains(".a { background: url(light.png) }"), "{same}");
|
||||
assert!(same.contains("url(\"../img/hero.jpg?v=3\")"), "{same}");
|
||||
// A sheet above the page walks back up.
|
||||
let up = rewrite_sheet_urls(".a { background: url(light.png) }", "/site", "/site/pages");
|
||||
assert_eq!(up, ".a { background: url(../light.png) }");
|
||||
// A stray `url(` in a comment, or an unclosed quote, never swallows the
|
||||
// rules after it: comments pass through untouched and a url form ends
|
||||
// where CSS says it ends.
|
||||
let hazards = concat!(
|
||||
"/* see url( for details */ .g { color: red }\n",
|
||||
".h { background: url(a.png) }\n",
|
||||
".i { background: url(\"oops }\n",
|
||||
".j { background: url(b.png) }\n",
|
||||
"/* url(unterminated.png",
|
||||
);
|
||||
let out = rewrite_sheet_urls(hazards, "/site/css", "/site");
|
||||
assert!(
|
||||
out.contains("/* see url( for details */ .g { color: red }"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(out.contains(".h { background: url(css/a.png) }"), "{out}");
|
||||
assert!(out.contains(".i { background: url(\"oops }"), "{out}");
|
||||
assert!(out.contains(".j { background: url(css/b.png) }"), "{out}");
|
||||
assert!(out.ends_with("/* url(unterminated.png"), "{out}");
|
||||
// A `/*` inside a string or an unquoted url is content, not a comment,
|
||||
// so the rules after it are still rewritten.
|
||||
let strings = concat!(
|
||||
".k { content: \"a/*b\"; background: url(k.png) }\n",
|
||||
".l { background: url(l/*.png) }\n",
|
||||
".m { content: 'c/*d'; } /* real url( */ .n { background: url(n.png) }\n",
|
||||
);
|
||||
let out = rewrite_sheet_urls(strings, "/site/css", "/site");
|
||||
assert!(
|
||||
out.contains(".k { content: \"a/*b\"; background: url(css/k.png) }"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(out.contains(".l { background: url(css/l/*.png) }"), "{out}");
|
||||
assert!(
|
||||
out.contains("/* real url( */ .n { background: url(css/n.png) }"),
|
||||
"{out}"
|
||||
);
|
||||
// `myurl(` is a custom function, not the url token: its comment stays a
|
||||
// comment and its argument is not rewritten.
|
||||
let custom = ".q { mask: myurl(a /* url(x.png) */); background: url(q.png) }";
|
||||
let out = rewrite_sheet_urls(custom, "/site/css", "/site");
|
||||
assert_eq!(
|
||||
out,
|
||||
".q { mask: myurl(a /* url(x.png) */); background: url(css/q.png) }"
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -596,6 +596,18 @@ fn write_carbonize_banner(event: &Map<String, Value>, io: &mut Io) {
|
||||
}
|
||||
}
|
||||
|
||||
fn reply_ack_json(reply: &Reply) -> Value {
|
||||
let mut m = Map::new();
|
||||
m.insert("ok".into(), json!(true));
|
||||
m.insert("id".into(), json!(reply.id));
|
||||
m.insert("status".into(), json!(reply.ty));
|
||||
if let Some(f) = &reply.file {
|
||||
m.insert("file".into(), json!(f));
|
||||
}
|
||||
m.insert("_instructions".into(), json!("Poll again now."));
|
||||
Value::Object(m)
|
||||
}
|
||||
|
||||
/// JS: printPollEvent(event) — a wire-supplied `_instructions` must never
|
||||
/// win over the locally generated one (#488).
|
||||
fn print_poll_event(event: &mut Value, io: &mut Io) {
|
||||
@@ -718,7 +730,13 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
|
||||
}
|
||||
};
|
||||
return match post_reply(&base, &token, &reply) {
|
||||
Ok(()) => 0,
|
||||
Ok(()) => {
|
||||
println(
|
||||
io,
|
||||
&serde_json::to_string(&reply_ack_json(&reply)).unwrap_or_default(),
|
||||
);
|
||||
0
|
||||
}
|
||||
Err(PollError::ConnRefused) => {
|
||||
io.err(&format!(
|
||||
"Live server not running. Start one with: {}\n",
|
||||
@@ -839,4 +857,40 @@ mod tests {
|
||||
}));
|
||||
assert!(parsed.get("_instructions").is_none(), "{}", parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_ack_json_includes_file_when_present() {
|
||||
let reply = Reply {
|
||||
id: "ab12cd34".into(),
|
||||
ty: "done".into(),
|
||||
message: None,
|
||||
file: Some("index.html".into()),
|
||||
data: None,
|
||||
source_event_type: None,
|
||||
};
|
||||
let parsed = reply_ack_json(&reply);
|
||||
assert_eq!(parsed["ok"], json!(true));
|
||||
assert_eq!(parsed["id"], json!("ab12cd34"));
|
||||
assert_eq!(parsed["status"], json!("done"));
|
||||
assert_eq!(parsed["file"], json!("index.html"));
|
||||
assert_eq!(parsed["_instructions"], json!("Poll again now."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_ack_json_omits_file_when_absent() {
|
||||
let reply = Reply {
|
||||
id: "ab12cd34".into(),
|
||||
ty: "steer_done".into(),
|
||||
message: None,
|
||||
file: None,
|
||||
data: None,
|
||||
source_event_type: None,
|
||||
};
|
||||
let parsed = reply_ack_json(&reply);
|
||||
assert_eq!(parsed["ok"], json!(true));
|
||||
assert_eq!(parsed["id"], json!("ab12cd34"));
|
||||
assert_eq!(parsed["status"], json!("steer_done"));
|
||||
assert!(parsed.get("file").is_none(), "{}", parsed);
|
||||
assert_eq!(parsed["_instructions"], json!("Poll again now."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +418,16 @@ fn push_diag(next: &mut Map<String, Value>, d: Value) {
|
||||
next.insert("diagnostics".to_string(), Value::Array(arr));
|
||||
}
|
||||
|
||||
fn drop_diag(next: &mut Map<String, Value>, error: &str) {
|
||||
let mut arr = next
|
||||
.get("diagnostics")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
arr.retain(|d| d.get("error").and_then(|e| e.as_str()) != Some(error));
|
||||
next.insert("diagnostics".to_string(), Value::Array(arr));
|
||||
}
|
||||
|
||||
/// JS: applyEvent(snapshot, entry)
|
||||
pub fn apply_event(snapshot: &Map<String, Value>, entry: &Value) -> Map<String, Value> {
|
||||
let event: Map<String, Value> = match entry.get("event") {
|
||||
@@ -864,6 +874,7 @@ pub fn apply_event(snapshot: &Map<String, Value>, entry: &Value) -> Map<String,
|
||||
set!("phase", json!("discarded"));
|
||||
set!("pendingEventSeq", Value::Null);
|
||||
set!("pendingEvent", Value::Null);
|
||||
drop_diag(&mut next, "carbonize_cleanup_required");
|
||||
}
|
||||
"complete" => {
|
||||
set!("phase", json!("completed"));
|
||||
@@ -876,6 +887,7 @@ pub fn apply_event(snapshot: &Map<String, Value>, entry: &Value) -> Map<String,
|
||||
set_if!("previewMode", ev("previewMode"));
|
||||
set!("pendingEventSeq", Value::Null);
|
||||
set!("pendingEvent", Value::Null);
|
||||
drop_diag(&mut next, "carbonize_cleanup_required");
|
||||
}
|
||||
"agent_error" => {
|
||||
if canceled && ev("sourceEventType").and_then(|v| v.as_str()) == Some("generate") {
|
||||
@@ -925,3 +937,63 @@ fn write_snapshot(path: &str, snapshot: &Map<String, Value>, journal_bytes: i64,
|
||||
pub fn get_str<'a>(m: &'a Map<String, Value>, k: &str) -> Option<&'a str> {
|
||||
get(m, k).and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn journal_entry(seq: i64, event: Value) -> Value {
|
||||
json!({ "seq": seq, "ts": "2026-01-01T00:00:00.000Z", "event": event })
|
||||
}
|
||||
|
||||
fn has_diag(snapshot: &Map<String, Value>, error: &str) -> bool {
|
||||
snapshot
|
||||
.get("diagnostics")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.any(|d| d.get("error").and_then(|e| e.as_str()) == Some(error))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn replay(id: &str, events: &[Value]) -> Map<String, Value> {
|
||||
let mut snap = base_snapshot(id);
|
||||
for entry in events {
|
||||
snap = apply_event(&snap, entry);
|
||||
}
|
||||
snap
|
||||
}
|
||||
|
||||
fn accept_carbonize_done(id: &str, terminal: &str) -> Map<String, Value> {
|
||||
replay(
|
||||
id,
|
||||
&[
|
||||
journal_entry(
|
||||
1,
|
||||
json!({ "id": id, "type": "accept", "variantId": 2 }),
|
||||
),
|
||||
journal_entry(
|
||||
2,
|
||||
json!({ "id": id, "type": "agent_done", "carbonize": true, "file": "index.html" }),
|
||||
),
|
||||
journal_entry(3, json!({ "id": id, "type": terminal })),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_drops_carbonize_cleanup_required() {
|
||||
let snap = accept_carbonize_done("ab12cd34", "complete");
|
||||
assert_eq!(snap.get("phase").and_then(|p| p.as_str()), Some("completed"));
|
||||
assert!(!has_diag(&snap, "carbonize_cleanup_required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discarded_drops_carbonize_cleanup_required() {
|
||||
let snap = accept_carbonize_done("ab12cd34", "discarded");
|
||||
assert_eq!(snap.get("phase").and_then(|p| p.as_str()), Some("discarded"));
|
||||
assert!(!has_diag(&snap, "carbonize_cleanup_required"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,13 +219,10 @@ fn rewrite_value(value: &Value, provider: &str, quoted: &QuotedPath, win32: bool
|
||||
}
|
||||
}
|
||||
|
||||
/// JS: valueHasImpeccableHookMarker(value). Command separators are
|
||||
/// normalized to `/` first so a legacy Windows-path guard is still
|
||||
/// recognized as ours and replaced instead of duplicated (upstream
|
||||
/// 665c51b9, #604).
|
||||
/// JS: valueHasImpeccableHookMarker(value).
|
||||
pub fn value_has_impeccable_hook_marker(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::String(s) => is_impeccable_hook_command(&s.replace('\\', "/")),
|
||||
Value::String(s) => is_impeccable_hook_command(s),
|
||||
Value::Array(a) => a.iter().any(value_has_impeccable_hook_marker),
|
||||
Value::Object(o) => o.values().any(value_has_impeccable_hook_marker),
|
||||
_ => false,
|
||||
@@ -233,12 +230,9 @@ pub fn value_has_impeccable_hook_marker(value: &Value) -> bool {
|
||||
}
|
||||
|
||||
/// True when `value` names an Impeccable hook in the launcher generation.
|
||||
/// Separators are normalized to `/` first, matching
|
||||
/// `value_has_impeccable_hook_marker`, so a legacy Windows-path launcher
|
||||
/// command is still recognized.
|
||||
pub fn value_has_launcher_hook_marker(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::String(s) => is_launcher_hook_command(&s.replace('\\', "/")),
|
||||
Value::String(s) => is_launcher_hook_command(s),
|
||||
Value::Array(a) => a.iter().any(value_has_launcher_hook_marker),
|
||||
Value::Object(o) => o.values().any(value_has_launcher_hook_marker),
|
||||
_ => false,
|
||||
|
||||
@@ -357,3 +357,57 @@ fn hook_artifacts_map_providers_to_manifest_files() {
|
||||
assert_eq!(c[0].dest, jsp::join(&["/p", ".codex", "hooks.json"]));
|
||||
assert!(c[0].shared_dest.is_none());
|
||||
}
|
||||
|
||||
fn windows_user_scope_hook_command() -> String {
|
||||
let launcher = r"C:\Users\alice\.claude\skills\impeccable\scripts\impeccable";
|
||||
let q = json_string(launcher);
|
||||
format!("[ ! -f {q} ] || {q} hook")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_json_escaped_windows_launcher_is_idempotent() {
|
||||
let cmd = windows_user_scope_hook_command();
|
||||
let hook_entry = |cmd: String| {
|
||||
json!({ "matcher": "Edit", "hooks": [{ "type": "command", "command": cmd }] })
|
||||
};
|
||||
let stop_entry = |cmd: String| {
|
||||
json!({ "hooks": [{ "type": "command", "command": cmd, "timeout": 30 }] })
|
||||
};
|
||||
let existing = json!({
|
||||
"hooks": {
|
||||
"PostToolUse": [hook_entry(cmd.clone())],
|
||||
"Stop": [stop_entry(cmd.clone())]
|
||||
}
|
||||
});
|
||||
let fresh = json!({
|
||||
"description": "fresh",
|
||||
"hooks": {
|
||||
"PostToolUse": [hook_entry(cmd.clone())],
|
||||
"Stop": [stop_entry(cmd.clone())]
|
||||
}
|
||||
});
|
||||
let merged = merge_hook_manifests(&existing, &fresh);
|
||||
assert_eq!(merged["hooks"]["PostToolUse"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(merged["hooks"]["Stop"].as_array().unwrap().len(), 1);
|
||||
let merged2 = merge_hook_manifests(&merged, &fresh);
|
||||
assert_eq!(merged2["hooks"]["PostToolUse"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(merged2["hooks"]["Stop"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_heals_triplicated_stop_groups() {
|
||||
let cmd = windows_user_scope_hook_command();
|
||||
let stop_entry = json!({ "hooks": [{ "type": "command", "command": cmd.clone(), "timeout": 30 }] });
|
||||
let existing = json!({
|
||||
"hooks": {
|
||||
"Stop": [stop_entry.clone(), stop_entry.clone(), stop_entry]
|
||||
}
|
||||
});
|
||||
let fresh = json!({
|
||||
"hooks": {
|
||||
"Stop": [json!({ "hooks": [{ "type": "command", "command": cmd, "timeout": 30 }] })]
|
||||
}
|
||||
});
|
||||
let merged = merge_hook_manifests(&existing, &fresh);
|
||||
assert_eq!(merged["hooks"]["Stop"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"version": "4.2.3",
|
||||
"version": "4.3.1",
|
||||
"description": "Design and refine interfaces with Impeccable skills, specialist agents, and design checks.",
|
||||
"author": {
|
||||
"name": "Renaissance Geek, Inc."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 4.2.3
|
||||
version: 4.3.1
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user