@darkrei08/setup-ai 3.0.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.
package/setup-ai.sh ADDED
@@ -0,0 +1,1091 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # ==============================================================================
4
+ # AI Dev Suite — Engineering Excellence Edition
5
+ # Version: 3.0.0
6
+ #
7
+ # Cross-platform (macOS + all major Linux distros) installer for an AI coding
8
+ # toolchain. Windows is handled by the sibling setup-ai.ps1; the Node launcher
9
+ # bin/setup-ai.mjs dispatches to the right script per OS and offers an
10
+ # interactive, gentle-ai-style module menu.
11
+ #
12
+ # Design:
13
+ # - modular: every tool is its own mod_* function, driven by an ordered
14
+ # registry; run a subset with --only, list them with --list.
15
+ # - official, non-deprecated install method per OS for every tool.
16
+ # - deterministic logging (human + JSONL), fail-fast with ERR diagnostics.
17
+ #
18
+ # Usage:
19
+ # ./setup-ai.sh # install the core module set
20
+ # ./setup-ai.sh --all # every module (incl. optional GUI apps)
21
+ # ./setup-ai.sh --only pi,codex,opencode
22
+ # ./setup-ai.sh --list # print modules and exit
23
+ # ./setup-ai.sh --help
24
+ # ==============================================================================
25
+
26
+ set -Eeuo pipefail
27
+ IFS=$'\n\t'
28
+
29
+ SCRIPT_VERSION="3.0.0"
30
+
31
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
32
+ LOG_DIR="${SCRIPT_DIR}/logs"
33
+ mkdir -p "${LOG_DIR}"
34
+
35
+ RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
36
+
37
+ HUMAN_LOG="${LOG_DIR}/setup_${RUN_ID}.log"
38
+ JSONL_LOG="${LOG_DIR}/setup_${RUN_ID}.jsonl"
39
+ REPORT_FILE="${LOG_DIR}/engineering-report_${RUN_ID}.md"
40
+
41
+ TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ai-dev-suite.XXXXXXXX")"
42
+
43
+ export RUN_ID
44
+
45
+ DEBUG="${DEBUG:-0}"
46
+ PI_WORKFLOW_VERSION="${PI_WORKFLOW_VERSION:-}"
47
+
48
+ DOTENV_REPO="${DOTENV_REPO:-https://github.com/vekexasia/dotenv.git}"
49
+
50
+ ENGINEERING_EXCELLENCE_SLUG="${ENGINEERING_EXCELLENCE_SLUG:-micio86dev/Engineering-Excellence}"
51
+ ENGINEERING_EXCELLENCE_SKILL="engineering-excellence"
52
+
53
+ PI_AGENT_DIR="${HOME}/.pi/agent"
54
+ PI_EXTENSIONS_DIR="${PI_AGENT_DIR}/extensions"
55
+ PI_NPM_DIR="${PI_AGENT_DIR}/npm"
56
+ ENGINEERING_EXCELLENCE_DIR="${PI_AGENT_DIR}/skills/${ENGINEERING_EXCELLENCE_SKILL}"
57
+
58
+ DOTENV_DIR="${HOME}/git/personale/dotenv"
59
+ DOTENV_EXT_DIR="${DOTENV_DIR}/pi/agent/extensions/pi-ext-workflows"
60
+
61
+ COCKPIT_REPO="jlcodes99/cockpit-tools"
62
+ GENTLE_AI_INSTALL="https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh"
63
+
64
+ # ------------------------------------------------------------------------------
65
+ # Cleanup
66
+ # ------------------------------------------------------------------------------
67
+
68
+ cleanup() {
69
+ rm -rf -- "${TMP_DIR}"
70
+ }
71
+ trap cleanup EXIT
72
+
73
+ # ------------------------------------------------------------------------------
74
+ # JSON escaping without Python (works before Python is installed)
75
+ # ------------------------------------------------------------------------------
76
+
77
+ json_escape() {
78
+ local value="${1-}"
79
+ value="${value//\\/\\\\}"
80
+ value="${value//\"/\\\"}"
81
+ value="${value//$'\n'/\\n}"
82
+ value="${value//$'\r'/\\r}"
83
+ value="${value//$'\t'/\\t}"
84
+ printf '%s' "${value}"
85
+ }
86
+
87
+ json_log() {
88
+ local timestamp="$1" level="$2" phase="$3" event="$4" message="$5"
89
+ local return_code="${6:-0}" meta="${7:-}"
90
+
91
+ local j_ts j_level j_phase j_event j_message j_meta
92
+ j_ts="$(json_escape "${timestamp}")"
93
+ j_level="$(json_escape "${level}")"
94
+ j_phase="$(json_escape "${phase}")"
95
+ j_event="$(json_escape "${event}")"
96
+ j_message="$(json_escape "${message}")"
97
+ j_meta="$(json_escape "${meta}")"
98
+
99
+ {
100
+ printf '{"timestamp":"%s","level":"%s","phase":"%s","event":"%s","message":"%s","return_code":%s,"run_id":"%s","pid":%s' \
101
+ "${j_ts}" "${j_level}" "${j_phase}" "${j_event}" "${j_message}" \
102
+ "${return_code}" "${RUN_ID}" "$$"
103
+ if [[ -n "${meta}" ]]; then
104
+ printf ',"meta":"%s"' "${j_meta}"
105
+ fi
106
+ printf '}\n'
107
+ } >> "${JSONL_LOG}"
108
+ }
109
+
110
+ log_event() {
111
+ local level="$1" phase="$2" event="$3" message="$4"
112
+ local return_code="${5:-0}" meta="${6:-}"
113
+
114
+ local timestamp
115
+ timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
116
+
117
+ json_log "${timestamp}" "${level}" "${phase}" "${event}" "${message}" "${return_code}" "${meta}"
118
+
119
+ local line="${timestamp} [${level}] ${phase} ${event}: ${message}"
120
+ printf '%s\n' "${line}" >> "${HUMAN_LOG}"
121
+
122
+ case "${level}" in
123
+ INFO) printf '\033[1;34m%s\033[0m\n' "${line}" ;;
124
+ WARN) printf '\033[1;33m%s\033[0m\n' "${line}" ;;
125
+ ERROR) printf '\033[1;31m%s\033[0m\n' "${line}" ;;
126
+ DEBUG) (( DEBUG == 1 )) && printf '\033[0;90m%s\033[0m\n' "${line}" ;;
127
+ *) printf '%s\n' "${line}" ;;
128
+ esac
129
+ }
130
+
131
+ # ------------------------------------------------------------------------------
132
+ # Failure diagnostics
133
+ # ------------------------------------------------------------------------------
134
+
135
+ on_error() {
136
+ local rc=$?
137
+ local line="${BASH_LINENO[0]:-unknown}"
138
+ local source_file="${BASH_SOURCE[1]:-${BASH_SOURCE[0]:-unknown}}"
139
+ local function_name="${FUNCNAME[1]:-main}"
140
+ local command="${BASH_COMMAND:-unknown}"
141
+
142
+ log_event "ERROR" "bootstrap" "script_failed" "Setup failed" "${rc}" \
143
+ "line=${line};file=${source_file};function=${function_name};command=${command}"
144
+
145
+ write_report "FAILED" "${rc}" "${line}" "${source_file}" "${function_name}" "${command}"
146
+
147
+ printf '\n\033[1;31mSETUP FALLITO\033[0m\n'
148
+ printf 'Return code : %s\n' "${rc}"
149
+ printf 'File : %s\n' "${source_file}"
150
+ printf 'Function : %s\n' "${function_name}"
151
+ printf 'Line : %s\n' "${line}"
152
+ printf 'Command : %s\n' "${command}"
153
+ printf 'Human log : %s\n' "${HUMAN_LOG}"
154
+ printf 'JSONL log : %s\n' "${JSONL_LOG}"
155
+ printf 'Report : %s\n\n' "${REPORT_FILE}"
156
+
157
+ exit "${rc}"
158
+ }
159
+ trap 'on_error' ERR
160
+
161
+ # ------------------------------------------------------------------------------
162
+ # Command helpers
163
+ # ------------------------------------------------------------------------------
164
+
165
+ section() { log_event "INFO" "section" "start" "$1"; }
166
+
167
+ require_command() {
168
+ local command="$1"
169
+ if ! command -v "${command}" >/dev/null 2>&1; then
170
+ log_event "ERROR" "preflight" "missing_command" "Required command not found: ${command}" 127
171
+ return 127
172
+ fi
173
+ }
174
+
175
+ run_cmd() {
176
+ local phase="$1"; shift
177
+ local display; printf -v display '%q ' "$@"
178
+ log_event "INFO" "${phase}" "command_start" "Executing command" 0 "${display}"
179
+
180
+ local output="${TMP_DIR}/command_${RANDOM}.log" rc=0
181
+ "$@" >"${output}" 2>&1 || rc=$?
182
+ cat "${output}" | tee -a "${HUMAN_LOG}"
183
+
184
+ if (( rc == 0 )); then
185
+ log_event "INFO" "${phase}" "command_success" "Command completed" 0 "${display}"
186
+ return 0
187
+ fi
188
+ log_event "ERROR" "${phase}" "command_failed" "Command returned non-zero status" "${rc}" "${display}"
189
+ return "${rc}"
190
+ }
191
+
192
+ run_optional() {
193
+ local phase="$1"; shift
194
+ local display; printf -v display '%q ' "$@"
195
+ log_event "INFO" "${phase}" "optional_command_start" "Executing optional command" 0 "${display}"
196
+
197
+ local output="${TMP_DIR}/optional_${RANDOM}.log" rc=0
198
+ "$@" >"${output}" 2>&1 || rc=$?
199
+ cat "${output}" | tee -a "${HUMAN_LOG}"
200
+
201
+ if (( rc == 0 )); then
202
+ log_event "INFO" "${phase}" "optional_command_success" "Optional command completed" 0 "${display}"
203
+ else
204
+ log_event "WARN" "${phase}" "optional_command_failed" "Optional command failed; continuing" "${rc}" "${display}"
205
+ fi
206
+ return 0
207
+ }
208
+
209
+ capture_cmd() {
210
+ local output_var="$1" phase="$2"; shift 2
211
+ local display; printf -v display '%q ' "$@"
212
+ local output="${TMP_DIR}/capture_${RANDOM}.log" rc=0
213
+ log_event "INFO" "${phase}" "capture_start" "Collecting command output" 0 "${display}"
214
+ "$@" >"${output}" 2>&1 || rc=$?
215
+ cat "${output}" | tee -a "${HUMAN_LOG}"
216
+ if (( rc != 0 )); then
217
+ log_event "ERROR" "${phase}" "capture_failed" "Command failed while collecting output" "${rc}" "${display}"
218
+ return "${rc}"
219
+ fi
220
+ printf -v "${output_var}" '%s' "$(cat "${output}")"
221
+ log_event "INFO" "${phase}" "capture_success" "Output captured" 0 "${display}"
222
+ }
223
+
224
+ write_report() {
225
+ local status="$1" rc="$2" line="${3:-n/a}" file="${4:-n/a}"
226
+ local function_name="${5:-n/a}" command="${6:-n/a}"
227
+
228
+ cat > "${REPORT_FILE}" <<EOF
229
+ # AI Dev Suite — Engineering Report
230
+
231
+ **Status:** ${status}
232
+ **Script version:** ${SCRIPT_VERSION}
233
+ **Run ID:** ${RUN_ID}
234
+ **Exit code:** ${rc}
235
+ **OS family:** ${OS_FAMILY:-unknown}
236
+ **Selected modules:** ${SELECTED_DISPLAY:-<default>}
237
+
238
+ ## Failure diagnostics
239
+
240
+ - File: \`${file}\`
241
+ - Function: \`${function_name}\`
242
+ - Line: \`${line}\`
243
+ - Command: \`${command}\`
244
+
245
+ ## Artifacts
246
+
247
+ - Human log: \`${HUMAN_LOG}\`
248
+ - JSONL log: \`${JSONL_LOG}\`
249
+ - Report: \`${REPORT_FILE}\`
250
+ EOF
251
+ }
252
+
253
+ # ------------------------------------------------------------------------------
254
+ # resolve_workflow_version — find the package.json that really owns
255
+ # pi-extensible-workflows by walking up from where Node resolved its entry.
256
+ # ------------------------------------------------------------------------------
257
+
258
+ resolve_workflow_version() {
259
+ local search_root="$1"
260
+ node -e '
261
+ const path = require("path");
262
+ const fs = require("fs");
263
+ const searchRoot = process.argv[1];
264
+ let entry;
265
+ try {
266
+ entry = require.resolve("pi-extensible-workflows", { paths: [searchRoot] });
267
+ } catch (err) {
268
+ console.error(`pi-extensible-workflows is not resolvable from ${searchRoot}: ${err.message}`);
269
+ process.exit(1);
270
+ }
271
+ let dir = path.dirname(entry);
272
+ for (;;) {
273
+ const candidate = path.join(dir, "package.json");
274
+ if (fs.existsSync(candidate)) {
275
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
276
+ if (pkg.name === "pi-extensible-workflows") {
277
+ process.stderr.write(`resolved_package_json=${candidate}\n`);
278
+ console.log(pkg.version);
279
+ process.exit(0);
280
+ }
281
+ }
282
+ const parent = path.dirname(dir);
283
+ if (parent === dir) break;
284
+ dir = parent;
285
+ }
286
+ console.error(`Could not find pi-extensible-workflows package.json by walking up from ${entry}`);
287
+ process.exit(1);
288
+ ' "${search_root}"
289
+ }
290
+
291
+ # ==============================================================================
292
+ # Module registry
293
+ #
294
+ # ORDER matters (dependencies first). Each name maps to a mod_<name> function
295
+ # and a human description. DEFAULT_MODULES is the "core" set used when no
296
+ # --only/--all is given; OPTIONAL_MODULES (GUI apps etc.) are only installed
297
+ # via --all or an explicit --only.
298
+ # ==============================================================================
299
+
300
+ MODULE_ORDER=(base node bun pi go dotenv ee pi-workflows herdr gentle-ai engram codex antigravity opencode cockpit)
301
+
302
+ declare -A MODULE_DESC=(
303
+ [base]="System packages (build tools, git, gh, python, neovim, jq, imagemagick)"
304
+ [node]="Node.js via nvm (v22) + npm@latest + sudo-visible symlinks"
305
+ [bun]="Bun runtime"
306
+ [pi]="pi.dev coding agent CLI"
307
+ [go]="Go toolchain"
308
+ [dotenv]="vekexasia/dotenv dotfiles (Linux only: clones + runs setup_env.sh)"
309
+ [ee]="Engineering Excellence skill (npx skills add, all detected agents)"
310
+ [pi-workflows]="pi-extensible-workflows (fix module resolution for pi extensions)"
311
+ [herdr]="herdr terminal multiplexer"
312
+ [gentle-ai]="gentle-ai / gga (Gentleman's spec-driven agent runner) + gentle-pi package"
313
+ [engram]="Engram persistent memory for pi (gentle-engram: /remember /recall /memory /forget)"
314
+ [codex]="OpenAI Codex CLI"
315
+ [antigravity]="Google Antigravity CLI (agy)"
316
+ [opencode]="opencode agent CLI (opencode-ai)"
317
+ [cockpit]="cockpit-tools desktop GUI app (optional, CC BY-NC-SA)"
318
+ )
319
+
320
+ # Optional modules: excluded from the default/core run.
321
+ declare -A MODULE_OPTIONAL=(
322
+ [cockpit]=1
323
+ )
324
+
325
+ # ------------------------------------------------------------------------------
326
+ # OS / package-manager detection
327
+ # ------------------------------------------------------------------------------
328
+
329
+ OS_FAMILY=""
330
+ PM=""
331
+ DISTRO_ID=""
332
+ DISTRO_LIKE=""
333
+
334
+ detect_os() {
335
+ local uname_s
336
+ uname_s="$(uname -s)"
337
+
338
+ case "${uname_s}" in
339
+ Darwin)
340
+ OS_FAMILY="macos"
341
+ if ! command -v brew >/dev/null 2>&1; then
342
+ log_event "ERROR" "preflight" "brew_missing" \
343
+ "Homebrew is required on macOS. Install from https://brew.sh then re-run." 1
344
+ exit 1
345
+ fi
346
+ PM="brew"
347
+ ;;
348
+ Linux)
349
+ OS_FAMILY="linux"
350
+ if [[ -f /etc/os-release ]]; then
351
+ # shellcheck disable=SC1091
352
+ source /etc/os-release
353
+ DISTRO_ID="${ID:-unknown}"
354
+ DISTRO_LIKE="${ID_LIKE:-}"
355
+ else
356
+ log_event "ERROR" "preflight" "os_detection_failed" "/etc/os-release not found" 1
357
+ exit 1
358
+ fi
359
+ case "${DISTRO_ID}" in
360
+ debian|ubuntu|linuxmint|pop) PM="apt-get" ;;
361
+ fedora|rhel|centos|rocky|almalinux) PM="dnf" ;;
362
+ arch|manjaro|endeavouros|omarchy) PM="pacman" ;;
363
+ opensuse*|sles) PM="zypper" ;;
364
+ *)
365
+ if [[ "${DISTRO_LIKE}" == *debian* ]]; then PM="apt-get"
366
+ elif [[ "${DISTRO_LIKE}" == *fedora* || "${DISTRO_LIKE}" == *rhel* ]]; then PM="dnf"
367
+ elif [[ "${DISTRO_LIKE}" == *arch* ]]; then PM="pacman"
368
+ elif [[ "${DISTRO_LIKE}" == *suse* ]]; then PM="zypper"
369
+ else
370
+ log_event "ERROR" "preflight" "unsupported_distribution" \
371
+ "Unsupported Linux distribution: ${DISTRO_ID}" 1
372
+ exit 1
373
+ fi
374
+ ;;
375
+ esac
376
+ ;;
377
+ *)
378
+ log_event "ERROR" "preflight" "unsupported_os" \
379
+ "Unsupported OS: ${uname_s}. On Windows use setup-ai.ps1." 1
380
+ exit 1
381
+ ;;
382
+ esac
383
+
384
+ log_event "INFO" "preflight" "os_detected" "OS detected" 0 \
385
+ "family=${OS_FAMILY};pm=${PM};distro=${DISTRO_ID};like=${DISTRO_LIKE}"
386
+ }
387
+
388
+ # ==============================================================================
389
+ # Modules
390
+ # ==============================================================================
391
+
392
+ # --- base -------------------------------------------------------------------
393
+ mod_base() {
394
+ section "Base system dependencies"
395
+
396
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
397
+ run_cmd "base" brew install \
398
+ git curl wget jq unzip gnupg python neovim gh go imagemagick
399
+ return
400
+ fi
401
+
402
+ case "${PM}" in
403
+ apt-get)
404
+ local pkgs=(build-essential curl wget git unzip tar ca-certificates gnupg jq
405
+ python3 python3-venv python3-pip neovim gh golang-go imagemagick)
406
+ run_cmd "base" sudo apt-get update
407
+ run_cmd "base" sudo apt-get install -y "${pkgs[@]}"
408
+ ;;
409
+ dnf)
410
+ local pkgs=(gcc gcc-c++ make curl wget git unzip tar ca-certificates gnupg2 jq
411
+ python3 python3-pip neovim gh golang ImageMagick)
412
+ run_cmd "base" sudo dnf install -y "${pkgs[@]}"
413
+ ;;
414
+ pacman)
415
+ local pkgs=(base-devel curl wget git unzip tar ca-certificates gnupg jq
416
+ python python-pip neovim github-cli go imagemagick)
417
+ run_cmd "base" sudo pacman -Sy --needed --noconfirm "${pkgs[@]}"
418
+ ;;
419
+ zypper)
420
+ local pkgs=(gcc gcc-c++ make curl wget git unzip tar ca-certificates gpg2 jq
421
+ python3 python3-pip neovim gh go ImageMagick)
422
+ run_cmd "base" sudo zypper --non-interactive refresh
423
+ run_cmd "base" sudo zypper --non-interactive install --no-recommends "${pkgs[@]}"
424
+ ;;
425
+ esac
426
+
427
+ require_command python3
428
+ }
429
+
430
+ # --- node -------------------------------------------------------------------
431
+ mod_node() {
432
+ section "Node.js"
433
+
434
+ local NVM_VERSION="${NVM_VERSION:-v0.40.3}"
435
+ export NVM_DIR="${HOME}/.nvm"
436
+
437
+ if [[ ! -s "${NVM_DIR}/nvm.sh" ]]; then
438
+ local installer="${TMP_DIR}/install-nvm.sh"
439
+ run_cmd "node" curl -fsSL \
440
+ "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "${installer}"
441
+ run_cmd "node" bash "${installer}"
442
+ fi
443
+
444
+ # shellcheck disable=SC1090
445
+ source "${NVM_DIR}/nvm.sh"
446
+
447
+ run_cmd "node" nvm install 22
448
+ run_cmd "node" nvm alias default 22
449
+ run_cmd "node" nvm use 22
450
+
451
+ node - <<'NODE'
452
+ const [major, minor] = process.versions.node.split('.').map(Number);
453
+ if (major < 22 || (major === 22 && minor < 19)) {
454
+ console.error(`Node.js ${process.versions.node} is too old; pi-extensible-workflows needs >= 22.19.`);
455
+ process.exit(1);
456
+ }
457
+ NODE
458
+
459
+ # npm@latest (NOT npm@12 — that version does not exist).
460
+ run_cmd "node" npm install -g npm@latest
461
+ run_optional "node" npm cache verify
462
+
463
+ # Make node/npm reachable under sudo (nvm's dir is not on sudo's secure_path),
464
+ # so downstream "sudo npm ..." calls resolve. Optional: needs write to /usr/local/bin.
465
+ local node_bin npm_bin
466
+ node_bin="$(command -v node)"
467
+ npm_bin="$(command -v npm)"
468
+ run_optional "node" sudo ln -sf "${node_bin}" /usr/local/bin/node
469
+ run_optional "node" sudo ln -sf "${npm_bin}" /usr/local/bin/npm
470
+ }
471
+
472
+ # --- bun --------------------------------------------------------------------
473
+ mod_bun() {
474
+ section "Bun"
475
+ export BUN_INSTALL="${HOME}/.bun"
476
+ if [[ ! -x "${BUN_INSTALL}/bin/bun" ]]; then
477
+ local installer="${TMP_DIR}/install-bun.sh"
478
+ run_cmd "bun" curl -fsSL https://bun.sh/install -o "${installer}"
479
+ run_cmd "bun" bash "${installer}"
480
+ fi
481
+ export PATH="${BUN_INSTALL}/bin:${PATH}"
482
+ require_command bun
483
+ log_event "INFO" "bun" "runtime_ready" "Bun runtime validated" 0 "version=$(bun --version)"
484
+ }
485
+
486
+ # --- pi ---------------------------------------------------------------------
487
+ mod_pi() {
488
+ section "Pi"
489
+ export PATH="${HOME}/.pi/bin:${HOME}/.local/bin:${PATH}"
490
+ if ! command -v pi >/dev/null 2>&1; then
491
+ local installer="${TMP_DIR}/install-pi.sh"
492
+ run_cmd "pi" curl -fsSL https://pi.dev/install.sh -o "${installer}"
493
+ run_cmd "pi" sh "${installer}"
494
+ fi
495
+ export PATH="${HOME}/.pi/bin:${HOME}/.local/bin:${PATH}"
496
+ require_command pi
497
+ PI_VERSION="$(pi --version 2>/dev/null || true)"
498
+ log_event "INFO" "pi" "cli_ready" "Pi CLI detected" 0 "version=${PI_VERSION}"
499
+ mkdir -p "${PI_AGENT_DIR}" "${PI_EXTENSIONS_DIR}" "${PI_NPM_DIR}" "${PI_AGENT_DIR}/skills"
500
+ }
501
+
502
+ # --- go ---------------------------------------------------------------------
503
+ mod_go() {
504
+ section "Go toolchain"
505
+ if command -v go >/dev/null 2>&1; then
506
+ log_event "INFO" "go" "already_present" "Go already installed" 0 "version=$(go version)"
507
+ return
508
+ fi
509
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
510
+ run_cmd "go" brew install go
511
+ else
512
+ # Installed by mod_base on Linux; if we got here it was skipped.
513
+ case "${PM}" in
514
+ apt-get) run_cmd "go" sudo apt-get install -y golang-go ;;
515
+ dnf) run_cmd "go" sudo dnf install -y golang ;;
516
+ pacman) run_cmd "go" sudo pacman -Sy --needed --noconfirm go ;;
517
+ zypper) run_cmd "go" sudo zypper --non-interactive install go ;;
518
+ esac
519
+ fi
520
+ require_command go
521
+ }
522
+
523
+ # --- dotenv (Linux only) ----------------------------------------------------
524
+ mod_dotenv() {
525
+ section "dotenv"
526
+ if [[ "${OS_FAMILY}" != "linux" ]]; then
527
+ log_event "WARN" "dotenv" "skipped_non_linux" \
528
+ "dotenv/setup_env.sh targets Linux package managers; skipped on ${OS_FAMILY}"
529
+ return
530
+ fi
531
+
532
+ mkdir -p "${HOME}/git/personale"
533
+ if [[ ! -d "${DOTENV_DIR}/.git" ]]; then
534
+ run_cmd "dotenv" git clone -- "${DOTENV_REPO}" "${DOTENV_DIR}"
535
+ else
536
+ log_event "INFO" "dotenv" "repository_exists" "Existing dotenv repository preserved"
537
+ fi
538
+
539
+ # Idempotent upstream-quirk patches (no-ops once upstream merges the fixes).
540
+ if grep -RIl --exclude-dir=.git "gthelding/monokai-pro.nvim" "${DOTENV_DIR}" \
541
+ >"${TMP_DIR}/monokai_hits" 2>/dev/null; then
542
+ while IFS= read -r file; do
543
+ sed -i 's|gthelding/monokai-pro.nvim|loctvl842/monokai-pro.nvim|g' "${file}"
544
+ log_event "INFO" "dotenv" "reference_patched" "Updated stale monokai-pro reference" 0 "file=${file}"
545
+ done < "${TMP_DIR}/monokai_hits"
546
+ fi
547
+
548
+ if grep -RIl --exclude-dir=.git 'sudo npm install -g --prefix /usr/local bun' "${DOTENV_DIR}" \
549
+ >"${TMP_DIR}/sudo_npm_hits" 2>/dev/null; then
550
+ while IFS= read -r file; do
551
+ sed -i 's|sudo npm install -g --prefix /usr/local bun|sudo "$(command -v npm)" install -g --prefix /usr/local bun|g' "${file}"
552
+ log_event "INFO" "dotenv" "reference_patched" "Patched sudo npm call to absolute path" 0 "file=${file}"
553
+ done < "${TMP_DIR}/sudo_npm_hits"
554
+ fi
555
+
556
+ if grep -RIl --exclude-dir=.git -e 'npm install -g --prefix "\$HOME/.local" tree-sitter-cli' "${DOTENV_DIR}" \
557
+ >"${TMP_DIR}/treesitter_hits" 2>/dev/null; then
558
+ while IFS= read -r file; do
559
+ grep -q 'AI_DEV_TS_CLI_PATCH' "${file}" && continue
560
+ python3 - "${file}" <<'PYPATCH'
561
+ import sys
562
+ path = sys.argv[1]
563
+ with open(path, "r", newline="") as fh:
564
+ text = fh.read()
565
+ old = 'command -v tree-sitter >/dev/null 2>&1 || npm install -g --prefix "$HOME/.local" tree-sitter-cli\n'
566
+ new = (
567
+ '# AI_DEV_TS_CLI_PATCH: force blocked postinstall and verify the real binary\n'
568
+ 'TS_CLI_BIN="$HOME/.local/lib/node_modules/tree-sitter-cli/tree-sitter"\n'
569
+ 'if [ ! -x "$TS_CLI_BIN" ]; then\n'
570
+ ' npm install -g --prefix "$HOME/.local" --foreground-scripts --include=optional tree-sitter-cli\n'
571
+ 'fi\n'
572
+ 'if [ ! -x "$TS_CLI_BIN" ] && [ -f "$HOME/.local/lib/node_modules/tree-sitter-cli/install.js" ]; then\n'
573
+ ' (cd "$HOME/.local/lib/node_modules/tree-sitter-cli" && node install.js)\n'
574
+ 'fi\n'
575
+ '[ -x "$TS_CLI_BIN" ] || { printf \'tree-sitter binary missing after install: %s\\n\' "$TS_CLI_BIN" >&2; exit 1; }\n'
576
+ )
577
+ if old in text:
578
+ text = text.replace(old, new)
579
+ with open(path, "w", newline="") as fh:
580
+ fh.write(text)
581
+ PYPATCH
582
+ log_event "INFO" "dotenv" "reference_patched" "Patched tree-sitter-cli install" 0 "file=${file}"
583
+ done < "${TMP_DIR}/treesitter_hits"
584
+ fi
585
+
586
+ if [[ -x "${DOTENV_DIR}/setup_env.sh" ]]; then
587
+ run_cmd "dotenv" bash "${DOTENV_DIR}/setup_env.sh"
588
+ else
589
+ log_event "WARN" "dotenv" "setup_script_missing" "dotenv/setup_env.sh missing or not executable"
590
+ fi
591
+ }
592
+
593
+ # --- engineering-excellence -------------------------------------------------
594
+ mod_ee() {
595
+ section "Engineering Excellence"
596
+ require_command npx
597
+
598
+ # Install the skill for every detected agent using the modern skills CLI
599
+ # (replaces the old git-clone-and-move). Agents are detected by their config dir.
600
+ local agent dir
601
+ local -A agent_dir=(
602
+ [pi]="${HOME}/.pi"
603
+ [claude]="${HOME}/.claude"
604
+ [gemini]="${HOME}/.gemini"
605
+ [cursor]="${HOME}/.cursor"
606
+ [antigravity]="${HOME}/.antigravity"
607
+ )
608
+ local installed_any=0
609
+ for agent in pi claude gemini cursor antigravity; do
610
+ dir="${agent_dir[$agent]}"
611
+ [[ -d "${dir}" ]] || continue
612
+ run_optional "engineering-excellence" \
613
+ npx --yes skills@latest add "${ENGINEERING_EXCELLENCE_SLUG}" \
614
+ --skill "${ENGINEERING_EXCELLENCE_SKILL}" --global --agent "${agent}" --copy --yes
615
+ installed_any=1
616
+ done
617
+
618
+ if (( installed_any == 0 )); then
619
+ # No agent detected yet — install at least for pi (created by mod_pi).
620
+ run_optional "engineering-excellence" \
621
+ npx --yes skills@latest add "${ENGINEERING_EXCELLENCE_SLUG}" \
622
+ --skill "${ENGINEERING_EXCELLENCE_SKILL}" --global --agent pi --copy --yes
623
+ fi
624
+
625
+ if [[ -f "${ENGINEERING_EXCELLENCE_DIR}/SKILL.md" ]]; then
626
+ log_event "INFO" "engineering-excellence" "skill_installed" "EE skill present for pi" 0 \
627
+ "path=${ENGINEERING_EXCELLENCE_DIR}"
628
+ else
629
+ log_event "WARN" "engineering-excellence" "skill_path_unverified" \
630
+ "EE SKILL.md not found at pi path; check other agents' skill dirs" 0 \
631
+ "expected=${ENGINEERING_EXCELLENCE_DIR}"
632
+ fi
633
+ }
634
+
635
+ # --- pi-extensible-workflows ------------------------------------------------
636
+ mod_pi_workflows() {
637
+ section "pi-extensible-workflows"
638
+ require_command pi
639
+ require_command node
640
+
641
+ if [[ -z "${PI_WORKFLOW_VERSION}" ]]; then
642
+ capture_cmd PI_WORKFLOW_VERSION "pi-workflows" npm view pi-extensible-workflows version
643
+ fi
644
+ [[ -n "${PI_WORKFLOW_VERSION}" ]] || {
645
+ log_event "ERROR" "pi-workflows" "version_unresolved" "Cannot determine published version" 1
646
+ exit 1
647
+ }
648
+ log_event "INFO" "pi-workflows" "version_selected" "Workflow version selected" 0 "version=${PI_WORKFLOW_VERSION}"
649
+
650
+ run_cmd "pi-workflows" pi install "npm:pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
651
+
652
+ mkdir -p "${PI_EXTENSIONS_DIR}"
653
+ pushd "${PI_EXTENSIONS_DIR}" >/dev/null
654
+ printf '%s\n' 'ignore-scripts=false' > .npmrc
655
+ run_cmd "pi-workflows-node" npm install --save-exact --no-audit --no-fund \
656
+ "pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
657
+ popd >/dev/null
658
+
659
+ local resolved
660
+ resolved="$(node -e 'console.log(require.resolve("pi-extensible-workflows",{paths:[process.argv[1]]}))' "${PI_EXTENSIONS_DIR}")"
661
+ log_event "INFO" "pi-workflows-node" "module_resolved" "Resolvable from pi extensions dir" 0 "resolved=${resolved}"
662
+
663
+ local installed
664
+ installed="$(resolve_workflow_version "${PI_EXTENSIONS_DIR}")"
665
+ if [[ "${installed}" != "${PI_WORKFLOW_VERSION}" ]]; then
666
+ log_event "ERROR" "pi-workflows-node" "version_mismatch" "Installed version mismatch" 1 \
667
+ "expected=${PI_WORKFLOW_VERSION};actual=${installed}"
668
+ exit 1
669
+ fi
670
+
671
+ if [[ -d "${DOTENV_EXT_DIR}" ]]; then
672
+ pushd "${DOTENV_EXT_DIR}" >/dev/null
673
+ printf '%s\n' 'ignore-scripts=false' > .npmrc
674
+ run_cmd "dotenv-workflows" npm install --save-exact --no-audit --no-fund \
675
+ "pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
676
+ popd >/dev/null
677
+ fi
678
+ }
679
+
680
+ # --- herdr ------------------------------------------------------------------
681
+ mod_herdr() {
682
+ section "herdr"
683
+ if command -v herdr >/dev/null 2>&1; then
684
+ log_event "INFO" "herdr" "already_present" "herdr already installed" 0 "version=$(herdr --version 2>/dev/null || true)"
685
+ return
686
+ fi
687
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
688
+ run_optional "herdr" brew install herdr
689
+ else
690
+ local installer="${TMP_DIR}/install-herdr.sh"
691
+ run_optional "herdr" curl -fsSL https://herdr.dev/install.sh -o "${installer}"
692
+ [[ -s "${installer}" ]] && run_optional "herdr" sh "${installer}"
693
+ fi
694
+ }
695
+
696
+ # --- gentle-ai --------------------------------------------------------------
697
+ mod_gentle_ai() {
698
+ section "gentle-ai"
699
+ if ! command -v gentle-ai >/dev/null 2>&1 && ! command -v gga >/dev/null 2>&1; then
700
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
701
+ run_optional "gentle-ai" brew tap gentleman-programming/tap
702
+ run_optional "gentle-ai" brew install gentle-ai
703
+ else
704
+ local installer="${TMP_DIR}/install-gentle-ai.sh"
705
+ run_optional "gentle-ai" curl -fsSL "${GENTLE_AI_INSTALL}" -o "${installer}"
706
+ [[ -s "${installer}" ]] && run_optional "gentle-ai" bash "${installer}"
707
+ fi
708
+ fi
709
+
710
+ # Enable gentle-ai INSIDE pi as packages (the standalone gga binary alone
711
+ # does not register anything in pi — this is why it wasn't visible there).
712
+ if command -v pi >/dev/null 2>&1; then
713
+ run_optional "gentle-ai" pi install npm:gentle-pi
714
+ run_optional "gentle-ai" pi install npm:pi-mcp-adapter
715
+ log_event "INFO" "gentle-ai" "pi_enabled" "gentle-pi registered in pi (verify: /gentle-ai:status)" 0
716
+ fi
717
+
718
+ # Surface gentle-ai's own next-steps (do NOT run — they are per-repo).
719
+ log_event "INFO" "gentle-ai" "next_steps" "gentle-ai post-install hints" 0
720
+ cat <<'HINT' | tee -a "${HUMAN_LOG}"
721
+ gentle-ai next steps (run yourself, per project):
722
+ 1) Set your API keys
723
+ 2) Run your selected agent
724
+ 3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models)
725
+ GGA (per project):
726
+ gga init # inside each repo
727
+ gga install
728
+ HINT
729
+ }
730
+
731
+ # --- engram (pi persistent memory) ------------------------------------------
732
+ # The gentle-engram pi extension auto-starts `engram serve`, so the Engram Go
733
+ # binary MUST be on PATH first — otherwise the extension loads but silently
734
+ # fails (the "engram doesn't work in pi" symptom). Install binary, then the pi
735
+ # packages, then init, then the user restarts pi.
736
+ mod_engram() {
737
+ section "Engram memory (pi)"
738
+
739
+ # 1. Engram binary (Go).
740
+ if ! command -v engram >/dev/null 2>&1; then
741
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
742
+ run_optional "engram" brew install gentleman-programming/tap/engram
743
+ fi
744
+ if ! command -v engram >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
745
+ run_optional "engram" go install github.com/Gentleman-Programming/engram/cmd/engram@latest
746
+ fi
747
+ fi
748
+ # go installs into $GOPATH/bin (default ~/go/bin), which may not be on PATH yet.
749
+ [[ -x "${HOME}/go/bin/engram" ]] && export PATH="${HOME}/go/bin:${PATH}"
750
+
751
+ if ! command -v engram >/dev/null 2>&1; then
752
+ log_event "WARN" "engram" "binary_missing" \
753
+ "engram binary not found after install; the pi extension needs it on PATH" 1
754
+ fi
755
+
756
+ # 2. pi integration (in-process extension is the primary path; MCP adapter optional).
757
+ if command -v pi >/dev/null 2>&1; then
758
+ run_optional "engram" pi install npm:gentle-engram
759
+ run_optional "engram" pi install npm:pi-mcp-adapter
760
+ run_optional "engram" npm exec --yes --package gentle-engram@latest -- pi-engram init
761
+ log_event "INFO" "engram" "enabled" \
762
+ "Engram enabled — RESTART pi, then verify with mem_current_project / mem_doctor / 'engram tui'" 0
763
+ else
764
+ log_event "WARN" "engram" "pi_missing" "pi not found; Engram pi integration skipped"
765
+ fi
766
+ }
767
+
768
+ # --- codex ------------------------------------------------------------------
769
+ mod_codex() {
770
+ section "Codex CLI"
771
+ if command -v codex >/dev/null 2>&1; then
772
+ log_event "INFO" "codex" "already_present" "codex already installed" 0
773
+ return
774
+ fi
775
+ if [[ "${OS_FAMILY}" == "macos" ]] && command -v brew >/dev/null 2>&1; then
776
+ run_optional "codex" brew install --cask codex
777
+ fi
778
+ if ! command -v codex >/dev/null 2>&1; then
779
+ local installer="${TMP_DIR}/install-codex.sh"
780
+ run_optional "codex" curl -fsSL https://chatgpt.com/codex/install.sh -o "${installer}"
781
+ [[ -s "${installer}" ]] && run_optional "codex" sh "${installer}"
782
+ fi
783
+ }
784
+
785
+ # --- antigravity ------------------------------------------------------------
786
+ mod_antigravity() {
787
+ section "Antigravity CLI"
788
+ if command -v agy >/dev/null 2>&1; then
789
+ log_event "INFO" "antigravity" "already_present" "agy already installed" 0
790
+ return
791
+ fi
792
+ local installer="${TMP_DIR}/install-antigravity.sh"
793
+ run_optional "antigravity" curl -fsSL https://antigravity.google/cli/install.sh -o "${installer}"
794
+ [[ -s "${installer}" ]] && run_optional "antigravity" bash "${installer}"
795
+ }
796
+
797
+ # --- opencode ---------------------------------------------------------------
798
+ mod_opencode() {
799
+ section "opencode"
800
+ if command -v opencode >/dev/null 2>&1; then
801
+ log_event "INFO" "opencode" "already_present" "opencode already installed" 0
802
+ return
803
+ fi
804
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
805
+ run_optional "opencode" brew install anomalyco/tap/opencode
806
+ else
807
+ local installer="${TMP_DIR}/install-opencode.sh"
808
+ run_optional "opencode" curl -fsSL https://opencode.ai/install -o "${installer}"
809
+ [[ -s "${installer}" ]] && run_optional "opencode" bash "${installer}"
810
+ fi
811
+
812
+ log_event "INFO" "opencode" "zen_hint" "OpenCode Go / Zen provider hint" 0
813
+ cat <<'HINT' | tee -a "${HUMAN_LOG}"
814
+ OpenCode Go (paid) is hosted-model access; after install run: opencode auth login
815
+ The SAME key works in pi.dev (no lock-in) — add a custom provider in pi:
816
+ pi.registerProvider("opencode-go", {
817
+ baseUrl: "https://opencode.ai/zen/v1",
818
+ apiKey: "$OPENCODE_API_KEY",
819
+ authHeader: true,
820
+ api: "openai-completions",
821
+ models: [ /* e.g. your Go plan model ids */ ]
822
+ });
823
+ or run /provider add inside pi. Docs: https://pi.dev/docs/latest/custom-provider
824
+ HINT
825
+ }
826
+
827
+ # --- cockpit-tools (GUI, optional) ------------------------------------------
828
+ mod_cockpit() {
829
+ section "cockpit-tools (GUI)"
830
+ log_event "INFO" "cockpit" "license_notice" \
831
+ "cockpit-tools is a desktop GUI app under CC BY-NC-SA 4.0 (non-commercial)" 0
832
+
833
+ if [[ "${OS_FAMILY}" == "macos" ]]; then
834
+ run_optional "cockpit" brew tap "${COCKPIT_REPO}" "https://github.com/${COCKPIT_REPO}"
835
+ run_optional "cockpit" brew install --cask cockpit-tools
836
+ return
837
+ fi
838
+
839
+ # Linux: fetch the latest .deb (apt/dpkg) or .rpm (dnf) from GitHub Releases.
840
+ local release_json="${TMP_DIR}/cockpit-release.json"
841
+ run_optional "cockpit" curl -fsSL \
842
+ "https://api.github.com/repos/${COCKPIT_REPO}/releases/latest" -o "${release_json}"
843
+ [[ -s "${release_json}" ]] || {
844
+ log_event "WARN" "cockpit" "release_unavailable" "Could not fetch cockpit-tools release metadata"
845
+ return
846
+ }
847
+
848
+ local asset_pat=""
849
+ case "${PM}" in
850
+ apt-get) asset_pat='amd64[^"]+\.deb|x86_64[^"]+\.deb|_amd64\.deb' ;;
851
+ dnf|zypper) asset_pat='x86_64[^"]+\.rpm|\.rpm' ;;
852
+ *) asset_pat='\.AppImage' ;;
853
+ esac
854
+
855
+ local url
856
+ url="$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "${release_json}" \
857
+ | cut -d '"' -f4 | grep -iE "${asset_pat}" | head -n1 || true)"
858
+
859
+ if [[ -z "${url}" ]]; then
860
+ log_event "WARN" "cockpit" "no_matching_asset" \
861
+ "No matching cockpit-tools asset for ${PM}; download manually from GitHub Releases"
862
+ return
863
+ fi
864
+
865
+ local installer="${TMP_DIR}/cockpit-asset"
866
+ run_optional "cockpit" curl -fsSL "${url}" -o "${installer}"
867
+ [[ -s "${installer}" ]] || return
868
+
869
+ case "${PM}" in
870
+ apt-get) run_optional "cockpit" sudo apt-get install -y "${installer}" ;;
871
+ dnf) run_optional "cockpit" sudo dnf install -y "${installer}" ;;
872
+ zypper) run_optional "cockpit" sudo zypper --non-interactive install "${installer}" ;;
873
+ *)
874
+ local dest="${HOME}/.local/bin/cockpit-tools.AppImage"
875
+ mkdir -p "${HOME}/.local/bin"
876
+ run_optional "cockpit" install -Dm755 "${installer}" "${dest}"
877
+ log_event "INFO" "cockpit" "appimage_installed" "AppImage placed" 0 "path=${dest}"
878
+ ;;
879
+ esac
880
+ }
881
+
882
+ # ==============================================================================
883
+ # Shell environment
884
+ # ==============================================================================
885
+
886
+ configure_shell_env() {
887
+ section "Shell environment"
888
+ local shell_config="${HOME}/.bashrc"
889
+ [[ -n "${ZSH_VERSION:-}" ]] && shell_config="${HOME}/.zshrc"
890
+ [[ "${OS_FAMILY}" == "macos" && -f "${HOME}/.zshrc" ]] && shell_config="${HOME}/.zshrc"
891
+
892
+ local marker="# AI Dev Toolsuite Environment"
893
+ if ! grep -Fq "${marker}" "${shell_config}" 2>/dev/null; then
894
+ cat >> "${shell_config}" <<'EOF'
895
+
896
+ # ==========================================
897
+ # AI Dev Toolsuite Environment
898
+ # ==========================================
899
+ export NVM_DIR="$HOME/.nvm"
900
+ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
901
+
902
+ export BUN_INSTALL="$HOME/.bun"
903
+ export GOPATH="$HOME/go"
904
+
905
+ export PATH="$BUN_INSTALL/bin:$HOME/.pi/bin:$HOME/.local/bin:$GOPATH/bin:$HOME/.cargo/bin:$PATH"
906
+ EOF
907
+ log_event "INFO" "shell" "environment_added" "Shell env added" 0 "file=${shell_config}"
908
+ else
909
+ log_event "INFO" "shell" "environment_exists" "Shell env already present" 0 "file=${shell_config}"
910
+ fi
911
+ }
912
+
913
+ # ==============================================================================
914
+ # Quality gates (run only for modules that were selected)
915
+ # ==============================================================================
916
+
917
+ quality_gates() {
918
+ section "Quality gates"
919
+ run_cmd "quality" bash -n "${BASH_SOURCE[0]}"
920
+
921
+ is_selected node && { run_cmd "quality" node --version; run_cmd "quality" npm --version; }
922
+ is_selected bun && run_optional "quality" bun --version
923
+ is_selected pi && run_optional "quality" pi --no-extensions --version
924
+
925
+ if is_selected pi-workflows; then
926
+ run_cmd "quality" node -e \
927
+ 'const root=process.argv[1]; console.log("RESOLVED="+require.resolve("pi-extensible-workflows",{paths:[root]}))' \
928
+ "${PI_EXTENSIONS_DIR}"
929
+ fi
930
+
931
+ if is_selected ee && [[ -f "${ENGINEERING_EXCELLENCE_DIR}/SKILL.md" ]]; then
932
+ log_event "INFO" "quality" "ee_gate_passed" "Engineering Excellence SKILL.md present"
933
+ fi
934
+
935
+ log_event "INFO" "quality" "gates_done" "Quality gates completed for selected modules"
936
+ }
937
+
938
+ # ==============================================================================
939
+ # CLI parsing / module selection
940
+ # ==============================================================================
941
+
942
+ SELECTED_MODULES=()
943
+ SELECTED_DISPLAY=""
944
+
945
+ is_selected() {
946
+ local needle="$1" m
947
+ for m in "${SELECTED_MODULES[@]}"; do
948
+ [[ "${m}" == "${needle}" ]] && return 0
949
+ done
950
+ return 1
951
+ }
952
+
953
+ print_list() {
954
+ printf 'AI Dev Suite %s — modules (core = installed by default):\n\n' "${SCRIPT_VERSION}"
955
+ local m tag
956
+ for m in "${MODULE_ORDER[@]}"; do
957
+ if [[ -n "${MODULE_OPTIONAL[$m]:-}" ]]; then tag="optional"; else tag="core "; fi
958
+ printf ' [%s] %-14s %s\n' "${tag}" "${m}" "${MODULE_DESC[$m]}"
959
+ done
960
+ printf '\nUse: --only <csv> | --all | (default = core)\n'
961
+ }
962
+
963
+ print_help() {
964
+ sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
965
+ printf '\n'
966
+ print_list
967
+ }
968
+
969
+ parse_args() {
970
+ local mode="default" only_csv=""
971
+ while [[ $# -gt 0 ]]; do
972
+ case "$1" in
973
+ --all) mode="all"; shift ;;
974
+ --only) mode="only"; only_csv="${2:-}"; shift 2 ;;
975
+ --only=*) mode="only"; only_csv="${1#*=}"; shift ;;
976
+ --yes|-y) shift ;; # accepted for launcher parity
977
+ --list) print_list; exit 0 ;;
978
+ -h|--help) print_help; exit 0 ;;
979
+ *) printf 'Unknown argument: %s\n' "$1" >&2; exit 2 ;;
980
+ esac
981
+ done
982
+
983
+ local requested=()
984
+ case "${mode}" in
985
+ all) requested=("${MODULE_ORDER[@]}") ;;
986
+ only)
987
+ local IFS_SAVE="${IFS}"; IFS=','
988
+ # shellcheck disable=SC2206
989
+ local raw=(${only_csv})
990
+ IFS="${IFS_SAVE}"
991
+ local r
992
+ for r in "${raw[@]}"; do
993
+ r="$(printf '%s' "${r}" | tr -d '[:space:]')"
994
+ [[ -z "${r}" ]] && continue
995
+ [[ -n "${MODULE_DESC[$r]:-}" ]] || { printf 'Unknown module: %s\n' "${r}" >&2; exit 2; }
996
+ requested+=("${r}")
997
+ done
998
+ ;;
999
+ default)
1000
+ local m
1001
+ for m in "${MODULE_ORDER[@]}"; do
1002
+ [[ -n "${MODULE_OPTIONAL[$m]:-}" ]] && continue
1003
+ requested+=("${m}")
1004
+ done
1005
+ ;;
1006
+ esac
1007
+
1008
+ # Order the selection by MODULE_ORDER so dependencies run first.
1009
+ local m r
1010
+ for m in "${MODULE_ORDER[@]}"; do
1011
+ for r in "${requested[@]}"; do
1012
+ if [[ "${m}" == "${r}" ]]; then
1013
+ SELECTED_MODULES+=("${m}")
1014
+ break
1015
+ fi
1016
+ done
1017
+ done
1018
+
1019
+ SELECTED_DISPLAY="$(printf '%s ' "${SELECTED_MODULES[@]}")"
1020
+ }
1021
+
1022
+ run_module() {
1023
+ local name="$1"
1024
+ local fn="mod_${name//-/_}"
1025
+ if ! declare -F "${fn}" >/dev/null; then
1026
+ log_event "WARN" "modules" "unknown_module" "No function for module ${name}"
1027
+ return 0
1028
+ fi
1029
+ "${fn}"
1030
+ }
1031
+
1032
+ # ==============================================================================
1033
+ # Main
1034
+ # ==============================================================================
1035
+
1036
+ : > "${HUMAN_LOG}"
1037
+ : > "${JSONL_LOG}"
1038
+
1039
+ log_event "INFO" "bootstrap" "start" "AI Dev Suite setup started" 0 "script_version=${SCRIPT_VERSION}"
1040
+
1041
+ parse_args "$@"
1042
+
1043
+ section "Preflight"
1044
+ require_command bash
1045
+ require_command curl
1046
+ require_command git
1047
+ detect_os
1048
+
1049
+ if [[ "${OS_FAMILY}" == "linux" ]]; then
1050
+ require_command sudo
1051
+ fi
1052
+
1053
+ log_event "INFO" "bootstrap" "modules_selected" "Modules queued" 0 "modules=${SELECTED_DISPLAY}"
1054
+
1055
+ PI_VERSION=""
1056
+ for _mod in "${SELECTED_MODULES[@]}"; do
1057
+ run_module "${_mod}"
1058
+ done
1059
+
1060
+ configure_shell_env
1061
+ quality_gates
1062
+
1063
+ write_report "SUCCESS" 0 "n/a" "n/a" "n/a" "n/a"
1064
+
1065
+ cat >> "${REPORT_FILE}" <<EOF
1066
+
1067
+ ## Installed (selected modules)
1068
+
1069
+ ${SELECTED_DISPLAY}
1070
+
1071
+ ## Versions
1072
+
1073
+ - Node.js: \`$(command -v node >/dev/null 2>&1 && node --version || echo n/a)\`
1074
+ - npm: \`$(command -v npm >/dev/null 2>&1 && npm --version || echo n/a)\`
1075
+ - Bun: \`$(command -v bun >/dev/null 2>&1 && bun --version || echo n/a)\`
1076
+ - Pi: \`${PI_VERSION:-n/a}\`
1077
+ - Go: \`$(command -v go >/dev/null 2>&1 && go version || echo n/a)\`
1078
+ EOF
1079
+
1080
+ log_event "INFO" "bootstrap" "completed" "AI Dev Suite setup completed successfully" 0 \
1081
+ "human_log=${HUMAN_LOG};jsonl_log=${JSONL_LOG};report=${REPORT_FILE}"
1082
+
1083
+ printf '\n\033[1;32m============================================================\033[0m\n'
1084
+ printf '\033[1;32m AI Dev Suite setup completed successfully\033[0m\n'
1085
+ printf '\033[1;32m============================================================\033[0m\n'
1086
+ printf 'OS family : %s\n' "${OS_FAMILY}"
1087
+ printf 'Modules : %s\n' "${SELECTED_DISPLAY}"
1088
+ printf 'Human log : %s\n' "${HUMAN_LOG}"
1089
+ printf 'JSONL log : %s\n' "${JSONL_LOG}"
1090
+ printf 'Report : %s\n' "${REPORT_FILE}"
1091
+ printf '\nNext: restart your shell (or source your rc file) so PATH updates apply.\n'