@polderlabs/bizar 4.3.0 → 4.4.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.
Files changed (57) hide show
  1. package/bizar-dash/src/server/opencode-sdk.mjs +72 -0
  2. package/bizar-dash/src/server/routes/background.mjs +92 -0
  3. package/bizar-dash/src/server/routes/chat.mjs +300 -123
  4. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +26 -0
  5. package/bizar-dash/src/server/routes/tasks.mjs +59 -1
  6. package/bizar-dash/src/server/task-delegator.mjs +154 -8
  7. package/bizar-dash/src/server/tasks-store.mjs +50 -2
  8. package/bizar-dash/src/web/components/background/AttachButton.tsx +96 -0
  9. package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +122 -0
  10. package/bizar-dash/src/web/components/chat/AgentNode.tsx +127 -0
  11. package/bizar-dash/src/web/components/chat/AgentTree.tsx +80 -0
  12. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +76 -0
  13. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +144 -0
  14. package/bizar-dash/src/web/components/chat/ChatRail.tsx +387 -0
  15. package/bizar-dash/src/web/components/chat/ChatThread.tsx +28 -7
  16. package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +58 -0
  17. package/bizar-dash/src/web/components/chat/MessageBlock.tsx +260 -0
  18. package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +206 -0
  19. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +33 -5
  20. package/bizar-dash/src/web/components/chat/_legacy.ts +30 -0
  21. package/bizar-dash/src/web/components/chat/index.ts +11 -0
  22. package/bizar-dash/src/web/components/chat/useChat.ts +345 -167
  23. package/bizar-dash/src/web/components/tasks/BacklogPanel.css +109 -0
  24. package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +209 -0
  25. package/bizar-dash/src/web/styles/chat.css +1536 -133
  26. package/bizar-dash/src/web/views/BackgroundAgents.tsx +3 -0
  27. package/bizar-dash/src/web/views/Chat.tsx +147 -57
  28. package/bizar-dash/src/web/views/Tasks.tsx +23 -1
  29. package/cli/bg.mjs +94 -71
  30. package/cli/bin.mjs +19 -7
  31. package/cli/service-controller.mjs +587 -0
  32. package/cli/service-controller.test.mjs +92 -0
  33. package/cli/service.mjs +162 -14
  34. package/config/agents/baldr.md +2 -0
  35. package/config/agents/browser-harness.md +2 -0
  36. package/config/agents/forseti.md +2 -0
  37. package/config/agents/frigg.md +2 -0
  38. package/config/agents/heimdall.md +2 -0
  39. package/config/agents/hermod.md +2 -0
  40. package/config/agents/mimir.md +2 -0
  41. package/config/agents/odin.md +2 -0
  42. package/config/agents/quick.md +2 -0
  43. package/config/agents/semble-search.md +2 -0
  44. package/config/agents/thor.md +2 -0
  45. package/config/agents/tyr.md +2 -0
  46. package/config/agents/vidarr.md +2 -0
  47. package/config/agents/vor.md +2 -0
  48. package/config/opencode.json.template +1 -0
  49. package/install.sh +448 -787
  50. package/package.json +1 -1
  51. package/packages/sdk/package.json +20 -0
  52. package/packages/sdk/src/client.ts +5 -0
  53. package/packages/sdk/src/errors.ts +11 -2
  54. package/packages/sdk/src/index.ts +19 -0
  55. package/packages/sdk/src/opencode-events.ts +134 -0
  56. package/packages/sdk/src/opencode-types.ts +66 -0
  57. package/packages/sdk/src/opencode.ts +335 -0
package/install.sh CHANGED
@@ -1,47 +1,30 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # install.sh — One-shot BizarHarness installer.
3
+ # install.sh — Cross-platform BizarHarness installer.
4
4
  #
5
- # v4.0.0 — Single-package layout. Dashboard, plugin, and SDK all ship
6
- # inside @polderlabs/bizar. No more separate npm packages.
5
+ # v3.22.0 — Rewritten for cross-platform dispatch. Linux (apt/dnf/pacman/zypper),
6
+ # macOS (brew), and a pointer to install.ps1 for Windows.
7
7
  #
8
- # v3.20.11 — Comprehensive auto-installer with full dependency
9
- # resolution. Replaces the older "print 4 manual next steps" flow
10
- # with: (1) install every system dep we can reasonably fetch
11
- # (uv, python3.12, chrome-headless-shell, jq, Chrome runtime libs);
12
- # (2) install browser-harness + register its skill; (3) start Chrome
13
- # for browser-driven E2E; (4) install + configure all BizarHarness
14
- # files (agents, commands, opencode.json, plugin); (5) print a
15
- # single status banner that tells the operator what is ready and
16
- # what (if anything) they still need to do.
17
- #
18
- # Idempotent — every step checks for the existing install and
19
- # skips re-work. Re-running after a failed install picks up
20
- # where it left off. Same script can run from a fresh `git clone`
21
- # or via `bizar install` (which spawns this script under bash).
8
+ # Features:
9
+ # - Dependency detection via node scripts/check-deps.mjs
10
+ # - Auto-install missing deps for the current platform/distro
11
+ # - Idempotent every step checks before acting
12
+ # - --update: git pull --ff-only, re-install config, re-install service
13
+ # - --dry-run: print what would happen, do nothing
14
+ # - --non-interactive: skip all prompts (CI safe)
15
+ # - --force: overwrite files even when they match
16
+ # - Plugin registration verification in ~/.config/opencode/opencode.json
17
+ # - Service registration via node scripts/install-service.mjs
22
18
  #
23
19
  # What this script does NOT do (intentional):
24
- # - Configure provider API keys (MINIMAX_API_KEY, etc.). The user
25
- # does that via `/connect` in opencode after install. We deliberately
26
- # do not collect secrets here — the install is idempotent and safe
27
- # to re-run, and asking for keys during install breaks CI / scripted
28
- # deployments.
29
- # - Reload opencode. The opencode session picks up the new config on
30
- # next start. We print a status line at the end; we do not kill or
31
- # restart the opencode process from here (it would interrupt
32
- # in-flight agent sessions).
33
- # - Hide `.obsidian/`. The Obsidian vault is a first-class part of
34
- # the project tree. We do not add it to any .gitignore / .npmignore
35
- # / IDE-exclusion list.
36
- #
37
- # Idempotent: every step checks for the existing install and skips
38
- # re-work. Re-running after a failed install picks up where it left off.
39
- #
40
- # Cross-platform: bash + curl work on Linux + macOS. On Windows run
41
- # `npm install -g @polderlabs/bizar` which triggers the npm postinstall
42
- # hook (cli/install.mjs → install.sh via git bash).
20
+ # - Configure provider API keys. The user does that via /connect.
21
+ # - Reload opencode. The next opencode session picks up new config.
22
+ # - Windows installation (see install.ps1 for that).
23
+
43
24
  set -euo pipefail
44
25
 
26
+ # ── Constants ──────────────────────────────────────────────────────────────────
27
+
45
28
  BOLD='\033[1m'
46
29
  GREEN='\033[0;32m'
47
30
  YELLOW='\033[1;33m'
@@ -52,824 +35,502 @@ NC='\033[0m'
52
35
 
53
36
  REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
54
37
  CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
