@darkrei08/setup-ai 3.0.5 → 3.3.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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  # ==============================================================================
4
4
  # AI Dev Suite — Engineering Excellence Edition
5
- # Version: 3.0.5
5
+ # Version: 3.3.0
6
6
  #
7
7
  # Cross-platform (macOS + all major Linux distros) installer for an AI coding
8
8
  # toolchain. Windows is handled by the sibling setup-ai.ps1; the Node launcher
9
9
  # bin/setup-ai.mjs dispatches to the right script per OS and offers an
10
- # interactive, gentle-ai-style module menu.
10
+ # interactive arrow-key module menu.
11
11
  #
12
12
  # Design:
13
13
  # - modular: every tool is its own mod_* function, driven by an ordered
@@ -26,7 +26,7 @@
26
26
  set -Eeuo pipefail
27
27
  IFS=$'\n\t'
28
28
 
29
- SCRIPT_VERSION="3.0.5"
29
+ SCRIPT_VERSION="3.3.0"
30
30
 
31
31
  SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
32
32
  LOG_DIR="${SCRIPT_DIR}/logs"
@@ -47,19 +47,33 @@ PI_WORKFLOW_VERSION="${PI_WORKFLOW_VERSION:-}"
47
47
 
48
48
  DOTENV_REPO="${DOTENV_REPO:-https://github.com/vekexasia/dotenv.git}"
49
49
 
50
- ENGINEERING_EXCELLENCE_SLUG="${ENGINEERING_EXCELLENCE_SLUG:-micio86dev/Engineering-Excellence}"
50
+ # Gentle AI ecosystem configurator installer (macOS/Linux). `gentle-ai install`
51
+ # is the interactive per-agent/per-IDE selector that also wires each agent's MCP
52
+ # servers, so the tools show up under /mcp.
53
+ GENTLE_AI_INSTALL="${GENTLE_AI_INSTALL:-https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh}"
54
+
55
+ ENGINEERING_EXCELLENCE_SLUG="${ENGINEERING_EXCELLENCE_SLUG:-darkrei08/Engineering-Excellence}"
51
56
  ENGINEERING_EXCELLENCE_SKILL="engineering-excellence"
52
57
 
58
+ # Upstream agent-skill stack mirrored from vekexasia/dotenv setup_env.sh so the
59
+ # same skills land on every OS (dotenv itself is Linux-only). Each entry is
60
+ # "<source> <skill> [<skill>...]" installed via `npx skills add`.
61
+ UPSTREAM_SKILL_SOURCES=(
62
+ "herdrdev/herdr herdr"
63
+ "mattpocock/skills triage grill-me grilling wayfinder domain-modeling prototype research"
64
+ "https://github.com/pedronauck/skills typescript-advanced"
65
+ "humanlayer/skills show-me"
66
+ )
67
+ UPSTREAM_SKILL_NAMES=(herdr triage grill-me grilling wayfinder domain-modeling prototype research typescript-advanced show-me)
68
+
53
69
  PI_AGENT_DIR="${HOME}/.pi/agent"
54
70
  PI_EXTENSIONS_DIR="${PI_AGENT_DIR}/extensions"
55
71
  PI_NPM_DIR="${PI_AGENT_DIR}/npm"
56
- ENGINEERING_EXCELLENCE_DIR="${PI_AGENT_DIR}/skills/${ENGINEERING_EXCELLENCE_SKILL}"
57
72
 
58
73
  DOTENV_DIR="${HOME}/git/personale/dotenv"
59
74
  DOTENV_EXT_DIR="${DOTENV_DIR}/pi/agent/extensions/pi-ext-workflows"
60
75
 
61
76
  COCKPIT_REPO="jlcodes99/cockpit-tools"
62
- GENTLE_AI_INSTALL="https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh"
63
77
 
64
78
  # ------------------------------------------------------------------------------
65
79
  # Cleanup
@@ -221,6 +235,38 @@ capture_cmd() {
221
235
  log_event "INFO" "${phase}" "capture_success" "Output captured" 0 "${display}"
222
236
  }
223
237
 
238
+ # Run a `grep` probe that distinguishes "no match" from a real failure.
239
+ # Writes matching lines to ${out_file}. Returns 0 when matches were found,
240
+ # 1 when there were none (grep exit 1), and hard-fails on any operational
241
+ # error (grep exit >1, e.g. an unreadable file) instead of hiding it.
242
+ grep_probe() {
243
+ local phase="$1" out_file="$2"; shift 2
244
+ local rc=0
245
+ grep "$@" >"${out_file}" 2>>"${HUMAN_LOG}" || rc=$?
246
+ case "${rc}" in
247
+ 0) return 0 ;;
248
+ 1) return 1 ;;
249
+ *)
250
+ log_event "ERROR" "${phase}" "grep_probe_failed" \
251
+ "grep probe failed with an operational error" "${rc}" "args=$*"
252
+ exit "${rc}"
253
+ ;;
254
+ esac
255
+ }
256
+
257
+ report_version() {
258
+ local output_var="$1" command_name="$2"; shift 2
259
+ if ! command -v "${command_name}" >/dev/null 2>&1; then
260
+ printf -v "${output_var}" '%s' "n/a"
261
+ return 0
262
+ fi
263
+ if ! capture_cmd "${output_var}" "report" "${command_name}" "$@"; then
264
+ printf -v "${output_var}" '%s' "n/a"
265
+ log_event "WARN" "report" "version_unavailable" \
266
+ "Could not read command version" 0 "command=${command_name}"
267
+ fi
268
+ }
269
+
224
270
  write_report() {
225
271
  local status="$1" rc="$2" line="${3:-n/a}" file="${4:-n/a}"
226
272
  local function_name="${5:-n/a}" command="${6:-n/a}"
@@ -250,44 +296,6 @@ write_report() {
250
296
  EOF
251
297
  }
252
298
 
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
299
  # ==============================================================================
292
300
  # Module registry
293
301
  #
@@ -297,30 +305,33 @@ resolve_workflow_version() {
297
305
  # via --all or an explicit --only.
298
306
  # ==============================================================================
299
307
 
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
- )
308
+ MODULE_ORDER=(base node bun pi go dotenv ee skills pi-workflows herdr gentle-ai codex antigravity opencode cockpit rotator)
309
+
310
+ module_desc() {
311
+ case "$1" in
312
+ base) printf '%s\n' "System packages (build tools, git, gh, python, neovim, jq, imagemagick, go)" ;;
313
+ node) printf '%s\n' "Node.js v22 + npm@latest (nvm on Unix, winget on Windows)" ;;
314
+ bun) printf '%s\n' "Bun runtime" ;;
315
+ pi) printf '%s\n' "pi.dev coding agent CLI" ;;
316
+ go) printf '%s\n' "Go toolchain" ;;
317
+ dotenv) printf '%s\n' "vekexasia/dotenv dotfiles (Linux only: clones + runs setup_env.sh)" ;;
318
+ ee) printf '%s\n' "Engineering Excellence skill (npx skills add, all detected agents)" ;;
319
+ skills) printf '%s\n' "Upstream agent skills (herdr, grilling, research, typescript-advanced, show-me, ...) via npx skills add" ;;
320
+ pi-workflows) printf '%s\n' "pi-extensible-workflows (fix module resolution for pi extensions)" ;;
321
+ herdr) printf '%s\n' "herdr terminal multiplexer" ;;
322
+ gentle-ai) printf '%s\n' "gentle-ai / gga ecosystem configurator (per-agent select + MCP) + gentle-pi" ;;
323
+ codex) printf '%s\n' "OpenAI Codex CLI" ;;
324
+ antigravity) printf '%s\n' "Google Antigravity CLI (agy)" ;;
325
+ opencode) printf '%s\n' "opencode agent CLI (opencode-ai)" ;;
326
+ cockpit) printf '%s\n' "cockpit-tools desktop GUI app (optional, CC BY-NC-SA)" ;;
327
+ rotator) printf '%s\n' "tuxevil-rotator multi-account Gemini/Antigravity gateway (optional, opt-in)" ;;
328
+ *) return 1 ;;
329
+ esac
330
+ }
319
331
 
