jekyll-theme-zer0 1.27.0 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +812 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +252 -0
- data/_data/features.yml +234 -5
- data/_data/i18n/fr.yml +103 -0
- data/_data/i18n/manifest.yml +1529 -0
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +22 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +387 -277
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +36 -3
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/theme-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +21 -0
- data/_includes/core/header.html +7 -4
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +4 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +38 -5
- data/_layouts/section.html +96 -25
- data/_sass/components/_book.scss +423 -0
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +235 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +263 -15
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/theme/_backgrounds.scss +21 -8
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +14 -1
- data/assets/js/setup-wizard.js +322 -20
- data/scripts/README.md +29 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +66 -16
- data/scripts/bin/validate +5 -1
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/design-system-check.rb +170 -0
- data/scripts/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/propagate.rb +277 -0
- data/scripts/test/lib/run_tests.sh +2 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/translate.rb +161 -15
- metadata +56 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for scripts/ci/agent_review_result.py (issue #418).
|
|
3
|
+
|
|
4
|
+
The bug these guard against: the Claude tier of ai-content-review.yml posted
|
|
5
|
+
|
|
6
|
+
Failed to authenticate. API Error: 401 OAuth access token has been revoked.
|
|
7
|
+
|
|
8
|
+
into PR #414 as its review, and the job still reported success. The credential
|
|
9
|
+
has since been rotated, but the SWALLOW is the durable defect — it would hide
|
|
10
|
+
the next revoked token exactly as well as it hid this one.
|
|
11
|
+
|
|
12
|
+
The first fixture below is that output verbatim, copied from the comment the
|
|
13
|
+
workflow actually posted (2026-08-23T04:51:47Z, run 32618860829). If the guard
|
|
14
|
+
ever stops failing on it, the regression is back.
|
|
15
|
+
|
|
16
|
+
Both directions are covered. A real review must NOT be flagged — including one
|
|
17
|
+
whose prose legitimately quotes an auth error, which is the false positive a
|
|
18
|
+
bare `grep 'Failed to authenticate'` would produce (and which would fire on this
|
|
19
|
+
repo's own troubleshooting docs).
|
|
20
|
+
|
|
21
|
+
Dependency-light on purpose: standard library only, so it runs anywhere the
|
|
22
|
+
workflow does.
|
|
23
|
+
|
|
24
|
+
python3 scripts/ci/test_agent_review_result.py
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
import tempfile
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
35
|
+
|
|
36
|
+
import agent_review_result as arr # noqa: E402
|
|
37
|
+
|
|
38
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
39
|
+
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ai-content-review.yml"
|
|
40
|
+
|
|
41
|
+
CHECKS: list[tuple[str, bool]] = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check(label: str, ok: bool) -> None:
|
|
45
|
+
CHECKS.append((label, bool(ok)))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# The 401 exactly as the CLI printed it and the workflow posted it.
|
|
49
|
+
AUTH_401 = "Failed to authenticate. API Error: 401 OAuth access token has been revoked.\n"
|
|
50
|
+
|
|
51
|
+
REAL_REVIEW = """**Verdict:** approve with minor suggestions
|
|
52
|
+
|
|
53
|
+
### pages/_docs/getting-started.md
|
|
54
|
+
|
|
55
|
+
- The `description` front matter reads as a keyword list rather than a sentence.
|
|
56
|
+
Rewrite it as prose so search snippets are readable.
|
|
57
|
+
- The second H2 duplicates the page title; demote it to an H3 or cut it.
|
|
58
|
+
- Two code fences are missing a language tag, so they render unhighlighted.
|
|
59
|
+
- Alt text on the architecture diagram describes the file, not the picture.
|
|
60
|
+
|
|
61
|
+
### pages/_docs/configuration.md
|
|
62
|
+
|
|
63
|
+
- Consistent terminology: this page says "config file" where the rest of the
|
|
64
|
+
docs say `_config.yml`. Pick one.
|
|
65
|
+
- The table of collection defaults is missing the `books` collection added in
|
|
66
|
+
v1.19, so the page is incomplete rather than merely terse.
|
|
67
|
+
- Sentence length in "Advanced overrides" averages 38 words; split the longest.
|
|
68
|
+
|
|
69
|
+
### Summary
|
|
70
|
+
|
|
71
|
+
Both files are accurate and well-organised. The findings above are polish, not
|
|
72
|
+
correctness — nothing here blocks the merge.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
# A real review that TALKS about auth failures. Must not be mistaken for one.
|
|
76
|
+
REVIEW_ABOUT_AUTH = """**Verdict:** approve
|
|
77
|
+
|
|
78
|
+
### docs/troubleshooting/credentials.md
|
|
79
|
+
|
|
80
|
+
- The page explains what to do when the CLI prints "Failed to authenticate. API
|
|
81
|
+
Error: 401 OAuth access token has been revoked." — good, that is the exact
|
|
82
|
+
string users will search for, so keep it verbatim rather than paraphrasing.
|
|
83
|
+
- Add a cross-link to the rotation runbook; the reader is told to "rotate the
|
|
84
|
+
token" with no pointer to how.
|
|
85
|
+
- The `authentication_error` example block is missing a language tag.
|
|
86
|
+
- Consider noting that the deterministic review tier keeps working while the
|
|
87
|
+
credential is broken, since that surprised at least one reader.
|
|
88
|
+
- Front matter `lastmod` is stale by two releases.
|
|
89
|
+
|
|
90
|
+
### Summary
|
|
91
|
+
|
|
92
|
+
Accurate and genuinely useful. Only polish items above; the troubleshooting
|
|
93
|
+
steps themselves check out against the current workflow definition.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_guard(status: int, stdout: str, stderr: str = "") -> tuple[int, str]:
|
|
98
|
+
"""Invoke the guard end-to-end; return (exit code, comment body)."""
|
|
99
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
100
|
+
d = Path(tmp)
|
|
101
|
+
(d / "out.txt").write_text(stdout, encoding="utf-8")
|
|
102
|
+
(d / "err.txt").write_text(stderr, encoding="utf-8")
|
|
103
|
+
rc = arr.main([
|
|
104
|
+
"--status", str(status),
|
|
105
|
+
"--stdout", str(d / "out.txt"),
|
|
106
|
+
"--stderr", str(d / "err.txt"),
|
|
107
|
+
"--out", str(d / "body.md"),
|
|
108
|
+
])
|
|
109
|
+
return rc, (d / "body.md").read_text(encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
# --- the observed failure, verbatim ------------------------------------ #
|
|
114
|
+
rc, body = run_guard(0, AUTH_401)
|
|
115
|
+
check("the verbatim PR #414 401 fails the step (exit 1) even on claude exit 0", rc == 1)
|
|
116
|
+
check("the 401 comment body says the review did not run", "did not run" in body)
|
|
117
|
+
check("the raw 401 line is no longer posted as if it were the review",
|
|
118
|
+
not body.startswith(f"{arr.TITLE}\n\nFailed to authenticate"))
|
|
119
|
+
check("the failure notice is visibly a caution, not a review", "[!CAUTION]" in body)
|
|
120
|
+
|
|
121
|
+
rc, _ = run_guard(1, AUTH_401)
|
|
122
|
+
check("a non-zero claude exit fails the step", rc == 1)
|
|
123
|
+
|
|
124
|
+
# --- nothing came back -------------------------------------------------- #
|
|
125
|
+
rc, body = run_guard(0, "")
|
|
126
|
+
check("empty agent output fails the step", rc == 1)
|
|
127
|
+
check("the empty-output notice names the reason", "no output" in body)
|
|
128
|
+
|
|
129
|
+
rc, _ = run_guard(0, "\n \n\t\n")
|
|
130
|
+
check("whitespace-only agent output fails the step", rc == 1)
|
|
131
|
+
|
|
132
|
+
# --- other credential failures ------------------------------------------ #
|
|
133
|
+
rc, _ = run_guard(0, "API Error: 400 Credit balance is too low to access the API.\n")
|
|
134
|
+
check("a credit-balance failure fails the step", rc == 1)
|
|
135
|
+
rc, _ = run_guard(0, "Invalid API key · Please run `claude login`\n")
|
|
136
|
+
check("an invalid-key failure fails the step", rc == 1)
|
|
137
|
+
rc, _ = run_guard(0, "API Error: 429 rate limit exceeded\n")
|
|
138
|
+
check("a rate-limit failure fails the step", rc == 1)
|
|
139
|
+
|
|
140
|
+
# --- real reviews pass --------------------------------------------------- #
|
|
141
|
+
rc, body = run_guard(0, REAL_REVIEW)
|
|
142
|
+
check("a genuine multi-section review passes", rc == 0)
|
|
143
|
+
check("the real review is posted under the review heading", "**Verdict:**" in body)
|
|
144
|
+
|
|
145
|
+
rc, _ = run_guard(0, REVIEW_ABOUT_AUTH)
|
|
146
|
+
check("a long review that QUOTES an auth error is not mistaken for a failure", rc == 0)
|
|
147
|
+
|
|
148
|
+
# --- the workflow actually uses the guard -------------------------------- #
|
|
149
|
+
# Without this the fix is one deleted line away from returning, and nothing
|
|
150
|
+
# in the fixture tests above would notice.
|
|
151
|
+
wf = WORKFLOW.read_text(encoding="utf-8")
|
|
152
|
+
check("ai-content-review.yml invokes the guard",
|
|
153
|
+
"scripts/ci/agent_review_result.py" in wf)
|
|
154
|
+
check("the agent step no longer ends by piping stdout into the comment body",
|
|
155
|
+
not re.search(r"cat /tmp/agent-review\.md\s*\n\s*\}\s*>\s*/tmp/agent-review-final\.md", wf))
|
|
156
|
+
check("the AI gate no longer implies a present credential is a working one",
|
|
157
|
+
"not validity" in wf)
|
|
158
|
+
|
|
159
|
+
failed = [label for label, ok in CHECKS if not ok]
|
|
160
|
+
print()
|
|
161
|
+
for label, ok in CHECKS:
|
|
162
|
+
print(f" {'PASS' if ok else 'FAIL'} {label}")
|
|
163
|
+
print()
|
|
164
|
+
if failed:
|
|
165
|
+
print(f"FAILED ({len(failed)}/{len(CHECKS)})")
|
|
166
|
+
return 1
|
|
167
|
+
print(f"OK ({len(CHECKS)} checks)")
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Feature: ZER0-081
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# design-system-check — token parity between the theme and its Claude Design
|
|
7
|
+
# mirror.
|
|
8
|
+
#
|
|
9
|
+
# The `--zer0-*` design tokens live twice on purpose:
|
|
10
|
+
# - `_sass/tokens/*.scss` — the source of truth the theme compiles
|
|
11
|
+
# - `_design-system/tokens/*.css` — the mirror pushed to the Claude Design
|
|
12
|
+
# project (see _design-system/SYNC.md)
|
|
13
|
+
#
|
|
14
|
+
# This check keeps the two in lockstep so the design system can be built from
|
|
15
|
+
# either side without silent drift:
|
|
16
|
+
#
|
|
17
|
+
# 1. Per file pair: the sets of `--zer0-*` names must match, and every value
|
|
18
|
+
# must resolve to the same string. `var(--x, fallback)` wrappers are
|
|
19
|
+
# resolved against the mirror's concrete `--bs-*`/`--bd-*` primitives, so
|
|
20
|
+
# the theme's alias form and the mirror's self-contained form compare
|
|
21
|
+
# equal.
|
|
22
|
+
# 2. Skins: `_sass/theme/_skins.scss` and `tokens/skins.css` must declare the
|
|
23
|
+
# same skin names, and each mirror skin's brand hex must appear in the
|
|
24
|
+
# matching SCSS block.
|
|
25
|
+
# 3. Dangling-token audit: any fallback-less `var(--zer0-…)` reference in
|
|
26
|
+
# `_sass/` must be declared somewhere (tokens, includes, or JS).
|
|
27
|
+
#
|
|
28
|
+
# ruby scripts/design-system-check.rb # exit 1 + report on drift
|
|
29
|
+
#
|
|
30
|
+
# Runs in the `core` test suite (test/test_core.sh). Plain Ruby, no gems.
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
ROOT = File.expand_path('..', __dir__)
|
|
34
|
+
|
|
35
|
+
PAIRS = {
|
|
36
|
+
'_sass/tokens/_color.scss' => '_design-system/tokens/colors.css',
|
|
37
|
+
'_sass/tokens/_spacing.scss' => '_design-system/tokens/spacing.css',
|
|
38
|
+
'_sass/tokens/_radius.scss' => '_design-system/tokens/radius.css',
|
|
39
|
+
'_sass/tokens/_typography.scss' => '_design-system/tokens/typography.css',
|
|
40
|
+
'_sass/tokens/_shadow.scss' => '_design-system/tokens/shadows.css',
|
|
41
|
+
'_sass/tokens/_motion.scss' => '_design-system/tokens/motion.css',
|
|
42
|
+
'_sass/tokens/_breakpoints.scss' => '_design-system/tokens/breakpoints.css',
|
|
43
|
+
'_sass/tokens/_layers.scss' => '_design-system/tokens/layers.css'
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
SKIN_SCSS = '_sass/theme/_skins.scss'
|
|
47
|
+
SKIN_CSS = '_design-system/tokens/skins.css'
|
|
48
|
+
|
|
49
|
+
def read(rel)
|
|
50
|
+
path = File.join(ROOT, rel)
|
|
51
|
+
abort "design-system-check: missing file #{rel}" unless File.file?(path)
|
|
52
|
+
File.read(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Strip /* */ blocks, // line comments, and everything from the first @media on
|
|
56
|
+
# (motion files re-declare durations under prefers-reduced-motion).
|
|
57
|
+
def strip(text, scss:)
|
|
58
|
+
t = text.split('@media', 2).first
|
|
59
|
+
t = t.gsub(%r{/\*.*?\*/}m, '')
|
|
60
|
+
t = t.gsub(%r{(?:^|\s)//.*$}, '') if scss
|
|
61
|
+
t
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# { '--zer0-foo' => 'raw value', ... } from declaration-position matches.
|
|
65
|
+
def declarations(text)
|
|
66
|
+
text.scan(/(--zer0-[\w-]+)\s*:\s*([^;]+);/).each_with_object({}) do |(k, v), h|
|
|
67
|
+
h[k] ||= v.strip.gsub(/\s+/, ' ')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# All custom properties (any prefix) declared in the mirror's colors.css light
|
|
72
|
+
# scope — the concrete primitives var() chains resolve against.
|
|
73
|
+
def primitives
|
|
74
|
+
light = read(SKIN_CSS.sub('skins', 'colors')).split('[data-bs-theme="dark"]', 2).first
|
|
75
|
+
strip(light, scss: false).scan(/(--[\w-]+)\s*:\s*([^;]+);/)
|
|
76
|
+
.each_with_object({}) { |(k, v), h| h[k] ||= v.strip.gsub(/\s+/, ' ') }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Resolve a value that is entirely a var() wrapper: prefer the primitive's
|
|
80
|
+
# concrete value, else the wrapper's own fallback. Inner var() references in
|
|
81
|
+
# composite values (color-mix, rgba) are left as-is and compared literally.
|
|
82
|
+
def resolve(value, prims, depth = 0)
|
|
83
|
+
v = value.strip.gsub(/\s+/, ' ')
|
|
84
|
+
return v if depth > 6
|
|
85
|
+
|
|
86
|
+
m = v.match(/\Avar\((--[\w-]+)(?:\s*,\s*(.+))?\)\z/)
|
|
87
|
+
return v unless m
|
|
88
|
+
|
|
89
|
+
name, fallback = m[1], m[2]
|
|
90
|
+
if prims.key?(name)
|
|
91
|
+
resolve(prims[name], prims, depth + 1)
|
|
92
|
+
elsif fallback
|
|
93
|
+
resolve(fallback, prims, depth + 1)
|
|
94
|
+
else
|
|
95
|
+
v
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
failures = []
|
|
100
|
+
prims = primitives
|
|
101
|
+
|
|
102
|
+
# --- 1. token parity per file pair -----------------------------------------
|
|
103
|
+
PAIRS.each do |scss_rel, css_rel|
|
|
104
|
+
scss = declarations(strip(read(scss_rel), scss: true))
|
|
105
|
+
css = declarations(strip(read(css_rel), scss: false))
|
|
106
|
+
|
|
107
|
+
(scss.keys - css.keys).each { |k| failures << "#{css_rel}: missing #{k} (declared in #{scss_rel})" }
|
|
108
|
+
(css.keys - scss.keys).each { |k| failures << "#{scss_rel}: missing #{k} (declared in #{css_rel})" }
|
|
109
|
+
|
|
110
|
+
(scss.keys & css.keys).each do |k|
|
|
111
|
+
a = resolve(scss[k], prims)
|
|
112
|
+
b = resolve(css[k], prims)
|
|
113
|
+
failures << "#{k}: theme resolves to `#{a}` but mirror to `#{b}`" unless a == b
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Both motion files must keep the reduced-motion guard.
|
|
118
|
+
[PAIRS.key('_design-system/tokens/motion.css'), '_design-system/tokens/motion.css'].each do |rel|
|
|
119
|
+
failures << "#{rel}: lost its prefers-reduced-motion guard" unless read(rel).include?('prefers-reduced-motion')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- 2. skins: same names, same brand hex ----------------------------------
|
|
123
|
+
scss_skins_src = read(SKIN_SCSS)
|
|
124
|
+
css_skins_src = read(SKIN_CSS)
|
|
125
|
+
scss_skins = scss_skins_src.scan(/\[data-theme-skin="(\w+)"\]/).flatten.uniq.sort
|
|
126
|
+
css_skins = css_skins_src.scan(/\[data-theme-skin="(\w+)"\]/).flatten.uniq.sort
|
|
127
|
+
unless scss_skins == css_skins
|
|
128
|
+
failures << "skins differ: theme has #{scss_skins.join(', ')} but mirror has #{css_skins.join(', ')}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
(scss_skins & css_skins).each do |skin|
|
|
132
|
+
block = css_skins_src[/\[data-theme-skin="#{skin}"\]\s*\{.*?\}/m].to_s
|
|
133
|
+
hex = block[/--zer0-color-primary:\s*(#\h+)/, 1]
|
|
134
|
+
next unless hex
|
|
135
|
+
|
|
136
|
+
scss_block = scss_skins_src[/\[data-theme-skin="#{skin}"\]\s*\{.*?\}/m].to_s
|
|
137
|
+
unless scss_block.downcase.include?(hex.downcase)
|
|
138
|
+
failures << "skin #{skin}: mirror brand #{hex} not found in #{SKIN_SCSS}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# --- 3. dangling-token audit ------------------------------------------------
|
|
143
|
+
declared = Dir.glob(File.join(ROOT, '_sass/**/*.scss'))
|
|
144
|
+
.flat_map { |f| File.read(f).scan(/(--zer0-[\w-]+)\s*:/) }.flatten
|
|
145
|
+
declared += Dir.glob(File.join(ROOT, '{_includes,_layouts}/**/*.html'))
|
|
146
|
+
.flat_map { |f| File.read(f).scan(/(--zer0-[\w-]+)\s*:/) }.flatten
|
|
147
|
+
declared += Dir.glob(File.join(ROOT, 'assets/js/**/*.js')).flat_map do |f|
|
|
148
|
+
src = File.read(f)
|
|
149
|
+
src.scan(/setProperty\(\s*['"](--zer0-[\w-]+)/).flatten +
|
|
150
|
+
src.scan(/['"`](--zer0-[\w-]+)['"`:]/).flatten
|
|
151
|
+
end
|
|
152
|
+
declared = declared.uniq
|
|
153
|
+
|
|
154
|
+
Dir.glob(File.join(ROOT, '_sass/**/*.scss')).sort.each do |f|
|
|
155
|
+
File.read(f).scan(/var\(\s*(--zer0-[\w-]+)\s*\)/).flatten.uniq.each do |token|
|
|
156
|
+
next if declared.include?(token)
|
|
157
|
+
|
|
158
|
+
failures << "#{f.sub("#{ROOT}/", '')}: references #{token} with no fallback, but it is never declared"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- report -----------------------------------------------------------------
|
|
163
|
+
if failures.empty?
|
|
164
|
+
puts "design-system-check: OK — #{PAIRS.size} token file pairs, #{css_skins.size} skins, no dangling tokens"
|
|
165
|
+
else
|
|
166
|
+
warn "design-system-check: #{failures.size} problem(s) — theme (_sass/tokens) and mirror (_design-system/tokens) have drifted:"
|
|
167
|
+
failures.each { |f| warn " - #{f}" }
|
|
168
|
+
warn 'Fix the drift (see _design-system/SYNC.md), then re-run: ruby scripts/design-system-check.rb'
|
|
169
|
+
exit 1
|
|
170
|
+
end
|
data/scripts/install/README.md
CHANGED
|
@@ -86,8 +86,11 @@ This installer runs on macOS's `/bin/bash` (3.2). Restrictions:
|
|
|
86
86
|
# Local development
|
|
87
87
|
./scripts/bin/install help
|
|
88
88
|
./scripts/bin/install init . --profile blog --site-title "My Blog"
|
|
89
|
-
./scripts/bin/install
|
|
90
|
-
./scripts/bin/install
|
|
89
|
+
./scripts/bin/install init . --config zer0.install.yml # config-file driven
|
|
90
|
+
./scripts/bin/install wizard . --ai # Claude Code OAuth if available
|
|
91
|
+
./scripts/bin/install wizard . --ai --ai-provider anthropic
|
|
92
|
+
./scripts/bin/install suggest . "a docs site for a Rust CLI"
|
|
93
|
+
./scripts/bin/install doctor . # shows the active AI provider
|
|
91
94
|
./scripts/bin/install diff .
|
|
92
95
|
./scripts/bin/install plan . --profile docs
|
|
93
96
|
|
|
@@ -110,17 +113,55 @@ curl -fsSL https://raw.githubusercontent.com/bamr87/zer0-mistakes/main/scripts/b
|
|
|
110
113
|
3. Add template to `templates/config/` or `templates/pages/`
|
|
111
114
|
4. List in the appropriate profile YAML under `templates/profiles/`
|
|
112
115
|
|
|
116
|
+
## Config file layer
|
|
117
|
+
|
|
118
|
+
Sites can persist installer choices in a small YAML file instead of re-typing flags. `config.sh` discovers and merges these (low→high precedence):
|
|
119
|
+
|
|
120
|
+
1. `~/.config/zer0/install.yml` (user-global)
|
|
121
|
+
2. `<target>/zer0.install.yml` (project-local)
|
|
122
|
+
3. `<target>/.zer0/config.yml` (project-local, hidden)
|
|
123
|
+
4. `$ZER0_CONFIG` / `--config FILE` (explicit — wins)
|
|
124
|
+
|
|
125
|
+
The config layer sits between profile defaults and env/flag overrides:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
defaults < profile < CONFIG FILE < env vars < CLI flags
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Recognised keys (nested **or** flat/dotted both work): `profile`, `site.{title,description,author,email,url,timezone,locale}`, `github.{user,repo,pages_branch,enable_pages}`, `theme.{source,version}`, `deploy`, `agents`, `tasks`, `ai.provider`, `ai.model`. Example:
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
profile: github-pages
|
|
135
|
+
site:
|
|
136
|
+
title: "My Docs"
|
|
137
|
+
deploy: [github-pages]
|
|
138
|
+
ai:
|
|
139
|
+
provider: claude-cli # auto | claude-cli | anthropic | openai | none
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
API keys are **never** read from config files — they stay in the environment.
|
|
143
|
+
|
|
113
144
|
## AI integration
|
|
114
145
|
|
|
115
146
|
The AI path is a first-class citizen but never mandatory:
|
|
116
147
|
|
|
117
148
|
- `ai_wizard_run` → interactive LLM spec generation
|
|
118
|
-
- `ai_diagnose_run` → post-build error analysis
|
|
119
|
-
- `ai_suggest_run` → profile + deploy recommendation
|
|
149
|
+
- `ai_diagnose_run` → post-build error analysis
|
|
150
|
+
- `ai_suggest_run` → profile + deploy recommendation (also `install suggest`)
|
|
151
|
+
|
|
152
|
+
All are guarded by the `ZER0_NO_AI=1` kill-switch and degrade gracefully to defaults or rule-based logic when AI is unavailable.
|
|
153
|
+
|
|
154
|
+
### Providers (`ai/client.sh`)
|
|
155
|
+
|
|
156
|
+
The client is multi-provider. `ZER0_AI_PROVIDER` (or config `ai.provider`, or `--ai-provider`) selects one; the default `auto` resolves in this order:
|
|
120
157
|
|
|
121
|
-
|
|
158
|
+
| # | Provider | Auth | Notes |
|
|
159
|
+
|---|--------------|-------------------------------------------------|-------|
|
|
160
|
+
| 1 | `claude-cli` | the logged-in `claude` CLI (**Claude Code OAuth**) | Zero key handling — reuses `claude setup-token` / `claude login`. Preferred when the `claude` binary is on `PATH`. |
|
|
161
|
+
| 2 | `anthropic` | `CLAUDE_CODE_OAUTH_TOKEN` (OAuth) or `ANTHROPIC_API_KEY` | Anthropic Messages API over HTTPS. |
|
|
162
|
+
| 3 | `openai` | `OPENAI_API_KEY` (or `OPENAI_BASE_URL` for Azure/Ollama) | OpenAI-compatible `/chat/completions`. |
|
|
122
163
|
|
|
123
|
-
|
|
164
|
+
Model is auto-defaulted per provider and overridable with `ZER0_AI_MODEL` / `--ai-model` (`ANTHROPIC_MODEL` / `OPENAI_MODEL` also honoured). Run `install doctor` to see which provider is active. All calls are single-attempt with a 30s timeout; user context is sanitized before it leaves the host.
|
|
124
165
|
|
|
125
166
|
## Deploy plugins
|
|
126
167
|
|