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,167 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/config.sh — User config-file layer
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Lets a site persist its installer choices in a small YAML file instead of
|
|
6
|
+
# re-typing flags every run. Discovered files are merged low→high precedence:
|
|
7
|
+
#
|
|
8
|
+
# 1. ~/.config/zer0/install.yml (user-global defaults)
|
|
9
|
+
# 2. <target>/zer0.install.yml (project-local)
|
|
10
|
+
# 3. <target>/.zer0/config.yml (project-local, hidden)
|
|
11
|
+
# 4. $ZER0_CONFIG / --config FILE (explicit — wins over the rest)
|
|
12
|
+
#
|
|
13
|
+
# In the plan pipeline this sits between profile defaults and env/flag
|
|
14
|
+
# overrides: defaults < profile < CONFIG FILE < env vars < CLI flags.
|
|
15
|
+
#
|
|
16
|
+
# Recognised keys (nested or flat/dotted both work):
|
|
17
|
+
# profile
|
|
18
|
+
# site.title|description|author|email|url|timezone|locale
|
|
19
|
+
# github.user|repo|pages_branch|enable_pages
|
|
20
|
+
# theme.source|version
|
|
21
|
+
# deploy (scalar or list → space-joined)
|
|
22
|
+
# agents (scalar or list → space-joined)
|
|
23
|
+
# tasks (scalar or list → space-joined)
|
|
24
|
+
# ai.provider (auto|claude-cli|anthropic|openai|none)
|
|
25
|
+
# ai.model
|
|
26
|
+
#
|
|
27
|
+
# API keys are deliberately NOT read from config files — they stay in the
|
|
28
|
+
# environment only (see ai/client.sh).
|
|
29
|
+
#
|
|
30
|
+
# Bash 3.2 compatible. No `declare -A`, no `set -euo pipefail` here.
|
|
31
|
+
# =============================================================================
|
|
32
|
+
[[ -n "${_HAS_CONFIG_LIB:-}" ]] && return 0
|
|
33
|
+
_HAS_CONFIG_LIB=1
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# config_discover TARGET_DIR → print existing config files, low→high priority.
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
config_discover() {
|
|
39
|
+
local target="${1:-$(pwd)}"
|
|
40
|
+
local f
|
|
41
|
+
for f in \
|
|
42
|
+
"${HOME:-/nonexistent}/.config/zer0/install.yml" \
|
|
43
|
+
"${target}/zer0.install.yml" \
|
|
44
|
+
"${target}/zer0.install.yaml" \
|
|
45
|
+
"${target}/.zer0/config.yml" \
|
|
46
|
+
"${_FLAG_CONFIG:-}" \
|
|
47
|
+
"${ZER0_CONFIG:-}" ; do
|
|
48
|
+
[[ -n "$f" && -f "$f" ]] && printf '%s\n' "$f"
|
|
49
|
+
done
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# _config_flatten FILE → emit `key=value` for a small YAML subset:
|
|
54
|
+
# - top-level scalars foo: bar → foo=bar
|
|
55
|
+
# - one level of nested maps site:\n title: x → site.title=x
|
|
56
|
+
# - block lists deploy:\n - a → deploy=a b
|
|
57
|
+
# - inline flow lists deploy: [a, b] → deploy=a b
|
|
58
|
+
# Quotes and inline comments are stripped. Not a full YAML parser — just
|
|
59
|
+
# enough for installer config, mirroring plan.sh's awk profile reader.
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
_config_flatten() {
|
|
62
|
+
local file="$1"
|
|
63
|
+
[[ -f "$file" ]] || return 0
|
|
64
|
+
awk '
|
|
65
|
+
function strip(s) {
|
|
66
|
+
sub(/[[:space:]]*#.*$/, "", s) # trailing comment
|
|
67
|
+
sub(/^[[:space:]]+/, "", s); sub(/[[:space:]]+$/, "", s)
|
|
68
|
+
gsub(/^["'"'"']|["'"'"']$/, "", s) # surrounding quotes
|
|
69
|
+
return s
|
|
70
|
+
}
|
|
71
|
+
# blank / comment lines
|
|
72
|
+
/^[[:space:]]*$/ { next }
|
|
73
|
+
/^[[:space:]]*#/ { next }
|
|
74
|
+
{
|
|
75
|
+
indent = 0
|
|
76
|
+
tmp = $0
|
|
77
|
+
while (substr(tmp,1,1) == " ") { indent++; tmp = substr(tmp,2) }
|
|
78
|
+
}
|
|
79
|
+
# list item under the current parent key
|
|
80
|
+
indent > 0 && tmp ~ /^-[[:space:]]+/ {
|
|
81
|
+
if (listkey != "") {
|
|
82
|
+
item = tmp; sub(/^-[[:space:]]+/, "", item); item = strip(item)
|
|
83
|
+
vals[listkey] = (vals[listkey] == "" ? item : vals[listkey] " " item)
|
|
84
|
+
}
|
|
85
|
+
next
|
|
86
|
+
}
|
|
87
|
+
# key: value (any depth we care about is 0 or 1)
|
|
88
|
+
match(tmp, /^[A-Za-z0-9_.-]+[[:space:]]*:/) {
|
|
89
|
+
key = tmp; sub(/[[:space:]]*:.*$/, "", key)
|
|
90
|
+
val = tmp; sub(/^[A-Za-z0-9_.-]+[[:space:]]*:/, "", val); val = strip(val)
|
|
91
|
+
|
|
92
|
+
if (indent == 0) {
|
|
93
|
+
parent = key
|
|
94
|
+
listkey = ""
|
|
95
|
+
if (val == "") { listkey = key; next } # map/list header
|
|
96
|
+
# inline flow list? key: [a, b, c]
|
|
97
|
+
if (val ~ /^\[.*\]$/) {
|
|
98
|
+
gsub(/^\[|\]$/, "", val); gsub(/[[:space:],]+/, " ", val)
|
|
99
|
+
val = strip(val)
|
|
100
|
+
}
|
|
101
|
+
print key "=" val
|
|
102
|
+
} else {
|
|
103
|
+
# nested child of the last top-level parent
|
|
104
|
+
if (parent != "") print parent "." key "=" val
|
|
105
|
+
}
|
|
106
|
+
next
|
|
107
|
+
}
|
|
108
|
+
END {
|
|
109
|
+
for (k in vals) print k "=" vals[k]
|
|
110
|
+
}
|
|
111
|
+
' "$file"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# config_apply_file FILE → map recognised keys onto SPEC_* globals + AI env.
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
config_apply_file() {
|
|
118
|
+
local file="$1"
|
|
119
|
+
[[ -f "$file" ]] || return 0
|
|
120
|
+
log_debug "config: loading ${file}"
|
|
121
|
+
|
|
122
|
+
local line key val
|
|
123
|
+
while IFS= read -r line; do
|
|
124
|
+
key="${line%%=*}"
|
|
125
|
+
val="${line#*=}"
|
|
126
|
+
[[ -z "$key" || "$key" == "$line" ]] && continue
|
|
127
|
+
case "$key" in
|
|
128
|
+
profile) SPEC_PROFILE="$val" ;;
|
|
129
|
+
site.title|site_title) SPEC_SITE_TITLE="$val" ;;
|
|
130
|
+
site.description|site_description) SPEC_SITE_DESCRIPTION="$val" ;;
|
|
131
|
+
site.author|site_author) SPEC_SITE_AUTHOR="$val" ;;
|
|
132
|
+
site.email|site_email) SPEC_SITE_EMAIL="$val" ;;
|
|
133
|
+
site.url|site_url) SPEC_SITE_URL="$val" ;;
|
|
134
|
+
site.timezone|site_timezone) SPEC_SITE_TIMEZONE="$val" ;;
|
|
135
|
+
site.locale|site_locale) SPEC_SITE_LOCALE="$val" ;;
|
|
136
|
+
github.user|github_user) SPEC_GITHUB_USER="$val" ;;
|
|
137
|
+
github.repo|github_repo) SPEC_GITHUB_REPO="$val" ;;
|
|
138
|
+
github.pages_branch) SPEC_GITHUB_PAGES_BRANCH="$val" ;;
|
|
139
|
+
github.enable_pages) SPEC_GITHUB_ENABLE_PAGES="$val" ;;
|
|
140
|
+
theme.source|theme_source) SPEC_THEME_SOURCE="$val" ;;
|
|
141
|
+
theme.version|theme_version) SPEC_THEME_VERSION="$val" ;;
|
|
142
|
+
deploy|deploy_targets) SPEC_DEPLOY="$val" ;;
|
|
143
|
+
agents|agent_files) SPEC_AGENTS="$val" ;;
|
|
144
|
+
tasks) SPEC_TASKS="$val" ;;
|
|
145
|
+
ai.provider|ai_provider)
|
|
146
|
+
SPEC_AI_PROVIDER="$val"
|
|
147
|
+
export ZER0_AI_PROVIDER="${ZER0_AI_PROVIDER:-$val}" ;;
|
|
148
|
+
ai.model|ai_model)
|
|
149
|
+
SPEC_AI_MODEL="$val"
|
|
150
|
+
export ZER0_AI_MODEL="${ZER0_AI_MODEL:-$val}" ;;
|
|
151
|
+
*) log_debug "config: ignoring unknown key '${key}'" ;;
|
|
152
|
+
esac
|
|
153
|
+
done < <(_config_flatten "$file")
|
|
154
|
+
return 0
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# config_load TARGET_DIR → apply every discovered config file in order.
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
config_load() {
|
|
161
|
+
local target="${1:-$(pwd)}"
|
|
162
|
+
local f
|
|
163
|
+
while IFS= read -r f; do
|
|
164
|
+
[[ -n "$f" ]] && config_apply_file "$f"
|
|
165
|
+
done < <(config_discover "$target")
|
|
166
|
+
return 0
|
|
167
|
+
}
|
data/scripts/install/doctor.sh
CHANGED
|
@@ -118,6 +118,43 @@ doctor_check_gh() {
|
|
|
118
118
|
_doctor_pass "GitHub CLI $ghver"
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# doctor_check_ai — report which AI provider (if any) will serve wizard/
|
|
123
|
+
# suggest/diagnose. Warn-only: AI is always opt-in and never blocks install.
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
doctor_check_ai() {
|
|
126
|
+
if [[ "$(type -t ai_client_provider)" != "function" ]]; then
|
|
127
|
+
local _d
|
|
128
|
+
_d="${_CLI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)}"
|
|
129
|
+
[[ -f "${_d}/ai/client.sh" ]] && source "${_d}/ai/client.sh"
|
|
130
|
+
fi
|
|
131
|
+
if [[ "$(type -t ai_client_provider)" != "function" ]]; then
|
|
132
|
+
_doctor_warn "AI: client module unavailable (wizard/suggest/diagnose disabled)"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if [[ "${ZER0_NO_AI:-0}" == "1" ]]; then
|
|
137
|
+
_doctor_pass "AI: disabled via ZER0_NO_AI (rule-based fallbacks only)"
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
case "${ZER0_AI_PROVIDER:-}" in
|
|
141
|
+
none|off|disabled)
|
|
142
|
+
_doctor_pass "AI: disabled by choice (rule-based fallbacks only)"
|
|
143
|
+
return 0 ;;
|
|
144
|
+
esac
|
|
145
|
+
|
|
146
|
+
local provider
|
|
147
|
+
provider="$(ai_client_provider)"
|
|
148
|
+
if [[ "$provider" == "none" ]] || ! ai_client_available; then
|
|
149
|
+
_doctor_warn "AI: no provider available — install the 'claude' CLI (Claude Code OAuth), or set CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY / OPENAI_API_KEY"
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
local model
|
|
154
|
+
model="$(ai_client_model "$provider")"
|
|
155
|
+
_doctor_pass "AI: ${provider} via $(ai_client_auth_source)${model:+ (model ${model})}"
|
|
156
|
+
}
|
|
157
|
+
|
|
121
158
|
# ---------------------------------------------------------------------------
|
|
122
159
|
# doctor_check_writable TARGET_DIR
|
|
123
160
|
# ---------------------------------------------------------------------------
|
|
@@ -155,6 +192,7 @@ doctor_run() {
|
|
|
155
192
|
doctor_check_docker
|
|
156
193
|
doctor_check_git || failures=$(( failures + 1 ))
|
|
157
194
|
doctor_check_gh
|
|
195
|
+
doctor_check_ai
|
|
158
196
|
[[ -n "$target" ]] && { doctor_check_writable "$target" || failures=$(( failures + 1 )); }
|
|
159
197
|
|
|
160
198
|
if [[ $failures -eq 0 ]]; then
|
data/scripts/install/plan.sh
CHANGED
|
@@ -144,6 +144,8 @@ plan_apply_flags() {
|
|
|
144
144
|
[[ -n "${_FLAG_DEPLOY:-}" ]] && SPEC_DEPLOY="$_FLAG_DEPLOY"
|
|
145
145
|
[[ -n "${_FLAG_AGENTS:-}" ]] && SPEC_AGENTS="$_FLAG_AGENTS"
|
|
146
146
|
[[ -n "${_FLAG_TASKS:-}" ]] && SPEC_TASKS="$_FLAG_TASKS"
|
|
147
|
+
[[ -n "${_FLAG_AI_PROVIDER:-}" ]] && SPEC_AI_PROVIDER="$_FLAG_AI_PROVIDER"
|
|
148
|
+
[[ -n "${_FLAG_AI_MODEL:-}" ]] && SPEC_AI_MODEL="$_FLAG_AI_MODEL"
|
|
147
149
|
|
|
148
150
|
# Booleans — flags are set to "1" when present
|
|
149
151
|
[[ "${_FLAG_DRY_RUN:-0}" == "1" ]] && SPEC_OPT_DRY_RUN=true
|
|
@@ -231,12 +233,19 @@ plan_build() {
|
|
|
231
233
|
# Layer 1: Profile defaults
|
|
232
234
|
[[ -n "$profile_file" ]] && plan_load_profile "$profile_file"
|
|
233
235
|
|
|
236
|
+
# Layer 1.5: User config file(s) — override profile, sit below env/flags.
|
|
237
|
+
if [[ "$(type -t config_load)" == "function" ]]; then
|
|
238
|
+
config_load "$target_dir"
|
|
239
|
+
fi
|
|
240
|
+
|
|
234
241
|
# Layer 2: Environment variable overrides (ZER0_SITE_* etc.)
|
|
235
242
|
[[ -n "${ZER0_SITE_TITLE:-}" ]] && SPEC_SITE_TITLE="$ZER0_SITE_TITLE"
|
|
236
243
|
[[ -n "${ZER0_SITE_AUTHOR:-}" ]] && SPEC_SITE_AUTHOR="$ZER0_SITE_AUTHOR"
|
|
237
244
|
[[ -n "${ZER0_SITE_EMAIL:-}" ]] && SPEC_SITE_EMAIL="$ZER0_SITE_EMAIL"
|
|
238
245
|
[[ -n "${ZER0_GITHUB_USER:-}" ]] && SPEC_GITHUB_USER="$ZER0_GITHUB_USER"
|
|
239
246
|
[[ -n "${ZER0_GITHUB_REPO:-}" ]] && SPEC_GITHUB_REPO="$ZER0_GITHUB_REPO"
|
|
247
|
+
[[ -n "${ZER0_AI_PROVIDER:-}" ]] && SPEC_AI_PROVIDER="$ZER0_AI_PROVIDER"
|
|
248
|
+
[[ -n "${ZER0_AI_MODEL:-}" ]] && SPEC_AI_MODEL="$ZER0_AI_MODEL"
|
|
240
249
|
|
|
241
250
|
# Layer 3: CLI flags (highest priority)
|
|
242
251
|
plan_apply_flags
|
|
@@ -271,6 +280,7 @@ plan_build() {
|
|
|
271
280
|
SPEC_OPT_DRY_RUN SPEC_OPT_FORCE SPEC_OPT_BACKUP \
|
|
272
281
|
SPEC_OPT_NON_INTERACTIVE SPEC_OPT_OUTPUT SPEC_OPT_AUTO_ACCEPT \
|
|
273
282
|
SPEC_OPT_SKIP_DOCTOR SPEC_OPT_VERBOSE \
|
|
283
|
+
SPEC_AI_PROVIDER SPEC_AI_MODEL \
|
|
274
284
|
SITE_TITLE SITE_DESCRIPTION SITE_AUTHOR SITE_EMAIL SITE_URL \
|
|
275
285
|
SITE_TIMEZONE SITE_LOCALE GITHUB_USER GITHUB_REPO \
|
|
276
286
|
GITHUB_PAGES_BRANCH THEME_SOURCE REPOSITORY_NAME INSTALL_PROFILE
|
data/scripts/install/spec.sh
CHANGED
|
@@ -85,7 +85,7 @@ spec_default() {
|
|
|
85
85
|
},
|
|
86
86
|
"ai": {
|
|
87
87
|
"used": false,
|
|
88
|
-
"provider": "
|
|
88
|
+
"provider": "auto",
|
|
89
89
|
"model": "",
|
|
90
90
|
"tokens_estimated": 0,
|
|
91
91
|
"spec_hash": ""
|
|
@@ -108,10 +108,18 @@ spec_write() {
|
|
|
108
108
|
local out_file="$1"
|
|
109
109
|
local dry_run="${_FS_DRY_RUN:-0}"
|
|
110
110
|
|
|
111
|
-
# Build tasks array from space-separated SPEC_TASKS
|
|
111
|
+
# Build tasks array from space-separated SPEC_TASKS.
|
|
112
|
+
# Distinguish UNSET (→ default task list) from EMPTY (→ [], e.g. a
|
|
113
|
+
# deploy-only run). Using `${SPEC_TASKS:-default}` here would turn an
|
|
114
|
+
# intentional empty list back into the full default and clobber content.
|
|
112
115
|
local tasks_json=""
|
|
113
|
-
local t
|
|
114
|
-
|
|
116
|
+
local t _tasks_src
|
|
117
|
+
if [[ -z "${SPEC_TASKS+set}" ]]; then
|
|
118
|
+
_tasks_src="config gemfile docker pages nav data gitignore readme marker"
|
|
119
|
+
else
|
|
120
|
+
_tasks_src="${SPEC_TASKS}"
|
|
121
|
+
fi
|
|
122
|
+
for t in ${_tasks_src}; do
|
|
115
123
|
tasks_json="${tasks_json}\"${t}\","
|
|
116
124
|
done
|
|
117
125
|
tasks_json="[${tasks_json%,}]"
|
|
@@ -168,7 +176,7 @@ spec_write() {
|
|
|
168
176
|
},
|
|
169
177
|
"ai": {
|
|
170
178
|
"used": ${SPEC_AI_USED:-false},
|
|
171
|
-
"provider": "${SPEC_AI_PROVIDER:-
|
|
179
|
+
"provider": "${SPEC_AI_PROVIDER:-auto}",
|
|
172
180
|
"model": "${SPEC_AI_MODEL:-}",
|
|
173
181
|
"tokens_estimated": ${SPEC_AI_TOKENS:-0},
|
|
174
182
|
"spec_hash": ""
|
|
@@ -248,7 +256,7 @@ _spec_read_jq() {
|
|
|
248
256
|
SPEC_OPT_SKIP_DOCTOR=$(jq -r '.options.skip_doctor // false' "$f")
|
|
249
257
|
SPEC_OPT_VERBOSE=$(jq -r '.options.verbose // false' "$f")
|
|
250
258
|
SPEC_AI_USED=$(jq -r '.ai.used // false' "$f")
|
|
251
|
-
SPEC_AI_PROVIDER=$(jq -r '.ai.provider // "
|
|
259
|
+
SPEC_AI_PROVIDER=$(jq -r '.ai.provider // "auto"' "$f")
|
|
252
260
|
SPEC_AI_MODEL=$(jq -r '.ai.model // ""' "$f")
|
|
253
261
|
SPEC_AI_TOKENS=$(jq -r '.ai.tokens_estimated // 0' "$f")
|
|
254
262
|
SPEC_SCRAPE_SOURCE_URL=$(jq -r '.scrape.source_url // ""' "$f")
|
|
@@ -369,7 +377,7 @@ _spec_read_awk() {
|
|
|
369
377
|
SPEC_OPT_SKIP_DOCTOR="${SPEC_OPT_SKIP_DOCTOR:-false}"
|
|
370
378
|
SPEC_OPT_VERBOSE="${SPEC_OPT_VERBOSE:-false}"
|
|
371
379
|
SPEC_AI_USED="${SPEC_AI_USED:-false}"
|
|
372
|
-
SPEC_AI_PROVIDER="${SPEC_AI_PROVIDER:-
|
|
380
|
+
SPEC_AI_PROVIDER="${SPEC_AI_PROVIDER:-auto}"
|
|
373
381
|
SPEC_AI_MODEL="${SPEC_AI_MODEL:-}"
|
|
374
382
|
SPEC_AI_TOKENS="${SPEC_AI_TOKENS:-0}"
|
|
375
383
|
SPEC_SCRAPE_SOURCE_URL="${SPEC_SCRAPE_SOURCE_URL:-$(awk '/"source_url"/ { gsub(/.*"source_url"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; exit }' "$f" 2>/dev/null)}"
|
data/scripts/install/template.sh
CHANGED
|
@@ -61,6 +61,7 @@ tmpl_render() {
|
|
|
61
61
|
-e "s|{{THEME_DISPLAY_NAME}}|${THEME_DISPLAY_NAME:-Zer0-Mistakes}|g" \
|
|
62
62
|
-e "s|{{THEME_VERSION}}|${THEME_VERSION:-}|g" \
|
|
63
63
|
-e "s|{{THEME_SOURCE}}|${THEME_SOURCE:-gem}|g" \
|
|
64
|
+
-e "s|{{THEME_REMOTE}}|${THEME_REMOTE:-bamr87/zer0-mistakes}|g" \
|
|
64
65
|
-e "s|{{GITHUB_USER}}|${GITHUB_USER:-}|g" \
|
|
65
66
|
-e "s|{{FORK_GITHUB_USER}}|${GITHUB_USER:-}|g" \
|
|
66
67
|
-e "s|{{GITHUB_REPO}}|${GITHUB_REPO:-}|g" \
|
|
@@ -72,6 +73,7 @@ tmpl_render() {
|
|
|
72
73
|
-e "s|{{REMOTE_BRANCH}}|${REMOTE_BRANCH:-${GITHUB_PAGES_BRANCH:-gh-pages}}|g" \
|
|
73
74
|
-e "s|{{REPOSITORY_NAME}}|${REPOSITORY_NAME:-${GITHUB_REPO:-my-site}}|g" \
|
|
74
75
|
-e "s|{{SITE_TITLE}}|${SITE_TITLE:-My Jekyll Site}|g" \
|
|
76
|
+
-e "s|{{SITE_NAME}}|${SITE_TITLE:-My Jekyll Site}|g" \
|
|
75
77
|
-e "s|{{SITE_DESCRIPTION}}|${SITE_DESCRIPTION:-A Jekyll site built with zer0-mistakes}|g" \
|
|
76
78
|
-e "s|{{SITE_AUTHOR}}|${SITE_AUTHOR:-Site Author}|g" \
|
|
77
79
|
-e "s|{{SITE_EMAIL}}|${SITE_EMAIL:-}|g" \
|
|
@@ -87,6 +89,8 @@ tmpl_render() {
|
|
|
87
89
|
-e "s|{{COMMONMARKER_MACOS_VERSION}}|${COMMONMARKER_MACOS_VERSION:-~> 0.23}|g" \
|
|
88
90
|
-e "s|{{GITHUB_PAGES_MAX_VERSION}}|${GITHUB_PAGES_MAX_VERSION:-232}|g" \
|
|
89
91
|
-e "s|{{RUBY_MIN_VERSION_MACOS}}|${RUBY_MIN_VERSION_MACOS:-2.6.0}|g" \
|
|
92
|
+
-e "s|{{DEFAULT_BRANCH}}|${DEFAULT_BRANCH:-main}|g" \
|
|
93
|
+
-e "s|{{RUBY_VERSION}}|${RUBY_VERSION:-3.3}|g" \
|
|
90
94
|
-e "s|{{INSTALL_PROFILE}}|${INSTALL_PROFILE:-default}|g" \
|
|
91
95
|
-e "s|{{INSTALL_MODE}}|${INSTALL_MODE:-full}|g" \
|
|
92
96
|
-e "s|{{CURRENT_DATE}}|$(date +%Y-%m-%d)|g" \
|
data/scripts/lib/audit.sh
CHANGED
|
@@ -205,13 +205,45 @@ detect_consumer_mode() {
|
|
|
205
205
|
return
|
|
206
206
|
fi
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
# Match a top-level `remote_theme` key. The whitespace class before the
|
|
209
|
+
# colon is load-bearing: most fleet consumers write column-aligned YAML
|
|
210
|
+
# (`remote_theme : "bamr87/zer0-mistakes"`), which a bare
|
|
211
|
+
# `^remote_theme:` never matches — every one of them was silently
|
|
212
|
+
# misreported as gem mode, skipping the plugin checks entirely.
|
|
213
|
+
# `|| true` keeps a no-match grep from tripping the caller's `set -o pipefail`.
|
|
214
|
+
local line value
|
|
215
|
+
line=$(grep -m1 -E '^remote_theme[[:space:]]*:' "$config" 2>/dev/null || true)
|
|
216
|
+
value=$(printf '%s' "$line" \
|
|
217
|
+
| sed -E 's/^remote_theme[[:space:]]*:[[:space:]]*//; s/[[:space:]]*(#.*)?$//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/')
|
|
218
|
+
|
|
219
|
+
# `remote_theme: false` (or an empty value) is how a consumer turns the
|
|
220
|
+
# remote theme OFF and resolves the theme as a gem instead.
|
|
221
|
+
if [[ -n "$value" && "$value" != "false" ]]; then
|
|
209
222
|
echo "remote_theme"
|
|
210
223
|
else
|
|
211
224
|
echo "gem"
|
|
212
225
|
fi
|
|
213
226
|
}
|
|
214
227
|
|
|
228
|
+
# detect_consumer_source <consumer_root>
|
|
229
|
+
# Echoes the Jekyll `source:` value from _config.yml (relative path), or empty.
|
|
230
|
+
#
|
|
231
|
+
# Jekyll's `source:` relocates the whole site tree — zer0-pages keeps every
|
|
232
|
+
# theme-shadowing file under `pages/`. Auditing the repo root there reports a
|
|
233
|
+
# falsely clean "0 overrides, 0 unique files"; the real surface only appears
|
|
234
|
+
# when the scan follows `source:`.
|
|
235
|
+
detect_consumer_source() {
|
|
236
|
+
local consumer_root="$1"
|
|
237
|
+
local config="$consumer_root/_config.yml"
|
|
238
|
+
|
|
239
|
+
[[ -f "$config" ]] || return 0
|
|
240
|
+
|
|
241
|
+
local line
|
|
242
|
+
line=$(grep -m1 -E '^source[[:space:]]*:' "$config" 2>/dev/null || true)
|
|
243
|
+
printf '%s' "$line" \
|
|
244
|
+
| sed -E 's/^source[[:space:]]*:[[:space:]]*//; s/[[:space:]]*(#.*)?$//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/; s#^\./##; s#/$##'
|
|
245
|
+
}
|
|
246
|
+
|
|
215
247
|
# ---------------------------------------------------------------------------
|
|
216
248
|
# Output helpers
|
|
217
249
|
# ---------------------------------------------------------------------------
|
|
@@ -277,7 +309,15 @@ print_classification() {
|
|
|
277
309
|
echo "::notice file=$relpath::OPTIONAL_PLUGIN (not required)" ;;
|
|
278
310
|
esac
|
|
279
311
|
elif [[ "$format" == "json" ]]; then
|
|
280
|
-
|
|
312
|
+
# Separator goes BEFORE each entry but the first, so the array never
|
|
313
|
+
# ends on a dangling comma. Emitting a trailing comma per entry (the
|
|
314
|
+
# previous behaviour) produced `...},\n]}` — which no JSON parser
|
|
315
|
+
# accepts, making --format json unusable for any CI consumer.
|
|
316
|
+
if [[ "${JSON_ENTRY_EMITTED:-0}" == "1" ]]; then
|
|
317
|
+
printf ',\n'
|
|
318
|
+
fi
|
|
319
|
+
JSON_ENTRY_EMITTED=1
|
|
320
|
+
printf ' {"path":"%s","status":"%s"}' "$relpath" "$classification"
|
|
281
321
|
else
|
|
282
322
|
echo -e "${color}[$classification]${NC} $relpath"
|
|
283
323
|
fi
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
# lint-liquid-raw — guard the Markdown the Pages build renders through Liquid
|
|
6
|
+
#
|
|
7
|
+
# `github-pages` ships jekyll-optional-front-matter, so EVERY non-excluded
|
|
8
|
+
# Markdown file in the repo becomes a page — CHANGELOG.md and the
|
|
9
|
+
# _design-system/ mirror included — and every one of them is run through Liquid
|
|
10
|
+
# before Markdown. Two failure modes follow, and neither is loud:
|
|
11
|
+
#
|
|
12
|
+
# 1. An unbalanced `{% raw %}`. Liquid's raw block swallows everything up to
|
|
13
|
+
# the FIRST `{% endraw %}`, so a stray opener silently eats the rest of the
|
|
14
|
+
# document. CHANGELOG.md carried one that consumed 945 lines while the
|
|
15
|
+
# build stayed green.
|
|
16
|
+
#
|
|
17
|
+
# 2. A literal `{% raw %}` / `{% endraw %}` written as documentation prose.
|
|
18
|
+
# It reads like text but parses as a tag, which is how (1) happens. To show
|
|
19
|
+
# the tag, escape it: `{% raw %}{% raw %}{% endraw %}`.
|
|
20
|
+
#
|
|
21
|
+
# A Liquid *syntax* error (e.g. `{{…}}`) is caught by the Pages workflow, which
|
|
22
|
+
# fails on "Liquid Warning" in the build output. This script catches the class
|
|
23
|
+
# the build cannot report, because swallowed text is not an error to Liquid.
|
|
24
|
+
#
|
|
25
|
+
# Usage:
|
|
26
|
+
# scripts/lint-liquid-raw.rb # scan the rendered Markdown surface
|
|
27
|
+
# scripts/lint-liquid-raw.rb PATH... # scan specific files
|
|
28
|
+
# scripts/lint-liquid-raw.rb --verbose # list every file scanned
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
ROOT = File.expand_path('..', __dir__)
|
|
32
|
+
TAG = /\{%-?\s*(raw|endraw)\s*-?%\}/.freeze
|
|
33
|
+
|
|
34
|
+
# The escape form: a raw block whose entire body is one raw/endraw tag, which
|
|
35
|
+
# is how you render the tag as text. Blanked before scanning so the literal it
|
|
36
|
+
# carries is not mistaken for a real tag.
|
|
37
|
+
ESCAPED = /\{%-?\s*raw\s*-?%\}\{%-?\s*(?:raw|endraw)\s*-?%\}\{%-?\s*endraw\s*-?%\}/.freeze
|
|
38
|
+
|
|
39
|
+
# Directories Jekyll never reads, plus the ones _config.yml excludes. Kept in
|
|
40
|
+
# sync with the `exclude:` list; a path missing here only costs a false
|
|
41
|
+
# positive, never a missed one.
|
|
42
|
+
SKIP_DIRS = %w[
|
|
43
|
+
.git .github .claude .cursor .devcontainer .frontmatter .vscode .obsidian
|
|
44
|
+
.jekyll-cache .sass-cache _site node_modules vendor pkg logs
|
|
45
|
+
docs scripts templates test tests examples submodules gemfiles docker lib
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# True when `pos` sits inside a single-backtick code span: an odd number of
|
|
49
|
+
# backtick runs precede it on its own line.
|
|
50
|
+
def in_code_span?(text, pos)
|
|
51
|
+
bol = text.rindex("\n", pos - 1)
|
|
52
|
+
line = text[(bol ? bol + 1 : 0)...pos]
|
|
53
|
+
line.scan(/`+/).size.odd?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scan(path)
|
|
57
|
+
# Blank the escape form (keeping byte offsets and newlines) so the literal
|
|
58
|
+
# tag it renders is not read as a tag itself.
|
|
59
|
+
text = File.read(path, encoding: 'UTF-8')
|
|
60
|
+
.gsub(ESCAPED) { |m| ' ' * m.length }
|
|
61
|
+
problems = []
|
|
62
|
+
in_raw = false
|
|
63
|
+
opened_at = nil
|
|
64
|
+
opened_in_span = false
|
|
65
|
+
|
|
66
|
+
text.to_enum(:scan, TAG).each do
|
|
67
|
+
match = Regexp.last_match
|
|
68
|
+
line = text[0, match.begin(0)].count("\n") + 1
|
|
69
|
+
if in_raw
|
|
70
|
+
# Inside a raw block every tag is literal text — nothing to check.
|
|
71
|
+
if match[1] == 'endraw'
|
|
72
|
+
if opened_in_span && line != opened_at
|
|
73
|
+
problems << "#{path}:#{opened_at}: `{% raw %}` inside a code span " \
|
|
74
|
+
"opens a block that only closes at line #{line} — " \
|
|
75
|
+
'escape it to show the tag as text'
|
|
76
|
+
end
|
|
77
|
+
in_raw = false
|
|
78
|
+
opened_at = nil
|
|
79
|
+
opened_in_span = false
|
|
80
|
+
end
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if match[1] == 'raw'
|
|
85
|
+
in_raw = true
|
|
86
|
+
opened_at = line
|
|
87
|
+
# `{% raw %}` inside an inline code span is the documentation-mention
|
|
88
|
+
# trap: it reads as text but opens a real block. Legitimate inline use
|
|
89
|
+
# (`{% raw %}{{ x }}{% endraw %}`) closes on the same line; a mention
|
|
90
|
+
# does not, and swallows everything up to the next `{% endraw %}`.
|
|
91
|
+
opened_in_span = in_code_span?(text, match.begin(0))
|
|
92
|
+
else
|
|
93
|
+
problems << "#{path}:#{line}: `{% endraw %}` with no matching `{% raw %}`"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if in_raw
|
|
98
|
+
problems << "#{path}:#{opened_at}: `{% raw %}` is never closed — " \
|
|
99
|
+
'Liquid will swallow the rest of the file'
|
|
100
|
+
end
|
|
101
|
+
problems
|
|
102
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError => e
|
|
103
|
+
["#{path}: could not read as UTF-8 (#{e.class})"]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def markdown_files
|
|
107
|
+
Dir.chdir(ROOT) do
|
|
108
|
+
Dir.glob('**/*.{md,markdown}', File::FNM_DOTMATCH).reject do |p|
|
|
109
|
+
p.split(File::SEPARATOR).any? { |seg| SKIP_DIRS.include?(seg) }
|
|
110
|
+
end.sort
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
verbose = ARGV.delete('--verbose')
|
|
115
|
+
targets = ARGV.empty? ? markdown_files : ARGV
|
|
116
|
+
|
|
117
|
+
problems = []
|
|
118
|
+
Dir.chdir(ROOT) do
|
|
119
|
+
targets.each do |path|
|
|
120
|
+
next unless File.file?(path)
|
|
121
|
+
|
|
122
|
+
warn " scanning #{path}" if verbose
|
|
123
|
+
problems.concat(scan(path))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if problems.empty?
|
|
128
|
+
puts "✓ lint-liquid-raw: #{targets.size} Markdown files, all " \
|
|
129
|
+
'`{% raw %}` blocks balanced'
|
|
130
|
+
exit 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
warn '✗ lint-liquid-raw: unbalanced Liquid raw blocks'
|
|
134
|
+
problems.each { |p| warn " #{p}" }
|
|
135
|
+
warn ''
|
|
136
|
+
warn 'To display a literal tag, escape it: `{% raw %}{% raw %}{% endraw %}`'
|
|
137
|
+
exit 1
|