320
- # Optional modules: excluded from the default/core run.
321
- declare -A MODULE_OPTIONAL=(
322
- [cockpit]=1
323
- )
332
+ module_is_optional() {
333
+ [[ "$1" == "cockpit" || "$1" == "rotator" ]]
334
+ }
324
335
 
325
336
  # ------------------------------------------------------------------------------
326
337
  # OS / package-manager detection
@@ -444,7 +455,7 @@ mod_node() {
444
455
  # nvm refuses to operate when npm_config_prefix is set (e.g. to /usr/local),
445
456
  # aborting with "nvm is not compatible with the npm_config_prefix ...".
446
457
  # It is only needed by nvm's own shims, so drop it for this process.
447
- unset npm_config_prefix NPM_CONFIG_PREFIX 2>/dev/null || true
458
+ unset npm_config_prefix NPM_CONFIG_PREFIX 2>/dev/null || log_event "WARN" "node" "unset_prefix_failed" "Could not unset npm prefix vars" 0
448
459
 
449
460
  # shellcheck disable=SC1090
450
461
  source "${NVM_DIR}/nvm.sh"
@@ -453,13 +464,7 @@ mod_node() {
453
464
  run_cmd "node" nvm alias default 22
454
465
  run_cmd "node" nvm use 22
455
466
 
456
- node - <<'NODE'
457
- const [major, minor] = process.versions.node.split('.').map(Number);
458
- if (major < 22 || (major === 22 && minor < 19)) {
459
- console.error(`Node.js ${process.versions.node} is too old; pi-extensible-workflows needs >= 22.19.`);
460
- process.exit(1);
461
- }
462
- NODE
467
+ assert_node_minimum
463
468
 
464
469
  # npm@latest (NOT npm@12 — that version does not exist).
465
470
  run_cmd "node" npm install -g npm@latest
@@ -474,6 +479,28 @@ NODE
474
479
  run_optional "node" sudo ln -sf "${npm_bin}" /usr/local/bin/npm
475
480
  }
476
481
 
482
+ assert_node_minimum() {
483
+ require_command node
484
+
485
+ local node_version major minor
486
+ capture_cmd node_version "node" node --version
487
+ if [[ "${node_version}" =~ ^v([0-9]+)\.([0-9]+) ]]; then
488
+ major="${BASH_REMATCH[1]}"
489
+ minor="${BASH_REMATCH[2]}"
490
+ else
491
+ log_event "ERROR" "node" "version_invalid" "Could not parse Node.js version" 1 "version=${node_version}"
492
+ return 1
493
+ fi
494
+
495
+ if (( major < 22 || (major == 22 && minor < 19) )); then
496
+ log_event "ERROR" "node" "version_unsupported" \
497
+ "Node.js ${node_version} is too old; pi-extensible-workflows needs >= 22.19" 1
498
+ return 1
499
+ fi
500
+ log_event "INFO" "node" "runtime_validated" "Node.js version satisfies workflow requirement" 0 \
501
+ "version=${node_version};minimum=22.19"
502
+ }
503
+
477
504
  # --- bun --------------------------------------------------------------------
478
505
  mod_bun() {
479
506
  section "Bun"
@@ -485,7 +512,12 @@ mod_bun() {
485
512
  fi
486
513
  export PATH="${BUN_INSTALL}/bin:${PATH}"
487
514
  require_command bun
488
- log_event "INFO" "bun" "runtime_ready" "Bun runtime validated" 0 "version=$(bun --version)"
515
+ local bun_version
516
+ if capture_cmd bun_version "bun" bun --version; then
517
+ log_event "INFO" "bun" "runtime_ready" "Bun runtime validated" 0 "version=${bun_version}"
518
+ else
519
+ return 1
520
+ fi
489
521
  }
490
522
 
491
523
  # --- pi ---------------------------------------------------------------------
@@ -499,7 +531,10 @@ mod_pi() {
499
531
  fi
500
532
  export PATH="${HOME}/.pi/bin:${HOME}/.local/bin:${PATH}"
501
533
  require_command pi
502
- PI_VERSION="$(pi --version 2>/dev/null || true)"
534
+ if ! PI_VERSION="$(pi --version 2>/dev/null)"; then
535
+ PI_VERSION="unknown"
536
+ log_event "WARN" "pi" "version_unavailable" "Could not read pi --version" 0
537
+ fi
503
538
  log_event "INFO" "pi" "cli_ready" "Pi CLI detected" 0 "version=${PI_VERSION}"
504
539
  mkdir -p "${PI_AGENT_DIR}" "${PI_EXTENSIONS_DIR}" "${PI_NPM_DIR}" "${PI_AGENT_DIR}/skills"
505
540
  }
@@ -508,8 +543,12 @@ mod_pi() {
508
543
  mod_go() {
509
544
  section "Go toolchain"
510
545
  if command -v go >/dev/null 2>&1; then
511
- log_event "INFO" "go" "already_present" "Go already installed" 0 "version=$(go version)"
512
- return
546
+ local go_version
547
+ if capture_cmd go_version "go" go version; then
548
+ log_event "INFO" "go" "already_present" "Go already installed" 0 "version=${go_version}"
549
+ return
550
+ fi
551
+ return 1
513
552
  fi
514
553
  if [[ "${OS_FAMILY}" == "macos" ]]; then
515
554
  run_cmd "go" brew install go
@@ -542,27 +581,37 @@ mod_dotenv() {
542
581
  fi
543
582
 
544
583
  # Idempotent upstream-quirk patches (no-ops once upstream merges the fixes).
545
- if grep -RIl --exclude-dir=.git "gthelding/monokai-pro.nvim" "${DOTENV_DIR}" \
546
- >"${TMP_DIR}/monokai_hits" 2>/dev/null; then
584
+ if grep_probe "dotenv" "${TMP_DIR}/monokai_hits" \
585
+ -RIl --exclude-dir=.git "gthelding/monokai-pro.nvim" "${DOTENV_DIR}"; then
547
586
  while IFS= read -r file; do
548
- sed -i 's|gthelding/monokai-pro.nvim|loctvl842/monokai-pro.nvim|g' "${file}"
587
+ run_cmd "dotenv" sed -i 's|gthelding/monokai-pro.nvim|loctvl842/monokai-pro.nvim|g' "${file}"
549
588
  log_event "INFO" "dotenv" "reference_patched" "Updated stale monokai-pro reference" 0 "file=${file}"
550
589
  done < "${TMP_DIR}/monokai_hits"
551
590
  fi
552
591
 
553
- if grep -RIl --exclude-dir=.git 'sudo npm install -g --prefix /usr/local bun' "${DOTENV_DIR}" \
554
- >"${TMP_DIR}/sudo_npm_hits" 2>/dev/null; then
592
+ if grep_probe "dotenv" "${TMP_DIR}/sudo_npm_hits" \
593
+ -RIl --exclude-dir=.git 'sudo npm install -g --prefix /usr/local bun' "${DOTENV_DIR}"; then
555
594
  while IFS= read -r file; do
556
- sed -i 's|sudo npm install -g --prefix /usr/local bun|sudo "$(command -v npm)" install -g --prefix /usr/local bun|g' "${file}"
595
+ run_cmd "dotenv" sed -i 's|sudo npm install -g --prefix /usr/local bun|sudo "$(command -v npm)" install -g --prefix /usr/local bun|g' "${file}"
557
596
  log_event "INFO" "dotenv" "reference_patched" "Patched sudo npm call to absolute path" 0 "file=${file}"
558
597
  done < "${TMP_DIR}/sudo_npm_hits"
559
598
  fi
560
599
 
561
- if grep -RIl --exclude-dir=.git -e 'npm install -g --prefix "\$HOME/.local" tree-sitter-cli' "${DOTENV_DIR}" \
562
- >"${TMP_DIR}/treesitter_hits" 2>/dev/null; then
600
+ if grep_probe "dotenv" "${TMP_DIR}/treesitter_hits" \
601
+ -RIl --exclude-dir=.git -e 'npm install -g --prefix "\$HOME/.local" tree-sitter-cli' "${DOTENV_DIR}"; then
563
602
  while IFS= read -r file; do
564
- grep -q 'AI_DEV_TS_CLI_PATCH' "${file}" && continue
565
- python3 - "${file}" <<'PYPATCH'
603
+ local marker_rc=0
604
+ grep -q 'AI_DEV_TS_CLI_PATCH' "${file}" 2>>"${HUMAN_LOG}" || marker_rc=$?
605
+ case "${marker_rc}" in
606
+ 0) continue ;; # already patched
607
+ 1) ;; # not patched yet; apply below
608
+ *)
609
+ log_event "ERROR" "dotenv" "marker_probe_failed" \
610
+ "grep marker probe failed with an operational error" "${marker_rc}" "file=${file}"
611
+ exit "${marker_rc}"
612
+ ;;
613
+ esac
614
+ run_cmd "dotenv" python3 - "${file}" <<'PYPATCH'
566
615
  import sys
567
616
  path = sys.argv[1]
568
617
  with open(path, "r", newline="") as fh:
@@ -591,10 +640,74 @@ PYPATCH
591
640
  if [[ -x "${DOTENV_DIR}/setup_env.sh" ]]; then
592
641
  run_cmd "dotenv" bash "${DOTENV_DIR}/setup_env.sh"
593
642
  else
594
- log_event "WARN" "dotenv" "setup_script_missing" "dotenv/setup_env.sh missing or not executable"
643
+ log_event "ERROR" "dotenv" "setup_script_missing" \
644
+ "dotenv/setup_env.sh missing or not executable" 1 \
645
+ "expected=${DOTENV_DIR}/setup_env.sh"
646
+ return 1
647
+ fi
648
+ }
649
+
650
+ # --- skill target helpers ----------------------------------------------------
651
+ agent_config_dir() {
652
+ case "$1" in
653
+ pi) printf '%s\n' "${HOME}/.pi" ;;
654
+ claude-code) printf '%s\n' "${HOME}/.claude" ;;
655
+ gemini-cli) printf '%s\n' "${HOME}/.gemini" ;;
656
+ cursor) printf '%s\n' "${HOME}/.cursor" ;;
657
+ antigravity) printf '%s\n' "${HOME}/.antigravity" ;;
658
+ codex) printf '%s\n' "${HOME}/.codex" ;;
659
+ opencode) printf '%s\n' "${HOME}/.config/opencode" ;;
660
+ *) return 1 ;;
661
+ esac
662
+ }
663
+
664
+ agent_skill_root() {
665
+ case "$1" in
666
+ pi) printf '%s\n' "${PI_AGENT_DIR}/skills" ;;
667
+ claude-code) printf '%s\n' "${HOME}/.claude/skills" ;;
668
+ gemini-cli) printf '%s\n' "${HOME}/.gemini/skills" ;;
669
+ cursor) printf '%s\n' "${HOME}/.cursor/skills" ;;
670
+ antigravity) printf '%s\n' "${HOME}/.antigravity/skills" ;;
671
+ codex) printf '%s\n' "${HOME}/.codex/skills" ;;
672
+ opencode) printf '%s\n' "${HOME}/.config/opencode/skills" ;;
673
+ *) return 1 ;;
674
+ esac
675
+ }
676
+
677
+ # Candidate skill roots per agent (one per line): verification passes if
678
+ # SKILL.md exists under any of them. Every agent keeps its single existing root;
679
+ # Codex also accepts ${HOME}/.agents/skills because upstream `skills add
680
+ # --global` writes Codex skills there instead of ${HOME}/.codex/skills.
681
+ agent_skill_roots() {
682
+ agent_skill_root "$1" || return 1
683
+ if [[ "$1" == codex ]]; then
684
+ printf '%s\n' "${HOME}/.agents/skills"
595
685
  fi
596
686
  }
