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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -3
  3. data/README.md +98 -7
  4. data/_data/content_statistics.yml +253 -251
  5. data/_includes/components/nav-export.html +61 -0
  6. data/_includes/components/nav-overview.html +54 -0
  7. data/scripts/bin/install +52 -705
  8. data/scripts/github-setup.sh +0 -0
  9. data/scripts/install/README.md +162 -0
  10. data/scripts/install/ai/client.sh +164 -0
  11. data/scripts/install/ai/diagnose.sh +81 -0
  12. data/scripts/install/ai/prompts/diagnose.system.md +42 -0
  13. data/scripts/install/ai/prompts/spec.schema.json +129 -0
  14. data/scripts/install/ai/prompts/suggest.system.md +43 -0
  15. data/scripts/install/ai/prompts/wizard.system.md +142 -0
  16. data/scripts/install/ai/suggest.sh +57 -0
  17. data/scripts/install/ai/wizard.sh +150 -0
  18. data/scripts/install/apply.sh +156 -0
  19. data/scripts/install/cli.sh +561 -0
  20. data/scripts/install/diff.sh +128 -0
  21. data/scripts/install/doctor.sh +168 -0
  22. data/scripts/install/fs.sh +138 -0
  23. data/scripts/install/log.sh +119 -0
  24. data/scripts/install/plan.sh +299 -0
  25. data/scripts/install/platform.sh +122 -0
  26. data/scripts/install/prompt.sh +124 -0
  27. data/scripts/install/repair.sh +45 -0
  28. data/scripts/install/scrape.sh +535 -0
  29. data/scripts/install/scrape_html.py +764 -0
  30. data/scripts/install/spec.sh +486 -0
  31. data/scripts/install/tasks/_registry.sh +65 -0
  32. data/scripts/install/tasks/agents.sh +60 -0
  33. data/scripts/install/tasks/config.sh +37 -0
  34. data/scripts/install/tasks/data.sh +18 -0
  35. data/scripts/install/tasks/deploy_azure-swa.sh +17 -0
  36. data/scripts/install/tasks/deploy_docker-prod.sh +21 -0
  37. data/scripts/install/tasks/deploy_github-pages.sh +18 -0
  38. data/scripts/install/tasks/devcontainer.sh +26 -0
  39. data/scripts/install/tasks/docker.sh +29 -0
  40. data/scripts/install/tasks/gemfile.sh +42 -0
  41. data/scripts/install/tasks/gitignore.sh +26 -0
  42. data/scripts/install/tasks/marker.sh +46 -0
  43. data/scripts/install/tasks/nav.sh +18 -0
  44. data/scripts/install/tasks/pages.sh +61 -0
  45. data/scripts/install/tasks/readme.sh +27 -0
  46. data/scripts/install/tasks/scrape.sh +348 -0
  47. data/scripts/install/template.sh +138 -0
  48. data/scripts/install/tui.sh +110 -0
  49. data/scripts/install/upgrade.sh +49 -0
  50. data/scripts/lib/install/template.sh +1 -0
  51. 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