jekyll-theme-zer0 0.22.21 โ†’ 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.
data/scripts/bin/test CHANGED
@@ -30,11 +30,12 @@ show_usage() {
30
30
  USAGE:
31
31
  ./scripts/bin/test [OPTIONS] [TEST_SUITE]
32
32
 
33
- TEST SUITES:
33
+ TEST_SUITES:
34
34
  all Run all tests (default)
35
35
  lib Run library unit tests only
36
36
  theme Run theme validation tests only
37
37
  integration Run integration tests only
38
+ install Run installer e2e suites only (test/test_install_*.sh)
38
39
 
39
40
  OPTIONS:
40
41
  --verbose, -v Show detailed test output
@@ -67,7 +68,7 @@ for arg in "$@"; do
67
68
  --dry-run)
68
69
  export DRY_RUN=true
69
70
  ;;
70
- all|lib|theme|integration)
71
+ all|lib|theme|integration|install)
71
72
  TEST_SUITE="$arg"
72
73
  ;;
73
74
  *)
@@ -161,6 +162,43 @@ run_integration_tests() {
161
162
  fi
162
163
  }
163
164
 
165
+ # Run installer e2e tests (test/test_install_*.sh in repo root)
166
+ run_install_tests() {
167
+ log_info "Running installer e2e tests..."
168
+
169
+ # Repo root is two levels up from scripts/bin/
170
+ local repo_root
171
+ repo_root="$(cd "$SCRIPTS_ROOT/.." && pwd)"
172
+ local install_test_dir="$repo_root/test"
173
+
174
+ if [[ ! -d "$install_test_dir" ]]; then
175
+ log_warning "Installer test dir not found: $install_test_dir"
176
+ return
177
+ fi
178
+
179
+ local found=false
180
+ for test_file in "$install_test_dir"/test_install_*.sh; do
181
+ [[ -f "$test_file" ]] || continue
182
+ found=true
183
+ local test_name
184
+ test_name=$(basename "$test_file")
185
+ log_info "Running installer suite: $test_name"
186
+
187
+ if bash "$test_file"; then
188
+ log_success "$test_name passed"
189
+ ((TOTAL_PASSED++)) || true
190
+ else
191
+ log_error "$test_name failed"
192
+ ((TOTAL_FAILED++)) || true
193
+ fi
194
+ ((TOTAL_TESTS++)) || true
195
+ done
196
+
197
+ if [[ "$found" == "false" ]]; then
198
+ log_warning "No installer test suites found (test_install_*.sh)"
199
+ fi
200
+ }
201
+
164
202
  # Main execution
165
203
  log_info "๐Ÿงช Starting test suite: $TEST_SUITE"
166
204
  echo ""
@@ -172,6 +210,8 @@ case $TEST_SUITE in
172
210
  run_theme_tests
173
211
  echo ""
174
212
  run_integration_tests
213
+ echo ""
214
+ run_install_tests
175
215
  ;;
176
216
  lib)
177
217
  run_lib_tests
@@ -182,6 +222,9 @@ case $TEST_SUITE in
182
222
  integration)
183
223
  run_integration_tests
184
224
  ;;
225
+ install)
226
+ run_install_tests
227
+ ;;
185
228
  esac
186
229
 
187
230
  # Summary