597
687
 
688
+ verify_skill_for_agents() {
689
+ local phase="$1" skill="$2"; shift 2
690
+ local agent root found checked
691
+ for agent in "$@"; do
692
+ found=0
693
+ checked=""
694
+ while IFS= read -r root; do
695
+ checked="${checked:+${checked}, }${root}/${skill}/SKILL.md"
696
+ if [[ -f "${root}/${skill}/SKILL.md" ]]; then
697
+ found=1
698
+ break
699
+ fi
700
+ done < <(agent_skill_roots "${agent}")
701
+ if (( found == 0 )); then
702
+ log_event "ERROR" "${phase}" "skill_missing" \
703
+ "Skill SKILL.md missing for targeted agent" 1 \
704
+ "agent=${agent};skill=${skill};checked=${checked}"
705
+ return 1
706
+ fi
707
+ done
708
+ return 0
709
+ }
710
+
598
711
  # --- engineering-excellence -------------------------------------------------
599
712
  mod_ee() {
600
713
  section "Engineering Excellence"
@@ -604,41 +717,72 @@ mod_ee() {
604
717
  # (replaces the old git-clone-and-move). Agents are detected by their config dir.
605
718
  # Keys are the `skills` CLI agent names (claude-code, gemini-cli, ...), which
606
719
  # differ from the config-dir basename; values are the dir we detect them by.
607
- local agent dir
608
- local -A agent_dir=(
609
- [pi]="${HOME}/.pi"
610
- [claude-code]="${HOME}/.claude"
611
- [gemini-cli]="${HOME}/.gemini"
612
- [cursor]="${HOME}/.cursor"
613
- [antigravity]="${HOME}/.antigravity"
614
- [codex]="${HOME}/.codex"
615
- [opencode]="${HOME}/.config/opencode"
616
- )
720
+ local agent
617
721
  local installed_any=0
722
+ local -a target_agents=()
618
723
  for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
619
- dir="${agent_dir[$agent]}"
620
- [[ -d "${dir}" ]] || continue
621
- run_optional "engineering-excellence" \
724
+ [[ -d "$(agent_config_dir "${agent}")" ]] || continue
725
+ run_cmd "engineering-excellence" \
622
726
  npx --yes skills@latest add "${ENGINEERING_EXCELLENCE_SLUG}" \
623
727
  --skill "${ENGINEERING_EXCELLENCE_SKILL}" --global --agent "${agent}" --copy --yes
728
+ target_agents+=("${agent}")
624
729
  installed_any=1
625
730
  done
626
731
 
627
732
  if (( installed_any == 0 )); then
628
733
  # No agent detected yet — install at least for pi (created by mod_pi).
629
- run_optional "engineering-excellence" \
734
+ run_cmd "engineering-excellence" \
630
735
  npx --yes skills@latest add "${ENGINEERING_EXCELLENCE_SLUG}" \
631
736
  --skill "${ENGINEERING_EXCELLENCE_SKILL}" --global --agent pi --copy --yes
737
+ target_agents=(pi)
632
738
  fi
633
739
 
634
- if [[ -f "${ENGINEERING_EXCELLENCE_DIR}/SKILL.md" ]]; then
635
- log_event "INFO" "engineering-excellence" "skill_installed" "EE skill present for pi" 0 \
636
- "path=${ENGINEERING_EXCELLENCE_DIR}"
637
- else
638
- log_event "WARN" "engineering-excellence" "skill_path_unverified" \
639
- "EE SKILL.md not found at pi path; check other agents' skill dirs" 0 \
640
- "expected=${ENGINEERING_EXCELLENCE_DIR}"
641
- fi
740
+ verify_skill_for_agents "engineering-excellence" "${ENGINEERING_EXCELLENCE_SKILL}" "${target_agents[@]}"
741
+ log_event "INFO" "engineering-excellence" "skills_verified" \
742
+ "Engineering Excellence skill verified for every targeted agent" 0 \
743
+ "agents=$(IFS=,; printf '%s' "${target_agents[*]}")"
744
+ }
745
+
746
+ # --- upstream agent skills --------------------------------------------------
747
+ # Installs vekexasia/dotenv's skill stack on every OS via `npx skills add`.
748
+ # On Linux the dotenv module may already install these; skills add --copy is
749
+ # idempotent, so a re-run is safe.
750
+ mod_skills() {
751
+ section "Agent skills (upstream stack)"
752
+ require_command npx
753
+
754
+ # Agent keys are the `skills` CLI names (claude-code, gemini-cli, ...),
755
+ # detected by their config dir — same mapping as mod_ee.
756
+ local agent
757
+ local -a agents=()
758
+ for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
759
+ [[ -d "$(agent_config_dir "${agent}")" ]] && agents+=("${agent}")
760
+ done
761
+ # pi is created by mod_pi; guarantee at least pi so the stack always lands.
762
+ (( ${#agents[@]} == 0 )) && agents=(pi)
763
+
764
+ local source_spec source skill_csv
765
+ local -a skill_list
766
+ for source_spec in "${UPSTREAM_SKILL_SOURCES[@]}"; do
767
+ source="${source_spec%% *}"
768
+ skill_csv="${source_spec#* }"
769
+ # The script keeps global IFS at newline/tab for safe line reads;
770
+ # explicitly restore spaces for this space-separated skill list.
771
+ IFS=' ' read -r -a skill_list <<< "${skill_csv}"
772
+ for agent in "${agents[@]}"; do
773
+ run_cmd "skills" \
774
+ npx --yes skills@latest add "${source}" \
775
+ --skill "${skill_list[@]}" --global --agent "${agent}" --copy --yes
776
+ done
777
+ done
778
+
779
+ local sk
780
+ for sk in "${UPSTREAM_SKILL_NAMES[@]}"; do
781
+ verify_skill_for_agents "skills" "${sk}" "${agents[@]}"
782
+ done
783
+ log_event "INFO" "skills" "skills_verified" \
784
+ "Upstream skills verified for every targeted agent" 0 \
785
+ "agents=$(IFS=,; printf '%s' "${agents[*]}")"
642
786
  }
643
787
 
644
788
  # --- pi-extensible-workflows ------------------------------------------------
@@ -646,6 +790,7 @@ mod_pi_workflows() {
646
790
  section "pi-extensible-workflows"
647
791
  require_command pi
648
792
  require_command node
793
+ assert_node_minimum
649
794
 
650
795
  if [[ -z "${PI_WORKFLOW_VERSION}" ]]; then
651
796
  capture_cmd PI_WORKFLOW_VERSION "pi-workflows" npm view pi-extensible-workflows version
@@ -661,16 +806,33 @@ mod_pi_workflows() {
661
806
  mkdir -p "${PI_EXTENSIONS_DIR}"
662
807
  pushd "${PI_EXTENSIONS_DIR}" >/dev/null
663
808
  printf '%s\n' 'ignore-scripts=false' > .npmrc
809
+ # Mark this directory as an npm project root so `npm install` lands HERE and
810
+ # cannot walk up the tree into an ancestor project. This matters when
811
+ # ~/.pi/agent is symlinked into another repo (e.g. the dotenv dotfiles):
812
+ # without a local package.json, npm would install into that repo's
813
+ # node_modules and verification would then pick up a stale, shadowing copy.
814
+ if [[ ! -f package.json ]]; then
815
+ printf '%s\n' '{"name":"pi-extensions","private":true}' > package.json
816
+ fi
664
817
  run_cmd "pi-workflows-node" npm install --save-exact --no-audit --no-fund \
665
818
  "pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
666
819
  popd >/dev/null
667
820
 
668
- local resolved
669
- resolved="$(node -e 'console.log(require.resolve("pi-extensible-workflows",{paths:[process.argv[1]]}))' "${PI_EXTENSIONS_DIR}")"
670
- log_event "INFO" "pi-workflows-node" "module_resolved" "Resolvable from pi extensions dir" 0 "resolved=${resolved}"
821
+ # Verify the version that actually landed in THIS directory by reading its
822
+ # local package.json directly. Do NOT use require.resolve here: it ascends
823
+ # the directory tree and can resolve a shadowing copy in an ancestor
824
+ # node_modules, producing a false version_mismatch.
825
+ local local_pkg="${PI_EXTENSIONS_DIR}/node_modules/pi-extensible-workflows/package.json"
826
+ if [[ ! -f "${local_pkg}" ]]; then
827
+ log_event "ERROR" "pi-workflows-node" "install_missing" "pi-extensible-workflows not installed in extensions dir" 1 \
828
+ "expected_path=${local_pkg}"
829
+ exit 1
830
+ fi
831
+ log_event "INFO" "pi-workflows-node" "module_resolved" "Installed in pi extensions dir" 0 "resolved=${local_pkg}"
671
832
 
672
833
  local installed
673
- installed="$(resolve_workflow_version "${PI_EXTENSIONS_DIR}")"
834
+ capture_cmd installed "pi-workflows-node" node -e \
835
+ 'console.log(require(process.argv[1]).version)' "${local_pkg}"
674
836
  if [[ "${installed}" != "${PI_WORKFLOW_VERSION}" ]]; then
675
837
  log_event "ERROR" "pi-workflows-node" "version_mismatch" "Installed version mismatch" 1 \
676
838
  "expected=${PI_WORKFLOW_VERSION};actual=${installed}"
@@ -680,9 +842,31 @@ mod_pi_workflows() {
680
842
  if [[ -d "${DOTENV_EXT_DIR}" ]]; then
681
843
  pushd "${DOTENV_EXT_DIR}" >/dev/null
682
844
  printf '%s\n' 'ignore-scripts=false' > .npmrc
845
+ if [[ ! -f package.json ]]; then
846
+ printf '%s\n' '{"name":"pi-ext-workflows","private":true}' > package.json
847
+ fi
683
848
  run_cmd "dotenv-workflows" npm install --save-exact --no-audit --no-fund \
684
849
  "pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
685
850
  popd >/dev/null
851
+
852
+ local dotenv_workflow_pkg="${DOTENV_EXT_DIR}/node_modules/pi-extensible-workflows/package.json"
853
+ if [[ ! -f "${dotenv_workflow_pkg}" ]]; then
854
+ log_event "ERROR" "dotenv-workflows" "install_missing" \
855
+ "pi-extensible-workflows not installed in dotenv extensions dir" 1 \
856
+ "expected_path=${dotenv_workflow_pkg}"
857
+ exit 1
858
+ fi
859
+ local dotenv_installed
860
+ capture_cmd dotenv_installed "dotenv-workflows" node -e \
861
+ 'console.log(require(process.argv[1]).version)' "${dotenv_workflow_pkg}"
862
+ if [[ "${dotenv_installed}" != "${PI_WORKFLOW_VERSION}" ]]; then
863
+ log_event "ERROR" "dotenv-workflows" "version_mismatch" \
864
+ "Installed dotenv workflow version mismatch" 1 \
865
+ "expected=${PI_WORKFLOW_VERSION};actual=${dotenv_installed}"
866
+ exit 1
867
+ fi
868
+ log_event "INFO" "dotenv-workflows" "module_verified" \
869
+ "Verified installed dotenv workflow package" 0 "path=${dotenv_workflow_pkg}"
686
870
  fi
687
871
  }
688
872
 
@@ -690,92 +874,126 @@ mod_pi_workflows() {
690
874
  mod_herdr() {
691
875
  section "herdr"
692
876
  if command -v herdr >/dev/null 2>&1; then
693
- log_event "INFO" "herdr" "already_present" "herdr already installed" 0 "version=$(herdr --version 2>/dev/null || true)"
694
- return
877
+ local herdr_version
878
+ if capture_cmd herdr_version "herdr" herdr --version; then
879
+ log_event "INFO" "herdr" "already_present" "herdr already installed" 0 "version=${herdr_version}"
880
+ return
881
+ fi
882
+ return 1
695
883
  fi
696
884
  if [[ "${OS_FAMILY}" == "macos" ]]; then
697
- run_optional "herdr" brew install herdr
885
+ run_cmd "herdr" brew install herdr
698
886
  else
699
887
  local installer="${TMP_DIR}/install-herdr.sh"
700
- run_optional "herdr" curl -fsSL https://herdr.dev/install.sh -o "${installer}"
701
- [[ -s "${installer}" ]] && run_optional "herdr" sh "${installer}"
888
+ run_cmd "herdr" curl -fsSL https://herdr.dev/install.sh -o "${installer}"
889
+ [[ -s "${installer}" ]] || {
890
+ log_event "ERROR" "herdr" "installer_missing" "herdr installer is empty" 1 "path=${installer}"
891
+ return 1
892
+ }
893
+ run_cmd "herdr" sh "${installer}"
702
894
  fi
895
+ require_command herdr
703
896
  }
704
897
 
705
898
  # --- gentle-ai --------------------------------------------------------------
899
+ # Reinstates the Gentle AI ecosystem configurator. `gentle-ai install` is the
900
+ # per-agent/per-IDE selector (Pi, Claude Code, Cursor, Codex, ...) that also
901
+ # wires each selected agent's MCP servers, so tools appear under /mcp. The
902
+ # configurator is a CORE step: it always runs (interactively with a TTY, or
903
+ # non-interactively over the detected agents in CI/pipes) and a failure fails
904
+ # the module. For pi we additionally guarantee + verify the first-class
905
+ # gentle-pi harness and pi-mcp-adapter. Idempotent: safe to re-run.
706
906
  mod_gentle_ai() {
707
907
  section "gentle-ai"
708
- if ! command -v gentle-ai >/dev/null 2>&1 && ! command -v gga >/dev/null 2>&1; then
908
+
909
+ # Installers drop the CLI into a PATH dir; make sure the usual ones resolve
910
+ # in this live process so the post-install verification can find it.
911
+ export PATH="${HOME}/.local/bin:${HOME}/go/bin:${PATH}"
912
+
913
+ # The per-agent selector and the pi harness both require the gentle-ai CLI
914
+ # itself; a legacy standalone `gga` is NOT enough, so install whenever the
915
+ # gentle-ai CLI is missing even if an old gga is on PATH.
916
+ if ! command -v gentle-ai >/dev/null 2>&1; then
709
917
  if [[ "${OS_FAMILY}" == "macos" ]]; then
710
- run_optional "gentle-ai" brew tap gentleman-programming/tap
711
- run_optional "gentle-ai" brew install gentle-ai
918
+ run_cmd "gentle-ai" brew tap gentleman-programming/tap
919
+ run_cmd "gentle-ai" brew install gentle-ai
712
920
  else
713
921
  local installer="${TMP_DIR}/install-gentle-ai.sh"
714
- run_optional "gentle-ai" curl -fsSL "${GENTLE_AI_INSTALL}" -o "${installer}"
715
- [[ -s "${installer}" ]] && run_optional "gentle-ai" bash "${installer}"
922
+ run_cmd "gentle-ai" curl -fsSL "${GENTLE_AI_INSTALL}" -o "${installer}"
923
+ [[ -s "${installer}" ]] || {
924
+ log_event "ERROR" "gentle-ai" "installer_missing" "gentle-ai installer is empty" 1 "path=${installer}"
925
+ return 1
926
+ }
927
+ run_cmd "gentle-ai" bash "${installer}"
928
+ fi
929
+ fi
930
+
931
+ # Verify the CLI is present (a binary, not an npm tree — command -v + --version
932
+ # is the correct check). Required for both the selector and the pi harness.
933
+ command -v gentle-ai >/dev/null 2>&1 || {
934
+ log_event "ERROR" "gentle-ai" "binary_missing" "gentle-ai CLI not found on PATH after install" 1
935
+ return 1
936
+ }
937
+ run_cmd "gentle-ai" gentle-ai --version
938
+
939
+ # Detect the agents/IDEs present on this machine (same mapping as mod_ee).
940
+ local agent
941
+ local -a detected_agents=()
942
+ for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
943
+ [[ -d "$(agent_config_dir "${agent}")" ]] && detected_agents+=("${agent}")
944
+ done
945
+ (( ${#detected_agents[@]} == 0 )) && detected_agents=(pi)
946
+
947
+ # Per-agent / per-IDE selection + MCP wiring, owned by gentle-ai. This is a
948
+ # core step and must actually run: with a real TTY we launch the interactive
949
+ # selector (run directly — run_cmd would redirect stdout and hide prompts);
950
+ # otherwise we run it non-interactively over the detected agents so CI/pipes
951
+ # never hang. A failure fails the module (no silent downgrade).
952
+ if [[ -t 0 && -t 1 ]]; then
953
+ log_event "INFO" "gentle-ai" "configurator_start" "Launching gentle-ai install (choose agents/IDEs + MCP)" 0
954
+ if ! gentle-ai install --scope global; then
955
+ log_event "ERROR" "gentle-ai" "configurator_failed" "gentle-ai install failed" 1
956
+ return 1
716
957
  fi
958
+ else
959
+ local agents_csv; agents_csv="$(IFS=,; printf '%s' "${detected_agents[*]}")"
960
+ log_event "INFO" "gentle-ai" "configurator_noninteractive" \
961
+ "No TTY; installing gentle-ai for detected agents" 0 "agents=${agents_csv}"
962
+ run_cmd "gentle-ai" gentle-ai install --scope global --agents "${agents_csv}"
717
963
  fi
964
+ log_event "INFO" "gentle-ai" "configurator_done" "gentle-ai install completed" 0
718
965
 
719
- # Enable gentle-ai INSIDE pi as packages (the standalone gga binary alone
720
- # does not register anything in pi this is why it wasn't visible there).
966
+ # Guarantee pi reads gentle-ai in its MCP list (/mcp): install the first-class
967
+ # gentle-pi harness and the pi-mcp-adapter bridge, then verify the exact
968
+ # target (pi's own settings file), not a walked resolution.
721
969
  if command -v pi >/dev/null 2>&1; then
722
- run_optional "gentle-ai" pi install npm:gentle-pi
723
- run_optional "gentle-ai" pi install npm:pi-mcp-adapter
724
- log_event "INFO" "gentle-ai" "pi_enabled" "gentle-pi registered in pi (verify: /gentle-ai:status)" 0
970
+ run_cmd "gentle-ai" pi install npm:gentle-pi
971
+ run_cmd "gentle-ai" pi install npm:pi-mcp-adapter
972
+ local pi_settings="${PI_AGENT_DIR}/settings.json"
973
+ if [[ -f "${pi_settings}" ]] \
974
+ && grep -q '"npm:gentle-pi"' "${pi_settings}" \
975
+ && grep -q '"npm:pi-mcp-adapter"' "${pi_settings}"; then
976
+ log_event "INFO" "gentle-ai" "pi_enabled" \
977
+ "gentle-pi + pi-mcp-adapter registered in pi (verify: /mcp, /gentle-ai:status)" 0
978
+ else
979
+ log_event "ERROR" "gentle-ai" "pi_enable_failed" \
980
+ "gentle-pi and/or pi-mcp-adapter not present in pi settings after install" 1 "expected=${pi_settings}"
981
+ return 1
982
+ fi
725
983
  fi
726
984
 
727
- # Surface gentle-ai's own next-steps (do NOT run — they are per-repo).
728
985
  log_event "INFO" "gentle-ai" "next_steps" "gentle-ai post-install hints" 0
729
986
  cat <<'HINT' | tee -a "${HUMAN_LOG}"
730
987
  gentle-ai next steps (run yourself, per project):
731
988
  1) Set your API keys
732
989
  2) Run your selected agent
733
- 3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models)
990
+ 3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models, /mcp)
734
991
  GGA (per project):
735
992
  gga init # inside each repo
736
993
  gga install
737
994
  HINT
738
995
  }
739
996
 
740
- # --- engram (pi persistent memory) ------------------------------------------
741
- # The gentle-engram pi extension auto-starts `engram serve`, so the Engram Go
742
- # binary MUST be on PATH first — otherwise the extension loads but silently
743
- # fails (the "engram doesn't work in pi" symptom). Install binary, then the pi
744
- # packages, then init, then the user restarts pi.
745
- mod_engram() {
746
- section "Engram memory (pi)"
747
-
748
- # 1. Engram binary (Go).
749
- if ! command -v engram >/dev/null 2>&1; then
750
- if [[ "${OS_FAMILY}" == "macos" ]]; then
751
- run_optional "engram" brew install gentleman-programming/tap/engram
752
- fi
753
- if ! command -v engram >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
754
- run_optional "engram" go install github.com/Gentleman-Programming/engram/cmd/engram@latest
755
- fi
756
- fi
757
- # go installs into $GOPATH/bin (default ~/go/bin), which may not be on PATH yet.
758
- [[ -x "${HOME}/go/bin/engram" ]] && export PATH="${HOME}/go/bin:${PATH}"
759
-
760
- if ! command -v engram >/dev/null 2>&1; then
761
- log_event "WARN" "engram" "binary_missing" \
762
- "engram binary not found after install; the pi extension needs it on PATH" 1
763
- fi
764
-
765
- # 2. pi integration. `pi-engram init` is the single source of truth: it adds
766
- # the pinned gentle-engram to settings.json and wires the Engram MCP server
767
- # in mcp.json. Do NOT also `pi install npm:gentle-engram` — that adds a
768
- # second, unversioned entry and creates a duplicate on every run.
769
- if command -v pi >/dev/null 2>&1; then
770
- run_optional "engram" pi install npm:pi-mcp-adapter
771
- run_optional "engram" npm exec --yes --package gentle-engram@latest -- pi-engram init
772
- log_event "INFO" "engram" "enabled" \
773
- "Engram enabled — RESTART pi, then verify with mem_current_project / mem_doctor / 'engram tui'" 0
774
- else
775
- log_event "WARN" "engram" "pi_missing" "pi not found; Engram pi integration skipped"
776
- fi
777
- }
778
-
779
997
  # --- codex ------------------------------------------------------------------
780
998
  mod_codex() {
781
999
  section "Codex CLI"
@@ -784,13 +1002,18 @@ mod_codex() {
784
1002
  return
785
1003
  fi
786
1004
  if [[ "${OS_FAMILY}" == "macos" ]] && command -v brew >/dev/null 2>&1; then
787
- run_optional "codex" brew install --cask codex
1005
+ run_cmd "codex" brew install --cask codex
788
1006
  fi
789
1007
  if ! command -v codex >/dev/null 2>&1; then
790
1008
  local installer="${TMP_DIR}/install-codex.sh"
791
- run_optional "codex" curl -fsSL https://chatgpt.com/codex/install.sh -o "${installer}"
792
- [[ -s "${installer}" ]] && run_optional "codex" sh "${installer}"
1009
+ run_cmd "codex" curl -fsSL https://chatgpt.com/codex/install.sh -o "${installer}"
1010
+ [[ -s "${installer}" ]] || {
1011
+ log_event "ERROR" "codex" "installer_missing" "codex installer is empty" 1 "path=${installer}"
1012
+ return 1
1013
+ }
1014
+ run_cmd "codex" sh "${installer}"
793
1015
  fi
1016
+ require_command codex
794
1017
  }
795
1018
 
796
1019
  # --- antigravity ------------------------------------------------------------
@@ -801,8 +1024,13 @@ mod_antigravity() {
801
1024
  return
802
1025
  fi
803
1026
  local installer="${TMP_DIR}/install-antigravity.sh"
804
- run_optional "antigravity" curl -fsSL https://antigravity.google/cli/install.sh -o "${installer}"
805
- [[ -s "${installer}" ]] && run_optional "antigravity" bash "${installer}"
1027
+ run_cmd "antigravity" curl -fsSL https://antigravity.google/cli/install.sh -o "${installer}"
1028
+ [[ -s "${installer}" ]] || {
1029
+ log_event "ERROR" "antigravity" "installer_missing" "Antigravity installer is empty" 1 "path=${installer}"
1030
+ return 1
1031
+ }
1032
+ run_cmd "antigravity" bash "${installer}"
1033
+ require_command agy
806
1034
  }
807
1035
 
808
1036
  # --- opencode ---------------------------------------------------------------
@@ -813,12 +1041,17 @@ mod_opencode() {
813
1041
  return
814
1042
  fi
815
1043
  if [[ "${OS_FAMILY}" == "macos" ]]; then
816
- run_optional "opencode" brew install anomalyco/tap/opencode
1044
+ run_cmd "opencode" brew install anomalyco/tap/opencode
817
1045
  else
818
1046
  local installer="${TMP_DIR}/install-opencode.sh"
819
- run_optional "opencode" curl -fsSL https://opencode.ai/install -o "${installer}"
820
- [[ -s "${installer}" ]] && run_optional "opencode" bash "${installer}"
1047
+ run_cmd "opencode" curl -fsSL https://opencode.ai/install -o "${installer}"
1048
+ [[ -s "${installer}" ]] || {
1049
+ log_event "ERROR" "opencode" "installer_missing" "opencode installer is empty" 1 "path=${installer}"
1050
+ return 1
1051
+ }
1052
+ run_cmd "opencode" bash "${installer}"
821
1053
  fi
1054
+ require_command opencode
822
1055
 
823
1056
  log_event "INFO" "opencode" "zen_hint" "OpenCode Go / Zen provider hint" 0
824
1057
  cat <<'HINT' | tee -a "${HUMAN_LOG}"
@@ -864,8 +1097,11 @@ mod_cockpit() {
864
1097
  esac
865
1098
 
866
1099
  local url
867
- url="$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "${release_json}" \
868
- | cut -d '"' -f4 | grep -iE "${asset_pat}" | head -n1 || true)"
1100
+ if ! url="$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "${release_json}" \
1101
+ | cut -d '"' -f4 | grep -iE "${asset_pat}" | sed -n '1p')"; then
1102
+ log_event "WARN" "cockpit" "asset_parse_failed" "Could not parse cockpit-tools release assets"
1103
+ return 0
1104
+ fi
869
1105
 
870
1106
  if [[ -z "${url}" ]]; then
871
1107
  log_event "WARN" "cockpit" "no_matching_asset" \
@@ -875,7 +1111,10 @@ mod_cockpit() {
875
1111
 
876
1112
  local installer="${TMP_DIR}/cockpit-asset"
877
1113
  run_optional "cockpit" curl -fsSL "${url}" -o "${installer}"
878
- [[ -s "${installer}" ]] || return
1114
+ if [[ ! -s "${installer}" ]]; then
1115
+ log_event "WARN" "cockpit" "download_unavailable" "Could not download the selected cockpit-tools asset"
1116
+ return 0
1117
+ fi
879
1118
 
880
1119
  case "${PM}" in
881
1120
  apt-get) run_optional "cockpit" sudo apt-get install -y "${installer}" ;;
@@ -890,6 +1129,70 @@ mod_cockpit() {
890
1129
  esac
891
1130
  }
892
1131
 
1132
+ # --- rotator (opt-in: tuxevil-rotator multi-account gateway) -----------------
1133
+ mod_rotator() {
1134
+ section "tuxevil-rotator gateway"
1135
+ # Detect a desktop cockpit-tools data dir by marker file only; never read tokens.
1136
+ local -a candidates=(
1137
+ "${HOME}/.antigravity_cockpit"
1138
+ "${HOME}/.local/share/cockpit-tools"
1139
+ "${HOME}/.config/cockpit-tools"
1140
+ "${HOME}/.wizard-ai/cockpit-tools"
1141
+ "${HOME}/Library/Application Support/cockpit-tools"
1142
+ )
1143
+ # Windows-only locations, appended only when set (mirrors the ps1 behavior).
1144
+ [[ -n "${APPDATA:-}" ]] && candidates+=("${APPDATA}/cockpit-tools")
1145
+ [[ -n "${LOCALAPPDATA:-}" ]] && candidates+=("${LOCALAPPDATA}/cockpit-tools")
1146
+ local dir cockpit_dir=""
1147
+ for dir in "${candidates[@]}"; do
1148
+ if [[ -f "${dir}/accounts.json" || -f "${dir}/account-token.key" ]]; then
1149
+ cockpit_dir="${dir}"; break
1150
+ fi
1151
+ done
1152
+ if [[ -n "${cockpit_dir}" ]]; then
1153
+ log_event "INFO" "rotator" "cockpit_detected" "cockpit-tools data directory detected" 0 "dir=${cockpit_dir}"
1154
+ else
1155
+ log_event "INFO" "rotator" "cockpit_absent" "No cockpit-tools data directory detected; the rotator can still use its own accounts" 0
1156
+ fi
1157
+ # Non-fatal health probe.
1158
+ local gw="http://localhost:51200/v1/models" body count
1159
+ if body="$(curl -fsS -m 5 -H 'Authorization: Bearer tuxevil' "${gw}" 2>/dev/null)"; then
1160
+ # curl -fsS already proved reachability; the count is informational only
1161
+ # (awk always exits 0, so no operational failure is masked here).
1162
+ count="$(printf '%s' "${body}" | awk '{c+=gsub(/"id"/,"&")} END{print c+0}')"
1163
+ log_event "INFO" "rotator" "gateway_up" "tuxevil-rotator gateway is reachable" 0 "url=${gw};models=${count}"
1164
+ else
1165
+ log_event "INFO" "rotator" "gateway_down" "tuxevil-rotator gateway not reachable; start it with 'tuxevil-rotator start'" 0 "url=${gw}"
1166
+ fi
1167
+ # Install the CLI idempotently. Never runs login/start or writes secrets.
1168
+ if command -v tuxevil-rotator >/dev/null 2>&1; then
1169
+ log_event "INFO" "rotator" "already_present" "tuxevil-rotator already installed" 0
1170
+ else
1171
+ run_cmd "rotator" npm install --global tuxevil-rotator
1172
+ require_command tuxevil-rotator
1173
+ fi
1174
+ if command -v pi >/dev/null 2>&1; then
1175
+ run_cmd "rotator" pi install "github:darkrei08/pi-cockpit-tools-sync"
1176
+ local pi_settings="${PI_AGENT_DIR}/settings.json"
1177
+ if [[ ! -f "${pi_settings}" ]] || ! grep -Fq 'github:darkrei08/pi-cockpit-tools-sync' "${pi_settings}"; then
1178
+ log_event "ERROR" "rotator" "pi_extension_missing" "Pi did not register cockpit sync extension" 1 "expected=${pi_settings}"
1179
+ return 1
1180
+ fi
1181
+ log_event "INFO" "rotator" "pi_extension_verified" "Cockpit sync extension registered in Pi" 0 "path=${pi_settings}"
1182
+ else
1183
+ log_event "INFO" "rotator" "pi_extension_skipped" "pi not found; cockpit sync extension was not installed" 0
1184
+ fi
1185
+ cat <<'HINT' | tee -a "${HUMAN_LOG}"
1186
+ tuxevil-rotator installed. To use the multi-account Gemini/Antigravity gateway:
1187
+ tuxevil-rotator login # add a Google Antigravity account (repeat to add more)
1188
+ tuxevil-rotator import # or bulk-import accounts from a cockpit-tools JSON
1189
+ tuxevil-rotator start # start the rotating proxy on http://localhost:51200
1190
+ Pi reaches it through the 'tuxevil-rotator' provider configured in your dotenv.
1191
+ The cockpit sync extension provides /cockpit-sync, /cockpit-provision, and /cockpit-proxy.
1192
+ Login/start are never run by setup-ai and no tokens are read or stored.
1193
+ HINT
1194
+ }
1195
+
893
1196
  # ==============================================================================
894
1197
  # Shell environment
895
1198
  # ==============================================================================
@@ -930,17 +1233,75 @@ quality_gates() {
930
1233
  run_cmd "quality" bash -n "${BASH_SOURCE[0]}"
931
1234
 
932
1235
  is_selected node && { run_cmd "quality" node --version; run_cmd "quality" npm --version; }
933
- is_selected bun && run_optional "quality" bun --version
934
- is_selected pi && run_optional "quality" pi --no-extensions --version
1236
+ is_selected bun && run_cmd "quality" bun --version
1237
+ if is_selected pi; then
1238
+ require_command pi
1239
+ run_cmd "quality" pi --no-extensions --version
1240
+ fi
1241
+
1242
+ if is_selected node || is_selected pi-workflows; then
1243
+ assert_node_minimum
1244
+ fi
935
1245
 
936
1246
  if is_selected pi-workflows; then
1247
+ local workflow_pkg="${PI_EXTENSIONS_DIR}/node_modules/pi-extensible-workflows/package.json"
1248
+ if [[ ! -f "${workflow_pkg}" ]]; then
1249
+ log_event "ERROR" "quality" "workflow_package_missing" \
1250
+ "pi-extensible-workflows package.json missing from extensions dir" 1 \
1251
+ "expected_path=${workflow_pkg}"
1252
+ exit 1
1253
+ fi
937
1254
  run_cmd "quality" node -e \
938
- 'const root=process.argv[1]; console.log("RESOLVED="+require.resolve("pi-extensible-workflows",{paths:[root]}))' \
939
- "${PI_EXTENSIONS_DIR}"
1255
+ 'const fs=require("fs"); const path=process.argv[1]; const pkg=JSON.parse(fs.readFileSync(path,"utf8")); console.log("VERSION="+pkg.version)' \
1256
+ "${workflow_pkg}"
940
1257
  fi
941
1258
 
942
- if is_selected ee && [[ -f "${ENGINEERING_EXCELLENCE_DIR}/SKILL.md" ]]; then
943
- log_event "INFO" "quality" "ee_gate_passed" "Engineering Excellence SKILL.md present"
1259
+ local -a target_agents=()
1260
+ local agent sk
1261
+ for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
1262
+ [[ -d "$(agent_config_dir "${agent}")" ]] && target_agents+=("${agent}")
1263
+ done
1264
+ (( ${#target_agents[@]} == 0 )) && target_agents=(pi)
1265
+
1266
+ if is_selected ee; then
1267
+ verify_skill_for_agents "quality" "${ENGINEERING_EXCELLENCE_SKILL}" "${target_agents[@]}"
1268
+ log_event "INFO" "quality" "ee_gate_passed" \
1269
+ "Engineering Excellence skill verified for every targeted agent" 0
1270
+ fi
1271
+
1272
+ if is_selected skills; then
1273
+ for sk in "${UPSTREAM_SKILL_NAMES[@]}"; do
1274
+ verify_skill_for_agents "quality" "${sk}" "${target_agents[@]}"
1275
+ done
1276
+ log_event "INFO" "quality" "skills_gate_passed" \
1277
+ "All upstream skills verified for every targeted agent" 0
1278
+ fi
1279
+
1280
+ if is_selected gentle-ai; then
1281
+ # The module requires the gentle-ai CLI specifically; a legacy standalone
1282
+ # gga cannot prove the per-agent/MCP configuration ran.
1283
+ if ! command -v gentle-ai >/dev/null 2>&1; then
1284
+ log_event "ERROR" "quality" "gentle_ai_missing" \
1285
+ "gentle-ai CLI not found on PATH after install" 1
1286
+ exit 1
1287
+ fi
1288
+ if command -v pi >/dev/null 2>&1; then
1289
+ local gentle_settings="${PI_AGENT_DIR}/settings.json"
1290
+ if [[ ! -f "${gentle_settings}" ]] \
1291
+ || ! grep -q '"npm:gentle-pi"' "${gentle_settings}" \
1292
+ || ! grep -q '"npm:pi-mcp-adapter"' "${gentle_settings}"; then
1293
+ log_event "ERROR" "quality" "gentle_pi_missing" \
1294
+ "gentle-pi and/or pi-mcp-adapter not registered in pi settings" 1 "expected=${gentle_settings}"
1295
+ exit 1
1296
+ fi
1297
+ log_event "INFO" "quality" "gentle_ai_gate_passed" \
1298
+ "gentle-ai verified (CLI present; gentle-pi + pi-mcp-adapter registered in pi)" 0
1299
+ else
1300
+ # No pi on PATH: pi MCP wiring is genuinely not applicable, so do not
1301
+ # claim it was registered.
1302
+ log_event "INFO" "quality" "gentle_ai_gate_passed" \
1303
+ "gentle-ai CLI verified; pi not present, so pi MCP wiring was not required" 0
1304
+ fi
944
1305
  fi
945
1306
 
946
1307
  log_event "INFO" "quality" "gates_done" "Quality gates completed for selected modules"
@@ -965,8 +1326,8 @@ print_list() {
965
1326
  printf 'AI Dev Suite %s — modules (core = installed by default):\n\n' "${SCRIPT_VERSION}"
966
1327
  local m tag
967
1328
  for m in "${MODULE_ORDER[@]}"; do
968
- if [[ -n "${MODULE_OPTIONAL[$m]:-}" ]]; then tag="optional"; else tag="core "; fi
969
- printf ' [%s] %-14s %s\n' "${tag}" "${m}" "${MODULE_DESC[$m]}"
1329
+ if module_is_optional "${m}"; then tag="optional"; else tag="core "; fi
1330
+ printf ' [%s] %-14s %s\n' "${tag}" "${m}" "$(module_desc "${m}")"
970
1331
  done
971
1332
  printf '\nUse: --only <csv> | --all | (default = core)\n'
972
1333
  }
@@ -978,11 +1339,14 @@ print_help() {
978
1339
  }
979
1340
 
980
1341
  parse_args() {
981
- local mode="default" only_csv=""
1342
+ local mode="default" only_csv="" all_requested=0
982
1343
  while [[ $# -gt 0 ]]; do
983
1344
  case "$1" in
984
- --all) mode="all"; shift ;;
985
- --only) mode="only"; only_csv="${2:-}"; shift 2 ;;
1345
+ --all) mode="all"; all_requested=1; shift ;;
1346
+ --only)
1347
+ [[ $# -ge 2 ]] || { printf '%s\n' '--only requires a non-empty comma-separated module list.' >&2; exit 2; }
1348
+ mode="only"; only_csv="$2"; shift 2
1349
+ ;;
986
1350
  --only=*) mode="only"; only_csv="${1#*=}"; shift ;;
987
1351
  --yes|-y) shift ;; # accepted for launcher parity
988
1352
  --list) print_list; exit 0 ;;
@@ -991,26 +1355,37 @@ parse_args() {
991
1355
  esac
992
1356
  done
993
1357
 
1358
+ # Match PowerShell: -All wins whenever it is present, regardless of
1359
+ # where it appears relative to --only.
1360
+ (( all_requested == 1 )) && mode="all"
1361
+
994
1362
  local requested=()
995
1363
  case "${mode}" in
996
1364
  all) requested=("${MODULE_ORDER[@]}") ;;
997
1365
  only)
998
- local IFS_SAVE="${IFS}"; IFS=','
999
- # shellcheck disable=SC2206
1000
- local raw=(${only_csv})
1001
- IFS="${IFS_SAVE}"
1366
+ # Split on commas without word-splitting or glob expansion.
1367
+ local raw=()
1368
+ IFS=',' read -r -a raw <<< "${only_csv}"
1002
1369
  local r
1003
1370
  for r in "${raw[@]}"; do
1004
- r="$(printf '%s' "${r}" | tr -d '[:space:]')"
1371
+ # Trim only surrounding whitespace (match PowerShell .Trim());
1372
+ # internal whitespace makes the token invalid instead of being
1373
+ # silently collapsed (e.g. "co dex" must not become "codex").
1374
+ r="${r#"${r%%[![:space:]]*}"}"
1375
+ r="${r%"${r##*[![:space:]]}"}"
1005
1376
  [[ -z "${r}" ]] && continue
1006
- [[ -n "${MODULE_DESC[$r]:-}" ]] || { printf 'Unknown module: %s\n' "${r}" >&2; exit 2; }
1377
+ module_desc "${r}" >/dev/null || { printf 'Unknown module: %s\n' "${r}" >&2; exit 2; }
1007
1378
  requested+=("${r}")
1008
1379
  done
1380
+ (( ${#requested[@]} > 0 )) || {
1381
+ printf '%s\n' '--only requires a non-empty comma-separated module list.' >&2
1382
+ exit 2
1383
+ }
1009
1384
  ;;
1010
1385
  default)
1011
1386
  local m
1012
1387
  for m in "${MODULE_ORDER[@]}"; do
1013
- [[ -n "${MODULE_OPTIONAL[$m]:-}" ]] && continue
1388
+ module_is_optional "${m}" && continue
1014
1389
  requested+=("${m}")
1015
1390
  done
1016
1391
  ;;
@@ -1053,10 +1428,15 @@ parse_args "$@"
1053
1428
 
1054
1429
  section "Preflight"
1055
1430
  require_command bash
1056
- require_command curl
1057
- require_command git
1058
1431
  detect_os
1059
1432
 
1433
+ # The base module owns bootstrap tools on a clean machine. For explicit
1434
+ # selections that skip base, fail early because later installers need them.
1435
+ if ! is_selected base; then
1436
+ require_command curl
1437
+ require_command git
1438
+ fi
1439
+
1060
1440
  if [[ "${OS_FAMILY}" == "linux" ]]; then
1061
1441
  require_command sudo
1062
1442
  fi
@@ -1073,6 +1453,11 @@ quality_gates
1073
1453
 
1074
1454
  write_report "SUCCESS" 0 "n/a" "n/a" "n/a" "n/a"
1075
1455
 
1456
+ report_version REPORT_NODE_VERSION node --version
1457
+ report_version REPORT_NPM_VERSION npm --version
1458
+ report_version REPORT_BUN_VERSION bun --version
1459
+ report_version REPORT_GO_VERSION go version
1460
+
1076
1461
  cat >> "${REPORT_FILE}" <<EOF
1077
1462
 
1078
1463
  ## Installed (selected modules)
@@ -1081,11 +1466,11 @@ ${SELECTED_DISPLAY}
1081
1466
 
1082
1467
  ## Versions
1083
1468
 
1084
- - Node.js: \`$(command -v node >/dev/null 2>&1 && node --version || echo n/a)\`
1085
- - npm: \`$(command -v npm >/dev/null 2>&1 && npm --version || echo n/a)\`
1086
- - Bun: \`$(command -v bun >/dev/null 2>&1 && bun --version || echo n/a)\`
1469
+ - Node.js: \`${REPORT_NODE_VERSION}\`
1470
+ - npm: \`${REPORT_NPM_VERSION}\`
1471
+ - Bun: \`${REPORT_BUN_VERSION}\`
1087
1472
  - Pi: \`${PI_VERSION:-n/a}\`
1088
- - Go: \`$(command -v go >/dev/null 2>&1 && go version || echo n/a)\`
1473
+ - Go: \`${REPORT_GO_VERSION}\`
1089
1474
  EOF
1090
1475
 
1091
1476
  log_event "INFO" "bootstrap" "completed" "AI Dev Suite setup completed successfully" 0 \