jekyll-theme-zer0 0.22.21 → 1.0.0

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,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
+ }
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/doctor.sh
3
+ #
4
+ # `install doctor` — environment + site health check.
5
+ #
6
+ # Runs platform-specific prerequisite checks (delegating to
7
+ # scripts/platform/setup-{macos,linux,wsl}.sh in check-only mode), then
8
+ # layers zer0-mistakes-specific checks on top: gh CLI, Docker compose,
9
+ # Bundler/Jekyll versions, agent files, and OpenAI connectivity (opt-in).
10
+ #
11
+ # Output: structured pass/warn/fail report. Exits 0 when no FAIL,
12
+ # 1 when at least one FAIL.
13
+ #
14
+ # Public API:
15
+ # doctor_run <target_dir> <repo_root> [--ai] [--quiet] [--json]
16
+ #
17
+ # Bash 3.2-compatible. Pure shell — no jq required.
18
+
19
+ # shellcheck disable=SC2034
20
+ DOCTOR_LIB_VERSION="1.0.0"
21
+
22
+ # Counters (reset on every doctor_run call)
23
+ DOCTOR_PASS=0
24
+ DOCTOR_WARN=0
25
+ DOCTOR_FAIL=0
26
+ DOCTOR_REPORT=""
27
+
28
+ # Append a structured row.
29
+ # args: status (PASS|WARN|FAIL) name detail [remediation]
30
+ _doctor_row() {
31
+ local status="$1" name="$2" detail="$3" remediation="${4:-}"
32
+ case "$status" in
33
+ PASS) DOCTOR_PASS=$((DOCTOR_PASS+1)); log_success "$name: $detail" ;;
34
+ WARN) DOCTOR_WARN=$((DOCTOR_WARN+1)); log_warning "$name: $detail"; [[ -n "$remediation" ]] && log_info " → $remediation" ;;
35
+ FAIL) DOCTOR_FAIL=$((DOCTOR_FAIL+1)); log_error "$name: $detail"; [[ -n "$remediation" ]] && log_info " → $remediation" ;;
36
+ esac
37
+ DOCTOR_REPORT="${DOCTOR_REPORT}${status}|${name}|${detail}|${remediation}
38
+ "
39
+ }
40
+
41
+ # ── Platform checks ────────────────────────────────────────────────────────
42
+ _doctor_platform() {
43
+ local repo_root="$1"
44
+ local os
45
+ os="$(uname -s 2>/dev/null || echo unknown)"
46
+
47
+ local script=""
48
+ case "$os" in
49
+ Darwin) script="$repo_root/scripts/platform/setup-macos.sh" ;;
50
+ Linux)
51
+ # Distinguish WSL from native Linux
52
+ if grep -qi microsoft /proc/version 2>/dev/null; then
53
+ script="$repo_root/scripts/platform/setup-wsl.sh"
54
+ else
55
+ script="$repo_root/scripts/platform/setup-linux.sh"
56
+ fi
57
+ ;;
58
+ *)
59
+ _doctor_row WARN "Platform" "$os not directly supported" \
60
+ "Doctor will only run zer0-mistakes-specific checks"
61
+ return 0
62
+ ;;
63
+ esac
64
+
65
+ if [[ ! -f "$script" ]]; then
66
+ _doctor_row WARN "Platform script" "Not found at ${script#$repo_root/}" \
67
+ "Falling back to inline checks"
68
+ _doctor_inline_platform_checks
69
+ return 0
70
+ fi
71
+
72
+ log_info "Running platform checks: $(basename "$script")"
73
+ # Source so we can call individual check functions; suppress its main
74
+ # entrypoint by setting a guard env.
75
+ # shellcheck source=/dev/null
76
+ if ! source "$script" 2>/dev/null; then
77
+ _doctor_row WARN "Platform script" "Failed to source" \
78
+ "Falling back to inline checks"
79
+ _doctor_inline_platform_checks
80
+ return 0
81
+ fi
82
+
83
+ # Each setup script exposes check_* functions. Probe the common set.
84
+ local suffix=""
85
+ case "$os" in
86
+ Darwin) suffix="macos" ;;
87
+ Linux) suffix="linux"; grep -qi microsoft /proc/version 2>/dev/null && suffix="wsl" ;;
88
+ esac
89
+
90
+ if declare -F "check_git_${suffix}" >/dev/null 2>&1; then
91
+ if "check_git_${suffix}"; then
92
+ _doctor_row PASS "Git" "$(git --version 2>/dev/null | head -1)"
93
+ else
94
+ _doctor_row FAIL "Git" "Not installed" "Install via your package manager"
95
+ fi
96
+ fi
97
+ if declare -F "check_docker_${suffix}" >/dev/null 2>&1; then
98
+ if "check_docker_${suffix}"; then
99
+ _doctor_row PASS "Docker" "$(docker --version 2>/dev/null)"
100
+ elif command -v docker >/dev/null 2>&1; then
101
+ _doctor_row WARN "Docker" "Installed but daemon not reachable" \
102
+ "Start Docker Desktop / 'sudo systemctl start docker'"
103
+ else
104
+ _doctor_row WARN "Docker" "Not installed" \
105
+ "Optional — only needed for containerized dev"
106
+ fi
107
+ fi
108
+ if declare -F "check_ruby_${suffix}" >/dev/null 2>&1; then
109
+ if "check_ruby_${suffix}"; then
110
+ _doctor_row PASS "Ruby" "$(ruby --version 2>/dev/null)"
111
+ elif command -v ruby >/dev/null 2>&1; then
112
+ _doctor_row WARN "Ruby" "$(ruby --version 2>/dev/null) (3.0+ recommended)" \
113
+ "Upgrade via Homebrew/rbenv for best compatibility"
114
+ else
115
+ _doctor_row WARN "Ruby" "Not installed" \
116
+ "Optional — only needed for native (non-Docker) dev"
117
+ fi
118
+ fi
119
+ }
120
+
121
+ # Fallback when platform script is missing.
122
+ _doctor_inline_platform_checks() {
123
+ if command -v git >/dev/null 2>&1; then
124
+ _doctor_row PASS "Git" "$(git --version 2>/dev/null | head -1)"
125
+ else
126
+ _doctor_row FAIL "Git" "Not installed" "Required for cloning + version control"
127
+ fi
128
+ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
129
+ _doctor_row PASS "Docker" "$(docker --version 2>/dev/null)"
130
+ else
131
+ _doctor_row WARN "Docker" "Not running or not installed" \
132
+ "Optional — needed for containerized dev"
133
+ fi
134
+ if command -v ruby >/dev/null 2>&1; then
135
+ _doctor_row PASS "Ruby" "$(ruby --version 2>/dev/null)"
136
+ else
137
+ _doctor_row WARN "Ruby" "Not installed" "Optional — needed for native dev"
138
+ fi
139
+ }
140
+
141
+ # ── Tooling checks ─────────────────────────────────────────────────────────
142
+ _doctor_tooling() {
143
+ # gh CLI (used by deploy + agent flows that touch repos)
144
+ if command -v gh >/dev/null 2>&1; then
145
+ _doctor_row PASS "GitHub CLI" "$(gh --version 2>/dev/null | head -1)"
146
+ else
147
+ _doctor_row WARN "GitHub CLI" "Not installed" \
148
+ "Optional — install from https://cli.github.com for repo automation"
149
+ fi
150
+
151
+ # Docker Compose v2
152
+ if docker compose version >/dev/null 2>&1; then
153
+ _doctor_row PASS "Docker Compose" "$(docker compose version --short 2>/dev/null)"
154
+ elif command -v docker-compose >/dev/null 2>&1; then
155
+ _doctor_row WARN "Docker Compose" "v1 detected ($(docker-compose --version 2>/dev/null))" \
156
+ "Upgrade to v2: docker compose plugin"
157
+ else
158
+ _doctor_row WARN "Docker Compose" "Not available" \
159
+ "Optional — needed for 'docker compose up'"
160
+ fi
161
+
162
+ # Bundler
163
+ if command -v bundle >/dev/null 2>&1; then
164
+ # bundle --version may fail when run inside a dir with a Gemfile.lock
165
+ # pinning a different bundler. Probe in a neutral cwd to avoid noise.
166
+ local bv
167
+ bv="$(cd / && bundle --version 2>/dev/null | head -1)"
168
+ if [[ -n "$bv" ]]; then
169
+ _doctor_row PASS "Bundler" "$bv"
170
+ else
171
+ _doctor_row WARN "Bundler" "Installed but version probe failed" \
172
+ "Likely Gemfile.lock pins an unavailable bundler — run 'bundle update --bundler'"
173
+ fi
174
+ else
175
+ _doctor_row WARN "Bundler" "Not installed" \
176
+ "Install: gem install bundler (only needed for native dev)"
177
+ fi
178
+ }
179
+
180
+ # ── Site checks (run inside target_dir) ────────────────────────────────────
181
+ _doctor_site() {
182
+ local target_dir="$1"
183
+
184
+ if [[ ! -d "$target_dir" ]]; then
185
+ _doctor_row FAIL "Target dir" "Does not exist: $target_dir" "Run 'install init' first"
186
+ return
187
+ fi
188
+
189
+ if [[ -f "$target_dir/_config.yml" ]]; then
190
+ _doctor_row PASS "_config.yml" "Present"
191
+ else
192
+ _doctor_row WARN "_config.yml" "Missing in $target_dir" \
193
+ "Run 'install init' to scaffold a site"
194
+ fi
195
+
196
+ if [[ -f "$target_dir/Gemfile" ]]; then
197
+ _doctor_row PASS "Gemfile" "Present"
198
+ else
199
+ _doctor_row WARN "Gemfile" "Missing" "Run 'install init' or 'install deploy'"
200
+ fi
201
+
202
+ if [[ -f "$target_dir/AGENTS.md" ]]; then
203
+ _doctor_row PASS "AI agent files" "AGENTS.md present"
204
+ else
205
+ _doctor_row WARN "AI agent files" "AGENTS.md not installed" \
206
+ "Run 'install agents' to add AI guidance"
207
+ fi
208
+
209
+ # Basic _config.yml sanity
210
+ if [[ -f "$target_dir/_config.yml" ]]; then
211
+ if grep -qE "^(remote_theme|theme):" "$target_dir/_config.yml" 2>/dev/null; then
212
+ _doctor_row PASS "Theme config" "remote_theme/theme set in _config.yml"
213
+ else
214
+ _doctor_row WARN "Theme config" "Neither 'theme' nor 'remote_theme' found" \
215
+ "Set 'remote_theme: bamr87/zer0-mistakes' or 'theme: jekyll-theme-zer0'"
216
+ fi
217
+ fi
218
+ }
219
+
220
+ # ── AI connectivity (opt-in) ───────────────────────────────────────────────
221
+ _doctor_ai_connectivity() {
222
+ if [[ "${ZER0_NO_AI:-0}" = "1" ]]; then
223
+ _doctor_row WARN "OpenAI connectivity" "Skipped (ZER0_NO_AI=1)" \
224
+ "Unset ZER0_NO_AI to enable AI checks"
225
+ return
226
+ fi
227
+ if [[ -z "${OPENAI_API_KEY:-}" ]]; then
228
+ _doctor_row WARN "OpenAI API key" "OPENAI_API_KEY not set" \
229
+ "Export OPENAI_API_KEY to enable AI features"
230
+ return
231
+ fi
232
+ # Lightweight ping: check we can reach the API and the key authenticates.
233
+ local http_code
234
+ http_code="$(curl -sS -o /dev/null -w '%{http_code}' \
235
+ --max-time 10 \
236
+ -H "Authorization: Bearer ${OPENAI_API_KEY}" \
237
+ https://api.openai.com/v1/models 2>/dev/null || echo "000")"
238
+ case "$http_code" in
239
+ 200) _doctor_row PASS "OpenAI connectivity" "Authenticated (HTTP 200)" ;;
240
+ 401) _doctor_row FAIL "OpenAI connectivity" "Authentication failed (HTTP 401)" \
241
+ "Verify OPENAI_API_KEY is valid" ;;
242
+ 000) _doctor_row WARN "OpenAI connectivity" "No network response" \
243
+ "Check internet connection / firewall" ;;
244
+ *) _doctor_row WARN "OpenAI connectivity" "Unexpected response (HTTP $http_code)" \
245
+ "Inspect with: curl -v https://api.openai.com/v1/models" ;;
246
+ esac
247
+ }
248
+
249
+ # ── Public entrypoint ──────────────────────────────────────────────────────
250
+ doctor_run() {
251
+ local target_dir="$1" repo_root="$2"
252
+ shift 2 || true
253
+
254
+ local check_ai=0 quiet=0 emit_json=0
255
+ while [[ $# -gt 0 ]]; do
256
+ case "$1" in
257
+ --ai) check_ai=1 ;;
258
+ --quiet) quiet=1 ;;
259
+ --json) emit_json=1 ;;
260
+ *) log_warning "doctor_run: ignoring unknown flag: $1" ;;
261
+ esac
262
+ shift
263
+ done
264
+
265
+ DOCTOR_PASS=0; DOCTOR_WARN=0; DOCTOR_FAIL=0; DOCTOR_REPORT=""
266
+
267
+ [[ "$quiet" = "1" ]] || log_info "🩺 Running zer0-mistakes doctor..."
268
+ [[ "$quiet" = "1" ]] || echo
269
+
270
+ [[ "$quiet" = "1" ]] || log_info "── Platform ─────────────────────────────"
271
+ _doctor_platform "$repo_root"
272
+ [[ "$quiet" = "1" ]] || echo
273
+
274
+ [[ "$quiet" = "1" ]] || log_info "── Tooling ──────────────────────────────"
275
+ _doctor_tooling
276
+ [[ "$quiet" = "1" ]] || echo
277
+
278
+ [[ "$quiet" = "1" ]] || log_info "── Site ($target_dir) ───"
279
+ _doctor_site "$target_dir"
280
+ [[ "$quiet" = "1" ]] || echo
281
+
282
+ if [[ "$check_ai" = "1" ]]; then
283
+ [[ "$quiet" = "1" ]] || log_info "── AI ────────────────────────────────────"
284
+ _doctor_ai_connectivity
285
+ [[ "$quiet" = "1" ]] || echo
286
+ fi
287
+
288
+ if [[ "$emit_json" = "1" ]]; then
289
+ # Emit a tiny JSON summary — no jq, just printf.
290
+ printf '{"pass":%d,"warn":%d,"fail":%d}\n' \
291
+ "$DOCTOR_PASS" "$DOCTOR_WARN" "$DOCTOR_FAIL"
292
+ else
293
+ log_info "── Summary ──────────────────────────────"
294
+ log_info " ✅ PASS: $DOCTOR_PASS"
295
+ log_info " ⚠️ WARN: $DOCTOR_WARN"
296
+ log_info " ❌ FAIL: $DOCTOR_FAIL"
297
+ fi
298
+
299
+ [[ "$DOCTOR_FAIL" = "0" ]] && return 0
300
+ return 1
301
+ }