55
- SKILLS_DIR="$HOME/.opencode/skills"
56
- BIZAR_STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/bizar"
57
- BIZAR_STATE_FILE="$BIZAR_STATE_DIR/install-state.json"
58
- PYTHON_BIN="${PYTHON_BIN:-python3.12}"
59
- CHROME_PORT="${CHROME_PORT:-9222}"
60
-
61
- # Counters for the final summary.
62
- INSTALLED=()
63
- SKIPPED=()
64
- FAILED=()
65
-
66
- note() {
67
- echo -e " ${GREEN}✓${NC} $1"
68
- INSTALLED+=("$1")
69
- }
70
- warn() {
71
- echo -e " ${YELLOW}⚠${NC} $1"
72
- SKIPPED+=("$1")
73
- }
74
- err() {
75
- echo -e " ${RED}✗${NC} $1"
76
- FAILED+=("$1")
77
- }
78
- section() {
38
+ PLUGIN_DIR="$CONFIG_DIR/plugins/bizar"
39
+
40
+ # Flags parsed from CLI args
41
+ DRY_RUN=false
42
+ NON_INTERACTIVE=false
43
+ FORCE=false
44
+ UPDATE_MODE=false
45
+
46
+ # ── UI helpers ─────────────────────────────────────────────────────────────────
47
+
48
+ note() { echo -e " ${GREEN}✓${NC} $1"; }
49
+ warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
50
+ err() { echo -e " ${RED}✗${NC} $1"; }
51
+ action(){ echo -e " ${CYAN}→${NC} $1"; }
52
+ dim() { echo -e " ${DIM}$1${NC}"; }
53
+ section(){
79
54
  echo ""
80
55
  echo -e "${BOLD}${CYAN}── $1 ──${NC}"
81
56
  }
82
57
 
83
- # ── verify_dependencies: final check that all required tools are on PATH ─
84
- verify_dependencies() {
85
- local missing=()
86
- for cmd in node uv uvx bun jq sqlite3 gh opencode python3 make g++; do
87
- if ! command -v "$cmd" &>/dev/null; then
88
- missing+=("$cmd")
89
- fi
58
+ have_cmd() { command -v "$1" >/dev/null 2>&1; }
59
+
60
+ # ── Usage ──────────────────────────────────────────────────────────────────────
61
+
62
+ show_usage() {
63
+ cat <<'EOF'
64
+
65
+ install.sh — BizarHarness cross-platform installer
66
+
67
+ Usage:
68
+ ./install.sh Interactive install (Linux/macOS)
69
+ ./install.sh --help Show this help
70
+ ./install.sh --dry-run Dry run — print actions, no changes
71
+ ./install.sh --non-interactive Non-interactive (CI safe)
72
+ ./install.sh --force Overwrite existing files
73
+ ./install.sh --update Pull latest + re-install config + service
74
+
75
+ On Windows, run install.ps1 instead.
76
+
77
+ EOF
78
+ }
79
+
80
+ # ── Parse CLI flags ────────────────────────────────────────────────────────────
81
+
82
+ parse_flags() {
83
+ for arg in "$@"; do
84
+ case "$arg" in
85
+ --help|-h) show_usage; exit 0 ;;
86
+ --dry-run) DRY_RUN=true ;;
87
+ --non-interactive) NON_INTERACTIVE=true ;;
88
+ --force) FORCE=true ;;
89
+ --update) UPDATE_MODE=true ;;
90
+ *) warn "Unknown flag: $arg"; show_usage; exit 1 ;;
91
+ esac
90
92
  done
91
- if [ ${#missing[@]} -gt 0 ]; then
92
- echo -e " ${RED}✗${NC} missing required commands: ${missing[*]}"
93
- return 1
94
- fi
95
- return 0
96
93
  }
97
94
 
98
- # ── Pre-flight: which tools are missing? ───────────────────────────────
99
- section "Pre-flight: checking system tools"
95
+ # ── Dry-run guard ──────────────────────────────────────────────────────────────
100
96
 
101
- have_cmd() { command -v "$1" >/dev/null 2>&1; }
102
- have_file() { [ -e "$1" ]; }
103
-
104
- # Pre-populate PATH with the common install locations for opencode + npm
105
- # so the rest of this script can find them even if the calling shell
106
- # didn't source ~/.profile first.
107
- for p in "$HOME/.opencode/bin" "$HOME/.local/npm/bin" "$HOME/.cargo/bin"; do
108
- case ":$PATH:" in
109
- *":$p:"*) ;;
110
- *) [ -d "$p" ] && export PATH="$p:$PATH" ;;
111
- esac
112
- done
113
-
114
- MISSING=()
115
- have_cmd node || MISSING+=("node")
116
- have_cmd uv || MISSING+=("uv")
117
- have_cmd uvx || MISSING+=("uvx")
118
- have_cmd "$PYTHON_BIN" || MISSING+=("$PYTHON_BIN")
119
- have_cmd jq || MISSING+=("jq")
120
- have_cmd npx || MISSING+=("npx")
121
- have_cmd git || MISSING+=("git")
122
- have_cmd bun || MISSING+=("bun")
123
- have_cmd gh || MISSING+=("gh")
124
- have_cmd opencode || MISSING+=("opencode")
125
- have_cmd sqlite3 || MISSING+=("sqlite3")
126
- have_cmd make || MISSING+=("make")
127
- have_cmd g++ || MISSING+=("g++")
128
-
129
- # Chrome detection: prefer chrome-headless-shell from puppeteer cache, then
130
- # system chromium / chrome / google-chrome. We don't require Chrome — the
131
- # installer works without it — but we report it so the operator knows.
132
- CHROME_BIN=""
133
- for cand in \
134
- "$HOME/.cache/puppeteer/chrome-headless-shell"*/chrome-headless-shell/chrome-headless-shell \
135
- /usr/bin/chromium \
136
- /usr/bin/chrome \
137
- /usr/bin/google-chrome \
138
- "$(command -v chromium 2>/dev/null || true)" \
139
- "$(command -v chrome 2>/dev/null || true)" \
140
- "$(command -v google-chrome 2>/dev/null || true)"; do
141
- [ -n "$cand" ] && [ -x "$cand" ] && CHROME_BIN="$cand" && break
142
- done
143
-
144
- for t in "${MISSING[@]}"; do warn "missing system tool: $t"; done
145
- if [ -z "$CHROME_BIN" ]; then warn "no chrome / chromium found — browser-harness won't work until installed"; fi
146
- [ ${#MISSING[@]} -eq 0 ] && [ -n "$CHROME_BIN" ] && note "all system tools present"
147
-
148
- # ── Install missing system tools (best effort, never abort) ─────────────
149
- section "Install missing system tools"
150
-
151
- # uv (Python package manager — needed for browser-harness + Semble)
152
- if ! have_cmd uv; then
153
- echo -e " ${CYAN}→${NC} Installing uv (Python package manager)..."
154
- if have_cmd curl; then
155
- if curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; then
156
- # uv's installer drops the binary at ~/.local/bin/uv
157
- export PATH="$HOME/.local/bin:$PATH"
158
- if have_cmd uv; then
159
- note "uv installed at $(command -v uv)"
160
- else
161
- warn "uv install ran but uv not on PATH — try 'export PATH=\$HOME/.local/bin:\$PATH'"
162
- fi
163
- else
164
- warn "uv installer script failed — install manually: https://docs.astral.sh/uv/"
165
- fi
166
- else
167
- warn "curl not found — install uv manually: https://docs.astral.sh/uv/"
97
+ dry() {
98
+ if $DRY_RUN; then
99
+ dim " [DRY-RUN] $*"
100
+ return 0
168
101
  fi
169
- else
170
- note "uv $(uv --version 2>/dev/null | head -1)"
171
- fi
172
-
173
- # python3.12 (uv will manage its own python via --python 3.12, but
174
- # we also accept system python3.12 if present). Only install if uv
175
- # is available — uv can fetch its own python.
176
- if ! have_cmd "$PYTHON_BIN"; then
177
- if have_cmd uv; then
178
- echo -e " ${CYAN}→${NC} python3.12 not on PATH — uv will fetch on first use"
179
- note "python3.12 will be auto-fetched by uv (browser-harness will trigger first fetch)"
102
+ "$@"
103
+ }
104
+
105
+ sudo_if_needed() {
106
+ if [ "$EUID" -eq 0 ]; then
107
+ dry "$@"
180
108
  else
181
- warn "no $PYTHON_BIN and no uv — install manually: sudo apt install python3.12 / brew install python@3.12"
109
+ dry sudo "$@"
182
110
  fi
183
- else
184
- note "$PYTHON_BIN $($PYTHON_BIN --version 2>&1 | head -1)"
185
- fi
186
-
187
- # jq (for safe opencode.json merging)
188
- if ! have_cmd jq; then
189
- echo -e " ${CYAN}→${NC} Trying to install jq..."
190
- case "$(uname -s 2>/dev/null || echo unknown)" in
191
- Linux)
192
- if have_cmd apt-get; then sudo apt-get install -y jq 2>/dev/null && note "jq installed (apt)" || warn "apt install jq failed — install manually"
193
- elif have_cmd dnf; then sudo dnf install -y jq 2>/dev/null && note "jq installed (dnf)" || warn "dnf install jq failed — install manually"
194
- elif have_cmd pacman; then sudo pacman -S --noconfirm jq 2>/dev/null && note "jq installed (pacman)" || warn "pacman install jq failed — install manually"
195
- elif have_cmd apk; then sudo apk add jq 2>/dev/null && note "jq installed (apk)" || warn "apk install jq failed — install manually"
196
- else warn "no known package manager install jq manually"
197
- fi
198
- ;;
199
- Darwin)
200
- if have_cmd brew; then brew install jq 2>/dev/null && note "jq installed (brew)" || warn "brew install jq failed — install manually"
201
- else warn "no brew — install jq manually"
202
- fi
203
- ;;
204
- *) warn "unknown OS — install jq manually";;
205
- esac
206
- else
207
- note "jq $(jq --version)"
208
- fi
209
-
210
- # Chrome (best effort — install chrome-headless-shell from chrome-for-testing
211
- # or the puppeteer cache). The dashboard/browser-harness will work without
212
- # it (the user can install later), but we try.
213
- if [ -z "$CHROME_BIN" ]; then
214
- echo -e " ${CYAN}→${NC} Trying to install chrome-headless-shell..."
215
- PUPPETEER_CACHE_DIR="$HOME/.cache/puppeteer"
216
- mkdir -p "$PUPPETEER_CACHE_DIR"
217
- # chrome-for-testing JSON API — find the latest stable chrome-headless-shell
218
- # URL for linux64. Fall back to nothing if the request fails.
219
- CHROME_CT_URL=$(curl -fsSL "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" 2>/dev/null \
220
- | grep -o '"url": *"[^"]*chrome-headless-shell-linux64[^"]*"' \
221
- | head -1 \
222
- | sed 's/.*"url": *"\([^"]*\)".*/\1/')
223
- if [ -n "$CHROME_CT_URL" ]; then
224
- TMP_DIR=$(mktemp -d)
225
- if curl -fsSL "$CHROME_CT_URL" -o "$TMP_DIR/chrome-headless-shell.zip" 2>/dev/null; then
226
- TARGET_DIR="$PUPPETEER_CACHE_DIR/chrome-headless-shell"
227
- mkdir -p "$TARGET_DIR"
228
- unzip -q -o "$TMP_DIR/chrome-headless-shell.zip" -d "$TARGET_DIR"
229
- CHROME_BIN=$(find "$TARGET_DIR" -name chrome-headless-shell -type f 2>/dev/null | head -1)
230
- [ -n "$CHROME_BIN" ] && [ -x "$CHROME_BIN" ] && note "chrome-headless-shell installed at $CHROME_BIN" || warn "chrome-headless-shell install failed"
231
- rm -rf "$TMP_DIR"
232
- else
233
- warn "download from chrome-for-testing failed — install Chrome manually"
234
- fi
235
- else
236
- warn "could not resolve latest chrome-for-testing URL — install Chrome manually"
111
+ }
112
+
113
+ # ── Shared arrays (populated by check_deps, consumed by install_missing_deps) ──
114
+ MISSING_DEPS=()
115
+ MISSING_CMDS=()
116
+
117
+ # ── check_deps: run node scripts/check-deps.mjs and parse result ───────────────
118
+
119
+ check_deps() {
120
+ section "Checking dependencies"
121
+ action "Running dependency detector..."
122
+ local json
123
+ if ! json=$(node "$REPO_DIR/scripts/check-deps.mjs" --strict 2>/dev/null); then
124
+ warn "dependency detector failedproceeding with best-effort checks"
125
+ return
237
126
  fi
238
- fi
239
-
240
- # Chrome runtime libs (v3.20.11). chrome-headless-shell needs ~10 shared
241
- # libraries that Debian/Ubuntu don't ship by default. Without these,
242
- # chrome starts but immediately errors with `libnspr4.so: cannot open
243
- # shared object file`. We install them on apt-based systems; on others
244
- # we warn but continue (Chrome won't work, but the rest of the install
245
- # will).
246
- if [ -n "$CHROME_BIN" ] || [ -z "$CHROME_BIN" ]; then
247
- # Check if chrome-headless-shell actually runs (libs satisfied)
248
- if [ -n "$CHROME_BIN" ] && ! "$CHROME_BIN" --version >/dev/null 2>&1; then
249
- case "$(uname -s 2>/dev/null || echo unknown)" in
250
- Linux)
251
- if have_cmd apt-get; then
252
- echo -e " ${CYAN}→${NC} chrome-headless-shell needs runtime libs — installing via apt..."
253
- if sudo -n apt-get install -y --no-install-recommends \
254
- libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
255
- libxdamage1 libxkbcommon0 libasound2t64 libatspi2.0-0 \
256
- 2>/dev/null; then
257
- note "chrome runtime libs installed (libnss3, libnspr4, libatk*, libxkbcommon, libasound, libatspi)"
258
- else
259
- warn "apt install of chrome runtime libs failed (needs sudo). run manually:"
260
- warn " sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \\"
261
- warn " libxdamage1 libxkbcommon0 libasound2t64 libatspi2.0-0"
262
- fi
263
- else
264
- warn "chrome-headless-shell runtime libs missing — install libnss3, libnspr4, libatk* for your distro"
265
- fi
266
- ;;
267
- Darwin)
268
- warn "chrome-headless-shell runtime libs missing — on macOS: brew install --cask chromium"
269
- ;;
270
- *) warn "chrome-headless-shell runtime libs missing — install via your package manager";;
271
- esac
127
+
128
+ local ok
129
+ ok=$(echo "$json" | node -e "console.log(JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).ok)")
130
+ if [ "$ok" = "true" ]; then
131
+ note "all required dependencies satisfied"
132
+ return
272
133
  fi
