jekyll-theme-zer0 1.15.0 → 1.16.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 +16 -0
- data/README.md +4 -4
- data/_data/features.yml +15 -0
- data/_data/theme-manifest.yml +428 -0
- data/_includes/components/ai-chat.html +579 -0
- data/_includes/core/footer.html +3 -1
- data/_layouts/root.html +3 -0
- data/_plugins/obsidian_links.rb +18 -5
- data/_sass/tokens/_layers.scss +1 -0
- data/scripts/bin/audit-consumer +336 -0
- data/scripts/bin/manifest +183 -0
- data/scripts/bin/release +15 -1
- data/scripts/bin/sync-plugins +356 -0
- data/scripts/bin/validate +1 -1
- data/scripts/lib/audit.sh +284 -0
- data/scripts/lib/frontmatter.sh +4 -2
- data/scripts/test/lib/test_locale_independence.sh +3 -0
- metadata +7 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# scripts/bin/sync-plugins — copy/update theme plugins into a remote_theme consumer repo
|
|
3
|
+
#
|
|
4
|
+
# When using remote_theme on GitHub Pages, Jekyll does not load plugins from
|
|
5
|
+
# the theme. Consumers must vendor required plugins locally. This command
|
|
6
|
+
# reads the theme manifest, compares SHAs, and installs or updates plugins
|
|
7
|
+
# in the consumer's _plugins/ directory.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ./scripts/bin/sync-plugins [OPTIONS]
|
|
11
|
+
#
|
|
12
|
+
# --consumer-path <dir> Target consumer repo root (default: cwd)
|
|
13
|
+
# --theme-path <dir> Source theme path (auto-resolved if omitted)
|
|
14
|
+
# --plugins {required|optional|all|<name>}
|
|
15
|
+
# Which plugins to sync (default: required)
|
|
16
|
+
# --dry-run Show what would happen without making changes
|
|
17
|
+
# --force Overwrite plugins listed in .theme-overrides.yml
|
|
18
|
+
# --format {text|github} Output format (default: text)
|
|
19
|
+
# --verbose Debug output
|
|
20
|
+
# --help, -h Show this help
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
LIB_DIR="$SCRIPT_DIR/../lib"
|
|
26
|
+
THEME_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
27
|
+
|
|
28
|
+
source "$LIB_DIR/common.sh"
|
|
29
|
+
source "$LIB_DIR/audit.sh"
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
show_usage() {
|
|
33
|
+
cat << 'EOF'
|
|
34
|
+
🔌 Plugin Sync for zer0-mistakes remote_theme consumers
|
|
35
|
+
|
|
36
|
+
USAGE:
|
|
37
|
+
./scripts/bin/sync-plugins [OPTIONS]
|
|
38
|
+
|
|
39
|
+
DESCRIPTION:
|
|
40
|
+
Copies theme plugins from zer0-mistakes into a remote_theme consumer
|
|
41
|
+
(e.g. it-journey). When using remote_theme on GitHub Pages, Jekyll does
|
|
42
|
+
not load plugins from the theme — consumers must vendor them locally.
|
|
43
|
+
|
|
44
|
+
This command reads the theme manifest to determine which plugins are
|
|
45
|
+
available, compares SHA-256 checksums, and copies new or outdated plugins
|
|
46
|
+
into the consumer's _plugins/ directory.
|
|
47
|
+
|
|
48
|
+
Sync state reported per plugin:
|
|
49
|
+
INSTALLED Plugin was missing — now copied from theme
|
|
50
|
+
UPDATED Plugin existed but content differed — updated to theme version
|
|
51
|
+
CURRENT Plugin already matches the theme version — no change needed
|
|
52
|
+
SKIPPED Plugin not in sync scope (e.g. optional when --plugins required)
|
|
53
|
+
CONFLICT Plugin differs AND is listed in .theme-overrides.yml
|
|
54
|
+
→ intentional custom version; use --force to overwrite
|
|
55
|
+
|
|
56
|
+
OPTIONS:
|
|
57
|
+
--consumer-path <dir> Consumer repo root (default: current directory)
|
|
58
|
+
--theme-path <dir> Source theme path (auto-resolved if omitted)
|
|
59
|
+
--plugins <scope> Which plugins to sync:
|
|
60
|
+
required Required plugins only (default)
|
|
61
|
+
optional Optional plugins only
|
|
62
|
+
all Both required and optional
|
|
63
|
+
<name> Specific plugin, e.g. obsidian_links.rb
|
|
64
|
+
--dry-run Show what would happen without making changes
|
|
65
|
+
--force Sync even if plugin is listed in .theme-overrides.yml
|
|
66
|
+
--format {text|github} Output format (default: text)
|
|
67
|
+
--verbose Debug output
|
|
68
|
+
--help, -h Show this help
|
|
69
|
+
|
|
70
|
+
EXAMPLES:
|
|
71
|
+
# Install required plugins into a sibling consumer repo
|
|
72
|
+
./scripts/bin/sync-plugins --consumer-path ../it-journey
|
|
73
|
+
|
|
74
|
+
# Preview all plugins that would be installed/updated
|
|
75
|
+
./scripts/bin/sync-plugins --consumer-path ../it-journey --plugins all --dry-run
|
|
76
|
+
|
|
77
|
+
# Sync only the Obsidian wiki-link plugin
|
|
78
|
+
./scripts/bin/sync-plugins --consumer-path ../it-journey --plugins obsidian_links.rb
|
|
79
|
+
|
|
80
|
+
# Update optional plugins too, forcing over any custom versions
|
|
81
|
+
./scripts/bin/sync-plugins --consumer-path ../it-journey --plugins all --force
|
|
82
|
+
|
|
83
|
+
# GitHub Actions annotation format
|
|
84
|
+
./scripts/bin/sync-plugins --consumer-path ../it-journey --format github
|
|
85
|
+
EOF
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Argument parsing
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
CONSUMER_PATH=""
|
|
92
|
+
THEME_PATH_OVERRIDE=""
|
|
93
|
+
PLUGINS_SCOPE="required"
|
|
94
|
+
DRY_RUN_FLAG=false
|
|
95
|
+
FORCE=false
|
|
96
|
+
FORMAT="text"
|
|
97
|
+
VERBOSE_FLAG=false
|
|
98
|
+
SHOW_HELP=false
|
|
99
|
+
|
|
100
|
+
while [[ $# -gt 0 ]]; do
|
|
101
|
+
case "$1" in
|
|
102
|
+
--consumer-path) shift; CONSUMER_PATH="$1" ;;
|
|
103
|
+
--theme-path) shift; THEME_PATH_OVERRIDE="$1" ;;
|
|
104
|
+
--plugins) shift; PLUGINS_SCOPE="$1" ;;
|
|
105
|
+
--dry-run) DRY_RUN_FLAG=true ;;
|
|
106
|
+
--force) FORCE=true ;;
|
|
107
|
+
--format) shift; FORMAT="$1" ;;
|
|
108
|
+
--verbose) VERBOSE_FLAG=true ;;
|
|
109
|
+
--help|-h) SHOW_HELP=true ;;
|
|
110
|
+
*) warn "Unknown option: $1" ;;
|
|
111
|
+
esac
|
|
112
|
+
shift
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
[[ "$SHOW_HELP" == "true" ]] && { show_usage; exit 0; }
|
|
116
|
+
export VERBOSE="$VERBOSE_FLAG"
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Resolve paths
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
CONSUMER_PATH="${CONSUMER_PATH:-$PWD}"
|
|
122
|
+
CONSUMER_PATH="$(cd "$CONSUMER_PATH" && pwd)"
|
|
123
|
+
|
|
124
|
+
if [[ -n "$THEME_PATH_OVERRIDE" ]]; then
|
|
125
|
+
RESOLVED_THEME="$(cd "$THEME_PATH_OVERRIDE" && pwd)"
|
|
126
|
+
else
|
|
127
|
+
RESOLVED_THEME="$THEME_ROOT"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
MANIFEST="$RESOLVED_THEME/_data/theme-manifest.yml"
|
|
131
|
+
if [[ ! -f "$MANIFEST" ]]; then
|
|
132
|
+
error "Manifest not found: $MANIFEST — run ./scripts/bin/manifest first"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
debug "Consumer: $CONSUMER_PATH"
|
|
136
|
+
debug "Theme: $RESOLVED_THEME"
|
|
137
|
+
debug "Manifest: $MANIFEST"
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Load override manifest (detects CONFLICT — consumer intentionally differs)
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
OVERRIDES_FILE="$CONSUMER_PATH/.theme-overrides.yml"
|
|
143
|
+
OVERRIDE_PATHS_TMP=$(mktemp)
|
|
144
|
+
trap 'rm -f "$OVERRIDE_PATHS_TMP"' EXIT
|
|
145
|
+
|
|
146
|
+
if [[ -f "$OVERRIDES_FILE" ]]; then
|
|
147
|
+
overrides_list "$OVERRIDES_FILE" > "$OVERRIDE_PATHS_TMP"
|
|
148
|
+
debug "Loaded overrides from $OVERRIDES_FILE"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Build the list of plugins to process based on --plugins scope
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
REQUIRED_PLUGINS=()
|
|
155
|
+
OPTIONAL_PLUGINS=()
|
|
156
|
+
|
|
157
|
+
while IFS= read -r line; do
|
|
158
|
+
[[ -n "$line" ]] && REQUIRED_PLUGINS+=("$line")
|
|
159
|
+
done < <(manifest_list "$MANIFEST" "required_plugin_paths")
|
|
160
|
+
|
|
161
|
+
while IFS= read -r line; do
|
|
162
|
+
[[ -n "$line" ]] && OPTIONAL_PLUGINS+=("$line")
|
|
163
|
+
done < <(manifest_list "$MANIFEST" "optional_plugin_paths")
|
|
164
|
+
|
|
165
|
+
# Legacy fallback: if manifest doesn't have the split keys, use plugin_paths as required
|
|
166
|
+
if [[ ${#REQUIRED_PLUGINS[@]} -eq 0 && ${#OPTIONAL_PLUGINS[@]} -eq 0 ]]; then
|
|
167
|
+
while IFS= read -r line; do
|
|
168
|
+
[[ -n "$line" ]] && REQUIRED_PLUGINS+=("$line")
|
|
169
|
+
done < <(manifest_list "$MANIFEST" "plugin_paths")
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Default hardcoded fallback if manifest has no plugin entries at all
|
|
173
|
+
if [[ ${#REQUIRED_PLUGINS[@]} -eq 0 && ${#OPTIONAL_PLUGINS[@]} -eq 0 ]]; then
|
|
174
|
+
REQUIRED_PLUGINS=(
|
|
175
|
+
"_plugins/preview_image_generator.rb"
|
|
176
|
+
"_plugins/obsidian_links.rb"
|
|
177
|
+
)
|
|
178
|
+
OPTIONAL_PLUGINS=(
|
|
179
|
+
"_plugins/admin_page_urls.rb"
|
|
180
|
+
"_plugins/content_statistics_generator.rb"
|
|
181
|
+
"_plugins/theme_version.rb"
|
|
182
|
+
)
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
TARGET_PLUGINS=()
|
|
186
|
+
SKIPPED_PLUGINS=()
|
|
187
|
+
|
|
188
|
+
case "$PLUGINS_SCOPE" in
|
|
189
|
+
required)
|
|
190
|
+
[[ ${#REQUIRED_PLUGINS[@]} -gt 0 ]] && TARGET_PLUGINS+=("${REQUIRED_PLUGINS[@]}")
|
|
191
|
+
[[ ${#OPTIONAL_PLUGINS[@]} -gt 0 ]] && SKIPPED_PLUGINS+=("${OPTIONAL_PLUGINS[@]}")
|
|
192
|
+
;;
|
|
193
|
+
optional)
|
|
194
|
+
[[ ${#OPTIONAL_PLUGINS[@]} -gt 0 ]] && TARGET_PLUGINS+=("${OPTIONAL_PLUGINS[@]}")
|
|
195
|
+
[[ ${#REQUIRED_PLUGINS[@]} -gt 0 ]] && SKIPPED_PLUGINS+=("${REQUIRED_PLUGINS[@]}")
|
|
196
|
+
;;
|
|
197
|
+
all)
|
|
198
|
+
[[ ${#REQUIRED_PLUGINS[@]} -gt 0 ]] && TARGET_PLUGINS+=("${REQUIRED_PLUGINS[@]}")
|
|
199
|
+
[[ ${#OPTIONAL_PLUGINS[@]} -gt 0 ]] && TARGET_PLUGINS+=("${OPTIONAL_PLUGINS[@]}")
|
|
200
|
+
;;
|
|
201
|
+
*)
|
|
202
|
+
# Specific plugin name — accept with or without path prefix
|
|
203
|
+
needle="$PLUGINS_SCOPE"
|
|
204
|
+
[[ "$needle" != _plugins/* ]] && needle="_plugins/$needle"
|
|
205
|
+
# Warn if not found in manifest
|
|
206
|
+
found=false
|
|
207
|
+
for p in "${REQUIRED_PLUGINS[@]:-}" "${OPTIONAL_PLUGINS[@]:-}"; do
|
|
208
|
+
if [[ "$p" == "$needle" || "$(basename "$p")" == "$(basename "$needle")" ]]; then
|
|
209
|
+
found=true
|
|
210
|
+
needle="$p" # use the manifest-canonical path
|
|
211
|
+
break
|
|
212
|
+
fi
|
|
213
|
+
done
|
|
214
|
+
[[ "$found" == "false" ]] && warn "Plugin '$PLUGINS_SCOPE' not in manifest — attempting anyway"
|
|
215
|
+
TARGET_PLUGINS=("$needle")
|
|
216
|
+
;;
|
|
217
|
+
esac
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Output helpers
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
print_result() {
|
|
223
|
+
local state="$1" # INSTALLED | UPDATED | CURRENT | SKIPPED | CONFLICT
|
|
224
|
+
local relpath="$2"
|
|
225
|
+
local note="${3:-}"
|
|
226
|
+
case "$state" in
|
|
227
|
+
INSTALLED)
|
|
228
|
+
if [[ "$FORMAT" == "github" ]]; then
|
|
229
|
+
echo "::notice file=$relpath::INSTALLED plugin from theme"
|
|
230
|
+
else
|
|
231
|
+
echo -e "${GREEN}[INSTALLED]${NC} $relpath${note:+ ($note)}"
|
|
232
|
+
fi ;;
|
|
233
|
+
UPDATED)
|
|
234
|
+
if [[ "$FORMAT" == "github" ]]; then
|
|
235
|
+
echo "::notice file=$relpath::UPDATED plugin from theme"
|
|
236
|
+
else
|
|
237
|
+
echo -e "${CYAN}[UPDATED]${NC} $relpath${note:+ ($note)}"
|
|
238
|
+
fi ;;
|
|
239
|
+
CURRENT)
|
|
240
|
+
if [[ "$FORMAT" == "github" ]]; then
|
|
241
|
+
echo "::debug file=$relpath::CURRENT"
|
|
242
|
+
else
|
|
243
|
+
echo -e "${GREEN}[CURRENT]${NC} $relpath"
|
|
244
|
+
fi ;;
|
|
245
|
+
SKIPPED)
|
|
246
|
+
if [[ "$FORMAT" == "github" ]]; then
|
|
247
|
+
echo "::debug file=$relpath::SKIPPED${note:+ ($note)}"
|
|
248
|
+
else
|
|
249
|
+
echo -e " [SKIPPED] $relpath${note:+ ($note)}"
|
|
250
|
+
fi ;;
|
|
251
|
+
CONFLICT)
|
|
252
|
+
if [[ "$FORMAT" == "github" ]]; then
|
|
253
|
+
echo "::warning file=$relpath::CONFLICT - listed in .theme-overrides.yml; use --force to overwrite"
|
|
254
|
+
else
|
|
255
|
+
echo -e "${YELLOW}[CONFLICT]${NC} $relpath (listed in .theme-overrides.yml — use --force to overwrite)"
|
|
256
|
+
fi ;;
|
|
257
|
+
esac
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Header
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
info "Theme plugin sync"
|
|
264
|
+
info " Consumer : $CONSUMER_PATH"
|
|
265
|
+
info " Theme : $RESOLVED_THEME"
|
|
266
|
+
info " Scope : $PLUGINS_SCOPE"
|
|
267
|
+
[[ "$DRY_RUN_FLAG" == "true" ]] && info " Mode : dry-run (no files written)"
|
|
268
|
+
[[ "$FORCE" == "true" ]] && info " Force : yes (overrides respected only by omission)"
|
|
269
|
+
echo ""
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# Sync loop
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
COUNT_INSTALLED=0
|
|
275
|
+
COUNT_UPDATED=0
|
|
276
|
+
COUNT_CURRENT=0
|
|
277
|
+
COUNT_CONFLICT=0
|
|
278
|
+
COUNT_SKIPPED=0
|
|
279
|
+
|
|
280
|
+
for plugin_rel in "${TARGET_PLUGINS[@]}"; do
|
|
281
|
+
theme_src="$RESOLVED_THEME/$plugin_rel"
|
|
282
|
+
consumer_dst="$CONSUMER_PATH/$plugin_rel"
|
|
283
|
+
|
|
284
|
+
if [[ ! -f "$theme_src" ]]; then
|
|
285
|
+
warn "Plugin not found in theme: $plugin_rel — skipping"
|
|
286
|
+
COUNT_SKIPPED=$((COUNT_SKIPPED + 1))
|
|
287
|
+
continue
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
theme_sha=$(sha256_file "$theme_src")
|
|
291
|
+
|
|
292
|
+
if [[ ! -f "$consumer_dst" ]]; then
|
|
293
|
+
# ── INSTALL ──────────────────────────────────────────────────────────
|
|
294
|
+
if [[ "$DRY_RUN_FLAG" == "true" ]]; then
|
|
295
|
+
print_result "INSTALLED" "$plugin_rel" "dry-run"
|
|
296
|
+
else
|
|
297
|
+
mkdir -p "$(dirname "$consumer_dst")"
|
|
298
|
+
cp "$theme_src" "$consumer_dst"
|
|
299
|
+
print_result "INSTALLED" "$plugin_rel"
|
|
300
|
+
fi
|
|
301
|
+
COUNT_INSTALLED=$((COUNT_INSTALLED + 1))
|
|
302
|
+
else
|
|
303
|
+
consumer_sha=$(sha256_file "$consumer_dst")
|
|
304
|
+
|
|
305
|
+
if [[ "$theme_sha" == "$consumer_sha" ]]; then
|
|
306
|
+
# ── CURRENT ──────────────────────────────────────────────────────
|
|
307
|
+
print_result "CURRENT" "$plugin_rel"
|
|
308
|
+
COUNT_CURRENT=$((COUNT_CURRENT + 1))
|
|
309
|
+
else
|
|
310
|
+
# Content differs — check for intentional override
|
|
311
|
+
if grep -qxF "$plugin_rel" "$OVERRIDE_PATHS_TMP" 2>/dev/null \
|
|
312
|
+
&& [[ "$FORCE" == "false" ]]; then
|
|
313
|
+
# ── CONFLICT ─────────────────────────────────────────────────
|
|
314
|
+
print_result "CONFLICT" "$plugin_rel"
|
|
315
|
+
COUNT_CONFLICT=$((COUNT_CONFLICT + 1))
|
|
316
|
+
else
|
|
317
|
+
# ── UPDATE ───────────────────────────────────────────────────
|
|
318
|
+
if [[ "$DRY_RUN_FLAG" == "true" ]]; then
|
|
319
|
+
print_result "UPDATED" "$plugin_rel" "dry-run"
|
|
320
|
+
else
|
|
321
|
+
cp "$theme_src" "$consumer_dst"
|
|
322
|
+
print_result "UPDATED" "$plugin_rel"
|
|
323
|
+
fi
|
|
324
|
+
COUNT_UPDATED=$((COUNT_UPDATED + 1))
|
|
325
|
+
fi
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
# Show skipped scope plugins at verbose level
|
|
331
|
+
if [[ "$VERBOSE_FLAG" == "true" && ${#SKIPPED_PLUGINS[@]} -gt 0 ]]; then
|
|
332
|
+
for plugin_rel in "${SKIPPED_PLUGINS[@]}"; do
|
|
333
|
+
print_result "SKIPPED" "$plugin_rel" "out of scope (--plugins $PLUGINS_SCOPE)"
|
|
334
|
+
done
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# Summary
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
echo ""
|
|
341
|
+
info "--- Summary ---"
|
|
342
|
+
[[ "$COUNT_INSTALLED" -gt 0 ]] && info "Installed: $COUNT_INSTALLED"
|
|
343
|
+
[[ "$COUNT_UPDATED" -gt 0 ]] && info "Updated: $COUNT_UPDATED"
|
|
344
|
+
[[ "$COUNT_CURRENT" -gt 0 ]] && info "Current: $COUNT_CURRENT"
|
|
345
|
+
[[ "$COUNT_CONFLICT" -gt 0 ]] && warn "Conflicts: $COUNT_CONFLICT (use --force to overwrite)"
|
|
346
|
+
[[ "$COUNT_SKIPPED" -gt 0 ]] && info "Skipped: $COUNT_SKIPPED"
|
|
347
|
+
|
|
348
|
+
TOTAL_CHANGES=$((COUNT_INSTALLED + COUNT_UPDATED))
|
|
349
|
+
echo ""
|
|
350
|
+
if [[ "$DRY_RUN_FLAG" == "true" && "$TOTAL_CHANGES" -gt 0 ]]; then
|
|
351
|
+
info "$TOTAL_CHANGES plugin(s) would be synced (dry-run — re-run without --dry-run to apply)"
|
|
352
|
+
elif [[ "$TOTAL_CHANGES" -gt 0 ]]; then
|
|
353
|
+
success "$TOTAL_CHANGES plugin(s) synced to $CONSUMER_PATH/_plugins/"
|
|
354
|
+
else
|
|
355
|
+
success "All in-scope plugins are current — nothing to do"
|
|
356
|
+
fi
|
data/scripts/bin/validate
CHANGED
|
@@ -380,7 +380,7 @@ if File.file?('_config_secrets_local.yml')
|
|
|
380
380
|
assert(!tracked, '_config_secrets_local.yml must remain untracked and ignored')
|
|
381
381
|
end
|
|
382
382
|
|
|
383
|
-
ignored_dirs = %w[.git _site node_modules vendor .jekyll-cache .sass-cache]
|
|
383
|
+
ignored_dirs = %w[.git _site node_modules vendor .jekyll-cache .sass-cache test/fixtures]
|
|
384
384
|
config_like_files = []
|
|
385
385
|
|
|
386
386
|
Find.find('.') do |path|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# audit.sh — shared helpers for theme-consumer file auditing
|
|
3
|
+
# Source this from scripts/bin/audit-consumer and scripts/bin/manifest.
|
|
4
|
+
# Requires: common.sh to already be sourced.
|
|
5
|
+
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# SHA-256 helpers (cross-platform: macOS sha256sum vs GNU sha256sum)
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
sha256_file() {
|
|
10
|
+
local file="$1"
|
|
11
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
12
|
+
sha256sum "$file" | awk '{print $1}'
|
|
13
|
+
else
|
|
14
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
15
|
+
fi
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Manifest parsing — reads _data/theme-manifest.yml via awk (no yq needed)
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Returns the value of a top-level scalar key in a YAML file.
|
|
22
|
+
manifest_scalar() {
|
|
23
|
+
local manifest_file="$1"
|
|
24
|
+
local key="$2"
|
|
25
|
+
awk -v k="$key" '
|
|
26
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*:/ {
|
|
27
|
+
match($0, /^([a-zA-Z_][a-zA-Z0-9_]*): *(.*)/, arr)
|
|
28
|
+
if (arr[1] == k) { gsub(/^[[:space:]"'"'"']+|[[:space:]"'"'"']+$/, "", arr[2]); print arr[2]; exit }
|
|
29
|
+
}
|
|
30
|
+
' "$manifest_file"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Returns each item in a top-level YAML list (- item entries).
|
|
34
|
+
manifest_list() {
|
|
35
|
+
local manifest_file="$1"
|
|
36
|
+
local key="$2"
|
|
37
|
+
awk -v k="$key" '
|
|
38
|
+
$0 ~ "^" k ":[ ]*$" { in_block=1; next }
|
|
39
|
+
in_block && /^[[:space:]]*- / {
|
|
40
|
+
line=$0; gsub(/^[[:space:]]*- |[[:space:]]*$/, "", line)
|
|
41
|
+
# strip optional inline comment
|
|
42
|
+
sub(/ *#.*$/, "", line)
|
|
43
|
+
if (line != "") print line
|
|
44
|
+
}
|
|
45
|
+
in_block && /^[a-zA-Z_]/ { in_block=0 }
|
|
46
|
+
' "$manifest_file"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Read the per-file checksum section from the manifest.
|
|
50
|
+
# Emits lines of: <relpath> <sha256>
|
|
51
|
+
manifest_checksums() {
|
|
52
|
+
local manifest_file="$1"
|
|
53
|
+
awk '
|
|
54
|
+
/^file_checksums:/ { in_block=1; next }
|
|
55
|
+
in_block && /^ [^ ]/ {
|
|
56
|
+
# lines look like: _layouts/default.html: "abc123..."
|
|
57
|
+
line=$0; gsub(/^[[:space:]]+/, "", line)
|
|
58
|
+
split(line, arr, ": ")
|
|
59
|
+
path=arr[1]; sha=arr[2]
|
|
60
|
+
gsub(/"/, "", sha); gsub(/[[:space:]]/, "", sha)
|
|
61
|
+
if (path != "" && sha != "") print path " " sha
|
|
62
|
+
}
|
|
63
|
+
in_block && /^[a-zA-Z_]/ { in_block=0 }
|
|
64
|
+
' "$manifest_file"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Override manifest parsing (.theme-overrides.yml in consumer root)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Returns each justified override path (under overrides: list).
|
|
71
|
+
overrides_list() {
|
|
72
|
+
local overrides_file="$1"
|
|
73
|
+
awk '
|
|
74
|
+
/^overrides:/ { in_block=1; next }
|
|
75
|
+
in_block && /^ - path: / {
|
|
76
|
+
path=$0; gsub(/^.*path: */, "", path); gsub(/[[:space:]]*$/, "", path)
|
|
77
|
+
print path
|
|
78
|
+
}
|
|
79
|
+
in_block && /^[a-zA-Z_]/ { in_block=0 }
|
|
80
|
+
' "$overrides_file" | tr -d '"'"'"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Classification logic
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# classify_file <theme_path> <consumer_path> <relpath> <override_paths_file>
|
|
87
|
+
# Emits one of: IDENTICAL DIFFERS_JUSTIFIED DIFFERS_UNJUSTIFIED UNIQUE MISSING_LOCALLY
|
|
88
|
+
classify_file() {
|
|
89
|
+
local theme_root="$1"
|
|
90
|
+
local consumer_root="$2"
|
|
91
|
+
local relpath="$3"
|
|
92
|
+
local override_paths_file="$4" # temp file listing justified paths (one per line)
|
|
93
|
+
|
|
94
|
+
local theme_file="$theme_root/$relpath"
|
|
95
|
+
local consumer_file="$consumer_root/$relpath"
|
|
96
|
+
|
|
97
|
+
# File only in consumer (theme doesn't have it → unique to consumer)
|
|
98
|
+
if [[ ! -f "$theme_file" ]]; then
|
|
99
|
+
echo "UNIQUE"
|
|
100
|
+
return
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# File missing in consumer
|
|
104
|
+
if [[ ! -f "$consumer_file" ]]; then
|
|
105
|
+
echo "MISSING_LOCALLY"
|
|
106
|
+
return
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Compare content
|
|
110
|
+
local theme_sha consumer_sha
|
|
111
|
+
theme_sha=$(sha256_file "$theme_file")
|
|
112
|
+
consumer_sha=$(sha256_file "$consumer_file")
|
|
113
|
+
|
|
114
|
+
if [[ "$theme_sha" == "$consumer_sha" ]]; then
|
|
115
|
+
echo "IDENTICAL"
|
|
116
|
+
return
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Files differ — check if justified
|
|
120
|
+
if grep -qxF "$relpath" "$override_paths_file" 2>/dev/null; then
|
|
121
|
+
echo "DIFFERS_JUSTIFIED"
|
|
122
|
+
else
|
|
123
|
+
echo "DIFFERS_UNJUSTIFIED"
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# classify_plugin <plugin_relpath> <consumer_root> <mode>
|
|
128
|
+
# mode: gem | remote_theme
|
|
129
|
+
# Emits: MISSING_PLUGIN STALE_PLUGIN OK NOT_REQUIRED
|
|
130
|
+
classify_plugin() {
|
|
131
|
+
local plugin_relpath="$1"
|
|
132
|
+
local consumer_root="$2"
|
|
133
|
+
local mode="$3"
|
|
134
|
+
local theme_root="$4"
|
|
135
|
+
|
|
136
|
+
local consumer_plugin="$consumer_root/$plugin_relpath"
|
|
137
|
+
|
|
138
|
+
if [[ "$mode" != "remote_theme" ]]; then
|
|
139
|
+
# Gem mode: theme loads plugins itself — consumer doesn't need a copy
|
|
140
|
+
echo "NOT_REQUIRED"
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if [[ ! -f "$consumer_plugin" ]]; then
|
|
145
|
+
echo "MISSING_PLUGIN"
|
|
146
|
+
return
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# Check staleness
|
|
150
|
+
local theme_plugin="$theme_root/$plugin_relpath"
|
|
151
|
+
if [[ -f "$theme_plugin" ]]; then
|
|
152
|
+
local theme_sha consumer_sha
|
|
153
|
+
theme_sha=$(sha256_file "$theme_plugin")
|
|
154
|
+
consumer_sha=$(sha256_file "$consumer_plugin")
|
|
155
|
+
if [[ "$theme_sha" != "$consumer_sha" ]]; then
|
|
156
|
+
echo "STALE_PLUGIN"
|
|
157
|
+
return
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
echo "OK"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Theme-path auto-resolution
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
resolve_theme_path() {
|
|
168
|
+
local mode="$1"
|
|
169
|
+
local consumer_root="$2"
|
|
170
|
+
local sibling_theme_root="${3:-}" # optional explicit path
|
|
171
|
+
|
|
172
|
+
if [[ -n "$sibling_theme_root" ]]; then
|
|
173
|
+
echo "$sibling_theme_root"
|
|
174
|
+
return
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
if [[ "$mode" == "gem" ]]; then
|
|
178
|
+
local gem_path
|
|
179
|
+
gem_path=$(cd "$consumer_root" && bundle show jekyll-theme-zer0 2>/dev/null || true)
|
|
180
|
+
if [[ -n "$gem_path" && -d "$gem_path" ]]; then
|
|
181
|
+
echo "$gem_path"
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# remote_theme mode or gem resolution failed: check work/theme-cache/
|
|
187
|
+
local cache_dir="$consumer_root/work/theme-cache/zer0-mistakes"
|
|
188
|
+
if [[ -d "$cache_dir" ]]; then
|
|
189
|
+
echo "$cache_dir"
|
|
190
|
+
return
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
echo ""
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Detect consumer mode from _config.yml
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
detect_consumer_mode() {
|
|
200
|
+
local consumer_root="$1"
|
|
201
|
+
local config="$consumer_root/_config.yml"
|
|
202
|
+
|
|
203
|
+
if [[ ! -f "$config" ]]; then
|
|
204
|
+
echo "unknown"
|
|
205
|
+
return
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if grep -qE '^remote_theme:' "$config"; then
|
|
209
|
+
echo "remote_theme"
|
|
210
|
+
else
|
|
211
|
+
echo "gem"
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Output helpers
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
print_classification() {
|
|
219
|
+
local classification="$1"
|
|
220
|
+
local relpath="$2"
|
|
221
|
+
local format="${3:-text}"
|
|
222
|
+
|
|
223
|
+
case "$classification" in
|
|
224
|
+
IDENTICAL)
|
|
225
|
+
local color="${GREEN}"
|
|
226
|
+
local icon="✓"
|
|
227
|
+
;;
|
|
228
|
+
DIFFERS_JUSTIFIED)
|
|
229
|
+
local color="${CYAN}"
|
|
230
|
+
local icon="~"
|
|
231
|
+
;;
|
|
232
|
+
DIFFERS_UNJUSTIFIED)
|
|
233
|
+
local color="${RED}"
|
|
234
|
+
local icon="✗"
|
|
235
|
+
;;
|
|
236
|
+
UNIQUE)
|
|
237
|
+
local color="${BLUE}"
|
|
238
|
+
local icon="+"
|
|
239
|
+
;;
|
|
240
|
+
MISSING_LOCALLY)
|
|
241
|
+
local color="${YELLOW}"
|
|
242
|
+
local icon="?"
|
|
243
|
+
;;
|
|
244
|
+
MISSING_PLUGIN)
|
|
245
|
+
local color="${RED}"
|
|
246
|
+
local icon="!"
|
|
247
|
+
;;
|
|
248
|
+
STALE_PLUGIN)
|
|
249
|
+
local color="${YELLOW}"
|
|
250
|
+
local icon="~"
|
|
251
|
+
;;
|
|
252
|
+
OK)
|
|
253
|
+
local color="${GREEN}"
|
|
254
|
+
local icon="✓"
|
|
255
|
+
;;
|
|
256
|
+
NOT_REQUIRED)
|
|
257
|
+
local color="${BLUE}"
|
|
258
|
+
local icon="-"
|
|
259
|
+
;;
|
|
260
|
+
OPTIONAL_PLUGIN)
|
|
261
|
+
local color="${CYAN}"
|
|
262
|
+
local icon="?"
|
|
263
|
+
;;
|
|
264
|
+
*)
|
|
265
|
+
local color="${NC}"
|
|
266
|
+
local icon="?"
|
|
267
|
+
;;
|
|
268
|
+
esac
|
|
269
|
+
|
|
270
|
+
if [[ "$format" == "github" ]]; then
|
|
271
|
+
case "$classification" in
|
|
272
|
+
DIFFERS_UNJUSTIFIED|MISSING_PLUGIN)
|
|
273
|
+
echo "::warning file=$relpath::$classification" ;;
|
|
274
|
+
STALE_PLUGIN)
|
|
275
|
+
echo "::notice file=$relpath::STALE_PLUGIN" ;;
|
|
276
|
+
OPTIONAL_PLUGIN)
|
|
277
|
+
echo "::notice file=$relpath::OPTIONAL_PLUGIN (not required)" ;;
|
|
278
|
+
esac
|
|
279
|
+
elif [[ "$format" == "json" ]]; then
|
|
280
|
+
echo " {\"path\":\"$relpath\",\"status\":\"$classification\"},"
|
|
281
|
+
else
|
|
282
|
+
echo -e "${color}[$classification]${NC} $relpath"
|
|
283
|
+
fi
|
|
284
|
+
}
|
data/scripts/lib/frontmatter.sh
CHANGED
|
@@ -437,8 +437,10 @@ parse_file_frontmatter_all() {
|
|
|
437
437
|
|
|
438
438
|
ruby -ryaml -rdate -e '
|
|
439
439
|
path = ARGV[0]
|
|
440
|
-
# Extract frontmatter block (between first pair of --- on their own lines)
|
|
441
|
-
|
|
440
|
+
# Extract frontmatter block (between first pair of --- on their own lines).
|
|
441
|
+
# Explicit UTF-8: default external encoding is US-ASCII without a UTF-8
|
|
442
|
+
# locale, and posts contain multibyte characters (T-015 bug class).
|
|
443
|
+
content = File.read(path, encoding: "UTF-8")
|
|
442
444
|
unless content =~ /\A---\s*\n(.*?)\n---\s*$/m
|
|
443
445
|
puts "__NO_FRONTMATTER__"
|
|
444
446
|
exit 0
|
|
@@ -22,6 +22,9 @@ assert_true "(cd '$REPO_ROOT' && LC_ALL=C LANG=C ruby scripts/generate-roadmap.r
|
|
|
22
22
|
assert_true "(cd '$REPO_ROOT' && LC_ALL=C LANG=C ruby scripts/sync-backlog.rb --check >/dev/null 2>&1)" \
|
|
23
23
|
"sync-backlog.rb --check survives a C locale"
|
|
24
24
|
|
|
25
|
+
assert_true "(cd '$REPO_ROOT' && LC_ALL=C LANG=C ./scripts/lint-pages --strict >/dev/null 2>&1)" \
|
|
26
|
+
"lint-pages --strict survives a C locale"
|
|
27
|
+
|
|
25
28
|
# validate --quick covers the package.json read in scripts/bin/validate
|
|
26
29
|
assert_true "(cd '$REPO_ROOT' && LC_ALL=C LANG=C ./scripts/bin/validate --quick >/dev/null 2>&1)" \
|
|
27
30
|
"validate --quick survives a C locale"
|