jekyll-theme-zer0 1.8.2 → 1.9.1
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 +33 -3
- data/README.md +98 -7
- data/_data/content_statistics.yml +253 -251
- data/_includes/components/nav-export.html +61 -0
- data/_includes/components/nav-overview.html +54 -0
- data/scripts/bin/install +52 -705
- data/scripts/github-setup.sh +0 -0
- data/scripts/install/README.md +162 -0
- data/scripts/install/ai/client.sh +164 -0
- data/scripts/install/ai/diagnose.sh +81 -0
- data/scripts/install/ai/prompts/diagnose.system.md +42 -0
- data/scripts/install/ai/prompts/spec.schema.json +129 -0
- data/scripts/install/ai/prompts/suggest.system.md +43 -0
- data/scripts/install/ai/prompts/wizard.system.md +142 -0
- data/scripts/install/ai/suggest.sh +57 -0
- data/scripts/install/ai/wizard.sh +150 -0
- data/scripts/install/apply.sh +156 -0
- data/scripts/install/cli.sh +561 -0
- data/scripts/install/diff.sh +128 -0
- data/scripts/install/doctor.sh +168 -0
- data/scripts/install/fs.sh +138 -0
- data/scripts/install/log.sh +119 -0
- data/scripts/install/plan.sh +299 -0
- data/scripts/install/platform.sh +122 -0
- data/scripts/install/prompt.sh +124 -0
- data/scripts/install/repair.sh +45 -0
- data/scripts/install/scrape.sh +535 -0
- data/scripts/install/scrape_html.py +764 -0
- data/scripts/install/spec.sh +486 -0
- data/scripts/install/tasks/_registry.sh +65 -0
- data/scripts/install/tasks/agents.sh +60 -0
- data/scripts/install/tasks/config.sh +37 -0
- data/scripts/install/tasks/data.sh +18 -0
- data/scripts/install/tasks/deploy_azure-swa.sh +17 -0
- data/scripts/install/tasks/deploy_docker-prod.sh +21 -0
- data/scripts/install/tasks/deploy_github-pages.sh +18 -0
- data/scripts/install/tasks/devcontainer.sh +26 -0
- data/scripts/install/tasks/docker.sh +29 -0
- data/scripts/install/tasks/gemfile.sh +42 -0
- data/scripts/install/tasks/gitignore.sh +26 -0
- data/scripts/install/tasks/marker.sh +46 -0
- data/scripts/install/tasks/nav.sh +18 -0
- data/scripts/install/tasks/pages.sh +61 -0
- data/scripts/install/tasks/readme.sh +27 -0
- data/scripts/install/tasks/scrape.sh +348 -0
- data/scripts/install/template.sh +138 -0
- data/scripts/install/tui.sh +110 -0
- data/scripts/install/upgrade.sh +49 -0
- data/scripts/lib/install/template.sh +1 -0
- metadata +49 -6
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/tasks/scrape.sh — Generate Jekyll content from a scraped site
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Reads SPEC_SCRAPE_SOURCE_URL (+ depth/max_pages) from the spec, runs the
|
|
6
|
+
# crawler, then materialises the rendered Jekyll content under the target:
|
|
7
|
+
#
|
|
8
|
+
# - Home page → ${target}/index.md (permalink: /)
|
|
9
|
+
# - Events → ${target}/pages/events/<slug>.md
|
|
10
|
+
# - Posts → ${target}/pages/news/<slug>.md
|
|
11
|
+
# - Everything → ${target}/pages/<slug>.md
|
|
12
|
+
# - Site nav → ${target}/_data/navigation/main.yml (top-level YAML array)
|
|
13
|
+
# - Site data → ${target}/_data/scraped_site.json
|
|
14
|
+
# - Assets → ${target}/assets/scraped/ (images downloaded locally)
|
|
15
|
+
# - Config → ${target}/_config.yml seeded with title/desc/lang/logo
|
|
16
|
+
#
|
|
17
|
+
# Behaviour:
|
|
18
|
+
# - Skipped when SPEC_SCRAPE_SOURCE_URL is empty.
|
|
19
|
+
# - Existing files are preserved unless --force is in effect.
|
|
20
|
+
# - Existing _data/navigation/main.yml is backed up to main.yml.bak on first
|
|
21
|
+
# overwrite (so theme defaults are recoverable).
|
|
22
|
+
# - Honors _FS_DRY_RUN.
|
|
23
|
+
#
|
|
24
|
+
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
25
|
+
# =============================================================================
|
|
26
|
+
[[ -n "${_HAS_TASK_SCRAPE:-}" ]] && return 0
|
|
27
|
+
_HAS_TASK_SCRAPE=1
|
|
28
|
+
|
|
29
|
+
# Source the crawler module (idempotent).
|
|
30
|
+
_TASK_SCRAPE_DIR="${_TASK_SCRAPE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)}"
|
|
31
|
+
# shellcheck source=/dev/null
|
|
32
|
+
[[ -f "${_TASK_SCRAPE_DIR}/scrape.sh" ]] && source "${_TASK_SCRAPE_DIR}/scrape.sh"
|
|
33
|
+
|
|
34
|
+
task_scrape_run() {
|
|
35
|
+
local target="$1"
|
|
36
|
+
local src_url="${SPEC_SCRAPE_SOURCE_URL:-}"
|
|
37
|
+
|
|
38
|
+
if [[ -z "$src_url" ]]; then
|
|
39
|
+
log_debug "scrape task: no scrape.source_url in spec — skipping"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
local depth="${SPEC_SCRAPE_DEPTH:-2}"
|
|
44
|
+
local max_pages="${SPEC_SCRAPE_MAX_PAGES:-25}"
|
|
45
|
+
local include_nav="${SPEC_SCRAPE_INCLUDE_NAV:-true}"
|
|
46
|
+
local scrape_dir="${SPEC_SCRAPE_OUT_DIR:-${target}/.zer0/scrape}"
|
|
47
|
+
|
|
48
|
+
log_info "scrape: $src_url (depth=$depth, max=$max_pages)"
|
|
49
|
+
|
|
50
|
+
if [[ "${_FS_DRY_RUN:-0}" == "1" ]]; then
|
|
51
|
+
log_warning "DRY RUN — scrape would fetch $src_url → $scrape_dir"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
mkdir -p "$scrape_dir"
|
|
56
|
+
if ! scrape_run "$src_url" "$scrape_dir" "$depth" "$max_pages"; then
|
|
57
|
+
log_error "scrape: crawl failed for $src_url"
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
local site_json="$scrape_dir/site.json"
|
|
62
|
+
local pages_dir="$scrape_dir/pages"
|
|
63
|
+
local jekyll_dir="$scrape_dir/jekyll"
|
|
64
|
+
|
|
65
|
+
if [[ ! -d "$jekyll_dir" ]]; then
|
|
66
|
+
log_warning "scrape: no jekyll output produced (skipping content copy)"
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# --- Distribute pages by kind ----------------------------------------
|
|
71
|
+
_task_scrape_distribute_pages "$target" "$scrape_dir" || return 1
|
|
72
|
+
|
|
73
|
+
# --- Copy downloaded assets ------------------------------------------
|
|
74
|
+
_task_scrape_copy_assets "$target" "$scrape_dir"
|
|
75
|
+
|
|
76
|
+
# --- Publish site metadata as a Jekyll data file ---------------------
|
|
77
|
+
if [[ -f "$site_json" ]]; then
|
|
78
|
+
mkdir -p "${target}/_data"
|
|
79
|
+
cp "$site_json" "${target}/_data/scraped_site.json"
|
|
80
|
+
log_debug " wrote _data/scraped_site.json"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# --- Wire navigation to the file the theme actually reads ------------
|
|
84
|
+
if [[ "$include_nav" == "true" && -f "$site_json" ]]; then
|
|
85
|
+
_task_scrape_write_nav "$target" "$site_json" "$src_url"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# --- Seed _config.yml from scraped site metadata ---------------------
|
|
89
|
+
_task_scrape_seed_config "$target" "$site_json"
|
|
90
|
+
|
|
91
|
+
log_success "scrape: content imported from $src_url"
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Distribute rendered Jekyll markdown into kind-based directories.
|
|
97
|
+
# Routes each scrape_dir/jekyll/<slug>.md based on the matching
|
|
98
|
+
# scrape_dir/pages/<slug>.json `kind` field.
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
_task_scrape_distribute_pages() {
|
|
101
|
+
local target="$1" scrape_dir="$2"
|
|
102
|
+
local jekyll_dir="$scrape_dir/jekyll"
|
|
103
|
+
local pages_dir="$scrape_dir/pages"
|
|
104
|
+
local copied=0 skipped=0
|
|
105
|
+
local f base slug kind dest
|
|
106
|
+
|
|
107
|
+
for f in "$jekyll_dir"/*.md; do
|
|
108
|
+
[[ -f "$f" ]] || continue
|
|
109
|
+
base=$(basename "$f")
|
|
110
|
+
slug="${base%.md}"
|
|
111
|
+
|
|
112
|
+
# Look up kind from the per-page JSON; default to "page".
|
|
113
|
+
kind="page"
|
|
114
|
+
if [[ -f "$pages_dir/$slug.json" ]]; then
|
|
115
|
+
kind=$(python3 -c '
|
|
116
|
+
import json, sys
|
|
117
|
+
try:
|
|
118
|
+
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
119
|
+
print((d.get("kind") or "page").strip() or "page")
|
|
120
|
+
except Exception:
|
|
121
|
+
print("page")
|
|
122
|
+
' "$pages_dir/$slug.json" 2>/dev/null)
|
|
123
|
+
[[ -z "$kind" ]] && kind="page"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
case "$kind" in
|
|
127
|
+
home)
|
|
128
|
+
dest="${target}/index.md"
|
|
129
|
+
;;
|
|
130
|
+
event)
|
|
131
|
+
dest="${target}/pages/events/${slug}.md"
|
|
132
|
+
;;
|
|
133
|
+
post)
|
|
134
|
+
dest="${target}/pages/news/${slug}.md"
|
|
135
|
+
;;
|
|
136
|
+
*)
|
|
137
|
+
dest="${target}/pages/${slug}.md"
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
|
|
141
|
+
# Home always wins — overwrite the installer's placeholder index.
|
|
142
|
+
if [[ "$kind" != "home" && -f "$dest" && "${_FS_FORCE:-0}" != "1" ]]; then
|
|
143
|
+
log_debug " skip (exists): ${dest#${target}/}"
|
|
144
|
+
skipped=$((skipped + 1))
|
|
145
|
+
continue
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
mkdir -p "$(dirname "$dest")"
|
|
149
|
+
if [[ "$(type -t fs_copy)" == "function" && "$kind" != "home" ]]; then
|
|
150
|
+
fs_copy "$f" "$dest"
|
|
151
|
+
else
|
|
152
|
+
cp "$f" "$dest"
|
|
153
|
+
fi
|
|
154
|
+
copied=$((copied + 1))
|
|
155
|
+
log_debug " $kind → ${dest#${target}/}"
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
log_info "scrape: distributed ${copied} page(s) (skipped ${skipped})"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Copy any downloaded images from scrape_dir/assets/ → target/assets/scraped/.
|
|
163
|
+
# Markdown already references /assets/scraped/<file> (set during scrape).
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
_task_scrape_copy_assets() {
|
|
166
|
+
local target="$1" scrape_dir="$2"
|
|
167
|
+
local src="$scrape_dir/assets"
|
|
168
|
+
local dst="${target}/assets/scraped"
|
|
169
|
+
[[ -d "$src" ]] || return 0
|
|
170
|
+
mkdir -p "$dst"
|
|
171
|
+
local count=0 f
|
|
172
|
+
for f in "$src"/*; do
|
|
173
|
+
[[ -f "$f" ]] || continue
|
|
174
|
+
cp "$f" "$dst/" 2>/dev/null && count=$((count + 1))
|
|
175
|
+
done
|
|
176
|
+
[[ $count -gt 0 ]] && log_info "scrape: copied $count asset(s) → assets/scraped/"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Write the scraped navigation into the file the theme actually reads:
|
|
181
|
+
# _data/navigation/main.yml — top-level YAML array (NOT under a `main:` key).
|
|
182
|
+
# Backs up any existing file once to main.yml.bak.
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
_task_scrape_write_nav() {
|
|
185
|
+
local target="$1" site_json="$2" src_url="$3"
|
|
186
|
+
local nav_yml="${target}/_data/navigation/main.yml"
|
|
187
|
+
mkdir -p "$(dirname "$nav_yml")"
|
|
188
|
+
|
|
189
|
+
# Back up the theme default the first time we overwrite it.
|
|
190
|
+
if [[ -f "$nav_yml" && ! -f "${nav_yml}.bak" ]]; then
|
|
191
|
+
cp "$nav_yml" "${nav_yml}.bak"
|
|
192
|
+
log_debug " backed up existing main.yml → main.yml.bak"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
SITE_JSON="$site_json" OUT="$nav_yml" SRC="$src_url" python3 <<'PY'
|
|
196
|
+
import json, os
|
|
197
|
+
from urllib.parse import urlparse
|
|
198
|
+
|
|
199
|
+
src_url = os.environ["SRC"]
|
|
200
|
+
with open(os.environ["SITE_JSON"], "r", encoding="utf-8") as f:
|
|
201
|
+
d = json.load(f)
|
|
202
|
+
|
|
203
|
+
nav = d.get("nav") or []
|
|
204
|
+
base_host = urlparse(src_url).netloc
|
|
205
|
+
|
|
206
|
+
# Map scraped pages by URL path so we can pick reasonable icons by kind.
|
|
207
|
+
pages = d.get("pages") or []
|
|
208
|
+
kind_by_path = {}
|
|
209
|
+
for p in pages:
|
|
210
|
+
u = (p.get("url") or "").strip()
|
|
211
|
+
if not u: continue
|
|
212
|
+
path = (urlparse(u).path or "/").rstrip("/") or "/"
|
|
213
|
+
kind_by_path[path] = p.get("kind") or "page"
|
|
214
|
+
|
|
215
|
+
ICONS = {
|
|
216
|
+
"home": "bi-house-door",
|
|
217
|
+
"event": "bi-calendar-event",
|
|
218
|
+
"post": "bi-newspaper",
|
|
219
|
+
"about": "bi-info-circle",
|
|
220
|
+
"contact": "bi-envelope",
|
|
221
|
+
"service": "bi-gear",
|
|
222
|
+
"faq": "bi-question-circle",
|
|
223
|
+
"page": "bi-file-earmark-text",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
out = []
|
|
227
|
+
seen_paths = set()
|
|
228
|
+
|
|
229
|
+
# Always lead with Home.
|
|
230
|
+
out.append({"title": "Home", "icon": "bi-house-door", "url": "/"})
|
|
231
|
+
seen_paths.add("/")
|
|
232
|
+
|
|
233
|
+
for item in nav:
|
|
234
|
+
label = (item.get("label") or "").strip()
|
|
235
|
+
url = (item.get("url") or "").strip()
|
|
236
|
+
if not label or not url: continue
|
|
237
|
+
p = urlparse(url)
|
|
238
|
+
if p.netloc and p.netloc != base_host: continue
|
|
239
|
+
path = (p.path or "/").rstrip("/") or "/"
|
|
240
|
+
if path in seen_paths: continue
|
|
241
|
+
seen_paths.add(path)
|
|
242
|
+
kind = kind_by_path.get(path, "page")
|
|
243
|
+
out.append({
|
|
244
|
+
"title": label,
|
|
245
|
+
"icon": ICONS.get(kind, "bi-file-earmark-text"),
|
|
246
|
+
"url": path if path == "/" else (path + "/"),
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
# If the source nav was empty/blocked, fall back to top scraped pages.
|
|
250
|
+
if len(out) <= 1:
|
|
251
|
+
for p in pages[:6]:
|
|
252
|
+
url = (p.get("url") or "").strip()
|
|
253
|
+
if not url: continue
|
|
254
|
+
path = (urlparse(url).path or "/").rstrip("/") or "/"
|
|
255
|
+
if path in seen_paths: continue
|
|
256
|
+
seen_paths.add(path)
|
|
257
|
+
out.append({
|
|
258
|
+
"title": (p.get("title") or path.strip("/") or "Page")[:60],
|
|
259
|
+
"icon": ICONS.get(p.get("kind") or "page", "bi-file-earmark-text"),
|
|
260
|
+
"url": path if path == "/" else (path + "/"),
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
def yq(s):
|
|
264
|
+
return '"' + str(s).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
265
|
+
|
|
266
|
+
lines = ["# Auto-generated from scrape: " + src_url, ""]
|
|
267
|
+
for item in out:
|
|
268
|
+
lines.append(f'- title: {yq(item["title"])}')
|
|
269
|
+
lines.append(f' icon: {item["icon"]}')
|
|
270
|
+
lines.append(f' url: {yq(item["url"])}')
|
|
271
|
+
lines.append("")
|
|
272
|
+
|
|
273
|
+
with open(os.environ["OUT"], "w", encoding="utf-8") as f:
|
|
274
|
+
f.write("\n".join(lines).rstrip() + "\n")
|
|
275
|
+
PY
|
|
276
|
+
log_info "scrape: wrote $(basename "$nav_yml") with $(grep -c '^- title:' "$nav_yml" 2>/dev/null || echo 0) entries"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Seed _config.yml from scraped metadata. Non-destructive: only fills empty
|
|
281
|
+
# or installer-placeholder values.
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
_task_scrape_seed_config() {
|
|
284
|
+
local target="$1" site_json="$2"
|
|
285
|
+
[[ -f "$site_json" ]] || return 0
|
|
286
|
+
local cfg="${target}/_config.yml"
|
|
287
|
+
[[ -f "$cfg" ]] || return 0
|
|
288
|
+
|
|
289
|
+
SITE_JSON="$site_json" CFG="$cfg" python3 <<'PY'
|
|
290
|
+
import json, os, re
|
|
291
|
+
|
|
292
|
+
with open(os.environ["SITE_JSON"], encoding="utf-8") as f:
|
|
293
|
+
s = json.load(f)
|
|
294
|
+
title = (s.get("title") or "").strip()
|
|
295
|
+
desc = (s.get("description") or "").strip()
|
|
296
|
+
lang = (s.get("lang") or "").strip() or "en"
|
|
297
|
+
image = (s.get("image") or "").strip()
|
|
298
|
+
|
|
299
|
+
cfg_path = os.environ["CFG"]
|
|
300
|
+
with open(cfg_path, encoding="utf-8") as f:
|
|
301
|
+
txt = f.read()
|
|
302
|
+
|
|
303
|
+
PLACEHOLDERS = {
|
|
304
|
+
"title": {"", "My Jekyll Site", "Your Site Title", "Jekyll Theme zer0"},
|
|
305
|
+
"description": {"", "A Jekyll site built with zer0-mistakes",
|
|
306
|
+
"A description of your site"},
|
|
307
|
+
"lang": {"", "en-US"},
|
|
308
|
+
"logo": {"", "/assets/images/logo.png", "/assets/logo.png"},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
def repl_scalar(s, key, val, placeholders):
|
|
312
|
+
if not val:
|
|
313
|
+
return s
|
|
314
|
+
pat = re.compile(r'^(' + re.escape(key) + r'\s*:\s*)(.*)$', re.M)
|
|
315
|
+
def fn(m):
|
|
316
|
+
cur = m.group(2).strip().strip('"').strip("'")
|
|
317
|
+
if cur and cur not in placeholders:
|
|
318
|
+
return m.group(0)
|
|
319
|
+
safe = val.replace('"', "")
|
|
320
|
+
return f'{m.group(1)}"{safe}"'
|
|
321
|
+
return pat.sub(fn, s, count=1)
|
|
322
|
+
|
|
323
|
+
txt = repl_scalar(txt, "title", title, PLACEHOLDERS["title"])
|
|
324
|
+
txt = repl_scalar(txt, "description", desc, PLACEHOLDERS["description"])
|
|
325
|
+
txt = repl_scalar(txt, "lang", lang, PLACEHOLDERS["lang"])
|
|
326
|
+
if image:
|
|
327
|
+
txt = repl_scalar(txt, "logo", image, PLACEHOLDERS["logo"])
|
|
328
|
+
|
|
329
|
+
# Ensure lang/logo exist; if not, append a small block.
|
|
330
|
+
appended = []
|
|
331
|
+
if title and not re.search(r'^title\s*:', txt, re.M):
|
|
332
|
+
appended.append(f'title: "{title}"')
|
|
333
|
+
if desc and not re.search(r'^description\s*:', txt, re.M):
|
|
334
|
+
appended.append(f'description: "{desc}"')
|
|
335
|
+
if lang and not re.search(r'^lang\s*:', txt, re.M):
|
|
336
|
+
appended.append(f'lang: "{lang}"')
|
|
337
|
+
if image and not re.search(r'^logo\s*:', txt, re.M):
|
|
338
|
+
appended.append(f'logo: "{image}"')
|
|
339
|
+
if appended:
|
|
340
|
+
if not txt.endswith("\n"): txt += "\n"
|
|
341
|
+
txt += "\n# Seeded from scrape\n" + "\n".join(appended) + "\n"
|
|
342
|
+
|
|
343
|
+
with open(cfg_path, "w", encoding="utf-8") as f:
|
|
344
|
+
f.write(txt)
|
|
345
|
+
PY
|
|
346
|
+
log_debug " seeded _config.yml from scrape metadata"
|
|
347
|
+
}
|
|
348
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/template.sh — Template rendering
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Single source of truth: EVERY generated file comes from a template.
|
|
6
|
+
# Never inline heredocs anywhere else.
|
|
7
|
+
#
|
|
8
|
+
# Provides:
|
|
9
|
+
# tmpl_render TEMPLATE_FILE [OUTPUT_FILE]
|
|
10
|
+
# Substitutes {{VAR}} placeholders from the current environment.
|
|
11
|
+
# If OUTPUT_FILE is omitted, writes to stdout.
|
|
12
|
+
#
|
|
13
|
+
# tmpl_apply TEMPLATE_REL_PATH OUTPUT_FILE
|
|
14
|
+
# Resolution order:
|
|
15
|
+
# 1. $TEMPLATES_DIR/TEMPLATE_REL_PATH (local checkout)
|
|
16
|
+
# 2. Remote fetch from $ZER0_RAW_URL/templates/TEMPLATE_REL_PATH
|
|
17
|
+
# (when ZER0_REMOTE_INSTALL=1)
|
|
18
|
+
# Respects _FS_DRY_RUN, _FS_FORCE via fs.sh.
|
|
19
|
+
# Returns 1 if template not found anywhere.
|
|
20
|
+
#
|
|
21
|
+
# Variables substituted (extend by editing the sed chain below):
|
|
22
|
+
# SITE_TITLE, SITE_DESCRIPTION, SITE_AUTHOR, SITE_EMAIL, SITE_URL
|
|
23
|
+
# SITE_TIMEZONE, SITE_LOCALE
|
|
24
|
+
# GITHUB_USER, GITHUB_REPO, GITHUB_URL, ZER0_RAW_URL
|
|
25
|
+
# GITHUB_PAGES_BRANCH, REPOSITORY_NAME
|
|
26
|
+
# THEME_NAME, THEME_GEM_NAME, THEME_DISPLAY_NAME, THEME_VERSION
|
|
27
|
+
# THEME_SOURCE (gem|remote|vendored)
|
|
28
|
+
# DEFAULT_PORT, DEFAULT_URL
|
|
29
|
+
# JEKYLL_VERSION, FFI_VERSION, WEBRICK_VERSION
|
|
30
|
+
# COMMONMARKER_VERSION, COMMONMARKER_MACOS_VERSION
|
|
31
|
+
# GITHUB_PAGES_MAX_VERSION
|
|
32
|
+
# RUBY_MIN_VERSION_MACOS
|
|
33
|
+
# CURRENT_DATE, CURRENT_YEAR
|
|
34
|
+
# INSTALL_PROFILE, INSTALL_MODE (legacy compat)
|
|
35
|
+
# REMOTE_BRANCH
|
|
36
|
+
#
|
|
37
|
+
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
38
|
+
# =============================================================================
|
|
39
|
+
[[ -n "${_HAS_TEMPLATE_LIB:-}" ]] && return 0
|
|
40
|
+
_HAS_TEMPLATE_LIB=1
|
|
41
|
+
|
|
42
|
+
# Substitute all {{VAR}} tokens from the environment.
|
|
43
|
+
# BSD sed and GNU sed compatible. Processes one line at a time via awk-delegation.
|
|
44
|
+
tmpl_render() {
|
|
45
|
+
local template_file="$1"
|
|
46
|
+
local output_file="${2:-}"
|
|
47
|
+
|
|
48
|
+
if [[ ! -f "$template_file" ]]; then
|
|
49
|
+
log_error "tmpl_render: template not found: $template_file"
|
|
50
|
+
return 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
local content
|
|
54
|
+
content=$(cat "$template_file")
|
|
55
|
+
|
|
56
|
+
# Apply all substitutions. BSD-sed compatible (no -i without extension).
|
|
57
|
+
# Pipe chain: each sed -e is one substitution pass.
|
|
58
|
+
content=$(printf '%s' "$content" | sed \
|
|
59
|
+
-e "s|{{THEME_NAME}}|${THEME_NAME:-zer0-mistakes}|g" \
|
|
60
|
+
-e "s|{{THEME_GEM_NAME}}|${THEME_GEM_NAME:-jekyll-theme-zer0}|g" \
|
|
61
|
+
-e "s|{{THEME_DISPLAY_NAME}}|${THEME_DISPLAY_NAME:-Zer0-Mistakes}|g" \
|
|
62
|
+
-e "s|{{THEME_VERSION}}|${THEME_VERSION:-}|g" \
|
|
63
|
+
-e "s|{{THEME_SOURCE}}|${THEME_SOURCE:-gem}|g" \
|
|
64
|
+
-e "s|{{GITHUB_USER}}|${GITHUB_USER:-}|g" \
|
|
65
|
+
-e "s|{{FORK_GITHUB_USER}}|${GITHUB_USER:-}|g" \
|
|
66
|
+
-e "s|{{GITHUB_REPO}}|${GITHUB_REPO:-}|g" \
|
|
67
|
+
-e "s|{{GITHUB_FULL_REPO}}|${GITHUB_USER:-}/${GITHUB_REPO:-}|g" \
|
|
68
|
+
-e "s|{{GITHUB_URL}}|${GITHUB_URL:-https://github.com/bamr87/zer0-mistakes}|g" \
|
|
69
|
+
-e "s|{{ZER0_RAW_URL}}|${ZER0_RAW_URL:-https://raw.githubusercontent.com/bamr87/zer0-mistakes/main}|g" \
|
|
70
|
+
-e "s|{{GITHUB_RAW_URL}}|${ZER0_RAW_URL:-https://raw.githubusercontent.com/bamr87/zer0-mistakes/main}|g" \
|
|
71
|
+
-e "s|{{GITHUB_PAGES_BRANCH}}|${GITHUB_PAGES_BRANCH:-gh-pages}|g" \
|
|
72
|
+
-e "s|{{REMOTE_BRANCH}}|${REMOTE_BRANCH:-${GITHUB_PAGES_BRANCH:-gh-pages}}|g" \
|
|
73
|
+
-e "s|{{REPOSITORY_NAME}}|${REPOSITORY_NAME:-${GITHUB_REPO:-my-site}}|g" \
|
|
74
|
+
-e "s|{{SITE_TITLE}}|${SITE_TITLE:-My Jekyll Site}|g" \
|
|
75
|
+
-e "s|{{SITE_DESCRIPTION}}|${SITE_DESCRIPTION:-A Jekyll site built with zer0-mistakes}|g" \
|
|
76
|
+
-e "s|{{SITE_AUTHOR}}|${SITE_AUTHOR:-Site Author}|g" \
|
|
77
|
+
-e "s|{{SITE_EMAIL}}|${SITE_EMAIL:-}|g" \
|
|
78
|
+
-e "s|{{SITE_URL}}|${SITE_URL:-}|g" \
|
|
79
|
+
-e "s|{{SITE_TIMEZONE}}|${SITE_TIMEZONE:-UTC}|g" \
|
|
80
|
+
-e "s|{{SITE_LOCALE}}|${SITE_LOCALE:-en}|g" \
|
|
81
|
+
-e "s|{{DEFAULT_PORT}}|${DEFAULT_PORT:-4000}|g" \
|
|
82
|
+
-e "s|{{DEFAULT_URL}}|${DEFAULT_URL:-http://localhost:4000}|g" \
|
|
83
|
+
-e "s|{{JEKYLL_VERSION}}|${JEKYLL_VERSION:-~> 4.3}|g" \
|
|
84
|
+
-e "s|{{FFI_VERSION}}|${FFI_VERSION:-~> 1.15}|g" \
|
|
85
|
+
-e "s|{{WEBRICK_VERSION}}|${WEBRICK_VERSION:-~> 1.8}|g" \
|
|
86
|
+
-e "s|{{COMMONMARKER_VERSION}}|${COMMONMARKER_VERSION:-~> 0.23}|g" \
|
|
87
|
+
-e "s|{{COMMONMARKER_MACOS_VERSION}}|${COMMONMARKER_MACOS_VERSION:-~> 0.23}|g" \
|
|
88
|
+
-e "s|{{GITHUB_PAGES_MAX_VERSION}}|${GITHUB_PAGES_MAX_VERSION:-232}|g" \
|
|
89
|
+
-e "s|{{RUBY_MIN_VERSION_MACOS}}|${RUBY_MIN_VERSION_MACOS:-2.6.0}|g" \
|
|
90
|
+
-e "s|{{INSTALL_PROFILE}}|${INSTALL_PROFILE:-default}|g" \
|
|
91
|
+
-e "s|{{INSTALL_MODE}}|${INSTALL_MODE:-full}|g" \
|
|
92
|
+
-e "s|{{CURRENT_DATE}}|$(date +%Y-%m-%d)|g" \
|
|
93
|
+
-e "s|{{CURRENT_YEAR}}|$(date +%Y)|g" \
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if [[ -n "$output_file" ]]; then
|
|
97
|
+
# Delegate to fs.sh for safe write (backup, dry-run, force awareness)
|
|
98
|
+
if [[ "$(type -t fs_write_file)" == "function" ]]; then
|
|
99
|
+
fs_write_file "$output_file" "$content"
|
|
100
|
+
else
|
|
101
|
+
mkdir -p "$(dirname "$output_file")"
|
|
102
|
+
printf '%s\n' "$content" > "$output_file"
|
|
103
|
+
fi
|
|
104
|
+
else
|
|
105
|
+
printf '%s\n' "$content"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Apply a template by relative path → output file.
|
|
110
|
+
# Resolution: local TEMPLATES_DIR → remote fetch.
|
|
111
|
+
tmpl_apply() {
|
|
112
|
+
local tmpl_rel="$1"
|
|
113
|
+
local output_file="$2"
|
|
114
|
+
|
|
115
|
+
# 1. Local templates dir
|
|
116
|
+
if [[ -n "${TEMPLATES_DIR:-}" && -f "${TEMPLATES_DIR}/${tmpl_rel}" ]]; then
|
|
117
|
+
tmpl_render "${TEMPLATES_DIR}/${tmpl_rel}" "$output_file"
|
|
118
|
+
return $?
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# 2. Remote fetch (only in remote install mode)
|
|
122
|
+
if [[ "${ZER0_REMOTE_INSTALL:-0}" == "1" ]]; then
|
|
123
|
+
local raw_url="${ZER0_RAW_URL:-https://raw.githubusercontent.com/bamr87/zer0-mistakes/main}"
|
|
124
|
+
local fetch_url="${raw_url}/templates/${tmpl_rel}"
|
|
125
|
+
local tmp_file
|
|
126
|
+
tmp_file=$(mktemp /tmp/zer0-tmpl-XXXXXX)
|
|
127
|
+
if curl -fsSL --max-time 15 "$fetch_url" -o "$tmp_file" 2>/dev/null; then
|
|
128
|
+
tmpl_render "$tmp_file" "$output_file"
|
|
129
|
+
local ret=$?
|
|
130
|
+
rm -f "$tmp_file"
|
|
131
|
+
return $ret
|
|
132
|
+
fi
|
|
133
|
+
rm -f "$tmp_file"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
log_error "tmpl_apply: template not found: $tmpl_rel"
|
|
137
|
+
return 1
|
|
138
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/tui.sh — Non-AI interactive wizard
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Prompts the user through site configuration using prompt.sh and builds
|
|
6
|
+
# a spec. Replaces the old install.sh interactive path.
|
|
7
|
+
#
|
|
8
|
+
# Provides:
|
|
9
|
+
# tui_run TARGET_DIR
|
|
10
|
+
#
|
|
11
|
+
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
12
|
+
# =============================================================================
|
|
13
|
+
[[ -n "${_HAS_TUI_LIB:-}" ]] && return 0
|
|
14
|
+
_HAS_TUI_LIB=1
|
|
15
|
+
|
|
16
|
+
tui_run() {
|
|
17
|
+
local target="${1:-$(pwd)}"
|
|
18
|
+
|
|
19
|
+
log_banner "zer0-mistakes Setup Wizard"
|
|
20
|
+
log_info "Let's configure your Jekyll site."
|
|
21
|
+
log_info "(Press Enter to accept defaults shown in brackets)"
|
|
22
|
+
printf "\n" >&2
|
|
23
|
+
|
|
24
|
+
# Profile
|
|
25
|
+
prompt_select SPEC_PROFILE "Installation profile" \
|
|
26
|
+
default minimal blog docs portfolio github-pages fork
|
|
27
|
+
export SPEC_PROFILE
|
|
28
|
+
|
|
29
|
+
# Site info
|
|
30
|
+
prompt_ask SPEC_SITE_TITLE "Site title" "My Jekyll Site"
|
|
31
|
+
prompt_ask SPEC_SITE_DESCRIPTION "Short description" "A Jekyll site built with zer0-mistakes"
|
|
32
|
+
prompt_ask SPEC_SITE_AUTHOR "Author name" "$(git config user.name 2>/dev/null || echo "Site Author")"
|
|
33
|
+
prompt_ask SPEC_SITE_EMAIL "Author email" "$(git config user.email 2>/dev/null || echo "")"
|
|
34
|
+
|
|
35
|
+
export SPEC_SITE_TITLE SPEC_SITE_DESCRIPTION SPEC_SITE_AUTHOR SPEC_SITE_EMAIL
|
|
36
|
+
|
|
37
|
+
# GitHub
|
|
38
|
+
if prompt_confirm "Configure GitHub integration?"; then
|
|
39
|
+
prompt_ask SPEC_GITHUB_USER "GitHub username" "${SPEC_GITHUB_USER:-}"
|
|
40
|
+
prompt_ask SPEC_GITHUB_REPO "Repository name" "$(basename "$target")"
|
|
41
|
+
export SPEC_GITHUB_USER SPEC_GITHUB_REPO
|
|
42
|
+
|
|
43
|
+
if prompt_confirm "Enable GitHub Pages?"; then
|
|
44
|
+
SPEC_GITHUB_ENABLE_PAGES=true
|
|
45
|
+
else
|
|
46
|
+
SPEC_GITHUB_ENABLE_PAGES=false
|
|
47
|
+
fi
|
|
48
|
+
export SPEC_GITHUB_ENABLE_PAGES
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Deploy
|
|
52
|
+
if prompt_confirm "Add a deployment configuration?"; then
|
|
53
|
+
prompt_select _TUI_DEPLOY "Deploy target" \
|
|
54
|
+
github-pages azure-swa docker-prod vercel netlify cloudflare-pages none
|
|
55
|
+
[[ "$_TUI_DEPLOY" != "none" ]] && SPEC_DEPLOY="$_TUI_DEPLOY"
|
|
56
|
+
export SPEC_DEPLOY
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Agent files
|
|
60
|
+
if prompt_confirm "Install AI agent files (AGENTS.md, etc.)?"; then
|
|
61
|
+
printf "\nSelect agent integrations (space-separated, e.g. 'copilot claude'):\n" >&2
|
|
62
|
+
printf " generic copilot claude cursor aider all\n" >&2
|
|
63
|
+
prompt_ask _TUI_AGENTS "Agents" "generic"
|
|
64
|
+
[[ "$_TUI_AGENTS" != "none" ]] && SPEC_AGENTS="$_TUI_AGENTS"
|
|
65
|
+
export SPEC_AGENTS
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Options
|
|
69
|
+
if prompt_confirm "Enable dry-run preview (no files written)?"; then
|
|
70
|
+
SPEC_OPT_DRY_RUN=true
|
|
71
|
+
else
|
|
72
|
+
SPEC_OPT_DRY_RUN=false
|
|
73
|
+
fi
|
|
74
|
+
export SPEC_OPT_DRY_RUN
|
|
75
|
+
|
|
76
|
+
printf "\n" >&2
|
|
77
|
+
|
|
78
|
+
# Build final spec
|
|
79
|
+
SPEC_TARGET_DIR="$target"
|
|
80
|
+
export SPEC_TARGET_DIR
|
|
81
|
+
plan_apply_platform
|
|
82
|
+
|
|
83
|
+
if [[ -z "${SPEC_TASKS:-}" ]]; then
|
|
84
|
+
SPEC_TASKS="config gemfile docker pages nav data gitignore readme marker"
|
|
85
|
+
fi
|
|
86
|
+
export SPEC_TASKS
|
|
87
|
+
|
|
88
|
+
# Show summary
|
|
89
|
+
log_info "--- Configuration Summary ---"
|
|
90
|
+
log_info "Profile : ${SPEC_PROFILE}"
|
|
91
|
+
log_info "Title : ${SPEC_SITE_TITLE}"
|
|
92
|
+
log_info "Author : ${SPEC_SITE_AUTHOR}"
|
|
93
|
+
log_info "GitHub : ${SPEC_GITHUB_USER:-not set}/${SPEC_GITHUB_REPO:-not set}"
|
|
94
|
+
log_info "Deploy : ${SPEC_DEPLOY:-none}"
|
|
95
|
+
log_info "Agents : ${SPEC_AGENTS:-none}"
|
|
96
|
+
log_info "Dry run : ${SPEC_OPT_DRY_RUN}"
|
|
97
|
+
log_info "Target : ${target}"
|
|
98
|
+
printf "\n" >&2
|
|
99
|
+
|
|
100
|
+
if ! prompt_confirm "Proceed with installation?"; then
|
|
101
|
+
log_info "Aborted."
|
|
102
|
+
return 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Write spec and apply
|
|
106
|
+
local spec_file
|
|
107
|
+
spec_file="$(spec_path "$target")"
|
|
108
|
+
spec_write "$spec_file"
|
|
109
|
+
apply_run "$spec_file"
|
|
110
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/upgrade.sh — Re-apply spec to an existing install
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Reads the existing .zer0/install.spec.json, applies flag overrides,
|
|
6
|
+
# and re-runs apply.sh — safely updating config/agent files without
|
|
7
|
+
# touching user content.
|
|
8
|
+
#
|
|
9
|
+
# Provides:
|
|
10
|
+
# upgrade_run TARGET_DIR
|
|
11
|
+
#
|
|
12
|
+
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
13
|
+
# =============================================================================
|
|
14
|
+
[[ -n "${_HAS_UPGRADE_LIB:-}" ]] && return 0
|
|
15
|
+
_HAS_UPGRADE_LIB=1
|
|
16
|
+
|
|
17
|
+
upgrade_run() {
|
|
18
|
+
local target="${1:-$(pwd)}"
|
|
19
|
+
local spec_file
|
|
20
|
+
spec_file="$(spec_path "$target")"
|
|
21
|
+
|
|
22
|
+
if [[ ! -f "$spec_file" ]]; then
|
|
23
|
+
log_error "upgrade: no spec found at $spec_file"
|
|
24
|
+
log_info "Run 'install init $target' first, or use 'install init --force' to re-create."
|
|
25
|
+
return 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
log_info "Upgrading existing install at: $target"
|
|
29
|
+
spec_read "$spec_file"
|
|
30
|
+
|
|
31
|
+
# Apply any flag overrides
|
|
32
|
+
plan_apply_flags
|
|
33
|
+
plan_apply_platform
|
|
34
|
+
|
|
35
|
+
# Never upgrade user content pages by default
|
|
36
|
+
local safe_tasks=""
|
|
37
|
+
local t
|
|
38
|
+
for t in ${SPEC_TASKS}; do
|
|
39
|
+
case "$t" in
|
|
40
|
+
pages|nav|data) ;; # skip content tasks on upgrade
|
|
41
|
+
*) safe_tasks="${safe_tasks} $t" ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
SPEC_TASKS="${safe_tasks# }"
|
|
45
|
+
export SPEC_TASKS
|
|
46
|
+
|
|
47
|
+
spec_write "$spec_file"
|
|
48
|
+
apply_run "$spec_file"
|
|
49
|
+
}
|
|
@@ -75,6 +75,7 @@ render_template() {
|
|
|
75
75
|
-e "s|{{RAW_GITHUB_URL}}|${GITHUB_RAW_URL}|g" \
|
|
76
76
|
-e "s|{{FORK_GITHUB_USER}}|${FORK_GITHUB_USER:-${GITHUB_USER}}|g" \
|
|
77
77
|
-e "s|{{INSTALL_MODE}}|${INSTALL_MODE:-full}|g" \
|
|
78
|
+
-e "s|{{REMOTE_BRANCH}}|${REMOTE_BRANCH:-gh-pages}|g" \
|
|
78
79
|
-e "s|{{GITHUB_PAGES_URL}}|https://${FORK_GITHUB_USER:-${GITHUB_USER}}.github.io/${REPOSITORY_NAME:-$THEME_NAME}|g")
|
|
79
80
|
|
|
80
81
|
if [[ -n "$output_file" ]]; then
|