blamcode 0.4.0__py3-none-any.whl
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.
- blamcode/__init__.py +9 -0
- blamcode/cli.py +122 -0
- blamcode/layer/README.md +43 -0
- blamcode/layer/config/agent/build.md +118 -0
- blamcode/layer/config/agent/debugger.md +32 -0
- blamcode/layer/config/agent/designer.md +31 -0
- blamcode/layer/config/agent/vision.md +26 -0
- blamcode/layer/config/agent/writer.md +28 -0
- blamcode/layer/config/command/ask.md +41 -0
- blamcode/layer/config/command/blam.md +24 -0
- blamcode/layer/config/command/explore.md +14 -0
- blamcode/layer/config/command/fix.md +14 -0
- blamcode/layer/config/command/open.md +18 -0
- blamcode/layer/config/command/review.md +14 -0
- blamcode/layer/config/opencode.json +53 -0
- blamcode/layer/config/themes/bangladeshi.json +67 -0
- blamcode/layer/config/themes/blamcode.json +217 -0
- blamcode/layer/config/tui.json +4 -0
- blamcode/layer/install.sh +752 -0
- blamcode/layer/scripts/__pycache__/patch-brand.cpython-312.pyc +0 -0
- blamcode/layer/scripts/blamcode +518 -0
- blamcode/layer/scripts/blamcode-browser +315 -0
- blamcode/layer/scripts/blamcode-menu +140 -0
- blamcode/layer/scripts/blamcode-uninstall +71 -0
- blamcode/layer/scripts/blamcode-vision +542 -0
- blamcode/layer/scripts/oc-settings.sh +138 -0
- blamcode/layer/scripts/patch-brand.py +267 -0
- blamcode/layer/skills/android-app/SKILL.md +53 -0
- blamcode/layer/skills/api-integration/SKILL.md +49 -0
- blamcode/layer/skills/bash-cli-expert/SKILL.md +48 -0
- blamcode/layer/skills/bot-development/SKILL.md +53 -0
- blamcode/layer/skills/clean-code-performance/SKILL.md +45 -0
- blamcode/layer/skills/database/SKILL.md +56 -0
- blamcode/layer/skills/debugging-fixes/SKILL.md +45 -0
- blamcode/layer/skills/deploy-hosting/SKILL.md +38 -0
- blamcode/layer/skills/docker/SKILL.md +74 -0
- blamcode/layer/skills/firebase-supabase/SKILL.md +61 -0
- blamcode/layer/skills/git-workflow/SKILL.md +63 -0
- blamcode/layer/skills/lets-scroll/SKILL.md +877 -0
- blamcode/layer/skills/lets-scroll/references/index-template.html +73 -0
- blamcode/layer/skills/lets-scroll/references/knockout.py +89 -0
- blamcode/layer/skills/lets-scroll/references/pipeline.md +312 -0
- blamcode/layer/skills/lets-scroll/references/prompts.md +194 -0
- blamcode/layer/skills/lets-scroll/references/scrub-engine.js +448 -0
- blamcode/layer/skills/project-structure/SKILL.md +74 -0
- blamcode/layer/skills/python-automation/SKILL.md +52 -0
- blamcode/layer/skills/react-next-best-practices/SKILL.md +54 -0
- blamcode/layer/skills/security-review/SKILL.md +48 -0
- blamcode/layer/skills/seo-basics/SKILL.md +44 -0
- blamcode/layer/skills/testing/SKILL.md +58 -0
- blamcode/layer/skills/ui-ux-responsive/SKILL.md +53 -0
- blamcode/layer/skills/website-builder/SKILL.md +47 -0
- blamcode-0.4.0.dist-info/METADATA +62 -0
- blamcode-0.4.0.dist-info/RECORD +58 -0
- blamcode-0.4.0.dist-info/WHEEL +5 -0
- blamcode-0.4.0.dist-info/entry_points.txt +2 -0
- blamcode-0.4.0.dist-info/licenses/LICENSE +21 -0
- blamcode-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# BLAMCODE — one-line installer
|
|
4
|
+
# Install: curl -fsSL https://raw.githubusercontent.com/
|
|
5
|
+
# zyvo9/blamcode/main/install.sh | bash
|
|
6
|
+
# or: pip install blamcode && blamcode (same installer)
|
|
7
|
+
#
|
|
8
|
+
# When run by the pip bootstrap, the BLAMCODE layer is already on
|
|
9
|
+
# disk at $BLAMCODE_BOOT and is used instead of downloading it.
|
|
10
|
+
#
|
|
11
|
+
# Shows a SINGLE live progress line:
|
|
12
|
+
# [██████░░░░░░░░░░░░░░] 32% | 9.4/29.1 MB | 1.1 MB/s | downloading core
|
|
13
|
+
# Everything else is logged to a file; errors print the log tail.
|
|
14
|
+
# Safe to re-run (idempotent): existing tools/binaries are skipped.
|
|
15
|
+
# ============================================================
|
|
16
|
+
set -e
|
|
17
|
+
umask 022
|
|
18
|
+
|
|
19
|
+
# ------------------------------------------------------------
|
|
20
|
+
# Configuration
|
|
21
|
+
# ------------------------------------------------------------
|
|
22
|
+
GH_REPO="${GH_REPO:-zyvo9/blamcode}"
|
|
23
|
+
CORE_REPO="guysoft/opencode-termux"
|
|
24
|
+
API="https://api.github.com"
|
|
25
|
+
CORE_API="$API/repos/$CORE_REPO"
|
|
26
|
+
ZEN_KEY_DEFAULT="${ZEN_API_KEY:-sk-PKOWRt2391BL0MP3W90yaG8qx4vofQJQgigJreBBYjrArj0lwuU1HkWUqOHgDGHP}"
|
|
27
|
+
OPENCODE_INSTALL_URL="https://opencode.ai/install"
|
|
28
|
+
AARCH64_MATCH="android-aarch64"
|
|
29
|
+
|
|
30
|
+
PREFIX="${PREFIX:-}"
|
|
31
|
+
# Termux detection is intentionally broad: some devices/ROMs start the
|
|
32
|
+
# installer without PREFIX exported, or with pkg missing from PATH.
|
|
33
|
+
TERMUX_ROOT="/data/data/com.termux/files/usr"
|
|
34
|
+
if [ -z "$PREFIX" ] && [ -d "$TERMUX_ROOT" ]; then
|
|
35
|
+
PREFIX="$TERMUX_ROOT"
|
|
36
|
+
export PREFIX
|
|
37
|
+
fi
|
|
38
|
+
if [ -n "$TERMUX_VERSION" ] || { [ -n "$PREFIX" ] && [ -d "$PREFIX/bin" ] && \
|
|
39
|
+
{ [ -x "$PREFIX/bin/pkg" ] || [ -x "$PREFIX/bin/apt" ] || [ -d "$PREFIX/etc/termux" ]; }; }; then
|
|
40
|
+
ENV_KIND="termux"
|
|
41
|
+
elif [ "$(uname -o 2>/dev/null)" = "Android" ]; then
|
|
42
|
+
# Android shell outside Termux (adb, other terminal apps)
|
|
43
|
+
ENV_KIND="android"
|
|
44
|
+
else
|
|
45
|
+
case "$(uname -s 2>/dev/null)" in
|
|
46
|
+
Linux) ENV_KIND="linux" ;;
|
|
47
|
+
Darwin) ENV_KIND="macos" ;;
|
|
48
|
+
FreeBSD|OpenBSD|NetBSD|DragonFly) ENV_KIND="bsd" ;;
|
|
49
|
+
SunOS|AIX) ENV_KIND="unix" ;;
|
|
50
|
+
MINGW*|MSYS*|CYGWIN*|Windows_NT) ENV_KIND="windows" ;;
|
|
51
|
+
*) ENV_KIND="posix" ;; # never a dead end
|
|
52
|
+
esac
|
|
53
|
+
fi
|
|
54
|
+
# iSH / Alpine on iOS, chromeOS crostini, WSL — all behave like linux
|
|
55
|
+
[ -n "$WSL_DISTRO_NAME" ] && ENV_KIND="linux"
|
|
56
|
+
|
|
57
|
+
RAW_ARCH="$(uname -m)"
|
|
58
|
+
# Normalize the many names Android/Termux report for the same CPU.
|
|
59
|
+
case "$RAW_ARCH" in
|
|
60
|
+
aarch64|arm64|armv8b|armv8|arm64-v8a|evbarm) ARCH="aarch64" ;;
|
|
61
|
+
armv8l|armv7l|armv7|armv7a|armv6l|arm|armhf|armel|armeabi-v7a) ARCH="arm" ;; # 32-bit userland
|
|
62
|
+
x86_64|amd64|x64) ARCH="x86_64" ;;
|
|
63
|
+
i686|i386|i586|x86|ia32) ARCH="i686" ;;
|
|
64
|
+
riscv64|ppc64le|ppc64|s390x|mips|mips64|mips64el|loongarch64) ARCH="$RAW_ARCH" ;;
|
|
65
|
+
*) ARCH="${RAW_ARCH:-unknown}" ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
TMP="$(mktemp -d)"
|
|
69
|
+
LOG="$TMP/install.log"
|
|
70
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------
|
|
73
|
+
# Single-line progress UI
|
|
74
|
+
# ------------------------------------------------------------
|
|
75
|
+
TTY=0; [ -t 1 ] && TTY=1
|
|
76
|
+
PROG=0
|
|
77
|
+
STATUS="starting"
|
|
78
|
+
SPEED=0
|
|
79
|
+
|
|
80
|
+
sizeMB() { awk -v b="$1" 'BEGIN{ printf "%.1f MB", b/1048576 }'; }
|
|
81
|
+
speed() { awk -v b="$1" 'BEGIN{
|
|
82
|
+
if (b >= 1048576) printf "%.1f MB/s", b/1048576
|
|
83
|
+
else if (b >= 1024) printf "%.1f KB/s", b/1024
|
|
84
|
+
else printf "%d B/s", b }'; }
|
|
85
|
+
|
|
86
|
+
bar() { # <pct> -> 20-char bar
|
|
87
|
+
local pct=$1 filled empty i s=""
|
|
88
|
+
[ "$pct" -gt 100 ] && pct=100
|
|
89
|
+
filled=$(( pct * 20 / 100 )); empty=$(( 20 - filled ))
|
|
90
|
+
i=0; while [ $i -lt $filled ]; do s="${s}█"; i=$((i+1)); done
|
|
91
|
+
i=0; while [ $i -lt $empty ]; do s="${s}░"; i=$((i+1)); done
|
|
92
|
+
printf '%s' "$s"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
draw() {
|
|
96
|
+
if [ "$TTY" = 1 ]; then
|
|
97
|
+
printf "\r\033[K [%s] %3d%% | %s" "$(bar "$PROG")" "$PROG" "$STATUS"
|
|
98
|
+
else
|
|
99
|
+
printf "[%3d%%] %s\n" "$PROG" "$STATUS"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
status() { STATUS="$1"; draw; }
|
|
103
|
+
bump() { PROG="$1"; draw; }
|
|
104
|
+
log() { printf '%s\n' "$*" >> "$LOG" 2>/dev/null || true; }
|
|
105
|
+
run() { "$@" >> "$LOG" 2>&1; }
|
|
106
|
+
|
|
107
|
+
run_bg() { # <label> <cmd...> — run in background, animate the status line
|
|
108
|
+
local label="$1"; shift
|
|
109
|
+
"$@" >> "$LOG" 2>&1 &
|
|
110
|
+
local pid=$! i=0
|
|
111
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
112
|
+
i=$(( (i % 10) + 1 ))
|
|
113
|
+
STATUS="$label $(spinner "$i")"
|
|
114
|
+
draw; sleep 0.12
|
|
115
|
+
done
|
|
116
|
+
wait "$pid" || return 1
|
|
117
|
+
STATUS="$label"
|
|
118
|
+
draw
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
spinner() { # <tick 1-10> -> braille frame (POSIX sh compatible)
|
|
122
|
+
case $(( $1 % 10 )) in
|
|
123
|
+
1) printf '⠋';; 2) printf '⠙';; 3) printf '⠹';; 4) printf '⠸';;
|
|
124
|
+
5) printf '⠼';; 6) printf '⠴';; 7) printf '⠦';; 8) printf '⠧';;
|
|
125
|
+
9) printf '⠇';; *) printf '⠏';;
|
|
126
|
+
esac
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
download_file() { # <url> <out> <label> — live %, MB + speed on the same line
|
|
130
|
+
local url="$1" out="$2" label="$3"
|
|
131
|
+
local total=0 got=0 last=0 t0 t1 pct pid
|
|
132
|
+
total="$(curl -sIL --connect-timeout 10 --max-time 20 "$url" \
|
|
133
|
+
| awk 'tolower($1)=="content-length:"{n=$2} END{print n+0}')"
|
|
134
|
+
t0="$(date +%s)"; last=0
|
|
135
|
+
curl -fLsS --retry 3 --retry-delay 2 -C - --connect-timeout 15 --max-time 900 -o "$out" "$url" >> "$LOG" 2>&1 &
|
|
136
|
+
pid=$!
|
|
137
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
138
|
+
got="$( { [ -f "$out" ] && wc -c < "$out" || true; } 2>/dev/null | awk '{print $1+0}')"
|
|
139
|
+
t1="$(date +%s)"
|
|
140
|
+
if [ "$t1" -gt "$t0" ]; then
|
|
141
|
+
SPEED=$(( (got - last) / (t1 - t0) )); last="$got"; t0="$t1"
|
|
142
|
+
fi
|
|
143
|
+
if [ "$total" -gt 0 ]; then
|
|
144
|
+
PROG=$(( 10 + got * 55 / total ))
|
|
145
|
+
fi
|
|
146
|
+
if [ "$total" -gt 0 ]; then
|
|
147
|
+
STATUS="$label $(sizeMB "$got")/$(sizeMB "$total") $(speed "$SPEED")"
|
|
148
|
+
else
|
|
149
|
+
STATUS="$label $(sizeMB "$got")"
|
|
150
|
+
fi
|
|
151
|
+
draw; sleep 0.25
|
|
152
|
+
done
|
|
153
|
+
if wait "$pid"; then
|
|
154
|
+
PROG=65
|
|
155
|
+
STATUS="$label done ($(sizeMB "$(wc -c < "$out" 2>/dev/null || echo 0)"))"
|
|
156
|
+
draw; return 0
|
|
157
|
+
fi
|
|
158
|
+
return 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fatal() {
|
|
162
|
+
printf "\r\033[K\n ${C_R}✖ ERROR${C_N} %s\n" "$1" 2>/dev/null || printf "\n ✖ ERROR %s\n" "$1"
|
|
163
|
+
[ -n "$2" ] && printf " → %s\n" "$2"
|
|
164
|
+
[ -s "$LOG" ] && { printf " ${C_D}last log lines:${C_N}\n"; tail -n 5 "$LOG"; }
|
|
165
|
+
exit 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# colors (only for the final summary + errors)
|
|
169
|
+
C_G=$'\033[32m'; C_C=$'\033[36m'; C_Y=$'\033[33m'; C_R=$'\033[31m'; C_D=$'\033[2m'; C_B=$'\033[1m'; C_N=$'\033[0m'
|
|
170
|
+
|
|
171
|
+
cmd_exists() { command -v "$1" >/dev/null 2>&1; }
|
|
172
|
+
sys_has_python() { cmd_exists python3 || cmd_exists python; }
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------
|
|
175
|
+
# JSON getters (python preferred; sed fallback)
|
|
176
|
+
# ------------------------------------------------------------
|
|
177
|
+
json_tag() { # <url> -> tag_name
|
|
178
|
+
local url="$1"
|
|
179
|
+
if cmd_exists python3; then
|
|
180
|
+
python3 - "$url" <<'PY' 2>/dev/null
|
|
181
|
+
import json, sys, urllib.request
|
|
182
|
+
try:
|
|
183
|
+
with urllib.request.urlopen(sys.argv[1], timeout=20) as r:
|
|
184
|
+
print(json.load(r).get("tag_name", ""))
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
PY
|
|
188
|
+
elif sys_has_python; then
|
|
189
|
+
python - "$url" <<'PY' 2>/dev/null
|
|
190
|
+
import json, sys, urllib.request
|
|
191
|
+
try:
|
|
192
|
+
with urllib.request.urlopen(sys.argv[1], timeout=20) as r:
|
|
193
|
+
print(json.load(r).get("tag_name", ""))
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
PY
|
|
197
|
+
else
|
|
198
|
+
curl -fsSL --connect-timeout 8 --max-time 20 "$url" 2>/dev/null \
|
|
199
|
+
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
|
|
200
|
+
fi
|
|
201
|
+
}
|
|
202
|
+
json_asset_url() { # <url> <match> -> first .zip asset URL
|
|
203
|
+
local url="$1" match="$2"
|
|
204
|
+
if cmd_exists python3; then
|
|
205
|
+
python3 - "$url" "$match" <<'PY' 2>/dev/null
|
|
206
|
+
import json, sys, urllib.request
|
|
207
|
+
try:
|
|
208
|
+
with urllib.request.urlopen(sys.argv[1], timeout=30) as r:
|
|
209
|
+
rel = json.load(r)
|
|
210
|
+
for a in rel.get("assets", []):
|
|
211
|
+
n = a.get("name", "")
|
|
212
|
+
if sys.argv[2] in n and n.endswith(".zip"):
|
|
213
|
+
print(a["browser_download_url"]); break
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
PY
|
|
217
|
+
elif sys_has_python; then
|
|
218
|
+
python - "$url" "$match" <<'PY' 2>/dev/null
|
|
219
|
+
import json, sys, urllib.request
|
|
220
|
+
try:
|
|
221
|
+
with urllib.request.urlopen(sys.argv[1], timeout=30) as r:
|
|
222
|
+
rel = json.load(r)
|
|
223
|
+
for a in rel.get("assets", []):
|
|
224
|
+
n = a.get("name", "")
|
|
225
|
+
if sys.argv[2] in n and n.endswith(".zip"):
|
|
226
|
+
print(a["browser_download_url"]); break
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
PY
|
|
230
|
+
else
|
|
231
|
+
curl -fsSL --connect-timeout 10 --max-time 30 "$url" 2>/dev/null \
|
|
232
|
+
| grep -o "https://[^\"]*${match}\.zip" | head -n1
|
|
233
|
+
fi
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------
|
|
237
|
+
# Step 1 — environment + paths
|
|
238
|
+
# ------------------------------------------------------------
|
|
239
|
+
setup_env() {
|
|
240
|
+
status "checking environment"
|
|
241
|
+
case "$ENV_KIND" in
|
|
242
|
+
termux)
|
|
243
|
+
BIN_DIR="$PREFIX/bin"; STAMP_DIR="$PREFIX/libexec/opencode"; LIB_DIR="$PREFIX/lib";;
|
|
244
|
+
*)
|
|
245
|
+
# linux, macos, android, bsd, unix, windows (git-bash/msys), posix
|
|
246
|
+
BIN_DIR="${BLAMCODE_BIN_DIR:-$HOME/.local/bin}"
|
|
247
|
+
STAMP_DIR="$HOME/.local/libexec/blamcode"; LIB_DIR="$HOME/.local/lib/blamcode"
|
|
248
|
+
mkdir -p "$BIN_DIR" "$STAMP_DIR" "$LIB_DIR" 2>/dev/null || {
|
|
249
|
+
# read-only or exotic $HOME — keep going in a writable place
|
|
250
|
+
BIN_DIR="${TMPDIR:-/tmp}/blamcode/bin"; STAMP_DIR="${TMPDIR:-/tmp}/blamcode/libexec"
|
|
251
|
+
LIB_DIR="${TMPDIR:-/tmp}/blamcode/lib"
|
|
252
|
+
mkdir -p "$BIN_DIR" "$STAMP_DIR" "$LIB_DIR"
|
|
253
|
+
};;
|
|
254
|
+
esac
|
|
255
|
+
# No hard stop for 32-bit / uncommon CPUs any more: those devices fall
|
|
256
|
+
# back to the Node (npm) build of the core, which is architecture neutral.
|
|
257
|
+
if [ -n "$BLAMCODE_FORCE_NODE" ]; then
|
|
258
|
+
NATIVE_CORE=0
|
|
259
|
+
status "forced Node core (BLAMCODE_FORCE_NODE)"
|
|
260
|
+
bump 2
|
|
261
|
+
return
|
|
262
|
+
fi
|
|
263
|
+
case "$ENV_KIND/$ARCH" in
|
|
264
|
+
termux/aarch64|termux/x86_64|linux/aarch64|linux/x86_64|macos/aarch64|macos/x86_64) NATIVE_CORE=1 ;;
|
|
265
|
+
*)
|
|
266
|
+
NATIVE_CORE=0
|
|
267
|
+
printf "\r\033[K ${C_Y}!${C_N} %s CPU has no native core build — using the Node build\n" "$RAW_ARCH"
|
|
268
|
+
;;
|
|
269
|
+
esac
|
|
270
|
+
status "environment ok ($ENV_KIND/$RAW_ARCH)"
|
|
271
|
+
bump 2
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# ------------------------------------------------------------
|
|
275
|
+
# Universal package installer — works on every mainstream package
|
|
276
|
+
# manager; silently succeeds as a no-op when none is usable.
|
|
277
|
+
# ------------------------------------------------------------
|
|
278
|
+
SUDO=""
|
|
279
|
+
if [ "$(id -u 2>/dev/null)" != "0" ] && [ "$ENV_KIND" != "termux" ] && cmd_exists sudo; then
|
|
280
|
+
SUDO="sudo"
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
pm_install() { # <packages...> — runs the package manager LIVE on screen.
|
|
284
|
+
# The user watches the real pkg/apt/npm-style progress instead of a
|
|
285
|
+
# silent spinner, so long installs never feel frozen. Every branch is
|
|
286
|
+
# wrapped in `if` so a failing PM just falls through to the next one
|
|
287
|
+
# (set -e must not kill the installer on a failed branch).
|
|
288
|
+
[ $# -gt 0 ] || return 0
|
|
289
|
+
if [ "$ENV_KIND" = "termux" ] && cmd_exists pkg; then
|
|
290
|
+
status "installing deps (pkg): $*"
|
|
291
|
+
if pkg install -y "$@"; then return 0; fi
|
|
292
|
+
fi
|
|
293
|
+
if cmd_exists apt-get; then status "installing deps (apt): $*"; if $SUDO apt-get install -y "$@"; then return 0; fi; fi
|
|
294
|
+
if cmd_exists dnf; then status "installing deps (dnf): $*"; if $SUDO dnf install -y "$@"; then return 0; fi; fi
|
|
295
|
+
if cmd_exists yum; then status "installing deps (yum): $*"; if $SUDO yum install -y "$@"; then return 0; fi; fi
|
|
296
|
+
if cmd_exists zypper; then status "installing deps (zypper): $*"; if $SUDO zypper --non-interactive install "$@"; then return 0; fi; fi
|
|
297
|
+
if cmd_exists pacman; then status "installing deps (pacman): $*"; if $SUDO pacman -Sy --noconfirm "$@"; then return 0; fi; fi
|
|
298
|
+
if cmd_exists apk; then status "installing deps (apk): $*"; if $SUDO apk add --no-cache "$@"; then return 0; fi; fi
|
|
299
|
+
if cmd_exists xbps-install; then status "installing deps (xbps): $*"; if $SUDO xbps-install -Sy "$@"; then return 0; fi; fi
|
|
300
|
+
if cmd_exists emerge; then status "installing deps (emerge): $*"; if $SUDO emerge -q "$@"; then return 0; fi; fi
|
|
301
|
+
if cmd_exists opkg; then status "installing deps (opkg): $*"; if $SUDO opkg install "$@"; then return 0; fi; fi
|
|
302
|
+
if cmd_exists brew; then status "installing deps (brew): $*"; if brew install "$@"; then return 0; fi; fi
|
|
303
|
+
if cmd_exists port; then status "installing deps (port): $*"; if $SUDO port -N install "$@"; then return 0; fi; fi
|
|
304
|
+
if cmd_exists pkg_add; then status "installing deps (pkg_add): $*"; if $SUDO pkg_add "$@"; then return 0; fi; fi
|
|
305
|
+
if cmd_exists pacapt; then status "installing deps (pacapt): $*"; if $SUDO pacapt -S --noconfirm "$@"; then return 0; fi; fi
|
|
306
|
+
if cmd_exists choco; then status "installing deps (choco): $*"; if choco install -y "$@"; then return 0; fi; fi
|
|
307
|
+
if cmd_exists winget; then status "installing deps (winget): $*"; if winget install --silent "$@"; then return 0; fi; fi
|
|
308
|
+
return 1
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------
|
|
312
|
+
# Step 2 — dependencies (auto-install missing, skip existing)
|
|
313
|
+
# ------------------------------------------------------------
|
|
314
|
+
setup_deps() {
|
|
315
|
+
status "checking dependencies"
|
|
316
|
+
cmd_exists curl || cmd_exists wget \
|
|
317
|
+
|| pm_install curl \
|
|
318
|
+
|| fatal "missing required tool: curl" "Install curl (or wget) with your package manager, then rerun."
|
|
319
|
+
cmd_exists tar || pm_install tar \
|
|
320
|
+
|| fatal "missing required tool: tar" "Install tar with your package manager, then rerun."
|
|
321
|
+
local WANT="python3 ripgrep unzip git openssh" MISSING="" p
|
|
322
|
+
for p in $WANT; do
|
|
323
|
+
if [ "$p" = "openssh" ]; then
|
|
324
|
+
cmd_exists ssh || MISSING="$MISSING openssh"
|
|
325
|
+
else
|
|
326
|
+
cmd_exists "$p" || MISSING="$MISSING $p"
|
|
327
|
+
fi
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
if [ -n "$MISSING" ]; then
|
|
331
|
+
pm_install $MISSING || warn_deps "$MISSING"
|
|
332
|
+
else
|
|
333
|
+
status "dependencies ok (skip)"
|
|
334
|
+
fi
|
|
335
|
+
bump 10
|
|
336
|
+
}
|
|
337
|
+
warn_deps() {
|
|
338
|
+
printf "\r\033[K ${C_Y}!${C_N} could not auto-install:%s — continuing with what exists\n" "$1"
|
|
339
|
+
STATUS="dependencies partial"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# ------------------------------------------------------------
|
|
343
|
+
# Step 3 — core engine (delta aware)
|
|
344
|
+
# ------------------------------------------------------------
|
|
345
|
+
install_portable_node() { # last resort: official Node tarball into $LIB_DIR
|
|
346
|
+
local nver="v22.14.0" nos narch url
|
|
347
|
+
case "$ENV_KIND" in
|
|
348
|
+
macos) nos="darwin" ;;
|
|
349
|
+
linux|android|termux|posix) nos="linux" ;;
|
|
350
|
+
*) return 1 ;;
|
|
351
|
+
esac
|
|
352
|
+
case "$ARCH" in
|
|
353
|
+
aarch64) narch="arm64" ;;
|
|
354
|
+
x86_64) narch="x64" ;;
|
|
355
|
+
arm) narch="armv7l" ;;
|
|
356
|
+
ppc64le) narch="ppc64le" ;;
|
|
357
|
+
s390x) narch="s390x" ;;
|
|
358
|
+
*) return 1 ;;
|
|
359
|
+
esac
|
|
360
|
+
url="https://nodejs.org/dist/$nver/node-$nver-$nos-$narch.tar.gz"
|
|
361
|
+
status "downloading Node runtime ($narch)"
|
|
362
|
+
download_file "$url" "$TMP/node.tar.gz" "downloading node" || return 1
|
|
363
|
+
mkdir -p "$LIB_DIR/node"
|
|
364
|
+
tar -xzf "$TMP/node.tar.gz" -C "$LIB_DIR/node" --strip-components=1 >> "$LOG" 2>&1 || return 1
|
|
365
|
+
PATH="$LIB_DIR/node/bin:$PATH"; export PATH
|
|
366
|
+
cmd_exists npm
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
setup_core_npm() { # architecture-neutral fallback (32-bit ARM, x86, odd ROMs)
|
|
370
|
+
status "preparing Node core (no native build for $RAW_ARCH)"
|
|
371
|
+
if ! cmd_exists npm; then
|
|
372
|
+
if [ "$ENV_KIND" = "termux" ]; then
|
|
373
|
+
pm_install nodejs-lts || pm_install nodejs || true
|
|
374
|
+
else
|
|
375
|
+
pm_install nodejs npm || pm_install nodejs || pm_install node || true
|
|
376
|
+
fi
|
|
377
|
+
fi
|
|
378
|
+
cmd_exists npm || install_portable_node || true
|
|
379
|
+
if ! cmd_exists npm; then
|
|
380
|
+
fatal "could not install Node for $ENV_KIND/$RAW_ARCH" \
|
|
381
|
+
"Install Node.js manually (e.g. pkg install nodejs-lts / apt install nodejs npm), then rerun."
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
mkdir -p "$BIN_DIR" "$STAMP_DIR" "$LIB_DIR"
|
|
385
|
+
status "installing core (npm) — live output:"
|
|
386
|
+
if ! npm install -g opencode-ai; then
|
|
387
|
+
fatal "npm core install failed" "Check your internet, then rerun."
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
local target="" c nroot nprefix
|
|
391
|
+
nroot="$(npm root -g 2>/dev/null)"
|
|
392
|
+
nprefix="$(npm config get prefix 2>/dev/null)"
|
|
393
|
+
for c in \
|
|
394
|
+
"$(command -v opencode 2>/dev/null)" \
|
|
395
|
+
"$nprefix/bin/opencode" \
|
|
396
|
+
"$nprefix/opencode" \
|
|
397
|
+
"$nroot/opencode-ai/bin/opencode" \
|
|
398
|
+
"$nroot/opencode-ai/bin/opencode.exe" \
|
|
399
|
+
"$LIB_DIR/node/bin/opencode"
|
|
400
|
+
do
|
|
401
|
+
[ -n "$c" ] && [ -x "$c" ] && { target="$c"; break; }
|
|
402
|
+
done
|
|
403
|
+
[ -n "$target" ] || fatal "core binary missing after npm install" \
|
|
404
|
+
"Run: npm install -g opencode-ai and check its output."
|
|
405
|
+
|
|
406
|
+
# shim so every launcher path keeps working unchanged
|
|
407
|
+
printf '#!/usr/bin/env sh\nPATH="%s:$PATH"; export PATH\nexec "%s" "$@"\n' \
|
|
408
|
+
"$LIB_DIR/node/bin" "$target" > "$STAMP_DIR/opencode.bin"
|
|
409
|
+
chmod 755 "$STAMP_DIR/opencode.bin"
|
|
410
|
+
printf 'npm' > "$STAMP_DIR/blamcode-core-kind"
|
|
411
|
+
status "core installed (node build)"
|
|
412
|
+
bump 70
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
setup_core() {
|
|
416
|
+
if [ "${NATIVE_CORE:-1}" != "1" ]; then
|
|
417
|
+
setup_core_npm
|
|
418
|
+
return
|
|
419
|
+
fi
|
|
420
|
+
if [ "$ENV_KIND" = "termux" ]; then
|
|
421
|
+
local tag installed zip_url
|
|
422
|
+
status "checking core version"
|
|
423
|
+
tag="$(json_tag "$CORE_API/releases/latest")"
|
|
424
|
+
installed=""
|
|
425
|
+
[ -f "$STAMP_DIR/blamcode-core-version" ] && installed="$(tr -d '[:space:]' < "$STAMP_DIR/blamcode-core-version" 2>/dev/null)"
|
|
426
|
+
if [ -n "$tag" ] && [ "$tag" = "$installed" ] && [ -x "$STAMP_DIR/opencode.bin" ]; then
|
|
427
|
+
status "core up-to-date ($tag) — 0 MB"
|
|
428
|
+
bump 70
|
|
429
|
+
return
|
|
430
|
+
fi
|
|
431
|
+
[ -n "$installed" ] && status "core update: $installed → ${tag:-latest}"
|
|
432
|
+
|
|
433
|
+
if [ "$ARCH" = "x86_64" ]; then
|
|
434
|
+
zip_url="$(json_asset_url "$CORE_API/releases/latest" "android-x86_64")"
|
|
435
|
+
[ -n "$zip_url" ] || zip_url="$(json_asset_url "$CORE_API/releases/latest" "$AARCH64_MATCH")"
|
|
436
|
+
else
|
|
437
|
+
zip_url="$(json_asset_url "$CORE_API/releases/latest" "$AARCH64_MATCH")"
|
|
438
|
+
fi
|
|
439
|
+
if [ -z "$zip_url" ]; then
|
|
440
|
+
# release has no matching Android build — stay installable
|
|
441
|
+
setup_core_npm
|
|
442
|
+
return
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
if ! download_file "$zip_url" "$TMP/core.zip" "downloading core"; then
|
|
446
|
+
printf "\r\033[K ${C_Y}!${C_N} native core download failed — trying the Node build\n"
|
|
447
|
+
setup_core_npm
|
|
448
|
+
return
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# checksum when available
|
|
452
|
+
if curl -fsSL --retry 2 --connect-timeout 10 --max-time 30 -o "$TMP/SHA256SUMS" "${zip_url%/*}/SHA256SUMS" 2>/dev/null; then
|
|
453
|
+
local expected actual
|
|
454
|
+
expected="$(grep "$(basename "$zip_url")" "$TMP/SHA256SUMS" | awk '{print $1}' | head -n1)"
|
|
455
|
+
actual="$(sha256sum "$TMP/core.zip" | awk '{print $1}')"
|
|
456
|
+
if [ -n "$expected" ] && [ "$expected" != "$actual" ]; then
|
|
457
|
+
fatal "checksum mismatch — download corrupted" "Rerun the installer."
|
|
458
|
+
fi
|
|
459
|
+
status "integrity ok (SHA256)"
|
|
460
|
+
fi
|
|
461
|
+
bump 68
|
|
462
|
+
run_bg "extracting core" bash -c "cd '$TMP' && unzip -oq core.zip" \
|
|
463
|
+
|| fatal "couldn't extract the core zip" "Rerun the installer."
|
|
464
|
+
[ -f "$TMP/opencode.bin" ] || [ -f "$TMP/opencode" ] \
|
|
465
|
+
|| fatal "core zip has no binary" "The release format changed — report it."
|
|
466
|
+
|
|
467
|
+
mkdir -p "$BIN_DIR" "$STAMP_DIR" "$LIB_DIR"
|
|
468
|
+
if [ -f "$TMP/opencode.bin" ]; then install -m755 "$TMP/opencode.bin" "$STAMP_DIR/opencode.bin"
|
|
469
|
+
else install -m755 "$TMP/opencode" "$STAMP_DIR/opencode.bin"; fi
|
|
470
|
+
for lib in libtagfix.so libopentui.so libc++_shared.so librust_pty_arm64.so; do
|
|
471
|
+
[ -f "$TMP/$lib" ] && install -m644 "$TMP/$lib" "$LIB_DIR/$lib" 2>/dev/null || true
|
|
472
|
+
done
|
|
473
|
+
[ -e "$PREFIX/bin/opencode" ] && mv "$PREFIX/bin/opencode" "$PREFIX/bin/opencode.bak" 2>/dev/null || true
|
|
474
|
+
# last safety net: some 32-bit/older kernels cannot exec the native
|
|
475
|
+
# binary even when the CPU name looked right — fall back silently.
|
|
476
|
+
if ! "$STAMP_DIR/opencode.bin" --version >/dev/null 2>&1; then
|
|
477
|
+
printf "\r\033[K ${C_Y}!${C_N} native core cannot run on this device — using the Node build\n"
|
|
478
|
+
setup_core_npm
|
|
479
|
+
return
|
|
480
|
+
fi
|
|
481
|
+
[ -n "$tag" ] && printf '%s' "$tag" > "$STAMP_DIR/blamcode-core-version"
|
|
482
|
+
printf 'native' > "$STAMP_DIR/blamcode-core-kind"
|
|
483
|
+
status "core installed"
|
|
484
|
+
bump 70
|
|
485
|
+
else
|
|
486
|
+
local tag installed check
|
|
487
|
+
tag="$(json_tag "$API/repos/$GH_REPO/releases/latest" 2>/dev/null)"
|
|
488
|
+
installed=""
|
|
489
|
+
[ -f "$STAMP_DIR/blamcode-core-version" ] && installed="$(tr -d '[:space:]' < "$STAMP_DIR/blamcode-core-version" 2>/dev/null)"
|
|
490
|
+
check="$HOME/.opencode/bin/opencode"
|
|
491
|
+
if [ -n "$tag" ] && [ "$tag" = "$installed" ] && [ -x "$check" ]; then
|
|
492
|
+
status "core up-to-date ($tag) — 0 MB"
|
|
493
|
+
bump 70
|
|
494
|
+
return
|
|
495
|
+
fi
|
|
496
|
+
status "installing official core — live output:"
|
|
497
|
+
if ! bash -c \
|
|
498
|
+
"curl -fsSL --retry 3 --connect-timeout 15 --max-time 120 -o '$TMP/oc-install.sh' '$OPENCODE_INSTALL_URL' && bash '$TMP/oc-install.sh'"; then
|
|
499
|
+
printf "\r\033[K ${C_Y}!${C_N} official core installer failed — using the Node build\n"
|
|
500
|
+
setup_core_npm
|
|
501
|
+
return
|
|
502
|
+
fi
|
|
503
|
+
if [ ! -x "$check" ]; then
|
|
504
|
+
setup_core_npm
|
|
505
|
+
return
|
|
506
|
+
fi
|
|
507
|
+
cp "$check" "$STAMP_DIR/opencode.bin"; chmod 755 "$STAMP_DIR/opencode.bin"
|
|
508
|
+
if ! "$STAMP_DIR/opencode.bin" --version >/dev/null 2>&1; then
|
|
509
|
+
setup_core_npm
|
|
510
|
+
return
|
|
511
|
+
fi
|
|
512
|
+
[ -n "$tag" ] && printf '%s' "$tag" > "$STAMP_DIR/blamcode-core-version"
|
|
513
|
+
status "core installed (official)"
|
|
514
|
+
bump 70
|
|
515
|
+
fi
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# ------------------------------------------------------------
|
|
519
|
+
# Step 4 — BLAMCODE layer (wrapper, config, skills, commands)
|
|
520
|
+
# ------------------------------------------------------------
|
|
521
|
+
setup_layer() {
|
|
522
|
+
# pip/bootstrap route: layer already on disk — skip network fetch
|
|
523
|
+
if [ -n "$BLAMCODE_BOOT" ] && [ -d "$BLAMCODE_BOOT/scripts" ] && [ -d "$BLAMCODE_BOOT/config" ]; then
|
|
524
|
+
status "blamcode layer ready (pip bootstrap)"
|
|
525
|
+
rm -rf "$TMP"/* 2>/dev/null || true
|
|
526
|
+
cp -a "$BLAMCODE_BOOT/." "$TMP/"
|
|
527
|
+
bump 75
|
|
528
|
+
else
|
|
529
|
+
status "downloading blamcode layer"
|
|
530
|
+
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 --max-time 120 \
|
|
531
|
+
-o "$TMP/layer.tar.gz" "https://codeload.github.com/$GH_REPO/tar.gz/refs/heads/main" 2>/dev/null; then
|
|
532
|
+
tar -xzf "$TMP/layer.tar.gz" -C "$TMP" --strip-components=1 2>/dev/null \
|
|
533
|
+
&& [ -d "$TMP/config" ] && [ -d "$TMP/skills" ] || { rm -rf "$TMP"/* 2>/dev/null || true; }
|
|
534
|
+
fi
|
|
535
|
+
if [ ! -d "$TMP/config" ]; then
|
|
536
|
+
run_bg "fetching blamcode layer (git)" bash -c \
|
|
537
|
+
"git clone --depth 1 -q 'https://github.com/$GH_REPO.git' '$TMP/src' && mv '$TMP/src'/* '$TMP/' && rm -rf '$TMP/src'" \
|
|
538
|
+
|| fatal "couldn't download the BLAMCODE layer" "Check your internet and rerun."
|
|
539
|
+
fi
|
|
540
|
+
bump 75
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# wrapper + helpers
|
|
544
|
+
install_script "$TMP/scripts/blamcode" "$BIN_DIR/blamcode"
|
|
545
|
+
[ -f "$TMP/scripts/blamcode-menu" ] && install_script "$TMP/scripts/blamcode-menu" "$BIN_DIR/blamcode-menu"
|
|
546
|
+
[ -f "$TMP/scripts/blamcode-browser" ] && install_script "$TMP/scripts/blamcode-browser" "$BIN_DIR/blamcode-browser"
|
|
547
|
+
[ -f "$TMP/scripts/blamcode-vision" ] && install_script "$TMP/scripts/blamcode-vision" "$BIN_DIR/blamcode-vision"
|
|
548
|
+
[ -f "$TMP/scripts/blamcode-uninstall" ] && install_script "$TMP/scripts/blamcode-uninstall" "$BIN_DIR/blamcode-uninstall"
|
|
549
|
+
[ -f "$TMP/scripts/oc-settings.sh" ] && install_script "$TMP/scripts/oc-settings.sh" "$BIN_DIR/oc-settings"
|
|
550
|
+
[ -f "$TMP/scripts/patch-brand.py" ] && install -m644 "$TMP/scripts/patch-brand.py" "$LIB_DIR/patch-brand.py"
|
|
551
|
+
|
|
552
|
+
# config + skills (merge, never clobber)
|
|
553
|
+
local CONFIG_DIR="$HOME/.config/opencode"
|
|
554
|
+
mkdir -p "$CONFIG_DIR/agent" "$CONFIG_DIR/command" "$CONFIG_DIR/themes" "$CONFIG_DIR/skills"
|
|
555
|
+
if sys_has_python; then
|
|
556
|
+
if [ -f "$CONFIG_DIR/opencode.json" ]; then
|
|
557
|
+
cp -f "$TMP/config/opencode.json" "$TMP/opencode.json.new" 2>/dev/null || true
|
|
558
|
+
merge_json "$CONFIG_DIR/opencode.json" "$TMP/opencode.json.new"
|
|
559
|
+
rm -f "$TMP/opencode.json.new"
|
|
560
|
+
else
|
|
561
|
+
cp -f "$TMP/config/opencode.json" "$CONFIG_DIR/opencode.json"
|
|
562
|
+
fi
|
|
563
|
+
else
|
|
564
|
+
[ -f "$CONFIG_DIR/opencode.json" ] || cp -f "$TMP/config/opencode.json" "$CONFIG_DIR/opencode.json"
|
|
565
|
+
fi
|
|
566
|
+
# also strip stale model keys from opencode.jsonc (oc-settings writes both)
|
|
567
|
+
if sys_has_python && [ -f "$CONFIG_DIR/opencode.jsonc" ]; then
|
|
568
|
+
if cmd_exists python3; then _py=python3; else _py=python; fi
|
|
569
|
+
"$_py" - "$CONFIG_DIR/opencode.jsonc" <<'PYC' 2>/dev/null || true
|
|
570
|
+
import json, sys
|
|
571
|
+
p = sys.argv[1]
|
|
572
|
+
try:
|
|
573
|
+
d = json.load(open(p))
|
|
574
|
+
except Exception:
|
|
575
|
+
sys.exit(0)
|
|
576
|
+
c = False
|
|
577
|
+
for k in ("model", "small_model"):
|
|
578
|
+
if k in d:
|
|
579
|
+
del d[k]; c = True
|
|
580
|
+
a = d.get("agent")
|
|
581
|
+
if isinstance(a, dict) and isinstance(a.get("build"), dict) and "model" in a["build"]:
|
|
582
|
+
del a["build"]["model"]; c = True
|
|
583
|
+
if c:
|
|
584
|
+
json.dump(d, open(p, "w"), indent=2)
|
|
585
|
+
PYC
|
|
586
|
+
fi
|
|
587
|
+
[ -f "$TMP/config/tui.json" ] && cp -f "$TMP/config/tui.json" "$CONFIG_DIR/tui.json" 2>/dev/null || true
|
|
588
|
+
cp -f "$TMP"/config/agent/*.md "$CONFIG_DIR/agent/" 2>/dev/null || true
|
|
589
|
+
cp -f "$TMP"/config/command/*.md "$CONFIG_DIR/command/" 2>/dev/null || true
|
|
590
|
+
cp -f "$TMP"/config/themes/*.json "$CONFIG_DIR/themes/" 2>/dev/null || true
|
|
591
|
+
cp -rf "$TMP"/skills/* "$CONFIG_DIR/skills/" 2>/dev/null || true
|
|
592
|
+
|
|
593
|
+
status "config + skills installed"
|
|
594
|
+
bump 85
|
|
595
|
+
|
|
596
|
+
# rc file: PATH + provider key
|
|
597
|
+
local rc="$HOME/.bashrc"
|
|
598
|
+
[ "$ENV_KIND" != "termux" ] && [ ! -f "$HOME/.bashrc" ] && [ -f "$HOME/.profile" ] && rc="$HOME/.profile"
|
|
599
|
+
if ! grep -q 'BLAMCODE PATH' "$rc" 2>/dev/null; then
|
|
600
|
+
printf '# BLAMCODE PATH\ncase ":$PATH:" in *":%s:"*) ;; *) export PATH="%s:$PATH";; esac\n' \
|
|
601
|
+
"$BIN_DIR" "$BIN_DIR" >> "$rc"
|
|
602
|
+
fi
|
|
603
|
+
if ! grep -q "OPENCODE_API_KEY" "$rc" 2>/dev/null; then
|
|
604
|
+
printf '\n# BLAMCODE AI\nexport OPENCODE_API_KEY="%s"\n' "$ZEN_KEY_DEFAULT" >> "$rc"
|
|
605
|
+
fi
|
|
606
|
+
status "provider + PATH ready"
|
|
607
|
+
bump 95
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
install_script() { # $1 src $2 dest — shebang rewritten for non-Termux
|
|
611
|
+
if [ "$ENV_KIND" = "termux" ]; then
|
|
612
|
+
install -m755 "$1" "$2"
|
|
613
|
+
else
|
|
614
|
+
sed '1s|^#!.*|#!/usr/bin/env bash|' "$1" > "$2" && chmod 755 "$2"
|
|
615
|
+
fi
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
brand_logo() { # put the BLAMCODE brand logo into the core binary
|
|
619
|
+
# patch-brand.py replaces the "OPENCODE" pixel-art wordmark with the
|
|
620
|
+
# "BLAMCODE" design, same byte length (layout stays intact — no crash),
|
|
621
|
+
# on every install/update. Runs after the layer is on disk (the
|
|
622
|
+
# script ships in the layer). Idempotent — safe on already-branded
|
|
623
|
+
# binaries (missing rows are skipped).
|
|
624
|
+
[ -f "$LIB_DIR/patch-brand.py" ] || return 0
|
|
625
|
+
[ -f "$STAMP_DIR/opencode.bin" ] || return 0
|
|
626
|
+
sys_has_python || return 0
|
|
627
|
+
local py
|
|
628
|
+
if cmd_exists python3; then py=python3; else py=python; fi
|
|
629
|
+
status "branding logo (BLAMCODE)"
|
|
630
|
+
if ! run "$py" "$LIB_DIR/patch-brand.py" "$STAMP_DIR/opencode.bin" "$STAMP_DIR/opencode.bin"; then
|
|
631
|
+
log "brand_logo: failed (ignored — binary untouched)"
|
|
632
|
+
fi
|
|
633
|
+
# an older release could blank the art beyond in-place repair; when
|
|
634
|
+
# no known logo state remains, pull a fresh core once and brand that
|
|
635
|
+
if ! run "$py" "$LIB_DIR/patch-brand.py" "$STAMP_DIR/opencode.bin" /dev/null --verify; then
|
|
636
|
+
status "logo unrecoverable — refreshing core (one-time)"
|
|
637
|
+
rm -f "$STAMP_DIR/blamcode-core-version"
|
|
638
|
+
if ! setup_core; then
|
|
639
|
+
log "core refresh failed — keeping the current binary"
|
|
640
|
+
return 0
|
|
641
|
+
fi
|
|
642
|
+
run "$py" "$LIB_DIR/patch-brand.py" "$STAMP_DIR/opencode.bin" "$STAMP_DIR/opencode.bin" \
|
|
643
|
+
|| log "brand_logo: failed after refresh (ignored)"
|
|
644
|
+
fi
|
|
645
|
+
status "brand logo applied"
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
merge_json() { # $1 user config $2 repo config — merge with force-keys
|
|
649
|
+
local py
|
|
650
|
+
if cmd_exists python3; then py=python3; else py=python; fi
|
|
651
|
+
"$py" - "$1" "$2" <<'PY'
|
|
652
|
+
import json, sys
|
|
653
|
+
user_path, repo_path = sys.argv[1], sys.argv[2]
|
|
654
|
+
try:
|
|
655
|
+
user = json.load(open(user_path))
|
|
656
|
+
except Exception:
|
|
657
|
+
user = {}
|
|
658
|
+
try:
|
|
659
|
+
repo = json.load(open(repo_path))
|
|
660
|
+
except Exception:
|
|
661
|
+
repo = {}
|
|
662
|
+
# these always come from the repo — BLAMCODE's core defaults must win
|
|
663
|
+
# (provider, permissions), otherwise an old install keeps stale values
|
|
664
|
+
FORCE = {"provider", "permission", "agent"}
|
|
665
|
+
for k, v in repo.items():
|
|
666
|
+
if k in FORCE or k not in user:
|
|
667
|
+
user[k] = v
|
|
668
|
+
# model defaults are intentionally NOT forced — BLAMCODE uses whatever model
|
|
669
|
+
# the user picked (opencode's own default). Remove stale deepseek keys
|
|
670
|
+
# left by older versions so the picker falls back to the stock default.
|
|
671
|
+
for k in ("model", "small_model"):
|
|
672
|
+
user.pop(k, None)
|
|
673
|
+
json.dump(user, open(user_path, "w"), indent=2)
|
|
674
|
+
PY
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
# ------------------------------------------------------------
|
|
678
|
+
# Step 5 — verify
|
|
679
|
+
# ------------------------------------------------------------
|
|
680
|
+
verify_install() {
|
|
681
|
+
status "verifying"
|
|
682
|
+
bump 97
|
|
683
|
+
if [ ! -x "$BIN_DIR/blamcode" ]; then
|
|
684
|
+
fatal "wrapper missing at $BIN_DIR/blamcode" "Rerun the installer."
|
|
685
|
+
fi
|
|
686
|
+
if [ -f "$STAMP_DIR/opencode.bin" ]; then
|
|
687
|
+
status "verifying core"
|
|
688
|
+
else
|
|
689
|
+
fatal "core binary missing at $STAMP_DIR/opencode.bin" "Rerun the installer."
|
|
690
|
+
fi
|
|
691
|
+
VERSION="$("$BIN_DIR/blamcode" --version 2>&1 || true)"
|
|
692
|
+
bump 99
|
|
693
|
+
STATUS="verify ok"
|
|
694
|
+
draw
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
# ------------------------------------------------------------
|
|
698
|
+
# entry
|
|
699
|
+
# ------------------------------------------------------------
|
|
700
|
+
if [ "${1:-}" = "uninstall" ]; then
|
|
701
|
+
shift # drop "uninstall"; remaining args pass through (POSIX)
|
|
702
|
+
if [ -n "$BLAMCODE_BOOT" ] && [ -f "$BLAMCODE_BOOT/scripts/blamcode-uninstall" ]; then
|
|
703
|
+
exec sh "$BLAMCODE_BOOT/scripts/blamcode-uninstall" "$@"
|
|
704
|
+
fi
|
|
705
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo "$PWD")"
|
|
706
|
+
if [ -f "$SCRIPT_DIR/scripts/blamcode-uninstall" ]; then
|
|
707
|
+
exec sh "$SCRIPT_DIR/scripts/blamcode-uninstall" "$@"
|
|
708
|
+
fi
|
|
709
|
+
curl -fsSL --retry 2 --connect-timeout 15 --max-time 60 \
|
|
710
|
+
-o "$TMP/blamcode-uninstall" "https://raw.githubusercontent.com/$GH_REPO/main/scripts/blamcode-uninstall" \
|
|
711
|
+
&& exec sh "$TMP/blamcode-uninstall" "$@"
|
|
712
|
+
echo "couldn't fetch the uninstaller" >&2
|
|
713
|
+
exit 1
|
|
714
|
+
fi
|
|
715
|
+
|
|
716
|
+
setup_env
|
|
717
|
+
setup_deps
|
|
718
|
+
setup_core
|
|
719
|
+
setup_layer
|
|
720
|
+
brand_logo
|
|
721
|
+
verify_install
|
|
722
|
+
|
|
723
|
+
# ---- done ----
|
|
724
|
+
PROG=100; STATUS="done ✔"
|
|
725
|
+
draw
|
|
726
|
+
printf "\n\n"
|
|
727
|
+
printf " ${C_C}${C_B}██████╗ ██╗ █████╗ ███╗ ███╗${C_N}\n"
|
|
728
|
+
printf " ${C_C}${C_B}██╔══██╗ ██║ ██╔══██╗ ████╗ ████║${C_N}\n"
|
|
729
|
+
printf " ${C_C}${C_B}██████╔╝ ██║ ███████║ ██╔████╔██║${C_N}\n"
|
|
730
|
+
printf " ${C_C}${C_B}██╔══██╗ ██║ ██╔══██║ ██║╚██╔╝██║${C_N}\n"
|
|
731
|
+
printf " ${C_C}${C_B}██████╔╝ ███████╗ ██║ ██║ ██║ ╚═╝ ██║${C_N}\n"
|
|
732
|
+
printf " ${C_C}${C_B}╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝${C_N}\n"
|
|
733
|
+
printf " ${C_C}${C_B}${C_N}\n"
|
|
734
|
+
printf " ${C_C}${C_B} ██████╗ ██████╗ ██████╗ ███████╗${C_N}\n"
|
|
735
|
+
printf " ${C_C}${C_B}██╔════╝ ██╔═══██╗ ██╔══██╗ ██╔════╝${C_N}\n"
|
|
736
|
+
printf " ${C_C}${C_B}██║ ██║ ██║ ██║ ██║ █████╗${C_N}\n"
|
|
737
|
+
printf " ${C_C}${C_B}██║ ██║ ██║ ██║ ██║ ██╔══╝${C_N}\n"
|
|
738
|
+
printf " ${C_C}${C_B}╚██████╗ ╚██████╔╝ ██████╔╝ ███████╗${C_N}\n"
|
|
739
|
+
printf " ${C_C}${C_B} ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${C_N}\n"
|
|
740
|
+
printf "\n"
|
|
741
|
+
printf " ${C_G}${C_B}⚡ BLAMCODE READY${C_N} ${C_D}%s · %s · install time ~%ss${C_N}\n" "$ENV_KIND" "$ARCH" "$SECONDS"
|
|
742
|
+
printf " ${C_B}blamcode${C_N} start the AI (full-power)\n"
|
|
743
|
+
printf " ${C_B}blamcode preview${C_N} open a page in the browser\n"
|
|
744
|
+
printf " ${C_B}blamcode update${C_N} delta update\n"
|
|
745
|
+
printf " ${C_B}blamcode session${C_N} session menu (↑↓ to pick)\n"
|
|
746
|
+
printf " ${C_B}blamcode doctor${C_N} health check\n"
|
|
747
|
+
printf " ${C_B}blamcode backup${C_N} backup config + projects\n"
|
|
748
|
+
printf " ${C_B}blamcode uninstall${C_N} remove BLAMCODE (keeps projects)\n"
|
|
749
|
+
if [ -n "$BLAMCODE_BOOT" ]; then
|
|
750
|
+
printf " ${C_D}installed via pip — update layer with:${C_N} ${C_B}blamcode install${C_N}\n"
|
|
751
|
+
fi
|
|
752
|
+
printf "\n ${C_D}open a new shell, then run:${C_N} ${C_B}blamcode${C_N}\n\n"
|