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.
@@ -0,0 +1,717 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/bin/install
3
+ #
4
+ # Canonical CLI entrypoint for jekyll-theme-zer0 installer.
5
+ # Sibling of scripts/bin/{build,release,test}.
6
+ #
7
+ # This is the Phase 2 dispatcher. Subcommands progressively delegate to
8
+ # scripts/lib/install/* modules. Until each subcommand's backing module
9
+ # lands (Phases 3-6), most subcommands print a clear "not yet wired"
10
+ # notice and exit non-zero.
11
+ #
12
+ # The legacy install.sh remains the supported entrypoint for `init`
13
+ # (the default subcommand). This shim translates the new subcommand
14
+ # surface into legacy flags where possible.
15
+
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
20
+ LEGACY_INSTALL="$REPO_ROOT/install.sh"
21
+ LIB_DIR="$REPO_ROOT/scripts/lib/install"
22
+
23
+ # Source minimal logging if available; otherwise inline shim.
24
+ if [[ -f "$LIB_DIR/logging.sh" ]]; then
25
+ # shellcheck source=../lib/install/logging.sh
26
+ source "$LIB_DIR/logging.sh"
27
+ else
28
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
29
+ log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
30
+ log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
31
+ log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
32
+ log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
33
+ fi
34
+
35
+ # Source profile loader (Phase 3). Falls back to a minimal stub when missing.
36
+ if [[ -f "$LIB_DIR/profile.sh" ]]; then
37
+ # shellcheck source=../lib/install/profile.sh
38
+ source "$LIB_DIR/profile.sh"
39
+ _HAS_PROFILE_LOADER=1
40
+ else
41
+ _HAS_PROFILE_LOADER=0
42
+ fi
43
+
44
+ # Source deploy registry (Phase 4). Optional — list-targets/deploy degrade
45
+ # to a static help message when missing.
46
+ if [[ -f "$LIB_DIR/deploy/registry.sh" ]]; then
47
+ # shellcheck source=../lib/install/deploy/registry.sh
48
+ source "$LIB_DIR/deploy/registry.sh"
49
+ _HAS_DEPLOY_REGISTRY=1
50
+ else
51
+ _HAS_DEPLOY_REGISTRY=0
52
+ fi
53
+
54
+ # Source agents copier (Phase 5). Optional.
55
+ if [[ -f "$LIB_DIR/agents.sh" ]]; then
56
+ # shellcheck source=../lib/install/agents.sh
57
+ source "$LIB_DIR/agents.sh"
58
+ _HAS_AGENTS_LIB=1
59
+ else
60
+ _HAS_AGENTS_LIB=0
61
+ fi
62
+
63
+ # Source AI modules (Phase 5). All optional and gated behind explicit flags.
64
+ _HAS_AI_LIBS=0
65
+ if [[ -f "$LIB_DIR/ai/openai.sh" ]]; then
66
+ # shellcheck source=../lib/install/ai/openai.sh
67
+ source "$LIB_DIR/ai/openai.sh"
68
+ [[ -f "$LIB_DIR/ai/wizard.sh" ]] && source "$LIB_DIR/ai/wizard.sh"
69
+ [[ -f "$LIB_DIR/ai/diagnose.sh" ]] && source "$LIB_DIR/ai/diagnose.sh"
70
+ [[ -f "$LIB_DIR/ai/suggest.sh" ]] && source "$LIB_DIR/ai/suggest.sh"
71
+ _HAS_AI_LIBS=1
72
+ fi
73
+
74
+ # Source Phase 6 modules (doctor / upgrade / non-AI wizard). All optional.
75
+ _HAS_DOCTOR_LIB=0
76
+ [[ -f "$LIB_DIR/doctor.sh" ]] && { source "$LIB_DIR/doctor.sh"; _HAS_DOCTOR_LIB=1; }
77
+ _HAS_UPGRADE_LIB=0
78
+ [[ -f "$LIB_DIR/upgrade.sh" ]] && { source "$LIB_DIR/upgrade.sh"; _HAS_UPGRADE_LIB=1; }
79
+ _HAS_WIZARD_INTERACTIVE_LIB=0
80
+ [[ -f "$LIB_DIR/wizard_interactive.sh" ]] && { source "$LIB_DIR/wizard_interactive.sh"; _HAS_WIZARD_INTERACTIVE_LIB=1; }
81
+
82
+ # ---------- Help text -------------------------------------------------------
83
+ show_usage() {
84
+ cat <<'EOF'
85
+ 🚀 jekyll-theme-zer0 installer (scripts/bin/install)
86
+
87
+ USAGE:
88
+ ./scripts/bin/install <subcommand> [options] [target-dir]
89
+
90
+ SUBCOMMANDS:
91
+ init [target] Install / scaffold a site (default subcommand)
92
+ Maps to legacy install.sh with --full / --minimal /
93
+ --fork / --github / --remote selected via --profile.
94
+
95
+ wizard [target] Interactive guided setup. Add --ai for OpenAI-
96
+ powered config generation (requires OPENAI_API_KEY).
97
+ deploy <target> Configure a deployment target
98
+ (github-pages | azure-swa | docker-prod). Comma-
99
+ separate to install multiple in one run.
100
+ Use --ai-suggest to ask for a recommendation first.
101
+ diagnose [target] Analyze the last `jekyll build` output for known
102
+ errors. Add --ai for OpenAI-powered patch suggestions.
103
+ doctor [target] Environment + site health check (platform tools,
104
+ Docker, Ruby, gh CLI; --ai for OpenAI connectivity)
105
+ upgrade [target] Re-run installer over an existing site (refreshes
106
+ agent files + flags drifted workflows; never touches
107
+ user content). Use --from <ver> to override detection.
108
+ list-profiles Print available install profiles
109
+ list-targets Print available deploy targets
110
+ agents [target] Install AI agent guidance files (AGENTS.md +
111
+ .github/copilot-instructions.md + instructions/
112
+ + prompts/). Add --cursor / --claude / --aider /
113
+ --all to include extra integrations.
114
+ version Print theme version
115
+ help Show this help
116
+
117
+ GLOBAL OPTIONS:
118
+ -p, --profile <name> Profile to apply: minimal|full|fork|github|remote
119
+ (default: full)
120
+ -f, --force Skip safety prompts
121
+ -n, --dry-run Print actions without executing (when supported)
122
+ -v, --verbose Enable verbose logging
123
+ -h, --help Show help for a subcommand
124
+
125
+ EXAMPLES:
126
+ ./scripts/bin/install init # full install in CWD
127
+ ./scripts/bin/install init --profile minimal /tmp/demo
128
+ ./scripts/bin/install init --profile fork
129
+ ./scripts/bin/install agents
130
+ ./scripts/bin/install version
131
+
132
+ NOTES:
133
+ • This is the Phase 2 dispatcher. Subcommands marked "Phase N" emit a
134
+ clear notice and exit non-zero until that phase lands.
135
+ • The legacy install.sh remains the supported low-level entrypoint and
136
+ is invoked by `init` under the hood.
137
+
138
+ EOF
139
+ }
140
+
141
+ # ---------- Helpers ---------------------------------------------------------
142
+ require_legacy_install() {
143
+ if [[ ! -x "$LEGACY_INSTALL" ]]; then
144
+ log_error "Cannot find executable install.sh at: $LEGACY_INSTALL"
145
+ exit 1
146
+ fi
147
+ }
148
+
149
+ print_phase_stub() {
150
+ local name="$1" phase="$2"
151
+ log_warning "Subcommand '$name' is not yet wired (planned for $phase)."
152
+ log_info "Track progress in scripts/lib/install/README.md (Roadmap)."
153
+ exit 2
154
+ }
155
+
156
+ print_version() {
157
+ local vfile="$REPO_ROOT/lib/jekyll-theme-zer0/version.rb"
158
+ if [[ -f "$vfile" ]]; then
159
+ # Extract the VERSION = "x.y.z" line
160
+ local v
161
+ v="$(grep -E 'VERSION\s*=' "$vfile" | head -n1 | sed -E 's/.*"([^"]+)".*/\1/')"
162
+ echo "jekyll-theme-zer0 ${v:-unknown}"
163
+ else
164
+ echo "jekyll-theme-zer0 (version file not found)"
165
+ fi
166
+ }
167
+
168
+ print_agents_index() {
169
+ local agents_file="$REPO_ROOT/AGENTS.md"
170
+ if [[ -f "$agents_file" ]]; then
171
+ log_info "AGENTS.md (cross-tool entry point):"
172
+ echo " $agents_file"
173
+ echo
174
+ log_info "Detailed Copilot instructions:"
175
+ echo " $REPO_ROOT/.github/copilot-instructions.md"
176
+ echo
177
+ log_info "File-scoped instruction sets:"
178
+ if [[ -d "$REPO_ROOT/.github/instructions" ]]; then
179
+ ls "$REPO_ROOT/.github/instructions"/*.instructions.md 2>/dev/null \
180
+ | sed "s#$REPO_ROOT/# #"
181
+ fi
182
+ else
183
+ log_warning "AGENTS.md not found at $agents_file"
184
+ fi
185
+ }
186
+
187
+ # Run the `agents` subcommand: copy agent guidance into a target dir.
188
+ # Falls back to print_agents_index if no target given (or `--index` flag).
189
+ run_agents() {
190
+ local target="" with_cursor="" with_claude="" with_aider="" with_all="" force="" want_index=0
191
+ while [[ $# -gt 0 ]]; do
192
+ case "$1" in
193
+ --cursor) with_cursor="--cursor" ;;
194
+ --claude) with_claude="--claude" ;;
195
+ --aider) with_aider="--aider" ;;
196
+ --all) with_all="--all" ;;
197
+ -f|--force) force="--force" ;;
198
+ --index) want_index=1 ;;
199
+ -h|--help)
200
+ cat <<EOF
201
+ Usage: ./scripts/bin/install agents [target-dir] [options]
202
+
203
+ Copies AI agent guidance files from this theme into TARGET-DIR (defaults to CWD):
204
+ - AGENTS.md
205
+ - .github/copilot-instructions.md
206
+ - .github/instructions/*.md
207
+ - .github/prompts/*.md
208
+
209
+ Options:
210
+ --cursor Also copy .cursor/commands/*.md
211
+ --claude Also write CLAUDE.md stub
212
+ --aider Also write .aider.conf.yml
213
+ --all Shortcut for --cursor --claude --aider
214
+ --force, -f Overwrite existing files (default: skip + warn)
215
+ --index Print the agent guidance index (legacy behavior) and exit
216
+
217
+ Examples:
218
+ ./scripts/bin/install agents # Copy core set into CWD
219
+ ./scripts/bin/install agents /tmp/site --all
220
+ ./scripts/bin/install agents --index # Just print where files live
221
+ EOF
222
+ exit 0 ;;
223
+ -*) log_error "Unknown option: $1"; exit 1 ;;
224
+ *)
225
+ if [[ -z "$target" ]]; then target="$1"
226
+ else log_error "Unexpected argument: $1"; exit 1
227
+ fi ;;
228
+ esac
229
+ shift
230
+ done
231
+
232
+ if [[ "$want_index" = "1" ]]; then
233
+ print_agents_index
234
+ return 0
235
+ fi
236
+
237
+ if [[ "$_HAS_AGENTS_LIB" != "1" ]]; then
238
+ log_error "agents.sh module not found at $LIB_DIR/agents.sh"
239
+ exit 1
240
+ fi
241
+
242
+ [[ -z "$target" ]] && target="$PWD"
243
+ agents_install "$target" "$REPO_ROOT" $with_cursor $with_claude $with_aider $with_all $force
244
+ }
245
+
246
+ # Run the `wizard` subcommand. With --ai, delegates to wizard_ai_run.
247
+ # Without --ai, currently still routes to legacy install.sh --full as a
248
+ # scaffolding fallback (Phase 5 ships AI path; non-AI wizard is Phase 6).
249
+ run_wizard() {
250
+ local use_ai=0 auto_accept=0 target=""
251
+ while [[ $# -gt 0 ]]; do
252
+ case "$1" in
253
+ --ai) use_ai=1 ;;
254
+ --auto-accept) auto_accept=1 ;;
255
+ -h|--help)
256
+ cat <<EOF
257
+ Usage: ./scripts/bin/install wizard [target-dir] [--ai] [--auto-accept]
258
+
259
+ --ai Use OpenAI to generate site title, description, navigation,
260
+ and a welcome-post outline (requires OPENAI_API_KEY).
261
+ --auto-accept Skip confirmation prompts (for CI use).
262
+
263
+ Without --ai, runs an interactive prompt-based scaffold (no network calls).
264
+ The wizard auto-skips the doctor preflight when dispatching to 'init'.
265
+ EOF
266
+ exit 0 ;;
267
+ -*) log_error "Unknown option: $1"; exit 1 ;;
268
+ *)
269
+ if [[ -z "$target" ]]; then target="$1"
270
+ else log_error "Unexpected argument: $1"; exit 1
271
+ fi ;;
272
+ esac
273
+ shift
274
+ done
275
+ [[ -z "$target" ]] && target="$PWD"
276
+
277
+ if [[ "$use_ai" = "1" ]]; then
278
+ if [[ "$_HAS_AI_LIBS" != "1" ]]; then
279
+ log_error "AI modules not found at $LIB_DIR/ai/"
280
+ exit 1
281
+ fi
282
+ local extra=""
283
+ [[ "$auto_accept" = "1" ]] && extra="--auto-accept"
284
+ if wizard_ai_run "$target" "$REPO_ROOT" $extra; then
285
+ return 0
286
+ fi
287
+ log_warning "AI wizard failed — falling back to interactive scaffold."
288
+ fi
289
+
290
+ # Non-AI interactive wizard (Phase 6). Falls back to legacy behavior
291
+ # if the module is missing.
292
+ if [[ "$_HAS_WIZARD_INTERACTIVE_LIB" = "1" ]]; then
293
+ local extra=""
294
+ [[ "$auto_accept" = "1" ]] && extra="--auto-accept"
295
+ wizard_interactive_run "$target" "$REPO_ROOT" $extra
296
+ return $?
297
+ fi
298
+
299
+ log_warning "Interactive wizard module not available; running 'init --profile full' instead."
300
+ run_init --profile full "$target"
301
+ }
302
+
303
+ # Run the `diagnose` subcommand.
304
+ run_diagnose() {
305
+ if [[ "$_HAS_AI_LIBS" != "1" ]]; then
306
+ log_error "Diagnose modules not found at $LIB_DIR/ai/"
307
+ exit 1
308
+ fi
309
+ local args=() target=""
310
+ while [[ $# -gt 0 ]]; do
311
+ case "$1" in
312
+ -h|--help)
313
+ cat <<EOF
314
+ Usage: ./scripts/bin/install diagnose [target-dir] [--log <file>] [--ai] [--auto-accept]
315
+
316
+ Pattern-matches a Jekyll build log against a curated list of known errors.
317
+ With --ai, also sends a sanitized log + _config.yml + Gemfile to OpenAI for
318
+ a proposed fix (requires OPENAI_API_KEY).
319
+
320
+ Options:
321
+ --log <file> Use an existing log file instead of running 'jekyll build'.
322
+ --ai Enable AI analysis (opt-in; sanitized).
323
+ --auto-accept Skip the API-call confirmation prompt.
324
+
325
+ Examples:
326
+ ./scripts/bin/install diagnose
327
+ ./scripts/bin/install diagnose --log build.log --ai
328
+ EOF
329
+ exit 0 ;;
330
+ --log)
331
+ args+=("--log" "${2:-}")
332
+ shift ;;
333
+ --ai|--auto-accept) args+=("$1") ;;
334
+ -*) log_error "Unknown option: $1"; exit 1 ;;
335
+ *)
336
+ if [[ -z "$target" ]]; then target="$1"
337
+ else args+=("$1")
338
+ fi ;;
339
+ esac
340
+ shift
341
+ done
342
+ [[ -z "$target" ]] && target="$PWD"
343
+ diagnose_run "$target" "$REPO_ROOT" ${args[@]+"${args[@]}"}
344
+ }
345
+
346
+ # Run the `doctor` subcommand (Phase 6).
347
+ run_doctor() {
348
+ if [[ "$_HAS_DOCTOR_LIB" != "1" ]]; then
349
+ log_error "doctor.sh module not found at $LIB_DIR/doctor.sh"
350
+ exit 1
351
+ fi
352
+ local target="" args=()
353
+ while [[ $# -gt 0 ]]; do
354
+ case "$1" in
355
+ -h|--help)
356
+ cat <<EOF
357
+ Usage: ./scripts/bin/install doctor [target-dir] [--ai] [--quiet] [--json]
358
+
359
+ Runs platform + tooling + site health checks. Exits 0 when no FAIL,
360
+ 1 otherwise. Use --ai to also probe OpenAI API connectivity (opt-in;
361
+ honors ZER0_NO_AI=1 kill-switch).
362
+
363
+ Options:
364
+ --ai Include OpenAI connectivity check (requires OPENAI_API_KEY)
365
+ --quiet Suppress section headers (still prints summary)
366
+ --json Emit a machine-readable {"pass":N,"warn":N,"fail":N} summary
367
+ EOF
368
+ exit 0 ;;
369
+ --ai|--quiet|--json) args+=("$1") ;;
370
+ -*) log_error "Unknown option: $1"; exit 1 ;;
371
+ *)
372
+ if [[ -z "$target" ]]; then target="$1"
373
+ else log_error "Unexpected argument: $1"; exit 1
374
+ fi ;;
375
+ esac
376
+ shift
377
+ done
378
+ [[ -z "$target" ]] && target="$PWD"
379
+ doctor_run "$target" "$REPO_ROOT" ${args[@]+"${args[@]}"}
380
+ }
381
+
382
+ # Run the `upgrade` subcommand (Phase 6).
383
+ run_upgrade() {
384
+ if [[ "$_HAS_UPGRADE_LIB" != "1" ]]; then
385
+ log_error "upgrade.sh module not found at $LIB_DIR/upgrade.sh"
386
+ exit 1
387
+ fi
388
+ local target="" args=()
389
+ while [[ $# -gt 0 ]]; do
390
+ case "$1" in
391
+ -h|--help)
392
+ cat <<EOF
393
+ Usage: ./scripts/bin/install upgrade [target-dir] [--from <version>]
394
+ [--force] [--dry-run] [--auto-accept]
395
+
396
+ Re-runs the installer over an existing site. Refreshes AI agent files
397
+ (always additive), flags drifted .github/workflows/, and writes a
398
+ .zer0-installed marker. Never touches user content under pages/, _posts/,
399
+ _drafts/.
400
+
401
+ Options:
402
+ --from <ver> Override detected installed version
403
+ -f, --force Re-run even if already on the latest version
404
+ -n, --dry-run Show what would change without writing files
405
+ --auto-accept Skip the interactive confirmation prompt
406
+ EOF
407
+ exit 0 ;;
408
+ --from)
409
+ args+=("--from" "${2:-}"); shift ;;
410
+ -f|--force|-n|--dry-run|--auto-accept) args+=("$1") ;;
411
+ -*) log_error "Unknown option: $1"; exit 1 ;;
412
+ *)
413
+ if [[ -z "$target" ]]; then target="$1"
414
+ else log_error "Unexpected argument: $1"; exit 1
415
+ fi ;;
416
+ esac
417
+ shift
418
+ done
419
+ [[ -z "$target" ]] && target="$PWD"
420
+ upgrade_run "$target" "$REPO_ROOT" ${args[@]+"${args[@]}"}
421
+ }
422
+
423
+ list_profiles() {
424
+ if [[ "$_HAS_PROFILE_LOADER" == "1" ]] && [[ -d "$(profiles_dir "$REPO_ROOT")" ]]; then
425
+ echo "Available install profiles (from templates/profiles/):"
426
+ echo
427
+ local p path
428
+ while IFS= read -r p; do
429
+ [ -z "$p" ] && continue
430
+ path="$(profile_path "$REPO_ROOT" "$p")" || continue
431
+ profile_print_summary "$path"
432
+ done < <(list_profile_names "$REPO_ROOT")
433
+ return
434
+ fi
435
+ cat <<EOF
436
+ Available install profiles (Phase 3 — profile loader not available, showing static map):
437
+
438
+ minimal Smallest viable site (Gemfile, _config.yml, index.md)
439
+ → install.sh --minimal
440
+ full Default. All starter pages, navigation, admin settings, wizard
441
+ → install.sh --full
442
+ fork Fork-friendly install: cleans creator content, seeds welcome post
443
+ → install.sh --fork
444
+ github GitHub Pages remote_theme installation
445
+ → install.sh --github
446
+ remote Bootstrap from raw.githubusercontent.com (curl|bash style)
447
+ → install.sh --remote
448
+ EOF
449
+ }
450
+
451
+ list_targets() {
452
+ if [[ "$_HAS_DEPLOY_REGISTRY" == "1" ]]; then
453
+ echo "Available deploy targets:"
454
+ echo
455
+ local t
456
+ for t in $DEPLOY_TARGETS_LIST; do
457
+ deploy_print_summary "$t" "$REPO_ROOT"
458
+ done
459
+ echo "Usage: ./scripts/bin/install deploy <target>[,<target>...] [target-dir]"
460
+ return
461
+ fi
462
+ cat <<EOF
463
+ Available deploy targets (Phase 4 — registry not available, showing static map):
464
+
465
+ github-pages GitHub Pages with peaceiris/actions-gh-pages workflow
466
+ azure-swa Azure Static Web Apps (workflow + staticwebapp.config.json)
467
+ docker-prod Self-hosted production Docker image (Ruby builder + nginx:alpine)
468
+ EOF
469
+ }
470
+
471
+ # Translate `deploy <target>[,<target>...] [dir]` into registry calls.
472
+ run_deploy() {
473
+ if [[ "$_HAS_DEPLOY_REGISTRY" != "1" ]]; then
474
+ log_error "Deploy registry not available at $LIB_DIR/deploy/registry.sh"
475
+ exit 1
476
+ fi
477
+
478
+ local targets_arg=""
479
+ local target_dir=""
480
+ local force=0
481
+ local ai_suggest=0
482
+ local auto_accept=0
483
+
484
+ while [[ $# -gt 0 ]]; do
485
+ case "$1" in
486
+ -f|--force) force=1; shift ;;
487
+ --ai-suggest) ai_suggest=1; shift ;;
488
+ --auto-accept) auto_accept=1; shift ;;
489
+ -h|--help)
490
+ cat <<EOF
491
+ Usage: ./scripts/bin/install deploy <target>[,<target>...] [target-dir]
492
+
493
+ Targets:
494
+ github-pages, azure-swa, docker-prod
495
+
496
+ Options:
497
+ -f, --force Overwrite existing files (default: skip + warn)
498
+ --ai-suggest Recommend a target before installing (rule-based;
499
+ add OPENAI_API_KEY for AI-assisted rationale)
500
+ --auto-accept Skip API-call confirmation prompts
501
+ -h, --help Show this help
502
+
503
+ Examples:
504
+ ./scripts/bin/install deploy github-pages
505
+ ./scripts/bin/install deploy --ai-suggest /tmp/site
506
+ ./scripts/bin/install deploy github-pages,docker-prod /tmp/site
507
+ EOF
508
+ exit 0
509
+ ;;
510
+ -*)
511
+ log_error "Unknown option: $1"
512
+ exit 1
513
+ ;;
514
+ *)
515
+ if [[ -z "$targets_arg" ]]; then
516
+ targets_arg="$1"
517
+ elif [[ -z "$target_dir" ]]; then
518
+ target_dir="$1"
519
+ else
520
+ log_error "Unexpected argument: $1"
521
+ exit 1
522
+ fi
523
+ shift
524
+ ;;
525
+ esac
526
+ done
527
+
528
+ # AI-suggest mode: recommend a target, then either install or just print.
529
+ if [[ "$ai_suggest" = "1" ]]; then
530
+ if [[ "$_HAS_AI_LIBS" != "1" ]]; then
531
+ log_error "AI modules not found at $LIB_DIR/ai/"
532
+ exit 1
533
+ fi
534
+ # If no target arg, treat the first positional as the dir instead.
535
+ if [[ -z "$target_dir" ]] && [[ -n "$targets_arg" ]] && [[ -d "$targets_arg" ]]; then
536
+ target_dir="$targets_arg"
537
+ targets_arg=""
538
+ fi
539
+ [[ -z "$target_dir" ]] && target_dir="$PWD"
540
+
541
+ local ai_args=()
542
+ # Use OpenAI path only if key present; otherwise rule-based (no flag).
543
+ [[ -n "${OPENAI_API_KEY:-}" ]] && ai_args+=("--ai")
544
+ [[ "$auto_accept" = "1" ]] && ai_args+=("--auto-accept")
545
+
546
+ local recommended
547
+ if ! recommended="$(suggest_deploy_target "$target_dir" "$REPO_ROOT" ${ai_args[@]+"${ai_args[@]}"})"; then
548
+ log_error "Suggestion failed."
549
+ exit 1
550
+ fi
551
+ echo
552
+ log_info "Recommended target: $recommended"
553
+ if [[ -z "$targets_arg" ]]; then
554
+ log_info "Re-run with the slug to install: ./scripts/bin/install deploy $recommended ${target_dir}"
555
+ return 0
556
+ fi
557
+ # If user also provided an explicit slug, honor that.
558
+ fi
559
+
560
+ if [[ -z "$targets_arg" ]]; then
561
+ log_error "No deploy target specified. Run 'install list-targets' to see options."
562
+ exit 1
563
+ fi
564
+
565
+ [[ -z "$target_dir" ]] && target_dir="$PWD"
566
+ [[ "$force" = "1" ]] && export DEPLOY_FORCE=1
567
+
568
+ # Default site name from target dir for placeholder substitution.
569
+ [[ -z "${DEPLOY_SITE_NAME:-}" ]] && export DEPLOY_SITE_NAME="$(basename "$target_dir")"
570
+
571
+ local IFS=','
572
+ # Capture into a positional array, then restore IFS before dispatching
573
+ # so registry helpers (which split space-separated lists) work correctly.
574
+ set -- $targets_arg
575
+ unset IFS
576
+ local t fail=0
577
+ for t in "$@"; do
578
+ [ -z "$t" ] && continue
579
+ deploy_run_target "$t" "$target_dir" "$REPO_ROOT" || fail=1
580
+ echo
581
+ done
582
+ [ "$fail" = "0" ] || exit 1
583
+ }
584
+
585
+ # Translate `init` subcommand options into legacy install.sh flags.
586
+ run_init() {
587
+ require_legacy_install
588
+ local profile="full"
589
+ local target=""
590
+ local extra_args=()
591
+ local skip_doctor=0
592
+
593
+ while [[ $# -gt 0 ]]; do
594
+ case "$1" in
595
+ -p|--profile)
596
+ profile="${2:-}"
597
+ if [[ -z "$profile" ]]; then
598
+ log_error "--profile requires a value"
599
+ exit 1
600
+ fi
601
+ shift 2
602
+ ;;
603
+ --skip-doctor)
604
+ skip_doctor=1
605
+ shift
606
+ ;;
607
+ -h|--help)
608
+ cat <<EOF
609
+ Usage: ./scripts/bin/install init [options] [target-dir]
610
+
611
+ -p, --profile <name> minimal | full | fork | github | remote (default: full)
612
+ --skip-doctor Skip the doctor preflight (faster but no env validation)
613
+ -h, --help Show this help
614
+
615
+ Any other flags are forwarded to install.sh as-is.
616
+ EOF
617
+ exit 0
618
+ ;;
619
+ --)
620
+ shift
621
+ extra_args+=("$@")
622
+ break
623
+ ;;
624
+ -*)
625
+ extra_args+=("$1")
626
+ shift
627
+ ;;
628
+ *)
629
+ if [[ -z "$target" ]]; then
630
+ target="$1"
631
+ else
632
+ extra_args+=("$1")
633
+ fi
634
+ shift
635
+ ;;
636
+ esac
637
+ done
638
+
639
+ local profile_flag=""
640
+
641
+ # Prefer the profile YAML for legacy_flag resolution when available.
642
+ if [[ "$_HAS_PROFILE_LOADER" == "1" ]]; then
643
+ local profile_file
644
+ if profile_file="$(profile_path "$REPO_ROOT" "$profile" 2>/dev/null)"; then
645
+ profile_flag="$(profile_get_scalar "$profile_file" legacy_flag)"
646
+ fi
647
+ fi
648
+
649
+ if [[ -z "$profile_flag" ]]; then
650
+ # Fallback to the static map (also used when profile.sh is absent).
651
+ case "$profile" in
652
+ minimal) profile_flag="--minimal" ;;
653
+ full) profile_flag="--full" ;;
654
+ fork) profile_flag="--fork" ;;
655
+ github) profile_flag="--github" ;;
656
+ remote) profile_flag="--remote" ;;
657
+ *)
658
+ log_error "Unknown profile: $profile (use list-profiles to see options)"
659
+ exit 1
660
+ ;;
661
+ esac
662
+ fi
663
+
664
+ log_info "Dispatching to install.sh ($profile profile, $profile_flag)${target:+ → $target}"
665
+
666
+ # Phase 6: doctor preflight. Quick (--quiet) sanity check before
667
+ # invoking the heavy installer. Skipped on --skip-doctor or when the
668
+ # module is unavailable. FAILs are surfaced but never block install
669
+ # (user can still proceed; doctor is advisory at this stage).
670
+ if [[ "$skip_doctor" != "1" ]] && [[ "$_HAS_DOCTOR_LIB" = "1" ]]; then
671
+ local doctor_target="${target:-$PWD}"
672
+ log_info "Running doctor preflight (skip with --skip-doctor)..."
673
+ if ! doctor_run "$doctor_target" "$REPO_ROOT" --quiet; then
674
+ log_warning "Doctor reported FAILs — proceeding anyway. Re-run 'install doctor' for details."
675
+ fi
676
+ echo
677
+ fi
678
+
679
+ if [[ -n "$target" ]]; then
680
+ exec "$LEGACY_INSTALL" "$profile_flag" ${extra_args[@]+"${extra_args[@]}"} "$target"
681
+ else
682
+ exec "$LEGACY_INSTALL" "$profile_flag" ${extra_args[@]+"${extra_args[@]}"}
683
+ fi
684
+ }
685
+
686
+ # ---------- Dispatcher ------------------------------------------------------
687
+ main() {
688
+ if [[ $# -eq 0 ]]; then
689
+ show_usage
690
+ exit 0
691
+ fi
692
+
693
+ local subcmd="$1"
694
+ shift || true
695
+
696
+ case "$subcmd" in
697
+ init) run_init "$@" ;;
698
+ wizard) run_wizard "$@" ;;
699
+ deploy) run_deploy "$@" ;;
700
+ diagnose) run_diagnose "$@" ;;
701
+ doctor) run_doctor "$@" ;;
702
+ upgrade) run_upgrade "$@" ;;
703
+ list-profiles) list_profiles ;;
704
+ list-targets) list_targets ;;
705
+ agents) run_agents "$@" ;;
706
+ version|--version|-V) print_version ;;
707
+ help|--help|-h) show_usage ;;
708
+ *)
709
+ log_error "Unknown subcommand: $subcmd"
710
+ echo
711
+ show_usage
712
+ exit 1
713
+ ;;
714
+ esac
715
+ }
716
+
717
+ main "$@"