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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +98 -7
  4. data/_data/content_statistics.yml +253 -251
  5. data/_includes/components/nav-export.html +61 -0
  6. data/_includes/components/nav-overview.html +54 -0
  7. data/scripts/bin/install +52 -705
  8. data/scripts/install/README.md +162 -0
  9. data/scripts/install/ai/client.sh +164 -0
  10. data/scripts/install/ai/diagnose.sh +81 -0
  11. data/scripts/install/ai/prompts/diagnose.system.md +42 -0
  12. data/scripts/install/ai/prompts/spec.schema.json +129 -0
  13. data/scripts/install/ai/prompts/suggest.system.md +43 -0
  14. data/scripts/install/ai/prompts/wizard.system.md +142 -0
  15. data/scripts/install/ai/suggest.sh +57 -0
  16. data/scripts/install/ai/wizard.sh +150 -0
  17. data/scripts/install/apply.sh +156 -0
  18. data/scripts/install/cli.sh +561 -0
  19. data/scripts/install/diff.sh +128 -0
  20. data/scripts/install/doctor.sh +168 -0
  21. data/scripts/install/fs.sh +138 -0
  22. data/scripts/install/log.sh +119 -0
  23. data/scripts/install/plan.sh +299 -0
  24. data/scripts/install/platform.sh +122 -0
  25. data/scripts/install/prompt.sh +124 -0
  26. data/scripts/install/repair.sh +45 -0
  27. data/scripts/install/scrape.sh +535 -0
  28. data/scripts/install/scrape_html.py +764 -0
  29. data/scripts/install/spec.sh +486 -0
  30. data/scripts/install/tasks/_registry.sh +65 -0
  31. data/scripts/install/tasks/agents.sh +60 -0
  32. data/scripts/install/tasks/config.sh +37 -0
  33. data/scripts/install/tasks/data.sh +18 -0
  34. data/scripts/install/tasks/deploy_azure-swa.sh +17 -0
  35. data/scripts/install/tasks/deploy_docker-prod.sh +21 -0
  36. data/scripts/install/tasks/deploy_github-pages.sh +18 -0
  37. data/scripts/install/tasks/devcontainer.sh +26 -0
  38. data/scripts/install/tasks/docker.sh +29 -0
  39. data/scripts/install/tasks/gemfile.sh +42 -0
  40. data/scripts/install/tasks/gitignore.sh +26 -0
  41. data/scripts/install/tasks/marker.sh +46 -0
  42. data/scripts/install/tasks/nav.sh +18 -0
  43. data/scripts/install/tasks/pages.sh +61 -0
  44. data/scripts/install/tasks/readme.sh +27 -0
  45. data/scripts/install/tasks/scrape.sh +348 -0
  46. data/scripts/install/template.sh +138 -0
  47. data/scripts/install/tui.sh +110 -0
  48. data/scripts/install/upgrade.sh +49 -0
  49. data/scripts/lib/install/template.sh +1 -0
  50. metadata +45 -2