273
- fi
274
134
 
275
- # ── Native build deps (better-sqlite3 compiles, etc.) ─────────────────
276
- section "Install native build dependencies"
135
+ # Print missing deps
136
+ while IFS='|' read -r name cmd; do
137
+ [ -z "$name" ] && continue
138
+ warn "missing dependency: $name"
139
+ if [ -n "$cmd" ]; then
140
+ MISSING_CMDS+=("$cmd")
141
+ fi
142
+ done < <(echo "$json" | node -e "
143
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
144
+ for (const m of d.missing) {
145
+ if (m.name === 'tmux') continue;
146
+ console.log(m.name + '|' + (m.installCmd || ''));
147
+ }
148
+ ")
149
+ }
277
150
 
278
- if [ "${NO_SUDO:-}" != "1" ] && have_cmd apt-get; then
279
- echo -e " ${CYAN}→${NC} Installing build-essential, python3, sqlite3-dev..."
280
- if sudo -n apt-get install -y --no-install-recommends \
281
- build-essential python3 sqlite3 libsqlite3-dev \
282
- 2>/dev/null; then
283
- note "native build deps installed (build-essential, python3, sqlite3-dev)"
284
- else
285
- warn "apt install of native build deps failed (needs sudo). Install manually:"
286
- warn " sudo apt-get install -y build-essential python3 sqlite3 libsqlite3-dev"
151
+ # ── install_missing_deps: run install commands for missing deps ─────────────────
152
+
153
+ install_missing_deps() {
154
+ if [ ${#MISSING_CMDS[@]} -eq 0 ]; then
155
+ return
287
156
  fi
288
- else
289
- note "native build deps check skipped (NO_SUDO=1 or non-apt system)"
290
- fi
291
-
292
- # ── Skills CLI (for skills.sh domain skills) ──────────────────────────
293
- section "Install skills CLI"
294
-
295
- if ! have_cmd skills; then
296
- if have_cmd npm; then
297
- echo -e " ${CYAN}→${NC} Installing skills CLI via npm..."
298
- if npm install -g skills >/dev/null 2>&1; then
299
- note "skills CLI installed at $(command -v skills 2>/dev/null || echo 'npm global')"
300
- else
301
- warn "skills CLI install failed (run manually: npm install -g skills)"
157
+
158
+ section "Installing missing dependencies"
159
+
160
+ local i
161
+ for i in "${!MISSING_CMDS[@]}"; do
162
+ local cmd="${MISSING_CMDS[$i]}"
163
+ action "Running: $cmd"
164
+ if $DRY_RUN; then
165
+ dim " would execute: $cmd"
166
+ continue
302
167
  fi
303
- else
304
- warn "npm not found — cannot install skills CLI"
305
- fi
306
- else
307
- note "skills CLI already installed"
308
- fi
309
-
310
- # ── Bun (JavaScript runtime, for plugin tests) ────────────────────────
311
- section "Install Bun"
312
-
313
- if ! have_cmd bun; then
314
- echo -e " ${CYAN}→${NC} Installing Bun..."
315
- if have_cmd curl; then
316
- if curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1; then
317
- export PATH="$HOME/.bun/bin:$PATH"
318
- if have_cmd bun; then
319
- note "Bun installed at $(command -v bun)"
320
- else
321
- warn "Bun installer ran but bun not on PATH — try 'export PATH=\$HOME/.bun/bin:\$PATH'"
322
- fi
168
+ if eval "$cmd" >/dev/null 2>&1; then
169
+ note "dependency installed"
323
170
  else
324
- warn "Bun installer failed — install manually: https://bun.sh"
171
+ warn "install command failed — you may need to install this manually"
325
172
  fi
326
- else
327
- warn "curl not found — install Bun manually: https://bun.sh"
328
- fi
329
- else
330
- note "Bun $(bun --version 2>/dev/null | head -1)"
331
- fi
173
+ done
174
+ }
332
175
 
333
- # ── GitHub CLI (gh, for PR/issue operations) ──────────────────────────
334
- section "Install GitHub CLI"
176
+ # ── ensure_node: node MUST be available for the rest of the install ────────────
335
177
 
336
- if ! have_cmd gh; then
337
- case "$(uname -s 2>/dev/null || echo unknown)" in
178
+ ensure_node() {
179
+ if have_cmd node; then return; fi
180
+ section "Node.js is required"
181
+ action "Install Node.js 18+ first, then re-run this script."
182
+ if $DRY_RUN; then
183
+ dim " would install nodejs via platform package manager"
184
+ return
185
+ fi
186
+ case "$(uname -s)" in
338
187
  Linux)
339
188
  if have_cmd apt-get; then
340
- echo -e " ${CYAN}→${NC} Installing gh via apt..."
341
- if sudo -n apt-get install -y gh >/dev/null 2>&1; then
342
- note "gh installed (apt)"
343
- else
344
- warn "apt install gh failed. Install manually: https://cli.github.com/"
345
- fi
189
+ action "Detected apt installing via NodeSource..."
190
+ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
191
+ sudo apt-get install -y nodejs
346
192
  elif have_cmd dnf; then
347
- sudo -n dnf install -y gh >/dev/null 2>&1 && note "gh installed (dnf)" || warn "dnf install gh failed"
193
+ action "Detected dnf installing nodejs..."
194
+ sudo dnf install -y nodejs
348
195
  elif have_cmd pacman; then
349
- sudo -n pacman -S --noconfirm gh >/dev/null 2>&1 && note "gh installed (pacman)" || warn "pacman install gh failed"
196
+ action "Detected pacman installing nodejs..."
197
+ sudo pacman -S --noconfirm nodejs npm
198
+ elif have_cmd zypper; then
199
+ action "Detected zypper — installing nodejs..."
200
+ sudo zypper install -y nodejs20
350
201
  else
351
- warn "no known package manager install gh manually: https://cli.github.com/"
202
+ err "No known package manager. Install Node.js manually: https://nodejs.org/"
203
+ exit 1
352
204
  fi
353
205
  ;;
354
206
  Darwin)
355
- if have_cmd brew; then
356
- brew install gh >/dev/null 2>&1 && note "gh installed (brew)" || warn "brew install gh failed"
357
- else
358
- warn "no brew — install gh manually: https://cli.github.com/"
359
- fi
207
+ action "Installing node via Homebrew..."
208
+ brew install node@18
360
209
  ;;
361
210
  *)
