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.
Files changed (58) hide show
  1. blamcode/__init__.py +9 -0
  2. blamcode/cli.py +122 -0
  3. blamcode/layer/README.md +43 -0
  4. blamcode/layer/config/agent/build.md +118 -0
  5. blamcode/layer/config/agent/debugger.md +32 -0
  6. blamcode/layer/config/agent/designer.md +31 -0
  7. blamcode/layer/config/agent/vision.md +26 -0
  8. blamcode/layer/config/agent/writer.md +28 -0
  9. blamcode/layer/config/command/ask.md +41 -0
  10. blamcode/layer/config/command/blam.md +24 -0
  11. blamcode/layer/config/command/explore.md +14 -0
  12. blamcode/layer/config/command/fix.md +14 -0
  13. blamcode/layer/config/command/open.md +18 -0
  14. blamcode/layer/config/command/review.md +14 -0
  15. blamcode/layer/config/opencode.json +53 -0
  16. blamcode/layer/config/themes/bangladeshi.json +67 -0
  17. blamcode/layer/config/themes/blamcode.json +217 -0
  18. blamcode/layer/config/tui.json +4 -0
  19. blamcode/layer/install.sh +752 -0
  20. blamcode/layer/scripts/__pycache__/patch-brand.cpython-312.pyc +0 -0
  21. blamcode/layer/scripts/blamcode +518 -0
  22. blamcode/layer/scripts/blamcode-browser +315 -0
  23. blamcode/layer/scripts/blamcode-menu +140 -0
  24. blamcode/layer/scripts/blamcode-uninstall +71 -0
  25. blamcode/layer/scripts/blamcode-vision +542 -0
  26. blamcode/layer/scripts/oc-settings.sh +138 -0
  27. blamcode/layer/scripts/patch-brand.py +267 -0
  28. blamcode/layer/skills/android-app/SKILL.md +53 -0
  29. blamcode/layer/skills/api-integration/SKILL.md +49 -0
  30. blamcode/layer/skills/bash-cli-expert/SKILL.md +48 -0
  31. blamcode/layer/skills/bot-development/SKILL.md +53 -0
  32. blamcode/layer/skills/clean-code-performance/SKILL.md +45 -0
  33. blamcode/layer/skills/database/SKILL.md +56 -0
  34. blamcode/layer/skills/debugging-fixes/SKILL.md +45 -0
  35. blamcode/layer/skills/deploy-hosting/SKILL.md +38 -0
  36. blamcode/layer/skills/docker/SKILL.md +74 -0
  37. blamcode/layer/skills/firebase-supabase/SKILL.md +61 -0
  38. blamcode/layer/skills/git-workflow/SKILL.md +63 -0
  39. blamcode/layer/skills/lets-scroll/SKILL.md +877 -0
  40. blamcode/layer/skills/lets-scroll/references/index-template.html +73 -0
  41. blamcode/layer/skills/lets-scroll/references/knockout.py +89 -0
  42. blamcode/layer/skills/lets-scroll/references/pipeline.md +312 -0
  43. blamcode/layer/skills/lets-scroll/references/prompts.md +194 -0
  44. blamcode/layer/skills/lets-scroll/references/scrub-engine.js +448 -0
  45. blamcode/layer/skills/project-structure/SKILL.md +74 -0
  46. blamcode/layer/skills/python-automation/SKILL.md +52 -0
  47. blamcode/layer/skills/react-next-best-practices/SKILL.md +54 -0
  48. blamcode/layer/skills/security-review/SKILL.md +48 -0
  49. blamcode/layer/skills/seo-basics/SKILL.md +44 -0
  50. blamcode/layer/skills/testing/SKILL.md +58 -0
  51. blamcode/layer/skills/ui-ux-responsive/SKILL.md +53 -0
  52. blamcode/layer/skills/website-builder/SKILL.md +47 -0
  53. blamcode-0.4.0.dist-info/METADATA +62 -0
  54. blamcode-0.4.0.dist-info/RECORD +58 -0
  55. blamcode-0.4.0.dist-info/WHEEL +5 -0
  56. blamcode-0.4.0.dist-info/entry_points.txt +2 -0
  57. blamcode-0.4.0.dist-info/licenses/LICENSE +21 -0
  58. 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"