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,142 @@
1
+ # zer0-mistakes AI Installation Wizard
2
+
3
+ You are the **zer0-mistakes installer wizard**. You convert the user's intent
4
+ into a single, valid **install spec JSON** that conforms to the schema the
5
+ caller will provide in the user prompt.
6
+
7
+ You NEVER write files yourself. You ONLY emit a JSON spec. The installer's
8
+ `apply.sh` is the sole writer.
9
+
10
+ ---
11
+
12
+ ## Hard constraints (do not violate)
13
+
14
+ 1. **Output ONLY one JSON object.** No prose. No explanations. No markdown
15
+ fences (no triple-backticks). The first character of your reply MUST be
16
+ `{` and the last MUST be `}`.
17
+ 2. **Conform exactly to the provided schema.** All `required` keys must be
18
+ present. All enums must match. `additionalProperties: false` is enforced.
19
+ 3. **Honor every value the user already provided** in the "Key info" block of
20
+ the user prompt — copy them through unchanged. Only fill in fields the
21
+ user left as `not set`.
22
+ 4. **Always include `schema_version: "1"`**, the supplied `target_dir`, and
23
+ a complete `options` object (all 5 required keys: `dry_run`, `force`,
24
+ `backup`, `non_interactive`, `output`).
25
+ 5. **`tasks` MUST be non-empty and end with `"marker"`.**
26
+ 6. **Be decisive.** If information is ambiguous, pick the most reasonable
27
+ default and move on. Do not ask follow-up questions in non-interactive
28
+ mode.
29
+
30
+ ---
31
+
32
+ ## Profile selection (decision rules)
33
+
34
+ Pick the **single** profile that best matches the user's stated purpose:
35
+
36
+ | Signal in user input | Profile |
37
+ | -------------------------------------------------------- | -------------- |
38
+ | "blog", "posts", "writing", "articles", "newsletter" | `blog` |
39
+ | "documentation", "API docs", "manual", "knowledge base" | `docs` |
40
+ | "portfolio", "resume", "showcase", "personal site" | `portfolio` |
41
+ | "GitHub project page", "open source readme site" | `github-pages` |
42
+ | "fork the theme", "customize the theme itself" | `fork` |
43
+ | "tiny", "minimal", "barebones", "just the basics" | `minimal` |
44
+ | anything else / unclear | `default` |
45
+
46
+ If a flag in "Key info" already names a profile, USE IT — do not second-guess.
47
+
48
+ ---
49
+
50
+ ## Task defaults (per profile)
51
+
52
+ If you are unsure which tasks to enable, use these defaults. `marker` is
53
+ always last.
54
+
55
+ - `minimal`: `["config", "gemfile", "gitignore", "marker"]`
56
+ - `default`: `["config", "gemfile", "docker", "pages", "nav", "data", "gitignore", "readme", "agents", "marker"]`
57
+ - `blog`: `["config", "gemfile", "docker", "pages", "nav", "data", "devcontainer", "gitignore", "readme", "agents", "marker"]`
58
+ - `docs`: same as `blog`
59
+ - `portfolio`: same as `default`
60
+ - `github-pages`: `["config", "gemfile", "pages", "nav", "data", "gitignore", "readme", "agents", "marker"]`
61
+ - `fork`: `["config", "gemfile", "docker", "theme", "pages", "nav", "data", "devcontainer", "gitignore", "readme", "agents", "marker"]`
62
+
63
+ ---
64
+
65
+ ## Deploy target heuristics
66
+
67
+ Populate `deploy` (array, may be empty) based on signals:
68
+
69
+ - User mentions GitHub Pages, `github.io`, or `enable_pages: true` → include `"github-pages"`.
70
+ - User mentions Netlify → include `"netlify"`.
71
+ - User mentions Vercel → include `"vercel"`.
72
+ - User mentions Cloudflare Pages → include `"cloudflare-pages"`.
73
+ - User mentions self-hosting, "production Docker", or VPS → include `"docker-prod"`.
74
+ - User mentions Azure (Static Web Apps) → include `"azure-swa"`.
75
+ - No deploy signals AND profile is `github-pages` → default to `["github-pages"]`.
76
+ - No signals otherwise → empty array `[]`.
77
+
78
+ ---
79
+
80
+ ## Agent file heuristics
81
+
82
+ Populate `agents` (array, may be empty) based on signals:
83
+
84
+ - User mentions Copilot, GitHub Copilot, or VS Code AI → include `"copilot"`.
85
+ - User mentions Cursor → include `"cursor"`.
86
+ - User mentions Claude, Claude Code, Anthropic → include `"claude"`.
87
+ - User mentions Aider → include `"aider"`.
88
+ - User mentions "AI agents", "agent files", or wants cross-tool support → include `"generic"` (writes AGENTS.md).
89
+ - If user says "all agents" → `["generic", "copilot", "claude", "cursor", "aider"]`.
90
+ - If unclear but AI was clearly invoked → default to `["generic", "copilot"]`.
91
+ - If the user explicitly says "no AI files" or similar → `[]`.
92
+
93
+ ---
94
+
95
+ ## Theme source
96
+
97
+ - Profile `github-pages` → `theme.source = "remote"`.
98
+ - Profile `fork` → `theme.source = "vendored"`.
99
+ - Everything else → `theme.source = "gem"` (default).
100
+
101
+ ---
102
+
103
+ ## Required output shape (example)
104
+
105
+ The example below is illustrative. Adapt every value to the user's request.
106
+ The exact field set is enforced by the schema in the user prompt.
107
+
108
+ ```
109
+ {
110
+ "schema_version": "1",
111
+ "target_dir": "/abs/path/from/user/prompt",
112
+ "profile": "blog",
113
+ "site": {
114
+ "title": "Travel Stories",
115
+ "description": "A travel blog built with zer0-mistakes",
116
+ "url": "",
117
+ "author": "Jane Doe",
118
+ "email": "",
119
+ "timezone": "UTC",
120
+ "locale": "en"
121
+ },
122
+ "github": {
123
+ "user": "janedoe",
124
+ "repo": "travel-blog",
125
+ "pages_branch": "gh-pages",
126
+ "enable_pages": true
127
+ },
128
+ "theme": { "source": "gem", "version": "" },
129
+ "tasks": ["config", "gemfile", "docker", "pages", "nav", "data", "devcontainer", "gitignore", "readme", "agents", "marker"],
130
+ "deploy": ["github-pages"],
131
+ "agents": ["generic", "copilot"],
132
+ "options": {
133
+ "dry_run": false,
134
+ "force": false,
135
+ "backup": true,
136
+ "non_interactive": true,
137
+ "output": "human"
138
+ }
139
+ }
140
+ ```
141
+
142
+ (Do NOT include surrounding triple-backtick fences. Emit only the raw JSON object.)
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/ai/suggest.sh — AI-assisted profile + deploy suggestions
4
+ # =============================================================================
5
+ # Given a target directory and optional goal description, suggests:
6
+ # - Best installation profile
7
+ # - Best deploy target(s)
8
+ #
9
+ # Provides:
10
+ # ai_suggest_run TARGET_DIR [GOAL_TEXT]
11
+ # → print JSON: {"profile":"...", "deploy":["..."]}
12
+ #
13
+ # Bash 3.2 compatible. No set -euo pipefail here.
14
+ # =============================================================================
15
+ [[ -n "${_HAS_AI_SUGGEST:-}" ]] && return 0
16
+ _HAS_AI_SUGGEST=1
17
+
18
+ _AI_SUGGEST_DIR="${_AI_SUGGEST_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)}"
19
+
20
+ ai_suggest_run() {
21
+ local target="${1:-$(pwd)}"
22
+ local goal="${2:-}"
23
+
24
+ if [[ "$(type -t ai_client_available)" != "function" ]]; then
25
+ source "${_AI_SUGGEST_DIR}/client.sh" || return 1
26
+ fi
27
+
28
+ if ! ai_client_available; then
29
+ log_warning "ai_suggest: AI not available — using rule-based defaults"
30
+ printf '{"profile":"default","deploy":["github-pages"]}\n'
31
+ return 0
32
+ fi
33
+
34
+ local sys_prompt_file="${_AI_SUGGEST_DIR}/prompts/suggest.system.md"
35
+ local sys_prompt
36
+ if [[ -f "$sys_prompt_file" ]]; then
37
+ sys_prompt=$(cat "$sys_prompt_file")
38
+ else
39
+ sys_prompt="You are a Jekyll setup advisor. Based on the user's goal and context, recommend the best zer0-mistakes profile and deploy target. Profiles: default, minimal, blog, docs, portfolio, github-pages, fork. Deploy targets: github-pages, azure-swa, docker-prod, vercel, netlify, cloudflare-pages. Return JSON: {\"profile\":\"...\",\"deploy\":[\"...\"]}."
40
+ fi
41
+
42
+ local user_prompt
43
+ user_prompt="Target directory: $target"
44
+ [[ -n "$goal" ]] && user_prompt="${user_prompt}
45
+ Goal: $goal"
46
+
47
+ # Use json_object mode to prevent markdown wrapping
48
+ local resp
49
+ resp=$(ai_client_chat "$sys_prompt" "$user_prompt" '{"name":"suggest","schema":{"type":"object","properties":{"profile":{"type":"string"},"deploy":{"type":"array","items":{"type":"string"}},"rationale":{"type":"string"}},"required":["profile","deploy"]}}') || {
50
+ printf '{"profile":"default","deploy":["github-pages"]}\n'
51
+ return 0
52
+ }
53
+
54
+ local result
55
+ result=$(ai_client_extract_text "$resp")
56
+ printf '%s\n' "$result"
57
+ }
@@ -0,0 +1,150 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/ai/wizard.sh — AI-driven spec generator
4
+ # =============================================================================
5
+ # Conversations with an LLM to gather site requirements and emit a valid
6
+ # install spec JSON. The AI is constrained to the spec.schema.json contract.
7
+ #
8
+ # The AI NEVER writes files. It returns a spec JSON only. apply.sh is the
9
+ # sole writer.
10
+ #
11
+ # Provides:
12
+ # ai_wizard_run TARGET_DIR
13
+ # → populate SPEC_* globals from AI-generated spec
14
+ # → write spec to $(spec_path TARGET_DIR)
15
+ # → return 0 on success
16
+ #
17
+ # Environment:
18
+ # OPENAI_API_KEY / OPENAI_BASE_URL — see ai/client.sh
19
+ # ZER0_NO_AI=1 — disable AI (returns error)
20
+ #
21
+ # Bash 3.2 compatible. No set -euo pipefail here.
22
+ # =============================================================================
23
+ [[ -n "${_HAS_AI_WIZARD:-}" ]] && return 0
24
+ _HAS_AI_WIZARD=1
25
+
26
+ _AI_WIZARD_DIR="${_AI_WIZARD_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)}"
27
+
28
+ ai_wizard_run() {
29
+ local target="$1"
30
+ [[ -z "$target" ]] && target="$(pwd)"
31
+
32
+ # Source client
33
+ if [[ "$(type -t ai_client_available)" != "function" ]]; then
34
+ local client="${_AI_WIZARD_DIR}/client.sh"
35
+ [[ -f "$client" ]] || { log_error "ai/client.sh not found"; return 1; }
36
+ # shellcheck source=./client.sh
37
+ source "$client"
38
+ fi
39
+
40
+ if ! ai_client_available; then
41
+ log_error "AI wizard requires OPENAI_API_KEY or OPENAI_BASE_URL"
42
+ return 1
43
+ fi
44
+
45
+ # Load system prompt
46
+ local sys_prompt_file="${_AI_WIZARD_DIR}/prompts/wizard.system.md"
47
+ local sys_prompt
48
+ if [[ -f "$sys_prompt_file" ]]; then
49
+ sys_prompt=$(cat "$sys_prompt_file")
50
+ else
51
+ sys_prompt="You are the zer0-mistakes installer wizard. Ask the user questions to gather site requirements, then output a valid install spec JSON matching the provided schema. Output only the JSON object, no prose."
52
+ fi
53
+
54
+ # Load schema for reference (shown to AI in prompt, not as structured output)
55
+ local schema_file="${_AI_WIZARD_DIR}/prompts/spec.schema.json"
56
+ local schema_hint=""
57
+ [[ -f "$schema_file" ]] && schema_hint=$(cat "$schema_file")
58
+
59
+ # Build user context prompt
60
+ local user_prompt
61
+ user_prompt=$(cat <<PROMPT
62
+ Target directory: $target
63
+ Platform: $(uname -s) $(uname -m)
64
+ Current user: $(id -un 2>/dev/null || echo "unknown")
65
+
66
+ Please generate the install spec JSON for my zer0-mistakes Jekyll site.
67
+
68
+ Key info:
69
+ - Profile preference: ${SPEC_PROFILE:-not set}
70
+ - Site title: ${SPEC_SITE_TITLE:-not set}
71
+ - GitHub username: ${SPEC_GITHUB_USER:-not set}
72
+ - Deploy preference: ${SPEC_DEPLOY:-not set}
73
+ - AI agents needed: ${SPEC_AGENTS:-not set}
74
+
75
+ The JSON must conform to this schema:
76
+ ${schema_hint}
77
+
78
+ Output ONLY the JSON object, no prose, no markdown fences.
79
+ PROMPT
80
+ )
81
+
82
+ log_banner "AI Installation Wizard"
83
+ log_info "Connecting to AI... (model: ${OPENAI_MODEL:-gpt-4o-mini})"
84
+
85
+ local resp
86
+ resp=$(ai_client_chat "$sys_prompt" "$user_prompt")
87
+ local ret=$?
88
+
89
+ if [[ $ret -ne 0 ]]; then
90
+ log_error "AI wizard: API call failed"
91
+ return 1
92
+ fi
93
+
94
+ # Extract JSON spec from response
95
+ local spec_json
96
+ spec_json=$(ai_client_extract_text "$resp")
97
+
98
+ if [[ -z "$spec_json" ]]; then
99
+ log_error "AI wizard: empty response"
100
+ return 1
101
+ fi
102
+
103
+ # Write spec to temp file and read it
104
+ local tmp
105
+ tmp=$(mktemp /tmp/zer0-ai-spec-XXXXXX.json)
106
+ printf '%s\n' "$spec_json" > "$tmp"
107
+
108
+ if spec_validate "$tmp"; then
109
+ log_success "AI generated a valid spec"
110
+ # Populate globals
111
+ spec_read "$tmp"
112
+ # Override target dir (AI may not know the exact local path)
113
+ SPEC_TARGET_DIR="$target"
114
+ export SPEC_TARGET_DIR
115
+ # If AI omitted deploy/agents, fall back to profile defaults so the
116
+ # user still gets sensible files. CLI flags then win below.
117
+ if [[ -z "${SPEC_DEPLOY:-}" || -z "${SPEC_AGENTS:-}" ]]; then
118
+ local _profile_file="${TEMPLATES_DIR:-}/profiles/${SPEC_PROFILE:-default}.yml"
119
+ if [[ -f "$_profile_file" && "$(type -t plan_load_profile)" == "function" ]]; then
120
+ local _saved_deploy="$SPEC_DEPLOY"
121
+ local _saved_agents="$SPEC_AGENTS"
122
+ SPEC_DEPLOY=""; SPEC_AGENTS=""
123
+ plan_load_profile "$_profile_file" 2>/dev/null || true
124
+ [[ -n "$_saved_deploy" ]] && SPEC_DEPLOY="$_saved_deploy"
125
+ [[ -n "$_saved_agents" ]] && SPEC_AGENTS="$_saved_agents"
126
+ fi
127
+ fi
128
+ # Re-apply CLI flags so user-provided --site-title, --github-user, etc.
129
+ # always win over AI guesses.
130
+ if [[ "$(type -t plan_apply_flags)" == "function" ]]; then
131
+ plan_apply_flags
132
+ fi
133
+ # Apply platform defaults
134
+ plan_apply_platform
135
+ # Record AI metadata
136
+ SPEC_AI_USED=true
137
+ SPEC_AI_PROVIDER="${OPENAI_PROVIDER:-openai}"
138
+ SPEC_AI_MODEL="${OPENAI_MODEL:-gpt-4o-mini}"
139
+ export SPEC_AI_USED SPEC_AI_PROVIDER SPEC_AI_MODEL
140
+ # Write final spec
141
+ spec_write "$(spec_path "$target")"
142
+ rm -f "$tmp"
143
+ return 0
144
+ else
145
+ log_error "AI returned an invalid spec. Review: $tmp"
146
+ log_warning "Falling back to interactive wizard..."
147
+ rm -f "$tmp"
148
+ return 1
149
+ fi
150
+ }
@@ -0,0 +1,156 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/apply.sh — Execute an install spec
4
+ # =============================================================================
5
+ # apply.sh is THE ONLY WRITER in the installer. All tasks call fs.sh and
6
+ # template.sh — never write directly.
7
+ #
8
+ # Provides:
9
+ # apply_run SPEC_FILE
10
+ # Read spec, run doctor (unless skip_doctor), execute task sequence,
11
+ # write marker on success.
12
+ #
13
+ # apply_task TASK_NAME TARGET_DIR
14
+ # Source and execute a single task from scripts/install/tasks/TASK.sh
15
+ #
16
+ # Bash 3.2 compatible. No set -euo pipefail here.
17
+ # =============================================================================
18
+ [[ -n "${_HAS_APPLY_LIB:-}" ]] && return 0
19
+ _HAS_APPLY_LIB=1
20
+
21
+ # Directory where task modules live
22
+ _APPLY_TASKS_DIR="${_APPLY_TASKS_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)/tasks}"
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # apply_task TASK TARGET_DIR — source and run one task
26
+ # ---------------------------------------------------------------------------
27
+ apply_task() {
28
+ local task="$1"
29
+ local target="$2"
30
+ local task_file="${_APPLY_TASKS_DIR}/${task}.sh"
31
+
32
+ if [[ ! -f "$task_file" ]]; then
33
+ log_warning "apply_task: task not found: $task (skipping)"
34
+ return 0
35
+ fi
36
+
37
+ log_step "Task: $task"
38
+ # shellcheck source=/dev/null
39
+ source "$task_file"
40
+
41
+ local fn_name="task_${task}_run"
42
+ if [[ "$(type -t "$fn_name")" != "function" ]]; then
43
+ log_error "apply_task: task file $task_file does not define $fn_name"
44
+ log_step_done
45
+ return 1
46
+ fi
47
+
48
+ "$fn_name" "$target"
49
+ local ret=$?
50
+ log_step_done
51
+
52
+ return $ret
53
+ }
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # apply_run SPEC_FILE
57
+ # ---------------------------------------------------------------------------
58
+ apply_run() {
59
+ local spec_file="$1"
60
+
61
+ if [[ ! -f "$spec_file" ]]; then
62
+ log_error "apply_run: spec file not found: $spec_file"
63
+ return 1
64
+ fi
65
+
66
+ # Load spec into SPEC_* globals
67
+ spec_read "$spec_file"
68
+
69
+ local target="${SPEC_TARGET_DIR:-}"
70
+ if [[ -z "$target" ]]; then
71
+ log_error "apply_run: spec.target_dir is empty"
72
+ return 1
73
+ fi
74
+
75
+ # Sync option globals → fs.sh and prompt.sh
76
+ [[ "$SPEC_OPT_DRY_RUN" == "true" ]] && _FS_DRY_RUN=1 || _FS_DRY_RUN=0
77
+ [[ "$SPEC_OPT_FORCE" == "true" ]] && _FS_FORCE=1 || _FS_FORCE=0
78
+ [[ "$SPEC_OPT_BACKUP" == "false" ]] && _FS_BACKUP=0 || _FS_BACKUP=1
79
+ [[ "$SPEC_OPT_VERBOSE" == "true" ]] && _LOG_VERBOSE=1 || _LOG_VERBOSE=0
80
+ [[ "$SPEC_OPT_NON_INTERACTIVE" == "true" ]] && _PROMPT_NON_INTERACTIVE=1 || _PROMPT_NON_INTERACTIVE=0
81
+ [[ "$SPEC_OPT_AUTO_ACCEPT" == "true" ]] && _PROMPT_AUTO_ACCEPT=1 || _PROMPT_AUTO_ACCEPT=0
82
+ [[ "$SPEC_OPT_OUTPUT" == "json" ]] && _LOG_OUTPUT="json" || _LOG_OUTPUT="human"
83
+
84
+ export _FS_DRY_RUN _FS_FORCE _FS_BACKUP _LOG_VERBOSE \
85
+ _PROMPT_NON_INTERACTIVE _PROMPT_AUTO_ACCEPT _LOG_OUTPUT
86
+
87
+ # Propagate spec → template globals
88
+ SITE_TITLE="${SPEC_SITE_TITLE:-My Jekyll Site}"
89
+ SITE_DESCRIPTION="${SPEC_SITE_DESCRIPTION:-}"
90
+ SITE_AUTHOR="${SPEC_SITE_AUTHOR:-}"
91
+ SITE_EMAIL="${SPEC_SITE_EMAIL:-}"
92
+ SITE_URL="${SPEC_SITE_URL:-}"
93
+ SITE_TIMEZONE="${SPEC_SITE_TIMEZONE:-UTC}"
94
+ SITE_LOCALE="${SPEC_SITE_LOCALE:-en}"
95
+ GITHUB_USER="${SPEC_GITHUB_USER:-}"
96
+ GITHUB_REPO="${SPEC_GITHUB_REPO:-}"
97
+ GITHUB_PAGES_BRANCH="${SPEC_GITHUB_PAGES_BRANCH:-gh-pages}"
98
+ REPOSITORY_NAME="${SPEC_GITHUB_REPO:-${REPOSITORY_NAME:-}}"
99
+ THEME_SOURCE="${SPEC_THEME_SOURCE:-gem}"
100
+ INSTALL_PROFILE="${SPEC_PROFILE:-default}"
101
+ export SITE_TITLE SITE_DESCRIPTION SITE_AUTHOR SITE_EMAIL SITE_URL \
102
+ SITE_TIMEZONE SITE_LOCALE GITHUB_USER GITHUB_REPO \
103
+ GITHUB_PAGES_BRANCH REPOSITORY_NAME THEME_SOURCE INSTALL_PROFILE
104
+
105
+ log_banner "zer0-mistakes installer — applying spec"
106
+ log_info "Target : $target"
107
+ log_info "Profile: ${SPEC_PROFILE:-default}"
108
+ log_info "Tasks : ${SPEC_TASKS}"
109
+ [[ -n "${SPEC_DEPLOY:-}" ]] && log_info "Deploy : $SPEC_DEPLOY"
110
+ [[ -n "${SPEC_AGENTS:-}" ]] && log_info "Agents : $SPEC_AGENTS"
111
+ [[ "$_FS_DRY_RUN" == "1" ]] && log_warning "DRY RUN — no files will be written"
112
+
113
+ # Ensure target directory exists
114
+ if [[ "$(type -t fs_ensure_dir)" == "function" ]]; then
115
+ fs_ensure_dir "$target"
116
+ else
117
+ mkdir -p "$target"
118
+ fi
119
+
120
+ # Run doctor unless skipped
121
+ if [[ "$SPEC_OPT_SKIP_DOCTOR" != "true" ]]; then
122
+ if [[ "$(type -t doctor_run)" == "function" ]]; then
123
+ doctor_run "$target" || true # warn but don't abort
124
+ fi
125
+ fi
126
+
127
+ # Execute tasks in order
128
+ local failed_tasks=""
129
+ local task
130
+ for task in ${SPEC_TASKS}; do
131
+ apply_task "$task" "$target" || {
132
+ log_error "Task failed: $task"
133
+ failed_tasks="${failed_tasks} $task"
134
+ }
135
+ done
136
+
137
+ # Deploy plugins
138
+ for task in ${SPEC_DEPLOY:-}; do
139
+ apply_task "deploy_${task}" "$target" || \
140
+ log_warning "Deploy task failed: $task (continuing)"
141
+ done
142
+
143
+ # Agent files
144
+ if [[ -n "${SPEC_AGENTS:-}" ]]; then
145
+ apply_task "agents" "$target" || \
146
+ log_warning "Agents task failed (continuing)"
147
+ fi
148
+
149
+ if [[ -n "$failed_tasks" ]]; then
150
+ log_error "Some tasks failed:$failed_tasks"
151
+ return 1
152
+ fi
153
+
154
+ log_success "All tasks completed for: $target"
155
+ return 0
156
+ }