362
- warn "unknown OS install gh manually: https://cli.github.com/"
211
+ err "Unsupported OS. Install Node.js manually: https://nodejs.org/"
212
+ exit 1
363
213
  ;;
364
214
  esac
365
- else
366
- note "gh $(gh --version 2>/dev/null | head -1)"
367
- fi
368
-
369
- # ── opencode CLI (via curl installer) ────────────────────────────────
370
- section "Install opencode CLI"
371
-
372
- if ! have_cmd opencode; then
373
- echo -e " ${CYAN}→${NC} Installing opencode CLI..."
374
- if have_cmd curl; then
375
- if curl -fsSL https://opencode.ai/install | sh >/dev/null 2>&1; then
376
- if have_cmd opencode; then
377
- note "opencode CLI installed at $(command -v opencode)"
378
- else
379
- warn "opencode installer ran but binary not on PATH"
380
- fi
381
- else
382
- warn "opencode installer failed — install manually: https://opencode.ai"
383
- fi
384
- else
385
- warn "curl not found — install opencode manually: https://opencode.ai"
215
+ note "Node.js installed: $(node --version)"
216
+ }
217
+
218
+ # ── git operations (clone or update) ───────────────────────────────────────────
219
+
220
+ ensure_repo() {
221
+ if $UPDATE_MODE; then
222
+ section "Updating BizarHarness repository"
223
+ action "Pulling latest via git pull --ff-only..."
224
+ dry git -C "$REPO_DIR" pull --ff-only
225
+ note "repository updated"
226
+ return
386
227
  fi
387
- else
388
- note "opencode CLI already installed"
389
- fi
390
-
391
- # ── npm packages (BizarHarness — dashboard + plugin + SDK all in one) ──
392
- section "Install BizarHarness npm packages"
393
-
394
- if have_cmd npm; then
395
- for pkg in @polderlabs/bizar; do
396
- if npm ls -g "$pkg" --depth=0 >/dev/null 2>&1; then
397
- cur=$(npm ls -g "$pkg" --depth=0 --json 2>/dev/null | grep -oE '"version":\s*"[^"]+"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
398
- note "$pkg already installed (v$cur)"
399
- else
400
- echo -e " ${CYAN}→${NC} Installing $pkg..."
401
- if npm install -g "$pkg" >/dev/null 2>&1; then
402
- note "$pkg installed"
403
- else
404
- err "$pkg install failed (run manually: npm install -g $pkg)"
405
- fi
406
- fi
407
- done
408
- else
409
- err "npm not found — install Node.js 20+ first: https://nodejs.org/"
410
- fi
411
-
412
- # ── browser-harness (Python tool via uv, requires Python 3.12) ─────────
413
- section "Install browser-harness (Python via uv)"
414
-
415
- if have_cmd browser-harness; then
416
- note "browser-harness $(browser-harness --version 2>&1 | head -1) already installed"
417
- else
418
- if have_cmd uv; then
419
- echo -e " ${CYAN}→${NC} uv tool install --python 3.12 --upgrade --force browser-harness"
420
- if uv tool install --python 3.12 --upgrade --force browser-harness >/dev/null 2>&1; then
421
- note "browser-harness installed"
422
- else
423
- err "browser-harness install failed (run manually: uv tool install --python 3.12 --upgrade --force browser-harness)"
228
+
229
+ # If we're already inside the repo, skip clone.
230
+ if [ -d "$REPO_DIR/.git" ]; then
231
+ note "repository already present at $REPO_DIR"
232
+ if $FORCE || $UPDATE_MODE; then
233
+ dry git -C "$REPO_DIR" pull --ff-only
424
234
  fi
425
- else
426
- warn "uv not installed — skipping browser-harness (install uv first, then re-run this script)"
235
+ return
427
236
  fi
428
- fi
429
-
430
- # Register the browser-harness skill so any agent that needs it picks it up.
431
- if have_cmd browser-harness; then
432
- BH_SKILL_DIR="$SKILLS_DIR/browser-harness"
433
- mkdir -p "$BH_SKILL_DIR"
434
- if browser-harness skill > "$BH_SKILL_DIR/SKILL.md" 2>/dev/null; then
435
- note "browser-harness skill registered at $BH_SKILL_DIR/SKILL.md"
436
- else
437
- warn "browser-harness skill registration failed (re-run: browser-harness skill > $BH_SKILL_DIR/SKILL.md)"
438
- fi
439
- fi
440
-
441
- # ── Chrome lifecycle (start browser-harness-up.sh) ────────────────────
442
- section "Start Chrome for browser-harness"
443
-
444
- if [ -x "$REPO_DIR/cli/browser-harness-up.sh" ]; then
445
- # The script knows how to find chrome-headless-shell from the
446
- # puppeteer cache (or any system chrome).
447
- if BH_OUTPUT=$("$REPO_DIR/cli/browser-harness-up.sh" start 2>&1); then
448
- # Trim verbose output; just keep the success lines.
449
- while IFS= read -r line; do
450
- case "$line" in
451
- *"✓"*|*"already"*|*"stopped"*|*"restarted"*) note "$line" ;;
452
- *) ;;
453
- esac
454
- done <<< "$BH_OUTPUT"
237
+
238
+ section "Cloning BizarHarness repository"
239
+ local target
240
+ target="$(dirname "$REPO_DIR")/BizarHarness"
241
+ if [ ! -d "$target" ]; then
242
+ action "Cloning into $target..."
243
+ dry git clone https://github.com/DrB0rk/BizarHarness.git "$target"
244
+ note "cloned"
455
245
  else
