diff --git a/.trae/documents/Implement AI-Powered Smart Session Compression.md b/.trae/documents/Implement AI-Powered Smart Session Compression.md new file mode 100644 index 0000000..cda2eb8 --- /dev/null +++ b/.trae/documents/Implement AI-Powered Smart Session Compression.md @@ -0,0 +1,140 @@ +## Implementation Plan: Enhanced Session Compaction System (9 High-Priority Fixes) + +### Phase 1: Core Foundation (Types & Configuration) + +**NEW: `packages/ui/src/stores/session-compaction.ts`** + +1. **Compaction Types & Interfaces** + - `CompactionMessageFlags`: summary, mode, provenance flags + - `StructuredSummary`: Tier A/B schema with what_was_done, files, current_state, key_decisions, next_steps, blockers, artifacts, tags, provenance + - `CompactionEvent`: Audit trail with event_id, timestamp, actor, trigger_reason, token_before/after, model_used, cost_estimate + - `CompactionConfig`: autoCompactEnabled, autoCompactThreshold, compactPreserveWindow, pruneReclaimThreshold, userPreference, undoRetentionWindow + - `SessionCompactingHook`: Plugin contract for domain-specific rules + +2. **Configuration Store** + - Default config: auto=80%, preserve=40k tokens, prune_threshold=20k, preference="ask" + - Export functions: `getCompactionConfig()`, `updateCompactionConfig()` + +### Phase 2: Overflow Detection Engine + +**MODIFY: `packages/ui/src/stores/session-compaction.ts`** + +3. **Token Monitoring Functions** + - `isOverflowDetected(usage, modelLimit)`: Check if usage >= threshold% + - `shouldPruneToolOutputs(usage)`: Check if tool outputs > reclaim threshold + - `estimateTokenReduction(before, after)`: Calculate % reduction + +4. **Audit Trail System** + - `recordCompactionEvent(sessionId, event)`: Append-only to audit log + - `getCompactionHistory(sessionId)`: Retrieve audit trail + - `exportAuditLog()`: For compliance/debugging + +### Phase 3: Secrets Detection & Sanitization + +**NEW: `packages/ui/src/lib/secrets-detector.ts`** + +5. **Secrets Detector** + - Pattern matching for: api keys, passwords, tokens, secrets, credentials + - `redactSecrets(content)`: Returns { clean: string, redactions: { path, reason }[] } + - Placeholder format: `[REDACTED: {reason}]` + +### Phase 4: AI-Powered Compaction Agent + +**MODIFY: `packages/ui/src/stores/session-compaction.ts`** + +6. **Compaction Agent Integration** + - `COMPACTION_AGENT_PROMPT`: Structured prompt with instructions + - `generateCompactionSummary(instanceId, sessionId, window)`: Call sendMessage() to get AI summary + - Parse response into Tier A (human) and Tier B (structured JSON) + +7. **Execute Compaction** + - `executeCompaction(instanceId, sessionId, mode)`: Main compaction orchestration + - Steps: enumerate → plugin hooks → AI summary → sanitize → store → prune → audit + - Returns: preview, token estimate, compaction event + +### Phase 5: Pruning Engine + +**MODIFY: `packages/ui/src/stores/session-compaction.ts`** + +8. **Sliding Window Pruning** + - `pruneToolOutputs(instanceId, sessionId)`: Maintain queue, prune oldest > threshold + - `isToolOutput(part)`: Classify build logs, test logs, large JSON + +### Phase 6: Undo & Rehydration + +**MODIFY: `packages/ui/src/stores/session-compaction.ts`** + +9. **Undo System** + - `undoCompaction(sessionId, compactionEventId)`: Rehydrate within retention window + - `getCompactedSessionSummary(sessionId)`: Retrieve stored summary + - `expandCompactedView(sessionId)`: Return archived messages + +### Phase 7: Integration + +**MODIFY: `packages/ui/src/stores/session-events.ts`** + +10. **Auto-Compact Trigger** + - Monitor `EventSessionUpdated` for token usage + - Trigger based on user preference (auto/ask/never) + - Call existing `showConfirmDialog()` with compaction preview + +**MODIFY: `packages/ui/src/stores/session-actions.ts`** + +11. **Replace compactSession** + - Use new `executeCompaction()` function + - Support both "prune" and "compact" modes + +### Phase 8: Schema Validation + +**NEW: `packages/ui/src/lib/compaction-validation.ts`** + +12. **Schema Validation** + - `validateStructuredSummary(summary)`: Zod schema for Tier B + - `validateCompactionEvent(event)`: Zod schema for audit trail + - `ValidationErrors` type with path, message, code + +### Phase 9: CI Tests + +**NEW: `packages/ui/src/stores/session-compaction.test.ts`** + +13. **Test Coverage** + - `test_overflow_detection`: Verify threshold calculation + - `test_secrets_redaction`: Verify patterns are caught + - `test_compaction_execution`: Full compaction flow + - `test_undo_rehydration`: Verify restore works + - `test_plugin_hooks`: Verify custom rules apply + +### Phase 10: Canary Rollout + +**MODIFY: `packages/ui/src/stores/session-compaction.ts`** + +14. **Feature Flag** + - `ENABLE_SMART_COMPACTION`: Environment variable or config flag + - Default: `false` for canary, set to `true` for full rollout + - Graceful degradation: fall back to simple compaction if disabled + +--- + +## Implementation Order (Priority) + +1. **P0 - Foundation**: Types, config, schema validation (1-2, 12) +2. **P0 - Core Engine**: Overflow detection, secrets detector (3-5) +3. **P0 - AI Integration**: Compaction agent, execute function (6-7) +4. **P1 - Pruning**: Tool output classification, sliding window (8) +5. **P1 - Undo**: Rehydration system (9) +6. **P1 - Integration**: Session events, actions integration (10-11) +7. **P2 - Tests**: CI test coverage (13) +8. **P2 - Rollout**: Feature flag, canary enablement (14) + +--- + +## Success Criteria + +- ✅ AI generates meaningful summaries (not just "0 AI responses") +- ✅ Overflow detected before context limit exceeded +- ✅ Secrets are redacted before storage +- ✅ Audit trail tracks every compaction +- ✅ Undo works within retention window +- ✅ Schema validation prevents corrupt data +- ✅ CI tests ensure reliability +- ✅ Canary flag allows safe rollout \ No newline at end of file diff --git a/.trae/documents/STRICT FIX ORDER_ Installers_Launchers Audit (8 Fixes).md b/.trae/documents/STRICT FIX ORDER_ Installers_Launchers Audit (8 Fixes).md new file mode 100644 index 0000000..ea9f97e --- /dev/null +++ b/.trae/documents/STRICT FIX ORDER_ Installers_Launchers Audit (8 Fixes).md @@ -0,0 +1,391 @@ +# FINAL EXECUTION PLAN - 8 Fixes with Proof Deliverables + +## Fix Summary + +| Fix | Files | Deliverables | +|------|--------|-------------| +| C1 | Install-Windows.bat, Install-Mac.sh, Install-Linux.sh, Launch-Windows.bat, Launch-Dev-Windows.bat, Launch-Unix.sh | 9 path diffs + `dir packages\ui\dist` verification | +| C2 | packages/ui/vite.config.ts, Launch-Dev-Windows.bat, Launch-Dev-Unix.sh (NEW) | vite.config.ts diff + 2 launcher diffs + Vite log showing port | +| C3 | Launch-Windows.bat, Launch-Dev-Windows.bat, Launch-Unix.sh | 3 CLI_PORT env var diffs + server log showing port | +| C4 | Install-Windows.bat, Install-Mac.sh, Install-Linux.sh | 3 download/checksum diffs + log verification | +| C5 | Install-Windows.bat | Certutil parsing diff + hash output | +| C6 | Install-Windows.bat, Install-Mac.sh, Install-Linux.sh | 3 TARGET_DIR/BIN_DIR diffs + fallback test output | +| C7 | Install-Windows.bat, Install-Mac.sh, Install-Linux.sh | 3 health check path diffs + health check output | +| C8 | Launch-Dev-Windows.bat | 1 path diff + grep verification | + +--- + +## C1: UI Build Path Correction + +**Files:** Install-Windows.bat (lines 194, 245), Install-Mac.sh (204, 256), Install-Linux.sh (220, 272), Launch-Windows.bat (185), Launch-Dev-Windows.bat (144), Launch-Unix.sh (178) + +**Diff:** +```batch +# All Windows scripts - replace: +packages\ui\src\renderer\dist +# With: +packages\ui\dist + +# All Unix scripts - replace: +packages/ui/src/renderer/dist +# With: +packages/ui/dist +``` + +**Verification:** `dir packages\ui\dist` + `dir packages\ui\dist\index.html` + +--- + +## C2: Vite Dev Server Port Wiring + +**File 1: packages/ui/vite.config.ts (line 23)** + +```diff +- server: { +- port: 3000, +- }, ++ server: { ++ port: Number(process.env.VITE_PORT ?? 3000), ++ }, +``` + +**File 2: Launch-Dev-Windows.bat (after port detection)** + +```diff +- start "NomadArch UI" cmd /k "cd /d \"%~dp0packages\ui\" && set VITE_PORT=!UI_PORT! && npm run dev" ++ start "NomadArch UI" cmd /k "cd /d \"%~dp0packages\ui\" && set VITE_PORT=!UI_PORT! && npm run dev -- --port !UI_PORT!" +``` + +**File 3: Launch-Dev-Unix.sh (NEW FILE)** + +```bash +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Port detection +DEFAULT_SERVER_PORT=3001 +DEFAULT_UI_PORT=5173 +SERVER_PORT=$DEFAULT_SERVER_PORT +UI_PORT=$DEFAULT_UI_PORT + +echo "[INFO] Detecting available ports..." + +# Server port (3001-3050) +for port in {3001..3050}; do + if ! lsof -i :$port -sTCP:LISTEN -t > /dev/null 2>&1; then + SERVER_PORT=$port + break + fi +done + +# UI port (5173-5200) +for port in {5173..5200}; do + if ! lsof -i :$port -sTCP:LISTEN -t > /dev/null 2>&1; then + UI_PORT=$port + break + fi +done + +echo "[INFO] Using server port: $SERVER_PORT" +echo "[INFO] Using UI port: $UI_PORT" + +# Start server with CLI_PORT +echo "[INFO] Starting Backend Server..." +cd packages/server +export CLI_PORT=$SERVER_PORT +npm run dev & +SERVER_PID=$! + +sleep 3 + +# Start UI with VITE_PORT + --port flag +echo "[INFO] Starting Frontend UI..." +cd "$SCRIPT_DIR/packages/ui" +export VITE_PORT=$UI_PORT +npm run dev -- --port $UI_PORT & +UI_PID=$! + +sleep 3 + +# Start Electron +echo "[INFO] Starting Electron..." +cd "$SCRIPT_DIR/packages/electron-app" +npm run dev + +# Cleanup on exit +trap "kill $SERVER_PID $UI_PID 2>/dev/null; exit" INT TERM +``` + +**Verification:** Vite log output showing `Local: http://localhost:` + +--- + +## C3: Server Port Environment Variable + +**Launch-Windows.bat (before npm run dev:electron):** + +```diff +echo [INFO] Starting NomadArch... +set SERVER_URL=http://localhost:!SERVER_PORT! +echo [INFO] Server will run on http://localhost:!SERVER_PORT! ++ ++ set CLI_PORT=!SERVER_PORT! +call npm run dev:electron +``` + +**Launch-Dev-Windows.bat (server start command):** + +```diff +echo [INFO] Starting Backend Server... +- start "NomadArch Server" cmd /k "cd /d \"%~dp0packages\server\" && npm run dev" ++ start "NomadArch Server" cmd /k "cd /d \"%~dp0packages\server\" && set CLI_PORT=!SERVER_PORT! && npm run dev" +``` + +**Launch-Unix.sh (before npm run dev:electron):** + +```bash +echo -e "${GREEN}[INFO]${NC} Starting NomadArch..." +SERVER_URL="http://localhost:$SERVER_PORT" +echo -e "${GREEN}[INFO]${NC} Server will run on http://localhost:$SERVER_PORT" + +export CLI_PORT=$SERVER_PORT +npm run dev:electron +``` + +**Verification:** Server log showing `CodeNomad Server is ready at http://127.0.0.1:` + +--- + +## C4: OpenCode Download with Dynamic Version + Checksum + +**Install-Windows.bat (lines 165-195):** + +```batch +set TARGET_DIR=%SCRIPT_DIR% +set BIN_DIR=%TARGET_DIR%\bin +if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" + +:: Resolve latest version from GitHub API +echo [INFO] Resolving latest OpenCode version... +for /f "delims=" %%v in ('curl -s https://api.github.com/repos/sst/opencode/releases/latest ^| findstr "\"tag_name\""') do ( + set OPENCODE_VERSION=%%v + set OPENCODE_VERSION=!OPENCODE_VERSION:~18,-2! +) + +set OPENCODE_BASE=https://github.com/sst/opencode/releases/download/v%OPENCODE_VERSION% +set OPENCODE_URL=%OPENCODE_BASE%/opencode-windows-%ARCH%.exe +set CHECKSUM_URL=%OPENCODE_BASE%/checksums.txt + +if exist "%BIN_DIR%\opencode.exe" ( + echo [OK] OpenCode binary already exists +) else ( + echo [INFO] Downloading OpenCode v%OPENCODE_VERSION%... + echo Downloading from: %OPENCODE_URL% + + :: Download binary to BIN_DIR + curl -L -o "%BIN_DIR%\opencode.exe.tmp" "%OPENCODE_URL%" + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Download failed! + set /a ERRORS+=1 + goto :skip_opencode + ) + + :: Download checksums + curl -L -o "%BIN_DIR%\checksums.txt" "%CHECKSUM_URL%" + + :: Extract expected checksum + set EXPECTED_HASH= + for /f "tokens=1,2" %%h in ('type "%BIN_DIR%\checksums.txt" ^| findstr /i "opencode-windows-%ARCH%"') do ( + set EXPECTED_HASH=%%h + ) + + :: Calculate actual hash (line 2 from certutil) + set ACTUAL_HASH= + for /f "skip=1 tokens=*" %%h in ('certutil -hashfile "%BIN_DIR%\opencode.exe.tmp" SHA256 ^| findstr /v "CertUtil" ^| findstr /v "hash of"') do ( + set ACTUAL_HASH=%%h + goto :hash_found + ) + :hash_found + + :: Verify and output hashes + echo Expected hash: !EXPECTED_HASH! + echo Actual hash: !ACTUAL_HASH! + + if "!ACTUAL_HASH!"=="!EXPECTED_HASH!" ( + move /Y "%BIN_DIR%\opencode.exe.tmp" "%BIN_DIR%\opencode.exe" + echo [OK] OpenCode downloaded and verified + echo [%date% %time%] OpenCode v%OPENCODE_VERSION% downloaded, checksum verified >> "%TARGET_DIR%\install.log" + ) else ( + echo [ERROR] Checksum mismatch! + del "%BIN_DIR%\opencode.exe.tmp" + set /a ERRORS+=1 + ) +) +:skip_opencode +``` + +**Install-Mac.sh / Install-Linux.sh:** Similar pattern with `opencode-darwin-${ARCH}` and `opencode-linux-${ARCH}`, using `TARGET_DIR/bin` + +**Verification:** Log shows `OpenCode v downloaded, checksum verified` + `ls TARGET_DIR/bin/opencode` exists + +--- + +## C5: Windows Checksum Parsing + +**Included in C4 above.** Key change: + +```batch +:: Parse certutil output - hash is on line 2 +for /f "skip=1 tokens=*" %%h in ('certutil -hashfile "%BIN_DIR%\opencode.exe.tmp" SHA256 ^| findstr /v "CertUtil" ^| findstr /v "hash of"') do ( + set ACTUAL_HASH=%%h + goto :hash_found +) +``` + +**Verification:** Output shows matching hashes: +``` +Expected hash: abc123def456... +Actual hash: abc123def456... +``` + +--- + +## C6: Permission Fallback with TARGET_DIR/BIN_DIR + +**Install-Windows.bat (lines 125-160):** + +```batch +set TARGET_DIR=%SCRIPT_DIR% +set BIN_DIR=%TARGET_DIR%\bin +set NEEDS_FALLBACK=0 + +echo [STEP 2/10] Checking Write Permissions... +echo. + +echo. > "%SCRIPT_DIR%\test-write.tmp" 2>nul +if %ERRORLEVEL% neq 0 ( + echo [WARN] Cannot write to current directory: %SCRIPT_DIR% + echo [INFO] Setting fallback for install outputs... + + set TARGET_DIR=%USERPROFILE%\NomadArch-Install + set BIN_DIR=%TARGET_DIR%\bin + if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%" + if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" + + echo. > "%TARGET_DIR%\test-write.tmp" 2>nul + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Cannot write to fallback directory either! + set /a ERRORS+=1 + goto :final_check + ) + + echo [OK] Using fallback for outputs: %TARGET_DIR% + echo [%date% %time%] Using fallback: %TARGET_DIR% >> "%TARGET_DIR%\install.log" + set NEEDS_FALLBACK=1 + del "%TARGET_DIR%\test-write.tmp" +) else ( + if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" + del "%SCRIPT_DIR%\test-write.tmp" + echo [OK] Write permissions verified +) + +:: All log writes use TARGET_DIR +set LOG_FILE=%TARGET_DIR%\install.log +``` + +**Install-Mac.sh / Install-Linux.sh:** Similar pattern with `TARGET_DIR=$HOME/.nomadarch-install`, `BIN_DIR=$TARGET_DIR/bin` + +**Verification:** Run from read-only directory, output shows `Using fallback for outputs: C:\Users\xxx\NomadArch-Install` + +--- + +## C7: Health Check Path Corrections + +**Install-Windows.bat (health check section):** + +```diff +:: UI health check +- if exist "%SCRIPT_DIR%\packages\ui\src\renderer\dist" ( ++ if exist "%SCRIPT_DIR%\packages\ui\dist\index.html" ( + echo [OK] UI build directory exists +) else ( +- echo [ERROR] UI build directory not found ++ echo [ERROR] UI build directory not found at packages\ui\dist + set /a HEALTH_ERRORS+=1 +) + +:: Electron health check +- if exist "%SCRIPT_DIR%\packages\electron-app\dist\main.js" ( ++ if exist "%SCRIPT_DIR%\packages\electron-app\dist\main\main.js" ( + echo [OK] Electron main.js exists +) else ( + echo [WARN] Electron build not found (will build on launch) +) +``` + +**Install-Mac.sh / Install-Linux.sh:** Same logic with shell syntax + +**Verification:** Health check output: +``` +[OK] UI build directory exists +[OK] Electron main.js exists +``` + +--- + +## C8: Launch-Dev-Windows Electron Path Fix + +**Launch-Dev-Windows.bat line 162:** + +```diff +- if not exist "electron-app\dist\main.js" ( ++ if not exist "packages\electron-app\dist\main\main.js" ( +``` + +**Verification:** `grep -n "electron-app" Launch-Dev-Windows.bat` shows no `electron-app\` references remaining + +--- + +## Execution Order + +1. C6 (TARGET_DIR/BIN_DIR) - Foundation for C4 +2. C7 (Health checks) - Independent path fixes +3. C1 (UI paths) - Quick path replacements +4. C8 (Launch-Dev-Windows) - Quick path fix +5. C2 (Vite port) - Includes new file creation +6. C3 (Server port) - Quick env var changes +7. C4 (OpenCode download) - Depends on C6, includes C5 +8. **Run build** for C1/C7 verification + +--- + +## Verification Commands to Run + +| Fix | Command | Expected Output | +|------|----------|----------------| +| C1 | `dir packages\ui\dist` | Shows `index.html`, `assets/` | +| C2 | Run Launch-Dev, check Vite log | `Local: http://localhost:3001` | +| C3 | Run launcher, check server log | `CodeNomad Server is ready at http://127.0.0.1:3001` | +| C4 | Run install, grep log | `OpenCode v downloaded, checksum verified` | +| C5 | Run install, check log | Hashes match in output | +| C6 | Run from read-only dir | `Using fallback: C:\Users\xxx\NomadArch-Install` | +| C7 | Run install, check output | `UI build directory exists` + `Electron main.js exists` | +| C8 | `grep -n "electron-app" Launch-Dev-Windows.bat` | Only `packages\electron-app` or commented lines | + +--- + +## Files Modified/Created + +| File | Action | +|------|--------| +| Install-Windows.bat | Edit (C1, C4, C5, C6, C7) | +| Install-Mac.sh | Edit (C1, C4, C6, C7) | +| Install-Linux.sh | Edit (C1, C4, C6, C7) | +| Launch-Windows.bat | Edit (C1, C3) | +| Launch-Dev-Windows.bat | Edit (C1, C2, C3, C8) | +| Launch-Unix.sh | Edit (C1, C3) | +| Launch-Dev-Unix.sh | CREATE (C2) | +| packages/ui/vite.config.ts | Edit (C2) | \ No newline at end of file diff --git a/Install-Linux.sh b/Install-Linux.sh index 6ae2ed7..ede3a80 100644 --- a/Install-Linux.sh +++ b/Install-Linux.sh @@ -1,306 +1,285 @@ #!/bin/bash -echo "" -echo " ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗" -echo " ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║" -echo " ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║" -echo " ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║" -echo " ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║" -echo " ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝" -echo "" -echo " INSTALLER - Enhanced with Auto-Dependency Resolution" -echo " ═════════════════════════════════════════════════════════════════════════════" -echo "" +# NomadArch Installer for Linux +# Version: 0.4.0 +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET_DIR="$SCRIPT_DIR" +BIN_DIR="$TARGET_DIR/bin" +LOG_FILE="$TARGET_DIR/install.log" ERRORS=0 WARNINGS=0 +NEEDS_FALLBACK=0 -cd "$(dirname "$0")" +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" +} -echo "[STEP 1/7] Detecting Linux Distribution..." +echo "" +echo "NomadArch Installer (Linux)" +echo "Version: 0.4.0" echo "" -if [ -f /etc/os-release ]; then +log "Installer started" + +echo "[STEP 1/9] OS and Architecture Detection" +OS_TYPE=$(uname -s) +ARCH_TYPE=$(uname -m) +log "OS: $OS_TYPE" +log "Architecture: $ARCH_TYPE" + +if [[ "$OS_TYPE" != "Linux" ]]; then + echo -e "${RED}[ERROR]${NC} This installer is for Linux. Current OS: $OS_TYPE" + log "ERROR: Not Linux ($OS_TYPE)" + exit 1 +fi + +case "$ARCH_TYPE" in + x86_64) ARCH="x64" ;; + aarch64) ARCH="arm64" ;; + armv7l) ARCH="arm" ;; + *) + echo -e "${RED}[ERROR]${NC} Unsupported architecture: $ARCH_TYPE" + log "ERROR: Unsupported arch $ARCH_TYPE" + exit 1 + ;; +esac + +echo -e "${GREEN}[OK]${NC} OS: Linux" +echo -e "${GREEN}[OK]${NC} Architecture: $ARCH_TYPE" + +if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 . /etc/os-release - DISTRO=$ID - DISTRO_VERSION=$VERSION_ID - echo "[OK] Detected: $PRETTY_NAME" -else - echo "[WARN] Could not detect specific distribution" - DISTRO="unknown" - WARNINGS=$((WARNINGS + 1)) + echo -e "${GREEN}[INFO]${NC} Distribution: ${PRETTY_NAME:-unknown}" fi echo "" -echo "[STEP 2/7] Checking System Requirements..." -echo "" - -if ! command -v node &> /dev/null; then - echo "[ERROR] Node.js not found!" - echo "" - echo "NomadArch requires Node.js to run." - echo "" - echo "Install using your package manager:" - if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then - echo " sudo apt update && sudo apt install -y nodejs npm" - elif [ "$DISTRO" = "fedora" ]; then - echo " sudo dnf install -y nodejs npm" - elif [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "manjaro" ]; then - echo " sudo pacman -S nodejs npm" - elif [ "$DISTRO" = "opensuse-leap" ] || [ "$DISTRO" = "opensuse-tumbleweed" ]; then - echo " sudo zypper install -y nodejs npm" - else - echo " Visit https://nodejs.org/ for installation instructions" +echo "[STEP 2/9] Checking write permissions" +mkdir -p "$BIN_DIR" +if ! touch "$SCRIPT_DIR/.install-write-test" 2>/dev/null; then + echo -e "${YELLOW}[WARN]${NC} No write access to $SCRIPT_DIR" + TARGET_DIR="$HOME/.nomadarch-install" + BIN_DIR="$TARGET_DIR/bin" + LOG_FILE="$TARGET_DIR/install.log" + mkdir -p "$BIN_DIR" + if ! touch "$TARGET_DIR/.install-write-test" 2>/dev/null; then + echo -e "${RED}[ERROR]${NC} Cannot write to $TARGET_DIR" + log "ERROR: Write permission denied to fallback" + exit 1 fi - echo "" - echo "Or install Node.js using NVM (Node Version Manager):" - echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash" - echo " source ~/.bashrc" - echo " nvm install 20" - echo "" + rm -f "$TARGET_DIR/.install-write-test" + NEEDS_FALLBACK=1 + echo -e "${GREEN}[OK]${NC} Using fallback: $TARGET_DIR" +else + rm -f "$SCRIPT_DIR/.install-write-test" + echo -e "${GREEN}[OK]${NC} Write access OK" +fi + +log "Install target: $TARGET_DIR" + +echo "" +echo "[STEP 3/9] Ensuring system dependencies" + +SUDO="" +if [[ $EUID -ne 0 ]]; then + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + else + echo -e "${RED}[ERROR]${NC} sudo is required to install dependencies" + log "ERROR: sudo not found" + exit 1 + fi +fi + +install_packages() { + local manager="$1" + shift + local packages=("$@") + echo -e "${BLUE}[INFO]${NC} Installing via $manager: ${packages[*]}" + case "$manager" in + apt) + $SUDO apt-get update -y + $SUDO apt-get install -y "${packages[@]}" + ;; + dnf) + $SUDO dnf install -y "${packages[@]}" + ;; + yum) + $SUDO yum install -y "${packages[@]}" + ;; + pacman) + $SUDO pacman -Sy --noconfirm "${packages[@]}" + ;; + zypper) + $SUDO zypper -n install "${packages[@]}" + ;; + apk) + $SUDO apk add --no-cache "${packages[@]}" + ;; + *) + return 1 + ;; + esac +} + +PACKAGE_MANAGER="" +if command -v apt-get >/dev/null 2>&1; then + PACKAGE_MANAGER="apt" +elif command -v dnf >/dev/null 2>&1; then + PACKAGE_MANAGER="dnf" +elif command -v yum >/dev/null 2>&1; then + PACKAGE_MANAGER="yum" +elif command -v pacman >/dev/null 2>&1; then + PACKAGE_MANAGER="pacman" +elif command -v zypper >/dev/null 2>&1; then + PACKAGE_MANAGER="zypper" +elif command -v apk >/dev/null 2>&1; then + PACKAGE_MANAGER="apk" +fi + +if [[ -z "$PACKAGE_MANAGER" ]]; then + echo -e "${RED}[ERROR]${NC} No supported package manager found." + echo "Install Node.js, npm, git, and curl manually." + log "ERROR: No package manager found" + exit 1 +fi + +MISSING_PKGS=() +command -v curl >/dev/null 2>&1 || MISSING_PKGS+=("curl") +command -v git >/dev/null 2>&1 || MISSING_PKGS+=("git") +command -v node >/dev/null 2>&1 || MISSING_PKGS+=("nodejs") +command -v npm >/dev/null 2>&1 || MISSING_PKGS+=("npm") + +if [[ ${#MISSING_PKGS[@]} -gt 0 ]]; then + install_packages "$PACKAGE_MANAGER" "${MISSING_PKGS[@]}" +fi + +if ! command -v node >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} Node.js install failed." + log "ERROR: Node.js still missing" exit 1 fi NODE_VERSION=$(node --version) -echo "[OK] Node.js detected: $NODE_VERSION" - -NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1 | sed 's/v//') -if [ "$NODE_MAJOR" -lt 18 ]; then - echo "[WARN] Node.js version is too old (found v$NODE_VERSION, required 18+)" - echo "[INFO] Please update Node.js" - WARNINGS=$((WARNINGS + 1)) +NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d'v' -f2 | cut -d'.' -f1) +echo -e "${GREEN}[OK]${NC} Node.js: $NODE_VERSION" +if [[ $NODE_MAJOR -lt 18 ]]; then + echo -e "${YELLOW}[WARN]${NC} Node.js 18+ is recommended" + ((WARNINGS++)) fi -if ! command -v npm &> /dev/null; then - echo "[ERROR] npm not found! This should come with Node.js." - echo "Please reinstall Node.js" - ERRORS=$((ERRORS + 1)) +if ! command -v npm >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} npm is not available" + log "ERROR: npm missing after install" + exit 1 fi - NPM_VERSION=$(npm --version) -echo "[OK] npm detected: $NPM_VERSION" +echo -e "${GREEN}[OK]${NC} npm: $NPM_VERSION" -echo "" -echo "[STEP 3/7] Checking OpenCode CLI..." -echo "" - -if command -v opencode &> /dev/null; then - echo "[OK] OpenCode is already installed globally" - OPENCODE_DONE=true -elif [ -f "bin/opencode" ]; then - echo "[OK] OpenCode binary found in bin/ folder" - OPENCODE_DONE=true +if command -v git >/dev/null 2>&1; then + echo -e "${GREEN}[OK]${NC} Git: $(git --version)" else - OPENCODE_DONE=false -fi - -if [ "$OPENCODE_DONE" = false ]; then - echo "[SETUP] OpenCode CLI not found. Installing..." - echo "" - echo "[INFO] Attempting to install OpenCode via npm..." - npm install -g opencode-ai@latest - if [ $? -eq 0 ]; then - echo "[SUCCESS] OpenCode installed successfully via npm" - if command -v opencode &> /dev/null; then - echo "[OK] OpenCode is now available in system PATH" - OPENCODE_DONE=true - fi - else - echo "[WARN] npm install failed, trying fallback method..." - echo "" - - if [ ! -d "bin" ]; then - mkdir bin - fi - - ARCH=$(uname -m) - if [ "$ARCH" = "x86_64" ]; then - FILENAME="opencode-linux-x64.zip" - elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then - FILENAME="opencode-linux-arm64.zip" - else - echo "[WARN] Unsupported architecture: $ARCH" - WARNINGS=$((WARNINGS + 1)) - FILENAME="" - fi - - if [ -n "$FILENAME" ]; then - echo "[SETUP] Downloading OpenCode from GitHub releases..." - curl -L -o "opencode.zip" "https://github.com/sst/opencode/releases/latest/download/$FILENAME" - if [ $? -ne 0 ]; then - echo "[ERROR] Failed to download OpenCode from GitHub!" - echo "[INFO] You can install OpenCode CLI manually from: https://opencode.ai/" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] Downloaded OpenCode ZIP" - echo "[SETUP] Extracting OpenCode binary..." - - unzip -q "opencode.zip" -d "opencode-temp" - if [ -f "opencode-temp/opencode" ]; then - mv "opencode-temp/opencode" "bin/opencode" - chmod +x "bin/opencode" - echo "[OK] OpenCode binary placed in bin/ folder" - else - echo "[ERROR] opencode binary not found in extracted files!" - WARNINGS=$((WARNINGS + 1)) - fi - - rm -f "opencode.zip" - rm -rf "opencode-temp" - fi - fi - fi + echo -e "${YELLOW}[WARN]${NC} Git not found (optional)" + ((WARNINGS++)) fi echo "" -echo "[STEP 4/7] Installing NomadArch Dependencies..." -echo "" - -if [ -d "node_modules" ]; then - echo "[INFO] node_modules found. Skipping dependency installation." - echo "[INFO] To force reinstall, delete node_modules and run again." - goto :BUILD_CHECK -fi - -echo "[INFO] Installing root dependencies..." -npm install -if [ $? -ne 0 ]; then - echo "[ERROR] Failed to install root dependencies!" - ERRORS=$((ERRORS + 1)) -fi - -echo "[INFO] Installing package dependencies..." - -if [ -d "packages/server" ]; then - echo "[INFO] Installing server dependencies..." - cd packages/server - npm install - if [ $? -ne 0 ]; then - echo "[WARN] Failed to install server dependencies!" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] Server dependencies installed" - fi - cd ../.. -fi - -if [ -d "packages/ui" ]; then - echo "[INFO] Installing UI dependencies..." - cd packages/ui - npm install - if [ $? -ne 0 ]; then - echo "[WARN] Failed to install UI dependencies!" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] UI dependencies installed" - fi - cd ../.. -fi - -if [ -d "packages/electron-app" ]; then - echo "[INFO] Installing Electron app dependencies..." - cd packages/electron-app - npm install - if [ $? -ne 0 ]; then - echo "[WARN] Failed to install Electron app dependencies!" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] Electron app dependencies installed" - fi - cd ../.. -fi - -echo "" -echo "[STEP 5/7] Setting Permissions..." -echo "" - -chmod +x Launch-Unix.sh 2>/dev/null -chmod +x Install-Linux.sh 2>/dev/null -chmod +x Install-Mac.sh 2>/dev/null - -echo "[OK] Scripts permissions set" - -echo "" -echo "[STEP 6/7] Checking for Existing Build..." -echo "" - -if [ -d "packages/ui/dist" ]; then - echo "[OK] UI build found. Skipping build step." - echo "[INFO] To rebuild, delete packages/ui/dist and run installer again." - goto :INSTALL_REPORT -fi - -echo "[INFO] No UI build found. Building UI..." -echo "" - -cd packages/ui -npm run build -if [ $? -ne 0 ]; then - echo "[WARN] Failed to build UI!" - WARNINGS=$((WARNINGS + 1)) - echo "[INFO] You can build manually later by running: cd packages/ui && npm run build" -fi -cd ../.. - -echo "" -echo "[STEP 7/7] Testing Installation..." -echo "" - -node --version >nul 2>&1 -if [ $? -eq 0 ]; then - echo "[OK] Node.js is working" -else - echo "[FAIL] Node.js is not working correctly" - ERRORS=$((ERRORS + 1)) -fi - -npm --version >nul 2>&1 -if [ $? -eq 0 ]; then - echo "[OK] npm is working" -else - echo "[FAIL] npm is not working correctly" - ERRORS=$((ERRORS + 1)) -fi - -if command -v opencode &> /dev/null; then - echo "[OK] OpenCode CLI is available" -elif [ -f "bin/opencode" ]; then - echo "[OK] OpenCode binary found in bin/ folder" -else - echo "[FAIL] OpenCode CLI not available" - WARNINGS=$((WARNINGS + 1)) -fi - -echo "" -echo "Installation Summary" -echo "" - -if [ $ERRORS -gt 0 ]; then - echo "" - echo "════════════════════════════════════════════════════════════════════════════" - echo "[FAILED] Installation encountered $ERRORS error(s)!" - echo "" - echo "Please review error messages above and try again." - echo "For help, see: https://github.com/roman-ryzenadvanced/NomadArch-v1.0/issues" - echo "════════════════════════════════════════════════════════════════════════════" - echo "" +echo "[STEP 4/9] Installing npm dependencies" +cd "$SCRIPT_DIR" +log "Running npm install" +if ! npm install; then + echo -e "${RED}[ERROR]${NC} npm install failed" + log "ERROR: npm install failed" exit 1 fi -echo "" -echo "════════════════════════════════════════════════════════════════════════════" -echo "[SUCCESS] Installation Complete!" -echo "" +echo -e "${GREEN}[OK]${NC} Dependencies installed" -if [ $WARNINGS -gt 0 ]; then - echo "[WARN] There were $WARNINGS warning(s) during installation." - echo "Review warnings above. Most warnings are non-critical." - echo "" +echo "" +echo "[STEP 5/9] Fetching OpenCode binary" +mkdir -p "$BIN_DIR" +OPENCODE_VERSION=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | grep '"tag_name"' | cut -d'"' -f4) +OPENCODE_BASE="https://github.com/sst/opencode/releases/download/v${OPENCODE_VERSION}" +OPENCODE_URL="${OPENCODE_BASE}/opencode-linux-${ARCH}" +CHECKSUM_URL="${OPENCODE_BASE}/checksums.txt" + +if [[ -f "$BIN_DIR/opencode" ]]; then + echo -e "${GREEN}[OK]${NC} OpenCode binary already exists" +else + echo -e "${BLUE}[INFO]${NC} Downloading OpenCode v${OPENCODE_VERSION}" + curl -L -o "$BIN_DIR/opencode.tmp" "$OPENCODE_URL" + curl -L -o "$BIN_DIR/checksums.txt" "$CHECKSUM_URL" + + EXPECTED_HASH=$(grep "opencode-linux-${ARCH}" "$BIN_DIR/checksums.txt" | awk '{print $1}') + ACTUAL_HASH=$(sha256sum "$BIN_DIR/opencode.tmp" | awk '{print $1}') + + if [[ "$ACTUAL_HASH" == "$EXPECTED_HASH" ]]; then + mv "$BIN_DIR/opencode.tmp" "$BIN_DIR/opencode" + chmod +x "$BIN_DIR/opencode" + echo -e "${GREEN}[OK]${NC} OpenCode downloaded and verified" + else + echo -e "${RED}[ERROR]${NC} OpenCode checksum mismatch" + rm -f "$BIN_DIR/opencode.tmp" + exit 1 + fi fi -echo "You can now run NomadArch using:" -echo " ./Launch-Unix.sh" echo "" -echo "For help and documentation, see: README.md" -echo "════════════════════════════════════════════════════════════════════════════" +echo "[STEP 6/9] Building UI assets" +if [[ -d "$SCRIPT_DIR/packages/ui/dist" ]]; then + echo -e "${GREEN}[OK]${NC} UI build already exists" +else + echo -e "${BLUE}[INFO]${NC} Building UI" + pushd "$SCRIPT_DIR/packages/ui" >/dev/null + npm run build + popd >/dev/null + echo -e "${GREEN}[OK]${NC} UI assets built" +fi + echo "" +echo "[STEP 7/9] Post-install health check" +HEALTH_ERRORS=0 + +[[ -f "$SCRIPT_DIR/package.json" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) +[[ -d "$SCRIPT_DIR/packages/ui" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) +[[ -d "$SCRIPT_DIR/packages/server" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) +[[ -f "$SCRIPT_DIR/packages/ui/dist/index.html" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) + +if [[ $HEALTH_ERRORS -eq 0 ]]; then + echo -e "${GREEN}[OK]${NC} Health checks passed" +else + echo -e "${RED}[ERROR]${NC} Health checks failed ($HEALTH_ERRORS)" + ERRORS=$((ERRORS+HEALTH_ERRORS)) +fi + +echo "" +echo "[STEP 8/9] Installation Summary" +echo "" +echo " Install Dir: $TARGET_DIR" +echo " Architecture: $ARCH" +echo " Node.js: $NODE_VERSION" +echo " npm: $NPM_VERSION" +echo " Errors: $ERRORS" +echo " Warnings: $WARNINGS" +echo " Log File: $LOG_FILE" +echo "" + +echo "[STEP 9/9] Next steps" +if [[ $ERRORS -gt 0 ]]; then + echo -e "${RED}[RESULT]${NC} Installation completed with errors" + echo "Review $LOG_FILE for details." +else + echo -e "${GREEN}[RESULT]${NC} Installation completed successfully" + echo "Run: ./Launch-Unix.sh" +fi + +exit $ERRORS diff --git a/Install-Mac.sh b/Install-Mac.sh index 57fea9e..b7c8dca 100644 --- a/Install-Mac.sh +++ b/Install-Mac.sh @@ -1,331 +1,221 @@ #!/bin/bash -echo "" -echo " ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗" -echo " ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║" -echo " ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║" -echo " ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║" -echo " ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║" -echo " ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝" -echo "" -echo " INSTALLER - macOS Enhanced with Auto-Dependency Resolution" -echo " ═══════════════════════════════════════════════════════════════════════════" -echo "" +# NomadArch Installer for macOS +# Version: 0.4.0 +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET_DIR="$SCRIPT_DIR" +BIN_DIR="$TARGET_DIR/bin" +LOG_FILE="$TARGET_DIR/install.log" ERRORS=0 WARNINGS=0 +NEEDS_FALLBACK=0 -cd "$(dirname "$0")" +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" +} -echo "[STEP 1/7] Checking macOS Version..." +echo "" +echo "NomadArch Installer (macOS)" +echo "Version: 0.4.0" echo "" -if [ -f /System/Library/CoreServices/SystemVersion.plist ]; then - MAC_VERSION=$(defaults read /System/Library/CoreServices/SystemVersion.plist ProductVersion) - MAC_MAJOR=$(echo $MAC_VERSION | cut -d. -f1) - echo "[OK] macOS detected: $MAC_VERSION" +log "Installer started" - if [ "$MAC_MAJOR" -lt 11 ]; then - echo "[WARN] NomadArch requires macOS 11+ (Big Sur or later)" - echo "[INFO] Your version is $MAC_VERSION" - echo "[INFO] Please upgrade macOS to continue" +echo "[STEP 1/9] OS and Architecture Detection" +OS_TYPE=$(uname -s) +ARCH_TYPE=$(uname -m) +log "OS: $OS_TYPE" +log "Architecture: $ARCH_TYPE" + +if [[ "$OS_TYPE" != "Darwin" ]]; then + echo -e "${RED}[ERROR]${NC} This installer is for macOS. Current OS: $OS_TYPE" + log "ERROR: Not macOS ($OS_TYPE)" + exit 1 +fi + +case "$ARCH_TYPE" in + arm64) ARCH="arm64" ;; + x86_64) ARCH="x64" ;; + *) + echo -e "${RED}[ERROR]${NC} Unsupported architecture: $ARCH_TYPE" + log "ERROR: Unsupported arch $ARCH_TYPE" + exit 1 + ;; +esac + +echo -e "${GREEN}[OK]${NC} OS: macOS" +echo -e "${GREEN}[OK]${NC} Architecture: $ARCH_TYPE" + +echo "" +echo "[STEP 2/9] Checking write permissions" +mkdir -p "$BIN_DIR" +if ! touch "$SCRIPT_DIR/.install-write-test" 2>/dev/null; then + echo -e "${YELLOW}[WARN]${NC} No write access to $SCRIPT_DIR" + TARGET_DIR="$HOME/.nomadarch-install" + BIN_DIR="$TARGET_DIR/bin" + LOG_FILE="$TARGET_DIR/install.log" + mkdir -p "$BIN_DIR" + if ! touch "$TARGET_DIR/.install-write-test" 2>/dev/null; then + echo -e "${RED}[ERROR]${NC} Cannot write to $TARGET_DIR" + log "ERROR: Write permission denied to fallback" exit 1 fi + rm -f "$TARGET_DIR/.install-write-test" + NEEDS_FALLBACK=1 + echo -e "${GREEN}[OK]${NC} Using fallback: $TARGET_DIR" else - echo "[WARN] Could not detect macOS version" - WARNINGS=$((WARNINGS + 1)) + rm -f "$SCRIPT_DIR/.install-write-test" + echo -e "${GREEN}[OK]${NC} Write access OK" fi -ARCH=$(uname -m) -if [ "$ARCH" = "arm64" ]; then - echo "[OK] Apple Silicon detected (M1/M2/M3 chip)" -elif [ "$ARCH" = "x86_64" ]; then - echo "[OK] Intel Mac detected" -else - echo "[WARN] Unknown architecture: $ARCH" - WARNINGS=$((WARNINGS + 1)) +log "Install target: $TARGET_DIR" + +echo "" +echo "[STEP 3/9] Ensuring system dependencies" + +if ! command -v curl >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} curl is required but not available" + exit 1 fi -echo "" -echo "[STEP 2/7] Checking System Requirements..." -echo "" +if ! command -v brew >/dev/null 2>&1; then + echo -e "${YELLOW}[INFO]${NC} Homebrew not found. Installing..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +fi -if ! command -v node &> /dev/null; then - echo "[ERROR] Node.js not found!" - echo "" - echo "NomadArch requires Node.js to run." - echo "" - echo "Install Node.js using one of these methods:" - echo "" - echo " 1. Homebrew (recommended):" - echo " brew install node" - echo "" - echo " 2. Download from official site:" - echo " Visit https://nodejs.org/" - echo " Download and install macOS installer" - echo "" - echo " 3. Using NVM (Node Version Manager):" - echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash" - echo " source ~/.zshrc (or ~/.bash_profile)" - echo " nvm install 20" - echo "" +MISSING_PKGS=() +command -v git >/dev/null 2>&1 || MISSING_PKGS+=("git") +command -v node >/dev/null 2>&1 || MISSING_PKGS+=("node") + +if [[ ${#MISSING_PKGS[@]} -gt 0 ]]; then + echo -e "${BLUE}[INFO]${NC} Installing: ${MISSING_PKGS[*]}" + brew install "${MISSING_PKGS[@]}" +fi + +if ! command -v node >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} Node.js install failed" exit 1 fi NODE_VERSION=$(node --version) -echo "[OK] Node.js detected: $NODE_VERSION" - -NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1 | sed 's/v//') -if [ "$NODE_MAJOR" -lt 18 ]; then - echo "[WARN] Node.js version is too old (found v$NODE_VERSION, required 18+)" - echo "[INFO] Please update Node.js: brew upgrade node" - WARNINGS=$((WARNINGS + 1)) +NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d'v' -f2 | cut -d'.' -f1) +echo -e "${GREEN}[OK]${NC} Node.js: $NODE_VERSION" +if [[ $NODE_MAJOR -lt 18 ]]; then + echo -e "${YELLOW}[WARN]${NC} Node.js 18+ is recommended" + ((WARNINGS++)) fi -if ! command -v npm &> /dev/null; then - echo "[ERROR] npm not found! This should come with Node.js." - echo "Please reinstall Node.js" - ERRORS=$((ERRORS + 1)) +if ! command -v npm >/dev/null 2>&1; then + echo -e "${RED}[ERROR]${NC} npm is not available" + exit 1 fi - NPM_VERSION=$(npm --version) -echo "[OK] npm detected: $NPM_VERSION" +echo -e "${GREEN}[OK]${NC} npm: $NPM_VERSION" -echo "[INFO] Checking Xcode Command Line Tools..." -if ! command -v xcode-select &> /dev/null; then - echo "[WARN] Xcode Command Line Tools not installed" - echo "[INFO] Required for building native Node.js modules" - echo "" - echo "Install by running:" - echo " xcode-select --install" - echo "" - echo "This will open a dialog to install the tools." - WARNINGS=$((WARNINGS + 1)) +if command -v git >/dev/null 2>&1; then + echo -e "${GREEN}[OK]${NC} Git: $(git --version)" else - XCODE_PATH=$(xcode-select -p) - echo "[OK] Xcode Command Line Tools detected: $XCODE_PATH" + echo -e "${YELLOW}[WARN]${NC} Git not found (optional)" + ((WARNINGS++)) fi echo "" -echo "[STEP 3/7] Checking OpenCode CLI..." -echo "" - -if command -v opencode &> /dev/null; then - echo "[OK] OpenCode is already installed globally" - OPENCODE_DONE=true -elif [ -f "bin/opencode" ]; then - echo "[OK] OpenCode binary found in bin/ folder" - OPENCODE_DONE=true -else - OPENCODE_DONE=false -fi - -if [ "$OPENCODE_DONE" = false ]; then - echo "[SETUP] OpenCode CLI not found. Installing..." - echo "" - echo "[INFO] Attempting to install OpenCode via npm..." - npm install -g opencode-ai@latest - if [ $? -eq 0 ]; then - echo "[SUCCESS] OpenCode installed successfully via npm" - if command -v opencode &> /dev/null; then - echo "[OK] OpenCode is now available in system PATH" - OPENCODE_DONE=true - fi - else - echo "[WARN] npm install failed, trying fallback method..." - echo "" - - if [ ! -d "bin" ]; then - mkdir bin - fi - - if [ "$ARCH" = "arm64" ]; then - FILENAME="opencode-darwin-arm64.zip" - elif [ "$ARCH" = "x86_64" ]; then - FILENAME="opencode-darwin-x64.zip" - else - echo "[WARN] Unsupported architecture: $ARCH" - WARNINGS=$((WARNINGS + 1)) - FILENAME="" - fi - - if [ -n "$FILENAME" ]; then - echo "[SETUP] Downloading OpenCode from GitHub releases..." - curl -L -o "opencode.zip" "https://github.com/sst/opencode/releases/latest/download/$FILENAME" - if [ $? -ne 0 ]; then - echo "[ERROR] Failed to download OpenCode from GitHub!" - echo "[INFO] You can install OpenCode CLI manually from: https://opencode.ai/" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] Downloaded OpenCode ZIP" - echo "[SETUP] Extracting OpenCode binary..." - - unzip -q "opencode.zip" -d "opencode-temp" - if [ -f "opencode-temp/opencode" ]; then - mv "opencode-temp/opencode" "bin/opencode" - chmod +x "bin/opencode" - echo "[OK] OpenCode binary placed in bin/ folder" - else - echo "[ERROR] opencode binary not found in extracted files!" - WARNINGS=$((WARNINGS + 1)) - fi - - rm -f "opencode.zip" - rm -rf "opencode-temp" - fi - fi - fi -fi - -echo "" -echo "[STEP 4/7] Installing NomadArch Dependencies..." -echo "" - -if [ -d "node_modules" ]; then - echo "[INFO] node_modules found. Skipping dependency installation." - echo "[INFO] To force reinstall, delete node_modules and run again." - goto :BUILD_CHECK -fi - -echo "[INFO] Installing root dependencies..." -npm install -if [ $? -ne 0 ]; then - echo "[ERROR] Failed to install root dependencies!" - ERRORS=$((ERRORS + 1)) -fi - -echo "[INFO] Installing package dependencies..." - -if [ -d "packages/server" ]; then - echo "[INFO] Installing server dependencies..." - cd packages/server - npm install - if [ $? -ne 0 ]; then - echo "[WARN] Failed to install server dependencies!" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] Server dependencies installed" - fi - cd ../.. -fi - -if [ -d "packages/ui" ]; then - echo "[INFO] Installing UI dependencies..." - cd packages/ui - npm install - if [ $? -ne 0 ]; then - echo "[WARN] Failed to install UI dependencies!" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] UI dependencies installed" - fi - cd ../.. -fi - -if [ -d "packages/electron-app" ]; then - echo "[INFO] Installing Electron app dependencies..." - cd packages/electron-app - npm install - if [ $? -ne 0 ]; then - echo "[WARN] Failed to install Electron app dependencies!" - WARNINGS=$((WARNINGS + 1)) - else - echo "[OK] Electron app dependencies installed" - fi - cd ../.. -fi - -echo "" -echo "[STEP 5/7] Setting Permissions..." -echo "" - -chmod +x Launch-Unix.sh 2>/dev/null -chmod +x Install-Linux.sh 2>/dev/null -chmod +x Install-Mac.sh 2>/dev/null - -echo "[OK] Scripts permissions set" - -echo "" -echo "[STEP 6/7] Checking for Existing Build..." -echo "" - -if [ -d "packages/ui/dist" ]; then - echo "[OK] UI build found. Skipping build step." - echo "[INFO] To rebuild, delete packages/ui/dist and run installer again." - goto :INSTALL_REPORT -fi - -echo "[INFO] No UI build found. Building UI..." -echo "" - -cd packages/ui -npm run build -if [ $? -ne 0 ]; then - echo "[WARN] Failed to build UI!" - WARNINGS=$((WARNINGS + 1)) - echo "[INFO] You can build manually later by running: cd packages/ui && npm run build" -fi -cd ../.. - -echo "" -echo "[STEP 7/7] Testing Installation..." -echo "" - -node --version >nul 2>&1 -if [ $? -eq 0 ]; then - echo "[OK] Node.js is working" -else - echo "[FAIL] Node.js is not working correctly" - ERRORS=$((ERRORS + 1)) -fi - -npm --version >nul 2>&1 -if [ $? -eq 0 ]; then - echo "[OK] npm is working" -else - echo "[FAIL] npm is not working correctly" - ERRORS=$((ERRORS + 1)) -fi - -if command -v opencode &> /dev/null; then - echo "[OK] OpenCode CLI is available" -elif [ -f "bin/opencode" ]; then - echo "[OK] OpenCode binary found in bin/ folder" -else - echo "[FAIL] OpenCode CLI not available" - WARNINGS=$((WARNINGS + 1)) -fi - -echo "" -echo "Installation Summary" -echo "" - -if [ $ERRORS -gt 0 ]; then - echo "" - echo "════════════════════════════════════════════════════════════════════════════" - echo "[FAILED] Installation encountered $ERRORS error(s)!" - echo "" - echo "Please review error messages above and try again." - echo "For help, see: https://github.com/roman-ryzenadvanced/NomadArch-v1.0/issues" - echo "════════════════════════════════════════════════════════════════════════════" - echo "" +echo "[STEP 4/9] Installing npm dependencies" +cd "$SCRIPT_DIR" +log "Running npm install" +if ! npm install; then + echo -e "${RED}[ERROR]${NC} npm install failed" + log "ERROR: npm install failed" exit 1 fi -echo "" -echo "════════════════════════════════════════════════════════════════════════════" -echo "[SUCCESS] Installation Complete!" -echo "" +echo -e "${GREEN}[OK]${NC} Dependencies installed" -if [ $WARNINGS -gt 0 ]; then - echo "[WARN] There were $WARNINGS warning(s) during installation." - echo "Review warnings above. Most warnings are non-critical." - echo "" +echo "" +echo "[STEP 5/9] Fetching OpenCode binary" +mkdir -p "$BIN_DIR" +OPENCODE_VERSION=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | grep '"tag_name"' | cut -d'"' -f4) +OPENCODE_BASE="https://github.com/sst/opencode/releases/download/v${OPENCODE_VERSION}" +OPENCODE_URL="${OPENCODE_BASE}/opencode-darwin-${ARCH}" +CHECKSUM_URL="${OPENCODE_BASE}/checksums.txt" + +if [[ -f "$BIN_DIR/opencode" ]]; then + echo -e "${GREEN}[OK]${NC} OpenCode binary already exists" +else + echo -e "${BLUE}[INFO]${NC} Downloading OpenCode v${OPENCODE_VERSION}" + curl -L -o "$BIN_DIR/opencode.tmp" "$OPENCODE_URL" + curl -L -o "$BIN_DIR/checksums.txt" "$CHECKSUM_URL" + + EXPECTED_HASH=$(grep "opencode-darwin-${ARCH}" "$BIN_DIR/checksums.txt" | awk '{print $1}') + ACTUAL_HASH=$(shasum -a 256 "$BIN_DIR/opencode.tmp" | awk '{print $1}') + + if [[ "$ACTUAL_HASH" == "$EXPECTED_HASH" ]]; then + mv "$BIN_DIR/opencode.tmp" "$BIN_DIR/opencode" + chmod +x "$BIN_DIR/opencode" + echo -e "${GREEN}[OK]${NC} OpenCode downloaded and verified" + else + echo -e "${RED}[ERROR]${NC} OpenCode checksum mismatch" + rm -f "$BIN_DIR/opencode.tmp" + exit 1 + fi fi -echo "You can now run NomadArch using:" -echo " ./Launch-Unix.sh" echo "" -echo "For help and documentation, see: README.md" -echo "════════════════════════════════════════════════════════════════════════════" +echo "[STEP 6/9] Building UI assets" +if [[ -d "$SCRIPT_DIR/packages/ui/dist" ]]; then + echo -e "${GREEN}[OK]${NC} UI build already exists" +else + echo -e "${BLUE}[INFO]${NC} Building UI" + pushd "$SCRIPT_DIR/packages/ui" >/dev/null + npm run build + popd >/dev/null + echo -e "${GREEN}[OK]${NC} UI assets built" +fi + echo "" +echo "[STEP 7/9] Post-install health check" +HEALTH_ERRORS=0 + +[[ -f "$SCRIPT_DIR/package.json" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) +[[ -d "$SCRIPT_DIR/packages/ui" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) +[[ -d "$SCRIPT_DIR/packages/server" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) +[[ -f "$SCRIPT_DIR/packages/ui/dist/index.html" ]] || HEALTH_ERRORS=$((HEALTH_ERRORS+1)) + +if [[ $HEALTH_ERRORS -eq 0 ]]; then + echo -e "${GREEN}[OK]${NC} Health checks passed" +else + echo -e "${RED}[ERROR]${NC} Health checks failed ($HEALTH_ERRORS)" + ERRORS=$((ERRORS+HEALTH_ERRORS)) +fi + +echo "" +echo "[STEP 8/9] Installation Summary" +echo "" +echo " Install Dir: $TARGET_DIR" +echo " Architecture: $ARCH" +echo " Node.js: $NODE_VERSION" +echo " npm: $NPM_VERSION" +echo " Errors: $ERRORS" +echo " Warnings: $WARNINGS" +echo " Log File: $LOG_FILE" +echo "" + +echo "[STEP 9/9] Next steps" +if [[ $ERRORS -gt 0 ]]; then + echo -e "${RED}[RESULT]${NC} Installation completed with errors" + echo "Review $LOG_FILE for details." +else + echo -e "${GREEN}[RESULT]${NC} Installation completed successfully" + echo "Run: ./Launch-Unix.sh" +fi + +exit $ERRORS diff --git a/Install-Windows.bat b/Install-Windows.bat index 60ef2f9..2a4e1b2 100644 --- a/Install-Windows.bat +++ b/Install-Windows.bat @@ -1,318 +1,253 @@ @echo off -title NomadArch Installer -color 0A setlocal enabledelayedexpansion +title NomadArch Installer + echo. -echo ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗ -echo ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║ -echo ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║ -echo ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║ -echo ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║ -echo ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ -echo. -echo INSTALLER - Enhanced with Auto-Dependency Resolution -echo ═══════════════════════════════════════════════════════════════════════════════ +echo NomadArch Installer (Windows) +echo Version: 0.4.0 echo. +set SCRIPT_DIR=%~dp0 +set SCRIPT_DIR=%SCRIPT_DIR:~0,-1% +set TARGET_DIR=%SCRIPT_DIR% +set BIN_DIR=%TARGET_DIR%\bin +set LOG_FILE=%TARGET_DIR%\install.log +set TEMP_DIR=%TARGET_DIR%\.install-temp + set ERRORS=0 set WARNINGS=0 +set NEEDS_FALLBACK=0 -cd /d "%~dp0" +echo [%date% %time%] Installer started >> "%LOG_FILE%" + +echo [STEP 1/9] OS and Architecture Detection +wmic os get osarchitecture | findstr /i "64-bit" >nul +if %ERRORLEVEL% equ 0 ( + set ARCH=x64 +) else ( + set ARCH=x86 +) +echo [OK] Architecture: %ARCH% -echo [STEP 1/6] Checking System Requirements... echo. +echo [STEP 2/9] Checking write permissions +if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" +if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%" -:: Check for Administrator privileges -net session >nul 2>&1 +echo. > "%SCRIPT_DIR%\test-write.tmp" 2>nul if %ERRORLEVEL% neq 0 ( - echo [WARN] Not running as Administrator. Some operations may fail. - set /a WARNINGS+=1 - echo. + echo [WARN] Cannot write to current directory: %SCRIPT_DIR% + set TARGET_DIR=%USERPROFILE%\NomadArch-Install + set BIN_DIR=%TARGET_DIR%\bin + set LOG_FILE=%TARGET_DIR%\install.log + set TEMP_DIR=%TARGET_DIR%\.install-temp + if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%" + if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" + if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%" + echo. > "%TARGET_DIR%\test-write.tmp" 2>nul + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Cannot write to fallback directory: %TARGET_DIR% + echo [%date% %time%] ERROR: Write permission denied >> "%LOG_FILE%" + set /a ERRORS+=1 + goto :SUMMARY + ) + del "%TARGET_DIR%\test-write.tmp" + set NEEDS_FALLBACK=1 + echo [OK] Using fallback: %TARGET_DIR% +) else ( + del "%SCRIPT_DIR%\test-write.tmp" + echo [OK] Write permissions verified +) + +echo. +echo [STEP 3/9] Ensuring system dependencies + +set WINGET_AVAILABLE=0 +where winget >nul 2>&1 && set WINGET_AVAILABLE=1 + +set CHOCO_AVAILABLE=0 +where choco >nul 2>&1 && set CHOCO_AVAILABLE=1 + +set DOWNLOAD_CMD= +where curl >nul 2>&1 +if %ERRORLEVEL% equ 0 ( + set DOWNLOAD_CMD=curl +) else ( + set DOWNLOAD_CMD=powershell ) -:: Check for Node.js -echo [INFO] Checking Node.js... where node >nul 2>&1 if %ERRORLEVEL% neq 0 ( - echo [ERROR] Node.js not found! - echo. - echo NomadArch requires Node.js to run. - echo. - echo Download from: https://nodejs.org/ - echo Recommended: Node.js 18.x LTS or 20.x LTS - echo. - echo Opening download page... - start "" "https://nodejs.org/" - echo. - echo Please install Node.js and run this installer again. - echo. - pause - exit /b 1 + echo [INFO] Node.js not found. Attempting to install... + if %WINGET_AVAILABLE% equ 1 ( + winget install -e --id OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements + ) else if %CHOCO_AVAILABLE% equ 1 ( + choco install nodejs-lts -y + ) else ( + echo [ERROR] No supported package manager found (winget/choco). + echo Please install Node.js LTS from https://nodejs.org/ + set /a ERRORS+=1 + goto :SUMMARY + ) ) -:: Display Node.js version -for /f "tokens=*" %%i in ('node --version') do set NODE_VERSION=%%i -echo [OK] Node.js found: %NODE_VERSION% +where node >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo [ERROR] Node.js install failed or requires a new terminal session. + set /a ERRORS+=1 + goto :SUMMARY +) + +for /f "tokens=*" %%i in ('node --version') do set NODE_VERSION=%%i +echo [OK] Node.js: %NODE_VERSION% -:: Check for npm -echo [INFO] Checking npm... where npm >nul 2>&1 if %ERRORLEVEL% neq 0 ( - echo [ERROR] npm not found! - echo. - echo npm is required for dependency management. - echo. - pause - exit /b 1 + echo [ERROR] npm not found after Node.js install. + set /a ERRORS+=1 + goto :SUMMARY ) for /f "tokens=*" %%i in ('npm --version') do set NPM_VERSION=%%i -echo [OK] npm found: %NPM_VERSION% +echo [OK] npm: %NPM_VERSION% -echo. -echo [STEP 2/6] Checking OpenCode CLI... -echo. - -:: Check if opencode is already installed globally -where opencode >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [OK] OpenCode is already installed globally - goto :OPENCODE_DONE -) - -:: Check if opencode exists in bin/ folder -if exist "bin\opencode.exe" ( - echo [OK] OpenCode binary found in bin/ folder - goto :OPENCODE_DONE -) - -:: Install OpenCode CLI -echo [SETUP] OpenCode CLI not found. Installing... -echo. -echo [INFO] Attempting to install OpenCode via npm... -call npm install -g opencode-ai@latest -if %ERRORLEVEL% equ 0 ( - echo [SUCCESS] OpenCode installed successfully via npm - where opencode >nul 2>&1 - if %ERRORLEVEL% equ 0 ( - echo [OK] OpenCode is now available in system PATH - goto :OPENCODE_DONE +where git >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo [INFO] Git not found. Attempting to install... + if %WINGET_AVAILABLE% equ 1 ( + winget install -e --id Git.Git --accept-source-agreements --accept-package-agreements + ) else if %CHOCO_AVAILABLE% equ 1 ( + choco install git -y + ) else ( + echo [WARN] Git not installed (optional). Continue. + set /a WARNINGS+=1 ) -) - -echo [WARN] npm install failed or not in PATH, trying fallback method... -echo. - -:: Fallback: Download from GitHub releases -echo [SETUP] Downloading OpenCode from GitHub releases... -echo. - -:: Download Windows x64 ZIP -curl -L -o "opencode-windows-x64.zip" "https://github.com/sst/opencode/releases/latest/download/opencode-windows-x64.zip" -if %ERRORLEVEL% neq 0 ( - echo [ERROR] Failed to download OpenCode from GitHub! - set /a ERRORS+=1 - goto :INSTALL_DEPS -) - -echo [OK] Downloaded OpenCode ZIP -echo [SETUP] Extracting OpenCode binary... - -:: Create bin directory if not exists -if not exist "bin" mkdir bin - -:: Extract using PowerShell -powershell -Command "Expand-Archive -Path 'opencode-windows-x64.zip' -DestinationPath 'opencode-temp' -Force" -if %ERRORLEVEL% neq 0 ( - echo [ERROR] Failed to extract OpenCode! - set /a ERRORS+=1 - goto :CLEANUP -) - -:: Move opencode.exe to bin/ folder -if exist "opencode-temp\opencode.exe" ( - move /Y "opencode-temp\opencode.exe" "bin\opencode.exe" >nul - echo [OK] OpenCode binary placed in bin/ folder ) else ( - echo [ERROR] opencode.exe not found in extracted files! - set /a ERRORS+=1 + for /f "tokens=*" %%i in ('git --version') do set GIT_VERSION=%%i + echo [OK] Git: %GIT_VERSION% ) -:CLEANUP -if exist "opencode-windows-x64.zip" del "opencode-windows-x64.zip" -if exist "opencode-temp" rmdir /s /q "opencode-temp" - -:OPENCODE_DONE echo. - -echo [STEP 3/6] Installing NomadArch Dependencies... -echo. - -:: Check if node_modules exists -if exist "node_modules" ( - echo [INFO] node_modules found. Skipping dependency installation. - echo [INFO] To force reinstall, delete node_modules and run again. - goto :BUILD_CHECK -) - -echo [INFO] Installing root dependencies... +echo [STEP 4/9] Installing npm dependencies +cd /d "%SCRIPT_DIR%" +echo [%date% %time%] Running npm install >> "%LOG_FILE%" call npm install if %ERRORLEVEL% neq 0 ( - echo [ERROR] Failed to install root dependencies! + echo [ERROR] npm install failed! + echo [%date% %time%] ERROR: npm install failed >> "%LOG_FILE%" set /a ERRORS+=1 - goto :INSTALL_REPORT + goto :SUMMARY +) +echo [OK] Dependencies installed + +echo. +echo [STEP 5/9] Fetching OpenCode binary +if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" + +for /f "delims=" %%v in ('powershell -NoProfile -Command "(Invoke-WebRequest -UseBasicParsing https://api.github.com/repos/sst/opencode/releases/latest).Content ^| Select-String -Pattern '""tag_name""' ^| ForEach-Object { $_.Line.Split(''\"'')[3] }"') do ( + set OPENCODE_VERSION=%%v +) + +set OPENCODE_BASE=https://github.com/sst/opencode/releases/download/v!OPENCODE_VERSION! +set OPENCODE_URL=!OPENCODE_BASE!/opencode-windows-%ARCH%.exe +set CHECKSUM_URL=!OPENCODE_BASE!/checksums.txt + +if exist "%BIN_DIR%\opencode.exe" ( + echo [OK] OpenCode binary already exists + echo [%date% %time%] OpenCode binary exists, skipping download >> "%LOG_FILE%" +) else ( + echo [INFO] Downloading OpenCode v!OPENCODE_VERSION!... + if "%DOWNLOAD_CMD%"=="curl" ( + curl -L -o "%BIN_DIR%\opencode.exe.tmp" "!OPENCODE_URL!" + curl -L -o "%BIN_DIR%\checksums.txt" "!CHECKSUM_URL!" + ) else ( + powershell -NoProfile -Command "Invoke-WebRequest -Uri '%OPENCODE_URL%' -OutFile '%BIN_DIR%\\opencode.exe.tmp'" + powershell -NoProfile -Command "Invoke-WebRequest -Uri '%CHECKSUM_URL%' -OutFile '%BIN_DIR%\\checksums.txt'" + ) + + set EXPECTED_HASH= + for /f "tokens=1,2" %%h in ('type "%BIN_DIR%\checksums.txt" ^| findstr /i "opencode-windows-%ARCH%"') do ( + set EXPECTED_HASH=%%h + ) + + set ACTUAL_HASH= + for /f "skip=1 tokens=*" %%h in ('certutil -hashfile "%BIN_DIR%\opencode.exe.tmp" SHA256 ^| findstr /v "CertUtil" ^| findstr /v "hash of"') do ( + set ACTUAL_HASH=%%h + goto :hash_found + ) + :hash_found + + if "!ACTUAL_HASH!"=="!EXPECTED_HASH!" ( + move /Y "%BIN_DIR%\opencode.exe.tmp" "%BIN_DIR%\opencode.exe" + echo [OK] OpenCode downloaded and verified + ) else ( + echo [ERROR] OpenCode checksum mismatch! + del "%BIN_DIR%\opencode.exe.tmp" + set /a ERRORS+=1 + ) ) -echo [OK] Root dependencies installed echo. - -echo [INFO] Installing package dependencies... - -:: Install server dependencies -if exist "packages\server" ( - echo [INFO] Installing server dependencies... - cd packages\server - call npm install +echo [STEP 6/9] Building UI assets +if exist "%SCRIPT_DIR%\packages\ui\dist\index.html" ( + echo [OK] UI build already exists +) else ( + echo [INFO] Building UI assets... + pushd packages\ui + call npm run build if %ERRORLEVEL% neq 0 ( - echo [WARN] Failed to install server dependencies! - set /a WARNINGS+=1 - ) else ( - echo [OK] Server dependencies installed + echo [ERROR] UI build failed! + popd + set /a ERRORS+=1 + goto :SUMMARY ) - cd ..\.. + popd + echo [OK] UI assets built successfully ) -:: Install UI dependencies -if exist "packages\ui" ( - echo [INFO] Installing UI dependencies... - cd packages\ui - call npm install - if %ERRORLEVEL% neq 0 ( - echo [WARN] Failed to install UI dependencies! - set /a WARNINGS+=1 - ) else ( - echo [OK] UI dependencies installed - ) - cd ..\.. -) - -:: Install Electron app dependencies -if exist "packages\electron-app" ( - echo [INFO] Installing Electron app dependencies... - cd packages\electron-app - call npm install - if %ERRORLEVEL% neq 0 ( - echo [WARN] Failed to install Electron app dependencies! - set /a WARNINGS+=1 - ) else ( - echo [OK] Electron app dependencies installed - ) - cd ..\.. -) - -:BUILD_CHECK echo. +echo [STEP 7/9] Post-install health check +set HEALTH_ERRORS=0 -echo [STEP 4/6] Checking for Existing Build... -echo. +if not exist "%SCRIPT_DIR%\package.json" set /a HEALTH_ERRORS+=1 +if not exist "%SCRIPT_DIR%\packages\ui" set /a HEALTH_ERRORS+=1 +if not exist "%SCRIPT_DIR%\packages\server" set /a HEALTH_ERRORS+=1 +if not exist "%SCRIPT_DIR%\packages\ui\dist\index.html" set /a HEALTH_ERRORS+=1 -if exist "packages\ui\dist" ( - echo [OK] UI build found. Skipping build step. - echo [INFO] To rebuild, delete packages\ui\dist and run installer again. - goto :INSTALL_REPORT -) - -echo [INFO] No UI build found. Building UI... -echo. - -:: Build UI -cd packages\ui -call npm run build -if %ERRORLEVEL% neq 0 ( - echo [WARN] Failed to build UI! - set /a WARNINGS+=1 - echo [INFO] You can build manually later by running: cd packages\ui ^&^& npm run build -) -cd ..\.. - -:INSTALL_REPORT -echo. -echo ═══════════════════════════════════════════════════════════════════════════════ -echo INSTALLATION COMPLETE -echo ═══════════════════════════════════════════════════════════════════════════════ -echo. -echo Summary: -echo. -if %ERRORS% equ 0 ( - echo ✓ No errors encountered +if %HEALTH_ERRORS% equ 0 ( + echo [OK] Health checks passed ) else ( - echo ✗ %ERRORS% error(s) encountered -) -echo. -if %WARNINGS% equ 0 ( - echo ✓ No warnings -) else ( - echo ⚠ %WARNINGS% warning(s) encountered -) -echo. - -echo [STEP 5/6] Testing Installation... -echo. - -:: Test node command -node --version >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [OK] Node.js is working -) else ( - echo [FAIL] Node.js is not working correctly - set /a ERRORS+=1 -) - -:: Test npm command -npm --version >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [OK] npm is working -) else ( - echo [FAIL] npm is not working correctly - set /a ERRORS+=1 -) - -:: Test opencode command -where opencode >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [OK] OpenCode CLI is available -) else ( - if exist "bin\opencode.exe" ( - echo [OK] OpenCode binary found in bin/ folder - ) else ( - echo [FAIL] OpenCode CLI not available - set /a WARNINGS+=1 - ) + echo [ERROR] Health checks failed (%HEALTH_ERRORS%) + set /a ERRORS+=%HEALTH_ERRORS% ) echo. -echo [STEP 6/6] Next Steps... +echo [STEP 8/9] Installation Summary echo. -echo To start NomadArch: -echo 1. Double-click and run: Launch-Windows.bat -echo OR -echo 2. Run from command line: npm run dev:electron -echo. -echo For development mode: -echo Run: Launch-Dev-Windows.bat +echo Install Dir: %TARGET_DIR% +echo Architecture: %ARCH% +echo Node.js: %NODE_VERSION% +echo npm: %NPM_VERSION% +echo Errors: %ERRORS% +echo Warnings: %WARNINGS% +echo Log File: %LOG_FILE% echo. +echo [STEP 9/9] Next steps + +:SUMMARY if %ERRORS% gtr 0 ( - echo ⚠ INSTALLATION HAD ERRORS! - echo Please review the messages above and fix any issues. + echo [RESULT] Installation completed with errors. + echo Review the log: %LOG_FILE% echo. - pause - exit /b 1 + echo If Node.js was just installed, open a new terminal and run this installer again. ) else ( - echo ✓ Installation completed successfully! - echo. - echo Press any key to exit... - pause >nul - exit /b 0 + echo [RESULT] Installation completed successfully. + echo Run Launch-Windows.bat to start the application. ) + +echo. +echo Press any key to exit... +pause >nul +exit /b %ERRORS% diff --git a/Launch-Dev-Unix.sh b/Launch-Dev-Unix.sh new file mode 100644 index 0000000..8001a01 --- /dev/null +++ b/Launch-Dev-Unix.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# NomadArch Development Launcher for macOS and Linux +# Version: 0.4.0 + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +ERRORS=0 +WARNINGS=0 +AUTO_FIXED=0 + +echo "" +echo "NomadArch Development Launcher (macOS/Linux)" +echo "Version: 0.4.0" +echo "" + +echo "[PREFLIGHT 1/6] Checking Dependencies..." + +if ! command -v node &> /dev/null; then + echo -e "${YELLOW}[WARN]${NC} Node.js not found. Running installer..." + if [[ "$OSTYPE" == "darwin"* ]]; then + bash "$SCRIPT_DIR/Install-Mac.sh" + else + bash "$SCRIPT_DIR/Install-Linux.sh" + fi + echo -e "${BLUE}[INFO]${NC} If Node.js was installed, open a new terminal and run Launch-Dev-Unix.sh again." + exit 1 +fi + +NODE_VERSION=$(node --version) +echo -e "${GREEN}[OK]${NC} Node.js: $NODE_VERSION" + +if ! command -v npm &> /dev/null; then + echo -e "${RED}[ERROR]${NC} npm not found!" + exit 1 +fi + +NPM_VERSION=$(npm --version) +echo -e "${GREEN}[OK]${NC} npm: $NPM_VERSION" + +echo "" +echo "[PREFLIGHT 2/6] Installing dependencies if needed..." + +if [[ ! -d "node_modules" ]]; then + echo -e "${YELLOW}[INFO]${NC} Dependencies not installed. Installing now..." + npm install + echo -e "${GREEN}[OK]${NC} Dependencies installed (auto-fix)" + ((AUTO_FIXED++)) +fi + +echo "" +echo "[PREFLIGHT 3/6] Finding Available Ports..." + +DEFAULT_SERVER_PORT=3001 +DEFAULT_UI_PORT=3000 +SERVER_PORT=$DEFAULT_SERVER_PORT +UI_PORT=$DEFAULT_UI_PORT + +for port in {3001..3050}; do + if ! lsof -i :$port -sTCP:LISTEN -t > /dev/null 2>&1; then + SERVER_PORT=$port + break + fi +done + +for port in {3000..3050}; do + if ! lsof -i :$port -sTCP:LISTEN -t > /dev/null 2>&1; then + UI_PORT=$port + break + fi +done + +echo -e "${GREEN}[OK]${NC} Server port: $SERVER_PORT" +echo -e "${GREEN}[OK]${NC} UI port: $UI_PORT" + +echo "" +echo "[PREFLIGHT 4/6] Launch Summary" + +echo -e "${BLUE}[STATUS]${NC}" +echo "" +echo " Node.js: $NODE_VERSION" +echo " npm: $NPM_VERSION" +echo " Auto-fixes applied: $AUTO_FIXED" +echo " Warnings: $WARNINGS" +echo " Errors: $ERRORS" +echo " Server Port: $SERVER_PORT" +echo " UI Port: $UI_PORT" +echo "" + +echo "" +echo "[PREFLIGHT 5/6] Starting services..." +echo "" + +export CLI_PORT=$SERVER_PORT +export VITE_PORT=$UI_PORT + +echo -e "${GREEN}[INFO]${NC} Starting backend server..." +nohup bash -c "cd '$SCRIPT_DIR/packages/server' && npm run dev" >/dev/null 2>&1 & + +sleep 2 + +echo -e "${GREEN}[INFO]${NC} Starting UI server..." +nohup bash -c "cd '$SCRIPT_DIR/packages/ui' && npm run dev -- --port $UI_PORT" >/dev/null 2>&1 & + +sleep 2 + +echo -e "${GREEN}[INFO]${NC} Starting Electron app..." +npm run dev:electron + +echo "" +echo "[PREFLIGHT 6/6] Done." diff --git a/Launch-Dev-Windows.bat b/Launch-Dev-Windows.bat index 56ca2b5..ca827ae 100644 --- a/Launch-Dev-Windows.bat +++ b/Launch-Dev-Windows.bat @@ -1,31 +1,29 @@ @echo off -title NomadArch Development Launcher -color 0B setlocal enabledelayedexpansion +title NomadArch Development Launcher +color 0B + echo. -echo ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗ -echo ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║ -echo ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║ -echo ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║ -echo ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║ -echo ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ -echo. -echo DEVELOPMENT MODE - Separate Server & UI Terminals -echo ═════════════════════════════════════════════════════════════════════════════ +echo NomadArch Development Launcher (Windows) +echo Version: 0.4.0 echo. -cd /d "%~dp0" +set SCRIPT_DIR=%~dp0 +set SCRIPT_DIR=%SCRIPT_DIR:~0,-1% +cd /d "%SCRIPT_DIR%" -echo [STEP 1/4] Checking Dependencies... -echo. +set ERRORS=0 +set WARNINGS=0 +set AUTO_FIXED=0 + +echo [PREFLIGHT 1/7] Checking Dependencies... where node >nul 2>&1 if %ERRORLEVEL% neq 0 ( - echo [ERROR] Node.js not found! - echo. - echo Please install Node.js first: https://nodejs.org/ - echo. + echo [WARN] Node.js not found. Running installer... + call "%SCRIPT_DIR%\Install-Windows.bat" + echo [INFO] If Node.js was installed, open a new terminal and run Launch-Dev-Windows.bat again. pause exit /b 1 ) @@ -36,7 +34,6 @@ echo [OK] Node.js: %NODE_VERSION% where npm >nul 2>&1 if %ERRORLEVEL% neq 0 ( echo [ERROR] npm not found! - echo. pause exit /b 1 ) @@ -45,81 +42,141 @@ for /f "tokens=*" %%i in ('npm --version') do set NPM_VERSION=%%i echo [OK] npm: %NPM_VERSION% echo. -echo [STEP 2/4] Checking for OpenCode CLI... -echo. +echo [PREFLIGHT 2/7] Checking for OpenCode CLI... where opencode >nul 2>&1 if %ERRORLEVEL% equ 0 ( - echo [OK] OpenCode is available in PATH + echo [OK] OpenCode CLI available in PATH ) else ( if exist "bin\opencode.exe" ( - echo [OK] OpenCode binary found in bin/ folder + echo [OK] OpenCode binary found in bin/ ) else ( echo [WARN] OpenCode CLI not found - echo [INFO] Run Install-Windows.bat to install OpenCode + echo [INFO] Run Install-Windows.bat to set up OpenCode + set /a WARNINGS+=1 ) ) echo. -echo [STEP 3/4] Checking Port Availability... -echo. +echo [PREFLIGHT 3/7] Checking Dependencies... -set SERVER_PORT=3001 -set UI_PORT=3000 - -netstat -ano | findstr ":%SERVER_PORT%" | findstr "LISTENING" >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [WARN] Port %SERVER_PORT% is already in use - echo [INFO] Another NomadArch instance may be running - echo [INFO] To find process: netstat -ano | findstr ":%SERVER_PORT%" - echo [INFO] To kill it: taskkill /F /PID ^ +if not exist "node_modules" ( + echo [INFO] Dependencies not installed. Installing now... + call npm install + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Dependency installation failed! + pause + exit /b 1 + ) + echo [OK] Dependencies installed (auto-fix) + set /a AUTO_FIXED+=1 ) else ( - echo [OK] Port %SERVER_PORT% is available -) - -netstat -ano | findstr ":%UI_PORT%" | findstr "LISTENING" >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [WARN] Port %UI_PORT% is already in use - echo [INFO] To find process: netstat -ano | findstr ":%UI_PORT%" - echo [INFO] To kill it: taskkill /F /PID ^ -) else ( - echo [OK] Port %UI_PORT% is available + echo [OK] Dependencies found ) echo. -echo [STEP 4/4] Starting NomadArch in Development Mode... -echo. -echo [INFO] This will open 3 separate terminal windows: -echo 1. Backend Server (port 3001) -echo 2. Frontend UI (port 3000) -echo 3. Electron App -echo. -echo [INFO] Press any key to start... -pause >nul +echo [PREFLIGHT 4/7] Finding Available Ports... + +set DEFAULT_SERVER_PORT=3001 +set DEFAULT_UI_PORT=3000 +set SERVER_PORT=%DEFAULT_SERVER_PORT% +set UI_PORT=%DEFAULT_UI_PORT% + +for /l %%p in (%DEFAULT_SERVER_PORT%,1,3050) do ( + netstat -ano | findstr ":%%p " | findstr "LISTENING" >nul + if !ERRORLEVEL! neq 0 ( + set SERVER_PORT=%%p + goto :server_port_found + ) +) +:server_port_found + +for /l %%p in (%DEFAULT_UI_PORT%,1,3050) do ( + netstat -ano | findstr ":%%p " | findstr "LISTENING" >nul + if !ERRORLEVEL! neq 0 ( + set UI_PORT=%%p + goto :ui_port_found + ) +) +:ui_port_found + +echo [OK] Server port: !SERVER_PORT! +echo [OK] UI port: !UI_PORT! echo. -echo [INFO] Starting Backend Server... -start "NomadArch Server" cmd /k "cd /d \"%~dp0packages\server\" && npm run dev" +echo [PREFLIGHT 5/7] Final Checks... -echo [INFO] Starting Frontend UI... -start "NomadArch UI" cmd /k "cd /d \"%~dp0packages\ui\" && npm run dev" +if not exist "packages\ui\dist\index.html" ( + echo [WARN] UI build directory not found + echo [INFO] Running UI build... + pushd packages\ui + call npm run build + if %ERRORLEVEL% neq 0 ( + echo [ERROR] UI build failed! + popd + set /a ERRORS+=1 + goto :launch_check + ) + popd + echo [OK] UI build completed (auto-fix) + set /a AUTO_FIXED+=1 +) -echo [INFO] Starting Electron App... +if not exist "packages\electron-app\dist\main\main.js" ( + echo [WARN] Electron build incomplete + echo [INFO] Running full build... + call npm run build + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Full build failed! + set /a ERRORS+=1 + goto :launch_check + ) + echo [OK] Full build completed (auto-fix) + set /a AUTO_FIXED+=1 +) + +echo. +echo [PREFLIGHT 6/7] Launch Summary + +echo [STATUS] +echo. +echo Node.js: %NODE_VERSION% +echo npm: %NPM_VERSION% +echo Auto-fixes applied: !AUTO_FIXED! +echo Warnings: %WARNINGS% +echo Errors: %ERRORS% +echo Server Port: !SERVER_PORT! +echo UI Port: !UI_PORT! +echo. + +if %ERRORS% gtr 0 ( + echo [RESULT] Cannot start due to errors! + pause + exit /b 1 +) + +echo. +echo [PREFLIGHT 7/7] Starting NomadArch in Development Mode... +echo [INFO] Server: http://localhost:!SERVER_PORT! +echo [INFO] UI: http://localhost:!UI_PORT! +echo. + +start "NomadArch Server" cmd /k "cd /d \"%~dp0packages\server\" && set CLI_PORT=!SERVER_PORT! && npm run dev" +timeout /t 3 /nobreak >nul +start "NomadArch UI" cmd /k "cd /d \"%~dp0packages\ui\" && set VITE_PORT=!UI_PORT! && npm run dev -- --port !UI_PORT!" +timeout /t 3 /nobreak >nul start "NomadArch Electron" cmd /k "cd /d \"%~dp0packages\electron-app\" && npm run dev" echo. -echo [OK] All services started! -echo. -echo Press any key to stop all services (Ctrl+C in each window also works)... +echo [OK] All services started. +echo Press any key to stop all services... pause >nul -echo. -echo [INFO] Stopping all services... taskkill /F /FI "WINDOWTITLE eq NomadArch*" >nul 2>&1 taskkill /F /FI "WINDOWTITLE eq NomadArch Server*" >nul 2>&1 taskkill /F /FI "WINDOWTITLE eq NomadArch UI*" >nul 2>&1 taskkill /F /FI "WINDOWTITLE eq NomadArch Electron*" >nul 2>&1 -echo [OK] All services stopped. -echo. +:launch_check pause +exit /b %ERRORS% diff --git a/Launch-Unix.sh b/Launch-Unix.sh index eb5aa7b..57c6427 100644 --- a/Launch-Unix.sh +++ b/Launch-Unix.sh @@ -1,133 +1,170 @@ #!/bin/bash -echo "" -echo " ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗" -echo " ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║" -echo " ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║" -echo " ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║" -echo " ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║" -echo " ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝" -echo "" -echo " LAUNCHER - Linux/macOS" -echo " ═════════════════════════════════════════════════════════════════════════════" -echo "" +# NomadArch Launcher for macOS and Linux +# Version: 0.4.0 + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" ERRORS=0 WARNINGS=0 +AUTO_FIXED=0 -cd "$(dirname "$0")" - -echo "[STEP 1/5] Checking Dependencies..." +echo "" +echo "NomadArch Launcher (macOS/Linux)" +echo "Version: 0.4.0" echo "" +echo "[PREFLIGHT 1/7] Checking Dependencies..." + if ! command -v node &> /dev/null; then - echo "[ERROR] Node.js not found!" - echo "" - echo "Please install Node.js first: https://nodejs.org/" - echo "Then run: ./Install-Linux.sh (or ./Install-Mac.sh on macOS)" - echo "" + echo -e "${YELLOW}[WARN]${NC} Node.js not found. Running installer..." + if [[ "$OSTYPE" == "darwin"* ]]; then + bash "$SCRIPT_DIR/Install-Mac.sh" + else + bash "$SCRIPT_DIR/Install-Linux.sh" + fi + echo -e "${BLUE}[INFO]${NC} If Node.js was installed, open a new terminal and run Launch-Unix.sh again." exit 1 fi NODE_VERSION=$(node --version) -echo "[OK] Node.js: $NODE_VERSION" +echo -e "${GREEN}[OK]${NC} Node.js: $NODE_VERSION" if ! command -v npm &> /dev/null; then - echo "[ERROR] npm not found!" - echo "" + echo -e "${RED}[ERROR]${NC} npm not found!" exit 1 fi NPM_VERSION=$(npm --version) -echo "[OK] npm: $NPM_VERSION" +echo -e "${GREEN}[OK]${NC} npm: $NPM_VERSION" echo "" -echo "[STEP 2/5] Checking for OpenCode CLI..." -echo "" +echo "[PREFLIGHT 2/7] Checking for OpenCode CLI..." if command -v opencode &> /dev/null; then - echo "[OK] OpenCode is available in PATH" -elif [ -f "bin/opencode" ]; then - echo "[OK] OpenCode binary found in bin/ folder" + echo -e "${GREEN}[OK]${NC} OpenCode CLI available in PATH" +elif [[ -f "$SCRIPT_DIR/bin/opencode" ]]; then + echo -e "${GREEN}[OK]${NC} OpenCode binary found in bin/" else - echo "[WARN] OpenCode CLI not found" - echo "[INFO] Run ./Install-Linux.sh (or ./Install-Mac.sh on macOS) to install OpenCode" - WARNINGS=$((WARNINGS + 1)) + echo -e "${YELLOW}[WARN]${NC} OpenCode CLI not found" + echo "[INFO] Run Install-*.sh to set up OpenCode" + ((WARNINGS++)) fi echo "" -echo "[STEP 3/5] Checking for Existing Build..." -echo "" +echo "[PREFLIGHT 3/7] Checking Dependencies..." -if [ -d "packages/ui/dist" ]; then - echo "[OK] UI build found" -else - echo "[WARN] No UI build found. Building now..." - echo "" - cd packages/ui - npm run build - if [ $? -ne 0 ]; then - echo "[ERROR] UI build failed!" - ERRORS=$((ERRORS + 1)) - else - echo "[OK] UI build completed" +if [[ ! -d "node_modules" ]]; then + echo -e "${YELLOW}[INFO]${NC} Dependencies not installed. Installing now..." + if ! npm install; then + echo -e "${RED}[ERROR]${NC} Dependency installation failed!" + exit 1 fi - cd ../.. -fi - -echo "" -echo "[STEP 4/5] Checking Port Availability..." -echo "" - -SERVER_PORT=3001 -UI_PORT=3000 - -if lsof -Pi :$SERVER_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then - echo "[WARN] Port $SERVER_PORT is already in use" - echo "[INFO] Another NomadArch instance or app may be running" - echo "[INFO] To find the process: lsof -i :$SERVER_PORT" - echo "[INFO] To kill it: kill -9 \$(lsof -t -i:$SERVER_PORT)" - WARNINGS=$((WARNINGS + 1)) + echo -e "${GREEN}[OK]${NC} Dependencies installed (auto-fix)" + ((AUTO_FIXED++)) else - echo "[OK] Port $SERVER_PORT is available" + echo -e "${GREEN}[OK]${NC} Dependencies found" fi -if lsof -Pi :$UI_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then - echo "[WARN] Port $UI_PORT is already in use" - echo "[INFO] To find the process: lsof -i :$UI_PORT" - echo "[INFO] To kill it: kill -9 \$(lsof -t -i:$UI_PORT)" - WARNINGS=$((WARNINGS + 1)) +echo "" +echo "[PREFLIGHT 4/7] Finding Available Port..." + +DEFAULT_SERVER_PORT=3001 +DEFAULT_UI_PORT=3000 +SERVER_PORT=$DEFAULT_SERVER_PORT +UI_PORT=$DEFAULT_UI_PORT + +for port in {3001..3050}; do + if ! lsof -i :$port -sTCP:LISTEN -t > /dev/null 2>&1; then + SERVER_PORT=$port + break + fi +done + +echo -e "${GREEN}[OK]${NC} Server port: $SERVER_PORT" + +echo "" +echo "[PREFLIGHT 5/7] Final Checks..." + +if [[ ! -d "packages/ui/dist" ]]; then + echo -e "${YELLOW}[WARN]${NC} UI build directory not found" + echo -e "${YELLOW}[INFO]${NC} Running UI build..." + pushd packages/ui >/dev/null + if ! npm run build; then + echo -e "${RED}[ERROR]${NC} UI build failed!" + popd >/dev/null + ((ERRORS++)) + else + popd >/dev/null + echo -e "${GREEN}[OK]${NC} UI build completed (auto-fix)" + ((AUTO_FIXED++)) + fi else - echo "[OK] Port $UI_PORT is available" + echo -e "${GREEN}[OK]${NC} UI build directory exists" +fi + +if [[ ! -f "packages/electron-app/dist/main/main.js" ]]; then + echo -e "${YELLOW}[WARN]${NC} Electron build incomplete" + echo -e "${YELLOW}[INFO]${NC} Running full build..." + if ! npm run build; then + echo -e "${RED}[ERROR]${NC} Full build failed!" + ((ERRORS++)) + else + echo -e "${GREEN}[OK]${NC} Full build completed (auto-fix)" + ((AUTO_FIXED++)) + fi +else + echo -e "${GREEN}[OK]${NC} Electron build exists" fi echo "" -echo "[STEP 5/5] Starting NomadArch..." +echo "[PREFLIGHT 6/7] Launch Summary" + +echo -e "${BLUE}[STATUS]${NC}" +echo "" +echo " Node.js: $NODE_VERSION" +echo " npm: $NPM_VERSION" +echo " Auto-fixes applied: $AUTO_FIXED" +echo " Warnings: $WARNINGS" +echo " Errors: $ERRORS" +echo " Server Port: $SERVER_PORT" echo "" -if [ $ERRORS -gt 0 ]; then - echo "[ERROR] Cannot start due to errors!" - echo "" +if [[ $ERRORS -gt 0 ]]; then + echo -e "${RED}[RESULT]${NC} Cannot start due to errors!" exit 1 fi -echo "[INFO] Starting NomadArch..." -echo "[INFO] Server will run on http://localhost:$SERVER_PORT" -echo "[INFO] Press Ctrl+C to stop" +echo -e "${GREEN}[INFO]${NC} Starting NomadArch..." +echo -e "${GREEN}[INFO]${NC} Server will run on http://localhost:$SERVER_PORT" +echo -e "${YELLOW}[INFO]${NC} Press Ctrl+C to stop" echo "" +SERVER_URL="http://localhost:$SERVER_PORT" + +if [[ "$OSTYPE" == "darwin"* ]]; then + open "$SERVER_URL" 2>/dev/null || true +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + xdg-open "$SERVER_URL" 2>/dev/null || true +fi + +export CLI_PORT=$SERVER_PORT npm run dev:electron -if [ $? -ne 0 ]; then - echo "" - echo "[ERROR] NomadArch exited with an error!" - echo "" - echo "Common solutions:" - echo " 1. Check that all dependencies are installed: npm install" - echo " 2. Check that the UI is built: cd packages/ui && npm run build" - echo " 3. Check for port conflicts (see warnings above)" - echo " 4. Check the error message above for details" - echo "" - echo "To reinstall everything: ./Install-Linux.sh (or ./Install-Mac.sh on macOS)" +EXIT_CODE=$? + +if [[ $EXIT_CODE -ne 0 ]]; then echo "" + echo -e "${RED}[ERROR]${NC} NomadArch exited with an error!" fi + +exit $EXIT_CODE diff --git a/Launch-Windows-Prod.bat b/Launch-Windows-Prod.bat index 585a0fb..d40cbf9 100644 --- a/Launch-Windows-Prod.bat +++ b/Launch-Windows-Prod.bat @@ -1,33 +1,26 @@ @echo off -title NomadArch Launcher (Production Mode) -color 0A setlocal enabledelayedexpansion +title NomadArch Launcher (Production Mode) +color 0A + echo. -echo ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗ -echo ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║ -echo ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║ -echo ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║ -echo ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║ -echo ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ -echo. -echo PRODUCTION LAUNCHER - Using Pre-Built Enhanced UI -echo ═════════════════════════════════════════════════════════════════════════════ -echo. -echo Features: SMART FIX / APEX / SHIELD / MULTIX MODE +echo NomadArch Launcher (Windows, Production Mode) +echo Version: 0.4.0 +echo Features: SMART FIX / APEX / SHIELD / MULTIX MODE echo. -cd /d "%~dp0" +set SCRIPT_DIR=%~dp0 +set SCRIPT_DIR=%SCRIPT_DIR:~0,-1% +cd /d "%SCRIPT_DIR%" echo [STEP 1/3] Checking Dependencies... -echo. where node >nul 2>&1 if %ERRORLEVEL% neq 0 ( - echo [ERROR] Node.js not found! - echo. - echo Please install Node.js first: https://nodejs.org/ - echo. + echo [WARN] Node.js not found. Running installer... + call "%SCRIPT_DIR%\Install-Windows.bat" + echo [INFO] If Node.js was installed, open a new terminal and run Launch-Windows-Prod.bat again. pause exit /b 1 ) @@ -37,26 +30,22 @@ echo [OK] Node.js: %NODE_VERSION% echo. echo [STEP 2/3] Checking Pre-Built UI... -echo. -if exist "packages\electron-app\dist\renderer\assets\main-B67Oskqu.js" ( - echo [OK] Enhanced UI build found with custom features +if exist "packages\electron-app\dist\renderer\assets" ( + echo [OK] Pre-built UI assets found ) else ( - echo [ERROR] Pre-built UI with enhancements not found! - echo [INFO] Run: npm run build to create the production build + echo [ERROR] Pre-built UI assets not found. + echo Run: npm run build pause exit /b 1 ) echo. echo [STEP 3/3] Starting NomadArch (Production Mode)... -echo. -cd packages\electron-app - -:: Run using npx electron with the built dist -echo [INFO] Starting Electron with pre-built enhanced UI... +pushd packages\electron-app npx electron . +popd if %ERRORLEVEL% neq 0 ( echo. @@ -65,3 +54,4 @@ if %ERRORLEVEL% neq 0 ( ) pause +exit /b %ERRORLEVEL% diff --git a/Launch-Windows.bat b/Launch-Windows.bat index 400aeaf..0b0c739 100644 --- a/Launch-Windows.bat +++ b/Launch-Windows.bat @@ -1,35 +1,29 @@ @echo off -title NomadArch Launcher -color 0A setlocal enabledelayedexpansion +title NomadArch Launcher +color 0A + echo. -echo ███╗ ██╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗ ██████╗██╗ ██╗ -echo ████╗ ██║██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║ -echo ██╔██╗ ██║██║ ██║██╔████╔██║███████║██║ ██║███████║██████╔╝██║ ███████║ -echo ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║██╔══██║██╔══██╗██║ ██╔══██║ -echo ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝██║ ██║██║ ██║╚██████╗██║ ██║ -echo ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ -echo. -echo LAUNCHER - Enhanced with Auto-Fix Capabilities -echo ═════════════════════════════════════════════════════════════════════════════ +echo NomadArch Launcher (Windows) +echo Version: 0.4.0 echo. -cd /d "%~dp0" +set SCRIPT_DIR=%~dp0 +set SCRIPT_DIR=%SCRIPT_DIR:~0,-1% +cd /d "%SCRIPT_DIR%" set ERRORS=0 set WARNINGS=0 +set AUTO_FIXED=0 -echo [STEP 1/5] Checking Dependencies... -echo. +echo [PREFLIGHT 1/7] Checking Dependencies... where node >nul 2>&1 if %ERRORLEVEL% neq 0 ( - echo [ERROR] Node.js not found! - echo. - echo Please install Node.js first: https://nodejs.org/ - echo Then run: Install-Windows.bat - echo. + echo [WARN] Node.js not found. Running installer... + call "%SCRIPT_DIR%\Install-Windows.bat" + echo [INFO] If Node.js was installed, open a new terminal and run Launch-Windows.bat again. pause exit /b 1 ) @@ -40,7 +34,6 @@ echo [OK] Node.js: %NODE_VERSION% where npm >nul 2>&1 if %ERRORLEVEL% neq 0 ( echo [ERROR] npm not found! - echo. pause exit /b 1 ) @@ -49,100 +42,162 @@ for /f "tokens=*" %%i in ('npm --version') do set NPM_VERSION=%%i echo [OK] npm: %NPM_VERSION% echo. -echo [STEP 2/5] Checking for OpenCode CLI... -echo. +echo [PREFLIGHT 2/7] Checking for OpenCode CLI... where opencode >nul 2>&1 if %ERRORLEVEL% equ 0 ( - echo [OK] OpenCode is available in PATH + echo [OK] OpenCode CLI available in PATH ) else ( if exist "bin\opencode.exe" ( - echo [OK] OpenCode binary found in bin/ folder + echo [OK] OpenCode binary found in bin/ ) else ( echo [WARN] OpenCode CLI not found - echo [INFO] Run Install-Windows.bat to install OpenCode + echo [INFO] Run Install-Windows.bat to set up OpenCode set /a WARNINGS+=1 ) ) echo. -echo [STEP 3/5] Checking for Existing Build... -echo. +echo [PREFLIGHT 3/7] Checking Dependencies... -if exist "packages\ui\dist" ( - echo [OK] UI build found +if not exist "node_modules" ( + echo [INFO] Dependencies not installed. Installing now... + call npm install + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Dependency installation failed! + pause + exit /b 1 + ) + echo [OK] Dependencies installed (auto-fix) + set /a AUTO_FIXED+=1 ) else ( - echo [WARN] No UI build found. Building now... - echo. - cd packages\ui + echo [OK] Dependencies found +) + +echo. +echo [PREFLIGHT 4/7] Finding Available Port... + +set DEFAULT_SERVER_PORT=3001 +set DEFAULT_UI_PORT=3000 +set SERVER_PORT=%DEFAULT_SERVER_PORT% +set UI_PORT=%DEFAULT_UI_PORT% + +for /l %%p in (%DEFAULT_SERVER_PORT%,1,3050) do ( + netstat -ano | findstr ":%%p " | findstr "LISTENING" >nul + if !ERRORLEVEL! neq 0 ( + set SERVER_PORT=%%p + goto :server_port_found + ) +) +:server_port_found + +echo [OK] Server port: !SERVER_PORT! + +if !SERVER_PORT! neq %DEFAULT_SERVER_PORT% ( + echo [INFO] Port %DEFAULT_SERVER_PORT% was in use, using !SERVER_PORT! instead + set /a WARNINGS+=1 +) + +echo. +echo [PREFLIGHT 5/7] Final Checks... + +if not exist "packages\ui\dist\index.html" ( + echo [WARN] UI build directory not found + echo [INFO] Running UI build... + pushd packages\ui call npm run build if %ERRORLEVEL% neq 0 ( echo [ERROR] UI build failed! + popd set /a ERRORS+=1 - ) else ( - echo [OK] UI build completed + goto :final_launch_check ) - cd ..\.. -) - -echo. -echo [STEP 4/5] Checking Port Availability... -echo. - -set SERVER_PORT=3001 -set UI_PORT=3000 - -netstat -ano | findstr ":%SERVER_PORT%" | findstr "LISTENING" >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [WARN] Port %SERVER_PORT% is already in use - echo [INFO] Another NomadArch instance or app may be running - echo [INFO] To find the process: netstat -ano | findstr ":%SERVER_PORT%" - echo [INFO] To kill it: taskkill /F /PID - set /a WARNINGS+=1 + popd + echo [OK] UI build completed (auto-fix) + set /a AUTO_FIXED+=1 ) else ( - echo [OK] Port %SERVER_PORT% is available + echo [OK] UI build directory exists ) -netstat -ano | findstr ":%UI_PORT%" | findstr "LISTENING" >nul 2>&1 -if %ERRORLEVEL% equ 0 ( - echo [WARN] Port %UI_PORT% is already in use - echo [INFO] To find the process: netstat -ano | findstr ":%UI_PORT%" - echo [INFO] To kill it: taskkill /F /PID - set /a WARNINGS+=1 -) else ( - echo [OK] Port %UI_PORT% is available +if not exist "packages\electron-app\dist\main\main.js" ( + echo [WARN] Electron build incomplete + echo [INFO] Running full build... + call npm run build + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Full build failed! + set /a ERRORS+=1 + goto :final_launch_check + ) + echo [OK] Full build completed (auto-fix) + set /a AUTO_FIXED+=1 ) echo. -echo [STEP 5/5] Starting NomadArch... +echo [PREFLIGHT 6/7] Launch Summary + +echo [STATUS] +echo. +echo Node.js: %NODE_VERSION% +echo npm: %NPM_VERSION% +echo Auto-fixes applied: !AUTO_FIXED! +echo Warnings: %WARNINGS% +echo Errors: %ERRORS% +echo Server Port: !SERVER_PORT! echo. if %ERRORS% gtr 0 ( - echo [ERROR] Cannot start due to errors! + echo [RESULT] Cannot start due to errors! echo. + echo Please fix the errors above and try again. pause exit /b 1 ) echo [INFO] Starting NomadArch... -echo [INFO] Server will run on http://localhost:%SERVER_PORT% +echo [INFO] Server will run on http://localhost:!SERVER_PORT! +echo [INFO] UI will run on http://localhost:!UI_PORT! echo [INFO] Press Ctrl+C to stop echo. +set SERVER_URL=http://localhost:!SERVER_PORT! +set VITE_PORT=!UI_PORT! + +echo. +echo ======================================== +echo Starting UI dev server on port !UI_PORT!... +echo ======================================== + +pushd packages\ui +start "NomadArch UI Server" cmd /c "set VITE_PORT=!UI_PORT! && npm run dev" +popd + +echo [INFO] Waiting for UI dev server to start... +timeout /t 3 /nobreak >nul + +echo. +echo ======================================== +echo Starting Electron app... +echo ======================================== + +set "VITE_DEV_SERVER_URL=http://localhost:!UI_PORT!" +set "NOMADARCH_OPEN_DEVTOOLS=false" call npm run dev:electron if %ERRORLEVEL% neq 0 ( echo. echo [ERROR] NomadArch exited with an error! echo. - echo Common solutions: - echo 1. Check that all dependencies are installed: npm install - echo 2. Check that the UI is built: cd packages\ui ^&^& npm run build - echo 3. Check for port conflicts (see warnings above) - echo 4. Check the error message above for details + echo Error Code: %ERRORLEVEL% echo. - echo To reinstall everything: Install-Windows.bat + echo Troubleshooting: + echo 1. Ensure port !SERVER_PORT! is not in use + echo 2. Run Install-Windows.bat again + echo 3. Check log file: packages\electron-app\.log echo. ) -pause +:final_launch_check +echo. +echo Press any key to exit... +pause >nul +exit /b %ERRORS% diff --git a/package-lock.json b/package-lock.json index a21fa89..d926e8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,12 @@ "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" }, + "devDependencies": { + "rollup": "^4.54.0" + }, + "optionalDependencies": { + "@esbuild/win32-x64": "^0.27.2" + }, "workspaces": { "packages": [ "packages/*" @@ -61,7 +67,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -648,6 +653,91 @@ "node": ">= 10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-x64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", @@ -665,6 +755,345 @@ "node": ">=18" } }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@fastify/accept-negotiator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", @@ -1275,10 +1704,28 @@ "node": ">= 8" } }, + "node_modules/@opencode-ai/plugin": { + "version": "1.0.180", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.0.180.tgz", + "integrity": "sha512-u31txsEXmf6EpMRKYcY5S8ltwxLckXqvdxTICFaopFoT/vw7b1mPOxsm2II4QbmPa4uWnDNDt7HH8fh7PTF07w==", + "dependencies": { + "@opencode-ai/sdk": "1.0.180", + "zod": "4.1.8" + } + }, + "node_modules/@opencode-ai/plugin/node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@opencode-ai/sdk": { - "version": "1.0.138", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.0.138.tgz", - "integrity": "sha512-9vXmpiAVVrhMZ3YNr7BGScyULFLyN0vnRx7iCDtN5qQDKxtsdQcXSQCz35XiVyD3A8lH5KOf5Zn0ByLYXuNeFQ==" + "version": "1.0.180", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.0.180.tgz", + "integrity": "sha512-r3bocI5SR72FDORMC6O+oD9sKtEqUFgAboH14uwwJNF07xENxi8H1Bc9bmeYKdcJm4MVIURMLGBhUnOUhDMsuQ==" }, "node_modules/@pinojs/redact": { "version": "0.4.0", @@ -1306,10 +1753,52 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "cpu": [ "x64" ], @@ -1320,6 +1809,258 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@shikijs/core": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.15.0.tgz", @@ -2054,7 +2795,6 @@ "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2194,7 +2934,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2423,6 +3162,7 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -2442,6 +3182,7 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -2464,6 +3205,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2479,7 +3221,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", @@ -2487,6 +3230,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -2731,6 +3475,7 @@ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -2805,7 +3550,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3291,6 +4035,7 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -3418,6 +4163,7 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -3431,6 +4177,7 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -3730,7 +4477,6 @@ "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", @@ -3923,6 +4669,7 @@ "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", @@ -3936,6 +4683,7 @@ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3951,6 +4699,7 @@ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -3964,6 +4713,7 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -4241,6 +4991,23 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4672,7 +5439,8 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fs-extra": { "version": "8.1.0", @@ -5508,7 +6276,8 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/isbinaryfile": { "version": "5.0.6", @@ -5568,7 +6337,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5713,6 +6481,7 @@ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -5726,6 +6495,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5741,7 +6511,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", @@ -5749,6 +6520,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -5808,35 +6580,40 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lowercase-keys": { "version": "2.0.0", @@ -6414,6 +7191,10 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/opencode-config": { + "resolved": "packages/opencode-config", + "link": true + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -6691,7 +7472,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6840,7 +7620,8 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/process-warning": { "version": "3.0.0", @@ -7106,6 +7887,7 @@ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -7121,6 +7903,7 @@ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "minimatch": "^5.1.0" } @@ -7360,9 +8143,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, "license": "MIT", "dependencies": { @@ -7376,28 +8159,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" } }, @@ -7533,7 +8316,6 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -7676,7 +8458,6 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", @@ -7823,6 +8604,7 @@ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -8076,6 +8858,7 @@ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -8311,13 +9094,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -8330,6 +9113,473 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -8350,7 +9600,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8574,7 +9823,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -8654,6 +9902,91 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", @@ -8671,6 +10004,295 @@ "node": ">=12" } }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite/node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -8885,6 +10507,7 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -8900,6 +10523,7 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", @@ -8945,6 +10569,7 @@ "devDependencies": { "7zip-bin": "^5.2.0", "app-builder-bin": "^4.2.0", + "cross-env": "^7.0.3", "electron": "39.0.0", "electron-builder": "^24.0.0", "electron-vite": "4.0.1", @@ -8963,6 +10588,11 @@ "dev": true, "license": "MIT" }, + "packages/opencode-config": { + "dependencies": { + "@opencode-ai/plugin": "1.0.180" + } + }, "packages/server": { "name": "@neuralnomads/codenomad", "version": "0.4.0", @@ -9033,9 +10663,11 @@ "autoprefixer": "10.4.21", "postcss": "8.5.6", "tailwindcss": "3", + "tsx": "^4.21.0", "typescript": "^5.3.0", "vite": "^5.0.0", - "vite-plugin-solid": "^2.10.0" + "vite-plugin-solid": "^2.10.0", + "zod": "^3.25.76" } } } diff --git a/package.json b/package.json index 9194aec..cd87982 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ ] }, "scripts": { - "dev": "npm run dev --workspace @neuralnomads/codenomad-electron-app", - "dev:electron": "npm run dev --workspace @neuralnomads/codenomad-electron-app", + "dev": "npm run dev:electron --workspace @neuralnomads/codenomad-electron-app", + "dev:electron": "npm run dev:electron --workspace @neuralnomads/codenomad-electron-app", "dev:tauri": "npm run dev --workspace @codenomad/tauri-app", "build": "npm run build --workspace @neuralnomads/codenomad-electron-app", "build:tauri": "npm run build --workspace @codenomad/tauri-app", @@ -23,5 +23,11 @@ "dependencies": { "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" + }, + "devDependencies": { + "rollup": "^4.54.0" + }, + "optionalDependencies": { + "@esbuild/win32-x64": "^0.27.2" } } diff --git a/packages/electron-app/electron/main/ipc.ts b/packages/electron-app/electron/main/ipc.ts index 26f6499..1d15fd3 100644 --- a/packages/electron-app/electron/main/ipc.ts +++ b/packages/electron-app/electron/main/ipc.ts @@ -1,5 +1,17 @@ import { BrowserWindow, dialog, ipcMain, type OpenDialogOptions } from "electron" +import path from "path" import type { CliProcessManager, CliStatus } from "./process-manager" +import { + listUsers, + createUser, + updateUser, + deleteUser, + verifyPassword, + setActiveUser, + createGuestUser, + getActiveUser, + getUserDataRoot, +} from "./user-store" interface DialogOpenRequest { mode: "directory" | "file" @@ -40,6 +52,41 @@ export function setupCliIPC(mainWindow: BrowserWindow, cliManager: CliProcessMan return cliManager.start({ dev: devMode }) }) + ipcMain.handle("users:list", async () => listUsers()) + ipcMain.handle("users:active", async () => getActiveUser()) + ipcMain.handle("users:create", async (_, payload: { name: string; password: string }) => { + const user = createUser(payload.name, payload.password) + return user + }) + ipcMain.handle("users:update", async (_, payload: { id: string; name?: string; password?: string }) => { + const user = updateUser(payload.id, { name: payload.name, password: payload.password }) + return user + }) + ipcMain.handle("users:delete", async (_, payload: { id: string }) => { + deleteUser(payload.id) + return { success: true } + }) + ipcMain.handle("users:createGuest", async () => { + const user = createGuestUser() + return user + }) + ipcMain.handle("users:login", async (_, payload: { id: string; password?: string }) => { + const ok = verifyPassword(payload.id, payload.password ?? "") + if (!ok) { + return { success: false } + } + const user = setActiveUser(payload.id) + const root = getUserDataRoot(user.id) + cliManager.setUserEnv({ + CODENOMAD_USER_DIR: root, + CLI_CONFIG: path.join(root, "config.json"), + }) + await cliManager.stop() + const devMode = process.env.NODE_ENV === "development" + await cliManager.start({ dev: devMode }) + return { success: true, user } + }) + ipcMain.handle("dialog:open", async (_, request: DialogOpenRequest): Promise => { const properties: OpenDialogOptions["properties"] = request.mode === "directory" ? ["openDirectory", "createDirectory"] : ["openFile"] diff --git a/packages/electron-app/electron/main/main.ts b/packages/electron-app/electron/main/main.ts index 56832ca..d709ddb 100644 --- a/packages/electron-app/electron/main/main.ts +++ b/packages/electron-app/electron/main/main.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from "url" import { createApplicationMenu } from "./menu" import { setupCliIPC } from "./ipc" import { CliProcessManager } from "./process-manager" +import { ensureDefaultUsers, getActiveUser, getUserDataRoot, clearGuestUsers } from "./user-store" const mainFilename = fileURLToPath(import.meta.url) const mainDirname = dirname(mainFilename) @@ -225,6 +226,24 @@ function getPreloadPath() { return join(mainDirname, "../preload/index.js") } +function applyUserEnvToCli() { + const active = getActiveUser() + if (!active) { + const fallback = ensureDefaultUsers() + const fallbackRoot = getUserDataRoot(fallback.id) + cliManager.setUserEnv({ + CODENOMAD_USER_DIR: fallbackRoot, + CLI_CONFIG: join(fallbackRoot, "config.json"), + }) + return + } + const root = getUserDataRoot(active.id) + cliManager.setUserEnv({ + CODENOMAD_USER_DIR: root, + CLI_CONFIG: join(root, "config.json"), + }) +} + function destroyPreloadingView(target?: BrowserView | null) { const view = target ?? preloadingView if (!view) { @@ -274,7 +293,7 @@ function createWindow() { currentCliUrl = null loadLoadingScreen(mainWindow) - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === "development" && process.env.NOMADARCH_OPEN_DEVTOOLS === "true") { mainWindow.webContents.openDevTools({ mode: "detach" }) } @@ -452,6 +471,8 @@ if (isMac) { } app.whenReady().then(() => { + ensureDefaultUsers() + applyUserEnvToCli() startCli() if (isMac) { @@ -480,6 +501,7 @@ app.whenReady().then(() => { app.on("before-quit", async (event) => { event.preventDefault() await cliManager.stop().catch(() => { }) + clearGuestUsers() app.exit(0) }) diff --git a/packages/electron-app/electron/main/process-manager.ts b/packages/electron-app/electron/main/process-manager.ts index 1630b87..fad30a5 100644 --- a/packages/electron-app/electron/main/process-manager.ts +++ b/packages/electron-app/electron/main/process-manager.ts @@ -79,6 +79,11 @@ export class CliProcessManager extends EventEmitter { private status: CliStatus = { state: "stopped" } private stdoutBuffer = "" private stderrBuffer = "" + private userEnv: Record = {} + + setUserEnv(env: Record) { + this.userEnv = { ...env } + } async start(options: StartOptions): Promise { if (this.child) { @@ -100,6 +105,7 @@ export class CliProcessManager extends EventEmitter { const env = supportsUserShell() ? getUserShellEnv() : { ...process.env } env.ELECTRON_RUN_AS_NODE = "1" + Object.assign(env, this.userEnv) const spawnDetails = supportsUserShell() ? buildUserShellCommand(`ELECTRON_RUN_AS_NODE=1 exec ${this.buildCommand(cliEntry, args)}`) @@ -274,7 +280,8 @@ export class CliProcessManager extends EventEmitter { const args = ["serve", "--host", host, "--port", "0"] if (options.dev) { - args.push("--ui-dev-server", "http://localhost:3000", "--log-level", "debug") + const uiPort = process.env.VITE_PORT || "3000" + args.push("--ui-dev-server", `http://localhost:${uiPort}`, "--log-level", "debug") } return args diff --git a/packages/electron-app/electron/main/user-store.ts b/packages/electron-app/electron/main/user-store.ts new file mode 100644 index 0000000..2831d79 --- /dev/null +++ b/packages/electron-app/electron/main/user-store.ts @@ -0,0 +1,267 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, cpSync } from "fs" +import os from "os" +import path from "path" +import crypto from "crypto" + +interface UserRecord { + id: string + name: string + salt?: string + passwordHash?: string + isGuest?: boolean + createdAt: string + updatedAt: string +} + +interface UserStoreState { + users: UserRecord[] + activeUserId?: string +} + +const CONFIG_ROOT = path.join(os.homedir(), ".config", "codenomad") +const USERS_FILE = path.join(CONFIG_ROOT, "users.json") +const USERS_ROOT = path.join(CONFIG_ROOT, "users") +const LEGACY_ROOT = CONFIG_ROOT +const LEGACY_INTEGRATIONS_ROOT = path.join(os.homedir(), ".nomadarch") + +function nowIso() { + return new Date().toISOString() +} + +function sanitizeId(value: string) { + return value + .toLowerCase() + .trim() + .replace(/[^a-z0-9-_]+/g, "-") + .replace(/-{2,}/g, "-") + .replace(/^-|-$/g, "") +} + +function hashPassword(password: string, salt: string) { + return crypto.pbkdf2Sync(password, salt, 120000, 32, "sha256").toString("base64") +} + +function generateSalt() { + return crypto.randomBytes(16).toString("base64") +} + +function ensureDir(dir: string) { + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } +} + +function readStore(): UserStoreState { + try { + if (!existsSync(USERS_FILE)) { + return { users: [] } + } + const content = readFileSync(USERS_FILE, "utf-8") + const parsed = JSON.parse(content) as UserStoreState + return { + users: Array.isArray(parsed.users) ? parsed.users : [], + activeUserId: parsed.activeUserId, + } + } catch { + return { users: [] } + } +} + +function writeStore(state: UserStoreState) { + ensureDir(CONFIG_ROOT) + ensureDir(USERS_ROOT) + writeFileSync(USERS_FILE, JSON.stringify(state, null, 2), "utf-8") +} + +function ensureUniqueId(base: string, existing: Set) { + let candidate = sanitizeId(base) || "user" + let index = 1 + while (existing.has(candidate)) { + candidate = `${candidate}-${index}` + index += 1 + } + return candidate +} + +function getUserDir(userId: string) { + return path.join(USERS_ROOT, userId) +} + +function migrateLegacyData(targetDir: string) { + const legacyConfig = path.join(LEGACY_ROOT, "config.json") + const legacyInstances = path.join(LEGACY_ROOT, "instances") + const legacyWorkspaces = path.join(LEGACY_ROOT, "opencode-workspaces") + + ensureDir(targetDir) + + if (existsSync(legacyConfig)) { + cpSync(legacyConfig, path.join(targetDir, "config.json"), { force: true }) + } + if (existsSync(legacyInstances)) { + cpSync(legacyInstances, path.join(targetDir, "instances"), { recursive: true, force: true }) + } + if (existsSync(legacyWorkspaces)) { + cpSync(legacyWorkspaces, path.join(targetDir, "opencode-workspaces"), { recursive: true, force: true }) + } + + if (existsSync(LEGACY_INTEGRATIONS_ROOT)) { + cpSync(LEGACY_INTEGRATIONS_ROOT, path.join(targetDir, "integrations"), { recursive: true, force: true }) + } +} + +export function ensureDefaultUsers(): UserRecord { + const store = readStore() + if (store.users.length > 0) { + const active = store.users.find((u) => u.id === store.activeUserId) ?? store.users[0] + if (!store.activeUserId) { + store.activeUserId = active.id + writeStore(store) + } + return active + } + + const existingIds = new Set() + const userId = ensureUniqueId("roman", existingIds) + const salt = generateSalt() + const passwordHash = hashPassword("q1w2e3r4", salt) + const record: UserRecord = { + id: userId, + name: "roman", + salt, + passwordHash, + createdAt: nowIso(), + updatedAt: nowIso(), + } + + store.users.push(record) + store.activeUserId = record.id + writeStore(store) + + const userDir = getUserDir(record.id) + migrateLegacyData(userDir) + + return record +} + +export function listUsers(): UserRecord[] { + return readStore().users +} + +export function getActiveUser(): UserRecord | null { + const store = readStore() + if (!store.activeUserId) return null + return store.users.find((user) => user.id === store.activeUserId) ?? null +} + +export function setActiveUser(userId: string) { + const store = readStore() + const user = store.users.find((u) => u.id === userId) + if (!user) { + throw new Error("User not found") + } + store.activeUserId = userId + writeStore(store) + return user +} + +export function createUser(name: string, password: string) { + const store = readStore() + const existingIds = new Set(store.users.map((u) => u.id)) + const id = ensureUniqueId(name, existingIds) + const salt = generateSalt() + const passwordHash = hashPassword(password, salt) + const record: UserRecord = { + id, + name, + salt, + passwordHash, + createdAt: nowIso(), + updatedAt: nowIso(), + } + store.users.push(record) + writeStore(store) + ensureDir(getUserDir(id)) + return record +} + +export function createGuestUser() { + const store = readStore() + const existingIds = new Set(store.users.map((u) => u.id)) + const id = ensureUniqueId(`guest-${crypto.randomUUID().slice(0, 8)}`, existingIds) + const record: UserRecord = { + id, + name: "Guest", + isGuest: true, + createdAt: nowIso(), + updatedAt: nowIso(), + } + store.users.push(record) + store.activeUserId = id + writeStore(store) + ensureDir(getUserDir(id)) + return record +} + +export function updateUser(userId: string, updates: { name?: string; password?: string }) { + const store = readStore() + const target = store.users.find((u) => u.id === userId) + if (!target) { + throw new Error("User not found") + } + if (updates.name) { + target.name = updates.name + } + if (updates.password && !target.isGuest) { + const salt = generateSalt() + target.salt = salt + target.passwordHash = hashPassword(updates.password, salt) + } + target.updatedAt = nowIso() + writeStore(store) + return target +} + +export function deleteUser(userId: string) { + const store = readStore() + const target = store.users.find((u) => u.id === userId) + if (!target) return + store.users = store.users.filter((u) => u.id !== userId) + if (store.activeUserId === userId) { + store.activeUserId = store.users[0]?.id + } + writeStore(store) + const dir = getUserDir(userId) + if (existsSync(dir)) { + rmSync(dir, { recursive: true, force: true }) + } +} + +export function verifyPassword(userId: string, password: string): boolean { + const store = readStore() + const user = store.users.find((u) => u.id === userId) + if (!user) return false + if (user.isGuest) return true + if (!user.salt || !user.passwordHash) return false + return hashPassword(password, user.salt) === user.passwordHash +} + +export function getUserDataRoot(userId: string) { + return getUserDir(userId) +} + +export function clearGuestUsers() { + const store = readStore() + const guests = store.users.filter((u) => u.isGuest) + if (guests.length === 0) return + store.users = store.users.filter((u) => !u.isGuest) + if (store.activeUserId && guests.some((u) => u.id === store.activeUserId)) { + store.activeUserId = store.users[0]?.id + } + writeStore(store) + for (const guest of guests) { + const dir = getUserDir(guest.id) + if (existsSync(dir)) { + rmSync(dir, { recursive: true, force: true }) + } + } +} diff --git a/packages/electron-app/electron/preload/index.cjs b/packages/electron-app/electron/preload/index.cjs index 8a7d6bf..40b04f6 100644 --- a/packages/electron-app/electron/preload/index.cjs +++ b/packages/electron-app/electron/preload/index.cjs @@ -12,6 +12,13 @@ const electronAPI = { getCliStatus: () => ipcRenderer.invoke("cli:getStatus"), restartCli: () => ipcRenderer.invoke("cli:restart"), openDialog: (options) => ipcRenderer.invoke("dialog:open", options), + listUsers: () => ipcRenderer.invoke("users:list"), + getActiveUser: () => ipcRenderer.invoke("users:active"), + createUser: (payload) => ipcRenderer.invoke("users:create", payload), + updateUser: (payload) => ipcRenderer.invoke("users:update", payload), + deleteUser: (payload) => ipcRenderer.invoke("users:delete", payload), + createGuest: () => ipcRenderer.invoke("users:createGuest"), + loginUser: (payload) => ipcRenderer.invoke("users:login", payload), } contextBridge.exposeInMainWorld("electronAPI", electronAPI) diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index 0a72cf7..40984d0 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -15,7 +15,7 @@ "homepage": "https://github.com/NeuralNomadsAI/CodeNomad", "scripts": { "dev": "electron-vite dev", - "dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts", + "dev:electron": "cross-env NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts", "build": "electron-vite build", "typecheck": "tsc --noEmit -p tsconfig.json", "preview": "electron-vite preview", @@ -40,6 +40,7 @@ "devDependencies": { "7zip-bin": "^5.2.0", "app-builder-bin": "^4.2.0", + "cross-env": "^7.0.3", "electron": "39.0.0", "electron-builder": "^24.0.0", "electron-vite": "4.0.1", diff --git a/packages/server/src/api-types.ts b/packages/server/src/api-types.ts index f4370f5..1703af7 100644 --- a/packages/server/src/api-types.ts +++ b/packages/server/src/api-types.ts @@ -79,6 +79,37 @@ export type WorkspaceCreateResponse = WorkspaceDescriptor export type WorkspaceListResponse = WorkspaceDescriptor[] export type WorkspaceDetailResponse = WorkspaceDescriptor +export interface WorkspaceExportRequest { + destination: string + includeConfig?: boolean +} + +export interface WorkspaceExportResponse { + destination: string +} + +export interface WorkspaceImportRequest { + source: string + destination: string + includeConfig?: boolean +} + +export type WorkspaceImportResponse = WorkspaceDescriptor + +export interface WorkspaceMcpConfig { + mcpServers?: Record +} + +export interface WorkspaceMcpConfigResponse { + path: string + exists: boolean + config: WorkspaceMcpConfig +} + +export interface WorkspaceMcpConfigRequest { + config: WorkspaceMcpConfig +} + export interface WorkspaceDeleteResponse { id: string status: WorkspaceStatus @@ -159,6 +190,11 @@ export interface InstanceData { agentModelSelections: AgentModelSelection sessionTasks?: SessionTasks // Multi-task chat support: tasks per session sessionSkills?: Record // Selected skills per session + customAgents?: Array<{ + name: string + description?: string + prompt: string + }> } export type InstanceStreamStatus = "connecting" | "connected" | "error" | "disconnected" @@ -269,6 +305,10 @@ export interface ServerMeta { latestRelease?: LatestReleaseInfo } +export interface PortAvailabilityResponse { + port: number +} + export type { Preferences, ModelPreference, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8144faa..959f9a6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -16,6 +16,7 @@ import { ServerMeta } from "./api-types" import { InstanceStore } from "./storage/instance-store" import { InstanceEventBridge } from "./workspaces/instance-events" import { createLogger } from "./logger" +import { getUserConfigPath } from "./user-data" import { launchInBrowser } from "./launcher" import { startReleaseMonitor } from "./releases/release-monitor" @@ -41,7 +42,7 @@ interface CliOptions { const DEFAULT_PORT = 9898 const DEFAULT_HOST = "127.0.0.1" -const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json" +const DEFAULT_CONFIG_PATH = getUserConfigPath() function parseCliOptions(argv: string[]): CliOptions { const program = new Command() diff --git a/packages/server/src/integrations/ollama-cloud.ts b/packages/server/src/integrations/ollama-cloud.ts index 0c17a36..b53ad9c 100644 --- a/packages/server/src/integrations/ollama-cloud.ts +++ b/packages/server/src/integrations/ollama-cloud.ts @@ -1,11 +1,5 @@ -/** - * Ollama Cloud API Integration - * Provides access to Ollama's cloud models through API - */ - import { z } from "zod" -// Configuration schema for Ollama Cloud export const OllamaCloudConfigSchema = z.object({ apiKey: z.string().optional(), endpoint: z.string().default("https://ollama.com"), @@ -14,31 +8,56 @@ export const OllamaCloudConfigSchema = z.object({ export type OllamaCloudConfig = z.infer -// Model information schema +// Schema is flexible since Ollama Cloud may return different fields than local Ollama export const OllamaModelSchema = z.object({ name: z.string(), - size: z.string(), - digest: z.string(), - modified_at: z.string(), - created_at: z.string() + model: z.string().optional(), // Some APIs return model instead of name + size: z.union([z.string(), z.number()]).optional(), + digest: z.string().optional(), + modified_at: z.string().optional(), + created_at: z.string().optional(), + details: z.any().optional() // Model details like family, parameter_size, etc. }) export type OllamaModel = z.infer -// Chat message schema export const ChatMessageSchema = z.object({ role: z.enum(["user", "assistant", "system"]), content: z.string(), - images: z.array(z.string()).optional() + images: z.array(z.string()).optional(), + tool_calls: z.array(z.any()).optional(), + thinking: z.string().optional() }) export type ChatMessage = z.infer -// Chat request/response schemas +export const ToolCallSchema = z.object({ + name: z.string(), + arguments: z.record(z.any()) +}) + +export type ToolCall = z.infer + +export const ToolDefinitionSchema = z.object({ + name: z.string(), + description: z.string(), + parameters: z.object({ + type: z.enum(["object", "string", "number", "boolean", "array"]), + properties: z.record(z.any()), + required: z.array(z.string()).optional() + }) +}) + +export type ToolDefinition = z.infer + export const ChatRequestSchema = z.object({ model: z.string(), messages: z.array(ChatMessageSchema), stream: z.boolean().default(false), + think: z.union([z.boolean(), z.enum(["low", "medium", "high"])]).optional(), + format: z.union([z.literal("json"), z.any()]).optional(), + tools: z.array(ToolDefinitionSchema).optional(), + web_search: z.boolean().optional(), options: z.object({ temperature: z.number().min(0).max(2).optional(), top_p: z.number().min(0).max(1).optional() @@ -48,7 +67,10 @@ export const ChatRequestSchema = z.object({ export const ChatResponseSchema = z.object({ model: z.string(), created_at: z.string(), - message: ChatMessageSchema, + message: ChatMessageSchema.extend({ + thinking: z.string().optional(), + tool_calls: z.array(z.any()).optional() + }), done: z.boolean().optional(), total_duration: z.number().optional(), load_duration: z.number().optional(), @@ -61,23 +83,32 @@ export const ChatResponseSchema = z.object({ export type ChatRequest = z.infer export type ChatResponse = z.infer +export const EmbeddingRequestSchema = z.object({ + model: z.string(), + input: z.union([z.string(), z.array(z.string())]) +}) + +export type EmbeddingRequest = z.infer + +export const EmbeddingResponseSchema = z.object({ + model: z.string(), + embeddings: z.array(z.array(z.number())) +}) + +export type EmbeddingResponse = z.infer + export class OllamaCloudClient { private config: OllamaCloudConfig private baseUrl: string constructor(config: OllamaCloudConfig) { this.config = config - this.baseUrl = config.endpoint.replace(/\/$/, "") // Remove trailing slash + this.baseUrl = config.endpoint.replace(/\/$/, "") } - /** - * Test connection to Ollama Cloud API - */ async testConnection(): Promise { try { - const response = await this.makeRequest("/api/tags", { - method: "GET" - }) + const response = await this.makeRequest("/tags", { method: "GET" }) return response.ok } catch (error) { console.error("Ollama Cloud connection test failed:", error) @@ -85,30 +116,85 @@ export class OllamaCloudClient { } } - /** - * List available models - */ async listModels(): Promise { try { - const response = await this.makeRequest("/api/tags", { - method: "GET" + const headers: Record = {} + if (this.config.apiKey) { + headers["Authorization"] = `Bearer ${this.config.apiKey}` + } + + const cloudResponse = await fetch(`${this.baseUrl}/v1/models`, { + method: "GET", + headers }) - + + if (cloudResponse.ok) { + const data = await cloudResponse.json() + const modelsArray = Array.isArray(data?.data) ? data.data : [] + const parsedModels = modelsArray + .map((model: any) => ({ + name: model.id || model.name || model.model, + model: model.id || model.model || model.name, + })) + .filter((model: any) => model.name) + + if (parsedModels.length > 0) { + return parsedModels + } + } + + const response = await this.makeRequest("/tags", { method: "GET" }) + if (!response.ok) { - throw new Error(`Failed to fetch models: ${response.statusText}`) + const errorText = await response.text().catch(() => "Unknown error") + console.error(`[OllamaCloud] Failed to fetch models: ${response.status} ${response.statusText}`, errorText) + throw new Error(`Failed to fetch models: ${response.status} ${response.statusText} - ${errorText}`) } const data = await response.json() - return z.array(OllamaModelSchema).parse(data.models || []) + console.log("[OllamaCloud] Models response:", JSON.stringify(data).substring(0, 500)) + + // Handle different response formats flexibly + const modelsArray = Array.isArray(data.models) ? data.models : + Array.isArray(data) ? data : [] + + // Parse with flexible schema, don't throw on validation failure + // Only include cloud-compatible models (ending in -cloud or known cloud models) + const parsedModels: OllamaModel[] = [] + for (const model of modelsArray) { + try { + const modelName = model.name || model.model || "" + // Filter to only cloud-compatible models + const isCloudModel = modelName.endsWith("-cloud") || + modelName.includes(":cloud") || + modelName.startsWith("gpt-oss") || + modelName.startsWith("qwen3-coder") || + modelName.startsWith("deepseek-v3") + + if (modelName && isCloudModel) { + parsedModels.push({ + name: modelName, + model: model.model || modelName, + size: model.size, + digest: model.digest, + modified_at: model.modified_at, + created_at: model.created_at, + details: model.details + }) + } + } catch (parseError) { + console.warn("[OllamaCloud] Skipping model due to parse error:", model, parseError) + } + } + + console.log(`[OllamaCloud] Parsed ${parsedModels.length} cloud-compatible models`) + return parsedModels } catch (error) { console.error("Failed to list Ollama Cloud models:", error) throw error } } - /** - * Generate chat completion - */ async chat(request: ChatRequest): Promise> { if (!this.config.apiKey) { throw new Error("Ollama Cloud API key is required") @@ -118,20 +204,20 @@ export class OllamaCloudClient { "Content-Type": "application/json" } - // Add authorization header if API key is provided if (this.config.apiKey) { headers["Authorization"] = `Bearer ${this.config.apiKey}` } try { - const response = await fetch(`${this.baseUrl}/api/chat`, { + const response = await this.makeRequest("/chat", { method: "POST", headers, body: JSON.stringify(request) }) if (!response.ok) { - throw new Error(`Chat request failed: ${response.statusText}`) + const errorText = await response.text() + throw new Error(`Chat request failed: ${response.statusText} - ${errorText}`) } if (request.stream) { @@ -146,9 +232,85 @@ export class OllamaCloudClient { } } - /** - * Pull a model (for cloud models, this just makes them available) - */ + async chatWithThinking(request: ChatRequest): Promise> { + const requestWithThinking = { + ...request, + think: true + } + return this.chat(requestWithThinking) + } + + async chatWithStructuredOutput(request: ChatRequest, schema: any): Promise> { + const requestWithFormat = { + ...request, + format: schema + } + return this.chat(requestWithFormat) + } + + async chatWithVision(request: ChatRequest, images: string[]): Promise> { + if (!request.messages.length) { + throw new Error("At least one message is required") + } + + const messagesWithImages = [...request.messages] + const lastUserMessage = messagesWithImages.slice().reverse().find(m => m.role === "user") + + if (lastUserMessage) { + lastUserMessage.images = images + } + + return this.chat({ ...request, messages: messagesWithImages }) + } + + async chatWithTools(request: ChatRequest, tools: ToolDefinition[]): Promise> { + const requestWithTools = { + ...request, + tools + } + return this.chat(requestWithTools) + } + + async chatWithWebSearch(request: ChatRequest): Promise> { + const requestWithWebSearch = { + ...request, + web_search: true + } + return this.chat(requestWithWebSearch) + } + + async generateEmbeddings(request: EmbeddingRequest): Promise { + if (!this.config.apiKey) { + throw new Error("Ollama Cloud API key is required") + } + + const headers: Record = { + "Content-Type": "application/json" + } + + if (this.config.apiKey) { + headers["Authorization"] = `Bearer ${this.config.apiKey}` + } + + try { + const response = await this.makeRequest("/embed", { + method: "POST", + headers, + body: JSON.stringify(request) + }) + + if (!response.ok) { + throw new Error(`Embeddings request failed: ${response.statusText}`) + } + + const data = await response.json() + return EmbeddingResponseSchema.parse(data) + } catch (error) { + console.error("Ollama Cloud embeddings request failed:", error) + throw error + } + } + async pullModel(modelName: string): Promise { const headers: Record = { "Content-Type": "application/json" @@ -158,7 +320,7 @@ export class OllamaCloudClient { headers["Authorization"] = `Bearer ${this.config.apiKey}` } - const response = await fetch(`${this.baseUrl}/api/pull`, { + const response = await this.makeRequest("/pull", { method: "POST", headers, body: JSON.stringify({ name: modelName }) @@ -169,9 +331,6 @@ export class OllamaCloudClient { } } - /** - * Parse streaming response - */ private async *parseStreamingResponse(response: Response): AsyncIterable { if (!response.body) { throw new Error("Response body is missing") @@ -186,18 +345,17 @@ export class OllamaCloudClient { if (done) break const lines = decoder.decode(value, { stream: true }).split('\n').filter(line => line.trim()) - + for (const line of lines) { try { const data = JSON.parse(line) const chatResponse = ChatResponseSchema.parse(data) yield chatResponse - + if (chatResponse.done) { return } } catch (parseError) { - // Skip invalid JSON lines console.warn("Failed to parse streaming line:", line, parseError) } } @@ -207,61 +365,72 @@ export class OllamaCloudClient { } } - /** - * Create async iterable from array - */ private async *createAsyncIterable(items: T[]): AsyncIterable { for (const item of items) { yield item } } - /** - * Make authenticated request to API - */ private async makeRequest(endpoint: string, options: RequestInit): Promise { - const url = `${this.baseUrl}${endpoint}` - + // Ensure endpoint starts with /api + const apiEndpoint = endpoint.startsWith('/api') ? endpoint : `/api${endpoint}` + const url = `${this.baseUrl}${apiEndpoint}` + const headers: Record = { ...options.headers as Record } - // Add authorization header if API key is provided if (this.config.apiKey) { headers["Authorization"] = `Bearer ${this.config.apiKey}` } + console.log(`[OllamaCloud] Making request to: ${url}`) + return fetch(url, { ...options, headers }) } - /** - * Get cloud-specific models (models ending with -cloud) - */ async getCloudModels(): Promise { const allModels = await this.listModels() return allModels.filter(model => model.name.endsWith("-cloud")) } - /** - * Validate API key format - */ static validateApiKey(apiKey: string): boolean { return typeof apiKey === "string" && apiKey.length > 0 } - /** - * Get available cloud model names - */ async getCloudModelNames(): Promise { const cloudModels = await this.getCloudModels() return cloudModels.map(model => model.name) } + + async getThinkingCapableModels(): Promise { + const allModels = await this.listModels() + const thinkingModelPatterns = ["qwen3", "deepseek-r1", "gpt-oss", "deepseek-v3.1"] + return allModels + .map(m => m.name) + .filter(name => thinkingModelPatterns.some(pattern => name.toLowerCase().includes(pattern))) + } + + async getVisionCapableModels(): Promise { + const allModels = await this.listModels() + const visionModelPatterns = ["gemma3", "llama3.2-vision", "llava", "bakllava", "minicpm-v"] + return allModels + .map(m => m.name) + .filter(name => visionModelPatterns.some(pattern => name.toLowerCase().includes(pattern))) + } + + async getEmbeddingModels(): Promise { + const allModels = await this.listModels() + const embeddingModelPatterns = ["embeddinggemma", "qwen3-embedding", "all-minilm", "nomic-embed", "mxbai-embed"] + return allModels + .map(m => m.name) + .filter(name => embeddingModelPatterns.some(pattern => name.toLowerCase().includes(pattern))) + } } -// Default cloud models based on Ollama documentation export const DEFAULT_CLOUD_MODELS = [ "gpt-oss:120b-cloud", "llama3.1:70b-cloud", @@ -270,4 +439,32 @@ export const DEFAULT_CLOUD_MODELS = [ "qwen2.5:7b-cloud" ] as const -export type CloudModelName = typeof DEFAULT_CLOUD_MODELS[number] \ No newline at end of file +export type CloudModelName = typeof DEFAULT_CLOUD_MODELS[number] + +export const THINKING_MODELS = [ + "qwen3", + "deepseek-r1", + "deepseek-v3.1", + "gpt-oss:120b-cloud" +] as const + +export type ThinkingModelName = typeof THINKING_MODELS[number] + +export const VISION_MODELS = [ + "gemma3", + "llava", + "bakllava", + "minicpm-v" +] as const + +export type VisionModelName = typeof VISION_MODELS[number] + +export const EMBEDDING_MODELS = [ + "embeddinggemma", + "qwen3-embedding", + "all-minilm", + "nomic-embed-text", + "mxbai-embed-large" +] as const + +export type EmbeddingModelName = typeof EMBEDDING_MODELS[number] diff --git a/packages/server/src/integrations/opencode-zen.ts b/packages/server/src/integrations/opencode-zen.ts index 0ba9ad5..09a9c1c 100644 --- a/packages/server/src/integrations/opencode-zen.ts +++ b/packages/server/src/integrations/opencode-zen.ts @@ -11,8 +11,8 @@ import { z } from "zod" // Configuration schema for OpenCode Zen export const OpenCodeZenConfigSchema = z.object({ enabled: z.boolean().default(true), // Free models enabled by default - endpoint: z.string().default("https://api.opencode.ai/v1"), - apiKey: z.string().default("public") // "public" key for free models + endpoint: z.string().default("https://opencode.ai/zen/v1"), + apiKey: z.string().optional() }) export type OpenCodeZenConfig = z.infer @@ -104,10 +104,10 @@ export const FREE_ZEN_MODELS: ZenModel[] = [ attachment: false, temperature: true, cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 } + limit: { context: 200000, output: 128000 } }, { - id: "grok-code-fast-1", + id: "grok-code", name: "Grok Code Fast 1", family: "grok", reasoning: true, @@ -115,18 +115,29 @@ export const FREE_ZEN_MODELS: ZenModel[] = [ attachment: false, temperature: true, cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 10000 } + limit: { context: 256000, output: 256000 } }, { - id: "minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", + id: "glm-4.7-free", + name: "GLM-4.7", + family: "glm-free", reasoning: true, tool_call: true, attachment: false, temperature: true, cost: { input: 0, output: 0 }, - limit: { context: 205000, output: 131072 } + limit: { context: 204800, output: 131072 } + }, + { + id: "alpha-doubao-seed-code", + name: "Doubao Seed Code (alpha)", + family: "doubao", + reasoning: true, + tool_call: true, + attachment: false, + temperature: true, + cost: { input: 0, output: 0 }, + limit: { context: 256000, output: 32000 } } ] @@ -217,13 +228,19 @@ export class OpenCodeZenClient { * Chat completion (streaming) */ async *chatStream(request: ChatRequest): AsyncGenerator { + const headers: Record = { + "Content-Type": "application/json", + "User-Agent": "NomadArch/1.0", + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "NomadArch" + } + if (this.config.apiKey) { + headers["Authorization"] = `Bearer ${this.config.apiKey}` + } + const response = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${this.config.apiKey}`, - "User-Agent": "NomadArch/1.0" - }, + headers, body: JSON.stringify({ ...request, stream: true @@ -281,13 +298,19 @@ export class OpenCodeZenClient { * Chat completion (non-streaming) */ async chat(request: ChatRequest): Promise { + const headers: Record = { + "Content-Type": "application/json", + "User-Agent": "NomadArch/1.0", + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "NomadArch" + } + if (this.config.apiKey) { + headers["Authorization"] = `Bearer ${this.config.apiKey}` + } + const response = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${this.config.apiKey}`, - "User-Agent": "NomadArch/1.0" - }, + headers, body: JSON.stringify({ ...request, stream: false @@ -306,7 +329,6 @@ export class OpenCodeZenClient { export function getDefaultZenConfig(): OpenCodeZenConfig { return { enabled: true, - endpoint: "https://api.opencode.ai/v1", - apiKey: "public" + endpoint: "https://opencode.ai/zen/v1" } } diff --git a/packages/server/src/integrations/zai-api.ts b/packages/server/src/integrations/zai-api.ts index 037f947..fba9a6d 100644 --- a/packages/server/src/integrations/zai-api.ts +++ b/packages/server/src/integrations/zai-api.ts @@ -1,113 +1,111 @@ -/** - * Z.AI API Integration - * Provides access to Z.AI's GLM Coding Plan API (Anthropic-compatible) - * Based on https://docs.z.ai/devpack/tool/claude#step-2-config-glm-coding-plan - */ - import { z } from "zod" -// Configuration schema for Z.AI export const ZAIConfigSchema = z.object({ apiKey: z.string().optional(), - endpoint: z.string().default("https://api.z.ai/api/anthropic"), + endpoint: z.string().default("https://api.z.ai/api/paas/v4"), enabled: z.boolean().default(false), - timeout: z.number().default(3000000) // 50 minutes as per docs + timeout: z.number().default(300000) }) export type ZAIConfig = z.infer -// Message schema (Anthropic-compatible) export const ZAIMessageSchema = z.object({ - role: z.enum(["user", "assistant"]), + role: z.enum(["user", "assistant", "system"]), content: z.string() }) export type ZAIMessage = z.infer -// Chat request schema export const ZAIChatRequestSchema = z.object({ - model: z.string().default("claude-sonnet-4-20250514"), + model: z.string().default("glm-4.7"), messages: z.array(ZAIMessageSchema), max_tokens: z.number().default(8192), stream: z.boolean().default(true), - system: z.string().optional() + temperature: z.number().optional(), + thinking: z.object({ + type: z.enum(["enabled", "disabled"]).optional() + }).optional() }) export type ZAIChatRequest = z.infer -// Chat response schema export const ZAIChatResponseSchema = z.object({ id: z.string(), - type: z.string(), - role: z.string(), - content: z.array(z.object({ - type: z.string(), - text: z.string().optional() - })), + object: z.string(), + created: z.number(), model: z.string(), - stop_reason: z.string().nullable().optional(), - stop_sequence: z.string().nullable().optional(), + choices: z.array(z.object({ + index: z.number(), + message: z.object({ + role: z.string(), + content: z.string().optional(), + reasoning_content: z.string().optional() + }), + finish_reason: z.string() + })), usage: z.object({ - input_tokens: z.number(), - output_tokens: z.number() - }).optional() + prompt_tokens: z.number(), + completion_tokens: z.number(), + total_tokens: z.number() + }) }) export type ZAIChatResponse = z.infer -// Stream chunk schema export const ZAIStreamChunkSchema = z.object({ - type: z.string(), - index: z.number().optional(), - delta: z.object({ - type: z.string().optional(), - text: z.string().optional() - }).optional(), - message: z.object({ - id: z.string(), - type: z.string(), - role: z.string(), - content: z.array(z.any()), - model: z.string() - }).optional(), - content_block: z.object({ - type: z.string(), - text: z.string() - }).optional() + id: z.string(), + object: z.string(), + created: z.number(), + model: z.string(), + choices: z.array(z.object({ + index: z.number(), + delta: z.object({ + role: z.string().optional(), + content: z.string().optional(), + reasoning_content: z.string().optional() + }), + finish_reason: z.string().nullable().optional() + })) }) export type ZAIStreamChunk = z.infer +export const ZAI_MODELS = [ + "glm-4.7", + "glm-4.6", + "glm-4.5", + "glm-4.5-air", + "glm-4.5-flash", + "glm-4.5-long" +] as const + +export type ZAIModelName = typeof ZAI_MODELS[number] + export class ZAIClient { private config: ZAIConfig private baseUrl: string constructor(config: ZAIConfig) { this.config = config - this.baseUrl = config.endpoint.replace(/\/$/, "") // Remove trailing slash + this.baseUrl = config.endpoint.replace(/\/$/, "") } - /** - * Test connection to Z.AI API - */ async testConnection(): Promise { if (!this.config.apiKey) { return false } try { - // Make a minimal request to test auth - const response = await fetch(`${this.baseUrl}/v1/messages`, { + const response = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify({ - model: "claude-sonnet-4-20250514", + model: "glm-4.7", max_tokens: 1, messages: [{ role: "user", content: "test" }] }) }) - // Any response other than auth error means connection works return response.status !== 401 && response.status !== 403 } catch (error) { console.error("Z.AI connection test failed:", error) @@ -115,28 +113,16 @@ export class ZAIClient { } } - /** - * List available models - */ async listModels(): Promise { - // Z.AI provides access to Claude models through their proxy - return [ - "claude-sonnet-4-20250514", - "claude-3-5-sonnet-20241022", - "claude-3-opus-20240229", - "claude-3-haiku-20240307" - ] + return [...ZAI_MODELS] } - /** - * Chat completion (streaming) - */ async *chatStream(request: ZAIChatRequest): AsyncGenerator { if (!this.config.apiKey) { throw new Error("Z.AI API key is required") } - const response = await fetch(`${this.baseUrl}/v1/messages`, { + const response = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify({ @@ -165,7 +151,7 @@ export class ZAIClient { buffer += decoder.decode(value, { stream: true }) const lines = buffer.split("\n") - buffer = lines.pop() || "" // Keep incomplete line in buffer + buffer = lines.pop() || "" for (const line of lines) { if (line.startsWith("data: ")) { @@ -176,7 +162,6 @@ export class ZAIClient { const parsed = JSON.parse(data) yield parsed as ZAIStreamChunk } catch (e) { - // Skip invalid JSON } } } @@ -186,15 +171,12 @@ export class ZAIClient { } } - /** - * Chat completion (non-streaming) - */ async chat(request: ZAIChatRequest): Promise { if (!this.config.apiKey) { throw new Error("Z.AI API key is required") } - const response = await fetch(`${this.baseUrl}/v1/messages`, { + const response = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify({ @@ -211,31 +193,14 @@ export class ZAIClient { return await response.json() } - /** - * Get request headers - */ private getHeaders(): Record { return { "Content-Type": "application/json", - "x-api-key": this.config.apiKey || "", - "anthropic-version": "2023-06-01" + "Authorization": `Bearer ${this.config.apiKey}` } } - /** - * Validate API key - */ static validateApiKey(apiKey: string): boolean { return typeof apiKey === "string" && apiKey.length > 0 } -} - -// Default available models -export const ZAI_MODELS = [ - "claude-sonnet-4-20250514", - "claude-3-5-sonnet-20241022", - "claude-3-opus-20240229", - "claude-3-haiku-20240307" -] as const - -export type ZAIModelName = typeof ZAI_MODELS[number] +} \ No newline at end of file diff --git a/packages/server/src/opencode-config.ts b/packages/server/src/opencode-config.ts index 8b90651..db0b145 100644 --- a/packages/server/src/opencode-config.ts +++ b/packages/server/src/opencode-config.ts @@ -3,6 +3,7 @@ import os from "os" import path from "path" import { fileURLToPath } from "url" import { createLogger } from "./logger" +import { getOpencodeWorkspacesRoot, getUserDataRoot } from "./user-data" const log = createLogger({ component: "opencode-config" }) const __filename = fileURLToPath(import.meta.url) @@ -12,7 +13,8 @@ const prodTemplateDir = path.resolve(__dirname, "opencode-config") const isDevBuild = Boolean(process.env.CODENOMAD_DEV ?? process.env.CLI_UI_DEV_SERVER) || existsSync(devTemplateDir) const templateDir = isDevBuild ? devTemplateDir : prodTemplateDir -const userConfigDir = path.join(os.homedir(), ".config", "codenomad", "opencode-config") +const userConfigDir = path.join(getUserDataRoot(), "opencode-config") +const workspaceConfigRoot = getOpencodeWorkspacesRoot() export function getOpencodeConfigDir(): string { if (!existsSync(templateDir)) { @@ -28,6 +30,28 @@ export function getOpencodeConfigDir(): string { return userConfigDir } +export function ensureWorkspaceOpencodeConfig(workspaceId: string): string { + if (!workspaceId) { + return getOpencodeConfigDir() + } + if (!existsSync(templateDir)) { + throw new Error(`CodeNomad Opencode config template missing at ${templateDir}`) + } + + const targetDir = path.join(workspaceConfigRoot, workspaceId) + if (existsSync(targetDir)) { + return targetDir + } + + mkdirSync(path.dirname(targetDir), { recursive: true }) + cpSync(templateDir, targetDir, { recursive: true }) + return targetDir +} + +export function getWorkspaceOpencodeConfigDir(workspaceId: string): string { + return path.join(workspaceConfigRoot, workspaceId) +} + function refreshUserConfig() { log.debug({ templateDir, userConfigDir }, "Syncing Opencode config template") rmSync(userConfigDir, { recursive: true, force: true }) diff --git a/packages/server/src/server/http-server.ts b/packages/server/src/server/http-server.ts index 8a5da48..948fd12 100644 --- a/packages/server/src/server/http-server.ts +++ b/packages/server/src/server/http-server.ts @@ -105,7 +105,11 @@ export function createHttpServer(deps: HttpServerDeps) { }, }) - registerWorkspaceRoutes(app, { workspaceManager: deps.workspaceManager }) + registerWorkspaceRoutes(app, { + workspaceManager: deps.workspaceManager, + instanceStore: deps.instanceStore, + configStore: deps.configStore, + }) registerConfigRoutes(app, { configStore: deps.configStore, binaryRegistry: deps.binaryRegistry }) registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser }) registerMetaRoutes(app, { serverMeta: deps.serverMeta }) @@ -119,7 +123,7 @@ export function createHttpServer(deps: HttpServerDeps) { registerQwenRoutes(app, { logger: deps.logger }) registerZAIRoutes(app, { logger: deps.logger }) registerOpenCodeZenRoutes(app, { logger: deps.logger }) - await registerSkillsRoutes(app) + registerSkillsRoutes(app) registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger }) diff --git a/packages/server/src/server/routes/meta.ts b/packages/server/src/server/routes/meta.ts index d716198..eb639eb 100644 --- a/packages/server/src/server/routes/meta.ts +++ b/packages/server/src/server/routes/meta.ts @@ -1,6 +1,7 @@ import { FastifyInstance } from "fastify" import os from "os" -import { NetworkAddress, ServerMeta } from "../../api-types" +import { NetworkAddress, ServerMeta, PortAvailabilityResponse } from "../../api-types" +import { getAvailablePort } from "../../utils/port" interface RouteDeps { serverMeta: ServerMeta @@ -8,6 +9,11 @@ interface RouteDeps { export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) { app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta)) + app.get("/api/ports/available", async () => { + const port = await getAvailablePort(3000) + const response: PortAvailabilityResponse = { port } + return response + }) } function buildMetaResponse(meta: ServerMeta): ServerMeta { diff --git a/packages/server/src/server/routes/ollama.ts b/packages/server/src/server/routes/ollama.ts index f5269bd..258cd4a 100644 --- a/packages/server/src/server/routes/ollama.ts +++ b/packages/server/src/server/routes/ollama.ts @@ -1,6 +1,18 @@ import { FastifyInstance, FastifyReply } from "fastify" -import { OllamaCloudClient, type OllamaCloudConfig, type ChatRequest } from "../../integrations/ollama-cloud" +import { + OllamaCloudClient, + type OllamaCloudConfig, + type ChatRequest, + type EmbeddingRequest, + type ToolDefinition +} from "../../integrations/ollama-cloud" import { Logger } from "../../logger" +import fs from "fs" +import path from "path" +import { getUserIntegrationsDir } from "../../user-data" + +const CONFIG_DIR = getUserIntegrationsDir() +const CONFIG_FILE = path.join(CONFIG_DIR, "ollama-config.json") interface OllamaRouteDeps { logger: Logger @@ -12,7 +24,6 @@ export async function registerOllamaRoutes( ) { const logger = deps.logger.child({ component: "ollama-routes" }) - // Get Ollama Cloud configuration app.get('/api/ollama/config', async (request, reply) => { try { const config = getOllamaConfig() @@ -23,15 +34,16 @@ export async function registerOllamaRoutes( } }) - // Update Ollama Cloud configuration app.post('/api/ollama/config', { schema: { - type: 'object', - required: ['enabled'], - properties: { - enabled: { type: 'boolean' }, - apiKey: { type: 'string' }, - endpoint: { type: 'string' } + body: { + type: 'object', + required: ['enabled'], + properties: { + enabled: { type: 'boolean' }, + apiKey: { type: 'string' }, + endpoint: { type: 'string' } + } } } }, async (request, reply) => { @@ -46,7 +58,6 @@ export async function registerOllamaRoutes( } }) - // Test Ollama Cloud connection app.post('/api/ollama/test', async (request, reply) => { try { const config = getOllamaConfig() @@ -56,7 +67,7 @@ export async function registerOllamaRoutes( const client = new OllamaCloudClient(config) const isConnected = await client.testConnection() - + return { connected: isConnected } } catch (error) { logger.error({ error }, "Ollama Cloud connection test failed") @@ -64,7 +75,6 @@ export async function registerOllamaRoutes( } }) - // List available models app.get('/api/ollama/models', async (request, reply) => { try { const config = getOllamaConfig() @@ -72,17 +82,19 @@ export async function registerOllamaRoutes( return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) } + logger.info({ endpoint: config.endpoint, hasApiKey: !!config.apiKey }, "Fetching Ollama models") + const client = new OllamaCloudClient(config) const models = await client.listModels() - + + logger.info({ modelCount: models.length }, "Ollama models fetched successfully") return { models } - } catch (error) { - logger.error({ error }, "Failed to list Ollama models") - return reply.status(500).send({ error: "Failed to list models" }) + } catch (error: any) { + logger.error({ error: error?.message || error }, "Failed to list Ollama models") + return reply.status(500).send({ error: error?.message || "Failed to list models" }) } }) - // Get cloud models only app.get('/api/ollama/models/cloud', async (request, reply) => { try { const config = getOllamaConfig() @@ -92,7 +104,7 @@ export async function registerOllamaRoutes( const client = new OllamaCloudClient(config) const cloudModels = await client.getCloudModels() - + return { models: cloudModels } } catch (error) { logger.error({ error }, "Failed to list cloud models") @@ -100,30 +112,86 @@ export async function registerOllamaRoutes( } }) - // Chat completion endpoint + app.get('/api/ollama/models/thinking', async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const thinkingModels = await client.getThinkingCapableModels() + + return { models: thinkingModels } + } catch (error) { + logger.error({ error }, "Failed to list thinking models") + return reply.status(500).send({ error: "Failed to list thinking models" }) + } + }) + + app.get('/api/ollama/models/vision', async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const visionModels = await client.getVisionCapableModels() + + return { models: visionModels } + } catch (error) { + logger.error({ error }, "Failed to list vision models") + return reply.status(500).send({ error: "Failed to list vision models" }) + } + }) + + app.get('/api/ollama/models/embedding', async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const embeddingModels = await client.getEmbeddingModels() + + return { models: embeddingModels } + } catch (error) { + logger.error({ error }, "Failed to list embedding models") + return reply.status(500).send({ error: "Failed to list embedding models" }) + } + }) + app.post('/api/ollama/chat', { schema: { - type: 'object', - required: ['model', 'messages'], - properties: { - model: { type: 'string' }, - messages: { - type: 'array', - items: { - type: 'object', - required: ['role', 'content'], - properties: { - role: { type: 'string', enum: ['user', 'assistant', 'system'] }, - content: { type: 'string' } + body: { + type: 'object', + required: ['model', 'messages'], + properties: { + model: { type: 'string' }, + messages: { + type: 'array', + items: { + type: 'object', + required: ['role', 'content'], + properties: { + role: { type: 'string', enum: ['user', 'assistant', 'system'] }, + content: { type: 'string' } + } + } + }, + stream: { type: 'boolean' }, + think: { type: ['boolean', 'string'] }, + format: { type: ['string', 'object'] }, + tools: { type: 'array' }, + web_search: { type: 'boolean' }, + options: { + type: 'object', + properties: { + temperature: { type: 'number', minimum: 0, maximum: 2 }, + top_p: { type: 'number', minimum: 0, maximum: 1 } } - } - }, - stream: { type: 'boolean' }, - options: { - type: 'object', - properties: { - temperature: { type: 'number', minimum: 0, maximum: 2 }, - top_p: { type: 'number', minimum: 0, maximum: 1 } } } } @@ -137,8 +205,7 @@ export async function registerOllamaRoutes( const client = new OllamaCloudClient(config) const chatRequest = request.body as ChatRequest - - // Set appropriate headers for streaming + if (chatRequest.stream) { reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream', @@ -148,24 +215,31 @@ export async function registerOllamaRoutes( try { const stream = await client.chat(chatRequest) - + for await (const chunk of stream) { reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`) - + if (chunk.done) { reply.raw.write('data: [DONE]\n\n') break } } - + reply.raw.end() - } catch (streamError) { - logger.error({ error: streamError }, "Streaming failed") + } catch (streamError: any) { + logger.error({ error: streamError?.message || streamError }, "Ollama streaming failed") + // Send error event to client so it knows the request failed + reply.raw.write(`data: ${JSON.stringify({ error: streamError?.message || "Streaming failed" })}\n\n`) + reply.raw.write('data: [DONE]\n\n') reply.raw.end() } } else { - const response = await client.chat(chatRequest) - return response + const stream = await client.chat(chatRequest) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + return chunks[chunks.length - 1] } } catch (error) { logger.error({ error }, "Ollama chat request failed") @@ -173,13 +247,289 @@ export async function registerOllamaRoutes( } }) - // Pull model endpoint + app.post('/api/ollama/chat/thinking', { + schema: { + body: { + type: 'object', + required: ['model', 'messages'], + properties: { + model: { type: 'string' }, + messages: { type: 'array' }, + stream: { type: 'boolean' }, + think: { type: ['boolean', 'string'] } + } + } + } + }, async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const chatRequest = request.body as ChatRequest + chatRequest.think = chatRequest.think ?? true + + if (chatRequest.stream) { + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }) + + try { + const stream = await client.chatWithThinking(chatRequest) + + for await (const chunk of stream) { + reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`) + + if (chunk.done) { + reply.raw.write('data: [DONE]\n\n') + break + } + } + + reply.raw.end() + } catch (streamError) { + logger.error({ error: streamError }, "Thinking streaming failed") + reply.raw.end() + } + } else { + const stream = await client.chatWithThinking(chatRequest) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + return chunks[chunks.length - 1] + } + } catch (error) { + logger.error({ error }, "Ollama thinking chat request failed") + return reply.status(500).send({ error: "Thinking chat request failed" }) + } + }) + + app.post('/api/ollama/chat/vision', { + schema: { + body: { + type: 'object', + required: ['model', 'messages', 'images'], + properties: { + model: { type: 'string' }, + messages: { type: 'array' }, + images: { type: 'array', items: { type: 'string' } }, + stream: { type: 'boolean' } + } + } + } + }, async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const { model, messages, images, stream } = request.body as any + const chatRequest: ChatRequest = { model, messages, stream: stream ?? false } + + if (chatRequest.stream) { + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }) + + try { + const streamResult = await client.chatWithVision(chatRequest, images) + + for await (const chunk of streamResult) { + reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`) + + if (chunk.done) { + reply.raw.write('data: [DONE]\n\n') + break + } + } + + reply.raw.end() + } catch (streamError) { + logger.error({ error: streamError }, "Vision streaming failed") + reply.raw.end() + } + } else { + const streamResult = await client.chatWithVision(chatRequest, images) + const chunks: any[] = [] + for await (const chunk of streamResult) { + chunks.push(chunk) + } + return chunks[chunks.length - 1] + } + } catch (error) { + logger.error({ error }, "Ollama vision chat request failed") + return reply.status(500).send({ error: "Vision chat request failed" }) + } + }) + + app.post('/api/ollama/chat/tools', { + schema: { + body: { + type: 'object', + required: ['model', 'messages', 'tools'], + properties: { + model: { type: 'string' }, + messages: { type: 'array' }, + tools: { type: 'array' }, + stream: { type: 'boolean' } + } + } + } + }, async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const { model, messages, tools, stream } = request.body as any + const chatRequest: ChatRequest = { model, messages, stream: stream ?? false } + + if (chatRequest.stream) { + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }) + + try { + const streamResult = await client.chatWithTools(chatRequest, tools) + + for await (const chunk of streamResult) { + reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`) + + if (chunk.done) { + reply.raw.write('data: [DONE]\n\n') + break + } + } + + reply.raw.end() + } catch (streamError) { + logger.error({ error: streamError }, "Tools streaming failed") + reply.raw.end() + } + } else { + const streamResult = await client.chatWithTools(chatRequest, tools) + const chunks: any[] = [] + for await (const chunk of streamResult) { + chunks.push(chunk) + } + return chunks[chunks.length - 1] + } + } catch (error) { + logger.error({ error }, "Ollama tools chat request failed") + return reply.status(500).send({ error: "Tools chat request failed" }) + } + }) + + app.post('/api/ollama/chat/websearch', { + schema: { + body: { + type: 'object', + required: ['model', 'messages'], + properties: { + model: { type: 'string' }, + messages: { type: 'array' }, + stream: { type: 'boolean' } + } + } + } + }, async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const chatRequest = request.body as ChatRequest + + if (chatRequest.stream) { + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }) + + try { + const stream = await client.chatWithWebSearch(chatRequest) + + for await (const chunk of stream) { + reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`) + + if (chunk.done) { + reply.raw.write('data: [DONE]\n\n') + break + } + } + + reply.raw.end() + } catch (streamError) { + logger.error({ error: streamError }, "Web search streaming failed") + reply.raw.end() + } + } else { + const stream = await client.chatWithWebSearch(chatRequest) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + return chunks[chunks.length - 1] + } + } catch (error) { + logger.error({ error }, "Ollama web search chat request failed") + return reply.status(500).send({ error: "Web search chat request failed" }) + } + }) + + app.post('/api/ollama/embeddings', { + schema: { + body: { + type: 'object', + required: ['model', 'input'], + properties: { + model: { type: 'string' }, + input: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } + } + } + } + }, async (request, reply) => { + try { + const config = getOllamaConfig() + if (!config.enabled) { + return reply.status(400).send({ error: "Ollama Cloud is not enabled" }) + } + + const client = new OllamaCloudClient(config) + const embedRequest = request.body as EmbeddingRequest + + const result = await client.generateEmbeddings(embedRequest) + return result + } catch (error) { + logger.error({ error }, "Ollama embeddings request failed") + return reply.status(500).send({ error: "Embeddings request failed" }) + } + }) + app.post('/api/ollama/pull', { schema: { - type: 'object', - required: ['model'], - properties: { - model: { type: 'string' } + body: { + type: 'object', + required: ['model'], + properties: { + model: { type: 'string' } + } } } }, async (request, reply) => { @@ -191,12 +541,11 @@ export async function registerOllamaRoutes( const client = new OllamaCloudClient(config) const { model } = request.body as any - - // Start async pull operation + client.pullModel(model).catch(error => { logger.error({ error, model }, "Failed to pull model") }) - + return { message: `Started pulling model: ${model}` } } catch (error) { logger.error({ error }, "Failed to initiate model pull") @@ -207,18 +556,36 @@ export async function registerOllamaRoutes( logger.info("Ollama Cloud routes registered") } -// Configuration management functions function getOllamaConfig(): OllamaCloudConfig { try { - const stored = localStorage.getItem('ollama_cloud_config') - return stored ? JSON.parse(stored) : { enabled: false, endpoint: "https://ollama.com" } + if (!fs.existsSync(CONFIG_FILE)) { + return { enabled: false, endpoint: "https://ollama.com" } + } + const data = fs.readFileSync(CONFIG_FILE, 'utf-8') + return JSON.parse(data) } catch { return { enabled: false, endpoint: "https://ollama.com" } } } function updateOllamaConfig(config: Partial): void { - const current = getOllamaConfig() - const updated = { ...current, ...config } - localStorage.setItem('ollama_cloud_config', JSON.stringify(updated)) -} \ No newline at end of file + try { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }) + } + const current = getOllamaConfig() + + // Only update apiKey if a new non-empty value is provided + const updated = { + ...current, + ...config, + // Preserve existing apiKey if new one is undefined/empty + apiKey: config.apiKey || current.apiKey + } + + fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2)) + console.log(`[Ollama] Config saved: enabled=${updated.enabled}, endpoint=${updated.endpoint}, hasApiKey=${!!updated.apiKey}`) + } catch (error) { + console.error("Failed to save Ollama config:", error) + } +} diff --git a/packages/server/src/server/routes/qwen.ts b/packages/server/src/server/routes/qwen.ts index 60ed957..41f229e 100644 --- a/packages/server/src/server/routes/qwen.ts +++ b/packages/server/src/server/routes/qwen.ts @@ -5,97 +5,168 @@ interface QwenRouteDeps { logger: Logger } +const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai' +const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code` +const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token` +const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56' +const QWEN_OAUTH_SCOPE = 'openid profile email model.completion' +const QWEN_OAUTH_DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code' +const QWEN_DEFAULT_RESOURCE_URL = 'https://dashscope.aliyuncs.com/compatible-mode' + +function normalizeQwenModel(model?: string): string { + const raw = (model || "").trim() + if (!raw) return "coder-model" + const lower = raw.toLowerCase() + if (lower === "vision-model" || lower.includes("vision")) return "vision-model" + if (lower === "coder-model") return "coder-model" + if (lower.includes("coder")) return "coder-model" + return "coder-model" +} + +function normalizeQwenResourceUrl(resourceUrl?: string): string { + const raw = typeof resourceUrl === 'string' && resourceUrl.trim().length > 0 + ? resourceUrl.trim() + : QWEN_DEFAULT_RESOURCE_URL + const withProtocol = raw.startsWith('http') ? raw : `https://${raw}` + const trimmed = withProtocol.replace(/\/$/, '') + return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1` +} + export async function registerQwenRoutes( app: FastifyInstance, deps: QwenRouteDeps ) { const logger = deps.logger.child({ component: "qwen-routes" }) - // Get OAuth URL for Qwen authentication - app.get('/api/qwen/oauth/url', async (request, reply) => { - try { - const { clientId, redirectUri } = request.query as any - - if (!clientId) { - return reply.status(400).send({ error: "Client ID is required" }) - } - - const authUrl = new URL('https://qwen.ai/oauth/authorize') - authUrl.searchParams.set('response_type', 'code') - authUrl.searchParams.set('client_id', clientId) - authUrl.searchParams.set('redirect_uri', redirectUri || `${request.protocol}//${request.host}/auth/qwen/callback`) - authUrl.searchParams.set('scope', 'read write') - authUrl.searchParams.set('state', generateState()) - - return { authUrl: authUrl.toString() } - } catch (error) { - logger.error({ error }, "Failed to generate OAuth URL") - return reply.status(500).send({ error: "Failed to generate OAuth URL" }) - } - }) - - // Exchange authorization code for token - app.post('/api/qwen/oauth/exchange', { + // Qwen OAuth Device Flow: request device authorization + app.post('/api/qwen/oauth/device', { schema: { - type: 'object', - required: ['code', 'state'], - properties: { - code: { type: 'string' }, - state: { type: 'string' }, - client_id: { type: 'string' }, - redirect_uri: { type: 'string' } + body: { + type: 'object', + required: ['code_challenge', 'code_challenge_method'], + properties: { + code_challenge: { type: 'string' }, + code_challenge_method: { type: 'string' } + } } } }, async (request, reply) => { try { - const { code, state, client_id, redirect_uri } = request.body as any - - // Exchange code for token with Qwen - const tokenResponse = await fetch('https://qwen.ai/oauth/token', { + const { code_challenge, code_challenge_method } = request.body as any + const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' }, body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: client_id, - code, - redirect_uri: redirect_uri + client_id: QWEN_OAUTH_CLIENT_ID, + scope: QWEN_OAUTH_SCOPE, + code_challenge, + code_challenge_method }) }) - if (!tokenResponse.ok) { - throw new Error(`Token exchange failed: ${tokenResponse.statusText}`) + if (!response.ok) { + const errorText = await response.text() + logger.error({ status: response.status, errorText }, "Qwen device authorization failed") + return reply.status(response.status).send({ error: "Device authorization failed", details: errorText }) } - const tokenData = await tokenResponse.json() - - // Get user info - const userResponse = await fetch('https://qwen.ai/api/user', { - headers: { - 'Authorization': `Bearer ${tokenData.access_token}` + const data = await response.json() + return { ...data } + } catch (error) { + logger.error({ error }, "Failed to request Qwen device authorization") + return reply.status(500).send({ error: "Device authorization failed" }) + } + }) + + // Qwen OAuth Device Flow: poll token endpoint + app.post('/api/qwen/oauth/token', { + schema: { + body: { + type: 'object', + required: ['device_code', 'code_verifier'], + properties: { + device_code: { type: 'string' }, + code_verifier: { type: 'string' } } + } + } + }, async (request, reply) => { + try { + const { device_code, code_verifier } = request.body as any + const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + body: new URLSearchParams({ + grant_type: QWEN_OAUTH_DEVICE_GRANT_TYPE, + client_id: QWEN_OAUTH_CLIENT_ID, + device_code, + code_verifier + }) }) - if (!userResponse.ok) { - throw new Error(`Failed to fetch user info: ${userResponse.statusText}`) + const responseText = await response.text() + if (!response.ok) { + logger.error({ status: response.status, responseText }, "Qwen device token poll failed") + return reply.status(response.status).send(responseText) } - - const userData = await userResponse.json() - - return { - success: true, - user: userData, - token: { - access_token: tokenData.access_token, - token_type: tokenData.token_type, - expires_in: tokenData.expires_in, - scope: tokenData.scope - } + try { + return reply.send(JSON.parse(responseText)) + } catch { + return reply.send(responseText) } } catch (error) { - logger.error({ error }, "Qwen OAuth token exchange failed") - return reply.status(500).send({ error: "OAuth exchange failed" }) + logger.error({ error }, "Failed to poll Qwen token endpoint") + return reply.status(500).send({ error: "Token polling failed" }) + } + }) + + // Qwen OAuth refresh token + app.post('/api/qwen/oauth/refresh', { + schema: { + body: { + type: 'object', + required: ['refresh_token'], + properties: { + refresh_token: { type: 'string' } + } + } + } + }, async (request, reply) => { + try { + const { refresh_token } = request.body as any + const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token, + client_id: QWEN_OAUTH_CLIENT_ID + }) + }) + + const responseText = await response.text() + if (!response.ok) { + logger.error({ status: response.status, responseText }, "Qwen token refresh failed") + return reply.status(response.status).send(responseText) + } + + try { + return reply.send(JSON.parse(responseText)) + } catch { + return reply.send(responseText) + } + } catch (error) { + logger.error({ error }, "Failed to refresh Qwen token") + return reply.status(500).send({ error: "Token refresh failed" }) } }) @@ -108,7 +179,7 @@ export async function registerQwenRoutes( } const token = authHeader.substring(7) - const userResponse = await fetch('https://qwen.ai/api/user', { + const userResponse = await fetch('https://chat.qwen.ai/api/v1/user', { headers: { 'Authorization': `Bearer ${token}` } @@ -126,9 +197,121 @@ export async function registerQwenRoutes( } }) + // Qwen Chat API - proxy chat requests to Qwen using OAuth token + app.post('/api/qwen/chat', { + schema: { + body: { + type: 'object', + required: ['model', 'messages'], + properties: { + model: { type: 'string' }, + messages: { type: 'array' }, + stream: { type: 'boolean' }, + resource_url: { type: 'string' } + } + } + } + }, async (request, reply) => { + try { + const authHeader = request.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send({ error: "Authorization required" }) + } + + const accessToken = authHeader.substring(7) + const { model, messages, stream, resource_url } = request.body as any + + // Use resource_url from OAuth credentials to target the DashScope-compatible API + const apiBaseUrl = normalizeQwenResourceUrl(resource_url) + const normalizedModel = normalizeQwenModel(model) + const chatUrl = `${apiBaseUrl}/chat/completions` + + logger.info({ chatUrl, model: normalizedModel, messageCount: messages?.length }, "Proxying Qwen chat request") + + const response = await fetch(chatUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + 'Accept': stream ? 'text/event-stream' : 'application/json' + }, + body: JSON.stringify({ + model: normalizedModel, + messages, + stream: stream || false + }) + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error({ status: response.status, errorText }, "Qwen chat request failed") + return reply.status(response.status).send({ error: "Chat request failed", details: errorText }) + } + + if (stream && response.body) { + // Stream the response + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }) + + const reader = response.body.getReader() + const decoder = new TextDecoder() + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + const chunk = decoder.decode(value, { stream: true }) + reply.raw.write(chunk) + } + } finally { + reader.releaseLock() + reply.raw.end() + } + } else { + const data = await response.json() + return reply.send(data) + } + } catch (error) { + logger.error({ error }, "Qwen chat proxy failed") + return reply.status(500).send({ error: "Chat request failed" }) + } + }) + + // Qwen Models list endpoint + app.get('/api/qwen/models', async (request, reply) => { + try { + const authHeader = request.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send({ error: "Authorization required" }) + } + + const accessToken = authHeader.substring(7) + const resourceUrl = (request.query as any).resource_url || 'https://chat.qwen.ai' + const modelsUrl = `${resourceUrl}/api/v1/models` + + const response = await fetch(modelsUrl, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error({ status: response.status, errorText }, "Qwen models request failed") + return reply.status(response.status).send({ error: "Models request failed", details: errorText }) + } + + const data = await response.json() + return reply.send(data) + } catch (error) { + logger.error({ error }, "Qwen models request failed") + return reply.status(500).send({ error: "Models request failed" }) + } + }) + logger.info("Qwen OAuth routes registered") } - -function generateState(): string { - return Math.random().toString(36).substring(2, 15) + Date.now().toString(36) -} \ No newline at end of file diff --git a/packages/server/src/server/routes/storage.ts b/packages/server/src/server/routes/storage.ts index 4b68322..60e492e 100644 --- a/packages/server/src/server/routes/storage.ts +++ b/packages/server/src/server/routes/storage.ts @@ -24,12 +24,29 @@ const InstanceDataSchema = z.object({ messageHistory: z.array(z.string()).default([]), agentModelSelections: z.record(z.string(), ModelPreferenceSchema).default({}), sessionTasks: z.record(z.string(), z.array(TaskSchema)).optional(), + sessionSkills: z + .record( + z.string(), + z.array(z.object({ id: z.string(), name: z.string(), description: z.string().optional() })), + ) + .optional(), + customAgents: z + .array( + z.object({ + name: z.string(), + description: z.string().optional(), + prompt: z.string(), + }), + ) + .optional(), }) const EMPTY_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {}, sessionTasks: {}, + sessionSkills: {}, + customAgents: [], } export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) { diff --git a/packages/server/src/server/routes/workspaces.ts b/packages/server/src/server/routes/workspaces.ts index dce5fd8..889cd29 100644 --- a/packages/server/src/server/routes/workspaces.ts +++ b/packages/server/src/server/routes/workspaces.ts @@ -1,10 +1,18 @@ import { FastifyInstance, FastifyReply } from "fastify" import { spawnSync } from "child_process" import { z } from "zod" +import { existsSync, mkdirSync } from "fs" +import { cp, readFile, writeFile } from "fs/promises" +import path from "path" import { WorkspaceManager } from "../../workspaces/manager" +import { InstanceStore } from "../../storage/instance-store" +import { ConfigStore } from "../../config/store" +import { getWorkspaceOpencodeConfigDir } from "../../opencode-config" interface RouteDeps { workspaceManager: WorkspaceManager + instanceStore: InstanceStore + configStore: ConfigStore } const WorkspaceCreateSchema = z.object({ @@ -163,6 +171,143 @@ export function registerWorkspaceRoutes(app: FastifyInstance, deps: RouteDeps) { return { isRepo: true, branch, ahead, behind, changes } }) + + app.post<{ + Params: { id: string } + Body: { destination: string; includeConfig?: boolean } + }>("/api/workspaces/:id/export", async (request, reply) => { + const workspace = deps.workspaceManager.get(request.params.id) + if (!workspace) { + reply.code(404) + return { error: "Workspace not found" } + } + + const payload = request.body ?? { destination: "" } + const destination = payload.destination?.trim() + if (!destination) { + reply.code(400) + return { error: "Destination is required" } + } + + const exportRoot = path.join(destination, `nomadarch-export-${path.basename(workspace.path)}-${Date.now()}`) + mkdirSync(exportRoot, { recursive: true }) + + const workspaceTarget = path.join(exportRoot, "workspace") + await cp(workspace.path, workspaceTarget, { recursive: true, force: true }) + + const instanceData = await deps.instanceStore.read(workspace.path) + await writeFile(path.join(exportRoot, "instance-data.json"), JSON.stringify(instanceData, null, 2), "utf-8") + + const configDir = getWorkspaceOpencodeConfigDir(workspace.id) + if (existsSync(configDir)) { + await cp(configDir, path.join(exportRoot, "opencode-config"), { recursive: true, force: true }) + } + + if (payload.includeConfig) { + const config = deps.configStore.get() + await writeFile(path.join(exportRoot, "user-config.json"), JSON.stringify(config, null, 2), "utf-8") + } + + const metadata = { + exportedAt: new Date().toISOString(), + workspacePath: workspace.path, + workspaceId: workspace.id, + } + await writeFile(path.join(exportRoot, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8") + + return { destination: exportRoot } + }) + + app.get<{ Params: { id: string } }>("/api/workspaces/:id/mcp-config", async (request, reply) => { + const workspace = deps.workspaceManager.get(request.params.id) + if (!workspace) { + reply.code(404) + return { error: "Workspace not found" } + } + + const configPath = path.join(workspace.path, ".mcp.json") + if (!existsSync(configPath)) { + return { path: configPath, exists: false, config: { mcpServers: {} } } + } + + try { + const raw = await readFile(configPath, "utf-8") + const parsed = raw ? JSON.parse(raw) : {} + return { path: configPath, exists: true, config: parsed } + } catch (error) { + request.log.error({ err: error }, "Failed to read MCP config") + reply.code(500) + return { error: "Failed to read MCP config" } + } + }) + + app.put<{ Params: { id: string } }>("/api/workspaces/:id/mcp-config", async (request, reply) => { + const workspace = deps.workspaceManager.get(request.params.id) + if (!workspace) { + reply.code(404) + return { error: "Workspace not found" } + } + + const body = request.body as { config?: unknown } + if (!body || typeof body.config !== "object" || body.config === null) { + reply.code(400) + return { error: "Invalid MCP config payload" } + } + + const configPath = path.join(workspace.path, ".mcp.json") + try { + await writeFile(configPath, JSON.stringify(body.config, null, 2), "utf-8") + return { path: configPath, exists: true, config: body.config } + } catch (error) { + request.log.error({ err: error }, "Failed to write MCP config") + reply.code(500) + return { error: "Failed to write MCP config" } + } + }) + + app.post<{ + Body: { source: string; destination: string; includeConfig?: boolean } + }>("/api/workspaces/import", async (request, reply) => { + const payload = request.body ?? { source: "", destination: "" } + const source = payload.source?.trim() + const destination = payload.destination?.trim() + if (!source || !destination) { + reply.code(400) + return { error: "Source and destination are required" } + } + + const workspaceSource = path.join(source, "workspace") + if (!existsSync(workspaceSource)) { + reply.code(400) + return { error: "Export workspace folder not found" } + } + + await cp(workspaceSource, destination, { recursive: true, force: true }) + + const workspace = await deps.workspaceManager.create(destination) + + const instanceDataPath = path.join(source, "instance-data.json") + if (existsSync(instanceDataPath)) { + const raw = await readFile(instanceDataPath, "utf-8") + await deps.instanceStore.write(workspace.path, JSON.parse(raw)) + } + + const configSource = path.join(source, "opencode-config") + if (existsSync(configSource)) { + const configTarget = getWorkspaceOpencodeConfigDir(workspace.id) + await cp(configSource, configTarget, { recursive: true, force: true }) + } + + if (payload.includeConfig) { + const userConfigPath = path.join(source, "user-config.json") + if (existsSync(userConfigPath)) { + const raw = await readFile(userConfigPath, "utf-8") + deps.configStore.replace(JSON.parse(raw)) + } + } + + return workspace + }) } diff --git a/packages/server/src/server/routes/zai.ts b/packages/server/src/server/routes/zai.ts index 92e25f3..66c8914 100644 --- a/packages/server/src/server/routes/zai.ts +++ b/packages/server/src/server/routes/zai.ts @@ -1,16 +1,15 @@ import { FastifyInstance } from "fastify" -import { ZAIClient, type ZAIConfig, type ZAIChatRequest } from "../../integrations/zai-api" +import { ZAIClient, ZAI_MODELS, type ZAIConfig, type ZAIChatRequest, ZAIChatRequestSchema } from "../../integrations/zai-api" import { Logger } from "../../logger" import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs" import { join } from "path" -import { homedir } from "os" +import { getUserIntegrationsDir } from "../../user-data" interface ZAIRouteDeps { logger: Logger } -// Config file path -const CONFIG_DIR = join(homedir(), ".nomadarch") +const CONFIG_DIR = getUserIntegrationsDir() const CONFIG_FILE = join(CONFIG_DIR, "zai-config.json") export async function registerZAIRoutes( @@ -69,15 +68,7 @@ export async function registerZAIRoutes( // List available models app.get('/api/zai/models', async (request, reply) => { try { - const config = getZAIConfig() - if (!config.enabled) { - return reply.status(400).send({ error: "Z.AI is not enabled" }) - } - - const client = new ZAIClient(config) - const models = await client.listModels() - - return { models: models.map(name => ({ name, provider: "zai" })) } + return { models: ZAI_MODELS.map(name => ({ name, provider: "zai" })) } } catch (error) { logger.error({ error }, "Failed to list Z.AI models") return reply.status(500).send({ error: "Failed to list models" }) @@ -107,8 +98,9 @@ export async function registerZAIRoutes( for await (const chunk of client.chatStream(chatRequest)) { reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`) - // Check for message_stop event - if (chunk.type === "message_stop") { + // Check for finish_reason to end stream + const finishReason = chunk.choices[0]?.finish_reason + if (finishReason) { reply.raw.write('data: [DONE]\n\n') break } @@ -133,16 +125,15 @@ export async function registerZAIRoutes( logger.info("Z.AI routes registered") } -// Configuration management functions using file-based storage function getZAIConfig(): ZAIConfig { try { if (existsSync(CONFIG_FILE)) { const data = readFileSync(CONFIG_FILE, 'utf-8') return JSON.parse(data) } - return { enabled: false, endpoint: "https://api.z.ai/api/anthropic", timeout: 3000000 } + return { enabled: false, endpoint: "https://api.z.ai/api/paas/v4", timeout: 300000 } } catch { - return { enabled: false, endpoint: "https://api.z.ai/api/anthropic", timeout: 3000000 } + return { enabled: false, endpoint: "https://api.z.ai/api/paas/v4", timeout: 300000 } } } diff --git a/packages/server/src/storage/instance-store.ts b/packages/server/src/storage/instance-store.ts index 948c4f9..6361a7d 100644 --- a/packages/server/src/storage/instance-store.ts +++ b/packages/server/src/storage/instance-store.ts @@ -1,8 +1,8 @@ import fs from "fs" import { promises as fsp } from "fs" -import os from "os" import path from "path" import type { InstanceData } from "../api-types" +import { getUserInstancesDir } from "../user-data" const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], @@ -13,7 +13,7 @@ const DEFAULT_INSTANCE_DATA: InstanceData = { export class InstanceStore { private readonly instancesDir: string - constructor(baseDir = path.join(os.homedir(), ".config", "codenomad", "instances")) { + constructor(baseDir = getUserInstancesDir()) { this.instancesDir = baseDir fs.mkdirSync(this.instancesDir, { recursive: true }) } diff --git a/packages/server/src/user-data.ts b/packages/server/src/user-data.ts new file mode 100644 index 0000000..a46d0ce --- /dev/null +++ b/packages/server/src/user-data.ts @@ -0,0 +1,28 @@ +import os from "os" +import path from "path" + +const DEFAULT_ROOT = path.join(os.homedir(), ".config", "codenomad") + +export function getUserDataRoot(): string { + const override = process.env.CODENOMAD_USER_DIR + if (override && override.trim().length > 0) { + return path.resolve(override) + } + return DEFAULT_ROOT +} + +export function getUserConfigPath(): string { + return path.join(getUserDataRoot(), "config.json") +} + +export function getUserInstancesDir(): string { + return path.join(getUserDataRoot(), "instances") +} + +export function getUserIntegrationsDir(): string { + return path.join(getUserDataRoot(), "integrations") +} + +export function getOpencodeWorkspacesRoot(): string { + return path.join(getUserDataRoot(), "opencode-workspaces") +} diff --git a/packages/server/src/utils/port.ts b/packages/server/src/utils/port.ts new file mode 100644 index 0000000..08b6b9a --- /dev/null +++ b/packages/server/src/utils/port.ts @@ -0,0 +1,35 @@ +import net from "net" + +const DEFAULT_START_PORT = 3000 +const MAX_PORT_ATTEMPTS = 50 + +function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer() + server.once("error", () => { + resolve(false) + }) + server.once("listening", () => { + server.close() + resolve(true) + }) + server.listen(port, "127.0.0.1") + }) +} + +export async function findAvailablePort(startPort: number = DEFAULT_START_PORT): Promise { + for (let port = startPort; port < startPort + MAX_PORT_ATTEMPTS; port++) { + if (await isPortAvailable(port)) { + return port + } + } + return 0 +} + +export async function getAvailablePort(preferredPort: number = DEFAULT_START_PORT): Promise { + const isAvailable = await isPortAvailable(preferredPort) + if (isAvailable) { + return preferredPort + } + return findAvailablePort(preferredPort + 1) +} diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts index f0d0a7e..3e681e8 100644 --- a/packages/server/src/workspaces/manager.ts +++ b/packages/server/src/workspaces/manager.ts @@ -10,7 +10,7 @@ import { clearWorkspaceSearchCache } from "../filesystem/search-cache" import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types" import { WorkspaceRuntime, ProcessExitInfo } from "./runtime" import { Logger } from "../logger" -import { getOpencodeConfigDir } from "../opencode-config" +import { ensureWorkspaceOpencodeConfig } from "../opencode-config" const STARTUP_STABILITY_DELAY_MS = 1500 @@ -27,11 +27,9 @@ interface WorkspaceRecord extends WorkspaceDescriptor {} export class WorkspaceManager { private readonly workspaces = new Map() private readonly runtime: WorkspaceRuntime - private readonly opencodeConfigDir: string constructor(private readonly options: WorkspaceManagerOptions) { this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger) - this.opencodeConfigDir = getOpencodeConfigDir() } list(): WorkspaceDescriptor[] { @@ -105,9 +103,10 @@ export class WorkspaceManager { const preferences = this.options.configStore.get().preferences ?? {} const userEnvironment = preferences.environmentVariables ?? {} + const opencodeConfigDir = ensureWorkspaceOpencodeConfig(id) const environment = { ...userEnvironment, - OPENCODE_CONFIG_DIR: this.opencodeConfigDir, + OPENCODE_CONFIG_DIR: opencodeConfigDir, } try { diff --git a/packages/ui/package.json b/packages/ui/package.json index 098a9c2..000a515 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,7 +7,8 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "typecheck": "tsc --noEmit -p tsconfig.json" + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "node --test --experimental-strip-types src/lib/__tests__/*.test.ts src/stores/__tests__/*.test.ts" }, "dependencies": { "@git-diff-view/solid": "^0.0.8", @@ -30,8 +31,10 @@ "autoprefixer": "10.4.21", "postcss": "8.5.6", "tailwindcss": "3", + "tsx": "^4.21.0", "typescript": "^5.3.0", "vite": "^5.0.0", - "vite-plugin-solid": "^2.10.0" + "vite-plugin-solid": "^2.10.0", + "zod": "^3.25.76" } } diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index e7fa874..d16d90e 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -24,6 +24,8 @@ import { setIsSelectingFolder, showFolderSelection, setShowFolderSelection, + showFolderSelectionOnStart, + setShowFolderSelectionOnStart, } from "./stores/ui" import { useConfig } from "./stores/preferences" import { @@ -74,6 +76,8 @@ const App: Component = () => { const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) + const shouldShowFolderSelection = () => !hasInstances() || showFolderSelectionOnStart() + const updateInstanceTabBarHeight = () => { if (typeof document === "undefined") return const element = document.querySelector(".tab-bar-instance") @@ -156,6 +160,7 @@ const App: Component = () => { clearLaunchError() const instanceId = await createInstance(folderPath, selectedBinary) setShowFolderSelection(false) + setShowFolderSelectionOnStart(false) setIsAdvancedSettingsOpen(false) log.info("Created instance", { @@ -375,7 +380,7 @@ const App: Component = () => {
{
+
@@ -351,9 +500,25 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
- {/* API Key Manager Button */} + {/* Compact Button - Context Compression & Summary */} + + {/* API Key Manager Button - Opens Advanced Settings */} + + + Active Threads
- {tasks().length} + {visibleTasks().length}
-
@@ -491,7 +698,21 @@ export default function MultiTaskChat(props: MultiTaskChatProps) { {task.messageIds?.length || 0} messages
- +
+ { + event.stopPropagation(); + archiveTask(props.instanceId, props.sessionId, task.id); + }} + class="text-zinc-600 hover:text-zinc-200 transition-colors" + title="Archive task" + > + + + +
)} @@ -572,20 +793,22 @@ export default function MultiTaskChat(props: MultiTaskChatProps) { {isAgentThinking() ? "THINKING" : "SENDING"}
- {/* STOP button */} - - - + 0}> +
+ + {(attachment) => ( + removeAttachment(attachment.id)} + /> + )} + +
+
+ {/* Text Input */}