@@ -0,0 +1,299 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/plan.sh — Build an install spec from inputs
4
+ # =============================================================================
5
+ # Accepts: profile, CLI flags, environment variables, detected platform.
6
+ # Produces: populated SPEC_* globals (see spec.sh for the full list).
7
+ # The three front-ends (flags, tui.sh, ai/wizard.sh) all call plan_build.
8
+ #
9
+ # Provides:
10
+ # plan_build TARGET_DIR [PROFILE]
11
+ # Merge layers (profile → env-var overrides → CLI flags → platform
12
+ # defaults) into SPEC_* globals. Does NOT write any files.
13
+ #
14
+ # plan_load_profile PROFILE_FILE
15
+ # Read a templates/profiles/*.yml file into SPEC_* globals
16
+ # (lower priority than explicit flag overrides).
17
+ #
18
+ # plan_apply_flags
19
+ # Copy flag globals (_FLAG_*) set by cli.sh into SPEC_* globals.
20
+ #
21
+ # plan_apply_platform
22
+ # Auto-detect SPEC_THEME_SOURCE if not set:
23
+ # - Docker available → gem
24
+ # - GitHub Pages mode → remote
25
+ # - fallback → gem
26
+ #
27
+ # plan_print [FILE]
28
+ # Print the spec to stdout or FILE as formatted JSON.
29
+ #
30
+ # Bash 3.2 compatible. No set -euo pipefail here.
31
+ # =============================================================================
32
+ [[ -n "${_HAS_PLAN_LIB:-}" ]] && return 0
33
+ _HAS_PLAN_LIB=1
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # plan_load_profile PROFILE_FILE
37
+ # ---------------------------------------------------------------------------
38
+ plan_load_profile() {
39
+ local profile_file="$1"
40
+ [[ -f "$profile_file" ]] || return 0
41
+
42
+ # Read scalar fields
43
+ local val
44
+ # profile_get_scalar is provided by profile.sh if sourced; fall back to awk
45
+ _plan_yaml_scalar() {
46
+ local file="$1" key="$2"
47
+ awk -v k="$key" '
48
+ $0 ~ "^[[:space:]]*" k "[[:space:]]*:" {
49
+ sub("^[[:space:]]*" k "[[:space:]]*:[[:space:]]*", "")
50
+ sub(/[[:space:]]+#.*$/, "")
51
+ gsub(/^["'"'"']|["'"'"']$/, "")
52
+ print; exit
53
+ }
54
+ ' "$file"
55
+ }
56
+
57
+ val=$(_plan_yaml_scalar "$profile_file" "profile")
58
+ [[ -n "$val" ]] && SPEC_PROFILE="$val"
59
+
60
+ val=$(_plan_yaml_scalar "$profile_file" "theme_source")
61
+ [[ -n "$val" ]] && SPEC_THEME_SOURCE="$val"
62
+
63
+ val=$(_plan_yaml_scalar "$profile_file" "theme_version")
64
+ [[ -n "$val" ]] && SPEC_THEME_VERSION="$val"
65
+
66
+ val=$(_plan_yaml_scalar "$profile_file" "site_title")
67
+ [[ -n "$val" ]] && : "${SPEC_SITE_TITLE:=$val}"
68
+
69
+ val=$(_plan_yaml_scalar "$profile_file" "site_description")
70
+ [[ -n "$val" ]] && : "${SPEC_SITE_DESCRIPTION:=$val}"
71
+
72
+ val=$(_plan_yaml_scalar "$profile_file" "site_timezone")
73
+ [[ -n "$val" ]] && : "${SPEC_SITE_TIMEZONE:=$val}"
74
+
75
+ val=$(_plan_yaml_scalar "$profile_file" "github_pages_branch")
76
+ [[ -n "$val" ]] && : "${SPEC_GITHUB_PAGES_BRANCH:=$val}"
77
+
78
+ # Tasks list from profile (space-separated after join)
79
+ local tasks
80
+ tasks=$(awk '
81
+ /^[[:space:]]*tasks[[:space:]]*:/ { found=1; next }
82
+ found && /^[[:space:]]*-[[:space:]]+/ {
83
+ line=$0; sub(/^[[:space:]]*-[[:space:]]+/, "", line)
84
+ gsub(/[[:space:]]/, "", line)
85
+ printf "%s ", line
86
+ }
87
+ found && !/^[[:space:]]*-/ && NF { exit }
88
+ ' "$profile_file" | sed 's/[[:space:]]*$//')
89
+ [[ -n "$tasks" ]] && SPEC_TASKS="$tasks"
90
+
91
+ # Deploy list from profile (accepts `deploy:` or `deploy_targets:`)
92
+ local deploy
93
+ deploy=$(awk '
94
+ /^[[:space:]]*deploy(_targets)?[[:space:]]*:/ { found=1; next }
95
+ found && /^[[:space:]]*-[[:space:]]+/ {
96
+ line=$0; sub(/^[[:space:]]*-[[:space:]]+/, "", line)
97
+ gsub(/[[:space:]]/, "", line)
98
+ printf "%s ", line
99
+ }
100
+ found && !/^[[:space:]]*-/ && NF { exit }
101
+ ' "$profile_file" | sed 's/[[:space:]]*$//')
102
+ [[ -n "$deploy" ]] && : "${SPEC_DEPLOY:=$deploy}"
103
+
104
+ # Agents list from profile (accepts top-level `agents:` or nested `ai_features.agent_files:`)
105
+ local agents
106
+ agents=$(awk '
107
+ /^[[:space:]]*agents[[:space:]]*:/ { in_agents=1; in_af=0; next }
108
+ /^[[:space:]]*ai_features[[:space:]]*:/ { in_af_block=1; next }
109
+ in_af_block && /^[[:space:]]*agent_files[[:space:]]*:/ {
110
+ in_af=1
111
+ # Inline flow form: agent_files: [foo, bar]
112
+ line=$0; sub(/^[^\[]*\[/, "", line); sub(/\].*$/, "", line)
113
+ gsub(/[[:space:],]+/, " ", line)
114
+ if (length(line) > 0 && line !~ /^[[:space:]]*$/) printf "%s ", line
115
+ next
116
+ }
117
+ (in_agents || in_af) && /^[[:space:]]*-[[:space:]]+/ {
118
+ line=$0; sub(/^[[:space:]]*-[[:space:]]+/, "", line)
119
+ gsub(/[[:space:]]/, "", line)
120
+ printf "%s ", line
121
+ next
122
+ }
123
+ (in_agents || in_af) && /^[^[:space:]-]/ { in_agents=0; in_af=0 }
124
+ /^[^[:space:]]/ { in_af_block=0 }
125
+ ' "$profile_file" | sed 's/[[:space:]]*$//')
126
+ [[ -n "$agents" ]] && : "${SPEC_AGENTS:=$agents}"
127
+ return 0
128
+ }
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # plan_apply_flags — copy _FLAG_* set by cli.sh → SPEC_* (highest priority)
132
+ # ---------------------------------------------------------------------------
133
+ plan_apply_flags() {
134
+ # Strings — only override when flag was explicitly set (non-empty)
135
+ [[ -n "${_FLAG_PROFILE:-}" ]] && SPEC_PROFILE="$_FLAG_PROFILE"
136
+ [[ -n "${_FLAG_SITE_TITLE:-}" ]] && SPEC_SITE_TITLE="$_FLAG_SITE_TITLE"
137
+ [[ -n "${_FLAG_SITE_DESC:-}" ]] && SPEC_SITE_DESCRIPTION="$_FLAG_SITE_DESC"
138
+ [[ -n "${_FLAG_SITE_URL:-}" ]] && SPEC_SITE_URL="$_FLAG_SITE_URL"
139
+ [[ -n "${_FLAG_SITE_AUTHOR:-}" ]] && SPEC_SITE_AUTHOR="$_FLAG_SITE_AUTHOR"
140
+ [[ -n "${_FLAG_SITE_EMAIL:-}" ]] && SPEC_SITE_EMAIL="$_FLAG_SITE_EMAIL"
141
+ [[ -n "${_FLAG_GITHUB_USER:-}" ]] && SPEC_GITHUB_USER="$_FLAG_GITHUB_USER"
142
+ [[ -n "${_FLAG_GITHUB_REPO:-}" ]] && SPEC_GITHUB_REPO="$_FLAG_GITHUB_REPO"
143
+ [[ -n "${_FLAG_THEME_SOURCE:-}" ]] && SPEC_THEME_SOURCE="$_FLAG_THEME_SOURCE"
144
+ [[ -n "${_FLAG_DEPLOY:-}" ]] && SPEC_DEPLOY="$_FLAG_DEPLOY"
145
+ [[ -n "${_FLAG_AGENTS:-}" ]] && SPEC_AGENTS="$_FLAG_AGENTS"
146
+ [[ -n "${_FLAG_TASKS:-}" ]] && SPEC_TASKS="$_FLAG_TASKS"
147
+
148
+ # Booleans — flags are set to "1" when present
149
+ [[ "${_FLAG_DRY_RUN:-0}" == "1" ]] && SPEC_OPT_DRY_RUN=true
150
+ [[ "${_FLAG_FORCE:-0}" == "1" ]] && SPEC_OPT_FORCE=true
151
+ [[ "${_FLAG_NO_BACKUP:-0}" == "1" ]] && SPEC_OPT_BACKUP=false
152
+ [[ "${_FLAG_NON_INTERACTIVE:-0}" == "1" ]] && SPEC_OPT_NON_INTERACTIVE=true
153
+ [[ "${_FLAG_AUTO_ACCEPT:-0}" == "1" ]] && SPEC_OPT_AUTO_ACCEPT=true
154
+ [[ "${_FLAG_SKIP_DOCTOR:-0}" == "1" ]] && SPEC_OPT_SKIP_DOCTOR=true
155
+ [[ "${_FLAG_VERBOSE:-0}" == "1" ]] && SPEC_OPT_VERBOSE=true
156
+ [[ -n "${_FLAG_OUTPUT:-}" ]] && SPEC_OPT_OUTPUT="$_FLAG_OUTPUT"
157
+
158
+ # Scrape flags — when --scrape URL is given, register the scrape task
159
+ # so apply.sh runs it (after pages so it can overlay).
160
+ if [[ -n "${_FLAG_SCRAPE_URL:-}" ]]; then
161
+ SPEC_SCRAPE_SOURCE_URL="$_FLAG_SCRAPE_URL"
162
+ [[ -n "${_FLAG_SCRAPE_DEPTH:-}" ]] && SPEC_SCRAPE_DEPTH="$_FLAG_SCRAPE_DEPTH"
163
+ [[ -n "${_FLAG_SCRAPE_MAX_PAGES:-}" ]] && SPEC_SCRAPE_MAX_PAGES="$_FLAG_SCRAPE_MAX_PAGES"
164
+ case " ${SPEC_TASKS:-} " in
165
+ *" scrape "*) ;;
166
+ *) SPEC_TASKS="${SPEC_TASKS:-} scrape" ;;
167
+ esac
168
+ fi
169
+ return 0
170
+ }
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # plan_apply_platform — fill platform-dependent defaults when not set
174
+ # ---------------------------------------------------------------------------
175
+ plan_apply_platform() {
176
+ # Theme source heuristic
177
+ if [[ -z "${SPEC_THEME_SOURCE:-}" ]]; then
178
+ case "${SPEC_PROFILE:-default}" in
179
+ github-pages) SPEC_THEME_SOURCE="remote" ;;
180
+ *) SPEC_THEME_SOURCE="gem" ;;
181
+ esac
182
+ fi
183
+
184
+ # Ensure github.repo defaults to REPOSITORY_NAME from env when set
185
+ if [[ -z "${SPEC_GITHUB_REPO:-}" && -n "${REPOSITORY_NAME:-}" ]]; then
186
+ SPEC_GITHUB_REPO="$REPOSITORY_NAME"
187
+ fi
188
+
189
+ # Devcontainer task auto-include for github-pages profile
190
+ if [[ "${SPEC_PROFILE:-}" == "github-pages" && -n "${SPEC_TASKS:-}" ]]; then
191
+ case "$SPEC_TASKS" in
192
+ *devcontainer*) ;;
193
+ *) SPEC_TASKS="$SPEC_TASKS devcontainer" ;;
194
+ esac
195
+ fi
196
+ }
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # plan_build TARGET_DIR [PROFILE]
200
+ # Orchestrates: set target_dir, resolve profile file, load it, apply flags,
201
+ # apply platform defaults.
202
+ # ---------------------------------------------------------------------------
203
+ plan_build() {
204
+ local target_dir="$1"
205
+ local profile_hint="${2:-${_FLAG_PROFILE:-default}}"
206
+
207
+ # Canonical absolute path
208
+ case "$target_dir" in
209
+ /*) ;;
210
+ *) target_dir="$(pwd)/${target_dir}" ;;
211
+ esac
212
+
213
+ # Seed required fields
214
+ SPEC_TARGET_DIR="$target_dir"
215
+ SPEC_PROFILE="${profile_hint}"
216
+
217
+ # Set global template variable mirrors used by template.sh
218
+ SITE_TITLE="${SPEC_SITE_TITLE:-${SITE_TITLE:-My Jekyll Site}}"
219
+ INSTALL_PROFILE="$SPEC_PROFILE"
220
+
221
+ # Locate profile file
222
+ local profile_file=""
223
+ if [[ -n "${TEMPLATES_DIR:-}" ]]; then
224
+ profile_file="${TEMPLATES_DIR}/profiles/${SPEC_PROFILE}.yml"
225
+ if [[ ! -f "$profile_file" ]]; then
226
+ log_warning "Profile not found: ${SPEC_PROFILE} — using defaults"
227
+ profile_file=""
228
+ fi
229
+ fi
230
+
231
+ # Layer 1: Profile defaults
232
+ [[ -n "$profile_file" ]] && plan_load_profile "$profile_file"
233
+
234
+ # Layer 2: Environment variable overrides (ZER0_SITE_* etc.)
235
+ [[ -n "${ZER0_SITE_TITLE:-}" ]] && SPEC_SITE_TITLE="$ZER0_SITE_TITLE"
236
+ [[ -n "${ZER0_SITE_AUTHOR:-}" ]] && SPEC_SITE_AUTHOR="$ZER0_SITE_AUTHOR"
237
+ [[ -n "${ZER0_SITE_EMAIL:-}" ]] && SPEC_SITE_EMAIL="$ZER0_SITE_EMAIL"
238
+ [[ -n "${ZER0_GITHUB_USER:-}" ]] && SPEC_GITHUB_USER="$ZER0_GITHUB_USER"
239
+ [[ -n "${ZER0_GITHUB_REPO:-}" ]] && SPEC_GITHUB_REPO="$ZER0_GITHUB_REPO"
240
+
241
+ # Layer 3: CLI flags (highest priority)
242
+ plan_apply_flags
243
+
244
+ # Layer 4: Platform defaults
245
+ plan_apply_platform
246
+
247
+ # Ensure tasks list is never empty
248
+ if [[ -z "${SPEC_TASKS:-}" ]]; then
249
+ SPEC_TASKS="config gemfile docker pages nav data gitignore readme marker"
250
+ fi
251
+
252
+ # Propagate spec values to template.sh globals
253
+ SITE_TITLE="${SPEC_SITE_TITLE:-My Jekyll Site}"
254
+ SITE_DESCRIPTION="${SPEC_SITE_DESCRIPTION:-}"
255
+ SITE_AUTHOR="${SPEC_SITE_AUTHOR:-}"
256
+ SITE_EMAIL="${SPEC_SITE_EMAIL:-}"
257
+ SITE_URL="${SPEC_SITE_URL:-}"
258
+ SITE_TIMEZONE="${SPEC_SITE_TIMEZONE:-UTC}"
259
+ SITE_LOCALE="${SPEC_SITE_LOCALE:-en}"
260
+ GITHUB_USER="${SPEC_GITHUB_USER:-}"
261
+ GITHUB_REPO="${SPEC_GITHUB_REPO:-}"
262
+ GITHUB_PAGES_BRANCH="${SPEC_GITHUB_PAGES_BRANCH:-gh-pages}"
263
+ THEME_SOURCE="${SPEC_THEME_SOURCE:-gem}"
264
+ REPOSITORY_NAME="${SPEC_GITHUB_REPO:-${REPOSITORY_NAME:-}}"
265
+
266
+ export SPEC_TARGET_DIR SPEC_PROFILE SPEC_TASKS SPEC_DEPLOY SPEC_AGENTS \
267
+ SPEC_SITE_TITLE SPEC_SITE_DESCRIPTION SPEC_SITE_URL \
268
+ SPEC_SITE_AUTHOR SPEC_SITE_EMAIL SPEC_SITE_TIMEZONE SPEC_SITE_LOCALE \
269
+ SPEC_GITHUB_USER SPEC_GITHUB_REPO SPEC_GITHUB_PAGES_BRANCH \
270
+ SPEC_GITHUB_ENABLE_PAGES SPEC_THEME_SOURCE SPEC_THEME_VERSION \
271
+ SPEC_OPT_DRY_RUN SPEC_OPT_FORCE SPEC_OPT_BACKUP \
272
+ SPEC_OPT_NON_INTERACTIVE SPEC_OPT_OUTPUT SPEC_OPT_AUTO_ACCEPT \
273
+ SPEC_OPT_SKIP_DOCTOR SPEC_OPT_VERBOSE \
274
+ SITE_TITLE SITE_DESCRIPTION SITE_AUTHOR SITE_EMAIL SITE_URL \
275
+ SITE_TIMEZONE SITE_LOCALE GITHUB_USER GITHUB_REPO \
276
+ GITHUB_PAGES_BRANCH THEME_SOURCE REPOSITORY_NAME INSTALL_PROFILE
277
+
278
+ log_debug "plan_build: target=$SPEC_TARGET_DIR profile=$SPEC_PROFILE tasks='$SPEC_TASKS'"
279
+ }
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # plan_print [FILE] — print the spec (uses spec_write to a tmp then cat)
283
+ # ---------------------------------------------------------------------------
284
+ plan_print() {
285
+ local out="${1:-}"
286
+ # Temporarily disable dry-run so spec_write actually writes
287
+ local saved_dry="${_FS_DRY_RUN:-0}"
288
+ _FS_DRY_RUN=0
289
+ if [[ -n "$out" ]]; then
290
+ spec_write "$out"
291
+ else
292
+ local tmp
293
+ tmp=$(mktemp /tmp/zer0-plan-XXXXXX.json)
294
+ spec_write "$tmp"
295
+ cat "$tmp"
296
+ rm -f "$tmp"
297
+ fi
298
+ _FS_DRY_RUN="$saved_dry"
299
+ }
@@ -0,0 +1,122 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/platform.sh — Platform & dependency detection
4
+ # =============================================================================
5
+ # Provides:
6
+ # platform_detect_os → Darwin | Linux | CYGWIN | MINGW | unknown
7
+ # platform_detect_runtime → macos | linux | wsl | windows | unknown
8
+ # platform_detect_ruby → "X.Y.Z" or "none"
9
+ # platform_ruby_lt_27 → returns 0 (true) when ruby < 2.7
10
+ # platform_needs_macos_gemfile → returns 0 when macOS + ruby < 2.7
11
+ # platform_detect_docker → "yes" | "no"
12
+ # platform_detect_gh → "yes" | "no"
13
+ # platform_detect_git → "yes" | "no"
14
+ # platform_detect_jq → "yes" | "no"
15
+ # platform_detect_bundler → "X.Y.Z" or "none"
16
+ # platform_summary → emit a JSON-compatible summary object
17
+ #
18
+ # Bash 3.2 compatible. No declare -A. No set -euo pipefail here.
19
+ # =============================================================================
20
+ [[ -n "${_HAS_PLATFORM_LIB:-}" ]] && return 0
21
+ _HAS_PLATFORM_LIB=1
22
+
23
+ platform_detect_os() {
24
+ uname -s 2>/dev/null || echo "unknown"
25
+ }
26
+
27
+ # Returns: macos | linux | wsl | windows | unknown
28
+ platform_detect_runtime() {
29
+ if [[ "${PLATFORM:-auto}" != "auto" ]]; then
30
+ echo "$PLATFORM"
31
+ return
32
+ fi
33
+ if grep -qiE '(microsoft|wsl)' /proc/version 2>/dev/null; then
34
+ echo "wsl"
35
+ return
36
+ fi
37
+ local os
38
+ os=$(platform_detect_os)
39
+ case "$os" in
40
+ Darwin) echo "macos" ;;
41
+ Linux) echo "linux" ;;
42
+ CYGWIN*|MINGW*) echo "windows" ;;
43
+ *) echo "unknown" ;;
44
+ esac
45
+ }
46
+
47
+ # Returns ruby version "X.Y.Z" or "none"
48
+ platform_detect_ruby() {
49
+ if ! command -v ruby >/dev/null 2>&1; then
50
+ echo "none"
51
+ return
52
+ fi
53
+ # Wrap in subshell at / to avoid Gemfile.lock interference
54
+ ( cd / && ruby --version 2>/dev/null ) \
55
+ | awk '{print $2}' \
56
+ | sed 's/p[0-9]*//' \
57
+ | sed 's/-.*//' \
58
+ | tr -d '\r\n'
59
+ }
60
+
61
+ # Returns 0 (true) when ruby < 2.7
62
+ platform_ruby_lt_27() {
63
+ local ver
64
+ ver=$(platform_detect_ruby)
65
+ [ "$ver" = "none" ] && return 1
66
+ awk -v ver="$ver" 'BEGIN {
67
+ n = split(ver, a, ".")
68
+ if (n >= 2 && a[1]+0 == 2 && a[2]+0 < 7) exit 0
69
+ exit 1
70
+ }'
71
+ }
72
+
73
+ # Returns 0 when macOS + system ruby < 2.7 (needs special Gemfile caps)
74
+ platform_needs_macos_gemfile() {
75
+ local os
76
+ os=$(platform_detect_os)
77
+ [ "$os" = "Darwin" ] && platform_ruby_lt_27
78
+ }
79
+
80
+ # Returns "X.Y.Z" or "none"
81
+ platform_detect_bundler() {
82
+ if ! command -v bundler >/dev/null 2>&1 && ! command -v bundle >/dev/null 2>&1; then
83
+ echo "none"
84
+ return
85
+ fi
86
+ ( cd / && bundle --version 2>/dev/null ) \
87
+ | awk '{print $NF}' \
88
+ | tr -d '\r\n'
89
+ }
90
+
91
+ platform_detect_docker() {
92
+ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
93
+ echo "yes"
94
+ else
95
+ echo "no"
96
+ fi
97
+ }
98
+
99
+ platform_detect_gh() {
100
+ command -v gh >/dev/null 2>&1 && echo "yes" || echo "no"
101
+ }
102
+
103
+ platform_detect_git() {
104
+ command -v git >/dev/null 2>&1 && echo "yes" || echo "no"
105
+ }
106
+
107
+ platform_detect_jq() {
108
+ command -v jq >/dev/null 2>&1 && echo "yes" || echo "no"
109
+ }
110
+
111
+ # Emit a compact single-line summary of detected platform (JSON-compatible)
112
+ platform_summary() {
113
+ printf '{"os":"%s","runtime":"%s","ruby":"%s","bundler":"%s","docker":"%s","gh":"%s","git":"%s","jq":"%s"}\n' \
114
+ "$(platform_detect_os)" \
115
+ "$(platform_detect_runtime)" \
116
+ "$(platform_detect_ruby)" \
117
+ "$(platform_detect_bundler)" \
118
+ "$(platform_detect_docker)" \
119
+ "$(platform_detect_gh)" \
120
+ "$(platform_detect_git)" \
121
+ "$(platform_detect_jq)"
122
+ }
@@ -0,0 +1,124 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/prompt.sh — Interactive TTY prompts
4
+ # =============================================================================
5
+ # Provides:
6
+ # prompt_ask VAR_NAME QUESTION [DEFAULT]
7
+ # Read a string answer into VAR_NAME. Prints default in brackets.
8
+ # Returns default if user enters blank.
9
+ # No-ops (uses DEFAULT) when _PROMPT_NON_INTERACTIVE=1.
10
+ #
11
+ # prompt_confirm QUESTION [DEFAULT_Y]
12
+ # Ask a y/N question. Returns 0 for yes, 1 for no.
13
+ # DEFAULT_Y="y" makes the default yes.
14
+ # No-ops returning yes when _PROMPT_AUTO_ACCEPT=1.
15
+ #
16
+ # prompt_select VAR_NAME QUESTION OPTION1 [OPTION2 ...]
17
+ # Present a numbered menu. Sets VAR_NAME to chosen option.
18
+ # No-ops (uses first option) when _PROMPT_NON_INTERACTIVE=1.
19
+ #
20
+ # Globals:
21
+ # _PROMPT_NON_INTERACTIVE — "1" → never read, use defaults
22
+ # _PROMPT_AUTO_ACCEPT — "1" → confirm always returns yes
23
+ #
24
+ # Bash 3.2 compatible. No set -euo pipefail here.
25
+ # =============================================================================
26
+ [[ -n "${_HAS_PROMPT_LIB:-}" ]] && return 0
27
+ _HAS_PROMPT_LIB=1
28
+
29
+ _PROMPT_NON_INTERACTIVE="${_PROMPT_NON_INTERACTIVE:-0}"
30
+ _PROMPT_AUTO_ACCEPT="${_PROMPT_AUTO_ACCEPT:-0}"
31
+
32
+ # Prompt for a string value.
33
+ # Usage: prompt_ask VARNAME "Question text" ["default value"]
34
+ prompt_ask() {
35
+ local _var_name="$1"
36
+ local _question="$2"
37
+ local _default="${3:-}"
38
+ local _answer=""
39
+
40
+ if [[ "${_PROMPT_NON_INTERACTIVE:-0}" == "1" ]]; then
41
+ # Non-interactive: use default or fail if required
42
+ if [[ -z "$_default" ]]; then
43
+ log_error "prompt_ask: required value '$_var_name' has no default in non-interactive mode"
44
+ return 1
45
+ fi
46
+ _answer="$_default"
47
+ else
48
+ if [[ -n "$_default" ]]; then
49
+ printf "${_LOG_BLUE:-}?${_LOG_NC:-} %s [%s]: " "$_question" "$_default" >&2
50
+ else
51
+ printf "${_LOG_BLUE:-}?${_LOG_NC:-} %s: " "$_question" >&2
52
+ fi
53
+ read -r _answer </dev/tty
54
+ _answer="${_answer:-$_default}"
55
+ fi
56
+
57
+ # Assign to the named variable (bash 3.2 compatible — no nameref)
58
+ eval "${_var_name}=\$_answer"
59
+ }
60
+
61
+ # Ask a yes/no confirmation.
62
+ # Usage: prompt_confirm "Question?" ["y"|"n"] → returns 0=yes 1=no
63
+ prompt_confirm() {
64
+ local _question="$1"
65
+ local _default="${2:-n}"
66
+
67
+ if [[ "${_PROMPT_AUTO_ACCEPT:-0}" == "1" || "${_PROMPT_NON_INTERACTIVE:-0}" == "1" ]]; then
68
+ return 0
69
+ fi
70
+
71
+ local _prompt
72
+ if [[ "$_default" == "y" || "$_default" == "Y" ]]; then
73
+ _prompt="[Y/n]"
74
+ else
75
+ _prompt="[y/N]"
76
+ fi
77
+
78
+ local _answer=""
79
+ printf "${_LOG_BLUE:-}?${_LOG_NC:-} %s %s: " "$_question" "$_prompt" >&2
80
+ read -r _answer </dev/tty
81
+ _answer="${_answer:-$_default}"
82
+
83
+ case "$_answer" in
84
+ [yY][eE][sS]|[yY]) return 0 ;;
85
+ *) return 1 ;;
86
+ esac
87
+ }
88
+
89
+ # Present a numbered menu.
90
+ # Usage: prompt_select VARNAME "Question?" opt1 opt2 opt3
91
+ prompt_select() {
92
+ local _var_name="$1"
93
+ local _question="$2"
94
+ shift 2
95
+ local _options=("$@")
96
+ local _count=${#_options[@]}
97
+
98
+ if [[ "${_PROMPT_NON_INTERACTIVE:-0}" == "1" ]]; then
99
+ eval "${_var_name}=\${_options[0]}"
100
+ return 0
101
+ fi
102
+
103
+ printf "${_LOG_BLUE:-}?${_LOG_NC:-} %s\n" "$_question" >&2
104
+ local _i=1
105
+ while [[ $_i -le $_count ]]; do
106
+ printf " %d) %s\n" "$_i" "${_options[$(( _i - 1 ))]}" >&2
107
+ _i=$(( _i + 1 ))
108
+ done
109
+
110
+ local _choice=""
111
+ while true; do
112
+ printf " Enter 1-%d [1]: " "$_count" >&2
113
+ read -r _choice </dev/tty
114
+ _choice="${_choice:-1}"
115
+ if [[ "$_choice" =~ ^[0-9]+$ ]] && \
116
+ [[ "$_choice" -ge 1 ]] && \
117
+ [[ "$_choice" -le "$_count" ]]; then
118
+ break
119
+ fi
120
+ printf " Invalid choice. Try again.\n" >&2
121
+ done
122
+
123
+ eval "${_var_name}=\${_options[$(( _choice - 1 ))]}"
124
+ }
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # scripts/install/repair.sh — Re-apply spec to fix drift
4
+ # =============================================================================
5
+ # Compares spec intent against disk state (diff.sh), then re-applies
6
+ # only the tasks whose output files are missing or changed.
7
+ #
8
+ # Provides:
9
+ # repair_run TARGET_DIR
10
+ #
11
+ # Bash 3.2 compatible. No set -euo pipefail here.
12
+ # =============================================================================
13
+ [[ -n "${_HAS_REPAIR_LIB:-}" ]] && return 0
14
+ _HAS_REPAIR_LIB=1
15
+
16
+ repair_run() {
17
+ local target="${1:-$(pwd)}"
18
+ local spec_file
19
+ spec_file="$(spec_path "$target")"
20
+
21
+ if [[ ! -f "$spec_file" ]]; then
22
+ log_error "repair: no spec found at $spec_file"
23
+ return 1
24
+ fi
25
+
26
+ log_info "Checking for drift at: $target"
27
+
28
+ # Run diff (non-destructive) — prints what needs repair
29
+ diff_spec "$spec_file"
30
+ local diff_ret=$?
31
+
32
+ if [[ $diff_ret -eq 0 ]]; then
33
+ log_success "No drift detected — nothing to repair."
34
+ return 0
35
+ fi
36
+
37
+ log_info "Drift detected. Re-applying spec..."
38
+ spec_read "$spec_file"
39
+
40
+ # Force-rewrite everything to fix drift
41
+ _FS_FORCE=1
42
+ export _FS_FORCE
43
+
44
+ apply_run "$spec_file"
45
+ }