456
- warn "browser-harness-up.sh start failed (run manually: $REPO_DIR/cli/browser-harness-up.sh start)"
457
- fi
458
- elif have_cmd "$REPO_DIR/../bin/bizar"; then
459
- if "$REPO_DIR/../bin/bizar" browser-harness-up start 2>&1 | grep -E '✓|✗' | head -3; then
460
- note "Chrome started via bizar browser-harness-up"
246
+ note "already cloned at $target"
461
247
  fi
462
- else
463
- warn "cli/browser-harness-up.sh not found at $REPO_DIR — skipping Chrome auto-start"
464
- fi
248
+ }
465
249
 
466
- # ── BizarHarness config files ─────────────────────────────────────────
467
- section "Install BizarHarness config files"
250
+ # ── Install agent/config files via cli/install.mjs ──────────────────────────────
468
251
 
469
- mkdir -p "$CONFIG_DIR/agents"
252
+ install_config() {
253
+ section "Installing BizarHarness config files"
470
254
 
471
- # Agents — copy every .md except those already in place.
472
- for f in "$REPO_DIR/config/agents/"*.md; do
473
- [ -f "$f" ] || continue
474
- name="$(basename "$f")"
475
- if [ -f "$CONFIG_DIR/agents/$name" ] && cmp -s "$f" "$CONFIG_DIR/agents/$name"; then
476
- : # identical, skip silently
477
- else
478
- cp "$f" "$CONFIG_DIR/agents/$name"
479
- INSTALLED+=("agents/$name")
255
+ if $DRY_RUN; then
256
+ dim " would run: node \"$REPO_DIR/cli/install.mjs\" --non-interactive"
257
+ return
480
258
  fi
481
- done
482
- note "agents synced ($(ls "$CONFIG_DIR/agents/"*.md 2>/dev/null | wc -l) files)"
483
-
484
- # Shared baseline (used as a skill AND as the on-disk reference)
485
- if [ -d "$REPO_DIR/config/agents/_shared" ]; then
486
- mkdir -p "$CONFIG_DIR/agents/_shared"
487
- cp -R "$REPO_DIR/config/agents/_shared/." "$CONFIG_DIR/agents/_shared/"
488
- note "agents/_shared/ synced"
489
- fi
490
-
491
- # Slash commands
492
- if [ -d "$REPO_DIR/config/commands/" ]; then
493
- mkdir -p "$CONFIG_DIR/commands"
494
- for f in "$REPO_DIR/config/commands/"*.md; do
495
- [ -f "$f" ] || continue
496
- name="$(basename "$f")"
497
- cp "$f" "$CONFIG_DIR/commands/$name"
498
- done
499
- note "slash commands synced"
500
- fi
501
-
502
- # Hooks
503
- if [ -d "$REPO_DIR/config/hooks/" ]; then
504
- mkdir -p "$CONFIG_DIR/hooks"
505
- for entry in "$REPO_DIR/config/hooks/"*; do
506
- [ -e "$entry" ] || continue
507
- name="$(basename "$entry")"
508
- cp -R "$entry" "$CONFIG_DIR/hooks/$name"
509
- done
510
- note "hooks synced"
511
- fi
512
-
513
- # AGENTS.md
514
- if [ -f "$REPO_DIR/config/AGENTS.md" ]; then
515
- cp "$REPO_DIR/config/AGENTS.md" "$CONFIG_DIR/AGENTS.md"
516
- note "AGENTS.md synced"
517
- fi
518
-
519
- # ── Bundled skills (bizar, self-improvement, agent-baseline, …) ───────
520
- section "Install bundled skills"
521
-
522
- mkdir -p "$SKILLS_DIR"
523
- # Core skills — bundled from config/skills/ in the repo.
524
- for skill in bizar self-improvement cpp-coding-standards cpp-testing embedded-esp-idf \
525
- obsidian glyph read-the-damn-docs; do
526
- if [ -d "$REPO_DIR/config/skills/$skill" ]; then
527
- mkdir -p "$SKILLS_DIR/$skill"
528
- cp -R "$REPO_DIR/config/skills/$skill/." "$SKILLS_DIR/$skill/"
529
- note "skill: $skill"
259
+
260
+ if [ -f "$REPO_DIR/cli/install.mjs" ]; then
261
+ action "Running cli/install.mjs (agent files, skills, plugin)..."
262
+
263
+ # cli/install.mjs runInstaller() spawns bash install.sh — we need to pass
264
+ # the flags through. Use the canonical entry point with environment hints.
265
+ # The installer automatically copies agents, commands, hooks, skills, plugin.
266
+ if node "$REPO_DIR/cli/install.mjs" --non-interactive 2>&1; then
267
+ note "config files installed"
268
+ else
269
+ warn "cli/install.mjs exited non-zero — some config may be incomplete"
270
+ fi
530
271
  else
531
- # Fallback: write inline SKILL.md so install.sh remains self-contained.
532
- mkdir -p "$SKILLS_DIR/$skill"
533
- case "$skill" in
534
- obsidian)
535
- # v3.24.0 — Prefer the on-disk SKILL.md so the inline copy can't
536
- # drift. Fall back to an inline heredoc ONLY when the repo source
537
- # is missing (e.g. someone running install.sh outside a clone).
538
- if [ -f "$REPO_DIR/config/skills/obsidian/SKILL.md" ]; then
539
- cp "$REPO_DIR/config/skills/obsidian/SKILL.md" "$SKILLS_DIR/$skill/SKILL.md"
540
- note "skill: $skill (from repo)"
541
- else
542
- cat > "$SKILLS_DIR/$skill/SKILL.md" << 'OBSIDIAN_SKILL'
543
- ---
544
- name: obsidian
545
- description: Interact with the Obsidian vault for persistent per-project memory. Use when searching notes, creating notes, writing wikilinks, using callouts, setting properties, or managing the project's obsidian knowledge bank.
546
- ---
547
-
548
- # Obsidian Vault
549
-
550
- Per-project persistent memory for Bizar agents. Every project gets its own vault bank, keyed by project name.
551
-
552
- ## Operations
553
-
554
- - **Search vault**: `obsidian_search` with `bank_id: "<project-name>"` — use before writing code that might have prior context
555
- - **Create/update notes**: `obsidian_write` with bank_id — store decisions, ADRs, architecture notes
556
- - **Append to notes**: `obsidian_append` — add to existing notes
557
- - **List notes**: `obsidian_list_notes` with optional directory prefix
558
-
559
- ## Conventions
560
-
561
- - Use `bank_id: "<project-name>"` determined from the project root (the directory name)
562
- - Use wikilinks `[[Note Name]]` for cross-references within the vault
563
- - Use frontmatter for structured properties (tags, status, date)
564
- - Store ADRs under `adr/` prefix
565
- - Store implementation notes under `notes/` prefix
566
- OBSIDIAN_SKILL
567
- note "skill: $skill (inline)"
568
- fi
569
- ;;
570
- glyph)
571
- cat > "$SKILLS_DIR/$skill/SKILL.md" << 'GLYPH_SKILL'
572
- ---
573
- name: glyph
574
- description: Work with the project knowledge graph (graphify). Query the graph before searching source files for structural and relationship questions. Use for architecture discovery, dependency mapping, and cross-module understanding.
575
- ---
576
-
577
- # Glyph — Knowledge Graph
578
-
579
- Graph-based codebase navigation using graphify (`.bizar/graph/`). Maps source files, functions, types, and their relationships into a queryable graph with community detection.
580
-
581
- ## Commands
582
-
583
- ```bash
584
- bizar graph status # node/edge/community counts
585
- bizar graph query <term> # BFS traversal from a concept
586
- bizar graph path <A> <B> # shortest path between two concepts
587
- bizar graph explain <X> # all nodes related to X
588
- bizar graph update # incremental rebuild
589
- bizar graph build # full rebuild
590
- ```
591
-
592
- ## When to Use
593
-
594
- - Before reading a large file: `bizar graph explain "<module>"`
595
- - Mapping unfamiliar code: `bizar graph query "<feature>"`
596
- - Debugging cross-module: `bizar graph path "<symptom>" "<root-cause>"`
597
- - Before grep: check the graph first — may point directly to the file
598
- GLYPH_SKILL
599
- note "skill: $skill (inline)"
600
- ;;
601
- read-the-damn-docs)
602
- cat > "$SKILLS_DIR/$skill/SKILL.md" << 'RTDD_SKILL'
603
- ---
604
- name: read-the-damn-docs
605
- description: Before writing code, read the official documentation. Use websearch/webfetch to find docs for any API, library, framework, or tool. Never guess API signatures, parameter names, or return types.
606
- ---
607
-
608
- # Read the Damn Docs
609
-
610
- Discipline: always check official documentation before writing code against an unfamiliar API.
611
-
612
- ## Rules
613
-
614
- 1. **Search docs first**: Before using any function/API/component you haven't used recently, `websearch` or `webfetch` the official docs
615
- 2. **No guessing**: Never invent parameter names, return types, or method signatures from training data alone
616
- 3. **Read error messages**: When something fails, read the full error and search docs for the exact error text
617
- 4. **Version matters**: Note the library/tool version and use docs matching that version
618
- 5. **Check examples**: Most good docs have worked examples — find and follow them
619
-
620
- ## When to Apply
621
-
622
- - Adding a new dependency — read its install/setup docs
623
- - Using an unfamiliar API method — check signature and return type
624
- - Debugging an error you don't understand — search docs + error message
625
- - Configuring a tool — read the official config reference
626
- RTDD_SKILL
627
- note "skill: $skill (inline)"
628
- ;;
629
- *)
630
- warn "skill source missing: $skill (no inline fallback)"
631
- ;;
632
- esac
272
+ warn "cli/install.mjs not found config files not auto-installed"
633
273
  fi
