jekyll-theme-zer0 0.22.20 → 0.22.22

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.
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/ai/wizard.sh
3
+ #
4
+ # `install wizard --ai` — opt-in OpenAI-powered config generation.
5
+ #
6
+ # Flow:
7
+ # 1. ai_enabled() check (ZER0_NO_AI kill-switch)
8
+ # 2. ai_require_key() check (OPENAI_API_KEY)
9
+ # 3. Prompt user for: site description, target audience, deploy preference
10
+ # 4. Read system prompt from templates/ai/prompts/wizard-system.md
11
+ # 5. ai_estimate_cost() → user sees price estimate
12
+ # 6. ai_call_chat() → returns JSON: {title, description, tagline, navigation,
13
+ # welcome_post_outline, suggested_deploy_target}
14
+ # 7. ai_show_diff_confirm() per generated file (_config.yml, navigation,
15
+ # welcome post)
16
+ # 8. On any failure → fall back to non-AI wizard (delegated to caller).
17
+ #
18
+ # Public API:
19
+ # wizard_ai_run <target_dir> <repo_root> [--auto-accept]
20
+ #
21
+ # Returns 0 on success, 1 on failure (caller should fall back).
22
+
23
+ # shellcheck disable=SC2034
24
+ AI_WIZARD_LIB_VERSION="1.0.0"
25
+
26
+ wizard_ai_run() {
27
+ local target_dir="$1" repo_root="$2"
28
+ shift 2 || true
29
+
30
+ local auto_accept=0
31
+ while [[ $# -gt 0 ]]; do
32
+ case "$1" in
33
+ --auto-accept) auto_accept=1 ;;
34
+ *) log_warning "wizard_ai_run: ignoring unknown flag: $1" ;;
35
+ esac
36
+ shift
37
+ done
38
+
39
+ if ! ai_enabled; then
40
+ log_warning "AI is disabled (ZER0_NO_AI=1) — cannot run --ai wizard."
41
+ return 1
42
+ fi
43
+ if ! ai_require_key; then
44
+ return 1
45
+ fi
46
+
47
+ # 1. Gather user inputs
48
+ log_info "AI Wizard — describe your site in a sentence or two."
49
+ log_info "Examples: 'Personal blog about Rust performance' or 'Docs site for an open-source CLI'."
50
+ printf "Site description: "
51
+ local site_desc audience deploy_pref
52
+ read -r site_desc
53
+ if [[ -z "$site_desc" ]]; then
54
+ log_error "Empty description — aborting AI wizard."
55
+ return 1
56
+ fi
57
+ printf "Target audience (e.g., 'developers', 'data scientists', 'general'): "
58
+ read -r audience
59
+ [[ -z "$audience" ]] && audience="developers"
60
+ printf "Deploy preference (github-pages | azure-swa | docker-prod | unsure): "
61
+ read -r deploy_pref
62
+ [[ -z "$deploy_pref" ]] && deploy_pref="unsure"
63
+
64
+ # 2. Load system prompt
65
+ local sys_prompt_file="$repo_root/templates/ai/prompts/wizard-system.md"
66
+ if [[ ! -f "$sys_prompt_file" ]]; then
67
+ log_error "System prompt missing: $sys_prompt_file"
68
+ return 1
69
+ fi
70
+ local system_prompt
71
+ system_prompt="$(cat "$sys_prompt_file")"
72
+
73
+ # 3. Build user prompt
74
+ local user_prompt
75
+ user_prompt="Site description: ${site_desc}
76
+ Target audience: ${audience}
77
+ Deploy preference: ${deploy_pref}
78
+
79
+ Return ONLY a JSON object with keys: title, description, tagline, suggested_deploy_target, navigation (array of {label, url}), welcome_post_outline (string of 3-5 bullet points)."
80
+
81
+ # Sanitize (paranoid — user input could contain pasted secrets)
82
+ user_prompt="$(printf '%s' "$user_prompt" | ai_sanitize_text)"
83
+
84
+ # 4. Cost estimate + confirm
85
+ local model
86
+ model="$(ai_default_model wizard)"
87
+ local in_chars=$(( ${#system_prompt} + ${#user_prompt} ))
88
+ log_info "About to call OpenAI:"
89
+ ai_estimate_cost "$model" "$in_chars" 800
90
+ if [[ "$auto_accept" != "1" ]]; then
91
+ printf "Proceed with API call? [y/N] "
92
+ local go
93
+ read -r go
94
+ if [[ ! "$go" =~ ^[Yy]$ ]]; then
95
+ log_warning "Aborted by user."
96
+ return 1
97
+ fi
98
+ fi
99
+
100
+ # 5. Call API
101
+ log_info "Calling $model ..."
102
+ local raw
103
+ if ! raw="$(ai_call_chat "$model" "$system_prompt" "$user_prompt" 1024 0.4)"; then
104
+ log_error "OpenAI call failed."
105
+ return 1
106
+ fi
107
+
108
+ # 6. Parse JSON (strip code-fence if present)
109
+ local json
110
+ json="$(printf '%s' "$raw" | sed -E '/^```(json)?$/d')"
111
+ if ! command -v python3 >/dev/null 2>&1; then
112
+ log_error "python3 required to parse wizard response."
113
+ return 1
114
+ fi
115
+
116
+ # Extract fields safely
117
+ local title description tagline suggested_deploy
118
+ title="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("title",""))' 2>/dev/null || echo "")"
119
+ description="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("description",""))' 2>/dev/null || echo "")"
120
+ tagline="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("tagline",""))' 2>/dev/null || echo "")"
121
+ suggested_deploy="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("suggested_deploy_target",""))' 2>/dev/null || echo "")"
122
+
123
+ if [[ -z "$title" ]]; then
124
+ log_error "Failed to parse AI response (missing 'title' field)."
125
+ log_info "Raw response: $raw"
126
+ return 1
127
+ fi
128
+
129
+ # 7. Build proposed _config.yml fragment + diff
130
+ local cfg_file="$target_dir/_config.yml"
131
+ local proposed
132
+ proposed="$(cat <<EOF
133
+ # Generated by install wizard --ai
134
+ title: "$title"
135
+ description: "$description"
136
+ tagline: "$tagline"
137
+ EOF
138
+ )"
139
+ log_info "AI suggests deploy target: ${suggested_deploy:-(none)}"
140
+ if [[ -f "$cfg_file" ]]; then
141
+ log_warning "_config.yml already exists. The AI suggestions above won't be merged automatically."
142
+ log_info "Recommended: cp the values you like into _config.yml manually."
143
+ else
144
+ local accepted_path
145
+ if accepted_path="$(ai_show_diff_confirm "$cfg_file" "$proposed" "Create _config.yml" "$auto_accept")"; then
146
+ mv "$accepted_path" "$cfg_file"
147
+ log_success "Wrote $cfg_file"
148
+ fi
149
+ fi
150
+
151
+ # 8. Print full JSON for user reference (so welcome post outline + nav
152
+ # aren't lost). User can manually convert to files.
153
+ echo
154
+ log_info "Full AI response (use these to flesh out navigation + a welcome post):"
155
+ echo "─────────────────────────────────────────────────────────────"
156
+ printf '%s\n' "$json"
157
+ echo "─────────────────────────────────────────────────────────────"
158
+
159
+ return 0
160
+ }
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # =========================================================================
3
+ # scripts/lib/install/config.sh
4
+ # =========================================================================
5
+ # Configuration loader for install.sh.
6
+ #
7
+ # load_install_config <SCRIPT_DIR> [<SOURCE_DIR>]
8
+ # Searches for templates/config/install.conf in either of the supplied
9
+ # directories. On success: sources it, exports TEMPLATES_DIR, returns 0.
10
+ # On failure: applies hard-coded fallback defaults and returns 1.
11
+ #
12
+ # This function is identical in behaviour to the previous private
13
+ # `_load_install_config` inside install.sh; the only change is location +
14
+ # accepting the search roots as parameters (so the function is testable
15
+ # without globals).
16
+ # =========================================================================
17
+
18
+ load_install_config() {
19
+ local script_dir="${1:-${SCRIPT_DIR:-$(pwd)}}"
20
+ local source_dir="${2:-${SOURCE_DIR:-$script_dir}}"
21
+
22
+ local config_paths=(
23
+ "$script_dir/templates/config/install.conf"
24
+ "$source_dir/templates/config/install.conf"
25
+ )
26
+
27
+ local config_path
28
+ for config_path in "${config_paths[@]}"; do
29
+ if [[ -f "$config_path" ]]; then
30
+ # shellcheck source=/dev/null
31
+ source "$config_path"
32
+ export TEMPLATES_DIR="$(dirname "$(dirname "$config_path")")"
33
+ return 0
34
+ fi
35
+ done
36
+
37
+ # Fallback defaults when templates not available (remote install
38
+ # without bundled templates/, or stripped distribution).
39
+ export THEME_NAME="${THEME_NAME:-zer0-mistakes}"
40
+ export THEME_GEM_NAME="${THEME_GEM_NAME:-jekyll-theme-zer0}"
41
+ export THEME_DISPLAY_NAME="${THEME_DISPLAY_NAME:-Zer0-Mistakes Jekyll Theme}"
42
+ export GITHUB_USER="${GITHUB_USER:-bamr87}"
43
+ export GITHUB_REPO="${GITHUB_REPO:-bamr87/zer0-mistakes}"
44
+ export GITHUB_URL="${GITHUB_URL:-https://github.com/bamr87/zer0-mistakes}"
45
+ export GITHUB_RAW_URL="${GITHUB_RAW_URL:-https://raw.githubusercontent.com/bamr87/zer0-mistakes/main}"
46
+ export DEFAULT_PORT="${DEFAULT_PORT:-4000}"
47
+ export DEFAULT_URL="${DEFAULT_URL:-http://localhost:4000}"
48
+ export JEKYLL_VERSION="${JEKYLL_VERSION:-~> 4.3}"
49
+ export FFI_VERSION="${FFI_VERSION:-~> 1.17.0}"
50
+ export WEBRICK_VERSION="${WEBRICK_VERSION:-~> 1.7}"
51
+ export COMMONMARKER_VERSION="${COMMONMARKER_VERSION:-0.23.10}"
52
+ export GITHUB_PAGES_MAX_VERSION="${GITHUB_PAGES_MAX_VERSION:-232}"
53
+ export COMMONMARKER_MACOS_VERSION="${COMMONMARKER_MACOS_VERSION:-~> 0.23}"
54
+ export RUBY_MIN_VERSION_MACOS="${RUBY_MIN_VERSION_MACOS:-2.6.0}"
55
+ return 1
56
+ }
@@ -0,0 +1,52 @@
1
+ # scripts/lib/install/deploy/
2
+
3
+ Pluggable deploy-target modules consumed by `scripts/bin/install deploy`
4
+ (Phase 4 of the installer refactor). Each module configures one target;
5
+ the registry coordinates discovery, dispatch, and verification.
6
+
7
+ ## Files
8
+
9
+ | File | Role |
10
+ | ----------------- | ------------------------------------------------------------------- |
11
+ | `registry.sh` | Module discovery, dispatch, shared `deploy_render` / `deploy_copy`. |
12
+ | `github-pages.sh` | Actions workflow that publishes `_site/` to `gh-pages`. |
13
+ | `azure-swa.sh` | Azure SWA workflow + `staticwebapp.config.json`. |
14
+ | `docker-prod.sh` | Multi-stage Docker build + production compose + nginx config. |
15
+
16
+ ## Module contract
17
+
18
+ Every module must define:
19
+
20
+ | Symbol | Purpose |
21
+ | --------------------------------------- | -------------------------------------------------------- |
22
+ | `DEPLOY_<SLUG_UPPER>_TITLE` | One-line display name shown by `install list-targets`. |
23
+ | `DEPLOY_<SLUG_UPPER>_SUMMARY` | One-line description shown by `install list-targets`. |
24
+ | `deploy_<slug>_check_prereqs <dir>` | Print warnings; return non-zero only on hard blockers. |
25
+ | `deploy_<slug>_install <dir>` | Idempotent file install (uses `deploy_render_if_absent`).|
26
+ | `deploy_<slug>_verify <dir>` | Confirm expected files exist + look correct. |
27
+ | `deploy_<slug>_doc_url` | Print the canonical upstream documentation URL. |
28
+
29
+ Modules use the lightweight `deploy_render` placeholder set
30
+ (`{{RUBY_VERSION}}`, `{{DEFAULT_BRANCH}}`, `{{GITHUB_USER}}`,
31
+ `{{SITE_NAME}}`) so they can run without the full install.sh global
32
+ environment.
33
+
34
+ ## Adding a target
35
+
36
+ 1. Add `templates/deploy/<slug>/` with the assets (workflow YAML,
37
+ Dockerfile, README, etc.). Use `*.template` for files that need
38
+ variable substitution.
39
+ 2. Create `scripts/lib/install/deploy/<slug>.sh` exporting the four
40
+ hooks above.
41
+ 3. Add `<slug>` to `DEPLOY_TARGETS_LIST` in `registry.sh` (alphabetical).
42
+ 4. Optionally reference `<slug>` under `deploy_targets:` in
43
+ `templates/profiles/*.yml` so the profile can suggest it.
44
+ 5. Update `templates/deploy/README.md` with the new target row.
45
+
46
+ ## CLI integration
47
+
48
+ ```bash
49
+ ./scripts/bin/install list-targets
50
+ ./scripts/bin/install deploy github-pages /path/to/site
51
+ ./scripts/bin/install deploy azure-swa,docker-prod /path/to/site
52
+ ```
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/deploy/azure-swa.sh
3
+ #
4
+ # Deploy module: Azure Static Web Apps. Replaces the legacy
5
+ # install.sh::create_azure_static_web_apps_workflow heredoc and adds
6
+ # a sensible staticwebapp.config.json.
7
+
8
+ DEPLOY_AZURE_SWA_TITLE="Azure Static Web Apps"
9
+ DEPLOY_AZURE_SWA_SUMMARY="Workflow + config for Azure SWA. Requires AZURE_STATIC_WEB_APPS_API_TOKEN secret."
10
+
11
+ deploy_azure_swa_check_prereqs() {
12
+ local target_dir="$1"
13
+ if [ ! -f "$target_dir/Gemfile" ]; then
14
+ log_warning "Gemfile not found in $target_dir — Azure build step will fail until one exists."
15
+ fi
16
+ return 0
17
+ }
18
+
19
+ deploy_azure_swa_install() {
20
+ local target_dir="$1"
21
+ local repo_root="${REPO_ROOT:-$(deploy_repo_root)}"
22
+ local src_dir="$repo_root/templates/deploy/azure-swa"
23
+
24
+ deploy_render_if_absent \
25
+ "$src_dir/azure-static-web-apps.yml.template" \
26
+ "$target_dir/.github/workflows/azure-static-web-apps.yml"
27
+
28
+ deploy_copy \
29
+ "$src_dir/staticwebapp.config.json" \
30
+ "$target_dir/staticwebapp.config.json"
31
+ }
32
+
33
+ deploy_azure_swa_verify() {
34
+ local target_dir="$1"
35
+ local wf="$target_dir/.github/workflows/azure-static-web-apps.yml"
36
+ local cfg="$target_dir/staticwebapp.config.json"
37
+ local ok=0
38
+ [ -f "$wf" ] || { log_error "Missing $wf"; ok=1; }
39
+ [ -f "$cfg" ] || { log_error "Missing $cfg"; ok=1; }
40
+ [ "$ok" = "0" ] || return 1
41
+ grep -q 'Azure/static-web-apps-deploy' "$wf" || {
42
+ log_warning "Workflow does not reference Azure/static-web-apps-deploy"
43
+ return 1
44
+ }
45
+ return 0
46
+ }
47
+
48
+ deploy_azure_swa_doc_url() {
49
+ echo "https://learn.microsoft.com/azure/static-web-apps/"
50
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/deploy/docker-prod.sh
3
+ #
4
+ # Deploy module: self-hosted production Docker (multi-stage Ruby + Nginx).
5
+ # Installs docker/Dockerfile.prod, docker-compose.prod.yml, docker/nginx.conf,
6
+ # and (if absent) a .dockerignore tuned for the build context.
7
+
8
+ DEPLOY_DOCKER_PROD_TITLE="Self-hosted production Docker"
9
+ DEPLOY_DOCKER_PROD_SUMMARY="Two-stage build (Ruby builder + nginx:alpine runtime) with healthcheck + compose."
10
+
11
+ deploy_docker_prod_check_prereqs() {
12
+ local target_dir="$1"
13
+ if ! command -v docker >/dev/null 2>&1; then
14
+ log_warning "docker CLI not found in PATH — files will be installed but you cannot build the image locally."
15
+ fi
16
+ if [ ! -f "$target_dir/Gemfile" ]; then
17
+ log_warning "Gemfile not found in $target_dir — Docker build will fail until one exists."
18
+ fi
19
+ return 0
20
+ }
21
+
22
+ deploy_docker_prod_install() {
23
+ local target_dir="$1"
24
+ local repo_root="${REPO_ROOT:-$(deploy_repo_root)}"
25
+ local src_dir="$repo_root/templates/deploy/docker-prod"
26
+
27
+ DEPLOY_SITE_NAME="${DEPLOY_SITE_NAME:-$(basename "$target_dir")}"
28
+
29
+ deploy_render_if_absent \
30
+ "$src_dir/Dockerfile.prod.template" \
31
+ "$target_dir/docker/Dockerfile.prod"
32
+
33
+ deploy_render_if_absent \
34
+ "$src_dir/docker-compose.prod.yml.template" \
35
+ "$target_dir/docker-compose.prod.yml"
36
+
37
+ deploy_copy \
38
+ "$src_dir/nginx.conf" \
39
+ "$target_dir/docker/nginx.conf"
40
+
41
+ # .dockerignore: only install when missing so we never clobber user rules.
42
+ if [ ! -f "$target_dir/.dockerignore" ]; then
43
+ deploy_copy "$src_dir/.dockerignore" "$target_dir/.dockerignore"
44
+ else
45
+ log_warning ".dockerignore already exists, leaving untouched."
46
+ fi
47
+ }
48
+
49
+ deploy_docker_prod_verify() {
50
+ local target_dir="$1"
51
+ local ok=0
52
+ for f in \
53
+ "$target_dir/docker/Dockerfile.prod" \
54
+ "$target_dir/docker-compose.prod.yml" \
55
+ "$target_dir/docker/nginx.conf"; do
56
+ if [ ! -f "$f" ]; then
57
+ log_error "Missing $f"
58
+ ok=1
59
+ fi
60
+ done
61
+ [ "$ok" = "0" ] || return 1
62
+ grep -q 'nginx:alpine' "$target_dir/docker/Dockerfile.prod" || {
63
+ log_warning "Dockerfile.prod is missing the nginx:alpine runtime stage"
64
+ return 1
65
+ }
66
+ return 0
67
+ }
68
+
69
+ deploy_docker_prod_doc_url() {
70
+ echo "https://docs.docker.com/compose/production/"
71
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/deploy/github-pages.sh
3
+ #
4
+ # Deploy module: GitHub Pages (Actions-based, peaceiris/actions-gh-pages).
5
+ # Generates .github/workflows/jekyll-gh-pages.yml.
6
+
7
+ DEPLOY_GITHUB_PAGES_TITLE="GitHub Pages (Actions)"
8
+ DEPLOY_GITHUB_PAGES_SUMMARY="Builds with Bundler and publishes _site/ to the gh-pages branch."
9
+
10
+ deploy_github_pages_check_prereqs() {
11
+ local target_dir="$1"
12
+ if [ ! -f "$target_dir/_config.yml" ]; then
13
+ log_warning "_config.yml not found in $target_dir — workflow will still install but may not build."
14
+ fi
15
+ return 0
16
+ }
17
+
18
+ deploy_github_pages_install() {
19
+ local target_dir="$1"
20
+ local repo_root="${REPO_ROOT:-$(deploy_repo_root)}"
21
+ local src="$repo_root/templates/deploy/github-pages/jekyll-gh-pages.yml.template"
22
+ local dest="$target_dir/.github/workflows/jekyll-gh-pages.yml"
23
+
24
+ DEPLOY_SITE_NAME="${DEPLOY_SITE_NAME:-$(basename "$target_dir")}"
25
+ deploy_render_if_absent "$src" "$dest"
26
+ }
27
+
28
+ deploy_github_pages_verify() {
29
+ local target_dir="$1"
30
+ local f="$target_dir/.github/workflows/jekyll-gh-pages.yml"
31
+ if [ ! -f "$f" ]; then
32
+ log_error "Expected $f not present"
33
+ return 1
34
+ fi
35
+ grep -q 'peaceiris/actions-gh-pages' "$f" || {
36
+ log_warning "Workflow does not reference peaceiris/actions-gh-pages"
37
+ return 1
38
+ }
39
+ return 0
40
+ }
41
+
42
+ deploy_github_pages_doc_url() {
43
+ echo "https://docs.github.com/pages"
44
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/deploy/registry.sh
3
+ #
4
+ # Discovery + dispatch helpers for deploy target modules.
5
+ #
6
+ # Each module under scripts/lib/install/deploy/<slug>.sh must define the
7
+ # four hooks below (the registry verifies presence after sourcing):
8
+ #
9
+ # deploy_<slug>_check_prereqs <target_dir>
10
+ # Print warnings / errors. Return 0 if safe to proceed.
11
+ #
12
+ # deploy_<slug>_install <target_dir>
13
+ # Render templates / copy files into <target_dir>. Idempotent.
14
+ #
15
+ # deploy_<slug>_verify <target_dir>
16
+ # Confirm the install produced the expected files. Return 0 on OK.
17
+ #
18
+ # deploy_<slug>_doc_url
19
+ # Print a single URL pointing at upstream documentation.
20
+ #
21
+ # A target's display name + one-line description live next to the module
22
+ # in scripts/lib/install/deploy/<slug>.sh as `DEPLOY_<SLUG_UPPER>_TITLE`
23
+ # and `DEPLOY_<SLUG_UPPER>_SUMMARY` (sourced via `eval`).
24
+ #
25
+ # Bash 3.2 compatible. No associative arrays, no mapfile.
26
+
27
+ # Canonical list of supported targets (alphabetical).
28
+ DEPLOY_TARGETS_LIST="azure-swa docker-prod github-pages"
29
+
30
+ # Resolve REPO_ROOT lazily so callers can override via $1.
31
+ deploy_repo_root() {
32
+ if [ -n "${REPO_ROOT:-}" ]; then
33
+ echo "$REPO_ROOT"
34
+ return 0
35
+ fi
36
+ local here
37
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
38
+ ( cd "$here/../../.." && pwd )
39
+ }
40
+
41
+ deploy_targets_dir() {
42
+ local repo_root="${1:-$(deploy_repo_root)}"
43
+ echo "$repo_root/templates/deploy"
44
+ }
45
+
46
+ deploy_modules_dir() {
47
+ local repo_root="${1:-$(deploy_repo_root)}"
48
+ echo "$repo_root/scripts/lib/install/deploy"
49
+ }
50
+
51
+ deploy_target_known() {
52
+ local slug="$1" t
53
+ for t in $DEPLOY_TARGETS_LIST; do
54
+ [ "$t" = "$slug" ] && return 0
55
+ done
56
+ return 1
57
+ }
58
+
59
+ # Convert kebab-case to function-name fragment: github-pages -> github_pages
60
+ deploy_slug_fn() {
61
+ echo "$1" | tr '-' '_'
62
+ }
63
+
64
+ # Convert kebab-case to upper var fragment: github-pages -> GITHUB_PAGES
65
+ deploy_slug_var() {
66
+ echo "$1" | tr '[:lower:]-' '[:upper:]_'
67
+ }
68
+
69
+ # Source a target module (idempotent). Sets DEPLOY_LAST_LOADED on success.
70
+ deploy_load_module() {
71
+ local slug="$1"
72
+ local repo_root="${2:-$(deploy_repo_root)}"
73
+ local module="$(deploy_modules_dir "$repo_root")/${slug}.sh"
74
+ if [ ! -f "$module" ]; then
75
+ log_error "Deploy module not found: $module"
76
+ return 1
77
+ fi
78
+ # shellcheck disable=SC1090
79
+ . "$module"
80
+ DEPLOY_LAST_LOADED="$slug"
81
+ }
82
+
83
+ # Print one-line summary for `install list-targets`.
84
+ deploy_print_summary() {
85
+ local slug="$1"
86
+ local repo_root="${2:-$(deploy_repo_root)}"
87
+ deploy_load_module "$slug" "$repo_root" >/dev/null 2>&1 || return 0
88
+ local var_frag title summary
89
+ var_frag="$(deploy_slug_var "$slug")"
90
+ eval "title=\${DEPLOY_${var_frag}_TITLE:-$slug}"
91
+ eval "summary=\${DEPLOY_${var_frag}_SUMMARY:-(no summary)}"
92
+ printf ' %-13s %s\n' "$slug" "$title"
93
+ printf ' %s\n' "$summary"
94
+ }
95
+
96
+ # Run the four hooks for a single target.
97
+ deploy_run_target() {
98
+ local slug="$1" target_dir="$2"
99
+ local repo_root="${3:-$(deploy_repo_root)}"
100
+ local fn
101
+
102
+ if ! deploy_target_known "$slug"; then
103
+ log_error "Unknown deploy target: $slug"
104
+ log_info "Available targets: $DEPLOY_TARGETS_LIST"
105
+ return 1
106
+ fi
107
+
108
+ if [ ! -d "$target_dir" ]; then
109
+ log_error "Target directory does not exist: $target_dir"
110
+ return 1
111
+ fi
112
+
113
+ deploy_load_module "$slug" "$repo_root" || return 1
114
+ fn="$(deploy_slug_fn "$slug")"
115
+
116
+ log_info "▶ Configuring deploy target: $slug"
117
+
118
+ if ! "deploy_${fn}_check_prereqs" "$target_dir"; then
119
+ log_error "Prerequisite check failed for $slug"
120
+ return 1
121
+ fi
122
+
123
+ if ! "deploy_${fn}_install" "$target_dir"; then
124
+ log_error "Install step failed for $slug"
125
+ return 1
126
+ fi
127
+
128
+ if ! "deploy_${fn}_verify" "$target_dir"; then
129
+ log_warning "Verification reported issues for $slug (manual review recommended)"
130
+ else
131
+ log_success "Deploy target $slug installed successfully"
132
+ fi
133
+
134
+ local url
135
+ url="$("deploy_${fn}_doc_url" 2>/dev/null || true)"
136
+ [ -n "$url" ] && log_info "Documentation: $url"
137
+ }
138
+
139
+ # Lightweight renderer used by all deploy modules. Operates on a small,
140
+ # explicit allow-list of placeholders so modules don't need to set up
141
+ # install.sh's full global environment.
142
+ #
143
+ # Usage: deploy_render <template_file> <output_file>
144
+ # Variables consulted (with defaults):
145
+ # DEPLOY_RUBY_VERSION (default 3.3)
146
+ # DEPLOY_DEFAULT_BRANCH (default main)
147
+ # DEPLOY_GITHUB_USER (default $GITHUB_USER, then $USER, then "me")
148
+ # DEPLOY_SITE_NAME (default basename of target dir, then "site")
149
+ deploy_render() {
150
+ local src="$1" dest="$2"
151
+ [ -f "$src" ] || { log_error "Template not found: $src"; return 1; }
152
+
153
+ local ruby_v branch user site
154
+ ruby_v="${DEPLOY_RUBY_VERSION:-3.3}"
155
+ branch="${DEPLOY_DEFAULT_BRANCH:-main}"
156
+ user="${DEPLOY_GITHUB_USER:-${GITHUB_USER:-${USER:-me}}}"
157
+ site="${DEPLOY_SITE_NAME:-site}"
158
+
159
+ mkdir -p "$(dirname "$dest")"
160
+ sed \
161
+ -e "s|{{RUBY_VERSION}}|${ruby_v}|g" \
162
+ -e "s|{{DEFAULT_BRANCH}}|${branch}|g" \
163
+ -e "s|{{GITHUB_USER}}|${user}|g" \
164
+ -e "s|{{SITE_NAME}}|${site}|g" \
165
+ "$src" > "$dest"
166
+ }
167
+
168
+ # Copy a file verbatim (no rendering). Skips when destination exists
169
+ # unless DEPLOY_FORCE=1.
170
+ deploy_copy() {
171
+ local src="$1" dest="$2"
172
+ [ -f "$src" ] || { log_error "Source not found: $src"; return 1; }
173
+ if [ -f "$dest" ] && [ "${DEPLOY_FORCE:-0}" != "1" ]; then
174
+ log_warning "Exists, skipping: ${dest}"
175
+ return 0
176
+ fi
177
+ mkdir -p "$(dirname "$dest")"
178
+ cp "$src" "$dest"
179
+ log_info "Wrote: ${dest}"
180
+ }
181
+
182
+ # Same as deploy_copy but for rendered templates (logs accordingly).
183
+ deploy_render_if_absent() {
184
+ local src="$1" dest="$2"
185
+ if [ -f "$dest" ] && [ "${DEPLOY_FORCE:-0}" != "1" ]; then
186
+ log_warning "Exists, skipping: ${dest}"
187
+ return 0
188
+ fi
189
+ deploy_render "$src" "$dest" && log_info "Rendered: ${dest}"
190
+ }