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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +294 -23
- data/scripts/bin/install +717 -0
- data/scripts/bin/test +45 -2
- data/scripts/lib/changelog.sh +24 -17
- data/scripts/lib/install/README.md +63 -0
- data/scripts/lib/install/agents.sh +166 -0
- data/scripts/lib/install/ai/diagnose.sh +199 -0
- data/scripts/lib/install/ai/openai.sh +233 -0
- data/scripts/lib/install/ai/suggest.sh +182 -0
- data/scripts/lib/install/ai/wizard.sh +160 -0
- data/scripts/lib/install/config.sh +56 -0
- data/scripts/lib/install/deploy/README.md +52 -0
- data/scripts/lib/install/deploy/azure-swa.sh +50 -0
- data/scripts/lib/install/deploy/docker-prod.sh +71 -0
- data/scripts/lib/install/deploy/github-pages.sh +44 -0
- data/scripts/lib/install/deploy/registry.sh +190 -0
- data/scripts/lib/install/doctor.sh +301 -0
- data/scripts/lib/install/fs.sh +52 -0
- data/scripts/lib/install/logging.sh +33 -0
- data/scripts/lib/install/pages.sh +255 -0
- data/scripts/lib/install/platform.sh +71 -0
- data/scripts/lib/install/profile.sh +113 -0
- data/scripts/lib/install/template.sh +137 -0
- data/scripts/lib/install/upgrade.sh +184 -0
- data/scripts/lib/install/wizard_interactive.sh +189 -0
- data/scripts/test/lib/run_tests.sh +1 -0
- data/scripts/test/lib/test_analyze_commits.sh +109 -0
- data/scripts/utils/analyze-commits +37 -17
- metadata +25 -2
|
@@ -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
|
+
}
|