634
- done
274
+ }
635
275
 
636
- # Shared agent baseline skill referenced by every agent file.
637
- if [ -f "$REPO_DIR/config/agents/_shared/AGENT_BASELINE.md" ]; then
638
- mkdir -p "$SKILLS_DIR/agent-baseline"
639
- cp "$REPO_DIR/config/agents/_shared/AGENT_BASELINE.md" "$SKILLS_DIR/agent-baseline/SKILL.md"
640
- note "skill: agent-baseline (shared by all 14 agents)"
641
- fi
276
+ # ── Verify opencode.json has the Bizar plugin registered ───────────────────────
642
277
 
643
- # Make bundled scripts executable.
644
- chmod +x "$SKILLS_DIR"/embedded-esp-idf/scripts/*.sh 2>/dev/null || true
278
+ verify_plugin() {
279
+ section "Verifying Bizar plugin registration"
645
280
 
646
- # ── Domain skills via skills.sh (impeccable, ponytail, obsidian-skills) ─
647
- section "Install domain skills from skills.sh"
281
+ local opencode_config="$CONFIG_DIR/opencode.json"
648
282
 
649
- if have_cmd npx; then
650
- # Impeccable UI anti-pattern detection (frontend quality)
651
- if npx --yes impeccable skills install -y --scope=user --providers=opencode >/dev/null 2>&1; then
652
- note "impeccable (UI anti-pattern detector)"
653
- else
654
- warn "impeccable install failed (run: npx impeccable skills install)"
283
+ if [ ! -f "$opencode_config" ]; then
284
+ warn "opencode.json not found at $opencode_config plugin not registered"
285
+ return
655
286
  fi
656
287
 
657
- # Ponytail minimal-code skill for AI agents
658
- if npx --yes @dietrichgebert/ponytail install --scope=user >/dev/null 2>&1; then
659
- note "ponytail (minimal-code skill)"
660
- else
661
- warn "ponytail install failed (run: npx @dietrichgebert/ponytail install)"
288
+ # Check if plugin entry exists using node's JSON parser (avoid jq dependency)
289
+ local has_plugin
290
+ has_plugin=$(node -e "
291
+ const cfg = JSON.parse(require('fs').readFileSync('$opencode_config', 'utf8'));
292
+ const plugins = cfg.plugin || [];
293
+ const found = plugins.some(function(p) {
294
+ return Array.isArray(p) && p[0] && p[0].includes('plugins/bizar');
295
+ });
296
+ console.log(found ? 'yes' : 'no');
297
+ " 2>/dev/null || echo "no")
298
+
299
+ if [ "$has_plugin" = "yes" ]; then
300
+ note "Bizar plugin registered in opencode.json"
301
+ return
662
302
  fi
663
303
 
664
- # Obsidian skills Obsidian Flavored Markdown, Bases, JSON Canvas, CLI
665
- if npx --yes skills add https://github.com/kepano/obsidian-skills --all -y >/dev/null 2>&1; then
666
- note "obsidian-skills (Obsidian Markdown/Bases/Canvas)"
667
- else
668
- warn "obsidian-skills install failed (run: npx skills add kepano/obsidian-skills)"
304
+ action "Adding Bizar plugin entry to opencode.json..."
305
+
306
+ if $DRY_RUN; then
307
+ dim " would patch opencode.json to add plugin entry"
308
+ return
669
309
  fi
670
- else
671
- warn "npx not found — skipping domain skill installation"
672
- fi
673
310
 
674
- # ── Bizar plugin (mirrored from npm package to ~/.config/opencode/plugins/)
675
- section "Install Bizar opencode plugin"
311
+ # Patch via node script (no jq dependency needed)
312
+ node -e "
313
+ const fs = require('fs');
314
+ const path = '$opencode_config';
315
+ let cfg;
316
+ try {
317
+ cfg = JSON.parse(fs.readFileSync(path, 'utf8'));
318
+ } catch(e) {
319
+ console.error('Failed to read opencode.json:', e.message);
320
+ process.exit(1);
321
+ }
322
+ if (!cfg.plugin) cfg.plugin = [];
323
+ const has = cfg.plugin.some(function(p) {
324
+ return Array.isArray(p) && p[0] && p[0].includes('plugins/bizar');
325
+ });
326
+ if (!has) {
327
+ cfg.plugin.push(['./plugins/bizar/index.ts', {
328
+ loopThresholdWarn: 5,
329
+ loopThresholdEscalate: 8,
330
+ loopThresholdBlock: 12,
331
+ loopWindowSize: 10
332
+ }]);
333
+ }
334
+ fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
335
+ console.log(has ? 'already registered' : 'plugin entry added');
336
+ "
337
+
338
+ note "opencode.json updated with Bizar plugin"
339
+ }
676
340
 
677
- PLUGIN_DST="$CONFIG_DIR/plugins/bizar"
341
+ # ── Install background service ─────────────────────────────────────────────────
678
342
 
679
- # v4.0.0: The plugin, dashboard, and SDK are all part of the same
680
- # @polderlabs/bizar npm package, so there is no separate
681
- # @polderlabs/bizar-plugin to resolve from the global node_modules.
682
- # The local-repo copy path is the only one we exercise now; the global
683
- # fallback is kept for users who still have a legacy install with the
684
- # old plugin on disk.
685
- #
686
- # v3.20.12: two sources, in priority order:
687
- # 1. Local repo's plugins/bizar/ (for `git clone` users)
688
- # 2. Globally installed @polderlabs/bizar-plugin (for `npm i -g` users)
689
- # The npm-installed plugin also needs its bundled node_modules/ copied
690
- # alongside (it depends on @polderlabs/bizar-sdk, which isn't on npm).
691
- # This previously lived in cli/install.mjs:installPluginFromGlobal();
692
- # duplicating the logic here makes install.sh the single source of truth.
693
- PLUGIN_SRC=""
694
- if [ -d "$REPO_DIR/plugins/bizar" ]; then
695
- PLUGIN_SRC="$REPO_DIR/plugins/bizar"
696
- PLUGIN_SRC_KIND="local repo"
697
- elif have_cmd npm; then
698
- # Resolve the npm global plugin path. `npm root -g` gives us
699
- # /usr/local/lib/node_modules; the plugin lives in @polderlabs/bizar-plugin/.
700
- NPM_GLOBAL_ROOT=$(npm root -g 2>/dev/null || echo "")
701
- if [ -n "$NPM_GLOBAL_ROOT" ] && [ -d "$NPM_GLOBAL_ROOT/@polderlabs/bizar-plugin" ]; then
702
- PLUGIN_SRC="$NPM_GLOBAL_ROOT/@polderlabs/bizar-plugin"
703
- PLUGIN_SRC_KIND="@polderlabs/bizar-plugin (global)"
704
- fi
705
- fi
706
-
707
- if [ -n "$PLUGIN_SRC" ]; then
708
- mkdir -p "$PLUGIN_DST"
709
- while IFS= read -r -d '' f; do
710
- rel="${f#$PLUGIN_SRC/}"
711
- case "$rel" in
712
- node_modules/*|dist/*|*.log|.DS_Store) continue ;;
713
- esac
714
- mkdir -p "$(dirname "$PLUGIN_DST/$rel")"
715
- cp "$f" "$PLUGIN_DST/$rel"
716
- done < <(find "$PLUGIN_SRC" -type f -print0)
717
- note "plugins/bizar/ copied from $PLUGIN_SRC_KIND"
718
- # v4.0.0: No bundled node_modules copy. The SDK ships inside the same
719
- # @polderlabs/bizar package, so plugin imports resolve from opencode's
720
- # own node_modules at runtime.
721
- else
722
- warn "Bizar plugin source not found (install @polderlabs/bizar: npm i -g @polderlabs/bizar)"
723
- fi
724
-
725
- # ── Merge opencode.json (template + user's existing config) ────────────
726
- section "Configure opencode.json"
727
-
728
- # Prefer the .template copy; fall back to the legacy tracked file.
729
- if [ -f "$REPO_DIR/config/opencode.json.template" ]; then
730
- TEMPLATE="$REPO_DIR/config/opencode.json.template"
731
- elif [ -f "$REPO_DIR/config/opencode.json" ]; then
732
- TEMPLATE="$REPO_DIR/config/opencode.json"
733
- else
734
- warn "No opencode.json template found"
735
- TEMPLATE=""
736
- fi
737
-
738
- if [ -n "$TEMPLATE" ]; then
739
- if [ -f "$CONFIG_DIR/opencode.json" ]; then
740
- cp "$CONFIG_DIR/opencode.json" "$CONFIG_DIR/opencode.json.bak"
343
+ install_service() {
344
+ section "Installing background service"
345
+
346
+ if $DRY_RUN; then
347
+ dim " would run: node \"$REPO_DIR/scripts/install-service.mjs\""
348
+ return
741
349
  fi
742
350
 
743
- if have_cmd jq; then
744
- MERGE_TMP="$CONFIG_DIR/opencode.json.merge.$$"
745
- if [ -f "$CONFIG_DIR/opencode.json" ]; then
746
- jq -s '.[0] * .[1]' "$TEMPLATE" "$CONFIG_DIR/opencode.json" > "$MERGE_TMP" 2>/dev/null \
747
- && mv "$MERGE_TMP" "$CONFIG_DIR/opencode.json" \
748
- || { rm -f "$MERGE_TMP"; cp "$TEMPLATE" "$CONFIG_DIR/opencode.json"; }
351
+ if [ -f "$REPO_DIR/scripts/install-service.mjs" ]; then
352
+ local result
353
+ result=$(node "$REPO_DIR/scripts/install-service.mjs" 2>&1) || true
354
+ # Parse the JSON result
355
+ local ok
356
+ ok=$(echo "$result" | node -e "
357
+ try {
358
+ const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
359
+ console.log(r.ok !== false ? 'yes' : 'no');
360
+ } catch(e) { console.log('no'); }
361
+ " 2>/dev/null <<< "$result" || echo "no")
362
+
363
+ local skipped
364
+ skipped=$(echo "$result" | node -e "
365
+ try {
366
+ const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
367
+ console.log(r.skipped ? 'yes' : 'no');
368
+ } catch(e) { console.log('no'); }
369
+ " 2>/dev/null <<< "$result" || echo "no")
370
+
371
+ if [ "$ok" = "yes" ]; then
372
+ if [ "$skipped" = "yes" ]; then
373
+ dim " (service-controller not yet available — will be completed when Stream A lands)"
374
+ fi
375
+ note "service registration complete"
749
376
  else
750
- cp "$TEMPLATE" "$CONFIG_DIR/opencode.json"
377
+ warn "service registration had issues — see output above"
378
+ dim " manual command: node cli/service-controller.mjs install"
751
379
  fi
752
-
753
- # Ensure the Bizar plugin entry is registered (idempotent).
754
- PLUGIN_TMP="$CONFIG_DIR/opencode.json.tmp.$$"
755
- jq '
756
- if (.plugin // []) | map(.[0] == "./plugins/bizar/index.ts") | any then .
757
- else .plugin = (.plugin // []) + [["./plugins/bizar/index.ts", {
758
- "loopThresholdWarn": 5,
759
- "loopThresholdEscalate": 8,
760
- "loopThresholdBlock": 12,
761
- "loopWindowSize": 10
762
- }]]
763
- end
764
- ' "$CONFIG_DIR/opencode.json" > "$PLUGIN_TMP" \
765
- && mv "$PLUGIN_TMP" "$CONFIG_DIR/opencode.json" \
766
- || rm -f "$PLUGIN_TMP"
767
- note "opencode.json (template merged with existing config; plugin entry ensured)"
768
380
  else
769
- cp "$TEMPLATE" "$CONFIG_DIR/opencode.json"
770
- warn "jq not available — copied template directly. Re-install jq for safe merging."
381
+ warn "scripts/install-service.mjs not found"
771
382
  fi
772
- fi
773
-
774
- # ── Bootstrap Bizar Memory Service ───────────────────────────────────
775
- section "Bootstrap Bizar Memory Service"
776
-
777
- if have_cmd node; then
778
- MEMORY_REPO_NAME="${BIZAR_MEMORY_REPO_NAME:-bizar-memory}"
779
- note "initializing local memory vault"
780
- if node "$REPO_DIR/cli/bin.mjs" memory init \
781
- --memory-mode managed \
782
- --memory-repo-name "$MEMORY_REPO_NAME" \
783
- --yes 2>/dev/null; then
784
- note "memory service ready (mode: managed, repo: ~/.local/share/bizar/memory/$MEMORY_REPO_NAME)"
785
- note "next steps:"
786
- note " bizar memory status # check vault + git state"
787
- note " bizar memory sync # pull, validate, scan, commit, push"
788
- note " bizar memory doctor # health check"
383
+ }
384
+
385
+ # ── Verify .opencode/plugins/bizar exists ──────────────────────────────────────
386
+
387
+ verify_plugin_dir() {
388
+ section "Verifying plugin directory"
389
+
390
+ local dst_plugin="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/plugins/bizar"
391
+ if [ -d "$dst_plugin" ]; then
392
+ note "plugin directory exists at $dst_plugin"
789
393
  else
790
- warn "memory init skipped (run 'bizar memory init' manually later)"
394
+ action "Setting up plugin directory..."
395
+ dry mkdir -p "$dst_plugin"
396
+ # Copy from repo if available
397
+ if [ -d "$REPO_DIR/plugins/bizar" ]; then
398
+ if ! $DRY_RUN; then
399
+ cp -R "$REPO_DIR/plugins/bizar/." "$dst_plugin/"
400
+ fi
401
+ note "plugin files copied from repo"
402
+ else
403
+ warn "no plugin source found in repo — run 'bizar install' or 'npm install -g @polderlabs/bizar-plugin'"
404
+ fi
791
405
  fi
792
- else
793
- warn "node not on PATH — skipping memory service bootstrap"
794
- fi
795
-
796
- # ── Write install-state.json (used by `bizar update` for migrations) ───
797
- section "Write install-state"
798
-
799
- mkdir -p "$BIZAR_STATE_DIR"
800
- BH_VERSION=$(browser-harness --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' || echo "")
801
- BIZAR_VERSION=$(npm ls -g @polderlabs/bizar --depth=0 --json 2>/dev/null | grep -oE '"@polderlabs/bizar":\s*\{[^}]*"version":\s*"[^"]+"' | grep -oE '"version":\s*"[^"]+"' | grep -oE '"[^"]+"' | tail -1 | tr -d '"' || echo "")
802
- PY_VERSION=$("$PYTHON_BIN" --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' || echo "")
803
- UV_VERSION=$(uv --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' || echo "")
804
- JQ_VERSION=$(jq --version 2>/dev/null | sed 's/[^0-9.]//g' || echo "")
805
-
806
- STATE_JSON=$(cat <<JSON
807
- {
808
- "installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S.%NZ | sed 's/\.[0-9]*//')",
809
- "components": {
810
- "bizar": "${BIZAR_VERSION:-unknown}",
811
- "browser-harness": "${BH_VERSION:-unknown}",
812
- "uv": "${UV_VERSION:-unknown}",
813
- "python3.12": "${PY_VERSION:-unknown}",
814
- "jq": "${JQ_VERSION:-unknown}"
815
- }
816
406
  }
