diff --git a/bun.lock b/bun.lock index c49caaf..412952a 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "packages/codeblog": { "name": "codeblog-app", - "version": "2.2.6", + "version": "2.3.0", "bin": { "codeblog": "./bin/codeblog", }, @@ -28,7 +28,7 @@ "@opentui/core": "^0.1.79", "@opentui/solid": "^0.1.79", "ai": "^6.0.86", - "codeblog-mcp": "^2.1.5", + "codeblog-mcp": "2.2.0", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "^3.1.0", "hono": "4.10.7", @@ -49,11 +49,11 @@ "typescript": "5.8.2", }, "optionalDependencies": { - "codeblog-app-darwin-arm64": "2.2.6", - "codeblog-app-darwin-x64": "2.2.6", - "codeblog-app-linux-arm64": "2.2.6", - "codeblog-app-linux-x64": "2.2.6", - "codeblog-app-windows-x64": "2.2.6", + "codeblog-app-darwin-arm64": "2.3.0", + "codeblog-app-darwin-x64": "2.3.0", + "codeblog-app-linux-arm64": "2.3.0", + "codeblog-app-linux-x64": "2.3.0", + "codeblog-app-windows-x64": "2.3.0", }, }, "packages/util": { @@ -440,15 +440,15 @@ "codeblog-app": ["codeblog-app@workspace:packages/codeblog"], - "codeblog-app-darwin-arm64": ["codeblog-app-darwin-arm64@2.2.6", "", { "os": "darwin", "cpu": "arm64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-LmzgOZz6EVYm7IZucyLQdpAP56hJ8VEtamukcyenXKlrq320J0zyqPMn1WVLtamNMxaYgOkGg8ypUE/siHLo4Q=="], + "codeblog-app-darwin-arm64": ["codeblog-app-darwin-arm64@2.3.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-5mEOC656MGMHxuwLcUKyRtzWSa4nf+Td+5upsw7uH/TyxT9X7PBxepdH1GvU+7u5UbrVBfbHbXMs0FdhgRIycA=="], - "codeblog-app-darwin-x64": ["codeblog-app-darwin-x64@2.2.6", "", { "os": "darwin", "cpu": "x64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-ekCPyRuZeUc1xY4UoSaiA9uK6Pp3Pp1twf48yYB6pLJ/yLK/3ZOpycnzqc8Cvryl81jIGpms3dOUz8Ck/oCkFw=="], + "codeblog-app-darwin-x64": ["codeblog-app-darwin-x64@2.3.0", "", { "os": "darwin", "cpu": "x64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-MPoHR5jOB917ligEvp8TPo/7wJhxKcE/uXjGaEoFnm0KIMwzopLRWnQ3K/hINEOSowhGlY7eaZavJkmvI0dhUg=="], - "codeblog-app-linux-arm64": ["codeblog-app-linux-arm64@2.2.6", "", { "os": "linux", "cpu": "arm64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-1fpe1xYdSmkLm2ivFtRQ0bg28+v3sDW0pTh1sWYjzPDxaEswj6fvjDA8yzv/VAHKcg1tSGnQvlSv1B8DujKGMw=="], + "codeblog-app-linux-arm64": ["codeblog-app-linux-arm64@2.3.0", "", { "os": "linux", "cpu": "arm64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-op9EzVsNlnsl7Q+qRNNiWqa/7nbrE/4Jevzbrnlr6GaA9QRMOetWfpuo732vBpYp3yJWpLnuQxH1by2SZF+oeg=="], - "codeblog-app-linux-x64": ["codeblog-app-linux-x64@2.2.6", "", { "os": "linux", "cpu": "x64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-yhWZb/mQtB8JjD5IOM2nssng8jq1Ch7+ioEHFx3klmh5VMLcAivEVSW4bTFCfLUzqqtM/AY6zsCSz05ClxuqwA=="], + "codeblog-app-linux-x64": ["codeblog-app-linux-x64@2.3.0", "", { "os": "linux", "cpu": "x64", "bin": { "codeblog": "bin/codeblog" } }, "sha512-Pp1P0mVdgiQqGxBeVhra2XL2ZDF5/1fW4SNFJl2m7WUDWwmZEO07mxf8iUgHqoKn9JbjMWANKgOU8c/YEOtScg=="], - "codeblog-app-windows-x64": ["codeblog-app-windows-x64@2.2.6", "", { "os": "win32", "cpu": "x64", "bin": { "codeblog": "bin/codeblog.exe" } }, "sha512-mvyyjfyU3pIj8n6e3DzMB+i12s3vSN/VTaiiPoliegwMNSU7Dh/Cd5P7OHn32qSXHus2DVMJRutyhu3rlJ1Psg=="], + "codeblog-app-windows-x64": ["codeblog-app-windows-x64@2.3.0", "", { "os": "win32", "cpu": "x64", "bin": { "codeblog": "bin/codeblog.exe" } }, "sha512-VIsnHrN19Iqt3b6bhCWLc3lPs4kZhAEToYI3+6PPdM72Rju0DwKgV+lXAkmQWVP860MuTQXBQBz3HUbr3J5m8w=="], "codeblog-mcp": ["codeblog-mcp@2.1.4", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "better-sqlite3": "^12.6.2", "zod": "^3.24.0" }, "bin": { "codeblog-mcp": "dist/index.js" } }, "sha512-RhcuARt4XiJbew2uCKXqIPIDfz19yqX9LFlEIf5tQRrho8Y6iA4hAy8ThiceUxVtzML6cgekLAMbyNR8udF/9A=="], @@ -980,7 +980,7 @@ "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], - "codeblog-app/codeblog-mcp": ["codeblog-mcp@2.1.5", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "better-sqlite3": "^12.6.2", "zod": "^3.24.0" }, "bin": { "codeblog-mcp": "dist/index.js" } }, "sha512-vNfTeB6KhsWN4xsDjnyzwhrdoNGwqIPlx24KmNcPCRAKHPSJ3vrewBLo5A0a5xncr508exTcOTrvEBOJUR+QZw=="], + "codeblog-app/codeblog-mcp": ["codeblog-mcp@2.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "better-sqlite3": "^12.6.2", "zod": "^3.24.0" }, "bin": { "codeblog-mcp": "dist/index.js" } }, "sha512-ebPyJG+l/b6WO9qea0TnDHjUHN9JOoXwrbT18QDwRjJCKjenJ41SD/RY/AabTVOaTkuqGDZvCBM8U1dYfNWc7Q=="], "codeblog-mcp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], diff --git a/install.ps1 b/install.ps1 index b0976f2..cef0670 100644 --- a/install.ps1 +++ b/install.ps1 @@ -6,80 +6,232 @@ $ErrorActionPreference = "Stop" $InstallDir = if ($env:CODEBLOG_INSTALL_DIR) { $env:CODEBLOG_INSTALL_DIR } else { "$env:USERPROFILE\.local\bin" } $BinName = "codeblog" $NpmRegistry = "https://registry.npmjs.org" +$CurrentStep = 0 +$TotalSteps = 4 +$WasInstalled = $false -function Write-Info($msg) { Write-Host "[codeblog] $msg" -ForegroundColor Cyan } -function Write-Ok($msg) { Write-Host "[codeblog] $msg" -ForegroundColor Green } -function Write-Err($msg) { Write-Host "[codeblog] $msg" -ForegroundColor Red; exit 1 } +# ── Logging ───────────────────────────────────────────────────────────────── +function Write-Step($msg) { + $script:CurrentStep++ + Write-Host "" + Write-Host " " -NoNewline + Write-Host ([char]0x25C6) -NoNewline -ForegroundColor Cyan + Write-Host " Step $script:CurrentStep/$TotalSteps" -NoNewline -ForegroundColor White + Write-Host " - " -NoNewline -ForegroundColor DarkGray + Write-Host $msg +} + +function Write-Info($msg) { + Write-Host " " -NoNewline + Write-Host ([char]0x2502) -NoNewline -ForegroundColor Cyan + Write-Host " $msg" +} + +function Write-Ok($msg) { + Write-Host " " -NoNewline + Write-Host ([char]0x2502) -NoNewline -ForegroundColor Green + Write-Host " " -NoNewline + Write-Host ([char]0x2714) -NoNewline -ForegroundColor Green + Write-Host " $msg" +} + +function Write-Warn($msg) { + Write-Host " " -NoNewline + Write-Host ([char]0x2502) -NoNewline -ForegroundColor Yellow + Write-Host " $msg" -ForegroundColor Yellow +} +function Write-Fail($msg) { + Write-Host "" + Write-Host " " -NoNewline + Write-Host ([char]0x2716) -NoNewline -ForegroundColor Red + Write-Host " $msg" -ForegroundColor Red + Write-Host "" + exit 1 +} + +# ── Header ────────────────────────────────────────────────────────────────── +function Write-Header { + Write-Host "" + Write-Host " " -NoNewline + $logo = @( + " ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ " + " ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝ " + " ██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║ ██║██║ ███╗" + " ██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║" + " ╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝███████╗╚██████╔╝╚██████╔╝" + " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ " + ) + foreach ($line in $logo) { + Write-Host " $line" -ForegroundColor Cyan + } + Write-Host "" + Write-Host " AI-powered coding forum - codeblog.ai" -ForegroundColor DarkGray + Write-Host " ────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray +} + +# ── Platform detection ────────────────────────────────────────────────────── +function Get-Platform { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + if ($arch -eq "x64") { return "x64" } + elseif ($arch -eq "arm64") { return "arm64" } + else { Write-Fail "Unsupported architecture: $arch" } +} + +function Format-Platform($arch) { + switch ($arch) { + "x64" { return "Windows (x86_64)" } + "arm64" { return "Windows (ARM64)" } + default { return "Windows ($arch)" } + } +} + +# ── Version fetching ──────────────────────────────────────────────────────── function Get-LatestVersion { $resp = Invoke-RestMethod -Uri "$NpmRegistry/codeblog-app/latest" -ErrorAction Stop return $resp.version } -function Install-Binary { - $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() - if ($arch -eq "x64") { $arch = "x64" } - elseif ($arch -eq "arm64") { $arch = "arm64" } - else { Write-Err "Unsupported architecture: $arch" } - +# ── Binary install ────────────────────────────────────────────────────────── +function Install-Binary($arch) { $pkg = "codeblog-app-windows-$arch" + $binPath = Join-Path $InstallDir "$BinName.exe" + + if (Test-Path $binPath) { + $script:WasInstalled = $true + } - Write-Info "Checking latest version..." $version = Get-LatestVersion - if (-not $version) { Write-Err "Failed to fetch latest version" } - Write-Info "Latest version: $version" + if (-not $version) { Write-Fail "Failed to fetch latest version from npm registry" } + Write-Ok "Latest version: v$version" + + # Check if already up to date + if ($script:WasInstalled) { + try { + $currentVersion = & $binPath --version 2>$null + if ($currentVersion -eq $version) { + Write-Ok "Already up to date (v$version)" + return + } + Write-Info "Updating: $currentVersion -> v$version" + } catch {} + } - Write-Info "Downloading $pkg@$version..." $url = "$NpmRegistry/$pkg/-/$pkg-$version.tgz" - $tmpDir = Join-Path $env:TEMP "codeblog-install-$(Get-Random)" New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null try { $tgz = Join-Path $tmpDir "pkg.tgz" + Write-Info "Downloading ${pkg}@${version}..." Invoke-WebRequest -Uri $url -OutFile $tgz -ErrorAction Stop + Write-Ok "Downloaded" - Write-Info "Installing..." + Write-Info "Extracting..." tar -xzf $tgz -C $tmpDir New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null - Copy-Item -Force (Join-Path $tmpDir "package\bin\codeblog.exe") (Join-Path $InstallDir "$BinName.exe") - - Write-Ok "Installed codeblog v$version to $InstallDir\$BinName.exe" + Copy-Item -Force (Join-Path $tmpDir "package\bin\codeblog.exe") $binPath + Write-Ok "Installed to $binPath" } finally { Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue } } +# ── PATH setup ────────────────────────────────────────────────────────────── function Setup-Path { $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") - if ($currentPath -split ";" | Where-Object { $_ -eq $InstallDir }) { return } + if ($currentPath -split ";" | Where-Object { $_ -eq $InstallDir }) { + Write-Ok "Already in PATH" + return + } [Environment]::SetEnvironmentVariable("Path", "$InstallDir;$currentPath", "User") $env:PATH = "$InstallDir;$env:PATH" - Write-Info "Added $InstallDir to user PATH" + Write-Ok "Added to user PATH" } -function Main { +# ── Outro ─────────────────────────────────────────────────────────────────── +function Write-OutroFresh { Write-Host "" - Write-Host " CodeBlog CLI Installer" -ForegroundColor Cyan + Write-Host " " -NoNewline + Write-Host ([char]0x25C7) -NoNewline -ForegroundColor Green + Write-Host " Installation complete!" -ForegroundColor Green Write-Host "" + Write-Host " ───────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + Write-Host " Welcome to " -NoNewline + Write-Host "CodeBlog" -NoNewline -ForegroundColor Cyan + Write-Host " -- the AI-powered coding forum." + Write-Host "" + Write-Host " Your AI agent analyzes your coding sessions and shares" + Write-Host " insights with the community. Other developers read," + Write-Host " vote, and discuss -- all powered by real coding context." + Write-Host "" + Write-Host " Let's get you set up. The setup wizard will walk you" + Write-Host " through connecting your account and creating your agent." + Write-Host "" + Write-Host " ───────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" +} - Write-Info "Platform: windows-$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower())" - - Install-Binary - Setup-Path - +function Write-OutroUpdate { Write-Host "" - Write-Ok "codeblog installed successfully!" + Write-Host " " -NoNewline + Write-Host ([char]0x25C7) -NoNewline -ForegroundColor Green + Write-Host " Installation complete!" -ForegroundColor Green Write-Host "" - Write-Host " Get started:" -ForegroundColor White + Write-Host " " -NoNewline; Write-Host "codeblog" -NoNewline -ForegroundColor Cyan; Write-Host " Launch interactive TUI" + Write-Host " " -NoNewline; Write-Host "codeblog setup" -NoNewline -ForegroundColor Cyan; Write-Host " First-time setup" + Write-Host " " -NoNewline; Write-Host "codeblog --help" -NoNewline -ForegroundColor Cyan; Write-Host " See all commands" Write-Host "" - Write-Host " codeblog " -NoNewline -ForegroundColor Cyan; Write-Host "Launch interactive TUI" - Write-Host " codeblog --help " -NoNewline -ForegroundColor Cyan; Write-Host "See all commands" + Write-Host " " -NoNewline + Write-Host ([char]0x25B2) -NoNewline -ForegroundColor Yellow + Write-Host " Restart your terminal for PATH changes to take effect." -ForegroundColor Yellow Write-Host "" - Write-Host " Note: Restart your terminal for PATH changes to take effect." -ForegroundColor Yellow +} + +# ── Launch prompt ─────────────────────────────────────────────────────────── +function Prompt-Launch { + Write-Host " " -NoNewline + Write-Host ([char]0x25C6) -NoNewline -ForegroundColor Cyan + Write-Host " Press Enter to launch codeblog" -NoNewline -ForegroundColor White + Write-Host " (or Ctrl+C to exit)" -ForegroundColor DarkGray Write-Host "" + Read-Host | Out-Null + + $binPath = Join-Path $InstallDir "$BinName.exe" + & $binPath +} + +# ── Main ──────────────────────────────────────────────────────────────────── +function Main { + Write-Header + + # Step 1: Detect platform + Write-Step "Detecting platform" + $arch = Get-Platform + $platformDisplay = Format-Platform $arch + Write-Ok "$platformDisplay (windows-$arch)" + + # Step 2: Download and install + Write-Step "Installing codeblog" + Install-Binary $arch + + # Step 3: Configure PATH + Write-Step "Configuring PATH" + Setup-Path + + # Step 4: Post-install + Write-Step "Post-install" + if ($script:WasInstalled) { + Write-Ok "Update complete" + Write-OutroUpdate + } else { + Write-Ok "Ready to go" + Write-OutroFresh + Prompt-Launch + } } Main diff --git a/install.sh b/install.sh index 191f926..99371bc 100755 --- a/install.sh +++ b/install.sh @@ -9,19 +9,73 @@ BIN_NAME="codeblog" NPM_REGISTRY="https://registry.npmjs.org" RUN_ONBOARD="auto" WAS_INSTALLED=0 +CURRENT_STEP=0 +TOTAL_STEPS=4 +# ── Colors ────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' +BLUE='\033[0;34m' +DIM='\033[2m' BOLD='\033[1m' NC='\033[0m' -info() { echo -e "${CYAN}[codeblog]${NC} $1"; } -success() { echo -e "${GREEN}[codeblog]${NC} $1"; } -warn() { echo -e "${YELLOW}[codeblog]${NC} $1"; } -error() { echo -e "${RED}[codeblog]${NC} $1" >&2; exit 1; } +# ── Logging ───────────────────────────────────────────────────────────────── +info() { echo -e " ${CYAN}│${NC} $1"; } +success() { echo -e " ${GREEN}│${NC} ${GREEN}✔${NC} $1"; } +warn() { echo -e " ${YELLOW}│${NC} ${YELLOW}▲${NC} $1"; } +fail() { echo -e "\n ${RED}✖${NC} $1\n" >&2; exit 1; } +step() { + CURRENT_STEP=$((CURRENT_STEP + 1)) + echo "" + echo -e " ${CYAN}◆${NC} ${BOLD}Step ${CURRENT_STEP}/${TOTAL_STEPS}${NC} ${DIM}─${NC} $1" +} + +# ── Spinner ───────────────────────────────────────────────────────────────── +SPINNER_PID="" + +spinner_start() { + local msg="$1" + if [ ! -t 1 ]; then + info "$msg" + return + fi + ( + local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") + local i=0 + while true; do + printf "\r ${CYAN}│${NC} ${BLUE}${frames[$i]}${NC} %s" "$msg" + i=$(( (i + 1) % ${#frames[@]} )) + sleep 0.08 + done + ) & + SPINNER_PID=$! +} + +spinner_stop() { + local msg="$1" + if [ -n "$SPINNER_PID" ]; then + kill "$SPINNER_PID" 2>/dev/null || true + wait "$SPINNER_PID" 2>/dev/null || true + SPINNER_PID="" + printf "\r\033[K" + fi + echo -e " ${GREEN}│${NC} ${GREEN}✔${NC} $msg" +} + +# ── Cleanup ───────────────────────────────────────────────────────────────── +cleanup() { + if [ -n "$SPINNER_PID" ]; then + kill "$SPINNER_PID" 2>/dev/null || true + wait "$SPINNER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# ── Usage ─────────────────────────────────────────────────────────────────── usage() { cat </dev/null | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4 + local response + response="$(curl -fsSL "$NPM_REGISTRY/codeblog-app/latest" 2>/dev/null)" || return 1 + echo "$response" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4 } +# ── Binary install ────────────────────────────────────────────────────────── install_binary() { local platform="$1" local pkg="codeblog-app-${platform}" + if [ -x "$INSTALL_DIR/$BIN_NAME" ]; then WAS_INSTALLED=1 fi - info "Checking latest version..." + spinner_start "Fetching latest version..." local version - version="$(get_latest_version)" + version="$(get_latest_version)" || true if [ -z "$version" ]; then - error "Failed to fetch latest version from npm" + spinner_stop "Version check" + fail "Failed to fetch latest version from npm registry" + fi + spinner_stop "Latest version: ${BOLD}v${version}${NC}" + + # Check if already up to date + if [ "$WAS_INSTALLED" -eq 1 ]; then + local current_version + current_version="$("$INSTALL_DIR/$BIN_NAME" --version 2>/dev/null || echo "")" + if [ "$current_version" = "$version" ]; then + success "Already up to date ${DIM}(v${version})${NC}" + return 0 + fi + info "Updating: ${DIM}${current_version:-unknown}${NC} → ${BOLD}v${version}${NC}" fi - info "Latest version: $version" - info "Downloading $pkg@$version..." local tarball_url="$NPM_REGISTRY/$pkg/-/$pkg-$version.tgz" - local tmpdir tmpdir="$(mktemp -d)" - trap "rm -rf $tmpdir" EXIT - curl -fsSL "$tarball_url" -o "$tmpdir/pkg.tgz" || error "Failed to download binary for $platform" + spinner_start "Downloading ${pkg}@${version}..." + curl -fsSL "$tarball_url" -o "$tmpdir/pkg.tgz" || fail "Failed to download binary for $platform" + spinner_stop "Downloaded ${DIM}(${pkg}@${version})${NC}" - info "Installing..." + spinner_start "Extracting and installing..." mkdir -p "$INSTALL_DIR" tar -xzf "$tmpdir/pkg.tgz" -C "$tmpdir" - cp "$tmpdir/package/bin/$BIN_NAME" "$INSTALL_DIR/$BIN_NAME" chmod +x "$INSTALL_DIR/$BIN_NAME" + # macOS code signing if [ "$(uname -s)" = "Darwin" ] && command -v codesign >/dev/null 2>&1; then codesign --sign - --force "$INSTALL_DIR/$BIN_NAME" 2>/dev/null || true fi + spinner_stop "Installed to ${DIM}${INSTALL_DIR}/${BIN_NAME}${NC}" - success "Installed codeblog v$version to $INSTALL_DIR/$BIN_NAME" + rm -rf "$tmpdir" } +# ── Onboarding ────────────────────────────────────────────────────────────── should_run_onboard() { - if [ "$RUN_ONBOARD" = "no" ]; then - return 1 - fi - if [ "$RUN_ONBOARD" = "yes" ]; then - return 0 - fi - if [ "$WAS_INSTALLED" -eq 0 ]; then - return 0 - fi + if [ "$RUN_ONBOARD" = "no" ]; then return 1; fi + if [ "$RUN_ONBOARD" = "yes" ]; then return 0; fi + # Auto: only on fresh install + if [ "$WAS_INSTALLED" -eq 0 ]; then return 0; fi return 1 } run_onboard() { - if ! should_run_onboard; then - return - fi + if ! should_run_onboard; then return; fi if [ ! -t 1 ] || [ ! -r /dev/tty ]; then - warn "Skipping first-time setup (no interactive TTY). Run: codeblog setup" + warn "Skipping first-time setup (no interactive TTY)" + info "Run later: ${CYAN}codeblog setup${NC}" return fi echo "" info "Starting first-time setup wizard..." + echo "" if ! "$INSTALL_DIR/$BIN_NAME" setup < /dev/tty > /dev/tty 2>&1; then - warn "Setup was not completed. You can rerun anytime: codeblog setup" + warn "Setup was not completed. Run anytime: ${CYAN}codeblog setup${NC}" fi } +# ── PATH setup ────────────────────────────────────────────────────────────── setup_path() { - if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then - return - fi + if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then return; fi local shell_rc case "${SHELL:-/bin/bash}" in - */zsh) shell_rc="$HOME/.zshrc" ;; + */zsh) shell_rc="$HOME/.zshrc" ;; */bash) shell_rc="$HOME/.bashrc" ;; - *) shell_rc="$HOME/.profile" ;; + *) shell_rc="$HOME/.profile" ;; esac if ! grep -q "codeblog" "$shell_rc" 2>/dev/null; then echo "" >> "$shell_rc" echo "# codeblog" >> "$shell_rc" echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$shell_rc" - info "Added $INSTALL_DIR to PATH in $shell_rc" + success "Added to PATH in ${DIM}${shell_rc}${NC}" fi } -main() { - parse_args "$@" +# ── Header ────────────────────────────────────────────────────────────────── +print_header() { + echo "" + echo -e " ${CYAN}${BOLD}" + echo -e " ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ " + echo -e " ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝ " + echo -e " ██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║ ██║██║ ███╗" + echo -e " ██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║" + echo -e " ╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝███████╗╚██████╔╝╚██████╔╝" + echo -e " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ " + echo -e " ${NC}" + echo -e " ${DIM}AI-powered coding forum — codeblog.ai${NC}" + echo -e " ${DIM}────────────────────────────────────────────────────────────────${NC}" +} + +# ── Outro ─────────────────────────────────────────────────────────────────── +print_outro() { + local is_fresh=$1 # "fresh" or "update" echo "" - echo -e "${CYAN} ██████╗ ██████╗ ██████╗ ███████╗${BOLD}██████╗ ██╗ ██████╗ ██████╗ ${NC}" - echo -e "${CYAN} ██╔════╝██╔═══██╗██╔══██╗██╔════╝${BOLD}██╔══██╗██║ ██╔═══██╗██╔════╝ ${NC}" - echo -e "${CYAN} ██║ ██║ ██║██║ ██║█████╗ ${BOLD}██████╔╝██║ ██║ ██║██║ ███╗${NC}" - echo -e "${CYAN} ██║ ██║ ██║██║ ██║██╔══╝ ${BOLD}██╔══██╗██║ ██║ ██║██║ ██║${NC}" - echo -e "${CYAN} ╚██████╗╚██████╔╝██████╔╝███████╗${BOLD}██████╔╝███████╗╚██████╔╝╚██████╔╝${NC}" - echo -e "${CYAN} ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${BOLD}╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ${NC}" + echo -e " ${GREEN}◇${NC} ${GREEN}${BOLD}Installation complete!${NC}" echo "" + if [ "$is_fresh" = "fresh" ]; then + echo -e " ${DIM}───────────────────────────────────────────────────────────${NC}" + echo "" + echo -e " Welcome to ${CYAN}${BOLD}CodeBlog${NC} -- the AI-powered coding forum." + echo "" + echo -e " Your AI agent analyzes your coding sessions and shares" + echo -e " insights with the community. Other developers read," + echo -e " vote, and discuss -- all powered by real coding context." + echo "" + echo -e " Let's get you set up. The setup wizard will walk you" + echo -e " through connecting your account and creating your agent." + echo "" + echo -e " ${DIM}───────────────────────────────────────────────────────────${NC}" + else + echo "" + echo -e " ${CYAN}codeblog${NC} Launch interactive TUI" + echo -e " ${CYAN}codeblog setup${NC} First-time setup" + echo -e " ${CYAN}codeblog --help${NC} See all commands" + fi + + if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + echo "" + echo -e " ${YELLOW}▲${NC} Restart your terminal or run:" + local shell_name + case "${SHELL:-/bin/bash}" in + */zsh) shell_name=".zshrc" ;; + */bash) shell_name=".bashrc" ;; + *) shell_name=".profile" ;; + esac + echo -e " ${DIM}source ~/${shell_name}${NC}" + fi + echo "" +} + +# ── Launch prompt ─────────────────────────────────────────────────────────── +prompt_launch() { + # Only prompt on fresh install with TTY + if [ "$WAS_INSTALLED" -eq 1 ]; then return; fi + if [ "$RUN_ONBOARD" = "no" ]; then return; fi + if [ ! -t 1 ] || [ ! -r /dev/tty ]; then + info "Run ${CYAN}codeblog${NC} to get started." + return + fi + + echo -e " ${CYAN}◆${NC} ${BOLD}Press Enter to launch codeblog${NC} ${DIM}(or Ctrl+C to exit)${NC}" + echo "" + read -r < /dev/tty + + # Source PATH if needed so the binary is found + export PATH="$INSTALL_DIR:$PATH" + exec "$INSTALL_DIR/$BIN_NAME" < /dev/tty > /dev/tty 2>&1 +} + +# ── Main ──────────────────────────────────────────────────────────────────── +main() { + parse_args "$@" + print_header + + # Step 1: Detect platform + step "Detecting platform" local platform platform="$(detect_platform)" - info "Platform: $platform" + local platform_display + platform_display="$(format_platform "$platform")" + success "${platform_display} ${DIM}(${platform})${NC}" + # Step 2: Download and install + step "Installing codeblog" install_binary "$platform" - setup_path - run_onboard - echo "" - success "codeblog installed successfully!" - echo "" - echo -e " ${BOLD}Get started:${NC}" - echo "" - echo -e " ${CYAN}codeblog${NC} Launch interactive TUI" - echo -e " ${CYAN}codeblog setup${NC} Run first-time setup wizard" - echo -e " ${CYAN}codeblog --help${NC} See all commands" - echo "" - echo -e " ${YELLOW}Note:${NC} Restart your terminal or run: source ~/.zshrc" - echo "" + # Step 3: Configure PATH + step "Configuring PATH" + if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then + success "Already in PATH" + else + setup_path + fi + + # Step 4: Post-install + step "Post-install" + if [ "$WAS_INSTALLED" -eq 1 ]; then + success "Update complete" + else + success "Ready to go" + fi + + # Show outro — different for fresh vs update + if [ "$WAS_INSTALLED" -eq 0 ]; then + print_outro "fresh" + prompt_launch + else + print_outro "update" + fi } main "$@" diff --git a/packages/codeblog/package.json b/packages/codeblog/package.json index 3a84d1a..5b0599f 100644 --- a/packages/codeblog/package.json +++ b/packages/codeblog/package.json @@ -73,7 +73,7 @@ "@opentui/core": "^0.1.79", "@opentui/solid": "^0.1.79", "ai": "^6.0.86", - "codeblog-mcp": "^2.1.5", + "codeblog-mcp": "2.2.0", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "^3.1.0", "hono": "4.10.7", diff --git a/packages/codeblog/script/build.ts b/packages/codeblog/script/build.ts index 73ef429..a5ae294 100644 --- a/packages/codeblog/script/build.ts +++ b/packages/codeblog/script/build.ts @@ -53,6 +53,7 @@ for (const item of targets) { tsconfig: "./tsconfig.json", plugins: [solidPlugin], sourcemap: "none", + external: ["better-sqlite3"], compile: { autoloadBunfig: false, autoloadDotenv: false, diff --git a/packages/codeblog/src/mcp/client.ts b/packages/codeblog/src/mcp/client.ts index 7983ce5..c22f8b0 100644 --- a/packages/codeblog/src/mcp/client.ts +++ b/packages/codeblog/src/mcp/client.ts @@ -1,103 +1,48 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" -import { resolve, dirname } from "path" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import { createServer } from "codeblog-mcp" import { Log } from "../util/log" const log = Log.create({ service: "mcp" }) -const CONNECTION_TIMEOUT_MS = 30_000 - let client: Client | null = null -let transport: StdioClientTransport | null = null +let clientTransport: InstanceType | null = null let connecting: Promise | null = null -function getMcpBinaryPath(): string { - try { - const resolved = require.resolve("codeblog-mcp/dist/index.js") - return resolved - } catch { - return resolve(dirname(new URL(import.meta.url).pathname), "../../node_modules/codeblog-mcp/dist/index.js") - } -} - -function withTimeout(promise: Promise, ms: number, label: string): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms) - promise.then( - (v) => { clearTimeout(timer); resolve(v) }, - (e) => { clearTimeout(timer); reject(e) }, - ) - }) -} - async function connect(): Promise { if (client) return client - - // If another caller is already connecting, reuse that promise if (connecting) return connecting connecting = (async (): Promise => { - const mcpPath = getMcpBinaryPath() - log.info("connecting", { path: mcpPath }) - - const env: Record = {} - for (const [k, v] of Object.entries(process.env)) { - if (v !== undefined) env[k] = v - } + log.info("connecting via InMemoryTransport") - const t = new StdioClientTransport({ - command: "node", - args: [mcpPath], - env, - stderr: "pipe", - }) - - // Capture subprocess stderr for diagnostics - const stderrChunks: string[] = [] - const stderrStream = t.stderr - if (stderrStream) { - stderrStream.on("data", (chunk: Buffer) => { - const line = chunk.toString().trim() - if (line) { - stderrChunks.push(line) - log.warn("mcp-subprocess-stderr", { output: line }) - } - }) - } + const server = createServer() + const [ct, serverTransport] = InMemoryTransport.createLinkedPair() const c = new Client({ name: "codeblog-cli", version: "2.0.0" }) - // Log when the subprocess exits unexpectedly c.onclose = () => { - log.warn("mcp-connection-closed", { - stderrTail: stderrChunks.slice(-5).join("\n") || "(empty)", - }) - // Clear module-level refs so next call will reconnect + log.warn("mcp-connection-closed") client = null - transport = null + clientTransport = null } try { - await withTimeout(c.connect(t), CONNECTION_TIMEOUT_MS, "MCP connection") + await server.connect(serverTransport) + await c.connect(ct) } catch (err) { const errMsg = err instanceof Error ? err.message : String(err) - log.error("mcp-connect-failed", { - error: errMsg, - stderrTail: stderrChunks.slice(-5).join("\n") || "(empty)", - }) - // Clean up on failure so next call can retry - await t.close().catch(() => {}) + log.error("mcp-connect-failed", { error: errMsg }) + await ct.close().catch(() => {}) throw err } log.info("connected", { server: c.getServerVersion()?.name, version: c.getServerVersion()?.version, - pid: t.pid, }) - // Only assign to module-level vars after successful connection - transport = t + clientTransport = ct client = c return c })() @@ -105,17 +50,12 @@ async function connect(): Promise { try { return await connecting } catch (err) { - // Reset connecting so next call can retry connecting = null throw err } } export namespace McpBridge { - /** - * Call an MCP tool by name with arguments. - * Returns the text content from the tool result. - */ export async function callTool( name: string, args: Record = {}, @@ -140,9 +80,6 @@ export namespace McpBridge { return extractText(result) } - /** - * Call an MCP tool and parse the result as JSON. - */ export async function callToolJSON( name: string, args: Record = {}, @@ -155,23 +92,17 @@ export namespace McpBridge { } } - /** - * List all available MCP tools. - */ export async function listTools() { const c = await connect() return c.listTools() } - /** - * Disconnect the MCP client and kill the subprocess. - */ export async function disconnect(): Promise { - log.info("disconnecting", { hadClient: !!client, hadTransport: !!transport }) + log.info("disconnecting", { hadClient: !!client }) connecting = null - if (transport) { - await transport.close().catch(() => {}) - transport = null + if (clientTransport) { + await clientTransport.close().catch(() => {}) + clientTransport = null } client = null }