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,486 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/spec.sh — Install spec JSON I/O and validation
4
+ # =============================================================================
5
+ # The spec is a JSON document that fully describes an installer run.
6
+ # This module handles creating, reading, writing, and validating specs
7
+ # without requiring jq (uses a pure-bash fallback for basic reads).
8
+ #
9
+ # Provides:
10
+ # spec_default → print a minimal valid spec to stdout
11
+ # spec_write FILE → write current spec env-vars to FILE as JSON
12
+ # spec_read FILE → export spec env-vars from FILE
13
+ # spec_validate FILE → validate FILE against schema; print errors
14
+ # spec_get FILE KEY → print value for KEY (dot-notation: site.title)
15
+ # spec_hash FILE → print sha256 of spec content
16
+ # spec_path TARGET → canonical spec file path for TARGET dir
17
+ #
18
+ # Spec globals (populated by spec_read / plan.sh):
19
+ # SPEC_SCHEMA_VERSION SPEC_TARGET_DIR SPEC_PROFILE
20
+ # SPEC_SITE_TITLE SPEC_SITE_DESCRIPTION SPEC_SITE_URL
21
+ # SPEC_SITE_AUTHOR SPEC_SITE_EMAIL SPEC_SITE_TIMEZONE SPEC_SITE_LOCALE
22
+ # SPEC_GITHUB_USER SPEC_GITHUB_REPO SPEC_GITHUB_PAGES_BRANCH SPEC_GITHUB_ENABLE_PAGES
23
+ # SPEC_THEME_SOURCE SPEC_THEME_VERSION
24
+ # SPEC_TASKS (space-separated)
25
+ # SPEC_DEPLOY (space-separated)
26
+ # SPEC_AGENTS (space-separated)
27
+ # SPEC_OPT_DRY_RUN SPEC_OPT_FORCE SPEC_OPT_BACKUP
28
+ # SPEC_OPT_NON_INTERACTIVE SPEC_OPT_OUTPUT SPEC_OPT_AUTO_ACCEPT
29
+ # SPEC_OPT_SKIP_DOCTOR SPEC_OPT_VERBOSE
30
+ # SPEC_AI_USED SPEC_AI_PROVIDER SPEC_AI_MODEL
31
+ #
32
+ # Bash 3.2 compatible. No set -euo pipefail here.
33
+ # =============================================================================
34
+ [[ -n "${_HAS_SPEC_LIB:-}" ]] && return 0
35
+ _HAS_SPEC_LIB=1
36
+
37
+ # Relative path inside target dir where the spec is stored
38
+ SPEC_FILE_NAME=".zer0/install.spec.json"
39
+
40
+ spec_path() {
41
+ local target_dir="$1"
42
+ echo "${target_dir}/${SPEC_FILE_NAME}"
43
+ }
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # spec_default — print a minimal valid spec with sensible defaults
47
+ # ---------------------------------------------------------------------------
48
+ spec_default() {
49
+ cat <<'SPEC'
50
+ {
51
+ "schema_version": "1",
52
+ "target_dir": "",
53
+ "profile": "default",
54
+ "site": {
55
+ "title": "My Jekyll Site",
56
+ "description": "A Jekyll site built with zer0-mistakes",
57
+ "url": "",
58
+ "author": "Site Author",
59
+ "email": "",
60
+ "timezone": "UTC",
61
+ "locale": "en"
62
+ },
63
+ "github": {
64
+ "user": "",
65
+ "repo": "",
66
+ "pages_branch": "gh-pages",
67
+ "enable_pages": false
68
+ },
69
+ "theme": {
70
+ "source": "gem",
71
+ "version": ""
72
+ },
73
+ "tasks": ["config","gemfile","docker","pages","nav","data","gitignore","readme","marker"],
74
+ "deploy": [],
75
+ "agents": [],
76
+ "options": {
77
+ "dry_run": false,
78
+ "force": false,
79
+ "backup": true,
80
+ "non_interactive": false,
81
+ "output": "human",
82
+ "auto_accept": false,
83
+ "skip_doctor": false,
84
+ "verbose": false
85
+ },
86
+ "ai": {
87
+ "used": false,
88
+ "provider": "openai",
89
+ "model": "",
90
+ "tokens_estimated": 0,
91
+ "spec_hash": ""
92
+ },
93
+ "scrape": {
94
+ "source_url": "",
95
+ "depth": 2,
96
+ "max_pages": 25,
97
+ "out_dir": "",
98
+ "include_nav": true
99
+ }
100
+ }
101
+ SPEC
102
+ }
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # spec_write FILE — serialise current SPEC_* globals to JSON
106
+ # ---------------------------------------------------------------------------
107
+ spec_write() {
108
+ local out_file="$1"
109
+ local dry_run="${_FS_DRY_RUN:-0}"
110
+
111
+ # Build tasks array from space-separated SPEC_TASKS
112
+ local tasks_json=""
113
+ local t
114
+ for t in ${SPEC_TASKS:-config gemfile docker pages nav data gitignore readme marker}; do
115
+ tasks_json="${tasks_json}\"${t}\","
116
+ done
117
+ tasks_json="[${tasks_json%,}]"
118
+
119
+ local deploy_json=""
120
+ for t in ${SPEC_DEPLOY:-}; do
121
+ deploy_json="${deploy_json}\"${t}\","
122
+ done
123
+ deploy_json="[${deploy_json%,}]"
124
+
125
+ local agents_json=""
126
+ for t in ${SPEC_AGENTS:-}; do
127
+ agents_json="${agents_json}\"${t}\","
128
+ done
129
+ agents_json="[${agents_json%,}]"
130
+
131
+ local json
132
+ json=$(cat <<JSON
133
+ {
134
+ "schema_version": "1",
135
+ "target_dir": "${SPEC_TARGET_DIR:-}",
136
+ "profile": "${SPEC_PROFILE:-default}",
137
+ "site": {
138
+ "title": "${SPEC_SITE_TITLE:-My Jekyll Site}",
139
+ "description": "${SPEC_SITE_DESCRIPTION:-A Jekyll site built with zer0-mistakes}",
140
+ "url": "${SPEC_SITE_URL:-}",
141
+ "author": "${SPEC_SITE_AUTHOR:-Site Author}",
142
+ "email": "${SPEC_SITE_EMAIL:-}",
143
+ "timezone": "${SPEC_SITE_TIMEZONE:-UTC}",
144
+ "locale": "${SPEC_SITE_LOCALE:-en}"
145
+ },
146
+ "github": {
147
+ "user": "${SPEC_GITHUB_USER:-}",
148
+ "repo": "${SPEC_GITHUB_REPO:-}",
149
+ "pages_branch": "${SPEC_GITHUB_PAGES_BRANCH:-gh-pages}",
150
+ "enable_pages": ${SPEC_GITHUB_ENABLE_PAGES:-false}
151
+ },
152
+ "theme": {
153
+ "source": "${SPEC_THEME_SOURCE:-gem}",
154
+ "version": "${SPEC_THEME_VERSION:-}"
155
+ },
156
+ "tasks": ${tasks_json},
157
+ "deploy": ${deploy_json},
158
+ "agents": ${agents_json},
159
+ "options": {
160
+ "dry_run": ${SPEC_OPT_DRY_RUN:-false},
161
+ "force": ${SPEC_OPT_FORCE:-false},
162
+ "backup": ${SPEC_OPT_BACKUP:-true},
163
+ "non_interactive": ${SPEC_OPT_NON_INTERACTIVE:-false},
164
+ "output": "${SPEC_OPT_OUTPUT:-human}",
165
+ "auto_accept": ${SPEC_OPT_AUTO_ACCEPT:-false},
166
+ "skip_doctor": ${SPEC_OPT_SKIP_DOCTOR:-false},
167
+ "verbose": ${SPEC_OPT_VERBOSE:-false}
168
+ },
169
+ "ai": {
170
+ "used": ${SPEC_AI_USED:-false},
171
+ "provider": "${SPEC_AI_PROVIDER:-openai}",
172
+ "model": "${SPEC_AI_MODEL:-}",
173
+ "tokens_estimated": ${SPEC_AI_TOKENS:-0},
174
+ "spec_hash": ""
175
+ },
176
+ "scrape": {
177
+ "source_url": "${SPEC_SCRAPE_SOURCE_URL:-}",
178
+ "depth": ${SPEC_SCRAPE_DEPTH:-2},
179
+ "max_pages": ${SPEC_SCRAPE_MAX_PAGES:-25},
180
+ "out_dir": "${SPEC_SCRAPE_OUT_DIR:-}",
181
+ "include_nav": ${SPEC_SCRAPE_INCLUDE_NAV:-true}
182
+ }
183
+ }
184
+ JSON
185
+ )
186
+
187
+ if [[ "$dry_run" == "1" ]]; then
188
+ log_debug "[dry-run] would write spec: $out_file"
189
+ return 0
190
+ fi
191
+
192
+ if [[ "$(type -t fs_ensure_dir)" == "function" ]]; then
193
+ fs_ensure_dir "$(dirname "$out_file")"
194
+ else
195
+ mkdir -p "$(dirname "$out_file")"
196
+ fi
197
+
198
+ printf '%s\n' "$json" > "$out_file"
199
+ log_debug "Spec written: $out_file"
200
+ }
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # spec_read FILE — parse JSON spec → export SPEC_* globals
204
+ # Use jq when available; fall back to pure-bash awk parser for core fields.
205
+ # ---------------------------------------------------------------------------
206
+ spec_read() {
207
+ local in_file="$1"
208
+ if [[ ! -f "$in_file" ]]; then
209
+ log_error "spec_read: file not found: $in_file"
210
+ return 1
211
+ fi
212
+
213
+ if command -v jq >/dev/null 2>&1; then
214
+ _spec_read_jq "$in_file"
215
+ else
216
+ _spec_read_awk "$in_file"
217
+ fi
218
+ }
219
+
220
+ # jq-powered reader
221
+ _spec_read_jq() {
222
+ local f="$1"
223
+ SPEC_SCHEMA_VERSION=$(jq -r '.schema_version // "1"' "$f")
224
+ SPEC_TARGET_DIR=$(jq -r '.target_dir // ""' "$f")
225
+ SPEC_PROFILE=$(jq -r '.profile // "default"' "$f")
226
+ SPEC_SITE_TITLE=$(jq -r '.site.title // "My Jekyll Site"' "$f")
227
+ SPEC_SITE_DESCRIPTION=$(jq -r '.site.description // ""' "$f")
228
+ SPEC_SITE_URL=$(jq -r '.site.url // ""' "$f")
229
+ SPEC_SITE_AUTHOR=$(jq -r '.site.author // ""' "$f")
230
+ SPEC_SITE_EMAIL=$(jq -r '.site.email // ""' "$f")
231
+ SPEC_SITE_TIMEZONE=$(jq -r '.site.timezone // "UTC"' "$f")
232
+ SPEC_SITE_LOCALE=$(jq -r '.site.locale // "en"' "$f")
233
+ SPEC_GITHUB_USER=$(jq -r '.github.user // ""' "$f")
234
+ SPEC_GITHUB_REPO=$(jq -r '.github.repo // ""' "$f")
235
+ SPEC_GITHUB_PAGES_BRANCH=$(jq -r '.github.pages_branch // "gh-pages"' "$f")
236
+ SPEC_GITHUB_ENABLE_PAGES=$(jq -r '.github.enable_pages // false' "$f")
237
+ SPEC_THEME_SOURCE=$(jq -r '.theme.source // "gem"' "$f")
238
+ SPEC_THEME_VERSION=$(jq -r '.theme.version // ""' "$f")
239
+ SPEC_TASKS=$(jq -r '.tasks // [] | join(" ")' "$f")
240
+ SPEC_DEPLOY=$(jq -r '.deploy // [] | join(" ")' "$f")
241
+ SPEC_AGENTS=$(jq -r '.agents // [] | join(" ")' "$f")
242
+ SPEC_OPT_DRY_RUN=$(jq -r '.options.dry_run // false' "$f")
243
+ SPEC_OPT_FORCE=$(jq -r '.options.force // false' "$f")
244
+ SPEC_OPT_BACKUP=$(jq -r '.options.backup // true' "$f")
245
+ SPEC_OPT_NON_INTERACTIVE=$(jq -r '.options.non_interactive // false' "$f")
246
+ SPEC_OPT_OUTPUT=$(jq -r '.options.output // "human"' "$f")
247
+ SPEC_OPT_AUTO_ACCEPT=$(jq -r '.options.auto_accept // false' "$f")
248
+ SPEC_OPT_SKIP_DOCTOR=$(jq -r '.options.skip_doctor // false' "$f")
249
+ SPEC_OPT_VERBOSE=$(jq -r '.options.verbose // false' "$f")
250
+ SPEC_AI_USED=$(jq -r '.ai.used // false' "$f")
251
+ SPEC_AI_PROVIDER=$(jq -r '.ai.provider // "openai"' "$f")
252
+ SPEC_AI_MODEL=$(jq -r '.ai.model // ""' "$f")
253
+ SPEC_AI_TOKENS=$(jq -r '.ai.tokens_estimated // 0' "$f")
254
+ SPEC_SCRAPE_SOURCE_URL=$(jq -r '.scrape.source_url // ""' "$f")
255
+ SPEC_SCRAPE_DEPTH=$(jq -r '.scrape.depth // 2' "$f")
256
+ SPEC_SCRAPE_MAX_PAGES=$(jq -r '.scrape.max_pages // 25' "$f")
257
+ SPEC_SCRAPE_OUT_DIR=$(jq -r '.scrape.out_dir // ""' "$f")
258
+ SPEC_SCRAPE_INCLUDE_NAV=$(jq -r '.scrape.include_nav // true' "$f")
259
+ export SPEC_SCHEMA_VERSION SPEC_TARGET_DIR SPEC_PROFILE \
260
+ SPEC_SITE_TITLE SPEC_SITE_DESCRIPTION SPEC_SITE_URL \
261
+ SPEC_SITE_AUTHOR SPEC_SITE_EMAIL SPEC_SITE_TIMEZONE SPEC_SITE_LOCALE \
262
+ SPEC_GITHUB_USER SPEC_GITHUB_REPO SPEC_GITHUB_PAGES_BRANCH SPEC_GITHUB_ENABLE_PAGES \
263
+ SPEC_THEME_SOURCE SPEC_THEME_VERSION \
264
+ SPEC_TASKS SPEC_DEPLOY SPEC_AGENTS \
265
+ SPEC_OPT_DRY_RUN SPEC_OPT_FORCE SPEC_OPT_BACKUP \
266
+ SPEC_OPT_NON_INTERACTIVE SPEC_OPT_OUTPUT SPEC_OPT_AUTO_ACCEPT \
267
+ SPEC_OPT_SKIP_DOCTOR SPEC_OPT_VERBOSE \
268
+ SPEC_AI_USED SPEC_AI_PROVIDER SPEC_AI_MODEL SPEC_AI_TOKENS \
269
+ SPEC_SCRAPE_SOURCE_URL SPEC_SCRAPE_DEPTH SPEC_SCRAPE_MAX_PAGES \
270
+ SPEC_SCRAPE_OUT_DIR SPEC_SCRAPE_INCLUDE_NAV
271
+ }
272
+
273
+ # Minimal awk-based JSON reader for when jq is absent.
274
+ # Handles flat and one-level-deep scalar fields. Arrays are returned
275
+ # as space-separated values.
276
+ _spec_read_awk() {
277
+ local f="$1"
278
+ # Helper: extract a value for a given key pattern
279
+ _awk_get() {
280
+ local key="$1"
281
+ local default="${2:-}"
282
+ awk -v k="$key" '
283
+ $0 ~ "\"" k "\"[[:space:]]*:[[:space:]]*\"" {
284
+ # string value
285
+ match($0, "\"" k "\"[[:space:]]*:[[:space:]]*\"([^\"]*)", a)
286
+ # fallback awk for bash 3.2 (no match capture groups in some builds)
287
+ line = $0
288
+ sub(".*\"" k "\"[[:space:]]*:[[:space:]]*\"", "", line)
289
+ sub("\".*", "", line)
290
+ print line
291
+ found=1
292
+ exit
293
+ }
294
+ $0 ~ "\"" k "\"[[:space:]]*:[[:space:]]*[0-9tf]" {
295
+ # boolean/number value
296
+ line = $0
297
+ sub(".*\"" k "\"[[:space:]]*:[[:space:]]*", "", line)
298
+ sub("[,}].*", "", line)
299
+ gsub(/[[:space:]]/, "", line)
300
+ print line
301
+ found=1
302
+ exit
303
+ }
304
+ END { if (!found) print "" }
305
+ ' "$f" || echo "$default"
306
+ }
307
+
308
+ SPEC_SCHEMA_VERSION=$(_awk_get "schema_version" "1")
309
+ SPEC_TARGET_DIR=$(_awk_get "target_dir" "")
310
+ SPEC_PROFILE=$(_awk_get "profile" "default")
311
+ SPEC_SITE_TITLE=$(_awk_get "title" "My Jekyll Site")
312
+ SPEC_SITE_AUTHOR=$(_awk_get "author" "")
313
+ SPEC_SITE_EMAIL=$(_awk_get "email" "")
314
+ SPEC_SITE_TIMEZONE=$(_awk_get "timezone" "UTC")
315
+ SPEC_GITHUB_USER=$(_awk_get "user" "")
316
+ SPEC_GITHUB_REPO=$(_awk_get "repo" "")
317
+ SPEC_GITHUB_PAGES_BRANCH=$(_awk_get "pages_branch" "gh-pages")
318
+ SPEC_THEME_SOURCE=$(_awk_get "source" "gem")
319
+ SPEC_THEME_VERSION=$(_awk_get "version" "")
320
+ SPEC_OPT_OUTPUT=$(_awk_get "output" "human")
321
+
322
+ # Arrays: extract list items from JSON arrays
323
+ SPEC_TASKS=$(awk '
324
+ /"tasks"/ { found=1 }
325
+ found && /\[/ { inlist=1 }
326
+ inlist && /"[a-z]/ {
327
+ val = $0
328
+ gsub(/^[^"]*"/, "", val)
329
+ gsub(/".*/, "", val)
330
+ printf "%s ", val
331
+ }
332
+ inlist && /\]/ { exit }
333
+ ' "$f" | sed 's/[[:space:]]*$//')
334
+
335
+ SPEC_DEPLOY=$(awk '
336
+ /"deploy"/ { found=1 }
337
+ found && /\[/ { inlist=1 }
338
+ inlist && /"[a-z]/ {
339
+ val = $0
340
+ gsub(/^[^"]*"/, "", val)
341
+ gsub(/".*/, "", val)
342
+ printf "%s ", val
343
+ }
344
+ inlist && /\]/ { exit }
345
+ ' "$f" | sed 's/[[:space:]]*$//')
346
+
347
+ SPEC_AGENTS=$(awk '
348
+ /"agents"/ { found=1 }
349
+ found && /\[/ { inlist=1 }
350
+ inlist && /"[a-z]/ {
351
+ val = $0
352
+ gsub(/^[^"]*"/, "", val)
353
+ gsub(/".*/, "", val)
354
+ printf "%s ", val
355
+ }
356
+ inlist && /\]/ { exit }
357
+ ' "$f" | sed 's/[[:space:]]*$//')
358
+
359
+ # Fallback defaults for fields we couldn't parse without jq
360
+ SPEC_SITE_DESCRIPTION="${SPEC_SITE_DESCRIPTION:-A Jekyll site built with zer0-mistakes}"
361
+ SPEC_SITE_URL="${SPEC_SITE_URL:-}"
362
+ SPEC_SITE_LOCALE="${SPEC_SITE_LOCALE:-en}"
363
+ SPEC_GITHUB_ENABLE_PAGES="${SPEC_GITHUB_ENABLE_PAGES:-false}"
364
+ SPEC_OPT_DRY_RUN="${SPEC_OPT_DRY_RUN:-false}"
365
+ SPEC_OPT_FORCE="${SPEC_OPT_FORCE:-false}"
366
+ SPEC_OPT_BACKUP="${SPEC_OPT_BACKUP:-true}"
367
+ SPEC_OPT_NON_INTERACTIVE="${SPEC_OPT_NON_INTERACTIVE:-false}"
368
+ SPEC_OPT_AUTO_ACCEPT="${SPEC_OPT_AUTO_ACCEPT:-false}"
369
+ SPEC_OPT_SKIP_DOCTOR="${SPEC_OPT_SKIP_DOCTOR:-false}"
370
+ SPEC_OPT_VERBOSE="${SPEC_OPT_VERBOSE:-false}"
371
+ SPEC_AI_USED="${SPEC_AI_USED:-false}"
372
+ SPEC_AI_PROVIDER="${SPEC_AI_PROVIDER:-openai}"
373
+ SPEC_AI_MODEL="${SPEC_AI_MODEL:-}"
374
+ SPEC_AI_TOKENS="${SPEC_AI_TOKENS:-0}"
375
+ SPEC_SCRAPE_SOURCE_URL="${SPEC_SCRAPE_SOURCE_URL:-$(awk '/"source_url"/ { gsub(/.*"source_url"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; exit }' "$f" 2>/dev/null)}"
376
+ SPEC_SCRAPE_DEPTH="${SPEC_SCRAPE_DEPTH:-2}"
377
+ SPEC_SCRAPE_MAX_PAGES="${SPEC_SCRAPE_MAX_PAGES:-25}"
378
+ SPEC_SCRAPE_OUT_DIR="${SPEC_SCRAPE_OUT_DIR:-}"
379
+ SPEC_SCRAPE_INCLUDE_NAV="${SPEC_SCRAPE_INCLUDE_NAV:-true}"
380
+
381
+ export SPEC_SCHEMA_VERSION SPEC_TARGET_DIR SPEC_PROFILE \
382
+ SPEC_SITE_TITLE SPEC_SITE_DESCRIPTION SPEC_SITE_URL \
383
+ SPEC_SITE_AUTHOR SPEC_SITE_EMAIL SPEC_SITE_TIMEZONE SPEC_SITE_LOCALE \
384
+ SPEC_GITHUB_USER SPEC_GITHUB_REPO SPEC_GITHUB_PAGES_BRANCH SPEC_GITHUB_ENABLE_PAGES \
385
+ SPEC_THEME_SOURCE SPEC_THEME_VERSION \
386
+ SPEC_TASKS SPEC_DEPLOY SPEC_AGENTS \
387
+ SPEC_OPT_DRY_RUN SPEC_OPT_FORCE SPEC_OPT_BACKUP \
388
+ SPEC_OPT_NON_INTERACTIVE SPEC_OPT_OUTPUT SPEC_OPT_AUTO_ACCEPT \
389
+ SPEC_OPT_SKIP_DOCTOR SPEC_OPT_VERBOSE \
390
+ SPEC_AI_USED SPEC_AI_PROVIDER SPEC_AI_MODEL SPEC_AI_TOKENS \
391
+ SPEC_SCRAPE_SOURCE_URL SPEC_SCRAPE_DEPTH SPEC_SCRAPE_MAX_PAGES \
392
+ SPEC_SCRAPE_OUT_DIR SPEC_SCRAPE_INCLUDE_NAV
393
+ }
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # spec_validate FILE — lightweight validation (no jq required)
397
+ # ---------------------------------------------------------------------------
398
+ spec_validate() {
399
+ local f="$1"
400
+ local errors=0
401
+
402
+ if [[ ! -f "$f" ]]; then
403
+ log_error "spec_validate: file not found: $f"
404
+ return 1
405
+ fi
406
+
407
+ # Check schema_version is present
408
+ if ! grep -q '"schema_version"' "$f"; then
409
+ log_error "spec_validate: missing required field: schema_version"
410
+ errors=$(( errors + 1 ))
411
+ fi
412
+
413
+ # Check target_dir is present and non-empty
414
+ if ! grep -q '"target_dir"[[:space:]]*:[[:space:]]*"[^"]' "$f"; then
415
+ log_error "spec_validate: target_dir is missing or empty"
416
+ errors=$(( errors + 1 ))
417
+ fi
418
+
419
+ # Check profile is a known value
420
+ local profile
421
+ if command -v jq >/dev/null 2>&1; then
422
+ profile=$(jq -r '.profile // ""' "$f")
423
+ else
424
+ profile=$(awk '/"profile"/ { gsub(/.*"profile"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; exit }' "$f")
425
+ fi
426
+ case "$profile" in
427
+ default|minimal|blog|docs|portfolio|github-pages|fork) ;;
428
+ *)
429
+ log_error "spec_validate: unknown profile: '$profile'"
430
+ errors=$(( errors + 1 ))
431
+ ;;
432
+ esac
433
+
434
+ # Check theme.source
435
+ local theme_src
436
+ if command -v jq >/dev/null 2>&1; then
437
+ theme_src=$(jq -r '.theme.source // ""' "$f")
438
+ else
439
+ theme_src=$(awk '/"source"/ { gsub(/.*"source"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; exit }' "$f")
440
+ fi
441
+ case "$theme_src" in
442
+ gem|remote|vendored) ;;
443
+ *)
444
+ log_error "spec_validate: invalid theme.source: '$theme_src'"
445
+ errors=$(( errors + 1 ))
446
+ ;;
447
+ esac
448
+
449
+ if [[ $errors -eq 0 ]]; then
450
+ log_debug "spec_validate: OK — $f"
451
+ return 0
452
+ fi
453
+ return 1
454
+ }
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # spec_get FILE KEY — print the value for a dot-notation key
458
+ # spec_get spec.json site.title
459
+ # ---------------------------------------------------------------------------
460
+ spec_get() {
461
+ local f="$1"
462
+ local key="$2"
463
+ if command -v jq >/dev/null 2>&1; then
464
+ jq -r ".${key} // \"\"" "$f"
465
+ else
466
+ # Fallback: load into SPEC_* and echo the matching var
467
+ spec_read "$f"
468
+ local var_name
469
+ var_name=$(echo "SPEC_${key}" | tr '.' '_' | tr '[:lower:]' '[:upper:]')
470
+ eval "echo \"\${${var_name}:-}\""
471
+ fi
472
+ }
473
+
474
+ # ---------------------------------------------------------------------------
475
+ # spec_hash FILE — sha256 hex of the file content
476
+ # ---------------------------------------------------------------------------
477
+ spec_hash() {
478
+ local f="$1"
479
+ if command -v sha256sum >/dev/null 2>&1; then
480
+ sha256sum "$f" | awk '{print $1}'
481
+ elif command -v shasum >/dev/null 2>&1; then
482
+ shasum -a 256 "$f" | awk '{print $1}'
483
+ else
484
+ echo "unknown"
485
+ fi
486
+ }
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/tasks/_registry.sh — Task discovery and metadata
4
+ # =============================================================================
5
+ # Describes the canonical task graph: dependencies, conditions, descriptions.
6
+ # Sourced by apply.sh and diff.sh.
7
+ #
8
+ # Provides:
9
+ # task_list_all → print all task names, one per line
10
+ # task_description TASK → print one-line description
11
+ # task_depends TASK → print space-separated dependency names
12
+ #
13
+ # Bash 3.2 compatible. No set -euo pipefail here.
14
+ # =============================================================================
15
+ [[ -n "${_HAS_TASK_REGISTRY:-}" ]] && return 0
16
+ _HAS_TASK_REGISTRY=1
17
+
18
+ # Ordered canonical task list (deps must appear before dependents)
19
+ _TASK_ALL="config gemfile docker theme pages scrape nav data devcontainer agents gitignore readme marker"
20
+
21
+ # One-line descriptions
22
+ _task_desc_config="Write _config.yml and _config_dev.yml"
23
+ _task_desc_gemfile="Write Gemfile (variant: gem|remote|macos)"
24
+ _task_desc_docker="Write docker-compose.yml and docker/Dockerfile"
25
+ _task_desc_theme="Copy _layouts _includes _sass assets (vendor|local)"
26
+ _task_desc_pages="Generate starter pages from templates"
27
+ _task_desc_scrape="Import content from an existing website (spec.scrape.source_url)"
28
+ _task_desc_nav="Generate _data/navigation/ from template"
29
+ _task_desc_data="Generate _data/authors.yml and seed data"
30
+ _task_desc_devcontainer="Write .devcontainer/devcontainer.json"
31
+ _task_desc_agents="Write AI agent files (AGENTS.md, CLAUDE.md, etc.)"
32
+ _task_desc_gitignore="Write .gitignore"
33
+ _task_desc_readme="Write INSTALLATION.md and README seed"
34
+ _task_desc_marker="Write .zer0-installed marker + persist spec"
35
+
36
+ # Dependencies (space-separated, empty = no deps)
37
+ _task_deps_config=""
38
+ _task_deps_gemfile="config"
39
+ _task_deps_docker="config"
40
+ _task_deps_theme=""
41
+ _task_deps_pages="config"
42
+ _task_deps_scrape="config pages"
43
+ _task_deps_nav="config"
44
+ _task_deps_data="config"
45
+ _task_deps_devcontainer=""
46
+ _task_deps_agents=""
47
+ _task_deps_gitignore=""
48
+ _task_deps_readme=""
49
+ _task_deps_marker="config gemfile"
50
+
51
+ task_list_all() {
52
+ echo "$_TASK_ALL" | tr ' ' '\n'
53
+ }
54
+
55
+ task_description() {
56
+ local task="$1"
57
+ local var="_task_desc_${task}"
58
+ eval "echo \"\${${var}:-unknown task: $task}\""
59
+ }
60
+
61
+ task_depends() {
62
+ local task="$1"
63
+ local var="_task_deps_${task}"
64
+ eval "echo \"\${${var}:-}\""
65
+ }
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/tasks/agents.sh — Write AI agent files
4
+ # =============================================================================
5
+ # Writes agent-specific files based on SPEC_AGENTS (space-separated list):
6
+ # copilot → .github/copilot-instructions.md
7
+ # claude → CLAUDE.md
8
+ # cursor → .cursor/rules/zer0.mdc
9
+ # aider → .aider.conf.yml
10
+ # generic → AGENTS.md (always written as baseline)
11
+ #
12
+ # Bash 3.2 compatible. No set -euo pipefail here.
13
+ # =============================================================================
14
+ [[ -n "${_HAS_TASK_AGENTS:-}" ]] && return 0
15
+ _HAS_TASK_AGENTS=1
16
+
17
+ task_agents_run() {
18
+ local target="$1"
19
+ local agents="${SPEC_AGENTS:-}"
20
+
21
+ [[ -z "$agents" ]] && { log_debug "agents task: no agents configured (skipping)"; return 0; }
22
+
23
+ log_info "Writing AI agent files..."
24
+
25
+ local agent
26
+ for agent in $agents; do
27
+ case "$agent" in
28
+ generic)
29
+ tmpl_apply "agents/AGENTS.md.template" "${target}/AGENTS.md"
30
+ ;;
31
+ copilot)
32
+ fs_ensure_dir "${target}/.github"
33
+ tmpl_apply "agents/copilot-instructions.md.template" \
34
+ "${target}/.github/copilot-instructions.md"
35
+ ;;
36
+ claude)
37
+ tmpl_apply "agents/CLAUDE.md.template" "${target}/CLAUDE.md"
38
+ ;;
39
+ cursor)
40
+ fs_ensure_dir "${target}/.cursor/rules"
41
+ tmpl_apply "agents/cursor-rule.mdc.template" \
42
+ "${target}/.cursor/rules/zer0.mdc"
43
+ ;;
44
+ aider)
45
+ tmpl_apply "agents/aider.conf.yml.template" \
46
+ "${target}/.aider.conf.yml"
47
+ ;;
48
+ all)
49
+ # Re-invoke with each known agent
50
+ SPEC_AGENTS="generic copilot claude cursor aider" task_agents_run "$target"
51
+ return $?
52
+ ;;
53
+ *)
54
+ log_warning "agents task: unknown agent '$agent' (skipping)"
55
+ ;;
56
+ esac
57
+ done
58
+
59
+ log_success "Agent files written"
60
+ }
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/tasks/config.sh — Write Jekyll config files
4
+ # =============================================================================
5
+ # Writes: _config.yml, _config_dev.yml (if profile != minimal/github-pages)
6
+ # All writes go through template.sh::tmpl_apply → fs.sh.
7
+ # Bash 3.2 compatible. No set -euo pipefail here.
8
+ # =============================================================================
9
+ [[ -n "${_HAS_TASK_CONFIG:-}" ]] && return 0
10
+ _HAS_TASK_CONFIG=1
11
+
12
+ task_config_run() {
13
+ local target="$1"
14
+
15
+ log_info "Writing Jekyll configuration..."
16
+
17
+ # Choose template variant based on theme.source
18
+ local config_tmpl
19
+ case "${SPEC_THEME_SOURCE:-${THEME_SOURCE:-gem}}" in
20
+ remote) config_tmpl="config/_config.remote.yml.template" ;;
21
+ fork) config_tmpl="config/_config.fork.yml.template" ;;
22
+ *) config_tmpl="config/_config.starter.yml.template" ;;
23
+ esac
24
+
25
+ tmpl_apply "$config_tmpl" "${target}/_config.yml"
26
+
27
+ # Dev config — skip for github-pages (remote_theme) and minimal profiles
28
+ local profile="${SPEC_PROFILE:-default}"
29
+ case "$profile" in
30
+ minimal|github-pages) ;;
31
+ *)
32
+ tmpl_apply "config/_config_dev.yml.template" "${target}/_config_dev.yml"
33
+ ;;
34
+ esac
35
+
36
+ log_success "Jekyll configuration written"
37
+ }
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/tasks/data.sh — Generate seed data files
4
+ # =============================================================================
5
+ # Writes: _data/authors.yml
6
+ # Bash 3.2 compatible. No set -euo pipefail here.
7
+ # =============================================================================
8
+ [[ -n "${_HAS_TASK_DATA:-}" ]] && return 0
9
+ _HAS_TASK_DATA=1
10
+
11
+ task_data_run() {
12
+ local target="$1"
13
+
14
+ log_info "Writing seed data..."
15
+ fs_ensure_dir "${target}/_data"
16
+ tmpl_apply "data/authors.yml.template" "${target}/_data/authors.yml"
17
+ log_success "Seed data written"
18
+ }