817
- JSON
818
- )
819
- echo "$STATE_JSON" > "$BIZAR_STATE_FILE"
820
- note "install-state written to $BIZAR_STATE_FILE"
821
-
822
- # ── Final dependency verification ────────────────────────────────────
823
- section "Verify all dependencies"
824
-
825
- if verify_dependencies; then
826
- note "all required tools available on PATH"
827
- else
828
- err "some required tools are missing — see list above"
829
- fi
830
-
831
- # ── Final status banner ──────────────────────────────────────────────
832
- section "Install complete"
833
-
834
- echo ""
835
- echo -e "${BOLD}${CYAN}┌────────────────────────────────────────────────────────────┐${NC}"
836
- echo -e "${BOLD}${CYAN}│${NC} ${BOLD}BizarHarness ready.${NC} │"
837
- echo -e "${BOLD}${CYAN}│${NC} │"
838
-
839
- if [ ${#INSTALLED[@]} -gt 0 ]; then
840
- echo -e "${BOLD}${CYAN}│${NC} ${GREEN}✓ Installed:${NC} │"
841
- for entry in "${INSTALLED[@]}"; do
842
- short=$(echo "$entry" | head -c 56)
843
- echo -e "${BOLD}${CYAN}│${NC} • $short$(printf '%*s' $((56 - ${#short})) '') │"
844
- done
845
- fi
846
407
 
847
- if [ ${#SKIPPED[@]} -gt 0 ]; then
408
+ # ── Final success banner ───────────────────────────────────────────────────────
409
+
410
+ print_banner() {
411
+ local dashboard_url="http://localhost:3333"
412
+ section "Install complete"
413
+
414
+ echo ""
415
+ echo -e "${BOLD}${CYAN}┌────────────────────────────────────────────────────────────┐${NC}"
416
+ echo -e "${BOLD}${CYAN}│${NC} ${BOLD}BizarHarness ready.${NC} │"
848
417
  echo -e "${BOLD}${CYAN}│${NC} │"
849
- echo -e "${BOLD}${CYAN}│${NC} ${YELLOW}⚠ Skipped:${NC} │"
850
- for entry in "${SKIPPED[@]}"; do
851
- short=$(echo "$entry" | head -c 56)
852
- echo -e "${BOLD}${CYAN}│${NC} • $short$(printf '%*s' $((56 - ${#short})) '') │"
418
+ echo -e "${BOLD}${CYAN}│${NC} ${GREEN}✓${NC} Cross-platform installer v3.22.0 │"
419
+ echo -e "${BOLD}${CYAN}│${NC} │"
420
+ echo -e "${BOLD}${CYAN}│${NC} Dashboard: ${CYAN}$dashboard_url${NC} │"
421
+ echo -e "${BOLD}${CYAN}│${NC} │"
422
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM}Next:${NC} │"
423
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} 1. Restart opencode to pick up new config${NC} │"
424
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} 2. Run /connect in opencode to add API keys${NC} │"
425
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} 3. Run 'bizar dash start' to launch the dashboard${NC} │"
426
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} 4. Visit $dashboard_url in your browser${NC} │"
427
+ echo -e "${BOLD}${CYAN}│${NC} │"
428
+ echo -e "${BOLD}${CYAN}└────────────────────────────────────────────────────────────┘${NC}"
429
+ echo ""
430
+
431
+ if $DRY_RUN; then
432
+ warn "DRY RUN — no changes were made"
433
+ fi
434
+ }
435
+
436
+ # ── install_linux: Debian/Ubuntu, Fedora/RHEL, Arch, openSUSE ──────────────────
437
+
438
+ install_linux() {
439
+ note "Detected Linux ($(uname -m))"
440
+
441
+ ensure_node
442
+
443
+ # Pre-flight: populate PATH with common locations
444
+ for p in "$HOME/.opencode/bin" "$HOME/.local/bin" "$HOME/.bun/bin"; do
445
+ case ":$PATH:" in *":$p:"*) ;; *) [ -d "$p" ] && export PATH="$p:$PATH" ;; esac
853
446
  done
