jekyll-theme-zer0 1.8.2 → 1.9.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 +26 -0
- data/README.md +98 -7
- data/_data/content_statistics.yml +253 -251
- data/_includes/components/nav-export.html +61 -0
- data/_includes/components/nav-overview.html +54 -0
- data/scripts/bin/install +52 -705
- data/scripts/install/README.md +162 -0
- data/scripts/install/ai/client.sh +164 -0
- data/scripts/install/ai/diagnose.sh +81 -0
- data/scripts/install/ai/prompts/diagnose.system.md +42 -0
- data/scripts/install/ai/prompts/spec.schema.json +129 -0
- data/scripts/install/ai/prompts/suggest.system.md +43 -0
- data/scripts/install/ai/prompts/wizard.system.md +142 -0
- data/scripts/install/ai/suggest.sh +57 -0
- data/scripts/install/ai/wizard.sh +150 -0
- data/scripts/install/apply.sh +156 -0
- data/scripts/install/cli.sh +561 -0
- data/scripts/install/diff.sh +128 -0
- data/scripts/install/doctor.sh +168 -0
- data/scripts/install/fs.sh +138 -0
- data/scripts/install/log.sh +119 -0
- data/scripts/install/plan.sh +299 -0
- data/scripts/install/platform.sh +122 -0
- data/scripts/install/prompt.sh +124 -0
- data/scripts/install/repair.sh +45 -0
- data/scripts/install/scrape.sh +535 -0
- data/scripts/install/scrape_html.py +764 -0
- data/scripts/install/spec.sh +486 -0
- data/scripts/install/tasks/_registry.sh +65 -0
- data/scripts/install/tasks/agents.sh +60 -0
- data/scripts/install/tasks/config.sh +37 -0
- data/scripts/install/tasks/data.sh +18 -0
- data/scripts/install/tasks/deploy_azure-swa.sh +17 -0
- data/scripts/install/tasks/deploy_docker-prod.sh +21 -0
- data/scripts/install/tasks/deploy_github-pages.sh +18 -0
- data/scripts/install/tasks/devcontainer.sh +26 -0
- data/scripts/install/tasks/docker.sh +29 -0
- data/scripts/install/tasks/gemfile.sh +42 -0
- data/scripts/install/tasks/gitignore.sh +26 -0
- data/scripts/install/tasks/marker.sh +46 -0
- data/scripts/install/tasks/nav.sh +18 -0
- data/scripts/install/tasks/pages.sh +61 -0
- data/scripts/install/tasks/readme.sh +27 -0
- data/scripts/install/tasks/scrape.sh +348 -0
- data/scripts/install/template.sh +138 -0
- data/scripts/install/tui.sh +110 -0
- data/scripts/install/upgrade.sh +49 -0
- data/scripts/lib/install/template.sh +1 -0
- metadata +45 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/doctor.sh — Pre-install environment health checks
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Checks system prerequisites and reports pass/warn/fail for each.
|
|
6
|
+
# Never blocks apply_run by default — warnings only. Fatal conditions
|
|
7
|
+
# (e.g. target dir not writable) return non-zero.
|
|
8
|
+
#
|
|
9
|
+
# Provides:
|
|
10
|
+
# doctor_run [TARGET_DIR] — run all checks; return 0 if clean, 1 if any FAIL
|
|
11
|
+
# doctor_check_ruby — Ruby version check
|
|
12
|
+
# doctor_check_bundler — Bundler present
|
|
13
|
+
# doctor_check_docker — Docker daemon
|
|
14
|
+
# doctor_check_git — git present
|
|
15
|
+
# doctor_check_gh — GitHub CLI present (warn only)
|
|
16
|
+
# doctor_check_writable — target dir writable
|
|
17
|
+
#
|
|
18
|
+
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
19
|
+
# =============================================================================
|
|
20
|
+
[[ -n "${_HAS_DOCTOR_LIB:-}" ]] && return 0
|
|
21
|
+
_HAS_DOCTOR_LIB=1
|
|
22
|
+
|
|
23
|
+
# Internal check helpers
|
|
24
|
+
_doctor_pass() { printf " [PASS] %s\n" "$*" >&2; }
|
|
25
|
+
_doctor_warn() { printf " [WARN] %s\n" "$*" >&2; }
|
|
26
|
+
_doctor_fail() { printf " [FAIL] %s\n" "$*" >&2; }
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# doctor_check_ruby
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
doctor_check_ruby() {
|
|
32
|
+
local ruby_ver
|
|
33
|
+
ruby_ver=$(ruby --version 2>/dev/null | awk '{print $2}')
|
|
34
|
+
if [[ -z "$ruby_ver" ]]; then
|
|
35
|
+
_doctor_fail "Ruby not found. Install Ruby >= 2.7 (rbenv / brew / asdf recommended)"
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Extract major.minor
|
|
40
|
+
local major minor
|
|
41
|
+
major=$(echo "$ruby_ver" | cut -d. -f1)
|
|
42
|
+
minor=$(echo "$ruby_ver" | cut -d. -f2)
|
|
43
|
+
|
|
44
|
+
if [[ "$major" -lt 2 || ( "$major" -eq 2 && "$minor" -lt 5 ) ]]; then
|
|
45
|
+
_doctor_fail "Ruby $ruby_ver is too old (need >= 2.5). Upgrade recommended."
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [[ "$major" -eq 2 && "$minor" -lt 7 ]]; then
|
|
50
|
+
_doctor_warn "Ruby $ruby_ver is supported but >= 3.0 is recommended"
|
|
51
|
+
else
|
|
52
|
+
_doctor_pass "Ruby $ruby_ver"
|
|
53
|
+
fi
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# doctor_check_bundler
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
doctor_check_bundler() {
|
|
61
|
+
if ! command -v bundle >/dev/null 2>&1; then
|
|
62
|
+
_doctor_warn "Bundler not found — run: gem install bundler"
|
|
63
|
+
return 0 # warn, not fail
|
|
64
|
+
fi
|
|
65
|
+
local bver
|
|
66
|
+
bver=$(bundle --version 2>/dev/null | awk '{print $NF}')
|
|
67
|
+
_doctor_pass "Bundler $bver"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# doctor_check_docker
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
doctor_check_docker() {
|
|
74
|
+
local profile="${SPEC_PROFILE:-default}"
|
|
75
|
+
case "$profile" in
|
|
76
|
+
minimal|github-pages)
|
|
77
|
+
_doctor_pass "Docker: not required for profile '$profile'"
|
|
78
|
+
return 0
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
|
|
82
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
83
|
+
_doctor_warn "Docker not found. Install Docker Desktop for best dev experience."
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if ! docker info >/dev/null 2>&1; then
|
|
88
|
+
_doctor_warn "Docker daemon not running. Start Docker Desktop."
|
|
89
|
+
return 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
_doctor_pass "Docker (daemon running)"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# doctor_check_git
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
doctor_check_git() {
|
|
99
|
+
if ! command -v git >/dev/null 2>&1; then
|
|
100
|
+
_doctor_fail "git not found. Install git before proceeding."
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
local gver
|
|
104
|
+
gver=$(git --version | awk '{print $3}')
|
|
105
|
+
_doctor_pass "git $gver"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# doctor_check_gh
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
doctor_check_gh() {
|
|
112
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
113
|
+
_doctor_warn "GitHub CLI (gh) not found. Some features will be limited."
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
local ghver
|
|
117
|
+
ghver=$(gh --version 2>/dev/null | head -1 | awk '{print $3}')
|
|
118
|
+
_doctor_pass "GitHub CLI $ghver"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# doctor_check_writable TARGET_DIR
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
doctor_check_writable() {
|
|
125
|
+
local target="$1"
|
|
126
|
+
if [[ -z "$target" ]]; then
|
|
127
|
+
_doctor_warn "doctor_check_writable: no target_dir specified"
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# If directory doesn't exist, check parent
|
|
132
|
+
local check_dir="$target"
|
|
133
|
+
while [[ ! -d "$check_dir" ]]; do
|
|
134
|
+
check_dir=$(dirname "$check_dir")
|
|
135
|
+
done
|
|
136
|
+
|
|
137
|
+
if [[ ! -w "$check_dir" ]]; then
|
|
138
|
+
_doctor_fail "Target directory not writable: $check_dir"
|
|
139
|
+
return 1
|
|
140
|
+
fi
|
|
141
|
+
_doctor_pass "Target directory writable: $check_dir"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# doctor_run [TARGET_DIR]
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
doctor_run() {
|
|
148
|
+
local target="${1:-${SPEC_TARGET_DIR:-}}"
|
|
149
|
+
local failures=0
|
|
150
|
+
|
|
151
|
+
printf "\n${_LOG_BOLD:-}Doctor: pre-install checks${_LOG_NC:-}\n" >&2
|
|
152
|
+
|
|
153
|
+
doctor_check_ruby || failures=$(( failures + 1 ))
|
|
154
|
+
doctor_check_bundler
|
|
155
|
+
doctor_check_docker
|
|
156
|
+
doctor_check_git || failures=$(( failures + 1 ))
|
|
157
|
+
doctor_check_gh
|
|
158
|
+
[[ -n "$target" ]] && { doctor_check_writable "$target" || failures=$(( failures + 1 )); }
|
|
159
|
+
|
|
160
|
+
if [[ $failures -eq 0 ]]; then
|
|
161
|
+
printf "\n${_LOG_GREEN:-}Doctor: all checks passed.${_LOG_NC:-}\n\n" >&2
|
|
162
|
+
return 0
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
printf "\n${_LOG_YELLOW:-}Doctor: %d issue(s) found. See above.${_LOG_NC:-}\n\n" \
|
|
166
|
+
"$failures" >&2
|
|
167
|
+
return 1
|
|
168
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/fs.sh — Idempotent filesystem operations
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# ALL file writes in the installer go through this module.
|
|
6
|
+
# Provides:
|
|
7
|
+
# fs_copy_file SRC DEST [--force] [--dry-run]
|
|
8
|
+
# fs_copy_dir SRC DEST [--force] [--dry-run]
|
|
9
|
+
# fs_write_file DEST CONTENT [--force] [--dry-run]
|
|
10
|
+
# fs_ensure_dir PATH [--dry-run]
|
|
11
|
+
# fs_backup_file FILE → creates FILE.backup.YYYYMMDD_HHMMSS
|
|
12
|
+
# fs_file_exists FILE → returns 0 if exists
|
|
13
|
+
#
|
|
14
|
+
# Globals read (set by caller / spec):
|
|
15
|
+
# _FS_DRY_RUN — "1" → never write, only log
|
|
16
|
+
# _FS_FORCE — "1" → overwrite without prompting (still backs up)
|
|
17
|
+
# _FS_BACKUP — "1" (default) → create backups before overwriting
|
|
18
|
+
# _FS_VERBOSE — "1" → extra debug output
|
|
19
|
+
#
|
|
20
|
+
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
[[ -n "${_HAS_FS_LIB:-}" ]] && return 0
|
|
23
|
+
_HAS_FS_LIB=1
|
|
24
|
+
|
|
25
|
+
_FS_DRY_RUN="${_FS_DRY_RUN:-0}"
|
|
26
|
+
_FS_FORCE="${_FS_FORCE:-0}"
|
|
27
|
+
_FS_BACKUP="${_FS_BACKUP:-1}"
|
|
28
|
+
_FS_VERBOSE="${_FS_VERBOSE:-0}"
|
|
29
|
+
|
|
30
|
+
# Create a timestamped backup of FILE; prints backup path.
|
|
31
|
+
fs_backup_file() {
|
|
32
|
+
local file="$1"
|
|
33
|
+
[ -e "$file" ] || return 0
|
|
34
|
+
local bak="${file}.backup.$(date +%Y%m%d_%H%M%S)"
|
|
35
|
+
if [[ "${_FS_DRY_RUN:-0}" == "1" ]]; then
|
|
36
|
+
log_debug "[dry-run] would backup: $file → $bak"
|
|
37
|
+
return 0
|
|
38
|
+
fi
|
|
39
|
+
cp -a "$file" "$bak"
|
|
40
|
+
log_warning "Backed up: $(basename "$file") → $(basename "$bak")"
|
|
41
|
+
echo "$bak"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Copy a single file SRC → DEST. Backs up DEST if it exists (unless --force).
|
|
45
|
+
fs_copy_file() {
|
|
46
|
+
local src="$1"
|
|
47
|
+
local dest="$2"
|
|
48
|
+
|
|
49
|
+
if [[ ! -f "$src" ]]; then
|
|
50
|
+
log_error "fs_copy_file: source not found: $src"
|
|
51
|
+
return 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ "${_FS_DRY_RUN:-0}" == "1" ]]; then
|
|
55
|
+
log_info "[dry-run] copy: $src → $dest"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
fs_ensure_dir "$(dirname "$dest")"
|
|
60
|
+
|
|
61
|
+
if [[ -f "$dest" ]]; then
|
|
62
|
+
if [[ "${_FS_BACKUP:-1}" == "1" ]]; then
|
|
63
|
+
fs_backup_file "$dest" > /dev/null
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
cp "$src" "$dest"
|
|
68
|
+
log_info "Wrote: $dest"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Recursively copy directory SRC → DEST.
|
|
72
|
+
fs_copy_dir() {
|
|
73
|
+
local src="$1"
|
|
74
|
+
local dest="$2"
|
|
75
|
+
|
|
76
|
+
if [[ ! -d "$src" ]]; then
|
|
77
|
+
log_error "fs_copy_dir: source not found: $src"
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [[ "${_FS_DRY_RUN:-0}" == "1" ]]; then
|
|
82
|
+
log_info "[dry-run] copy dir: $src → $dest"
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
if [[ -d "$dest" ]]; then
|
|
87
|
+
if [[ "${_FS_BACKUP:-1}" == "1" ]]; then
|
|
88
|
+
local bak="${dest}.backup.$(date +%Y%m%d_%H%M%S)"
|
|
89
|
+
cp -a "$dest" "$bak"
|
|
90
|
+
log_warning "Backed up dir: $(basename "$dest") → $(basename "$bak")"
|
|
91
|
+
fi
|
|
92
|
+
rm -rf "$dest"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
cp -r "$src" "$dest"
|
|
96
|
+
log_info "Copied dir: $dest"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Write CONTENT string to DEST.
|
|
100
|
+
fs_write_file() {
|
|
101
|
+
local dest="$1"
|
|
102
|
+
local content="$2"
|
|
103
|
+
|
|
104
|
+
if [[ "${_FS_DRY_RUN:-0}" == "1" ]]; then
|
|
105
|
+
log_info "[dry-run] write: $dest"
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
fs_ensure_dir "$(dirname "$dest")"
|
|
110
|
+
|
|
111
|
+
if [[ -f "$dest" && "${_FS_FORCE:-0}" != "1" ]]; then
|
|
112
|
+
log_warning "$(basename "$dest") already exists — skipping (use --force to overwrite)"
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if [[ -f "$dest" && "${_FS_BACKUP:-1}" == "1" ]]; then
|
|
117
|
+
fs_backup_file "$dest" > /dev/null
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
printf '%s\n' "$content" > "$dest"
|
|
121
|
+
log_info "Wrote: $dest"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Ensure directory exists (including parents).
|
|
125
|
+
fs_ensure_dir() {
|
|
126
|
+
local dir="$1"
|
|
127
|
+
[[ -d "$dir" ]] && return 0
|
|
128
|
+
if [[ "${_FS_DRY_RUN:-0}" == "1" ]]; then
|
|
129
|
+
log_debug "[dry-run] mkdir -p: $dir"
|
|
130
|
+
return 0
|
|
131
|
+
fi
|
|
132
|
+
mkdir -p "$dir"
|
|
133
|
+
log_debug "Created dir: $dir"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fs_file_exists() {
|
|
137
|
+
[[ -f "$1" ]]
|
|
138
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# scripts/install/log.sh — Logging primitives
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Provides: log_info, log_success, log_warning, log_error, log_debug,
|
|
6
|
+
# log_json, log_step, log_indent_push, log_indent_pop
|
|
7
|
+
#
|
|
8
|
+
# Output modes:
|
|
9
|
+
# human (default) — coloured, prefixed text to stderr
|
|
10
|
+
# json — machine-readable JSON lines to stdout
|
|
11
|
+
#
|
|
12
|
+
# Callers set _LOG_OUTPUT="json" before sourcing to switch modes.
|
|
13
|
+
# Callers set _LOG_VERBOSE=1 to enable log_debug output.
|
|
14
|
+
#
|
|
15
|
+
# Bash 3.2 compatible. No declare -A. No set -euo pipefail here.
|
|
16
|
+
# =============================================================================
|
|
17
|
+
[[ -n "${_HAS_LOG_LIB:-}" ]] && return 0
|
|
18
|
+
_HAS_LOG_LIB=1
|
|
19
|
+
|
|
20
|
+
# Colour codes — only apply when stderr is a TTY
|
|
21
|
+
_LOG_RED=''
|
|
22
|
+
_LOG_GREEN=''
|
|
23
|
+
_LOG_YELLOW=''
|
|
24
|
+
_LOG_BLUE=''
|
|
25
|
+
_LOG_CYAN=''
|
|
26
|
+
_LOG_BOLD=''
|
|
27
|
+
_LOG_NC=''
|
|
28
|
+
if [[ -t 2 ]]; then
|
|
29
|
+
_LOG_RED='\033[0;31m'
|
|
30
|
+
_LOG_GREEN='\033[0;32m'
|
|
31
|
+
_LOG_YELLOW='\033[1;33m'
|
|
32
|
+
_LOG_BLUE='\033[0;34m'
|
|
33
|
+
_LOG_CYAN='\033[0;36m'
|
|
34
|
+
_LOG_BOLD='\033[1m'
|
|
35
|
+
_LOG_NC='\033[0m'
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Output mode: "human" | "json"
|
|
39
|
+
_LOG_OUTPUT="${_LOG_OUTPUT:-human}"
|
|
40
|
+
# Verbose: 1 = show debug lines
|
|
41
|
+
_LOG_VERBOSE="${_LOG_VERBOSE:-0}"
|
|
42
|
+
# Indent level (incremented by log_step sections)
|
|
43
|
+
_LOG_INDENT=0
|
|
44
|
+
|
|
45
|
+
# Internal: emit a single log line
|
|
46
|
+
_log_emit() {
|
|
47
|
+
local level="$1"
|
|
48
|
+
local msg="$2"
|
|
49
|
+
local prefix color
|
|
50
|
+
local indent=""
|
|
51
|
+
local i=0
|
|
52
|
+
while [[ $i -lt $_LOG_INDENT ]]; do
|
|
53
|
+
indent=" $indent"
|
|
54
|
+
i=$(( i + 1 ))
|
|
55
|
+
done
|
|
56
|
+
case "$level" in
|
|
57
|
+
INFO) color="$_LOG_BLUE"; prefix="[INFO]" ;;
|
|
58
|
+
SUCCESS) color="$_LOG_GREEN"; prefix="[SUCCESS]" ;;
|
|
59
|
+
WARNING) color="$_LOG_YELLOW"; prefix="[WARNING]" ;;
|
|
60
|
+
ERROR) color="$_LOG_RED"; prefix="[ERROR]" ;;
|
|
61
|
+
DEBUG) color="$_LOG_CYAN"; prefix="[DEBUG]" ;;
|
|
62
|
+
STEP) color="$_LOG_BOLD"; prefix="[STEP]" ;;
|
|
63
|
+
*) color="$_LOG_NC"; prefix="[$level]" ;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
if [[ "$_LOG_OUTPUT" == "json" ]]; then
|
|
67
|
+
# Emit JSON line to stdout for machine parsing
|
|
68
|
+
local ts
|
|
69
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
|
|
70
|
+
# Escape double quotes in message
|
|
71
|
+
local safe_msg
|
|
72
|
+
safe_msg=$(printf '%s' "$msg" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
73
|
+
printf '{"ts":"%s","level":"%s","msg":"%s"}\n' "$ts" "$level" "$safe_msg"
|
|
74
|
+
else
|
|
75
|
+
printf "${color}${prefix}${_LOG_NC} %s%s\n" "$indent" "$msg" >&2
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
log_info() { _log_emit "INFO" "$1"; }
|
|
80
|
+
log_success() { _log_emit "SUCCESS" "$1"; }
|
|
81
|
+
log_warning() { _log_emit "WARNING" "$1"; }
|
|
82
|
+
log_error() { _log_emit "ERROR" "$1"; }
|
|
83
|
+
log_debug() { [[ "${_LOG_VERBOSE:-0}" == "1" ]] && _log_emit "DEBUG" "$1" || true; }
|
|
84
|
+
|
|
85
|
+
# log_step TITLE — print a section header and increment indent
|
|
86
|
+
log_step() {
|
|
87
|
+
_log_emit "STEP" "── $1"
|
|
88
|
+
_LOG_INDENT=$(( _LOG_INDENT + 1 ))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# log_step_done — decrement indent after a section
|
|
92
|
+
log_step_done() {
|
|
93
|
+
[[ $_LOG_INDENT -gt 0 ]] && _LOG_INDENT=$(( _LOG_INDENT - 1 )) || true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# log_json KEY VALUE — emit a JSON key/value pair to stdout (for --output json)
|
|
97
|
+
# Use inside sections that need structured output regardless of log mode.
|
|
98
|
+
log_json() {
|
|
99
|
+
local key="$1"
|
|
100
|
+
local val="$2"
|
|
101
|
+
printf '{"key":"%s","value":"%s"}\n' "$key" "$val"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# log_banner TEXT — print a visible section banner
|
|
105
|
+
log_banner() {
|
|
106
|
+
local msg="$1"
|
|
107
|
+
local width=70
|
|
108
|
+
local line=""
|
|
109
|
+
local i=0
|
|
110
|
+
while [[ $i -lt $width ]]; do
|
|
111
|
+
line="${line}="
|
|
112
|
+
i=$(( i + 1 ))
|
|
113
|
+
done
|
|
114
|
+
if [[ "$_LOG_OUTPUT" != "json" ]]; then
|
|
115
|
+
printf "${_LOG_BOLD}%s${_LOG_NC}\n" "$line" >&2
|
|
116
|
+
printf "${_LOG_BOLD} %s${_LOG_NC}\n" "$msg" >&2
|
|
117
|
+
printf "${_LOG_BOLD}%s${_LOG_NC}\n" "$line" >&2
|
|
118
|
+
fi
|
|
119
|
+
}
|