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,71 @@
1
+ #!/bin/bash
2
+ # =========================================================================
3
+ # scripts/lib/install/platform.sh
4
+ # =========================================================================
5
+ # Platform / Ruby detection helpers used by install.sh.
6
+ #
7
+ # Bash 3.2-compatible — no `declare -A`, no `=~` capture groups.
8
+ #
9
+ # Functions exported:
10
+ # detect_os -> Darwin | Linux | CYGWIN | MINGW | unknown
11
+ # detect_ruby_version -> e.g. "2.6.8" or "none"
12
+ # ruby_version_lt_27 -> exit 0 when ruby < 2.7
13
+ # needs_macos_gemfile -> exit 0 when macOS + ruby < 2.7
14
+ # detect_platform -> auto | wsl | macos | linux | unknown
15
+ # =========================================================================
16
+
17
+ # Returns: Darwin | Linux | CYGWIN | MINGW | unknown
18
+ detect_os() {
19
+ uname -s 2>/dev/null || echo "unknown"
20
+ }
21
+
22
+ # Returns the ruby version string (e.g. "2.6.8"), or "none" if ruby is absent.
23
+ detect_ruby_version() {
24
+ if ! command -v ruby >/dev/null 2>&1; then
25
+ echo "none"
26
+ return
27
+ fi
28
+ # ruby --version prints: ruby 2.6.8p205 (2021-07-07 ...) [platform]
29
+ # We want "2.6.8" — strip the trailing pNNN patch indicator via sed.
30
+ ruby --version 2>/dev/null | awk '{print $2}' | sed 's/p[0-9]*//' | sed 's/-.*//' | tr -d '\r'
31
+ }
32
+
33
+ # Returns 0 (true) if the current Ruby version is < 2.7.0, 1 (false) otherwise.
34
+ # Uses awk so arithmetic is safe even with partial version strings.
35
+ ruby_version_lt_27() {
36
+ local ver
37
+ ver=$(detect_ruby_version)
38
+ [ "$ver" = "none" ] && return 1 # no ruby → don't apply macOS caps
39
+ awk -v ver="$ver" 'BEGIN {
40
+ n = split(ver, a, ".")
41
+ if (a[1]+0 == 2 && a[2]+0 < 7) exit 0
42
+ exit 1
43
+ }'
44
+ }
45
+
46
+ # Returns 0 (true) when running on macOS with system Ruby < 2.7.
47
+ # This is the condition that triggers use of Gemfile.macos.template.
48
+ needs_macos_gemfile() {
49
+ local os
50
+ os=$(detect_os)
51
+ [ "$os" = "Darwin" ] && ruby_version_lt_27
52
+ }
53
+
54
+ # Detect runtime platform. Honours $PLATFORM if explicitly set.
55
+ # Returns: macos | linux | wsl | unknown
56
+ detect_platform() {
57
+ if [[ "${PLATFORM:-auto}" != "auto" ]]; then
58
+ echo "$PLATFORM"
59
+ return
60
+ fi
61
+ # WSL detection (check before generic Linux)
62
+ if grep -qiE '(microsoft|wsl)' /proc/version 2>/dev/null; then
63
+ echo "wsl"
64
+ elif [[ "$(uname -s)" == "Darwin" ]]; then
65
+ echo "macos"
66
+ elif [[ "$(uname -s)" == "Linux" ]]; then
67
+ echo "linux"
68
+ else
69
+ echo "unknown"
70
+ fi
71
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/profile.sh
3
+ #
4
+ # Minimal pure-bash YAML reader for templates/profiles/*.yml.
5
+ # Bash 3.2 compatible. Schema is intentionally simple (scalar key/value
6
+ # + flat string lists) so we don't need yq/python.
7
+ #
8
+ # Public API:
9
+ # profiles_dir <REPO_ROOT> — absolute path to profiles dir
10
+ # list_profile_names <REPO_ROOT> — newline-separated profile slugs
11
+ # profile_path <REPO_ROOT> <name> — absolute path to a profile yml
12
+ # profile_get_scalar <file> <key> — print a scalar value or empty
13
+ # profile_get_list <file> <key> — print list items, one per line
14
+ # profile_print_summary <file> — pretty-print full profile
15
+
16
+ profiles_dir() {
17
+ local repo_root="$1"
18
+ echo "$repo_root/templates/profiles"
19
+ }
20
+
21
+ list_profile_names() {
22
+ local repo_root="$1"
23
+ local dir
24
+ dir="$(profiles_dir "$repo_root")"
25
+ [ -d "$dir" ] || return 1
26
+ # List *.yml stems, sorted, excluding any leading-dot files
27
+ ( cd "$dir" && for f in *.yml; do
28
+ [ -e "$f" ] || continue
29
+ echo "${f%.yml}"
30
+ done ) | sort
31
+ }
32
+
33
+ profile_path() {
34
+ local repo_root="$1" name="$2"
35
+ local p
36
+ p="$(profiles_dir "$repo_root")/${name}.yml"
37
+ [ -f "$p" ] || return 1
38
+ echo "$p"
39
+ }
40
+
41
+ # Extract a scalar value: lines like `key: value`. Strips surrounding
42
+ # whitespace and quotes. Ignores list lines (starting with `-`).
43
+ profile_get_scalar() {
44
+ local file="$1" key="$2"
45
+ [ -f "$file" ] || return 1
46
+ awk -v k="$key" '
47
+ # match " key: value" or "key: value" at top level (no leading dash)
48
+ $0 ~ "^[[:space:]]*" k "[[:space:]]*:" {
49
+ sub("^[[:space:]]*" k "[[:space:]]*:[[:space:]]*", "")
50
+ # strip trailing comments
51
+ sub(/[[:space:]]+#.*$/, "")
52
+ # strip surrounding quotes
53
+ gsub(/^["'\'']|["'\'']$/, "")
54
+ print
55
+ exit
56
+ }
57
+ ' "$file"
58
+ }
59
+
60
+ # Extract a flat list under a key. Returns nothing for `key: []` or missing.
61
+ profile_get_list() {
62
+ local file="$1" key="$2"
63
+ [ -f "$file" ] || return 1
64
+ awk -v k="$key" '
65
+ BEGIN { inblock = 0 }
66
+ # Start: "key:" line with nothing (or just a comment) after it
67
+ $0 ~ "^[[:space:]]*" k "[[:space:]]*:[[:space:]]*(#.*)?$" {
68
+ inblock = 1
69
+ next
70
+ }
71
+ inblock {
72
+ # End block on next non-list, non-blank, non-indented line
73
+ if ($0 ~ /^[[:space:]]*-[[:space:]]+/) {
74
+ line = $0
75
+ sub(/^[[:space:]]*-[[:space:]]+/, "", line)
76
+ sub(/[[:space:]]+#.*$/, "", line)
77
+ gsub(/^["'\'']|["'\'']$/, "", line)
78
+ print line
79
+ next
80
+ }
81
+ if ($0 ~ /^[[:space:]]*#/) next
82
+ if ($0 ~ /^[[:space:]]*$/) next
83
+ inblock = 0
84
+ }
85
+ ' "$file"
86
+ }
87
+
88
+ profile_print_summary() {
89
+ local file="$1"
90
+ [ -f "$file" ] || { echo "(profile file not found: $file)" >&2; return 1; }
91
+ local name display desc legacy rec
92
+ name="$(profile_get_scalar "$file" name)"
93
+ display="$(profile_get_scalar "$file" display_name)"
94
+ desc="$(profile_get_scalar "$file" description)"
95
+ legacy="$(profile_get_scalar "$file" legacy_flag)"
96
+ rec="$(profile_get_scalar "$file" recommended_for)"
97
+ printf ' %-9s %s\n' "$name" "$display"
98
+ [ -n "$desc" ] && printf ' %s\n' "$desc"
99
+ [ -n "$legacy" ] && printf ' → install.sh %s\n' "$legacy"
100
+ [ -n "$rec" ] && printf ' For: %s\n' "$rec"
101
+ local includes excludes
102
+ includes="$(profile_get_list "$file" includes)"
103
+ excludes="$(profile_get_list "$file" excludes)"
104
+ if [ -n "$includes" ]; then
105
+ printf ' Includes:\n'
106
+ echo "$includes" | sed 's/^/ - /'
107
+ fi
108
+ if [ -n "$excludes" ]; then
109
+ printf ' Excludes:\n'
110
+ echo "$excludes" | sed 's/^/ - /'
111
+ fi
112
+ echo
113
+ }
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ # =========================================================================
3
+ # scripts/lib/install/template.sh
4
+ # =========================================================================
5
+ # Template rendering for install.sh.
6
+ #
7
+ # Functions exported:
8
+ # render_template TEMPLATE_FILE [OUTPUT_FILE]
9
+ # Replace {{VAR_NAME}} placeholders. If OUTPUT_FILE omitted, writes
10
+ # to stdout.
11
+ #
12
+ # create_from_template TEMPLATE_REL OUTPUT_FILE [FALLBACK_CONTENT]
13
+ # Resolution order:
14
+ # 1. Local templates ($TEMPLATES_DIR/$TEMPLATE_REL)
15
+ # 2. Remote fetch from $GITHUB_RAW_URL/templates/$TEMPLATE_REL
16
+ # (only when REMOTE_INSTALL=true)
17
+ # 3. FALLBACK_CONTENT (literal string)
18
+ # Existing OUTPUT_FILE is preserved (skipped with a warning).
19
+ #
20
+ # templates_available
21
+ # Returns 0 when $TEMPLATES_DIR is set and points to an existing dir.
22
+ #
23
+ # Required globals (provided by install.sh / install.conf):
24
+ # THEME_NAME, THEME_GEM_NAME, THEME_DISPLAY_NAME,
25
+ # GITHUB_USER, GITHUB_REPO, GITHUB_URL, GITHUB_RAW_URL,
26
+ # DEFAULT_PORT, DEFAULT_URL,
27
+ # JEKYLL_VERSION, FFI_VERSION, WEBRICK_VERSION, COMMONMARKER_VERSION,
28
+ # GITHUB_PAGES_MAX_VERSION, COMMONMARKER_MACOS_VERSION,
29
+ # RUBY_MIN_VERSION_MACOS, INSTALL_MODE, REMOTE_INSTALL, TEMPLATES_DIR
30
+ #
31
+ # Optional globals (used when set):
32
+ # FORK_GITHUB_USER, FORK_SITE_NAME, FORK_AUTHOR, FORK_EMAIL,
33
+ # SITE_TITLE, SITE_DESCRIPTION, SITE_AUTHOR, SITE_EMAIL,
34
+ # REPOSITORY_NAME
35
+ # =========================================================================
36
+
37
+ # Render a template file, replacing {{VAR_NAME}} placeholders.
38
+ render_template() {
39
+ local template_file="$1"
40
+ local output_file="${2:-}"
41
+
42
+ if [[ ! -f "$template_file" ]]; then
43
+ return 1
44
+ fi
45
+
46
+ local content
47
+ content=$(cat "$template_file")
48
+
49
+ # Replace all known placeholders. Order matches the original install.sh
50
+ # implementation to guarantee identical output.
51
+ content=$(echo "$content" | sed \
52
+ -e "s|{{THEME_NAME}}|${THEME_NAME}|g" \
53
+ -e "s|{{THEME_GEM_NAME}}|${THEME_GEM_NAME}|g" \
54
+ -e "s|{{THEME_DISPLAY_NAME}}|${THEME_DISPLAY_NAME}|g" \
55
+ -e "s|{{GITHUB_USER}}|${FORK_GITHUB_USER:-$GITHUB_USER}|g" \
56
+ -e "s|{{GITHUB_REPO}}|${GITHUB_REPO}|g" \
57
+ -e "s|{{GITHUB_URL}}|${GITHUB_URL}|g" \
58
+ -e "s|{{GITHUB_RAW_URL}}|${GITHUB_RAW_URL}|g" \
59
+ -e "s|{{DEFAULT_PORT}}|${DEFAULT_PORT}|g" \
60
+ -e "s|{{DEFAULT_URL}}|${DEFAULT_URL}|g" \
61
+ -e "s|{{JEKYLL_VERSION}}|${JEKYLL_VERSION}|g" \
62
+ -e "s|{{FFI_VERSION}}|${FFI_VERSION}|g" \
63
+ -e "s|{{WEBRICK_VERSION}}|${WEBRICK_VERSION}|g" \
64
+ -e "s|{{COMMONMARKER_VERSION}}|${COMMONMARKER_VERSION}|g" \
65
+ -e "s|{{GITHUB_PAGES_MAX_VERSION}}|${GITHUB_PAGES_MAX_VERSION:-232}|g" \
66
+ -e "s|{{COMMONMARKER_MACOS_VERSION}}|${COMMONMARKER_MACOS_VERSION:-~> 0.23}|g" \
67
+ -e "s|{{RUBY_MIN_VERSION_MACOS}}|${RUBY_MIN_VERSION_MACOS:-2.6.0}|g" \
68
+ -e "s|{{SITE_TITLE}}|${FORK_SITE_NAME:-${SITE_TITLE:-My Jekyll Site}}|g" \
69
+ -e "s|{{SITE_DESCRIPTION}}|${SITE_DESCRIPTION:-A Jekyll site built with ${THEME_NAME}}|g" \
70
+ -e "s|{{SITE_AUTHOR}}|${FORK_AUTHOR:-${SITE_AUTHOR:-Site Author}}|g" \
71
+ -e "s|{{SITE_EMAIL}}|${FORK_EMAIL:-${SITE_EMAIL:-your@email.com}}|g" \
72
+ -e "s|{{CURRENT_DATE}}|$(date +%Y-%m-%d)|g" \
73
+ -e "s|{{CURRENT_YEAR}}|$(date +%Y)|g" \
74
+ -e "s|{{REPOSITORY_NAME}}|${REPOSITORY_NAME:-$THEME_NAME}|g" \
75
+ -e "s|{{RAW_GITHUB_URL}}|${GITHUB_RAW_URL}|g" \
76
+ -e "s|{{FORK_GITHUB_USER}}|${FORK_GITHUB_USER:-${GITHUB_USER}}|g" \
77
+ -e "s|{{INSTALL_MODE}}|${INSTALL_MODE:-full}|g" \
78
+ -e "s|{{GITHUB_PAGES_URL}}|https://${FORK_GITHUB_USER:-${GITHUB_USER}}.github.io/${REPOSITORY_NAME:-$THEME_NAME}|g")
79
+
80
+ if [[ -n "$output_file" ]]; then
81
+ mkdir -p "$(dirname "$output_file")"
82
+ echo "$content" > "$output_file"
83
+ else
84
+ echo "$content"
85
+ fi
86
+ }
87
+
88
+ # Create a file from template with automatic fallback to embedded content.
89
+ create_from_template() {
90
+ local template_path="$1"
91
+ local output_file="$2"
92
+ local fallback_content="${3:-}"
93
+
94
+ # Skip if output already exists
95
+ if [[ -f "$output_file" ]]; then
96
+ log_warning "$(basename "$output_file") already exists, skipping to preserve content"
97
+ return 0
98
+ fi
99
+
100
+ # Try local template first
101
+ if [[ -n "${TEMPLATES_DIR:-}" ]] && [[ -f "$TEMPLATES_DIR/$template_path" ]]; then
102
+ render_template "$TEMPLATES_DIR/$template_path" "$output_file"
103
+ log_info "Created $(basename "$output_file") from template"
104
+ return 0
105
+ fi
106
+
107
+ # Try to fetch from GitHub for remote installs
108
+ if [[ "${REMOTE_INSTALL:-false}" == "true" ]]; then
109
+ local remote_url="${GITHUB_RAW_URL}/templates/$template_path"
110
+ local remote_content
111
+ if remote_content=$(curl -fsSL "$remote_url" 2>/dev/null); then
112
+ local temp_file
113
+ temp_file=$(mktemp)
114
+ echo "$remote_content" > "$temp_file"
115
+ render_template "$temp_file" "$output_file"
116
+ rm -f "$temp_file"
117
+ log_info "Created $(basename "$output_file") from remote template"
118
+ return 0
119
+ fi
120
+ fi
121
+
122
+ # Use fallback content if provided
123
+ if [[ -n "$fallback_content" ]]; then
124
+ mkdir -p "$(dirname "$output_file")"
125
+ echo "$fallback_content" > "$output_file"
126
+ log_info "Created $(basename "$output_file") from fallback"
127
+ return 0
128
+ fi
129
+
130
+ log_warning "Could not create $(basename "$output_file") (no template or fallback)"
131
+ return 1
132
+ }
133
+
134
+ # Check if templates are available (TEMPLATES_DIR set + directory exists).
135
+ templates_available() {
136
+ [[ -n "${TEMPLATES_DIR:-}" ]] && [[ -d "$TEMPLATES_DIR" ]]
137
+ }
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/upgrade.sh
3
+ #
4
+ # `install upgrade` — version-aware migration over an existing site.
5
+ #
6
+ # Strategy:
7
+ # 1. Detect installed version from .zer0-installed (drop-marker) or
8
+ # _config.yml's `version:` field, or fall back to "unknown".
9
+ # 2. Print a diff-summary of what will change (theme files only — never
10
+ # touches user content under pages/, _posts/, _drafts/).
11
+ # 3. Re-runs the agent-files install (always safe, additive) and offers
12
+ # to refresh templated workflows under .github/workflows/.
13
+ # 4. Writes a fresh .zer0-installed marker with the new version + date.
14
+ #
15
+ # Public API:
16
+ # upgrade_run <target_dir> <repo_root> [--from <version>] [--force]
17
+ # [--dry-run] [--auto-accept]
18
+ #
19
+ # Bash 3.2-compatible. Idempotent. Never destructive without explicit --force.
20
+
21
+ # shellcheck disable=SC2034
22
+ UPGRADE_LIB_VERSION="1.0.0"
23
+
24
+ UPGRADE_MARKER=".zer0-installed"
25
+
26
+ # Read theme version from lib/jekyll-theme-zer0/version.rb
27
+ _upgrade_theme_version() {
28
+ local repo_root="$1"
29
+ local vfile="$repo_root/lib/jekyll-theme-zer0/version.rb"
30
+ [[ -f "$vfile" ]] || { echo "unknown"; return; }
31
+ grep -E 'VERSION\s*=' "$vfile" | head -n1 | sed -E 's/.*"([^"]+)".*/\1/'
32
+ }
33
+
34
+ # Detect the previously installed version (best-effort).
35
+ _upgrade_detect_installed() {
36
+ local target_dir="$1"
37
+ if [[ -f "$target_dir/$UPGRADE_MARKER" ]]; then
38
+ grep -E '^version:' "$target_dir/$UPGRADE_MARKER" 2>/dev/null \
39
+ | head -n1 | sed -E 's/version:[[:space:]]*//'
40
+ return
41
+ fi
42
+ # Fallback: probe _config.yml for a `version:` line
43
+ if [[ -f "$target_dir/_config.yml" ]]; then
44
+ local v
45
+ v="$(grep -E '^version:' "$target_dir/_config.yml" 2>/dev/null \
46
+ | head -n1 | sed -E 's/version:[[:space:]]*//' \
47
+ | tr -d '"' | tr -d "'")"
48
+ [[ -n "$v" ]] && { echo "$v"; return; }
49
+ fi
50
+ echo "unknown"
51
+ }
52
+
53
+ # Write/update the install marker.
54
+ _upgrade_write_marker() {
55
+ local target_dir="$1" version="$2" dry_run="$3"
56
+ local marker="$target_dir/$UPGRADE_MARKER"
57
+ local content
58
+ content="$(cat <<EOF
59
+ # zer0-mistakes install marker — do not edit manually
60
+ version: $version
61
+ upgraded_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)
62
+ EOF
63
+ )"
64
+ if [[ "$dry_run" = "1" ]]; then
65
+ log_info "[dry-run] Would write marker: $marker"
66
+ return
67
+ fi
68
+ printf '%s\n' "$content" > "$marker"
69
+ log_success "Wrote $UPGRADE_MARKER (version: $version)"
70
+ }
71
+
72
+ # Re-run agents install in --force mode (theme files are safe to refresh).
73
+ _upgrade_refresh_agents() {
74
+ local target_dir="$1" repo_root="$2" force="$3" dry_run="$4"
75
+ if ! declare -F agents_install >/dev/null 2>&1; then
76
+ log_warning "agents.sh not loaded — skipping agent-file refresh"
77
+ return 0
78
+ fi
79
+ if [[ "$dry_run" = "1" ]]; then
80
+ log_info "[dry-run] Would refresh agent files in $target_dir"
81
+ return 0
82
+ fi
83
+ local force_flag=""
84
+ [[ "$force" = "1" ]] && force_flag="--force"
85
+ log_info "Refreshing AI agent files..."
86
+ agents_install "$target_dir" "$repo_root" $force_flag || true
87
+ }
88
+
89
+ # Compare a workflow file in target_dir vs theme template — list
90
+ # differences so user can decide whether to merge.
91
+ _upgrade_check_workflows() {
92
+ local target_dir="$1" repo_root="$2"
93
+ local wf_dir="$target_dir/.github/workflows"
94
+ [[ -d "$wf_dir" ]] || return 0
95
+ local f base tpl
96
+ log_info "Checking .github/workflows/ for theme-managed files..."
97
+ local found=0
98
+ for f in "$wf_dir"/*.yml "$wf_dir"/*.yaml; do
99
+ [[ -f "$f" ]] || continue
100
+ base="$(basename "$f")"
101
+ # Look for a matching template under templates/deploy/*/
102
+ for tpl in "$repo_root/templates/deploy"/*/"$base.template" \
103
+ "$repo_root/templates/deploy"/*/"$base"; do
104
+ [[ -f "$tpl" ]] || continue
105
+ found=$((found+1))
106
+ if diff -q "$f" "$tpl" >/dev/null 2>&1; then
107
+ log_success " $base — up to date"
108
+ else
109
+ log_warning " $base — differs from theme template"
110
+ log_info " Compare: diff $f $tpl"
111
+ fi
112
+ break
113
+ done
114
+ done
115
+ [[ "$found" = "0" ]] && log_info " No theme-managed workflows detected"
116
+ }
117
+
118
+ # Public entrypoint.
119
+ upgrade_run() {
120
+ local target_dir="$1" repo_root="$2"
121
+ shift 2 || true
122
+
123
+ local from_version="" force=0 dry_run=0 auto_accept=0
124
+ while [[ $# -gt 0 ]]; do
125
+ case "$1" in
126
+ --from) from_version="${2:-}"; shift ;;
127
+ -f|--force) force=1 ;;
128
+ -n|--dry-run) dry_run=1 ;;
129
+ --auto-accept) auto_accept=1 ;;
130
+ *) log_warning "upgrade_run: ignoring unknown flag: $1" ;;
131
+ esac
132
+ shift
133
+ done
134
+
135
+ if [[ ! -d "$target_dir" ]]; then
136
+ log_error "Target directory does not exist: $target_dir"
137
+ return 1
138
+ fi
139
+
140
+ local installed_version theme_version
141
+ installed_version="${from_version:-$(_upgrade_detect_installed "$target_dir")}"
142
+ theme_version="$(_upgrade_theme_version "$repo_root")"
143
+
144
+ log_info "🔧 Upgrading site at: $target_dir"
145
+ log_info " From version: $installed_version"
146
+ log_info " To version: $theme_version"
147
+ [[ "$dry_run" = "1" ]] && log_warning " Mode: dry-run (no files will be changed)"
148
+ echo
149
+
150
+ if [[ "$installed_version" = "$theme_version" ]] && [[ "$force" != "1" ]]; then
151
+ log_success "Already on $theme_version. Use --force to re-run anyway."
152
+ return 0
153
+ fi
154
+
155
+ # Confirmation gate (skipped when --auto-accept or --dry-run)
156
+ if [[ "$auto_accept" != "1" ]] && [[ "$dry_run" != "1" ]]; then
157
+ printf "Proceed with upgrade? [y/N] "
158
+ local reply
159
+ read -r reply || reply=""
160
+ case "$reply" in
161
+ y|Y|yes|YES) ;;
162
+ *) log_warning "Upgrade cancelled by user."; return 0 ;;
163
+ esac
164
+ fi
165
+
166
+ # 1. Refresh agent files (always additive/safe)
167
+ _upgrade_refresh_agents "$target_dir" "$repo_root" "$force" "$dry_run"
168
+ echo
169
+
170
+ # 2. Check workflows (read-only — never auto-overwrite)
171
+ _upgrade_check_workflows "$target_dir" "$repo_root"
172
+ echo
173
+
174
+ # 3. Write/update marker (only if not dry-run)
175
+ _upgrade_write_marker "$target_dir" "$theme_version" "$dry_run"
176
+ echo
177
+
178
+ log_success "Upgrade complete."
179
+ log_info "Next steps:"
180
+ log_info " 1. Run 'install doctor' to verify environment"
181
+ log_info " 2. Review any workflow files flagged above"
182
+ log_info " 3. Check CHANGELOG.md in the theme repo for breaking changes"
183
+ return 0
184
+ }