854
- fi
855
447
 
856
- if [ ${#FAILED[@]} -gt 0 ]; then
857
- echo -e "${BOLD}${CYAN}│${NC} │"
858
- echo -e "${BOLD}${CYAN}│${NC} ${RED}✗ Failed:${NC} │"
859
- for entry in "${FAILED[@]}"; do
860
- short=$(echo "$entry" | head -c 56)
861
- echo -e "${BOLD}${CYAN}│${NC} • $short$(printf '%*s' $((56 - ${#short})) '') │"
448
+ check_deps
449
+ install_missing_deps
450
+
451
+ ensure_repo
452
+ install_config
453
+ verify_plugin_dir
454
+ verify_plugin
455
+ install_service
456
+ }
457
+
458
+ # ── install_macos: Homebrew-based ──────────────────────────────────────────────
459
+
460
+ install_macos() {
461
+ note "Detected macOS ($(uname -m))"
462
+
463
+ # Ensure Homebrew
464
+ if ! have_cmd brew; then
465
+ section "Homebrew"
466
+ action "Installing Homebrew..."
467
+ if $DRY_RUN; then
468
+ dim " would install Homebrew via /bin/bash -c '\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'"
469
+ else
470
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
471
+ note "Homebrew installed"
472
+ fi
473
+ else
474
+ note "Homebrew already installed"
475
+ fi
476
+
477
+ ensure_node
478
+
479
+ # Pre-flight: populate PATH
480
+ for p in "$HOME/.opencode/bin" "$HOME/.local/bin" "$HOME/.bun/bin" "/opt/homebrew/bin"; do
481
+ case ":$PATH:" in *":$p:"*) ;; *) [ -d "$p" ] && export PATH="$p:$PATH" ;; esac
862
482
  done
863
- fi
864
-
865
- echo -e "${BOLD}${CYAN}│${NC} │"
866
- echo -e "${BOLD}${CYAN}│${NC} ${DIM}Next: open /connect in your next opencode session${NC} │"
867
- echo -e "${BOLD}${CYAN}│${NC} ${DIM} to add API keys (opencode-zen is free; MiniMax${NC} │"
868
- echo -e "${BOLD}${CYAN}│${NC} ${DIM} is paid). The installer never asks for keys —${NC} │"
869
- echo -e "${BOLD}${CYAN}│${NC} ${DIM} do that part yourself so secrets stay on your${NC} │"
870
- echo -e "${BOLD}${CYAN}│${NC} ${DIM} machine, not in any installer log.${NC} │"
871
- echo -e "${BOLD}${CYAN}└────────────────────────────────────────────────────────────┘${NC}"
872
- echo ""
873
-
874
- [ ${#FAILED[@]} -gt 0 ] && exit 1
875
- exit 0
483
+
484
+ check_deps
485
+ install_missing_deps
486
+
487
+ ensure_repo
488
+ install_config
489
+ verify_plugin_dir
490
+ verify_plugin
491
+ install_service
492
+ }
493
+
494
+ # ── Main entry ──────────────────────────────────────────────────────────────────
495
+
496
+ main() {
497
+ parse_flags "$@"
498
+
499
+ if $DRY_RUN; then
500
+ dim "DRY RUN MODE — no changes will be made"
501
+ echo ""
502
+ fi
503
+
504
+ # Header
505
+ echo ""
506
+ echo -e "${BOLD}${CYAN} ⚡ BizarHarness Installer v3.22.0${NC}"
507
+ if $UPDATE_MODE; then echo -e " ${DIM}Update mode${NC}"; fi
508
+ echo ""
509
+
510
+ # OS detection
511
+ local os
512
+ os="$(uname -s)"
513
+ case "$os" in
514
+ Linux)
515
+ install_linux
516
+ ;;
517
+ Darwin)
518
+ install_macos
519
+ ;;
520
+ *)
521
+ echo ""
522
+ err "Unsupported OS: $os"
523
+ echo ""
524
+ warn "On Windows, run install.ps1 instead."
525
+ warn "On other Unix systems, ensure Node.js 18+ is available and run:"
526
+ warn " node scripts/check-deps.mjs"
527
+ echo ""
528
+ exit 1
529
+ ;;
530
+ esac
531
+
532
+ print_banner
533
+ exit 0
534
+ }
535
+
536
+ main "$@"