@@ -0,0 +1,63 @@
1
+ # `scripts/lib/install/` โ€” Installer Library Modules
2
+
3
+ Focused modules sourced by [`install.sh`](../../../install.sh) at the repository root. Each module is self-contained and โ‰ค 200 lines for readability and reuse.
4
+
5
+ ## Modules
6
+
7
+ | File | Purpose | Key Functions |
8
+ |------|---------|----------------|
9
+ | [`logging.sh`](logging.sh) | `log_info` / `log_success` / `log_warning` / `log_error` shim used throughout `install.sh`. | `log_info`, `log_success`, `log_warning`, `log_error` |
10
+ | [`platform.sh`](platform.sh) | OS, Ruby version, and platform detection (bash 3.2-compatible). | `detect_os`, `detect_ruby_version`, `ruby_version_lt_27`, `needs_macos_gemfile`, `detect_platform` |
11
+ | [`fs.sh`](fs.sh) | Idempotent file/directory copy with timestamped backups. | `copy_file_with_backup`, `copy_directory_with_backup` |
12
+ | [`template.sh`](template.sh) | `{{VAR}}` placeholder substitution + local/remote/fallback resolution. | `render_template`, `create_from_template`, `templates_available` |
13
+ | [`config.sh`](config.sh) | Loads `templates/config/install.conf` with hard-coded defaults as fallback. | `load_install_config` |
14
+ | [`pages.sh`](pages.sh) | Manifest-driven starter-page renderer. Replaces 8 legacy `create_*_page` heredoc functions with one driver. | `render_starter_pages`, `render_admin_settings_pages` (+ `create_starter_pages`/`create_admin_pages` aliases) |
15
+ | [`profile.sh`](profile.sh) | Pure-bash YAML reader for [`templates/profiles/*.yml`](../../../templates/profiles/). Bash 3.2 compatible (no yq/python). | `list_profile_names`, `profile_path`, `profile_get_scalar`, `profile_get_list`, `profile_print_summary` |
16
+ | [`deploy/`](deploy/) | Pluggable deployment-target modules (`github-pages`, `azure-swa`, `docker-prod`) with a uniform `check_prereqs`/`install`/`verify`/`doc_url` contract. | `deploy_run_target`, `deploy_print_summary`, `deploy_render`, `deploy_copy` (see [`deploy/README.md`](deploy/README.md)) |
17
+
18
+ ## Loading
19
+
20
+ `install.sh` sources these modules when `scripts/lib/install/` is present. When it isn't (a stripped distribution or a `curl | bash` one-liner that didn't bundle the libraries), `install.sh` falls back to inlined copies of the same functions defined at the top of the script.
21
+
22
+ ## CLI Entrypoint
23
+
24
+ For day-to-day use prefer the canonical dispatcher:
25
+
26
+ ```bash
27
+ ./scripts/bin/install help # subcommand index
28
+ ./scripts/bin/install init # full install in CWD
29
+ ./scripts/bin/install init --profile minimal /tmp/demo
30
+ ./scripts/bin/install list-profiles # available profiles
31
+ ./scripts/bin/install list-targets # available deploy targets
32
+ ./scripts/bin/install deploy github-pages /tmp/demo
33
+ ./scripts/bin/install deploy azure-swa,docker-prod /tmp/demo
34
+ ./scripts/bin/install agents # AGENTS.md / instructions index
35
+ ./scripts/bin/install version # theme version from version.rb
36
+ ```
37
+
38
+ `init` translates `--profile` into the appropriate legacy flag and execs `install.sh`. `deploy` dispatches to the modules under `deploy/` (Phase 4). The remaining subcommands (`wizard`, `diagnose`, `doctor`, `upgrade`) are stubs that print a clear notice until their backing modules land in Phases 5-6.
39
+
40
+ ## Roadmap
41
+
42
+ Phases 1, 1.5, 2, 3 (declarative profiles), and 4 (deploy modules) are complete. Future phases will add:
43
+
44
+ - `bootstrap.sh` โ€” remote install (`download_theme_files`, `cleanup_temp_dir`)
45
+ - `wizard.sh` โ€” interactive prompts (`gather_user_input`, `prompt_with_default`)
46
+ - `ai/{wizard,diagnose,suggest}.sh` โ€” opt-in AI integration
47
+ - `doctor.sh` โ€” pre-flight environment + site health checks
48
+
49
+ See the session refactor plan for the full sequence.
50
+
51
+ ## Compatibility
52
+
53
+ All modules target **bash 3.2** (the macOS default `/bin/bash`). No `declare -A`, no `=~` capture groups, no `mapfile`/`readarray`.
54
+
55
+ ## Conventions
56
+
57
+ - Every function documents its required globals at the top of the file
58
+ - No module calls `exit` โ€” caller decides; modules return non-zero on recoverable failure
59
+ - Modules don't `set -euo pipefail` themselves โ€” they inherit from the caller (`install.sh` already sets it)
60
+
61
+ ---
62
+
63
+ **Last updated**: 2026-04-20 โ€” Phase 4 (`deploy/registry.sh` + `deploy/{github-pages,azure-swa,docker-prod}.sh` + `templates/deploy/`).
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/agents.sh
3
+ #
4
+ # `install agents` โ€” copy AI agent guidance files into a target site.
5
+ #
6
+ # Pure file copy. No network. No AI calls. Source of truth is the theme
7
+ # repo's own .github/ + AGENTS.md, so the theme is self-canonical:
8
+ # whatever the theme dogfoods is what consumers receive.
9
+ #
10
+ # Public API:
11
+ # agents_install <target_dir> <repo_root> [--cursor] [--claude] [--aider] [--force]
12
+ #
13
+ # Always installs the core set (AGENTS.md + .github/copilot-instructions.md
14
+ # + .github/instructions/ + .github/prompts/). Optional flags add:
15
+ # --cursor .cursor/commands/*.md (mirrors prompts as slash commands)
16
+ # --claude CLAUDE.md stub pointing to AGENTS.md
17
+ # --aider .aider.conf.yml referencing AGENTS.md as read-only context
18
+ #
19
+ # Idempotent: skips files that already exist unless --force.
20
+
21
+ # shellcheck disable=SC2034 # script intended to be sourced
22
+ AGENTS_LIB_VERSION="1.0.0"
23
+
24
+ # Copy a single file with skip/force semantics. Returns 0 on copy, 1 on skip.
25
+ _agents_copy_file() {
26
+ local src="$1" dst="$2" force="$3"
27
+ if [[ ! -f "$src" ]]; then
28
+ log_warning "Source not found, skipping: ${src#$REPO_ROOT/}"
29
+ return 1
30
+ fi
31
+ if [[ -f "$dst" ]] && [[ "$force" != "1" ]]; then
32
+ log_warning "Exists, skipping: ${dst#$PWD/} (use --force to overwrite)"
33
+ return 1
34
+ fi
35
+ mkdir -p "$(dirname "$dst")"
36
+ cp "$src" "$dst"
37
+ log_success "Wrote ${dst#$PWD/}"
38
+ return 0
39
+ }
40
+
41
+ # Copy every file in a source dir matching a glob into a dest dir.
42
+ _agents_copy_glob() {
43
+ local src_dir="$1" pattern="$2" dst_dir="$3" force="$4"
44
+ local copied=0 skipped=0
45
+ if [[ ! -d "$src_dir" ]]; then
46
+ log_warning "Source dir not found: ${src_dir#$REPO_ROOT/}"
47
+ return 0
48
+ fi
49
+ local f base
50
+ for f in "$src_dir"/$pattern; do
51
+ [[ -f "$f" ]] || continue
52
+ base="$(basename "$f")"
53
+ if _agents_copy_file "$f" "$dst_dir/$base" "$force"; then
54
+ copied=$((copied+1))
55
+ else
56
+ skipped=$((skipped+1))
57
+ fi
58
+ done
59
+ log_info " โ†’ $copied copied, $skipped skipped in ${dst_dir#$PWD/}"
60
+ }
61
+
62
+ agents_install() {
63
+ local target_dir="$1" repo_root="$2"
64
+ shift 2 || true
65
+
66
+ local with_cursor=0 with_claude=0 with_aider=0 force=0
67
+ while [[ $# -gt 0 ]]; do
68
+ case "$1" in
69
+ --cursor) with_cursor=1 ;;
70
+ --claude) with_claude=1 ;;
71
+ --aider) with_aider=1 ;;
72
+ --all) with_cursor=1; with_claude=1; with_aider=1 ;;
73
+ -f|--force) force=1 ;;
74
+ *) log_warning "agents_install: ignoring unknown flag: $1" ;;
75
+ esac
76
+ shift
77
+ done
78
+
79
+ if [[ ! -d "$target_dir" ]]; then
80
+ log_error "Target directory does not exist: $target_dir"
81
+ return 1
82
+ fi
83
+
84
+ log_info "Installing AI agent guidance into: $target_dir"
85
+ [[ "$force" = "1" ]] && log_info " (--force: overwrite enabled)"
86
+
87
+ # 1. Core: AGENTS.md (cross-tool entry point)
88
+ _agents_copy_file "$repo_root/AGENTS.md" "$target_dir/AGENTS.md" "$force" || true
89
+
90
+ # 2. Copilot main instructions
91
+ _agents_copy_file \
92
+ "$repo_root/.github/copilot-instructions.md" \
93
+ "$target_dir/.github/copilot-instructions.md" \
94
+ "$force" || true
95
+
96
+ # 3. File-scoped instructions
97
+ log_info "Copying .github/instructions/*.md ..."
98
+ _agents_copy_glob \
99
+ "$repo_root/.github/instructions" "*.md" \
100
+ "$target_dir/.github/instructions" "$force"
101
+
102
+ # 4. Reusable prompts
103
+ log_info "Copying .github/prompts/*.md ..."
104
+ _agents_copy_glob \
105
+ "$repo_root/.github/prompts" "*.md" \
106
+ "$target_dir/.github/prompts" "$force"
107
+
108
+ # 5. Optional: Cursor slash-commands
109
+ if [[ "$with_cursor" = "1" ]]; then
110
+ log_info "Copying .cursor/commands/*.md ..."
111
+ _agents_copy_glob \
112
+ "$repo_root/.cursor/commands" "*.md" \
113
+ "$target_dir/.cursor/commands" "$force"
114
+ fi
115
+
116
+ # 6. Optional: Claude stub
117
+ if [[ "$with_claude" = "1" ]]; then
118
+ local claude_tpl="$repo_root/templates/agents/CLAUDE.md.template"
119
+ if [[ -f "$claude_tpl" ]]; then
120
+ _agents_copy_file "$claude_tpl" "$target_dir/CLAUDE.md" "$force" || true
121
+ else
122
+ cat > "$target_dir/CLAUDE.md.tmp" <<'EOF'
123
+ # Claude Code Instructions
124
+
125
+ This project uses [`AGENTS.md`](./AGENTS.md) as the single source of truth for
126
+ AI agent guidance. Please read it first.
127
+
128
+ For detailed conventions, see:
129
+ - `.github/copilot-instructions.md`
130
+ - `.github/instructions/*.instructions.md`
131
+ - `.github/prompts/*.prompt.md`
132
+ EOF
133
+ if [[ -f "$target_dir/CLAUDE.md" ]] && [[ "$force" != "1" ]]; then
134
+ rm -f "$target_dir/CLAUDE.md.tmp"
135
+ log_warning "Exists, skipping: CLAUDE.md (use --force to overwrite)"
136
+ else
137
+ mv "$target_dir/CLAUDE.md.tmp" "$target_dir/CLAUDE.md"
138
+ log_success "Wrote CLAUDE.md"
139
+ fi
140
+ fi
141
+ fi
142
+
143
+ # 7. Optional: Aider config
144
+ if [[ "$with_aider" = "1" ]]; then
145
+ local aider_tpl="$repo_root/templates/agents/aider.conf.yml.template"
146
+ if [[ -f "$aider_tpl" ]]; then
147
+ _agents_copy_file "$aider_tpl" "$target_dir/.aider.conf.yml" "$force" || true
148
+ else
149
+ if [[ -f "$target_dir/.aider.conf.yml" ]] && [[ "$force" != "1" ]]; then
150
+ log_warning "Exists, skipping: .aider.conf.yml (use --force to overwrite)"
151
+ else
152
+ cat > "$target_dir/.aider.conf.yml" <<'EOF'
153
+ # Aider configuration โ€” see https://aider.chat
154
+ # Loads project agent guidance as read-only context for every session.
155
+ read:
156
+ - AGENTS.md
157
+ - .github/copilot-instructions.md
158
+ EOF
159
+ log_success "Wrote .aider.conf.yml"
160
+ fi
161
+ fi
162
+ fi
163
+
164
+ log_success "Agent guidance installation complete."
165
+ return 0
166
+ }
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/install/ai/diagnose.sh
3
+ #
4
+ # `install diagnose [--ai]` โ€” Jekyll build / runtime error analysis.
5
+ #
6
+ # Two modes:
7
+ #
8
+ # 1. Rule-based (default, no network). Pattern-matches a curated list of
9
+ # known errors against a build log and prints structured fixes.
10
+ #
11
+ # 2. AI-assisted (`--ai`). Sends a sanitized error log + the most relevant
12
+ # config files to OpenAI, returns a unified diff for user review.
13
+ #
14
+ # Public API:
15
+ # diagnose_run <target_dir> <repo_root> [--log <file>] [--ai] [--auto-accept]
16
+ #
17
+ # If --log is not provided, runs `jekyll build` once and captures output.
18
+
19
+ # shellcheck disable=SC2034
20
+ AI_DIAGNOSE_LIB_VERSION="1.0.0"
21
+
22
+ # ----- Rule-based pattern table --------------------------------------------
23
+ # Each rule: [pattern_regex] [short_label] [explanation+fix]
24
+ # Order matters โ€” first match wins. Keep patterns specific.
25
+ _diagnose_rules() {
26
+ cat <<'EOF'
27
+ theme could not be found|MISSING_THEME|The Jekyll theme gem is not installed. Run `bundle install` (or `bundle update jekyll-theme-zer0`) and confirm the gem name in _config.yml matches your Gemfile entry.
28
+ Address already in use|PORT_IN_USE|Port 4000 is already bound. Either stop the existing process (`lsof -ti :4000 | xargs kill`) or run on a different port (`bundle exec jekyll serve --port 4001`).
29
+ You have requested:.*Could not find compatible versions|GEM_VERSION_CONFLICT|A gem version constraint cannot be satisfied. Run `bundle update` to refresh the lockfile, or pin to compatible versions in your Gemfile.
30
+ Liquid Exception:.*Could not locate the included file|MISSING_INCLUDE|A `{% include %}` tag references a file that doesn't exist. Verify the path inside `_includes/` and check for typos.
31
+ Liquid Exception|LIQUID_ERROR|A Liquid template raised an error. Check the file path printed above the exception and look for unmatched tags ({% if %} without {% endif %}, etc.).
32
+ SassC::SyntaxError|SASS_SYNTAX|Sass compilation failed. Review the file/line printed in the error and check for missing semicolons, unclosed braces, or invalid @import paths.
33
+ No such file or directory @ rb_sysopen|MISSING_FILE|Jekyll tried to open a file that doesn't exist. Common causes: deleted but still referenced in _config.yml or front matter; typo in include path.
34
+ incompatible character encodings|ENCODING_ISSUE|A file has mixed encodings. Re-save the offending file as UTF-8 without BOM.
35
+ EOF
36
+ }
37
+
38
+ _diagnose_rule_based() {
39
+ local log_file="$1"
40
+ local matched=0 line key label fix
41
+ while IFS='|' read -r pattern label fix; do
42
+ [[ -z "$pattern" ]] && continue
43
+ if grep -qE "$pattern" "$log_file" 2>/dev/null; then
44
+ log_info "Matched rule: $label"
45
+ echo " โ†ณ $fix"
46
+ echo
47
+ matched=$((matched+1))
48
+ fi
49
+ done < <(_diagnose_rules)
50
+
51
+ if [[ "$matched" = "0" ]]; then
52
+ log_warning "No known patterns matched. Re-run with --ai for AI analysis (requires OPENAI_API_KEY)."
53
+ return 1
54
+ fi
55
+ log_success "Matched $matched rule(s)."
56
+ return 0
57
+ }
58
+
59
+ _diagnose_capture_build_log() {
60
+ local target_dir="$1" log_file="$2"
61
+ log_info "Running 'jekyll build' to capture errors ..."
62
+ (
63
+ cd "$target_dir" || exit 1
64
+ if [[ -f Gemfile ]] && command -v bundle >/dev/null 2>&1; then
65
+ bundle exec jekyll build 2>&1 | tee "$log_file"
66
+ elif command -v jekyll >/dev/null 2>&1; then
67
+ jekyll build 2>&1 | tee "$log_file"
68
+ else
69
+ echo "neither bundler nor jekyll is installed" > "$log_file"
70
+ return 1
71
+ fi
72
+ ) || true
73
+ }
74
+
75
+ _diagnose_ai() {
76
+ local log_file="$1" target_dir="$2" repo_root="$3" auto_accept="$4"
77
+
78
+ if ! ai_enabled; then
79
+ log_warning "AI is disabled (ZER0_NO_AI=1) โ€” using rule-based mode only."
80
+ return 1
81
+ fi
82
+ if ! ai_require_key; then
83
+ return 1
84
+ fi
85
+
86
+ local sys_prompt_file="$repo_root/templates/ai/prompts/diagnose-system.md"
87
+ if [[ ! -f "$sys_prompt_file" ]]; then
88
+ log_error "System prompt missing: $sys_prompt_file"
89
+ return 1
90
+ fi
91
+ local system_prompt
92
+ system_prompt="$(cat "$sys_prompt_file")"
93
+
94
+ # Build sanitized context (last 80 lines of log + _config.yml + Gemfile)
95
+ local sanitized_log sanitized_cfg sanitized_gem
96
+ sanitized_log="$(tail -n 80 "$log_file" | ai_sanitize_text)"
97
+ if [[ -f "$target_dir/_config.yml" ]]; then
98
+ sanitized_cfg="$(ai_sanitize_text < "$target_dir/_config.yml")"
99
+ else
100
+ sanitized_cfg="(missing)"
101
+ fi
102
+ if [[ -f "$target_dir/Gemfile" ]]; then
103
+ sanitized_gem="$(ai_sanitize_text < "$target_dir/Gemfile")"
104
+ else
105
+ sanitized_gem="(missing)"
106
+ fi
107
+
108
+ local user_prompt="===== BUILD LOG (last 80 lines) =====
109
+ ${sanitized_log}
110
+
111
+ ===== _config.yml =====
112
+ ${sanitized_cfg}
113
+
114
+ ===== Gemfile =====
115
+ ${sanitized_gem}
116
+
117
+ Diagnose the failure and propose a minimal fix. If a file change is needed, output a unified diff. Be concise."
118
+
119
+ local model
120
+ model="$(ai_default_model diagnose)"
121
+ local in_chars=$(( ${#system_prompt} + ${#user_prompt} ))
122
+ log_info "About to call OpenAI:"
123
+ ai_estimate_cost "$model" "$in_chars" 600
124
+
125
+ if [[ "$auto_accept" != "1" ]]; then
126
+ printf "Proceed with API call? [y/N] "
127
+ local go
128
+ read -r go
129
+ if [[ ! "$go" =~ ^[Yy]$ ]]; then
130
+ log_warning "Aborted by user."
131
+ return 1
132
+ fi
133
+ fi
134
+
135
+ log_info "Calling $model ..."
136
+ local resp
137
+ if ! resp="$(ai_call_chat "$model" "$system_prompt" "$user_prompt" 800 0.2)"; then
138
+ log_error "OpenAI call failed."
139
+ return 1
140
+ fi
141
+
142
+ echo
143
+ log_info "AI diagnosis:"
144
+ echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€"
145
+ printf '%s\n' "$resp"
146
+ echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€"
147
+ log_info "If the response includes a unified diff, save it to a file and apply with: patch -p0 < fix.diff"
148
+ return 0
149
+ }
150
+
151
+ diagnose_run() {
152
+ local target_dir="$1" repo_root="$2"
153
+ shift 2 || true
154
+
155
+ local log_file="" use_ai=0 auto_accept=0
156
+ while [[ $# -gt 0 ]]; do
157
+ case "$1" in
158
+ --log) log_file="${2:-}"; shift ;;
159
+ --ai) use_ai=1 ;;
160
+ --auto-accept) auto_accept=1 ;;
161
+ *) log_warning "diagnose_run: ignoring unknown flag: $1" ;;
162
+ esac
163
+ shift
164
+ done
165
+
166
+ if [[ ! -d "$target_dir" ]]; then
167
+ log_error "Target directory does not exist: $target_dir"
168
+ return 1
169
+ fi
170
+
171
+ local cleanup_log=0
172
+ if [[ -z "$log_file" ]]; then
173
+ log_file="$(mktemp)"
174
+ cleanup_log=1
175
+ _diagnose_capture_build_log "$target_dir" "$log_file"
176
+ fi
177
+
178
+ if [[ ! -f "$log_file" ]]; then
179
+ log_error "Log file not found: $log_file"
180
+ [[ "$cleanup_log" = "1" ]] && rm -f "$log_file"
181
+ return 1
182
+ fi
183
+
184
+ log_info "Diagnosing build log ($(wc -l < "$log_file" | tr -d ' ') lines) ..."
185
+ echo
186
+
187
+ # Always run rule-based first
188
+ local rule_result=0
189
+ _diagnose_rule_based "$log_file" || rule_result=1
190
+
191
+ # AI if requested
192
+ if [[ "$use_ai" = "1" ]]; then
193
+ echo
194
+ _diagnose_ai "$log_file" "$target_dir" "$repo_root" "$auto_accept" || true
195
+ fi
196
+
197
+ [[ "$cleanup_log" = "1" ]] && rm -f "$log_file"
198
+ return $rule_result
199
+ }