@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/README.md +114 -0
- package/bin/setup-ai.mjs +191 -0
- package/package.json +45 -0
- package/setup-ai.ps1 +418 -0
- package/setup-ai.sh +1091 -0
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'
|